diff --git a/dashboard/data.js b/dashboard/data.js new file mode 100644 index 0000000..6cc210f --- /dev/null +++ b/dashboard/data.js @@ -0,0 +1,348 @@ +// Auto-generated by siem_analyzer.py -- do not edit by hand +const SIEM_DATA = { + "generated_at": "2026-07-29T00:57:48.516747Z", + "meta": { + "endpoints": [ + "web-server (endpoint-1)", + "firewall (endpoint-2)", + "ssh-auth (endpoint-3)" + ], + "lines_parsed": { + "web-server (endpoint-1)": 45, + "firewall (endpoint-2)": 34, + "ssh-auth (endpoint-3)": 27 + } + }, + "stats": { + "total_alerts": 17, + "by_severity": { + "critical": 9, + "high": 5, + "medium": 3, + "low": 0 + }, + "unique_malicious_ips": 4, + "total_events_parsed": 106 + }, + "alerts": [ + { + "id": "ALT-0012", + "timestamp": "2026-07-27T10:07:41", + "endpoint": "ssh-auth (endpoint-3)", + "src_ip": "198.51.100.234", + "alert_type": "BRUTE_FORCE_SSH", + "severity": "high", + "description": "12 failed SSH login attempts within 300s trying 9 usernames (admin, administrator, backup, guest, oracle, postgres)", + "evidence": [ + "Jul 27 10:07:41 app-srv-05 sshd[10201]: Failed password for invalid user admin from 198.51.100.234 port 39001 ssh2", + "Jul 27 10:07:43 app-srv-05 sshd[10202]: Failed password for invalid user admin from 198.51.100.234 port 39002 ssh2", + "Jul 27 10:07:45 app-srv-05 sshd[10203]: Failed password for invalid user administrator from 198.51.100.234 port 39003 ssh2", + "Jul 27 10:07:47 app-srv-05 sshd[10204]: Failed password for invalid user user from 198.51.100.234 port 39004 ssh2", + "Jul 27 10:07:49 app-srv-05 sshd[10205]: Failed password for invalid user guest from 198.51.100.234 port 39005 ssh2" + ] + }, + { + "id": "ALT-0006", + "timestamp": "2026-07-27T10:06:11", + "endpoint": "web-server (endpoint-1)", + "src_ip": "192.0.2.77", + "alert_type": "SQL_INJECTION", + "severity": "critical", + "description": "Sql Injection attempt detected in request to /api/users?id=1'%20OR%20'1'='1", + "evidence": [ + "192.0.2.77 - - [27/Jul/2026:10:06:11 +0000] \"GET /api/users?id=1'%20OR%20'1'='1 HTTP/1.1\" 500 640 \"-\" \"python-requests/2.31\"" + ] + }, + { + "id": "ALT-0009", + "timestamp": "2026-07-27T10:06:00", + "endpoint": "web-server (endpoint-1)", + "src_ip": "192.0.2.77", + "alert_type": "HIGH_REQUEST_RATE", + "severity": "medium", + "description": "21 requests from a single IP within 60s (possible scraping/DoS/automation)", + "evidence": [ + "192.0.2.77 - - [27/Jul/2026:10:06:00 +0000] \"GET /api/data?page=1 HTTP/1.1\" 200 1200 \"-\" \"python-requests/2.31\"", + "192.0.2.77 - - [27/Jul/2026:10:06:01 +0000] \"GET /api/data?page=2 HTTP/1.1\" 200 1200 \"-\" \"python-requests/2.31\"", + "192.0.2.77 - - [27/Jul/2026:10:06:01 +0000] \"GET /api/data?page=3 HTTP/1.1\" 200 1200 \"-\" \"python-requests/2.31\"", + "192.0.2.77 - - [27/Jul/2026:10:06:02 +0000] \"GET /api/data?page=4 HTTP/1.1\" 200 1200 \"-\" \"python-requests/2.31\"", + "192.0.2.77 - - [27/Jul/2026:10:06:02 +0000] \"GET /api/data?page=5 HTTP/1.1\" 200 1200 \"-\" \"python-requests/2.31\"" + ] + }, + { + "id": "ALT-0016", + "timestamp": "2026-07-27T10:06:00", + "endpoint": "web-server (endpoint-1)", + "src_ip": "192.0.2.77", + "alert_type": "BLACKLISTED_IP_ACTIVITY", + "severity": "critical", + "description": "Traffic from known-malicious IP 192.0.2.77: Listed in external threat-intel feed (example seed entry)", + "evidence": [ + "192.0.2.77 - - [27/Jul/2026:10:06:00 +0000] \"GET /api/data?page=1 HTTP/1.1\" 200 1200 \"-\" \"python-requests/2.31\"" + ] + }, + { + "id": "ALT-0013", + "timestamp": "2026-07-27T10:05:24", + "endpoint": "ssh-auth (endpoint-3)", + "src_ip": "192.0.2.77", + "alert_type": "ACCOUNT_COMPROMISE_SUSPECTED", + "severity": "critical", + "description": "Successful SSH login as 'root' immediately following a brute-force burst from the same IP -- account may be compromised", + "evidence": [ + "Jul 27 10:05:24 app-srv-05 sshd[10111]: Accepted password for root from 192.0.2.77 port 41011 ssh2" + ] + }, + { + "id": "ALT-0011", + "timestamp": "2026-07-27T10:05:12", + "endpoint": "ssh-auth (endpoint-3)", + "src_ip": "192.0.2.77", + "alert_type": "BRUTE_FORCE_SSH", + "severity": "high", + "description": "10 failed SSH login attempts within 300s trying 7 usernames (admin, deploy, oracle, postgres, root, test)", + "evidence": [ + "Jul 27 10:05:12 app-srv-05 sshd[10101]: Failed password for invalid user admin from 192.0.2.77 port 41001 ssh2", + "Jul 27 10:05:13 app-srv-05 sshd[10102]: Failed password for invalid user root from 192.0.2.77 port 41002 ssh2", + "Jul 27 10:05:14 app-srv-05 sshd[10103]: Failed password for invalid user test from 192.0.2.77 port 41003 ssh2", + "Jul 27 10:05:15 app-srv-05 sshd[10104]: Failed password for invalid user oracle from 192.0.2.77 port 41004 ssh2", + "Jul 27 10:05:16 app-srv-05 sshd[10105]: Failed password for invalid user ubuntu from 192.0.2.77 port 41005 ssh2" + ] + }, + { + "id": "ALT-0015", + "timestamp": "2026-07-27T10:05:12", + "endpoint": "ssh-auth (endpoint-3)", + "src_ip": "192.0.2.77", + "alert_type": "BLACKLISTED_IP_ACTIVITY", + "severity": "critical", + "description": "Traffic from known-malicious IP 192.0.2.77: Listed in external threat-intel feed (example seed entry)", + "evidence": [ + "Jul 27 10:05:12 app-srv-05 sshd[10101]: Failed password for invalid user admin from 192.0.2.77 port 41001 ssh2" + ] + }, + { + "id": "ALT-0010", + "timestamp": "2026-07-27T10:05:01", + "endpoint": "firewall (endpoint-2)", + "src_ip": "192.0.2.77", + "alert_type": "PORT_SCAN", + "severity": "high", + "description": "Port scan detected: 20 distinct destination ports probed against 10.0.0.5 within 120s (17 denied, 3 allowed)", + "evidence": [ + "2026-07-27 10:05:01 SRC=192.0.2.77 SPT=33001 DST=10.0.0.5 DPT=20 PROTO=TCP ACTION=DENY", + "2026-07-27 10:05:01 SRC=192.0.2.77 SPT=33002 DST=10.0.0.5 DPT=21 PROTO=TCP ACTION=DENY", + "2026-07-27 10:05:02 SRC=192.0.2.77 SPT=33003 DST=10.0.0.5 DPT=22 PROTO=TCP ACTION=ALLOW", + "2026-07-27 10:05:02 SRC=192.0.2.77 SPT=33004 DST=10.0.0.5 DPT=23 PROTO=TCP ACTION=DENY", + "2026-07-27 10:05:03 SRC=192.0.2.77 SPT=33005 DST=10.0.0.5 DPT=25 PROTO=TCP ACTION=DENY" + ] + }, + { + "id": "ALT-0014", + "timestamp": "2026-07-27T10:05:01", + "endpoint": "firewall (endpoint-2)", + "src_ip": "192.0.2.77", + "alert_type": "BLACKLISTED_IP_ACTIVITY", + "severity": "critical", + "description": "Traffic from known-malicious IP 192.0.2.77: Listed in external threat-intel feed (example seed entry)", + "evidence": [ + "2026-07-27 10:05:01 SRC=192.0.2.77 SPT=33001 DST=10.0.0.5 DPT=20 PROTO=TCP ACTION=DENY" + ] + }, + { + "id": "ALT-0017", + "timestamp": "2026-07-27T10:05:01", + "endpoint": "correlation-engine", + "src_ip": "192.0.2.77", + "alert_type": "MULTI_VECTOR_ATTACK", + "severity": "critical", + "description": "IP 192.0.2.77 triggered alerts across 3 different endpoints (firewall (endpoint-2), ssh-auth (endpoint-3), web-server (endpoint-1)) -- consistent with a coordinated, multi-stage attack (recon -> exploitation -> access)", + "evidence": [] + }, + { + "id": "ALT-0008", + "timestamp": "2026-07-27T10:03:01", + "endpoint": "web-server (endpoint-1)", + "src_ip": "203.0.113.99", + "alert_type": "RECON_SCANNER", + "severity": "medium", + "description": "Automated vulnerability scan detected (nikto): 7 probe requests against paths such as /admin, /admin.php, /phpmyadmin/", + "evidence": [ + "203.0.113.99 - - [27/Jul/2026:10:03:01 +0000] \"GET /admin HTTP/1.1\" 404 190 \"-\" \"Mozilla/5.0 (compatible; Nikto/2.5.0)\"", + "203.0.113.99 - - [27/Jul/2026:10:03:02 +0000] \"GET /admin.php HTTP/1.1\" 404 190 \"-\" \"Mozilla/5.0 (compatible; Nikto/2.5.0)\"", + "203.0.113.99 - - [27/Jul/2026:10:03:03 +0000] \"GET /phpmyadmin/ HTTP/1.1\" 404 190 \"-\" \"Mozilla/5.0 (compatible; Nikto/2.5.0)\"", + "203.0.113.99 - - [27/Jul/2026:10:03:04 +0000] \"GET /.env HTTP/1.1\" 404 190 \"-\" \"Mozilla/5.0 (compatible; Nikto/2.5.0)\"", + "203.0.113.99 - - [27/Jul/2026:10:03:05 +0000] \"GET /wp-login.php HTTP/1.1\" 404 190 \"-\" \"Mozilla/5.0 (compatible; Nikto/2.5.0)\"" + ] + }, + { + "id": "ALT-0005", + "timestamp": "2026-07-27T10:01:12", + "endpoint": "web-server (endpoint-1)", + "src_ip": "203.0.113.66", + "alert_type": "PATH_TRAVERSAL", + "severity": "high", + "description": "Path Traversal attempt detected in request to /download?file=../../../../etc/passwd", + "evidence": [ + "203.0.113.66 - - [27/Jul/2026:10:01:12 +0000] \"GET /download?file=../../../../etc/passwd HTTP/1.1\" 403 210 \"-\" \"sqlmap/1.7.2#stable\"" + ] + }, + { + "id": "ALT-0004", + "timestamp": "2026-07-27T10:01:09", + "endpoint": "web-server (endpoint-1)", + "src_ip": "203.0.113.66", + "alert_type": "XSS", + "severity": "high", + "description": "Xss attempt detected in request to /search?q= + + + diff --git a/dashboard/script.js b/dashboard/script.js new file mode 100644 index 0000000..088f86a --- /dev/null +++ b/dashboard/script.js @@ -0,0 +1,361 @@ +/* ========================================================================== + SIEM Threat Console — dashboard logic + Loads siem_report.json (falls back to embedded data.js so the dashboard + still works when opened directly via file://), then renders every panel. + + Security note: alert descriptions/evidence are LOG DATA -- they may + literally contain attacker-supplied strings like "", + "evidence": [ + "203.0.113.66 - - [27/Jul/2026:10:01:09 +0000] \"GET /search?q= HTTP/1.1\" 200 998 \"-\" \"sqlmap/1.7.2#stable\"" + ] + }, + { + "id": "ALT-0003", + "timestamp": "2026-07-27T10:01:06", + "endpoint": "web-server (endpoint-1)", + "src_ip": "203.0.113.66", + "alert_type": "SQL_INJECTION", + "severity": "critical", + "description": "Sql Injection attempt detected in request to /products?id=1'%20AND%20SLEEP(5)--", + "evidence": [ + "203.0.113.66 - - [27/Jul/2026:10:01:06 +0000] \"GET /products?id=1'%20AND%20SLEEP(5)-- HTTP/1.1\" 500 812 \"-\" \"sqlmap/1.7.2#stable\"" + ] + }, + { + "id": "ALT-0002", + "timestamp": "2026-07-27T10:01:04", + "endpoint": "web-server (endpoint-1)", + "src_ip": "203.0.113.66", + "alert_type": "SQL_INJECTION", + "severity": "critical", + "description": "Sql Injection attempt detected in request to /products?id=1%20UNION%20SELECT%20username,password%20FROM%20users--", + "evidence": [ + "203.0.113.66 - - [27/Jul/2026:10:01:04 +0000] \"GET /products?id=1%20UNION%20SELECT%20username,password%20FROM%20users-- HTTP/1.1\" 500 812 \"-\" \"sqlmap/1.7.2#stable\"" + ] + }, + { + "id": "ALT-0001", + "timestamp": "2026-07-27T10:01:02", + "endpoint": "web-server (endpoint-1)", + "src_ip": "203.0.113.66", + "alert_type": "SQL_INJECTION", + "severity": "critical", + "description": "Sql Injection attempt detected in request to /login.php?user=admin'%20OR%20'1'='1'--", + "evidence": [ + "203.0.113.66 - - [27/Jul/2026:10:01:02 +0000] \"GET /login.php?user=admin'%20OR%20'1'='1'-- HTTP/1.1\" 401 512 \"-\" \"sqlmap/1.7.2#stable\"" + ] + }, + { + "id": "ALT-0007", + "timestamp": "2026-07-27T10:01:02", + "endpoint": "web-server (endpoint-1)", + "src_ip": "203.0.113.66", + "alert_type": "RECON_SCANNER", + "severity": "medium", + "description": "Automated vulnerability scan detected (sqlmap): 5 probe requests against paths such as /login.php?user=admin'%20OR%20'1'='1'--, /products?id=1%20UNION%20SELECT%20username,password%20FROM%20users--, /products?id=1'%20AND%20SLEEP(5)--", + "evidence": [ + "203.0.113.66 - - [27/Jul/2026:10:01:02 +0000] \"GET /login.php?user=admin'%20OR%20'1'='1'-- HTTP/1.1\" 401 512 \"-\" \"sqlmap/1.7.2#stable\"", + "203.0.113.66 - - [27/Jul/2026:10:01:04 +0000] \"GET /products?id=1%20UNION%20SELECT%20username,password%20FROM%20users-- HTTP/1.1\" 500 812 \"-\" \"sqlmap/1.7.2#stable\"", + "203.0.113.66 - - [27/Jul/2026:10:01:06 +0000] \"GET /products?id=1'%20AND%20SLEEP(5)-- HTTP/1.1\" 500 812 \"-\" \"sqlmap/1.7.2#stable\"", + "203.0.113.66 - - [27/Jul/2026:10:01:09 +0000] \"GET /search?q= HTTP/1.1\" 200 998 \"-\" \"sqlmap/1.7.2#stable\"", + "203.0.113.66 - - [27/Jul/2026:10:01:12 +0000] \"GET /download?file=../../../../etc/passwd HTTP/1.1\" 403 210 \"-\" \"sqlmap/1.7.2#stable\"" + ] + } + ], + "malicious_ips": [ + { + "ip": "192.0.2.77", + "threat_score": 72, + "severity_counts": { + "critical": 6, + "high": 2, + "medium": 1, + "low": 0 + }, + "total_alerts": 9, + "alert_types": [ + "ACCOUNT_COMPROMISE_SUSPECTED", + "BLACKLISTED_IP_ACTIVITY", + "BRUTE_FORCE_SSH", + "HIGH_REQUEST_RATE", + "MULTI_VECTOR_ATTACK", + "PORT_SCAN", + "SQL_INJECTION" + ], + "endpoints_involved": [ + "firewall (endpoint-2)", + "ssh-auth (endpoint-3)", + "web-server (endpoint-1)" + ], + "first_seen": "2026-07-27T10:05:01", + "last_seen": "2026-07-27T10:06:11", + "known_threat_intel": "Listed in external threat-intel feed (example seed entry)" + }, + { + "ip": "203.0.113.66", + "threat_score": 42, + "severity_counts": { + "critical": 3, + "high": 2, + "medium": 1, + "low": 0 + }, + "total_alerts": 6, + "alert_types": [ + "PATH_TRAVERSAL", + "RECON_SCANNER", + "SQL_INJECTION", + "XSS" + ], + "endpoints_involved": [ + "web-server (endpoint-1)" + ], + "first_seen": "2026-07-27T10:01:02", + "last_seen": "2026-07-27T10:01:12", + "known_threat_intel": null + }, + { + "ip": "198.51.100.234", + "threat_score": 5, + "severity_counts": { + "critical": 0, + "high": 1, + "medium": 0, + "low": 0 + }, + "total_alerts": 1, + "alert_types": [ + "BRUTE_FORCE_SSH" + ], + "endpoints_involved": [ + "ssh-auth (endpoint-3)" + ], + "first_seen": "2026-07-27T10:07:41", + "last_seen": "2026-07-27T10:07:41", + "known_threat_intel": null + }, + { + "ip": "203.0.113.99", + "threat_score": 2, + "severity_counts": { + "critical": 0, + "high": 0, + "medium": 1, + "low": 0 + }, + "total_alerts": 1, + "alert_types": [ + "RECON_SCANNER" + ], + "endpoints_involved": [ + "web-server (endpoint-1)" + ], + "first_seen": "2026-07-27T10:03:01", + "last_seen": "2026-07-27T10:03:01", + "known_threat_intel": null + } + ] +} \ No newline at end of file diff --git a/dashboard/style.css b/dashboard/style.css new file mode 100644 index 0000000..989d57e --- /dev/null +++ b/dashboard/style.css @@ -0,0 +1,472 @@ +/* ========================================================================== + SIEM Threat Console — stylesheet + Visual identity: a night-ops monitoring console. Deep indigo-black field, + a hairline signal grid, and severity colour used as literal information + (red/orange/yellow/blue map 1:1 to critical/high/medium/low everywhere). + ========================================================================== */ + +@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600;700&family=Inter:wght@400;500;600;700&display=swap'); + +:root { + --bg: #080B10; + --bg-grid: #0D121A; + --panel: #10161F; + --panel-raised: #141C27; + --border: #212D3D; + --border-soft: #1A2432; + --text: #E7EDF4; + --text-dim: #8592A3; + --text-faint: #4E5A6B; + + --critical: #FF3B4E; + --critical-dim: #4A1620; + --high: #FF8C42; + --high-dim: #45280F; + --medium: #FFD23F; + --medium-dim: #453B10; + --low: #3FA7FF; + --low-dim: #10293F; + --ok: #35D48A; + + --font-display: 'JetBrains Mono', monospace; + --font-body: 'Inter', sans-serif; + --font-mono: 'JetBrains Mono', monospace; + + --radius: 6px; +} + +* { box-sizing: border-box; } + +html, body { + margin: 0; + padding: 0; + background: var(--bg); + color: var(--text); + font-family: var(--font-body); + -webkit-font-smoothing: antialiased; +} + +body { + background-image: + linear-gradient(var(--bg-grid) 1px, transparent 1px), + linear-gradient(90deg, var(--bg-grid) 1px, transparent 1px); + background-size: 34px 34px; + min-height: 100vh; +} + +::selection { background: var(--critical); color: #fff; } + +a { color: var(--low); } + +/* ---------- layout shell ---------- */ + +.console { + max-width: 1320px; + margin: 0 auto; + padding: 22px 24px 60px; +} + +/* ---------- header ---------- */ + +.console-header { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 24px; + padding-bottom: 18px; + border-bottom: 1px solid var(--border); + margin-bottom: 22px; + flex-wrap: wrap; +} + +.brand { + display: flex; + align-items: center; + gap: 14px; +} + +.brand-mark { + width: 40px; + height: 40px; + border-radius: 8px; + border: 1px solid var(--border); + background: var(--panel-raised); + display: flex; + align-items: center; + justify-content: center; + position: relative; + flex: none; +} + +.brand-mark svg { width: 22px; height: 22px; } + +.brand-text .eyebrow { + font-family: var(--font-mono); + font-size: 11px; + letter-spacing: 0.16em; + text-transform: uppercase; + color: var(--text-faint); + margin: 0 0 2px; +} + +.brand-text h1 { + margin: 0; + font-family: var(--font-display); + font-size: 20px; + font-weight: 600; + letter-spacing: 0.01em; +} + +.brand-text h1 .slash { color: var(--text-faint); font-weight: 400; margin: 0 6px; } + +.header-status { + display: flex; + align-items: center; + gap: 22px; + font-family: var(--font-mono); + font-size: 12px; + color: var(--text-dim); +} + +.status-item { display: flex; flex-direction: column; align-items: flex-end; gap: 3px; } +.status-item .value { color: var(--text); font-size: 13px; } + +.live-dot { + display: inline-flex; + align-items: center; + gap: 7px; + color: var(--ok); + font-weight: 600; +} +.live-dot::before { + content: ""; + width: 7px; + height: 7px; + border-radius: 50%; + background: var(--ok); + box-shadow: 0 0 0 0 rgba(53, 212, 138, 0.6); + animation: pulse 1.8s infinite; +} +@keyframes pulse { + 0% { box-shadow: 0 0 0 0 rgba(53, 212, 138, 0.55); } + 70% { box-shadow: 0 0 0 7px rgba(53, 212, 138, 0); } + 100% { box-shadow: 0 0 0 0 rgba(53, 212, 138, 0); } +} + +.btn { + font-family: var(--font-mono); + font-size: 12px; + color: var(--text); + background: var(--panel-raised); + border: 1px solid var(--border); + border-radius: var(--radius); + padding: 8px 14px; + cursor: pointer; + transition: border-color .15s ease, transform .1s ease; +} +.btn:hover { border-color: var(--low); color: var(--low); } +.btn:active { transform: translateY(1px); } + +/* ---------- stat cards ---------- */ + +.stat-grid { + display: grid; + grid-template-columns: repeat(6, 1fr); + gap: 12px; + margin-bottom: 22px; +} + +.stat-card { + background: var(--panel); + border: 1px solid var(--border); + border-top: 2px solid var(--border-soft); + border-radius: var(--radius); + padding: 14px 16px; + position: relative; + overflow: hidden; +} +.stat-card.crit { border-top-color: var(--critical); } +.stat-card.high { border-top-color: var(--high); } +.stat-card.med { border-top-color: var(--medium); } +.stat-card.low { border-top-color: var(--low); } +.stat-card.neutral { border-top-color: var(--text-faint); } + +.stat-card .label { + font-family: var(--font-mono); + font-size: 10.5px; + letter-spacing: 0.09em; + text-transform: uppercase; + color: var(--text-dim); + margin: 0 0 8px; +} +.stat-card .num { + font-family: var(--font-display); + font-size: 28px; + font-weight: 700; + line-height: 1; +} +.stat-card.crit .num { color: var(--critical); } +.stat-card.high .num { color: var(--high); } +.stat-card.med .num { color: var(--medium); } +.stat-card.low .num { color: var(--low); } + +/* ---------- panel grid ---------- */ + +.panel-row { + display: grid; + grid-template-columns: 1.1fr 1.4fr; + gap: 14px; + margin-bottom: 14px; + align-items: stretch; +} + +.panel { + background: var(--panel); + border: 1px solid var(--border); + border-radius: var(--radius); + padding: 16px 18px 18px; + display: flex; + flex-direction: column; + min-width: 0; +} + +.panel-title { + display: flex; + align-items: baseline; + justify-content: space-between; + margin-bottom: 12px; +} +.panel-title h2 { + font-family: var(--font-mono); + font-size: 12.5px; + letter-spacing: 0.1em; + text-transform: uppercase; + color: var(--text-dim); + margin: 0; + font-weight: 600; +} +.panel-title .hint { font-size: 11px; color: var(--text-faint); font-family: var(--font-mono); } + +/* ---------- radar ---------- */ + +.radar-sweep { + transform-origin: 150px 150px; + animation: radar-spin 6s linear infinite; +} +@keyframes radar-spin { + from { transform: rotate(0deg); } + to { transform: rotate(360deg); } +} +.radar-ping { + animation: radar-ping 1.8s ease-out infinite; + transform-origin: center; +} +@keyframes radar-ping { + 0% { r: 4.5; opacity: 0.9; } + 100% { r: 13; opacity: 0; } +} + +.radar-wrap { display: flex; align-items: center; justify-content: center; padding: 6px 0 2px; } +.radar-legend { + display: flex; gap: 14px; justify-content: center; margin-top: 8px; + font-family: var(--font-mono); font-size: 10.5px; color: var(--text-dim); +} +.radar-legend span { display: inline-flex; align-items: center; gap: 5px; } +.legend-dot { width: 8px; height: 8px; border-radius: 50%; display: inline-block; } + +/* ---------- severity meter ---------- */ + +.sev-meter { margin-top: auto; } +.sev-meter-bar { + display: flex; + height: 22px; + border-radius: 4px; + overflow: hidden; + border: 1px solid var(--border-soft); +} +.sev-meter-bar .seg { transition: flex-grow .4s ease; } +.sev-meter-bar .seg.crit { background: var(--critical); } +.sev-meter-bar .seg.high { background: var(--high); } +.sev-meter-bar .seg.med { background: var(--medium); } +.sev-meter-bar .seg.low { background: var(--low); } +.sev-meter-bar .seg.none { background: var(--border-soft); flex-grow: 1; } + +.sev-meter-labels { + display: flex; + justify-content: space-between; + margin-top: 8px; + font-family: var(--font-mono); + font-size: 11px; + color: var(--text-dim); +} +.sev-meter-labels span { display: flex; align-items: center; gap: 6px; } + +/* ---------- top IP list ---------- */ + +.ip-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 10px; } +.ip-row { + display: grid; + grid-template-columns: 22px 130px 1fr 46px; + align-items: center; + gap: 10px; + font-family: var(--font-mono); + font-size: 12px; +} +.ip-rank { color: var(--text-faint); text-align: right; } +.ip-addr { color: var(--text); font-weight: 600; } +.ip-bar-track { height: 8px; background: var(--border-soft); border-radius: 4px; overflow: hidden; } +.ip-bar-fill { height: 100%; border-radius: 4px; } +.ip-score { text-align: right; color: var(--text-dim); } + +/* ---------- filter bar ---------- */ + +.filter-bar { + display: flex; + gap: 10px; + flex-wrap: wrap; + margin-bottom: 14px; +} +.filter-bar input[type="text"], +.filter-bar select { + background: var(--panel-raised); + border: 1px solid var(--border); + color: var(--text); + font-family: var(--font-mono); + font-size: 12.5px; + border-radius: var(--radius); + padding: 9px 12px; +} +.filter-bar input[type="text"] { flex: 1; min-width: 200px; } +.filter-bar input::placeholder { color: var(--text-faint); } +.filter-bar select { cursor: pointer; } +.filter-bar input:focus, .filter-bar select:focus, +button:focus-visible, .btn:focus-visible { + outline: 2px solid var(--low); + outline-offset: 1px; +} + +/* ---------- alerts table ---------- */ + +.alerts-panel { padding: 16px 0 4px; } +.alerts-panel .panel-title { padding: 0 18px; } +.table-scroll { overflow-x: auto; } + +table.alerts { + width: 100%; + border-collapse: collapse; + font-size: 12.5px; +} +table.alerts th { + text-align: left; + font-family: var(--font-mono); + font-size: 10.5px; + letter-spacing: 0.08em; + text-transform: uppercase; + color: var(--text-faint); + padding: 8px 10px; + border-bottom: 1px solid var(--border); + white-space: nowrap; + position: sticky; + top: 0; + background: var(--panel); +} +table.alerts td { + padding: 10px 10px; + border-bottom: 1px solid var(--border-soft); + vertical-align: top; +} +table.alerts tbody tr { cursor: pointer; transition: background .12s ease; } +table.alerts tbody tr:hover { background: var(--panel-raised); } +table.alerts tbody tr.expanded { background: var(--panel-raised); } + +td.ts, td.ip { font-family: var(--font-mono); color: var(--text-dim); white-space: nowrap; } +td.ip { color: var(--text); } +td.endpoint { color: var(--text-dim); white-space: nowrap; } +td.desc { color: var(--text); line-height: 1.4; } + +.badge { + display: inline-block; + font-family: var(--font-mono); + font-size: 10.5px; + font-weight: 700; + letter-spacing: 0.04em; + padding: 3px 8px; + border-radius: 3px; + text-transform: uppercase; + white-space: nowrap; +} +.badge.critical { background: var(--critical-dim); color: var(--critical); } +.badge.high { background: var(--high-dim); color: var(--high); } +.badge.medium { background: var(--medium-dim); color: var(--medium); } +.badge.low { background: var(--low-dim); color: var(--low); } + +.type-tag { + font-family: var(--font-mono); + font-size: 10.5px; + color: var(--text-dim); + background: var(--panel-raised); + border: 1px solid var(--border-soft); + border-radius: 3px; + padding: 2px 6px; + white-space: nowrap; +} + +.evidence-row td { + padding: 0 10px 14px 10px; + border-bottom: 1px solid var(--border-soft); +} +.evidence-box { + background: var(--bg); + border: 1px solid var(--border-soft); + border-left: 3px solid var(--low); + border-radius: 4px; + padding: 10px 12px; + font-family: var(--font-mono); + font-size: 11.5px; + color: var(--text-dim); + white-space: pre-wrap; + word-break: break-all; + max-height: 160px; + overflow-y: auto; +} + +.empty-row td { + text-align: center; + color: var(--text-faint); + font-family: var(--font-mono); + padding: 36px 0; +} + +/* ---------- footer ---------- */ + +.console-footer { + margin-top: 24px; + padding-top: 14px; + border-top: 1px solid var(--border); + display: flex; + justify-content: space-between; + align-items: center; + font-family: var(--font-mono); + font-size: 11px; + color: var(--text-faint); + flex-wrap: wrap; + gap: 8px; +} + +/* ---------- responsive ---------- */ + +@media (max-width: 980px) { + .panel-row { grid-template-columns: 1fr; } + .stat-grid { grid-template-columns: repeat(3, 1fr); } +} + +@media (max-width: 620px) { + .stat-grid { grid-template-columns: repeat(2, 1fr); } + .header-status { display: none; } + .ip-row { grid-template-columns: 18px 96px 1fr 40px; } +} + +@media (prefers-reduced-motion: reduce) { + .live-dot::before { animation: none; } + * { transition: none !important; animation-duration: 0.001s !important; } +}