guardlink 1.4.2 → 1.4.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +83 -9
- package/README.md +38 -1
- package/dist/agents/config.d.ts +7 -0
- package/dist/agents/config.d.ts.map +1 -1
- package/dist/agents/config.js.map +1 -1
- package/dist/agents/index.d.ts +1 -1
- package/dist/agents/index.d.ts.map +1 -1
- package/dist/agents/index.js +1 -1
- package/dist/agents/index.js.map +1 -1
- package/dist/agents/prompts.d.ts +14 -0
- package/dist/agents/prompts.d.ts.map +1 -1
- package/dist/agents/prompts.js +445 -2
- package/dist/agents/prompts.js.map +1 -1
- package/dist/analyze/format.d.ts +72 -0
- package/dist/analyze/format.d.ts.map +1 -0
- package/dist/analyze/format.js +176 -0
- package/dist/analyze/format.js.map +1 -0
- package/dist/analyze/index.d.ts +76 -0
- package/dist/analyze/index.d.ts.map +1 -1
- package/dist/analyze/index.js +165 -2
- package/dist/analyze/index.js.map +1 -1
- package/dist/analyze/prompts.d.ts +3 -2
- package/dist/analyze/prompts.d.ts.map +1 -1
- package/dist/analyze/prompts.js +16 -2
- package/dist/analyze/prompts.js.map +1 -1
- package/dist/analyzer/sarif.d.ts +3 -2
- package/dist/analyzer/sarif.d.ts.map +1 -1
- package/dist/analyzer/sarif.js +29 -3
- package/dist/analyzer/sarif.js.map +1 -1
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +380 -28
- package/dist/cli/index.js.map +1 -1
- package/dist/dashboard/data.d.ts +11 -0
- package/dist/dashboard/data.d.ts.map +1 -1
- package/dist/dashboard/data.js +12 -0
- package/dist/dashboard/data.js.map +1 -1
- package/dist/dashboard/diagrams.d.ts +81 -12
- package/dist/dashboard/diagrams.d.ts.map +1 -1
- package/dist/dashboard/diagrams.js +750 -362
- package/dist/dashboard/diagrams.js.map +1 -1
- package/dist/dashboard/generate.d.ts +5 -2
- package/dist/dashboard/generate.d.ts.map +1 -1
- package/dist/dashboard/generate.js +2516 -244
- package/dist/dashboard/generate.js.map +1 -1
- package/dist/diff/engine.d.ts +2 -1
- package/dist/diff/engine.d.ts.map +1 -1
- package/dist/diff/engine.js +3 -2
- package/dist/diff/engine.js.map +1 -1
- package/dist/init/index.d.ts.map +1 -1
- package/dist/init/index.js +24 -5
- package/dist/init/index.js.map +1 -1
- package/dist/init/migrate.d.ts +39 -0
- package/dist/init/migrate.d.ts.map +1 -0
- package/dist/init/migrate.js +45 -0
- package/dist/init/migrate.js.map +1 -0
- package/dist/init/templates.d.ts +8 -0
- package/dist/init/templates.d.ts.map +1 -1
- package/dist/init/templates.js +71 -9
- package/dist/init/templates.js.map +1 -1
- package/dist/mcp/lookup.d.ts +1 -0
- package/dist/mcp/lookup.d.ts.map +1 -1
- package/dist/mcp/lookup.js +138 -10
- package/dist/mcp/lookup.js.map +1 -1
- package/dist/mcp/server.d.ts +2 -1
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +20 -8
- package/dist/mcp/server.js.map +1 -1
- package/dist/parser/clear.js +1 -1
- package/dist/parser/clear.js.map +1 -1
- package/dist/parser/feature-filter.d.ts +42 -0
- package/dist/parser/feature-filter.d.ts.map +1 -0
- package/dist/parser/feature-filter.js +109 -0
- package/dist/parser/feature-filter.js.map +1 -0
- package/dist/parser/format.d.ts +24 -0
- package/dist/parser/format.d.ts.map +1 -0
- package/dist/parser/format.js +29 -0
- package/dist/parser/format.js.map +1 -0
- package/dist/parser/index.d.ts +2 -0
- package/dist/parser/index.d.ts.map +1 -1
- package/dist/parser/index.js +1 -0
- package/dist/parser/index.js.map +1 -1
- package/dist/parser/parse-file.d.ts.map +1 -1
- package/dist/parser/parse-file.js +3 -1
- package/dist/parser/parse-file.js.map +1 -1
- package/dist/parser/parse-line.d.ts +3 -0
- package/dist/parser/parse-line.d.ts.map +1 -1
- package/dist/parser/parse-line.js +78 -22
- package/dist/parser/parse-line.js.map +1 -1
- package/dist/parser/parse-project.js +19 -0
- package/dist/parser/parse-project.js.map +1 -1
- package/dist/parser/validate.d.ts +3 -0
- package/dist/parser/validate.d.ts.map +1 -1
- package/dist/parser/validate.js +7 -0
- package/dist/parser/validate.js.map +1 -1
- package/dist/report/index.d.ts +1 -0
- package/dist/report/index.d.ts.map +1 -1
- package/dist/report/index.js +1 -0
- package/dist/report/index.js.map +1 -1
- package/dist/report/report.d.ts.map +1 -1
- package/dist/report/report.js +924 -24
- package/dist/report/report.js.map +1 -1
- package/dist/report/sequence.d.ts +11 -0
- package/dist/report/sequence.d.ts.map +1 -0
- package/dist/report/sequence.js +140 -0
- package/dist/report/sequence.js.map +1 -0
- package/dist/tui/commands.d.ts +1 -0
- package/dist/tui/commands.d.ts.map +1 -1
- package/dist/tui/commands.js +83 -4
- package/dist/tui/commands.js.map +1 -1
- package/dist/tui/index.d.ts.map +1 -1
- package/dist/tui/index.js +7 -2
- package/dist/tui/index.js.map +1 -1
- package/dist/types/index.d.ts +57 -3
- package/dist/types/index.d.ts.map +1 -1
- package/dist/workspace/merge.d.ts.map +1 -1
- package/dist/workspace/merge.js +6 -2
- package/dist/workspace/merge.js.map +1 -1
- package/package.json +1 -1
|
@@ -10,33 +10,44 @@
|
|
|
10
10
|
* @exposes #dashboard to #path-traversal [medium] cwe:CWE-22 -- "readFileSync reads code files for annotation context"
|
|
11
11
|
* @mitigates #dashboard against #path-traversal using #path-validation -- "resolve() with root constrains file access"
|
|
12
12
|
* @flows ThreatModel -> #dashboard via computeStats -- "Model statistics input"
|
|
13
|
+
* @flows ThreatModel -> #dashboard via topologyData -- "Serialized diagram graph consumed by client-side D3 renderer"
|
|
13
14
|
* @flows SourceFiles -> #dashboard via readFileSync -- "Code snippet reads"
|
|
14
15
|
* @flows #dashboard -> HTML via return -- "Generated HTML output"
|
|
16
|
+
* @mitigates #dashboard against #xss using #output-encoding -- "Serialized diagram data escapes closing script tags; D3 writes labels as text"
|
|
15
17
|
* @handles internal on #dashboard -- "Processes and displays threat model data"
|
|
18
|
+
* @feature "Dashboard" -- "Interactive HTML threat model dashboard"
|
|
16
19
|
*/
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
20
|
+
import { listFeatures } from '../parser/feature-filter.js';
|
|
21
|
+
import { computeStats, computeSeverity, computeExposures, computeConfirmed, computeAssetHeatmap } from './data.js';
|
|
22
|
+
import { generateThreatGraph, generateDataFlowDiagram, generateAttackSurface, generateTopologyData } from './diagrams.js';
|
|
23
|
+
import { formatConfidence } from '../analyze/format.js';
|
|
19
24
|
import { readFileSync } from 'fs';
|
|
20
25
|
import { resolve, isAbsolute } from 'path';
|
|
21
|
-
export function generateDashboardHTML(model, root, analyses) {
|
|
26
|
+
export function generateDashboardHTML(model, root, analyses, pentestData) {
|
|
22
27
|
const stats = computeStats(model);
|
|
23
28
|
const severity = computeSeverity(model);
|
|
24
29
|
const exposures = computeExposures(model);
|
|
30
|
+
const confirmedRows = computeConfirmed(model);
|
|
25
31
|
const heatmap = computeAssetHeatmap(model);
|
|
26
32
|
const threatGraph = generateThreatGraph(model);
|
|
33
|
+
const threatGraphFull = generateThreatGraph(model, { showAll: true });
|
|
27
34
|
const dataFlow = generateDataFlowDiagram(model);
|
|
28
35
|
const attackSurface = generateAttackSurface(model);
|
|
36
|
+
const topology = generateTopologyData(model);
|
|
37
|
+
const featureNames = listFeatures(model);
|
|
29
38
|
const timestamp = new Date().toISOString().slice(0, 19).replace('T', ' ');
|
|
30
39
|
const unmitigated = exposures.filter(e => !e.mitigated && !e.accepted);
|
|
31
40
|
const mitigatedCount = exposures.filter(e => e.mitigated).length;
|
|
32
41
|
const mitigationCoveragePercent = exposures.length > 0
|
|
33
42
|
? Math.round((mitigatedCount / exposures.length) * 100)
|
|
34
43
|
: 0;
|
|
35
|
-
const riskScore = computeRiskGrade(severity, unmitigated.length, exposures.length);
|
|
44
|
+
const riskScore = computeRiskGrade(severity, unmitigated.length, exposures.length, confirmedRows.length);
|
|
36
45
|
// Build file annotations data for code browser + drawer
|
|
37
46
|
const fileAnnotations = buildFileAnnotations(model, root);
|
|
38
47
|
// Build analysis data for drawer
|
|
39
48
|
const analysisData = buildAnalysisData(model, exposures);
|
|
49
|
+
// Pentest data (may be null/empty)
|
|
50
|
+
const pentest = pentestData || { scans: [], templates: [], totalFindings: 0, findingsBySeverity: {} };
|
|
40
51
|
// Check for saved AI analyses
|
|
41
52
|
// (we embed the latest one if model has it, otherwise empty)
|
|
42
53
|
const aiAnalysis = ''; // Will be loaded from .guardlink/analyses/ by CLI
|
|
@@ -64,16 +75,32 @@ ${CSS_CONTENT}
|
|
|
64
75
|
<span class="badge">Threat Model</span>
|
|
65
76
|
</div>
|
|
66
77
|
<div class="topnav-right">
|
|
67
|
-
<div class="
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
78
|
+
<div class="topnav-metrics">
|
|
79
|
+
<div class="tn-stat"><span class="tn-k">Assets</span> <span class="tn-v blue">${stats.assets}</span></div>
|
|
80
|
+
<div class="tn-stat"><span class="tn-k">Open</span> <span class="tn-v red">${unmitigated.length}</span></div>
|
|
81
|
+
<div class="tn-stat"><span class="tn-k">Controls</span> <span class="tn-v green">${stats.controls}</span></div>
|
|
82
|
+
<div class="tn-stat"><span class="tn-k">Coverage</span> <span class="tn-v ${stats.coveragePercent >= 70 ? 'green' : stats.coveragePercent >= 40 ? 'yellow' : 'red'}">${stats.coveragePercent}%</span></div>
|
|
83
|
+
</div>
|
|
84
|
+
${featureNames.length > 0 ? ` <div class="feature-filter-wrap">
|
|
85
|
+
<select id="featureFilter" class="feature-filter-select" onchange="applyFeatureFilter(this.value)" title="Filter by feature">
|
|
86
|
+
<option value="">All Features</option>
|
|
87
|
+
${featureNames.map(f => ` <option value="${esc(f)}">${esc(f)}</option>`).join('\n')}
|
|
88
|
+
</select>
|
|
89
|
+
</div>` : ''}
|
|
71
90
|
<button id="themeToggle" onclick="toggleTheme()" title="Toggle light/dark mode">
|
|
72
91
|
<span class="icon-sun">☀️</span><span class="icon-moon">🌙</span>
|
|
73
92
|
</button>
|
|
74
93
|
</div>
|
|
75
94
|
</div>
|
|
76
95
|
|
|
96
|
+
<!-- Feature filter banner -->
|
|
97
|
+
<div id="feature-banner" class="feature-banner">
|
|
98
|
+
<span>Filtered to feature:</span>
|
|
99
|
+
<strong id="feature-banner-name"></strong>
|
|
100
|
+
<span id="feature-banner-files" class="feature-banner-files"></span>
|
|
101
|
+
<button class="feature-banner-clear" onclick="document.getElementById('featureFilter').value='';applyFeatureFilter('')">Clear Filter</button>
|
|
102
|
+
</div>
|
|
103
|
+
|
|
77
104
|
<div class="layout">
|
|
78
105
|
|
|
79
106
|
<!-- ═══════════ SIDEBAR ═══════════ -->
|
|
@@ -81,6 +108,7 @@ ${CSS_CONTENT}
|
|
|
81
108
|
<div class="sidebar-nav">
|
|
82
109
|
<a class="active" onclick="showSection('summary',this)"><span class="nav-icon"><svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor"><path d="M8 2l6 4v6l-6 4-6-4V6l6-4z"/></svg></span> <span class="nav-text">Executive Summary</span></a>
|
|
83
110
|
<a onclick="showSection('ai-analysis',this)"><span class="nav-icon"><svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor"><path d="M8 1l2 5h5l-4 3 2 5-5-3-5 3 2-5-4-3h5l2-5z"/></svg></span> <span class="nav-text">Threat Reports</span></a>
|
|
111
|
+
<a onclick="showSection('pentest',this)"><span class="nav-icon"><svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor"><path d="M8 1a7 7 0 100 14A7 7 0 008 1zm0 2a1.5 1.5 0 110 3 1.5 1.5 0 010-3zM6 7h4v1.5H8.5V13h-1V8.5H6V7z"/></svg></span> <span class="nav-text">Pentest Findings</span></a>
|
|
84
112
|
<a onclick="showSection('threats',this)"><span class="nav-icon"><svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor"><path d="M8 1L1 15h14L8 1zm0 4l3 8H5l3-8z"/></svg></span> <span class="nav-text">Threats & Exposures</span></a>
|
|
85
113
|
<a onclick="showSection('diagrams',this)"><span class="nav-icon"><svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor"><circle cx="8" cy="8" r="6" stroke="currentColor" stroke-width="1.5" fill="none"/><circle cx="8" cy="8" r="2"/></svg></span> <span class="nav-text">Diagrams</span></a>
|
|
86
114
|
<a onclick="showSection('code',this)"><span class="nav-icon"><svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor"><path d="M5 4L1 8l4 4v-2L3 8l2-2V4zm6 0v2l2 2-2 2v2l4-4-4-4z"/></svg></span> <span class="nav-text">Code & Annotations</span></a>
|
|
@@ -99,8 +127,9 @@ ${CSS_CONTENT}
|
|
|
99
127
|
|
|
100
128
|
${renderSummaryPage(stats, severity, riskScore, unmitigated, exposures, model, mitigatedCount, mitigationCoveragePercent)}
|
|
101
129
|
${renderAIAnalysisPage(analyses || [])}
|
|
102
|
-
${
|
|
103
|
-
${
|
|
130
|
+
${renderPentestPage(pentest)}
|
|
131
|
+
${renderThreatsPage(exposures, confirmedRows, model)}
|
|
132
|
+
${renderDiagramsPage(threatGraph, threatGraphFull, dataFlow, attackSurface, topology)}
|
|
104
133
|
${renderCodePage(fileAnnotations, model)}
|
|
105
134
|
${renderDataPage(model)}
|
|
106
135
|
${renderAssetsPage(heatmap)}
|
|
@@ -123,9 +152,12 @@ ${renderAssetsPage(heatmap)}
|
|
|
123
152
|
const fileAnnotations = ${JSON.stringify(fileAnnotations).replace(/<\//g, '<\\/')};
|
|
124
153
|
const analysisData = ${JSON.stringify(analysisData).replace(/<\//g, '<\\/')};
|
|
125
154
|
const exposuresData = ${JSON.stringify(exposures).replace(/<\//g, '<\\/')};
|
|
155
|
+
const confirmedData = ${JSON.stringify(confirmedRows).replace(/<\//g, '<\\/')};
|
|
126
156
|
const savedAnalyses = ${JSON.stringify(analyses || []).replace(/<\//g, '<\\/')};
|
|
157
|
+
const pentestData = ${JSON.stringify(pentest).replace(/<\//g, '<\\/')};
|
|
127
158
|
const heatmapData = ${JSON.stringify(heatmap).replace(/<\//g, '<\\/')};
|
|
128
159
|
const threatModel = ${JSON.stringify(model).replace(/<\//g, '<\\/')};
|
|
160
|
+
const topologyData = ${JSON.stringify(topology).replace(/<\//g, '<\\/')};
|
|
129
161
|
/* ===== SECTION NAV ===== */
|
|
130
162
|
function showSection(id, el) {
|
|
131
163
|
document.querySelectorAll('.section-content').forEach(s => s.classList.remove('active'));
|
|
@@ -134,8 +166,8 @@ function showSection(id, el) {
|
|
|
134
166
|
if (sec) sec.classList.add('active');
|
|
135
167
|
if (el) el.classList.add('active');
|
|
136
168
|
closeDrawer();
|
|
137
|
-
if (id === 'diagrams'
|
|
138
|
-
setTimeout(() => {
|
|
169
|
+
if (id === 'diagrams') {
|
|
170
|
+
setTimeout(() => { renderActiveDiagram(); }, 100);
|
|
139
171
|
}
|
|
140
172
|
if (id === 'ai-analysis' && !window._aiAnalysisRendered) {
|
|
141
173
|
renderAIAnalysis();
|
|
@@ -185,6 +217,17 @@ function openDrawer(type, idx) {
|
|
|
185
217
|
h += sec('Asset', '<code>' + esc(e.asset) + '</code>');
|
|
186
218
|
if (e.description) h += sec('Description', esc(e.description));
|
|
187
219
|
h += sec('Location', '<span style="font-family:var(--font-mono);font-size:.78rem;color:var(--muted)">' + esc(e.file) + ':' + e.line + '</span>');
|
|
220
|
+
} else if (type === 'confirmed') {
|
|
221
|
+
const c = confirmedData[idx];
|
|
222
|
+
title.textContent = c.threat + ' (Confirmed)';
|
|
223
|
+
h += '<div style="background:var(--badge-red-bg);border:1px solid var(--sev-crit);border-radius:6px;padding:.6rem;margin-bottom:1rem"><div style="font-size:.82rem;font-weight:700;color:var(--sev-crit)">🔴 CONFIRMED EXPLOITABLE</div><div style="font-size:.75rem;margin-top:.2rem;color:var(--muted)">Verified through testing — not a false positive</div></div>';
|
|
224
|
+
h += sec('Severity', '<span class="fc-sev ' + sevCls(c.severity) + '">' + esc(c.severity) + '</span>');
|
|
225
|
+
h += sec('Asset', '<code>' + esc(c.asset) + '</code>');
|
|
226
|
+
h += sec('Threat', '<code>' + esc(c.threat) + '</code>');
|
|
227
|
+
if (c.description) h += sec('Evidence', esc(c.description));
|
|
228
|
+
if (c.external_refs && c.external_refs.length) h += sec('References', c.external_refs.map(r => '<code>' + esc(r) + '</code>').join(', '));
|
|
229
|
+
h += sec('Location', '<span style="font-family:var(--font-mono);font-size:.78rem;color:var(--muted)">' + esc(c.file) + ':' + c.line + '</span>');
|
|
230
|
+
h += '<div class="d-section" style="margin-top:1rem;padding:.6rem;background:var(--badge-red-bg);border:1px solid var(--sev-crit);border-radius:6px;opacity:.85"><div style="font-size:.78rem;color:var(--sev-crit);font-weight:600">Immediate Action Required</div><div style="font-size:.78rem;margin-top:.3rem">This threat has been verified exploitable. Apply a <code>@mitigates</code> control urgently, or <code>@accepts</code> with explicit risk sign-off from security.</div></div>';
|
|
188
231
|
} else if (type === 'exposure') {
|
|
189
232
|
const e = exposuresData[idx];
|
|
190
233
|
title.textContent = e.threat;
|
|
@@ -409,6 +452,110 @@ function openAnnotationDrawer(fileIdx, annIdx) {
|
|
|
409
452
|
document.getElementById('drawer-overlay').classList.add('open');
|
|
410
453
|
}
|
|
411
454
|
|
|
455
|
+
function openPentestDrawer(scanIdx, findingIdx) {
|
|
456
|
+
// Mirror of server-side formatConfidence — keep these two in sync.
|
|
457
|
+
// CXG emits confidence as integer (most versions), severity-style string
|
|
458
|
+
// ("high"), or missing entirely. Render whatever it is, never crash.
|
|
459
|
+
function formatConf(v) {
|
|
460
|
+
if (v == null || v === '') return '\u2014';
|
|
461
|
+
if (typeof v === 'number' && isFinite(v)) {
|
|
462
|
+
return Math.max(0, Math.min(100, Math.round(v))) + '%';
|
|
463
|
+
}
|
|
464
|
+
if (typeof v === 'string') {
|
|
465
|
+
var t = v.trim();
|
|
466
|
+
if (!t) return '\u2014';
|
|
467
|
+
var m = t.match(/^(-?\d+(?:\.\d+)?)\s*%?$/);
|
|
468
|
+
if (m) return Math.max(0, Math.min(100, Math.round(parseFloat(m[1])))) + '%';
|
|
469
|
+
return t.toUpperCase();
|
|
470
|
+
}
|
|
471
|
+
return '\u2014';
|
|
472
|
+
}
|
|
473
|
+
var title = document.getElementById('drawer-title');
|
|
474
|
+
var body = document.getElementById('drawer-body');
|
|
475
|
+
var scan = pentestData.scans[scanIdx];
|
|
476
|
+
if (!scan) return;
|
|
477
|
+
var f = scan.findings[findingIdx];
|
|
478
|
+
if (!f) return;
|
|
479
|
+
title.textContent = f.title;
|
|
480
|
+
var h = '';
|
|
481
|
+
var sevColor = f.severity === 'critical' ? 'var(--sev-crit)' : f.severity === 'high' ? 'var(--sev-high)' : f.severity === 'medium' ? 'var(--sev-med)' : 'var(--sev-low)';
|
|
482
|
+
h += sec('Severity', '<span style="color:' + sevColor + ';font-weight:600;text-transform:uppercase">' + esc(f.severity) + '</span>');
|
|
483
|
+
h += sec('Confidence', '<span style="font-weight:600">' + formatConf(f.confidence) + '</span>');
|
|
484
|
+
h += sec('Template', '<code>' + esc(f.template_id) + '</code>');
|
|
485
|
+
if (f.cwe_ids && f.cwe_ids.length) h += sec('CWE', f.cwe_ids.map(function(c){return '<code>' + esc(c) + '</code>'}).join(', '));
|
|
486
|
+
h += sec('Description', '<div style="line-height:1.5">' + esc(f.description) + '</div>');
|
|
487
|
+
|
|
488
|
+
// Evidence section
|
|
489
|
+
if (f.evidence) {
|
|
490
|
+
h += '<div class="sub-h" style="margin-top:1rem">Evidence</div>';
|
|
491
|
+
if (f.evidence.request) {
|
|
492
|
+
h += sec('Request / Payload', '<div class="code-block" style="max-height:200px;overflow:auto;white-space:pre-wrap">' + esc(String(f.evidence.request).slice(0, 2000)) + '</div>');
|
|
493
|
+
}
|
|
494
|
+
if (f.evidence.response) {
|
|
495
|
+
h += sec('Response / Output', '<div class="code-block" style="max-height:200px;overflow:auto;white-space:pre-wrap">' + esc(String(f.evidence.response).slice(0, 2000)) + '</div>');
|
|
496
|
+
}
|
|
497
|
+
if (f.evidence.matched_patterns && f.evidence.matched_patterns.length) {
|
|
498
|
+
h += sec('Matched Patterns', f.evidence.matched_patterns.map(function(p){return '<span class="ann-badge ann-threat" style="margin-right:4px;margin-bottom:2px">' + esc(String(p)) + '</span>'}).join(''));
|
|
499
|
+
}
|
|
500
|
+
if (f.evidence.data && Object.keys(f.evidence.data).length > 0) {
|
|
501
|
+
var dataHtml = '<table style="font-size:.75rem"><tbody>';
|
|
502
|
+
Object.keys(f.evidence.data).forEach(function(k) {
|
|
503
|
+
var val = f.evidence.data[k];
|
|
504
|
+
var vs = typeof val === 'string' ? val : JSON.stringify(val);
|
|
505
|
+
if (vs && vs.length > 500) vs = vs.slice(0, 500) + '...';
|
|
506
|
+
dataHtml += '<tr><td style="font-weight:600;white-space:nowrap;vertical-align:top;padding-right:.5rem">' + esc(k) + '</td><td style="word-break:break-all">' + esc(vs) + '</td></tr>';
|
|
507
|
+
});
|
|
508
|
+
dataHtml += '</tbody></table>';
|
|
509
|
+
h += sec('Evidence Data', dataHtml);
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
if (f.remediation) h += sec('Remediation', '<div style="line-height:1.5;white-space:pre-line">' + esc(f.remediation) + '</div>');
|
|
514
|
+
h += sec('Scan ID', '<code style="font-size:.72rem">' + esc(scan.scan_id) + '</code>');
|
|
515
|
+
h += sec('Timestamp', esc(f.timestamp || scan.completed_at || ''));
|
|
516
|
+
|
|
517
|
+
body.innerHTML = h;
|
|
518
|
+
document.getElementById('drawer').classList.add('open');
|
|
519
|
+
document.getElementById('drawer-overlay').classList.add('open');
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
function openTemplateDrawer(idx) {
|
|
523
|
+
var title = document.getElementById('drawer-title');
|
|
524
|
+
var body = document.getElementById('drawer-body');
|
|
525
|
+
var t = pentestData.templates[idx];
|
|
526
|
+
if (!t) return;
|
|
527
|
+
title.textContent = t.id;
|
|
528
|
+
var h = '';
|
|
529
|
+
h += sec('File', '<code>' + esc(t.filename) + '</code>');
|
|
530
|
+
h += sec('Language', '<span class="ann-badge ann-control">' + esc(t.language) + '</span>');
|
|
531
|
+
h += sec('Severity', '<span class="fc-sev ' + (t.severity === 'critical' ? 'crit' : t.severity === 'high' ? 'high' : t.severity === 'medium' ? 'med' : 'low') + '">' + esc(t.severity) + '</span>');
|
|
532
|
+
if (t.tags && t.tags.length) h += sec('Tags', t.tags.map(function(tag){return '<span class="ann-badge ann-asset" style="margin-right:4px">' + esc(tag) + '</span>'}).join(''));
|
|
533
|
+
|
|
534
|
+
// Find related findings
|
|
535
|
+
var relatedFindings = [];
|
|
536
|
+
pentestData.scans.forEach(function(scan) {
|
|
537
|
+
scan.findings.forEach(function(f) {
|
|
538
|
+
if (f.template_id === t.id) relatedFindings.push(f);
|
|
539
|
+
});
|
|
540
|
+
});
|
|
541
|
+
if (relatedFindings.length > 0) {
|
|
542
|
+
h += '<div class="sub-h" style="margin-top:1rem;color:var(--red)">Findings from this Template (' + relatedFindings.length + ')</div>';
|
|
543
|
+
relatedFindings.forEach(function(f) {
|
|
544
|
+
var sc = f.severity === 'critical' ? 'var(--sev-crit)' : f.severity === 'high' ? 'var(--sev-high)' : f.severity === 'medium' ? 'var(--sev-med)' : 'var(--sev-low)';
|
|
545
|
+
h += '<div style="background:var(--surface2);border:1px solid var(--border);border-left:3px solid ' + sc + ';padding:0.5rem 0.8rem;border-radius:4px;margin-bottom:0.5rem">';
|
|
546
|
+
h += '<strong>' + esc(f.title) + '</strong>';
|
|
547
|
+
h += '<div style="font-size:.75rem;color:var(--muted);margin-top:.2rem">' + esc(f.description.slice(0, 200)) + '</div>';
|
|
548
|
+
h += '</div>';
|
|
549
|
+
});
|
|
550
|
+
} else {
|
|
551
|
+
h += sec('Findings', '<span class="empty-state">No findings from this template yet — run CXG scan to test</span>');
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
body.innerHTML = h;
|
|
555
|
+
document.getElementById('drawer').classList.add('open');
|
|
556
|
+
document.getElementById('drawer-overlay').classList.add('open');
|
|
557
|
+
}
|
|
558
|
+
|
|
412
559
|
function closeDrawer() {
|
|
413
560
|
document.getElementById('drawer').classList.remove('open');
|
|
414
561
|
document.getElementById('drawer-overlay').classList.remove('open');
|
|
@@ -425,6 +572,289 @@ function sevCls(s) {
|
|
|
425
572
|
return 'unset';
|
|
426
573
|
}
|
|
427
574
|
|
|
575
|
+
/* ===== FEATURE FILTER ===== */
|
|
576
|
+
var _activeFeature = '';
|
|
577
|
+
|
|
578
|
+
function _featureFilesFor(featureName) {
|
|
579
|
+
var files = new Set();
|
|
580
|
+
if (!featureName) return files;
|
|
581
|
+
if (threatModel.features) {
|
|
582
|
+
threatModel.features.forEach(function(f) {
|
|
583
|
+
if (f.feature.toLowerCase() === featureName.toLowerCase()) {
|
|
584
|
+
files.add(f.location.file);
|
|
585
|
+
}
|
|
586
|
+
});
|
|
587
|
+
}
|
|
588
|
+
return files;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
function applyFeatureFilter(featureName) {
|
|
592
|
+
_activeFeature = featureName;
|
|
593
|
+
var banner = document.getElementById('feature-banner');
|
|
594
|
+
|
|
595
|
+
if (!featureName) {
|
|
596
|
+
// ── Clear filter ──────────────────────────────────────────────
|
|
597
|
+
if (banner) banner.style.display = 'none';
|
|
598
|
+
document.querySelectorAll('[data-ff]').forEach(function(el) { el.style.display = ''; });
|
|
599
|
+
document.querySelectorAll('[data-ff-asset]').forEach(function(el) { el.style.display = ''; });
|
|
600
|
+
_restoreFullStats();
|
|
601
|
+
return;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
// ── Compute matching file set ─────────────────────────────────
|
|
605
|
+
var featureFiles = _featureFilesFor(featureName);
|
|
606
|
+
|
|
607
|
+
// ── Show banner ───────────────────────────────────────────────
|
|
608
|
+
if (banner) {
|
|
609
|
+
banner.style.display = 'flex';
|
|
610
|
+
document.getElementById('feature-banner-name').textContent = featureName;
|
|
611
|
+
document.getElementById('feature-banner-files').textContent = featureFiles.size + ' file(s)';
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
// ── Filter rows/cards by file ─────────────────────────────────
|
|
615
|
+
document.querySelectorAll('[data-ff]').forEach(function(el) {
|
|
616
|
+
var file = el.getAttribute('data-ff');
|
|
617
|
+
// Empty data-ff means the annotation had no location — keep visible
|
|
618
|
+
el.style.display = (!file || featureFiles.has(file)) ? '' : 'none';
|
|
619
|
+
});
|
|
620
|
+
|
|
621
|
+
// ── Filter asset heatmap cells by asset name ──────────────────
|
|
622
|
+
// An asset belongs to the feature if any of the feature files contains
|
|
623
|
+
// an annotation that references that asset.
|
|
624
|
+
var featureAssets = new Set();
|
|
625
|
+
exposuresData.forEach(function(e) { if (featureFiles.has(e.file)) { featureAssets.add(e.asset); } });
|
|
626
|
+
threatModel.flows.forEach(function(f) {
|
|
627
|
+
if (f.location && featureFiles.has(f.location.file)) {
|
|
628
|
+
featureAssets.add(f.source); featureAssets.add(f.target);
|
|
629
|
+
}
|
|
630
|
+
});
|
|
631
|
+
threatModel.exposures.forEach(function(e) {
|
|
632
|
+
if (e.location && featureFiles.has(e.location.file)) featureAssets.add(e.asset);
|
|
633
|
+
});
|
|
634
|
+
threatModel.mitigations.forEach(function(m) {
|
|
635
|
+
if (m.location && featureFiles.has(m.location.file)) featureAssets.add(m.asset);
|
|
636
|
+
});
|
|
637
|
+
|
|
638
|
+
document.querySelectorAll('[data-ff-asset]').forEach(function(el) {
|
|
639
|
+
var asset = el.getAttribute('data-ff-asset');
|
|
640
|
+
el.style.display = featureAssets.has(asset) ? '' : 'none';
|
|
641
|
+
});
|
|
642
|
+
|
|
643
|
+
// ── Recompute & update all live stats ─────────────────────────
|
|
644
|
+
_updateStatsForFilter(featureFiles);
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
function _updateStatsForFilter(featureFiles) {
|
|
648
|
+
// Compute filtered exposure subsets from the raw data arrays
|
|
649
|
+
var visExp = exposuresData.filter(function(e) { return !featureFiles.size || featureFiles.has(e.file); });
|
|
650
|
+
var visOpen = visExp.filter(function(e) { return !e.mitigated && !e.accepted; });
|
|
651
|
+
var visMit = visExp.filter(function(e) { return e.mitigated; });
|
|
652
|
+
|
|
653
|
+
var sev = { critical: 0, high: 0, medium: 0, low: 0, unset: 0 };
|
|
654
|
+
visExp.forEach(function(e) {
|
|
655
|
+
var s = (e.severity || '').toLowerCase();
|
|
656
|
+
if (s === 'critical' || s === 'p0') sev.critical++;
|
|
657
|
+
else if (s === 'high' || s === 'p1') sev.high++;
|
|
658
|
+
else if (s === 'medium' || s === 'p2') sev.medium++;
|
|
659
|
+
else if (s === 'low' || s === 'p3') sev.low++;
|
|
660
|
+
else sev.unset++;
|
|
661
|
+
});
|
|
662
|
+
|
|
663
|
+
var totalExp = visExp.length;
|
|
664
|
+
var mitPct = totalExp > 0 ? Math.round(visMit.length / totalExp * 100) : 0;
|
|
665
|
+
|
|
666
|
+
// ── Top nav ───────────────────────────────────────────────────
|
|
667
|
+
var tnStats = document.querySelectorAll('.tn-stat');
|
|
668
|
+
tnStats.forEach(function(s) {
|
|
669
|
+
var label = s.querySelector('span:first-child');
|
|
670
|
+
var val = s.querySelector('.tn-v');
|
|
671
|
+
if (!label || !val) return;
|
|
672
|
+
var lbl = label.textContent.trim();
|
|
673
|
+
if (lbl === 'Open') val.textContent = visOpen.length;
|
|
674
|
+
if (lbl === 'Coverage') {
|
|
675
|
+
val.textContent = mitPct + '%';
|
|
676
|
+
val.className = 'tn-v ' + (mitPct >= 70 ? 'green' : mitPct >= 40 ? 'yellow' : 'red');
|
|
677
|
+
}
|
|
678
|
+
});
|
|
679
|
+
|
|
680
|
+
// ── Summary page stats grid ───────────────────────────────────
|
|
681
|
+
_setStat('Open Threats', visOpen.length);
|
|
682
|
+
_setStat('Mitigated', visMit.length);
|
|
683
|
+
|
|
684
|
+
// ── Coverage bar ──────────────────────────────────────────────
|
|
685
|
+
var covPct = document.querySelector('.coverage-pct');
|
|
686
|
+
if (covPct) {
|
|
687
|
+
covPct.textContent = mitPct + '%';
|
|
688
|
+
covPct.className = 'coverage-pct ' + (mitPct >= 70 ? 'good' : mitPct >= 40 ? 'warn' : 'bad');
|
|
689
|
+
}
|
|
690
|
+
var covLabel = document.querySelector('.posture-fill')?.parentElement?.nextElementSibling;
|
|
691
|
+
var covFill = document.querySelector('.posture-fill');
|
|
692
|
+
if (covFill) {
|
|
693
|
+
covFill.style.width = Math.min(mitPct, 100) + '%';
|
|
694
|
+
covFill.className = 'posture-fill ' + (mitPct >= 70 ? 'good' : mitPct >= 40 ? 'warn' : 'bad');
|
|
695
|
+
}
|
|
696
|
+
// Update "X of Y exposures mitigated" label
|
|
697
|
+
document.querySelectorAll('#sec-summary span').forEach(function(sp) {
|
|
698
|
+
if (sp.textContent.includes('exposures mitigated')) {
|
|
699
|
+
sp.textContent = visMit.length + ' of ' + totalExp + ' exposures mitigated';
|
|
700
|
+
}
|
|
701
|
+
});
|
|
702
|
+
|
|
703
|
+
// ── Severity bars ─────────────────────────────────────────────
|
|
704
|
+
_updateSevBar('Critical', sev.critical, totalExp);
|
|
705
|
+
_updateSevBar('High', sev.high, totalExp);
|
|
706
|
+
_updateSevBar('Medium', sev.medium, totalExp);
|
|
707
|
+
_updateSevBar('Low', sev.low, totalExp);
|
|
708
|
+
_updateSevBar('Unset', sev.unset, totalExp);
|
|
709
|
+
|
|
710
|
+
// ── Section headings with counts ─────────────────────────────
|
|
711
|
+
_updateHeading('sec-threats', 'Open Threats', visOpen.length);
|
|
712
|
+
_updateHeading('sec-threats', 'Mitigated Threats', visMit.length);
|
|
713
|
+
_updateHeading('sec-threats', 'All Exposures', totalExp);
|
|
714
|
+
|
|
715
|
+
// ── Risk banner (recompute grade) ─────────────────────────────
|
|
716
|
+
var visConf = confirmedData.filter(function(c) { return !featureFiles.size || featureFiles.has(c.file); });
|
|
717
|
+
_updateRiskBanner(sev, visOpen.length, totalExp, visConf.length);
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
function _restoreFullStats() {
|
|
721
|
+
// Restore all counts from the original full data sets
|
|
722
|
+
var allOpen = exposuresData.filter(function(e) { return !e.mitigated && !e.accepted; });
|
|
723
|
+
var allMit = exposuresData.filter(function(e) { return e.mitigated; });
|
|
724
|
+
var totalExp = exposuresData.length;
|
|
725
|
+
var mitPct = totalExp > 0 ? Math.round(allMit.length / totalExp * 100) : 0;
|
|
726
|
+
|
|
727
|
+
var sev = { critical: 0, high: 0, medium: 0, low: 0, unset: 0 };
|
|
728
|
+
exposuresData.forEach(function(e) {
|
|
729
|
+
var s = (e.severity || '').toLowerCase();
|
|
730
|
+
if (s === 'critical' || s === 'p0') sev.critical++;
|
|
731
|
+
else if (s === 'high' || s === 'p1') sev.high++;
|
|
732
|
+
else if (s === 'medium' || s === 'p2') sev.medium++;
|
|
733
|
+
else if (s === 'low' || s === 'p3') sev.low++;
|
|
734
|
+
else sev.unset++;
|
|
735
|
+
});
|
|
736
|
+
|
|
737
|
+
// Top nav
|
|
738
|
+
var tnStats = document.querySelectorAll('.tn-stat');
|
|
739
|
+
tnStats.forEach(function(s) {
|
|
740
|
+
var label = s.querySelector('span:first-child');
|
|
741
|
+
var val = s.querySelector('.tn-v');
|
|
742
|
+
if (!label || !val) return;
|
|
743
|
+
var lbl = label.textContent.trim();
|
|
744
|
+
if (lbl === 'Open') val.textContent = allOpen.length;
|
|
745
|
+
if (lbl === 'Coverage') {
|
|
746
|
+
val.textContent = mitPct + '%';
|
|
747
|
+
val.className = 'tn-v ' + (mitPct >= 70 ? 'green' : mitPct >= 40 ? 'yellow' : 'red');
|
|
748
|
+
}
|
|
749
|
+
});
|
|
750
|
+
|
|
751
|
+
_setStat('Open Threats', allOpen.length);
|
|
752
|
+
_setStat('Mitigated', allMit.length);
|
|
753
|
+
|
|
754
|
+
var covPct = document.querySelector('.coverage-pct');
|
|
755
|
+
if (covPct) {
|
|
756
|
+
covPct.textContent = mitPct + '%';
|
|
757
|
+
covPct.className = 'coverage-pct ' + (mitPct >= 70 ? 'good' : mitPct >= 40 ? 'warn' : 'bad');
|
|
758
|
+
}
|
|
759
|
+
var covFill = document.querySelector('.posture-fill');
|
|
760
|
+
if (covFill) {
|
|
761
|
+
covFill.style.width = Math.min(mitPct, 100) + '%';
|
|
762
|
+
covFill.className = 'posture-fill ' + (mitPct >= 70 ? 'good' : mitPct >= 40 ? 'warn' : 'bad');
|
|
763
|
+
}
|
|
764
|
+
document.querySelectorAll('#sec-summary span').forEach(function(sp) {
|
|
765
|
+
if (sp.textContent.includes('exposures mitigated')) {
|
|
766
|
+
sp.textContent = allMit.length + ' of ' + totalExp + ' exposures mitigated';
|
|
767
|
+
}
|
|
768
|
+
});
|
|
769
|
+
|
|
770
|
+
_updateSevBar('Critical', sev.critical, totalExp);
|
|
771
|
+
_updateSevBar('High', sev.high, totalExp);
|
|
772
|
+
_updateSevBar('Medium', sev.medium, totalExp);
|
|
773
|
+
_updateSevBar('Low', sev.low, totalExp);
|
|
774
|
+
_updateSevBar('Unset', sev.unset, totalExp);
|
|
775
|
+
|
|
776
|
+
_updateHeading('sec-threats', 'Open Threats', allOpen.length);
|
|
777
|
+
_updateHeading('sec-threats', 'Mitigated Threats', allMit.length);
|
|
778
|
+
_updateHeading('sec-threats', 'All Exposures', totalExp);
|
|
779
|
+
|
|
780
|
+
_updateRiskBanner(sev, allOpen.length, totalExp, confirmedData.length);
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
/* ── Helpers ──────────────────────────────────────────────────────── */
|
|
784
|
+
|
|
785
|
+
function _setStat(label, value) {
|
|
786
|
+
document.querySelectorAll('.stat-card').forEach(function(card) {
|
|
787
|
+
var lbl = card.querySelector('.label');
|
|
788
|
+
var val = card.querySelector('.value');
|
|
789
|
+
if (lbl && val && lbl.textContent.trim() === label) {
|
|
790
|
+
val.textContent = value;
|
|
791
|
+
}
|
|
792
|
+
});
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
function _updateSevBar(label, count, total) {
|
|
796
|
+
var pct = total > 0 ? Math.round(count / total * 100) : 0;
|
|
797
|
+
document.querySelectorAll('.sev-row').forEach(function(row) {
|
|
798
|
+
var lbl = row.querySelector('.sev-label');
|
|
799
|
+
if (!lbl || lbl.textContent.trim() !== label) return;
|
|
800
|
+
var fill = row.querySelector('.sev-fill');
|
|
801
|
+
var cnt = row.querySelector('.sev-count');
|
|
802
|
+
if (fill) fill.style.width = pct + '%';
|
|
803
|
+
if (cnt) cnt.textContent = count;
|
|
804
|
+
});
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
function _updateHeading(sectionId, prefix, count) {
|
|
808
|
+
var sec = document.getElementById(sectionId);
|
|
809
|
+
if (!sec) return;
|
|
810
|
+
sec.querySelectorAll('.sub-h').forEach(function(h) {
|
|
811
|
+
if (h.textContent.trim().startsWith(prefix)) {
|
|
812
|
+
// Replace trailing (N) count
|
|
813
|
+
h.textContent = h.textContent.replace(/\(\d+\)$/, '(' + count + ')').replace(/\s+\d+$/, ' ' + count);
|
|
814
|
+
}
|
|
815
|
+
});
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
function _updateRiskBanner(sev, openCount, totalExp, confirmedCount) {
|
|
819
|
+
var grade, label, summary;
|
|
820
|
+
if (confirmedCount > 0) {
|
|
821
|
+
grade = 'F'; label = 'Critical Risk';
|
|
822
|
+
summary = confirmedCount + ' confirmed exploitable finding(s) — immediate remediation required';
|
|
823
|
+
} else if (sev.critical > 0) {
|
|
824
|
+
grade = 'F'; label = 'Critical Risk';
|
|
825
|
+
summary = sev.critical + ' critical exposure(s) require immediate attention';
|
|
826
|
+
} else if (sev.high >= 3 || openCount >= 5) {
|
|
827
|
+
grade = 'D'; label = 'High Risk';
|
|
828
|
+
summary = openCount + ' unmitigated exposure(s), ' + sev.high + ' high severity';
|
|
829
|
+
} else if (sev.high >= 1 || openCount >= 3) {
|
|
830
|
+
grade = 'C'; label = 'Moderate Risk';
|
|
831
|
+
summary = openCount + ' unmitigated exposure(s) need remediation';
|
|
832
|
+
} else if (openCount >= 1) {
|
|
833
|
+
grade = 'B'; label = 'Low Risk';
|
|
834
|
+
summary = openCount + ' minor unmitigated exposure(s)';
|
|
835
|
+
} else if (totalExp === 0) {
|
|
836
|
+
grade = 'A'; label = 'Excellent';
|
|
837
|
+
summary = 'No exposures detected — consider adding more annotations';
|
|
838
|
+
} else {
|
|
839
|
+
grade = 'A'; label = 'Excellent';
|
|
840
|
+
summary = 'All exposures mitigated or accepted';
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
var banner = document.querySelector('.risk-banner');
|
|
844
|
+
if (!banner) return;
|
|
845
|
+
// Update grade class
|
|
846
|
+
banner.className = banner.className.replace(/risk-[a-z]/g, 'risk-' + grade.toLowerCase());
|
|
847
|
+
var gradeEl = banner.querySelector('.risk-grade');
|
|
848
|
+
if (gradeEl) gradeEl.textContent = grade;
|
|
849
|
+
var detail = banner.querySelector('.risk-detail');
|
|
850
|
+
if (detail) {
|
|
851
|
+
var strong = detail.querySelector('strong');
|
|
852
|
+
var span = detail.querySelector('span');
|
|
853
|
+
if (strong) strong.textContent = label;
|
|
854
|
+
if (span) span.textContent = summary;
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
|
|
428
858
|
/* ===== THEME ===== */
|
|
429
859
|
function toggleTheme() {
|
|
430
860
|
const html = document.documentElement;
|
|
@@ -433,7 +863,7 @@ function toggleTheme() {
|
|
|
433
863
|
// Re-render mermaid with new theme
|
|
434
864
|
window._mermaidRendered = false;
|
|
435
865
|
if (document.getElementById('sec-diagrams').classList.contains('active')) {
|
|
436
|
-
|
|
866
|
+
renderActiveDiagram();
|
|
437
867
|
}
|
|
438
868
|
}
|
|
439
869
|
|
|
@@ -444,8 +874,529 @@ function switchDiagramTab(id, btn) {
|
|
|
444
874
|
const panel = document.getElementById('dtab-' + id);
|
|
445
875
|
if (panel) panel.classList.add('active');
|
|
446
876
|
if (btn) btn.classList.add('active');
|
|
447
|
-
|
|
448
|
-
|
|
877
|
+
setTimeout(() => { renderActiveDiagram(); }, 50);
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
function toggleThreatGraphAll(btn) {
|
|
881
|
+
const panel = document.getElementById('dtab-threat-graph');
|
|
882
|
+
if (!panel) return;
|
|
883
|
+
const filtered = panel.querySelector('.mermaid[data-variant="filtered"]');
|
|
884
|
+
const full = panel.querySelector('.mermaid[data-variant="full"]');
|
|
885
|
+
if (!filtered || !full) return;
|
|
886
|
+
const showFull = full.style.display === 'none';
|
|
887
|
+
filtered.style.display = showFull ? 'none' : '';
|
|
888
|
+
full.style.display = showFull ? '' : 'none';
|
|
889
|
+
if (btn) {
|
|
890
|
+
btn.classList.toggle('active', showFull);
|
|
891
|
+
btn.textContent = showFull ? 'High/Critical only' : 'All severities';
|
|
892
|
+
}
|
|
893
|
+
// Force mermaid to re-render the now-visible variant
|
|
894
|
+
panel._diagramZoom = null;
|
|
895
|
+
setTimeout(() => { renderActiveDiagram(); }, 50);
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
function diagramZoom(action) {
|
|
899
|
+
const panel = document.querySelector('.diagram-panel.active');
|
|
900
|
+
if (panel && panel.id === 'dtab-risk-topology' && window._topologyZoom) {
|
|
901
|
+
const state = window._topologyZoom;
|
|
902
|
+
if (action === 'fit') {
|
|
903
|
+
state.svg.transition().duration(420).call(state.zoom.transform, d3.zoomIdentity);
|
|
904
|
+
return;
|
|
905
|
+
}
|
|
906
|
+
const topologyFactor = action === 'in' ? 1.18 : 1 / 1.18;
|
|
907
|
+
state.svg.transition().duration(220).call(state.zoom.scaleBy, topologyFactor);
|
|
908
|
+
return;
|
|
909
|
+
}
|
|
910
|
+
if (!panel || !panel._diagramZoom) return;
|
|
911
|
+
const state = panel._diagramZoom;
|
|
912
|
+
const svg = state.svg;
|
|
913
|
+
const zoom = state.zoom;
|
|
914
|
+
if (!svg || !zoom) return;
|
|
915
|
+
|
|
916
|
+
if (action === 'fit') {
|
|
917
|
+
svg.transition().duration(420).call(zoom.transform, d3.zoomIdentity);
|
|
918
|
+
return;
|
|
919
|
+
}
|
|
920
|
+
const factor = action === 'in' ? 1.2 : 1 / 1.2;
|
|
921
|
+
svg.transition().duration(220).call(zoom.scaleBy, factor);
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
/* ===== TOPOLOGY DIAGRAM ===== */
|
|
925
|
+
function topologyColor(name, fallback) {
|
|
926
|
+
return getComputedStyle(document.documentElement).getPropertyValue(name).trim() || fallback;
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
function topologyEnabledKinds() {
|
|
930
|
+
const boxes = document.querySelectorAll('[data-topology-kind]');
|
|
931
|
+
const enabled = new Set();
|
|
932
|
+
boxes.forEach(box => { if (box.checked) enabled.add(box.getAttribute('data-topology-kind')); });
|
|
933
|
+
return enabled;
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
function topologyEnabledLinkKinds() {
|
|
937
|
+
const boxes = document.querySelectorAll('[data-topology-link]');
|
|
938
|
+
if (boxes.length === 0) return null; // null = "all enabled"
|
|
939
|
+
const enabled = new Set();
|
|
940
|
+
boxes.forEach(box => { if (box.checked) enabled.add(box.getAttribute('data-topology-link')); });
|
|
941
|
+
return enabled;
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
function topologyShort(s, max) {
|
|
945
|
+
s = String(s || '');
|
|
946
|
+
return s.length > max ? s.slice(0, max - 1) + '…' : s;
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
// Severity- and risk-scaled radius. Bigger = more risk, so the eye goes to it first.
|
|
950
|
+
function topologyRadius(node) {
|
|
951
|
+
const base = node.kind === 'asset' ? 20 : node.kind === 'threat' ? 18 : 15;
|
|
952
|
+
const sevBoost = (node.severity === 'critical' || node.severity === 'p0') ? 6
|
|
953
|
+
: (node.severity === 'high' || node.severity === 'p1') ? 4
|
|
954
|
+
: (node.severity === 'medium' || node.severity === 'p2') ? 2 : 0;
|
|
955
|
+
const riskLoad = Math.min(10, Math.sqrt((node.openExposures || 0) * 2 + (node.confirmed || 0) * 5) * 2.8);
|
|
956
|
+
return base + sevBoost + riskLoad;
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
function topologyNodeClass(node) {
|
|
960
|
+
const classes = [
|
|
961
|
+
'topology-node',
|
|
962
|
+
'topology-' + node.kind,
|
|
963
|
+
'topology-status-' + (node.status || 'none'),
|
|
964
|
+
'topology-sev-' + (node.severity || 'unset'),
|
|
965
|
+
];
|
|
966
|
+
if (node.confirmed > 0) classes.push('topology-has-confirmed');
|
|
967
|
+
if (node.openExposures > 0) classes.push('topology-has-open');
|
|
968
|
+
if (node.mitigations > 0 && node.openExposures === 0 && node.confirmed === 0) classes.push('topology-fully-covered');
|
|
969
|
+
return classes.join(' ');
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
function topologyLinkClass(link) {
|
|
973
|
+
return 'topology-link topology-link-' + link.kind + ' topology-link-status-' + (link.status || 'none');
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
function topologyLabelFor(id) {
|
|
977
|
+
const n = topologyData.nodes.find(node => node.id === id);
|
|
978
|
+
return n ? n.label : id;
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
function topologyFiltered() {
|
|
982
|
+
const enabled = topologyEnabledKinds();
|
|
983
|
+
const enabledLinks = topologyEnabledLinkKinds();
|
|
984
|
+
const queryEl = document.getElementById('topologySearch');
|
|
985
|
+
const openOnlyEl = document.getElementById('topologyOpenOnly');
|
|
986
|
+
const query = queryEl ? queryEl.value.trim().toLowerCase() : '';
|
|
987
|
+
const openOnly = openOnlyEl ? openOnlyEl.checked : false;
|
|
988
|
+
|
|
989
|
+
let nodes = topologyData.nodes.filter(node => enabled.has(node.kind));
|
|
990
|
+
if (openOnly) {
|
|
991
|
+
nodes = nodes.filter(node => node.status === 'open' || node.status === 'confirmed' || node.openExposures > 0 || node.confirmed > 0);
|
|
992
|
+
}
|
|
993
|
+
if (query) {
|
|
994
|
+
const direct = new Set(nodes.filter(node => {
|
|
995
|
+
const refs = (node.refs || []).join(' ').toLowerCase();
|
|
996
|
+
return node.label.toLowerCase().includes(query) || refs.includes(query) || node.kind.includes(query);
|
|
997
|
+
}).map(node => node.id));
|
|
998
|
+
const expanded = new Set(direct);
|
|
999
|
+
topologyData.links.forEach(link => {
|
|
1000
|
+
if (direct.has(link.source) || direct.has(link.target)) {
|
|
1001
|
+
expanded.add(link.source);
|
|
1002
|
+
expanded.add(link.target);
|
|
1003
|
+
}
|
|
1004
|
+
});
|
|
1005
|
+
nodes = nodes.filter(node => expanded.has(node.id));
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
const ids = new Set(nodes.map(node => node.id));
|
|
1009
|
+
const links = topologyData.links.filter(link => {
|
|
1010
|
+
if (!ids.has(link.source) || !ids.has(link.target)) return false;
|
|
1011
|
+
if (enabledLinks && !enabledLinks.has(link.kind)) return false;
|
|
1012
|
+
return true;
|
|
1013
|
+
});
|
|
1014
|
+
return { nodes, links };
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
// Lane-based x target: assets left, threats middle, controls right.
|
|
1018
|
+
function topologyLaneX(node, width) {
|
|
1019
|
+
if (node.kind === 'asset') return width * 0.22;
|
|
1020
|
+
if (node.kind === 'threat') return width * 0.5;
|
|
1021
|
+
return width * 0.78;
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
// Vertical lane pull: critical/high float up, low/unset sink down.
|
|
1025
|
+
function topologyLaneY(node, height) {
|
|
1026
|
+
const rank = (node.severity === 'critical' || node.severity === 'p0') ? 0.2
|
|
1027
|
+
: (node.severity === 'high' || node.severity === 'p1') ? 0.35
|
|
1028
|
+
: (node.severity === 'medium' || node.severity === 'p2') ? 0.55
|
|
1029
|
+
: (node.severity === 'low' || node.severity === 'p3') ? 0.7 : 0.55;
|
|
1030
|
+
return height * rank;
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
function topologyNeighborhood(nodeId) {
|
|
1034
|
+
const neighbors = new Set([nodeId]);
|
|
1035
|
+
const linkSet = new Set();
|
|
1036
|
+
topologyData.links.forEach(l => {
|
|
1037
|
+
const s = l.source.id || l.source;
|
|
1038
|
+
const t = l.target.id || l.target;
|
|
1039
|
+
if (s === nodeId || t === nodeId) {
|
|
1040
|
+
neighbors.add(s);
|
|
1041
|
+
neighbors.add(t);
|
|
1042
|
+
linkSet.add(l);
|
|
1043
|
+
}
|
|
1044
|
+
});
|
|
1045
|
+
return { neighbors, linkSet };
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
function showTopologyOverview(nodeCount, linkCount) {
|
|
1049
|
+
const inspector = document.getElementById('topologyInspector');
|
|
1050
|
+
if (!inspector) return;
|
|
1051
|
+
const s = topologyData.summary;
|
|
1052
|
+
const totalExposures = s.open + s.mitigated + s.accepted;
|
|
1053
|
+
const coveragePct = totalExposures === 0 ? 100 : Math.round(((s.mitigated + s.accepted) / totalExposures) * 100);
|
|
1054
|
+
const barCls = coveragePct >= 75 ? 'good' : coveragePct >= 40 ? 'warn' : 'bad';
|
|
1055
|
+
let h = '';
|
|
1056
|
+
h += '<div class="topology-inspector-k">Topology</div>';
|
|
1057
|
+
h += '<h3>Threat Model Map</h3>';
|
|
1058
|
+
h += '<div class="topology-bar-wrap"><div class="topology-bar-label"><span>Coverage</span><strong>' + coveragePct + '%</strong></div>';
|
|
1059
|
+
h += '<div class="topology-bar"><div class="topology-bar-fill ' + barCls + '" style="width:' + coveragePct + '%"></div></div></div>';
|
|
1060
|
+
h += '<div class="topology-detail-grid">';
|
|
1061
|
+
h += '<span>Assets</span><strong>' + s.assets + '</strong>';
|
|
1062
|
+
h += '<span>Threats</span><strong>' + s.threats + '</strong>';
|
|
1063
|
+
h += '<span>Controls</span><strong>' + s.controls + '</strong>';
|
|
1064
|
+
h += '<span>Open</span><strong class="danger">' + s.open + '</strong>';
|
|
1065
|
+
if (s.confirmed) h += '<span>Confirmed</span><strong class="danger">' + s.confirmed + '</strong>';
|
|
1066
|
+
h += '<span>Mitigated</span><strong class="good">' + s.mitigated + '</strong>';
|
|
1067
|
+
if (s.accepted) h += '<span>Accepted</span><strong>' + s.accepted + '</strong>';
|
|
1068
|
+
if (s.criticalAssets) h += '<span>Critical</span><strong class="danger">' + s.criticalAssets + ' asset' + (s.criticalAssets > 1 ? 's' : '') + '</strong>';
|
|
1069
|
+
h += '<span>Visible</span><strong>' + nodeCount + ' / ' + linkCount + '</strong>';
|
|
1070
|
+
h += '</div>';
|
|
1071
|
+
h += '<div class="topology-inspector-note">Hover a node to highlight its neighborhood. Click to pin. Drag to reposition.</div>';
|
|
1072
|
+
inspector.innerHTML = h;
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
function showTopologyDetails(node) {
|
|
1076
|
+
const inspector = document.getElementById('topologyInspector');
|
|
1077
|
+
if (!inspector) return;
|
|
1078
|
+
const related = topologyData.links.filter(link => link.source === node.id || link.target === node.id);
|
|
1079
|
+
const shown = related.slice(0, 12);
|
|
1080
|
+
const mitCoverage = node.exposures > 0 ? Math.round(Math.min(1, node.mitigations / node.exposures) * 100) : (node.mitigations > 0 ? 100 : 0);
|
|
1081
|
+
const barCls = mitCoverage >= 75 ? 'good' : mitCoverage >= 40 ? 'warn' : 'bad';
|
|
1082
|
+
|
|
1083
|
+
let h = '';
|
|
1084
|
+
h += '<div class="topology-inspector-head"><span class="topology-inspector-k">' + esc(node.kind) + '</span>';
|
|
1085
|
+
if (node.confirmed > 0) h += '<span class="topology-badge topology-badge-confirmed">💥 Confirmed</span>';
|
|
1086
|
+
else if (node.openExposures > 0) h += '<span class="topology-badge topology-badge-open">⚠ Open</span>';
|
|
1087
|
+
else if (node.mitigations > 0) h += '<span class="topology-badge topology-badge-covered">✓ Covered</span>';
|
|
1088
|
+
h += '</div>';
|
|
1089
|
+
h += '<h3>' + esc(node.label) + '</h3>';
|
|
1090
|
+
|
|
1091
|
+
if (node.kind === 'asset' && (node.exposures > 0 || node.mitigations > 0)) {
|
|
1092
|
+
h += '<div class="topology-bar-wrap"><div class="topology-bar-label"><span>Mitigation</span><strong>' + mitCoverage + '%</strong></div>';
|
|
1093
|
+
h += '<div class="topology-bar"><div class="topology-bar-fill ' + barCls + '" style="width:' + mitCoverage + '%"></div></div></div>';
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
h += '<div class="topology-detail-grid">';
|
|
1097
|
+
h += '<span>Status</span><strong class="status-' + esc(node.status || 'none') + '">' + esc(node.status || 'none') + '</strong>';
|
|
1098
|
+
h += '<span>Severity</span><strong class="sev-' + esc(node.severity || 'unset') + '">' + esc(node.severity || 'unset') + '</strong>';
|
|
1099
|
+
if (node.exposures) h += '<span>Exposures</span><strong>' + node.exposures + '</strong>';
|
|
1100
|
+
if (node.openExposures) h += '<span>Open</span><strong class="danger">' + node.openExposures + '</strong>';
|
|
1101
|
+
if (node.mitigations) h += '<span>Mitigations</span><strong class="good">' + node.mitigations + '</strong>';
|
|
1102
|
+
if (node.confirmed) h += '<span>Confirmed</span><strong class="danger">' + node.confirmed + '</strong>';
|
|
1103
|
+
if (node.flows) h += '<span>Flows</span><strong>' + node.flows + '</strong>';
|
|
1104
|
+
if (node.riskScore) h += '<span>Risk score</span><strong>' + node.riskScore + '</strong>';
|
|
1105
|
+
if (node.owner) h += '<span>Owner</span><strong>' + esc(node.owner) + '</strong>';
|
|
1106
|
+
h += '</div>';
|
|
1107
|
+
if (node.classifications && node.classifications.length) {
|
|
1108
|
+
h += '<div class="topology-chip-row">' + node.classifications.map(c => '<span>' + esc(c) + '</span>').join('') + '</div>';
|
|
1109
|
+
}
|
|
1110
|
+
if (shown.length) {
|
|
1111
|
+
h += '<div class="topology-related-title">Relationships <em style="color:var(--muted);font-family:var(--font-mono);font-size:.66rem">(' + related.length + ')</em></div>';
|
|
1112
|
+
h += '<div class="topology-related">';
|
|
1113
|
+
shown.forEach(link => {
|
|
1114
|
+
const other = link.source === node.id ? link.target : link.source;
|
|
1115
|
+
const isOut = link.source === node.id;
|
|
1116
|
+
h += '<div class="topology-related-row topology-related-' + esc(link.kind) + '">';
|
|
1117
|
+
h += '<span class="topology-related-kind">' + (isOut ? '→' : '←') + ' ' + esc(link.kind) + '</span>';
|
|
1118
|
+
h += '<span>' + esc(topologyLabelFor(other)) + '</span>';
|
|
1119
|
+
if (link.count > 1) h += '<em>×' + link.count + '</em>';
|
|
1120
|
+
else h += '<em></em>';
|
|
1121
|
+
h += '</div>';
|
|
1122
|
+
});
|
|
1123
|
+
h += '</div>';
|
|
1124
|
+
}
|
|
1125
|
+
inspector.innerHTML = h;
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
function renderTopologyDiagram() {
|
|
1129
|
+
const host = document.getElementById('topologyGraph');
|
|
1130
|
+
if (!host) return;
|
|
1131
|
+
if (typeof d3 === 'undefined') {
|
|
1132
|
+
host.innerHTML = '<div class="empty-state">Topology renderer unavailable.</div>';
|
|
1133
|
+
return;
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
const filtered = topologyFiltered();
|
|
1137
|
+
const nodes = filtered.nodes.map(node => Object.assign({}, node));
|
|
1138
|
+
const links = filtered.links.map(link => Object.assign({}, link));
|
|
1139
|
+
const counter = document.getElementById('topologyVisibleCount');
|
|
1140
|
+
if (counter) counter.textContent = nodes.length + ' nodes · ' + links.length + ' links';
|
|
1141
|
+
|
|
1142
|
+
if (window._topologySimulation) window._topologySimulation.stop();
|
|
1143
|
+
host.innerHTML = '';
|
|
1144
|
+
if (nodes.length === 0) {
|
|
1145
|
+
host.innerHTML = '<div class="empty-state">No matching topology data.</div>';
|
|
1146
|
+
showTopologyOverview(0, 0);
|
|
1147
|
+
return;
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
const bounds = host.getBoundingClientRect();
|
|
1151
|
+
const width = Math.max(760, Math.floor(bounds.width || 960));
|
|
1152
|
+
const height = Math.max(560, Math.min(820, Math.floor(window.innerHeight * 0.66)));
|
|
1153
|
+
const nodeById = new Map(nodes.map(node => [node.id, node]));
|
|
1154
|
+
const linksByNodeId = new Map();
|
|
1155
|
+
nodes.forEach(n => linksByNodeId.set(n.id, new Set()));
|
|
1156
|
+
links.forEach(l => {
|
|
1157
|
+
const s = typeof l.source === 'string' ? l.source : l.source.id;
|
|
1158
|
+
const t = typeof l.target === 'string' ? l.target : l.target.id;
|
|
1159
|
+
if (linksByNodeId.has(s)) linksByNodeId.get(s).add(t);
|
|
1160
|
+
if (linksByNodeId.has(t)) linksByNodeId.get(t).add(s);
|
|
1161
|
+
});
|
|
1162
|
+
|
|
1163
|
+
const svg = d3.select(host).append('svg')
|
|
1164
|
+
.attr('viewBox', '0 0 ' + width + ' ' + height)
|
|
1165
|
+
.attr('width', width)
|
|
1166
|
+
.attr('height', height)
|
|
1167
|
+
.attr('role', 'img')
|
|
1168
|
+
.attr('aria-label', 'Threat model topology');
|
|
1169
|
+
const stage = svg.append('g');
|
|
1170
|
+
const zoom = d3.zoom().scaleExtent([0.25, 3.8]).on('zoom', event => {
|
|
1171
|
+
stage.attr('transform', event.transform);
|
|
1172
|
+
});
|
|
1173
|
+
svg.call(zoom);
|
|
1174
|
+
window._topologyZoom = { svg, zoom };
|
|
1175
|
+
|
|
1176
|
+
const defs = svg.append('defs');
|
|
1177
|
+
// Arrow markers — one per link kind so colour matches the edge.
|
|
1178
|
+
const arrowKinds = [
|
|
1179
|
+
{ id: 'exposes', color: topologyColor('--sev-crit', '#ea1d1d') },
|
|
1180
|
+
{ id: 'confirmed', color: topologyColor('--sev-crit', '#ea1d1d') },
|
|
1181
|
+
{ id: 'mitigates', color: topologyColor('--green', '#33d49d') },
|
|
1182
|
+
{ id: 'protects', color: topologyColor('--green', '#33d49d') },
|
|
1183
|
+
{ id: 'validates', color: topologyColor('--green', '#33d49d') },
|
|
1184
|
+
{ id: 'flows', color: topologyColor('--blue', '#0360a2') },
|
|
1185
|
+
{ id: 'boundary', color: topologyColor('--purple', '#7d5cff') },
|
|
1186
|
+
{ id: 'accepts', color: topologyColor('--sev-med', '#55899e') },
|
|
1187
|
+
{ id: 'transfers', color: topologyColor('--sev-med', '#55899e') },
|
|
1188
|
+
];
|
|
1189
|
+
arrowKinds.forEach(a => {
|
|
1190
|
+
defs.append('marker')
|
|
1191
|
+
.attr('id', 'topology-arrow-' + a.id)
|
|
1192
|
+
.attr('viewBox', '0 -5 10 10')
|
|
1193
|
+
.attr('refX', 20)
|
|
1194
|
+
.attr('refY', 0)
|
|
1195
|
+
.attr('markerWidth', 6)
|
|
1196
|
+
.attr('markerHeight', 6)
|
|
1197
|
+
.attr('orient', 'auto')
|
|
1198
|
+
.append('path')
|
|
1199
|
+
.attr('d', 'M0,-5L10,0L0,5')
|
|
1200
|
+
.attr('fill', a.color);
|
|
1201
|
+
});
|
|
1202
|
+
// Halo drop-shadow for confirmed/critical nodes
|
|
1203
|
+
const glow = defs.append('filter').attr('id', 'topology-glow').attr('x', '-50%').attr('y', '-50%').attr('width', '200%').attr('height', '200%');
|
|
1204
|
+
glow.append('feGaussianBlur').attr('stdDeviation', '3.5').attr('result', 'blur');
|
|
1205
|
+
const feMerge = glow.append('feMerge');
|
|
1206
|
+
feMerge.append('feMergeNode').attr('in', 'blur');
|
|
1207
|
+
feMerge.append('feMergeNode').attr('in', 'SourceGraphic');
|
|
1208
|
+
|
|
1209
|
+
// Link distances bias flows/boundaries to stretch out; exposures stay tight.
|
|
1210
|
+
const linkDistance = l => {
|
|
1211
|
+
if (l.kind === 'flows' || l.kind === 'boundary') return 160;
|
|
1212
|
+
if (l.kind === 'mitigates' || l.kind === 'protects' || l.kind === 'validates') return 120;
|
|
1213
|
+
if (l.kind === 'transfers') return 140;
|
|
1214
|
+
return 95;
|
|
1215
|
+
};
|
|
1216
|
+
|
|
1217
|
+
const simulation = d3.forceSimulation(nodes)
|
|
1218
|
+
.force('link', d3.forceLink(links).id(node => node.id).distance(linkDistance).strength(0.72))
|
|
1219
|
+
.force('charge', d3.forceManyBody().strength(node => node.kind === 'control' ? -320 : node.kind === 'threat' ? -520 : -460))
|
|
1220
|
+
.force('x', d3.forceX(n => topologyLaneX(n, width)).strength(0.22))
|
|
1221
|
+
.force('y', d3.forceY(n => topologyLaneY(n, height)).strength(0.10))
|
|
1222
|
+
.force('collide', d3.forceCollide().radius(node => topologyRadius(node) + 22).iterations(2));
|
|
1223
|
+
window._topologySimulation = simulation;
|
|
1224
|
+
|
|
1225
|
+
// Lane labels — subtle background text showing the three kind lanes.
|
|
1226
|
+
const laneLabels = stage.append('g').attr('class', 'topology-lane-labels');
|
|
1227
|
+
const lanes = [
|
|
1228
|
+
{ label: 'ASSETS', x: width * 0.22 },
|
|
1229
|
+
{ label: 'THREATS', x: width * 0.5 },
|
|
1230
|
+
{ label: 'CONTROLS', x: width * 0.78 },
|
|
1231
|
+
];
|
|
1232
|
+
laneLabels.selectAll('text')
|
|
1233
|
+
.data(lanes)
|
|
1234
|
+
.join('text')
|
|
1235
|
+
.attr('class', 'topology-lane-label')
|
|
1236
|
+
.attr('x', d => d.x)
|
|
1237
|
+
.attr('y', 22)
|
|
1238
|
+
.attr('text-anchor', 'middle')
|
|
1239
|
+
.text(d => d.label);
|
|
1240
|
+
|
|
1241
|
+
const link = stage.append('g')
|
|
1242
|
+
.attr('class', 'topology-links')
|
|
1243
|
+
.selectAll('line')
|
|
1244
|
+
.data(links)
|
|
1245
|
+
.join('line')
|
|
1246
|
+
.attr('class', topologyLinkClass)
|
|
1247
|
+
.attr('marker-end', d => 'url(#topology-arrow-' + d.kind + ')');
|
|
1248
|
+
link.append('title').text(d => topologyLabelFor(d.source.id || d.source) + ' → ' + topologyLabelFor(d.target.id || d.target) + ' · ' + d.label);
|
|
1249
|
+
|
|
1250
|
+
const node = stage.append('g')
|
|
1251
|
+
.attr('class', 'topology-nodes')
|
|
1252
|
+
.selectAll('g')
|
|
1253
|
+
.data(nodes)
|
|
1254
|
+
.join('g')
|
|
1255
|
+
.attr('class', topologyNodeClass)
|
|
1256
|
+
.call(d3.drag()
|
|
1257
|
+
.on('start', (event, d) => {
|
|
1258
|
+
if (!event.active) simulation.alphaTarget(0.25).restart();
|
|
1259
|
+
d.fx = d.x;
|
|
1260
|
+
d.fy = d.y;
|
|
1261
|
+
})
|
|
1262
|
+
.on('drag', (event, d) => {
|
|
1263
|
+
d.fx = event.x;
|
|
1264
|
+
d.fy = event.y;
|
|
1265
|
+
})
|
|
1266
|
+
.on('end', (event, d) => {
|
|
1267
|
+
if (!event.active) simulation.alphaTarget(0);
|
|
1268
|
+
d.fx = null;
|
|
1269
|
+
d.fy = null;
|
|
1270
|
+
}));
|
|
1271
|
+
|
|
1272
|
+
// Pulsing halo for confirmed / critical-open nodes
|
|
1273
|
+
node.filter(d => d.confirmed > 0 || ((d.severity === 'critical' || d.severity === 'p0') && d.openExposures > 0))
|
|
1274
|
+
.insert('circle', ':first-child')
|
|
1275
|
+
.attr('class', 'topology-node-halo')
|
|
1276
|
+
.attr('r', d => topologyRadius(d) + 8);
|
|
1277
|
+
|
|
1278
|
+
node.append('circle')
|
|
1279
|
+
.attr('class', 'topology-node-body')
|
|
1280
|
+
.attr('r', topologyRadius);
|
|
1281
|
+
|
|
1282
|
+
// Mitigation coverage ring (arc) — only for assets with both exposures and mitigations
|
|
1283
|
+
node.filter(d => d.kind === 'asset' && d.exposures > 0)
|
|
1284
|
+
.append('path')
|
|
1285
|
+
.attr('class', 'topology-coverage-arc')
|
|
1286
|
+
.attr('d', d => {
|
|
1287
|
+
const mit = d.exposures > 0 ? Math.min(1, d.mitigations / d.exposures) : 0;
|
|
1288
|
+
if (mit <= 0) return '';
|
|
1289
|
+
const r = topologyRadius(d) + 4;
|
|
1290
|
+
const angle = mit * 2 * Math.PI;
|
|
1291
|
+
const x1 = r * Math.sin(0);
|
|
1292
|
+
const y1 = -r * Math.cos(0);
|
|
1293
|
+
const x2 = r * Math.sin(angle);
|
|
1294
|
+
const y2 = -r * Math.cos(angle);
|
|
1295
|
+
const largeArc = mit > 0.5 ? 1 : 0;
|
|
1296
|
+
return 'M 0 ' + (-r) + ' A ' + r + ' ' + r + ' 0 ' + largeArc + ' 1 ' + x2.toFixed(2) + ' ' + y2.toFixed(2);
|
|
1297
|
+
});
|
|
1298
|
+
|
|
1299
|
+
node.append('text')
|
|
1300
|
+
.attr('class', 'topology-node-icon')
|
|
1301
|
+
.attr('text-anchor', 'middle')
|
|
1302
|
+
.attr('dy', '0.35em')
|
|
1303
|
+
.text(d => d.kind === 'asset' ? 'A' : d.kind === 'threat' ? 'T' : 'C');
|
|
1304
|
+
node.append('text')
|
|
1305
|
+
.attr('class', 'topology-node-label')
|
|
1306
|
+
.attr('text-anchor', 'middle')
|
|
1307
|
+
.attr('dy', d => topologyRadius(d) + 16)
|
|
1308
|
+
.text(d => topologyShort(d.label, 24));
|
|
1309
|
+
node.append('title').text(d => d.label + ' · ' + d.kind + ' · ' + d.status + (d.openExposures ? ' · ' + d.openExposures + ' open' : '') + (d.confirmed ? ' · ' + d.confirmed + ' confirmed' : ''));
|
|
1310
|
+
|
|
1311
|
+
// Connected-subgraph dim on hover
|
|
1312
|
+
const applyDim = (activeId) => {
|
|
1313
|
+
if (!activeId) {
|
|
1314
|
+
node.classed('dim', false).classed('emphasis', false);
|
|
1315
|
+
link.classed('dim', false).classed('emphasis', false);
|
|
1316
|
+
return;
|
|
1317
|
+
}
|
|
1318
|
+
const neighbors = new Set([activeId]);
|
|
1319
|
+
links.forEach(l => {
|
|
1320
|
+
const s = l.source.id || l.source;
|
|
1321
|
+
const t = l.target.id || l.target;
|
|
1322
|
+
if (s === activeId || t === activeId) {
|
|
1323
|
+
neighbors.add(s);
|
|
1324
|
+
neighbors.add(t);
|
|
1325
|
+
}
|
|
1326
|
+
});
|
|
1327
|
+
node.classed('dim', n => !neighbors.has(n.id)).classed('emphasis', n => neighbors.has(n.id));
|
|
1328
|
+
link.classed('dim', l => {
|
|
1329
|
+
const s = l.source.id || l.source;
|
|
1330
|
+
const t = l.target.id || l.target;
|
|
1331
|
+
return s !== activeId && t !== activeId;
|
|
1332
|
+
}).classed('emphasis', l => {
|
|
1333
|
+
const s = l.source.id || l.source;
|
|
1334
|
+
const t = l.target.id || l.target;
|
|
1335
|
+
return s === activeId || t === activeId;
|
|
1336
|
+
});
|
|
1337
|
+
};
|
|
1338
|
+
|
|
1339
|
+
node.on('mouseover', (event, d) => {
|
|
1340
|
+
if (window._topologyPinnedId) return;
|
|
1341
|
+
applyDim(d.id);
|
|
1342
|
+
}).on('mouseout', () => {
|
|
1343
|
+
if (window._topologyPinnedId) return;
|
|
1344
|
+
applyDim(null);
|
|
1345
|
+
});
|
|
1346
|
+
|
|
1347
|
+
node.on('click', (event, d) => {
|
|
1348
|
+
event.stopPropagation();
|
|
1349
|
+
window._topologyActiveNodeId = d.id;
|
|
1350
|
+
window._topologyPinnedId = d.id;
|
|
1351
|
+
node.classed('active', n => n.id === d.id);
|
|
1352
|
+
applyDim(d.id);
|
|
1353
|
+
showTopologyDetails(d);
|
|
1354
|
+
});
|
|
1355
|
+
svg.on('click', () => {
|
|
1356
|
+
window._topologyActiveNodeId = null;
|
|
1357
|
+
window._topologyPinnedId = null;
|
|
1358
|
+
node.classed('active', false);
|
|
1359
|
+
applyDim(null);
|
|
1360
|
+
showTopologyOverview(nodes.length, links.length);
|
|
1361
|
+
});
|
|
1362
|
+
|
|
1363
|
+
simulation.on('tick', () => {
|
|
1364
|
+
link
|
|
1365
|
+
.attr('x1', d => d.source.x)
|
|
1366
|
+
.attr('y1', d => d.source.y)
|
|
1367
|
+
.attr('x2', d => d.target.x)
|
|
1368
|
+
.attr('y2', d => d.target.y);
|
|
1369
|
+
node.attr('transform', d => 'translate(' + d.x + ',' + d.y + ')');
|
|
1370
|
+
});
|
|
1371
|
+
|
|
1372
|
+
const active = window._topologyActiveNodeId && nodeById.get(window._topologyActiveNodeId);
|
|
1373
|
+
if (active) {
|
|
1374
|
+
node.classed('active', n => n.id === active.id);
|
|
1375
|
+
applyDim(active.id);
|
|
1376
|
+
showTopologyDetails(active);
|
|
1377
|
+
} else {
|
|
1378
|
+
showTopologyOverview(nodes.length, links.length);
|
|
1379
|
+
}
|
|
1380
|
+
}
|
|
1381
|
+
|
|
1382
|
+
function exportTopologySVG() {
|
|
1383
|
+
const host = document.getElementById('topologyGraph');
|
|
1384
|
+
if (!host) return;
|
|
1385
|
+
const svg = host.querySelector('svg');
|
|
1386
|
+
if (!svg) return;
|
|
1387
|
+
const clone = svg.cloneNode(true);
|
|
1388
|
+
clone.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
|
|
1389
|
+
const xmlHeader = '<?xml version="1.0" standalone="no"?>';
|
|
1390
|
+
const source = xmlHeader + '\\n' + new XMLSerializer().serializeToString(clone);
|
|
1391
|
+
const blob = new Blob([source], { type: 'image/svg+xml;charset=utf-8' });
|
|
1392
|
+
const url = URL.createObjectURL(blob);
|
|
1393
|
+
const a = document.createElement('a');
|
|
1394
|
+
a.href = url;
|
|
1395
|
+
a.download = 'guardlink-topology.svg';
|
|
1396
|
+
document.body.appendChild(a);
|
|
1397
|
+
a.click();
|
|
1398
|
+
document.body.removeChild(a);
|
|
1399
|
+
setTimeout(() => URL.revokeObjectURL(url), 1000);
|
|
449
1400
|
}
|
|
450
1401
|
|
|
451
1402
|
/* ===== MERMAID ===== */
|
|
@@ -460,17 +1411,23 @@ async function getMermaidInstance() {
|
|
|
460
1411
|
startOnLoad: false,
|
|
461
1412
|
theme: isDark ? 'dark' : 'default',
|
|
462
1413
|
themeVariables: isDark ? {
|
|
463
|
-
primaryColor: '#
|
|
464
|
-
lineColor: '#
|
|
465
|
-
background: '#
|
|
466
|
-
clusterBkg: '
|
|
1414
|
+
primaryColor: '#17272e', primaryTextColor: '#f0f0f0', primaryBorderColor: '#55899e',
|
|
1415
|
+
lineColor: '#6b93a6', secondaryColor: '#1f3943', tertiaryColor: '#0f1b20',
|
|
1416
|
+
background: '#0f1b20', mainBkg: '#17272e', nodeBorder: '#55899e', secondBkg: '#1f3943',
|
|
1417
|
+
clusterBkg: 'rgba(23,39,46,.55)', clusterBorder: '#3b6779',
|
|
1418
|
+
titleColor: '#f0f0f0', edgeLabelBackground: '#0f1b20', labelBackground: '#0f1b20',
|
|
1419
|
+
nodeTextColor: '#f0f0f0',
|
|
1420
|
+
fontSize: '12px', fontFamily: 'Inter, system-ui, sans-serif',
|
|
467
1421
|
} : {
|
|
468
|
-
primaryColor: '#
|
|
469
|
-
lineColor: '#
|
|
470
|
-
background: '#ffffff', mainBkg: '#
|
|
471
|
-
clusterBkg: '#
|
|
1422
|
+
primaryColor: '#ffffff', primaryTextColor: '#1f3943', primaryBorderColor: '#55899e',
|
|
1423
|
+
lineColor: '#3b6779', secondaryColor: '#f4f7f8', tertiaryColor: '#ffffff',
|
|
1424
|
+
background: '#ffffff', mainBkg: '#ffffff', nodeBorder: '#55899e', secondBkg: '#e8eef0',
|
|
1425
|
+
clusterBkg: '#f7fafb', clusterBorder: '#d9e4e8',
|
|
1426
|
+
titleColor: '#1f3943', edgeLabelBackground: '#ffffff', labelBackground: '#ffffff',
|
|
1427
|
+
nodeTextColor: '#1f3943',
|
|
1428
|
+
fontSize: '12px', fontFamily: 'Inter, system-ui, sans-serif',
|
|
472
1429
|
},
|
|
473
|
-
flowchart: { curve: '
|
|
1430
|
+
flowchart: { curve: 'monotoneX', padding: 20, nodeSpacing: 48, rankSpacing: 62, htmlLabels: false, useMaxWidth: false, defaultRenderer: 'dagre-d3' },
|
|
474
1431
|
securityLevel: 'loose',
|
|
475
1432
|
});
|
|
476
1433
|
return mermaid;
|
|
@@ -479,18 +1436,23 @@ async function getMermaidInstance() {
|
|
|
479
1436
|
async function renderMermaidPanel(panel) {
|
|
480
1437
|
if (!panel) return;
|
|
481
1438
|
const mermaid = await getMermaidInstance();
|
|
482
|
-
|
|
483
|
-
|
|
1439
|
+
// Render only the currently visible mermaid block(s). Hidden variants (e.g. the
|
|
1440
|
+
// full threat graph behind the "All severities" toggle) would otherwise fail
|
|
1441
|
+
// getBBox during layout sizing.
|
|
1442
|
+
const allEls = Array.from(panel.querySelectorAll('.mermaid'));
|
|
1443
|
+
const els = allEls.filter(el => el.offsetParent !== null || el.style.display !== 'none');
|
|
1444
|
+
const targets = els.length > 0 ? els : allEls;
|
|
1445
|
+
|
|
484
1446
|
// Re-run mermaid
|
|
485
|
-
|
|
1447
|
+
targets.forEach(el => {
|
|
486
1448
|
el.removeAttribute('data-processed');
|
|
487
1449
|
el.innerHTML = el.getAttribute('data-original') || el.textContent;
|
|
488
1450
|
});
|
|
489
|
-
await mermaid.run({ nodes:
|
|
1451
|
+
await mermaid.run({ nodes: targets });
|
|
490
1452
|
|
|
491
1453
|
// Add interactive zoom/pan to the rendered SVG
|
|
492
1454
|
if (typeof d3 !== 'undefined') {
|
|
493
|
-
|
|
1455
|
+
targets.forEach(el => {
|
|
494
1456
|
const svg = d3.select(el).select('svg');
|
|
495
1457
|
if (!svg.empty()) {
|
|
496
1458
|
const inner = svg.select('.root'); // Mermaid puts everything in a .root group
|
|
@@ -501,6 +1463,7 @@ async function renderMermaidPanel(panel) {
|
|
|
501
1463
|
inner.attr('transform', e.transform);
|
|
502
1464
|
});
|
|
503
1465
|
svg.call(zoom);
|
|
1466
|
+
panel._diagramZoom = { svg, zoom };
|
|
504
1467
|
|
|
505
1468
|
// Preserve natural SVG size so long labels are not clipped.
|
|
506
1469
|
// Container scrolling handles overflow for large graphs.
|
|
@@ -538,6 +1501,16 @@ async function renderMermaid() {
|
|
|
538
1501
|
window._mermaidRendered = true;
|
|
539
1502
|
}
|
|
540
1503
|
|
|
1504
|
+
function renderActiveDiagram() {
|
|
1505
|
+
const active = document.querySelector('.diagram-panel.active');
|
|
1506
|
+
if (!active) return;
|
|
1507
|
+
if (active.id === 'dtab-risk-topology') {
|
|
1508
|
+
renderTopologyDiagram();
|
|
1509
|
+
return;
|
|
1510
|
+
}
|
|
1511
|
+
renderMermaidPanel(active).then(() => { window._mermaidRendered = true; });
|
|
1512
|
+
}
|
|
1513
|
+
|
|
541
1514
|
// Save original diagram source
|
|
542
1515
|
document.querySelectorAll('.mermaid').forEach(el => {
|
|
543
1516
|
el.setAttribute('data-original', el.textContent.trim());
|
|
@@ -647,6 +1620,7 @@ function renderSummaryPage(stats, severity, risk, unmitigated, exposures, model,
|
|
|
647
1620
|
<div class="stats-grid">
|
|
648
1621
|
${statCard(stats.assets, 'Assets')}
|
|
649
1622
|
${statCard(unmitigated.length, 'Open Threats', 'danger')}
|
|
1623
|
+
${stats.confirmed > 0 ? statCard(stats.confirmed, 'Confirmed', 'danger') : ''}
|
|
650
1624
|
${statCard(mitigatedCount, 'Mitigated', 'success')}
|
|
651
1625
|
${statCard(stats.controls, 'Controls', 'success')}
|
|
652
1626
|
${statCard(stats.flows, 'Data Flows')}
|
|
@@ -660,15 +1634,19 @@ function renderSummaryPage(stats, severity, risk, unmitigated, exposures, model,
|
|
|
660
1634
|
${stats.shields > 0 ? statCard(stats.shields, 'Shields', 'muted') : ''}
|
|
661
1635
|
</div>
|
|
662
1636
|
|
|
1637
|
+
<div class="summary-panels">
|
|
663
1638
|
<!-- Coverage Bar -->
|
|
1639
|
+
<div class="panel">
|
|
664
1640
|
<div class="sub-h">Threat Mitigation Coverage</div>
|
|
665
|
-
<div
|
|
1641
|
+
<div class="panel-row">
|
|
666
1642
|
<span class="coverage-pct ${mitigationCoveragePercent >= 70 ? 'good' : mitigationCoveragePercent >= 40 ? 'warn' : 'bad'}">${mitigationCoveragePercent}%</span>
|
|
667
|
-
<span
|
|
1643
|
+
<span class="panel-muted">${mitigatedCount} of ${exposures.length} exposures mitigated</span>
|
|
668
1644
|
</div>
|
|
669
1645
|
<div class="posture-bar"><div class="posture-fill ${mitigationCoveragePercent >= 70 ? 'good' : mitigationCoveragePercent >= 40 ? 'warn' : 'bad'}" style="width:${Math.min(mitigationCoveragePercent, 100)}%"></div></div>
|
|
1646
|
+
</div>
|
|
670
1647
|
|
|
671
1648
|
<!-- Severity Breakdown -->
|
|
1649
|
+
<div class="panel">
|
|
672
1650
|
<div class="sub-h">Severity Breakdown</div>
|
|
673
1651
|
<div class="severity-chart">
|
|
674
1652
|
${severityBar('Critical', severity.critical, stats.exposures, 'crit')}
|
|
@@ -677,50 +1655,163 @@ function renderSummaryPage(stats, severity, risk, unmitigated, exposures, model,
|
|
|
677
1655
|
${severityBar('Low', severity.low, stats.exposures, 'low')}
|
|
678
1656
|
${severity.unset > 0 ? severityBar('Unset', severity.unset, stats.exposures, 'unset') : ''}
|
|
679
1657
|
</div>
|
|
1658
|
+
</div>
|
|
1659
|
+
</div>
|
|
680
1660
|
|
|
681
1661
|
${unmitigated.length > 0 ? `
|
|
682
1662
|
<!-- Open Threats -->
|
|
683
|
-
<div class="
|
|
1663
|
+
<div class="panel">
|
|
1664
|
+
<div class="sub-h sub-h-alert">⚠ Open Threats (No Mitigation)</div>
|
|
684
1665
|
${unmitigated.map((e, i) => `
|
|
685
|
-
<div class="finding-card" onclick="openDrawer('open_exposure', ${i})">
|
|
1666
|
+
<div class="finding-card" data-ff="${esc(e.file)}" onclick="openDrawer('open_exposure', ${i})">
|
|
686
1667
|
<div class="fc-top">
|
|
687
1668
|
<span class="fc-risk">${esc(e.threat)}</span>
|
|
688
1669
|
<span class="fc-sev ${sevClass(e.severity)}">${esc(e.severity)}</span>
|
|
689
1670
|
</div>
|
|
690
1671
|
${e.description ? `<div class="fc-desc">${esc(e.description)}</div>` : ''}
|
|
691
1672
|
<div class="fc-assets">Asset: ${esc(e.asset)}</div>
|
|
692
|
-
</div>`).join('')}
|
|
1673
|
+
</div>`).join('')}
|
|
1674
|
+
</div>` : ''}
|
|
693
1675
|
|
|
694
1676
|
${model.flows.length > 0 ? `
|
|
695
1677
|
<!-- Data Flows -->
|
|
1678
|
+
<div class="panel">
|
|
696
1679
|
<div class="sub-h">Data Flows</div>
|
|
697
1680
|
<table>
|
|
698
1681
|
<thead><tr><th>Source</th><th></th><th>Target</th><th>Mechanism</th><th>Location</th></tr></thead>
|
|
699
1682
|
<tbody>
|
|
700
1683
|
${model.flows.map(f => `
|
|
701
|
-
<tr>
|
|
1684
|
+
<tr data-ff="${f.location ? esc(f.location.file) : ''}">
|
|
702
1685
|
<td><code>${esc(f.source)}</code></td>
|
|
703
|
-
<td
|
|
1686
|
+
<td class="flow-arrow">→</td>
|
|
704
1687
|
<td><code>${esc(f.target)}</code></td>
|
|
705
1688
|
<td>${esc(f.mechanism || '—')}</td>
|
|
706
1689
|
<td class="loc">${f.location ? `${esc(f.location.file)}:${f.location.line}` : ''}</td>
|
|
707
1690
|
</tr>`).join('')}
|
|
708
1691
|
</tbody>
|
|
709
|
-
</table
|
|
1692
|
+
</table>
|
|
1693
|
+
</div>` : ''}
|
|
710
1694
|
</div>`;
|
|
711
1695
|
}
|
|
712
1696
|
function renderAIAnalysisPage(analyses) {
|
|
713
1697
|
return `
|
|
714
1698
|
<div id="sec-ai-analysis" class="section-content">
|
|
715
1699
|
<div class="sec-h"><span class="sec-icon">✨</span> Threat Reports</div>
|
|
1700
|
+
<div class="panel ai-analysis-panel">
|
|
716
1701
|
<div class="ai-analysis-controls">
|
|
717
1702
|
<label for="report-selector" class="report-selector-label">Select Report:</label>
|
|
718
1703
|
<select id="report-selector" class="report-selector" aria-label="Select threat report"></select>
|
|
719
1704
|
</div>
|
|
720
1705
|
<div id="ai-content" class="md-content ai-analysis-main"></div>
|
|
1706
|
+
</div>
|
|
721
1707
|
</div>`;
|
|
722
1708
|
}
|
|
723
|
-
function
|
|
1709
|
+
function renderPentestPage(pentest) {
|
|
1710
|
+
const hasScanData = pentest.scans.length > 0;
|
|
1711
|
+
const hasTemplates = pentest.templates.length > 0;
|
|
1712
|
+
const latestScan = pentest.scans[0];
|
|
1713
|
+
let findingsHtml = '';
|
|
1714
|
+
if (hasScanData) {
|
|
1715
|
+
pentest.scans.forEach((scan, si) => {
|
|
1716
|
+
const scanDate = scan.completed_at ? scan.completed_at.slice(0, 19).replace('T', ' ') : 'unknown';
|
|
1717
|
+
const duration = scan.statistics?.duration
|
|
1718
|
+
? `${scan.statistics.duration.secs}s`
|
|
1719
|
+
: '?';
|
|
1720
|
+
findingsHtml += `
|
|
1721
|
+
<div style="margin-bottom:1.5rem">
|
|
1722
|
+
<div style="display:flex;align-items:center;gap:.6rem;margin-bottom:.5rem">
|
|
1723
|
+
<span style="font-weight:600;font-size:.9rem">Scan ${esc(scan.scan_id.slice(0, 8))}</span>
|
|
1724
|
+
<span style="font-size:.72rem;color:var(--muted)">${esc(scanDate)} · ${esc(duration)} · ${scan.findings.length} finding(s)</span>
|
|
1725
|
+
<span style="font-size:.68rem;color:var(--muted);background:var(--surface2);padding:1px 6px;border-radius:4px">${esc(scan.source_file)}</span>
|
|
1726
|
+
</div>`;
|
|
1727
|
+
if (scan.findings.length > 0) {
|
|
1728
|
+
findingsHtml += `
|
|
1729
|
+
<table>
|
|
1730
|
+
<thead><tr><th>Severity</th><th>Title</th><th>Template</th><th>CWE</th><th>Confidence</th></tr></thead>
|
|
1731
|
+
<tbody>
|
|
1732
|
+
${scan.findings.map((f, fi) => `
|
|
1733
|
+
<tr class="clickable" onclick="openPentestDrawer(${si}, ${fi})">
|
|
1734
|
+
<td><span class="fc-sev ${f.severity === 'critical' ? 'crit' : f.severity === 'high' ? 'high' : f.severity === 'medium' ? 'med' : 'low'}">${esc(f.severity)}</span></td>
|
|
1735
|
+
<td>${esc(f.title)}</td>
|
|
1736
|
+
<td><code style="font-size:.72rem">${esc(f.template_id)}</code></td>
|
|
1737
|
+
<td>${f.cwe_ids?.length ? f.cwe_ids.map(c => `<code style="font-size:.7rem">${esc(c)}</code>`).join(' ') : '—'}</td>
|
|
1738
|
+
<td>${formatConfidence(f.confidence)}</td>
|
|
1739
|
+
</tr>`).join('')}
|
|
1740
|
+
</tbody>
|
|
1741
|
+
</table>`;
|
|
1742
|
+
}
|
|
1743
|
+
else {
|
|
1744
|
+
findingsHtml += '<p class="empty-state">No findings in this scan — all checks passed.</p>';
|
|
1745
|
+
}
|
|
1746
|
+
findingsHtml += '</div>';
|
|
1747
|
+
});
|
|
1748
|
+
}
|
|
1749
|
+
let templatesHtml = '';
|
|
1750
|
+
if (hasTemplates) {
|
|
1751
|
+
templatesHtml = `
|
|
1752
|
+
<div class="sub-h">Templates (${pentest.templates.length})</div>
|
|
1753
|
+
<p style="color:var(--muted);font-size:.78rem;margin-bottom:.5rem">CXG templates in <code>.guardlink/cxg-templates/</code> — click for details.</p>
|
|
1754
|
+
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:8px">
|
|
1755
|
+
${pentest.templates.map((t, i) => `
|
|
1756
|
+
<div class="finding-card" onclick="openTemplateDrawer(${i})">
|
|
1757
|
+
<div class="fc-top">
|
|
1758
|
+
<span class="fc-risk">${esc(t.id)}</span>
|
|
1759
|
+
<span class="fc-sev ${t.severity === 'critical' ? 'crit' : t.severity === 'high' ? 'high' : t.severity === 'medium' ? 'med' : 'low'}">${esc(t.severity)}</span>
|
|
1760
|
+
</div>
|
|
1761
|
+
<div class="fc-assets">${esc(t.filename)} · ${esc(t.language)}</div>
|
|
1762
|
+
${t.tags.length > 0 ? `<div style="margin-top:.3rem">${t.tags.slice(0, 4).map(tag => `<span class="data-badge">${esc(tag)}</span>`).join(' ')}</div>` : ''}
|
|
1763
|
+
</div>`).join('')}
|
|
1764
|
+
</div>`;
|
|
1765
|
+
}
|
|
1766
|
+
const sevBreakdown = pentest.findingsBySeverity;
|
|
1767
|
+
const maxSevCount = Math.max(1, ...Object.values(sevBreakdown));
|
|
1768
|
+
return `
|
|
1769
|
+
<div id="sec-pentest" class="section-content">
|
|
1770
|
+
<div class="sec-h"><span class="sec-icon">🔬</span> Pentest Findings</div>
|
|
1771
|
+
|
|
1772
|
+
${pentest.redactionApplied ? `
|
|
1773
|
+
<div style="background:linear-gradient(135deg,rgba(56,178,172,0.08),rgba(56,178,172,0.03));border:1px solid rgba(56,178,172,0.35);border-radius:6px;padding:.55rem .8rem;margin-bottom:1rem;display:flex;align-items:center;gap:.55rem;font-size:.8rem">
|
|
1774
|
+
<svg width="14" height="14" viewBox="0 0 16 16" fill="rgb(56,178,172)" aria-hidden="true"><path d="M8 1a3 3 0 00-3 3v3H4a1 1 0 00-1 1v6a1 1 0 001 1h8a1 1 0 001-1V8a1 1 0 00-1-1h-1V4a3 3 0 00-3-3zM6 4a2 2 0 014 0v3H6V4z"/></svg>
|
|
1775
|
+
<span><strong style="color:rgb(56,178,172)">Evidence redaction: enabled</strong> — JWT signatures stripped, credential values masked. Claims and exploit payloads preserved. See <code>docs/handling-evidence.md</code>.</span>
|
|
1776
|
+
</div>
|
|
1777
|
+
` : ''}
|
|
1778
|
+
|
|
1779
|
+
${hasScanData || hasTemplates ? `
|
|
1780
|
+
<!-- Stats bar -->
|
|
1781
|
+
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(130px,1fr));gap:10px;margin-bottom:1.2rem">
|
|
1782
|
+
<div class="stat-card stat-red"><span class="value">${pentest.totalFindings}</span><span class="label">Total Findings</span></div>
|
|
1783
|
+
<div class="stat-card"><span class="value">${sevBreakdown['critical'] || 0}</span><span class="label" style="color:var(--sev-crit)">Critical</span></div>
|
|
1784
|
+
<div class="stat-card"><span class="value">${sevBreakdown['high'] || 0}</span><span class="label" style="color:var(--sev-high)">High</span></div>
|
|
1785
|
+
<div class="stat-card"><span class="value">${sevBreakdown['medium'] || 0}</span><span class="label" style="color:var(--sev-med)">Medium</span></div>
|
|
1786
|
+
<div class="stat-card"><span class="value">${sevBreakdown['low'] || 0}</span><span class="label" style="color:var(--sev-low)">Low</span></div>
|
|
1787
|
+
<div class="stat-card stat-muted"><span class="value">${pentest.templates.length}</span><span class="label">Templates</span></div>
|
|
1788
|
+
<div class="stat-card stat-muted"><span class="value">${pentest.scans.length}</span><span class="label">Scans</span></div>
|
|
1789
|
+
</div>
|
|
1790
|
+
|
|
1791
|
+
${hasScanData ? `
|
|
1792
|
+
<div class="sub-h" style="color:var(--red)">Findings (${pentest.totalFindings})</div>
|
|
1793
|
+
<p style="color:var(--muted);font-size:.78rem;margin-bottom:.5rem">Results from CXG security scans — click a finding for full evidence details.</p>
|
|
1794
|
+
${findingsHtml}
|
|
1795
|
+
` : ''}
|
|
1796
|
+
|
|
1797
|
+
${templatesHtml}
|
|
1798
|
+
` : `
|
|
1799
|
+
<div class="empty-state" style="text-align:center;padding:3rem 1rem">
|
|
1800
|
+
<div style="font-size:3rem;margin-bottom:1rem;opacity:0.5">🔬</div>
|
|
1801
|
+
<div style="font-size:1.1rem;font-weight:600;margin-bottom:0.5rem">No Pentest Data Yet</div>
|
|
1802
|
+
<div style="color:var(--muted);margin-bottom:1.5rem">Generate CXG templates and run scans to see findings here</div>
|
|
1803
|
+
<div style="display:flex;flex-direction:column;gap:0.5rem;max-width:550px;margin:0 auto;text-align:left">
|
|
1804
|
+
<div style="font-size:0.88rem;color:var(--muted)"><strong>Step 1 — Generate templates:</strong></div>
|
|
1805
|
+
<code style="display:block;padding:0.5rem;background:var(--surface2);border-radius:4px;font-size:0.82rem">guardlink translate "Create templates for critical threats" --claude-code</code>
|
|
1806
|
+
<div style="font-size:0.88rem;color:var(--muted);margin-top:0.5rem"><strong>Step 2 — Run CXG scan:</strong></div>
|
|
1807
|
+
<code style="display:block;padding:0.5rem;background:var(--surface2);border-radius:4px;font-size:0.82rem">cxg scan --scope local://. --template-dir .guardlink/cxg-templates/ --output .guardlink/pentest-findings/guardlink-pentest --output-format json,sarif</code>
|
|
1808
|
+
<div style="font-size:0.88rem;color:var(--muted);margin-top:0.5rem"><strong>Step 3 — View results:</strong></div>
|
|
1809
|
+
<code style="display:block;padding:0.5rem;background:var(--surface2);border-radius:4px;font-size:0.82rem">guardlink dashboard</code>
|
|
1810
|
+
</div>
|
|
1811
|
+
</div>`}
|
|
1812
|
+
</div>`;
|
|
1813
|
+
}
|
|
1814
|
+
function renderThreatsPage(exposures, confirmed, model) {
|
|
724
1815
|
const open = exposures.filter(e => !e.mitigated && !e.accepted);
|
|
725
1816
|
const mitigated = exposures.filter(e => e.mitigated);
|
|
726
1817
|
const accepted = exposures.filter(e => e.accepted);
|
|
@@ -728,14 +1819,31 @@ function renderThreatsPage(exposures, model) {
|
|
|
728
1819
|
<div id="sec-threats" class="section-content">
|
|
729
1820
|
<div class="sec-h"><span class="sec-icon">⚠</span> Threats & Exposures</div>
|
|
730
1821
|
|
|
731
|
-
|
|
732
|
-
<
|
|
1822
|
+
${confirmed.length > 0 ? `
|
|
1823
|
+
<div class="sub-h sub-h-critical">🔴 Confirmed Exploitable (${confirmed.length})</div>
|
|
1824
|
+
<p class="section-note">Verified through pentest, scanning, or manual reproduction — <strong>not false positives</strong>.</p>
|
|
1825
|
+
<table>
|
|
1826
|
+
<thead><tr><th>Asset</th><th>Threat</th><th>Severity</th><th>Description</th><th>Location</th></tr></thead>
|
|
1827
|
+
<tbody>
|
|
1828
|
+
${confirmed.map((c, i) => `
|
|
1829
|
+
<tr class="clickable row-open" data-ff="${esc(c.file)}" onclick="openDrawer('confirmed', ${i})" style="border-left:3px solid var(--sev-crit, #e74c3c)">
|
|
1830
|
+
<td><code>${esc(c.asset)}</code></td>
|
|
1831
|
+
<td><code>${esc(c.threat)}</code></td>
|
|
1832
|
+
<td><span class="fc-sev ${sevClass(c.severity)}">${esc(c.severity)}</span></td>
|
|
1833
|
+
<td>${esc(c.description || '—')}</td>
|
|
1834
|
+
<td class="loc">${esc(c.file)}:${c.line}</td>
|
|
1835
|
+
</tr>`).join('')}
|
|
1836
|
+
</tbody>
|
|
1837
|
+
</table>` : ''}
|
|
1838
|
+
|
|
1839
|
+
<div class="sub-h sub-h-alert">Open Threats (${open.length})</div>
|
|
1840
|
+
<p class="section-note">Exposed in code but <strong>not mitigated</strong> by any control.</p>
|
|
733
1841
|
${open.length > 0 ? `
|
|
734
1842
|
<table>
|
|
735
1843
|
<thead><tr><th>Asset</th><th>Threat</th><th>Severity</th><th>Description</th><th>Location</th></tr></thead>
|
|
736
1844
|
<tbody>
|
|
737
1845
|
${open.map((e, i) => `
|
|
738
|
-
<tr class="clickable" onclick="openDrawer('open_exposure', ${i})">
|
|
1846
|
+
<tr class="clickable" data-ff="${esc(e.file)}" onclick="openDrawer('open_exposure', ${i})">
|
|
739
1847
|
<td><code>${esc(e.asset)}</code></td>
|
|
740
1848
|
<td><code>${esc(e.threat)}</code></td>
|
|
741
1849
|
<td><span class="fc-sev ${sevClass(e.severity)}">${esc(e.severity)}</span></td>
|
|
@@ -745,13 +1853,13 @@ function renderThreatsPage(exposures, model) {
|
|
|
745
1853
|
</tbody>
|
|
746
1854
|
</table>` : '<p class="empty-state">All exposed threats are mitigated or accepted.</p>'}
|
|
747
1855
|
|
|
748
|
-
<div class="sub-h
|
|
1856
|
+
<div class="sub-h sub-h-ok">Mitigated Threats (${mitigated.length})</div>
|
|
749
1857
|
${mitigated.length > 0 ? `
|
|
750
1858
|
<table>
|
|
751
1859
|
<thead><tr><th>Asset</th><th>Threat</th><th>Severity</th><th>Description</th><th>Location</th></tr></thead>
|
|
752
1860
|
<tbody>
|
|
753
1861
|
${mitigated.map((e, i) => `
|
|
754
|
-
<tr class="clickable" onclick="openDrawer('mitigated_exposure', ${i})">
|
|
1862
|
+
<tr class="clickable" data-ff="${esc(e.file)}" onclick="openDrawer('mitigated_exposure', ${i})">
|
|
755
1863
|
<td><code>${esc(e.asset)}</code></td>
|
|
756
1864
|
<td><code>${esc(e.threat)}</code></td>
|
|
757
1865
|
<td><span class="fc-sev ${sevClass(e.severity)}">${esc(e.severity)}</span></td>
|
|
@@ -762,12 +1870,12 @@ function renderThreatsPage(exposures, model) {
|
|
|
762
1870
|
</table>` : '<p class="empty-state">No mitigations found.</p>'}
|
|
763
1871
|
|
|
764
1872
|
${accepted.length > 0 ? `
|
|
765
|
-
<div class="sub-h
|
|
1873
|
+
<div class="sub-h sub-h-neutral">Accepted Risks (${accepted.length})</div>
|
|
766
1874
|
<table>
|
|
767
1875
|
<thead><tr><th>Asset</th><th>Threat</th><th>Severity</th><th>Description</th><th>Location</th></tr></thead>
|
|
768
1876
|
<tbody>
|
|
769
1877
|
${accepted.map(e => `
|
|
770
|
-
<tr>
|
|
1878
|
+
<tr data-ff="${esc(e.file)}">
|
|
771
1879
|
<td><code>${esc(e.asset)}</code></td>
|
|
772
1880
|
<td><code>${esc(e.threat)}</code></td>
|
|
773
1881
|
<td><span class="fc-sev ${sevClass(e.severity)}">${esc(e.severity)}</span></td>
|
|
@@ -778,12 +1886,12 @@ function renderThreatsPage(exposures, model) {
|
|
|
778
1886
|
</table>` : ''}
|
|
779
1887
|
|
|
780
1888
|
${model.transfers.length > 0 ? `
|
|
781
|
-
<div class="sub-h
|
|
1889
|
+
<div class="sub-h sub-h-info">Transferred Risks (${model.transfers.length})</div>
|
|
782
1890
|
<table>
|
|
783
1891
|
<thead><tr><th>Source</th><th>Threat</th><th>Target</th><th>Description</th><th>Location</th></tr></thead>
|
|
784
1892
|
<tbody>
|
|
785
1893
|
${model.transfers.map(t => `
|
|
786
|
-
<tr>
|
|
1894
|
+
<tr data-ff="${t.location ? esc(t.location.file) : ''}">
|
|
787
1895
|
<td><code>${esc(t.source)}</code></td>
|
|
788
1896
|
<td><code>${esc(t.threat)}</code></td>
|
|
789
1897
|
<td><code>${esc(t.target)}</code></td>
|
|
@@ -799,7 +1907,7 @@ function renderThreatsPage(exposures, model) {
|
|
|
799
1907
|
<thead><tr><th>Status</th><th>Asset</th><th>Threat</th><th>Severity</th><th>Description</th><th>Location</th></tr></thead>
|
|
800
1908
|
<tbody>
|
|
801
1909
|
${exposures.map((e, i) => `
|
|
802
|
-
<tr class="clickable ${!e.mitigated && !e.accepted ? 'row-open' : ''}" onclick="openDrawer('exposure', ${i})">
|
|
1910
|
+
<tr class="clickable ${!e.mitigated && !e.accepted ? 'row-open' : ''}" data-ff="${esc(e.file)}" onclick="openDrawer('exposure', ${i})">
|
|
803
1911
|
<td>${e.mitigated ? '<span class="badge badge-green">Mitigated</span>' : e.accepted ? '<span class="badge badge-blue">Accepted</span>' : '<span class="badge badge-red">Open</span>'}</td>
|
|
804
1912
|
<td><code>${esc(e.asset)}</code></td>
|
|
805
1913
|
<td><code>${esc(e.threat)}</code></td>
|
|
@@ -811,20 +1919,126 @@ function renderThreatsPage(exposures, model) {
|
|
|
811
1919
|
</table>` : ''}
|
|
812
1920
|
</div>`;
|
|
813
1921
|
}
|
|
814
|
-
function renderDiagramsPage(threatGraph, dataFlow, attackSurface) {
|
|
1922
|
+
function renderDiagramsPage(threatGraph, threatGraphFull, dataFlow, attackSurface, topology) {
|
|
815
1923
|
const tabs = [];
|
|
816
1924
|
const panels = [];
|
|
1925
|
+
const diagramActions = `
|
|
1926
|
+
<div class="diagram-actions">
|
|
1927
|
+
<button class="diagram-btn" onclick="diagramZoom('out')" title="Zoom out">−</button>
|
|
1928
|
+
<button class="diagram-btn" onclick="diagramZoom('in')" title="Zoom in">+</button>
|
|
1929
|
+
<button class="diagram-btn" onclick="diagramZoom('fit')" title="Reset view">Reset</button>
|
|
1930
|
+
</div>`;
|
|
1931
|
+
if (topology.nodes.length > 0) {
|
|
1932
|
+
tabs.push({ id: 'risk-topology', label: 'Risk Topology', icon: '◎' });
|
|
1933
|
+
// Which link kinds are actually present? Only show filters that apply.
|
|
1934
|
+
const availableLinkKinds = new Set(topology.links.map(l => l.kind));
|
|
1935
|
+
const linkKindCatalog = [
|
|
1936
|
+
['exposes', 'Exposures'],
|
|
1937
|
+
['confirmed', 'Confirmed'],
|
|
1938
|
+
['mitigates', 'Mitigations'],
|
|
1939
|
+
['protects', 'Protects'],
|
|
1940
|
+
['validates', 'Validates'],
|
|
1941
|
+
['flows', 'Flows'],
|
|
1942
|
+
['boundary', 'Boundaries'],
|
|
1943
|
+
['transfers', 'Transfers'],
|
|
1944
|
+
['accepts', 'Accepts'],
|
|
1945
|
+
];
|
|
1946
|
+
const linkFilters = linkKindCatalog
|
|
1947
|
+
.filter(([k]) => availableLinkKinds.has(k))
|
|
1948
|
+
.map(([k, label]) => `<label class="topology-link-pill topology-link-pill-${k}"><input type="checkbox" data-topology-link="${k}" checked onchange="renderTopologyDiagram()"> ${label}</label>`)
|
|
1949
|
+
.join('\n ');
|
|
1950
|
+
panels.push(`<div id="dtab-risk-topology" class="diagram-panel active">
|
|
1951
|
+
<div class="diagram-shell topology-shell">
|
|
1952
|
+
<div class="diagram-toolbar topology-toolbar">
|
|
1953
|
+
<span class="diagram-title">Risk Topology</span>
|
|
1954
|
+
<div class="diagram-actions">
|
|
1955
|
+
<button class="diagram-btn" onclick="exportTopologySVG()" title="Download SVG">⤓ SVG</button>
|
|
1956
|
+
<button class="diagram-btn" onclick="diagramZoom('out')" title="Zoom out">−</button>
|
|
1957
|
+
<button class="diagram-btn" onclick="diagramZoom('in')" title="Zoom in">+</button>
|
|
1958
|
+
<button class="diagram-btn" onclick="diagramZoom('fit')" title="Reset view">Reset</button>
|
|
1959
|
+
</div>
|
|
1960
|
+
</div>
|
|
1961
|
+
<div class="topology-controls">
|
|
1962
|
+
<input id="topologySearch" class="diagram-search" type="search" placeholder="Search assets, threats, controls…" oninput="renderTopologyDiagram()">
|
|
1963
|
+
<div class="topology-kind-toggles">
|
|
1964
|
+
<label class="topology-kind-pill topology-kind-asset"><input type="checkbox" data-topology-kind="asset" checked onchange="renderTopologyDiagram()"> Assets</label>
|
|
1965
|
+
<label class="topology-kind-pill topology-kind-threat"><input type="checkbox" data-topology-kind="threat" checked onchange="renderTopologyDiagram()"> Threats</label>
|
|
1966
|
+
<label class="topology-kind-pill topology-kind-control"><input type="checkbox" data-topology-kind="control" checked onchange="renderTopologyDiagram()"> Controls</label>
|
|
1967
|
+
<label class="topology-kind-pill topology-kind-openonly"><input id="topologyOpenOnly" type="checkbox" onchange="renderTopologyDiagram()"> Open only</label>
|
|
1968
|
+
</div>
|
|
1969
|
+
<span id="topologyVisibleCount" class="topology-count">${topology.nodes.length} nodes · ${topology.links.length} links</span>
|
|
1970
|
+
</div>
|
|
1971
|
+
<div class="topology-link-filters">
|
|
1972
|
+
<span class="topology-link-filters-k">Edges</span>
|
|
1973
|
+
${linkFilters}
|
|
1974
|
+
</div>
|
|
1975
|
+
<div class="topology-layout">
|
|
1976
|
+
<div id="topologyGraph" class="topology-graph"></div>
|
|
1977
|
+
<aside id="topologyInspector" class="topology-inspector"></aside>
|
|
1978
|
+
</div>
|
|
1979
|
+
<div class="diagram-meta topology-legend">
|
|
1980
|
+
<span><i class="legend-dot asset"></i>Asset</span>
|
|
1981
|
+
<span><i class="legend-dot threat"></i>Threat</span>
|
|
1982
|
+
<span><i class="legend-dot control"></i>Control</span>
|
|
1983
|
+
<span><i class="legend-dot confirmed"></i>Confirmed</span>
|
|
1984
|
+
<span><i class="legend-line exposes"></i>Exposure</span>
|
|
1985
|
+
<span><i class="legend-line mitigates"></i>Mitigates</span>
|
|
1986
|
+
<span><i class="legend-line flow"></i>Flow</span>
|
|
1987
|
+
<span><i class="legend-line boundary"></i>Boundary</span>
|
|
1988
|
+
</div>
|
|
1989
|
+
</div>
|
|
1990
|
+
</div>`);
|
|
1991
|
+
}
|
|
817
1992
|
if (threatGraph) {
|
|
818
1993
|
tabs.push({ id: 'threat-graph', label: 'Threat Graph', icon: '🔷' });
|
|
819
|
-
|
|
1994
|
+
const hasFullVariant = threatGraphFull && threatGraphFull !== threatGraph;
|
|
1995
|
+
const showAllBtn = hasFullVariant
|
|
1996
|
+
? `<button id="threatGraphToggle" class="diagram-btn" onclick="toggleThreatGraphAll(this)" title="Show all threat severities (not just high/critical)">All severities</button>`
|
|
1997
|
+
: '';
|
|
1998
|
+
panels.push(`<div id="dtab-threat-graph" class="diagram-panel${panels.length === 0 ? ' active' : ''}">
|
|
1999
|
+
<div class="diagram-shell">
|
|
2000
|
+
<div class="diagram-toolbar">
|
|
2001
|
+
<span class="diagram-title">Threat Graph</span>
|
|
2002
|
+
<div class="diagram-actions">
|
|
2003
|
+
${showAllBtn}
|
|
2004
|
+
<button class="diagram-btn" onclick="diagramZoom('out')" title="Zoom out">−</button>
|
|
2005
|
+
<button class="diagram-btn" onclick="diagramZoom('in')" title="Zoom in">+</button>
|
|
2006
|
+
<button class="diagram-btn" onclick="diagramZoom('fit')" title="Reset view">Reset</button>
|
|
2007
|
+
</div>
|
|
2008
|
+
</div>
|
|
2009
|
+
<div class="mermaid-wrap">
|
|
2010
|
+
<pre class="mermaid" data-variant="filtered">\n${esc(threatGraph)}\n</pre>
|
|
2011
|
+
${hasFullVariant ? `<pre class="mermaid" data-variant="full" style="display:none">\n${esc(threatGraphFull)}\n</pre>` : ''}
|
|
2012
|
+
</div>
|
|
2013
|
+
<div class="diagram-meta">Assets, threats, controls, and mitigations. ${hasFullVariant ? 'Filtered to high/critical by default — click <em>All severities</em> to expand.' : ''}</div>
|
|
2014
|
+
</div>
|
|
2015
|
+
</div>`);
|
|
820
2016
|
}
|
|
821
2017
|
if (dataFlow) {
|
|
822
2018
|
tabs.push({ id: 'data-flow', label: 'Data Flow', icon: '↔' });
|
|
823
|
-
panels.push(`<div id="dtab-data-flow" class="diagram-panel"
|
|
2019
|
+
panels.push(`<div id="dtab-data-flow" class="diagram-panel">
|
|
2020
|
+
<div class="diagram-shell">
|
|
2021
|
+
<div class="diagram-toolbar">
|
|
2022
|
+
<span class="diagram-title">Data Flow</span>
|
|
2023
|
+
${diagramActions}
|
|
2024
|
+
</div>
|
|
2025
|
+
<div class="mermaid-wrap"><pre class="mermaid">\n${esc(dataFlow)}\n</pre></div>
|
|
2026
|
+
<div class="diagram-meta">Trust zones (🧱) and data movement across system boundaries. Each boundary shows both sides of the trust line.</div>
|
|
2027
|
+
</div>
|
|
2028
|
+
</div>`);
|
|
824
2029
|
}
|
|
825
2030
|
if (attackSurface) {
|
|
826
2031
|
tabs.push({ id: 'attack-surface', label: 'Attack Surface', icon: '⚠' });
|
|
827
|
-
panels.push(`<div id="dtab-attack-surface" class="diagram-panel"
|
|
2032
|
+
panels.push(`<div id="dtab-attack-surface" class="diagram-panel">
|
|
2033
|
+
<div class="diagram-shell">
|
|
2034
|
+
<div class="diagram-toolbar">
|
|
2035
|
+
<span class="diagram-title">Attack Surface</span>
|
|
2036
|
+
${diagramActions}
|
|
2037
|
+
</div>
|
|
2038
|
+
<div class="mermaid-wrap"><pre class="mermaid">\n${esc(attackSurface)}\n</pre></div>
|
|
2039
|
+
<div class="diagram-meta">Exposures per asset, severity-coloured. <strong>💥 confirmed</strong>, <strong>⚠️ open</strong>, <strong>✅ mitigated</strong>, <strong>🟦 accepted</strong>.</div>
|
|
2040
|
+
</div>
|
|
2041
|
+
</div>`);
|
|
828
2042
|
}
|
|
829
2043
|
if (tabs.length === 0) {
|
|
830
2044
|
return `<div id="sec-diagrams" class="section-content">
|
|
@@ -835,7 +2049,7 @@ function renderDiagramsPage(threatGraph, dataFlow, attackSurface) {
|
|
|
835
2049
|
return `
|
|
836
2050
|
<div id="sec-diagrams" class="section-content">
|
|
837
2051
|
<div class="sec-h"><span class="sec-icon">◉</span> Diagrams</div>
|
|
838
|
-
<p
|
|
2052
|
+
<p class="diagram-hint">Interactive diagrams generated from annotations. The topology view supports search, filtering, drag, pan, and node inspection.</p>
|
|
839
2053
|
<div class="diagram-tabs">
|
|
840
2054
|
${tabs.map((t, i) => `<button class="diagram-tab${i === 0 ? ' active' : ''}" onclick="switchDiagramTab('${t.id}', this)">${t.icon} ${t.label}</button>`).join('')}
|
|
841
2055
|
</div>
|
|
@@ -853,7 +2067,7 @@ function renderCodePage(fileAnnotations, model) {
|
|
|
853
2067
|
Every file with GuardLink annotations. Click any annotation to see details.
|
|
854
2068
|
</p>
|
|
855
2069
|
${fileAnnotations.length > 0 ? fileAnnotations.map((f, fi) => `
|
|
856
|
-
<div class="file-card">
|
|
2070
|
+
<div class="file-card" data-ff="${esc(f.file)}">
|
|
857
2071
|
<div class="file-card-header" onclick="toggleFile(this)">
|
|
858
2072
|
<span class="file-path">${esc(f.file)}</span>
|
|
859
2073
|
<span style="display:flex;align-items:center;gap:.4rem">
|
|
@@ -904,7 +2118,7 @@ function renderDataPage(model) {
|
|
|
904
2118
|
<thead><tr><th>Side A</th><th></th><th>Side B</th><th>Description</th><th>Location</th></tr></thead>
|
|
905
2119
|
<tbody>
|
|
906
2120
|
${model.boundaries.map(b => `
|
|
907
|
-
<tr>
|
|
2121
|
+
<tr data-ff="${b.location ? esc(b.location.file) : ''}">
|
|
908
2122
|
<td><code>${esc(b.asset_a)}</code></td>
|
|
909
2123
|
<td style="color:var(--purple)">↔</td>
|
|
910
2124
|
<td><code>${esc(b.asset_b)}</code></td>
|
|
@@ -920,7 +2134,7 @@ function renderDataPage(model) {
|
|
|
920
2134
|
<thead><tr><th>Classification</th><th>Asset</th><th>Description</th><th>Location</th></tr></thead>
|
|
921
2135
|
<tbody>
|
|
922
2136
|
${model.data_handling.map(d => `
|
|
923
|
-
<tr>
|
|
2137
|
+
<tr data-ff="${d.location ? esc(d.location.file) : ''}">
|
|
924
2138
|
<td><span class="ann-badge ann-data">${esc(d.classification)}</span></td>
|
|
925
2139
|
<td><code>${esc(d.asset || '—')}</code></td>
|
|
926
2140
|
<td>${esc(d.description || '—')}</td>
|
|
@@ -935,7 +2149,7 @@ function renderDataPage(model) {
|
|
|
935
2149
|
<thead><tr><th>Control</th><th>Asset</th><th>Description</th><th>Location</th></tr></thead>
|
|
936
2150
|
<tbody>
|
|
937
2151
|
${model.validations.map(v => `
|
|
938
|
-
<tr>
|
|
2152
|
+
<tr data-ff="${v.location ? esc(v.location.file) : ''}">
|
|
939
2153
|
<td><code>${esc(v.control)}</code></td>
|
|
940
2154
|
<td><code>${esc(v.asset)}</code></td>
|
|
941
2155
|
<td>${esc(v.description || '—')}</td>
|
|
@@ -950,7 +2164,7 @@ function renderDataPage(model) {
|
|
|
950
2164
|
<thead><tr><th>Asset</th><th>Owner</th><th>Description</th><th>Location</th></tr></thead>
|
|
951
2165
|
<tbody>
|
|
952
2166
|
${model.ownership.map(o => `
|
|
953
|
-
<tr>
|
|
2167
|
+
<tr data-ff="${o.location ? esc(o.location.file) : ''}">
|
|
954
2168
|
<td><code>${esc(o.asset)}</code></td>
|
|
955
2169
|
<td><strong>${esc(o.owner)}</strong></td>
|
|
956
2170
|
<td>${esc(o.description || '—')}</td>
|
|
@@ -965,7 +2179,7 @@ function renderDataPage(model) {
|
|
|
965
2179
|
<thead><tr><th>Asset</th><th>Description</th><th>Location</th></tr></thead>
|
|
966
2180
|
<tbody>
|
|
967
2181
|
${model.audits.map(a => `
|
|
968
|
-
<tr>
|
|
2182
|
+
<tr data-ff="${a.location ? esc(a.location.file) : ''}">
|
|
969
2183
|
<td><code>${esc(a.asset)}</code></td>
|
|
970
2184
|
<td>${esc(a.description || 'Needs review')}</td>
|
|
971
2185
|
<td class="loc">${a.location ? `${esc(a.location.file)}:${a.location.line}` : ''}</td>
|
|
@@ -980,7 +2194,7 @@ function renderDataPage(model) {
|
|
|
980
2194
|
<thead><tr><th>Asset</th><th>Assumption</th><th>Location</th></tr></thead>
|
|
981
2195
|
<tbody>
|
|
982
2196
|
${model.assumptions.map(a => `
|
|
983
|
-
<tr>
|
|
2197
|
+
<tr data-ff="${a.location ? esc(a.location.file) : ''}">
|
|
984
2198
|
<td><code>${esc(a.asset)}</code></td>
|
|
985
2199
|
<td>${esc(a.description || 'Unverified assumption')}</td>
|
|
986
2200
|
<td class="loc">${a.location ? `${esc(a.location.file)}:${a.location.line}` : ''}</td>
|
|
@@ -995,7 +2209,7 @@ function renderDataPage(model) {
|
|
|
995
2209
|
<thead><tr><th>Reason</th><th>Location</th></tr></thead>
|
|
996
2210
|
<tbody>
|
|
997
2211
|
${model.shields.map(s => `
|
|
998
|
-
<tr>
|
|
2212
|
+
<tr data-ff="${s.location ? esc(s.location.file) : ''}">
|
|
999
2213
|
<td>${esc(s.reason || 'No reason provided')}</td>
|
|
1000
2214
|
<td class="loc">${s.location ? `${esc(s.location.file)}:${s.location.line}` : ''}</td>
|
|
1001
2215
|
</tr>`).join('')}
|
|
@@ -1008,7 +2222,7 @@ function renderDataPage(model) {
|
|
|
1008
2222
|
<thead><tr><th>Comment</th><th>Location</th></tr></thead>
|
|
1009
2223
|
<tbody>
|
|
1010
2224
|
${model.comments.map(c => `
|
|
1011
|
-
<tr>
|
|
2225
|
+
<tr data-ff="${c.location ? esc(c.location.file) : ''}">
|
|
1012
2226
|
<td>${esc(c.description || '(no description)')}</td>
|
|
1013
2227
|
<td class="loc">${c.location ? `${esc(c.location.file)}:${c.location.line}` : ''}</td>
|
|
1014
2228
|
</tr>`).join('')}
|
|
@@ -1029,7 +2243,7 @@ function renderAssetsPage(heatmap) {
|
|
|
1029
2243
|
${heatmap.length > 0 ? `
|
|
1030
2244
|
<div class="heatmap">
|
|
1031
2245
|
${heatmap.map((a, i) => `
|
|
1032
|
-
<div class="heatmap-cell risk-cell-${a.riskLevel} clickable" onclick="openDrawer('asset', ${i})">
|
|
2246
|
+
<div class="heatmap-cell risk-cell-${a.riskLevel} clickable" data-ff-asset="${esc(a.name)}" onclick="openDrawer('asset', ${i})">
|
|
1033
2247
|
<div class="heatmap-name">${esc(a.name)}</div>
|
|
1034
2248
|
<div class="heatmap-stats">
|
|
1035
2249
|
<span title="Exposures">⚠ ${a.exposures}</span>
|
|
@@ -1086,6 +2300,8 @@ function buildFileAnnotations(model, root) {
|
|
|
1086
2300
|
addEntry('control', c, c.name);
|
|
1087
2301
|
for (const e of model.exposures)
|
|
1088
2302
|
addEntry('exposes', e, `${e.asset} → ${e.threat}`);
|
|
2303
|
+
for (const cf of (model.confirmed || []))
|
|
2304
|
+
addEntry('confirmed', cf, `${cf.asset} confirmed ${cf.threat}`);
|
|
1089
2305
|
for (const m of model.mitigations)
|
|
1090
2306
|
addEntry('mitigates', m, `${m.control} mitigates ${m.threat}`);
|
|
1091
2307
|
for (const a of model.acceptances)
|
|
@@ -1150,7 +2366,9 @@ function sevClass(s) {
|
|
|
1150
2366
|
return 'low';
|
|
1151
2367
|
return 'unset';
|
|
1152
2368
|
}
|
|
1153
|
-
function computeRiskGrade(sev, unmitigatedCount, totalExposures) {
|
|
2369
|
+
function computeRiskGrade(sev, unmitigatedCount, totalExposures, confirmedCount = 0) {
|
|
2370
|
+
if (confirmedCount > 0)
|
|
2371
|
+
return { grade: 'F', label: 'Critical Risk', summary: `${confirmedCount} confirmed exploitable finding(s) — immediate remediation required` };
|
|
1154
2372
|
if (sev.critical > 0)
|
|
1155
2373
|
return { grade: 'F', label: 'Critical Risk', summary: `${sev.critical} critical exposure(s) require immediate attention` };
|
|
1156
2374
|
if (sev.high >= 3 || unmitigatedCount >= 5)
|
|
@@ -1167,248 +2385,1302 @@ function computeRiskGrade(sev, unmitigatedCount, totalExposures) {
|
|
|
1167
2385
|
const CSS_CONTENT = `
|
|
1168
2386
|
/* ── Reset ── */
|
|
1169
2387
|
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
1170
|
-
|
|
2388
|
+
*::selection { background: color-mix(in oklab, var(--accent) 35%, transparent); color: var(--text); }
|
|
2389
|
+
:root {
|
|
2390
|
+
--font-ui: 'Inter', system-ui, -apple-system, sans-serif;
|
|
2391
|
+
--font-mono: 'JetBrains Mono', 'SF Mono', Menlo, Consolas, monospace;
|
|
2392
|
+
--ease: cubic-bezier(.2,.8,.2,1);
|
|
2393
|
+
--radius-sm: 6px; --radius-md: 8px; --radius-lg: 12px;
|
|
2394
|
+
--drawer-w: 440px; --sidebar-w: 220px;
|
|
2395
|
+
}
|
|
1171
2396
|
|
|
1172
|
-
/* ══ DARK THEME
|
|
2397
|
+
/* ══ DARK THEME — Modern deep-slate ══ */
|
|
1173
2398
|
[data-theme="dark"] {
|
|
1174
|
-
--bg: #
|
|
1175
|
-
--
|
|
1176
|
-
|
|
1177
|
-
--
|
|
1178
|
-
--
|
|
1179
|
-
--
|
|
1180
|
-
--
|
|
1181
|
-
--
|
|
1182
|
-
--
|
|
1183
|
-
|
|
1184
|
-
--
|
|
1185
|
-
--
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
2399
|
+
--bg: #000000;
|
|
2400
|
+
--bg-gradient: radial-gradient(ellipse 1000px 500px at 15% 0%, rgba(51,212,157,.09), transparent 58%),
|
|
2401
|
+
radial-gradient(ellipse 800px 400px at 95% 10%, rgba(3,96,162,.10), transparent 60%);
|
|
2402
|
+
--surface: #0f1b20;
|
|
2403
|
+
--surface2: #172930;
|
|
2404
|
+
--surface3: #1f3943;
|
|
2405
|
+
--border: #1f3943;
|
|
2406
|
+
--border-subtle: #1a3139;
|
|
2407
|
+
--border-strong: #55899e;
|
|
2408
|
+
|
|
2409
|
+
--text: #f0f0f0;
|
|
2410
|
+
--muted: #55899e;
|
|
2411
|
+
--text-dim: #3b6779;
|
|
2412
|
+
|
|
2413
|
+
--accent: #33d49d;
|
|
2414
|
+
--accent-soft: rgba(51,212,157,.12);
|
|
2415
|
+
--accent-dim: rgba(51,212,157,.22);
|
|
2416
|
+
--accent-hover: #ffffff;
|
|
2417
|
+
|
|
2418
|
+
--blue: #0360a2;
|
|
2419
|
+
--green: #33d49d;
|
|
2420
|
+
--red: #ea1d1d;
|
|
2421
|
+
--orange: #ea1d1d;
|
|
2422
|
+
--yellow: #55899e;
|
|
2423
|
+
--purple: #0360a2;
|
|
2424
|
+
|
|
2425
|
+
--sev-crit: #ea1d1d;
|
|
2426
|
+
--sev-high: #ea1d1d;
|
|
2427
|
+
--sev-med: #55899e;
|
|
2428
|
+
--sev-low: #0360a2;
|
|
2429
|
+
--sev-unset:#3b6779;
|
|
2430
|
+
|
|
2431
|
+
--sev-crit-bg: rgba(234,29,29,.14);
|
|
2432
|
+
--sev-high-bg: rgba(234,29,29,.10);
|
|
2433
|
+
--sev-med-bg: rgba(85,137,158,.16);
|
|
2434
|
+
--sev-low-bg: rgba(3,96,162,.16);
|
|
2435
|
+
|
|
2436
|
+
--badge-red-bg: rgba(234,29,29,.16);
|
|
2437
|
+
--badge-green-bg: rgba(51,212,157,.16);
|
|
2438
|
+
--badge-blue-bg: rgba(3,96,162,.16);
|
|
2439
|
+
--badge-red-fg: #f0f0f0;
|
|
2440
|
+
--badge-green-fg: #33d49d;
|
|
2441
|
+
--badge-blue-fg: #f0f0f0;
|
|
2442
|
+
|
|
2443
|
+
--risk-f: linear-gradient(135deg, rgba(234,29,29,.20), rgba(234,29,29,.05));
|
|
2444
|
+
--risk-d: linear-gradient(135deg, rgba(234,29,29,.14), rgba(234,29,29,.04));
|
|
2445
|
+
--risk-c: linear-gradient(135deg, rgba(85,137,158,.18), rgba(85,137,158,.05));
|
|
2446
|
+
--risk-b: linear-gradient(135deg, rgba(3,96,162,.18), rgba(3,96,162,.05));
|
|
2447
|
+
--risk-a: linear-gradient(135deg, rgba(51,212,157,.20), rgba(51,212,157,.05));
|
|
2448
|
+
--risk-border-f: rgba(234,29,29,.40);
|
|
2449
|
+
--risk-border-d: rgba(234,29,29,.28);
|
|
2450
|
+
--risk-border-c: rgba(85,137,158,.35);
|
|
2451
|
+
--risk-border-b: rgba(3,96,162,.35);
|
|
2452
|
+
--risk-border-a: rgba(51,212,157,.35);
|
|
2453
|
+
|
|
2454
|
+
--heatmap-crit: linear-gradient(135deg, rgba(234,29,29,.20), rgba(234,29,29,.05));
|
|
2455
|
+
--heatmap-high: linear-gradient(135deg, rgba(234,29,29,.14), rgba(234,29,29,.04));
|
|
2456
|
+
--heatmap-med: linear-gradient(135deg, rgba(85,137,158,.16), rgba(85,137,158,.04));
|
|
2457
|
+
--heatmap-low: linear-gradient(135deg, rgba(3,96,162,.16), rgba(3,96,162,.04));
|
|
2458
|
+
--heatmap-none: #172930;
|
|
2459
|
+
|
|
2460
|
+
--table-alt: #13232a;
|
|
2461
|
+
--table-hover: #1c323a;
|
|
2462
|
+
--shadow-sm: 0 1px 2px rgba(0,0,0,.3);
|
|
2463
|
+
--shadow-md: 0 4px 12px rgba(0,0,0,.35), 0 1px 2px rgba(0,0,0,.4);
|
|
2464
|
+
--shadow-lg: 0 12px 32px rgba(0,0,0,.45), 0 2px 6px rgba(0,0,0,.4);
|
|
2465
|
+
--glow-accent: 0 0 0 1px rgba(45,212,191,.25), 0 8px 24px rgba(45,212,191,.12);
|
|
2466
|
+
|
|
2467
|
+
--logo-bg: linear-gradient(135deg, #33d49d, #0360a2);
|
|
2468
|
+
--logo-text: #000000;
|
|
2469
|
+
}
|
|
2470
|
+
|
|
2471
|
+
/* ══ LIGHT THEME — Refined off-white ══ */
|
|
1189
2472
|
[data-theme="light"] {
|
|
1190
|
-
--bg: #
|
|
1191
|
-
--
|
|
1192
|
-
|
|
1193
|
-
--
|
|
1194
|
-
--
|
|
1195
|
-
--
|
|
1196
|
-
--
|
|
1197
|
-
--
|
|
1198
|
-
--
|
|
1199
|
-
|
|
1200
|
-
--
|
|
1201
|
-
--
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
2473
|
+
--bg: #f0f0f0;
|
|
2474
|
+
--bg-gradient: radial-gradient(ellipse 1000px 500px at 15% 0%, rgba(51,212,157,.10), transparent 58%),
|
|
2475
|
+
radial-gradient(ellipse 800px 400px at 95% 10%, rgba(3,96,162,.08), transparent 60%);
|
|
2476
|
+
--surface: #ffffff;
|
|
2477
|
+
--surface2: #f0f0f0;
|
|
2478
|
+
--surface3: #e8eef0;
|
|
2479
|
+
--border: #55899e;
|
|
2480
|
+
--border-subtle: #d9e4e8;
|
|
2481
|
+
--border-strong: #1f3943;
|
|
2482
|
+
|
|
2483
|
+
--text: #1f3943;
|
|
2484
|
+
--muted: #55899e;
|
|
2485
|
+
--text-dim: #3b6779;
|
|
2486
|
+
|
|
2487
|
+
--accent: #33d49d;
|
|
2488
|
+
--accent-soft: rgba(51,212,157,.12);
|
|
2489
|
+
--accent-dim: rgba(51,212,157,.20);
|
|
2490
|
+
--accent-hover: #0360a2;
|
|
2491
|
+
|
|
2492
|
+
--blue: #0360a2;
|
|
2493
|
+
--green: #33d49d;
|
|
2494
|
+
--red: #ea1d1d;
|
|
2495
|
+
--orange: #ea1d1d;
|
|
2496
|
+
--yellow: #55899e;
|
|
2497
|
+
--purple: #0360a2;
|
|
2498
|
+
|
|
2499
|
+
--sev-crit: #ea1d1d;
|
|
2500
|
+
--sev-high: #ea1d1d;
|
|
2501
|
+
--sev-med: #3b6779;
|
|
2502
|
+
--sev-low: #0360a2;
|
|
2503
|
+
--sev-unset:#55899e;
|
|
2504
|
+
|
|
2505
|
+
--sev-crit-bg: rgba(234,29,29,.10);
|
|
2506
|
+
--sev-high-bg: rgba(234,29,29,.08);
|
|
2507
|
+
--sev-med-bg: rgba(59,103,121,.10);
|
|
2508
|
+
--sev-low-bg: rgba(3,96,162,.10);
|
|
2509
|
+
|
|
2510
|
+
--badge-red-bg: rgba(234,29,29,.12);
|
|
2511
|
+
--badge-green-bg: rgba(51,212,157,.14);
|
|
2512
|
+
--badge-blue-bg: rgba(3,96,162,.14);
|
|
2513
|
+
--badge-red-fg: #ea1d1d;
|
|
2514
|
+
--badge-green-fg: #1f3943;
|
|
2515
|
+
--badge-blue-fg: #0360a2;
|
|
2516
|
+
|
|
2517
|
+
--risk-f: linear-gradient(135deg, rgba(234,29,29,.12), rgba(234,29,29,.02));
|
|
2518
|
+
--risk-d: linear-gradient(135deg, rgba(234,29,29,.09), rgba(234,29,29,.02));
|
|
2519
|
+
--risk-c: linear-gradient(135deg, rgba(59,103,121,.12), rgba(59,103,121,.02));
|
|
2520
|
+
--risk-b: linear-gradient(135deg, rgba(3,96,162,.12), rgba(3,96,162,.02));
|
|
2521
|
+
--risk-a: linear-gradient(135deg, rgba(51,212,157,.14), rgba(51,212,157,.02));
|
|
2522
|
+
--risk-border-f: rgba(234,29,29,.28);
|
|
2523
|
+
--risk-border-d: rgba(234,29,29,.2);
|
|
2524
|
+
--risk-border-c: rgba(59,103,121,.26);
|
|
2525
|
+
--risk-border-b: rgba(3,96,162,.24);
|
|
2526
|
+
--risk-border-a: rgba(51,212,157,.28);
|
|
2527
|
+
|
|
2528
|
+
--heatmap-crit: linear-gradient(135deg, rgba(234,29,29,.14), rgba(234,29,29,.02));
|
|
2529
|
+
--heatmap-high: linear-gradient(135deg, rgba(234,29,29,.10), rgba(234,29,29,.02));
|
|
2530
|
+
--heatmap-med: linear-gradient(135deg, rgba(59,103,121,.10), rgba(59,103,121,.02));
|
|
2531
|
+
--heatmap-low: linear-gradient(135deg, rgba(3,96,162,.10), rgba(3,96,162,.02));
|
|
2532
|
+
--heatmap-none: #f0f0f0;
|
|
2533
|
+
|
|
2534
|
+
--table-alt: #f7f9fa;
|
|
2535
|
+
--table-hover: #edf3f5;
|
|
2536
|
+
--shadow-sm: 0 1px 2px rgba(15,23,42,.04);
|
|
2537
|
+
--shadow-md: 0 4px 12px rgba(15,23,42,.06), 0 1px 2px rgba(15,23,42,.04);
|
|
2538
|
+
--shadow-lg: 0 12px 32px rgba(15,23,42,.08), 0 2px 6px rgba(15,23,42,.04);
|
|
2539
|
+
--glow-accent: 0 0 0 1px rgba(13,148,136,.20), 0 8px 24px rgba(13,148,136,.10);
|
|
2540
|
+
|
|
2541
|
+
--logo-bg: linear-gradient(135deg, #33d49d, #0360a2);
|
|
2542
|
+
--logo-text: #ffffff;
|
|
2543
|
+
}
|
|
2544
|
+
|
|
2545
|
+
html, body { height: 100%; }
|
|
2546
|
+
body {
|
|
2547
|
+
font-family: var(--font-ui);
|
|
2548
|
+
background: var(--bg-gradient), var(--bg);
|
|
2549
|
+
color: var(--text);
|
|
2550
|
+
line-height: 1.5;
|
|
2551
|
+
font-size: 13.5px;
|
|
2552
|
+
overflow: hidden;
|
|
2553
|
+
-webkit-font-smoothing: antialiased;
|
|
2554
|
+
-moz-osx-font-smoothing: grayscale;
|
|
2555
|
+
letter-spacing: -0.005em;
|
|
2556
|
+
}
|
|
2557
|
+
a { color: var(--accent); text-decoration: none; transition: color .15s var(--ease); }
|
|
2558
|
+
a:hover { color: var(--accent-hover); }
|
|
2559
|
+
code {
|
|
2560
|
+
background: var(--surface2);
|
|
2561
|
+
border: 1px solid var(--border-subtle);
|
|
2562
|
+
padding: 1px 5px;
|
|
2563
|
+
border-radius: 4px;
|
|
2564
|
+
font-size: .76rem;
|
|
2565
|
+
font-family: var(--font-mono);
|
|
2566
|
+
color: var(--text);
|
|
2567
|
+
}
|
|
2568
|
+
::-webkit-scrollbar { width: 10px; height: 10px; }
|
|
2569
|
+
::-webkit-scrollbar-track { background: transparent; }
|
|
2570
|
+
::-webkit-scrollbar-thumb { background: var(--border); border-radius: 10px; border: 2px solid var(--bg); }
|
|
2571
|
+
::-webkit-scrollbar-thumb:hover { background: var(--border-strong); }
|
|
1207
2572
|
|
|
1208
2573
|
/* ── Top Nav ── */
|
|
1209
|
-
.topnav {
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
2574
|
+
.topnav {
|
|
2575
|
+
height: 52px;
|
|
2576
|
+
background: color-mix(in oklab, var(--surface) 92%, transparent);
|
|
2577
|
+
backdrop-filter: saturate(160%) blur(12px);
|
|
2578
|
+
-webkit-backdrop-filter: saturate(160%) blur(12px);
|
|
2579
|
+
border-bottom: 1px solid var(--border);
|
|
2580
|
+
display: flex; align-items: center;
|
|
2581
|
+
padding: 0 1.4rem; gap: 1rem;
|
|
2582
|
+
z-index: 100;
|
|
2583
|
+
position: relative;
|
|
2584
|
+
}
|
|
2585
|
+
.topnav::after {
|
|
2586
|
+
content: ''; position: absolute; left: 0; right: 0; bottom: -1px; height: 1px;
|
|
2587
|
+
background: linear-gradient(90deg, transparent, var(--accent-dim) 40%, var(--accent-dim) 60%, transparent);
|
|
2588
|
+
opacity: .55; pointer-events: none;
|
|
2589
|
+
}
|
|
2590
|
+
.topnav-left { display: flex; align-items: center; gap: .7rem; }
|
|
2591
|
+
.topnav-right { margin-left: auto; display: flex; align-items: center; gap: .75rem; }
|
|
2592
|
+
.topnav-metrics { display: flex; align-items: center; gap: .5rem; }
|
|
2593
|
+
.topnav h1 { font-size: 1.02rem; font-weight: 650; white-space: nowrap; letter-spacing: -0.01em; }
|
|
2594
|
+
.badge {
|
|
2595
|
+
background: var(--accent-soft);
|
|
2596
|
+
color: var(--accent);
|
|
2597
|
+
border: 1px solid var(--accent-dim);
|
|
2598
|
+
padding: 2px 9px; border-radius: 999px;
|
|
2599
|
+
font-size: .64rem; font-weight: 600;
|
|
2600
|
+
text-transform: uppercase; letter-spacing: .6px;
|
|
2601
|
+
}
|
|
2602
|
+
.tn-stat {
|
|
2603
|
+
font-size: .72rem; color: var(--muted); display: flex; align-items: center; gap: 6px;
|
|
2604
|
+
background: color-mix(in oklab, var(--surface2) 88%, transparent);
|
|
2605
|
+
border: 1px solid var(--border);
|
|
2606
|
+
border-radius: 999px;
|
|
2607
|
+
padding: 4px 10px;
|
|
2608
|
+
backdrop-filter: blur(4px);
|
|
2609
|
+
}
|
|
2610
|
+
.tn-stat .tn-k { letter-spacing: .2px; }
|
|
2611
|
+
.tn-stat .tn-v { font-weight: 700; font-size: .85rem; color: var(--text); font-variant-numeric: tabular-nums; }
|
|
2612
|
+
.tn-v.red { color: var(--sev-crit); } .tn-v.green { color: var(--green); }
|
|
2613
|
+
.tn-v.blue { color: var(--accent); } .tn-v.yellow { color: var(--yellow); }
|
|
2614
|
+
.feature-filter-wrap { display: flex; align-items: center; }
|
|
2615
|
+
.feature-filter-select {
|
|
2616
|
+
max-width: 170px;
|
|
2617
|
+
background: var(--surface2);
|
|
2618
|
+
color: var(--text);
|
|
2619
|
+
border: 1px solid var(--border);
|
|
2620
|
+
border-radius: 999px;
|
|
2621
|
+
padding: 5px 10px;
|
|
2622
|
+
font-size: .74rem;
|
|
2623
|
+
font-family: var(--font-ui);
|
|
2624
|
+
cursor: pointer;
|
|
2625
|
+
transition: all .15s var(--ease);
|
|
2626
|
+
}
|
|
2627
|
+
.feature-filter-select:hover { border-color: var(--border-strong); background: var(--surface3); }
|
|
2628
|
+
.feature-filter-select:focus { outline: none; border-color: var(--accent); box-shadow: var(--glow-accent); }
|
|
2629
|
+
.logo {
|
|
2630
|
+
width: 34px; height: 34px;
|
|
2631
|
+
background: var(--logo-bg); color: var(--logo-text);
|
|
2632
|
+
border-radius: 9px;
|
|
2633
|
+
display: flex; align-items: center; justify-content: center;
|
|
2634
|
+
font-weight: 700; font-size: 12px; letter-spacing: .3px;
|
|
2635
|
+
box-shadow: var(--shadow-sm);
|
|
2636
|
+
}
|
|
2637
|
+
#themeToggle {
|
|
2638
|
+
background: var(--surface2); border: 1px solid var(--border);
|
|
2639
|
+
border-radius: 8px; padding: 5px 9px; cursor: pointer;
|
|
2640
|
+
font-size: 14px; line-height: 1; color: var(--text);
|
|
2641
|
+
transition: all .15s var(--ease);
|
|
2642
|
+
}
|
|
2643
|
+
#themeToggle:hover { background: var(--surface3); border-color: var(--border-strong); }
|
|
1220
2644
|
[data-theme="dark"] .icon-sun { display: none; }
|
|
1221
2645
|
[data-theme="light"] .icon-moon { display: none; }
|
|
2646
|
+
.feature-banner {
|
|
2647
|
+
display: none;
|
|
2648
|
+
align-items: center;
|
|
2649
|
+
gap: 8px;
|
|
2650
|
+
padding: 8px 16px;
|
|
2651
|
+
background: color-mix(in oklab, var(--accent) 88%, var(--surface));
|
|
2652
|
+
color: #fff;
|
|
2653
|
+
font-size: .8rem;
|
|
2654
|
+
font-weight: 600;
|
|
2655
|
+
border-bottom: 1px solid color-mix(in oklab, var(--accent) 45%, var(--border));
|
|
2656
|
+
}
|
|
2657
|
+
.feature-banner-files { opacity: .75; font-weight: 500; }
|
|
2658
|
+
.feature-banner-clear {
|
|
2659
|
+
margin-left: auto;
|
|
2660
|
+
background: rgba(255,255,255,.18);
|
|
2661
|
+
border: 1px solid rgba(255,255,255,.28);
|
|
2662
|
+
color: #fff;
|
|
2663
|
+
padding: 4px 10px;
|
|
2664
|
+
border-radius: 999px;
|
|
2665
|
+
cursor: pointer;
|
|
2666
|
+
font-size: .72rem;
|
|
2667
|
+
font-weight: 600;
|
|
2668
|
+
}
|
|
2669
|
+
.feature-banner-clear:hover { background: rgba(255,255,255,.28); }
|
|
1222
2670
|
|
|
1223
2671
|
/* ── Layout ── */
|
|
1224
|
-
.layout { display: flex; height: calc(100vh -
|
|
1225
|
-
.sidebar {
|
|
1226
|
-
|
|
1227
|
-
|
|
2672
|
+
.layout { display: flex; height: calc(100vh - 54px); position: relative; }
|
|
2673
|
+
.sidebar {
|
|
2674
|
+
width: var(--sidebar-w); min-width: var(--sidebar-w);
|
|
2675
|
+
background: var(--surface);
|
|
2676
|
+
border-right: 1px solid var(--border);
|
|
2677
|
+
display: flex; flex-direction: column;
|
|
2678
|
+
transition: width .25s var(--ease), min-width .25s var(--ease);
|
|
2679
|
+
}
|
|
2680
|
+
.sidebar-nav { flex: 1; overflow-y: auto; padding: .75rem .55rem; }
|
|
2681
|
+
.sidebar.collapsed { width: 52px; min-width: 52px; }
|
|
1228
2682
|
.sidebar.collapsed .nav-text { display: none; }
|
|
1229
2683
|
.sidebar.collapsed .sep { margin: .5rem .5rem; }
|
|
1230
2684
|
.sidebar.collapsed .chevron-left { display: none; }
|
|
1231
2685
|
.sidebar.collapsed .chevron-right { display: block; }
|
|
1232
|
-
#sidebarToggle {
|
|
1233
|
-
|
|
2686
|
+
#sidebarToggle {
|
|
2687
|
+
background: transparent; border: none; border-top: 1px solid var(--border);
|
|
2688
|
+
padding: .7rem; cursor: pointer; color: var(--muted);
|
|
2689
|
+
transition: all .15s var(--ease);
|
|
2690
|
+
display: flex; align-items: center; justify-content: center; width: 100%;
|
|
2691
|
+
}
|
|
2692
|
+
#sidebarToggle:hover { background: var(--surface2); color: var(--accent); }
|
|
1234
2693
|
#sidebarToggle svg { display: block; }
|
|
1235
2694
|
#sidebarToggle .chevron-right { display: none; }
|
|
1236
|
-
.sidebar a {
|
|
2695
|
+
.sidebar a {
|
|
2696
|
+
display: flex; align-items: center; gap: .65rem;
|
|
2697
|
+
padding: .52rem .75rem; margin: 1px .1rem;
|
|
2698
|
+
font-size: .8rem; color: var(--muted); cursor: pointer;
|
|
2699
|
+
border-radius: 7px;
|
|
2700
|
+
transition: background .12s var(--ease), color .12s var(--ease);
|
|
2701
|
+
user-select: none;
|
|
2702
|
+
position: relative;
|
|
2703
|
+
}
|
|
1237
2704
|
.sidebar a:hover { background: var(--surface2); color: var(--text); }
|
|
1238
|
-
.sidebar a.active {
|
|
1239
|
-
|
|
2705
|
+
.sidebar a.active {
|
|
2706
|
+
color: var(--accent);
|
|
2707
|
+
background: var(--accent-soft);
|
|
2708
|
+
font-weight: 550;
|
|
2709
|
+
}
|
|
2710
|
+
.sidebar a.active::before {
|
|
2711
|
+
content: ''; position: absolute; left: -.1rem; top: 20%; bottom: 20%; width: 2px;
|
|
2712
|
+
background: var(--accent); border-radius: 2px;
|
|
2713
|
+
}
|
|
2714
|
+
.sidebar .nav-icon { width: 18px; display: flex; align-items: center; justify-content: center; flex-shrink: 0; opacity: .85; }
|
|
2715
|
+
.sidebar a.active .nav-icon { opacity: 1; }
|
|
1240
2716
|
.sidebar .nav-icon svg { display: block; }
|
|
1241
|
-
.sidebar .sep { height: 1px; background: var(--border); margin: .
|
|
2717
|
+
.sidebar .sep { height: 1px; background: var(--border); margin: .6rem .8rem; }
|
|
1242
2718
|
.main { flex: 1; overflow-y: auto; padding: 0; }
|
|
1243
|
-
.section-content { display: none; padding: 1.2rem
|
|
2719
|
+
.section-content { display: none; padding: 1.6rem 2rem 3rem; max-width: 1400px; }
|
|
2720
|
+
.section-content.active { display: block; animation: fadeIn .2s var(--ease); }
|
|
2721
|
+
@keyframes fadeIn { from { opacity: 0; transform: translateY(3px); } to { opacity: 1; transform: none; } }
|
|
2722
|
+
.panel {
|
|
2723
|
+
background: color-mix(in oklab, var(--surface) 94%, transparent);
|
|
2724
|
+
border: 1px solid var(--border);
|
|
2725
|
+
border-radius: var(--radius-lg);
|
|
2726
|
+
padding: 1rem 1.1rem;
|
|
2727
|
+
box-shadow: var(--shadow-sm);
|
|
2728
|
+
margin-bottom: 1rem;
|
|
2729
|
+
}
|
|
2730
|
+
.panel-row { display: flex; align-items: center; gap: .8rem; margin-bottom: .35rem; }
|
|
2731
|
+
.panel-muted { color: var(--muted); font-size: .82rem; }
|
|
2732
|
+
.summary-panels {
|
|
2733
|
+
display: grid;
|
|
2734
|
+
grid-template-columns: minmax(250px, 1fr) minmax(320px, 1.3fr);
|
|
2735
|
+
gap: .9rem;
|
|
2736
|
+
}
|
|
2737
|
+
.ai-analysis-panel { padding-top: .9rem; }
|
|
1244
2738
|
|
|
1245
2739
|
/* ── Drawer ── */
|
|
1246
|
-
.drawer-overlay {
|
|
1247
|
-
|
|
1248
|
-
|
|
2740
|
+
.drawer-overlay {
|
|
2741
|
+
position: fixed; inset: 0;
|
|
2742
|
+
background: rgba(0,0,0,.48);
|
|
2743
|
+
backdrop-filter: blur(2px);
|
|
2744
|
+
-webkit-backdrop-filter: blur(2px);
|
|
2745
|
+
z-index: 200; display: none;
|
|
2746
|
+
}
|
|
2747
|
+
.drawer-overlay.open { display: block; animation: fadeIn .15s var(--ease); }
|
|
2748
|
+
.drawer {
|
|
2749
|
+
position: fixed; top: 0; right: 0;
|
|
2750
|
+
width: var(--drawer-w); height: 100vh;
|
|
2751
|
+
background: var(--surface);
|
|
2752
|
+
border-left: 1px solid var(--border);
|
|
2753
|
+
z-index: 201;
|
|
2754
|
+
transform: translateX(100%);
|
|
2755
|
+
transition: transform .28s var(--ease);
|
|
2756
|
+
overflow-y: auto;
|
|
2757
|
+
box-shadow: var(--shadow-lg);
|
|
2758
|
+
}
|
|
1249
2759
|
.drawer.open { transform: translateX(0); }
|
|
1250
|
-
.drawer-header {
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
2760
|
+
.drawer-header {
|
|
2761
|
+
display: flex; align-items: center; justify-content: space-between;
|
|
2762
|
+
padding: 1rem 1.2rem;
|
|
2763
|
+
border-bottom: 1px solid var(--border);
|
|
2764
|
+
position: sticky; top: 0;
|
|
2765
|
+
background: color-mix(in oklab, var(--surface) 94%, transparent);
|
|
2766
|
+
backdrop-filter: blur(8px);
|
|
2767
|
+
-webkit-backdrop-filter: blur(8px);
|
|
2768
|
+
z-index: 1;
|
|
2769
|
+
}
|
|
2770
|
+
.drawer-header h3 { font-size: .95rem; color: var(--text); font-weight: 650; letter-spacing: -.01em; }
|
|
2771
|
+
.drawer-close {
|
|
2772
|
+
background: var(--surface2); border: 1px solid var(--border);
|
|
2773
|
+
color: var(--muted); cursor: pointer;
|
|
2774
|
+
padding: 5px 11px; border-radius: 6px; font-size: .78rem;
|
|
2775
|
+
transition: all .15s var(--ease);
|
|
2776
|
+
}
|
|
2777
|
+
.drawer-close:hover { color: var(--text); border-color: var(--border-strong); background: var(--surface3); }
|
|
2778
|
+
.drawer-body { padding: 1.2rem; }
|
|
2779
|
+
.d-section { margin-bottom: 1.2rem; }
|
|
2780
|
+
.d-label { font-size: .68rem; text-transform: uppercase; color: var(--muted); letter-spacing: .8px; margin-bottom: .35rem; font-weight: 600; }
|
|
2781
|
+
.d-value { font-size: .85rem; }
|
|
2782
|
+
.d-code {
|
|
2783
|
+
background: var(--bg); border: 1px solid var(--border); border-radius: var(--radius-md);
|
|
2784
|
+
padding: .65rem .8rem; font-family: var(--font-mono); font-size: .72rem;
|
|
2785
|
+
line-height: 1.65; color: var(--muted); white-space: pre; overflow-x: auto;
|
|
2786
|
+
}
|
|
1257
2787
|
|
|
1258
2788
|
/* ── Section headings ── */
|
|
1259
|
-
.sec-h {
|
|
1260
|
-
|
|
1261
|
-
|
|
2789
|
+
.sec-h {
|
|
2790
|
+
font-size: 1.25rem; font-weight: 700;
|
|
2791
|
+
margin-bottom: 1rem;
|
|
2792
|
+
display: flex; align-items: center; gap: .6rem;
|
|
2793
|
+
letter-spacing: -.02em;
|
|
2794
|
+
}
|
|
2795
|
+
.sec-icon {
|
|
2796
|
+
font-size: 1rem;
|
|
2797
|
+
width: 30px; height: 30px;
|
|
2798
|
+
display: inline-flex; align-items: center; justify-content: center;
|
|
2799
|
+
background: var(--accent-soft);
|
|
2800
|
+
color: var(--accent);
|
|
2801
|
+
border: 1px solid var(--accent-dim);
|
|
2802
|
+
border-radius: 8px;
|
|
2803
|
+
}
|
|
2804
|
+
.sub-h {
|
|
2805
|
+
font-size: .78rem; font-weight: 600; color: var(--muted);
|
|
2806
|
+
margin: 1.2rem 0 .55rem 0;
|
|
2807
|
+
text-transform: uppercase; letter-spacing: .7px;
|
|
2808
|
+
display: flex; align-items: center; gap: .5rem;
|
|
2809
|
+
}
|
|
2810
|
+
.sub-h::after {
|
|
2811
|
+
content: ''; flex: 1; height: 1px;
|
|
2812
|
+
background: linear-gradient(90deg, var(--border), transparent);
|
|
2813
|
+
}
|
|
2814
|
+
.sub-h-alert { color: var(--red); }
|
|
2815
|
+
.sub-h-critical { color: var(--sev-crit); }
|
|
2816
|
+
.sub-h-ok { color: var(--green); }
|
|
2817
|
+
.sub-h-neutral { color: var(--yellow); }
|
|
2818
|
+
.sub-h-info { color: var(--blue); }
|
|
2819
|
+
.section-note { color: var(--muted); font-size: .78rem; margin-bottom: .55rem; }
|
|
2820
|
+
.flow-arrow { color: var(--muted); font-weight: 700; font-size: .88rem; text-align: center; }
|
|
1262
2821
|
|
|
1263
2822
|
/* ── Stats Grid ── */
|
|
1264
|
-
.stats-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(
|
|
1265
|
-
.stat-card {
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
2823
|
+
.stats-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(118px, 1fr)); gap: .6rem; margin-bottom: 1.2rem; }
|
|
2824
|
+
.stat-card {
|
|
2825
|
+
background: var(--surface);
|
|
2826
|
+
border: 1px solid var(--border);
|
|
2827
|
+
border-radius: var(--radius-md);
|
|
2828
|
+
padding: .75rem .7rem;
|
|
2829
|
+
text-align: center;
|
|
2830
|
+
transition: transform .15s var(--ease), border-color .15s var(--ease), box-shadow .15s var(--ease);
|
|
2831
|
+
position: relative; overflow: hidden;
|
|
2832
|
+
}
|
|
2833
|
+
.stat-card::before {
|
|
2834
|
+
content: ''; position: absolute; inset: 0 0 auto 0; height: 2px;
|
|
2835
|
+
background: var(--accent); opacity: 0; transition: opacity .15s var(--ease);
|
|
2836
|
+
}
|
|
2837
|
+
.stat-card:hover { transform: translateY(-1px); border-color: var(--border-strong); box-shadow: var(--shadow-sm); }
|
|
2838
|
+
.stat-card:hover::before { opacity: .8; }
|
|
2839
|
+
.stat-card .value { font-size: 1.5rem; font-weight: 700; color: var(--accent); font-variant-numeric: tabular-nums; letter-spacing: -.02em; line-height: 1.1; }
|
|
2840
|
+
.stat-card .label { font-size: .68rem; color: var(--muted); margin-top: 4px; text-transform: uppercase; letter-spacing: .4px; font-weight: 500; }
|
|
2841
|
+
.stat-danger .value { color: var(--sev-crit); } .stat-danger::before { background: var(--sev-crit); }
|
|
2842
|
+
.stat-success .value { color: var(--green); } .stat-success::before { background: var(--green); }
|
|
2843
|
+
.stat-muted .value { color: var(--muted); } .stat-muted::before { background: var(--muted); }
|
|
2844
|
+
.stat-muted .label { color: var(--text-dim); }
|
|
1270
2845
|
|
|
1271
2846
|
/* ── Risk Banner ── */
|
|
1272
|
-
.risk-banner {
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
.risk-
|
|
2847
|
+
.risk-banner {
|
|
2848
|
+
display: flex; align-items: center; gap: 22px;
|
|
2849
|
+
padding: 18px 22px;
|
|
2850
|
+
border-radius: var(--radius-lg);
|
|
2851
|
+
border: 1px solid var(--border);
|
|
2852
|
+
margin-bottom: 1.2rem;
|
|
2853
|
+
box-shadow: var(--shadow-sm);
|
|
2854
|
+
}
|
|
2855
|
+
.risk-grade {
|
|
2856
|
+
font-size: 34px; font-weight: 800;
|
|
2857
|
+
width: 58px; height: 58px;
|
|
2858
|
+
display: flex; align-items: center; justify-content: center;
|
|
2859
|
+
border-radius: var(--radius-md);
|
|
2860
|
+
letter-spacing: -.03em;
|
|
2861
|
+
box-shadow: 0 6px 16px rgba(0,0,0,.22), inset 0 1px 0 rgba(255,255,255,.15);
|
|
2862
|
+
}
|
|
2863
|
+
.risk-detail { display: flex; flex-direction: column; gap: 3px; }
|
|
2864
|
+
.risk-detail strong { font-size: 15px; font-weight: 650; letter-spacing: -.01em; }
|
|
2865
|
+
.risk-detail span { font-size: 13px; color: var(--muted); }
|
|
2866
|
+
.risk-f { background: var(--risk-f); border-color: var(--risk-border-f); } .risk-f .risk-grade { background: var(--sev-crit); color: #fff; }
|
|
2867
|
+
.risk-d { background: var(--risk-d); border-color: var(--risk-border-d); } .risk-d .risk-grade { background: var(--sev-high); color: #fff; }
|
|
2868
|
+
.risk-c { background: var(--risk-c); border-color: var(--risk-border-c); } .risk-c .risk-grade { background: var(--sev-med); color: #fff; }
|
|
2869
|
+
.risk-b { background: var(--risk-b); border-color: var(--risk-border-b); } .risk-b .risk-grade { background: var(--sev-low); color: #fff; }
|
|
2870
|
+
.risk-a { background: var(--risk-a); border-color: var(--risk-border-a); } .risk-a .risk-grade { background: var(--green); color: #fff; }
|
|
1281
2871
|
|
|
1282
2872
|
/* ── Coverage Bar ── */
|
|
1283
|
-
.coverage-pct { font-size:
|
|
1284
|
-
.coverage-pct.good { color: var(--green); } .coverage-pct.warn { color: var(--yellow); } .coverage-pct.bad { color: var(--
|
|
1285
|
-
.posture-bar { height:
|
|
1286
|
-
.posture-fill { height: 100%; border-radius:
|
|
1287
|
-
.posture-fill.good { background:
|
|
2873
|
+
.coverage-pct { font-size: 2rem; font-weight: 700; font-variant-numeric: tabular-nums; letter-spacing: -.03em; }
|
|
2874
|
+
.coverage-pct.good { color: var(--green); } .coverage-pct.warn { color: var(--yellow); } .coverage-pct.bad { color: var(--sev-crit); }
|
|
2875
|
+
.posture-bar { height: 10px; border-radius: 999px; background: var(--surface2); margin: .7rem 0; overflow: hidden; border: 1px solid var(--border-subtle); }
|
|
2876
|
+
.posture-fill { height: 100%; border-radius: 999px; transition: width .6s var(--ease); }
|
|
2877
|
+
.posture-fill.good { background: linear-gradient(90deg, var(--green), var(--accent)); }
|
|
2878
|
+
.posture-fill.warn { background: linear-gradient(90deg, var(--yellow), var(--muted)); }
|
|
2879
|
+
.posture-fill.bad { background: linear-gradient(90deg, var(--sev-crit), var(--red)); }
|
|
1288
2880
|
|
|
1289
2881
|
/* ── Severity Chart ── */
|
|
1290
|
-
.severity-chart { display: flex; flex-direction: column; gap:
|
|
1291
|
-
.sev-row { display: flex; align-items: center; gap:
|
|
1292
|
-
.sev-label { width:
|
|
1293
|
-
.sev-track { flex: 1; height: 22px; background: var(--surface2); border-radius:
|
|
1294
|
-
.sev-fill { height: 100%; border-radius:
|
|
1295
|
-
.sev-fill-crit { background: var(--sev-crit)
|
|
1296
|
-
.sev-fill-
|
|
2882
|
+
.severity-chart { display: flex; flex-direction: column; gap: 9px; margin-bottom: 1rem; }
|
|
2883
|
+
.sev-row { display: flex; align-items: center; gap: 12px; }
|
|
2884
|
+
.sev-label { width: 62px; font-size: 12.5px; font-weight: 550; text-align: right; color: var(--muted); }
|
|
2885
|
+
.sev-track { flex: 1; height: 22px; background: var(--surface2); border-radius: 6px; overflow: hidden; border: 1px solid var(--border-subtle); }
|
|
2886
|
+
.sev-fill { height: 100%; border-radius: 6px; min-width: 2px; transition: width .6s var(--ease); }
|
|
2887
|
+
.sev-fill-crit { background: linear-gradient(90deg, var(--sev-crit), var(--red)); }
|
|
2888
|
+
.sev-fill-high { background: linear-gradient(90deg, var(--sev-high), var(--red)); }
|
|
2889
|
+
.sev-fill-med { background: linear-gradient(90deg, var(--sev-med), var(--text-dim)); }
|
|
2890
|
+
.sev-fill-low { background: linear-gradient(90deg, var(--sev-low), var(--blue)); }
|
|
1297
2891
|
.sev-fill-unset { background: var(--sev-unset); }
|
|
1298
|
-
.sev-count { width:
|
|
2892
|
+
.sev-count { width: 32px; font-size: 14px; font-weight: 700; font-family: var(--font-mono); color: var(--text); font-variant-numeric: tabular-nums; }
|
|
1299
2893
|
|
|
1300
2894
|
/* ── Finding Cards ── */
|
|
1301
|
-
.finding-card {
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
2895
|
+
.finding-card {
|
|
2896
|
+
background: var(--surface);
|
|
2897
|
+
border: 1px solid var(--border);
|
|
2898
|
+
border-radius: var(--radius-md);
|
|
2899
|
+
padding: .8rem 1rem;
|
|
2900
|
+
margin-bottom: .55rem;
|
|
2901
|
+
cursor: pointer;
|
|
2902
|
+
transition: all .15s var(--ease);
|
|
2903
|
+
position: relative;
|
|
2904
|
+
}
|
|
2905
|
+
.finding-card::before {
|
|
2906
|
+
content: ''; position: absolute; left: 0; top: 0; bottom: 0; width: 3px;
|
|
2907
|
+
background: var(--sev-crit); border-radius: 3px 0 0 3px;
|
|
2908
|
+
opacity: .7;
|
|
2909
|
+
}
|
|
2910
|
+
.finding-card:hover {
|
|
2911
|
+
border-color: var(--border-strong);
|
|
2912
|
+
transform: translateX(2px);
|
|
2913
|
+
box-shadow: var(--shadow-sm);
|
|
2914
|
+
}
|
|
2915
|
+
.fc-top { display: flex; align-items: center; gap: .55rem; margin-bottom: .25rem; }
|
|
2916
|
+
.fc-risk { font-weight: 600; font-size: .88rem; letter-spacing: -.005em; }
|
|
2917
|
+
.fc-desc { font-size: .8rem; color: var(--muted); line-height: 1.5; }
|
|
2918
|
+
.fc-assets { font-size: .72rem; color: var(--muted); margin-top: .3rem; font-family: var(--font-mono); }
|
|
2919
|
+
.fc-sev {
|
|
2920
|
+
font-size: .66rem;
|
|
2921
|
+
padding: 2px 8px;
|
|
2922
|
+
border-radius: 999px;
|
|
2923
|
+
font-weight: 700;
|
|
2924
|
+
text-transform: uppercase;
|
|
2925
|
+
letter-spacing: .4px;
|
|
2926
|
+
border: 1px solid transparent;
|
|
2927
|
+
}
|
|
2928
|
+
.fc-sev.crit { background: var(--sev-crit-bg); color: var(--sev-crit); border-color: color-mix(in oklab, var(--sev-crit) 35%, transparent); }
|
|
2929
|
+
.fc-sev.high { background: var(--sev-high-bg); color: var(--sev-high); border-color: color-mix(in oklab, var(--sev-high) 35%, transparent); }
|
|
2930
|
+
.fc-sev.med { background: var(--sev-med-bg); color: var(--sev-med); border-color: color-mix(in oklab, var(--sev-med) 35%, transparent); }
|
|
2931
|
+
.fc-sev.low { background: var(--sev-low-bg); color: var(--sev-low); border-color: color-mix(in oklab, var(--sev-low) 35%, transparent); }
|
|
2932
|
+
.fc-sev.unset { background: var(--surface2); color: var(--muted); border-color: var(--border); }
|
|
1310
2933
|
|
|
1311
2934
|
/* ── Tables ── */
|
|
1312
|
-
table {
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
2935
|
+
table {
|
|
2936
|
+
width: 100%; border-collapse: separate; border-spacing: 0;
|
|
2937
|
+
background: var(--surface);
|
|
2938
|
+
border: 1px solid var(--border);
|
|
2939
|
+
border-radius: var(--radius-md);
|
|
2940
|
+
overflow: hidden;
|
|
2941
|
+
margin-bottom: 1rem;
|
|
2942
|
+
box-shadow: var(--shadow-sm);
|
|
2943
|
+
}
|
|
2944
|
+
th, td { padding: .6rem .85rem; text-align: left; border-bottom: 1px solid var(--border-subtle); font-size: .8rem; }
|
|
2945
|
+
tr:last-child td { border-bottom: none; }
|
|
2946
|
+
th {
|
|
2947
|
+
background: var(--surface2);
|
|
2948
|
+
color: var(--muted); font-weight: 650;
|
|
2949
|
+
text-transform: uppercase; font-size: .66rem; letter-spacing: .7px;
|
|
2950
|
+
position: sticky; top: 0;
|
|
2951
|
+
}
|
|
2952
|
+
tbody tr { transition: background .12s var(--ease); }
|
|
2953
|
+
tbody tr:nth-child(even) { background: var(--table-alt); }
|
|
2954
|
+
tr.clickable { cursor: pointer; }
|
|
2955
|
+
tr.clickable:hover { background: var(--table-hover); }
|
|
2956
|
+
.row-open { box-shadow: inset 3px 0 0 var(--sev-crit); }
|
|
1317
2957
|
.loc { color: var(--muted); font-family: var(--font-mono); font-size: .72rem; white-space: nowrap; }
|
|
1318
|
-
.empty-state {
|
|
2958
|
+
.empty-state {
|
|
2959
|
+
color: var(--muted); font-style: italic;
|
|
2960
|
+
padding: 1.2rem;
|
|
2961
|
+
font-size: .82rem; text-align: center;
|
|
2962
|
+
background: var(--surface); border: 1px dashed var(--border);
|
|
2963
|
+
border-radius: var(--radius-md);
|
|
2964
|
+
}
|
|
1319
2965
|
|
|
1320
2966
|
/* ── Badges ── */
|
|
1321
|
-
.badge-red
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
.
|
|
1329
|
-
.
|
|
1330
|
-
.
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
.ann-
|
|
1334
|
-
|
|
1335
|
-
|
|
2967
|
+
.badge-red, .badge-green, .badge-blue {
|
|
2968
|
+
display: inline-block; padding: 2px 9px;
|
|
2969
|
+
border-radius: 999px;
|
|
2970
|
+
font-size: .66rem; font-weight: 650;
|
|
2971
|
+
text-transform: uppercase; letter-spacing: .4px;
|
|
2972
|
+
border: 1px solid transparent;
|
|
2973
|
+
}
|
|
2974
|
+
.badge-red { background: var(--badge-red-bg); color: var(--badge-red-fg); border-color: color-mix(in oklab, var(--sev-crit) 25%, transparent); }
|
|
2975
|
+
.badge-green { background: var(--badge-green-bg); color: var(--badge-green-fg); border-color: color-mix(in oklab, var(--green) 25%, transparent); }
|
|
2976
|
+
.badge-blue { background: var(--badge-blue-bg); color: var(--badge-blue-fg); border-color: color-mix(in oklab, var(--sev-low) 25%, transparent); }
|
|
2977
|
+
|
|
2978
|
+
/* ── Annotation badges (theme-aware) ── */
|
|
2979
|
+
.ann-badge {
|
|
2980
|
+
display: inline-block; padding: 2px 8px;
|
|
2981
|
+
border-radius: 5px;
|
|
2982
|
+
font-size: .66rem; font-weight: 650;
|
|
2983
|
+
text-transform: uppercase; letter-spacing: .35px;
|
|
2984
|
+
border: 1px solid transparent;
|
|
2985
|
+
}
|
|
2986
|
+
.ann-asset { background: rgba(3,96,162,.14); color: #f0f0f0; border-color: rgba(3,96,162,.3); }
|
|
2987
|
+
.ann-threat { background: rgba(234,29,29,.14); color: #f0f0f0; border-color: rgba(234,29,29,.3); }
|
|
2988
|
+
.ann-control { background: rgba(51,212,157,.14); color: #33d49d; border-color: rgba(51,212,157,.3); }
|
|
2989
|
+
.ann-exposes { background: rgba(234,29,29,.14); color: #f0f0f0; border-color: rgba(234,29,29,.3); }
|
|
2990
|
+
.ann-mitigates{ background: rgba(51,212,157,.14); color: #33d49d; border-color: rgba(51,212,157,.3); }
|
|
2991
|
+
.ann-accepts { background: rgba(85,137,158,.16); color: #f0f0f0; border-color: rgba(85,137,158,.3); }
|
|
2992
|
+
.ann-transfers{ background: rgba(59,103,121,.18); color: #f0f0f0; border-color: rgba(59,103,121,.3); }
|
|
2993
|
+
.ann-flow { background: var(--surface2); color: var(--muted); border-color: var(--border); }
|
|
2994
|
+
.ann-boundary { background: rgba(59,103,121,.18); color: #f0f0f0; border-color: rgba(59,103,121,.3); }
|
|
2995
|
+
.ann-data { background: rgba(85,137,158,.16); color: #f0f0f0; border-color: rgba(85,137,158,.3); }
|
|
2996
|
+
.ann-handles { background: rgba(85,137,158,.16); color: #f0f0f0; border-color: rgba(85,137,158,.3); }
|
|
2997
|
+
.ann-validates{ background: rgba(51,212,157,.14); color: #33d49d; border-color: rgba(51,212,157,.3); }
|
|
2998
|
+
.ann-owns { background: rgba(3,96,162,.14); color: #f0f0f0; border-color: rgba(3,96,162,.3); }
|
|
2999
|
+
.ann-audit { background: rgba(85,137,158,.16); color: #f0f0f0; border-color: rgba(85,137,158,.3); }
|
|
3000
|
+
.ann-assumes { background: rgba(85,137,158,.16); color: #f0f0f0; border-color: rgba(85,137,158,.3); }
|
|
3001
|
+
.ann-shield { background: var(--surface2); color: var(--muted); border-color: var(--border); }
|
|
3002
|
+
.ann-comment { background: var(--surface2); color: var(--muted); border: 1px solid var(--border); }
|
|
3003
|
+
[data-theme="light"] .ann-asset { color: #0360a2; background: rgba(3,96,162,.10); border-color: rgba(3,96,162,.25); }
|
|
3004
|
+
[data-theme="light"] .ann-threat,
|
|
3005
|
+
[data-theme="light"] .ann-exposes { color: #ea1d1d; background: rgba(234,29,29,.10); border-color: rgba(234,29,29,.25); }
|
|
3006
|
+
[data-theme="light"] .ann-control,
|
|
3007
|
+
[data-theme="light"] .ann-mitigates,
|
|
3008
|
+
[data-theme="light"] .ann-validates{ color: #1f3943; background: rgba(51,212,157,.16); border-color: rgba(51,212,157,.25); }
|
|
3009
|
+
[data-theme="light"] .ann-accepts,
|
|
3010
|
+
[data-theme="light"] .ann-audit,
|
|
3011
|
+
[data-theme="light"] .ann-assumes { color: #3b6779; background: rgba(85,137,158,.14); border-color: rgba(85,137,158,.25); }
|
|
3012
|
+
[data-theme="light"] .ann-transfers,
|
|
3013
|
+
[data-theme="light"] .ann-boundary { color: #1f3943; background: rgba(59,103,121,.12); border-color: rgba(59,103,121,.25); }
|
|
3014
|
+
[data-theme="light"] .ann-data,
|
|
3015
|
+
[data-theme="light"] .ann-handles { color: #3b6779; background: rgba(85,137,158,.14); border-color: rgba(85,137,158,.25); }
|
|
3016
|
+
[data-theme="light"] .ann-owns { color: #0360a2; background: rgba(3,96,162,.10); border-color: rgba(3,96,162,.25); }
|
|
1336
3017
|
|
|
1337
3018
|
/* ── File Cards (Code Browser) ── */
|
|
1338
|
-
.file-card {
|
|
1339
|
-
|
|
1340
|
-
|
|
3019
|
+
.file-card {
|
|
3020
|
+
background: var(--surface);
|
|
3021
|
+
border: 1px solid var(--border);
|
|
3022
|
+
border-radius: var(--radius-md);
|
|
3023
|
+
margin-bottom: .7rem;
|
|
3024
|
+
overflow: hidden;
|
|
3025
|
+
transition: border-color .15s var(--ease);
|
|
3026
|
+
}
|
|
3027
|
+
.file-card:hover { border-color: var(--border-strong); }
|
|
3028
|
+
.file-card-header {
|
|
3029
|
+
display: flex; align-items: center; justify-content: space-between;
|
|
3030
|
+
padding: .65rem .9rem;
|
|
3031
|
+
background: var(--surface2);
|
|
3032
|
+
cursor: pointer; user-select: none;
|
|
3033
|
+
transition: background .12s var(--ease);
|
|
3034
|
+
}
|
|
3035
|
+
.file-card-header:hover { background: var(--surface3); }
|
|
1341
3036
|
.file-path { font-family: var(--font-mono); font-size: .78rem; color: var(--accent); font-weight: 600; }
|
|
1342
|
-
.file-count {
|
|
1343
|
-
.
|
|
3037
|
+
.file-count {
|
|
3038
|
+
font-size: .68rem; color: var(--muted);
|
|
3039
|
+
background: var(--surface); border: 1px solid var(--border);
|
|
3040
|
+
padding: 2px 9px; border-radius: 999px; font-weight: 600;
|
|
3041
|
+
}
|
|
3042
|
+
.chevron { color: var(--muted); transition: transform .2s var(--ease); font-size: .75rem; }
|
|
1344
3043
|
.file-card-header.open .chevron { transform: rotate(90deg); }
|
|
1345
|
-
.file-card-body { display: none; border-top: 1px solid var(--border); }
|
|
1346
|
-
.
|
|
1347
|
-
.ann-entry
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
3044
|
+
.file-card-body { display: none; border-top: 1px solid var(--border); }
|
|
3045
|
+
.file-card-body.open { display: block; animation: fadeIn .2s var(--ease); }
|
|
3046
|
+
.ann-entry {
|
|
3047
|
+
padding: .7rem .9rem;
|
|
3048
|
+
border-bottom: 1px solid var(--border-subtle);
|
|
3049
|
+
cursor: pointer;
|
|
3050
|
+
transition: background .1s var(--ease);
|
|
3051
|
+
}
|
|
3052
|
+
.ann-entry:hover { background: var(--accent-soft); }
|
|
3053
|
+
.ann-entry:last-child { border-bottom: none; }
|
|
3054
|
+
.ann-header { display: flex; align-items: center; gap: .5rem; margin-bottom: .25rem; }
|
|
3055
|
+
.ann-line { font-family: var(--font-mono); font-size: .68rem; color: var(--muted); min-width: 38px; }
|
|
3056
|
+
.ann-summary { font-size: .8rem; font-weight: 550; }
|
|
3057
|
+
.ann-desc {
|
|
3058
|
+
font-size: .76rem; color: var(--muted);
|
|
3059
|
+
margin: .2rem 0 .3rem 0;
|
|
3060
|
+
padding-left: .6rem; border-left: 2px solid var(--border);
|
|
3061
|
+
}
|
|
1352
3062
|
|
|
1353
3063
|
/* ── Diagrams ── */
|
|
1354
|
-
.diagram-hint { font-size: .
|
|
1355
|
-
.
|
|
3064
|
+
.diagram-hint { font-size: .78rem; color: var(--muted); margin-bottom: .7rem; }
|
|
3065
|
+
.diagram-shell {
|
|
3066
|
+
background: color-mix(in oklab, var(--surface) 92%, transparent);
|
|
3067
|
+
border: 1px solid var(--border);
|
|
3068
|
+
border-radius: var(--radius-lg);
|
|
3069
|
+
box-shadow: var(--shadow-sm);
|
|
3070
|
+
overflow: hidden;
|
|
3071
|
+
margin-bottom: 1rem;
|
|
3072
|
+
}
|
|
3073
|
+
.diagram-toolbar {
|
|
3074
|
+
display: flex;
|
|
3075
|
+
align-items: center;
|
|
3076
|
+
justify-content: space-between;
|
|
3077
|
+
gap: .8rem;
|
|
3078
|
+
padding: .65rem .8rem;
|
|
3079
|
+
border-bottom: 1px solid var(--border);
|
|
3080
|
+
background: color-mix(in oklab, var(--surface2) 92%, transparent);
|
|
3081
|
+
}
|
|
3082
|
+
.diagram-title {
|
|
3083
|
+
font-size: .8rem;
|
|
3084
|
+
font-weight: 650;
|
|
3085
|
+
letter-spacing: .3px;
|
|
3086
|
+
color: var(--text);
|
|
3087
|
+
text-transform: uppercase;
|
|
3088
|
+
}
|
|
3089
|
+
.diagram-actions { display: flex; align-items: center; gap: .35rem; }
|
|
3090
|
+
.diagram-btn {
|
|
3091
|
+
min-width: 28px;
|
|
3092
|
+
height: 28px;
|
|
3093
|
+
border-radius: 7px;
|
|
3094
|
+
border: 1px solid var(--border);
|
|
3095
|
+
background: var(--surface);
|
|
3096
|
+
color: var(--text);
|
|
3097
|
+
font-size: .78rem;
|
|
3098
|
+
font-weight: 650;
|
|
3099
|
+
cursor: pointer;
|
|
3100
|
+
transition: all .15s var(--ease);
|
|
3101
|
+
}
|
|
3102
|
+
.diagram-btn:hover { border-color: var(--border-strong); background: var(--surface3); }
|
|
3103
|
+
.diagram-meta {
|
|
3104
|
+
padding: .55rem .8rem;
|
|
3105
|
+
border-top: 1px solid var(--border);
|
|
3106
|
+
color: var(--muted);
|
|
3107
|
+
font-size: .74rem;
|
|
3108
|
+
}
|
|
3109
|
+
.mermaid-wrap {
|
|
3110
|
+
background: var(--surface);
|
|
3111
|
+
border: none;
|
|
3112
|
+
border-radius: 0;
|
|
3113
|
+
padding: 16px 18px;
|
|
3114
|
+
overflow: auto;
|
|
3115
|
+
margin-bottom: 0;
|
|
3116
|
+
box-shadow: none;
|
|
3117
|
+
}
|
|
1356
3118
|
.mermaid { text-align: left; width: max-content; min-width: 100%; }
|
|
1357
3119
|
.mermaid svg { max-width: none; height: auto; display: block; }
|
|
3120
|
+
.mermaid svg .cluster rect,
|
|
3121
|
+
.mermaid svg .cluster polygon {
|
|
3122
|
+
rx: 10;
|
|
3123
|
+
ry: 10;
|
|
3124
|
+
stroke-dasharray: 4 3;
|
|
3125
|
+
stroke-opacity: .72;
|
|
3126
|
+
}
|
|
3127
|
+
.mermaid svg .cluster-label .nodeLabel,
|
|
3128
|
+
.mermaid svg .cluster .cluster-label text {
|
|
3129
|
+
font-weight: 700 !important;
|
|
3130
|
+
letter-spacing: .3px;
|
|
3131
|
+
}
|
|
3132
|
+
.mermaid svg .edgeLabel { font-size: 11px !important; }
|
|
3133
|
+
.mermaid svg .edgeLabel rect { opacity: .92; }
|
|
3134
|
+
.mermaid svg .node rect,
|
|
3135
|
+
.mermaid svg .node polygon,
|
|
3136
|
+
.mermaid svg .node circle {
|
|
3137
|
+
filter: drop-shadow(0 2px 4px rgba(0,0,0,.18));
|
|
3138
|
+
}
|
|
3139
|
+
.mermaid svg path.flowchart-link {
|
|
3140
|
+
stroke-linecap: round;
|
|
3141
|
+
}
|
|
3142
|
+
.topology-shell { min-height: 720px; }
|
|
3143
|
+
.topology-toolbar { border-bottom-color: var(--border-subtle); }
|
|
3144
|
+
.topology-controls {
|
|
3145
|
+
display: flex;
|
|
3146
|
+
align-items: center;
|
|
3147
|
+
gap: .7rem;
|
|
3148
|
+
flex-wrap: wrap;
|
|
3149
|
+
padding: .7rem .85rem;
|
|
3150
|
+
border-bottom: 1px solid var(--border-subtle);
|
|
3151
|
+
background: color-mix(in oklab, var(--surface) 96%, transparent);
|
|
3152
|
+
}
|
|
3153
|
+
.diagram-search {
|
|
3154
|
+
min-width: 240px;
|
|
3155
|
+
flex: 1;
|
|
3156
|
+
max-width: 380px;
|
|
3157
|
+
height: 34px;
|
|
3158
|
+
border-radius: 8px;
|
|
3159
|
+
border: 1px solid var(--border);
|
|
3160
|
+
background: var(--surface2);
|
|
3161
|
+
color: var(--text);
|
|
3162
|
+
padding: 0 .85rem;
|
|
3163
|
+
font-family: var(--font-ui);
|
|
3164
|
+
font-size: .82rem;
|
|
3165
|
+
outline: none;
|
|
3166
|
+
transition: border-color .15s var(--ease), box-shadow .15s var(--ease);
|
|
3167
|
+
}
|
|
3168
|
+
.diagram-search:focus { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); }
|
|
3169
|
+
.topology-kind-toggles { display: inline-flex; align-items: center; gap: .35rem; flex-wrap: wrap; }
|
|
3170
|
+
.topology-kind-pill {
|
|
3171
|
+
display: inline-flex; align-items: center; gap: .4rem;
|
|
3172
|
+
padding: .3rem .65rem;
|
|
3173
|
+
border-radius: 999px;
|
|
3174
|
+
border: 1px solid var(--border);
|
|
3175
|
+
background: var(--surface2);
|
|
3176
|
+
color: var(--muted);
|
|
3177
|
+
font-size: .72rem;
|
|
3178
|
+
font-weight: 600;
|
|
3179
|
+
cursor: pointer;
|
|
3180
|
+
user-select: none;
|
|
3181
|
+
transition: all .15s var(--ease);
|
|
3182
|
+
}
|
|
3183
|
+
.topology-kind-pill:hover { border-color: var(--border-strong); color: var(--text); }
|
|
3184
|
+
.topology-kind-pill:has(input:checked) { background: var(--accent-soft); border-color: var(--accent-dim); color: var(--text); }
|
|
3185
|
+
.topology-kind-pill.topology-kind-asset:has(input:checked) { border-color: color-mix(in oklab, var(--blue) 50%, transparent); }
|
|
3186
|
+
.topology-kind-pill.topology-kind-threat:has(input:checked) { border-color: color-mix(in oklab, var(--sev-crit) 50%, transparent); }
|
|
3187
|
+
.topology-kind-pill.topology-kind-control:has(input:checked) { border-color: color-mix(in oklab, var(--green) 50%, transparent); }
|
|
3188
|
+
.topology-kind-pill input { position: absolute; opacity: 0; pointer-events: none; }
|
|
3189
|
+
.topology-kind-pill::before {
|
|
3190
|
+
content: '';
|
|
3191
|
+
width: 8px; height: 8px; border-radius: 50%;
|
|
3192
|
+
background: var(--muted);
|
|
3193
|
+
transition: background .15s var(--ease);
|
|
3194
|
+
}
|
|
3195
|
+
.topology-kind-pill.topology-kind-asset::before { background: var(--blue); }
|
|
3196
|
+
.topology-kind-pill.topology-kind-threat::before { background: var(--sev-crit); }
|
|
3197
|
+
.topology-kind-pill.topology-kind-control::before { background: var(--green); }
|
|
3198
|
+
.topology-kind-pill:not(:has(input:checked))::before { background: var(--text-dim); opacity: .5; }
|
|
3199
|
+
|
|
3200
|
+
.topology-link-filters {
|
|
3201
|
+
display: flex;
|
|
3202
|
+
align-items: center;
|
|
3203
|
+
gap: .5rem;
|
|
3204
|
+
flex-wrap: wrap;
|
|
3205
|
+
padding: .55rem .85rem;
|
|
3206
|
+
border-bottom: 1px solid var(--border-subtle);
|
|
3207
|
+
background: color-mix(in oklab, var(--surface2) 55%, var(--surface));
|
|
3208
|
+
}
|
|
3209
|
+
.topology-link-filters-k {
|
|
3210
|
+
color: var(--muted);
|
|
3211
|
+
font-size: .65rem;
|
|
3212
|
+
text-transform: uppercase;
|
|
3213
|
+
letter-spacing: .8px;
|
|
3214
|
+
font-weight: 700;
|
|
3215
|
+
margin-right: .2rem;
|
|
3216
|
+
}
|
|
3217
|
+
.topology-link-pill {
|
|
3218
|
+
display: inline-flex; align-items: center; gap: .35rem;
|
|
3219
|
+
padding: .22rem .55rem;
|
|
3220
|
+
border-radius: 999px;
|
|
3221
|
+
border: 1px solid var(--border);
|
|
3222
|
+
background: var(--surface);
|
|
3223
|
+
color: var(--muted);
|
|
3224
|
+
font-size: .68rem;
|
|
3225
|
+
font-weight: 600;
|
|
3226
|
+
cursor: pointer;
|
|
3227
|
+
user-select: none;
|
|
3228
|
+
transition: all .12s var(--ease);
|
|
3229
|
+
}
|
|
3230
|
+
.topology-link-pill input { position: absolute; opacity: 0; pointer-events: none; }
|
|
3231
|
+
.topology-link-pill::before {
|
|
3232
|
+
content: '';
|
|
3233
|
+
display: inline-block;
|
|
3234
|
+
width: 14px; height: 0;
|
|
3235
|
+
border-top: 2px solid currentColor;
|
|
3236
|
+
}
|
|
3237
|
+
.topology-link-pill:has(input:checked) { color: var(--text); border-color: var(--border-strong); }
|
|
3238
|
+
.topology-link-pill:not(:has(input:checked)) { opacity: .45; }
|
|
3239
|
+
.topology-link-pill-exposes::before,
|
|
3240
|
+
.topology-link-pill-confirmed::before { border-color: var(--sev-crit); border-top-style: solid; }
|
|
3241
|
+
.topology-link-pill-mitigates::before,
|
|
3242
|
+
.topology-link-pill-protects::before,
|
|
3243
|
+
.topology-link-pill-validates::before { border-color: var(--green); border-top-style: solid; }
|
|
3244
|
+
.topology-link-pill-flows::before { border-color: var(--blue); border-top-style: dashed; }
|
|
3245
|
+
.topology-link-pill-boundary::before { border-color: var(--purple); border-top-style: dashed; }
|
|
3246
|
+
.topology-link-pill-transfers::before,
|
|
3247
|
+
.topology-link-pill-accepts::before { border-color: var(--sev-med); border-top-style: dashed; }
|
|
3248
|
+
|
|
3249
|
+
.topology-count {
|
|
3250
|
+
margin-left: auto;
|
|
3251
|
+
color: var(--muted);
|
|
3252
|
+
font-size: .7rem;
|
|
3253
|
+
font-family: var(--font-mono);
|
|
3254
|
+
white-space: nowrap;
|
|
3255
|
+
letter-spacing: .4px;
|
|
3256
|
+
}
|
|
3257
|
+
.topology-layout {
|
|
3258
|
+
display: grid;
|
|
3259
|
+
grid-template-columns: minmax(0, 1fr) 300px;
|
|
3260
|
+
min-height: 600px;
|
|
3261
|
+
background: var(--surface);
|
|
3262
|
+
}
|
|
3263
|
+
.topology-graph {
|
|
3264
|
+
min-height: 600px;
|
|
3265
|
+
overflow: hidden;
|
|
3266
|
+
border-right: 1px solid var(--border-subtle);
|
|
3267
|
+
background:
|
|
3268
|
+
radial-gradient(ellipse 600px 320px at 30% 40%, color-mix(in oklab, var(--accent) 4%, transparent), transparent 70%),
|
|
3269
|
+
radial-gradient(ellipse 500px 280px at 75% 70%, color-mix(in oklab, var(--sev-crit) 5%, transparent), transparent 70%),
|
|
3270
|
+
linear-gradient(color-mix(in oklab, var(--border-subtle) 55%, transparent) 1px, transparent 1px),
|
|
3271
|
+
linear-gradient(90deg, color-mix(in oklab, var(--border-subtle) 55%, transparent) 1px, transparent 1px),
|
|
3272
|
+
var(--surface);
|
|
3273
|
+
background-size: auto, auto, 36px 36px, 36px 36px, auto;
|
|
3274
|
+
background-position: 0 0, 0 0, -1px -1px, -1px -1px, 0 0;
|
|
3275
|
+
}
|
|
3276
|
+
.topology-graph svg { width: 100%; height: 100%; display: block; cursor: grab; }
|
|
3277
|
+
.topology-graph svg:active { cursor: grabbing; }
|
|
3278
|
+
|
|
3279
|
+
.topology-lane-label {
|
|
3280
|
+
fill: color-mix(in oklab, var(--muted) 55%, transparent);
|
|
3281
|
+
font-size: 10px;
|
|
3282
|
+
font-weight: 800;
|
|
3283
|
+
letter-spacing: 3.5px;
|
|
3284
|
+
text-transform: uppercase;
|
|
3285
|
+
pointer-events: none;
|
|
3286
|
+
user-select: none;
|
|
3287
|
+
}
|
|
3288
|
+
|
|
3289
|
+
.topology-link {
|
|
3290
|
+
stroke: var(--muted);
|
|
3291
|
+
stroke-width: 1.4;
|
|
3292
|
+
stroke-opacity: .42;
|
|
3293
|
+
transition: stroke-opacity .2s var(--ease), stroke-width .2s var(--ease);
|
|
3294
|
+
}
|
|
3295
|
+
.topology-link-exposes { stroke: var(--sev-crit); stroke-opacity: .58; stroke-width: 1.6; }
|
|
3296
|
+
.topology-link-confirmed { stroke: var(--sev-crit); stroke-opacity: .9; stroke-width: 2.6; }
|
|
3297
|
+
.topology-link-mitigates,
|
|
3298
|
+
.topology-link-validates { stroke: var(--green); stroke-opacity: .62; stroke-width: 1.6; }
|
|
3299
|
+
.topology-link-protects { stroke: var(--green); stroke-opacity: .48; stroke-dasharray: 4 3; }
|
|
3300
|
+
.topology-link-flows { stroke: var(--blue); stroke-dasharray: 6 4; stroke-opacity: .6; stroke-width: 1.5; }
|
|
3301
|
+
.topology-link-boundary { stroke: var(--purple); stroke-dasharray: 2 5; stroke-width: 1.9; stroke-opacity: .75; }
|
|
3302
|
+
.topology-link-accepts { stroke: var(--sev-med); stroke-dasharray: 6 4; stroke-opacity: .7; }
|
|
3303
|
+
.topology-link-transfers { stroke: var(--sev-med); stroke-dasharray: 8 3; stroke-opacity: .7; }
|
|
3304
|
+
.topology-link-status-mitigated { stroke-opacity: .32; }
|
|
3305
|
+
.topology-link.emphasis { stroke-opacity: 1 !important; stroke-width: 2.4 !important; }
|
|
3306
|
+
.topology-link.dim { stroke-opacity: .08 !important; }
|
|
3307
|
+
|
|
3308
|
+
.topology-node { cursor: pointer; transition: opacity .2s var(--ease); }
|
|
3309
|
+
.topology-node circle.topology-node-body {
|
|
3310
|
+
stroke: var(--border-strong);
|
|
3311
|
+
stroke-width: 1.4;
|
|
3312
|
+
filter: drop-shadow(0 4px 10px rgba(0,0,0,.28));
|
|
3313
|
+
transition: stroke-width .15s var(--ease), filter .15s var(--ease), r .25s var(--ease);
|
|
3314
|
+
}
|
|
3315
|
+
.topology-node:hover circle.topology-node-body,
|
|
3316
|
+
.topology-node.active circle.topology-node-body,
|
|
3317
|
+
.topology-node.emphasis circle.topology-node-body {
|
|
3318
|
+
stroke-width: 2.6;
|
|
3319
|
+
filter: drop-shadow(0 0 14px color-mix(in oklab, var(--accent) 48%, transparent));
|
|
3320
|
+
}
|
|
3321
|
+
.topology-node.active circle.topology-node-body { stroke: var(--accent); }
|
|
3322
|
+
.topology-node.dim { opacity: .18; }
|
|
3323
|
+
|
|
3324
|
+
.topology-node-halo {
|
|
3325
|
+
fill: transparent;
|
|
3326
|
+
stroke: var(--sev-crit);
|
|
3327
|
+
stroke-width: 2;
|
|
3328
|
+
stroke-opacity: .55;
|
|
3329
|
+
pointer-events: none;
|
|
3330
|
+
filter: url(#topology-glow);
|
|
3331
|
+
animation: topology-pulse 1.9s ease-in-out infinite;
|
|
3332
|
+
}
|
|
3333
|
+
.topology-fully-covered .topology-node-halo { display: none; }
|
|
3334
|
+
|
|
3335
|
+
@keyframes topology-pulse {
|
|
3336
|
+
0%, 100% { stroke-opacity: .3; transform: scale(1); }
|
|
3337
|
+
50% { stroke-opacity: .75; transform: scale(1.08); }
|
|
3338
|
+
}
|
|
3339
|
+
|
|
3340
|
+
.topology-coverage-arc {
|
|
3341
|
+
fill: none;
|
|
3342
|
+
stroke: var(--green);
|
|
3343
|
+
stroke-width: 2.5;
|
|
3344
|
+
stroke-linecap: round;
|
|
3345
|
+
opacity: .85;
|
|
3346
|
+
pointer-events: none;
|
|
3347
|
+
}
|
|
3348
|
+
|
|
3349
|
+
.topology-asset circle.topology-node-body { fill: color-mix(in oklab, var(--blue) 60%, var(--surface)); }
|
|
3350
|
+
.topology-threat circle.topology-node-body { fill: color-mix(in oklab, var(--sev-med) 60%, var(--surface)); }
|
|
3351
|
+
.topology-control circle.topology-node-body { fill: color-mix(in oklab, var(--green) 58%, var(--surface)); }
|
|
3352
|
+
.topology-has-open circle.topology-node-body { stroke: var(--sev-crit); }
|
|
3353
|
+
.topology-has-confirmed circle.topology-node-body { stroke: var(--sev-crit); stroke-width: 2; }
|
|
3354
|
+
.topology-fully-covered circle.topology-node-body { stroke: var(--green); }
|
|
3355
|
+
|
|
3356
|
+
.topology-sev-critical circle.topology-node-body,
|
|
3357
|
+
.topology-sev-p0 circle.topology-node-body { fill: color-mix(in oklab, var(--sev-crit) 72%, var(--surface)); }
|
|
3358
|
+
.topology-sev-high circle.topology-node-body,
|
|
3359
|
+
.topology-sev-p1 circle.topology-node-body { fill: color-mix(in oklab, var(--red) 60%, var(--surface)); }
|
|
3360
|
+
|
|
3361
|
+
.topology-node-icon {
|
|
3362
|
+
fill: #fff;
|
|
3363
|
+
font-size: 11px;
|
|
3364
|
+
font-weight: 800;
|
|
3365
|
+
letter-spacing: .4px;
|
|
3366
|
+
pointer-events: none;
|
|
3367
|
+
}
|
|
3368
|
+
.topology-node-label {
|
|
3369
|
+
fill: var(--text);
|
|
3370
|
+
paint-order: stroke;
|
|
3371
|
+
stroke: var(--surface);
|
|
3372
|
+
stroke-width: 4px;
|
|
3373
|
+
stroke-linejoin: round;
|
|
3374
|
+
font-size: 11px;
|
|
3375
|
+
font-weight: 650;
|
|
3376
|
+
pointer-events: none;
|
|
3377
|
+
}
|
|
3378
|
+
.topology-node.emphasis .topology-node-label { font-weight: 750; }
|
|
3379
|
+
|
|
3380
|
+
.topology-inspector {
|
|
3381
|
+
padding: 1rem .95rem;
|
|
3382
|
+
overflow-y: auto;
|
|
3383
|
+
background: color-mix(in oklab, var(--surface2) 78%, var(--surface));
|
|
3384
|
+
border-left: 1px solid var(--border-subtle);
|
|
3385
|
+
}
|
|
3386
|
+
.topology-inspector-head {
|
|
3387
|
+
display: flex;
|
|
3388
|
+
align-items: center;
|
|
3389
|
+
justify-content: space-between;
|
|
3390
|
+
gap: .6rem;
|
|
3391
|
+
margin-bottom: .35rem;
|
|
3392
|
+
}
|
|
3393
|
+
.topology-inspector-k {
|
|
3394
|
+
color: var(--muted);
|
|
3395
|
+
font-size: .66rem;
|
|
3396
|
+
text-transform: uppercase;
|
|
3397
|
+
letter-spacing: .8px;
|
|
3398
|
+
font-weight: 700;
|
|
3399
|
+
}
|
|
3400
|
+
.topology-badge {
|
|
3401
|
+
font-size: .62rem;
|
|
3402
|
+
font-weight: 800;
|
|
3403
|
+
text-transform: uppercase;
|
|
3404
|
+
letter-spacing: .5px;
|
|
3405
|
+
padding: 2px 7px;
|
|
3406
|
+
border-radius: 999px;
|
|
3407
|
+
border: 1px solid transparent;
|
|
3408
|
+
}
|
|
3409
|
+
.topology-badge-open { background: var(--sev-crit-bg); color: var(--sev-crit); border-color: color-mix(in oklab, var(--sev-crit) 35%, transparent); }
|
|
3410
|
+
.topology-badge-confirmed { background: var(--sev-crit-bg); color: var(--sev-crit); border-color: var(--sev-crit); animation: topology-pulse 2s ease-in-out infinite; }
|
|
3411
|
+
.topology-badge-covered { background: color-mix(in oklab, var(--green) 15%, transparent); color: var(--green); border-color: color-mix(in oklab, var(--green) 40%, transparent); }
|
|
3412
|
+
|
|
3413
|
+
.topology-inspector h3 {
|
|
3414
|
+
font-size: 1rem;
|
|
3415
|
+
line-height: 1.25;
|
|
3416
|
+
margin-bottom: .75rem;
|
|
3417
|
+
word-break: break-word;
|
|
3418
|
+
}
|
|
3419
|
+
.topology-bar-wrap { margin: 0 0 .9rem 0; }
|
|
3420
|
+
.topology-bar-label {
|
|
3421
|
+
display: flex; justify-content: space-between; align-items: baseline;
|
|
3422
|
+
font-size: .68rem;
|
|
3423
|
+
color: var(--muted);
|
|
3424
|
+
text-transform: uppercase;
|
|
3425
|
+
letter-spacing: .8px;
|
|
3426
|
+
font-weight: 700;
|
|
3427
|
+
margin-bottom: .3rem;
|
|
3428
|
+
}
|
|
3429
|
+
.topology-bar-label strong { color: var(--text); font-family: var(--font-mono); }
|
|
3430
|
+
.topology-bar {
|
|
3431
|
+
height: 7px;
|
|
3432
|
+
border-radius: 999px;
|
|
3433
|
+
background: var(--surface3);
|
|
3434
|
+
overflow: hidden;
|
|
3435
|
+
}
|
|
3436
|
+
.topology-bar-fill { height: 100%; border-radius: 999px; transition: width .5s var(--ease); }
|
|
3437
|
+
.topology-bar-fill.good { background: linear-gradient(90deg, color-mix(in oklab, var(--green) 70%, transparent), var(--green)); }
|
|
3438
|
+
.topology-bar-fill.warn { background: linear-gradient(90deg, color-mix(in oklab, var(--sev-med) 65%, transparent), var(--sev-med)); }
|
|
3439
|
+
.topology-bar-fill.bad { background: linear-gradient(90deg, color-mix(in oklab, var(--sev-crit) 65%, transparent), var(--sev-crit)); }
|
|
3440
|
+
|
|
3441
|
+
.topology-detail-grid {
|
|
3442
|
+
display: grid;
|
|
3443
|
+
grid-template-columns: 88px 1fr;
|
|
3444
|
+
gap: .4rem .7rem;
|
|
3445
|
+
align-items: baseline;
|
|
3446
|
+
font-size: .78rem;
|
|
3447
|
+
}
|
|
3448
|
+
.topology-detail-grid span { color: var(--muted); }
|
|
3449
|
+
.topology-detail-grid strong { color: var(--text); font-weight: 700; word-break: break-word; }
|
|
3450
|
+
.topology-detail-grid .danger,
|
|
3451
|
+
.topology-detail-grid .status-open,
|
|
3452
|
+
.topology-detail-grid .status-confirmed,
|
|
3453
|
+
.topology-detail-grid .sev-critical,
|
|
3454
|
+
.topology-detail-grid .sev-p0,
|
|
3455
|
+
.topology-detail-grid .sev-high,
|
|
3456
|
+
.topology-detail-grid .sev-p1 { color: var(--sev-crit); }
|
|
3457
|
+
.topology-detail-grid .good,
|
|
3458
|
+
.topology-detail-grid .status-mitigated { color: var(--green); }
|
|
3459
|
+
.topology-chip-row {
|
|
3460
|
+
display: flex;
|
|
3461
|
+
flex-wrap: wrap;
|
|
3462
|
+
gap: .35rem;
|
|
3463
|
+
margin-top: .9rem;
|
|
3464
|
+
}
|
|
3465
|
+
.topology-chip-row span {
|
|
3466
|
+
border: 1px solid var(--accent-dim);
|
|
3467
|
+
background: var(--accent-soft);
|
|
3468
|
+
color: var(--accent);
|
|
3469
|
+
border-radius: 999px;
|
|
3470
|
+
padding: 2px 8px;
|
|
3471
|
+
font-size: .65rem;
|
|
3472
|
+
font-weight: 700;
|
|
3473
|
+
}
|
|
3474
|
+
.topology-related-title {
|
|
3475
|
+
margin: 1rem 0 .45rem;
|
|
3476
|
+
color: var(--muted);
|
|
3477
|
+
font-size: .68rem;
|
|
3478
|
+
text-transform: uppercase;
|
|
3479
|
+
letter-spacing: .7px;
|
|
3480
|
+
font-weight: 700;
|
|
3481
|
+
}
|
|
3482
|
+
.topology-related { display: flex; flex-direction: column; gap: .35rem; }
|
|
3483
|
+
.topology-related-row {
|
|
3484
|
+
display: grid;
|
|
3485
|
+
grid-template-columns: 88px 1fr auto;
|
|
3486
|
+
gap: .45rem;
|
|
3487
|
+
align-items: center;
|
|
3488
|
+
padding: .42rem .55rem;
|
|
3489
|
+
border: 1px solid var(--border-subtle);
|
|
3490
|
+
border-radius: 7px;
|
|
3491
|
+
background: var(--surface);
|
|
3492
|
+
font-size: .72rem;
|
|
3493
|
+
transition: border-color .12s var(--ease);
|
|
3494
|
+
}
|
|
3495
|
+
.topology-related-row:hover { border-color: var(--border-strong); }
|
|
3496
|
+
.topology-related-exposes,
|
|
3497
|
+
.topology-related-confirmed { border-left: 2px solid var(--sev-crit); }
|
|
3498
|
+
.topology-related-mitigates,
|
|
3499
|
+
.topology-related-protects,
|
|
3500
|
+
.topology-related-validates { border-left: 2px solid var(--green); }
|
|
3501
|
+
.topology-related-flows { border-left: 2px solid var(--blue); }
|
|
3502
|
+
.topology-related-boundary { border-left: 2px solid var(--purple); }
|
|
3503
|
+
.topology-related-transfers,
|
|
3504
|
+
.topology-related-accepts { border-left: 2px solid var(--sev-med); }
|
|
3505
|
+
.topology-related-kind {
|
|
3506
|
+
color: var(--muted);
|
|
3507
|
+
font-family: var(--font-mono);
|
|
3508
|
+
font-size: .64rem;
|
|
3509
|
+
font-weight: 600;
|
|
3510
|
+
}
|
|
3511
|
+
.topology-related-row em { color: var(--muted); font-style: normal; font-family: var(--font-mono); }
|
|
3512
|
+
.topology-inspector-note {
|
|
3513
|
+
margin-top: .9rem;
|
|
3514
|
+
color: var(--muted);
|
|
3515
|
+
font-size: .74rem;
|
|
3516
|
+
line-height: 1.5;
|
|
3517
|
+
}
|
|
3518
|
+
.topology-legend {
|
|
3519
|
+
display: flex;
|
|
3520
|
+
align-items: center;
|
|
3521
|
+
gap: .9rem;
|
|
3522
|
+
flex-wrap: wrap;
|
|
3523
|
+
}
|
|
3524
|
+
.topology-legend span { display: inline-flex; align-items: center; gap: .35rem; }
|
|
3525
|
+
.legend-dot {
|
|
3526
|
+
width: 10px;
|
|
3527
|
+
height: 10px;
|
|
3528
|
+
border-radius: 50%;
|
|
3529
|
+
display: inline-block;
|
|
3530
|
+
border: 1px solid color-mix(in oklab, currentColor 30%, transparent);
|
|
3531
|
+
}
|
|
3532
|
+
.legend-dot.asset { background: var(--blue); }
|
|
3533
|
+
.legend-dot.threat { background: var(--sev-med); }
|
|
3534
|
+
.legend-dot.control { background: var(--green); }
|
|
3535
|
+
.legend-dot.confirmed { background: var(--sev-crit); box-shadow: 0 0 8px color-mix(in oklab, var(--sev-crit) 60%, transparent); }
|
|
3536
|
+
.legend-line {
|
|
3537
|
+
display: inline-block;
|
|
3538
|
+
width: 20px;
|
|
3539
|
+
height: 0;
|
|
3540
|
+
border-top: 2px solid var(--muted);
|
|
3541
|
+
}
|
|
3542
|
+
.legend-line.exposes { border-color: var(--sev-crit); }
|
|
3543
|
+
.legend-line.open { border-color: var(--sev-crit); }
|
|
3544
|
+
.legend-line.mitigates { border-color: var(--green); }
|
|
3545
|
+
.legend-line.flow { border-color: var(--blue); border-top-style: dashed; }
|
|
3546
|
+
.legend-line.boundary { border-color: var(--purple); border-top-style: dashed; }
|
|
1358
3547
|
|
|
1359
3548
|
/* ── Heatmap ── */
|
|
1360
|
-
.heatmap { display: grid; grid-template-columns: repeat(auto-fill, minmax(
|
|
1361
|
-
.heatmap-cell {
|
|
3549
|
+
.heatmap { display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 12px; }
|
|
3550
|
+
.heatmap-cell {
|
|
3551
|
+
border-radius: var(--radius-md);
|
|
3552
|
+
padding: 14px;
|
|
3553
|
+
border: 1px solid var(--border);
|
|
3554
|
+
transition: all .15s var(--ease);
|
|
3555
|
+
box-shadow: var(--shadow-sm);
|
|
3556
|
+
}
|
|
1362
3557
|
.heatmap-cell.clickable { cursor: pointer; }
|
|
1363
|
-
.heatmap-cell.clickable:hover {
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
.
|
|
1369
|
-
.
|
|
1370
|
-
.
|
|
3558
|
+
.heatmap-cell.clickable:hover {
|
|
3559
|
+
border-color: var(--border-strong);
|
|
3560
|
+
transform: translateY(-2px);
|
|
3561
|
+
box-shadow: var(--shadow-md);
|
|
3562
|
+
}
|
|
3563
|
+
.heatmap-name { font-weight: 650; font-size: 13px; margin-bottom: 6px; font-family: var(--font-mono); word-break: break-all; color: var(--text); }
|
|
3564
|
+
.heatmap-stats { display: flex; gap: 12px; font-size: 12px; color: var(--muted); }
|
|
3565
|
+
.heatmap-data { margin-top: 6px; display: flex; gap: 4px; flex-wrap: wrap; }
|
|
3566
|
+
.data-badge {
|
|
3567
|
+
font-size: 10px; padding: 2px 7px;
|
|
3568
|
+
border-radius: 999px;
|
|
3569
|
+
background: var(--accent-soft);
|
|
3570
|
+
color: var(--accent);
|
|
3571
|
+
border: 1px solid var(--accent-dim);
|
|
3572
|
+
font-weight: 650; text-transform: uppercase; letter-spacing: .3px;
|
|
3573
|
+
}
|
|
3574
|
+
.risk-cell-critical { background: var(--heatmap-crit); border-color: var(--risk-border-f); }
|
|
3575
|
+
.risk-cell-high { background: var(--heatmap-high); border-color: var(--risk-border-d); }
|
|
3576
|
+
.risk-cell-medium { background: var(--heatmap-med); border-color: var(--risk-border-c); }
|
|
3577
|
+
.risk-cell-low { background: var(--heatmap-low); border-color: var(--risk-border-b); }
|
|
3578
|
+
.risk-cell-none { background: var(--heatmap-none); }
|
|
1371
3579
|
|
|
1372
3580
|
/* ── Code Blocks ── */
|
|
1373
|
-
.code-block {
|
|
3581
|
+
.code-block {
|
|
3582
|
+
background: var(--bg);
|
|
3583
|
+
border: 1px solid var(--border);
|
|
3584
|
+
border-radius: var(--radius-md);
|
|
3585
|
+
padding: .4rem .7rem;
|
|
3586
|
+
overflow-x: auto;
|
|
3587
|
+
margin-top: .3rem;
|
|
3588
|
+
font-family: var(--font-mono);
|
|
3589
|
+
font-size: .72rem; line-height: 1.5;
|
|
3590
|
+
tab-size: 2;
|
|
3591
|
+
}
|
|
1374
3592
|
.code-line-code { display: block; color: var(--muted); white-space: pre; }
|
|
1375
|
-
.code-line-ann {
|
|
3593
|
+
.code-line-ann {
|
|
3594
|
+
display: block; color: var(--accent);
|
|
3595
|
+
background: var(--accent-soft);
|
|
3596
|
+
margin: 0 -.7rem; padding: 0 .7rem;
|
|
3597
|
+
border-left: 2px solid var(--accent);
|
|
3598
|
+
white-space: pre;
|
|
3599
|
+
}
|
|
1376
3600
|
|
|
1377
3601
|
/* ── Diagram Tabs ── */
|
|
1378
|
-
.diagram-tabs { display: flex; gap:
|
|
1379
|
-
.diagram-tab {
|
|
3602
|
+
.diagram-tabs { display: flex; gap: .25rem; border-bottom: 1px solid var(--border); margin-bottom: 1rem; padding: 0 .2rem; }
|
|
3603
|
+
.diagram-tab {
|
|
3604
|
+
background: none; border: none; border-bottom: 2px solid transparent;
|
|
3605
|
+
padding: .55rem 1rem; color: var(--muted);
|
|
3606
|
+
font-size: .82rem; font-weight: 550;
|
|
3607
|
+
cursor: pointer; font-family: var(--font-ui);
|
|
3608
|
+
transition: all .15s var(--ease);
|
|
3609
|
+
border-radius: 6px 6px 0 0;
|
|
3610
|
+
}
|
|
1380
3611
|
.diagram-tab:hover { color: var(--text); background: var(--surface2); }
|
|
1381
|
-
.diagram-tab.active {
|
|
3612
|
+
.diagram-tab.active {
|
|
3613
|
+
color: var(--accent);
|
|
3614
|
+
border-bottom-color: var(--accent);
|
|
3615
|
+
background: var(--accent-soft);
|
|
3616
|
+
}
|
|
1382
3617
|
.diagram-panel { display: none; } .diagram-panel.active { display: block; }
|
|
1383
3618
|
|
|
1384
3619
|
/* ── AI Analysis Controls ── */
|
|
1385
|
-
.ai-analysis-controls { display: flex; align-items: center; gap:
|
|
1386
|
-
.report-selector-label { font-weight: 600; font-size:
|
|
1387
|
-
.report-selector {
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
.
|
|
1400
|
-
.
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
.
|
|
1405
|
-
.
|
|
3620
|
+
.ai-analysis-controls { display: flex; align-items: center; gap: .85rem; margin: .75rem 0 1.5rem; }
|
|
3621
|
+
.report-selector-label { font-weight: 600; font-size: .85rem; color: var(--text); }
|
|
3622
|
+
.report-selector {
|
|
3623
|
+
flex: 1; max-width: 600px;
|
|
3624
|
+
padding: .55rem .85rem;
|
|
3625
|
+
font-size: .88rem; font-family: var(--font-ui);
|
|
3626
|
+
background: var(--surface);
|
|
3627
|
+
color: var(--text);
|
|
3628
|
+
border: 1px solid var(--border);
|
|
3629
|
+
border-radius: 8px;
|
|
3630
|
+
cursor: pointer;
|
|
3631
|
+
transition: all .15s var(--ease);
|
|
3632
|
+
box-shadow: var(--shadow-sm);
|
|
3633
|
+
}
|
|
3634
|
+
.report-selector:hover { background: var(--surface2); border-color: var(--border-strong); }
|
|
3635
|
+
.report-selector:focus {
|
|
3636
|
+
outline: none; border-color: var(--accent);
|
|
3637
|
+
box-shadow: 0 0 0 3px var(--accent-soft);
|
|
3638
|
+
}
|
|
3639
|
+
.report-selector option { background: var(--surface); color: var(--text); padding: .5rem; }
|
|
3640
|
+
.ai-analysis-main { margin-top: .5rem; }
|
|
3641
|
+
|
|
3642
|
+
/* ── Markdown content ── */
|
|
3643
|
+
.md-content h1 { font-size: 1.5rem; font-weight: 700; margin: 1.3rem 0 .7rem; color: var(--text); letter-spacing: -.02em; }
|
|
3644
|
+
.md-content h2 {
|
|
3645
|
+
font-size: 1.2rem; font-weight: 650; margin: 1.2rem 0 .6rem;
|
|
3646
|
+
color: var(--text); border-bottom: 1px solid var(--border);
|
|
3647
|
+
padding-bottom: .4rem; letter-spacing: -.01em;
|
|
3648
|
+
}
|
|
3649
|
+
.md-content h3 { font-size: 1.02rem; font-weight: 650; margin: 1rem 0 .45rem; color: var(--text); letter-spacing: -.01em; }
|
|
3650
|
+
.md-content p { margin: .5rem 0; line-height: 1.65; color: var(--text); }
|
|
3651
|
+
.md-content ul, .md-content ol { margin: .5rem 0 .5rem 1.6rem; }
|
|
3652
|
+
.md-content li { margin: .25rem 0; line-height: 1.6; }
|
|
3653
|
+
.md-content code { font-family: var(--font-mono); font-size: .82rem; background: var(--surface2); border: 1px solid var(--border-subtle); padding: 1px 6px; border-radius: 4px; }
|
|
3654
|
+
.md-content pre { background: var(--bg); border: 1px solid var(--border); border-radius: var(--radius-md); padding: .9rem 1rem; overflow-x: auto; margin: .8rem 0; box-shadow: var(--shadow-sm); }
|
|
3655
|
+
.md-content pre code { background: none; padding: 0; border: none; font-size: .8rem; }
|
|
3656
|
+
.md-content blockquote { border-left: 3px solid var(--accent); padding: .1rem 0 .1rem .9rem; margin: .7rem 0; color: var(--muted); background: var(--accent-soft); border-radius: 0 6px 6px 0; }
|
|
3657
|
+
.md-content table { width: 100%; border-collapse: separate; border-spacing: 0; margin: .7rem 0; font-size: .82rem; border: 1px solid var(--border); border-radius: var(--radius-md); overflow: hidden; }
|
|
3658
|
+
.md-content th, .md-content td { padding: .5rem .75rem; border-bottom: 1px solid var(--border-subtle); text-align: left; }
|
|
3659
|
+
.md-content tr:last-child td { border-bottom: none; }
|
|
3660
|
+
.md-content th { background: var(--surface2); font-weight: 650; color: var(--muted); text-transform: uppercase; font-size: .68rem; letter-spacing: .5px; }
|
|
3661
|
+
.md-content strong { color: var(--text); font-weight: 650; }
|
|
1406
3662
|
|
|
1407
3663
|
/* ── Responsive ── */
|
|
3664
|
+
@media (max-width: 900px) {
|
|
3665
|
+
.section-content { padding: 1.2rem 1rem 2rem; }
|
|
3666
|
+
.summary-panels { grid-template-columns: 1fr; }
|
|
3667
|
+
.topology-layout { grid-template-columns: 1fr; }
|
|
3668
|
+
.topology-graph { border-right: none; border-bottom: 1px solid var(--border); }
|
|
3669
|
+
.topology-inspector { min-height: 220px; }
|
|
3670
|
+
}
|
|
1408
3671
|
@media (max-width: 768px) {
|
|
1409
|
-
.sidebar { width:
|
|
1410
|
-
.topnav .
|
|
3672
|
+
.sidebar { width: 52px; min-width: 52px; } .sidebar .nav-text { display: none; }
|
|
3673
|
+
.topnav .topnav-metrics { display: none; }
|
|
3674
|
+
.feature-filter-select { max-width: 130px; }
|
|
3675
|
+
.risk-banner { flex-direction: column; align-items: flex-start; gap: 12px; }
|
|
3676
|
+
.diagram-search { min-width: 100%; max-width: none; }
|
|
3677
|
+
.topology-count { margin-left: 0; width: 100%; }
|
|
3678
|
+
:root { --drawer-w: 100vw; }
|
|
3679
|
+
}
|
|
3680
|
+
@media print {
|
|
3681
|
+
.topnav, .sidebar, #sidebarToggle, #themeToggle { display: none; }
|
|
3682
|
+
.main { margin: 0; } .layout { display: block; }
|
|
3683
|
+
body { overflow: auto; height: auto; background: #fff; color: #000; }
|
|
1411
3684
|
}
|
|
1412
|
-
@media print { .topnav, .sidebar, #sidebarToggle { display: none; } .main { margin: 0; } .layout { display: block; } #themeToggle { display: none; } }
|
|
1413
3685
|
`;
|
|
1414
3686
|
//# sourceMappingURL=generate.js.map
|