From 9fd3972e28f489da650e6ae6a598e54a99a94727 Mon Sep 17 00:00:00 2001 From: Aleksei Fedorov Date: Wed, 18 Mar 2026 13:49:55 +0000 Subject: [PATCH] security subsystem --- app.py | 207 ++++++++ index.html | 7 +- static/js/kernel-context-menu.js | 64 +++ static/js/main.js | 16 +- static/js/right-semicircle-menu.js | 19 +- static/js/security-belt.js | 652 ++++++++++++++++++++++++ templates/linux-security-subsystem.html | 134 +++++ 7 files changed, 1091 insertions(+), 8 deletions(-) create mode 100644 static/js/security-belt.js create mode 100644 templates/linux-security-subsystem.html diff --git a/app.py b/app.py index c7b4545..1b220bf 100644 --- a/app.py +++ b/app.py @@ -67,6 +67,10 @@ "irq_totals": {}, "softirq_totals": {} } +SECURITY_PREV = { + "timestamp": None, + "events": 0 +} FRONTEND_LOG_WRITE_LOCK = Lock() FRONTEND_LOG_FILE = os.getenv("FRONTEND_LOG_FILE", "/opt/ring0/kernel-ai/logs/frontend-events.jsonl") @@ -726,6 +730,16 @@ def crypto_page_legacy(): """Legacy path redirect to Linux crypto subsystem page.""" return redirect('/linux-crypto-subsystem', code=301) +@app.route('/linux-security-subsystem') +def linux_security_subsystem_page(): + """SEO-friendly Linux security subsystem page.""" + return render_template('linux-security-subsystem.html') + +@app.route('/security') +def security_page_legacy(): + """Legacy path redirect to Linux security subsystem page.""" + return redirect('/linux-security-subsystem', code=301) + @app.route('/api/syscalls-realtime') def syscalls_realtime(): """API for real-time system calls""" @@ -3920,6 +3934,191 @@ def collect_crypto_realtime(): } } +def collect_security_realtime(): + """ + Stage-1 security subsystem telemetry: + - Threat decision pipeline + - Process trust graph + - Attack surface map + """ + now = time.time() + process_rows = [] + suspicious_tokens = { + "nmap", "masscan", "hydra", "sqlmap", "metasploit", "msfconsole", + "netcat", "nc", "ncat", "socat", "john", "hashcat", "strace", "gdb" + } + trusted_tokens = { + "systemd", "sshd", "nginx", "python", "containerd", "dockerd", + "kubelet", "cron", "rsyslogd", "dbus-daemon" + } + ptrace_like = {"strace", "gdb", "ltrace"} + + def classify_trust(score): + if score >= 70: + return "blocked" + if score >= 48: + return "suspicious" + if score >= 28: + return "observe" + return "trusted" + + # Process sample and heuristic score. + for proc in psutil.process_iter(["pid", "name", "username", "memory_percent", "status", "num_threads"]): + try: + pid = int(proc.info.get("pid") or 0) + name = str(proc.info.get("name") or "unknown").lower() + mem = float(proc.info.get("memory_percent") or 0.0) + threads = int(proc.info.get("num_threads") or 0) + status = str(proc.info.get("status") or "unknown") + user = str(proc.info.get("username") or "") + + score = 12 + if any(tok in name for tok in suspicious_tokens): + score += 38 + if any(tok in name for tok in trusted_tokens): + score -= 10 + if user == "root": + score += 14 + if threads > 120: + score += 8 + if mem > 8.0: + score += 8 + if status in {"zombie", "stopped"}: + score += 10 + score = max(0, min(100, score)) + trust = classify_trust(score) + + process_rows.append({ + "pid": pid, + "name": name, + "trust": trust, + "risk_score": score, + "threads": threads, + "mem_percent": round(mem, 2), + "status": status, + "user": user + }) + except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess): + continue + except Exception: + continue + + # Focus on the most security-relevant rows. + process_rows.sort(key=lambda p: (p["risk_score"], p["mem_percent"], p["threads"]), reverse=True) + trust_graph = process_rows[:12] + + # Build threat pipeline lanes from top rows. + request_candidates = [ + "open /etc/shadow", + "connect tcp:443", + "exec /usr/bin/sudo", + "ptrace attach", + "bpf program load", + "write /usr/lib/systemd/*" + ] + hook_candidates = [ + "security_file_open", + "security_socket_connect", + "security_bprm_check", + "seccomp-bpf", + "cgroup device policy", + "audit hook" + ] + lanes = [] + for idx, row in enumerate(trust_graph[:10]): + req = request_candidates[idx % len(request_candidates)] + hook = hook_candidates[idx % len(hook_candidates)] + score = int(row.get("risk_score") or 0) + if score >= 70: + verdict = "deny" + elif score >= 45: + verdict = "audit" + else: + verdict = "allow" + lanes.append({ + "process": row.get("name", "unknown"), + "pid": int(row.get("pid", 0)), + "request": req, + "hook": hook, + "verdict": verdict, + "reason": "risk-score-policy", + "risk_score": score + }) + + # Attack surface metrics. + try: + listen_ports = len([ + c for c in psutil.net_connections(kind="inet") + if str(getattr(c, "status", "") or "") == "LISTEN" + ]) + except Exception: + listen_ports = 0 + + try: + with open("/proc/modules", "r", encoding="utf-8") as f: + loaded_modules = sum(1 for _ in f) + except Exception: + loaded_modules = 0 + + ptrace_processes = sum(1 for p in process_rows if any(tok in p.get("name", "") for tok in ptrace_like)) + root_processes = sum(1 for p in process_rows if p.get("user") == "root") + suspicious_processes = sum(1 for p in process_rows if p.get("trust") in {"suspicious", "blocked"}) + + setuid_bins = 0 + try: + out = subprocess.check_output( + "find /usr/bin /usr/sbin -xdev -perm -4000 -type f 2>/dev/null | wc -l", + shell=True, + text=True, + timeout=1.8 + ).strip() + setuid_bins = int(out or 0) + except Exception: + setuid_bins = 0 + + attack_surface = [ + {"name": "open-listen-ports", "value": int(listen_ports), "severity": "high" if listen_ports > 40 else "medium"}, + {"name": "setuid-binaries", "value": int(setuid_bins), "severity": "high" if setuid_bins > 70 else "medium"}, + {"name": "loaded-kernel-modules", "value": int(loaded_modules), "severity": "medium" if loaded_modules > 180 else "low"}, + {"name": "ptrace-capable-processes", "value": int(ptrace_processes), "severity": "high" if ptrace_processes > 0 else "low"}, + {"name": "root-processes", "value": int(root_processes), "severity": "medium" if root_processes > 120 else "low"}, + {"name": "suspicious-processes", "value": int(suspicious_processes), "severity": "high" if suspicious_processes > 6 else "medium"} + ] + + prev_ts = SECURITY_PREV["timestamp"] + prev_events = int(SECURITY_PREV["events"] or 0) + current_events = len(lanes) + SECURITY_PREV["timestamp"] = now + SECURITY_PREV["events"] = current_events + if prev_ts: + dt = max(0.001, now - prev_ts) + decisions_per_sec = round((current_events / dt) + abs(current_events - prev_events) * 0.6, 2) + else: + decisions_per_sec = float(current_events) + + return { + "timestamp": datetime.utcnow().isoformat() + "Z", + "pipeline": { + "stages": [ + "request event", + "LSM/seccomp hook", + "policy verdict" + ], + "lanes": lanes + }, + "trust_graph": trust_graph, + "attack_surface": attack_surface, + "meta": { + "decisions_per_sec": decisions_per_sec, + "events": current_events, + "trusted": sum(1 for p in trust_graph if p.get("trust") == "trusted"), + "observe": sum(1 for p in trust_graph if p.get("trust") == "observe"), + "suspicious": sum(1 for p in trust_graph if p.get("trust") == "suspicious"), + "blocked": sum(1 for p in trust_graph if p.get("trust") == "blocked"), + "mode": "live-heuristic-v1" + } + } + @app.route('/api/kernel-dna') def kernel_dna(): """API endpoint for Kernel DNA visualization data""" @@ -3937,6 +4136,14 @@ def crypto_realtime(): except Exception as e: return jsonify({'error': str(e)}), 500 +@app.route('/api/security-realtime') +def security_realtime(): + """Realtime-ish security interaction feed for security visualization.""" + try: + return jsonify(collect_security_realtime()) + except Exception as e: + return jsonify({'error': str(e)}), 500 + @app.route('/api/frontend-logs', methods=['POST', 'OPTIONS']) def ingest_frontend_logs(): """Receive frontend logs in ECS-like JSON and append to local JSONL file.""" diff --git a/index.html b/index.html index 2956112..5c75a38 100755 --- a/index.html +++ b/index.html @@ -96,13 +96,14 @@

