diff --git a/app.py b/app.py index 1b220bf..f2a10ce 100644 --- a/app.py +++ b/app.py @@ -4085,6 +4085,140 @@ def classify_trust(score): {"name": "suspicious-processes", "value": int(suspicious_processes), "severity": "high" if suspicious_processes > 6 else "medium"} ] + # Stage 3: kernel security tools insights. + def _read_text(path): + try: + with open(path, "r", encoding="utf-8", errors="ignore") as f: + return str(f.read().strip()) + except Exception: + return "" + + # LSM status matrix (best effort, distro dependent). + apparmor_raw = _read_text("/sys/module/apparmor/parameters/enabled") + selinux_enforce = _read_text("/sys/fs/selinux/enforce") + yama_scope = _read_text("/proc/sys/kernel/yama/ptrace_scope") + bpf_unpriv = _read_text("/proc/sys/kernel/unprivileged_bpf_disabled") + landlock_present = os.path.exists("/sys/kernel/security/landlock") + ima_present = os.path.exists("/sys/kernel/security/ima") + lsm_status = [ + { + "name": "AppArmor", + "status": "enforcing" if apparmor_raw.lower().startswith("y") else ("disabled" if apparmor_raw else "unknown"), + "detail": apparmor_raw or "n/a" + }, + { + "name": "SELinux", + "status": "enforcing" if selinux_enforce == "1" else ("disabled" if selinux_enforce == "0" else "unknown"), + "detail": selinux_enforce or "n/a" + }, + { + "name": "Yama ptrace", + "status": "hardened" if yama_scope in {"2", "3"} else ("relaxed" if yama_scope in {"0", "1"} else "unknown"), + "detail": yama_scope or "n/a" + }, + { + "name": "unprivileged bpf", + "status": "blocked" if bpf_unpriv == "1" else ("allowed" if bpf_unpriv == "0" else "unknown"), + "detail": bpf_unpriv or "n/a" + }, + { + "name": "Landlock", + "status": "present" if landlock_present else "absent", + "detail": "sysfs" if landlock_present else "n/a" + }, + { + "name": "IMA/EVM", + "status": "present" if ima_present else "absent", + "detail": "sysfs" if ima_present else "n/a" + } + ] + + # Capabilities drift (CapEff/CapPrm from /proc//status). + dangerous_caps = { + 12: "CAP_NET_ADMIN", + 16: "CAP_SYS_MODULE", + 17: "CAP_SYS_RAWIO", + 19: "CAP_SYS_PTRACE", + 21: "CAP_SYS_ADMIN", + 39: "CAP_BPF" + } + capabilities_rows = [] + seccomp_counts = {"none": 0, "strict": 0, "filter": 0, "unknown": 0} + + for row in process_rows[:180]: + pid = int(row.get("pid") or 0) + if pid <= 0: + continue + cap_eff_hex = "" + cap_prm_hex = "" + seccomp_mode = "unknown" + try: + with open(f"/proc/{pid}/status", "r", encoding="utf-8", errors="ignore") as f: + for ln in f: + if ln.startswith("CapEff:"): + cap_eff_hex = ln.split(":", 1)[1].strip() + elif ln.startswith("CapPrm:"): + cap_prm_hex = ln.split(":", 1)[1].strip() + elif ln.startswith("Seccomp:"): + seccomp_raw = ln.split(":", 1)[1].strip() + if seccomp_raw == "0": + seccomp_mode = "none" + elif seccomp_raw == "1": + seccomp_mode = "strict" + elif seccomp_raw == "2": + seccomp_mode = "filter" + else: + seccomp_mode = "unknown" + except Exception: + pass + + seccomp_counts[seccomp_mode] = seccomp_counts.get(seccomp_mode, 0) + 1 + if not cap_eff_hex: + continue + try: + cap_eff_val = int(cap_eff_hex, 16) + cap_prm_val = int(cap_prm_hex or "0", 16) + except Exception: + continue + + matched = [name for bit, name in dangerous_caps.items() if (cap_eff_val & (1 << bit))] + if not matched: + continue + risk = min(100, 20 + len(matched) * 16 + (10 if row.get("user") == "root" else 0)) + capabilities_rows.append({ + "pid": pid, + "name": row.get("name", "unknown"), + "user": row.get("user", ""), + "seccomp": seccomp_mode, + "cap_eff": cap_eff_hex, + "cap_prm": cap_prm_hex or "0", + "dangerous": matched[:4], + "risk_score": int(risk) + }) + + capabilities_rows.sort(key=lambda x: (x.get("risk_score", 0), len(x.get("dangerous", []))), reverse=True) + capabilities_drift = capabilities_rows[:8] + + # Seccomp coverage summary + top unsandboxed risky processes. + total_seccomp_sample = max(1, sum(seccomp_counts.values())) + unsandboxed = [r for r in capabilities_rows if r.get("seccomp") == "none"] + unsandboxed.sort(key=lambda x: x.get("risk_score", 0), reverse=True) + seccomp_coverage = { + "none": int(seccomp_counts.get("none", 0)), + "strict": int(seccomp_counts.get("strict", 0)), + "filter": int(seccomp_counts.get("filter", 0)), + "unknown": int(seccomp_counts.get("unknown", 0)), + "coverage_percent": round((seccomp_counts.get("filter", 0) + seccomp_counts.get("strict", 0)) * 100.0 / total_seccomp_sample, 2), + "high_risk_unsandboxed": [ + { + "pid": int(r.get("pid", 0)), + "name": str(r.get("name", "unknown")), + "risk_score": int(r.get("risk_score", 0)) + } + for r in unsandboxed[:6] + ] + } + prev_ts = SECURITY_PREV["timestamp"] prev_events = int(SECURITY_PREV["events"] or 0) current_events = len(lanes) @@ -4108,6 +4242,11 @@ def classify_trust(score): }, "trust_graph": trust_graph, "attack_surface": attack_surface, + "security_tools": { + "lsm_status": lsm_status, + "capabilities_drift": capabilities_drift, + "seccomp_coverage": seccomp_coverage + }, "meta": { "decisions_per_sec": decisions_per_sec, "events": current_events, @@ -4115,6 +4254,7 @@ def classify_trust(score): "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"), + "seccomp_coverage_percent": seccomp_coverage.get("coverage_percent", 0.0), "mode": "live-heuristic-v1" } } diff --git a/index.html b/index.html index 5c75a38..69200c4 100755 --- a/index.html +++ b/index.html @@ -101,7 +101,7 @@

