Daemon (houndd) - realtime.rs: inotify monitor over watched dirs (default ~/Downloads, ~/Documents, ~/Desktop), ClamAV scan on touch, on_detect action (quarantine/rename/remove), ransomware heuristic (writes/renames per minute above threshold -> 'watching'/'alarm' + critical event) - quarantine.rs: SHA-256-keyed vault under ~/.local/share/hound/quarantine, add/list/restore/remove with original-path metadata - rootkit.rs: setuid anomaly detection (allowlisted stock binaries), deleted-but-executing inodes, world-writable /usr /bin; 3 severity levels - settings.rs: persisted ~/.config/hound/settings.json, hot-reload on set - events.rs: ring buffer of severity-tagged events, query + clear API (hound-api): Settings, Event, QuarantineEntry, RootkitScan/ RootkitFinding, RealtimeStatus types + 10 client methods; Status gains engine field (engine-agnostic seam) CLI (hound): events, quarantine list|add|restore|remove, settings [show|paused|auto-update|notify|realtime on|off|watch|on-detect| max-size|exclude], rootkit, realtime [status|on|off] — color human output, --json everywhere GUI (Tauri 2): - 16 backend commands bridging every client method - tray watcher: 1s poll loop, 4-state icon ladder (green/amber/red/gray), desktop notification on fresh critical events - 6-tab frontend: Protection (hero + scan + update), Quarantine (vault manager + manual add), Realtime (stats + watch list + toggle), Rootkit (on-demand scan), Alerts (event log + clear), Settings (full editor) - capabilities/default.json for dialog/notification/event permissions Verified: 27/27 workspace tests, live E2E — EICAR dropped in ~/Downloads auto-quarantined by the running daemon (critical event logged, file removed from origin).
446 lines
16 KiB
CSS
446 lines
16 KiB
CSS
:root {
|
|
/* surfaces */
|
|
--bg: #0B0E14;
|
|
--bg-raised: #12161F;
|
|
--bg-panel: #161B26;
|
|
--bg-hover: #1D2432;
|
|
--border: #232B3A;
|
|
--border-hi: #32405A;
|
|
|
|
/* text */
|
|
--fg: #E8ECF4;
|
|
--fg-dim: #8B96AB;
|
|
--fg-faint: #5A6478;
|
|
|
|
/* brand + state (dog head, 4-state ladder) */
|
|
--brand: #9896E0;
|
|
--ok: #22C55E;
|
|
--warn: #F59E0B;
|
|
--bad: #EF4444;
|
|
--off: #6B7280;
|
|
|
|
--radius: 14px;
|
|
--radius-sm: 9px;
|
|
--shadow: 0 8px 30px rgb(0 0 0 / 0.45);
|
|
}
|
|
|
|
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
|
|
html, body {
|
|
height: 100%;
|
|
background: radial-gradient(1200px 700px at 20% -10%, #141B2B 0%, var(--bg) 55%);
|
|
color: var(--fg);
|
|
font: 15px/1.5 "Inter", "Cantarell", "Segoe UI", system-ui, sans-serif;
|
|
-webkit-font-smoothing: antialiased;
|
|
}
|
|
|
|
#app {
|
|
max-width: 920px;
|
|
margin: 0 auto;
|
|
padding: 26px 26px 18px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 18px;
|
|
min-height: 100vh;
|
|
}
|
|
|
|
/* ── Header ─────────────────────────────────────────────────────── */
|
|
.header {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
}
|
|
.brand { display: flex; align-items: center; gap: 12px; }
|
|
.brand-mark { width: 34px; height: 34px; filter: drop-shadow(0 2px 8px rgb(152 150 224 / .35)); }
|
|
.brand-text h1 { font-size: 20px; font-weight: 700; letter-spacing: .2px; }
|
|
.tagline { font-size: 12px; color: var(--fg-dim); }
|
|
|
|
.header-status { display: flex; align-items: center; gap: 8px; }
|
|
.engine-dot {
|
|
width: 9px; height: 9px; border-radius: 50%;
|
|
background: var(--off);
|
|
box-shadow: 0 0 8px rgb(107 114 128 / .6);
|
|
transition: background .3s, box-shadow .3s;
|
|
}
|
|
.engine-dot.ok { background: var(--ok); box-shadow: 0 0 10px rgb(34 197 94 / .7); }
|
|
.engine-dot.warn { background: var(--warn); box-shadow: 0 0 10px rgb(245 158 11 / .7); }
|
|
.engine-dot.bad { background: var(--bad); box-shadow: 0 0 10px rgb(239 68 68 / .7); }
|
|
.engine-label { font-size: 12.5px; color: var(--fg-dim); }
|
|
|
|
/* ── Tabs ───────────────────────────────────────────────────────── */
|
|
.tabs {
|
|
display: flex;
|
|
gap: 4px;
|
|
background: var(--bg-raised);
|
|
border: 1px solid var(--border);
|
|
border-radius: var(--radius);
|
|
padding: 5px;
|
|
overflow-x: auto;
|
|
}
|
|
.tab {
|
|
appearance: none;
|
|
border: 1px solid transparent;
|
|
background: transparent;
|
|
color: var(--fg-dim);
|
|
font: 600 13px/1 inherit;
|
|
border-radius: 10px;
|
|
padding: 9px 14px;
|
|
cursor: pointer;
|
|
display: inline-flex; align-items: center; gap: 7px;
|
|
white-space: nowrap;
|
|
transition: background .15s, color .15s, border-color .15s;
|
|
position: relative;
|
|
}
|
|
.tab:hover { background: var(--bg-hover); color: var(--fg); }
|
|
.tab.active {
|
|
background: linear-gradient(180deg, #232B40, #1B2233);
|
|
border-color: var(--border-hi);
|
|
color: var(--fg);
|
|
box-shadow: inset 0 1px 0 rgb(255 255 255 / .04);
|
|
}
|
|
.tab svg { opacity: .8; }
|
|
.tab-badge {
|
|
min-width: 18px;
|
|
font-size: 10.5px;
|
|
font-weight: 700;
|
|
text-align: center;
|
|
padding: 1px 5px;
|
|
border-radius: 999px;
|
|
background: var(--bad);
|
|
color: #fff;
|
|
line-height: 1.5;
|
|
}
|
|
.tab-badge.quiet { background: var(--warn); }
|
|
|
|
.tab-panel { display: none; flex-direction: column; gap: 18px; }
|
|
.tab-panel.active { display: flex; }
|
|
|
|
/* ── Hero ───────────────────────────────────────────────────────── */
|
|
.hero {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 22px;
|
|
background: linear-gradient(180deg, var(--bg-panel), var(--bg-raised));
|
|
border: 1px solid var(--border);
|
|
border-radius: var(--radius);
|
|
padding: 24px 26px;
|
|
box-shadow: var(--shadow);
|
|
}
|
|
.shield {
|
|
flex: 0 0 auto;
|
|
width: 84px; height: 84px;
|
|
display: grid; place-items: center;
|
|
border-radius: 50%;
|
|
background: rgb(152 150 224 / .07);
|
|
border: 1px solid rgb(152 150 224 / .18);
|
|
transition: border-color .3s, background .3s;
|
|
}
|
|
@keyframes pulse {
|
|
0%, 100% { transform: scale(1); opacity: 1; }
|
|
50% { transform: scale(1.06); opacity: .85; }
|
|
}
|
|
.hero-copy h2 { font-size: 22px; font-weight: 700; }
|
|
.hero-copy p { color: var(--fg-dim); margin: 4px 0 12px; }
|
|
|
|
.hero[data-state="protected"] .shield { border-color: rgb(34 197 94 / .35); background: rgb(34 197 94 / .08); }
|
|
.hero[data-state="scanning"] .shield { border-color: rgb(245 158 11 / .4); background: rgb(245 158 11 / .09); }
|
|
.hero[data-state="threat"] .shield { border-color: rgb(239 68 68 / .45); background: rgb(239 68 68 / .1); }
|
|
.hero[data-state="paused"] .shield { border-color: rgb(107 114 128 / .4); background: rgb(107 114 128 / .09); }
|
|
.hero[data-state="scanning"] #shield-icon { animation: pulse 1.6s ease-in-out infinite; }
|
|
|
|
.pill {
|
|
display: inline-block;
|
|
font-size: 12px;
|
|
color: var(--fg-dim);
|
|
background: var(--bg-hover);
|
|
border: 1px solid var(--border);
|
|
border-radius: 999px;
|
|
padding: 3px 11px;
|
|
margin-right: 8px;
|
|
white-space: nowrap;
|
|
}
|
|
.pill.stale { color: var(--warn); border-color: rgb(245 158 11 / .4); }
|
|
|
|
/* ── Actions ────────────────────────────────────────────────────── */
|
|
.actions { display: flex; flex-wrap: wrap; gap: 10px; align-items: center; }
|
|
.btn {
|
|
appearance: none;
|
|
border: 1px solid var(--border-hi);
|
|
background: var(--bg-hover);
|
|
color: var(--fg);
|
|
font: 600 13.5px/1 inherit;
|
|
border-radius: var(--radius-sm);
|
|
padding: 11px 16px;
|
|
cursor: pointer;
|
|
display: inline-flex; align-items: center; gap: 8px;
|
|
transition: background .15s, border-color .15s, transform .05s;
|
|
}
|
|
.btn:hover { background: #232C3E; border-color: #41507a; }
|
|
.btn:active { transform: translateY(1px); }
|
|
.btn:disabled { opacity: .5; cursor: default; }
|
|
.btn.primary {
|
|
background: linear-gradient(180deg, #5A58D6, #4543C4);
|
|
border-color: #6a68e6;
|
|
}
|
|
.btn.primary:hover { background: linear-gradient(180deg, #6765e0, #4f4dd4); }
|
|
.btn.small { padding: 7px 12px; font-size: 12.5px; }
|
|
.btn.danger { border-color: rgb(239 68 68 / .5); }
|
|
.btn.danger:hover { background: rgb(239 68 68 / .15); border-color: var(--bad); }
|
|
|
|
/* ── Panels ─────────────────────────────────────────────────────── */
|
|
.panel {
|
|
background: var(--bg-panel);
|
|
border: 1px solid var(--border);
|
|
border-radius: var(--radius);
|
|
padding: 16px 18px;
|
|
}
|
|
.panel-head {
|
|
display: flex;
|
|
align-items: baseline;
|
|
justify-content: space-between;
|
|
margin-bottom: 10px;
|
|
gap: 12px;
|
|
}
|
|
.panel-head h3 { font-size: 14px; font-weight: 700; letter-spacing: .3px; text-transform: uppercase; color: var(--fg-dim); }
|
|
.muted { color: var(--fg-dim); font-size: 13px; }
|
|
.empty { padding: 6px 0 2px; }
|
|
.hidden { display: none !important; }
|
|
.row-actions { display: flex; align-items: center; gap: 10px; }
|
|
.scan-target { font-family: ui-monospace, monospace; font-size: 12.5px; color: var(--fg-faint); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
|
|
/* scan progress */
|
|
.progress {
|
|
height: 8px;
|
|
border-radius: 999px;
|
|
background: var(--bg-hover);
|
|
overflow: hidden;
|
|
margin-bottom: 10px;
|
|
}
|
|
.progress-bar {
|
|
height: 100%;
|
|
width: 0%;
|
|
background: linear-gradient(90deg, #f59e0b, #fbbf24);
|
|
border-radius: inherit;
|
|
transition: width .4s ease;
|
|
}
|
|
.progress.indeterminate .progress-bar {
|
|
width: 35%;
|
|
animation: slide 1.3s ease-in-out infinite alternate;
|
|
}
|
|
@keyframes slide { from { margin-left: 0; } to { margin-left: 65%; } }
|
|
|
|
/* results */
|
|
.results { display: flex; flex-direction: column; gap: 6px; }
|
|
.result-row {
|
|
display: grid;
|
|
grid-template-columns: auto 1fr auto;
|
|
align-items: center;
|
|
gap: 12px;
|
|
background: var(--bg-hover);
|
|
border: 1px solid var(--border);
|
|
border-radius: var(--radius-sm);
|
|
padding: 10px 14px;
|
|
}
|
|
.result-row .sig { font-weight: 700; font-size: 13.5px; }
|
|
.result-row .path { color: var(--fg-dim); font-size: 12.5px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-family: ui-monospace, monospace; }
|
|
.result-row.clean { border-left: 3px solid var(--ok); }
|
|
.result-row.clean .sig { color: var(--ok); }
|
|
.result-row.infected { border-left: 3px solid var(--bad); }
|
|
.result-row.infected .sig { color: var(--bad); }
|
|
|
|
/* log */
|
|
.log {
|
|
background: #0A0D13;
|
|
border: 1px solid var(--border);
|
|
border-radius: var(--radius-sm);
|
|
padding: 12px 14px;
|
|
font: 12px/1.6 ui-monospace, "JetBrains Mono", monospace;
|
|
color: var(--fg-dim);
|
|
max-height: 220px;
|
|
overflow: auto;
|
|
white-space: pre-wrap;
|
|
}
|
|
.log.ok { color: #7fd79a; }
|
|
.log.fail { color: #f2a2a2; }
|
|
|
|
/* ── Inputs & switches ──────────────────────────────────────────── */
|
|
.input {
|
|
appearance: none;
|
|
background: var(--bg-raised);
|
|
border: 1px solid var(--border-hi);
|
|
border-radius: var(--radius-sm);
|
|
color: var(--fg);
|
|
font: 13.5px/1.4 inherit;
|
|
padding: 9px 12px;
|
|
width: 100%;
|
|
}
|
|
.input:focus { outline: none; border-color: var(--brand); box-shadow: 0 0 0 3px rgb(152 150 224 / .15); }
|
|
.input.mono { font-family: ui-monospace, "JetBrains Mono", monospace; font-size: 12.5px; }
|
|
textarea.input { resize: vertical; }
|
|
select.input { cursor: pointer; }
|
|
|
|
.form-row { display: grid; grid-template-columns: 1fr 200px auto; gap: 10px; }
|
|
|
|
.field { margin: 14px 0; }
|
|
.field > label { display: block; font-size: 13px; font-weight: 600; color: var(--fg-dim); margin-bottom: 6px; }
|
|
|
|
.switch-row {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: 16px;
|
|
cursor: pointer;
|
|
padding: 4px 0;
|
|
}
|
|
.switch-row.setting { padding: 10px 0; border-bottom: 1px solid var(--border); }
|
|
.switch-row.setting:last-of-type { border-bottom: none; }
|
|
.switch-row strong { display: block; font-size: 14px; font-weight: 600; }
|
|
.switch-row small { display: block; color: var(--fg-dim); font-size: 12px; margin-top: 2px; }
|
|
.switch-row input[type="checkbox"] { position: absolute; opacity: 0; pointer-events: none; }
|
|
.switch {
|
|
flex: 0 0 auto;
|
|
width: 40px; height: 22px;
|
|
border-radius: 999px;
|
|
background: var(--bg-hover);
|
|
border: 1px solid var(--border-hi);
|
|
position: relative;
|
|
transition: background .2s, border-color .2s;
|
|
}
|
|
.switch::after {
|
|
content: "";
|
|
position: absolute;
|
|
top: 2px; left: 2px;
|
|
width: 16px; height: 16px;
|
|
border-radius: 50%;
|
|
background: var(--fg-dim);
|
|
transition: transform .2s, background .2s;
|
|
}
|
|
.switch-row input:checked + .switch { background: rgb(90 88 214 / .55); border-color: #6a68e6; }
|
|
.switch-row input:checked + .switch::after { transform: translateX(18px); background: #fff; }
|
|
|
|
/* ── Quarantine ─────────────────────────────────────────────────── */
|
|
.qt-list { display: flex; flex-direction: column; gap: 6px; }
|
|
.qt-row {
|
|
display: grid;
|
|
grid-template-columns: auto 1fr auto;
|
|
gap: 12px;
|
|
align-items: center;
|
|
background: var(--bg-hover);
|
|
border: 1px solid var(--border);
|
|
border-left: 3px solid var(--bad);
|
|
border-radius: var(--radius-sm);
|
|
padding: 10px 14px;
|
|
}
|
|
.qt-row.restored { border-left-color: var(--off); opacity: .75; }
|
|
.qt-row .qt-virus { font-weight: 700; font-size: 13px; color: var(--bad); white-space: nowrap; }
|
|
.qt-row.restored .qt-virus { color: var(--fg-dim); }
|
|
.qt-row .qt-path { font-family: ui-monospace, monospace; font-size: 12px; color: var(--fg-dim); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
.qt-row .qt-meta { font-size: 11.5px; color: var(--fg-faint); white-space: nowrap; }
|
|
.qt-row .qt-actions { display: flex; gap: 6px; }
|
|
|
|
/* ── Realtime ───────────────────────────────────────────────────── */
|
|
.stat-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(4, 1fr);
|
|
gap: 10px;
|
|
margin: 12px 0 6px;
|
|
}
|
|
.stat {
|
|
background: var(--bg-raised);
|
|
border: 1px solid var(--border);
|
|
border-radius: var(--radius-sm);
|
|
padding: 12px 14px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 2px;
|
|
}
|
|
.stat-val { font-size: 20px; font-weight: 700; font-variant-numeric: tabular-nums; }
|
|
.stat-val.alarm { color: var(--bad); }
|
|
.stat-val.watching { color: var(--warn); }
|
|
.stat-val.calm { color: var(--ok); }
|
|
.stat-key { font-size: 11.5px; color: var(--fg-faint); text-transform: uppercase; letter-spacing: .4px; }
|
|
|
|
.kv {
|
|
display: flex;
|
|
gap: 12px;
|
|
padding: 8px 0;
|
|
border-top: 1px solid var(--border);
|
|
font-size: 13px;
|
|
}
|
|
.kv-k { flex: 0 0 150px; color: var(--fg-faint); }
|
|
.kv-v { color: var(--fg-dim); font-family: ui-monospace, monospace; font-size: 12px; word-break: break-all; }
|
|
|
|
.watch-list { display: flex; flex-wrap: wrap; gap: 6px; }
|
|
.watch-chip {
|
|
font-family: ui-monospace, monospace;
|
|
font-size: 12px;
|
|
color: var(--fg-dim);
|
|
background: var(--bg-hover);
|
|
border: 1px solid var(--border);
|
|
border-radius: 999px;
|
|
padding: 4px 11px;
|
|
}
|
|
|
|
/* ── Rootkit ────────────────────────────────────────────────────── */
|
|
.verdict {
|
|
border-radius: var(--radius-sm);
|
|
padding: 12px 16px;
|
|
margin-bottom: 12px;
|
|
font-weight: 600;
|
|
font-size: 14px;
|
|
border: 1px solid;
|
|
}
|
|
.verdict.clean { background: rgb(34 197 94 / .08); border-color: rgb(34 197 94 / .35); color: #7fd79a; }
|
|
.verdict.dirty { background: rgb(239 68 68 / .08); border-color: rgb(239 68 68 / .4); color: #f2a2a2; }
|
|
|
|
.finding-list { display: flex; flex-direction: column; gap: 6px; }
|
|
.finding-row {
|
|
display: grid;
|
|
grid-template-columns: auto auto 1fr;
|
|
gap: 12px;
|
|
align-items: center;
|
|
background: var(--bg-hover);
|
|
border: 1px solid var(--border);
|
|
border-radius: var(--radius-sm);
|
|
padding: 10px 14px;
|
|
}
|
|
.finding-row .sev {
|
|
font-size: 10.5px; font-weight: 700;
|
|
text-transform: uppercase; letter-spacing: .5px;
|
|
padding: 3px 9px; border-radius: 999px;
|
|
}
|
|
.sev.critical { background: rgb(239 68 68 / .15); color: var(--bad); }
|
|
.sev.warn { background: rgb(245 158 11 / .15); color: var(--warn); }
|
|
.sev.info { background: rgb(107 114 128 / .2); color: var(--fg-dim); }
|
|
.finding-row .check { font-family: ui-monospace, monospace; font-size: 12px; color: var(--fg-dim); }
|
|
.finding-row .detail { font-size: 13px; color: var(--fg); word-break: break-all; }
|
|
|
|
/* ── Alerts ─────────────────────────────────────────────────────── */
|
|
.event-list { display: flex; flex-direction: column; gap: 4px; max-height: 520px; overflow-y: auto; }
|
|
.event-row {
|
|
display: grid;
|
|
grid-template-columns: auto auto 1fr auto;
|
|
gap: 12px;
|
|
align-items: center;
|
|
border-radius: var(--radius-sm);
|
|
padding: 8px 14px;
|
|
background: var(--bg-hover);
|
|
border: 1px solid var(--border);
|
|
}
|
|
.event-row .ev-sev { font-size: 10.5px; font-weight: 700; text-transform: uppercase; letter-spacing: .5px; width: 64px; }
|
|
.ev-sev.critical { color: var(--bad); }
|
|
.ev-sev.warn { color: var(--warn); }
|
|
.ev-sev.info { color: var(--fg-faint); }
|
|
.event-row .ev-kind { font-family: ui-monospace, monospace; font-size: 11.5px; color: var(--fg-dim); width: 96px; }
|
|
.event-row .ev-msg { font-size: 13px; color: var(--fg); word-break: break-word; }
|
|
.event-row .ev-ts { font-size: 11.5px; color: var(--fg-faint); white-space: nowrap; font-variant-numeric: tabular-nums; }
|
|
|
|
/* footer */
|
|
.footer {
|
|
margin-top: auto;
|
|
display: flex;
|
|
justify-content: space-between;
|
|
font-size: 12px;
|
|
color: var(--fg-faint);
|
|
padding-top: 4px;
|
|
}
|