diff --git a/kernel_ai/api/rest.py b/kernel_ai/api/rest.py
index 7c5624d..fb2ad56 100644
--- a/kernel_ai/api/rest.py
+++ b/kernel_ai/api/rest.py
@@ -32,6 +32,7 @@
("/proc-timeline-branches", "get_proc_timeline_branches", h.get_proc_timeline_branches, None),
("/execution-context", "get_execution_context", h.get_execution_context, None),
("/kernel-dna", "kernel_dna", h.kernel_dna, None),
+ ("/siem-alerts", "siem_alerts", h.siem_alerts, None),
("/ml-anomalies", "ml_anomalies", h.ml_anomalies, None),
("/ml-drift", "ml_drift", h.ml_drift, None),
("/crypto-realtime", "crypto_realtime", h.crypto_realtime, None),
diff --git a/kernel_ai/http/api.py b/kernel_ai/http/api.py
index c8e922c..d8e7e1a 100644
--- a/kernel_ai/http/api.py
+++ b/kernel_ai/http/api.py
@@ -11,6 +11,7 @@
nginx_files,
process_kernel_map,
sentry_test,
+ siem_alerts,
syscalls_realtime,
)
from kernel_ai.http.api_handlers.network_system import (
@@ -85,6 +86,7 @@
"scheduler_pelt",
"security_realtime",
"sentry_test",
+ "siem_alerts",
"syscalls_realtime",
"traceroute_info",
]
diff --git a/kernel_ai/http/api_handlers/kernel.py b/kernel_ai/http/api_handlers/kernel.py
index 42de9f0..630f42a 100644
--- a/kernel_ai/http/api_handlers/kernel.py
+++ b/kernel_ai/http/api_handlers/kernel.py
@@ -82,6 +82,29 @@ def kernel_dna():
return api_json(_telemetry.get_kernel_dna_data)
+def siem_alerts():
+ """Recent Elastic SIEM detection alerts (web attacks) for Kernel DNA.
+
+ Read-only bridge to the sibling Elastic server. Fails soft: if Elastic is
+ unreachable/unconfigured, returns available=False with an empty list.
+ """
+
+ def _payload():
+ from kernel_ai.services import siem as _siem
+
+ try:
+ hours = int(request.args.get("hours", 24))
+ except (TypeError, ValueError):
+ hours = 24
+ try:
+ limit = int(request.args.get("limit", 120))
+ except (TypeError, ValueError):
+ limit = 120
+ return _siem.get_siem_alerts(hours=hours, limit=limit)
+
+ return api_json(_payload)
+
+
def ml_anomalies():
"""Recent ML-detected anomalies (Stage 1 baselines) for Kernel DNA.
diff --git a/kernel_ai/http/pages.py b/kernel_ai/http/pages.py
index b2051de..43961c8 100644
--- a/kernel_ai/http/pages.py
+++ b/kernel_ai/http/pages.py
@@ -72,6 +72,42 @@ def linux_network_subsystem_html():
return redirect("/linux-network-subsystem", code=301)
+def linux_filesystem_subsystem_page():
+ return render_template("linux-filesystem-subsystem.html")
+
+
+def filesystem_page_legacy():
+ return redirect("/linux-filesystem-subsystem", code=301)
+
+
+def files_page_legacy():
+ return redirect("/linux-filesystem-subsystem", code=301)
+
+
+def linux_filesystem_subsystem_html():
+ return redirect("/linux-filesystem-subsystem", code=301)
+
+
+def kernel_dna_page():
+ return render_template("kernel-dna.html")
+
+
+def kernel_dna_html():
+ return redirect("/kernel-dna", code=301)
+
+
+def linux_devices_subsystem_page():
+ return render_template("linux-devices-subsystem.html")
+
+
+def devices_page_legacy():
+ return redirect("/linux-devices-subsystem", code=301)
+
+
+def linux_devices_subsystem_html():
+ return redirect("/linux-devices-subsystem", code=301)
+
+
def health_check():
return jsonify(
{
diff --git a/static/js/devices-belt.js b/static/js/devices-belt.js
index 4f5a295..e99755d 100644
--- a/static/js/devices-belt.js
+++ b/static/js/devices-belt.js
@@ -1,7 +1,7 @@
// Device Control Surface Visualization
-// Version: 23
+// Version: 24
-debugLog('🧲 devices-belt.js v23: Script loading...');
+debugLog('🧲 devices-belt.js v24: Script loading...');
class DevicesBeltVisualization {
constructor() {
@@ -118,6 +118,11 @@ class DevicesBeltVisualization {
this.resizeHandler = () => this.onResize();
window.addEventListener('resize', this.resizeHandler);
+ this.keyHandler = (event) => {
+ if (event.key === 'Escape') this.closeDrill();
+ };
+ window.addEventListener('keydown', this.keyHandler);
+
return true;
}
@@ -296,6 +301,8 @@ class DevicesBeltVisualization {
this.overlayNodes.push(detail);
this.deviceDetailNode = detail;
+ this.createDrillOverlay();
+
const err = document.createElement('div');
err.style.cssText = `
position: absolute;
@@ -346,7 +353,9 @@ class DevicesBeltVisualization {
if (window.kernelContextMenu) {
window.kernelContextMenu.deactivateViews();
} else {
- this.deactivate();
+ // Standalone page (e.g. /linux-devices-subsystem): go home like
+ // the other subsystem pages instead of leaving a blank view.
+ window.location.assign('/');
}
};
this.container.appendChild(btn);
@@ -588,7 +597,8 @@ class DevicesBeltVisualization {
if (!device) {
return `
DEVICE SIGNATURE
- hover a device block to inspect sysfs, driver and IRQ/DMA path
+ hover a device block to inspect sysfs, driver and IRQ/DMA path
+ click a device to open the full inspector
`;
}
const category = this.normalizeCategory(device.category);
@@ -617,6 +627,162 @@ class DevicesBeltVisualization {
window.setSafeHtml(this.deviceDetailNode, this.formatDeviceDetail(device));
}
+ subsystemLinkFor(device) {
+ const sub = String(device.subsystem || '').toLowerCase();
+ const cat = this.normalizeCategory(device.category);
+ if (sub === 'file-system' || cat === 'block') {
+ return { href: '/linux-filesystem-subsystem', label: 'OPEN FILESYSTEM SUBSYSTEM' };
+ }
+ if (sub === 'network-stack' || cat === 'net') {
+ return { href: '/linux-network-subsystem', label: 'OPEN NETWORK SUBSYSTEM' };
+ }
+ return null;
+ }
+
+ createDrillOverlay() {
+ const scrim = document.createElement('div');
+ scrim.style.cssText = `
+ position: absolute;
+ inset: 0;
+ background: radial-gradient(circle at 50% 42%, rgba(6, 12, 18, 0.68), rgba(2, 5, 9, 0.9));
+ z-index: 2000;
+ display: none;
+ `;
+ scrim.addEventListener('click', (event) => { if (event.target === scrim) this.closeDrill(); });
+
+ const panel = document.createElement('div');
+ panel.style.cssText = `
+ position: absolute;
+ left: 50%;
+ top: 50%;
+ transform: translate(-50%, -50%);
+ width: min(720px, 92vw);
+ max-height: 84vh;
+ overflow: auto;
+ background: rgba(6, 11, 17, 0.97);
+ border: 1px solid rgba(84, 216, 232, 0.42);
+ border-radius: 10px;
+ box-shadow: 0 0 44px rgba(84, 216, 232, 0.14);
+ padding: 22px 26px 24px;
+ font-family: 'Share Tech Mono', monospace;
+ color: #cfe8f2;
+ `;
+
+ const closeBtn = document.createElement('div');
+ closeBtn.textContent = '×';
+ closeBtn.style.cssText = `
+ position: absolute;
+ top: 12px;
+ right: 16px;
+ color: #7fd8ff;
+ font-size: 22px;
+ line-height: 1;
+ cursor: pointer;
+ opacity: 0.8;
+ `;
+ closeBtn.onmouseenter = () => { closeBtn.style.opacity = '1'; };
+ closeBtn.onmouseleave = () => { closeBtn.style.opacity = '0.8'; };
+ closeBtn.onclick = () => this.closeDrill();
+
+ const content = document.createElement('div');
+
+ const linkBtn = document.createElement('button');
+ linkBtn.style.cssText = `
+ margin-top: 16px;
+ padding: 8px 14px;
+ background: rgba(12, 22, 30, 0.9);
+ border: 1px solid rgba(127, 216, 255, 0.5);
+ color: #bff0ff;
+ font-family: 'Share Tech Mono', monospace;
+ font-size: 11px;
+ letter-spacing: 0.4px;
+ cursor: pointer;
+ display: none;
+ `;
+ linkBtn.onmouseenter = () => { linkBtn.style.background = 'rgba(20, 34, 44, 0.95)'; linkBtn.style.color = '#ffffff'; };
+ linkBtn.onmouseleave = () => { linkBtn.style.background = 'rgba(12, 22, 30, 0.9)'; linkBtn.style.color = '#bff0ff'; };
+ linkBtn.onclick = () => { if (this.drillLinkHref) window.location.assign(this.drillLinkHref); };
+
+ panel.appendChild(closeBtn);
+ panel.appendChild(content);
+ panel.appendChild(linkBtn);
+ scrim.appendChild(panel);
+ this.container.appendChild(scrim);
+ this.overlayNodes.push(scrim);
+
+ this.drillScrim = scrim;
+ this.drillContent = content;
+ this.drillLinkBtn = linkBtn;
+ this.drillLinkHref = null;
+ }
+
+ drillFlowHtml(device) {
+ const cat = this.normalizeCategory(device.category);
+ const bus = this.normalizeBus(device.bus, device.category);
+ const stages = [
+ ['BUS', bus.toUpperCase(), '#8ff0d2'],
+ ['DEVICE', String(device.name || '?').toUpperCase(), '#dffcff'],
+ ['DRIVER', String(device.driver || 'n/a').toUpperCase(), '#c9a6ff'],
+ ['KERNEL', String(device.subsystem || cat || 'core').toUpperCase(), '#7fd8ff'],
+ ['USERSPACE', String(device.user_interaction || 'syscall').toUpperCase(), '#b8c7da']
+ ];
+ const node = (label, value, col) => `
+
+
${this.escapeHtml(label)}
+
${this.escapeHtml(value)}
+
`;
+ const arrow = '→';
+ return `${stages.map(([l, v, c]) => node(l, v, c)).join(arrow)}
`;
+ }
+
+ formatDrillContent(device) {
+ const category = this.normalizeCategory(device.category);
+ const bus = this.normalizeBus(device.bus, device.category);
+ const majorMinor = device.major != null && device.minor != null ? `${device.major}:${device.minor}` : 'hotplug (dynamic)';
+ const loadPct = Math.round(Number(device.load_norm || 0) * 100);
+ const rows = [
+ ['bus / category', `${bus} / ${category}`],
+ ['driver', device.driver || 'n/a (no bound driver)'],
+ ['major:minor', majorMinor],
+ ['irq / sec', Number(device.irq_per_sec || 0).toFixed(2)],
+ ['irq total', Number(device.irq_total || 0).toLocaleString()],
+ ['throughput', `${Number(device.throughput_mb_s || 0).toFixed(3)} MB/s`],
+ ['user interaction', device.user_interaction || 'syscall / ioctl'],
+ ['sysfs path', device.sys_path || '/sys']
+ ];
+ if (device.category === 'net' || device.drops != null || device.errors != null) {
+ rows.push(['net drops / errors', `${Number(device.drops || 0)} / ${Number(device.errors || 0)}`]);
+ }
+ const header = `
+ DEVICE :: ${this.escapeHtml(String(device.name || 'unknown').toUpperCase())}
+ ${this.escapeHtml(bus.toUpperCase())} bus · ${this.escapeHtml(category)} class · load ${loadPct}%
`;
+ const flow = this.drillFlowHtml(device);
+ const flowCaption = 'enumeration path: how this device is reached from the bus down to userspace
';
+ const table = `${rows.map(([k, v]) => `
+
${this.escapeHtml(k)}${this.escapeHtml(v)}
`).join('')}
`;
+ return `${header}${flow}${flowCaption}${table}`;
+ }
+
+ openDrill(device) {
+ if (!this.drillScrim || !device) return;
+ window.setSafeHtml(this.drillContent, this.formatDrillContent(device));
+ const link = this.subsystemLinkFor(device);
+ if (link) {
+ this.drillLinkHref = link.href;
+ this.drillLinkBtn.textContent = `${link.label} \u2192`;
+ this.drillLinkBtn.style.display = 'inline-block';
+ } else {
+ this.drillLinkHref = null;
+ this.drillLinkBtn.style.display = 'none';
+ }
+ this.drillScrim.style.display = 'block';
+ }
+
+ closeDrill() {
+ if (this.drillScrim) this.drillScrim.style.display = 'none';
+ this.drillLinkHref = null;
+ }
+
linkLine(from, to, color, opacity, deviceRef, loadNorm) {
const curve = new THREE.CubicBezierCurve3(
from.clone(),
@@ -672,6 +838,7 @@ class DevicesBeltVisualization {
if (!device) return;
this.selectedDeviceKey = this.deviceKey(device);
this.updateDeviceDetail(device);
+ this.openDrill(device);
}
buildDeviceScene(devices) {
@@ -1024,6 +1191,7 @@ class DevicesBeltVisualization {
deactivate() {
this.isActive = false;
+ this.closeDrill();
if (this.animationId) {
cancelAnimationFrame(this.animationId);
this.animationId = null;
diff --git a/static/js/filesystem-map.js b/static/js/filesystem-map.js
index fa2c42b..bb62112 100644
--- a/static/js/filesystem-map.js
+++ b/static/js/filesystem-map.js
@@ -390,7 +390,9 @@ class FilesystemMapVisualization {
if (window.kernelContextMenu) {
window.kernelContextMenu.deactivateViews();
} else {
- this.deactivate();
+ // Standalone page (e.g. /linux-filesystem-subsystem): go home like
+ // the other subsystem pages instead of leaving a blank view.
+ window.location.assign('/');
}
};
this.container.appendChild(btn);
diff --git a/static/js/kernel-dna.js b/static/js/kernel-dna.js
index 718510d..a12f820 100755
--- a/static/js/kernel-dna.js
+++ b/static/js/kernel-dna.js
@@ -1,10 +1,10 @@
// Kernel DNA Visualization - Double Helix Structure
// Represents Linux kernel execution paths as DNA strands
-// Version: 31 — stable process selector during refresh
+// Version: 42 — feed group badge always-visible + limit 300
-debugLog('🧬 kernel-dna.js v31: Script loading...');
-debugLog('🧬 kernel-dna.js v31: THREE available:', typeof THREE);
-debugLog('🧬 kernel-dna.js v31: Browser:', navigator.userAgent);
+debugLog('🧬 kernel-dna.js v39: Script loading...');
+debugLog('🧬 kernel-dna.js v39: THREE available:', typeof THREE);
+debugLog('🧬 kernel-dna.js v39: Browser:', navigator.userAgent);
class KernelDNAVisualization {
constructor() {
@@ -45,6 +45,17 @@ class KernelDNAVisualization {
this._uxStylesInjected = false;
this.pinnedTimelineLabels = [];
this.pinnedLabelLayer = null;
+
+ // SIEM attack "scars": real Elastic detection alerts rendered as
+ // lesions on the helix, plus a live threat feed HUD.
+ this.siemScars = []; // [{group, shard, core, halo, severity, id, phase, focusUntil}]
+ this.siemLayer = null; // THREE.Group child of helixLeft
+ this.siemInterval = null; // polling timer
+ this.siemAlerts = []; // last normalized alert list
+ this.leftHelixCurve = null; // cached curve for placing scars
+ this.scarById = {}; // id -> scar record
+ this.hoveredScarId = null;
+ this._haloTextures = {}; // color -> CanvasTexture cache
// Color palette - New design system
this.colors = {
@@ -268,7 +279,9 @@ class KernelDNAVisualization {
if (window.kernelContextMenu && typeof window.kernelContextMenu.deactivateViews === 'function') {
window.kernelContextMenu.deactivateViews();
} else {
- this.deactivate();
+ // Standalone page (e.g. /kernel-dna): go home like the other
+ // subsystem pages instead of leaving a blank view.
+ window.location.assign('/');
}
};
this.container.appendChild(exitBtn);
@@ -498,6 +511,362 @@ class KernelDNAVisualization {
return group;
}
+ // ---- SIEM attack scars -------------------------------------------------
+
+ _sevRank(sev) {
+ return { critical: 4, high: 3, medium: 2, low: 1 }[String(sev || '').toLowerCase()] || 2;
+ }
+
+ _sevColorNum(sev) {
+ return {
+ critical: 0xFF4530,
+ high: 0xE0564E,
+ medium: 0xE6C15A,
+ low: 0x8A8F95
+ }[String(sev || '').toLowerCase()] || 0xE6C15A;
+ }
+
+ _sevColorHex(sev) {
+ return {
+ critical: '#ff4530',
+ high: '#e0564e',
+ medium: '#e6c15a',
+ low: '#8a8f95'
+ }[String(sev || '').toLowerCase()] || '#e6c15a';
+ }
+
+ _haloTexture(colorHex) {
+ if (this._haloTextures[colorHex]) return this._haloTextures[colorHex];
+ const c = document.createElement('canvas');
+ c.width = c.height = 64;
+ const ctx = c.getContext('2d');
+ const g = ctx.createRadialGradient(32, 32, 0, 32, 32, 32);
+ g.addColorStop(0, colorHex);
+ g.addColorStop(0.35, colorHex);
+ g.addColorStop(1, 'rgba(0,0,0,0)');
+ ctx.fillStyle = g;
+ ctx.globalAlpha = 0.85;
+ ctx.beginPath();
+ ctx.arc(32, 32, 32, 0, Math.PI * 2);
+ ctx.fill();
+ const tex = new THREE.CanvasTexture(c);
+ tex.needsUpdate = true;
+ this._haloTextures[colorHex] = tex;
+ return tex;
+ }
+
+ // Crisp, non-glowing dot (hard edge) — line-art look, no bloom.
+ _dotTexture(colorHex) {
+ const key = 'dot:' + colorHex;
+ if (this._haloTextures[key]) return this._haloTextures[key];
+ const c = document.createElement('canvas');
+ c.width = c.height = 32;
+ const ctx = c.getContext('2d');
+ ctx.fillStyle = colorHex;
+ ctx.beginPath();
+ ctx.arc(16, 16, 11, 0, Math.PI * 2);
+ ctx.fill();
+ const tex = new THREE.CanvasTexture(c);
+ tex.needsUpdate = true;
+ this._haloTextures[key] = tex;
+ return tex;
+ }
+
+ _hashStr(s) {
+ let h = 2166136261 >>> 0;
+ for (let i = 0; i < s.length; i++) { h ^= s.charCodeAt(i); h = Math.imul(h, 16777619) >>> 0; }
+ return h;
+ }
+
+ // Ghost-in-the-Shell style attack orbits: each MITRE tactic becomes a tilted,
+ // glowing dotted ring encircling the helix at the tactic's height, with a
+ // green micro-label riding the orbit. Radius/brightness scale with volume &
+ // severity.
+ _ATTACK_GREEN = 0x39E67A;
+
+ _accentForRank(rank) {
+ if (rank >= 4) return { num: 0xFF5A45, hex: '#ff5a45' }; // critical
+ if (rank >= 3) return { num: 0xFFCF5A, hex: '#ffcf5a' }; // high
+ return { num: this._ATTACK_GREEN, hex: '#39e67a' }; // med/low → green
+ }
+
+ // Thin finely-dashed orbit ring (reference look) in the local XY plane.
+ // headStrength > 0 bakes a per-vertex brightness gradient: a bright "head"
+ // near frac 0 fading into a dim tail, so the spinning ring reads as a
+ // travelling comet of light along the orbit.
+ _makeOrbitLine(R, colorNum, opacity, headStrength = 0) {
+ const seg = 220;
+ const pts = [];
+ const colors = new Float32Array((seg + 1) * 3);
+ const base = new THREE.Color(colorNum);
+ for (let i = 0; i <= seg; i++) {
+ const a = (i / seg) * Math.PI * 2;
+ pts.push(new THREE.Vector3(Math.cos(a) * R, Math.sin(a) * R, 0));
+ const frac = i / seg;
+ let d = Math.abs(frac); // distance from the head at frac 0
+ d = Math.min(d, 1 - d); // wrap around the loop -> 0..0.5
+ const intensity = Math.pow(Math.max(0, 1 - d * 2), 1.6);
+ const mul = (1 - headStrength) + headStrength * (0.18 + 0.82 * intensity);
+ colors[i * 3] = base.r * mul;
+ colors[i * 3 + 1] = base.g * mul;
+ colors[i * 3 + 2] = base.b * mul;
+ }
+ const geo = new THREE.BufferGeometry().setFromPoints(pts);
+ geo.setAttribute('color', new THREE.BufferAttribute(colors, 3));
+ const line = new THREE.Line(geo, new THREE.LineDashedMaterial({
+ vertexColors: true, transparent: true, opacity,
+ dashSize: 0.13, gapSize: 0.11
+ }));
+ line.computeLineDistances();
+ return line;
+ }
+
+ _makeLabelSprite(text, colorHex) {
+ const canvas = document.createElement('canvas');
+ canvas.width = 256; canvas.height = 40;
+ const ctx = canvas.getContext('2d');
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
+ ctx.fillStyle = 'rgba(6,14,10,0.62)';
+ ctx.fillRect(0, 8, canvas.width, 24);
+ ctx.strokeStyle = colorHex; ctx.lineWidth = 1;
+ ctx.strokeRect(0.5, 8.5, canvas.width - 1, 23);
+ ctx.fillStyle = colorHex;
+ ctx.font = 'bold 15px "Share Tech Mono", monospace';
+ ctx.shadowColor = colorHex; ctx.shadowBlur = 6;
+ ctx.fillText(String(text).slice(0, 26), 8, 26);
+ const sprite = new THREE.Sprite(new THREE.SpriteMaterial({
+ map: new THREE.CanvasTexture(canvas), transparent: true, depthTest: false, depthWrite: false
+ }));
+ sprite.scale.set(2.6, 0.4, 1);
+ return sprite;
+ }
+
+ // Small stacked "telemetry" readout that rides the orbit (GITS micro-text).
+ _makeMicroCluster(lines, colorHex) {
+ const canvas = document.createElement('canvas');
+ canvas.width = 128; canvas.height = 96;
+ const ctx = canvas.getContext('2d');
+ const texture = new THREE.CanvasTexture(canvas);
+ const sprite = new THREE.Sprite(new THREE.SpriteMaterial({
+ map: texture, transparent: true, depthTest: false, depthWrite: false,
+ opacity: 0.85
+ }));
+ sprite.scale.set(1.05, 0.8, 1);
+ const rec = {
+ sprite, canvas, ctx, texture,
+ lines: lines.slice(), colorHex,
+ seed: (Math.random() * 0xFFFF) | 0, nextUpdate: 0
+ };
+ this._drawCluster(rec, 0);
+ return rec;
+ }
+
+ // Redraw a cluster with a live-ticking hex telemetry line at the bottom.
+ _drawCluster(rec, tMs) {
+ const ctx = rec.ctx;
+ ctx.clearRect(0, 0, rec.canvas.width, rec.canvas.height);
+ ctx.font = '10px "Share Tech Mono", monospace';
+ ctx.textBaseline = 'top';
+ const live = '0x' + (((rec.seed ^ ((tMs / 110) | 0)) & 0xFFFF) >>> 0)
+ .toString(16).toUpperCase().padStart(4, '0');
+ const display = [rec.lines[0], rec.lines[1], rec.lines[2], live];
+ display.forEach((ln, i) => {
+ if (ln == null) return;
+ const y = 6 + i * 14;
+ ctx.fillStyle = rec.colorHex;
+ ctx.globalAlpha = 0.85;
+ ctx.fillRect(2, y + 5, 7, 1.5); // small leading tick, like the reference dashes
+ ctx.globalAlpha = i === 3 ? 0.7 : 1; // live line slightly dimmer
+ ctx.fillText(String(ln).slice(0, 14), 12, y);
+ });
+ rec.texture.needsUpdate = true;
+ }
+
+ createAttackOrbit(info, index, total) {
+ const group = new THREE.Group();
+ const rank = info.maxRank;
+ const accent = this._accentForRank(rank);
+
+ const h = this._hashStr(String(info.tactic || 'unmapped') + index);
+ // concentric bundle: nested radii around a shared centre
+ const R = 2.6 + index * 0.62;
+
+ // thin finely-dashed orbit line (no beads) — matches the reference's
+ // dashed arcs; severity tints the line colour, and a baked brightness
+ // head gives it a comet-like fade that sweeps as the ring spins
+ const ring = this._makeOrbitLine(R, accent.num, 0.85, 1);
+ group.add(ring);
+
+ // micro-text "telemetry" clusters riding the orbit (reference-style)
+ const clusters = [];
+ const samples = info.alerts.slice(0, 3);
+ const clusterCount = Math.min(3, Math.max(2, samples.length));
+ for (let i = 0; i < clusterCount; i++) {
+ const a = samples[i] || info.alerts[0] || {};
+ const ipTail = a.source_ip ? String(a.source_ip).split('.').slice(-2).join('.') : '--';
+ const lines = [
+ (a.technique || info.topTechnique || (info.tactic || 'ATK')),
+ ipTail,
+ `x${info.count}`
+ ];
+ const cl = this._makeMicroCluster(lines, accent.hex);
+ const ca = ((h + i * 137) % 360) * Math.PI / 180;
+ cl.sprite.position.set(Math.cos(ca) * R, Math.sin(ca) * R, 0);
+ group.add(cl.sprite);
+ clusters.push(cl);
+ }
+
+ // invisible torus = reliable raycast target for the whole ring
+ const hit = new THREE.Mesh(
+ new THREE.TorusGeometry(R, 0.4, 6, 48),
+ new THREE.MeshBasicMaterial({ transparent: true, opacity: 0, depthWrite: false })
+ );
+ hit.userData.isSiemRing = true;
+ hit.userData.siemGroup = info;
+ group.add(hit);
+
+ // concentric bundle: all rings share a centre, packed into a narrow
+ // height band; horizontal plane, spin about the vertical axis.
+ const n = Math.max(1, total || 1);
+ const centreY = 1.0;
+ const yPos = centreY + (index - (n - 1) / 2) * 0.5;
+ group.position.set(0, yPos, 0);
+ group.rotation.x = Math.PI / 2; // normal → world +Y (horizontal ring)
+ group.rotation.z = (h % 360) * Math.PI / 180; // start phase offset
+
+ const spin = (0.14 + ((h % 7) / 7) * 0.16) * ((index % 2) ? 1 : -1);
+
+ return {
+ group, ring, clusters, hit,
+ key: info.tactic || 'unmapped',
+ rank, R, spin, focusUntil: 0,
+ phase: (h % 100) / 100 * Math.PI * 2
+ };
+ }
+
+ _ensureSiemLayer() {
+ if (!this.scene) return null;
+ if (this.siemLayer && this.siemLayer.parent === this.scene) return this.siemLayer;
+ this.siemLayer = new THREE.Group();
+ this.scene.add(this.siemLayer);
+ return this.siemLayer;
+ }
+
+ _disposeSiemScars() {
+ if (this.siemLayer) {
+ const dispose = (o) => {
+ if (o.geometry) o.geometry.dispose();
+ if (o.material) {
+ const mats = Array.isArray(o.material) ? o.material : [o.material];
+ mats.forEach(m => {
+ // per-cluster canvas textures are unique -> free them; shared
+ // dot textures are cached and reused, so leave those.
+ if (m.map && m.map.image && m.map.image.width === 128 && m.map.image.height === 96) {
+ m.map.dispose();
+ }
+ m.dispose();
+ });
+ }
+ (o.children || []).forEach(dispose);
+ };
+ while (this.siemLayer.children.length) {
+ const c = this.siemLayer.children[0];
+ dispose(c);
+ this.siemLayer.remove(c);
+ }
+ }
+ this.siemScars = [];
+ this.scarById = {};
+ }
+
+ _groupAlertsByTactic(alerts) {
+ const groups = {};
+ alerts.forEach(a => {
+ const key = (a.tactic || 'unmapped');
+ if (!groups[key]) {
+ groups[key] = {
+ tactic: a.tactic || null, count: 0, posSum: 0,
+ maxRank: 0, sev: { critical: 0, high: 0, medium: 0, low: 0 },
+ techniques: {}, alerts: [], topRule: null
+ };
+ }
+ const g = groups[key];
+ g.count++;
+ g.posSum += (Number(a.position) || 0.5);
+ const rk = this._sevRank(a.severity);
+ if (rk > g.maxRank) g.maxRank = rk;
+ const sk = String(a.severity || 'medium').toLowerCase();
+ if (g.sev[sk] != null) g.sev[sk]++;
+ if (a.technique) g.techniques[a.technique] = (g.techniques[a.technique] || 0) + 1;
+ if (!g.topRule) g.topRule = a.rule;
+ g.alerts.push(a);
+ });
+ return Object.keys(groups).map(key => {
+ const g = groups[key];
+ const topTechnique = Object.keys(g.techniques).sort((x, y) => g.techniques[y] - g.techniques[x])[0] || null;
+ return {
+ tactic: g.tactic, count: g.count, position: g.posSum / g.count,
+ maxRank: g.maxRank, sev: g.sev, topTechnique, topRule: g.topRule,
+ alerts: g.alerts
+ };
+ });
+ }
+
+ renderSiemScars(payload) {
+ const alerts = (payload && Array.isArray(payload.alerts)) ? payload.alerts : this.siemAlerts;
+ if (!this.scene) return;
+ const layer = this._ensureSiemLayer();
+ if (!layer) return;
+ this._disposeSiemScars();
+ if (!alerts || !alerts.length) return;
+
+ const groups = this._groupAlertsByTactic(alerts)
+ .sort((a, b) => b.maxRank - a.maxRank || b.count - a.count);
+
+ groups.forEach((info, i) => {
+ const rec = this.createAttackOrbit(info, i, groups.length);
+ layer.add(rec.group);
+ this.siemScars.push(rec);
+ info.alerts.forEach(a => { if (a.id) this.scarById[a.id] = rec; });
+ });
+
+ // Decorative concentric echo-arcs: when there are few real orbits, pad
+ // the bundle with faint unlabelled rings so it reads like the reference's
+ // gyroscope of orbits (ambient structure, no data / no interaction).
+ const decorCount = Math.max(0, 6 - groups.length);
+ const outerR = 2.6 + groups.length * 0.62;
+ // teal-shifted green so the real (pure-green / severity) orbits pop
+ // against the ambient structure
+ const decorTint = 0x2f9f86;
+ for (let k = 0; k < decorCount; k++) {
+ const R = outerR + 0.55 * (k + 1);
+ // stagger base brightness for depth: some arcs sit "closer", some fade back
+ const baseOpacity = 0.09 + (k % 3) * 0.045;
+ const g = new THREE.Group();
+ const line = this._makeOrbitLine(R, decorTint, baseOpacity, 0);
+ g.add(line);
+ g.position.set(0, 1.0 + (k - decorCount / 2) * 0.45, 0);
+ // slight per-arc tilt off the horizontal plane -> volumetric gyroscope
+ const tilt = 0.12 * Math.sin(k * 1.7);
+ g.rotation.x = Math.PI / 2 + tilt;
+ g.rotation.y = 0.10 * Math.cos(k * 2.3);
+ g.rotation.z = k * 1.13;
+ layer.add(g);
+ this.siemScars.push({
+ group: g, ring: line, clusters: [], hit: null,
+ key: 'decor' + k, decor: true, rank: 0, R, baseOpacity,
+ spin: (0.07 + k * 0.015) * (k % 2 ? 1 : -1),
+ focusUntil: 0, phase: k * 0.9
+ });
+ }
+ }
+
+ _focusScar(id) {
+ const rec = this.scarById[id];
+ if (!rec) return;
+ rec.focusUntil = performance.now() + 2600;
+ }
+
async loadData() {
try {
const response = await fetch('/api/kernel-dna');
@@ -680,6 +1049,8 @@ class KernelDNAVisualization {
const rightHelix = this.createHelixStrand(false);
this.helixLeft = leftHelix.group;
this.helixRight = rightHelix.group;
+ this.leftHelixCurve = leftHelix.curve;
+ this.siemLayer = null; // recreated lazily; old one was disposed by clear()
// Restore rotation state to prevent jitter
this.helixLeft.rotation.y = savedLeftRotation;
@@ -737,6 +1108,11 @@ class KernelDNAVisualization {
this.mutations.push(mutationMesh);
});
}
+
+ // Re-attach SIEM attack scars from the last poll (helix was rebuilt).
+ if (this.siemAlerts && this.siemAlerts.length) {
+ this.renderSiemScars({ alerts: this.siemAlerts });
+ }
await this._hideLoadingOverlay();
@@ -1364,7 +1740,7 @@ class KernelDNAVisualization {
position: absolute;
top: 80px;
right: 20px;
- width: 300px;
+ width: 262px;
max-height: 500px;
background: rgba(12, 18, 28, 0.95);
border: 1px solid rgba(160, 170, 190, 0.35);
@@ -1572,6 +1948,7 @@ class KernelDNAVisualization {
âš Mutations = Anomalies
â—‡ RULE = threshold
â—‡ ML = baseline z-score
+ â—¯ ORBIT = SIEM attack (by tactic)
`);
legendDiv.style.cssText = `
@@ -1583,10 +1960,11 @@ class KernelDNAVisualization {
this.container.appendChild(legendDiv);
const driftEl = this._ensureDriftIndicator();
+ const threatEl = this._ensureThreatFeed();
await this.addProcessSelector();
const selectorEl = this.container.querySelector('.dna-process-selector');
- this._applyStaggeredReveal([titleDiv, devLabel, legendDiv, driftEl, selectorEl]);
+ this._applyStaggeredReveal([titleDiv, devLabel, legendDiv, driftEl, threatEl, selectorEl]);
}
/**
@@ -1605,45 +1983,20 @@ class KernelDNAVisualization {
bottom: 26px;
left: 20px;
z-index: 1001;
- width: 196px;
- padding: 11px 13px 12px;
+ width: 262px;
+ padding: 15px;
font-family: 'Share Tech Mono', monospace;
color: #c8ccd4;
cursor: default;
- background:
- linear-gradient(180deg, rgba(18,26,32,0.78), rgba(10,13,16,0.72));
- border: 1px solid rgba(103, 200, 224, 0.28);
- box-shadow: 0 0 18px rgba(103,200,224,0.08),
- inset 0 0 24px rgba(103,200,224,0.04);
- backdrop-filter: blur(3px);
+ background: rgba(12, 18, 28, 0.95);
+ border: 1px solid rgba(160, 170, 190, 0.35);
+ border-radius: 4px;
overflow: hidden;
- /* clipped "cut corner" -> instrument-panel silhouette */
- clip-path: polygon(0 0, calc(100% - 12px) 0, 100% 12px, 100% 100%, 12px 100%, 0 calc(100% - 12px));
}
- /* corner tick brackets */
+ /* unified with SELECT PROCESS: drop cut-corner ticks + scanline */
.dna-drift-indicator::before,
- .dna-drift-indicator::after {
- content: '';
- position: absolute;
- width: 9px; height: 9px;
- border: 1px solid rgba(103,200,224,0.65);
- pointer-events: none;
- }
- .dna-drift-indicator::before { top: 4px; left: 4px; border-width: 1px 0 0 1px; }
- .dna-drift-indicator::after { bottom: 4px; right: 4px; border-width: 0 1px 1px 0; }
- /* slow scanline sweep */
- .dna-drift-scan {
- position: absolute; left: 0; right: 0; top: 0; height: 28px;
- background: linear-gradient(180deg, rgba(103,200,224,0.10), rgba(103,200,224,0));
- pointer-events: none;
- animation: dna-drift-scan 4.6s linear infinite;
- }
- @keyframes dna-drift-scan {
- 0% { transform: translateY(-30px); opacity: 0; }
- 12% { opacity: 1; }
- 88% { opacity: 1; }
- 100% { transform: translateY(150px); opacity: 0; }
- }
+ .dna-drift-indicator::after { content: none; }
+ .dna-drift-scan { display: none; }
.dna-drift-head { display:flex; align-items:center; gap:7px; margin-bottom:8px; }
.dna-drift-led {
width:7px; height:7px; border-radius:50%;
@@ -1717,6 +2070,276 @@ class KernelDNAVisualization {
return `${Math.round(sec / 86400)}d ago`;
}
+ // ---- Live threat feed HUD (Elastic SIEM) -------------------------------
+
+ _ensureThreatStyles() {
+ if (this._threatStylesInjected || typeof document === 'undefined') return;
+ const style = document.createElement('style');
+ style.id = 'kernel-dna-threat-styles';
+ style.textContent = `
+ .dna-threat-feed {
+ position: absolute; top: 262px; left: 20px; z-index: 1001;
+ width: 292px; max-height: 46vh; display: flex; flex-direction: column;
+ font-family: 'Share Tech Mono', monospace; color: #c8ccd4;
+ background: rgba(12, 18, 28, 0.95);
+ border: 1px solid rgba(160, 170, 190, 0.35);
+ border-radius: 4px; overflow: hidden;
+ }
+ /* unified with SELECT PROCESS: no cut-corner / tick brackets */
+ .dna-threat-feed::before, .dna-threat-feed::after { content: none; }
+ .dna-threat-head { display:flex; align-items:center; gap:7px; padding:11px 13px 8px; }
+ .dna-threat-led {
+ width:7px; height:7px; border-radius:50%; background:#E0564E;
+ box-shadow:0 0 7px rgba(224,86,78,0.9); animation: dna-threat-led 1.5s ease-in-out infinite;
+ }
+ @keyframes dna-threat-led { 0%,100%{opacity:0.45;} 50%{opacity:1;} }
+ .dna-threat-title { font-size:10px; letter-spacing:1.2px; opacity:0.9; flex:1;
+ text-shadow:0 0 8px rgba(224,86,78,0.25); }
+ .dna-threat-counts { font-size:9px; opacity:0.85; padding:0 13px 8px; letter-spacing:0.4px; }
+ .dna-threat-counts b { color:#ff6a5c; }
+ .dna-threat-list { overflow-y:auto; padding:0 8px 10px; }
+ .dna-threat-list::-webkit-scrollbar { width:5px; }
+ .dna-threat-list::-webkit-scrollbar-thumb { background:rgba(224,86,78,0.3); border-radius:3px; }
+ .dna-threat-row {
+ display:flex; gap:7px; padding:6px 6px; margin:3px 0; cursor:pointer;
+ border-left:2px solid transparent; align-items:flex-start;
+ transition: background 0.15s, border-color 0.15s;
+ }
+ .dna-threat-row:hover, .dna-threat-row.active {
+ background: rgba(224,86,78,0.10); border-left-color:#e0564e;
+ }
+ .dna-threat-dot { width:8px; height:8px; border-radius:50%; margin-top:3px; flex:0 0 auto; }
+ .dna-threat-body { flex:1; min-width:0; }
+ .dna-threat-rule { font-size:10.5px; line-height:1.25; color:#dbe0e6;
+ white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
+ .dna-threat-meta { font-size:9px; opacity:0.7; margin-top:2px;
+ white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
+ .dna-threat-meta .tech { color:#e6c15a; }
+ .dna-threat-count {
+ flex:0 0 auto; align-self:center; margin-left:4px; padding:0 5px;
+ font-size:9px; line-height:15px; border-radius:8px; color:#0c121c;
+ font-weight:700; background:#e0564e;
+ }
+ .dna-threat-caret { flex:0 0 auto; align-self:center; font-size:9px; opacity:0.55;
+ transition: transform 0.15s; margin-left:2px; }
+ .dna-threat-row.expanded .dna-threat-caret { transform: rotate(90deg); opacity:0.9; }
+ .dna-threat-sub { display:none; margin:0 0 4px 17px;
+ border-left:1px solid rgba(160,170,190,0.18); padding-left:8px; }
+ .dna-threat-subrow {
+ display:flex; justify-content:space-between; gap:8px; padding:3px 4px;
+ font-size:9px; cursor:pointer; border-radius:2px;
+ }
+ .dna-threat-subrow:hover { background: rgba(224,86,78,0.10); }
+ .dna-threat-subrow .p { color:#c3c9d2; white-space:nowrap; overflow:hidden;
+ text-overflow:ellipsis; font-family:"Share Tech Mono", monospace; }
+ .dna-threat-subrow .t { opacity:0.55; flex:0 0 auto; }
+ .dna-threat-empty { font-size:10px; opacity:0.6; padding:12px 14px; }
+ `;
+ document.head.appendChild(style);
+ this._threatStylesInjected = true;
+ }
+
+ _ensureThreatFeed() {
+ let panel = this.container.querySelector('.dna-threat-feed');
+ if (panel) return panel;
+ this._ensureThreatStyles();
+ panel = document.createElement('div');
+ panel.className = 'dna-threat-feed';
+ window.setSafeHtml(panel, `
+
+
+ LIVE THREAT FEED
+
+ …
+
+ `);
+ this.container.appendChild(panel);
+ this._startSiemPolling();
+ return panel;
+ }
+
+ _startSiemPolling() {
+ if (this.siemInterval) return;
+ const poll = async () => {
+ if (!this.isActive) return;
+ try {
+ const resp = await fetch('/api/siem-alerts?hours=24&limit=300');
+ const payload = await resp.json();
+ this.siemAlerts = (payload && Array.isArray(payload.alerts)) ? payload.alerts : [];
+ this._renderThreatFeed(payload);
+ this.renderSiemScars(payload);
+ } catch (e) {
+ this._renderThreatFeed({ available: false });
+ }
+ };
+ poll();
+ this.siemInterval = setInterval(poll, 20000);
+ }
+
+ _relTime(iso) {
+ if (!iso) return '';
+ const then = Date.parse(iso);
+ if (isNaN(then)) return '';
+ const sec = Math.max(0, (Date.now() - then) / 1000);
+ return this._fmtAge(sec);
+ }
+
+ _renderThreatFeed(payload) {
+ const panel = this.container && this.container.querySelector('.dna-threat-feed');
+ if (!panel) return;
+ const counts = panel.querySelector('.dna-threat-counts');
+ const list = panel.querySelector('.dna-threat-list');
+ if (!list) return;
+
+ if (!payload || payload.available === false) {
+ if (counts) counts.textContent = 'SIEM offline';
+ while (list.firstChild) list.removeChild(list.firstChild);
+ const empty = document.createElement('div');
+ empty.className = 'dna-threat-empty';
+ empty.textContent = 'SIEM unreachable';
+ list.appendChild(empty);
+ return;
+ }
+
+ const bysev = payload.by_severity || {};
+ if (counts) {
+ const total = payload.total != null ? payload.total : (payload.count || 0);
+ window.setSafeHtml(counts,
+ `24h: ${total} alerts · ${bysev.critical || 0} crit · ${bysev.high || 0} high · ${bysev.medium || 0} med`);
+ }
+
+ const alerts = Array.isArray(payload.alerts) ? payload.alerts : [];
+ while (list.firstChild) list.removeChild(list.firstChild);
+ if (!alerts.length) {
+ const empty = document.createElement('div');
+ empty.className = 'dna-threat-empty';
+ empty.textContent = 'no attacks in window';
+ list.appendChild(empty);
+ return;
+ }
+
+ // Group by (source_ip + rule): a single scanner hitting 130 dotfile
+ // variants collapses into one line with an ×N count, expandable to show
+ // the individual paths that differ.
+ const groups = this._groupFeed(alerts);
+
+ groups.forEach(g => {
+ const row = document.createElement('div');
+ row.className = 'dna-threat-row';
+ row.dataset.id = g.repId || '';
+
+ const dot = document.createElement('div');
+ dot.className = 'dna-threat-dot';
+ dot.style.background = this._sevColorHex(g.severity);
+ dot.style.boxShadow = `0 0 6px ${this._sevColorHex(g.severity)}`;
+
+ const body = document.createElement('div');
+ body.className = 'dna-threat-body';
+
+ const rule = document.createElement('div');
+ rule.className = 'dna-threat-rule';
+ rule.textContent = g.rule;
+
+ const meta = document.createElement('div');
+ meta.className = 'dna-threat-meta';
+ const tech = g.technique ? `${g.technique} · ` : '';
+ const rel = this._relTime(g.latest);
+ const np = g.paths ? g.paths.size : 0;
+ const paths = np > 1 ? ` · ${np} paths` : '';
+ window.setSafeHtml(meta, `${tech}${g.ip}${rel ? ' · ' + rel : ''}${paths}`);
+
+ body.appendChild(rule);
+ body.appendChild(meta);
+ row.appendChild(dot);
+ row.appendChild(body);
+
+ if (g.count > 1) {
+ const badge = document.createElement('span');
+ badge.className = 'dna-threat-count';
+ badge.textContent = `×${g.count}`;
+ badge.style.background = this._sevColorHex(g.severity);
+ row.appendChild(badge);
+ const caret = document.createElement('span');
+ caret.className = 'dna-threat-caret';
+ caret.textContent = 'â–¸';
+ row.appendChild(caret);
+ }
+ list.appendChild(row);
+
+ // expandable sublist of the individual (differing) paths / hits
+ let sub = null;
+ if (g.count > 1) {
+ sub = document.createElement('div');
+ sub.className = 'dna-threat-sub';
+ g.items.forEach(a => {
+ const sr = document.createElement('div');
+ sr.className = 'dna-threat-subrow';
+ sr.dataset.id = a.id || '';
+ const path = a.url_path || a.url_query || '(no path)';
+ const srel = this._relTime(a.time);
+ window.setSafeHtml(sr,
+ `${String(path).slice(0, 42)}${srel ? ' ' + srel + '' : ''}`);
+ sr.addEventListener('click', ev => {
+ ev.stopPropagation();
+ this._focusScar(a.id);
+ });
+ sr.addEventListener('mouseenter', () => { this.hoveredScarId = a.id; });
+ sr.addEventListener('mouseleave', () => { if (this.hoveredScarId === a.id) this.hoveredScarId = null; });
+ sub.appendChild(sr);
+ });
+ list.appendChild(sub);
+ }
+
+ row.addEventListener('click', () => {
+ this._focusScar(g.repId);
+ panel.querySelectorAll('.dna-threat-row.active').forEach(r => r.classList.remove('active'));
+ row.classList.add('active');
+ if (sub) {
+ const open = row.classList.toggle('expanded');
+ sub.style.display = open ? 'block' : 'none';
+ }
+ });
+ row.addEventListener('mouseenter', () => { this.hoveredScarId = g.repId; });
+ row.addEventListener('mouseleave', () => { if (this.hoveredScarId === g.repId) this.hoveredScarId = null; });
+ });
+ }
+
+ // Collapse alerts by (source_ip + rule) into ranked groups for the feed.
+ _groupFeed(alerts) {
+ const map = new Map();
+ alerts.forEach(a => {
+ const key = `${a.source_ip || '—'}||${a.rule || 'web attack'}`;
+ let g = map.get(key);
+ if (!g) {
+ g = {
+ key, rule: a.rule || 'web attack', ip: a.source_ip || '—',
+ technique: a.technique || null, severity: a.severity || 'medium',
+ maxRank: this._sevRank(a.severity), count: 0, latest: a.time,
+ repId: a.id || '', items: [], paths: new Set()
+ };
+ map.set(key, g);
+ }
+ g.count += 1;
+ g.items.push(a);
+ if (a.url_path) g.paths.add(a.url_path);
+ if (!g.technique && a.technique) g.technique = a.technique;
+ const rk = this._sevRank(a.severity);
+ if (rk > g.maxRank) { g.maxRank = rk; g.severity = a.severity; g.repId = a.id || g.repId; }
+ if (a.time && (!g.latest || Date.parse(a.time) > Date.parse(g.latest))) g.latest = a.time;
+ });
+
+ const groups = Array.from(map.values());
+ groups.forEach(g => {
+ g.distinctPaths = g.paths.size || g.count;
+ g.items.sort((x, y) => Date.parse(y.time || 0) - Date.parse(x.time || 0));
+ });
+ groups.sort((a, b) =>
+ b.maxRank - a.maxRank ||
+ b.count - a.count ||
+ Date.parse(b.latest || 0) - Date.parse(a.latest || 0));
+ return groups;
+ }
+
_renderDriftIndicator(payload) {
const panel = this.container && this.container.querySelector('.dna-drift-indicator');
if (!panel) return;
@@ -1862,6 +2485,40 @@ class KernelDNAVisualization {
}
});
});
+
+ // Animate SIEM attack orbits: rings spin (gyroscope), the line gently
+ // breathes, and the focused/hovered orbit brightens.
+ if (this.siemScars && this.siemScars.length) {
+ const now = currentTime * 0.001;
+ this.siemScars.forEach(rec => {
+ if (rec.group) rec.group.rotation.z += rec.spin * deltaTime;
+ const focused = (rec.focusUntil && currentTime < rec.focusUntil)
+ || (this.hoveredScarId && (this.hoveredScarId === rec.key || this.scarById[this.hoveredScarId] === rec));
+ if (rec.ring) {
+ if (rec.decor) {
+ // faint ambient arcs: breathe around their staggered depth level
+ const b = rec.baseOpacity != null ? rec.baseOpacity : 0.11;
+ rec.ring.material.opacity = b + Math.sin(now * 0.8 + rec.phase) * 0.03;
+ } else {
+ // real orbits: brightness scales with severity, loud ones pulse
+ let base = 0.60 + rec.rank * 0.07; // low..crit -> ~.67/.74/.81/.88
+ if (rec.rank >= 3) base += Math.sin(now * 3 + rec.phase) * 0.12;
+ else base += Math.sin(now * 1.4 + rec.phase) * 0.08;
+ rec.ring.material.opacity = focused ? 1 : Math.max(0.4, Math.min(1, base));
+ }
+ }
+ if (rec.clusters && rec.clusters.length) {
+ const co = focused ? 1 : 0.82 + Math.sin(now * 1.2 + rec.phase) * 0.1;
+ rec.clusters.forEach(cl => {
+ if (currentTime > cl.nextUpdate) {
+ this._drawCluster(cl, currentTime);
+ cl.nextUpdate = currentTime + 300 + Math.random() * 260;
+ }
+ cl.sprite.material.opacity = co;
+ });
+ }
+ });
+ }
// Rotate camera around helix - smooth camera movement
// In timeline mode, camera looks from side to see growth
@@ -1921,6 +2578,10 @@ class KernelDNAVisualization {
this.genes = [];
this.mutations = [];
this.mutationAnimations = [];
+ // Scars live under helixLeft and were disposed above; drop stale refs.
+ this.siemScars = [];
+ this.scarById = {};
+ this.siemLayer = null;
this.helixLeft = null;
this.helixRight = null;
@@ -2034,6 +2695,16 @@ class KernelDNAVisualization {
}
const driftPanel = this.container && this.container.querySelector('.dna-drift-indicator');
if (driftPanel && driftPanel.parentNode) driftPanel.parentNode.removeChild(driftPanel);
+
+ if (this.siemInterval) {
+ clearInterval(this.siemInterval);
+ this.siemInterval = null;
+ }
+ const threatPanel = this.container && this.container.querySelector('.dna-threat-feed');
+ if (threatPanel && threatPanel.parentNode) threatPanel.parentNode.removeChild(threatPanel);
+ this.siemScars = [];
+ this.scarById = {};
+ this.hoveredScarId = null;
// Explicitly remove exit button before hiding container
if (this.exitButton && this.exitButton.parentNode) {
@@ -2132,6 +2803,13 @@ class KernelDNAVisualization {
allInteractiveObjects.push(mutation);
});
}
+
+ // Add SIEM attack orbits (invisible torus carries the tooltip payload)
+ if (this.siemScars && this.siemScars.length > 0) {
+ this.siemScars.forEach(rec => {
+ if (rec.hit) allInteractiveObjects.push(rec.hit);
+ });
+ }
// Add helix strands (for timeline markers, etc.)
if (this.helixLeft && this.helixLeft.children) {
@@ -2166,7 +2844,7 @@ class KernelDNAVisualization {
// Traverse up the parent chain to find object with userData
let current = obj;
while (current) {
- if (current.userData && (current.userData.name || current.userData.code || current.userData.event || current.userData.isNucleotide)) {
+ if (current.userData && (current.userData.name || current.userData.code || current.userData.event || current.userData.isNucleotide || current.userData.isSiemScar || current.userData.isSiemRing || current.userData.mutationType)) {
targetObject = current;
break;
}
@@ -2191,6 +2869,7 @@ class KernelDNAVisualization {
// Format tooltip content based on object type
let tooltipContent = '';
+ this.hoveredScarId = null; // re-set below only when a scar is hovered
// Check if this is a timeline event
if (userData.event && userData.timestamp) {
@@ -2216,6 +2895,49 @@ class KernelDNAVisualization {
Subsystem: ${userData.subsystem || 'kernel'}
`;
+ } else if (userData.isSiemRing && userData.siemGroup) {
+ // Attack orbit = one MITRE tactic. Summary; per-alert detail in feed.
+ const g = userData.siemGroup;
+ const acc = this._accentForRank(g.maxRank);
+ this.hoveredScarId = g.tactic || 'unmapped';
+ const feedPanel = this.container.querySelector('.dna-threat-feed');
+ if (feedPanel) feedPanel.querySelectorAll('.dna-threat-row.active').forEach(r => r.classList.remove('active'));
+ const sevParts = ['critical', 'high', 'medium', 'low']
+ .filter(k => g.sev[k]).map(k => `${g.sev[k]} ${k}`).join(' · ');
+ const tech = g.topTechnique ? `technique: ${g.topTechnique}
` : '';
+ tooltipContent = `
+ ⦿ ${(g.tactic || 'UNMAPPED').toUpperCase()}
+ ${g.count} attack${g.count > 1 ? 's' : ''} on this orbit
+ ${sevParts || '—'}
+ ${tech}
+ e.g. ${g.topRule || 'web attack'}
+ Elastic SIEM · see feed for detail
+ `;
+ } else if (userData.mutationSource === 'siem' && userData.siem) {
+ // Real Elastic SIEM detection alert (attack "scar").
+ const a = userData.siem;
+ const headColor = this._sevColorHex(a.severity);
+ const sev = String(a.severity || 'medium').toUpperCase();
+ this.hoveredScarId = a.id;
+ const feedPanel = this.container.querySelector('.dna-threat-feed');
+ if (feedPanel) {
+ feedPanel.querySelectorAll('.dna-threat-row.active').forEach(r => r.classList.remove('active'));
+ const r = feedPanel.querySelector(`.dna-threat-row[data-id="${(a.id || '').replace(/"/g, '')}"]`);
+ if (r) r.classList.add('active');
+ }
+ const tactic = a.tactic ? `ATT&CK: ${a.tactic}${a.technique ? ' · ' + a.technique : ''}
` : '';
+ const req = (a.method || a.url_path)
+ ? `${a.method || ''} ${a.url_path || ''}
` : '';
+ const q = a.url_query
+ ? `?${a.url_query}
` : '';
+ const rel = this._relTime(a.time);
+ tooltipContent = `
+ ⚠ATTACK — ${sev}
+ ${a.rule || 'web attack'}
+ src ${a.source_ip || '—'}${rel ? ' · ' + rel : ''}
+ ${tactic}${req}${q}
+ Elastic SIEM detection
+ `;
} else if (userData.mutationType) {
// Mutation - source-coloured header (ML cyan, rule yellow).
const isML = userData.mutationSource === 'ml';
@@ -2255,6 +2977,7 @@ class KernelDNAVisualization {
this.hoveredNucleotide.userData.ring.visible = false;
}
this.hoveredNucleotide = null;
+ this.hoveredScarId = null;
}
}
diff --git a/static/js/right-semicircle-menu.js b/static/js/right-semicircle-menu.js
index 3673f26..8a2b463 100644
--- a/static/js/right-semicircle-menu.js
+++ b/static/js/right-semicircle-menu.js
@@ -104,15 +104,21 @@ class RightSemicircleMenuManager {
window.location.assign('/linux-network-subsystem');
return;
}
- if (!window.kernelContextMenu) return;
+ if (itemId === 'files') {
+ // Force hard navigation to dedicated SEO page (same as the other
+ // subsystems), instead of opening the in-page overlay view.
+ window.location.assign('/linux-filesystem-subsystem');
+ return;
+ }
if (itemId === 'kernel') {
- window.kernelContextMenu.activateDNAView();
+ window.location.assign('/kernel-dna');
+ return;
+ }
+ if (itemId === 'devices') {
+ window.location.assign('/linux-devices-subsystem');
return;
- } else if (itemId === 'devices') {
- window.kernelContextMenu.activateDevicesView();
- } else if (itemId === 'files') {
- window.kernelContextMenu.activateFilesView();
}
+ if (!window.kernelContextMenu) return;
}
renderRightSemicircleMenu() {