Linux Kernel Ring 0 Visualization

- + diff --git a/static/js/security-belt.js b/static/js/security-belt.js index ed16d37..06431e3 100644 --- a/static/js/security-belt.js +++ b/static/js/security-belt.js @@ -1,7 +1,7 @@ -// Security Subsystem Visualization (Stage 1) -// Version: 6 +// Security Subsystem Visualization (Stage 3) +// Version: 10 -debugLog('🛡️ security-belt.js v6: Script loading...'); +debugLog('🛡️ security-belt.js v10: Script loading...'); class SecuritySubsystemVisualization { constructor() { @@ -22,6 +22,7 @@ class SecuritySubsystemVisualization { this.selectedProcessFilter = null; this.hoveredProcessPid = null; this.trustNodeHitAreas = []; + this.capabilityRowHitAreas = []; this.canvasClickHandler = null; this.canvasMouseMoveHandler = null; } @@ -85,7 +86,7 @@ class SecuritySubsystemVisualization { z-index: 1001; text-shadow: 0 0 8px rgba(180, 210, 255, 0.25); `; - title.textContent = 'KERNEL SECURITY SUBSYSTEM (stage 1)'; + title.textContent = 'KERNEL SECURITY SUBSYSTEM (stage 3)'; this.container.appendChild(title); this.overlayNodes.push(title); @@ -100,7 +101,7 @@ class SecuritySubsystemVisualization { font-size: 11px; z-index: 1001; `; - legend.textContent = 'threat decision pipeline + process trust graph + attack surface map'; + legend.textContent = 'pipeline + trust + attack surface + lsm/capabilities/seccomp tools'; this.container.appendChild(legend); this.overlayNodes.push(legend); @@ -265,6 +266,14 @@ class SecuritySubsystemVisualization { break; } } + if (!matched) { + for (const row of this.capabilityRowHitAreas) { + if (x >= row.x && x <= row.x + row.w && y >= row.y && y <= row.y + row.h) { + matched = row; + break; + } + } + } if (!matched) return; const pid = Number(matched.pid || 0); if (this.selectedProcessFilter && Number(this.selectedProcessFilter.pid || 0) === pid) { @@ -294,6 +303,14 @@ class SecuritySubsystemVisualization { break; } } + if (!hovered) { + for (const row of this.capabilityRowHitAreas) { + if (x >= row.x && x <= row.x + row.w && y >= row.y && y <= row.y + row.h) { + hovered = Number(row.pid || 0); + break; + } + } + } this.hoveredProcessPid = hovered; this.canvas.style.cursor = hovered ? 'pointer' : 'default'; } @@ -513,6 +530,110 @@ class SecuritySubsystemVisualization { }); } + drawLsmStatusCard(x, y, w, h, telemetry) { + this.drawPanel(x, y, w, h, 'LSM STATUS MATRIX'); + const rows = Array.isArray(telemetry?.security_tools?.lsm_status) + ? telemetry.security_tools.lsm_status.slice(0, 6) + : []; + rows.forEach((row, idx) => { + const yy = y + 40 + idx * 22; + const status = String(row.status || 'unknown').toLowerCase(); + let color = '#8a9cea'; + if (status.includes('enforcing') || status.includes('blocked') || status.includes('hardened') || status.includes('present')) color = '#60d69d'; + if (status.includes('relaxed')) color = '#f4c977'; + if (status.includes('disabled') || status.includes('absent')) color = '#eb7e7e'; + this.ctx.fillStyle = 'rgba(10, 14, 20, 0.46)'; + this.ctx.fillRect(x + 12, yy - 12, w - 24, 18); + this.ctx.fillStyle = '#cfdced'; + this.ctx.font = '10px "Share Tech Mono", monospace'; + this.ctx.fillText(String(row.name || 'lsm'), x + 16, yy); + this.ctx.fillStyle = color; + this.ctx.fillText(String(row.status || 'unknown').toUpperCase(), x + Math.floor(w * 0.56), yy); + this.ctx.fillStyle = 'rgba(185, 200, 220, 0.7)'; + this.ctx.fillText(String(row.detail || ''), x + w - 74, yy); + }); + } + + drawCapabilitiesCard(x, y, w, h, telemetry) { + this.drawPanel(x, y, w, h, 'CAPABILITIES DRIFT'); + this.capabilityRowHitAreas = []; + const rows = Array.isArray(telemetry?.security_tools?.capabilities_drift) + ? telemetry.security_tools.capabilities_drift.slice(0, 6) + : []; + rows.forEach((row, idx) => { + const yy = y + 40 + idx * 22; + const pid = Number(row.pid || 0); + const risk = Number(row.risk_score || 0); + const danger = Array.isArray(row.dangerous) ? row.dangerous : []; + const color = risk >= 70 ? '#eb7e7e' : (risk >= 45 ? '#f4c977' : '#8a9cea'); + const isSelected = this.selectedProcessFilter && Number(this.selectedProcessFilter.pid || 0) === pid; + const isHovered = Number(this.hoveredProcessPid || 0) === pid; + const rowX = x + 12; + const rowY = yy - 12; + const rowW = w - 24; + const rowH = 18; + this.ctx.fillStyle = isSelected ? 'rgba(32, 52, 81, 0.62)' : (isHovered ? 'rgba(22, 34, 52, 0.56)' : 'rgba(10, 14, 20, 0.46)'); + this.ctx.fillRect(rowX, rowY, rowW, rowH); + if (isSelected || isHovered) { + this.ctx.strokeStyle = isSelected ? 'rgba(124, 178, 255, 0.95)' : 'rgba(112, 152, 216, 0.72)'; + this.ctx.lineWidth = isSelected ? 1.2 : 0.9; + this.ctx.strokeRect(rowX + 0.5, rowY + 0.5, rowW - 1, rowH - 1); + } + this.ctx.fillStyle = '#d8e5f7'; + this.ctx.font = '10px "Share Tech Mono", monospace'; + this.ctx.fillText(`${String(row.name || 'proc').slice(0, 10)}:${pid || 0}`, x + 16, yy); + this.ctx.fillStyle = color; + this.ctx.fillText(`risk ${risk}`, x + Math.floor(w * 0.40), yy); + this.ctx.fillStyle = 'rgba(185, 200, 220, 0.72)'; + this.ctx.fillText(danger.slice(0, 2).join(','), x + Math.floor(w * 0.56), yy); + this.capabilityRowHitAreas.push({ + x: rowX, + y: rowY, + w: rowW, + h: rowH, + pid, + name: row.name || 'unknown' + }); + }); + } + + drawSeccompCoverageCard(x, y, w, h, telemetry) { + this.drawPanel(x, y, w, h, 'SECCOMP COVERAGE'); + const cov = telemetry?.security_tools?.seccomp_coverage || {}; + const none = Number(cov.none || 0); + const strict = Number(cov.strict || 0); + const filter = Number(cov.filter || 0); + const unknown = Number(cov.unknown || 0); + const coverage = Number(cov.coverage_percent || 0); + + this.ctx.fillStyle = 'rgba(179, 203, 232, 0.9)'; + this.ctx.font = '11px "Share Tech Mono", monospace'; + this.ctx.fillText(`coverage ${coverage.toFixed(2)}%`, x + 14, y + 42); + this.ctx.fillText(`filter:${filter} strict:${strict} none:${none} unknown:${unknown}`, x + 14, y + 60); + + const barX = x + 14; + const barY = y + 72; + const barW = w - 28; + const barH = 12; + this.ctx.fillStyle = 'rgba(12, 16, 22, 0.62)'; + this.ctx.fillRect(barX, barY, barW, barH); + const fillW = Math.max(0, Math.min(barW, (coverage / 100) * barW)); + this.ctx.fillStyle = coverage >= 80 ? '#60d69d' : (coverage >= 50 ? '#f4c977' : '#eb7e7e'); + this.ctx.fillRect(barX, barY, fillW, barH); + + const unsandboxed = Array.isArray(cov.high_risk_unsandboxed) ? cov.high_risk_unsandboxed.slice(0, 4) : []; + unsandboxed.forEach((row, idx) => { + const yy = y + 102 + idx * 20; + this.ctx.fillStyle = 'rgba(10, 14, 20, 0.46)'; + this.ctx.fillRect(x + 12, yy - 11, w - 24, 16); + this.ctx.fillStyle = '#d8e5f7'; + this.ctx.font = '10px "Share Tech Mono", monospace'; + this.ctx.fillText(`${String(row.name || 'proc').slice(0, 14)}:${row.pid || 0}`, x + 16, yy); + this.ctx.fillStyle = '#eb7e7e'; + this.ctx.fillText(`risk ${Number(row.risk_score || 0)}`, x + w - 88, yy); + }); + } + drawHeaderStats(telemetry) { const meta = telemetry?.meta || {}; this.ctx.fillStyle = 'rgba(179, 203, 232, 0.92)'; @@ -525,12 +646,13 @@ class SecuritySubsystemVisualization { 500, 114 ); + this.ctx.fillText(`seccomp coverage ${Number(meta.seccomp_coverage_percent || 0).toFixed(2)}%`, 930, 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); + this.ctx.fillText('tip: click a process node or CAPABILITIES DRIFT row to filter pipeline', 26, 132); } } @@ -577,7 +699,8 @@ class SecuritySubsystemVisualization { const gap = 16; const panelTop = 172; - const panelH = Math.max(340, h - panelTop - 24); + const toolsH = Math.max(128, Math.min(176, Math.floor(h * 0.22))); + const panelH = Math.max(220, h - panelTop - toolsH - gap - 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); @@ -585,10 +708,17 @@ class SecuritySubsystemVisualization { const leftX = gap; const centerX = leftX + leftW + gap; const rightX = centerX + centerW + gap; + const toolsY = panelTop + panelH + gap; + const toolsW = Math.max(220, Math.floor((w - gap * 4) / 3)); + const tools2X = leftX + toolsW + gap; + const tools3X = tools2X + toolsW + 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); + this.drawLsmStatusCard(leftX, toolsY, toolsW, toolsH, this.telemetry); + this.drawCapabilitiesCard(tools2X, toolsY, toolsW, toolsH, this.telemetry); + this.drawSeccompCoverageCard(tools3X, toolsY, toolsW, toolsH, this.telemetry); } animate() { diff --git a/templates/linux-security-subsystem.html b/templates/linux-security-subsystem.html index 0add164..967cc73 100644 --- a/templates/linux-security-subsystem.html +++ b/templates/linux-security-subsystem.html @@ -111,7 +111,7 @@

Attack Surface Map

- +