guardlink 1.0.0
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 +30 -0
- package/LICENSE +21 -0
- package/README.md +344 -0
- package/dist/agents/config.d.ts +46 -0
- package/dist/agents/config.d.ts.map +1 -0
- package/dist/agents/config.js +189 -0
- package/dist/agents/config.js.map +1 -0
- package/dist/agents/index.d.ts +24 -0
- package/dist/agents/index.d.ts.map +1 -0
- package/dist/agents/index.js +42 -0
- package/dist/agents/index.js.map +1 -0
- package/dist/agents/launcher.d.ts +54 -0
- package/dist/agents/launcher.d.ts.map +1 -0
- package/dist/agents/launcher.js +152 -0
- package/dist/agents/launcher.js.map +1 -0
- package/dist/agents/prompts.d.ts +14 -0
- package/dist/agents/prompts.d.ts.map +1 -0
- package/dist/agents/prompts.js +120 -0
- package/dist/agents/prompts.js.map +1 -0
- package/dist/analyze/index.d.ts +80 -0
- package/dist/analyze/index.d.ts.map +1 -0
- package/dist/analyze/index.js +306 -0
- package/dist/analyze/index.js.map +1 -0
- package/dist/analyze/llm.d.ts +52 -0
- package/dist/analyze/llm.d.ts.map +1 -0
- package/dist/analyze/llm.js +295 -0
- package/dist/analyze/llm.js.map +1 -0
- package/dist/analyze/prompts.d.ts +14 -0
- package/dist/analyze/prompts.d.ts.map +1 -0
- package/dist/analyze/prompts.js +205 -0
- package/dist/analyze/prompts.js.map +1 -0
- package/dist/analyzer/index.d.ts +5 -0
- package/dist/analyzer/index.d.ts.map +1 -0
- package/dist/analyzer/index.js +5 -0
- package/dist/analyzer/index.js.map +1 -0
- package/dist/analyzer/sarif.d.ts +84 -0
- package/dist/analyzer/sarif.d.ts.map +1 -0
- package/dist/analyzer/sarif.js +149 -0
- package/dist/analyzer/sarif.js.map +1 -0
- package/dist/cli/index.d.ts +25 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +821 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/dashboard/data.d.ts +52 -0
- package/dist/dashboard/data.d.ts.map +1 -0
- package/dist/dashboard/data.js +93 -0
- package/dist/dashboard/data.js.map +1 -0
- package/dist/dashboard/diagrams.d.ts +25 -0
- package/dist/dashboard/diagrams.d.ts.map +1 -0
- package/dist/dashboard/diagrams.js +243 -0
- package/dist/dashboard/diagrams.js.map +1 -0
- package/dist/dashboard/generate.d.ts +17 -0
- package/dist/dashboard/generate.d.ts.map +1 -0
- package/dist/dashboard/generate.js +1258 -0
- package/dist/dashboard/generate.js.map +1 -0
- package/dist/dashboard/index.d.ts +7 -0
- package/dist/dashboard/index.d.ts.map +1 -0
- package/dist/dashboard/index.js +7 -0
- package/dist/dashboard/index.js.map +1 -0
- package/dist/diff/engine.d.ts +51 -0
- package/dist/diff/engine.d.ts.map +1 -0
- package/dist/diff/engine.js +153 -0
- package/dist/diff/engine.js.map +1 -0
- package/dist/diff/format.d.ts +10 -0
- package/dist/diff/format.d.ts.map +1 -0
- package/dist/diff/format.js +111 -0
- package/dist/diff/format.js.map +1 -0
- package/dist/diff/git.d.ts +24 -0
- package/dist/diff/git.d.ts.map +1 -0
- package/dist/diff/git.js +85 -0
- package/dist/diff/git.js.map +1 -0
- package/dist/diff/index.d.ts +7 -0
- package/dist/diff/index.d.ts.map +1 -0
- package/dist/diff/index.js +7 -0
- package/dist/diff/index.js.map +1 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +17 -0
- package/dist/index.js.map +1 -0
- package/dist/init/detect.d.ts +42 -0
- package/dist/init/detect.d.ts.map +1 -0
- package/dist/init/detect.js +185 -0
- package/dist/init/detect.js.map +1 -0
- package/dist/init/index.d.ts +39 -0
- package/dist/init/index.d.ts.map +1 -0
- package/dist/init/index.js +228 -0
- package/dist/init/index.js.map +1 -0
- package/dist/init/picker.d.ts +32 -0
- package/dist/init/picker.d.ts.map +1 -0
- package/dist/init/picker.js +105 -0
- package/dist/init/picker.js.map +1 -0
- package/dist/init/templates.d.ts +25 -0
- package/dist/init/templates.d.ts.map +1 -0
- package/dist/init/templates.js +263 -0
- package/dist/init/templates.js.map +1 -0
- package/dist/mcp/index.d.ts +12 -0
- package/dist/mcp/index.d.ts.map +1 -0
- package/dist/mcp/index.js +18 -0
- package/dist/mcp/index.js.map +1 -0
- package/dist/mcp/lookup.d.ts +27 -0
- package/dist/mcp/lookup.d.ts.map +1 -0
- package/dist/mcp/lookup.js +282 -0
- package/dist/mcp/lookup.js.map +1 -0
- package/dist/mcp/server.d.ts +41 -0
- package/dist/mcp/server.d.ts.map +1 -0
- package/dist/mcp/server.js +388 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/mcp/suggest.d.ts +35 -0
- package/dist/mcp/suggest.d.ts.map +1 -0
- package/dist/mcp/suggest.js +268 -0
- package/dist/mcp/suggest.js.map +1 -0
- package/dist/parser/comment-strip.d.ts +15 -0
- package/dist/parser/comment-strip.d.ts.map +1 -0
- package/dist/parser/comment-strip.js +76 -0
- package/dist/parser/comment-strip.js.map +1 -0
- package/dist/parser/index.d.ts +10 -0
- package/dist/parser/index.d.ts.map +1 -0
- package/dist/parser/index.js +9 -0
- package/dist/parser/index.js.map +1 -0
- package/dist/parser/normalize.d.ts +22 -0
- package/dist/parser/normalize.d.ts.map +1 -0
- package/dist/parser/normalize.js +42 -0
- package/dist/parser/normalize.js.map +1 -0
- package/dist/parser/parse-file.d.ts +18 -0
- package/dist/parser/parse-file.d.ts.map +1 -0
- package/dist/parser/parse-file.js +68 -0
- package/dist/parser/parse-file.js.map +1 -0
- package/dist/parser/parse-line.d.ts +21 -0
- package/dist/parser/parse-line.d.ts.map +1 -0
- package/dist/parser/parse-line.js +230 -0
- package/dist/parser/parse-line.js.map +1 -0
- package/dist/parser/parse-project.d.ts +31 -0
- package/dist/parser/parse-project.d.ts.map +1 -0
- package/dist/parser/parse-project.js +281 -0
- package/dist/parser/parse-project.js.map +1 -0
- package/dist/report/index.d.ts +6 -0
- package/dist/report/index.d.ts.map +1 -0
- package/dist/report/index.js +6 -0
- package/dist/report/index.js.map +1 -0
- package/dist/report/mermaid.d.ts +15 -0
- package/dist/report/mermaid.d.ts.map +1 -0
- package/dist/report/mermaid.js +260 -0
- package/dist/report/mermaid.js.map +1 -0
- package/dist/report/report.d.ts +16 -0
- package/dist/report/report.d.ts.map +1 -0
- package/dist/report/report.js +211 -0
- package/dist/report/report.js.map +1 -0
- package/dist/tui/commands.d.ts +42 -0
- package/dist/tui/commands.d.ts.map +1 -0
- package/dist/tui/commands.js +1216 -0
- package/dist/tui/commands.js.map +1 -0
- package/dist/tui/config.d.ts +27 -0
- package/dist/tui/config.d.ts.map +1 -0
- package/dist/tui/config.js +27 -0
- package/dist/tui/config.js.map +1 -0
- package/dist/tui/format.d.ts +63 -0
- package/dist/tui/format.d.ts.map +1 -0
- package/dist/tui/format.js +253 -0
- package/dist/tui/format.js.map +1 -0
- package/dist/tui/index.d.ts +18 -0
- package/dist/tui/index.d.ts.map +1 -0
- package/dist/tui/index.js +470 -0
- package/dist/tui/index.js.map +1 -0
- package/dist/tui/input.d.ts +63 -0
- package/dist/tui/input.d.ts.map +1 -0
- package/dist/tui/input.js +454 -0
- package/dist/tui/input.js.map +1 -0
- package/dist/types/index.d.ts +254 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +6 -0
- package/dist/types/index.js.map +1 -0
- package/package.json +97 -0
|
@@ -0,0 +1,1258 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GuardLink Dashboard — HTML generator (Giggs-style layout).
|
|
3
|
+
*
|
|
4
|
+
* Sidebar navigation + drawer detail panel + dark/light toggle.
|
|
5
|
+
* 7 pages: Summary, AI Analysis, Threats, Diagrams, Code, Data, Assets.
|
|
6
|
+
* Mermaid.js via CDN for diagrams. Zero build step.
|
|
7
|
+
*
|
|
8
|
+
* @exposes #dashboard to #xss [high] cwe:CWE-79 -- "Interpolates threat model data into HTML output"
|
|
9
|
+
* @mitigates #dashboard against #xss using #output-encoding -- "esc() function HTML-encodes all dynamic content"
|
|
10
|
+
* @flows #parser -> #dashboard via ThreatModel -- "Dashboard receives parsed threat model for visualization"
|
|
11
|
+
* @flows #dashboard -> Filesystem via writeFile -- "Generated HTML written to disk"
|
|
12
|
+
* @comment -- "esc() defined at line ~399 and ~1016 performs HTML entity encoding"
|
|
13
|
+
*/
|
|
14
|
+
import { computeStats, computeSeverity, computeExposures, computeAssetHeatmap } from './data.js';
|
|
15
|
+
import { generateThreatGraph, generateDataFlowDiagram, generateAttackSurface } from './diagrams.js';
|
|
16
|
+
import { readFileSync } from 'fs';
|
|
17
|
+
import { resolve, isAbsolute } from 'path';
|
|
18
|
+
export function generateDashboardHTML(model, root, analyses) {
|
|
19
|
+
const stats = computeStats(model);
|
|
20
|
+
const severity = computeSeverity(model);
|
|
21
|
+
const exposures = computeExposures(model);
|
|
22
|
+
const heatmap = computeAssetHeatmap(model);
|
|
23
|
+
const threatGraph = generateThreatGraph(model);
|
|
24
|
+
const dataFlow = generateDataFlowDiagram(model);
|
|
25
|
+
const attackSurface = generateAttackSurface(model);
|
|
26
|
+
const timestamp = new Date().toISOString().slice(0, 19).replace('T', ' ');
|
|
27
|
+
const unmitigated = exposures.filter(e => !e.mitigated && !e.accepted);
|
|
28
|
+
const mitigatedCount = exposures.filter(e => e.mitigated).length;
|
|
29
|
+
const mitigationCoveragePercent = exposures.length > 0
|
|
30
|
+
? Math.round((mitigatedCount / exposures.length) * 100)
|
|
31
|
+
: 0;
|
|
32
|
+
const riskScore = computeRiskGrade(severity, unmitigated.length, exposures.length);
|
|
33
|
+
// Build file annotations data for code browser + drawer
|
|
34
|
+
const fileAnnotations = buildFileAnnotations(model, root);
|
|
35
|
+
// Build analysis data for drawer
|
|
36
|
+
const analysisData = buildAnalysisData(model, exposures);
|
|
37
|
+
// Check for saved AI analyses
|
|
38
|
+
// (we embed the latest one if model has it, otherwise empty)
|
|
39
|
+
const aiAnalysis = ''; // Will be loaded from .guardlink/analyses/ by CLI
|
|
40
|
+
return `<!DOCTYPE html>
|
|
41
|
+
<html lang="en" data-theme="dark">
|
|
42
|
+
<head>
|
|
43
|
+
<meta charset="utf-8">
|
|
44
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
45
|
+
<title>GuardLink — ${esc(model.project)} Threat Model</title>
|
|
46
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
47
|
+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
|
|
48
|
+
<style>
|
|
49
|
+
${CSS_CONTENT}
|
|
50
|
+
</style>
|
|
51
|
+
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
|
52
|
+
<script src="https://d3js.org/d3.v7.min.js"></script>
|
|
53
|
+
</head>
|
|
54
|
+
<body>
|
|
55
|
+
|
|
56
|
+
<!-- ═══════════ TOP NAV ═══════════ -->
|
|
57
|
+
<div class="topnav">
|
|
58
|
+
<div class="topnav-left">
|
|
59
|
+
<div class="logo">TS</div>
|
|
60
|
+
<h1>${esc(model.project)}</h1>
|
|
61
|
+
<span class="badge">Threat Model</span>
|
|
62
|
+
</div>
|
|
63
|
+
<div class="topnav-right">
|
|
64
|
+
<div class="tn-stat"><span>Assets</span> <span class="tn-v blue">${stats.assets}</span></div>
|
|
65
|
+
<div class="tn-stat"><span>Open</span> <span class="tn-v red">${unmitigated.length}</span></div>
|
|
66
|
+
<div class="tn-stat"><span>Controls</span> <span class="tn-v green">${stats.controls}</span></div>
|
|
67
|
+
<div class="tn-stat"><span>Coverage</span> <span class="tn-v ${stats.coveragePercent >= 70 ? 'green' : stats.coveragePercent >= 40 ? 'yellow' : 'red'}">${stats.coveragePercent}%</span></div>
|
|
68
|
+
<button id="themeToggle" onclick="toggleTheme()" title="Toggle light/dark mode">
|
|
69
|
+
<span class="icon-sun">☀️</span><span class="icon-moon">🌙</span>
|
|
70
|
+
</button>
|
|
71
|
+
</div>
|
|
72
|
+
</div>
|
|
73
|
+
|
|
74
|
+
<div class="layout">
|
|
75
|
+
|
|
76
|
+
<!-- ═══════════ SIDEBAR ═══════════ -->
|
|
77
|
+
<nav class="sidebar">
|
|
78
|
+
<a class="active" onclick="showSection('summary',this)"><span class="nav-icon">◆</span> Executive Summary</a>
|
|
79
|
+
<a onclick="showSection('ai-analysis',this)"><span class="nav-icon">✨</span> Threat Reports</a>
|
|
80
|
+
<a onclick="showSection('threats',this)"><span class="nav-icon">⚠</span> Threats & Exposures</a>
|
|
81
|
+
<a onclick="showSection('diagrams',this)"><span class="nav-icon">◉</span> Diagrams</a>
|
|
82
|
+
<a onclick="showSection('code',this)"><span class="nav-icon"></></span> Code & Annotations</a>
|
|
83
|
+
<div class="sep"></div>
|
|
84
|
+
<a onclick="showSection('data',this)"><span class="nav-icon">🔒</span> Data & Boundaries</a>
|
|
85
|
+
<a onclick="showSection('assets',this)"><span class="nav-icon">🗺</span> Asset Heatmap</a>
|
|
86
|
+
</nav>
|
|
87
|
+
|
|
88
|
+
<!-- ═══════════ MAIN ═══════════ -->
|
|
89
|
+
<div class="main">
|
|
90
|
+
|
|
91
|
+
${renderSummaryPage(stats, severity, riskScore, unmitigated, exposures, model, mitigatedCount, mitigationCoveragePercent)}
|
|
92
|
+
${renderAIAnalysisPage(analyses || [])}
|
|
93
|
+
${renderThreatsPage(exposures, model)}
|
|
94
|
+
${renderDiagramsPage(threatGraph, dataFlow, attackSurface)}
|
|
95
|
+
${renderCodePage(fileAnnotations)}
|
|
96
|
+
${renderDataPage(model)}
|
|
97
|
+
${renderAssetsPage(heatmap)}
|
|
98
|
+
|
|
99
|
+
</div><!-- /main -->
|
|
100
|
+
</div><!-- /layout -->
|
|
101
|
+
|
|
102
|
+
<!-- ═══════════ DRAWER ═══════════ -->
|
|
103
|
+
<div class="drawer-overlay" id="drawer-overlay" onclick="closeDrawer()"></div>
|
|
104
|
+
<div class="drawer" id="drawer">
|
|
105
|
+
<div class="drawer-header">
|
|
106
|
+
<h3 id="drawer-title">Details</h3>
|
|
107
|
+
<button class="drawer-close" onclick="closeDrawer()">× Close</button>
|
|
108
|
+
</div>
|
|
109
|
+
<div class="drawer-body" id="drawer-body"></div>
|
|
110
|
+
</div>
|
|
111
|
+
|
|
112
|
+
<script>
|
|
113
|
+
/* ===== DATA ===== */
|
|
114
|
+
const fileAnnotations = ${JSON.stringify(fileAnnotations).replace(/<\//g, '<\\/')};
|
|
115
|
+
const analysisData = ${JSON.stringify(analysisData).replace(/<\//g, '<\\/')};
|
|
116
|
+
const exposuresData = ${JSON.stringify(exposures).replace(/<\//g, '<\\/')};
|
|
117
|
+
const savedAnalyses = ${JSON.stringify(analyses || []).replace(/<\//g, '<\\/')};
|
|
118
|
+
const heatmapData = ${JSON.stringify(heatmap).replace(/<\//g, '<\\/')};
|
|
119
|
+
const threatModel = ${JSON.stringify(model).replace(/<\//g, '<\\/')};
|
|
120
|
+
/* ===== SECTION NAV ===== */
|
|
121
|
+
function showSection(id, el) {
|
|
122
|
+
document.querySelectorAll('.section-content').forEach(s => s.classList.remove('active'));
|
|
123
|
+
document.querySelectorAll('.sidebar a').forEach(a => a.classList.remove('active'));
|
|
124
|
+
const sec = document.getElementById('sec-' + id);
|
|
125
|
+
if (sec) sec.classList.add('active');
|
|
126
|
+
if (el) el.classList.add('active');
|
|
127
|
+
// Re-render mermaid if switching to diagrams
|
|
128
|
+
if (id === 'diagrams' && !window._mermaidRendered) {
|
|
129
|
+
setTimeout(() => { renderMermaid(); }, 100);
|
|
130
|
+
}
|
|
131
|
+
// Render AI analysis explorer on first visit
|
|
132
|
+
if (id === 'ai-analysis' && !window._aiAnalysisRendered) {
|
|
133
|
+
renderAIAnalysis();
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/* ===== FILE TOGGLE ===== */
|
|
138
|
+
function toggleFile(header) {
|
|
139
|
+
header.classList.toggle('open');
|
|
140
|
+
header.nextElementSibling.classList.toggle('open');
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/* ===== DRAWER ===== */
|
|
144
|
+
function openDrawer(type, idx) {
|
|
145
|
+
const title = document.getElementById('drawer-title');
|
|
146
|
+
const body = document.getElementById('drawer-body');
|
|
147
|
+
let h = '';
|
|
148
|
+
|
|
149
|
+
if (type === 'open_exposure') {
|
|
150
|
+
const e = analysisData.openExposures[idx];
|
|
151
|
+
title.textContent = e.threat + ' (Open)';
|
|
152
|
+
h += sec('Status', '<span style="color:var(--red);font-weight:600">OPEN — No mitigation</span>');
|
|
153
|
+
h += sec('Severity', '<span class="fc-sev ' + sevCls(e.severity) + '">' + esc(e.severity) + '</span>');
|
|
154
|
+
h += sec('Asset', '<code>' + esc(e.asset) + '</code>');
|
|
155
|
+
h += sec('Threat', '<code>' + esc(e.threat) + '</code>');
|
|
156
|
+
if (e.description) h += sec('Description', esc(e.description));
|
|
157
|
+
h += sec('Location', '<span style="font-family:var(--font-mono);font-size:.78rem;color:var(--muted)">' + esc(e.file) + ':' + e.line + '</span>');
|
|
158
|
+
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">Recommended Action</div><div style="font-size:.78rem;margin-top:.3rem">Add a <code>@mitigates</code> annotation with a control that addresses this threat, or <code>@accepts</code> if the risk is intentionally accepted.</div></div>';
|
|
159
|
+
} else if (type === 'mitigated_exposure') {
|
|
160
|
+
const e = analysisData.mitigatedExposures[idx];
|
|
161
|
+
title.textContent = e.threat + ' (Mitigated)';
|
|
162
|
+
h += sec('Status', '<span style="color:var(--green);font-weight:600">MITIGATED</span>');
|
|
163
|
+
h += sec('Severity', '<span class="fc-sev ' + sevCls(e.severity) + '">' + esc(e.severity) + '</span>');
|
|
164
|
+
h += sec('Asset', '<code>' + esc(e.asset) + '</code>');
|
|
165
|
+
if (e.description) h += sec('Description', esc(e.description));
|
|
166
|
+
h += sec('Location', '<span style="font-family:var(--font-mono);font-size:.78rem;color:var(--muted)">' + esc(e.file) + ':' + e.line + '</span>');
|
|
167
|
+
} else if (type === 'exposure') {
|
|
168
|
+
const e = exposuresData[idx];
|
|
169
|
+
title.textContent = e.threat;
|
|
170
|
+
const status = e.mitigated ? 'MITIGATED' : e.accepted ? 'ACCEPTED' : 'OPEN';
|
|
171
|
+
const color = e.mitigated ? 'var(--green)' : e.accepted ? 'var(--sev-low)' : 'var(--red)';
|
|
172
|
+
h += sec('Status', '<span style="color:' + color + ';font-weight:600">' + status + '</span>');
|
|
173
|
+
h += sec('Severity', '<span class="fc-sev ' + sevCls(e.severity) + '">' + esc(e.severity) + '</span>');
|
|
174
|
+
h += sec('Asset', '<code>' + esc(e.asset) + '</code>');
|
|
175
|
+
if (e.description) h += sec('Description', esc(e.description));
|
|
176
|
+
h += sec('Location', '<span style="font-family:var(--font-mono);font-size:.78rem;color:var(--muted)">' + esc(e.file) + ':' + e.line + '</span>');
|
|
177
|
+
} else if (type === 'asset') {
|
|
178
|
+
const a = heatmapData[idx];
|
|
179
|
+
title.textContent = a.name + ' (Asset)';
|
|
180
|
+
|
|
181
|
+
// Risk level banner
|
|
182
|
+
const riskColors = { critical: 'var(--sev-crit)', high: 'var(--sev-high)', medium: 'var(--sev-med)', low: 'var(--sev-low)', none: 'var(--border)' };
|
|
183
|
+
const rColor = riskColors[a.riskLevel] || 'var(--border)';
|
|
184
|
+
h += sec('Risk Level', '<span style="color:' + rColor + ';font-weight:600;text-transform:uppercase">' + a.riskLevel + '</span>');
|
|
185
|
+
|
|
186
|
+
// Stats
|
|
187
|
+
h += '<div style="display:flex;gap:1rem;margin-bottom:1rem">';
|
|
188
|
+
h += '<div style="flex:1">' + sec('Exposures', '<span style="font-size:1.1rem;font-weight:600;color:var(--red)">' + a.exposures + '</span>') + '</div>';
|
|
189
|
+
h += '<div style="flex:1">' + sec('Mitigations', '<span style="font-size:1.1rem;font-weight:600;color:var(--green)">' + a.mitigations + '</span>') + '</div>';
|
|
190
|
+
h += '<div style="flex:1">' + sec('Data Flows', '<span style="font-size:1.1rem;font-weight:600;color:var(--blue)">' + a.flows + '</span>') + '</div>';
|
|
191
|
+
h += '</div>';
|
|
192
|
+
|
|
193
|
+
// Data Handling
|
|
194
|
+
if (a.dataHandling && a.dataHandling.length > 0) {
|
|
195
|
+
h += sec('Data Handled', a.dataHandling.map(d => '<span class="ann-badge ann-data" style="margin-right:4px">' + esc(d) + '</span>').join(''));
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Find related open exposures to show in the drawer
|
|
199
|
+
const openForAsset = analysisData.openExposures.filter(e => e.asset === a.name);
|
|
200
|
+
if (openForAsset.length > 0) {
|
|
201
|
+
h += '<div class="sub-h" style="color:var(--red);margin-top:1.5rem">Open Threats</div>';
|
|
202
|
+
h += '<div style="display:flex;flex-direction:column;gap:0.5rem">';
|
|
203
|
+
openForAsset.forEach(e => {
|
|
204
|
+
h += '<div style="background:var(--surface2);border:1px solid var(--border);border-left:3px solid var(--red);padding:0.5rem 0.8rem;border-radius:4px">';
|
|
205
|
+
h += '<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:0.2rem">';
|
|
206
|
+
h += '<strong>' + esc(e.threat) + '</strong>';
|
|
207
|
+
h += '<span class="fc-sev ' + sevCls(e.severity) + '">' + esc(e.severity) + '</span>';
|
|
208
|
+
h += '</div>';
|
|
209
|
+
if (e.description) h += '<div style="font-size:0.75rem;color:var(--muted)">' + esc(e.description) + '</div>';
|
|
210
|
+
h += '<div style="font-family:var(--font-mono);font-size:0.7rem;color:var(--muted);margin-top:0.3rem">' + esc(e.file) + ':' + e.line + '</div>';
|
|
211
|
+
h += '</div>';
|
|
212
|
+
});
|
|
213
|
+
h += '</div>';
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Find related mitigated exposures
|
|
217
|
+
const mitigatedForAsset = analysisData.mitigatedExposures.filter(e => e.asset === a.name);
|
|
218
|
+
if (mitigatedForAsset.length > 0) {
|
|
219
|
+
h += '<div class="sub-h" style="color:var(--green);margin-top:1.5rem">Mitigated Threats</div>';
|
|
220
|
+
h += '<div style="display:flex;flex-direction:column;gap:0.5rem">';
|
|
221
|
+
mitigatedForAsset.forEach(e => {
|
|
222
|
+
h += '<div style="background:var(--surface2);border:1px solid var(--border);border-left:3px solid var(--green);padding:0.5rem 0.8rem;border-radius:4px">';
|
|
223
|
+
h += '<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:0.2rem">';
|
|
224
|
+
h += '<strong>' + esc(e.threat) + '</strong>';
|
|
225
|
+
h += '<span class="fc-sev ' + sevCls(e.severity) + '">' + esc(e.severity) + '</span>';
|
|
226
|
+
h += '</div>';
|
|
227
|
+
if (e.description) h += '<div style="font-size:0.75rem;color:var(--muted)">' + esc(e.description) + '</div>';
|
|
228
|
+
h += '<div style="font-family:var(--font-mono);font-size:0.7rem;color:var(--muted);margin-top:0.3rem">' + esc(e.file) + ':' + e.line + '</div>';
|
|
229
|
+
h += '</div>';
|
|
230
|
+
});
|
|
231
|
+
h += '</div>';
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Find related Data Flows
|
|
235
|
+
const flowsForAsset = threatModel.flows.filter(f => f.source === a.name || f.target === a.name);
|
|
236
|
+
if (flowsForAsset.length > 0) {
|
|
237
|
+
h += '<div class="sub-h" style="color:var(--blue);margin-top:1.5rem">Data Flows</div>';
|
|
238
|
+
h += '<div style="display:flex;flex-direction:column;gap:0.5rem">';
|
|
239
|
+
flowsForAsset.forEach(f => {
|
|
240
|
+
const isSource = f.source === a.name;
|
|
241
|
+
const icon = isSource ? '<span style="color:var(--blue)">→</span>' : '<span style="color:var(--orange)">←</span>';
|
|
242
|
+
const partner = isSource ? f.target : f.source;
|
|
243
|
+
const desc = isSource ? 'Sends data to' : 'Receives data from';
|
|
244
|
+
h += '<div style="background:var(--surface2);border:1px solid var(--border);padding:0.5rem 0.8rem;border-radius:4px;font-size:0.8rem">';
|
|
245
|
+
h += '<div style="display:flex;align-items:center;gap:0.5rem;margin-bottom:0.2rem">';
|
|
246
|
+
h += icon + ' <span style="color:var(--muted)">' + desc + '</span> <strong>' + esc(partner) + '</strong>';
|
|
247
|
+
h += '</div>';
|
|
248
|
+
if (f.mechanism) h += '<div style="font-family:var(--font-mono);font-size:0.7rem;color:var(--muted)">via ' + esc(f.mechanism) + '</div>';
|
|
249
|
+
h += '</div>';
|
|
250
|
+
});
|
|
251
|
+
h += '</div>';
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Find related Boundaries
|
|
255
|
+
const boundariesForAsset = threatModel.boundaries.filter(b => b.asset_a === a.name || b.asset_b === a.name);
|
|
256
|
+
if (boundariesForAsset.length > 0) {
|
|
257
|
+
h += '<div class="sub-h" style="color:var(--purple);margin-top:1.5rem">Trust Boundaries</div>';
|
|
258
|
+
h += '<div style="display:flex;flex-direction:column;gap:0.5rem">';
|
|
259
|
+
boundariesForAsset.forEach(b => {
|
|
260
|
+
const partner = b.asset_a === a.name ? b.asset_b : b.asset_a;
|
|
261
|
+
h += '<div style="background:var(--surface2);border:1px solid var(--border);padding:0.5rem 0.8rem;border-radius:4px;font-size:0.8rem">';
|
|
262
|
+
h += '<div style="display:flex;align-items:center;gap:0.5rem;margin-bottom:0.2rem">';
|
|
263
|
+
h += '<span style="color:var(--purple)">↔</span> <span style="color:var(--muted)">Boundary with</span> <strong>' + esc(partner) + '</strong>';
|
|
264
|
+
h += '</div>';
|
|
265
|
+
if (b.description) h += '<div style="font-size:0.75rem;color:var(--muted)">' + esc(b.description) + '</div>';
|
|
266
|
+
h += '</div>';
|
|
267
|
+
});
|
|
268
|
+
h += '</div>';
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Find related Acceptances
|
|
272
|
+
const acceptedForAsset = threatModel.acceptances.filter(ac => ac.asset === a.name);
|
|
273
|
+
if (acceptedForAsset.length > 0) {
|
|
274
|
+
h += '<div class="sub-h" style="color:var(--yellow);margin-top:1.5rem">Accepted Risks</div>';
|
|
275
|
+
h += '<div style="display:flex;flex-direction:column;gap:0.5rem">';
|
|
276
|
+
acceptedForAsset.forEach(ac => {
|
|
277
|
+
h += '<div style="background:var(--surface2);border:1px solid var(--border);border-left:3px solid var(--yellow);padding:0.5rem 0.8rem;border-radius:4px">';
|
|
278
|
+
h += '<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:0.2rem">';
|
|
279
|
+
h += '<strong>' + esc(ac.threat) + '</strong>';
|
|
280
|
+
h += '</div>';
|
|
281
|
+
if (ac.description) h += '<div style="font-size:0.75rem;color:var(--muted)">' + esc(ac.description) + '</div>';
|
|
282
|
+
h += '<div style="font-family:var(--font-mono);font-size:0.7rem;color:var(--muted);margin-top:0.3rem">' + esc(ac.location.file) + ':' + ac.location.line + '</div>';
|
|
283
|
+
h += '</div>';
|
|
284
|
+
});
|
|
285
|
+
h += '</div>';
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Find related Transfers
|
|
289
|
+
const transferredForAsset = threatModel.transfers.filter(t => t.source === a.name || t.target === a.name);
|
|
290
|
+
if (transferredForAsset.length > 0) {
|
|
291
|
+
h += '<div class="sub-h" style="color:var(--purple);margin-top:1.5rem">Transferred Risks</div>';
|
|
292
|
+
h += '<div style="display:flex;flex-direction:column;gap:0.5rem">';
|
|
293
|
+
transferredForAsset.forEach(t => {
|
|
294
|
+
const isSource = t.source === a.name;
|
|
295
|
+
h += '<div style="background:var(--surface2);border:1px solid var(--border);border-left:3px solid var(--purple);padding:0.5rem 0.8rem;border-radius:4px">';
|
|
296
|
+
h += '<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:0.2rem">';
|
|
297
|
+
h += '<strong>' + esc(t.threat) + '</strong>';
|
|
298
|
+
if (isSource) {
|
|
299
|
+
h += '<span style="font-size:0.75rem;color:var(--muted)">Transferred to <strong>' + esc(t.target) + '</strong></span>';
|
|
300
|
+
} else {
|
|
301
|
+
h += '<span style="font-size:0.75rem;color:var(--muted)">Transferred from <strong>' + esc(t.source) + '</strong></span>';
|
|
302
|
+
}
|
|
303
|
+
h += '</div>';
|
|
304
|
+
if (t.description) h += '<div style="font-size:0.75rem;color:var(--muted)">' + esc(t.description) + '</div>';
|
|
305
|
+
h += '<div style="font-family:var(--font-mono);font-size:0.7rem;color:var(--muted);margin-top:0.3rem">' + esc(t.location.file) + ':' + t.location.line + '</div>';
|
|
306
|
+
h += '</div>';
|
|
307
|
+
});
|
|
308
|
+
h += '</div>';
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// Additional asset lifecycle details
|
|
312
|
+
const validations = threatModel.validations.filter(v => v.asset === a.name);
|
|
313
|
+
if (validations.length > 0) {
|
|
314
|
+
h += '<div class="sub-h" style="color:var(--green);margin-top:1.5rem">Validations</div>';
|
|
315
|
+
h += '<div style="display:flex;flex-direction:column;gap:0.5rem">';
|
|
316
|
+
validations.forEach(v => {
|
|
317
|
+
h += '<div style="background:var(--surface2);border:1px solid var(--border);padding:0.5rem 0.8rem;border-radius:4px">';
|
|
318
|
+
h += '<strong>' + esc(v.control) + '</strong>';
|
|
319
|
+
if (v.description) h += '<div style="font-size:0.75rem;color:var(--muted);margin-top:0.2rem">' + esc(v.description) + '</div>';
|
|
320
|
+
h += '<div style="font-family:var(--font-mono);font-size:0.7rem;color:var(--muted);margin-top:0.3rem">' + esc(v.location.file) + ':' + v.location.line + '</div>';
|
|
321
|
+
h += '</div>';
|
|
322
|
+
});
|
|
323
|
+
h += '</div>';
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const ownership = threatModel.ownership.filter(o => o.asset === a.name);
|
|
327
|
+
if (ownership.length > 0) {
|
|
328
|
+
h += '<div class="sub-h" style="color:var(--blue);margin-top:1.5rem">Ownership</div>';
|
|
329
|
+
h += '<div style="display:flex;flex-direction:column;gap:0.5rem">';
|
|
330
|
+
ownership.forEach(o => {
|
|
331
|
+
h += '<div style="background:var(--surface2);border:1px solid var(--border);padding:0.5rem 0.8rem;border-radius:4px">';
|
|
332
|
+
h += 'Owned by <strong>' + esc(o.owner) + '</strong>';
|
|
333
|
+
if (o.description) h += '<div style="font-size:0.75rem;color:var(--muted);margin-top:0.2rem">' + esc(o.description) + '</div>';
|
|
334
|
+
h += '<div style="font-family:var(--font-mono);font-size:0.7rem;color:var(--muted);margin-top:0.3rem">' + esc(o.location.file) + ':' + o.location.line + '</div>';
|
|
335
|
+
h += '</div>';
|
|
336
|
+
});
|
|
337
|
+
h += '</div>';
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const assumptions = threatModel.assumptions.filter(asm => asm.asset === a.name);
|
|
341
|
+
if (assumptions.length > 0) {
|
|
342
|
+
h += '<div class="sub-h" style="color:var(--yellow);margin-top:1.5rem">Assumptions</div>';
|
|
343
|
+
h += '<div style="display:flex;flex-direction:column;gap:0.5rem">';
|
|
344
|
+
assumptions.forEach(asm => {
|
|
345
|
+
h += '<div style="background:var(--surface2);border:1px solid var(--border);padding:0.5rem 0.8rem;border-radius:4px">';
|
|
346
|
+
h += '<div style="font-size:0.75rem">' + esc(asm.description || 'Assumed risk or state without description') + '</div>';
|
|
347
|
+
h += '<div style="font-family:var(--font-mono);font-size:0.7rem;color:var(--muted);margin-top:0.3rem">' + esc(asm.location.file) + ':' + asm.location.line + '</div>';
|
|
348
|
+
h += '</div>';
|
|
349
|
+
});
|
|
350
|
+
h += '</div>';
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
const audits = threatModel.audits.filter(au => au.asset === a.name);
|
|
354
|
+
if (audits.length > 0) {
|
|
355
|
+
h += '<div class="sub-h" style="color:var(--accent);margin-top:1.5rem">Audits</div>';
|
|
356
|
+
h += '<div style="display:flex;flex-direction:column;gap:0.5rem">';
|
|
357
|
+
audits.forEach(au => {
|
|
358
|
+
h += '<div style="background:var(--surface2);border:1px solid var(--border);padding:0.5rem 0.8rem;border-radius:4px">';
|
|
359
|
+
h += '<div style="font-size:0.75rem">' + esc(au.description || 'Audit trail point') + '</div>';
|
|
360
|
+
h += '<div style="font-family:var(--font-mono);font-size:0.7rem;color:var(--muted);margin-top:0.3rem">' + esc(au.location.file) + ':' + au.location.line + '</div>';
|
|
361
|
+
h += '</div>';
|
|
362
|
+
});
|
|
363
|
+
h += '</div>';
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
body.innerHTML = h;
|
|
368
|
+
document.getElementById('drawer').classList.add('open');
|
|
369
|
+
document.getElementById('drawer-overlay').classList.add('open');
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function openAnnotationDrawer(fileIdx, annIdx) {
|
|
373
|
+
const title = document.getElementById('drawer-title');
|
|
374
|
+
const body = document.getElementById('drawer-body');
|
|
375
|
+
const fentry = fileAnnotations[fileIdx];
|
|
376
|
+
if (!fentry) return;
|
|
377
|
+
const ann = fentry.annotations[annIdx];
|
|
378
|
+
if (!ann) return;
|
|
379
|
+
|
|
380
|
+
title.textContent = ann.kind.toUpperCase() + ': ' + ann.summary;
|
|
381
|
+
let h = '';
|
|
382
|
+
h += sec('Type', '<span class="ann-badge ann-' + ann.kind + '">' + ann.kind + '</span>');
|
|
383
|
+
h += sec('Location', '<span style="font-family:var(--font-mono);font-size:.78rem;color:var(--muted)">' + esc(fentry.file) + ':' + ann.line + '</span>');
|
|
384
|
+
if (ann.description) h += sec('Description', esc(ann.description));
|
|
385
|
+
if (ann.raw) h += sec('Raw Annotation', '<div class="d-code">' + esc(ann.raw) + '</div>');
|
|
386
|
+
body.innerHTML = h;
|
|
387
|
+
document.getElementById('drawer').classList.add('open');
|
|
388
|
+
document.getElementById('drawer-overlay').classList.add('open');
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function closeDrawer() {
|
|
392
|
+
document.getElementById('drawer').classList.remove('open');
|
|
393
|
+
document.getElementById('drawer-overlay').classList.remove('open');
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function esc(s) { return s == null ? '' : String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"'); }
|
|
397
|
+
function sec(label, value) { return '<div class="d-section"><div class="d-label">' + label + '</div><div class="d-value">' + value + '</div></div>'; }
|
|
398
|
+
function sevCls(s) {
|
|
399
|
+
const l = (s || '').toLowerCase();
|
|
400
|
+
if (l === 'critical' || l === 'p0') return 'crit';
|
|
401
|
+
if (l === 'high' || l === 'p1') return 'high';
|
|
402
|
+
if (l === 'medium' || l === 'p2') return 'med';
|
|
403
|
+
if (l === 'low' || l === 'p3') return 'low';
|
|
404
|
+
return 'unset';
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/* ===== THEME ===== */
|
|
408
|
+
function toggleTheme() {
|
|
409
|
+
const html = document.documentElement;
|
|
410
|
+
const next = html.getAttribute('data-theme') === 'dark' ? 'light' : 'dark';
|
|
411
|
+
html.setAttribute('data-theme', next);
|
|
412
|
+
// Re-render mermaid with new theme
|
|
413
|
+
window._mermaidRendered = false;
|
|
414
|
+
if (document.getElementById('sec-diagrams').classList.contains('active')) {
|
|
415
|
+
renderMermaid();
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/* ===== DIAGRAM TABS ===== */
|
|
420
|
+
function switchDiagramTab(id, btn) {
|
|
421
|
+
document.querySelectorAll('.diagram-panel').forEach(p => p.classList.remove('active'));
|
|
422
|
+
document.querySelectorAll('.diagram-tab').forEach(t => t.classList.remove('active'));
|
|
423
|
+
const panel = document.getElementById('dtab-' + id);
|
|
424
|
+
if (panel) panel.classList.add('active');
|
|
425
|
+
if (btn) btn.classList.add('active');
|
|
426
|
+
// Always re-render mermaid for newly visible panel
|
|
427
|
+
setTimeout(() => { renderMermaidPanel(panel); }, 50);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/* ===== MERMAID ===== */
|
|
431
|
+
async function getMermaidInstance() {
|
|
432
|
+
if (!window._mermaidMod) {
|
|
433
|
+
const mod = await import('https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs');
|
|
434
|
+
window._mermaidMod = mod.default;
|
|
435
|
+
}
|
|
436
|
+
const mermaid = window._mermaidMod;
|
|
437
|
+
const isDark = document.documentElement.getAttribute('data-theme') === 'dark';
|
|
438
|
+
mermaid.initialize({
|
|
439
|
+
startOnLoad: false,
|
|
440
|
+
theme: isDark ? 'dark' : 'default',
|
|
441
|
+
themeVariables: isDark ? {
|
|
442
|
+
primaryColor: '#1a2228', primaryTextColor: '#f0f0f0', primaryBorderColor: '#3b6779',
|
|
443
|
+
lineColor: '#55899e', secondaryColor: '#1e2830', tertiaryColor: '#0d1117',
|
|
444
|
+
background: '#0a0d10', mainBkg: '#1a2228', nodeBorder: '#3b6779',
|
|
445
|
+
clusterBkg: '#0d1117', clusterBorder: '#1f3943', fontSize: '12px', fontFamily: 'var(--font-ui)'
|
|
446
|
+
} : {
|
|
447
|
+
primaryColor: '#e8f4f8', primaryTextColor: '#1a1a2e', primaryBorderColor: '#94a3b8',
|
|
448
|
+
lineColor: '#64748b', secondaryColor: '#f1f5f9', tertiaryColor: '#ffffff',
|
|
449
|
+
background: '#ffffff', mainBkg: '#e8f4f8', nodeBorder: '#94a3b8',
|
|
450
|
+
clusterBkg: '#f8fafc', clusterBorder: '#cbd5e1', fontSize: '12px', fontFamily: 'var(--font-ui)'
|
|
451
|
+
},
|
|
452
|
+
flowchart: { curve: 'basis', padding: 15, nodeSpacing: 40, rankSpacing: 50, htmlLabels: true, useMaxWidth: false },
|
|
453
|
+
securityLevel: 'loose',
|
|
454
|
+
});
|
|
455
|
+
return mermaid;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
async function renderMermaidPanel(panel) {
|
|
459
|
+
if (!panel) return;
|
|
460
|
+
const mermaid = await getMermaidInstance();
|
|
461
|
+
const els = panel.querySelectorAll('.mermaid');
|
|
462
|
+
|
|
463
|
+
// Re-run mermaid
|
|
464
|
+
els.forEach(el => {
|
|
465
|
+
el.removeAttribute('data-processed');
|
|
466
|
+
el.innerHTML = el.getAttribute('data-original') || el.textContent;
|
|
467
|
+
});
|
|
468
|
+
await mermaid.run({ nodes: Array.from(els) });
|
|
469
|
+
|
|
470
|
+
// Add interactive zoom/pan to the rendered SVG
|
|
471
|
+
if (typeof d3 !== 'undefined') {
|
|
472
|
+
els.forEach(el => {
|
|
473
|
+
const svg = d3.select(el).select('svg');
|
|
474
|
+
if (!svg.empty()) {
|
|
475
|
+
const inner = svg.select('.root'); // Mermaid puts everything in a .root group
|
|
476
|
+
if (!inner.empty()) {
|
|
477
|
+
const zoom = d3.zoom()
|
|
478
|
+
.scaleExtent([0.1, 4])
|
|
479
|
+
.on('zoom', (e) => {
|
|
480
|
+
inner.attr('transform', e.transform);
|
|
481
|
+
});
|
|
482
|
+
svg.call(zoom);
|
|
483
|
+
|
|
484
|
+
// Let the SVG fill the container
|
|
485
|
+
svg.attr('width', '100%').attr('height', '100%').style('max-width', 'none');
|
|
486
|
+
|
|
487
|
+
// Double click to reset
|
|
488
|
+
svg.on('dblclick.zoom', null); // disable default dblclick zoom
|
|
489
|
+
svg.on('dblclick', () => {
|
|
490
|
+
svg.transition().duration(750).call(zoom.transform, d3.zoomIdentity);
|
|
491
|
+
});
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
});
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
async function renderMermaid() {
|
|
499
|
+
// Render only the currently active diagram panel
|
|
500
|
+
const active = document.querySelector('.diagram-panel.active');
|
|
501
|
+
if (active) await renderMermaidPanel(active);
|
|
502
|
+
window._mermaidRendered = true;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// Save original diagram source
|
|
506
|
+
document.querySelectorAll('.mermaid').forEach(el => {
|
|
507
|
+
el.setAttribute('data-original', el.textContent.trim());
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
/* Keyboard: Escape closes drawer */
|
|
511
|
+
document.addEventListener('keydown', e => { if (e.key === 'Escape') closeDrawer(); });
|
|
512
|
+
|
|
513
|
+
/* ===== AI ANALYSIS EXPLORER ===== */
|
|
514
|
+
let _selectedAnalysisIdx = 0;
|
|
515
|
+
|
|
516
|
+
function renderAIAnalysisContent(container, content) {
|
|
517
|
+
if (!container) return;
|
|
518
|
+
if (content && content.trim()) {
|
|
519
|
+
if (typeof marked !== 'undefined') {
|
|
520
|
+
try { container.innerHTML = marked.parse(content); }
|
|
521
|
+
catch { container.innerHTML = '<pre style="white-space:pre-wrap">' + esc(content) + '</pre>'; }
|
|
522
|
+
} else {
|
|
523
|
+
container.innerHTML = '<pre style="white-space:pre-wrap">' + esc(content) + '</pre>';
|
|
524
|
+
}
|
|
525
|
+
} else {
|
|
526
|
+
container.innerHTML = '<div class="empty-state" style="text-align:center;padding:3rem 1rem">' +
|
|
527
|
+
'<div style="font-size:3rem;margin-bottom:1rem;opacity:0.5">✨</div>' +
|
|
528
|
+
'<div style="font-size:1.1rem;font-weight:600;margin-bottom:0.5rem">No Threat Reports Yet</div>' +
|
|
529
|
+
'<div style="color:var(--muted);margin-bottom:1.5rem">Generate an AI threat report using the threat-report command</div>' +
|
|
530
|
+
'<div style="display:flex;flex-direction:column;gap:0.5rem;max-width:500px;margin:0 auto;text-align:left">' +
|
|
531
|
+
'<div style="font-size:0.88rem;color:var(--muted)"><strong>Available modes:</strong></div>' +
|
|
532
|
+
'<code style="display:block;padding:0.5rem;background:var(--surface2);border-radius:4px;font-size:0.82rem">guardlink threat-report stride</code>' +
|
|
533
|
+
'<code style="display:block;padding:0.5rem;background:var(--surface2);border-radius:4px;font-size:0.82rem">guardlink threat-report dread</code>' +
|
|
534
|
+
'<code style="display:block;padding:0.5rem;background:var(--surface2);border-radius:4px;font-size:0.82rem">guardlink threat-report pasta</code>' +
|
|
535
|
+
'<code style="display:block;padding:0.5rem;background:var(--surface2);border-radius:4px;font-size:0.82rem">guardlink threat-report attacker</code>' +
|
|
536
|
+
'<code style="display:block;padding:0.5rem;background:var(--surface2);border-radius:4px;font-size:0.82rem">guardlink threat-report rapid</code>' +
|
|
537
|
+
'<code style="display:block;padding:0.5rem;background:var(--surface2);border-radius:4px;font-size:0.82rem">guardlink threat-report general</code>' +
|
|
538
|
+
'<div style="margin-top:0.5rem;font-size:0.82rem;color:var(--muted)">Or a custom prompt: <code>guardlink threat-report general --custom "focus on auth"</code></div>' +
|
|
539
|
+
'</div></div>';
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
function formatAnalysisDate(ts) {
|
|
544
|
+
try {
|
|
545
|
+
// Handle both ISO and filename-style timestamps
|
|
546
|
+
const normalized = ts.replace(/T(\\d{2})-(\\d{2})-(\\d{2})/, 'T$1:$2:$3');
|
|
547
|
+
const d = new Date(normalized);
|
|
548
|
+
if (isNaN(d.getTime())) return ts;
|
|
549
|
+
return d.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }) +
|
|
550
|
+
' ' + d.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' });
|
|
551
|
+
} catch { return ts; }
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
function renderAIAnalysis() {
|
|
555
|
+
window._aiAnalysisRendered = true;
|
|
556
|
+
const wrap = document.querySelector('.ai-analysis-wrap');
|
|
557
|
+
const explorer = document.getElementById('ai-analyses-explorer');
|
|
558
|
+
const container = document.getElementById('ai-content');
|
|
559
|
+
if (!container) return;
|
|
560
|
+
|
|
561
|
+
const list = savedAnalyses;
|
|
562
|
+
const hasList = Array.isArray(list) && list.length > 0;
|
|
563
|
+
|
|
564
|
+
if (hasList) {
|
|
565
|
+
if (wrap) wrap.classList.add('has-explorer');
|
|
566
|
+
if (explorer) {
|
|
567
|
+
explorer.innerHTML = list.map((a, i) =>
|
|
568
|
+
'<div class="ai-analysis-item ' + (i === _selectedAnalysisIdx ? 'active' : '') + '" data-index="' + i + '" role="button" tabindex="0">' +
|
|
569
|
+
'<div class="aai-type">' + esc(a.label || a.framework || 'Analysis') + '</div>' +
|
|
570
|
+
'<div class="aai-date">' + esc(formatAnalysisDate(a.timestamp || '')) + '</div>' +
|
|
571
|
+
(a.model ? '<div class="aai-model">' + esc(a.model) + '</div>' : '') +
|
|
572
|
+
'</div>'
|
|
573
|
+
).join('');
|
|
574
|
+
|
|
575
|
+
explorer.querySelectorAll('.ai-analysis-item').forEach(function(el) {
|
|
576
|
+
el.addEventListener('click', function() {
|
|
577
|
+
var idx = parseInt(this.getAttribute('data-index'), 10);
|
|
578
|
+
if (idx === _selectedAnalysisIdx) return;
|
|
579
|
+
_selectedAnalysisIdx = idx;
|
|
580
|
+
explorer.querySelectorAll('.ai-analysis-item').forEach(function(item, i) {
|
|
581
|
+
item.classList.toggle('active', i === idx);
|
|
582
|
+
});
|
|
583
|
+
renderAIAnalysisContent(container, (list[idx] && list[idx].content) ? list[idx].content : '');
|
|
584
|
+
});
|
|
585
|
+
});
|
|
586
|
+
}
|
|
587
|
+
renderAIAnalysisContent(container, (list[_selectedAnalysisIdx] && list[_selectedAnalysisIdx].content) ? list[_selectedAnalysisIdx].content : '');
|
|
588
|
+
} else {
|
|
589
|
+
if (wrap) wrap.classList.remove('has-explorer');
|
|
590
|
+
if (explorer) explorer.innerHTML = '';
|
|
591
|
+
renderAIAnalysisContent(container, '');
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
</script>
|
|
595
|
+
|
|
596
|
+
</body>
|
|
597
|
+
</html>`;
|
|
598
|
+
}
|
|
599
|
+
// ─── Page renderers ──────────────────────────────────────────────────
|
|
600
|
+
function renderSummaryPage(stats, severity, risk, unmitigated, exposures, model, mitigatedCount, mitigationCoveragePercent) {
|
|
601
|
+
return `
|
|
602
|
+
<div id="sec-summary" class="section-content active">
|
|
603
|
+
<div class="sec-h"><span class="sec-icon">◆</span> Executive Summary</div>
|
|
604
|
+
|
|
605
|
+
<!-- Risk Grade -->
|
|
606
|
+
<div class="risk-banner risk-${risk.grade.toLowerCase()}">
|
|
607
|
+
<div class="risk-grade">${risk.grade}</div>
|
|
608
|
+
<div class="risk-detail">
|
|
609
|
+
<strong>${risk.label}</strong>
|
|
610
|
+
<span>${risk.summary}</span>
|
|
611
|
+
</div>
|
|
612
|
+
</div>
|
|
613
|
+
|
|
614
|
+
<!-- Stats Grid -->
|
|
615
|
+
<div class="stats-grid">
|
|
616
|
+
${statCard(stats.assets, 'Assets')}
|
|
617
|
+
${statCard(unmitigated.length, 'Open Threats', 'danger')}
|
|
618
|
+
${statCard(mitigatedCount, 'Mitigated', 'success')}
|
|
619
|
+
${statCard(stats.controls, 'Controls', 'success')}
|
|
620
|
+
${statCard(stats.flows, 'Data Flows')}
|
|
621
|
+
${statCard(stats.boundaries, 'Boundaries')}
|
|
622
|
+
${statCard(stats.transfers, 'Transfers')}
|
|
623
|
+
${statCard(stats.comments, 'Comments', 'muted')}
|
|
624
|
+
</div>
|
|
625
|
+
|
|
626
|
+
<!-- Coverage Bar -->
|
|
627
|
+
<div class="sub-h">Threat Mitigation Coverage</div>
|
|
628
|
+
<div style="display:flex;align-items:center;gap:.8rem;margin-bottom:.3rem">
|
|
629
|
+
<span class="coverage-pct ${mitigationCoveragePercent >= 70 ? 'good' : mitigationCoveragePercent >= 40 ? 'warn' : 'bad'}">${mitigationCoveragePercent}%</span>
|
|
630
|
+
<span style="color:var(--muted);font-size:.82rem">${mitigatedCount} of ${exposures.length} exposures mitigated</span>
|
|
631
|
+
</div>
|
|
632
|
+
<div class="posture-bar"><div class="posture-fill ${mitigationCoveragePercent >= 70 ? 'good' : mitigationCoveragePercent >= 40 ? 'warn' : 'bad'}" style="width:${Math.min(mitigationCoveragePercent, 100)}%"></div></div>
|
|
633
|
+
|
|
634
|
+
<!-- Severity Breakdown -->
|
|
635
|
+
<div class="sub-h">Severity Breakdown</div>
|
|
636
|
+
<div class="severity-chart">
|
|
637
|
+
${severityBar('Critical', severity.critical, stats.exposures, 'crit')}
|
|
638
|
+
${severityBar('High', severity.high, stats.exposures, 'high')}
|
|
639
|
+
${severityBar('Medium', severity.medium, stats.exposures, 'med')}
|
|
640
|
+
${severityBar('Low', severity.low, stats.exposures, 'low')}
|
|
641
|
+
${severity.unset > 0 ? severityBar('Unset', severity.unset, stats.exposures, 'unset') : ''}
|
|
642
|
+
</div>
|
|
643
|
+
|
|
644
|
+
${unmitigated.length > 0 ? `
|
|
645
|
+
<!-- Open Threats -->
|
|
646
|
+
<div class="sub-h" style="color:var(--red)">⚠ Open Threats (No Mitigation)</div>
|
|
647
|
+
${unmitigated.map((e, i) => `
|
|
648
|
+
<div class="finding-card" onclick="openDrawer('open_exposure', ${i})">
|
|
649
|
+
<div class="fc-top">
|
|
650
|
+
<span class="fc-risk">${esc(e.threat)}</span>
|
|
651
|
+
<span class="fc-sev ${sevClass(e.severity)}">${esc(e.severity)}</span>
|
|
652
|
+
</div>
|
|
653
|
+
${e.description ? `<div class="fc-desc">${esc(e.description)}</div>` : ''}
|
|
654
|
+
<div class="fc-assets">Asset: ${esc(e.asset)}</div>
|
|
655
|
+
</div>`).join('')}` : ''}
|
|
656
|
+
|
|
657
|
+
${model.flows.length > 0 ? `
|
|
658
|
+
<!-- Data Flows -->
|
|
659
|
+
<div class="sub-h">Data Flows</div>
|
|
660
|
+
<table>
|
|
661
|
+
<thead><tr><th>Source</th><th></th><th>Target</th><th>Mechanism</th><th>Location</th></tr></thead>
|
|
662
|
+
<tbody>
|
|
663
|
+
${model.flows.map(f => `
|
|
664
|
+
<tr>
|
|
665
|
+
<td><code>${esc(f.source)}</code></td>
|
|
666
|
+
<td style="color:var(--muted)">→</td>
|
|
667
|
+
<td><code>${esc(f.target)}</code></td>
|
|
668
|
+
<td>${esc(f.mechanism || '—')}</td>
|
|
669
|
+
<td class="loc">${f.location ? `${esc(f.location.file)}:${f.location.line}` : ''}</td>
|
|
670
|
+
</tr>`).join('')}
|
|
671
|
+
</tbody>
|
|
672
|
+
</table>` : ''}
|
|
673
|
+
</div>`;
|
|
674
|
+
}
|
|
675
|
+
function renderAIAnalysisPage(analyses) {
|
|
676
|
+
return `
|
|
677
|
+
<div id="sec-ai-analysis" class="section-content">
|
|
678
|
+
<div class="sec-h"><span class="sec-icon">✨</span> Threat Reports</div>
|
|
679
|
+
<div class="ai-analysis-wrap">
|
|
680
|
+
<aside id="ai-analyses-explorer" class="ai-analyses-explorer" aria-label="Saved analyses"></aside>
|
|
681
|
+
<div id="ai-content" class="md-content ai-analysis-main"></div>
|
|
682
|
+
</div>
|
|
683
|
+
</div>`;
|
|
684
|
+
}
|
|
685
|
+
function renderThreatsPage(exposures, model) {
|
|
686
|
+
const open = exposures.filter(e => !e.mitigated && !e.accepted);
|
|
687
|
+
const mitigated = exposures.filter(e => e.mitigated);
|
|
688
|
+
const accepted = exposures.filter(e => e.accepted);
|
|
689
|
+
return `
|
|
690
|
+
<div id="sec-threats" class="section-content">
|
|
691
|
+
<div class="sec-h"><span class="sec-icon">⚠</span> Threats & Exposures</div>
|
|
692
|
+
|
|
693
|
+
<div class="sub-h" style="color:var(--red)">Open Threats (${open.length})</div>
|
|
694
|
+
<p style="color:var(--muted);font-size:.78rem;margin-bottom:.5rem">Exposed in code but <strong>not mitigated</strong> by any control.</p>
|
|
695
|
+
${open.length > 0 ? `
|
|
696
|
+
<table>
|
|
697
|
+
<thead><tr><th>Asset</th><th>Threat</th><th>Severity</th><th>Description</th><th>Location</th></tr></thead>
|
|
698
|
+
<tbody>
|
|
699
|
+
${open.map((e, i) => `
|
|
700
|
+
<tr class="clickable" onclick="openDrawer('open_exposure', ${i})">
|
|
701
|
+
<td><code>${esc(e.asset)}</code></td>
|
|
702
|
+
<td><code>${esc(e.threat)}</code></td>
|
|
703
|
+
<td><span class="fc-sev ${sevClass(e.severity)}">${esc(e.severity)}</span></td>
|
|
704
|
+
<td>${esc(e.description || '—')}</td>
|
|
705
|
+
<td class="loc">${esc(e.file)}:${e.line}</td>
|
|
706
|
+
</tr>`).join('')}
|
|
707
|
+
</tbody>
|
|
708
|
+
</table>` : '<p class="empty-state">All exposed threats are mitigated or accepted.</p>'}
|
|
709
|
+
|
|
710
|
+
<div class="sub-h" style="color:var(--green)">Mitigated Threats (${mitigated.length})</div>
|
|
711
|
+
${mitigated.length > 0 ? `
|
|
712
|
+
<table>
|
|
713
|
+
<thead><tr><th>Asset</th><th>Threat</th><th>Severity</th><th>Description</th><th>Location</th></tr></thead>
|
|
714
|
+
<tbody>
|
|
715
|
+
${mitigated.map((e, i) => `
|
|
716
|
+
<tr class="clickable" onclick="openDrawer('mitigated_exposure', ${i})">
|
|
717
|
+
<td><code>${esc(e.asset)}</code></td>
|
|
718
|
+
<td><code>${esc(e.threat)}</code></td>
|
|
719
|
+
<td><span class="fc-sev ${sevClass(e.severity)}">${esc(e.severity)}</span></td>
|
|
720
|
+
<td>${esc(e.description || '—')}</td>
|
|
721
|
+
<td class="loc">${esc(e.file)}:${e.line}</td>
|
|
722
|
+
</tr>`).join('')}
|
|
723
|
+
</tbody>
|
|
724
|
+
</table>` : '<p class="empty-state">No mitigations found.</p>'}
|
|
725
|
+
|
|
726
|
+
${accepted.length > 0 ? `
|
|
727
|
+
<div class="sub-h" style="color:var(--yellow)">Accepted Risks (${accepted.length})</div>
|
|
728
|
+
<table>
|
|
729
|
+
<thead><tr><th>Asset</th><th>Threat</th><th>Severity</th><th>Description</th><th>Location</th></tr></thead>
|
|
730
|
+
<tbody>
|
|
731
|
+
${accepted.map(e => `
|
|
732
|
+
<tr>
|
|
733
|
+
<td><code>${esc(e.asset)}</code></td>
|
|
734
|
+
<td><code>${esc(e.threat)}</code></td>
|
|
735
|
+
<td><span class="fc-sev ${sevClass(e.severity)}">${esc(e.severity)}</span></td>
|
|
736
|
+
<td>${esc(e.description || '—')}</td>
|
|
737
|
+
<td class="loc">${esc(e.file)}:${e.line}</td>
|
|
738
|
+
</tr>`).join('')}
|
|
739
|
+
</tbody>
|
|
740
|
+
</table>` : ''}
|
|
741
|
+
|
|
742
|
+
${model.transfers.length > 0 ? `
|
|
743
|
+
<div class="sub-h" style="color:var(--purple)">Transferred Risks (${model.transfers.length})</div>
|
|
744
|
+
<table>
|
|
745
|
+
<thead><tr><th>Source</th><th>Threat</th><th>Target</th><th>Description</th><th>Location</th></tr></thead>
|
|
746
|
+
<tbody>
|
|
747
|
+
${model.transfers.map(t => `
|
|
748
|
+
<tr>
|
|
749
|
+
<td><code>${esc(t.source)}</code></td>
|
|
750
|
+
<td><code>${esc(t.threat)}</code></td>
|
|
751
|
+
<td><code>${esc(t.target)}</code></td>
|
|
752
|
+
<td>${esc(t.description || '—')}</td>
|
|
753
|
+
<td class="loc">${t.location ? `${esc(t.location.file)}:${t.location.line}` : ''}</td>
|
|
754
|
+
</tr>`).join('')}
|
|
755
|
+
</tbody>
|
|
756
|
+
</table>` : ''}
|
|
757
|
+
|
|
758
|
+
${exposures.length > 0 ? `
|
|
759
|
+
<div class="sub-h">All Exposures (${exposures.length})</div>
|
|
760
|
+
<table>
|
|
761
|
+
<thead><tr><th>Status</th><th>Asset</th><th>Threat</th><th>Severity</th><th>Description</th><th>Location</th></tr></thead>
|
|
762
|
+
<tbody>
|
|
763
|
+
${exposures.map((e, i) => `
|
|
764
|
+
<tr class="clickable ${!e.mitigated && !e.accepted ? 'row-open' : ''}" onclick="openDrawer('exposure', ${i})">
|
|
765
|
+
<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>
|
|
766
|
+
<td><code>${esc(e.asset)}</code></td>
|
|
767
|
+
<td><code>${esc(e.threat)}</code></td>
|
|
768
|
+
<td><span class="fc-sev ${sevClass(e.severity)}">${esc(e.severity)}</span></td>
|
|
769
|
+
<td>${esc(e.description || '—')}</td>
|
|
770
|
+
<td class="loc">${esc(e.file)}:${e.line}</td>
|
|
771
|
+
</tr>`).join('')}
|
|
772
|
+
</tbody>
|
|
773
|
+
</table>` : ''}
|
|
774
|
+
</div>`;
|
|
775
|
+
}
|
|
776
|
+
function renderDiagramsPage(threatGraph, dataFlow, attackSurface) {
|
|
777
|
+
const tabs = [];
|
|
778
|
+
const panels = [];
|
|
779
|
+
if (threatGraph) {
|
|
780
|
+
tabs.push({ id: 'threat-graph', label: 'Threat Graph', icon: '🔷' });
|
|
781
|
+
panels.push(`<div id="dtab-threat-graph" class="diagram-panel active"><div class="mermaid-wrap"><pre class="mermaid">\n${threatGraph}\n</pre></div></div>`);
|
|
782
|
+
}
|
|
783
|
+
if (dataFlow) {
|
|
784
|
+
tabs.push({ id: 'data-flow', label: 'Data Flow', icon: '↔' });
|
|
785
|
+
panels.push(`<div id="dtab-data-flow" class="diagram-panel"><div class="mermaid-wrap"><pre class="mermaid">\n${dataFlow}\n</pre></div></div>`);
|
|
786
|
+
}
|
|
787
|
+
if (attackSurface) {
|
|
788
|
+
tabs.push({ id: 'attack-surface', label: 'Attack Surface', icon: '⚠' });
|
|
789
|
+
panels.push(`<div id="dtab-attack-surface" class="diagram-panel"><div class="mermaid-wrap"><pre class="mermaid">\n${attackSurface}\n</pre></div></div>`);
|
|
790
|
+
}
|
|
791
|
+
if (tabs.length === 0) {
|
|
792
|
+
return `<div id="sec-diagrams" class="section-content">
|
|
793
|
+
<div class="sec-h"><span class="sec-icon">◉</span> Diagrams</div>
|
|
794
|
+
<p class="empty-state">No diagram data — add @exposes, @flows, or @mitigates annotations.</p>
|
|
795
|
+
</div>`;
|
|
796
|
+
}
|
|
797
|
+
return `
|
|
798
|
+
<div id="sec-diagrams" class="section-content">
|
|
799
|
+
<div class="sec-h"><span class="sec-icon">◉</span> Diagrams</div>
|
|
800
|
+
<p style="color:var(--muted);font-size:.78rem;margin-bottom:.8rem">Interactive diagrams from annotations. Scroll to zoom, drag to pan, double-click to reset.</p>
|
|
801
|
+
<div class="diagram-tabs">
|
|
802
|
+
${tabs.map((t, i) => `<button class="diagram-tab${i === 0 ? ' active' : ''}" onclick="switchDiagramTab('${t.id}', this)">${t.icon} ${t.label}</button>`).join('')}
|
|
803
|
+
</div>
|
|
804
|
+
${panels.join('\n')}
|
|
805
|
+
</div>`;
|
|
806
|
+
}
|
|
807
|
+
function renderCodePage(fileAnnotations) {
|
|
808
|
+
return `
|
|
809
|
+
<div id="sec-code" class="section-content">
|
|
810
|
+
<div class="sec-h"><span class="sec-icon"></></span> Code & Annotations</div>
|
|
811
|
+
<p style="color:var(--muted);font-size:.78rem;margin-bottom:.8rem">
|
|
812
|
+
Every file with GuardLink annotations. Click any annotation to see details.
|
|
813
|
+
</p>
|
|
814
|
+
${fileAnnotations.length > 0 ? fileAnnotations.map((f, fi) => `
|
|
815
|
+
<div class="file-card">
|
|
816
|
+
<div class="file-card-header" onclick="toggleFile(this)">
|
|
817
|
+
<span class="file-path">${esc(f.file)}</span>
|
|
818
|
+
<span style="display:flex;align-items:center;gap:.4rem">
|
|
819
|
+
<span class="file-count">${f.annotations.length}</span>
|
|
820
|
+
<span class="chevron">▶</span>
|
|
821
|
+
</span>
|
|
822
|
+
</div>
|
|
823
|
+
<div class="file-card-body">
|
|
824
|
+
${f.annotations.map((ann, ai) => `
|
|
825
|
+
<div class="ann-entry" onclick="openAnnotationDrawer(${fi}, ${ai})">
|
|
826
|
+
<div class="ann-header">
|
|
827
|
+
<span class="ann-line">L${ann.line}</span>
|
|
828
|
+
<span class="ann-badge ann-${ann.kind}">${ann.kind}</span>
|
|
829
|
+
<span class="ann-summary">${esc(ann.summary)}</span>
|
|
830
|
+
</div>
|
|
831
|
+
${ann.description ? `<div class="ann-desc">${esc(ann.description)}</div>` : ''}
|
|
832
|
+
${ann.codeContext.length > 0 ? `<div class="code-block">${ann.codeContext.map((cl, ci) => `<span class="${ci === ann.annLineIdx ? 'code-line-ann' : 'code-line-code'}">${esc(cl)}</span>`).join('')}</div>` : ''}
|
|
833
|
+
</div>`).join('')}
|
|
834
|
+
</div>
|
|
835
|
+
</div>`).join('') : '<p class="empty-state">No annotations found.</p>'}
|
|
836
|
+
</div>`;
|
|
837
|
+
}
|
|
838
|
+
function renderDataPage(model) {
|
|
839
|
+
return `
|
|
840
|
+
<div id="sec-data" class="section-content">
|
|
841
|
+
<div class="sec-h"><span class="sec-icon">🔒</span> Data & Boundaries</div>
|
|
842
|
+
|
|
843
|
+
${model.boundaries.length > 0 ? `
|
|
844
|
+
<div class="sub-h">Trust Boundaries</div>
|
|
845
|
+
<table>
|
|
846
|
+
<thead><tr><th>Side A</th><th></th><th>Side B</th><th>Description</th><th>Location</th></tr></thead>
|
|
847
|
+
<tbody>
|
|
848
|
+
${model.boundaries.map(b => `
|
|
849
|
+
<tr>
|
|
850
|
+
<td><code>${esc(b.asset_a)}</code></td>
|
|
851
|
+
<td style="color:var(--purple)">↔</td>
|
|
852
|
+
<td><code>${esc(b.asset_b)}</code></td>
|
|
853
|
+
<td>${esc(b.description || '—')}</td>
|
|
854
|
+
<td class="loc">${b.location ? `${esc(b.location.file)}:${b.location.line}` : ''}</td>
|
|
855
|
+
</tr>`).join('')}
|
|
856
|
+
</tbody>
|
|
857
|
+
</table>` : ''}
|
|
858
|
+
|
|
859
|
+
${model.data_handling.length > 0 ? `
|
|
860
|
+
<div class="sub-h">Data Classifications</div>
|
|
861
|
+
<table>
|
|
862
|
+
<thead><tr><th>Classification</th><th>Asset</th><th>Description</th><th>Location</th></tr></thead>
|
|
863
|
+
<tbody>
|
|
864
|
+
${model.data_handling.map(d => `
|
|
865
|
+
<tr>
|
|
866
|
+
<td><span class="ann-badge ann-data">${esc(d.classification)}</span></td>
|
|
867
|
+
<td><code>${esc(d.asset || '—')}</code></td>
|
|
868
|
+
<td>${esc(d.description || '—')}</td>
|
|
869
|
+
<td class="loc">${d.location ? `${esc(d.location.file)}:${d.location.line}` : ''}</td>
|
|
870
|
+
</tr>`).join('')}
|
|
871
|
+
</tbody>
|
|
872
|
+
</table>` : ''}
|
|
873
|
+
|
|
874
|
+
${model.comments.length > 0 ? `
|
|
875
|
+
<div class="sub-h">Developer Comments (${model.comments.length})</div>
|
|
876
|
+
<table>
|
|
877
|
+
<thead><tr><th>Comment</th><th>Location</th></tr></thead>
|
|
878
|
+
<tbody>
|
|
879
|
+
${model.comments.map(c => `
|
|
880
|
+
<tr>
|
|
881
|
+
<td>${esc(c.description || '(no description)')}</td>
|
|
882
|
+
<td class="loc">${c.location ? `${esc(c.location.file)}:${c.location.line}` : ''}</td>
|
|
883
|
+
</tr>`).join('')}
|
|
884
|
+
</tbody>
|
|
885
|
+
</table>` : ''}
|
|
886
|
+
|
|
887
|
+
${model.boundaries.length === 0 && model.data_handling.length === 0 && model.comments.length === 0
|
|
888
|
+
? '<p class="empty-state">No data classifications, trust boundaries, or comments found.</p>' : ''}
|
|
889
|
+
</div>`;
|
|
890
|
+
}
|
|
891
|
+
function renderAssetsPage(heatmap) {
|
|
892
|
+
return `
|
|
893
|
+
<div id="sec-assets" class="section-content">
|
|
894
|
+
<div class="sec-h"><span class="sec-icon">🗺</span> Asset Risk Heatmap</div>
|
|
895
|
+
<p style="color:var(--muted);font-size:.78rem;margin-bottom:.8rem">Assets sorted by risk level. Unmitigated exposures increase risk. Click an asset for details.</p>
|
|
896
|
+
${heatmap.length > 0 ? `
|
|
897
|
+
<div class="heatmap">
|
|
898
|
+
${heatmap.map((a, i) => `
|
|
899
|
+
<div class="heatmap-cell risk-cell-${a.riskLevel} clickable" onclick="openDrawer('asset', ${i})">
|
|
900
|
+
<div class="heatmap-name">${esc(a.name)}</div>
|
|
901
|
+
<div class="heatmap-stats">
|
|
902
|
+
<span title="Exposures">⚠ ${a.exposures}</span>
|
|
903
|
+
<span title="Mitigations">🛡 ${a.mitigations}</span>
|
|
904
|
+
<span title="Data flows">↔ ${a.flows}</span>
|
|
905
|
+
</div>
|
|
906
|
+
${a.dataHandling.length > 0 ? `<div class="heatmap-data">${a.dataHandling.map(d => `<span class="data-badge">${esc(d)}</span>`).join('')}</div>` : ''}
|
|
907
|
+
</div>`).join('')}
|
|
908
|
+
</div>` : '<p class="empty-state">No assets found.</p>'}
|
|
909
|
+
</div>`;
|
|
910
|
+
}
|
|
911
|
+
/** Read source file lines and extract context around a given line */
|
|
912
|
+
function readCodeContext(filePath, line, root, contextLines = 5) {
|
|
913
|
+
try {
|
|
914
|
+
const abs = root && !isAbsolute(filePath) ? resolve(root, filePath) : filePath;
|
|
915
|
+
const content = readFileSync(abs, 'utf-8');
|
|
916
|
+
const allLines = content.split('\n');
|
|
917
|
+
const start = Math.max(0, line - 1 - contextLines);
|
|
918
|
+
const end = Math.min(allLines.length, line + contextLines);
|
|
919
|
+
const slice = allLines.slice(start, end).map((l, i) => {
|
|
920
|
+
const lineNum = start + i + 1;
|
|
921
|
+
return `${String(lineNum).padStart(4)} │ ${l}`;
|
|
922
|
+
});
|
|
923
|
+
return { lines: slice, annIdx: line - 1 - start };
|
|
924
|
+
}
|
|
925
|
+
catch {
|
|
926
|
+
return { lines: [], annIdx: 0 };
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
function buildFileAnnotations(model, root) {
|
|
930
|
+
const byFile = new Map();
|
|
931
|
+
const addEntry = (kind, item, summary) => {
|
|
932
|
+
if (!item.location)
|
|
933
|
+
return;
|
|
934
|
+
const file = item.location.file;
|
|
935
|
+
if (!byFile.has(file))
|
|
936
|
+
byFile.set(file, []);
|
|
937
|
+
const { lines: codeContext, annIdx } = readCodeContext(file, item.location.line, root);
|
|
938
|
+
byFile.get(file).push({
|
|
939
|
+
kind,
|
|
940
|
+
line: item.location.line,
|
|
941
|
+
summary,
|
|
942
|
+
description: item.description || '',
|
|
943
|
+
raw: item.location.raw_text || '',
|
|
944
|
+
codeContext,
|
|
945
|
+
annLineIdx: annIdx,
|
|
946
|
+
});
|
|
947
|
+
};
|
|
948
|
+
for (const a of model.assets)
|
|
949
|
+
addEntry('asset', a, a.path.join('.'));
|
|
950
|
+
for (const t of model.threats)
|
|
951
|
+
addEntry('threat', t, t.name);
|
|
952
|
+
for (const c of model.controls)
|
|
953
|
+
addEntry('control', c, c.name);
|
|
954
|
+
for (const e of model.exposures)
|
|
955
|
+
addEntry('exposes', e, `${e.asset} → ${e.threat}`);
|
|
956
|
+
for (const m of model.mitigations)
|
|
957
|
+
addEntry('mitigates', m, `${m.control} mitigates ${m.threat}`);
|
|
958
|
+
for (const a of model.acceptances)
|
|
959
|
+
addEntry('accepts', a, `${a.asset} accepts ${a.threat}`);
|
|
960
|
+
for (const t of model.transfers)
|
|
961
|
+
addEntry('transfers', t, `${t.source} → ${t.target}`);
|
|
962
|
+
for (const f of model.flows)
|
|
963
|
+
addEntry('flow', f, `${f.source} → ${f.target}`);
|
|
964
|
+
for (const b of model.boundaries)
|
|
965
|
+
addEntry('boundary', b, `${b.asset_a} ↔ ${b.asset_b}`);
|
|
966
|
+
for (const c of model.comments)
|
|
967
|
+
addEntry('comment', c, c.description || 'Developer note');
|
|
968
|
+
const result = [];
|
|
969
|
+
for (const [file, anns] of [...byFile.entries()].sort((a, b) => a[0].localeCompare(b[0]))) {
|
|
970
|
+
result.push({ file, annotations: anns.sort((a, b) => a.line - b.line) });
|
|
971
|
+
}
|
|
972
|
+
return result;
|
|
973
|
+
}
|
|
974
|
+
function buildAnalysisData(model, exposures) {
|
|
975
|
+
return {
|
|
976
|
+
openExposures: exposures.filter(e => !e.mitigated && !e.accepted),
|
|
977
|
+
mitigatedExposures: exposures.filter(e => e.mitigated),
|
|
978
|
+
acceptedExposures: exposures.filter(e => e.accepted),
|
|
979
|
+
};
|
|
980
|
+
}
|
|
981
|
+
// ─── Template helpers ────────────────────────────────────────────────
|
|
982
|
+
function esc(s) {
|
|
983
|
+
return (s ?? '').replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
984
|
+
}
|
|
985
|
+
function statCard(value, label, variant = '') {
|
|
986
|
+
return `<div class="stat-card${variant ? ` stat-${variant}` : ''}"><div class="value">${value}</div><div class="label">${label}</div></div>`;
|
|
987
|
+
}
|
|
988
|
+
function severityBar(label, count, total, cls) {
|
|
989
|
+
const pct = total > 0 ? Math.round((count / total) * 100) : 0;
|
|
990
|
+
return `<div class="sev-row">
|
|
991
|
+
<span class="sev-label">${label}</span>
|
|
992
|
+
<div class="sev-track"><div class="sev-fill sev-fill-${cls}" style="width:${pct}%"></div></div>
|
|
993
|
+
<span class="sev-count">${count}</span>
|
|
994
|
+
</div>`;
|
|
995
|
+
}
|
|
996
|
+
function sevClass(s) {
|
|
997
|
+
const l = (s || '').toLowerCase();
|
|
998
|
+
if (l === 'critical' || l === 'p0')
|
|
999
|
+
return 'crit';
|
|
1000
|
+
if (l === 'high' || l === 'p1')
|
|
1001
|
+
return 'high';
|
|
1002
|
+
if (l === 'medium' || l === 'p2')
|
|
1003
|
+
return 'med';
|
|
1004
|
+
if (l === 'low' || l === 'p3')
|
|
1005
|
+
return 'low';
|
|
1006
|
+
return 'unset';
|
|
1007
|
+
}
|
|
1008
|
+
function computeRiskGrade(sev, unmitigatedCount, totalExposures) {
|
|
1009
|
+
if (sev.critical > 0)
|
|
1010
|
+
return { grade: 'F', label: 'Critical Risk', summary: `${sev.critical} critical exposure(s) require immediate attention` };
|
|
1011
|
+
if (sev.high >= 3 || unmitigatedCount >= 5)
|
|
1012
|
+
return { grade: 'D', label: 'High Risk', summary: `${unmitigatedCount} unmitigated exposure(s), ${sev.high} high severity` };
|
|
1013
|
+
if (sev.high >= 1 || unmitigatedCount >= 3)
|
|
1014
|
+
return { grade: 'C', label: 'Moderate Risk', summary: `${unmitigatedCount} unmitigated exposure(s) need remediation` };
|
|
1015
|
+
if (unmitigatedCount >= 1)
|
|
1016
|
+
return { grade: 'B', label: 'Low Risk', summary: `${unmitigatedCount} minor unmitigated exposure(s)` };
|
|
1017
|
+
if (totalExposures === 0)
|
|
1018
|
+
return { grade: 'A', label: 'Excellent', summary: 'No exposures detected — consider adding more annotations' };
|
|
1019
|
+
return { grade: 'A', label: 'Excellent', summary: 'All exposures mitigated or accepted' };
|
|
1020
|
+
}
|
|
1021
|
+
// ─── CSS ─────────────────────────────────────────────────────────────
|
|
1022
|
+
const CSS_CONTENT = `
|
|
1023
|
+
/* ── Reset ── */
|
|
1024
|
+
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
1025
|
+
:root { --font-ui: 'Inter', system-ui, -apple-system, sans-serif; --font-mono: 'JetBrains Mono', 'SF Mono', Menlo, Consolas, monospace; }
|
|
1026
|
+
|
|
1027
|
+
/* ══ DARK THEME (Bravos) ══ */
|
|
1028
|
+
[data-theme="dark"] {
|
|
1029
|
+
--bg: #0a0d10; --surface: #0d1117; --surface2: #1a1f25; --border: #1f3943; --border-subtle: #162028;
|
|
1030
|
+
--text: #f0f0f0; --muted: #55899e; --text-dim: #3b6779;
|
|
1031
|
+
--accent: #2dd4a7; --blue: #0360a2; --green: #10b981; --red: #ea1d1d;
|
|
1032
|
+
--orange: #f97316; --yellow: #f59e0b; --purple: #a78bfa;
|
|
1033
|
+
--sev-crit: #ef4444; --sev-high: #f97316; --sev-med: #f59e0b; --sev-low: #3b82f6; --sev-unset: #6b7280;
|
|
1034
|
+
--badge-red-bg: #7f1d1d; --badge-green-bg: #065f46; --badge-blue-bg: #1e3a5f;
|
|
1035
|
+
--risk-f: #7f1d1d; --risk-d: #7c2d12; --risk-c: #78350f; --risk-b: #1e3a5f; --risk-a: #065f46;
|
|
1036
|
+
--heatmap-crit: #7f1d1d; --heatmap-high: #7c2d12; --heatmap-med: #78350f;
|
|
1037
|
+
--heatmap-low: #1e3a5f; --heatmap-none: #1a1f25;
|
|
1038
|
+
--table-alt: #141a20; --table-hover: #1e2830; --shadow: 0 1px 3px rgba(0,0,0,.4);
|
|
1039
|
+
--logo-bg: #2dd4a7; --logo-text: #0a0d10;
|
|
1040
|
+
--drawer-w: 420px; --sidebar-w: 210px;
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
/* ══ LIGHT THEME ══ */
|
|
1044
|
+
[data-theme="light"] {
|
|
1045
|
+
--bg: #f8fafc; --surface: #ffffff; --surface2: #f1f5f9; --border: #d1d5db; --border-subtle: #e5e7eb;
|
|
1046
|
+
--text: #1a1a2e; --muted: #4a5568; --text-dim: #9ca3af;
|
|
1047
|
+
--accent: #0d9373; --blue: #2563eb; --green: #059669; --red: #dc2626;
|
|
1048
|
+
--orange: #ea580c; --yellow: #d97706; --purple: #7c3aed;
|
|
1049
|
+
--sev-crit: #dc2626; --sev-high: #ea580c; --sev-med: #d97706; --sev-low: #2563eb; --sev-unset: #6b7280;
|
|
1050
|
+
--badge-red-bg: #fef2f2; --badge-green-bg: #ecfdf5; --badge-blue-bg: #eff6ff;
|
|
1051
|
+
--risk-f: #fef2f2; --risk-d: #fff7ed; --risk-c: #fffbeb; --risk-b: #eff6ff; --risk-a: #ecfdf5;
|
|
1052
|
+
--heatmap-crit: #fef2f2; --heatmap-high: #fff7ed; --heatmap-med: #fffbeb;
|
|
1053
|
+
--heatmap-low: #eff6ff; --heatmap-none: #f9fafb;
|
|
1054
|
+
--table-alt: #f9fafb; --table-hover: #f1f5f9; --shadow: 0 1px 3px rgba(0,0,0,.08);
|
|
1055
|
+
--logo-bg: #0d9373; --logo-text: #ffffff;
|
|
1056
|
+
--drawer-w: 420px; --sidebar-w: 210px;
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
body { font-family: var(--font-ui); background: var(--bg); color: var(--text); line-height: 1.5; overflow: hidden; height: 100vh; }
|
|
1060
|
+
a { color: var(--accent); text-decoration: none; }
|
|
1061
|
+
code { background: var(--border); padding: 1px 4px; border-radius: 3px; font-size: .75rem; font-family: var(--font-mono); }
|
|
1062
|
+
|
|
1063
|
+
/* ── Top Nav ── */
|
|
1064
|
+
.topnav { height: 48px; background: var(--surface); border-bottom: 1px solid var(--border); display: flex; align-items: center; padding: 0 1.2rem; gap: 1rem; z-index: 100; }
|
|
1065
|
+
.topnav-left { display: flex; align-items: center; gap: .6rem; }
|
|
1066
|
+
.topnav-right { margin-left: auto; display: flex; align-items: center; gap: 1rem; }
|
|
1067
|
+
.topnav h1 { font-size: 1.1rem; font-weight: 700; white-space: nowrap; }
|
|
1068
|
+
.badge { background: var(--accent); color: var(--logo-text); padding: 2px 8px; border-radius: 10px; font-size: .65rem; font-weight: 600; }
|
|
1069
|
+
.tn-stat { font-size: .72rem; color: var(--muted); display: flex; align-items: center; gap: 4px; }
|
|
1070
|
+
.tn-stat .tn-v { font-weight: 700; font-size: .82rem; }
|
|
1071
|
+
.tn-v.red { color: var(--red); } .tn-v.green { color: var(--green); } .tn-v.blue { color: var(--accent); } .tn-v.yellow { color: var(--yellow); }
|
|
1072
|
+
.logo { width: 32px; height: 32px; background: var(--logo-bg); color: var(--logo-text); border-radius: 6px; display: flex; align-items: center; justify-content: center; font-weight: 700; font-size: 13px; }
|
|
1073
|
+
#themeToggle { background: var(--surface2); border: 1px solid var(--border); border-radius: 6px; padding: 4px 8px; cursor: pointer; font-size: 14px; line-height: 1; }
|
|
1074
|
+
#themeToggle:hover { background: var(--border); }
|
|
1075
|
+
[data-theme="dark"] .icon-sun { display: none; }
|
|
1076
|
+
[data-theme="light"] .icon-moon { display: none; }
|
|
1077
|
+
|
|
1078
|
+
/* ── Layout ── */
|
|
1079
|
+
.layout { display: flex; height: calc(100vh - 48px); }
|
|
1080
|
+
.sidebar { width: var(--sidebar-w); min-width: var(--sidebar-w); background: var(--surface); border-right: 1px solid var(--border); overflow-y: auto; padding: .6rem 0; }
|
|
1081
|
+
.sidebar a { display: flex; align-items: center; gap: .6rem; padding: .55rem 1rem; font-size: .8rem; color: var(--muted); cursor: pointer; border-left: 3px solid transparent; transition: all .12s; user-select: none; }
|
|
1082
|
+
.sidebar a:hover { background: var(--surface2); color: var(--text); }
|
|
1083
|
+
.sidebar a.active { color: var(--accent); border-left-color: var(--accent); background: rgba(45,212,167,.08); }
|
|
1084
|
+
.sidebar .nav-icon { font-size: 1rem; width: 20px; text-align: center; }
|
|
1085
|
+
.sidebar .sep { height: 1px; background: var(--border); margin: .5rem 1rem; }
|
|
1086
|
+
.main { flex: 1; overflow-y: auto; padding: 0; }
|
|
1087
|
+
.section-content { display: none; padding: 1.2rem 1.5rem; } .section-content.active { display: block; }
|
|
1088
|
+
|
|
1089
|
+
/* ── Drawer ── */
|
|
1090
|
+
.drawer-overlay { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,.4); z-index: 200; display: none; }
|
|
1091
|
+
.drawer-overlay.open { display: block; }
|
|
1092
|
+
.drawer { position: fixed; top: 0; right: 0; width: var(--drawer-w); height: 100vh; background: var(--surface); border-left: 1px solid var(--border); z-index: 201; transform: translateX(100%); transition: transform .25s ease; overflow-y: auto; }
|
|
1093
|
+
.drawer.open { transform: translateX(0); }
|
|
1094
|
+
.drawer-header { display: flex; align-items: center; justify-content: space-between; padding: .8rem 1rem; border-bottom: 1px solid var(--border); position: sticky; top: 0; background: var(--surface); z-index: 1; }
|
|
1095
|
+
.drawer-header h3 { font-size: .95rem; color: var(--accent); }
|
|
1096
|
+
.drawer-close { background: none; border: 1px solid var(--border); color: var(--muted); cursor: pointer; padding: 4px 10px; border-radius: 4px; font-size: .8rem; }
|
|
1097
|
+
.drawer-close:hover { color: var(--text); border-color: var(--muted); }
|
|
1098
|
+
.drawer-body { padding: 1rem; }
|
|
1099
|
+
.d-section { margin-bottom: 1rem; } .d-label { font-size: .7rem; text-transform: uppercase; color: var(--muted); letter-spacing: .5px; margin-bottom: .3rem; } .d-value { font-size: .82rem; }
|
|
1100
|
+
.d-code { background: var(--bg); border: 1px solid var(--border); border-radius: 6px; padding: .5rem .7rem; font-family: var(--font-mono); font-size: .72rem; line-height: 1.6; color: var(--muted); white-space: pre; overflow-x: auto; }
|
|
1101
|
+
|
|
1102
|
+
/* ── Section headings ── */
|
|
1103
|
+
.sec-h { font-size: 1.1rem; font-weight: 700; margin-bottom: .8rem; display: flex; align-items: center; gap: .5rem; }
|
|
1104
|
+
.sec-icon { font-size: 1.2rem; }
|
|
1105
|
+
.sub-h { font-size: .9rem; font-weight: 600; color: var(--accent); margin: 1rem 0 .5rem 0; }
|
|
1106
|
+
|
|
1107
|
+
/* ── Stats Grid ── */
|
|
1108
|
+
.stats-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(100px, 1fr)); gap: .5rem; margin-bottom: 1.2rem; }
|
|
1109
|
+
.stat-card { background: var(--surface2); border: 1px solid var(--border); border-radius: 6px; padding: .5rem; text-align: center; }
|
|
1110
|
+
.stat-card .value { font-size: 1.3rem; font-weight: 700; color: var(--accent); }
|
|
1111
|
+
.stat-card .label { font-size: .65rem; color: var(--muted); margin-top: 2px; }
|
|
1112
|
+
.stat-danger .value { color: var(--red); } .stat-success .value { color: var(--green); }
|
|
1113
|
+
.stat-muted .value { color: var(--muted); } .stat-muted .label { color: var(--text-dim); }
|
|
1114
|
+
|
|
1115
|
+
/* ── Risk Banner ── */
|
|
1116
|
+
.risk-banner { display: flex; align-items: center; gap: 20px; padding: 16px 20px; border-radius: 10px; border: 1px solid var(--border); margin-bottom: 1rem; }
|
|
1117
|
+
.risk-grade { font-size: 40px; font-weight: 700; width: 60px; height: 60px; display: flex; align-items: center; justify-content: center; border-radius: 10px; }
|
|
1118
|
+
.risk-detail { display: flex; flex-direction: column; gap: 2px; }
|
|
1119
|
+
.risk-detail strong { font-size: 15px; } .risk-detail span { font-size: 13px; color: var(--muted); }
|
|
1120
|
+
.risk-f { background: var(--risk-f); } .risk-f .risk-grade { background: var(--sev-crit); color: #fff; }
|
|
1121
|
+
.risk-d { background: var(--risk-d); } .risk-d .risk-grade { background: var(--sev-high); color: #fff; }
|
|
1122
|
+
.risk-c { background: var(--risk-c); } .risk-c .risk-grade { background: var(--sev-med); color: #fff; }
|
|
1123
|
+
.risk-b { background: var(--risk-b); } .risk-b .risk-grade { background: var(--sev-low); color: #fff; }
|
|
1124
|
+
.risk-a { background: var(--risk-a); } .risk-a .risk-grade { background: var(--green); color: #fff; }
|
|
1125
|
+
|
|
1126
|
+
/* ── Coverage Bar ── */
|
|
1127
|
+
.coverage-pct { font-size: 1.8rem; font-weight: 700; }
|
|
1128
|
+
.coverage-pct.good { color: var(--green); } .coverage-pct.warn { color: var(--yellow); } .coverage-pct.bad { color: var(--red); }
|
|
1129
|
+
.posture-bar { height: 8px; border-radius: 4px; background: var(--border); margin: .6rem 0; overflow: hidden; }
|
|
1130
|
+
.posture-fill { height: 100%; border-radius: 4px; transition: width .4s; }
|
|
1131
|
+
.posture-fill.good { background: var(--green); } .posture-fill.warn { background: var(--yellow); } .posture-fill.bad { background: var(--red); }
|
|
1132
|
+
|
|
1133
|
+
/* ── Severity Chart ── */
|
|
1134
|
+
.severity-chart { display: flex; flex-direction: column; gap: 8px; margin-bottom: 1rem; }
|
|
1135
|
+
.sev-row { display: flex; align-items: center; gap: 10px; }
|
|
1136
|
+
.sev-label { width: 55px; font-size: 13px; font-weight: 500; text-align: right; }
|
|
1137
|
+
.sev-track { flex: 1; height: 22px; background: var(--surface2); border-radius: 5px; overflow: hidden; }
|
|
1138
|
+
.sev-fill { height: 100%; border-radius: 5px; min-width: 2px; transition: width .6s; }
|
|
1139
|
+
.sev-fill-crit { background: var(--sev-crit); } .sev-fill-high { background: var(--sev-high); }
|
|
1140
|
+
.sev-fill-med { background: var(--sev-med); } .sev-fill-low { background: var(--sev-low); }
|
|
1141
|
+
.sev-fill-unset { background: var(--sev-unset); }
|
|
1142
|
+
.sev-count { width: 28px; font-size: 14px; font-weight: 600; font-family: var(--font-mono); }
|
|
1143
|
+
|
|
1144
|
+
/* ── Finding Cards ── */
|
|
1145
|
+
.finding-card { background: var(--surface2); border: 1px solid var(--border); border-radius: 6px; padding: .7rem 1rem; margin-bottom: .5rem; cursor: pointer; transition: border-color .15s; }
|
|
1146
|
+
.finding-card:hover { border-color: var(--accent); }
|
|
1147
|
+
.fc-top { display: flex; align-items: center; gap: .5rem; margin-bottom: .2rem; }
|
|
1148
|
+
.fc-risk { font-weight: 600; font-size: .85rem; } .fc-desc { font-size: .78rem; color: var(--muted); }
|
|
1149
|
+
.fc-assets { font-size: .72rem; color: var(--muted); margin-top: .2rem; font-family: var(--font-mono); }
|
|
1150
|
+
.fc-sev { font-size: .7rem; padding: 1px 6px; border-radius: 3px; font-weight: 600; }
|
|
1151
|
+
.fc-sev.crit { background: var(--sev-crit); color: #fff; } .fc-sev.high { background: var(--sev-high); color: #fff; }
|
|
1152
|
+
.fc-sev.med { background: var(--sev-med); color: #000; } .fc-sev.low { background: var(--border); color: var(--muted); }
|
|
1153
|
+
.fc-sev.unset { background: var(--border); color: var(--muted); }
|
|
1154
|
+
|
|
1155
|
+
/* ── Tables ── */
|
|
1156
|
+
table { width: 100%; border-collapse: collapse; background: var(--surface2); border-radius: 6px; overflow: hidden; margin-bottom: .8rem; }
|
|
1157
|
+
th, td { padding: .45rem .7rem; text-align: left; border-bottom: 1px solid var(--border); font-size: .78rem; }
|
|
1158
|
+
th { background: var(--border); color: var(--muted); font-weight: 600; text-transform: uppercase; font-size: .68rem; }
|
|
1159
|
+
tr.clickable { cursor: pointer; } tr.clickable:hover { background: var(--table-hover); }
|
|
1160
|
+
.row-open { border-left: 3px solid var(--red); }
|
|
1161
|
+
.loc { color: var(--muted); font-family: var(--font-mono); font-size: .72rem; white-space: nowrap; }
|
|
1162
|
+
.empty-state { color: var(--muted); font-style: italic; padding: .8rem; font-size: .82rem; }
|
|
1163
|
+
|
|
1164
|
+
/* ── Badges ── */
|
|
1165
|
+
.badge-red { display: inline-block; padding: 1px 7px; border-radius: 4px; font-size: .68rem; font-weight: 600; background: var(--badge-red-bg); color: var(--sev-crit); }
|
|
1166
|
+
.badge-green { display: inline-block; padding: 1px 7px; border-radius: 4px; font-size: .68rem; font-weight: 600; background: var(--badge-green-bg); color: var(--green); }
|
|
1167
|
+
.badge-blue { display: inline-block; padding: 1px 7px; border-radius: 4px; font-size: .68rem; font-weight: 600; background: var(--badge-blue-bg); color: var(--sev-low); }
|
|
1168
|
+
|
|
1169
|
+
/* ── Annotation badges ── */
|
|
1170
|
+
.ann-badge { display: inline-block; padding: 1px 7px; border-radius: 4px; font-size: .68rem; font-weight: 600; text-transform: uppercase; letter-spacing: .3px; }
|
|
1171
|
+
.ann-asset { background: #1c3a5e; color: #58a6ff; } .ann-threat { background: #4a1a1a; color: #f85149; }
|
|
1172
|
+
.ann-control { background: #1a3a1a; color: #3fb950; } .ann-exposes { background: #4a1a1a; color: #f85149; }
|
|
1173
|
+
.ann-mitigates { background: #1a3a1a; color: #3fb950; } .ann-accepts { background: #3a3a1a; color: #d29922; }
|
|
1174
|
+
.ann-transfers { background: #2a1a3a; color: #bc8cff; } .ann-flow { background: #2a2a2a; color: #8b949e; }
|
|
1175
|
+
.ann-boundary { background: #2a1a3a; color: #bc8cff; } .ann-data { background: #3a2a1a; color: #db6d28; }
|
|
1176
|
+
.ann-comment { background: var(--surface2); color: var(--muted); border: 1px solid var(--border); }
|
|
1177
|
+
|
|
1178
|
+
/* ── File Cards (Code Browser) ── */
|
|
1179
|
+
.file-card { background: var(--surface2); border: 1px solid var(--border); border-radius: 6px; margin-bottom: .7rem; overflow: hidden; }
|
|
1180
|
+
.file-card-header { display: flex; align-items: center; justify-content: space-between; padding: .5rem .8rem; background: var(--surface); cursor: pointer; user-select: none; }
|
|
1181
|
+
.file-card-header:hover { background: var(--border); }
|
|
1182
|
+
.file-path { font-family: var(--font-mono); font-size: .78rem; color: var(--accent); font-weight: 600; }
|
|
1183
|
+
.file-count { font-size: .68rem; color: var(--muted); background: var(--border); padding: 1px 7px; border-radius: 10px; }
|
|
1184
|
+
.chevron { color: var(--muted); transition: transform .2s; font-size: .75rem; }
|
|
1185
|
+
.file-card-header.open .chevron { transform: rotate(90deg); }
|
|
1186
|
+
.file-card-body { display: none; border-top: 1px solid var(--border); } .file-card-body.open { display: block; }
|
|
1187
|
+
.ann-entry { padding: .6rem .8rem; border-bottom: 1px solid var(--border); cursor: pointer; }
|
|
1188
|
+
.ann-entry:hover { background: rgba(45,212,167,.04); } .ann-entry:last-child { border-bottom: none; }
|
|
1189
|
+
.ann-header { display: flex; align-items: center; gap: .4rem; margin-bottom: .2rem; }
|
|
1190
|
+
.ann-line { font-family: var(--font-mono); font-size: .68rem; color: var(--muted); min-width: 35px; }
|
|
1191
|
+
.ann-summary { font-size: .78rem; font-weight: 500; }
|
|
1192
|
+
.ann-desc { font-size: .75rem; color: var(--muted); margin: .15rem 0 .25rem 0; padding-left: .5rem; border-left: 2px solid var(--border); }
|
|
1193
|
+
|
|
1194
|
+
/* ── Diagrams ── */
|
|
1195
|
+
.diagram-hint { font-size: .75rem; color: var(--muted); margin-bottom: .6rem; }
|
|
1196
|
+
.mermaid-wrap { background: var(--surface); border: 1px solid var(--border); border-radius: 6px; padding: 16px; overflow-x: auto; margin-bottom: 1rem; }
|
|
1197
|
+
.mermaid { text-align: center; } .mermaid svg { max-width: 100%; height: auto; }
|
|
1198
|
+
|
|
1199
|
+
/* ── Heatmap ── */
|
|
1200
|
+
.heatmap { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 10px; }
|
|
1201
|
+
.heatmap-cell { border-radius: 8px; padding: 12px; border: 1px solid var(--border); transition: border-color 0.15s; }
|
|
1202
|
+
.heatmap-cell.clickable { cursor: pointer; }
|
|
1203
|
+
.heatmap-cell.clickable:hover { border-color: var(--accent); }
|
|
1204
|
+
.heatmap-name { font-weight: 600; font-size: 13px; margin-bottom: 4px; font-family: var(--font-mono); word-break: break-all; }
|
|
1205
|
+
.heatmap-stats { display: flex; gap: 10px; font-size: 12px; color: var(--muted); }
|
|
1206
|
+
.heatmap-data { margin-top: 4px; display: flex; gap: 4px; flex-wrap: wrap; }
|
|
1207
|
+
.data-badge { font-size: 10px; padding: 1px 6px; border-radius: 4px; background: rgba(45,212,167,.15); color: var(--accent); font-weight: 600; text-transform: uppercase; }
|
|
1208
|
+
.risk-cell-critical { background: var(--heatmap-crit); } .risk-cell-high { background: var(--heatmap-high); }
|
|
1209
|
+
.risk-cell-medium { background: var(--heatmap-med); } .risk-cell-low { background: var(--heatmap-low); }
|
|
1210
|
+
.risk-cell-none { background: var(--heatmap-none); }
|
|
1211
|
+
|
|
1212
|
+
/* ── Code Blocks ── */
|
|
1213
|
+
.code-block { background: var(--bg); border: 1px solid var(--border); border-radius: 5px; padding: .3rem .6rem; overflow-x: auto; margin-top: .25rem; font-family: var(--font-mono); font-size: .72rem; line-height: 1.45; tab-size: 2; }
|
|
1214
|
+
.code-line-code { display: block; color: var(--muted); white-space: pre; }
|
|
1215
|
+
.code-line-ann { display: block; color: var(--accent); background: rgba(45,212,167,.08); margin: 0 -.6rem; padding: 0 .6rem; border-left: 2px solid var(--accent); white-space: pre; }
|
|
1216
|
+
|
|
1217
|
+
/* ── Diagram Tabs ── */
|
|
1218
|
+
.diagram-tabs { display: flex; gap: 0; border-bottom: 1px solid var(--border); margin-bottom: 1rem; }
|
|
1219
|
+
.diagram-tab { background: none; border: none; border-bottom: 2px solid transparent; padding: .5rem 1rem; color: var(--muted); font-size: .82rem; cursor: pointer; font-family: var(--font-ui); transition: all .15s; }
|
|
1220
|
+
.diagram-tab:hover { color: var(--text); background: var(--surface2); }
|
|
1221
|
+
.diagram-tab.active { color: var(--accent); border-bottom-color: var(--accent); }
|
|
1222
|
+
.diagram-panel { display: none; } .diagram-panel.active { display: block; }
|
|
1223
|
+
|
|
1224
|
+
/* ── AI Analysis Explorer ── */
|
|
1225
|
+
.ai-analysis-wrap { display: flex; gap: 1.5rem; margin-top: 0.75rem; min-height: 400px; }
|
|
1226
|
+
.ai-analyses-explorer { width: 240px; min-width: 240px; max-height: calc(100vh - 220px); overflow-y: auto; padding-right: 0.5rem; border-right: 1px solid var(--border); }
|
|
1227
|
+
.ai-analyses-explorer:empty { display: none; }
|
|
1228
|
+
.ai-analysis-wrap:not(.has-explorer) .ai-analyses-explorer { display: none; }
|
|
1229
|
+
.ai-analysis-main { flex: 1; min-width: 0; }
|
|
1230
|
+
.ai-analysis-item { padding: 0.5rem 0.6rem; margin-bottom: 0.35rem; border-radius: 6px; font-size: 0.8rem; cursor: pointer; border: 1px solid transparent; transition: background 0.15s, border-color 0.15s; }
|
|
1231
|
+
.ai-analysis-item:hover { background: var(--surface2); }
|
|
1232
|
+
.ai-analysis-item.active { background: rgba(45,212,167,.12); border-color: var(--accent); }
|
|
1233
|
+
.ai-analysis-item .aai-type { font-weight: 600; color: var(--accent); text-transform: capitalize; }
|
|
1234
|
+
.ai-analysis-item .aai-date { color: var(--muted); font-size: 0.72rem; margin-top: 0.2rem; }
|
|
1235
|
+
.ai-analysis-item .aai-model { color: var(--text-dim); font-size: 0.68rem; margin-top: 0.15rem; font-family: var(--font-mono); }
|
|
1236
|
+
.md-content h1 { font-size: 1.4rem; font-weight: 700; margin: 1.2rem 0 .6rem; color: var(--text); }
|
|
1237
|
+
.md-content h2 { font-size: 1.15rem; font-weight: 600; margin: 1rem 0 .5rem; color: var(--text); border-bottom: 1px solid var(--border); padding-bottom: .3rem; }
|
|
1238
|
+
.md-content h3 { font-size: 1rem; font-weight: 600; margin: .8rem 0 .4rem; color: var(--text); }
|
|
1239
|
+
.md-content p { margin: .4rem 0; line-height: 1.6; }
|
|
1240
|
+
.md-content ul, .md-content ol { margin: .4rem 0 .4rem 1.5rem; }
|
|
1241
|
+
.md-content li { margin: .2rem 0; line-height: 1.5; }
|
|
1242
|
+
.md-content code { font-family: var(--font-mono); font-size: .82rem; background: var(--surface2); padding: 1px 5px; border-radius: 3px; }
|
|
1243
|
+
.md-content pre { background: var(--bg); border: 1px solid var(--border); border-radius: 6px; padding: .8rem; overflow-x: auto; margin: .6rem 0; }
|
|
1244
|
+
.md-content pre code { background: none; padding: 0; }
|
|
1245
|
+
.md-content blockquote { border-left: 3px solid var(--accent); padding-left: .8rem; margin: .6rem 0; color: var(--muted); }
|
|
1246
|
+
.md-content table { width: 100%; border-collapse: collapse; margin: .6rem 0; font-size: .82rem; }
|
|
1247
|
+
.md-content th, .md-content td { padding: .4rem .6rem; border: 1px solid var(--border); text-align: left; }
|
|
1248
|
+
.md-content th { background: var(--surface2); font-weight: 600; }
|
|
1249
|
+
.md-content strong { color: var(--text); }
|
|
1250
|
+
|
|
1251
|
+
/* ── Responsive ── */
|
|
1252
|
+
@media (max-width: 768px) {
|
|
1253
|
+
.sidebar { width: 50px; min-width: 50px; } .sidebar a span:not(.nav-icon) { display: none; }
|
|
1254
|
+
.topnav .tn-stat { display: none; }
|
|
1255
|
+
}
|
|
1256
|
+
@media print { .topnav, .sidebar { display: none; } .main { margin: 0; } .layout { display: block; } #themeToggle { display: none; } }
|
|
1257
|
+
`;
|
|
1258
|
+
//# sourceMappingURL=generate.js.map
|