kirograph 1.2.2 → 1.3.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/README.md +4 -3
- package/dist/bin/commands/attack-surface.js +15 -2
- package/dist/bin/commands/attack-surface.js.map +2 -2
- package/dist/bin/commands/help.js +4 -2
- package/dist/bin/commands/help.js.map +2 -2
- package/dist/bin/commands/memory.js +23 -3
- package/dist/bin/commands/memory.js.map +2 -2
- package/dist/bin/commands/security-export.js +7 -1
- package/dist/bin/commands/security-export.js.map +2 -2
- package/dist/bin/commands/wiki.js +12 -1
- package/dist/bin/commands/wiki.js.map +2 -2
- package/dist/bin/installer/common.js +25 -0
- package/dist/bin/installer/common.js.map +2 -2
- package/dist/bin/installer/config-prompt.js +25 -1
- package/dist/bin/installer/config-prompt.js.map +2 -2
- package/dist/bin/installer/index.js +23 -0
- package/dist/bin/installer/index.js.map +2 -2
- package/dist/bin/installer/steering.js +5 -1
- package/dist/bin/installer/steering.js.map +2 -2
- package/dist/bin/kirograph.js +1 -1
- package/dist/config.js +63 -2
- package/dist/config.js.map +2 -2
- package/dist/mcp/handlers/memory.js +17 -5
- package/dist/mcp/handlers/memory.js.map +2 -2
- package/dist/mcp/handlers/security.js +13 -2
- package/dist/mcp/handlers/security.js.map +2 -2
- package/dist/mcp/handlers/wiki.js +11 -1
- package/dist/mcp/handlers/wiki.js.map +2 -2
- package/dist/mcp/tools.js +4 -4
- package/dist/mcp/tools.js.map +2 -2
- package/dist/memory/index.js +40 -10
- package/dist/memory/index.js.map +2 -2
- package/dist/memory/relation-display.js +37 -0
- package/dist/memory/relation-display.js.map +7 -0
- package/dist/patterns/library/sql-injection-concat-js.yaml +3 -0
- package/dist/patterns/library/sql-injection-template-js.yaml +4 -0
- package/dist/security/attack-surface.js +20 -2
- package/dist/security/attack-surface.js.map +2 -2
- package/dist/security/data-flows.js +0 -10
- package/dist/security/data-flows.js.map +2 -2
- package/dist/wiki/index.js +15 -2
- package/dist/wiki/index.js.map +2 -2
- package/dist/wiki/lint.js +23 -11
- package/dist/wiki/lint.js.map +2 -2
- package/package.json +1 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/bin/commands/security-export.ts"],
|
|
4
|
-
"sourcesContent": ["import { Command } from 'commander';\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport { spawn } from 'child_process';\nimport { loadConfig } from '../../config';\nimport { dim, reset, violet, bold, green } from '../ui';\nimport { checkLicensePolicy } from '../../security/license';\nimport type { AttackSurfaceResult } from '../../security/attack-surface';\nimport type { SecretsResult } from '../../security/secrets';\nimport type { DataFlowFinding } from '../../security/data-flows';\nimport type { RemediationStatus } from '../../security/remediation';\n\nfunction openBrowser(filePath: string): void {\n const cmd = process.platform === 'darwin' ? 'open'\n : process.platform === 'win32' ? 'start'\n : 'xdg-open';\n spawn(cmd, [filePath], { stdio: 'ignore', detached: true, shell: process.platform === 'win32' }).unref();\n}\n\n// \u2500\u2500 DB row types \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\ninterface VulnRow {\n cve_id: string;\n severity_score: number | null;\n risk_score: number | null;\n epss_score: number | null;\n epss_percentile: number | null;\n fixed_version: string | null;\n summary: string | null;\n affected_ranges: string | null;\n package_name: string | null;\n ecosystem: string | null;\n resolved_version: string | null;\n declared_constraint: string | null;\n scope: string | null;\n license: string | null;\n verdict: string | null;\n paths: string | null;\n unresolved_symbols: string | null;\n reaching_entry_point_count: number | null;\n}\n\ninterface DepRow {\n node_id: string;\n package_name: string;\n ecosystem: string;\n resolved_version: string | null;\n declared_constraint: string;\n scope: string;\n /** 'direct' | 'transitive' \u2014 derived from declared_in edges, not the scope column */\n classification: string;\n license: string | null;\n latest_version: string | null;\n latest_published: number | null;\n staleness_score: number | null;\n}\n\n// \u2500\u2500 HTML generation \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nfunction generateHtml(params: {\n projectName: string;\n generatedAt: string;\n vulns: VulnRow[];\n deps: DepRow[];\n sbomJson: string;\n vexJson: string;\n denyList: string[];\n warnList: string[];\n attackSurface: AttackSurfaceResult | null;\n secretsResult: SecretsResult | null;\n flowsResult: DataFlowFinding[] | null;\n remediationStatus: RemediationStatus[] | null;\n}): string {\n const { projectName, generatedAt, vulns, deps, sbomJson, vexJson, denyList, warnList,\n attackSurface, secretsResult, flowsResult, remediationStatus } = params;\n\n // Compute license violations for the HTML\n const licDeps = deps.map(d => ({ package_name: d.package_name, ecosystem: d.ecosystem, license: d.license }));\n const violations = checkLicensePolicy(licDeps, { deny: denyList, warn: warnList });\n const violationMap: Record<string, 'deny' | 'warn'> = {};\n for (const v of violations) {\n violationMap[`${v.ecosystem}:${v.packageName}`] = v.severity;\n }\n const licenseData = deps.map(d => ({\n package_name: d.package_name,\n ecosystem: d.ecosystem,\n license: d.license ?? null,\n status: violationMap[`${d.ecosystem}:${d.package_name}`] ?? (d.license ? 'ok' : 'unknown'),\n }));\n\n // Stats\n const totalDeps = deps.length;\n const totalVulns = vulns.length;\n const affected = vulns.filter(v => v.verdict === 'affected').length;\n const underInvestigation = vulns.filter(v => v.verdict === 'under_investigation').length;\n const notAffected = vulns.filter(v => v.verdict === 'not_affected').length;\n const staleDeps = deps.filter(d => (d.staleness_score ?? 0) >= 0.3).length;\n\n // Top 5 by EPSS \u00D7 CVSS\n const top5 = [...vulns]\n .filter(v => v.epss_score != null || v.severity_score != null)\n .sort((a, b) => {\n const scoreA = (a.epss_score ?? 0) * (a.severity_score ?? 0);\n const scoreB = (b.epss_score ?? 0) * (b.severity_score ?? 0);\n return scoreB - scoreA;\n })\n .slice(0, 5);\n\n const directCount = deps.filter(d => d.classification === 'direct').length;\n const transitiveCount = deps.filter(d => d.classification !== 'direct').length;\n const stalenessRows = deps.filter(d => d.staleness_score != null).sort((a, b) => (b.staleness_score ?? 0) - (a.staleness_score ?? 0));\n\n // Top risk CVE (by risk_score if available, fallback to CVSS)\n const topRiskCve = vulns.length > 0\n ? [...vulns].sort((a, b) => ((b as any).risk_score ?? b.severity_score ?? 0) - ((a as any).risk_score ?? a.severity_score ?? 0))[0]\n : null;\n\n // OWASP category breakdown from flows\n const owaspCounts: Record<string, number> = {};\n if (flowsResult) {\n for (const f of flowsResult) {\n owaspCounts[f.owaspCategory] = (owaspCounts[f.owaspCategory] ?? 0) + 1;\n }\n }\n const owaspOrder = ['A01','A02','A03','A04','A05','A06','A07','A08','A09','A10'];\n const owaspNames: Record<string, string> = {\n A01: 'Broken Access Control', A02: 'Cryptographic Failures', A03: 'Injection',\n A04: 'Insecure Design', A05: 'Security Misconfiguration', A06: 'Vulnerable Components',\n A07: 'Auth Failures', A08: 'Integrity Failures', A09: 'Logging Failures', A10: 'SSRF',\n };\n const owaspMaxCount = Math.max(1, ...Object.values(owaspCounts));\n\n // New feature counts\n const secretsCount = secretsResult?.totalFindings ?? null;\n const criticalFlowsCount = flowsResult?.filter(f => f.severity === 'critical').length ?? null;\n\n // Embed JSON data safely\n const safeJson = (obj: unknown) => JSON.stringify(obj).replace(/<\\/script>/gi, '<\\\\/script>');\n\n return `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n<meta charset=\"UTF-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n<title>Security Dashboard \u2014 ${projectName}</title>\n<style>\n*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }\n:root {\n --bg: #0d1117;\n --bg-card: #161b22;\n --bg-input: #21262d;\n --border: #30363d;\n --accent: #a78bfa;\n --text: #c9d1d9;\n --text-muted: #8b949e;\n --text-bright: #f0f6fc;\n --green: #3fb950;\n --yellow: #d29922;\n --red: #f85149;\n --orange: #db6d28;\n --radius: 8px;\n}\nhtml { scroll-behavior: smooth; }\nbody {\n font-family: system-ui, -apple-system, 'Segoe UI', sans-serif;\n background: var(--bg);\n color: var(--text);\n line-height: 1.5;\n font-size: 14px;\n -webkit-font-smoothing: antialiased;\n}\na { color: var(--accent); text-decoration: none; }\na:hover { text-decoration: underline; }\n\n/* Header */\n.header {\n background: var(--bg-card);\n border-bottom: 1px solid var(--border);\n padding: 1rem 2rem;\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 1rem;\n}\n.header-title { font-size: 1.1rem; font-weight: 600; color: var(--text-bright); }\n.header-meta { font-size: 0.8rem; color: var(--text-muted); }\n.header-badge {\n display: inline-flex; align-items: center; gap: 0.35rem;\n background: rgba(167,139,250,0.12); color: var(--accent);\n border: 1px solid rgba(167,139,250,0.25);\n padding: 0.25rem 0.6rem; border-radius: 4px;\n font-size: 0.75rem; font-weight: 600;\n}\n\n/* Tabs */\n.tabs {\n display: flex; gap: 0;\n background: var(--bg-card);\n border-bottom: 1px solid var(--border);\n padding: 0 2rem;\n overflow-x: auto;\n}\n.tab {\n padding: 0.75rem 1.25rem;\n cursor: pointer;\n border: none;\n background: none;\n color: var(--text-muted);\n font-size: 0.875rem;\n font-weight: 500;\n border-bottom: 2px solid transparent;\n transition: color 0.15s, border-color 0.15s;\n white-space: nowrap;\n}\n.tab:hover { color: var(--text); }\n.tab.active { color: var(--accent); border-bottom-color: var(--accent); }\n\n/* Content */\n.content { padding: 1.5rem 2rem; }\n.tab-panel { display: none; }\n.tab-panel.active { display: block; }\n\n/* Stats cards */\n.stats-grid {\n display: grid;\n grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));\n gap: 1rem;\n margin-bottom: 1.5rem;\n}\n.stat-card {\n background: var(--bg-card);\n border: 1px solid var(--border);\n border-radius: var(--radius);\n padding: 1rem;\n}\n.stat-label { font-size: 0.75rem; color: var(--text-muted); margin-bottom: 0.4rem; text-transform: uppercase; letter-spacing: 0.04em; }\n.stat-value { font-size: 1.75rem; font-weight: 700; color: var(--text-bright); line-height: 1; }\n.stat-value.red { color: var(--red); }\n.stat-value.yellow { color: var(--yellow); }\n.stat-value.green { color: var(--green); }\n.stat-value.accent { color: var(--accent); }\n\n/* Section heading */\n.section-title {\n font-size: 0.85rem; font-weight: 600;\n color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.06em;\n margin-bottom: 1rem;\n}\n\n/* Chart bar */\n.bar-chart { margin-bottom: 1.5rem; }\n.bar-row {\n display: flex; align-items: center; gap: 0.75rem;\n margin-bottom: 0.5rem;\n}\n.bar-label { width: 140px; font-size: 0.8rem; color: var(--text-muted); flex-shrink: 0; }\n.bar-track { flex: 1; height: 14px; background: var(--bg-input); border-radius: 3px; overflow: hidden; }\n.bar-fill { height: 100%; border-radius: 3px; transition: width 0.3s; }\n.bar-count { width: 40px; text-align: right; font-size: 0.8rem; color: var(--text); }\n\n/* Top CVEs */\n.top-cve-list { display: flex; flex-direction: column; gap: 0.5rem; }\n.top-cve-item {\n display: flex; align-items: center; gap: 0.75rem;\n background: var(--bg-card); border: 1px solid var(--border);\n border-radius: var(--radius); padding: 0.6rem 0.9rem;\n}\n.top-cve-rank { width: 24px; color: var(--text-muted); font-size: 0.8rem; font-weight: 600; flex-shrink: 0; }\n.top-cve-id { font-family: monospace; font-size: 0.85rem; color: var(--accent); min-width: 130px; }\n.top-cve-pkg { font-size: 0.8rem; color: var(--text); flex: 1; }\n.top-cve-scores { display: flex; gap: 0.5rem; align-items: center; }\n\n/* Filters */\n.filters {\n display: flex; gap: 0.75rem; flex-wrap: wrap;\n margin-bottom: 1rem; align-items: center;\n}\n.filter-label { font-size: 0.8rem; color: var(--text-muted); }\nselect, input[type=text], input[type=range] {\n background: var(--bg-input); border: 1px solid var(--border);\n color: var(--text); padding: 0.35rem 0.6rem;\n border-radius: var(--radius); font-size: 0.8rem;\n outline: none;\n}\nselect:focus, input[type=text]:focus { border-color: var(--accent); }\ninput[type=text] { min-width: 180px; }\n.range-group { display: flex; align-items: center; gap: 0.5rem; }\n#epssValue { min-width: 32px; font-size: 0.8rem; color: var(--accent); }\n\n/* Tables */\n.table-wrap { overflow-x: auto; }\ntable { width: 100%; border-collapse: collapse; font-size: 0.8rem; }\nth {\n text-align: left; padding: 0.5rem 0.75rem;\n background: var(--bg-card); border-bottom: 1px solid var(--border);\n color: var(--text-muted); font-weight: 600; font-size: 0.75rem;\n text-transform: uppercase; letter-spacing: 0.04em;\n white-space: nowrap;\n}\nth.sortable { cursor: pointer; user-select: none; }\nth.sortable:hover { color: var(--text); }\nth.sorted-asc::after { content: ' \u25B2'; color: var(--accent); }\nth.sorted-desc::after { content: ' \u25BC'; color: var(--accent); }\ntd { padding: 0.5rem 0.75rem; border-bottom: 1px solid var(--border); vertical-align: top; }\ntr:last-child td { border-bottom: none; }\ntr:hover td { background: rgba(255,255,255,0.02); }\n\n/* Verdict row colors */\ntr.verdict-affected td:first-child { border-left: 3px solid var(--red); }\ntr.verdict-investigating td:first-child { border-left: 3px solid var(--yellow); }\ntr.verdict-not-affected td:first-child { border-left: 3px solid var(--green); }\n\n/* Badges */\n.badge {\n display: inline-flex; align-items: center;\n padding: 0.15rem 0.45rem; border-radius: 3px;\n font-size: 0.72rem; font-weight: 600; white-space: nowrap;\n}\n.badge-red { background: rgba(248,81,73,0.15); color: var(--red); }\n.badge-yellow { background: rgba(210,153,34,0.15); color: var(--yellow); }\n.badge-green { background: rgba(63,185,80,0.15); color: var(--green); }\n.badge-gray { background: rgba(139,148,158,0.15);color: var(--text-muted); }\n.badge-accent { background: rgba(167,139,250,0.15);color: var(--accent); }\n.badge-orange { background: rgba(219,109,40,0.15); color: var(--orange); }\n\n/* Expandable row */\n.expand-row td { padding: 0; border-bottom: 1px solid var(--border); }\n.expand-body {\n padding: 0.75rem 1rem;\n background: rgba(22,27,34,0.8);\n font-size: 0.8rem; color: var(--text);\n}\n.expand-body .expand-section { margin-bottom: 0.6rem; }\n.expand-body .expand-label { color: var(--text-muted); font-size: 0.75rem; margin-bottom: 0.25rem; }\n.expand-body pre { font-family: monospace; font-size: 0.78rem; white-space: pre-wrap; color: var(--text-muted); }\n.row-toggle { cursor: pointer; }\n\n/* Download buttons */\n.btn {\n display: inline-flex; align-items: center; gap: 0.4rem;\n padding: 0.45rem 1rem; border-radius: var(--radius);\n font-size: 0.82rem; font-weight: 500;\n cursor: pointer; border: none; transition: opacity 0.15s;\n}\n.btn:hover { opacity: 0.85; }\n.btn-accent { background: var(--accent); color: #0d1117; }\n.btn-outline {\n background: none; border: 1px solid var(--border);\n color: var(--text);\n}\n.btn-outline:hover { border-color: var(--accent); color: var(--accent); }\n\n/* Staleness bar */\n.stale-bar-track { width: 80px; height: 8px; background: var(--bg-input); border-radius: 2px; overflow: hidden; display: inline-block; vertical-align: middle; }\n.stale-bar-fill { height: 100%; border-radius: 2px; }\n\n/* Policy banners */\n.policy-banner {\n display: flex; align-items: center; gap: 0.6rem;\n padding: 0.65rem 1rem; border-radius: var(--radius);\n margin-bottom: 0.75rem; font-size: 0.85rem;\n}\n.policy-banner.deny { background: rgba(248,81,73,0.1); border: 1px solid rgba(248,81,73,0.3); color: var(--red); }\n.policy-banner.warn { background: rgba(210,153,34,0.1); border: 1px solid rgba(210,153,34,0.3); color: var(--yellow); }\n\n/* Score cell */\n.score-critical { color: var(--red); font-weight: 700; }\n.score-high { color: var(--orange); font-weight: 600; }\n.score-medium { color: var(--yellow); }\n.score-low { color: var(--green); }\n\n/* Empty state */\n.empty-state { text-align: center; padding: 2.5rem; color: var(--text-muted); font-size: 0.85rem; }\n\n/* SBOM summary badges */\n.badge-counts { display: flex; gap: 0.5rem; margin-bottom: 1rem; }\n\n/* VEX state colors */\n.state-affected { color: var(--red); }\n.state-not_affected { color: var(--green); }\n.state-investigating { color: var(--yellow); }\n</style>\n</head>\n<body>\n\n<div class=\"header\">\n <div>\n <div class=\"header-title\">Security Dashboard \u2014 ${projectName}</div>\n <div class=\"header-meta\">Generated ${generatedAt}</div>\n </div>\n <div class=\"header-badge\">KiroGraph Security</div>\n</div>\n\n<div class=\"tabs\">\n <button class=\"tab active\" data-tab=\"overview\">Overview</button>\n <button class=\"tab\" data-tab=\"vulns\">Vulnerabilities</button>\n <button class=\"tab\" data-tab=\"sbom\">SBOM</button>\n <button class=\"tab\" data-tab=\"vex\">VEX</button>\n <button class=\"tab\" data-tab=\"licenses\">Licenses</button>\n <button class=\"tab\" data-tab=\"staleness\">Staleness</button>\n <button class=\"tab\" data-tab=\"attack-surface\">\u2694\uFE0F Attack Surface</button>\n <button class=\"tab\" data-tab=\"secrets\">\uD83D\uDD11 Secrets</button>\n <button class=\"tab\" data-tab=\"sast\">\uD83D\uDD0D SAST Flows</button>\n <button class=\"tab\" data-tab=\"remediation\">\u23F1\uFE0F Remediation</button>\n</div>\n\n<div class=\"content\">\n\n <!-- Tab: Overview -->\n <div class=\"tab-panel active\" id=\"tab-overview\">\n <div class=\"stats-grid\">\n <div class=\"stat-card\">\n <div class=\"stat-label\">Total Dependencies</div>\n <div class=\"stat-value accent\" id=\"stat-deps\">${totalDeps}</div>\n </div>\n <div class=\"stat-card\">\n <div class=\"stat-label\">Vulnerabilities</div>\n <div class=\"stat-value ${totalVulns > 0 ? 'red' : 'green'}\" id=\"stat-vulns\">${totalVulns}</div>\n </div>\n <div class=\"stat-card\">\n <div class=\"stat-label\">Affected</div>\n <div class=\"stat-value ${affected > 0 ? 'red' : 'green'}\">${affected}</div>\n </div>\n <div class=\"stat-card\">\n <div class=\"stat-label\">Under Investigation</div>\n <div class=\"stat-value ${underInvestigation > 0 ? 'yellow' : 'green'}\">${underInvestigation}</div>\n </div>\n <div class=\"stat-card\">\n <div class=\"stat-label\">Not Affected</div>\n <div class=\"stat-value green\">${notAffected}</div>\n </div>\n <div class=\"stat-card\">\n <div class=\"stat-label\">Stale Dependencies</div>\n <div class=\"stat-value ${staleDeps > 0 ? 'yellow' : 'green'}\">${staleDeps}</div>\n </div>\n ${secretsCount !== null ? `<div class=\"stat-card\">\n <div class=\"stat-label\">Secrets Found</div>\n <div class=\"stat-value ${secretsCount > 0 ? 'red' : 'green'}\">${secretsCount}</div>\n </div>` : ''}\n ${criticalFlowsCount !== null ? `<div class=\"stat-card\">\n <div class=\"stat-label\">Critical SAST Findings</div>\n <div class=\"stat-value ${criticalFlowsCount > 0 ? 'red' : 'green'}\">${criticalFlowsCount}</div>\n </div>` : ''}\n </div>\n\n ${topRiskCve ? `\n <div class=\"section-title\">Top Risk CVE</div>\n <div class=\"top-cve-list\" style=\"margin-bottom:1.5rem\">\n <div class=\"top-cve-item\">\n <div class=\"top-cve-rank\">#1</div>\n <div class=\"top-cve-id\">${topRiskCve.cve_id}</div>\n <div class=\"top-cve-pkg\">${topRiskCve.package_name ?? '\u2014'} ${topRiskCve.ecosystem ? `<span style=\"color:var(--text-muted);font-size:0.75rem\">[${topRiskCve.ecosystem}]</span>` : ''}</div>\n <div class=\"top-cve-scores\">\n ${topRiskCve.risk_score != null ? `<span class=\"badge ${topRiskCve.risk_score >= 7 ? 'badge-red' : topRiskCve.risk_score >= 4 ? 'badge-yellow' : 'badge-gray'}\">Risk ${topRiskCve.risk_score.toFixed(1)}</span>` : ''}\n ${topRiskCve.severity_score != null ? `<span class=\"badge ${topRiskCve.severity_score >= 9 ? 'badge-red' : topRiskCve.severity_score >= 7 ? 'badge-orange' : topRiskCve.severity_score >= 4 ? 'badge-yellow' : 'badge-gray'}\">CVSS ${topRiskCve.severity_score.toFixed(1)}</span>` : ''}\n ${topRiskCve.epss_score != null ? `<span class=\"badge ${topRiskCve.epss_score >= 0.5 ? 'badge-red' : topRiskCve.epss_score >= 0.1 ? 'badge-yellow' : 'badge-gray'}\">EPSS ${(topRiskCve.epss_score*100).toFixed(2)}%</span>` : ''}\n ${topRiskCve.verdict ? `<span class=\"badge ${topRiskCve.verdict === 'affected' ? 'badge-red' : topRiskCve.verdict === 'not_affected' ? 'badge-green' : 'badge-yellow'}\">${topRiskCve.verdict}</span>` : ''}\n </div>\n </div>\n </div>\n ` : ''}\n\n ${totalVulns > 0 ? `\n <div class=\"section-title\">Verdict Breakdown</div>\n <div class=\"bar-chart\">\n ${affected > 0 ? `<div class=\"bar-row\">\n <div class=\"bar-label\">Affected</div>\n <div class=\"bar-track\"><div class=\"bar-fill\" style=\"width:${Math.round(affected/totalVulns*100)}%;background:var(--red)\"></div></div>\n <div class=\"bar-count\">${affected}</div>\n </div>` : ''}\n ${underInvestigation > 0 ? `<div class=\"bar-row\">\n <div class=\"bar-label\">Under Investigation</div>\n <div class=\"bar-track\"><div class=\"bar-fill\" style=\"width:${Math.round(underInvestigation/totalVulns*100)}%;background:var(--yellow)\"></div></div>\n <div class=\"bar-count\">${underInvestigation}</div>\n </div>` : ''}\n ${notAffected > 0 ? `<div class=\"bar-row\">\n <div class=\"bar-label\">Not Affected</div>\n <div class=\"bar-track\"><div class=\"bar-fill\" style=\"width:${Math.round(notAffected/totalVulns*100)}%;background:var(--green)\"></div></div>\n <div class=\"bar-count\">${notAffected}</div>\n </div>` : ''}\n ${(totalVulns - affected - underInvestigation - notAffected) > 0 ? `<div class=\"bar-row\">\n <div class=\"bar-label\">Pending Analysis</div>\n <div class=\"bar-track\"><div class=\"bar-fill\" style=\"width:${Math.round((totalVulns-affected-underInvestigation-notAffected)/totalVulns*100)}%;background:var(--text-muted)\"></div></div>\n <div class=\"bar-count\">${totalVulns - affected - underInvestigation - notAffected}</div>\n </div>` : ''}\n </div>\n ` : ''}\n\n ${Object.keys(owaspCounts).length > 0 ? `\n <div class=\"section-title\">OWASP Category Breakdown</div>\n <div class=\"bar-chart\" style=\"margin-bottom:1.5rem\">\n ${owaspOrder.filter(cat => owaspCounts[cat] > 0).map(cat => `<div class=\"bar-row\">\n <div class=\"bar-label\" style=\"width:200px\"><span class=\"badge badge-accent\" style=\"margin-right:0.4rem\">${cat}</span>${owaspNames[cat] ?? cat}</div>\n <div class=\"bar-track\"><div class=\"bar-fill\" style=\"width:${Math.round((owaspCounts[cat]??0)/owaspMaxCount*100)}%;background:var(--orange)\"></div></div>\n <div class=\"bar-count\">${owaspCounts[cat]}</div>\n </div>`).join('')}\n </div>\n ` : ''}\n\n ${top5.length > 0 ? `\n <div class=\"section-title\">Top 5 Critical CVEs</div>\n <div class=\"top-cve-list\">\n ${top5.map((v, i) => `\n <div class=\"top-cve-item\">\n <div class=\"top-cve-rank\">#${i+1}</div>\n <div class=\"top-cve-id\">${v.cve_id}</div>\n <div class=\"top-cve-pkg\">${v.package_name ?? '\u2014'} ${v.ecosystem ? `<span style=\"color:var(--text-muted);font-size:0.75rem\">[${v.ecosystem}]</span>` : ''}</div>\n <div class=\"top-cve-scores\">\n ${v.severity_score != null ? `<span class=\"badge ${v.severity_score >= 9 ? 'badge-red' : v.severity_score >= 7 ? 'badge-orange' : v.severity_score >= 4 ? 'badge-yellow' : 'badge-gray'}\">CVSS ${v.severity_score.toFixed(1)}</span>` : ''}\n ${v.epss_score != null ? `<span class=\"badge ${v.epss_score >= 0.5 ? 'badge-red' : v.epss_score >= 0.1 ? 'badge-yellow' : 'badge-gray'}\">EPSS ${(v.epss_score*100).toFixed(2)}%</span>` : ''}\n ${v.verdict ? `<span class=\"badge ${v.verdict === 'affected' ? 'badge-red' : v.verdict === 'not_affected' ? 'badge-green' : 'badge-yellow'}\">${v.verdict}</span>` : ''}\n </div>\n </div>`).join('')}\n </div>\n ` : totalVulns === 0 ? '<div class=\"empty-state\">No vulnerabilities found.</div>' : ''}\n </div>\n\n <!-- Tab: Vulnerabilities -->\n <div class=\"tab-panel\" id=\"tab-vulns\">\n <div class=\"filters\">\n <div class=\"filter-label\">Severity:</div>\n <select id=\"filterSeverity\">\n <option value=\"all\">All</option>\n <option value=\"critical\">Critical (\u22659)</option>\n <option value=\"high\">High (\u22657)</option>\n <option value=\"medium\">Medium (\u22654)</option>\n <option value=\"low\">Low (<4)</option>\n </select>\n <div class=\"filter-label\">Verdict:</div>\n <select id=\"filterVerdict\">\n <option value=\"all\">All</option>\n <option value=\"affected\">Affected</option>\n <option value=\"under_investigation\">Under Investigation</option>\n <option value=\"not_affected\">Not Affected</option>\n </select>\n <div class=\"filter-label\">Min EPSS:</div>\n <div class=\"range-group\">\n <input type=\"range\" id=\"filterEpss\" min=\"0\" max=\"1\" step=\"0.01\" value=\"0\">\n <span id=\"epssValue\">0.00</span>\n </div>\n </div>\n <div class=\"table-wrap\">\n <table id=\"vulnTable\">\n <thead>\n <tr>\n <th class=\"sortable\" data-col=\"cve_id\">CVE ID</th>\n <th class=\"sortable\" data-col=\"package_name\">Package</th>\n <th>Ecosystem</th>\n <th class=\"sortable\" data-col=\"severity_score\">CVSS</th>\n <th class=\"sortable sorted-desc\" data-col=\"epss_score\">EPSS</th>\n <th class=\"sortable\" data-col=\"epss_percentile\">Percentile</th>\n <th class=\"sortable\" data-col=\"risk_score\">Risk Score</th>\n <th>Verdict</th>\n <th>Fix</th>\n </tr>\n </thead>\n <tbody id=\"vulnBody\"></tbody>\n </table>\n </div>\n <div id=\"vulnEmpty\" class=\"empty-state\" style=\"display:none\">No vulnerabilities match the current filters.</div>\n </div>\n\n <!-- Tab: SBOM -->\n <div class=\"tab-panel\" id=\"tab-sbom\">\n <div style=\"display:flex;align-items:center;justify-content:space-between;margin-bottom:1rem\">\n <div class=\"badge-counts\">\n <span class=\"badge badge-accent\">${directCount} direct</span>\n <span class=\"badge badge-gray\">${transitiveCount} transitive</span>\n </div>\n <button class=\"btn btn-outline\" onclick=\"downloadSBOM()\">\u2193 Download SBOM JSON</button>\n </div>\n <div class=\"filters\">\n <div class=\"filter-label\">Ecosystem:</div>\n <select id=\"filterEco\">\n <option value=\"all\">All</option>\n ${[...new Set(deps.map(d => d.ecosystem))].sort().map(e => `<option value=\"${e}\">${e}</option>`).join('')}\n </select>\n <input type=\"text\" id=\"filterPkg\" placeholder=\"Search package\u2026\">\n </div>\n <div class=\"table-wrap\">\n <table id=\"sbomTable\">\n <thead>\n <tr>\n <th>Package</th>\n <th>Ecosystem</th>\n <th>Version</th>\n <th>Scope</th>\n <th>License</th>\n <th>purl</th>\n </tr>\n </thead>\n <tbody id=\"sbomBody\"></tbody>\n </table>\n </div>\n <div id=\"sbomEmpty\" class=\"empty-state\" style=\"display:none\">No packages match the current filters.</div>\n </div>\n\n <!-- Tab: VEX -->\n <div class=\"tab-panel\" id=\"tab-vex\">\n <div style=\"display:flex;justify-content:flex-end;margin-bottom:1rem\">\n <button class=\"btn btn-outline\" onclick=\"downloadVEX()\">\u2193 Download VEX JSON</button>\n </div>\n <div class=\"table-wrap\">\n <table id=\"vexTable\">\n <thead>\n <tr>\n <th>CVE ID</th>\n <th>Package</th>\n <th>Analysis State</th>\n <th>Justification</th>\n <th>Detail</th>\n </tr>\n </thead>\n <tbody id=\"vexBody\"></tbody>\n </table>\n </div>\n <div id=\"vexEmpty\" class=\"empty-state\" style=\"display:none\">No VEX entries found.</div>\n </div>\n\n <!-- Tab: Licenses -->\n <div class=\"tab-panel\" id=\"tab-licenses\">\n <div id=\"licBanners\"></div>\n <div class=\"filters\">\n <div class=\"filter-label\">Policy:</div>\n <select id=\"filterLicPolicy\">\n <option value=\"all\">All</option>\n <option value=\"deny\">Deny</option>\n <option value=\"warn\">Warn</option>\n <option value=\"ok\">OK</option>\n <option value=\"unknown\">Unknown</option>\n </select>\n </div>\n <div class=\"table-wrap\">\n <table id=\"licTable\">\n <thead>\n <tr>\n <th>Package</th>\n <th>Ecosystem</th>\n <th>License</th>\n <th>Policy Status</th>\n </tr>\n </thead>\n <tbody id=\"licBody\"></tbody>\n </table>\n </div>\n <div id=\"licEmpty\" class=\"empty-state\" style=\"display:none\">No packages match the current filters.</div>\n </div>\n\n <!-- Tab: Staleness -->\n <div class=\"tab-panel\" id=\"tab-staleness\">\n <div class=\"stats-grid\" style=\"grid-template-columns:repeat(3,1fr);max-width:600px;margin-bottom:1.5rem\">\n <div class=\"stat-card\">\n <div class=\"stat-label\">Scored</div>\n <div class=\"stat-value accent\">${stalenessRows.length}</div>\n </div>\n <div class=\"stat-card\">\n <div class=\"stat-label\">Low (0\u20130.3)</div>\n <div class=\"stat-value green\">${stalenessRows.filter(r => (r.staleness_score??0) < 0.3).length}</div>\n </div>\n <div class=\"stat-card\">\n <div class=\"stat-label\">High (0.7\u20131.0)</div>\n <div class=\"stat-value red\">${stalenessRows.filter(r => (r.staleness_score??0) >= 0.7).length}</div>\n </div>\n </div>\n\n ${stalenessRows.length > 0 ? `\n <div class=\"section-title\">Distribution</div>\n <div class=\"bar-chart\" style=\"margin-bottom:1.5rem\">\n <div class=\"bar-row\">\n <div class=\"bar-label\">Low (0\u20130.3)</div>\n <div class=\"bar-track\"><div class=\"bar-fill\" style=\"width:${Math.round(stalenessRows.filter(r=>(r.staleness_score??0)<0.3).length/stalenessRows.length*100)}%;background:var(--green)\"></div></div>\n <div class=\"bar-count\">${stalenessRows.filter(r=>(r.staleness_score??0)<0.3).length}</div>\n </div>\n <div class=\"bar-row\">\n <div class=\"bar-label\">Medium (0.3\u20130.7)</div>\n <div class=\"bar-track\"><div class=\"bar-fill\" style=\"width:${Math.round(stalenessRows.filter(r=>{const s=r.staleness_score??0;return s>=0.3&&s<0.7}).length/stalenessRows.length*100)}%;background:var(--yellow)\"></div></div>\n <div class=\"bar-count\">${stalenessRows.filter(r=>{const s=r.staleness_score??0;return s>=0.3&&s<0.7}).length}</div>\n </div>\n <div class=\"bar-row\">\n <div class=\"bar-label\">High (0.7\u20131.0)</div>\n <div class=\"bar-track\"><div class=\"bar-fill\" style=\"width:${Math.round(stalenessRows.filter(r=>(r.staleness_score??0)>=0.7).length/stalenessRows.length*100)}%;background:var(--red)\"></div></div>\n <div class=\"bar-count\">${stalenessRows.filter(r=>(r.staleness_score??0)>=0.7).length}</div>\n </div>\n </div>\n ` : ''}\n\n <div class=\"table-wrap\">\n <table id=\"staleTable\">\n <thead>\n <tr>\n <th>Package</th>\n <th>Ecosystem</th>\n <th>Current</th>\n <th>Latest</th>\n <th>Months Behind</th>\n <th class=\"sortable sorted-desc\" data-col=\"staleness_score\">Score</th>\n <th>Bar</th>\n </tr>\n </thead>\n <tbody id=\"staleBody\"></tbody>\n </table>\n </div>\n <div id=\"staleEmpty\" class=\"empty-state\" style=\"display:none\">No staleness data available.</div>\n </div>\n\n <!-- Tab: Attack Surface -->\n <div class=\"tab-panel\" id=\"tab-attack-surface\">\n ${attackSurface ? `\n <div class=\"stats-grid\" style=\"grid-template-columns:repeat(4,1fr);max-width:700px;margin-bottom:1.5rem\">\n <div class=\"stat-card\">\n <div class=\"stat-label\">Total Routes</div>\n <div class=\"stat-value accent\">${attackSurface.totalRoutes}</div>\n </div>\n <div class=\"stat-card\">\n <div class=\"stat-label\">Public</div>\n <div class=\"stat-value ${attackSurface.publicRoutes > 0 ? 'yellow' : 'green'}\">${attackSurface.publicRoutes}</div>\n </div>\n <div class=\"stat-card\">\n <div class=\"stat-label\">Authenticated</div>\n <div class=\"stat-value accent\">${attackSurface.authenticatedRoutes}</div>\n </div>\n <div class=\"stat-card\">\n <div class=\"stat-label\">With Vulnerabilities</div>\n <div class=\"stat-value ${attackSurface.routesWithVulns > 0 ? 'red' : 'green'}\">${attackSurface.routesWithVulns}</div>\n </div>\n </div>\n ${attackSurface.allRoutes.length > 0 ? `\n <div class=\"table-wrap\">\n <table id=\"attackSurfaceTable\">\n <thead>\n <tr>\n <th>Route</th>\n <th>Exposure</th>\n <th>Vuln Count</th>\n <th>Highest Risk</th>\n <th>Hops</th>\n </tr>\n </thead>\n <tbody>\n ${attackSurface.allRoutes.map(entry => {\n const exposureBadge = entry.exposureLevel === 'public'\n ? '<span class=\"badge badge-red\">public</span>'\n : entry.exposureLevel === 'authenticated'\n ? '<span class=\"badge badge-yellow\">authenticated</span>'\n : entry.exposureLevel === 'internal'\n ? '<span class=\"badge badge-gray\">internal</span>'\n : '<span class=\"badge badge-gray\">unknown</span>';\n const hasVulns = entry.vulnerableDeps.length > 0;\n const rowStyle = (entry.exposureLevel === 'public' && hasVulns)\n ? 'style=\"background:rgba(248,81,73,0.06)\"'\n : (entry.exposureLevel === 'authenticated' && hasVulns)\n ? 'style=\"background:rgba(210,153,34,0.06)\"'\n : '';\n const highestRisk = entry.riskScore > 0 ? entry.riskScore.toFixed(1) : '\u2014';\n const minHop = entry.vulnerableDeps.length > 0\n ? Math.min(...entry.vulnerableDeps.map(d => d.hopCount))\n : null;\n return `<tr ${rowStyle}>\n <td><code style=\"font-size:0.8rem;color:var(--accent)\">${entry.route.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>')}</code><br><span style=\"font-size:0.72rem;color:var(--text-muted)\">${(entry.filePath || '').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>')}</span></td>\n <td>${exposureBadge}</td>\n <td>${entry.vulnerableDeps.length > 0 ? `<span class=\"badge badge-red\">${entry.vulnerableDeps.length}</span>` : '<span style=\"color:var(--text-muted)\">0</span>'}</td>\n <td>${entry.riskScore >= 7 ? `<span class=\"score-critical\">${highestRisk}</span>` : entry.riskScore >= 4 ? `<span class=\"score-medium\">${highestRisk}</span>` : `<span class=\"score-low\">${highestRisk}</span>`}</td>\n <td>${minHop != null ? minHop : '\u2014'}</td>\n </tr>`;\n }).join('')}\n </tbody>\n </table>\n </div>` : '<div class=\"empty-state\">No routes found.</div>'}\n ` : '<div class=\"empty-state\">No routes found.</div>'}\n </div>\n\n <!-- Tab: Secrets -->\n <div class=\"tab-panel\" id=\"tab-secrets\">\n ${secretsResult ? `\n <div style=\"margin-bottom:1rem;color:var(--text-muted);font-size:0.85rem\">\n ${secretsResult.totalFindings} finding${secretsResult.totalFindings !== 1 ? 's' : ''} in ${secretsResult.filesScanned} file${secretsResult.filesScanned !== 1 ? 's' : ''} scanned\n ${secretsResult.criticalCount > 0 ? `\u2014 <span style=\"color:var(--red);font-weight:600\">${secretsResult.criticalCount} critical</span>` : ''}\n ${secretsResult.highCount > 0 ? `\u2014 <span style=\"color:var(--orange);font-weight:600\">${secretsResult.highCount} high</span>` : ''}\n </div>\n ${secretsResult.findings.length > 0 ? `\n <div class=\"table-wrap\">\n <table id=\"secretsTable\">\n <thead>\n <tr>\n <th>Severity</th>\n <th>Type</th>\n <th>File:Line</th>\n <th>Function</th>\n <th>Reachable From</th>\n </tr>\n </thead>\n <tbody>\n ${secretsResult.findings.map(f => {\n const sevBadge = f.severity === 'critical'\n ? '<span class=\"badge badge-red\">critical</span>'\n : f.severity === 'high'\n ? '<span class=\"badge badge-orange\">high</span>'\n : f.severity === 'medium'\n ? '<span class=\"badge badge-yellow\">medium</span>'\n : '<span class=\"badge badge-gray\">low</span>';\n const fileDisplay = (f.filePath || '').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');\n const funcDisplay = (f.nodeName || '\u2014').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');\n const typeDisplay = (f.type || '').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');\n return `<tr>\n <td>${sevBadge}</td>\n <td>${typeDisplay}</td>\n <td><code style=\"font-size:0.75rem;color:var(--text-muted)\">${fileDisplay}:${f.line}</code></td>\n <td><code style=\"font-size:0.78rem\">${funcDisplay}</code></td>\n <td>${f.entryPointCount > 0 ? `<span class=\"badge badge-yellow\">${f.entryPointCount} entry point${f.entryPointCount !== 1 ? 's' : ''}</span>` : '<span style=\"color:var(--text-muted)\">\u2014</span>'}</td>\n </tr>`;\n }).join('')}\n </tbody>\n </table>\n </div>` : '<div class=\"empty-state\">No secrets detected.</div>'}\n ` : '<div class=\"empty-state\">No secrets detected.</div>'}\n </div>\n\n <!-- Tab: SAST Flows -->\n <div class=\"tab-panel\" id=\"tab-sast\">\n ${flowsResult ? `\n <div style=\"margin-bottom:1rem;color:var(--text-muted);font-size:0.85rem\">\n ${flowsResult.length} finding${flowsResult.length !== 1 ? 's' : ''} across ${new Set(flowsResult.map(f => f.owaspCategory)).size} OWASP categor${new Set(flowsResult.map(f => f.owaspCategory)).size !== 1 ? 'ies' : 'y'}\n </div>\n ${flowsResult.length > 0 ? `\n <div class=\"table-wrap\">\n <table id=\"sastTable\">\n <thead>\n <tr>\n <th>OWASP</th>\n <th>Severity</th>\n <th>Type</th>\n <th>File:Line</th>\n <th>Symbol</th>\n <th>Recommendation</th>\n </tr>\n </thead>\n <tbody>\n ${flowsResult.map(f => {\n const owaspBadge = `<span class=\"badge badge-accent\">${f.owaspCategory}</span>`;\n const sevBadge = f.severity === 'critical'\n ? '<span class=\"badge badge-red\">critical</span>'\n : f.severity === 'high'\n ? '<span class=\"badge badge-orange\">high</span>'\n : '<span class=\"badge badge-yellow\">medium</span>';\n const fileDisplay = (f.filePath || '').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');\n const typeDisplay = (f.type || '').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');\n const symbolDisplay = (f.symbol || '\u2014').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');\n const recDisplay = (f.recommendation || '').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');\n return `<tr>\n <td>${owaspBadge}</td>\n <td>${sevBadge}</td>\n <td><code style=\"font-size:0.78rem\">${typeDisplay}</code></td>\n <td><code style=\"font-size:0.75rem;color:var(--text-muted)\">${fileDisplay}:${f.line}</code></td>\n <td><code style=\"font-size:0.78rem;color:var(--accent)\">${symbolDisplay}</code></td>\n <td style=\"max-width:300px;font-size:0.78rem;color:var(--text-muted)\">${recDisplay}</td>\n </tr>`;\n }).join('')}\n </tbody>\n </table>\n </div>` : '<div class=\"empty-state\">No dangerous patterns detected.</div>'}\n ` : '<div class=\"empty-state\">No dangerous patterns detected.</div>'}\n </div>\n\n <!-- Tab: Remediation -->\n <div class=\"tab-panel\" id=\"tab-remediation\">\n ${remediationStatus ? `\n <div class=\"stats-grid\" style=\"grid-template-columns:repeat(3,1fr);max-width:500px;margin-bottom:1.5rem\">\n <div class=\"stat-card\">\n <div class=\"stat-label\">Overdue</div>\n <div class=\"stat-value ${remediationStatus.filter(r => r.slaStatus === 'overdue').length > 0 ? 'red' : 'green'}\">${remediationStatus.filter(r => r.slaStatus === 'overdue').length}</div>\n </div>\n <div class=\"stat-card\">\n <div class=\"stat-label\">On Track</div>\n <div class=\"stat-value green\">${remediationStatus.filter(r => r.slaStatus === 'ok' || r.slaStatus === 'warning').length}</div>\n </div>\n <div class=\"stat-card\">\n <div class=\"stat-label\">No Fix Available</div>\n <div class=\"stat-value yellow\">${remediationStatus.filter(r => r.slaStatus === 'no_fix').length}</div>\n </div>\n </div>\n ${remediationStatus.length > 0 ? `\n <div class=\"table-wrap\">\n <table id=\"remediationTable\">\n <thead>\n <tr>\n <th>CVE</th>\n <th>Package</th>\n <th>Days Open</th>\n <th>Fix Available</th>\n <th>SLA (days)</th>\n <th>Status</th>\n </tr>\n </thead>\n <tbody>\n ${remediationStatus.map(r => {\n const statusChip = r.slaStatus === 'ok'\n ? '<span class=\"badge badge-green\">ok</span>'\n : r.slaStatus === 'warning'\n ? '<span class=\"badge badge-yellow\">warning</span>'\n : r.slaStatus === 'overdue'\n ? '<span class=\"badge badge-red\">overdue</span>'\n : '<span class=\"badge badge-gray\">no fix</span>';\n const cveDisplay = (r.cveId || '').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');\n const pkgDisplay = (r.packageName || '').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');\n const slaDays = r.severity !== null\n ? (r.severity >= 9 ? 7 : r.severity >= 7 ? 30 : r.severity >= 4 ? 90 : 180)\n : 180;\n return `<tr>\n <td><code style=\"font-size:0.8rem;color:var(--accent)\">${cveDisplay}</code></td>\n <td>${pkgDisplay}</td>\n <td>${r.daysOpen != null ? r.daysOpen + 'd' : '\u2014'}</td>\n <td>${r.daysWithFixAvailable != null ? `<span class=\"badge badge-green\">${r.daysWithFixAvailable}d ago</span>` : '<span style=\"color:var(--text-muted)\">\u2014</span>'}</td>\n <td>${slaDays}d</td>\n <td>${statusChip}</td>\n </tr>`;\n }).join('')}\n </tbody>\n </table>\n </div>` : '<div class=\"empty-state\">No open remediation items.</div>'}\n ` : '<div class=\"empty-state\">No remediation data available.</div>'}\n </div>\n\n</div>\n\n<script>\n// \u2500\u2500 Embedded data \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nconst VULNS = ${safeJson(vulns)};\nconst DEPS = ${safeJson(deps)};\nconst LIC_DATA = ${safeJson(licenseData)};\nconst STALE_DATA = ${safeJson(stalenessRows)};\nconst SBOM_JSON = ${safeJson(sbomJson)};\nconst VEX_JSON = ${safeJson(vexJson)};\n\n// \u2500\u2500 Tab switching \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\ndocument.querySelectorAll('.tab').forEach(btn => {\n btn.addEventListener('click', () => {\n document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));\n document.querySelectorAll('.tab-panel').forEach(p => p.classList.remove('active'));\n btn.classList.add('active');\n const tabId = btn.getAttribute('data-tab');\n document.getElementById('tab-' + tabId).classList.add('active');\n });\n});\n\n// \u2500\u2500 Utilities \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nfunction esc(str) {\n if (str == null) return '';\n return String(str).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/\"/g,'"');\n}\nfunction downloadJson(data, filename) {\n const blob = new Blob([data], {type: 'application/json'});\n const a = document.createElement('a');\n a.href = URL.createObjectURL(blob);\n a.download = filename;\n a.click();\n}\nfunction downloadSBOM() { downloadJson(SBOM_JSON, 'sbom.json'); }\nfunction downloadVEX() { downloadJson(VEX_JSON, 'vex.json'); }\n\nfunction epssClass(v) {\n if (v == null) return 'badge-gray';\n if (v >= 0.5) return 'badge-red';\n if (v >= 0.1) return 'badge-yellow';\n return 'badge-gray';\n}\nfunction cvssClass(v) {\n if (v == null) return '';\n if (v >= 9) return 'score-critical';\n if (v >= 7) return 'score-high';\n if (v >= 4) return 'score-medium';\n return 'score-low';\n}\nfunction verdictBadge(v) {\n if (!v) return '<span class=\"badge badge-gray\">\u2014</span>';\n if (v === 'affected') return '<span class=\"badge badge-red\">affected</span>';\n if (v === 'not_affected') return '<span class=\"badge badge-green\">not affected</span>';\n if (v === 'under_investigation') return '<span class=\"badge badge-yellow\">investigating</span>';\n return '<span class=\"badge badge-gray\">' + esc(v) + '</span>';\n}\nfunction staleColor(score) {\n if (score >= 0.7) return 'var(--red)';\n if (score >= 0.3) return 'var(--yellow)';\n return 'var(--green)';\n}\n\n// \u2500\u2500 Vulnerability table \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nlet vulnSortCol = 'epss_score';\nlet vulnSortDir = -1; // -1 = desc\n\nfunction renderVulns() {\n const sevFilter = document.getElementById('filterSeverity').value;\n const verdFilter = document.getElementById('filterVerdict').value;\n const epssMin = parseFloat(document.getElementById('filterEpss').value) || 0;\n\n let rows = VULNS.filter(v => {\n if (sevFilter !== 'all') {\n const s = v.severity_score ?? 0;\n if (sevFilter === 'critical' && s < 9) return false;\n if (sevFilter === 'high' && (s < 7 || s >= 9)) return false;\n if (sevFilter === 'medium' && (s < 4 || s >= 7)) return false;\n if (sevFilter === 'low' && s >= 4) return false;\n }\n if (verdFilter !== 'all') {\n if ((v.verdict ?? '') !== verdFilter) return false;\n }\n if ((v.epss_score ?? 0) < epssMin) return false;\n return true;\n });\n\n rows = rows.slice().sort((a, b) => {\n const av = a[vulnSortCol] ?? (typeof a[vulnSortCol] === 'number' ? -Infinity : '');\n const bv = b[vulnSortCol] ?? (typeof b[vulnSortCol] === 'number' ? -Infinity : '');\n if (av < bv) return vulnSortDir;\n if (av > bv) return -vulnSortDir;\n return 0;\n });\n\n const tbody = document.getElementById('vulnBody');\n const empty = document.getElementById('vulnEmpty');\n\n if (rows.length === 0) {\n tbody.innerHTML = '';\n empty.style.display = 'block';\n return;\n }\n empty.style.display = 'none';\n\n tbody.innerHTML = rows.map(v => {\n const rowClass = v.verdict === 'affected' ? 'verdict-affected'\n : v.verdict === 'under_investigation' ? 'verdict-investigating'\n : v.verdict === 'not_affected' ? 'verdict-not-affected' : '';\n const riskScore = v.risk_score;\n const riskBadge = riskScore != null\n ? \\`<span class=\"badge \\${riskScore >= 7 ? 'badge-red' : riskScore >= 4 ? 'badge-yellow' : 'badge-gray'}\">[Risk: \\${riskScore.toFixed(1)}]</span>\\`\n : '<span style=\"color:var(--text-muted)\">\u2014</span>';\n return \\`<tr class=\"row-toggle \\${rowClass}\" onclick=\"toggleExpand(this, \\${JSON.stringify(v).replace(/\"/g,'"')})\">\n <td><code style=\"font-size:0.8rem;color:var(--accent)\">\\${esc(v.cve_id)}</code></td>\n <td>\\${esc(v.package_name ?? '\u2014')}</td>\n <td><span class=\"badge badge-gray\">\\${esc(v.ecosystem ?? '\u2014')}</span></td>\n <td class=\"\\${cvssClass(v.severity_score)}\">\\${v.severity_score != null ? v.severity_score.toFixed(1) : '\u2014'}</td>\n <td><span class=\"badge \\${epssClass(v.epss_score)}\">\\${v.epss_score != null ? (v.epss_score*100).toFixed(2)+'%' : '\u2014'}</span></td>\n <td>\\${v.epss_percentile != null ? (v.epss_percentile*100).toFixed(0)+'th' : '\u2014'}</td>\n <td>\\${riskBadge}</td>\n <td>\\${verdictBadge(v.verdict)}</td>\n <td>\\${v.fixed_version ? '<span class=\"badge badge-green\">'+esc(v.fixed_version)+'</span>' : '<span style=\"color:var(--text-muted)\">\u2014</span>'}</td>\n </tr>\n <tr class=\"expand-row\" style=\"display:none\">\n <td colspan=\"9\">\n <div class=\"expand-body\" id=\"expand-\\${esc(v.cve_id)}\"></div>\n </td>\n </tr>\\`;\n }).join('');\n}\n\nfunction toggleExpand(row, data) {\n const expandRow = row.nextElementSibling;\n if (!expandRow) return;\n const isVisible = expandRow.style.display !== 'none';\n if (isVisible) {\n expandRow.style.display = 'none';\n return;\n }\n expandRow.style.display = '';\n const body = expandRow.querySelector('.expand-body');\n let html = '';\n if (data.summary) {\n html += '<div class=\"expand-section\"><div class=\"expand-label\">Summary</div><div>' + esc(data.summary) + '</div></div>';\n }\n if (data.affected_ranges) {\n try {\n const ranges = JSON.parse(data.affected_ranges);\n html += '<div class=\"expand-section\"><div class=\"expand-label\">Affected Ranges</div><pre>' + esc(JSON.stringify(ranges, null, 2)) + '</pre></div>';\n } catch {}\n }\n if (data.paths) {\n try {\n const paths = JSON.parse(data.paths);\n if (Array.isArray(paths) && paths.length > 0) {\n html += '<div class=\"expand-section\"><div class=\"expand-label\">Call Paths (' + paths.length + ')</div>';\n paths.slice(0, 3).forEach(p => {\n if (p.path && Array.isArray(p.path)) {\n html += '<div style=\"margin-bottom:0.3rem;font-family:monospace;font-size:0.78rem;color:var(--text-muted)\">' + p.path.map(n => esc(n.id || n)).join(' \u2192 ') + '</div>';\n }\n });\n if (paths.length > 3) html += '<div style=\"color:var(--text-muted);font-size:0.75rem\">\u2026 and ' + (paths.length-3) + ' more</div>';\n html += '</div>';\n }\n } catch {}\n }\n if (!html) html = '<div style=\"color:var(--text-muted)\">No additional details available.</div>';\n body.innerHTML = html;\n}\n\n// Sort headers\ndocument.querySelectorAll('#vulnTable th.sortable').forEach(th => {\n th.addEventListener('click', () => {\n const col = th.getAttribute('data-col');\n if (vulnSortCol === col) {\n vulnSortDir = -vulnSortDir;\n th.classList.toggle('sorted-asc', vulnSortDir === 1);\n th.classList.toggle('sorted-desc', vulnSortDir === -1);\n } else {\n document.querySelectorAll('#vulnTable th').forEach(t => { t.classList.remove('sorted-asc','sorted-desc'); });\n vulnSortCol = col;\n vulnSortDir = -1;\n th.classList.add('sorted-desc');\n }\n renderVulns();\n });\n});\n\ndocument.getElementById('filterSeverity').addEventListener('change', renderVulns);\ndocument.getElementById('filterVerdict').addEventListener('change', renderVulns);\ndocument.getElementById('filterEpss').addEventListener('input', function() {\n document.getElementById('epssValue').textContent = parseFloat(this.value).toFixed(2);\n renderVulns();\n});\n\n// \u2500\u2500 SBOM table \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nfunction renderSBOM() {\n const eco = document.getElementById('filterEco').value;\n const pkg = document.getElementById('filterPkg').value.toLowerCase();\n\n const rows = DEPS.filter(d => {\n if (eco !== 'all' && d.ecosystem !== eco) return false;\n if (pkg && !d.package_name.toLowerCase().includes(pkg)) return false;\n return true;\n });\n\n const tbody = document.getElementById('sbomBody');\n const empty = document.getElementById('sbomEmpty');\n if (rows.length === 0) {\n tbody.innerHTML = '';\n empty.style.display = 'block';\n return;\n }\n empty.style.display = 'none';\n\n tbody.innerHTML = rows.map(d => {\n const version = d.resolved_version || d.declared_constraint || '\u2014';\n const purl = d.ecosystem && d.package_name\n ? 'pkg:' + esc(d.ecosystem) + '/' + esc(d.package_name) + '@' + esc(version)\n : '\u2014';\n const scopeBadge = d.classification === 'direct'\n ? '<span class=\"badge badge-accent\">direct</span>'\n : '<span class=\"badge badge-gray\">transitive</span>';\n return \\`<tr>\n <td><strong>\\${esc(d.package_name)}</strong></td>\n <td><span class=\"badge badge-gray\">\\${esc(d.ecosystem)}</span></td>\n <td><code style=\"font-size:0.78rem\">\\${esc(version)}</code></td>\n <td>\\${scopeBadge}</td>\n <td>\\${d.license ? esc(d.license) : '<span style=\"color:var(--text-muted)\">\u2014</span>'}</td>\n <td><code style=\"font-size:0.72rem;color:var(--text-muted)\">\\${esc(purl)}</code></td>\n </tr>\\`;\n }).join('');\n}\n\ndocument.getElementById('filterEco').addEventListener('change', renderSBOM);\ndocument.getElementById('filterPkg').addEventListener('input', renderSBOM);\n\n// \u2500\u2500 VEX table \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nfunction renderVEX() {\n let vexData;\n try { vexData = JSON.parse(VEX_JSON); } catch { vexData = null; }\n const tbody = document.getElementById('vexBody');\n const empty = document.getElementById('vexEmpty');\n\n const vulns = vexData && vexData.vulnerabilities ? vexData.vulnerabilities : [];\n if (vulns.length === 0) {\n tbody.innerHTML = '';\n empty.style.display = 'block';\n return;\n }\n empty.style.display = 'none';\n\n tbody.innerHTML = vulns.map(entry => {\n const state = entry.analysis && entry.analysis.state ? entry.analysis.state : '\u2014';\n const justification = entry.analysis && entry.analysis.justification ? entry.analysis.justification : '\u2014';\n const detail = entry.analysis && entry.analysis.detail ? entry.analysis.detail : '\u2014';\n const pkg = entry.affects && entry.affects[0] ? entry.affects[0].ref : '\u2014';\n const stateClass = state === 'affected' ? 'state-affected'\n : state === 'not_affected' ? 'state-not_affected'\n : state === 'under_investigation' ? 'state-investigating' : '';\n return \\`<tr>\n <td><code style=\"font-size:0.8rem;color:var(--accent)\">\\${esc(entry.id)}</code></td>\n <td><code style=\"font-size:0.75rem;color:var(--text-muted)\">\\${esc(pkg)}</code></td>\n <td><span class=\"\\${stateClass}\" style=\"font-weight:500\">\\${esc(state)}</span></td>\n <td>\\${esc(justification)}</td>\n <td style=\"max-width:300px\">\\${esc(detail)}</td>\n </tr>\\`;\n }).join('');\n}\n\n// \u2500\u2500 Licenses table \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nfunction renderLicenses() {\n // Banners\n const bannerEl = document.getElementById('licBanners');\n const denyCount = LIC_DATA.filter(d => d.status === 'deny').length;\n const warnCount = LIC_DATA.filter(d => d.status === 'warn').length;\n let banners = '';\n if (denyCount > 0) banners += \\`<div class=\"policy-banner deny\">\u2717 \\${denyCount} package\\${denyCount !== 1 ? 's' : ''} with denied license\\${denyCount !== 1 ? 's' : ''}</div>\\`;\n if (warnCount > 0) banners += \\`<div class=\"policy-banner warn\">\u26A0 \\${warnCount} package\\${warnCount !== 1 ? 's' : ''} with warned license\\${warnCount !== 1 ? 's' : ''}</div>\\`;\n bannerEl.innerHTML = banners;\n\n const pol = document.getElementById('filterLicPolicy').value;\n const rows = LIC_DATA.filter(d => pol === 'all' || d.status === pol);\n\n const tbody = document.getElementById('licBody');\n const empty = document.getElementById('licEmpty');\n if (rows.length === 0) {\n tbody.innerHTML = '';\n empty.style.display = 'block';\n return;\n }\n empty.style.display = 'none';\n\n tbody.innerHTML = rows.map(d => {\n let statusHtml;\n if (d.status === 'deny') statusHtml = '<span class=\"badge badge-red\">\u2717 deny</span>';\n else if (d.status === 'warn') statusHtml = '<span class=\"badge badge-yellow\">\u26A0 warn</span>';\n else if (d.status === 'ok') statusHtml = '<span class=\"badge badge-green\">\u2713 ok</span>';\n else statusHtml = '<span class=\"badge badge-gray\">\u2014 unknown</span>';\n return \\`<tr>\n <td><strong>\\${esc(d.package_name)}</strong></td>\n <td><span class=\"badge badge-gray\">\\${esc(d.ecosystem)}</span></td>\n <td>\\${d.license ? esc(d.license) : '<span style=\"color:var(--text-muted)\">\u2014</span>'}</td>\n <td>\\${statusHtml}</td>\n </tr>\\`;\n }).join('');\n}\n\ndocument.getElementById('filterLicPolicy').addEventListener('change', renderLicenses);\n\n// \u2500\u2500 Staleness table \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nfunction renderStaleness() {\n const tbody = document.getElementById('staleBody');\n const empty = document.getElementById('staleEmpty');\n\n if (STALE_DATA.length === 0) {\n tbody.innerHTML = '';\n empty.style.display = 'block';\n return;\n }\n empty.style.display = 'none';\n\n tbody.innerHTML = STALE_DATA.map(d => {\n const version = d.resolved_version || d.declared_constraint || '\u2014';\n const latest = d.latest_version || '\u2014';\n const score = d.staleness_score ?? 0;\n const months = d.latest_published\n ? Math.round((Date.now() - d.latest_published) / (1000 * 60 * 60 * 24 * 30))\n : null;\n const barPct = Math.round(score * 100);\n const barColor = staleColor(score);\n const scoreClass = score >= 0.7 ? 'score-critical' : score >= 0.3 ? 'score-medium' : 'score-low';\n return \\`<tr>\n <td><strong>\\${esc(d.package_name)}</strong></td>\n <td><span class=\"badge badge-gray\">\\${esc(d.ecosystem)}</span></td>\n <td><code style=\"font-size:0.78rem\">\\${esc(version)}</code></td>\n <td><code style=\"font-size:0.78rem;color:var(--green)\">\\${esc(latest)}</code></td>\n <td>\\${months != null ? months + 'mo' : '\u2014'}</td>\n <td class=\"\\${scoreClass}\">\\${score.toFixed(2)}</td>\n <td>\n <div class=\"stale-bar-track\">\n <div class=\"stale-bar-fill\" style=\"width:\\${barPct}%;background:\\${barColor}\"></div>\n </div>\n </td>\n </tr>\\`;\n }).join('');\n}\n\n// \u2500\u2500 Initial render \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nrenderVulns();\nrenderSBOM();\nrenderVEX();\nrenderLicenses();\nrenderStaleness();\n</script>\n\n</body>\n</html>`;\n}\n\n// \u2500\u2500 Command registration \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport function register(secCmd: import('commander').Command): void {\n secCmd\n .command('export [projectPath]')\n .description('Generate a self-contained HTML security dashboard')\n .option('--output <file>', 'Output HTML file path', '.kirograph/security-export.html')\n .option('--open', 'Open in browser after generating')\n .action(async (projectPath: string | undefined, opts: { output: string; open?: boolean }) => {\n const target = path.resolve(projectPath ?? process.cwd());\n const config = await loadConfig(target);\n\n if (!config.enableSecurity) {\n console.error(`\\n ${'\\x1b[33m'}\u26A0 Security analysis is disabled.${reset}`);\n console.error(` ${dim}Enable it in .kirograph/config.json:${reset} ${violet}${bold}\"enableSecurity\": true${reset}`);\n console.error(` ${dim}Then re-run:${reset} ${violet}${bold}kirograph index${reset}\\n`);\n process.exit(1);\n }\n\n const KiroGraph = (await Promise.resolve().then(() => require('../../index.js'))).default;\n if (!KiroGraph.isInitialized(target)) {\n console.error(' \u2716 KiroGraph not initialized. Run: kirograph init');\n process.exit(1);\n }\n\n const cg = await KiroGraph.open(target);\n const db = cg.getDatabase();\n db.applySecuritySchema();\n const rawDb = db.getRawDb();\n\n // Query all vulnerability + dep data in one JOIN\n const rawVulns: VulnRow[] = rawDb.all(`\n SELECT v.cve_id, v.severity_score, v.risk_score, v.epss_score, v.epss_percentile,\n v.fixed_version, v.summary, v.affected_ranges,\n d.package_name, d.ecosystem, d.resolved_version, d.declared_constraint,\n d.scope, d.license,\n r.verdict, r.paths, r.unresolved_symbols, r.reaching_entry_point_count\n FROM sec_vulnerabilities v\n LEFT JOIN edges e ON e.target = v.node_id AND e.kind = 'has_vulnerability'\n LEFT JOIN sec_dependencies d ON d.node_id = e.source\n LEFT JOIN sec_reachability r ON r.vulnerability_node_id = v.node_id\n ORDER BY v.epss_score DESC NULLS LAST, v.severity_score DESC NULLS LAST\n `);\n\n // Deduplicate by (cve_id, package_name, ecosystem) \u2014 same CVE can appear multiple\n // times when a package is declared in multiple manifests (monorepo).\n const _vRank = (v: string | null) =>\n v === 'affected' ? 3 : v === 'under_investigation' ? 2 : v === 'not_affected' ? 1 : 0;\n const _dedupMap = new Map<string, VulnRow>();\n for (const row of rawVulns) {\n const key = `${row.cve_id}::${row.package_name ?? ''}::${row.ecosystem ?? ''}`;\n const ex = _dedupMap.get(key);\n if (!ex || _vRank(row.verdict) > _vRank(ex.verdict)) _dedupMap.set(key, row);\n }\n const vulns: VulnRow[] = [..._dedupMap.values()];\n\n // Determine which deps are direct (have a declared_in edge)\n const directNodeIds = new Set<string>(\n (rawDb.all(`SELECT source FROM edges WHERE kind = 'declared_in'`) as Array<{ source: string }>)\n .map((r) => r.source),\n );\n\n // All dependencies for SBOM tab + licenses + staleness\n const rawDeps: Omit<DepRow, 'classification'>[] = rawDb.all(\n `SELECT node_id, package_name, ecosystem, resolved_version, declared_constraint,\n scope, license, latest_version, latest_published, staleness_score\n FROM sec_dependencies ORDER BY package_name`,\n );\n const deps: DepRow[] = rawDeps.map(d => ({\n ...d,\n classification: directNodeIds.has(d.node_id) ? 'direct' : 'transitive',\n }));\n\n // Generate SBOM and VEX JSON for download buttons\n const { SBOMExporter } = await Promise.resolve().then(() => require('../../security/export/sbom.js'));\n const { VEXExporter } = await Promise.resolve().then(() => require('../../security/export/vex.js'));\n const sbomExporter = new SBOMExporter(db, target);\n const vexExporter = new VEXExporter(db, target);\n const sbomJson = sbomExporter.exportJSON();\n const vexJson = vexExporter.exportJSON();\n\n // Attack surface data\n let attackSurface: AttackSurfaceResult | null = null;\n try {\n const { AttackSurfaceAnalyzer } = await Promise.resolve().then(() => require('../../security/attack-surface.js'));\n const analyzer = new AttackSurfaceAnalyzer(db);\n attackSurface = await analyzer.analyze();\n } catch { /* non-critical */ }\n\n // Secrets data\n let secretsResult: SecretsResult | null = null;\n try {\n const { SecretsScanner } = await Promise.resolve().then(() => require('../../security/secrets.js'));\n const scanner = new SecretsScanner(db, target);\n secretsResult = await scanner.scan();\n } catch { /* non-critical */ }\n\n // Data flows\n let flowsResult: DataFlowFinding[] | null = null;\n try {\n const { DataFlowAnalyzer } = await Promise.resolve().then(() => require('../../security/data-flows.js'));\n const analyzer = new DataFlowAnalyzer(db);\n flowsResult = await analyzer.analyze();\n } catch { /* non-critical */ }\n\n // Remediation\n let remediationStatus: RemediationStatus[] | null = null;\n try {\n const { RemediationTracker } = await Promise.resolve().then(() => require('../../security/remediation.js'));\n const tracker = new RemediationTracker(db);\n remediationStatus = tracker.getStatus();\n } catch { /* non-critical */ }\n\n cg.close();\n\n // Determine license policy\n const denyList = config.securityLicensePolicy?.deny ?? [];\n const warnList = config.securityLicensePolicy?.warn ?? [];\n\n const projectName = path.basename(target);\n const generatedAt = new Date().toLocaleString();\n\n const html = generateHtml({\n projectName,\n generatedAt,\n vulns,\n deps,\n sbomJson,\n vexJson,\n denyList,\n warnList,\n attackSurface,\n secretsResult,\n flowsResult,\n remediationStatus,\n });\n\n // Write output\n const outPath = path.resolve(target, opts.output);\n fs.mkdirSync(path.dirname(outPath), { recursive: true });\n fs.writeFileSync(outPath, html, 'utf-8');\n\n console.error(` ${green}\u2713${reset} Security dashboard written to ${violet}${bold}${outPath}${reset}`);\n console.error(` ${dim}${vulns.length} vulnerabilities \u00B7 ${deps.length} dependencies${reset}`);\n\n if (opts.open) {\n openBrowser(outPath);\n console.error(` ${dim}Opening in browser\u2026${reset}`);\n }\n });\n}\n"],
|
|
5
|
-
"mappings": "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AAAA,mCAKqB,WAAW;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAA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",
|
|
4
|
+
"sourcesContent": ["import { Command } from 'commander';\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport { spawn } from 'child_process';\nimport { loadConfig } from '../../config';\nimport { dim, reset, violet, bold, green } from '../ui';\nimport { checkLicensePolicy } from '../../security/license';\nimport type { AttackSurfaceResult } from '../../security/attack-surface';\nimport type { SecretsResult } from '../../security/secrets';\nimport type { DataFlowFinding } from '../../security/data-flows';\nimport type { RemediationStatus } from '../../security/remediation';\n\nfunction openBrowser(filePath: string): void {\n const cmd = process.platform === 'darwin' ? 'open'\n : process.platform === 'win32' ? 'start'\n : 'xdg-open';\n spawn(cmd, [filePath], { stdio: 'ignore', detached: true, shell: process.platform === 'win32' }).unref();\n}\n\n// \u2500\u2500 DB row types \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\ninterface VulnRow {\n cve_id: string;\n severity_score: number | null;\n risk_score: number | null;\n epss_score: number | null;\n epss_percentile: number | null;\n fixed_version: string | null;\n summary: string | null;\n affected_ranges: string | null;\n package_name: string | null;\n ecosystem: string | null;\n resolved_version: string | null;\n declared_constraint: string | null;\n scope: string | null;\n license: string | null;\n verdict: string | null;\n paths: string | null;\n unresolved_symbols: string | null;\n reaching_entry_point_count: number | null;\n}\n\ninterface DepRow {\n node_id: string;\n package_name: string;\n ecosystem: string;\n resolved_version: string | null;\n declared_constraint: string;\n scope: string;\n /** 'direct' | 'transitive' \u2014 derived from declared_in edges, not the scope column */\n classification: string;\n license: string | null;\n latest_version: string | null;\n latest_published: number | null;\n staleness_score: number | null;\n}\n\n// \u2500\u2500 HTML generation \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nfunction generateHtml(params: {\n projectName: string;\n generatedAt: string;\n vulns: VulnRow[];\n deps: DepRow[];\n sbomJson: string;\n vexJson: string;\n denyList: string[];\n warnList: string[];\n attackSurface: AttackSurfaceResult | null;\n secretsResult: SecretsResult | null;\n flowsResult: DataFlowFinding[] | null;\n remediationStatus: RemediationStatus[] | null;\n}): string {\n const { projectName, generatedAt, vulns, deps, sbomJson, vexJson, denyList, warnList,\n attackSurface, secretsResult, flowsResult, remediationStatus } = params;\n\n // Compute license violations for the HTML\n const licDeps = deps.map(d => ({ package_name: d.package_name, ecosystem: d.ecosystem, license: d.license }));\n const violations = checkLicensePolicy(licDeps, { deny: denyList, warn: warnList });\n const violationMap: Record<string, 'deny' | 'warn'> = {};\n for (const v of violations) {\n violationMap[`${v.ecosystem}:${v.packageName}`] = v.severity;\n }\n const licenseData = deps.map(d => ({\n package_name: d.package_name,\n ecosystem: d.ecosystem,\n license: d.license ?? null,\n status: violationMap[`${d.ecosystem}:${d.package_name}`] ?? (d.license ? 'ok' : 'unknown'),\n }));\n\n // Stats\n const totalDeps = deps.length;\n const totalVulns = vulns.length;\n const affected = vulns.filter(v => v.verdict === 'affected').length;\n const underInvestigation = vulns.filter(v => v.verdict === 'under_investigation').length;\n const notAffected = vulns.filter(v => v.verdict === 'not_affected').length;\n const staleDeps = deps.filter(d => (d.staleness_score ?? 0) >= 0.3).length;\n\n // Top 5 by EPSS \u00D7 CVSS\n const top5 = [...vulns]\n .filter(v => v.epss_score != null || v.severity_score != null)\n .sort((a, b) => {\n const scoreA = (a.epss_score ?? 0) * (a.severity_score ?? 0);\n const scoreB = (b.epss_score ?? 0) * (b.severity_score ?? 0);\n return scoreB - scoreA;\n })\n .slice(0, 5);\n\n const directCount = deps.filter(d => d.classification === 'direct').length;\n const transitiveCount = deps.filter(d => d.classification !== 'direct').length;\n const stalenessRows = deps.filter(d => d.staleness_score != null).sort((a, b) => (b.staleness_score ?? 0) - (a.staleness_score ?? 0));\n\n // Top risk CVE (by risk_score if available, fallback to CVSS)\n const topRiskCve = vulns.length > 0\n ? [...vulns].sort((a, b) => ((b as any).risk_score ?? b.severity_score ?? 0) - ((a as any).risk_score ?? a.severity_score ?? 0))[0]\n : null;\n\n // OWASP category breakdown from flows\n const owaspCounts: Record<string, number> = {};\n if (flowsResult) {\n for (const f of flowsResult) {\n owaspCounts[f.owaspCategory] = (owaspCounts[f.owaspCategory] ?? 0) + 1;\n }\n }\n const owaspOrder = ['A01','A02','A03','A04','A05','A06','A07','A08','A09','A10'];\n const owaspNames: Record<string, string> = {\n A01: 'Broken Access Control', A02: 'Cryptographic Failures', A03: 'Injection',\n A04: 'Insecure Design', A05: 'Security Misconfiguration', A06: 'Vulnerable Components',\n A07: 'Auth Failures', A08: 'Integrity Failures', A09: 'Logging Failures', A10: 'SSRF',\n };\n const owaspMaxCount = Math.max(1, ...Object.values(owaspCounts));\n\n // New feature counts\n const secretsCount = secretsResult?.totalFindings ?? null;\n const criticalFlowsCount = flowsResult?.filter(f => f.severity === 'critical').length ?? null;\n\n // Embed JSON data safely\n const safeJson = (obj: unknown) => JSON.stringify(obj).replace(/<\\/script>/gi, '<\\\\/script>');\n\n return `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n<meta charset=\"UTF-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n<title>Security Dashboard \u2014 ${projectName}</title>\n<style>\n*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }\n:root {\n --bg: #0d1117;\n --bg-card: #161b22;\n --bg-input: #21262d;\n --border: #30363d;\n --accent: #a78bfa;\n --text: #c9d1d9;\n --text-muted: #8b949e;\n --text-bright: #f0f6fc;\n --green: #3fb950;\n --yellow: #d29922;\n --red: #f85149;\n --orange: #db6d28;\n --radius: 8px;\n}\nhtml { scroll-behavior: smooth; }\nbody {\n font-family: system-ui, -apple-system, 'Segoe UI', sans-serif;\n background: var(--bg);\n color: var(--text);\n line-height: 1.5;\n font-size: 14px;\n -webkit-font-smoothing: antialiased;\n}\na { color: var(--accent); text-decoration: none; }\na:hover { text-decoration: underline; }\n\n/* Header */\n.header {\n background: var(--bg-card);\n border-bottom: 1px solid var(--border);\n padding: 1rem 2rem;\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 1rem;\n}\n.header-title { font-size: 1.1rem; font-weight: 600; color: var(--text-bright); }\n.header-meta { font-size: 0.8rem; color: var(--text-muted); }\n.header-badge {\n display: inline-flex; align-items: center; gap: 0.35rem;\n background: rgba(167,139,250,0.12); color: var(--accent);\n border: 1px solid rgba(167,139,250,0.25);\n padding: 0.25rem 0.6rem; border-radius: 4px;\n font-size: 0.75rem; font-weight: 600;\n}\n\n/* Tabs */\n.tabs {\n display: flex; gap: 0;\n background: var(--bg-card);\n border-bottom: 1px solid var(--border);\n padding: 0 2rem;\n overflow-x: auto;\n}\n.tab {\n padding: 0.75rem 1.25rem;\n cursor: pointer;\n border: none;\n background: none;\n color: var(--text-muted);\n font-size: 0.875rem;\n font-weight: 500;\n border-bottom: 2px solid transparent;\n transition: color 0.15s, border-color 0.15s;\n white-space: nowrap;\n}\n.tab:hover { color: var(--text); }\n.tab.active { color: var(--accent); border-bottom-color: var(--accent); }\n\n/* Content */\n.content { padding: 1.5rem 2rem; }\n.tab-panel { display: none; }\n.tab-panel.active { display: block; }\n\n/* Stats cards */\n.stats-grid {\n display: grid;\n grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));\n gap: 1rem;\n margin-bottom: 1.5rem;\n}\n.stat-card {\n background: var(--bg-card);\n border: 1px solid var(--border);\n border-radius: var(--radius);\n padding: 1rem;\n}\n.stat-label { font-size: 0.75rem; color: var(--text-muted); margin-bottom: 0.4rem; text-transform: uppercase; letter-spacing: 0.04em; }\n.stat-value { font-size: 1.75rem; font-weight: 700; color: var(--text-bright); line-height: 1; }\n.stat-value.red { color: var(--red); }\n.stat-value.yellow { color: var(--yellow); }\n.stat-value.green { color: var(--green); }\n.stat-value.accent { color: var(--accent); }\n\n/* Section heading */\n.section-title {\n font-size: 0.85rem; font-weight: 600;\n color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.06em;\n margin-bottom: 1rem;\n}\n\n/* Chart bar */\n.bar-chart { margin-bottom: 1.5rem; }\n.bar-row {\n display: flex; align-items: center; gap: 0.75rem;\n margin-bottom: 0.5rem;\n}\n.bar-label { width: 140px; font-size: 0.8rem; color: var(--text-muted); flex-shrink: 0; }\n.bar-track { flex: 1; height: 14px; background: var(--bg-input); border-radius: 3px; overflow: hidden; }\n.bar-fill { height: 100%; border-radius: 3px; transition: width 0.3s; }\n.bar-count { width: 40px; text-align: right; font-size: 0.8rem; color: var(--text); }\n\n/* Top CVEs */\n.top-cve-list { display: flex; flex-direction: column; gap: 0.5rem; }\n.top-cve-item {\n display: flex; align-items: center; gap: 0.75rem;\n background: var(--bg-card); border: 1px solid var(--border);\n border-radius: var(--radius); padding: 0.6rem 0.9rem;\n}\n.top-cve-rank { width: 24px; color: var(--text-muted); font-size: 0.8rem; font-weight: 600; flex-shrink: 0; }\n.top-cve-id { font-family: monospace; font-size: 0.85rem; color: var(--accent); min-width: 130px; }\n.top-cve-pkg { font-size: 0.8rem; color: var(--text); flex: 1; }\n.top-cve-scores { display: flex; gap: 0.5rem; align-items: center; }\n\n/* Filters */\n.filters {\n display: flex; gap: 0.75rem; flex-wrap: wrap;\n margin-bottom: 1rem; align-items: center;\n}\n.filter-label { font-size: 0.8rem; color: var(--text-muted); }\nselect, input[type=text], input[type=range] {\n background: var(--bg-input); border: 1px solid var(--border);\n color: var(--text); padding: 0.35rem 0.6rem;\n border-radius: var(--radius); font-size: 0.8rem;\n outline: none;\n}\nselect:focus, input[type=text]:focus { border-color: var(--accent); }\ninput[type=text] { min-width: 180px; }\n.range-group { display: flex; align-items: center; gap: 0.5rem; }\n#epssValue { min-width: 32px; font-size: 0.8rem; color: var(--accent); }\n\n/* Tables */\n.table-wrap { overflow-x: auto; }\ntable { width: 100%; border-collapse: collapse; font-size: 0.8rem; }\nth {\n text-align: left; padding: 0.5rem 0.75rem;\n background: var(--bg-card); border-bottom: 1px solid var(--border);\n color: var(--text-muted); font-weight: 600; font-size: 0.75rem;\n text-transform: uppercase; letter-spacing: 0.04em;\n white-space: nowrap;\n}\nth.sortable { cursor: pointer; user-select: none; }\nth.sortable:hover { color: var(--text); }\nth.sorted-asc::after { content: ' \u25B2'; color: var(--accent); }\nth.sorted-desc::after { content: ' \u25BC'; color: var(--accent); }\ntd { padding: 0.5rem 0.75rem; border-bottom: 1px solid var(--border); vertical-align: top; }\ntr:last-child td { border-bottom: none; }\ntr:hover td { background: rgba(255,255,255,0.02); }\n\n/* Verdict row colors */\ntr.verdict-affected td:first-child { border-left: 3px solid var(--red); }\ntr.verdict-investigating td:first-child { border-left: 3px solid var(--yellow); }\ntr.verdict-not-affected td:first-child { border-left: 3px solid var(--green); }\n\n/* Badges */\n.badge {\n display: inline-flex; align-items: center;\n padding: 0.15rem 0.45rem; border-radius: 3px;\n font-size: 0.72rem; font-weight: 600; white-space: nowrap;\n}\n.badge-red { background: rgba(248,81,73,0.15); color: var(--red); }\n.badge-yellow { background: rgba(210,153,34,0.15); color: var(--yellow); }\n.badge-green { background: rgba(63,185,80,0.15); color: var(--green); }\n.badge-gray { background: rgba(139,148,158,0.15);color: var(--text-muted); }\n.badge-accent { background: rgba(167,139,250,0.15);color: var(--accent); }\n.badge-orange { background: rgba(219,109,40,0.15); color: var(--orange); }\n\n/* Expandable row */\n.expand-row td { padding: 0; border-bottom: 1px solid var(--border); }\n.expand-body {\n padding: 0.75rem 1rem;\n background: rgba(22,27,34,0.8);\n font-size: 0.8rem; color: var(--text);\n}\n.expand-body .expand-section { margin-bottom: 0.6rem; }\n.expand-body .expand-label { color: var(--text-muted); font-size: 0.75rem; margin-bottom: 0.25rem; }\n.expand-body pre { font-family: monospace; font-size: 0.78rem; white-space: pre-wrap; color: var(--text-muted); }\n.row-toggle { cursor: pointer; }\n\n/* Download buttons */\n.btn {\n display: inline-flex; align-items: center; gap: 0.4rem;\n padding: 0.45rem 1rem; border-radius: var(--radius);\n font-size: 0.82rem; font-weight: 500;\n cursor: pointer; border: none; transition: opacity 0.15s;\n}\n.btn:hover { opacity: 0.85; }\n.btn-accent { background: var(--accent); color: #0d1117; }\n.btn-outline {\n background: none; border: 1px solid var(--border);\n color: var(--text);\n}\n.btn-outline:hover { border-color: var(--accent); color: var(--accent); }\n\n/* Staleness bar */\n.stale-bar-track { width: 80px; height: 8px; background: var(--bg-input); border-radius: 2px; overflow: hidden; display: inline-block; vertical-align: middle; }\n.stale-bar-fill { height: 100%; border-radius: 2px; }\n\n/* Policy banners */\n.policy-banner {\n display: flex; align-items: center; gap: 0.6rem;\n padding: 0.65rem 1rem; border-radius: var(--radius);\n margin-bottom: 0.75rem; font-size: 0.85rem;\n}\n.policy-banner.deny { background: rgba(248,81,73,0.1); border: 1px solid rgba(248,81,73,0.3); color: var(--red); }\n.policy-banner.warn { background: rgba(210,153,34,0.1); border: 1px solid rgba(210,153,34,0.3); color: var(--yellow); }\n\n/* Score cell */\n.score-critical { color: var(--red); font-weight: 700; }\n.score-high { color: var(--orange); font-weight: 600; }\n.score-medium { color: var(--yellow); }\n.score-low { color: var(--green); }\n\n/* Empty state */\n.empty-state { text-align: center; padding: 2.5rem; color: var(--text-muted); font-size: 0.85rem; }\n\n/* SBOM summary badges */\n.badge-counts { display: flex; gap: 0.5rem; margin-bottom: 1rem; }\n\n/* VEX state colors */\n.state-affected { color: var(--red); }\n.state-not_affected { color: var(--green); }\n.state-investigating { color: var(--yellow); }\n</style>\n</head>\n<body>\n\n<div class=\"header\">\n <div>\n <div class=\"header-title\">Security Dashboard \u2014 ${projectName}</div>\n <div class=\"header-meta\">Generated ${generatedAt}</div>\n </div>\n <div class=\"header-badge\">KiroGraph Security</div>\n</div>\n\n<div class=\"tabs\">\n <button class=\"tab active\" data-tab=\"overview\">Overview</button>\n <button class=\"tab\" data-tab=\"vulns\">Vulnerabilities</button>\n <button class=\"tab\" data-tab=\"sbom\">SBOM</button>\n <button class=\"tab\" data-tab=\"vex\">VEX</button>\n <button class=\"tab\" data-tab=\"licenses\">Licenses</button>\n <button class=\"tab\" data-tab=\"staleness\">Staleness</button>\n <button class=\"tab\" data-tab=\"attack-surface\">\u2694\uFE0F Attack Surface</button>\n <button class=\"tab\" data-tab=\"secrets\">\uD83D\uDD11 Secrets</button>\n <button class=\"tab\" data-tab=\"sast\">\uD83D\uDD0D SAST Flows</button>\n <button class=\"tab\" data-tab=\"remediation\">\u23F1\uFE0F Remediation</button>\n</div>\n\n<div class=\"content\">\n\n <!-- Tab: Overview -->\n <div class=\"tab-panel active\" id=\"tab-overview\">\n <div class=\"stats-grid\">\n <div class=\"stat-card\">\n <div class=\"stat-label\">Total Dependencies</div>\n <div class=\"stat-value accent\" id=\"stat-deps\">${totalDeps}</div>\n </div>\n <div class=\"stat-card\">\n <div class=\"stat-label\">Vulnerabilities</div>\n <div class=\"stat-value ${totalVulns > 0 ? 'red' : 'green'}\" id=\"stat-vulns\">${totalVulns}</div>\n </div>\n <div class=\"stat-card\">\n <div class=\"stat-label\">Affected</div>\n <div class=\"stat-value ${affected > 0 ? 'red' : 'green'}\">${affected}</div>\n </div>\n <div class=\"stat-card\">\n <div class=\"stat-label\">Under Investigation</div>\n <div class=\"stat-value ${underInvestigation > 0 ? 'yellow' : 'green'}\">${underInvestigation}</div>\n </div>\n <div class=\"stat-card\">\n <div class=\"stat-label\">Not Affected</div>\n <div class=\"stat-value green\">${notAffected}</div>\n </div>\n <div class=\"stat-card\">\n <div class=\"stat-label\">Stale Dependencies</div>\n <div class=\"stat-value ${staleDeps > 0 ? 'yellow' : 'green'}\">${staleDeps}</div>\n </div>\n ${secretsCount !== null ? `<div class=\"stat-card\">\n <div class=\"stat-label\">Secrets Found</div>\n <div class=\"stat-value ${secretsCount > 0 ? 'red' : 'green'}\">${secretsCount}</div>\n </div>` : ''}\n ${criticalFlowsCount !== null ? `<div class=\"stat-card\">\n <div class=\"stat-label\">Critical SAST Findings</div>\n <div class=\"stat-value ${criticalFlowsCount > 0 ? 'red' : 'green'}\">${criticalFlowsCount}</div>\n </div>` : ''}\n </div>\n\n ${topRiskCve ? `\n <div class=\"section-title\">Top Risk CVE</div>\n <div class=\"top-cve-list\" style=\"margin-bottom:1.5rem\">\n <div class=\"top-cve-item\">\n <div class=\"top-cve-rank\">#1</div>\n <div class=\"top-cve-id\">${topRiskCve.cve_id}</div>\n <div class=\"top-cve-pkg\">${topRiskCve.package_name ?? '\u2014'} ${topRiskCve.ecosystem ? `<span style=\"color:var(--text-muted);font-size:0.75rem\">[${topRiskCve.ecosystem}]</span>` : ''}</div>\n <div class=\"top-cve-scores\">\n ${topRiskCve.risk_score != null ? `<span class=\"badge ${topRiskCve.risk_score >= 7 ? 'badge-red' : topRiskCve.risk_score >= 4 ? 'badge-yellow' : 'badge-gray'}\">Risk ${topRiskCve.risk_score.toFixed(1)}</span>` : ''}\n ${topRiskCve.severity_score != null ? `<span class=\"badge ${topRiskCve.severity_score >= 9 ? 'badge-red' : topRiskCve.severity_score >= 7 ? 'badge-orange' : topRiskCve.severity_score >= 4 ? 'badge-yellow' : 'badge-gray'}\">CVSS ${topRiskCve.severity_score.toFixed(1)}</span>` : ''}\n ${topRiskCve.epss_score != null ? `<span class=\"badge ${topRiskCve.epss_score >= 0.5 ? 'badge-red' : topRiskCve.epss_score >= 0.1 ? 'badge-yellow' : 'badge-gray'}\">EPSS ${(topRiskCve.epss_score*100).toFixed(2)}%</span>` : ''}\n ${topRiskCve.verdict ? `<span class=\"badge ${topRiskCve.verdict === 'affected' ? 'badge-red' : topRiskCve.verdict === 'not_affected' ? 'badge-green' : 'badge-yellow'}\">${topRiskCve.verdict}</span>` : ''}\n </div>\n </div>\n </div>\n ` : ''}\n\n ${totalVulns > 0 ? `\n <div class=\"section-title\">Verdict Breakdown</div>\n <div class=\"bar-chart\">\n ${affected > 0 ? `<div class=\"bar-row\">\n <div class=\"bar-label\">Affected</div>\n <div class=\"bar-track\"><div class=\"bar-fill\" style=\"width:${Math.round(affected/totalVulns*100)}%;background:var(--red)\"></div></div>\n <div class=\"bar-count\">${affected}</div>\n </div>` : ''}\n ${underInvestigation > 0 ? `<div class=\"bar-row\">\n <div class=\"bar-label\">Under Investigation</div>\n <div class=\"bar-track\"><div class=\"bar-fill\" style=\"width:${Math.round(underInvestigation/totalVulns*100)}%;background:var(--yellow)\"></div></div>\n <div class=\"bar-count\">${underInvestigation}</div>\n </div>` : ''}\n ${notAffected > 0 ? `<div class=\"bar-row\">\n <div class=\"bar-label\">Not Affected</div>\n <div class=\"bar-track\"><div class=\"bar-fill\" style=\"width:${Math.round(notAffected/totalVulns*100)}%;background:var(--green)\"></div></div>\n <div class=\"bar-count\">${notAffected}</div>\n </div>` : ''}\n ${(totalVulns - affected - underInvestigation - notAffected) > 0 ? `<div class=\"bar-row\">\n <div class=\"bar-label\">Pending Analysis</div>\n <div class=\"bar-track\"><div class=\"bar-fill\" style=\"width:${Math.round((totalVulns-affected-underInvestigation-notAffected)/totalVulns*100)}%;background:var(--text-muted)\"></div></div>\n <div class=\"bar-count\">${totalVulns - affected - underInvestigation - notAffected}</div>\n </div>` : ''}\n </div>\n ` : ''}\n\n ${Object.keys(owaspCounts).length > 0 ? `\n <div class=\"section-title\">OWASP Category Breakdown</div>\n <div class=\"bar-chart\" style=\"margin-bottom:1.5rem\">\n ${owaspOrder.filter(cat => owaspCounts[cat] > 0).map(cat => `<div class=\"bar-row\">\n <div class=\"bar-label\" style=\"width:200px\"><span class=\"badge badge-accent\" style=\"margin-right:0.4rem\">${cat}</span>${owaspNames[cat] ?? cat}</div>\n <div class=\"bar-track\"><div class=\"bar-fill\" style=\"width:${Math.round((owaspCounts[cat]??0)/owaspMaxCount*100)}%;background:var(--orange)\"></div></div>\n <div class=\"bar-count\">${owaspCounts[cat]}</div>\n </div>`).join('')}\n </div>\n ` : ''}\n\n ${top5.length > 0 ? `\n <div class=\"section-title\">Top 5 Critical CVEs</div>\n <div class=\"top-cve-list\">\n ${top5.map((v, i) => `\n <div class=\"top-cve-item\">\n <div class=\"top-cve-rank\">#${i+1}</div>\n <div class=\"top-cve-id\">${v.cve_id}</div>\n <div class=\"top-cve-pkg\">${v.package_name ?? '\u2014'} ${v.ecosystem ? `<span style=\"color:var(--text-muted);font-size:0.75rem\">[${v.ecosystem}]</span>` : ''}</div>\n <div class=\"top-cve-scores\">\n ${v.severity_score != null ? `<span class=\"badge ${v.severity_score >= 9 ? 'badge-red' : v.severity_score >= 7 ? 'badge-orange' : v.severity_score >= 4 ? 'badge-yellow' : 'badge-gray'}\">CVSS ${v.severity_score.toFixed(1)}</span>` : ''}\n ${v.epss_score != null ? `<span class=\"badge ${v.epss_score >= 0.5 ? 'badge-red' : v.epss_score >= 0.1 ? 'badge-yellow' : 'badge-gray'}\">EPSS ${(v.epss_score*100).toFixed(2)}%</span>` : ''}\n ${v.verdict ? `<span class=\"badge ${v.verdict === 'affected' ? 'badge-red' : v.verdict === 'not_affected' ? 'badge-green' : 'badge-yellow'}\">${v.verdict}</span>` : ''}\n </div>\n </div>`).join('')}\n </div>\n ` : totalVulns === 0 ? '<div class=\"empty-state\">No vulnerabilities found.</div>' : ''}\n </div>\n\n <!-- Tab: Vulnerabilities -->\n <div class=\"tab-panel\" id=\"tab-vulns\">\n <div class=\"filters\">\n <div class=\"filter-label\">Severity:</div>\n <select id=\"filterSeverity\">\n <option value=\"all\">All</option>\n <option value=\"critical\">Critical (\u22659)</option>\n <option value=\"high\">High (\u22657)</option>\n <option value=\"medium\">Medium (\u22654)</option>\n <option value=\"low\">Low (<4)</option>\n </select>\n <div class=\"filter-label\">Verdict:</div>\n <select id=\"filterVerdict\">\n <option value=\"all\">All</option>\n <option value=\"affected\">Affected</option>\n <option value=\"under_investigation\">Under Investigation</option>\n <option value=\"not_affected\">Not Affected</option>\n </select>\n <div class=\"filter-label\">Min EPSS:</div>\n <div class=\"range-group\">\n <input type=\"range\" id=\"filterEpss\" min=\"0\" max=\"1\" step=\"0.01\" value=\"0\">\n <span id=\"epssValue\">0.00</span>\n </div>\n </div>\n <div class=\"table-wrap\">\n <table id=\"vulnTable\">\n <thead>\n <tr>\n <th class=\"sortable\" data-col=\"cve_id\">CVE ID</th>\n <th class=\"sortable\" data-col=\"package_name\">Package</th>\n <th>Ecosystem</th>\n <th class=\"sortable\" data-col=\"severity_score\">CVSS</th>\n <th class=\"sortable sorted-desc\" data-col=\"epss_score\">EPSS</th>\n <th class=\"sortable\" data-col=\"epss_percentile\">Percentile</th>\n <th class=\"sortable\" data-col=\"risk_score\">Risk Score</th>\n <th>Verdict</th>\n <th>Fix</th>\n </tr>\n </thead>\n <tbody id=\"vulnBody\"></tbody>\n </table>\n </div>\n <div id=\"vulnEmpty\" class=\"empty-state\" style=\"display:none\">No vulnerabilities match the current filters.</div>\n </div>\n\n <!-- Tab: SBOM -->\n <div class=\"tab-panel\" id=\"tab-sbom\">\n <div style=\"display:flex;align-items:center;justify-content:space-between;margin-bottom:1rem\">\n <div class=\"badge-counts\">\n <span class=\"badge badge-accent\">${directCount} direct</span>\n <span class=\"badge badge-gray\">${transitiveCount} transitive</span>\n </div>\n <button class=\"btn btn-outline\" onclick=\"downloadSBOM()\">\u2193 Download SBOM JSON</button>\n </div>\n <div class=\"filters\">\n <div class=\"filter-label\">Ecosystem:</div>\n <select id=\"filterEco\">\n <option value=\"all\">All</option>\n ${[...new Set(deps.map(d => d.ecosystem))].sort().map(e => `<option value=\"${e}\">${e}</option>`).join('')}\n </select>\n <input type=\"text\" id=\"filterPkg\" placeholder=\"Search package\u2026\">\n </div>\n <div class=\"table-wrap\">\n <table id=\"sbomTable\">\n <thead>\n <tr>\n <th>Package</th>\n <th>Ecosystem</th>\n <th>Version</th>\n <th>Scope</th>\n <th>License</th>\n <th>purl</th>\n </tr>\n </thead>\n <tbody id=\"sbomBody\"></tbody>\n </table>\n </div>\n <div id=\"sbomEmpty\" class=\"empty-state\" style=\"display:none\">No packages match the current filters.</div>\n </div>\n\n <!-- Tab: VEX -->\n <div class=\"tab-panel\" id=\"tab-vex\">\n <div style=\"display:flex;justify-content:flex-end;margin-bottom:1rem\">\n <button class=\"btn btn-outline\" onclick=\"downloadVEX()\">\u2193 Download VEX JSON</button>\n </div>\n <div class=\"table-wrap\">\n <table id=\"vexTable\">\n <thead>\n <tr>\n <th>CVE ID</th>\n <th>Package</th>\n <th>Analysis State</th>\n <th>Justification</th>\n <th>Detail</th>\n </tr>\n </thead>\n <tbody id=\"vexBody\"></tbody>\n </table>\n </div>\n <div id=\"vexEmpty\" class=\"empty-state\" style=\"display:none\">No VEX entries found.</div>\n </div>\n\n <!-- Tab: Licenses -->\n <div class=\"tab-panel\" id=\"tab-licenses\">\n <div id=\"licBanners\"></div>\n <div class=\"filters\">\n <div class=\"filter-label\">Policy:</div>\n <select id=\"filterLicPolicy\">\n <option value=\"all\">All</option>\n <option value=\"deny\">Deny</option>\n <option value=\"warn\">Warn</option>\n <option value=\"ok\">OK</option>\n <option value=\"unknown\">Unknown</option>\n </select>\n </div>\n <div class=\"table-wrap\">\n <table id=\"licTable\">\n <thead>\n <tr>\n <th>Package</th>\n <th>Ecosystem</th>\n <th>License</th>\n <th>Policy Status</th>\n </tr>\n </thead>\n <tbody id=\"licBody\"></tbody>\n </table>\n </div>\n <div id=\"licEmpty\" class=\"empty-state\" style=\"display:none\">No packages match the current filters.</div>\n </div>\n\n <!-- Tab: Staleness -->\n <div class=\"tab-panel\" id=\"tab-staleness\">\n <div class=\"stats-grid\" style=\"grid-template-columns:repeat(3,1fr);max-width:600px;margin-bottom:1.5rem\">\n <div class=\"stat-card\">\n <div class=\"stat-label\">Scored</div>\n <div class=\"stat-value accent\">${stalenessRows.length}</div>\n </div>\n <div class=\"stat-card\">\n <div class=\"stat-label\">Low (0\u20130.3)</div>\n <div class=\"stat-value green\">${stalenessRows.filter(r => (r.staleness_score??0) < 0.3).length}</div>\n </div>\n <div class=\"stat-card\">\n <div class=\"stat-label\">High (0.7\u20131.0)</div>\n <div class=\"stat-value red\">${stalenessRows.filter(r => (r.staleness_score??0) >= 0.7).length}</div>\n </div>\n </div>\n\n ${stalenessRows.length > 0 ? `\n <div class=\"section-title\">Distribution</div>\n <div class=\"bar-chart\" style=\"margin-bottom:1.5rem\">\n <div class=\"bar-row\">\n <div class=\"bar-label\">Low (0\u20130.3)</div>\n <div class=\"bar-track\"><div class=\"bar-fill\" style=\"width:${Math.round(stalenessRows.filter(r=>(r.staleness_score??0)<0.3).length/stalenessRows.length*100)}%;background:var(--green)\"></div></div>\n <div class=\"bar-count\">${stalenessRows.filter(r=>(r.staleness_score??0)<0.3).length}</div>\n </div>\n <div class=\"bar-row\">\n <div class=\"bar-label\">Medium (0.3\u20130.7)</div>\n <div class=\"bar-track\"><div class=\"bar-fill\" style=\"width:${Math.round(stalenessRows.filter(r=>{const s=r.staleness_score??0;return s>=0.3&&s<0.7}).length/stalenessRows.length*100)}%;background:var(--yellow)\"></div></div>\n <div class=\"bar-count\">${stalenessRows.filter(r=>{const s=r.staleness_score??0;return s>=0.3&&s<0.7}).length}</div>\n </div>\n <div class=\"bar-row\">\n <div class=\"bar-label\">High (0.7\u20131.0)</div>\n <div class=\"bar-track\"><div class=\"bar-fill\" style=\"width:${Math.round(stalenessRows.filter(r=>(r.staleness_score??0)>=0.7).length/stalenessRows.length*100)}%;background:var(--red)\"></div></div>\n <div class=\"bar-count\">${stalenessRows.filter(r=>(r.staleness_score??0)>=0.7).length}</div>\n </div>\n </div>\n ` : ''}\n\n <div class=\"table-wrap\">\n <table id=\"staleTable\">\n <thead>\n <tr>\n <th>Package</th>\n <th>Ecosystem</th>\n <th>Current</th>\n <th>Latest</th>\n <th>Months Behind</th>\n <th class=\"sortable sorted-desc\" data-col=\"staleness_score\">Score</th>\n <th>Bar</th>\n </tr>\n </thead>\n <tbody id=\"staleBody\"></tbody>\n </table>\n </div>\n <div id=\"staleEmpty\" class=\"empty-state\" style=\"display:none\">No staleness data available.</div>\n </div>\n\n <!-- Tab: Attack Surface -->\n <div class=\"tab-panel\" id=\"tab-attack-surface\">\n ${attackSurface ? `\n <div class=\"stats-grid\" style=\"grid-template-columns:repeat(4,1fr);max-width:700px;margin-bottom:1.5rem\">\n <div class=\"stat-card\">\n <div class=\"stat-label\">Total Routes</div>\n <div class=\"stat-value accent\">${attackSurface.totalRoutes}</div>\n </div>\n <div class=\"stat-card\">\n <div class=\"stat-label\">Public</div>\n <div class=\"stat-value ${attackSurface.publicRoutes > 0 ? 'yellow' : 'green'}\">${attackSurface.publicRoutes}</div>\n </div>\n <div class=\"stat-card\">\n <div class=\"stat-label\">Authenticated</div>\n <div class=\"stat-value accent\">${attackSurface.authenticatedRoutes}</div>\n </div>\n <div class=\"stat-card\">\n <div class=\"stat-label\">With Vulnerabilities</div>\n <div class=\"stat-value ${attackSurface.routesWithVulns > 0 ? 'red' : 'green'}\">${attackSurface.routesWithVulns}</div>\n </div>\n </div>\n ${attackSurface.allRoutes.length > 0 ? `\n <div class=\"table-wrap\">\n <table id=\"attackSurfaceTable\">\n <thead>\n <tr>\n <th>Route</th>\n <th>Exposure</th>\n <th>Vuln Count</th>\n <th>Highest Risk</th>\n <th>Hops</th>\n </tr>\n </thead>\n <tbody>\n ${attackSurface.allRoutes.map(entry => {\n const exposureBadge = entry.exposureLevel === 'public'\n ? '<span class=\"badge badge-red\">public</span>'\n : entry.exposureLevel === 'authenticated'\n ? '<span class=\"badge badge-yellow\">authenticated</span>'\n : entry.exposureLevel === 'internal'\n ? '<span class=\"badge badge-gray\">internal</span>'\n : '<span class=\"badge badge-gray\">unknown</span>';\n const hasVulns = entry.vulnerableDeps.length > 0;\n const rowStyle = (entry.exposureLevel === 'public' && hasVulns)\n ? 'style=\"background:rgba(248,81,73,0.06)\"'\n : (entry.exposureLevel === 'authenticated' && hasVulns)\n ? 'style=\"background:rgba(210,153,34,0.06)\"'\n : '';\n const highestRisk = entry.riskScore > 0 ? entry.riskScore.toFixed(1) : '\u2014';\n const minHop = entry.vulnerableDeps.length > 0\n ? Math.min(...entry.vulnerableDeps.map(d => d.hopCount))\n : null;\n return `<tr ${rowStyle}>\n <td><code style=\"font-size:0.8rem;color:var(--accent)\">${entry.route.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>')}</code><br><span style=\"font-size:0.72rem;color:var(--text-muted)\">${(entry.filePath || '').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>')}</span></td>\n <td>${exposureBadge}</td>\n <td>${entry.vulnerableDeps.length > 0 ? `<span class=\"badge badge-red\">${entry.vulnerableDeps.length}</span>` : '<span style=\"color:var(--text-muted)\">0</span>'}</td>\n <td>${entry.riskScore >= 7 ? `<span class=\"score-critical\">${highestRisk}</span>` : entry.riskScore >= 4 ? `<span class=\"score-medium\">${highestRisk}</span>` : `<span class=\"score-low\">${highestRisk}</span>`}</td>\n <td>${minHop != null ? minHop : '\u2014'}</td>\n </tr>`;\n }).join('')}\n </tbody>\n </table>\n </div>` : '<div class=\"empty-state\">No routes found.</div>'}\n ` : '<div class=\"empty-state\">No routes found.</div>'}\n </div>\n\n <!-- Tab: Secrets -->\n <div class=\"tab-panel\" id=\"tab-secrets\">\n ${secretsResult ? `\n <div style=\"margin-bottom:1rem;color:var(--text-muted);font-size:0.85rem\">\n ${secretsResult.totalFindings} finding${secretsResult.totalFindings !== 1 ? 's' : ''} in ${secretsResult.filesScanned} file${secretsResult.filesScanned !== 1 ? 's' : ''} scanned\n ${secretsResult.criticalCount > 0 ? `\u2014 <span style=\"color:var(--red);font-weight:600\">${secretsResult.criticalCount} critical</span>` : ''}\n ${secretsResult.highCount > 0 ? `\u2014 <span style=\"color:var(--orange);font-weight:600\">${secretsResult.highCount} high</span>` : ''}\n </div>\n ${secretsResult.findings.length > 0 ? `\n <div class=\"table-wrap\">\n <table id=\"secretsTable\">\n <thead>\n <tr>\n <th>Severity</th>\n <th>Type</th>\n <th>File:Line</th>\n <th>Function</th>\n <th>Reachable From</th>\n </tr>\n </thead>\n <tbody>\n ${secretsResult.findings.map(f => {\n const sevBadge = f.severity === 'critical'\n ? '<span class=\"badge badge-red\">critical</span>'\n : f.severity === 'high'\n ? '<span class=\"badge badge-orange\">high</span>'\n : f.severity === 'medium'\n ? '<span class=\"badge badge-yellow\">medium</span>'\n : '<span class=\"badge badge-gray\">low</span>';\n const fileDisplay = (f.filePath || '').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');\n const funcDisplay = (f.nodeName || '\u2014').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');\n const typeDisplay = (f.type || '').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');\n return `<tr>\n <td>${sevBadge}</td>\n <td>${typeDisplay}</td>\n <td><code style=\"font-size:0.75rem;color:var(--text-muted)\">${fileDisplay}:${f.line}</code></td>\n <td><code style=\"font-size:0.78rem\">${funcDisplay}</code></td>\n <td>${f.entryPointCount > 0 ? `<span class=\"badge badge-yellow\">${f.entryPointCount} entry point${f.entryPointCount !== 1 ? 's' : ''}</span>` : '<span style=\"color:var(--text-muted)\">\u2014</span>'}</td>\n </tr>`;\n }).join('')}\n </tbody>\n </table>\n </div>` : '<div class=\"empty-state\">No secrets detected.</div>'}\n ` : '<div class=\"empty-state\">No secrets detected.</div>'}\n </div>\n\n <!-- Tab: SAST Flows -->\n <div class=\"tab-panel\" id=\"tab-sast\">\n ${flowsResult ? `\n <div style=\"margin-bottom:1rem;color:var(--text-muted);font-size:0.85rem\">\n ${flowsResult.length} finding${flowsResult.length !== 1 ? 's' : ''} across ${new Set(flowsResult.map(f => f.owaspCategory)).size} OWASP categor${new Set(flowsResult.map(f => f.owaspCategory)).size !== 1 ? 'ies' : 'y'}\n </div>\n ${flowsResult.length > 0 ? `\n <div class=\"table-wrap\">\n <table id=\"sastTable\">\n <thead>\n <tr>\n <th>OWASP</th>\n <th>Severity</th>\n <th>Type</th>\n <th>File:Line</th>\n <th>Symbol</th>\n <th>Recommendation</th>\n </tr>\n </thead>\n <tbody>\n ${flowsResult.map(f => {\n const owaspBadge = `<span class=\"badge badge-accent\">${f.owaspCategory}</span>`;\n const sevBadge = f.severity === 'critical'\n ? '<span class=\"badge badge-red\">critical</span>'\n : f.severity === 'high'\n ? '<span class=\"badge badge-orange\">high</span>'\n : '<span class=\"badge badge-yellow\">medium</span>';\n const fileDisplay = (f.filePath || '').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');\n const typeDisplay = (f.type || '').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');\n const symbolDisplay = (f.symbol || '\u2014').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');\n const recDisplay = (f.recommendation || '').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');\n return `<tr>\n <td>${owaspBadge}</td>\n <td>${sevBadge}</td>\n <td><code style=\"font-size:0.78rem\">${typeDisplay}</code></td>\n <td><code style=\"font-size:0.75rem;color:var(--text-muted)\">${fileDisplay}:${f.line}</code></td>\n <td><code style=\"font-size:0.78rem;color:var(--accent)\">${symbolDisplay}</code></td>\n <td style=\"max-width:300px;font-size:0.78rem;color:var(--text-muted)\">${recDisplay}</td>\n </tr>`;\n }).join('')}\n </tbody>\n </table>\n </div>` : '<div class=\"empty-state\">No dangerous patterns detected.</div>'}\n ` : '<div class=\"empty-state\">No dangerous patterns detected.</div>'}\n </div>\n\n <!-- Tab: Remediation -->\n <div class=\"tab-panel\" id=\"tab-remediation\">\n ${remediationStatus ? `\n <div class=\"stats-grid\" style=\"grid-template-columns:repeat(3,1fr);max-width:500px;margin-bottom:1.5rem\">\n <div class=\"stat-card\">\n <div class=\"stat-label\">Overdue</div>\n <div class=\"stat-value ${remediationStatus.filter(r => r.slaStatus === 'overdue').length > 0 ? 'red' : 'green'}\">${remediationStatus.filter(r => r.slaStatus === 'overdue').length}</div>\n </div>\n <div class=\"stat-card\">\n <div class=\"stat-label\">On Track</div>\n <div class=\"stat-value green\">${remediationStatus.filter(r => r.slaStatus === 'ok' || r.slaStatus === 'warning').length}</div>\n </div>\n <div class=\"stat-card\">\n <div class=\"stat-label\">No Fix Available</div>\n <div class=\"stat-value yellow\">${remediationStatus.filter(r => r.slaStatus === 'no_fix').length}</div>\n </div>\n </div>\n ${remediationStatus.length > 0 ? `\n <div class=\"table-wrap\">\n <table id=\"remediationTable\">\n <thead>\n <tr>\n <th>CVE</th>\n <th>Package</th>\n <th>Days Open</th>\n <th>Fix Available</th>\n <th>SLA (days)</th>\n <th>Status</th>\n </tr>\n </thead>\n <tbody>\n ${remediationStatus.map(r => {\n const statusChip = r.slaStatus === 'ok'\n ? '<span class=\"badge badge-green\">ok</span>'\n : r.slaStatus === 'warning'\n ? '<span class=\"badge badge-yellow\">warning</span>'\n : r.slaStatus === 'overdue'\n ? '<span class=\"badge badge-red\">overdue</span>'\n : '<span class=\"badge badge-gray\">no fix</span>';\n const cveDisplay = (r.cveId || '').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');\n const pkgDisplay = (r.packageName || '').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');\n const slaDays = r.severity !== null\n ? (r.severity >= 9 ? 7 : r.severity >= 7 ? 30 : r.severity >= 4 ? 90 : 180)\n : 180;\n return `<tr>\n <td><code style=\"font-size:0.8rem;color:var(--accent)\">${cveDisplay}</code></td>\n <td>${pkgDisplay}</td>\n <td>${r.daysOpen != null ? r.daysOpen + 'd' : '\u2014'}</td>\n <td>${r.daysWithFixAvailable != null ? `<span class=\"badge badge-green\">${r.daysWithFixAvailable}d ago</span>` : '<span style=\"color:var(--text-muted)\">\u2014</span>'}</td>\n <td>${slaDays}d</td>\n <td>${statusChip}</td>\n </tr>`;\n }).join('')}\n </tbody>\n </table>\n </div>` : '<div class=\"empty-state\">No open remediation items.</div>'}\n ` : '<div class=\"empty-state\">No remediation data available.</div>'}\n </div>\n\n</div>\n\n<script>\n// \u2500\u2500 Embedded data \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nconst VULNS = ${safeJson(vulns)};\nconst DEPS = ${safeJson(deps)};\nconst LIC_DATA = ${safeJson(licenseData)};\nconst STALE_DATA = ${safeJson(stalenessRows)};\nconst SBOM_JSON = ${safeJson(sbomJson)};\nconst VEX_JSON = ${safeJson(vexJson)};\n\n// \u2500\u2500 Tab switching \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\ndocument.querySelectorAll('.tab').forEach(btn => {\n btn.addEventListener('click', () => {\n document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));\n document.querySelectorAll('.tab-panel').forEach(p => p.classList.remove('active'));\n btn.classList.add('active');\n const tabId = btn.getAttribute('data-tab');\n document.getElementById('tab-' + tabId).classList.add('active');\n });\n});\n\n// \u2500\u2500 Utilities \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nfunction esc(str) {\n if (str == null) return '';\n return String(str).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/\"/g,'"');\n}\nfunction downloadJson(data, filename) {\n const blob = new Blob([data], {type: 'application/json'});\n const a = document.createElement('a');\n a.href = URL.createObjectURL(blob);\n a.download = filename;\n a.click();\n}\nfunction downloadSBOM() { downloadJson(SBOM_JSON, 'sbom.json'); }\nfunction downloadVEX() { downloadJson(VEX_JSON, 'vex.json'); }\n\nfunction epssClass(v) {\n if (v == null) return 'badge-gray';\n if (v >= 0.5) return 'badge-red';\n if (v >= 0.1) return 'badge-yellow';\n return 'badge-gray';\n}\nfunction cvssClass(v) {\n if (v == null) return '';\n if (v >= 9) return 'score-critical';\n if (v >= 7) return 'score-high';\n if (v >= 4) return 'score-medium';\n return 'score-low';\n}\nfunction verdictBadge(v) {\n if (!v) return '<span class=\"badge badge-gray\">\u2014</span>';\n if (v === 'affected') return '<span class=\"badge badge-red\">affected</span>';\n if (v === 'not_affected') return '<span class=\"badge badge-green\">not affected</span>';\n if (v === 'under_investigation') return '<span class=\"badge badge-yellow\">investigating</span>';\n return '<span class=\"badge badge-gray\">' + esc(v) + '</span>';\n}\nfunction staleColor(score) {\n if (score >= 0.7) return 'var(--red)';\n if (score >= 0.3) return 'var(--yellow)';\n return 'var(--green)';\n}\n\n// \u2500\u2500 Vulnerability table \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nlet vulnSortCol = 'epss_score';\nlet vulnSortDir = -1; // -1 = desc\n\nfunction renderVulns() {\n const sevFilter = document.getElementById('filterSeverity').value;\n const verdFilter = document.getElementById('filterVerdict').value;\n const epssMin = parseFloat(document.getElementById('filterEpss').value) || 0;\n\n let rows = VULNS.filter(v => {\n if (sevFilter !== 'all') {\n const s = v.severity_score ?? 0;\n if (sevFilter === 'critical' && s < 9) return false;\n if (sevFilter === 'high' && (s < 7 || s >= 9)) return false;\n if (sevFilter === 'medium' && (s < 4 || s >= 7)) return false;\n if (sevFilter === 'low' && s >= 4) return false;\n }\n if (verdFilter !== 'all') {\n if ((v.verdict ?? '') !== verdFilter) return false;\n }\n if ((v.epss_score ?? 0) < epssMin) return false;\n return true;\n });\n\n rows = rows.slice().sort((a, b) => {\n const av = a[vulnSortCol] ?? (typeof a[vulnSortCol] === 'number' ? -Infinity : '');\n const bv = b[vulnSortCol] ?? (typeof b[vulnSortCol] === 'number' ? -Infinity : '');\n if (av < bv) return vulnSortDir;\n if (av > bv) return -vulnSortDir;\n return 0;\n });\n\n const tbody = document.getElementById('vulnBody');\n const empty = document.getElementById('vulnEmpty');\n\n if (rows.length === 0) {\n tbody.innerHTML = '';\n empty.style.display = 'block';\n return;\n }\n empty.style.display = 'none';\n\n tbody.innerHTML = rows.map(v => {\n const rowClass = v.verdict === 'affected' ? 'verdict-affected'\n : v.verdict === 'under_investigation' ? 'verdict-investigating'\n : v.verdict === 'not_affected' ? 'verdict-not-affected' : '';\n const riskScore = v.risk_score;\n const riskBadge = riskScore != null\n ? \\`<span class=\"badge \\${riskScore >= 7 ? 'badge-red' : riskScore >= 4 ? 'badge-yellow' : 'badge-gray'}\">[Risk: \\${riskScore.toFixed(1)}]</span>\\`\n : '<span style=\"color:var(--text-muted)\">\u2014</span>';\n return \\`<tr class=\"row-toggle \\${rowClass}\" onclick=\"toggleExpand(this, \\${JSON.stringify(v).replace(/\"/g,'"')})\">\n <td><code style=\"font-size:0.8rem;color:var(--accent)\">\\${esc(v.cve_id)}</code></td>\n <td>\\${esc(v.package_name ?? '\u2014')}</td>\n <td><span class=\"badge badge-gray\">\\${esc(v.ecosystem ?? '\u2014')}</span></td>\n <td class=\"\\${cvssClass(v.severity_score)}\">\\${v.severity_score != null ? v.severity_score.toFixed(1) : '\u2014'}</td>\n <td><span class=\"badge \\${epssClass(v.epss_score)}\">\\${v.epss_score != null ? (v.epss_score*100).toFixed(2)+'%' : '\u2014'}</span></td>\n <td>\\${v.epss_percentile != null ? (v.epss_percentile*100).toFixed(0)+'th' : '\u2014'}</td>\n <td>\\${riskBadge}</td>\n <td>\\${verdictBadge(v.verdict)}</td>\n <td>\\${v.fixed_version ? '<span class=\"badge badge-green\">'+esc(v.fixed_version)+'</span>' : '<span style=\"color:var(--text-muted)\">\u2014</span>'}</td>\n </tr>\n <tr class=\"expand-row\" style=\"display:none\">\n <td colspan=\"9\">\n <div class=\"expand-body\" id=\"expand-\\${esc(v.cve_id)}\"></div>\n </td>\n </tr>\\`;\n }).join('');\n}\n\nfunction toggleExpand(row, data) {\n const expandRow = row.nextElementSibling;\n if (!expandRow) return;\n const isVisible = expandRow.style.display !== 'none';\n if (isVisible) {\n expandRow.style.display = 'none';\n return;\n }\n expandRow.style.display = '';\n const body = expandRow.querySelector('.expand-body');\n let html = '';\n if (data.summary) {\n html += '<div class=\"expand-section\"><div class=\"expand-label\">Summary</div><div>' + esc(data.summary) + '</div></div>';\n }\n if (data.affected_ranges) {\n try {\n const ranges = JSON.parse(data.affected_ranges);\n html += '<div class=\"expand-section\"><div class=\"expand-label\">Affected Ranges</div><pre>' + esc(JSON.stringify(ranges, null, 2)) + '</pre></div>';\n } catch {}\n }\n if (data.paths) {\n try {\n const paths = JSON.parse(data.paths);\n if (Array.isArray(paths) && paths.length > 0) {\n html += '<div class=\"expand-section\"><div class=\"expand-label\">Call Paths (' + paths.length + ')</div>';\n paths.slice(0, 3).forEach(p => {\n if (p.path && Array.isArray(p.path)) {\n html += '<div style=\"margin-bottom:0.3rem;font-family:monospace;font-size:0.78rem;color:var(--text-muted)\">' + p.path.map(n => esc(n.id || n)).join(' \u2192 ') + '</div>';\n }\n });\n if (paths.length > 3) html += '<div style=\"color:var(--text-muted);font-size:0.75rem\">\u2026 and ' + (paths.length-3) + ' more</div>';\n html += '</div>';\n }\n } catch {}\n }\n if (!html) html = '<div style=\"color:var(--text-muted)\">No additional details available.</div>';\n body.innerHTML = html;\n}\n\n// Sort headers\ndocument.querySelectorAll('#vulnTable th.sortable').forEach(th => {\n th.addEventListener('click', () => {\n const col = th.getAttribute('data-col');\n if (vulnSortCol === col) {\n vulnSortDir = -vulnSortDir;\n th.classList.toggle('sorted-asc', vulnSortDir === 1);\n th.classList.toggle('sorted-desc', vulnSortDir === -1);\n } else {\n document.querySelectorAll('#vulnTable th').forEach(t => { t.classList.remove('sorted-asc','sorted-desc'); });\n vulnSortCol = col;\n vulnSortDir = -1;\n th.classList.add('sorted-desc');\n }\n renderVulns();\n });\n});\n\ndocument.getElementById('filterSeverity').addEventListener('change', renderVulns);\ndocument.getElementById('filterVerdict').addEventListener('change', renderVulns);\ndocument.getElementById('filterEpss').addEventListener('input', function() {\n document.getElementById('epssValue').textContent = parseFloat(this.value).toFixed(2);\n renderVulns();\n});\n\n// \u2500\u2500 SBOM table \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nfunction renderSBOM() {\n const eco = document.getElementById('filterEco').value;\n const pkg = document.getElementById('filterPkg').value.toLowerCase();\n\n const rows = DEPS.filter(d => {\n if (eco !== 'all' && d.ecosystem !== eco) return false;\n if (pkg && !d.package_name.toLowerCase().includes(pkg)) return false;\n return true;\n });\n\n const tbody = document.getElementById('sbomBody');\n const empty = document.getElementById('sbomEmpty');\n if (rows.length === 0) {\n tbody.innerHTML = '';\n empty.style.display = 'block';\n return;\n }\n empty.style.display = 'none';\n\n tbody.innerHTML = rows.map(d => {\n const version = d.resolved_version || d.declared_constraint || '\u2014';\n const purl = d.ecosystem && d.package_name\n ? 'pkg:' + esc(d.ecosystem) + '/' + esc(d.package_name) + '@' + esc(version)\n : '\u2014';\n const scopeBadge = d.classification === 'direct'\n ? '<span class=\"badge badge-accent\">direct</span>'\n : '<span class=\"badge badge-gray\">transitive</span>';\n return \\`<tr>\n <td><strong>\\${esc(d.package_name)}</strong></td>\n <td><span class=\"badge badge-gray\">\\${esc(d.ecosystem)}</span></td>\n <td><code style=\"font-size:0.78rem\">\\${esc(version)}</code></td>\n <td>\\${scopeBadge}</td>\n <td>\\${d.license ? esc(d.license) : '<span style=\"color:var(--text-muted)\">\u2014</span>'}</td>\n <td><code style=\"font-size:0.72rem;color:var(--text-muted)\">\\${esc(purl)}</code></td>\n </tr>\\`;\n }).join('');\n}\n\ndocument.getElementById('filterEco').addEventListener('change', renderSBOM);\ndocument.getElementById('filterPkg').addEventListener('input', renderSBOM);\n\n// \u2500\u2500 VEX table \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nfunction renderVEX() {\n let vexData;\n try { vexData = JSON.parse(VEX_JSON); } catch { vexData = null; }\n const tbody = document.getElementById('vexBody');\n const empty = document.getElementById('vexEmpty');\n\n const vulns = vexData && vexData.vulnerabilities ? vexData.vulnerabilities : [];\n if (vulns.length === 0) {\n tbody.innerHTML = '';\n empty.style.display = 'block';\n return;\n }\n empty.style.display = 'none';\n\n tbody.innerHTML = vulns.map(entry => {\n const state = entry.analysis && entry.analysis.state ? entry.analysis.state : '\u2014';\n const justification = entry.analysis && entry.analysis.justification ? entry.analysis.justification : '\u2014';\n const detail = entry.analysis && entry.analysis.detail ? entry.analysis.detail : '\u2014';\n const pkg = entry.affects && entry.affects[0] ? entry.affects[0].ref : '\u2014';\n const stateClass = state === 'affected' ? 'state-affected'\n : state === 'not_affected' ? 'state-not_affected'\n : state === 'under_investigation' ? 'state-investigating' : '';\n return \\`<tr>\n <td><code style=\"font-size:0.8rem;color:var(--accent)\">\\${esc(entry.id)}</code></td>\n <td><code style=\"font-size:0.75rem;color:var(--text-muted)\">\\${esc(pkg)}</code></td>\n <td><span class=\"\\${stateClass}\" style=\"font-weight:500\">\\${esc(state)}</span></td>\n <td>\\${esc(justification)}</td>\n <td style=\"max-width:300px\">\\${esc(detail)}</td>\n </tr>\\`;\n }).join('');\n}\n\n// \u2500\u2500 Licenses table \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nfunction renderLicenses() {\n // Banners\n const bannerEl = document.getElementById('licBanners');\n const denyCount = LIC_DATA.filter(d => d.status === 'deny').length;\n const warnCount = LIC_DATA.filter(d => d.status === 'warn').length;\n let banners = '';\n if (denyCount > 0) banners += \\`<div class=\"policy-banner deny\">\u2717 \\${denyCount} package\\${denyCount !== 1 ? 's' : ''} with denied license\\${denyCount !== 1 ? 's' : ''}</div>\\`;\n if (warnCount > 0) banners += \\`<div class=\"policy-banner warn\">\u26A0 \\${warnCount} package\\${warnCount !== 1 ? 's' : ''} with warned license\\${warnCount !== 1 ? 's' : ''}</div>\\`;\n bannerEl.innerHTML = banners;\n\n const pol = document.getElementById('filterLicPolicy').value;\n const rows = LIC_DATA.filter(d => pol === 'all' || d.status === pol);\n\n const tbody = document.getElementById('licBody');\n const empty = document.getElementById('licEmpty');\n if (rows.length === 0) {\n tbody.innerHTML = '';\n empty.style.display = 'block';\n return;\n }\n empty.style.display = 'none';\n\n tbody.innerHTML = rows.map(d => {\n let statusHtml;\n if (d.status === 'deny') statusHtml = '<span class=\"badge badge-red\">\u2717 deny</span>';\n else if (d.status === 'warn') statusHtml = '<span class=\"badge badge-yellow\">\u26A0 warn</span>';\n else if (d.status === 'ok') statusHtml = '<span class=\"badge badge-green\">\u2713 ok</span>';\n else statusHtml = '<span class=\"badge badge-gray\">\u2014 unknown</span>';\n return \\`<tr>\n <td><strong>\\${esc(d.package_name)}</strong></td>\n <td><span class=\"badge badge-gray\">\\${esc(d.ecosystem)}</span></td>\n <td>\\${d.license ? esc(d.license) : '<span style=\"color:var(--text-muted)\">\u2014</span>'}</td>\n <td>\\${statusHtml}</td>\n </tr>\\`;\n }).join('');\n}\n\ndocument.getElementById('filterLicPolicy').addEventListener('change', renderLicenses);\n\n// \u2500\u2500 Staleness table \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nfunction renderStaleness() {\n const tbody = document.getElementById('staleBody');\n const empty = document.getElementById('staleEmpty');\n\n if (STALE_DATA.length === 0) {\n tbody.innerHTML = '';\n empty.style.display = 'block';\n return;\n }\n empty.style.display = 'none';\n\n tbody.innerHTML = STALE_DATA.map(d => {\n const version = d.resolved_version || d.declared_constraint || '\u2014';\n const latest = d.latest_version || '\u2014';\n const score = d.staleness_score ?? 0;\n const months = d.latest_published\n ? Math.round((Date.now() - d.latest_published) / (1000 * 60 * 60 * 24 * 30))\n : null;\n const barPct = Math.round(score * 100);\n const barColor = staleColor(score);\n const scoreClass = score >= 0.7 ? 'score-critical' : score >= 0.3 ? 'score-medium' : 'score-low';\n return \\`<tr>\n <td><strong>\\${esc(d.package_name)}</strong></td>\n <td><span class=\"badge badge-gray\">\\${esc(d.ecosystem)}</span></td>\n <td><code style=\"font-size:0.78rem\">\\${esc(version)}</code></td>\n <td><code style=\"font-size:0.78rem;color:var(--green)\">\\${esc(latest)}</code></td>\n <td>\\${months != null ? months + 'mo' : '\u2014'}</td>\n <td class=\"\\${scoreClass}\">\\${score.toFixed(2)}</td>\n <td>\n <div class=\"stale-bar-track\">\n <div class=\"stale-bar-fill\" style=\"width:\\${barPct}%;background:\\${barColor}\"></div>\n </div>\n </td>\n </tr>\\`;\n }).join('');\n}\n\n// \u2500\u2500 Initial render \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nrenderVulns();\nrenderSBOM();\nrenderVEX();\nrenderLicenses();\nrenderStaleness();\n</script>\n\n</body>\n</html>`;\n}\n\n// \u2500\u2500 Command registration \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport function register(secCmd: import('commander').Command): void {\n secCmd\n .command('export [projectPath]')\n .description('Generate a self-contained HTML security dashboard')\n .option('--output <file>', 'Output HTML file path', '.kirograph/security-export.html')\n .option('--open', 'Open in browser after generating')\n .action(async (projectPath: string | undefined, opts: { output: string; open?: boolean }) => {\n const target = path.resolve(projectPath ?? process.cwd());\n const config = await loadConfig(target);\n\n if (!config.enableSecurity) {\n console.error(`\\n ${'\\x1b[33m'}\u26A0 Security analysis is disabled.${reset}`);\n console.error(` ${dim}Enable it in .kirograph/config.json:${reset} ${violet}${bold}\"enableSecurity\": true${reset}`);\n console.error(` ${dim}Then re-run:${reset} ${violet}${bold}kirograph index${reset}\\n`);\n process.exit(1);\n }\n\n const KiroGraph = (await Promise.resolve().then(() => require('../../index.js'))).default;\n if (!KiroGraph.isInitialized(target)) {\n console.error(' \u2716 KiroGraph not initialized. Run: kirograph init');\n process.exit(1);\n }\n\n const cg = await KiroGraph.open(target);\n const db = cg.getDatabase();\n db.applySecuritySchema();\n const rawDb = db.getRawDb();\n\n // Query all vulnerability + dep data in one JOIN\n const rawVulns: VulnRow[] = rawDb.all(`\n SELECT v.cve_id, v.severity_score, v.risk_score, v.epss_score, v.epss_percentile,\n v.fixed_version, v.summary, v.affected_ranges,\n d.package_name, d.ecosystem, d.resolved_version, d.declared_constraint,\n d.scope, d.license,\n r.verdict, r.paths, r.unresolved_symbols, r.reaching_entry_point_count\n FROM sec_vulnerabilities v\n LEFT JOIN edges e ON e.target = v.node_id AND e.kind = 'has_vulnerability'\n LEFT JOIN sec_dependencies d ON d.node_id = e.source\n LEFT JOIN sec_reachability r ON r.vulnerability_node_id = v.node_id\n ORDER BY v.epss_score DESC NULLS LAST, v.severity_score DESC NULLS LAST\n `);\n\n // Deduplicate by (cve_id, package_name, ecosystem) \u2014 same CVE can appear multiple\n // times when a package is declared in multiple manifests (monorepo).\n const _vRank = (v: string | null) =>\n v === 'affected' ? 3 : v === 'under_investigation' ? 2 : v === 'not_affected' ? 1 : 0;\n const _dedupMap = new Map<string, VulnRow>();\n for (const row of rawVulns) {\n const key = `${row.cve_id}::${row.package_name ?? ''}::${row.ecosystem ?? ''}`;\n const ex = _dedupMap.get(key);\n if (!ex || _vRank(row.verdict) > _vRank(ex.verdict)) _dedupMap.set(key, row);\n }\n const vulns: VulnRow[] = [..._dedupMap.values()];\n\n // Determine which deps are direct (have a declared_in edge)\n const directNodeIds = new Set<string>(\n (rawDb.all(`SELECT source FROM edges WHERE kind = 'declared_in'`) as Array<{ source: string }>)\n .map((r) => r.source),\n );\n\n // All dependencies for SBOM tab + licenses + staleness\n const rawDeps: Omit<DepRow, 'classification'>[] = rawDb.all(\n `SELECT node_id, package_name, ecosystem, resolved_version, declared_constraint,\n scope, license, latest_version, latest_published, staleness_score\n FROM sec_dependencies ORDER BY package_name`,\n );\n const deps: DepRow[] = rawDeps.map(d => ({\n ...d,\n classification: directNodeIds.has(d.node_id) ? 'direct' : 'transitive',\n }));\n\n // Generate SBOM and VEX JSON for download buttons\n const { SBOMExporter } = await Promise.resolve().then(() => require('../../security/export/sbom.js'));\n const { VEXExporter } = await Promise.resolve().then(() => require('../../security/export/vex.js'));\n const sbomExporter = new SBOMExporter(db, target);\n const vexExporter = new VEXExporter(db, target);\n const sbomJson = sbomExporter.exportJSON();\n const vexJson = vexExporter.exportJSON();\n\n // Attack surface data\n let attackSurface: AttackSurfaceResult | null = null;\n try {\n const { AttackSurfaceAnalyzer } = await Promise.resolve().then(() => require('../../security/attack-surface.js'));\n const analyzer = new AttackSurfaceAnalyzer(db, {\n authDetectionMode: config.securityAuthDetectionMode,\n authConfidenceThreshold: config.securityAuthConfidenceThreshold,\n jevApiKey: config.jevApiKey,\n jevBaseUrl: config.jevBaseUrl,\n jevModel: config.jevModel,\n });\n attackSurface = await analyzer.analyze();\n } catch { /* non-critical */ }\n\n // Secrets data\n let secretsResult: SecretsResult | null = null;\n try {\n const { SecretsScanner } = await Promise.resolve().then(() => require('../../security/secrets.js'));\n const scanner = new SecretsScanner(db, target);\n secretsResult = await scanner.scan();\n } catch { /* non-critical */ }\n\n // Data flows\n let flowsResult: DataFlowFinding[] | null = null;\n try {\n const { DataFlowAnalyzer } = await Promise.resolve().then(() => require('../../security/data-flows.js'));\n const analyzer = new DataFlowAnalyzer(db);\n flowsResult = await analyzer.analyze();\n } catch { /* non-critical */ }\n\n // Remediation\n let remediationStatus: RemediationStatus[] | null = null;\n try {\n const { RemediationTracker } = await Promise.resolve().then(() => require('../../security/remediation.js'));\n const tracker = new RemediationTracker(db);\n remediationStatus = tracker.getStatus();\n } catch { /* non-critical */ }\n\n cg.close();\n\n // Determine license policy\n const denyList = config.securityLicensePolicy?.deny ?? [];\n const warnList = config.securityLicensePolicy?.warn ?? [];\n\n const projectName = path.basename(target);\n const generatedAt = new Date().toLocaleString();\n\n const html = generateHtml({\n projectName,\n generatedAt,\n vulns,\n deps,\n sbomJson,\n vexJson,\n denyList,\n warnList,\n attackSurface,\n secretsResult,\n flowsResult,\n remediationStatus,\n });\n\n // Write output\n const outPath = path.resolve(target, opts.output);\n fs.mkdirSync(path.dirname(outPath), { recursive: true });\n fs.writeFileSync(outPath, html, 'utf-8');\n\n console.error(` ${green}\u2713${reset} Security dashboard written to ${violet}${bold}${outPath}${reset}`);\n console.error(` ${dim}${vulns.length} vulnerabilities \u00B7 ${deps.length} dependencies${reset}`);\n\n if (opts.open) {\n openBrowser(outPath);\n console.error(` ${dim}Opening in browser\u2026${reset}`);\n }\n });\n}\n"],
|
|
5
|
+
"mappings": "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AAAA,mCAKqB,WAAW;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAA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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -53,6 +53,11 @@ async function getWiki(cwd) {
|
|
|
53
53
|
const kirographDir = `${cwd}/.kirograph`;
|
|
54
54
|
const wiki = new KiroGraphWiki(db.getRawDb(), kirographDir, {
|
|
55
55
|
autoResolveConflicts: config.wikiAutoResolveConflicts,
|
|
56
|
+
wikiContradictionMode: config.wikiContradictionMode,
|
|
57
|
+
wikiContradictionConfidenceThreshold: config.wikiContradictionConfidenceThreshold,
|
|
58
|
+
jevApiKey: config.jevApiKey,
|
|
59
|
+
jevBaseUrl: config.jevBaseUrl,
|
|
60
|
+
jevModel: config.jevModel,
|
|
56
61
|
typedPages: config.wikiTypedPages
|
|
57
62
|
});
|
|
58
63
|
wiki.initialize();
|
|
@@ -211,7 +216,13 @@ function register(program) {
|
|
|
211
216
|
wiki.command("lint").description("Health check the wiki for issues").action(async () => {
|
|
212
217
|
const cwd = process.cwd();
|
|
213
218
|
const w = await getWiki(cwd);
|
|
214
|
-
|
|
219
|
+
let issues;
|
|
220
|
+
try {
|
|
221
|
+
issues = await w.lint();
|
|
222
|
+
} catch (err) {
|
|
223
|
+
console.error(` \u2716 ${err instanceof Error ? err.message : String(err)}`);
|
|
224
|
+
process.exit(1);
|
|
225
|
+
}
|
|
215
226
|
if (issues.length === 0) {
|
|
216
227
|
console.log(`
|
|
217
228
|
${import_ui.green}\u2713${import_ui.reset} Wiki lint passed \u2014 no issues found.
|