Linux Kernel Ring 0 Visualization

- + + - + - + diff --git a/static/js/kernel-context-menu.js b/static/js/kernel-context-menu.js index 91b37df..223b547 100644 --- a/static/js/kernel-context-menu.js +++ b/static/js/kernel-context-menu.js @@ -17,6 +17,7 @@ class KernelContextMenu { this.cryptoVisualization = null; this.devicesVisualization = null; this.filesVisualization = null; + this.securityVisualization = null; } init() { @@ -614,6 +615,66 @@ class KernelContextMenu { console.error('❌ Error activating Filesystem Map visualization:', error); } } + + activateSecurityView() { + debugLog('πŸ›‘οΈ Activating Security Subsystem View'); + this.currentView = 'security'; + this.hideSubmenu(); + + if (!this.securityVisualization) { + if (typeof SecuritySubsystemVisualization !== 'undefined') { + try { + this.securityVisualization = new SecuritySubsystemVisualization(); + const initResult = this.securityVisualization.init(); + if (initResult === false) { + this.securityVisualization = null; + return; + } + } catch (error) { + console.error('❌ Error initializing SecuritySubsystemVisualization:', error); + alert('Security view initialization error: ' + error.message); + return; + } + } else if (typeof window.SecuritySubsystemVisualization !== 'undefined') { + try { + this.securityVisualization = new window.SecuritySubsystemVisualization(); + const initResult = this.securityVisualization.init(); + if (initResult === false) { + this.securityVisualization = null; + return; + } + } catch (error) { + console.error('❌ Error initializing window.SecuritySubsystemVisualization:', error); + alert('Security view initialization error: ' + error.message); + return; + } + } else { + console.error('❌ SecuritySubsystemVisualization class not loaded'); + alert('Security visualization is not loaded. Check console for details.'); + return; + } + } + + d3.selectAll('.syscall-box, .syscall-text').style('opacity', 0).style('pointer-events', 'none').style('visibility', 'hidden'); + d3.selectAll('.tag-icon, .connection-line').style('opacity', 0).style('pointer-events', 'none').style('visibility', 'hidden'); + d3.selectAll('.connection-box, .connection-text, .connection-details').style('opacity', 0).style('pointer-events', 'none').style('visibility', 'hidden'); + d3.selectAll('.subsystem-indicator').style('opacity', 0).style('pointer-events', 'none').style('visibility', 'hidden'); + d3.selectAll('.namespace-shell-layer, .cgroup-card-layer').style('opacity', 0).style('pointer-events', 'none').style('visibility', 'hidden'); + + if (window.syscallsManager) { + window.syscallsManager.stopAutoUpdate(); + } + if (window.connectionsManager) { + window.connectionsManager.stopAutoUpdate(); + } + + try { + this.securityVisualization.activate(); + debugLog('βœ… Security Subsystem visualization activated'); + } catch (error) { + console.error('❌ Error activating Security Subsystem visualization:', error); + } + } addExitButton() { // Remove existing exit button @@ -694,6 +755,9 @@ class KernelContextMenu { if (this.filesVisualization) { this.filesVisualization.deactivate(); } + if (this.securityVisualization) { + this.securityVisualization.deactivate(); + } // Clear exit button immediately (both old class and new class) d3.selectAll('.kernel-exit-button, .kernel-dna-exit-button').remove(); diff --git a/static/js/main.js b/static/js/main.js index b029deb..4276e2a 100755 --- a/static/js/main.js +++ b/static/js/main.js @@ -92,8 +92,8 @@ function initApp() { // Setup event handlers setupEventListeners(); - // When nginx serves SPA fallback (/index.html) for crypto route aliases, - // open the dedicated crypto view automatically by route. + // When nginx serves SPA fallback (/index.html) for subsystem route aliases, + // open dedicated views automatically by route. const path = String(window.location.pathname || '').replace(/\/+$/, '') || '/'; if ((path === '/crypto' || path === '/linux-crypto-subsystem') && window.kernelContextMenu @@ -102,6 +102,13 @@ function initApp() { window.kernelContextMenu.activateCryptoView(); }, 140); } + if ((path === '/security' || path === '/linux-security-subsystem') + && window.kernelContextMenu + && typeof window.kernelContextMenu.activateSecurityView === 'function') { + setTimeout(() => { + window.kernelContextMenu.activateSecurityView(); + }, 160); + } } // Setup event handlers @@ -162,14 +169,15 @@ function draw() { window.kernelContextMenu.currentView === 'dna-timeline' || window.kernelContextMenu.currentView === 'network' || window.kernelContextMenu.currentView === 'devices' || - window.kernelContextMenu.currentView === 'files' + window.kernelContextMenu.currentView === 'files' || + window.kernelContextMenu.currentView === 'security' )) { debugLog('⏸️ Skipping draw() - overlay view is active'); return; } // Safety: ensure overlay containers never leak into the main view. - ['kernel-dna-container', 'network-stack-container', 'devices-belt-container', 'filesystem-map-container'].forEach((id) => { + ['kernel-dna-container', 'network-stack-container', 'devices-belt-container', 'filesystem-map-container', 'security-belt-container'].forEach((id) => { const node = document.getElementById(id); if (node) { node.style.display = 'none'; diff --git a/static/js/right-semicircle-menu.js b/static/js/right-semicircle-menu.js index 6b4b734..35dda46 100644 --- a/static/js/right-semicircle-menu.js +++ b/static/js/right-semicircle-menu.js @@ -27,7 +27,12 @@ class RightSemicircleMenuManager { } isOverlayItem(itemId) { - return itemId === 'kernel' || itemId === 'network' || itemId === 'devices' || itemId === 'files' || itemId === 'scheduler'; + return itemId === 'kernel' + || itemId === 'network' + || itemId === 'devices' + || itemId === 'files' + || itemId === 'scheduler' + || itemId === 'security'; } setItemHoverState(itemGroup, isHovered, hudStrokeHair, hudStrokeNormal, hudStrokeAccent) { @@ -79,6 +84,10 @@ class RightSemicircleMenuManager { window.location.assign('/linux-crypto-subsystem'); return; } + if (itemId === 'security') { + window.location.assign('/linux-security-subsystem'); + return; + } if (!window.kernelContextMenu) return; if (itemId === 'kernel') { window.kernelContextMenu.activateDNAView(); @@ -351,6 +360,10 @@ class RightSemicircleMenuManager { window.location.assign('/linux-crypto-subsystem'); return; } + if (item.id === 'security') { + window.location.assign('/linux-security-subsystem'); + return; + } if (isOverlay) { this.activateOverlayView(item.id); } else { @@ -371,6 +384,10 @@ class RightSemicircleMenuManager { window.location.assign('/linux-crypto-subsystem'); return; } + if (item.id === 'security') { + window.location.assign('/linux-security-subsystem'); + return; + } // Prevent re-rendering during click this.isClickingOverlay = true; this.activateOverlayView(item.id); diff --git a/static/js/security-belt.js b/static/js/security-belt.js new file mode 100644 index 0000000..ed16d37 --- /dev/null +++ b/static/js/security-belt.js @@ -0,0 +1,652 @@ +// Security Subsystem Visualization (Stage 1) +// Version: 6 + +debugLog('πŸ›‘οΈ security-belt.js v6: Script loading...'); + +class SecuritySubsystemVisualization { + constructor() { + this.container = null; + this.canvas = null; + this.ctx = null; + this.isActive = false; + this.animationId = null; + this.telemetryInterval = null; + this.exitButton = null; + this.overlayNodes = []; + this.resizeHandler = null; + this.telemetry = null; + this.tick = 0; + this.verdictFilter = 'all'; + this.filterButtons = new Map(); + this.clearProcessButton = null; + this.selectedProcessFilter = null; + this.hoveredProcessPid = null; + this.trustNodeHitAreas = []; + this.canvasClickHandler = null; + this.canvasMouseMoveHandler = null; + } + + init(containerId = 'security-belt-container') { + const existing = document.getElementById(containerId); + if (existing) { + this.container = existing; + } else { + this.container = document.createElement('div'); + this.container.id = containerId; + this.container.style.cssText = ` + position: fixed; + inset: 0; + width: 100%; + height: 100%; + background: radial-gradient(circle at 50% 40%, #121821 0%, #0a0d12 70%); + z-index: 9999; + display: none; + visibility: hidden; + pointer-events: none; + overflow: hidden; + `; + document.body.appendChild(this.container); + } + + this.canvas = document.createElement('canvas'); + this.canvas.style.cssText = ` + position: absolute; + inset: 0; + width: 100%; + height: 100%; + `; + this.container.appendChild(this.canvas); + this.ctx = this.canvas.getContext('2d'); + this.canvasClickHandler = (event) => this.onCanvasClick(event); + this.canvasMouseMoveHandler = (event) => this.onCanvasMouseMove(event); + this.canvas.addEventListener('click', this.canvasClickHandler); + this.canvas.addEventListener('mousemove', this.canvasMouseMoveHandler); + + this.createOverlayUI(); + this.addExitButton(); + + this.resizeHandler = () => this.onResize(); + window.addEventListener('resize', this.resizeHandler); + this.onResize(); + return true; + } + + createOverlayUI() { + const title = document.createElement('div'); + title.style.cssText = ` + position: absolute; + top: 20px; + left: 50%; + transform: translateX(-50%); + color: #d4dbe8; + font-family: 'Share Tech Mono', monospace; + font-size: 24px; + letter-spacing: 1px; + z-index: 1001; + text-shadow: 0 0 8px rgba(180, 210, 255, 0.25); + `; + title.textContent = 'KERNEL SECURITY SUBSYSTEM (stage 1)'; + this.container.appendChild(title); + this.overlayNodes.push(title); + + const legend = document.createElement('div'); + legend.style.cssText = ` + position: absolute; + top: 72px; + left: 50%; + transform: translateX(-50%); + color: #a7b6cb; + font-family: 'Share Tech Mono', monospace; + font-size: 11px; + z-index: 1001; + `; + legend.textContent = 'threat decision pipeline + process trust graph + attack surface map'; + this.container.appendChild(legend); + this.overlayNodes.push(legend); + + const filterPanel = document.createElement('div'); + filterPanel.style.cssText = ` + position: absolute; + top: 72px; + right: 22px; + display: flex; + gap: 6px; + z-index: 1001; + `; + const filters = [ + { key: 'all', label: 'ALL' }, + { key: 'allow', label: 'ALLOW' }, + { key: 'audit', label: 'AUDIT' }, + { key: 'deny', label: 'DENY' } + ]; + filters.forEach((item) => { + const btn = document.createElement('button'); + btn.textContent = item.label; + btn.style.cssText = ` + padding: 4px 8px; + background: rgba(8, 12, 18, 0.86); + border: 1px solid rgba(150, 164, 188, 0.35); + color: #bcc8db; + font-family: 'Share Tech Mono', monospace; + font-size: 9px; + letter-spacing: 0.3px; + cursor: pointer; + `; + btn.onclick = () => this.setVerdictFilter(item.key); + filterPanel.appendChild(btn); + this.filterButtons.set(item.key, btn); + this.overlayNodes.push(btn); + }); + this.container.appendChild(filterPanel); + this.overlayNodes.push(filterPanel); + + const clearBtn = document.createElement('button'); + clearBtn.textContent = 'CLEAR PROCESS'; + clearBtn.style.cssText = ` + padding: 4px 8px; + background: rgba(8, 12, 18, 0.56); + border: 1px solid rgba(150, 164, 188, 0.22); + color: rgba(188, 200, 219, 0.55); + font-family: 'Share Tech Mono', monospace; + font-size: 9px; + letter-spacing: 0.3px; + cursor: not-allowed; + `; + clearBtn.onclick = () => { + if (!this.selectedProcessFilter) return; + this.selectedProcessFilter = null; + this.updateClearProcessButtonState(); + }; + filterPanel.appendChild(clearBtn); + this.clearProcessButton = clearBtn; + this.overlayNodes.push(clearBtn); + this.setVerdictFilter(this.verdictFilter); + this.updateClearProcessButtonState(); + } + + addExitButton() { + if (this.exitButton && this.exitButton.parentNode) { + this.exitButton.parentNode.removeChild(this.exitButton); + } + const btn = document.createElement('button'); + btn.textContent = 'EXIT VIEW'; + btn.style.cssText = ` + position: absolute; + top: 20px; + right: 20px; + padding: 10px 20px; + background: rgba(7, 10, 16, 0.92); + border: 1px solid rgba(178, 190, 212, 0.45); + color: #d5dce8; + font-family: 'Share Tech Mono', monospace; + font-size: 12px; + cursor: pointer; + z-index: 1001; + box-shadow: 0 0 14px rgba(150, 175, 220, 0.25); + transition: all 0.25s ease; + `; + btn.onmouseenter = () => { + btn.style.background = 'rgba(19, 25, 37, 0.95)'; + btn.style.color = '#fff'; + }; + btn.onmouseleave = () => { + btn.style.background = 'rgba(7, 10, 16, 0.92)'; + btn.style.color = '#d5dce8'; + }; + btn.onclick = () => { + const path = String(window.location.pathname || '').replace(/\/+$/, '') || '/'; + if (path === '/security' || path === '/linux-security-subsystem') { + window.history.replaceState({}, '', '/'); + } + if (window.kernelContextMenu) { + window.kernelContextMenu.deactivateViews(); + } else { + this.deactivate(); + } + }; + this.container.appendChild(btn); + this.exitButton = btn; + } + + setVerdictFilter(filterKey) { + this.verdictFilter = ['all', 'allow', 'audit', 'deny'].includes(filterKey) ? filterKey : 'all'; + this.filterButtons.forEach((btn, key) => { + const active = key === this.verdictFilter; + btn.style.background = active ? 'rgba(32, 52, 81, 0.92)' : 'rgba(8, 12, 18, 0.86)'; + btn.style.borderColor = active ? 'rgba(124, 178, 255, 0.9)' : 'rgba(150, 164, 188, 0.35)'; + btn.style.color = active ? '#d9ecff' : '#bcc8db'; + }); + } + + updateClearProcessButtonState() { + if (!this.clearProcessButton) return; + const active = Boolean(this.selectedProcessFilter); + this.clearProcessButton.style.background = active ? 'rgba(32, 52, 81, 0.92)' : 'rgba(8, 12, 18, 0.56)'; + this.clearProcessButton.style.borderColor = active ? 'rgba(124, 178, 255, 0.9)' : 'rgba(150, 164, 188, 0.22)'; + this.clearProcessButton.style.color = active ? '#d9ecff' : 'rgba(188, 200, 219, 0.55)'; + this.clearProcessButton.style.cursor = active ? 'pointer' : 'not-allowed'; + } + + fetchTelemetry() { + return fetch('/api/security-realtime') + .then((res) => res.json()) + .then((data) => { + if (!data || data.error) { + throw new Error(data?.error || 'No security data'); + } + this.telemetry = data; + }) + .catch(() => { + this.telemetry = { + pipeline: { lanes: [] }, + trust_graph: [], + attack_surface: [], + meta: { decisions_per_sec: 0, mode: 'fallback' } + }; + }); + } + + normalizeName(name) { + return String(name || '').trim().toLowerCase(); + } + + onCanvasClick(event) { + if (!this.canvas) return; + const rect = this.canvas.getBoundingClientRect(); + const x = event.clientX - rect.left; + const y = event.clientY - rect.top; + let matched = null; + for (const node of this.trustNodeHitAreas) { + const dx = x - node.x; + const dy = y - node.y; + const dist = Math.sqrt(dx * dx + dy * dy); + if (dist <= node.r + 4) { + matched = node; + break; + } + } + if (!matched) return; + const pid = Number(matched.pid || 0); + if (this.selectedProcessFilter && Number(this.selectedProcessFilter.pid || 0) === pid) { + this.selectedProcessFilter = null; + this.updateClearProcessButtonState(); + return; + } + this.selectedProcessFilter = { + pid, + name: this.normalizeName(matched.name || 'unknown') + }; + this.updateClearProcessButtonState(); + } + + onCanvasMouseMove(event) { + if (!this.canvas) return; + const rect = this.canvas.getBoundingClientRect(); + const x = event.clientX - rect.left; + const y = event.clientY - rect.top; + let hovered = null; + for (const node of this.trustNodeHitAreas) { + const dx = x - node.x; + const dy = y - node.y; + const dist = Math.sqrt(dx * dx + dy * dy); + if (dist <= node.r + 4) { + hovered = Number(node.pid || 0); + break; + } + } + this.hoveredProcessPid = hovered; + this.canvas.style.cursor = hovered ? 'pointer' : 'default'; + } + + drawRoundedRect(x, y, w, h, r) { + const ctx = this.ctx; + const radius = Math.max(0, Math.min(r, w / 2, h / 2)); + ctx.beginPath(); + ctx.moveTo(x + radius, y); + ctx.lineTo(x + w - radius, y); + ctx.quadraticCurveTo(x + w, y, x + w, y + radius); + ctx.lineTo(x + w, y + h - radius); + ctx.quadraticCurveTo(x + w, y + h, x + w - radius, y + h); + ctx.lineTo(x + radius, y + h); + ctx.quadraticCurveTo(x, y + h, x, y + h - radius); + ctx.lineTo(x, y + radius); + ctx.quadraticCurveTo(x, y, x + radius, y); + ctx.closePath(); + } + + drawPanel(x, y, w, h, title) { + this.drawRoundedRect(x, y, w, h, 8); + this.ctx.fillStyle = 'rgba(8, 11, 16, 0.88)'; + this.ctx.fill(); + this.ctx.strokeStyle = 'rgba(165, 178, 200, 0.35)'; + this.ctx.lineWidth = 0.95; + this.ctx.stroke(); + + this.ctx.fillStyle = '#d8e5f7'; + this.ctx.font = '13px "Share Tech Mono", monospace'; + this.ctx.fillText(title, x + 14, y + 20); + } + + verdictColor(verdict) { + if (verdict === 'deny') return '#eb7e7e'; + if (verdict === 'audit') return '#f4c977'; + if (verdict === 'allow') return '#60d69d'; + return '#8a9cea'; + } + + trustColor(trust) { + if (trust === 'blocked') return '#eb7e7e'; + if (trust === 'suspicious') return '#f4c977'; + if (trust === 'observe') return '#8a9cea'; + return '#60d69d'; + } + + drawDecisionPipeline(x, y, w, h, telemetry) { + this.drawPanel(x, y, w, h, 'THREAT DECISION PIPELINE'); + const lanes = Array.isArray(telemetry?.pipeline?.lanes) ? telemetry.pipeline.lanes : []; + const selected = this.selectedProcessFilter; + const filtered = lanes.filter((lane) => { + const verdictMatch = this.verdictFilter === 'all' || lane.verdict === this.verdictFilter; + if (!verdictMatch) return false; + if (!selected) return true; + const lanePid = Number(lane.pid || 0); + const laneName = this.normalizeName(lane.process); + return lanePid === Number(selected.pid || 0) || laneName === this.normalizeName(selected.name); + }); + const laneRows = filtered.slice(0, 7); + + const stageXs = [x + 18, x + Math.floor(w * 0.44), x + Math.floor(w * 0.78)]; + const stageW = 130; + const stageH = 24; + const stageTitles = ['request event', 'lsm/seccomp hook', 'policy verdict']; + stageXs.forEach((sx, i) => { + this.drawRoundedRect(sx, y + 34, stageW, stageH, 5); + this.ctx.fillStyle = 'rgba(12, 16, 22, 0.62)'; + this.ctx.fill(); + this.ctx.strokeStyle = 'rgba(112, 123, 140, 0.28)'; + this.ctx.stroke(); + this.ctx.fillStyle = '#9ed4ff'; + this.ctx.font = '10px "Share Tech Mono", monospace'; + this.ctx.fillText(stageTitles[i], sx + 8, y + 50); + if (i < stageXs.length - 1) { + this.ctx.beginPath(); + this.ctx.moveTo(sx + stageW + 6, y + 46); + this.ctx.lineTo(stageXs[i + 1] - 8, y + 46); + this.ctx.strokeStyle = 'rgba(155, 168, 190, 0.32)'; + this.ctx.stroke(); + } + }); + + laneRows.forEach((lane, idx) => { + const yy = y + 76 + idx * 30; + const color = this.verdictColor(String(lane.verdict || '')); + const selected = this.selectedProcessFilter; + const laneMatchSelected = !selected + || Number(lane.pid || 0) === Number(selected.pid || 0) + || this.normalizeName(lane.process) === this.normalizeName(selected.name); + this.ctx.fillStyle = laneMatchSelected ? 'rgba(11, 16, 22, 0.72)' : 'rgba(10, 14, 20, 0.46)'; + this.ctx.fillRect(x + 14, yy - 12, w - 28, 24); + + this.ctx.fillStyle = laneMatchSelected ? '#d8ebff' : 'rgba(185, 200, 220, 0.62)'; + this.ctx.font = '10px "Share Tech Mono", monospace'; + this.ctx.fillText(`${String(lane.process || 'unknown').slice(0, 16)}:${lane.pid || 0}`, stageXs[0] + 4, yy + 2); + this.ctx.fillText(String(lane.hook || '-').slice(0, 22), stageXs[1] + 4, yy + 2); + this.ctx.fillStyle = laneMatchSelected ? color : 'rgba(140, 154, 173, 0.62)'; + this.ctx.fillText(String(lane.verdict || '-').toUpperCase(), stageXs[2] + 4, yy + 2); + }); + + if (!laneRows.length) { + this.ctx.fillStyle = 'rgba(196, 207, 224, 0.72)'; + this.ctx.font = '11px "Share Tech Mono", monospace'; + if (selected) { + this.ctx.fillText(`NO EVENTS FOR ${selected.name || 'PROCESS'} + FILTER`, x + 18, y + h - 16); + } else { + this.ctx.fillText('NO EVENTS FOR CURRENT FILTER', x + 18, y + h - 16); + } + } + } + + drawTrustGraph(x, y, w, h, telemetry) { + this.drawPanel(x, y, w, h, 'PROCESS TRUST GRAPH'); + this.trustNodeHitAreas = []; + const rows = Array.isArray(telemetry?.trust_graph) ? telemetry.trust_graph.slice(0, 10) : []; + const cx = x + Math.floor(w * 0.5); + const cy = y + Math.floor(h * 0.56); + const radius = Math.min(w, h) * 0.28; + + this.ctx.beginPath(); + this.ctx.arc(cx, cy, radius + 24, 0, Math.PI * 2); + this.ctx.strokeStyle = 'rgba(162, 176, 198, 0.18)'; + this.ctx.stroke(); + this.ctx.beginPath(); + this.ctx.arc(cx, cy, radius, 0, Math.PI * 2); + this.ctx.strokeStyle = 'rgba(162, 176, 198, 0.32)'; + this.ctx.stroke(); + this.ctx.beginPath(); + this.ctx.arc(cx, cy, 26, 0, Math.PI * 2); + this.ctx.fillStyle = 'rgba(7, 10, 15, 0.9)'; + this.ctx.fill(); + this.ctx.strokeStyle = 'rgba(162, 176, 198, 0.32)'; + this.ctx.stroke(); + this.ctx.fillStyle = '#cde6ff'; + this.ctx.font = '10px "Share Tech Mono", monospace'; + this.ctx.fillText('LSM', cx - 10, cy + 3); + + rows.forEach((row, idx) => { + const a = ((Math.PI * 2) / Math.max(rows.length, 1)) * idx - Math.PI / 2; + const nx = cx + Math.cos(a) * radius; + const ny = cy + Math.sin(a) * radius; + const trust = String(row.trust || 'trusted'); + const color = this.trustColor(trust); + const pulse = 0.78 + 0.22 * Math.sin(this.tick * 0.04 + idx * 0.5); + const pid = Number(row.pid || 0); + const isSelected = this.selectedProcessFilter && Number(this.selectedProcessFilter.pid || 0) === pid; + const isHovered = Number(this.hoveredProcessPid || 0) === pid; + const nodeR = isSelected ? 12 : (isHovered ? 11 : 10); + const glowR = isSelected ? 18 : (isHovered ? 15 : 0); + + this.ctx.beginPath(); + this.ctx.moveTo(cx, cy); + this.ctx.lineTo(nx, ny); + this.ctx.strokeStyle = `rgba(155, 168, 190, ${0.20 + 0.15 * pulse})`; + this.ctx.lineWidth = isSelected ? 1.15 : 0.9; + this.ctx.stroke(); + + if (glowR > 0) { + const glow = this.ctx.createRadialGradient(nx, ny, 0, nx, ny, glowR); + glow.addColorStop(0, isSelected ? 'rgba(124, 178, 255, 0.38)' : 'rgba(138, 156, 234, 0.24)'); + glow.addColorStop(1, 'rgba(124, 178, 255, 0)'); + this.ctx.beginPath(); + this.ctx.arc(nx, ny, glowR, 0, Math.PI * 2); + this.ctx.fillStyle = glow; + this.ctx.fill(); + } + + this.ctx.beginPath(); + this.ctx.arc(nx, ny, nodeR, 0, Math.PI * 2); + this.ctx.fillStyle = color; + this.ctx.globalAlpha = 0.86; + this.ctx.fill(); + this.ctx.globalAlpha = 1; + this.ctx.strokeStyle = isSelected ? 'rgba(124, 178, 255, 0.95)' : '#0e1621'; + this.ctx.lineWidth = isSelected ? 1.5 : 0.95; + this.ctx.stroke(); + this.ctx.lineWidth = 1; + + this.ctx.fillStyle = '#cfdced'; + this.ctx.font = '9px "Share Tech Mono", monospace'; + const label = `${String(row.name || 'proc').slice(0, 9)} (${row.risk_score || 0})`; + this.ctx.fillText(label, nx - 32, ny + 21); + + this.trustNodeHitAreas.push({ + x: nx, + y: ny, + r: nodeR, + pid, + name: row.name || 'unknown' + }); + }); + } + + drawAttackSurface(x, y, w, h, telemetry) { + this.drawPanel(x, y, w, h, 'ATTACK SURFACE MAP'); + const rows = Array.isArray(telemetry?.attack_surface) ? telemetry.attack_surface.slice(0, 8) : []; + + rows.forEach((row, idx) => { + const yy = y + 42 + idx * 34; + const severity = String(row.severity || 'low'); + const color = severity === 'high' ? '#eb7e7e' : (severity === 'medium' ? '#f4c977' : '#8a9cea'); + this.drawRoundedRect(x + 14, yy, w - 28, 26, 5); + this.ctx.fillStyle = 'rgba(12, 16, 22, 0.62)'; + this.ctx.fill(); + this.ctx.strokeStyle = 'rgba(112, 123, 140, 0.28)'; + this.ctx.lineWidth = 0.9; + this.ctx.stroke(); + + this.ctx.fillStyle = '#b6c6da'; + this.ctx.font = '10px "Share Tech Mono", monospace'; + this.ctx.fillText(String(row.name || '-').toUpperCase(), x + 22, yy + 17); + this.ctx.fillStyle = '#d8e5f7'; + this.ctx.fillText(String(row.value || 0), x + w - 88, yy + 17); + this.ctx.fillStyle = color; + this.ctx.fillText(severity.toUpperCase(), x + w - 50, yy + 17); + }); + } + + drawHeaderStats(telemetry) { + const meta = telemetry?.meta || {}; + this.ctx.fillStyle = 'rgba(179, 203, 232, 0.92)'; + this.ctx.font = '11px "Share Tech Mono", monospace'; + this.ctx.fillText(`decisions/s ${Number(meta.decisions_per_sec || 0).toFixed(2)}`, 26, 114); + this.ctx.fillText(`events ${Number(meta.events || 0)}`, 226, 114); + this.ctx.fillText(`mode ${String(meta.mode || 'n/a')}`, 326, 114); + this.ctx.fillText( + `trust T:${meta.trusted || 0} O:${meta.observe || 0} S:${meta.suspicious || 0} B:${meta.blocked || 0}`, + 500, + 114 + ); + this.ctx.fillStyle = 'rgba(180, 196, 220, 0.86)'; + const selected = this.selectedProcessFilter; + if (selected) { + this.ctx.fillText(`selected process: ${selected.name}:${selected.pid} (click again to clear)`, 26, 132); + } else { + this.ctx.fillText('tip: click a process node in PROCESS TRUST GRAPH to filter pipeline', 26, 132); + } + } + + drawScene() { + if (!this.ctx || !this.canvas) return; + const w = window.innerWidth; + const h = window.innerHeight; + this.ctx.clearRect(0, 0, w, h); + this.tick += 1; + + // Match crypto page background treatment. + const bg = this.ctx.createRadialGradient( + w * 0.5, + h * 0.4, + 0, + w * 0.5, + h * 0.4, + Math.max(w, h) * 0.72 + ); + bg.addColorStop(0, '#121821'); + bg.addColorStop(0.7, '#0a0d12'); + bg.addColorStop(1, '#0a0d12'); + this.ctx.fillStyle = bg; + this.ctx.fillRect(0, 0, w, h); + + // Grid backdrop. + this.ctx.strokeStyle = 'rgba(108, 120, 139, 0.14)'; + this.ctx.lineWidth = 0.9; + for (let x = 0; x < w; x += 54) { + this.ctx.beginPath(); + this.ctx.moveTo(x, 0); + this.ctx.lineTo(x, h); + this.ctx.stroke(); + } + for (let y = 0; y < h; y += 42) { + this.ctx.beginPath(); + this.ctx.moveTo(0, y); + this.ctx.lineTo(w, y); + this.ctx.stroke(); + } + + if (!this.telemetry) return; + this.drawHeaderStats(this.telemetry); + + const gap = 16; + const panelTop = 172; + const panelH = Math.max(340, h - panelTop - 24); + const leftW = Math.max(440, Math.floor(w * 0.42)); + const centerW = Math.max(330, Math.floor(w * 0.26)); + const rightW = Math.max(310, w - leftW - centerW - gap * 4); + + const leftX = gap; + const centerX = leftX + leftW + gap; + const rightX = centerX + centerW + gap; + + this.drawDecisionPipeline(leftX, panelTop, leftW, panelH, this.telemetry); + this.drawTrustGraph(centerX, panelTop, centerW, panelH, this.telemetry); + this.drawAttackSurface(rightX, panelTop, rightW, panelH, this.telemetry); + } + + animate() { + if (!this.isActive) return; + this.animationId = requestAnimationFrame(() => this.animate()); + this.drawScene(); + } + + activate() { + if (!this.container) { + const ok = this.init(); + if (ok === false) return; + } + this.isActive = true; + this.container.style.display = 'block'; + this.container.style.visibility = 'visible'; + this.container.style.pointerEvents = 'auto'; + this.onResize(); + + this.fetchTelemetry(); + if (this.telemetryInterval) clearInterval(this.telemetryInterval); + this.telemetryInterval = setInterval(() => { + if (this.isActive) this.fetchTelemetry(); + }, 1300); + + this.animate(); + } + + deactivate() { + this.isActive = false; + if (this.animationId) { + cancelAnimationFrame(this.animationId); + this.animationId = null; + } + if (this.telemetryInterval) { + clearInterval(this.telemetryInterval); + this.telemetryInterval = null; + } + if (this.container) { + this.container.style.display = 'none'; + this.container.style.visibility = 'hidden'; + this.container.style.pointerEvents = 'none'; + } + if (this.canvas) { + this.canvas.style.cursor = 'default'; + } + } + + onResize() { + if (!this.canvas) return; + const dpr = Math.min(window.devicePixelRatio || 1, 2); + this.canvas.width = Math.floor(window.innerWidth * dpr); + this.canvas.height = Math.floor(window.innerHeight * dpr); + this.canvas.style.width = `${window.innerWidth}px`; + this.canvas.style.height = `${window.innerHeight}px`; + if (this.ctx) this.ctx.setTransform(dpr, 0, 0, dpr, 0, 0); + } +} + +window.SecuritySubsystemVisualization = SecuritySubsystemVisualization; +debugLog('πŸ›‘οΈ security-belt.js: SecuritySubsystemVisualization exported to window'); diff --git a/templates/linux-security-subsystem.html b/templates/linux-security-subsystem.html new file mode 100644 index 0000000..0add164 --- /dev/null +++ b/templates/linux-security-subsystem.html @@ -0,0 +1,134 @@ + + + + + + Linux Security Kernel Subsystem - Linux Kernel Architecture Visualization + + + + + + + + + + + + + + + + + + + + + + + +

Linux Security Kernel Subsystem

+ +
+

Linux Security Subsystem

+

+ The Linux security subsystem enforces runtime security policy in kernel space. + It evaluates process and I/O requests using Linux Security Module hooks, + seccomp filters, and policy checks before allowing or denying operations. +

+ +

Threat Decision Pipeline

+

+ Security decisions in Linux follow a request pipeline: request event, + policy hook evaluation, and final verdict. This page visualizes how + events are classified into allow, audit, or deny outcomes. +

+ +

Process Trust Graph

+

+ Process trust is represented as a graph driven by heuristic risk scoring. + It highlights trusted, observed, suspicious, and blocked candidates to + help understand the security posture in real time. +

+ +

Attack Surface Map

+

+ The attack surface map summarizes key indicators such as open listen ports, + setuid binaries, loaded kernel modules, ptrace-capable tooling, and root + process distribution. +

+
+ + + + + + + + +