fix(web): attack-graph canvas follows the app theme instead of forcing dark

Node/edge colors now use the same --sev-*-fg / --surface / --text / --border
CSS custom properties as the rest of the console (set via inline style=
attributes, since SVG presentation attributes don't resolve var()) — the
graph reads correctly in light mode instead of always rendering as a
dark canvas.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0129WdYHccPsH27k5GGuwijd
This commit is contained in:
CyberSecurityUP
2026-08-23 15:33:48 -03:00
co-authored by Claude Sonnet 5
parent 7f365b4e88
commit a083990ce4
2 changed files with 14 additions and 20 deletions
+12 -14
View File
@@ -595,11 +595,9 @@ $('#findingModal').addEventListener('click', (e) => { if (e.target.id === 'findi
const KILL_CHAIN_STAGES = ['recon', 'initial-access', 'execution', 'privesc', 'lateral', 'exfil', 'impact'];
// Bright, saturated palette for the dark graph canvas — the severity chip
// colors elsewhere are tuned for text-on-light-background legibility and
// read as muddy on a dark node graph.
const CANVAS_SEV_COLOR = { critical: '#ff6b5b', high: '#ffab52', medium: '#f0cf5c', low: '#7fd99a', info: '#8fa3ef' };
function canvasColor(sev) { return CANVAS_SEV_COLOR[['critical', 'high', 'medium', 'low', 'info'][sevRank(sev)]]; }
// Same severity tokens the rest of the console uses — the graph canvas
// follows the light/dark theme instead of a fixed dark palette.
function canvasColor(sev) { return `var(--sev-${['critical', 'high', 'medium', 'low', 'info'][sevRank(sev)]}-fg)`; }
function nodeIcon(f) {
const t = `${f.title} ${f.evidence} ${f.cwe} ${f.stage}`.toLowerCase();
@@ -671,20 +669,20 @@ function renderAttackPath(container, findings, target) {
const nodeSvg = (n) => {
if (n.root) {
return `<g>
<circle cx="${n.x}" cy="${n.y}" r="15" fill="#c0392b" stroke="#ff6b5b" stroke-width="2"/>
<text x="${n.x}" y="${n.y + 4}" text-anchor="middle" font-size="13" fill="#fff">🎯</text>
<text x="${n.x}" y="${n.y + 30}" text-anchor="middle" font-size="10.5" fill="#c9c6bf" font-family="var(--mono)">${esc(trimMid(n.label, 26))}</text>
<circle cx="${n.x}" cy="${n.y}" r="15" style="fill:var(--accent);stroke:var(--accent-hover);" stroke-width="2"/>
<text x="${n.x}" y="${n.y + 4}" text-anchor="middle" font-size="13" style="fill:var(--accent-contrast);">🎯</text>
<text x="${n.x}" y="${n.y + 30}" text-anchor="middle" font-size="10.5" style="fill:var(--text-dim);" font-family="var(--mono)">${esc(trimMid(n.label, 26))}</text>
</g>`;
}
const f = n.finding;
const color = canvasColor(f.severity);
const x = n.x - NODE_W / 2, y = n.y - NODE_H / 2;
return `<g class="ap-node-g" data-idx="${esc(findings.indexOf(f))}" style="cursor:pointer;">
<rect x="${x}" y="${y}" width="${NODE_W}" height="${NODE_H}" rx="8" fill="#181a20" stroke="${color}" stroke-width="1.6"/>
<rect x="${x}" y="${y}" width="${NODE_W}" height="${NODE_H}" rx="8" style="fill:var(--surface);stroke:${color};" stroke-width="1.6"/>
<text x="${x + 12}" y="${y + 20}" font-size="13">${nodeIcon(f)}</text>
<text x="${x + 32}" y="${y + 19}" font-size="11.5" fill="#e8e6e0" font-weight="600">${esc(trimMid(f.title, 22))}</text>
<text x="${x + 32}" y="${y + 36}" font-size="10" fill="#8b8880" font-family="var(--mono)">${esc((f.mitre || f.owasp || f.cwe || n.stageLabel || '').slice(0, 26))}</text>
<rect x="${x + NODE_W - 9}" y="${y + 6}" width="6" height="6" rx="1.5" fill="${color}"/>
<text x="${x + 32}" y="${y + 19}" font-size="11.5" style="fill:var(--text);" font-weight="600">${esc(trimMid(f.title, 22))}</text>
<text x="${x + 32}" y="${y + 36}" font-size="10" style="fill:var(--text-faint);" font-family="var(--mono)">${esc((f.mitre || f.owasp || f.cwe || n.stageLabel || '').slice(0, 26))}</text>
<rect x="${x + NODE_W - 9}" y="${y + 6}" width="6" height="6" rx="1.5" style="fill:${color};"/>
</g>`;
};
@@ -692,8 +690,8 @@ function renderAttackPath(container, findings, target) {
${!hasStages ? '<div class="field-help" style="margin-bottom:8px;">No kill-chain stage data yet — shown as a flat graph from the target.</div>' : ''}
<div class="ap-canvas-wrap">
<svg class="ap-canvas" viewBox="0 0 ${width} ${height}" width="${width}" height="${height}">
${groups.map((g, ci) => `<line x1="${PAD + NODE_W / 2 + (ci + 1) * COL_W - COL_W / 2}" y1="0" x2="${PAD + NODE_W / 2 + (ci + 1) * COL_W - COL_W / 2}" y2="${height}" stroke="#26282f" stroke-width="1"/>`).join('')}
${edges.map(([a, b]) => `<path d="${edgePath(a, b)}" fill="none" stroke="#3a3d47" stroke-width="1.5"/>`).join('')}
${groups.map((g, ci) => `<line x1="${PAD + NODE_W / 2 + (ci + 1) * COL_W - COL_W / 2}" y1="0" x2="${PAD + NODE_W / 2 + (ci + 1) * COL_W - COL_W / 2}" y2="${height}" style="stroke:var(--border);" stroke-width="1"/>`).join('')}
${edges.map(([a, b]) => `<path d="${edgePath(a, b)}" fill="none" style="stroke:var(--border-strong);" stroke-width="1.5"/>`).join('')}
${nodes.map(nodeSvg).join('')}
</svg>
</div>
+2 -6
View File
@@ -311,14 +311,10 @@ textarea { resize: vertical; min-height: 72px; }
/* Generative Attack Path Chaining */
.attackpath-empty { font-size: 12.5px; color: var(--text-faint); padding: var(--sp-5); text-align: center; border: 1px dashed var(--border-strong); border-radius: var(--radius-sm); }
/* The graph canvas is intentionally fixed-dark regardless of the app theme
a node/edge map reads better with bright severity colors against a near-
black surface, the way NodeZero/attack-graph tools render it, and it stays
legible whether the rest of the console is in light or dark mode. */
.ap-canvas-wrap { border-radius: var(--radius-md); overflow: auto; background: #0f1115; border: 1px solid #24262d; }
.ap-canvas-wrap { border-radius: var(--radius-md); overflow: auto; background: var(--surface-2); border: 1px solid var(--border); }
.ap-canvas { display: block; min-width: 100%; }
.ap-canvas text { font-family: var(--sans); }
.ap-node-g:hover rect:first-child { filter: brightness(1.35); }
.ap-node-g:hover rect:first-child { filter: brightness(0.97); }
/* findings table */
.data-table { width: 100%; border-collapse: collapse; font-size: 12.5px; }