remove infra.md.example, infra.md is the source of truth
This commit is contained in:
910
ayn-antivirus/ayn_antivirus/dashboard/templates.py
Normal file
910
ayn-antivirus/ayn_antivirus/dashboard/templates.py
Normal file
@@ -0,0 +1,910 @@
|
||||
"""AYN Antivirus Dashboard — HTML Template.
|
||||
|
||||
Single-page application with embedded CSS and JavaScript.
|
||||
All data is fetched from the ``/api/*`` endpoints.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
def get_dashboard_html() -> str:
|
||||
"""Return the complete HTML dashboard as a string."""
|
||||
return _HTML
|
||||
|
||||
|
||||
_HTML = r"""<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||
<title>AYN Antivirus — Security Dashboard</title>
|
||||
<style>
|
||||
:root{--bg:#0a0e17;--surface:#111827;--surface2:#1a2332;--surface3:#1f2b3d;
|
||||
--border:#2a3444;--text:#e2e8f0;--text-dim:#8892a4;--accent:#3b82f6;
|
||||
--green:#10b981;--red:#ef4444;--orange:#f59e0b;--yellow:#eab308;
|
||||
--purple:#8b5cf6;--cyan:#06b6d4;--radius:8px;--shadow:0 2px 8px rgba(0,0,0,.3)}
|
||||
*{margin:0;padding:0;box-sizing:border-box}
|
||||
body{background:var(--bg);color:var(--text);font-family:'Segoe UI',system-ui,-apple-system,sans-serif;font-size:14px;min-height:100vh}
|
||||
a{color:var(--accent);text-decoration:none}
|
||||
::-webkit-scrollbar{width:6px;height:6px}
|
||||
::-webkit-scrollbar-track{background:var(--surface)}
|
||||
::-webkit-scrollbar-thumb{background:var(--border);border-radius:3px}
|
||||
|
||||
/* ── Header ── */
|
||||
.header{background:var(--surface);border-bottom:1px solid var(--border);padding:12px 24px;display:flex;align-items:center;justify-content:space-between;position:sticky;top:0;z-index:100}
|
||||
.header-left{display:flex;align-items:center;gap:16px}
|
||||
.logo{font-size:1.3rem;font-weight:800;letter-spacing:.05em}
|
||||
.logo span{color:var(--accent)}
|
||||
.header-meta{display:flex;gap:20px;font-size:.82rem;color:var(--text-dim)}
|
||||
.header-meta b{color:var(--text);font-weight:600}
|
||||
.pulse{display:inline-block;width:8px;height:8px;background:var(--green);border-radius:50%;margin-right:6px;animation:pulse 2s infinite}
|
||||
@keyframes pulse{0%,100%{opacity:1}50%{opacity:.4}}
|
||||
|
||||
/* ── Navigation ── */
|
||||
.nav{background:var(--surface);border-bottom:1px solid var(--border);padding:0 24px;display:flex;gap:0;overflow-x:auto}
|
||||
.nav-tab{padding:12px 20px;cursor:pointer;color:var(--text-dim);font-weight:600;font-size:.85rem;border-bottom:2px solid transparent;transition:all .2s;white-space:nowrap;user-select:none}
|
||||
.nav-tab:hover{color:var(--text);background:var(--surface2)}
|
||||
.nav-tab.active{color:var(--accent);border-bottom-color:var(--accent)}
|
||||
|
||||
/* ── Layout ── */
|
||||
.content{padding:20px 24px;max-width:1440px;margin:0 auto}
|
||||
.tab-panel{display:none;animation:fadeIn .25s}
|
||||
.tab-panel.active{display:block}
|
||||
@keyframes fadeIn{from{opacity:0;transform:translateY(4px)}to{opacity:1;transform:none}}
|
||||
.grid{display:grid;gap:16px}
|
||||
.g2{grid-template-columns:repeat(2,1fr)}
|
||||
.g3{grid-template-columns:repeat(3,1fr)}
|
||||
.g4{grid-template-columns:repeat(4,1fr)}
|
||||
.g6{grid-template-columns:repeat(6,1fr)}
|
||||
@media(max-width:900px){.g2,.g3,.g4,.g6{grid-template-columns:1fr}}
|
||||
@media(min-width:901px) and (max-width:1200px){.g4{grid-template-columns:repeat(2,1fr)}.g6{grid-template-columns:repeat(3,1fr)}}
|
||||
.section{margin-bottom:24px}
|
||||
.section-title{font-size:1rem;font-weight:700;margin-bottom:12px;display:flex;align-items:center;gap:8px}
|
||||
.section-title .icon{font-size:1.1rem}
|
||||
|
||||
/* ── Cards ── */
|
||||
.card{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);padding:16px;transition:border-color .2s,box-shadow .2s}
|
||||
.card:hover{border-color:var(--accent);box-shadow:0 0 12px rgba(59,130,246,.1)}
|
||||
.card-label{font-size:.75rem;color:var(--text-dim);text-transform:uppercase;letter-spacing:.06em;margin-bottom:6px}
|
||||
.card-value{font-size:1.6rem;font-weight:700}
|
||||
.card-sub{font-size:.78rem;color:var(--text-dim);margin-top:4px}
|
||||
.card-green .card-value{color:var(--green)}
|
||||
.card-red .card-value{color:var(--red)}
|
||||
.card-orange .card-value{color:var(--orange)}
|
||||
.card-yellow .card-value{color:var(--yellow)}
|
||||
.card-accent .card-value{color:var(--accent)}
|
||||
.card-purple .card-value{color:var(--purple)}
|
||||
|
||||
/* ── Gauge (SVG circular) ── */
|
||||
.gauge-wrap{display:flex;flex-direction:column;align-items:center;padding:12px}
|
||||
.gauge{position:relative;width:110px;height:110px}
|
||||
.gauge svg{transform:rotate(-90deg)}
|
||||
.gauge-bg{fill:none;stroke:var(--border);stroke-width:10}
|
||||
.gauge-fill{fill:none;stroke-width:10;stroke-linecap:round;transition:stroke-dashoffset .8s ease}
|
||||
.gauge-text{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;font-size:1.3rem;font-weight:700}
|
||||
.gauge-label{margin-top:8px;font-size:.8rem;color:var(--text-dim);font-weight:600}
|
||||
|
||||
/* ── Badges ── */
|
||||
.badge{display:inline-block;padding:2px 8px;border-radius:4px;font-size:.72rem;font-weight:700;text-transform:uppercase;letter-spacing:.03em}
|
||||
.badge-critical{background:rgba(239,68,68,.15);color:var(--red);border:1px solid var(--red)}
|
||||
.badge-high{background:rgba(245,158,11,.15);color:var(--orange);border:1px solid var(--orange)}
|
||||
.badge-medium{background:rgba(234,179,8,.15);color:var(--yellow);border:1px solid var(--yellow)}
|
||||
.badge-low{background:rgba(16,185,129,.15);color:var(--green);border:1px solid var(--green)}
|
||||
.badge-success{background:rgba(16,185,129,.15);color:var(--green);border:1px solid var(--green)}
|
||||
.badge-error{background:rgba(239,68,68,.15);color:var(--red);border:1px solid var(--red)}
|
||||
.badge-running{background:rgba(59,130,246,.15);color:var(--accent);border:1px solid var(--accent)}
|
||||
.badge-info{background:rgba(59,130,246,.15);color:var(--accent);border:1px solid var(--accent)}
|
||||
.badge-warning{background:rgba(245,158,11,.15);color:var(--orange);border:1px solid var(--orange)}
|
||||
|
||||
/* ── Tables ── */
|
||||
.tbl-wrap{overflow-x:auto;border:1px solid var(--border);border-radius:var(--radius);background:var(--surface)}
|
||||
table{width:100%;border-collapse:collapse}
|
||||
th{background:var(--surface2);color:var(--text-dim);font-size:.75rem;text-transform:uppercase;letter-spacing:.05em;padding:10px 12px;text-align:left;position:sticky;top:0}
|
||||
td{padding:9px 12px;border-top:1px solid var(--border);font-size:.84rem;vertical-align:middle}
|
||||
tr:hover td{background:rgba(59,130,246,.04)}
|
||||
.mono{font-family:'Cascadia Code','Fira Code',monospace;font-size:.78rem}
|
||||
.trunc{max-width:260px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
||||
.empty-row td{text-align:center;padding:32px;color:var(--text-dim);font-size:.95rem}
|
||||
|
||||
/* ── Bar Chart (CSS) ── */
|
||||
.bar-chart{display:flex;align-items:flex-end;gap:4px;height:120px;padding:8px 0}
|
||||
.bar-col{display:flex;flex-direction:column;align-items:center;flex:1;min-width:0}
|
||||
.bar{width:100%;min-height:2px;border-radius:3px 3px 0 0;transition:height .4s;position:relative;cursor:default}
|
||||
.bar:hover::after{content:attr(data-tip);position:absolute;bottom:calc(100% + 4px);left:50%;transform:translateX(-50%);background:var(--surface3);border:1px solid var(--border);padding:3px 8px;border-radius:4px;font-size:.7rem;white-space:nowrap;z-index:5}
|
||||
.bar-label{font-size:.6rem;color:var(--text-dim);margin-top:4px;writing-mode:vertical-lr;transform:rotate(180deg);max-height:40px;overflow:hidden}
|
||||
.bar-threats{background:var(--red)}
|
||||
.bar-scans{background:var(--accent)}
|
||||
|
||||
/* ── Disk bars ── */
|
||||
.disk-row{display:flex;align-items:center;gap:12px;padding:6px 0}
|
||||
.disk-mount{width:100px;font-size:.8rem;color:var(--text-dim);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
||||
.disk-bar-outer{flex:1;height:14px;background:var(--surface2);border-radius:7px;overflow:hidden}
|
||||
.disk-bar-inner{height:100%;border-radius:7px;transition:width .6s}
|
||||
.disk-pct{width:50px;text-align:right;font-size:.8rem;font-weight:600}
|
||||
|
||||
/* ── Buttons ── */
|
||||
.btn{display:inline-flex;align-items:center;gap:6px;padding:8px 16px;border-radius:6px;border:1px solid var(--border);background:var(--surface2);color:var(--text);font-size:.82rem;font-weight:600;cursor:pointer;transition:all .2s;white-space:nowrap}
|
||||
.btn:hover{border-color:var(--accent);background:var(--surface3)}
|
||||
.btn-primary{background:var(--accent);border-color:var(--accent);color:#fff}
|
||||
.btn-primary:hover{background:#2563eb}
|
||||
.btn-sm{padding:5px 10px;font-size:.76rem}
|
||||
.btn:disabled{opacity:.5;cursor:not-allowed}
|
||||
.spinner{display:inline-block;width:14px;height:14px;border:2px solid rgba(255,255,255,.3);border-top-color:#fff;border-radius:50%;animation:spin .6s linear infinite}
|
||||
@keyframes spin{to{transform:rotate(360deg)}}
|
||||
.btn-row{display:flex;gap:8px;flex-wrap:wrap;margin-bottom:12px}
|
||||
|
||||
/* ── Filter bar ── */
|
||||
.filters{display:flex;gap:8px;flex-wrap:wrap;margin-bottom:14px;align-items:center}
|
||||
.filters select,.filters input{background:var(--surface2);border:1px solid var(--border);color:var(--text);padding:7px 10px;border-radius:6px;font-size:.82rem}
|
||||
.filters select:focus,.filters input:focus{outline:none;border-color:var(--accent)}
|
||||
.filters input{min-width:200px}
|
||||
|
||||
/* ── Sub-tabs ── */
|
||||
.sub-tabs{display:flex;gap:0;margin-bottom:14px;border-bottom:1px solid var(--border)}
|
||||
.sub-tab{padding:8px 16px;cursor:pointer;color:var(--text-dim);font-size:.82rem;font-weight:600;border-bottom:2px solid transparent;transition:all .2s}
|
||||
.sub-tab:hover{color:var(--text)}
|
||||
.sub-tab.active{color:var(--accent);border-bottom-color:var(--accent)}
|
||||
|
||||
/* ── Pagination ── */
|
||||
.pager{display:flex;align-items:center;justify-content:center;gap:12px;padding:12px;font-size:.84rem;color:var(--text-dim)}
|
||||
|
||||
/* ── Logs ── */
|
||||
.log-view{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);padding:8px;max-height:500px;overflow-y:auto;font-family:'Cascadia Code','Fira Code',monospace;font-size:.78rem;line-height:1.7}
|
||||
.log-line{padding:2px 4px;display:flex;gap:10px;border-bottom:1px solid rgba(42,52,68,.4)}
|
||||
.log-ts{color:var(--text-dim);min-width:140px}
|
||||
.log-src{color:var(--purple);min-width:80px}
|
||||
.log-msg{flex:1;word-break:break-all}
|
||||
|
||||
/* ── Toast ── */
|
||||
.toast-area{position:fixed;top:70px;right:20px;z-index:200;display:flex;flex-direction:column;gap:8px}
|
||||
.toast{padding:10px 18px;border-radius:6px;font-size:.84rem;font-weight:600;animation:slideIn .3s;box-shadow:var(--shadow)}
|
||||
.toast-success{background:#065f46;color:#a7f3d0;border:1px solid var(--green)}
|
||||
.toast-error{background:#7f1d1d;color:#fca5a5;border:1px solid var(--red)}
|
||||
.toast-info{background:#1e3a5f;color:#93c5fd;border:1px solid var(--accent)}
|
||||
@keyframes slideIn{from{opacity:0;transform:translateX(30px)}to{opacity:1;transform:none}}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<!-- HEADER -->
|
||||
<div class="header">
|
||||
<div class="header-left">
|
||||
<div class="logo">⚔️ <span>AYN</span> ANTIVIRUS</div>
|
||||
<div style="font-size:.75rem;color:var(--text-dim)">Security Dashboard</div>
|
||||
</div>
|
||||
<div class="header-meta">
|
||||
<div><span class="pulse"></span><b id="hd-host">—</b></div>
|
||||
<div>Up <b id="hd-uptime">—</b></div>
|
||||
<div id="hd-time">—</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- NAV -->
|
||||
<div class="nav" id="nav">
|
||||
<div class="nav-tab active" data-tab="overview">📊 Overview</div>
|
||||
<div class="nav-tab" data-tab="threats">🛡️ Threats</div>
|
||||
<div class="nav-tab" data-tab="scans">🔍 Scans</div>
|
||||
<div class="nav-tab" data-tab="definitions">📚 Definitions</div>
|
||||
<div class="nav-tab" data-tab="containers">🐳 Containers</div>
|
||||
<div class="nav-tab" data-tab="quarantine">🔒 Quarantine</div>
|
||||
<div class="nav-tab" data-tab="logs">📋 Logs</div>
|
||||
</div>
|
||||
|
||||
<!-- CONTENT -->
|
||||
<div class="content">
|
||||
|
||||
<!-- ═══════ TAB: OVERVIEW ═══════ -->
|
||||
<div class="tab-panel active" id="panel-overview">
|
||||
<!-- Status cards -->
|
||||
<div class="section">
|
||||
<div class="grid g4" id="status-cards">
|
||||
<div class="card card-green"><div class="card-label">Protection</div><div class="card-value" id="ov-prot">Active</div><div class="card-sub" id="ov-prot-sub">AI-powered analysis</div></div>
|
||||
<div class="card card-accent"><div class="card-label">Last Scan</div><div class="card-value" id="ov-scan">—</div><div class="card-sub" id="ov-scan-sub">—</div></div>
|
||||
<div class="card card-purple"><div class="card-label">Signatures</div><div class="card-value" id="ov-sigs">—</div><div class="card-sub" id="ov-sigs-sub">—</div></div>
|
||||
<div class="card"><div class="card-label">Quarantine</div><div class="card-value" id="ov-quar">0</div><div class="card-sub">Isolated items</div></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- CPU Per-Core -->
|
||||
<div class="section">
|
||||
<div class="section-title"><span class="icon">🧮</span> CPU Per Core <span id="cpu-summary" style="font-size:.8rem;color:var(--text-dim);margin-left:8px"></span></div>
|
||||
<div class="card" style="padding:12px">
|
||||
<canvas id="cpu-canvas" height="140" style="width:100%;display:block"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Memory Breakdown -->
|
||||
<div class="section">
|
||||
<div class="section-title"><span class="icon">🧠</span> Memory Usage <span id="mem-summary" style="font-size:.8rem;color:var(--text-dim);margin-left:8px"></span></div>
|
||||
<div class="grid g2">
|
||||
<div class="card" style="padding:12px">
|
||||
<canvas id="mem-canvas" height="160" style="width:100%;display:block"></canvas>
|
||||
</div>
|
||||
<div class="card" style="padding:12px">
|
||||
<div class="card-label">Memory Breakdown</div>
|
||||
<div id="mem-bars" style="margin-top:8px"></div>
|
||||
<div style="margin-top:12px;border-top:1px solid var(--border);padding-top:8px">
|
||||
<div class="card-label">Swap</div>
|
||||
<div id="swap-bar"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Load + Network + Processes -->
|
||||
<div class="section">
|
||||
<div class="grid g3">
|
||||
<div class="card"><div class="card-label">Load Average</div><div class="card-value" id="ov-load" style="font-size:1.2rem">—</div><div class="card-sub">1 / 5 / 15 min</div></div>
|
||||
<div class="card"><div class="card-label">Network Connections</div><div class="card-value" id="ov-netconn">—</div><div class="card-sub">Active inet sockets</div></div>
|
||||
<div class="card"><div class="card-label">CPU Frequency</div><div class="card-value" id="ov-freq" style="font-size:1.2rem">—</div><div class="card-sub">Current MHz</div></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Top Processes -->
|
||||
<div class="section">
|
||||
<div class="section-title"><span class="icon">⚙️</span> Top Processes</div>
|
||||
<div class="tbl-wrap">
|
||||
<table><thead><tr><th>PID</th><th>Process</th><th>CPU %</th><th>RAM %</th></tr></thead><tbody id="proc-tbody"><tr class="empty-row"><td colspan="4">Loading…</td></tr></tbody></table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Disk -->
|
||||
<div class="section">
|
||||
<div class="section-title"><span class="icon">💾</span> Disk Usage</div>
|
||||
<div class="card" id="disk-area"><div style="color:var(--text-dim);padding:8px">Loading…</div></div>
|
||||
</div>
|
||||
|
||||
<!-- Threat summary -->
|
||||
<div class="section">
|
||||
<div class="section-title"><span class="icon">⚠️</span> Threat Summary</div>
|
||||
<div class="grid g4">
|
||||
<div class="card card-red"><div class="card-label">Critical</div><div class="card-value" id="ov-tc">0</div></div>
|
||||
<div class="card card-orange"><div class="card-label">High</div><div class="card-value" id="ov-th">0</div></div>
|
||||
<div class="card card-yellow"><div class="card-label">Medium</div><div class="card-value" id="ov-tm">0</div></div>
|
||||
<div class="card card-green"><div class="card-label">Low</div><div class="card-value" id="ov-tl">0</div></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Scan Activity Chart -->
|
||||
<div class="section">
|
||||
<div class="section-title"><span class="icon">📈</span> Scan Activity (14 days)</div>
|
||||
<div class="card" style="padding:12px">
|
||||
<canvas id="scan-canvas" height="160" style="width:100%;display:block"></canvas>
|
||||
<div style="display:flex;gap:16px;justify-content:center;margin-top:8px;font-size:.75rem;color:var(--text-dim)">
|
||||
<span>🔵 Scans</span><span>🔴 Threats Found</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ═══════ TAB: THREATS ═══════ -->
|
||||
<div class="tab-panel" id="panel-threats">
|
||||
<div class="filters">
|
||||
<select id="f-severity"><option value="">All Severities</option><option value="CRITICAL">Critical</option><option value="HIGH">High</option><option value="MEDIUM">Medium</option><option value="LOW">Low</option></select>
|
||||
<select id="f-type"><option value="">All Types</option><option value="MALWARE">Malware</option><option value="MINER">Miner</option><option value="SPYWARE">Spyware</option><option value="VIRUS">Virus</option><option value="ROOTKIT">Rootkit</option></select>
|
||||
<input type="text" id="f-search" placeholder="Search threats…">
|
||||
</div>
|
||||
<div class="tbl-wrap">
|
||||
<table><thead><tr><th>Time</th><th>File Path</th><th>Threat</th><th>Type</th><th>Severity</th><th>Detector</th><th>AI Verdict</th><th>Status</th><th>Actions</th></tr></thead><tbody id="threat-tbody"><tr class="empty-row"><td colspan="9">Loading…</td></tr></tbody></table>
|
||||
</div>
|
||||
<div class="pager"><button class="btn btn-sm" id="threat-prev">← Prev</button><span id="threat-page">Page 1</span><button class="btn btn-sm" id="threat-next">Next →</button></div>
|
||||
</div>
|
||||
|
||||
<!-- ═══════ TAB: SCANS ═══════ -->
|
||||
<div class="tab-panel" id="panel-scans">
|
||||
<div class="btn-row">
|
||||
<button class="btn btn-primary" id="btn-quick-scan" onclick="doAction('quick-scan',this)">⚡ Run Quick Scan</button>
|
||||
<button class="btn" id="btn-full-scan" onclick="doAction('full-scan',this)">🔍 Run Full Scan</button>
|
||||
</div>
|
||||
<div class="section">
|
||||
<div class="section-title"><span class="icon">📈</span> Scan History (30 days)</div>
|
||||
<div class="card" style="padding:12px"><canvas id="scan-chart-canvas" height="160" style="width:100%;display:block"></canvas><div style="display:flex;gap:16px;justify-content:center;margin-top:8px;font-size:.75rem;color:var(--text-dim)"><span>🔵 Scans</span><span>🔴 Threats</span></div></div>
|
||||
</div>
|
||||
<div class="section">
|
||||
<div class="section-title"><span class="icon">📋</span> Recent Scans</div>
|
||||
<div class="tbl-wrap">
|
||||
<table><thead><tr><th>Time</th><th>Type</th><th>Path</th><th>Files</th><th>Threats</th><th>Duration</th><th>Status</th></tr></thead><tbody id="scan-tbody"><tr class="empty-row"><td colspan="7">Loading…</td></tr></tbody></table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ═══════ TAB: DEFINITIONS ═══════ -->
|
||||
<div class="tab-panel" id="panel-definitions">
|
||||
<div class="grid g4 section">
|
||||
<div class="card card-purple"><div class="card-label">Hashes</div><div class="card-value" id="def-hashes">0</div></div>
|
||||
<div class="card card-accent"><div class="card-label">Malicious IPs</div><div class="card-value" id="def-ips">0</div></div>
|
||||
<div class="card card-orange"><div class="card-label">Domains</div><div class="card-value" id="def-domains">0</div></div>
|
||||
<div class="card card-red"><div class="card-label">URLs</div><div class="card-value" id="def-urls">0</div></div>
|
||||
</div>
|
||||
<div class="btn-row">
|
||||
<button class="btn btn-primary" onclick="doAction('update-sigs',this)">🔄 Update All Feeds</button>
|
||||
<button class="btn btn-sm" onclick="doFeedUpdate('malwarebazaar',this)">MalwareBazaar</button>
|
||||
<button class="btn btn-sm" onclick="doFeedUpdate('threatfox',this)">ThreatFox</button>
|
||||
<button class="btn btn-sm" onclick="doFeedUpdate('urlhaus',this)">URLhaus</button>
|
||||
<button class="btn btn-sm" onclick="doFeedUpdate('feodotracker',this)">FeodoTracker</button>
|
||||
<button class="btn btn-sm" onclick="doFeedUpdate('emergingthreats',this)">EmergingThreats</button>
|
||||
</div>
|
||||
<div class="sub-tabs" id="def-subtabs">
|
||||
<div class="sub-tab active" data-def="all">All</div>
|
||||
<div class="sub-tab" data-def="hash">Hashes</div>
|
||||
<div class="sub-tab" data-def="ip">IPs</div>
|
||||
<div class="sub-tab" data-def="domain">Domains</div>
|
||||
<div class="sub-tab" data-def="url">URLs</div>
|
||||
</div>
|
||||
<div class="filters"><input type="text" id="def-search" placeholder="Search definitions…" style="flex:1;max-width:400px"></div>
|
||||
<div class="tbl-wrap"><table><thead id="def-thead"></thead><tbody id="def-tbody"><tr class="empty-row"><td colspan="6">Loading…</td></tr></tbody></table></div>
|
||||
<div class="pager"><button class="btn btn-sm" id="def-prev" onclick="defPage(-1)">← Prev</button><span id="def-page-info">Page 1</span><button class="btn btn-sm" id="def-next" onclick="defPage(1)">Next →</button></div>
|
||||
<div class="section" style="margin-top:20px">
|
||||
<div class="section-title"><span class="icon">🔄</span> Recent Updates</div>
|
||||
<div class="tbl-wrap"><table><thead><tr><th>Time</th><th>Feed</th><th>Hashes</th><th>IPs</th><th>Domains</th><th>URLs</th><th>Status</th></tr></thead><tbody id="sigup-tbody"></tbody></table></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ═══════ TAB: CONTAINERS ═══════ -->
|
||||
<div class="tab-panel" id="panel-containers">
|
||||
<div class="btn-row">
|
||||
<button class="btn btn-primary" id="btn-scan-containers" onclick="doAction('scan-containers',this)">🐳 Scan All Containers</button>
|
||||
</div>
|
||||
<div class="grid g3 section">
|
||||
<div class="card card-accent"><div class="card-label">Containers Found</div><div class="card-value" id="ct-count">0</div></div>
|
||||
<div class="card card-green"><div class="card-label">Available Runtimes</div><div class="card-value" id="ct-runtimes" style="font-size:1rem">—</div></div>
|
||||
<div class="card card-red"><div class="card-label">Container Threats</div><div class="card-value" id="ct-threats">0</div></div>
|
||||
</div>
|
||||
<div class="section">
|
||||
<div class="section-title"><span class="icon">📦</span> Discovered Containers</div>
|
||||
<div class="tbl-wrap">
|
||||
<table><thead><tr><th>ID</th><th>Name</th><th>Image</th><th>Runtime</th><th>Status</th><th>IP</th><th>Ports</th><th>Action</th></tr></thead>
|
||||
<tbody id="ct-tbody"><tr class="empty-row"><td colspan="8">Loading…</td></tr></tbody></table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section">
|
||||
<div class="section-title"><span class="icon">⚠️</span> Container Threats</div>
|
||||
<div class="tbl-wrap">
|
||||
<table><thead><tr><th>Time</th><th>Container</th><th>Threat</th><th>Type</th><th>Severity</th><th>Details</th></tr></thead>
|
||||
<tbody id="ct-threat-tbody"><tr class="empty-row"><td colspan="6">No container threats ✅</td></tr></tbody></table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ═══════ TAB: QUARANTINE ═══════ -->
|
||||
<div class="tab-panel" id="panel-quarantine">
|
||||
<div class="grid g2 section">
|
||||
<div class="card"><div class="card-label">Total Quarantined</div><div class="card-value" id="q-count">0</div></div>
|
||||
<div class="card"><div class="card-label">Vault Size</div><div class="card-value" id="q-size">0 B</div></div>
|
||||
</div>
|
||||
<div class="tbl-wrap"><table><thead><tr><th>ID</th><th>Original Path</th><th>Threat</th><th>Date</th><th>Size</th></tr></thead><tbody id="quar-tbody"><tr class="empty-row"><td colspan="5">Vault is empty ✅</td></tr></tbody></table></div>
|
||||
</div>
|
||||
|
||||
<!-- ═══════ TAB: LOGS ═══════ -->
|
||||
<div class="tab-panel" id="panel-logs">
|
||||
<div class="btn-row"><button class="btn btn-sm" onclick="loadLogs()">🔄 Refresh</button></div>
|
||||
<div class="log-view" id="log-view"><div style="color:var(--text-dim)">Loading…</div></div>
|
||||
</div>
|
||||
|
||||
</div><!-- /content -->
|
||||
|
||||
<!-- Toast area -->
|
||||
<div class="toast-area" id="toast-area"></div>
|
||||
|
||||
<script>
|
||||
/* ── State ── */
|
||||
let S={threats:[],threatPage:1,threatPerPage:25,defType:'all',defPage:1,defPerPage:50,defSearch:''};
|
||||
const $=id=>document.getElementById(id);
|
||||
const Q=(s,el)=>(el||document).querySelectorAll(s);
|
||||
|
||||
/* ── Helpers ── */
|
||||
function fmt(n){return n==null?'—':Number(n).toLocaleString()}
|
||||
function fmtBytes(b){if(!b)return '0 B';const u=['B','KB','MB','GB','TB'];let i=0;let v=b;while(v>=1024&&i<u.length-1){v/=1024;i++;}return v.toFixed(i?1:0)+' '+u[i];}
|
||||
function fmtDur(s){if(!s||s<0)return '0s';s=Math.round(s);if(s<60)return s+'s';if(s<3600)return Math.floor(s/60)+'m '+s%60+'s';return Math.floor(s/3600)+'h '+Math.floor(s%3600/60)+'m';}
|
||||
function ago(ts){if(!ts)return '—';const d=new Date(ts+'Z');const s=Math.floor((Date.now()-d)/1000);if(s<60)return s+'s ago';if(s<3600)return Math.floor(s/60)+'m ago';if(s<86400)return Math.floor(s/3600)+'h ago';return Math.floor(s/86400)+'d ago';}
|
||||
function sevBadge(s){const c=esc((s||'').toUpperCase());const cl=c.toLowerCase().replace(/[^a-z]/g,'');return `<span class="badge badge-${cl}">${c}</span>`;}
|
||||
function statusBadge(s){const m={completed:'success',success:'success',running:'running',failed:'error',error:'error'};const safe=esc(s||'');const cl=(m[s]||'info').replace(/[^a-z]/g,'');return `<span class="badge badge-${cl}">${safe}</span>`;}
|
||||
function esc(s){const d=document.createElement('div');d.textContent=s||'';return d.innerHTML;}
|
||||
function trunc(s,n){s=s||'';return s.length>n?s.slice(0,n)+'…':s;}
|
||||
function setGauge(id,pct,color){const g=$(id);if(!g)return;const c=g.querySelector('.gauge-fill');const t=g.querySelector('.gauge-text');const off=314-(314*Math.min(pct,100)/100);c.style.strokeDashoffset=off;if(color)c.style.stroke=color;t.textContent=Math.round(pct)+'%';}
|
||||
function gaugeColor(p){return p>90?'var(--red)':p>70?'var(--orange)':p>50?'var(--yellow)':'var(--green)';}
|
||||
|
||||
/* ── Toast ── */
|
||||
function toast(msg,type='info'){const t=document.createElement('div');t.className='toast toast-'+type;t.textContent=msg;$('toast-area').appendChild(t);setTimeout(()=>t.remove(),4000);}
|
||||
|
||||
/* ── API ── */
|
||||
async function api(path){try{const r=await fetch(path);if(!r.ok)throw new Error(r.statusText);return await r.json();}catch(e){console.error('API error:',path,e);return null;}}
|
||||
|
||||
/* ── Tab switching ── */
|
||||
Q('.nav-tab').forEach(t=>t.addEventListener('click',()=>{
|
||||
Q('.nav-tab').forEach(x=>x.classList.remove('active'));
|
||||
Q('.tab-panel').forEach(x=>x.classList.remove('active'));
|
||||
t.classList.add('active');
|
||||
$('panel-'+t.dataset.tab).classList.add('active');
|
||||
if(t.dataset.tab==='threats')loadThreats();
|
||||
if(t.dataset.tab==='scans')loadScans();
|
||||
if(t.dataset.tab==='definitions')loadDefs();
|
||||
if(t.dataset.tab==='containers')loadContainers();
|
||||
if(t.dataset.tab==='quarantine')loadQuarantine();
|
||||
if(t.dataset.tab==='logs')loadLogs();
|
||||
}));
|
||||
|
||||
/* ═══════ OVERVIEW ═══════ */
|
||||
/* ── Canvas Chart Helpers ── */
|
||||
let _cpuHistory=[];const _CPU_HIST_MAX=60;
|
||||
let _memHistory=[];const _MEM_HIST_MAX=60;
|
||||
|
||||
function drawLineChart(canvasId,datasets,opts={}){
|
||||
const cv=$(canvasId);if(!cv)return;
|
||||
const dpr=window.devicePixelRatio||1;
|
||||
const rect=cv.getBoundingClientRect();
|
||||
cv.width=rect.width*dpr;cv.height=(opts.height||rect.height)*dpr;
|
||||
const ctx=cv.getContext('2d');ctx.scale(dpr,dpr);
|
||||
const W=rect.width,H=opts.height||rect.height;
|
||||
const pad={t:10,r:10,b:24,l:42};
|
||||
const cw=W-pad.l-pad.r,ch=H-pad.t-pad.b;
|
||||
|
||||
// Background
|
||||
ctx.fillStyle='#0d1117';ctx.fillRect(0,0,W,H);
|
||||
|
||||
// Grid
|
||||
const gridLines=opts.gridLines||5;
|
||||
const maxVal=opts.maxVal||Math.max(...datasets.flatMap(d=>d.data),1);
|
||||
ctx.strokeStyle='#1e293b';ctx.lineWidth=1;ctx.font='10px system-ui';ctx.fillStyle='#6b7280';
|
||||
for(let i=0;i<=gridLines;i++){
|
||||
const y=pad.t+ch-(ch*i/gridLines);
|
||||
ctx.beginPath();ctx.moveTo(pad.l,y);ctx.lineTo(pad.l+cw,y);ctx.stroke();
|
||||
const v=((maxVal*i/gridLines)).toFixed(opts.decimals||0);
|
||||
ctx.fillText(v+(opts.unit||''),2,y+3);
|
||||
}
|
||||
|
||||
// Data lines
|
||||
datasets.forEach(ds=>{
|
||||
if(!ds.data.length)return;
|
||||
const n=ds.data.length;
|
||||
ctx.beginPath();ctx.strokeStyle=ds.color;ctx.lineWidth=ds.lineWidth||2;
|
||||
ds.data.forEach((v,i)=>{
|
||||
const x=pad.l+(cw*i/(n-1||1));
|
||||
const y=pad.t+ch-ch*(Math.min(v,maxVal)/maxVal);
|
||||
if(i===0)ctx.moveTo(x,y);else ctx.lineTo(x,y);
|
||||
});
|
||||
ctx.stroke();
|
||||
// Fill
|
||||
if(ds.fill){
|
||||
const n2=ds.data.length;
|
||||
ctx.lineTo(pad.l+cw,pad.t+ch);ctx.lineTo(pad.l,pad.t+ch);ctx.closePath();
|
||||
ctx.fillStyle=ds.fill;ctx.fill();
|
||||
}
|
||||
});
|
||||
|
||||
// Labels
|
||||
if(opts.labels&&opts.labels.length){
|
||||
ctx.fillStyle='#6b7280';ctx.font='9px system-ui';ctx.textAlign='center';
|
||||
const n=opts.labels.length;
|
||||
opts.labels.forEach((l,i)=>{
|
||||
if(i%Math.ceil(n/8)!==0&&i!==n-1)return;
|
||||
const x=pad.l+(cw*i/(n-1||1));
|
||||
ctx.fillText(l,x,H-2);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function drawBarChart(canvasId,data,opts={}){
|
||||
const cv=$(canvasId);if(!cv||!data.length)return;
|
||||
const dpr=window.devicePixelRatio||1;
|
||||
const rect=cv.getBoundingClientRect();
|
||||
cv.width=rect.width*dpr;cv.height=(opts.height||rect.height)*dpr;
|
||||
const ctx=cv.getContext('2d');ctx.scale(dpr,dpr);
|
||||
const W=rect.width,H=opts.height||rect.height;
|
||||
const pad={t:10,r:10,b:28,l:42};
|
||||
const cw=W-pad.l-pad.r,ch=H-pad.t-pad.b;
|
||||
|
||||
ctx.fillStyle='#0d1117';ctx.fillRect(0,0,W,H);
|
||||
|
||||
const maxVal=opts.maxVal||Math.max(...data.flatMap(d=>[(d.scans||0),(d.threats||0)]),1);
|
||||
// Grid
|
||||
ctx.strokeStyle='#1e293b';ctx.lineWidth=1;ctx.font='10px system-ui';ctx.fillStyle='#6b7280';
|
||||
for(let i=0;i<=4;i++){
|
||||
const y=pad.t+ch-(ch*i/4);
|
||||
ctx.beginPath();ctx.moveTo(pad.l,y);ctx.lineTo(pad.l+cw,y);ctx.stroke();
|
||||
ctx.fillText(Math.round(maxVal*i/4),2,y+3);
|
||||
}
|
||||
|
||||
const n=data.length;const bw=Math.max((cw/n)*0.35,2);const gap=cw/n;
|
||||
data.forEach((d,i)=>{
|
||||
const x=pad.l+gap*i+gap*0.15;
|
||||
const sh=ch*(d.scans||0)/maxVal;
|
||||
const th=ch*(d.threats||0)/maxVal;
|
||||
// Scans bar
|
||||
ctx.fillStyle='#3b82f6';ctx.fillRect(x,pad.t+ch-sh,bw,sh);
|
||||
// Threats bar
|
||||
ctx.fillStyle='#ef4444';ctx.fillRect(x+bw+1,pad.t+ch-th,bw,th);
|
||||
// Label
|
||||
ctx.fillStyle='#6b7280';ctx.font='9px system-ui';ctx.textAlign='center';
|
||||
const day=(d.day||'').slice(5);
|
||||
ctx.fillText(day,x+bw,H-4);
|
||||
});
|
||||
}
|
||||
|
||||
function drawCoreChart(canvasId,cores){
|
||||
const cv=$(canvasId);if(!cv||!cores.length)return;
|
||||
const dpr=window.devicePixelRatio||1;
|
||||
const rect=cv.getBoundingClientRect();
|
||||
cv.width=rect.width*dpr;cv.height=140*dpr;
|
||||
const ctx=cv.getContext('2d');ctx.scale(dpr,dpr);
|
||||
const W=rect.width,H=140;
|
||||
const pad={t:8,r:8,b:20,l:8};
|
||||
const n=cores.length;const gap=4;
|
||||
const bw=Math.min((W-pad.l-pad.r-(n-1)*gap)/n,60);
|
||||
|
||||
ctx.fillStyle='#0d1117';ctx.fillRect(0,0,W,H);
|
||||
|
||||
cores.forEach((pct,i)=>{
|
||||
const x=pad.l+i*(bw+gap);
|
||||
const barH=(H-pad.t-pad.b)*(pct/100);
|
||||
const c=pct>90?'#ef4444':pct>70?'#f59e0b':pct>50?'#eab308':'#3b82f6';
|
||||
// Background
|
||||
ctx.fillStyle='#1e293b';ctx.fillRect(x,pad.t,bw,H-pad.t-pad.b);
|
||||
// Bar
|
||||
ctx.fillStyle=c;ctx.fillRect(x,H-pad.b-barH,bw,barH);
|
||||
// Label
|
||||
ctx.fillStyle='#e2e8f0';ctx.font='bold 10px system-ui';ctx.textAlign='center';
|
||||
ctx.fillText(Math.round(pct)+'%',x+bw/2,H-pad.b-barH-4>pad.t?H-pad.b-barH-4:pad.t+12);
|
||||
ctx.fillStyle='#6b7280';ctx.font='9px system-ui';
|
||||
ctx.fillText('C'+i,x+bw/2,H-4);
|
||||
});
|
||||
}
|
||||
|
||||
function renderMemBars(h){
|
||||
const total=h.mem_total||1;
|
||||
const used=h.mem_used||0;
|
||||
const cached=h.mem_cached||0;
|
||||
const buffers=h.mem_buffers||0;
|
||||
const avail=h.mem_available||0;
|
||||
const app=used-cached-buffers;
|
||||
const items=[
|
||||
{label:'App/Used',val:Math.max(app,0),color:'var(--purple)'},
|
||||
{label:'Cached',val:cached,color:'var(--cyan)'},
|
||||
{label:'Buffers',val:buffers,color:'var(--accent)'},
|
||||
{label:'Available',val:avail,color:'var(--green)'},
|
||||
];
|
||||
$('mem-bars').innerHTML=items.map(it=>{
|
||||
const pct=(it.val/total*100).toFixed(1);
|
||||
return `<div class="disk-row"><div class="disk-mount" style="width:70px"><span style="display:inline-block;width:8px;height:8px;border-radius:50%;background:${it.color};margin-right:4px"></span>${it.label}</div><div class="disk-bar-outer"><div class="disk-bar-inner" style="width:${pct}%;background:${it.color}"></div></div><div class="disk-pct">${fmtBytes(it.val)}</div></div>`;
|
||||
}).join('');
|
||||
// Swap
|
||||
const spct=h.swap_total?(h.swap_used/h.swap_total*100).toFixed(1):0;
|
||||
$('swap-bar').innerHTML=h.swap_total?`<div class="disk-row"><div class="disk-mount" style="width:70px">${fmtBytes(h.swap_used)}/${fmtBytes(h.swap_total)}</div><div class="disk-bar-outer"><div class="disk-bar-inner" style="width:${spct}%;background:var(--orange)"></div></div><div class="disk-pct">${spct}%</div></div>`:'<div style="color:var(--text-dim);font-size:.8rem">No swap</div>';
|
||||
}
|
||||
|
||||
async function loadOverview(){
|
||||
const [st,h,ts,ch]=await Promise.all([api('/api/status'),api('/api/health'),api('/api/threat-stats'),api('/api/scan-chart?days=14')]);
|
||||
if(st){
|
||||
$('hd-host').textContent=st.hostname||'—';
|
||||
$('hd-uptime').textContent=fmtDur(st.uptime_seconds);
|
||||
$('hd-time').textContent=st.server_time||'';
|
||||
const ls=st.last_scan;
|
||||
$('ov-scan').textContent=ls?ago(ls.timestamp):'Never';
|
||||
$('ov-scan-sub').textContent=ls?`${fmt(ls.files_scanned)} files, ${ls.threats_found} threats`:'No scans yet';
|
||||
const sig=st.signatures||{};
|
||||
$('ov-sigs').textContent=fmt((sig.total_hashes||0)+(sig.total_ips||0)+(sig.total_domains||0)+(sig.total_urls||0));
|
||||
$('ov-sigs-sub').textContent=sig.last_update?'Updated '+ago(sig.last_update):'Not updated';
|
||||
$('ov-quar').textContent=fmt(st.quarantine_count);
|
||||
}
|
||||
if(h){
|
||||
// CPU per-core chart
|
||||
const cores=h.cpu_per_core||[];
|
||||
if(cores.length){
|
||||
drawCoreChart('cpu-canvas',cores);
|
||||
$('cpu-summary').textContent=`${cores.length} cores @ ${h.cpu_freq_mhz||'?'} MHz — avg ${Math.round(h.cpu_percent)}%`;
|
||||
}
|
||||
// CPU history
|
||||
_cpuHistory.push(h.cpu_percent||0);if(_cpuHistory.length>_CPU_HIST_MAX)_cpuHistory.shift();
|
||||
|
||||
// Memory
|
||||
_memHistory.push(h.mem_percent||0);if(_memHistory.length>_MEM_HIST_MAX)_memHistory.shift();
|
||||
drawLineChart('mem-canvas',[
|
||||
{data:_memHistory,color:'#8b5cf6',fill:'rgba(139,92,246,0.1)',lineWidth:2},
|
||||
],{maxVal:100,unit:'%',height:160,gridLines:4});
|
||||
$('mem-summary').textContent=`${fmtBytes(h.mem_used)} / ${fmtBytes(h.mem_total)} (${h.mem_percent?.toFixed(1)}%)`;
|
||||
renderMemBars(h);
|
||||
|
||||
// Load / Net / Freq
|
||||
const la=h.load_avg||[0,0,0];
|
||||
$('ov-load').textContent=la.map(v=>v.toFixed(2)).join(' / ');
|
||||
$('ov-netconn').textContent=fmt(h.net_connections||0);
|
||||
$('ov-freq').textContent=h.cpu_freq_mhz?h.cpu_freq_mhz+' MHz':'—';
|
||||
|
||||
// Top processes
|
||||
const procs=h.top_processes||[];
|
||||
const ptb=$('proc-tbody');
|
||||
if(procs.length){
|
||||
ptb.innerHTML=procs.map(p=>{
|
||||
const cpuC=p.cpu>50?'var(--red)':p.cpu>20?'var(--orange)':'var(--text)';
|
||||
return `<tr><td class="mono">${p.pid}</td><td>${esc(p.name)}</td><td style="color:${cpuC};font-weight:600">${p.cpu}%</td><td>${p.mem}%</td></tr>`;
|
||||
}).join('');
|
||||
}else{ptb.innerHTML='<tr class="empty-row"><td colspan="4">No active processes</td></tr>';}
|
||||
|
||||
// Disks
|
||||
const da=$('disk-area');
|
||||
const disks=h.disk_usage||[];
|
||||
if(disks.length){
|
||||
da.innerHTML=disks.map(d=>{
|
||||
const p=d.percent||0;const c=p>90?'var(--red)':p>70?'var(--orange)':'var(--accent)';
|
||||
return `<div class="disk-row"><div class="disk-mount" title="${esc(d.mount)}">${esc(d.mount)}</div><div class="disk-bar-outer"><div class="disk-bar-inner" style="width:${p}%;background:${c}"></div></div><div class="disk-pct">${p.toFixed(1)}%</div><div style="font-size:.75rem;color:var(--text-dim);min-width:100px">${fmtBytes(d.used)} / ${fmtBytes(d.total)}</div></div>`;
|
||||
}).join('');
|
||||
}else{da.innerHTML='<div style="color:var(--text-dim);padding:8px">No disk info</div>';}
|
||||
}
|
||||
if(ts){
|
||||
const bs=ts.by_severity||{};
|
||||
$('ov-tc').textContent=fmt(bs.CRITICAL||0);
|
||||
$('ov-th').textContent=fmt(bs.HIGH||0);
|
||||
$('ov-tm').textContent=fmt(bs.MEDIUM||0);
|
||||
$('ov-tl').textContent=fmt(bs.LOW||0);
|
||||
}
|
||||
if(ch&&ch.chart&&ch.chart.length){
|
||||
drawBarChart('scan-canvas',ch.chart.slice(-14),{height:160});
|
||||
}
|
||||
}
|
||||
|
||||
/* ═══════ THREATS ═══════ */
|
||||
async function loadThreats(){
|
||||
const d=await api(`/api/threats?limit=200`);
|
||||
if(!d)return;
|
||||
S.threats=d.threats||[];
|
||||
renderThreats();
|
||||
}
|
||||
function renderThreats(){
|
||||
const sev=$('f-severity').value.toUpperCase();
|
||||
const typ=$('f-type').value.toUpperCase();
|
||||
const q=$('f-search').value.toLowerCase();
|
||||
let f=S.threats;
|
||||
if(sev)f=f.filter(t=>(t.severity||'').toUpperCase()===sev);
|
||||
if(typ)f=f.filter(t=>(t.threat_type||'').toUpperCase()===typ);
|
||||
if(q)f=f.filter(t=>(t.threat_name||'').toLowerCase().includes(q)||(t.file_path||'').toLowerCase().includes(q));
|
||||
const total=f.length;const pages=Math.max(Math.ceil(total/S.threatPerPage),1);
|
||||
S.threatPage=Math.min(S.threatPage,pages);
|
||||
const start=(S.threatPage-1)*S.threatPerPage;
|
||||
const slice=f.slice(start,start+S.threatPerPage);
|
||||
const tb=$('threat-tbody');
|
||||
if(!slice.length){tb.innerHTML='<tr class="empty-row"><td colspan="9">No threats detected ✅</td></tr>';
|
||||
}else{tb.innerHTML=slice.map(t=>{
|
||||
const act=t.action_taken||'detected';
|
||||
const st=act==='detected'?'<span class="badge badge-warning">detected</span>':act==='quarantined'?'<span class="badge badge-info">quarantined</span>':statusBadge(act);
|
||||
let btns='';
|
||||
if(act==='detected'||act==='monitoring'){
|
||||
btns=`<div style="display:flex;gap:4px;flex-wrap:wrap"><button class="btn btn-sm" style="background:var(--purple);color:#fff;border-color:var(--purple);font-size:.7rem;padding:3px 8px" onclick="aiAnalyze(${t.id},this)">🧠 AI Analyze</button><button class="btn btn-sm" style="background:var(--red);color:#fff;border-color:var(--red);font-size:.7rem;padding:3px 8px" onclick="threatAction('quarantine',${t.id},'${esc(t.file_path).replace(/'/g,"\\'")}','${esc(t.threat_name).replace(/'/g,"\\'")}',this)">🔒 Quarantine</button><button class="btn btn-sm" style="font-size:.7rem;padding:3px 8px" onclick="threatAction('delete-threat',${t.id},'${esc(t.file_path).replace(/'/g,"\\'")}','',this)">🗑️ Delete</button><button class="btn btn-sm" style="font-size:.7rem;padding:3px 8px" onclick="threatAction('whitelist',${t.id},'','',this)">✅ Ignore</button></div>`;
|
||||
} else if(act==='quarantined'){
|
||||
btns=`<div style="display:flex;gap:4px;flex-wrap:wrap"><button class="btn btn-sm" style="background:var(--purple);color:#fff;border-color:var(--purple);font-size:.7rem;padding:3px 8px" onclick="aiAnalyze(${t.id},this)">🧠 AI Analyze</button><button class="btn btn-sm" style="background:var(--green);color:#fff;border-color:var(--green);font-size:.7rem;padding:3px 8px" onclick="threatAction('restore',${t.id},'${esc(t.file_path).replace(/'/g,"\\'")}','',this)">♻️ Restore</button><button class="btn btn-sm" style="font-size:.7rem;padding:3px 8px" onclick="threatAction('delete-threat',${t.id},'${esc(t.file_path).replace(/'/g,"\\'")}','',this)">🗑️ Delete</button><button class="btn btn-sm" style="font-size:.7rem;padding:3px 8px" onclick="threatAction('whitelist',${t.id},'','',this)">✅ Ignore</button></div>`;
|
||||
} else {
|
||||
btns=`<span style="color:var(--text-dim);font-size:.75rem">${esc(act)}</span>`;
|
||||
}
|
||||
const det=t.details||'';
|
||||
let aiCol='<span style="color:var(--text-dim);font-size:.75rem">—</span>';
|
||||
const aiMatch=det.match(/\[AI:\s*(\w+)\s+(\d+)%\]\s*(.*)/);
|
||||
if(aiMatch){const v=aiMatch[1],c=aiMatch[2],rsn=aiMatch[3];const vc=v==='safe'?'var(--green)':v==='threat'?'var(--red)':'var(--orange)';aiCol=`<div style="font-size:.75rem"><span style="color:${vc};font-weight:700">${v.toUpperCase()}</span> <span style="color:var(--text-dim)">${c}%</span><div style="color:var(--text-dim);max-width:150px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="${esc(rsn)}">${esc(rsn)}</div></div>`;}
|
||||
return `<tr><td>${ago(t.timestamp)}</td><td class="mono trunc" title="${esc(t.file_path)}">${esc(trunc(t.file_path,50))}</td><td>${esc(t.threat_name)}</td><td>${esc(t.threat_type)}</td><td>${sevBadge(t.severity)}</td><td>${esc(t.detector)}</td><td>${aiCol}</td><td>${st}</td><td>${btns}</td></tr>`;
|
||||
}).join('');}
|
||||
$('threat-page').textContent=`Page ${S.threatPage} of ${pages} (${total})`;
|
||||
}
|
||||
$('threat-prev').onclick=()=>{S.threatPage=Math.max(1,S.threatPage-1);renderThreats();};
|
||||
$('threat-next').onclick=()=>{S.threatPage++;renderThreats();};
|
||||
$('f-severity').onchange=$('f-type').onchange=()=>{S.threatPage=1;renderThreats();};
|
||||
let _tTimer;$('f-search').oninput=()=>{clearTimeout(_tTimer);_tTimer=setTimeout(()=>{S.threatPage=1;renderThreats();},300);};
|
||||
|
||||
/* ═══════ SCANS ═══════ */
|
||||
async function loadScans(){
|
||||
const [sc,ch]=await Promise.all([api('/api/scans?limit=30'),api('/api/scan-chart?days=30')]);
|
||||
if(sc){
|
||||
const tb=$('scan-tbody');
|
||||
const scans=sc.scans||[];
|
||||
if(!scans.length){tb.innerHTML='<tr class="empty-row"><td colspan="7">No scans yet</td></tr>';
|
||||
}else{tb.innerHTML=scans.map(s=>`<tr><td>${ago(s.timestamp)}</td><td>${esc(s.scan_type)}</td><td class="mono trunc" title="${esc(s.scan_path)}">${esc(trunc(s.scan_path,40))}</td><td>${fmt(s.files_scanned)}</td><td>${s.threats_found?'<span style="color:var(--red)">'+s.threats_found+'</span>':'0'}</td><td>${fmtDur(s.duration_seconds)}</td><td>${statusBadge(s.status)}</td></tr>`).join('');}
|
||||
}
|
||||
if(ch&&ch.chart&&ch.chart.length)drawBarChart('scan-chart-canvas',ch.chart,{height:160});
|
||||
}
|
||||
|
||||
/* ═══════ DEFINITIONS ═══════ */
|
||||
Q('#def-subtabs .sub-tab').forEach(t=>t.addEventListener('click',()=>{
|
||||
Q('#def-subtabs .sub-tab').forEach(x=>x.classList.remove('active'));
|
||||
t.classList.add('active');S.defType=t.dataset.def;S.defPage=1;loadDefs();
|
||||
}));
|
||||
let _dTimer;$('def-search').oninput=()=>{clearTimeout(_dTimer);_dTimer=setTimeout(()=>{S.defPage=1;S.defSearch=$('def-search').value;loadDefs();},400);};
|
||||
function defPage(d){S.defPage=Math.max(1,S.defPage+d);loadDefs();}
|
||||
|
||||
async function loadDefs(){
|
||||
const typ=S.defType==='all'?'':S.defType;
|
||||
const q=encodeURIComponent(S.defSearch||'');
|
||||
const [dd,su]=await Promise.all([
|
||||
api(`/api/definitions?type=${typ}&page=${S.defPage}&per_page=${S.defPerPage}&search=${q}`),
|
||||
api('/api/sig-updates?limit=10')
|
||||
]);
|
||||
if(dd){
|
||||
$('def-hashes').textContent=fmt(dd.total_hashes);
|
||||
$('def-ips').textContent=fmt(dd.total_ips);
|
||||
$('def-domains').textContent=fmt(dd.total_domains);
|
||||
$('def-urls').textContent=fmt(dd.total_urls);
|
||||
renderDefTable(dd);
|
||||
const total=dd.total_hashes+dd.total_ips+dd.total_domains+dd.total_urls;
|
||||
const pages=Math.max(Math.ceil(total/S.defPerPage),1);
|
||||
$('def-page-info').textContent=`Page ${S.defPage} of ${pages}`;
|
||||
}
|
||||
if(su){
|
||||
const tb=$('sigup-tbody');
|
||||
const ups=su.updates||[];
|
||||
if(!ups.length){tb.innerHTML='<tr class="empty-row"><td colspan="7">No updates yet</td></tr>';
|
||||
}else{tb.innerHTML=ups.map(u=>`<tr><td>${ago(u.timestamp)}</td><td>${esc(u.feed_name)}</td><td>${fmt(u.hashes_added)}</td><td>${fmt(u.ips_added)}</td><td>${fmt(u.domains_added)}</td><td>${fmt(u.urls_added)}</td><td>${statusBadge(u.status)}</td></tr>`).join('');}
|
||||
}
|
||||
}
|
||||
function renderDefTable(dd){
|
||||
const th=$('def-thead');const tb=$('def-tbody');
|
||||
let rows=[];const t=S.defType;
|
||||
if(t==='all'||t==='hash'){
|
||||
th.innerHTML='<tr><th>Hash</th><th>Threat Name</th><th>Type</th><th>Severity</th><th>Source</th><th>Date</th></tr>';
|
||||
rows=rows.concat((dd.hashes||[]).map(r=>`<tr><td class="mono">${esc(trunc(r.hash,16))}</td><td>${esc(r.threat_name)}</td><td>${esc(r.threat_type)}</td><td>${sevBadge(r.severity)}</td><td>${esc(r.source)}</td><td>${ago(r.added_date)}</td></tr>`));
|
||||
}
|
||||
if(t==='all'||t==='ip'){
|
||||
if(t==='ip')th.innerHTML='<tr><th>IP Address</th><th>Threat Name</th><th>Type</th><th>Source</th><th>Date</th></tr>';
|
||||
rows=rows.concat((dd.ips||[]).map(r=>`<tr><td class="mono">${esc(r.ip)}</td><td>${esc(r.threat_name)}</td><td>${esc(r.type)}</td><td>${esc(r.source)}</td><td>${ago(r.added_date)}</td></tr>`));
|
||||
}
|
||||
if(t==='all'||t==='domain'){
|
||||
if(t==='domain')th.innerHTML='<tr><th>Domain</th><th>Threat Name</th><th>Type</th><th>Source</th><th>Date</th></tr>';
|
||||
rows=rows.concat((dd.domains||[]).map(r=>`<tr><td class="mono">${esc(r.domain)}</td><td>${esc(r.threat_name)}</td><td>${esc(r.type)}</td><td>${esc(r.source)}</td><td>${ago(r.added_date)}</td></tr>`));
|
||||
}
|
||||
if(t==='all'||t==='url'){
|
||||
if(t==='url')th.innerHTML='<tr><th>URL</th><th>Threat Name</th><th>Type</th><th>Source</th><th>Date</th></tr>';
|
||||
rows=rows.concat((dd.urls||[]).map(r=>`<tr><td class="mono trunc" title="${esc(r.url)}">${esc(trunc(r.url,60))}</td><td>${esc(r.threat_name)}</td><td>${esc(r.type)}</td><td>${esc(r.source)}</td><td>${ago(r.added_date)}</td></tr>`));
|
||||
}
|
||||
if(t==='all'&&!rows.length&&!dd.hashes?.length)th.innerHTML='<tr><th>Hash</th><th>Threat Name</th><th>Type</th><th>Severity</th><th>Source</th><th>Date</th></tr>';
|
||||
tb.innerHTML=rows.length?rows.join(''):'<tr class="empty-row"><td colspan="6">No definitions found. Run an update to fetch threat feeds.</td></tr>';
|
||||
}
|
||||
|
||||
/* ═══════ CONTAINERS ═══════ */
|
||||
async function loadContainers(){
|
||||
const [cl,cs]=await Promise.all([api('/api/containers'),api('/api/container-scan')]);
|
||||
if(cl){
|
||||
$('ct-count').textContent=fmt(cl.count);
|
||||
$('ct-runtimes').textContent=cl.runtimes.length?cl.runtimes.join(', '):'None detected';
|
||||
const tb=$('ct-tbody');
|
||||
const cc=cl.containers||[];
|
||||
if(!cc.length){tb.innerHTML='<tr class="empty-row"><td colspan="8">No containers found. Install Docker, Podman, or LXC.</td></tr>';
|
||||
}else{
|
||||
tb.innerHTML=cc.map(c=>{
|
||||
const st=c.status==='running'?'<span class="badge badge-success">running</span>':c.status==='stopped'?'<span class="badge badge-error">stopped</span>':'<span class="badge badge-warning">'+esc(c.status)+'</span>';
|
||||
const ports=(c.ports||[]).slice(0,3).join(', ')||(c.status==='running'?'—':'');
|
||||
return `<tr><td class="mono">${esc(trunc(c.container_id,12))}</td><td>${esc(c.name)}</td><td class="mono trunc" title="${esc(c.image)}">${esc(trunc(c.image,30))}</td><td>${esc(c.runtime)}</td><td>${st}</td><td class="mono">${esc(c.ip_address||'—')}</td><td class="mono" style="font-size:.72rem">${esc(ports)}</td><td><button class="btn btn-sm" onclick="scanSingleContainer('${esc(c.container_id)}',this)">Scan</button></td></tr>`;
|
||||
}).join('');
|
||||
}
|
||||
}
|
||||
if(cs){
|
||||
const threats=cs.threats||[];
|
||||
$('ct-threats').textContent=fmt(threats.length);
|
||||
const tb=$('ct-threat-tbody');
|
||||
if(!threats.length){tb.innerHTML='<tr class="empty-row"><td colspan="6">No container threats ✅</td></tr>';
|
||||
}else{tb.innerHTML=threats.map(t=>`<tr><td>${ago(t.timestamp)}</td><td>${esc(trunc(t.file_path,30))}</td><td>${esc(t.threat_name)}</td><td>${esc(t.threat_type)}</td><td>${sevBadge(t.severity)}</td><td class="trunc" title="${esc(t.details)}">${esc(trunc(t.details,60))}</td></tr>`).join('');}
|
||||
}
|
||||
}
|
||||
async function scanSingleContainer(id,btn){
|
||||
const orig=btn.innerHTML;btn.innerHTML='<span class="spinner"></span>';btn.disabled=true;
|
||||
toast(`Scanning container ${id.slice(0,12)}…`,'info');
|
||||
try{
|
||||
const r=await fetch('/api/actions/scan-container',{method:'POST',headers:{'Content-Type':'application/json','X-API-Key':window.AYN_API_KEY||''},body:JSON.stringify({container_id:id})});
|
||||
const ct=r.headers.get('content-type')||'';
|
||||
if(!ct.includes('application/json')){const t=await r.text();toast('Server error: '+t.slice(0,100),'error');btn.innerHTML=orig;btn.disabled=false;return;}
|
||||
const d=await r.json();
|
||||
if(d.status==='error'){toast(d.error||'Failed','error');}
|
||||
else{toast(`Container scan: ${d.threats_found||0} threats found`,'success');}
|
||||
loadContainers();
|
||||
}catch(e){toast('Failed: '+e.message,'error');}
|
||||
btn.innerHTML=orig;btn.disabled=false;
|
||||
}
|
||||
|
||||
/* ═══════ QUARANTINE ═══════ */
|
||||
async function loadQuarantine(){
|
||||
const d=await api('/api/quarantine');
|
||||
if(!d)return;
|
||||
$('q-count').textContent=fmt(d.count);
|
||||
$('q-size').textContent=fmtBytes(d.total_size);
|
||||
const tb=$('quar-tbody');
|
||||
const items=d.items||[];
|
||||
if(!items.length){tb.innerHTML='<tr class="empty-row"><td colspan="5">Vault is empty ✅</td></tr>';
|
||||
}else{tb.innerHTML=items.map(i=>`<tr><td class="mono">${esc(trunc(i.id,12))}</td><td class="mono trunc" title="${esc(i.original_path)}">${esc(trunc(i.original_path,50))}</td><td>${esc(i.threat_name)}</td><td>${ago(i.quarantine_date)}</td><td>${fmtBytes(i.size||i.file_size||0)}</td></tr>`).join('');}
|
||||
}
|
||||
|
||||
/* ═══════ LOGS ═══════ */
|
||||
async function loadLogs(){
|
||||
const d=await api('/api/logs?limit=50');
|
||||
if(!d)return;
|
||||
const lv=$('log-view');
|
||||
const logs=d.logs||[];
|
||||
if(!logs.length){lv.innerHTML='<div style="color:var(--text-dim);padding:12px">No activity yet.</div>';return;}
|
||||
lv.innerHTML=logs.map(l=>{
|
||||
const lc=l.level==='ERROR'?'var(--red)':l.level==='WARNING'?'var(--orange)':'var(--accent)';
|
||||
return `<div class="log-line"><span class="log-ts">${l.timestamp||''}</span><span style="color:${lc};font-weight:700;min-width:56px">${l.level}</span><span class="log-src">${esc(l.source)}</span><span class="log-msg">${esc(l.message)}</span></div>`;
|
||||
}).join('');
|
||||
lv.scrollTop=0;
|
||||
}
|
||||
|
||||
/* ═══════ AI ANALYSIS ═══════ */
|
||||
async function aiAnalyze(threatId,btn){
|
||||
const orig=btn.innerHTML;btn.innerHTML='<span class="spinner"></span> Analyzing…';btn.disabled=true;
|
||||
try{
|
||||
const r=await fetch('/api/actions/ai-analyze',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({threat_id:threatId})});
|
||||
const ct=r.headers.get('content-type')||'';
|
||||
if(!ct.includes('application/json')){toast('Server error','error');btn.innerHTML=orig;btn.disabled=false;return;}
|
||||
const d=await r.json();
|
||||
if(d.status==='ok'){
|
||||
const emoji=d.verdict==='safe'?'✅':d.verdict==='threat'?'🚨':'⚠️';
|
||||
const color=d.verdict==='safe'?'success':d.verdict==='threat'?'error':'info';
|
||||
toast(`${emoji} AI: ${d.verdict.toUpperCase()} (${d.confidence}%) — ${d.reason}`,color);
|
||||
if(d.verdict==='safe'){toast(`Recommended: ${d.recommended_action}`,'info');}
|
||||
loadThreats();
|
||||
} else {toast(d.error||'AI analysis failed','error');}
|
||||
}catch(e){toast('Failed: '+e.message,'error');}
|
||||
btn.innerHTML=orig;btn.disabled=false;
|
||||
}
|
||||
|
||||
/* ═══════ THREAT ACTIONS ═══════ */
|
||||
async function threatAction(action,threatId,filePath,threatName,btn){
|
||||
const labels={'quarantine':'Quarantine','delete-threat':'Delete','whitelist':'Whitelist'};
|
||||
if(action==='delete-threat'&&!confirm('Permanently delete '+filePath+'?'))return;
|
||||
const orig=btn.parentElement.innerHTML;btn.parentElement.innerHTML='<span class="spinner"></span>';
|
||||
try{
|
||||
const body={threat_id:threatId};
|
||||
if(filePath)body.file_path=filePath;
|
||||
if(threatName)body.threat_name=threatName;
|
||||
const r=await fetch('/api/actions/'+action,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)});
|
||||
const ct=r.headers.get('content-type')||'';
|
||||
if(!ct.includes('application/json')){toast('Server error','error');return;}
|
||||
const d=await r.json();
|
||||
if(d.status==='ok'){toast((labels[action]||action)+' done','success');loadThreats();}
|
||||
else{toast(d.error||'Failed','error');}
|
||||
}catch(e){toast('Failed: '+e.message,'error');}
|
||||
}
|
||||
|
||||
/* ═══════ ACTIONS ═══════ */
|
||||
async function doAction(action,btn){
|
||||
const orig=btn.innerHTML;btn.innerHTML='<span class="spinner"></span> Running…';btn.disabled=true;
|
||||
toast(`${action} started…`,'info');
|
||||
try{
|
||||
const r=await fetch(`/api/actions/${action}`,{method:'POST',headers:{'X-API-Key':window.AYN_API_KEY||''}});
|
||||
const ct=r.headers.get('content-type')||'';
|
||||
if(!ct.includes('application/json')){const t=await r.text();toast('Server error: '+(r.status>=400?r.status+' ':'')+t.slice(0,100),'error');btn.innerHTML=orig;btn.disabled=false;return;}
|
||||
const d=await r.json();
|
||||
if(d.status==='error'){toast(d.error||'Failed','error');}
|
||||
else{toast(`${action} completed`,'success');}
|
||||
refreshAll();
|
||||
}catch(e){toast('Request failed: '+e.message,'error');}
|
||||
btn.innerHTML=orig;btn.disabled=false;
|
||||
}
|
||||
async function doFeedUpdate(feed,btn){
|
||||
const orig=btn.innerHTML;btn.innerHTML='<span class="spinner"></span>';btn.disabled=true;
|
||||
toast(`Updating ${feed}…`,'info');
|
||||
try{
|
||||
const r=await fetch('/api/actions/update-feed',{method:'POST',headers:{'Content-Type':'application/json','X-API-Key':window.AYN_API_KEY||''},body:JSON.stringify({feed})});
|
||||
const ct=r.headers.get('content-type')||'';
|
||||
if(!ct.includes('application/json')){const t=await r.text();toast(`${feed}: Server error `+t.slice(0,100),'error');btn.innerHTML=orig;btn.disabled=false;return;}
|
||||
const d=await r.json();
|
||||
if(d.status==='error'){toast(`${feed}: ${d.error}`,'error');}
|
||||
else{toast(`${feed} updated`,'success');}
|
||||
loadDefs();
|
||||
}catch(e){toast('Failed: '+e.message,'error');}
|
||||
btn.innerHTML=orig;btn.disabled=false;
|
||||
}
|
||||
|
||||
/* ═══════ REFRESH ═══════ */
|
||||
async function refreshAll(){
|
||||
await loadOverview();
|
||||
const active=document.querySelector('.nav-tab.active');
|
||||
if(active){
|
||||
const tab=active.dataset.tab;
|
||||
if(tab==='threats')loadThreats();
|
||||
if(tab==='scans')loadScans();
|
||||
if(tab==='definitions')loadDefs();
|
||||
if(tab==='containers')loadContainers();
|
||||
if(tab==='quarantine')loadQuarantine();
|
||||
if(tab==='logs')loadLogs();
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Boot ── */
|
||||
refreshAll();
|
||||
setInterval(refreshAll,30000);
|
||||
</script>
|
||||
</body>
|
||||
</html>"""
|
||||
Reference in New Issue
Block a user