/* * report-renderers.mjs — llm-security playground rapport-renderere + parsers. * * CANONICAL SOURCE. The same code lives INLINE in * playground/llm-security-playground.html as a duplicated copy. The two * MUST stay in sync. file:// + ESM `import` is blocked in Chrome and * Firefox without flags, so the playground retains an inline copy to * remain a single-file file:// distribution. This module is the canonical * source for the upcoming session 4 CLI (scripts/render-report.mjs). * * 45 exported functions + PARSERS routing-map + RENDERERS routing-map + * KEY_STATS_CONFIG. See the export-block at the bottom of this file. * * Renderer API: render*(data, slot) — mutates slot.innerHTML and returns * undefined. For Node CLI use, pass slot = { innerHTML: '' } and read * slot.innerHTML after the call. This matches the playground exactly * (bit-identical output by construction; identical code lives in both). */ // ============================================================ // ESCAPE HELPERS // ============================================================ function escapeHtml(str) { if (str == null) return ''; return String(str) .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); } function escapeAttr(str) { return escapeHtml(str); } // ============================================================ // PAGE-SHELL HELPERS (DS Tier 3) // ============================================================ function renderVerdictPill(verdict, sub) { const v = String(verdict || 'n-a').toLowerCase(); const labels = { 'go': 'GO', 'go-with-conditions': 'BETINGET', 'block': 'BLOKKERT', 'approved': 'GODKJENT', 'failed': 'UNDERKJENT', 'allow': 'TILLATT', 'warning': 'ADVARSEL', 'n-a': 'IKKE VURDERT' }; const dsVerdict = ( v === 'failed' ? 'block' : v === 'go-with-conditions' ? 'warning' : v === 'go' || v === 'approved' ? 'allow' : v ); const subHtml = sub ? '' + escapeHtml(String(sub)) + '' : ''; return ( '
' + '' + escapeHtml(labels[v] || v.toUpperCase()) + '' + subHtml + '
' ); } function renderKeyStatsGrid(stats) { if (!stats || !stats.length) return ''; const items = stats.map(function (s) { const cls = 'key-stat' + (s.modifier ? ' key-stat--' + escapeAttr(s.modifier) : ''); const hint = s.hint ? '' + escapeHtml(s.hint) + '' : ''; return '
' + '' + escapeHtml(s.label || '') + '' + '' + escapeHtml(String(s.value)) + '' + hint + '
'; }).join(''); return '
' + items + '
'; } /** * Render page-shell — DS Tier 3 page__header-klyngen brukt på alle 4 overflater: * - onboarding: page__eyebrow="ONBOARDING · n av 5 grupper komplette" * - home: page__eyebrow="HJEM" (m/ hero-modifier for editorial type-hierarki) * - catalog: page__eyebrow="KATALOG" * - project: page__eyebrow="PROSJEKT · " * Pluss alle 18 rapport-renderere (eyebrow per archetype). * Verdict-rendering via renderVerdictPill — produserer DS verdict-pill-lg. * opts: { eyebrow, title, lede, meta:[], verdict, verdictSub, hero, keyStats } */ function renderPageShell(opts, bodyHtml) { opts = opts || {}; const eyebrow = opts.eyebrow ? '' + escapeHtml(opts.eyebrow) + '' : ''; const title = '

' + escapeHtml(opts.title || '') + '

'; const lede = opts.lede ? '

' + escapeHtml(opts.lede) + '

' : ''; const meta = (opts.meta && opts.meta.length) ? '
' + opts.meta.map(function (m) { return '' + escapeHtml(m) + ''; }).join('') + '
' : ''; const verdict = (opts.verdict && opts.verdict !== 'n-a') ? renderVerdictPill(opts.verdict, opts.verdictSub) : ''; const aside = verdict ? '
' + verdict + '
' : ''; const stats = renderKeyStatsGrid(opts.keyStats); const heroClass = opts.hero ? ' page__header--hero' : ''; return ( '
' + '
' + eyebrow + title + lede + meta + '
' + aside + '
' + stats + (bodyHtml || '') ); } // ============================================================ // INFER VERDICT + KEY-STATS PER ARCHETYPE // ============================================================ function normalizeVerdict(v) { const s = String(v || '').toLowerCase().trim(); const map = { 'block': 'block', 'blokk': 'block', 'blokkert': 'block', 'failed': 'failed', 'underkjent': 'failed', 'warning': 'warning', 'advarsel': 'warning', 'go-with-conditions': 'go-with-conditions', 'betinget': 'go-with-conditions', 'conditional': 'go-with-conditions', 'go': 'go', 'tillatt': 'allow', 'allow': 'allow', 'approved': 'approved', 'godkjent': 'approved', 'n-a': 'n-a', 'na': 'n-a', 'ikke-vurdert': 'n-a' }; return map[s] || s || 'n-a'; } function inferVerdict(data, archetype) { if (!data) return 'n-a'; if (data.verdict) return normalizeVerdict(data.verdict); switch (archetype) { case 'findings': { const fs = data.findings || []; if (!fs.length) return 'allow'; const crit = fs.some(function (f) { return /crit|kritisk/i.test(f.severity || ''); }); return crit ? 'block' : 'warning'; } case 'findings-grade': { const g = String(data.grade || '').toUpperCase(); if (g === 'A' || g === 'B') return 'allow'; if (g === 'C' || g === 'D') return 'warning'; if (g === 'F') return 'block'; return 'n-a'; } case 'posture-cards': { const g = String(data.grade || '').toUpperCase(); if (g === 'A' || g === 'B') return 'allow'; if (g === 'C' || g === 'D') return 'warning'; if (g === 'F') return 'block'; return 'n-a'; } case 'risk-score-meter': { const score = Number(data.risk_score); if (isNaN(score)) return 'n-a'; if (score >= 65) return 'block'; if (score >= 15) return 'warning'; return 'allow'; } case 'dashboard-fleet': { const g = String(data.machine_grade || '').toUpperCase(); if (g === 'A' || g === 'B') return 'allow'; if (g === 'C' || g === 'D') return 'warning'; if (g === 'F') return 'block'; return 'n-a'; } case 'red-team-results': { const fail = Number(data.fail_count) || 0; if (fail > 5) return 'block'; if (fail > 0) return 'warning'; return 'allow'; } case 'diff-report': { const newCount = (data['new'] || []).length; if (newCount > 0) return 'warning'; return 'allow'; } case 'kanban-buckets': { const remove = (data.remove || []).length; if (remove > 0) return 'warning'; return 'allow'; } case 'matrix-risk': { const threats = data.threats || data.findings || []; const hasCritical = threats.some(function (t) { return /crit|kritisk/i.test(t.severity || ''); }); if (hasCritical) return 'block'; if (threats.length) return 'warning'; return 'n-a'; } default: return 'n-a'; } } const KEY_STATS_CONFIG = { 'findings': function (d) { const fs = d.findings || []; const crit = fs.filter(function (f) { return /crit|kritisk/i.test(f.severity || ''); }).length; const high = fs.filter(function (f) { return /^high|^høy/i.test(f.severity || ''); }).length; return [ { label: 'TOTALT', value: fs.length }, { label: 'KRITISK', value: crit, modifier: crit > 0 ? 'critical' : null }, { label: 'HØY', value: high, modifier: high > 0 ? 'high' : null } ]; }, 'findings-grade': function (d) { const out = []; if (d.grade) out.push({ label: 'GRADE', value: String(d.grade).toUpperCase(), modifier: /a|b/i.test(d.grade) ? 'low' : (/c|d/i.test(d.grade) ? 'medium' : 'critical') }); if (d.score != null) out.push({ label: 'SCORE', value: d.score }); if (d.findings) out.push({ label: 'FUNN', value: d.findings.length }); return out; }, 'risk-score-meter': function (d) { const out = []; if (d.risk_score != null) { const mod = d.risk_score >= 65 ? 'critical' : (d.risk_score >= 15 ? 'medium' : 'low'); out.push({ label: 'RISK SCORE', value: d.risk_score, modifier: mod }); } if (d.riskBand) out.push({ label: 'BAND', value: d.riskBand }); return out; }, 'red-team-results': function (d) { return [ { label: 'TOTALT', value: d.total || 0 }, { label: 'PASS', value: d.pass_count || 0, modifier: 'low' }, { label: 'FAIL', value: d.fail_count || 0, modifier: (d.fail_count > 0 ? 'critical' : null) } ]; }, 'dashboard-fleet': function (d) { return [ { label: 'PROSJEKTER', value: (d.projects || []).length }, { label: 'MASKINKLASSE', value: String(d.machine_grade || 'n/a').toUpperCase() }, { label: 'SVAKEST', value: d.weakest_link || '–' } ]; }, 'posture-cards': function (d) { const cats = d.categories || []; const pass = cats.filter(function (c) { return c.status === 'PASS'; }).length; const fail = cats.filter(function (c) { return c.status === 'FAIL'; }).length; return [ { label: 'GRADE', value: String(d.grade || '?').toUpperCase(), modifier: /a|b/i.test(d.grade) ? 'low' : (/c|d/i.test(d.grade) ? 'medium' : 'critical') }, { label: 'PASS', value: pass, modifier: 'low' }, { label: 'FAIL', value: fail, modifier: fail > 0 ? 'critical' : 'low' } ]; }, 'diff-report': function (d) { const newCount = (d['new'] || []).length; const unchangedCount = (d.unchanged || []).length; return [ { label: 'NÅ-GRADE', value: String(d.current_grade || '?').toUpperCase() }, { label: 'AKSJONER', value: newCount, modifier: newCount > 0 ? 'medium' : 'low' }, { label: 'SKIPPED', value: unchangedCount } ]; }, 'kanban-buckets': function (d) { const auto = (d.buckets && d.buckets.auto) || d.auto || []; const semi = (d.buckets && (d.buckets['semi-auto'] || d.buckets.semi_auto)) || d['semi-auto'] || d.semi_auto || []; const manual = (d.buckets && d.buckets.manual) || d.manual || []; return [ { label: 'AUTO', value: auto.length, modifier: 'low' }, { label: 'SEMI-AUTO', value: semi.length, modifier: semi.length ? 'medium' : 'low' }, { label: 'MANUAL', value: manual.length, modifier: manual.length ? 'high' : 'low' } ]; }, 'matrix-risk': function (d) { const threats = d.threats || d.findings || []; const cells = d.matrix_cells || []; const maxScore = cells.length ? Math.max.apply(null, cells.map(function (c) { return Number(c.score) || 0; })) : 0; const sev = maxScore >= 16 ? 'critical' : maxScore >= 9 ? 'high' : maxScore >= 4 ? 'medium' : 'low'; return [ { label: 'TRUSLER', value: threats.length }, { label: 'MAKS SCORE', value: maxScore || '–', modifier: sev }, { label: 'CELLER', value: cells.length } ]; } }; function inferKeyStats(data, archetype) { if (!data) return []; if (Array.isArray(data.keyStats)) return data.keyStats; const fn = KEY_STATS_CONFIG[archetype]; if (typeof fn !== 'function') return []; try { const out = fn(data); return Array.isArray(out) ? out : []; } catch (e) { return []; } } // ============================================================ // PARSER HELPERS (markdown → struktur) // ============================================================ function parseTableRow(line) { const inner = line.replace(/^\|/, '').replace(/\|$/, ''); return inner.split('|').map(function (c) { return c.trim(); }); } function parseTable(md, anchorRegex) { if (typeof md !== 'string') return null; let body = md; if (anchorRegex) { const m = anchorRegex.exec(md); if (!m) return null; body = md.slice(m.index + m[0].length); } const lines = body.split(/\r?\n/); for (let i = 0; i < lines.length - 1; i++) { const line = lines[i].trim(); const next = (lines[i + 1] || '').trim(); if (line.indexOf('|') === 0 && /^\|[\s\-:|]+\|$/.test(next)) { const headers = parseTableRow(line); const rows = []; for (let j = i + 2; j < lines.length; j++) { const rowLine = lines[j].trim(); if (rowLine.indexOf('|') !== 0) break; const cells = parseTableRow(rowLine); if (cells.length === 0) break; const row = {}; for (let k = 0; k < headers.length; k++) { row[headers[k]] = (cells[k] || '').trim(); } rows.push(row); } return { headers: headers, rows: rows }; } } return null; } function parseAllTables(md, anchorRegex) { // Returnerer alle tabeller etter (valgfri) anchor til neste H2 // Brukt av parsers som har flere severity-tabeller (### Critical, ### High osv). if (typeof md !== 'string') return []; let body = md; if (anchorRegex) { const m = anchorRegex.exec(md); if (!m) return []; body = md.slice(m.index + m[0].length); } const out = []; const lines = body.split(/\r?\n/); let i = 0; while (i < lines.length - 1) { const line = lines[i].trim(); const next = (lines[i + 1] || '').trim(); if (line.indexOf('|') === 0 && /^\|[\s\-:|]+\|$/.test(next)) { const headers = parseTableRow(line); const rows = []; let j = i + 2; for (; j < lines.length; j++) { const rowLine = lines[j].trim(); if (rowLine.indexOf('|') !== 0) break; const cells = parseTableRow(rowLine); if (cells.length === 0) break; const row = {}; for (let k = 0; k < headers.length; k++) { row[headers[k]] = (cells[k] || '').trim(); } rows.push(row); } out.push({ headers: headers, rows: rows }); i = j; } else { i++; } } return out; } function parseSections(md) { if (typeof md !== 'string') return []; const sections = []; const lines = md.split(/\r?\n/); let current = null; for (let i = 0; i < lines.length; i++) { const line = lines[i]; const m = /^##\s+(.+)$/.exec(line); if (m && line.charAt(2) === ' ') { if (current) sections.push(current); current = { heading: m[1].trim(), body: '' }; } else if (current) { current.body += (current.body ? '\n' : '') + line; } } if (current) sections.push(current); return sections.map(function (s) { return { heading: s.heading, body: s.body.trim() }; }); } function extractField(md, label) { if (typeof md !== 'string') return null; const escaped = label.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // Markdown-tabellrader: | **Label** | value | OR | Label | value | const tblRe = new RegExp('^\\s*\\|\\s*\\**\\s*' + escaped + '\\s*\\**\\s*\\|\\s*([^|]+?)\\s*\\|', 'mi'); const tbl = tblRe.exec(md); if (tbl) return tbl[1].trim(); // **Label:** value OR Label: value const re = new RegExp('^\\s*\\**\\s*' + escaped + '\\**\\s*:\\s*(.+)$', 'mi'); const m = re.exec(md); return m ? m[1].trim() : null; } function intOrZero(s) { if (s == null) return 0; if (typeof s !== 'string') s = String(s); const v = parseInt(s.replace(/[^\d-]/g, ''), 10); return isNaN(v) ? 0 : v; } function emptyInput(md) { return !md || typeof md !== 'string' || !md.trim(); } function normalizeSeverity(s) { const v = String(s || '').toLowerCase().trim(); if (/crit|kritisk/.test(v)) return 'critical'; if (/^high|^høy/.test(v)) return 'high'; if (/medium|moderat/.test(v)) return 'medium'; if (/^low|^lav/.test(v)) return 'low'; if (/^info|^observ/.test(v)) return 'info'; return v || 'info'; } function normalizeVerdictText(s) { const v = String(s || '').toUpperCase().trim(); if (/BLOCK|BLOKK|UNDERKJENT|FAIL/.test(v)) return 'block'; if (/GO[-\s]WITH[-\s]CONDITIONS|CONDITIONAL|BETINGET/.test(v)) return 'go-with-conditions'; if (/WARNING|ADVARSEL/.test(v)) return 'warning'; if (/ALLOW|TILLATT|GO|PASS|GODKJENT/.test(v)) return 'allow'; if (/N\/?A|IKKE/.test(v)) return 'n-a'; return ''; } function gradeFromText(s) { const m = /\b([A-F])\b/.exec(String(s || '').toUpperCase()); return m ? m[1] : null; } // Hjelper: parse Risk Dashboard-tabellen (fellesmønster) function parseRiskDashboard(md) { const out = {}; const score = extractField(md, 'Risk Score'); if (score) { const m = /(\d+)\s*\/\s*100/.exec(score); if (m) out.risk_score = parseInt(m[1], 10); else out.risk_score = intOrZero(score); } const band = extractField(md, 'Risk Band'); if (band) out.riskBand = band; const grade = extractField(md, 'Grade'); if (grade) out.grade = gradeFromText(grade); const verdict = extractField(md, 'Verdict'); if (verdict) { const norm = normalizeVerdictText(verdict); if (norm) out.verdict = norm; } const rationale = extractField(md, 'Verdict rationale'); if (rationale) out.verdict_rationale = rationale; // Severity counts-tabell (Severity | Count) — etter Risk Dashboard-headeren const sevTbl = parseTable(md, /\|\s*Severity\s*\|\s*Count/i); if (sevTbl && sevTbl.rows.length) { const counts = { critical: 0, high: 0, medium: 0, low: 0, info: 0, total: 0 }; sevTbl.rows.forEach(function (row) { const label = String(row[sevTbl.headers[0]] || '').toLowerCase().replace(/[*\s]/g, ''); const n = intOrZero(row[sevTbl.headers[1]] || '0'); if (/^critical|^kritisk/.test(label)) counts.critical = n; else if (/^high|^høy/.test(label)) counts.high = n; else if (/^medium/.test(label)) counts.medium = n; else if (/^low|^lav/.test(label)) counts.low = n; else if (/^info/.test(label)) counts.info = n; else if (/^total/.test(label)) counts.total = n; }); if (!counts.total) { counts.total = counts.critical + counts.high + counts.medium + counts.low + counts.info; } out.severity_counts = counts; } return out; } // Hjelper: parse alle findings-tabeller (### Critical / High / Medium / Low / Info) function parseFindingsTables(md) { const findings = []; // Match alle ### -headere innenfor ## Findings const findingsSection = parseSections(md).find(function (s) { return /^findings$/i.test(s.heading) || /^funn$/i.test(s.heading); }); if (!findingsSection) return findings; const body = findingsSection.body; // Splitt på ### -headere const subRe = /^###\s+(.+)$/gm; const matches = []; let m; while ((m = subRe.exec(body)) !== null) { matches.push({ severity: m[1].trim(), index: m.index }); } for (let i = 0; i < matches.length; i++) { const start = matches[i].index; const end = i + 1 < matches.length ? matches[i + 1].index : body.length; const chunk = body.slice(start, end); const tbl = parseTable(chunk); if (!tbl || !tbl.rows.length) continue; const sev = matches[i].severity.split(/[\s/,]/)[0]; // "Low / Info" → "Low" tbl.rows.forEach(function (row) { const idKey = tbl.headers[0]; const catKey = tbl.headers.find(function (h) { return /category|kategori/i.test(h); }); const fileKey = tbl.headers.find(function (h) { return /file|fil/i.test(h); }); const lineKey = tbl.headers.find(function (h) { return /^line$|linje/i.test(h); }); const descKey = tbl.headers.find(function (h) { return /description|beskriv/i.test(h); }); const owaspKey = tbl.headers.find(function (h) { return /owasp/i.test(h); }); findings.push({ id: row[idKey] || '', severity: normalizeSeverity(sev), category: catKey ? row[catKey] : '', file: fileKey ? row[fileKey] : '', line: lineKey ? row[lineKey] : '', description: descKey ? row[descKey] : '', owasp: owaspKey ? row[owaspKey] : '' }); }); } return findings; } function parseRecommendations(md) { const sec = parseSections(md).find(function (s) { return /^recommendations$|^anbefalinger$/i.test(s.heading); }); if (!sec) return []; const out = []; const lines = sec.body.split(/\r?\n/); lines.forEach(function (line) { const m = /^\s*(?:\d+\.|[-*])\s+(.+)$/.exec(line); if (m) out.push(m[1].replace(/^\*\*[^*]+\*\*[:]?\s*/, '').trim()); }); return out; } function safeOk(parser) { return function (md) { if (emptyInput(md)) return { ok: false, errors: [{ section: 'input', reason: 'Tom input' }] }; try { return parser(md); } catch (e) { return { ok: false, errors: [{ section: 'parser', reason: String(e && e.message || e) }] }; } }; } // ============================================================ // parseNarrativeAudit — v7.1.1 Narrative Audit-blokk // ============================================================ /** * Parse v7.1.1 Narrative Audit-blokk: "**Suppressed signals:** N (reason1: count examples, ...)" * Returnerer { count, by_category: {reason: count, ...}, examples: {reason: text, ...} } eller null. */ function parseNarrativeAudit(md) { const m = String(md || '').match(/Suppressed signals:\s*\*?\*?\s*(\d+)\s*(?:\(([^)]+)\))?/i); if (!m) return null; const count = Number(m[1]) || 0; const by_category = {}; const examples = {}; if (m[2]) { m[2].split(',').forEach(function (part) { const seg = part.trim(); const colonIdx = seg.indexOf(':'); if (colonIdx < 0) { by_category[seg] = (by_category[seg] || 0) + 1; return; } const reason = seg.slice(0, colonIdx).trim(); const rest = seg.slice(colonIdx + 1).trim(); const cm = rest.match(/^(\d+)\s+(.*)$/); if (cm) { by_category[reason] = (by_category[reason] || 0) + (Number(cm[1]) || 1); examples[reason] = cm[2].trim(); } else { by_category[reason] = (by_category[reason] || 0) + 1; examples[reason] = rest; } }); } return { count: count, by_category: by_category, examples: examples }; } // ============================================================ // 10 PARSERS — én per høy-prio kommando. // Returner { ok: true, data: { ...domain-specific } } eller // { ok: false, errors: [{ section, reason }] } // ============================================================ const parseScan = safeOk(function (md) { const dash = parseRiskDashboard(md); const findings = parseFindingsTables(md); const owaspTbl = parseTable(md, /##\s+OWASP\s+Categorization/i); const owasp = owaspTbl ? owaspTbl.rows.map(function (row) { return { category: row[owaspTbl.headers[0]] || '', findings: intOrZero(row[owaspTbl.headers[1]] || '0'), max_severity: normalizeSeverity(row[owaspTbl.headers[2]] || ''), scanners: row[owaspTbl.headers[3]] || '' }; }) : []; const supplyTbl = parseTable(md, /##\s+Supply\s+Chain\s+Assessment/i); const supply_chain = supplyTbl ? supplyTbl.rows.map(function (row) { return { component: row[supplyTbl.headers[0]] || '', type: row[supplyTbl.headers[1]] || '', source: row[supplyTbl.headers[2]] || '', trust: row[supplyTbl.headers[3]] || '', notes: row[supplyTbl.headers[4]] || '' }; }) : []; const exec = parseSections(md).find(function (s) { return /^executive\s+summary/i.test(s.heading); }); const suppressed = parseNarrativeAudit(md); return { ok: true, data: Object.assign({}, dash, { findings: findings, owasp: owasp, supply_chain: supply_chain, executive_summary: exec ? exec.body.split(/\n##/)[0].trim() : '', narrative_audit: suppressed ? { suppressed_findings: suppressed } : undefined, recommendations: parseRecommendations(md) }) }; }); const parseDeepScan = safeOk(function (md) { const dash = parseRiskDashboard(md); // Per-scanner-blokker: ### N. Name (TAG) — Status / Files / Findings / Time const scannerBlocks = []; const scannerRe = /^###\s+\d+\.\s+(.+?)\s+\(([A-Z]{2,4})\)\s*$([\s\S]*?)(?=^###\s+\d+\.|^##\s+|\Z)/gm; let m; while ((m = scannerRe.exec(md)) !== null) { const name = m[1].trim(); const tag = m[2].trim(); const body = m[3] || ''; const statusMatch = /\*\*Status:\*\*\s*([^|]+?)\s*\|/i.exec(body); const filesMatch = /\*\*Files:\*\*\s*([^|]+?)\s*\|/i.exec(body); const findingsMatch = /\*\*Findings:\*\*\s*(\d+)/i.exec(body); const timeMatch = /\*\*Time:\*\*\s*(\d+)/i.exec(body); const detailLines = body.split(/\r?\n/).filter(function (l) { return l.trim() && !/^\*\*Status:\*\*/i.test(l.trim()); }); scannerBlocks.push({ tag: tag, name: name, status: statusMatch ? statusMatch[1].trim() : '', files: filesMatch ? filesMatch[1].trim() : '', findings: findingsMatch ? parseInt(findingsMatch[1], 10) : 0, duration_ms: timeMatch ? parseInt(timeMatch[1], 10) : 0, details: detailLines.join(' ').trim() }); } // Scanner Risk Matrix const matrixTbl = parseTable(md, /##\s+Scanner\s+Risk\s+Matrix/i); const scanner_matrix = matrixTbl ? matrixTbl.rows .filter(function (row) { return !/^\s*\*\*total/i.test(row[matrixTbl.headers[0]] || ''); }) .map(function (row) { return { scanner: row[matrixTbl.headers[0]] || '', critical: intOrZero(row[matrixTbl.headers[1]] || '0'), high: intOrZero(row[matrixTbl.headers[2]] || '0'), medium: intOrZero(row[matrixTbl.headers[3]] || '0'), low: intOrZero(row[matrixTbl.headers[4]] || '0'), info: intOrZero(row[matrixTbl.headers[5]] || '0') }; }) : []; const exec = parseSections(md).find(function (s) { return /^executive\s+summary/i.test(s.heading); }); const suppressed = parseNarrativeAudit(md); return { ok: true, data: Object.assign({}, dash, { scanners: scannerBlocks, scanner_matrix: scanner_matrix, score: dash.risk_score, findings: parseFindingsTables(md), executive_summary: exec ? exec.body.split(/\n##/)[0].trim() : '', narrative_audit: suppressed ? { suppressed_findings: suppressed } : undefined, recommendations: parseRecommendations(md) }) }; }); const parsePluginAudit = safeOk(function (md) { const dash = parseRiskDashboard(md); // Plugin Metadata-tabell const metaTbl = parseTable(md, /##\s+Plugin\s+Metadata/i); const plugin_metadata = {}; if (metaTbl) { metaTbl.rows.forEach(function (row) { const k = String(row[metaTbl.headers[0]] || '').replace(/\*+/g, '').trim().toLowerCase().replace(/\s+/g, '_'); plugin_metadata[k] = row[metaTbl.headers[1]] || ''; }); } // Component Inventory const compTbl = parseTable(md, /##\s+Component\s+Inventory/i); const components = compTbl ? compTbl.rows.map(function (row) { return { component: row[compTbl.headers[0]] || '', count: intOrZero(row[compTbl.headers[1]] || '0'), notes: row[compTbl.headers[2]] || '' }; }) : []; // Permission Matrix const permTbl = parseTable(md, /##\s+Permission\s+Matrix/i); const permissions = permTbl ? permTbl.rows.map(function (row) { return { tool: row[permTbl.headers[0]] || '', required_by: row[permTbl.headers[1]] || '', justified: row[permTbl.headers[2]] || '' }; }) : []; // Trust Verdict-seksjon const sections = parseSections(md); const trustSec = sections.find(function (s) { return /trust\s+verdict/i.test(s.heading); }); let trust_verdict_text = ''; let trust_verdict_value = ''; if (trustSec) { trust_verdict_text = trustSec.body; const vmatch = /\*\*Verdict:\*\*\s*([A-Z\-]+)/i.exec(trustSec.body); if (vmatch) trust_verdict_value = normalizeVerdictText(vmatch[1]); } return { ok: true, data: Object.assign({}, dash, { plugin_metadata: plugin_metadata, components: components, permissions: permissions, trust_verdict_text: trust_verdict_text, trust_verdict: trust_verdict_value || dash.verdict || '', findings: parseFindingsTables(md), recommendations: parseRecommendations(md) }) }; }); const parseMcpAudit = safeOk(function (md) { const dash = parseRiskDashboard(md); // MCP Landscape-tabell const landTbl = parseTable(md, /##\s+MCP\s+Landscape/i); const mcp_servers = landTbl ? landTbl.rows.map(function (row) { return { server: row[landTbl.headers[0]] || '', type: row[landTbl.headers[1]] || '', trust: row[landTbl.headers[2]] || '', tools: intOrZero(row[landTbl.headers[3]] || '0'), active: /^yes|^aktiv|^ja/i.test(String(row[landTbl.headers[4]] || '')) }; }) : []; // Per-Server-Analysis er fritekst-seksjoner med ### server-name const sections = parseSections(md); const perServerSec = sections.find(function (s) { return /per-server\s+analysis/i.test(s.heading); }); const per_server = []; if (perServerSec) { const subRe = /^###\s+(.+)$/gm; const body = perServerSec.body; const heads = []; let m2; while ((m2 = subRe.exec(body)) !== null) heads.push({ name: m2[1].trim(), index: m2.index }); for (let i = 0; i < heads.length; i++) { const start = heads[i].index; const end = i + 1 < heads.length ? heads[i + 1].index : body.length; per_server.push({ name: heads[i].name.replace(/\s*\([^)]+\)\s*$/, ''), note: heads[i].name.match(/\(([^)]+)\)/) ? heads[i].name.match(/\(([^)]+)\)/)[1] : '', body: body.slice(start, end).replace(/^###[^\n]+\n+/, '').trim() }); } } // Keep / Review / Remove buckets const krrTbl = parseTable(md, /##\s+Keep\s*\/\s*Review\s*\/\s*Remove/i); const buckets = { keep: [], review: [], remove: [] }; if (krrTbl) { krrTbl.rows.forEach(function (row) { const decision = String(row[krrTbl.headers[0]] || '').toLowerCase().trim(); const item = { server: row[krrTbl.headers[1]] || '', reason: row[krrTbl.headers[2]] || '' }; if (/^keep/.test(decision)) buckets.keep.push(item); else if (/^review/.test(decision)) buckets.review.push(item); else if (/^remove/.test(decision)) buckets.remove.push(item); }); } // Findings: tabeller under ## Findings const findings = []; const findingsSec = sections.find(function (s) { return /^findings$/i.test(s.heading); }); if (findingsSec) { const subRe = /^###\s+(.+)$/gm; const body = findingsSec.body; const heads = []; let m3; while ((m3 = subRe.exec(body)) !== null) heads.push({ severity: m3[1].trim(), index: m3.index }); for (let i = 0; i < heads.length; i++) { const start = heads[i].index; const end = i + 1 < heads.length ? heads[i + 1].index : body.length; const chunk = body.slice(start, end); const tbl = parseTable(chunk); if (!tbl || !tbl.rows.length) continue; const sev = heads[i].severity.split(/[\s/,]/)[0]; tbl.rows.forEach(function (row) { const idKey = tbl.headers[0]; const serverKey = tbl.headers.find(function (h) { return /server/i.test(h); }); const descKey = tbl.headers.find(function (h) { return /description|beskriv/i.test(h); }); const owaspKey = tbl.headers.find(function (h) { return /owasp/i.test(h); }); findings.push({ id: row[idKey] || '', severity: normalizeSeverity(sev), server: serverKey ? row[serverKey] : '', description: descKey ? row[descKey] : '', owasp: owaspKey ? row[owaspKey] : '' }); }); } } return { ok: true, data: Object.assign({}, dash, { mcp_servers: mcp_servers, per_server: per_server, buckets: buckets, findings: findings, recommendations: parseRecommendations(md) }) }; }); const parseIdeScan = safeOk(function (md) { const dash = parseRiskDashboard(md); // Scan Coverage-tabell const covTbl = parseTable(md, /##\s+Scan\s+Coverage/i); const coverage = covTbl ? covTbl.rows .filter(function (row) { return !/^\s*\*\*total/i.test(row[covTbl.headers[0]] || ''); }) .map(function (row) { return { ide: row[covTbl.headers[0]] || '', extensions: intOrZero(row[covTbl.headers[1]] || '0'), findings: intOrZero(row[covTbl.headers[2]] || '0') }; }) : []; // Findings: under ### Critical/High/Medium/Low/Info — extension+IDE-spesifikk const findings = []; const sections = parseSections(md); const findingsSec = sections.find(function (s) { return /^findings$/i.test(s.heading); }); if (findingsSec) { const body = findingsSec.body; const subRe = /^###\s+(.+)$/gm; const heads = []; let m; while ((m = subRe.exec(body)) !== null) heads.push({ severity: m[1].trim(), index: m.index }); for (let i = 0; i < heads.length; i++) { const start = heads[i].index; const end = i + 1 < heads.length ? heads[i + 1].index : body.length; const chunk = body.slice(start, end); const tbl = parseTable(chunk); if (!tbl || !tbl.rows.length) continue; const sev = heads[i].severity.split(/[\s/,]/)[0]; tbl.rows.forEach(function (row) { const idKey = tbl.headers[0]; const extKey = tbl.headers.find(function (h) { return /extension/i.test(h); }); const ideKey = tbl.headers.find(function (h) { return /^ide$/i.test(h); }); const descKey = tbl.headers.find(function (h) { return /description|beskriv/i.test(h); }); const owaspKey = tbl.headers.find(function (h) { return /owasp/i.test(h); }); findings.push({ id: row[idKey] || '', severity: normalizeSeverity(sev), extension: extKey ? row[extKey] : '', ide: ideKey ? row[ideKey] : '', description: descKey ? row[descKey] : '', owasp: owaspKey ? row[owaspKey] : '' }); }); } } return { ok: true, data: Object.assign({}, dash, { coverage: coverage, findings: findings, recommendations: parseRecommendations(md) }) }; }); const parsePosture = safeOk(function (md) { const dash = parseRiskDashboard(md); // Overall Score-seksjon: "**N / M categories covered (Grade X)**" const overallSec = parseSections(md).find(function (s) { return /^overall\s+score/i.test(s.heading); }); let posture_score = null; let posture_applicable = null; if (overallSec) { const m = /\*\*\s*(\d+)\s*\/\s*(\d+)\s+categories/i.exec(overallSec.body); if (m) { posture_score = parseInt(m[1], 10); posture_applicable = parseInt(m[2], 10); } } // Category Scorecard-tabell const catTbl = parseTable(md, /##\s+Category\s+Scorecard/i); const categories = catTbl ? catTbl.rows.map(function (row) { const status = String(row[catTbl.headers.find(function (h) { return /status/i.test(h); }) || catTbl.headers[2]] || '').toUpperCase().trim(); return { num: intOrZero(row[catTbl.headers[0]] || '0'), name: row[catTbl.headers[1]] || '', status: status, findings: intOrZero(row[catTbl.headers[3]] || '0') }; }) : []; // Top findings under ## Top Findings (med ### severity-grupper) const findings = []; const sections = parseSections(md); const topSec = sections.find(function (s) { return /^top\s+findings/i.test(s.heading); }); if (topSec) { const body = topSec.body; const subRe = /^###\s+(.+)$/gm; const heads = []; let m; while ((m = subRe.exec(body)) !== null) heads.push({ severity: m[1].trim(), index: m.index }); for (let i = 0; i < heads.length; i++) { const start = heads[i].index; const end = i + 1 < heads.length ? heads[i + 1].index : body.length; const chunk = body.slice(start, end); const tbl = parseTable(chunk); if (!tbl || !tbl.rows.length) continue; tbl.rows.forEach(function (row) { findings.push({ id: row[tbl.headers[0]] || '', severity: normalizeSeverity(heads[i].severity), category: row[tbl.headers[1]] || '', file: row[tbl.headers[2]] || '', description: row[tbl.headers[3]] || '' }); }); } } // Quick Wins const quickSec = sections.find(function (s) { return /^quick\s+wins/i.test(s.heading); }); const quick_wins = quickSec ? quickSec.body.split(/\r?\n/).map(function (l) { const m = /^\s*\d+\.\s+(.+)$/.exec(l); return m ? m[1].replace(/^\*\*[^*]+\*\*\s*[—-]?\s*/, '').trim() : null; }).filter(Boolean) : []; return { ok: true, data: Object.assign({}, dash, { score: posture_score != null ? posture_score : dash.risk_score, posture_score: posture_score, posture_applicable: posture_applicable, categories: categories, findings: findings, quick_wins: quick_wins, recommendations: parseRecommendations(md) }) }; }); const parseAudit = safeOk(function (md) { const dash = parseRiskDashboard(md); // Radar Axes-tabell const radarTbl = parseTable(md, /##\s+Radar\s+Axes/i); const radar_axes = radarTbl ? radarTbl.rows.map(function (row) { return { name: row[radarTbl.headers[0]] || '', score: intOrZero(row[radarTbl.headers[1]] || '0') }; }) : []; // Category Assessment: ### Category N — Name + status-tabell const sections = parseSections(md); const catAssessSec = sections.find(function (s) { return /^category\s+assessment/i.test(s.heading); }); const categories = []; if (catAssessSec) { const body = catAssessSec.body; const subRe = /^###\s+Category\s+(\d+)\s+[—-]\s+(.+)$/gm; const heads = []; let m; while ((m = subRe.exec(body)) !== null) { heads.push({ num: parseInt(m[1], 10), name: m[2].trim(), index: m.index }); } for (let i = 0; i < heads.length; i++) { const start = heads[i].index; const end = i + 1 < heads.length ? heads[i + 1].index : body.length; const chunk = body.slice(start, end); const statusMatch = /\|\s*Status\s*\|\s*([A-Z\-]+)\s*\|/i.exec(chunk); categories.push({ num: heads[i].num, name: heads[i].name, status: statusMatch ? statusMatch[1].trim().toUpperCase() : '' }); } } // Risk Matrix (L×I) const riskTbl = parseTable(md, /##\s+Risk\s+Matrix/i); const risk_matrix = riskTbl ? riskTbl.rows.map(function (row) { return { category: row[riskTbl.headers[0]] || '', likelihood: intOrZero(row[riskTbl.headers[1]] || '0'), impact: intOrZero(row[riskTbl.headers[2]] || '0'), score: intOrZero(row[riskTbl.headers[3]] || '0') }; }) : []; // Action Plan: ### IMMEDIATE / HIGH / MEDIUM const actionSec = sections.find(function (s) { return /^action\s+plan/i.test(s.heading); }); const action_plan = { immediate: [], high: [], medium: [] }; if (actionSec) { const body = actionSec.body; const subRe = /^###\s+(IMMEDIATE|HIGH|MEDIUM)/gmi; const heads = []; let m; while ((m = subRe.exec(body)) !== null) heads.push({ tier: m[1].toLowerCase(), index: m.index }); for (let i = 0; i < heads.length; i++) { const start = heads[i].index; const end = i + 1 < heads.length ? heads[i + 1].index : body.length; const chunk = body.slice(start, end); chunk.split(/\r?\n/).forEach(function (line) { const mm = /^\s*\d+\.\s+(.+)$/.exec(line); if (mm) action_plan[heads[i].tier].push(mm[1].trim()); }); } } const exec = sections.find(function (s) { return /^executive\s+summary/i.test(s.heading); }); return { ok: true, data: Object.assign({}, dash, { score: dash.risk_score, radar_axes: radar_axes, categories: categories, risk_matrix: risk_matrix, action_plan: action_plan, findings: parseFindingsTables(md), executive_summary: exec ? exec.body.trim() : '' }) }; }); const parseDashboard = safeOk(function (md) { const dash = parseRiskDashboard(md); // Header-Risk Dashboard-tabell har egne felter const machine_grade = gradeFromText(extractField(md, 'Machine Grade') || ''); const projects_scanned = intOrZero(extractField(md, 'Projects Scanned') || '0'); const total_findings = intOrZero(extractField(md, 'Total Findings') || '0'); const cache = extractField(md, 'Cache') || ''; // Project Overview-tabell const projTbl = parseTable(md, /##\s+Project\s+Overview/i); const projects = projTbl ? projTbl.rows.map(function (row) { return { name: row[projTbl.headers[0]] || '', grade: gradeFromText(row[projTbl.headers[1]] || ''), risk: intOrZero(row[projTbl.headers[2]] || '0'), worst_category: row[projTbl.headers[3]] || '', findings: intOrZero(row[projTbl.headers[4]] || '0') }; }) : []; // Trend-tabell const trendTbl = parseTable(md, /##\s+Trend/i); const trends = trendTbl ? trendTbl.rows.map(function (row) { return { name: row[trendTbl.headers[0]] || '', trend: String(row[trendTbl.headers[1]] || '').toLowerCase().trim(), d_risk: row[trendTbl.headers[2]] || '', d_findings: row[trendTbl.headers[3]] || '' }; }) : []; // Errors-seksjon const errSec = parseSections(md).find(function (s) { return /^errors/i.test(s.heading); }); let errors = []; if (errSec) { const errTbl = parseTable(errSec.body); if (errTbl) { errors = errTbl.rows.map(function (row) { return { project: row[errTbl.headers[0]] || '', error: row[errTbl.headers[errTbl.headers.length - 1]] || '' }; }); } } // Weakest link = første prosjekt sortert worst-first (allerede sortert i fixture) const weakest = projects.length ? projects[0].name : ''; return { ok: true, data: Object.assign({}, dash, { machine_grade: machine_grade, projects_scanned: projects_scanned, total_findings: total_findings, cache: cache, projects: projects, trends: trends, errors: errors, weakest_link: weakest, recommendations: parseRecommendations(md) }) }; }); const parseHarden = safeOk(function (md) { const current_grade = gradeFromText(extractField(md, 'Current Grade') || ''); const project_type = extractField(md, 'Project Type') || ''; const recRaw = extractField(md, 'Recommendations') || ''; let actionable = 0, total = 0; const recMatch = /(\d+)\s*\/\s*(\d+)/.exec(recRaw); if (recMatch) { actionable = parseInt(recMatch[1], 10); total = parseInt(recMatch[2], 10); } const mode = extractField(md, 'Mode') || 'dry-run'; // Recommendations: ### N. Category — File med Action / Content preview const sections = parseSections(md); const recSec = sections.find(function (s) { return /^recommendations$/i.test(s.heading); }); const recommendations = []; if (recSec) { const body = recSec.body; const subRe = /^###\s+(\d+)\.\s+(.+?)\s+[—-]\s+(.+)$/gm; const heads = []; let m; while ((m = subRe.exec(body)) !== null) { heads.push({ num: parseInt(m[1], 10), category: m[2].trim(), file: m[3].trim(), index: m.index }); } for (let i = 0; i < heads.length; i++) { const start = heads[i].index; const end = i + 1 < heads.length ? heads[i + 1].index : body.length; const chunk = body.slice(start, end); const actionMatch = /-\s+\*\*Action:\*\*\s*(.+)$/im.exec(chunk); const contentMatch = /-\s+\*\*Content preview:\*\*\s*([\s\S]*?)(?=\n-\s+\*\*|\n###|\n##|$)/i.exec(chunk); recommendations.push({ num: heads[i].num, category: heads[i].category, file: heads[i].file, action: actionMatch ? actionMatch[1].trim() : '', content_preview: contentMatch ? contentMatch[1].trim() : '' }); } } // Diff Summary-tabell const diffTbl = parseTable(md, /##\s+Diff\s+Summary/i); const diff_summary = diffTbl ? diffTbl.rows .filter(function (row) { return !/^\s*\*\*total/i.test(row[diffTbl.headers[0]] || ''); }) .map(function (row) { return { file: row[diffTbl.headers[0]] || '', action: row[diffTbl.headers[1]] || '', lines: row[diffTbl.headers[2]] || '' }; }) : []; // Map til diff-archetype: new = create, resolved = (none), unchanged = skipped const newItems = recommendations.filter(function (r) { return /create|append|merge/i.test(r.action); }); const skippedItems = recommendations.filter(function (r) { return /none|skip/i.test(r.action); }); return { ok: true, data: { current_grade: current_grade, project_type: project_type, actionable: actionable, total: total, mode: mode, recommendations: recommendations, diff_summary: diff_summary, 'new': newItems, unchanged: skippedItems, resolved: [], moved: [] } }; }); const parseRedTeam = safeOk(function (md) { const dash = parseRiskDashboard(md); const defenseRaw = extractField(md, 'Defense Score') || ''; const defense_score = intOrZero(defenseRaw); const total = intOrZero(extractField(md, 'Total Scenarios') || '0'); const pass_count = intOrZero(extractField(md, 'Pass') || '0'); const fail_count = intOrZero(extractField(md, 'Fail') || '0'); const adaptive = /^on/i.test(String(extractField(md, 'Adaptive Mode') || '')); // Per-Category Breakdown-tabell const catTbl = parseTable(md, /##\s+Per-Category\s+Breakdown/i); const categories = catTbl ? catTbl.rows.map(function (row) { return { category: row[catTbl.headers[0]] || '', pass: intOrZero(row[catTbl.headers[1]] || '0'), fail: intOrZero(row[catTbl.headers[2]] || '0'), coverage: row[catTbl.headers[3]] || '' }; }) : []; // Failed Scenarios med severity-grupper const sections = parseSections(md); const failSec = sections.find(function (s) { return /failed\s+scenarios/i.test(s.heading); }); const scenarios = []; if (failSec) { const body = failSec.body; const subRe = /^###\s+(.+)$/gm; const heads = []; let m; while ((m = subRe.exec(body)) !== null) heads.push({ severity: m[1].trim(), index: m.index }); for (let i = 0; i < heads.length; i++) { const start = heads[i].index; const end = i + 1 < heads.length ? heads[i + 1].index : body.length; const chunk = body.slice(start, end); const tbl = parseTable(chunk); if (!tbl || !tbl.rows.length) continue; tbl.rows.forEach(function (row) { scenarios.push({ id: row[tbl.headers[0]] || '', severity: normalizeSeverity(heads[i].severity), category: row[tbl.headers[1]] || '', payload_class: row[tbl.headers[2]] || '', reason: row[tbl.headers[3]] || '' }); }); } } // Test History const histTbl = parseTable(md, /##\s+Test\s+History/i); const history = histTbl ? histTbl.rows.map(function (row) { return { run: row[histTbl.headers[0]] || '', date: row[histTbl.headers[1]] || '', defense_score: intOrZero(row[histTbl.headers[2]] || '0'), delta: row[histTbl.headers[3]] || '' }; }) : []; return { ok: true, data: Object.assign({}, dash, { defense_score: defense_score, total: total, pass_count: pass_count, fail_count: fail_count, adaptive: adaptive, categories: categories, scenarios: scenarios, history: history, recommendations: parseRecommendations(md) }) }; }); // ============================================================ // FASE 3: 8 PARSERS — én per gjenstående produces_report-kommando. // Mønstre gjenbrukes fra Fase 2 (parseRiskDashboard + parseFindingsTables // + safeOk). Matrix-risk-parsing er kopiert fra ms-ai-architect. // ============================================================ const parseMcpInspect = safeOk(function (md) { const dash = parseRiskDashboard(md); const invTbl = parseTable(md, /##\s+Server\s+Inventory/i); const server_inventory = invTbl ? invTbl.rows.map(function (row) { return { server: row[invTbl.headers[0]] || '', transport: row[invTbl.headers[1]] || '', tools: intOrZero(row[invTbl.headers[2]] || '0'), status: row[invTbl.headers[3]] || '', connected: /^yes|^ja/i.test(String(row[invTbl.headers[4]] || '')) }; }) : []; const cpTbl = parseTable(md, /##\s+Codepoint\s+Reveal/i); const codepoints = cpTbl ? cpTbl.rows.map(function (row) { return { server: row[cpTbl.headers[0]] || '', tool: row[cpTbl.headers[1]] || '', codepoints: row[cpTbl.headers[2]] || '', risk: row[cpTbl.headers[3]] || '' }; }) : []; // Findings: merge default finding-shape med server-spesifikk meta const findingsRaw = parseFindingsTables(md); const findings = findingsRaw.map(function (f) { // Severity-tabellene bruker «Server» som kolonne → category=Server, file=tom return Object.assign({}, f, { server: f.category || f.file || '', file: f.file || '' }); }); return { ok: true, data: Object.assign({}, dash, { server_inventory: server_inventory, codepoints: codepoints, findings: findings, recommendations: parseRecommendations(md) }) }; }); const parseSupplyCheck = safeOk(function (md) { const dash = parseRiskDashboard(md); const ecoTbl = parseTable(md, /##\s+Ecosystem\s+Coverage/i); const ecosystems = ecoTbl ? ecoTbl.rows .filter(function (row) { return !/^\s*\*\*total/i.test(row[ecoTbl.headers[0]] || ''); }) .map(function (row) { return { ecosystem: row[ecoTbl.headers[0]] || '', lockfile: row[ecoTbl.headers[1]] || '', packages: intOrZero(row[ecoTbl.headers[2]] || '0'), osv_hits: intOrZero(row[ecoTbl.headers[3]] || '0'), typosquats: intOrZero(row[ecoTbl.headers[4]] || '0') }; }) : []; return { ok: true, data: Object.assign({}, dash, { ecosystems: ecosystems, findings: parseFindingsTables(md), recommendations: parseRecommendations(md) }) }; }); const parsePreDeploy = safeOk(function (md) { const dash = parseRiskDashboard(md); const lightTbl = parseTable(md, /##\s+Traffic\s+Light\s+Categories/i); const traffic_lights = lightTbl ? lightTbl.rows.map(function (row) { const status = String(row[lightTbl.headers[1]] || '').toUpperCase().trim(); return { category: row[lightTbl.headers[0]] || '', status: status, notes: row[lightTbl.headers[2]] || '' }; }) : []; const condSec = parseSections(md).find(function (s) { return /^conditions/i.test(s.heading); }); const conditions = condSec ? condSec.body.split(/\r?\n/).map(function (l) { const m = /^\s*\d+\.\s+(.+)$/.exec(l); return m ? m[1].replace(/^\*\*[^*]+\*\*\s*[—:-]?\s*/, '').trim() : null; }).filter(Boolean) : []; const apprTbl = parseTable(md, /##\s+Approvals/i); const approvals = apprTbl ? apprTbl.rows.map(function (row) { return { role: row[apprTbl.headers[0]] || '', approver: row[apprTbl.headers[1]] || '', date: row[apprTbl.headers[2]] || '', notes: row[apprTbl.headers[3]] || '' }; }) : []; return { ok: true, data: Object.assign({}, dash, { traffic_lights: traffic_lights, conditions: conditions, approvals: approvals, findings: parseFindingsTables(md), recommendations: parseRecommendations(md) }) }; }); const parseDiff = safeOk(function (md) { // NB: diff har egen severity-tabell (New/Resolved/Unchanged) — bruker // ikke parseRiskDashboard sin Count-kolonne. const dash = parseRiskDashboard(md); const current_grade = gradeFromText(extractField(md, 'Current Grade') || dash.grade || ''); const baseline_grade = gradeFromText(extractField(md, 'Baseline Grade') || ''); const baseline_date = extractField(md, 'Baseline') || ''; // Per-severity matrix (Severity | New | Resolved | Unchanged) const sevTbl = parseTable(md, /\|\s*Severity\s*\|\s*New\s*\|\s*Resolved/i); const severity_matrix = { critical: {}, high: {}, medium: {}, low: {}, info: {} }; if (sevTbl) { sevTbl.rows.forEach(function (row) { const label = String(row[sevTbl.headers[0]] || '').toLowerCase().replace(/[*\s]/g, ''); const key = /^crit/.test(label) ? 'critical' : /^high/.test(label) ? 'high' : /^medium/.test(label) ? 'medium' : /^low/.test(label) ? 'low' : /^info/.test(label) ? 'info' : null; if (!key) return; severity_matrix[key] = { 'new': intOrZero(row[sevTbl.headers[1]] || '0'), resolved: intOrZero(row[sevTbl.headers[2]] || '0'), unchanged: intOrZero(row[sevTbl.headers[3]] || '0') }; }); } // Per-bucket finding-tabeller const newTbl = parseTable(md, /##\s+New\s*\(?\d*\)?/i); const newItems = newTbl ? newTbl.rows.map(function (row) { const idKey = newTbl.headers[0]; const sevKey = newTbl.headers.find(function (h) { return /severity/i.test(h); }); const catKey = newTbl.headers.find(function (h) { return /category|kategori/i.test(h); }); const fileKey = newTbl.headers.find(function (h) { return /file|fil/i.test(h); }); const descKey = newTbl.headers.find(function (h) { return /description|beskriv/i.test(h); }); const owaspKey = newTbl.headers.find(function (h) { return /owasp/i.test(h); }); return { id: row[idKey] || '', severity: normalizeSeverity(sevKey ? row[sevKey] : ''), category: catKey ? row[catKey] : '', file: fileKey ? row[fileKey] : '', description: descKey ? row[descKey] : '', owasp: owaspKey ? row[owaspKey] : '' }; }) : []; const resolvedTbl = parseTable(md, /##\s+Resolved\s*\(?\d*\)?/i); const resolvedItems = resolvedTbl ? resolvedTbl.rows.map(function (row) { const idKey = resolvedTbl.headers[0]; const sevKey = resolvedTbl.headers.find(function (h) { return /severity/i.test(h); }); const catKey = resolvedTbl.headers.find(function (h) { return /category|kategori/i.test(h); }); const fileKey = resolvedTbl.headers.find(function (h) { return /file|fil/i.test(h); }); const resKey = resolvedTbl.headers.find(function (h) { return /resolution|løsning/i.test(h); }); return { id: row[idKey] || '', severity: normalizeSeverity(sevKey ? row[sevKey] : ''), category: catKey ? row[catKey] : '', file: fileKey ? row[fileKey] : '', resolution: resKey ? row[resKey] : '' }; }) : []; const unchangedTbl = parseTable(md, /##\s+Unchanged\s*\(?\d*\)?/i); const unchangedItems = unchangedTbl ? unchangedTbl.rows.map(function (row) { const idKey = unchangedTbl.headers[0]; const sevKey = unchangedTbl.headers.find(function (h) { return /severity/i.test(h); }); const catKey = unchangedTbl.headers.find(function (h) { return /category|kategori/i.test(h); }); const fileKey = unchangedTbl.headers.find(function (h) { return /file|fil/i.test(h); }); const noteKey = unchangedTbl.headers.find(function (h) { return /notes|note|merknad/i.test(h); }); return { id: row[idKey] || '', severity: normalizeSeverity(sevKey ? row[sevKey] : ''), category: catKey ? row[catKey] : '', file: fileKey ? row[fileKey] : '', notes: noteKey ? row[noteKey] : '' }; }) : []; const movedTbl = parseTable(md, /##\s+Moved\s*\(?\d*\)?/i); const movedItems = movedTbl ? movedTbl.rows.map(function (row) { return { id: row[movedTbl.headers[0]] || '', from: row[movedTbl.headers[1]] || '', to: row[movedTbl.headers[2]] || '' }; }) : []; return { ok: true, data: Object.assign({}, dash, { current_grade: current_grade, baseline_grade: baseline_grade, baseline_date: baseline_date, severity_matrix: severity_matrix, 'new': newItems, resolved: resolvedItems, unchanged: unchangedItems, moved: movedItems, recommendations: parseRecommendations(md) }) }; }); const parseWatch = safeOk(function (md) { const dash = parseRiskDashboard(md); const meterTbl = parseTable(md, /##\s+Live\s+Meter/i); const live_meter = {}; if (meterTbl) { meterTbl.rows.forEach(function (row) { const k = String(row[meterTbl.headers[0]] || '').replace(/\*+/g, '').trim().toLowerCase().replace(/\s+/g, '_'); live_meter[k] = row[meterTbl.headers[1]] || ''; }); } const histTbl = parseTable(md, /##\s+Recent\s+History/i); const history = histTbl ? histTbl.rows.map(function (row) { return { run: row[histTbl.headers[0]] || '', time: row[histTbl.headers[1]] || '', grade: gradeFromText(row[histTbl.headers[2]] || ''), risk_score: intOrZero(row[histTbl.headers[3]] || '0'), delta: row[histTbl.headers[4]] || '' }; }) : []; const notTbl = parseTable(md, /##\s+Notify\s+Events/i); const notify_events = notTbl ? notTbl.rows.map(function (row) { return { time: row[notTbl.headers[0]] || '', event: row[notTbl.headers[1]] || '', channel: row[notTbl.headers[2]] || '', status: row[notTbl.headers[3]] || '' }; }) : []; return { ok: true, data: Object.assign({}, dash, { live_meter: live_meter, history: history, notify_events: notify_events, findings: parseFindingsTables(md), recommendations: parseRecommendations(md), interval: extractField(md, 'Interval') || '', last_run: extractField(md, 'Last Run') || '' }) }; }); const parseRegistry = safeOk(function (md) { const dash = parseRiskDashboard(md); const statsTbl = parseTable(md, /##\s+Registry\s+Stats/i); const stats = {}; if (statsTbl) { statsTbl.rows.forEach(function (row) { const k = String(row[statsTbl.headers[0]] || '').replace(/\*+/g, '').trim().toLowerCase().replace(/\s+/g, '_'); stats[k] = row[statsTbl.headers[1]] || ''; }); } const sigTbl = parseTable(md, /##\s+Signature\s+Table/i); const signatures = sigTbl ? sigTbl.rows.map(function (row) { return { skill: row[sigTbl.headers[0]] || '', source: row[sigTbl.headers[1]] || '', fingerprint: row[sigTbl.headers[2]] || '', status: String(row[sigTbl.headers[3]] || '').toUpperCase().trim(), first_seen: row[sigTbl.headers[4]] || '' }; }) : []; // Findings — bruk renderFindingsBlock men med skill+file som meta const findingsRaw = parseFindingsTables(md); const findings = findingsRaw.map(function (f) { // Tabell-header: «Skill» som 3. kolonne maps til category i parseFindingsTables return Object.assign({}, f, { skill: f.category || '', file: f.file || '' }); }); return { ok: true, data: Object.assign({}, dash, { stats: stats, signatures: signatures, findings: findings, recommendations: parseRecommendations(md) }) }; }); const parseClean = safeOk(function (md) { const dash = parseRiskDashboard(md); const sumTbl = parseTable(md, /##\s+Remediation\s+Summary/i); const summary = {}; if (sumTbl) { sumTbl.rows .filter(function (row) { return !/^\s*\*\*total/i.test(row[sumTbl.headers[0]] || ''); }) .forEach(function (row) { const k = String(row[sumTbl.headers[0]] || '').replace(/\*+/g, '').trim().toLowerCase().replace(/[\s-]/g, '_'); summary[k] = { count: intOrZero(row[sumTbl.headers[1]] || '0'), action: row[sumTbl.headers[2]] || '' }; }); } // Per-bucket-tabeller (Auto / Semi-auto / Manual / Suppressed) const bucketParse = function (heading) { const tbl = parseTable(md, new RegExp('##\\s+' + heading + '\\s*$', 'mi')); if (!tbl || !tbl.rows.length) return []; return tbl.rows.map(function (row) { const idKey = tbl.headers[0]; const actKey = tbl.headers[1]; const descKey = tbl.headers[2]; return { id: row[idKey] || '', action: row[actKey] || '', description: row[descKey] || '' }; }); }; const buckets = { auto: bucketParse('Auto'), 'semi-auto': bucketParse('Semi-auto'), manual: bucketParse('Manual'), suppressed: bucketParse('Suppressed') }; return { ok: true, data: Object.assign({}, dash, { summary: summary, buckets: buckets, findings: parseFindingsTables(md), recommendations: parseRecommendations(md), mode: extractField(md, 'Mode') || '' }) }; }); const parseThreatModel = safeOk(function (md) { const dash = parseRiskDashboard(md); // Risikomatrise: Trussel | Sannsynlighet | Konsekvens | Score const matrixTbl = parseTable(md, /##\s+Risikomatrise/i); const matrix_cells = matrixTbl ? matrixTbl.rows.map(function (row) { const labelKey = matrixTbl.headers[0]; const sannKey = matrixTbl.headers.find(function (h) { return /sannsynlig/i.test(h); }) || matrixTbl.headers[1]; const konsKey = matrixTbl.headers.find(function (h) { return /konsekvens/i.test(h); }) || matrixTbl.headers[2]; const scoreKey = matrixTbl.headers.find(function (h) { return /score/i.test(h); }) || matrixTbl.headers[3]; return { label: row[labelKey] || '', prob: intOrZero(row[sannKey] || '0'), cons: intOrZero(row[konsKey] || '0'), score: intOrZero(row[scoreKey] || '0') }; }) : []; // Trusler: ID | Beskrivelse | Severity | Mitigation const threatsTbl = parseTable(md, /##\s+Trusler/i); const threats = threatsTbl ? threatsTbl.rows.map(function (row) { const idKey = threatsTbl.headers[0]; const descKey = threatsTbl.headers.find(function (h) { return /beskrivelse|description/i.test(h); }) || threatsTbl.headers[1]; const sevKey = threatsTbl.headers.find(function (h) { return /severity|alvorlighet/i.test(h); }); const mitKey = threatsTbl.headers.find(function (h) { return /tiltak|mitigation/i.test(h); }); return { id: row[idKey] || '', description: row[descKey] || '', severity: normalizeSeverity(sevKey ? row[sevKey] : ''), mitigation: mitKey ? row[mitKey] : '' }; }) : []; // STRIDE / MAESTRO Coverage const strideTbl = parseTable(md, /##\s+STRIDE\s+Coverage/i); const stride = strideTbl ? strideTbl.rows.map(function (row) { return { category: row[strideTbl.headers[0]] || '', count: intOrZero(row[strideTbl.headers[1]] || '0'), notes: row[strideTbl.headers[2]] || '' }; }) : []; const maestroTbl = parseTable(md, /##\s+MAESTRO\s+Coverage/i); const maestro = maestroTbl ? maestroTbl.rows.map(function (row) { return { layer: row[maestroTbl.headers[0]] || '', count: intOrZero(row[maestroTbl.headers[1]] || '0'), notes: row[maestroTbl.headers[2]] || '' }; }) : []; // Mitigation Roadmap const roadTbl = parseTable(md, /##\s+Mitigation\s+Roadmap/i); const roadmap = roadTbl ? roadTbl.rows.map(function (row) { return { priority: row[roadTbl.headers[0]] || '', threat_id: row[roadTbl.headers[1]] || '', mitigation: row[roadTbl.headers[2]] || '', owner: row[roadTbl.headers[3]] || '', eta: row[roadTbl.headers[4]] || '' }; }) : []; return { ok: true, data: Object.assign({}, dash, { matrix_cells: matrix_cells, threats: threats, stride: stride, maestro: maestro, roadmap: roadmap, recommendations: parseRecommendations(md), framework: extractField(md, 'Framework') || '' }) }; }); // ============================================================ // PARSERS routing-map (commandId → parser). 18 produces_report=true. // ============================================================ const PARSERS = { 'scan': parseScan, 'deep-scan': parseDeepScan, 'plugin-audit': parsePluginAudit, 'mcp-audit': parseMcpAudit, 'mcp-inspect': parseMcpInspect, 'ide-scan': parseIdeScan, 'supply-check': parseSupplyCheck, 'posture': parsePosture, 'audit': parseAudit, 'dashboard': parseDashboard, 'pre-deploy': parsePreDeploy, 'diff': parseDiff, 'watch': parseWatch, 'registry': parseRegistry, 'clean': parseClean, 'harden': parseHarden, 'threat-model': parseThreatModel, 'red-team': parseRedTeam }; // ============================================================ // RENDERERS routing-map — populated inline after each renderer-fn. // ============================================================ const RENDERERS = {}; // ============================================================ // RENDERER HELPERS // ============================================================ function renderEmptyState(message) { return '
' + '' + '
' + '

' + escapeHtml(message || 'Ingen data å vise.') + '

' + '
' + '
'; } function renderFindingsBlock(findings, label) { if (!findings || !findings.length) return ''; const sevOrder = { critical: 0, high: 1, medium: 2, low: 3, info: 4 }; const sorted = findings.slice().sort(function (a, b) { return (sevOrder[a.severity] || 9) - (sevOrder[b.severity] || 9); }); const items = sorted.map(function (f) { const sev = String(f.severity || 'info').toLowerCase(); // DS Tier 3 (v7.6.0 fase 5h): card--severity-{level} modifier på outer // .findings__item gir severity-tinted left-border. Beholdes ved siden av // den eksisterende .findings__item-severity-dot for ARIA + visuell // redundans (border-farge + dot-fyll signaliserer samme severity). const sevClass = 'card--severity-' + (sev === 'info' ? 'info' : sev); const meta = [ f.file ? f.file + (f.line ? ':' + f.line : '') : '', f.category || '', f.owasp || '' ].filter(Boolean).join(' · '); return ( '
' + '
' + '
' + '
' + escapeHtml(f.id || '—') + '
' + '
' + escapeHtml(f.description || f.title || '') + '
' + (meta ? '
' + escapeHtml(meta) + '
' : '') + '
' + '
' ); }).join(''); // DS .findings outer-class er et 2-kolonners grid (360px list + 1fr detail-panel) — // playgroundet bruker bare list-delen, så vi wrapper i .findings__list (uten outer // .findings) for å unngå at headeren ender i venstre 360px-kolonne. v7.6.1 fix. return ( '
' + '

' + escapeHtml(label || 'Funn') + '

' + '
' + '
' + '
' + escapeHtml(label || 'Funn') + ' (' + findings.length + ')
' + '
' + items + '
' + '
' + '
' + '
' ); } /** * Render recommendation-card med ordnet liste av anbefalinger. * Tredje argument (severity) styrer DS-tier3 `data-severity`-attributtet: * 'critical' / 'high' / 'medium' / 'low' / 'positive'. Default 'low' * (info-tonet). Mapping: severity → border-left-farge + label-bakgrunn. */ function renderRecommendationsList(recs, label, severity) { if (!recs || !recs.length) return ''; const sev = severity || 'low'; const items = recs.map(function (r) { return '
  • ' + escapeHtml(r) + '
  • '; }).join(''); return ( '
    ' + '' + escapeHtml(label || 'Anbefalinger') + '' + '
      ' + items + '
    ' + '
    ' ); } /** * Map severity-string til DS-tier3 recommendation-card data-severity. * Aksepterer både severity-konvensjoner (critical/high/medium/low/info) * og action-types (CREATE/APPEND/MERGE/SKIP/NONE). */ function mapSeverityToCardLevel(input) { const s = String(input || '').toLowerCase().trim(); if (!s) return 'low'; if (s === 'critical' || s === 'crit') return 'critical'; if (s === 'high') return 'high'; if (s === 'medium' || s === 'med') return 'medium'; if (s === 'low') return 'low'; if (s === 'info') return 'low'; if (s === 'positive' || s === 'success' || s === 'ok' || s === 'pass') return 'positive'; // Action-types fra renderHarden if (s === 'create') return 'positive'; if (s === 'append') return 'medium'; if (s === 'merge') return 'low'; if (s === 'skip' || s === 'none') return 'low'; return 'low'; } function renderRiskMeter(score, band) { const s = Math.max(0, Math.min(100, Number(score) || 0)); const bands = [ { label: 'Low', from: 0, to: 14 }, { label: 'Medium', from: 15, to: 39 }, { label: 'High', from: 40, to: 64 }, { label: 'Critical', from: 65, to: 84 }, { label: 'Extreme', from: 85, to: 100 } ]; const labels = bands.map(function (b) { const w = (b.to - b.from + 1); return '' + escapeHtml(b.label) + ''; }).join(''); return ( '
    ' + '
    ' + s + ' / 100 · ' + escapeHtml(band || '') + '
    ' + '
    ' + '
    ' + labels + '
    ' + '
    050100
    ' + '
    ' ); } function renderSmallMultiples(items) { // items: [{ name, score, max, grade?, status? }] if (!items || !items.length) return ''; const cards = items.map(function (it) { const score = Number(it.score) || 0; const max = Number(it.max) || 5; const pct = Math.max(0, Math.min(100, (score / max) * 100)); const grade = it.grade || ''; const gradeAttr = grade ? ' data-grade="' + escapeAttr(grade) + '"' : ''; return ( '
    ' + '
    ' + '' + escapeHtml(it.name || '') + '' + (grade ? '' + escapeHtml(grade) + '' : '') + '
    ' + '
    ' + '' + escapeHtml(it.status || (score + ' / ' + max)) + '' + '
    ' ); }).join(''); return '
    ' + cards + '
    '; } function renderRadarSvg(axes) { // axes: [{ name, score (0-5) }] if (!axes || axes.length < 3) return ''; // v7.6.1 fix: øk SVG-bredden fra 280 til 380 og r fra 105 til 125 for å gi // labels mer plass. Bruk text-anchor basert på horisontal-posisjon for å // unngå at bottom-labels overlapper hverandre ved 6+ akser. const size = 380, cx = size / 2, cy = size / 2, r = 125; const n = axes.length; const axisRows = axes.map(function (a) { return '
    ' + escapeHtml(a.name) + '' + escapeHtml(String(a.score || 0)) + '/5
    '; }).join(''); const angle = function (i) { return -Math.PI / 2 + (i * 2 * Math.PI / n); }; const labelHtml = axes.map(function (a, i) { const ang = angle(i); const lx = cx + Math.cos(ang) * (r + 28); const ly = cy + Math.sin(ang) * (r + 28); // Velg text-anchor basert på posisjon: ankerene til venstre/høyre snur. const dx = Math.cos(ang); const anchor = Math.abs(dx) < 0.2 ? 'middle' : (dx > 0 ? 'start' : 'end'); return '' + escapeHtml(a.name) + ''; }).join(''); const grids = [1, 2, 3, 4, 5].map(function (k) { const rk = (r * k) / 5; const pts = axes.map(function (a, i) { const ang = angle(i); return (cx + Math.cos(ang) * rk).toFixed(1) + ',' + (cy + Math.sin(ang) * rk).toFixed(1); }).join(' '); return ''; }).join(''); const pts = axes.map(function (a, i) { const ang = angle(i); const sc = Math.max(0, Math.min(5, Number(a.score) || 0)); const rs = (r * sc) / 5; return (cx + Math.cos(ang) * rs).toFixed(1) + ',' + (cy + Math.sin(ang) * rs).toFixed(1); }).join(' '); return ( '
    ' + '
    ' + '' + grids + labelHtml + '' + '' + '
    ' + '
    ' + axisRows + '
    ' + '
    ' ); } // ============================================================ // TIER 3 SPESIALKOMPONENTER — DS-helpers (v7.6.0 fase 5a-d). // ============================================================ /** * Render tfa-flow + tfa-leg + tfa-arrow for et lethal trifecta-funn. * Brukes på scan + deep-scan-rapporter når findings inneholder * en trifecta-pattern (f.eks. SCN-002 "Lethal trifecta: [Bash, Read, WebFetch]"). * Synthesiserer 3-leddet kjede: untrusted-input → sensitive-access → exfil-sink. */ function renderToxicFlow(findings) { if (!findings || !findings.length) return ''; const trifectaFinding = findings.find(function (f) { const desc = String(f.description || ''); const cat = String(f.category || ''); const owasp = String(f.owasp || ''); return /trifecta/i.test(desc) || /trifecta/i.test(cat) || /excessive\s*agency/i.test(cat) || /ASI01/i.test(owasp); }); if (!trifectaFinding) return ''; const sev = String(trifectaFinding.severity || 'critical').toLowerCase(); const verdictMap = { critical: 'BLOCK', high: 'BLOCK', medium: 'WARN', low: 'ALLOW' }; const verdict = verdictMap[sev] || 'BLOCK'; const fileLine = trifectaFinding.file ? trifectaFinding.file + (trifectaFinding.line ? ':' + trifectaFinding.line : '') : 'agent definition'; // Default trifecta-bensin: WebFetch + Read + Bash. Override hvis description nevner andre. const desc = String(trifectaFinding.description || ''); const m = desc.match(/\[([^\]]+)\]/); let tools = ['WebFetch', 'Read', 'Bash']; if (m) { const parsed = m[1].split(',').map(function (s) { return s.trim(); }).filter(Boolean); if (parsed.length === 3) tools = parsed; } const legs = [ { label: 'Untrusted input', name: tools[0], source: fileLine, mit: 'unmitigated', mitText: 'Ingen pre-prompt-inject-scan eller post-mcp-verify guard' }, { label: 'Sensitive access', name: tools[1], source: '.env / credentials / git-history', mit: 'unmitigated', mitText: 'Ingen pre-write-pathguard på sti' }, { label: 'Exfil sink', name: tools[2], source: 'curl / fetch til ekstern host', mit: 'unmitigated', mitText: 'Ingen post-session-guard trifecta-deteksjon' } ]; const legHtml = function (leg) { return ( '' ); }; const arrowHtml = ''; return ( '
    ' + '

    Toxic flow — Lethal trifecta-kjede

    ' + '

    Den fulle 3-leddete kjeden som overskrider Rule of Two. Hver leg er umitigert — ingen hook bryter kjeden.

    ' + '
    ' + '
    ' + escapeHtml(verdict) + '
    ' + legHtml(legs[0]) + arrowHtml + legHtml(legs[1]) + arrowHtml + legHtml(legs[2]) + '
    ' + '
    ' ); } /** * Render mat-ladder + mat-step for posture-modenhet. * Mapper antall PASS-kategorier til 5 modenhetstrinn (Initial → Optimized). */ function renderMatLadder(categories, postureScore, postureApplicable) { if (!categories || !categories.length) return ''; const passCount = postureScore != null ? Number(postureScore) : categories.filter(function (c) { return c.status === 'PASS'; }).length; const total = postureApplicable != null ? Number(postureApplicable) : categories.filter(function (c) { return c.status !== 'N-A' && c.status !== 'N/A'; }).length; const pct = total > 0 ? Math.round((passCount / total) * 100) : 0; // 5 modenhetstrinn — terskler basert på % PASS const steps = [ { num: 1, name: 'Initial', threshold: 0, desc: 'Bare bones — ingen hooks eller minimal posture.' }, { num: 2, name: 'Aware', threshold: 25, desc: 'Posture-skanning aktiv, kjenner risikoene.' }, { num: 3, name: 'Defensive', threshold: 50, desc: 'Hooks engasjert på kritiske flater (PreToolUse, UserPromptSubmit).' }, { num: 4, name: 'Mature', threshold: 75, desc: 'De fleste 16 kategoriene dekket; trifecta-deteksjon på.' }, { num: 5, name: 'Optimized', threshold: 95, desc: 'Full coverage; A-grade på posture; aktiv overvåking.' } ]; const currentIdx = steps.reduce(function (acc, s, i) { return pct >= s.threshold ? i : acc; }, 0); const stepHtml = steps.map(function (s, i) { const state = i < currentIdx ? 'completed' : i === currentIdx ? 'current' : 'future'; const icon = state === 'completed' ? '✓' : String(s.num); const pillCls = state === 'current' ? ' mat-step__pill mat-step__pill--current' : state === 'completed' ? ' mat-step__pill mat-step__pill--complete' : ''; const pillText = state === 'current' ? 'Du er her' : state === 'completed' ? 'Oppnådd' : ''; const pill = pillText ? '' + escapeHtml(pillText) + '' : ''; const progress = state === 'current' ? ( '
    ' + '
    ' + '' + passCount + ' / ' + total + ' kategorier' + '
    ' ) : ''; return ( '
    ' + '' + '
    ' + '
    ' + escapeHtml(s.name) + pill + '
    ' + '
    ' + escapeHtml(s.desc) + '
    ' + progress + '
    ' + '
    ' ); }).join(''); return ( '
    ' + '

    Modenhetsstige — posture-progresjon

    ' + '

    Posture-score på ' + passCount + ' av ' + total + ' kategorier (' + pct + '%) plasserer dette prosjektet på trinn ' + (currentIdx + 1) + ' av 5.

    ' + '
    ' + stepHtml + '
    ' + '
    ' ); } /** * Render suppressed-group fra v7.1.1 narrative-audit. * Parser executive_summary-tekst for "Suppressed signals: N (reason1: count examples, ...)" * eller bruker data.narrative_audit.suppressed_findings hvis strukturert. */ function renderSuppressedGroup(data) { if (!data) return ''; const audit = data.narrative_audit || {}; const sf = audit.suppressed_findings || {}; let groups = []; let totalCount = 0; if (sf.by_category && typeof sf.by_category === 'object') { totalCount = Number(sf.count || 0); groups = Object.keys(sf.by_category).map(function (k) { return { reason: k, count: Number(sf.by_category[k]) || 0, example: '' }; }); } else { // Fall back: parse fra executive_summary const summary = String(data.executive_summary || ''); const m = summary.match(/Suppressed signals:\s*\*?\*?\s*(\d+)\s*\(([^)]+)\)/i); if (!m) return ''; totalCount = Number(m[1]) || 0; groups = m[2].split(',').map(function (part) { const seg = part.trim(); const colonIdx = seg.indexOf(':'); if (colonIdx < 0) return { reason: seg, count: 1, example: '' }; const reason = seg.slice(0, colonIdx).trim(); const rest = seg.slice(colonIdx + 1).trim(); const cm = rest.match(/^(\d+)\s+(.*)$/); if (cm) { return { reason: reason, count: Number(cm[1]) || 1, example: cm[2].trim() }; } return { reason: reason, count: 1, example: rest }; }); } if (!groups.length) return ''; const groupsHtml = groups.map(function (g) { const example = g.example ? ( '
    ' + '' + escapeHtml(g.example) + '' + '
    ' ) : ''; return ( '
    ' + '
    ' + '' + escapeHtml(g.reason) + '' + '' + g.count + ' ' + (g.count === 1 ? 'forekomst' : 'forekomster') + '' + '
    ' + example + '
    ' ); }).join(''); return ( '
    ' + '

    Narrative audit — supprimerte signaler

    ' + '

    ' + totalCount + ' signaler ble supprimert pre-rapport (v7.1.1 narrative_audit). Disse er ikke false-positives walked-back i prosa, men auto-suppress før klassifisering.

    ' + groupsHtml + '
    ' ); } /** * Render codepoint-reveal + cp-tag for Unicode-steganografi (UNI-funn). * Brukes på mcp-inspect-rapporter — bytter plain table mot side-by-side * "synlig vs. decoded codepoint"-visning per tool. */ function renderCodepointReveal(codepoints) { if (!codepoints || !codepoints.length) return ''; const tagFor = function (code) { // U+200B/200C/200D/FEFF = zero-width if (/U\+(200[B-D]|FEFF|2060|180E)/i.test(code)) return 'cp-zw'; // U+202E/202D/2066-2069 = bidi/RTL if (/U\+(202[ADE]|206[6-9])/i.test(code)) return 'cp-bidi'; // Other = generic cp-tag (warning class) return 'cp-tag'; }; const blocks = codepoints.map(function (c) { const risk = String(c.risk || '').trim(); const sev = /high/i.test(risk) ? 'critical' : /medium/i.test(risk) ? 'medium' : 'low'; const isClean = /clean|—|^-$/i.test(c.codepoints || '') || risk === '—' || risk === '-'; const cps = String(c.codepoints || ''); // Highlight U+XXXX-mønstre const highlighted = cps.replace(/U\+[0-9A-Fa-f]{4,6}/g, function (m) { return '' + m + ''; }); const headRisk = isClean ? 'Ren — ingen non-ASCII' : '' + escapeHtml(risk) + ' risk'; const visibleCol = isClean ? '
    ' + escapeHtml(c.tool || '—') + '
    ' : '
    ' + escapeHtml(c.tool || '—') + ' (rendert visuelt)
    '; const decodedCol = isClean ? '
    (ingen suspekte codepoints)
    ' : '
    ' + highlighted + '
    '; return ( '
    ' + '
    ' + '' + escapeHtml(c.server || '—') + ' · ' + escapeHtml(c.tool || '—') + '' + headRisk + '
    ' + '
    ' + '
    ' + 'Synlig (rendret tekst)' + visibleCol + '
    ' + '
    ' + 'Decoded (codepoints)' + decodedCol + '
    ' + '
    ' + '
    ' ); }).join(''); return ( '
    ' + '

    Codepoint-reveal — Unicode-steganografi

    ' + '

    Tools med non-ASCII codepoints i deskripsjoner — zero-width / homoglyph / bidi-override. Side-ved-side: synlig form vs. dekoded codepoints.

    ' + '
    ' + blocks + '
    ' + '
    ' ); } /** * Render top-risks + top-risk for rangert top-funn-listing. * Tar de N (default 5) høyeste alvorlighetsnivåene fra findings og * viser dem som ordnet liste. Bruker `.top-risks` / `.top-risk` med * `data-severity` for severity-tinted left-border per DS Tier 3-supplement. * Returnerer tom streng hvis ingen findings (eller kun info-funn). */ function renderTopRisks(findings, n) { if (!findings || !findings.length) return ''; const sevOrder = { critical: 0, high: 1, medium: 2, low: 3, info: 4 }; const max = typeof n === 'number' && n > 0 ? n : 5; // Filtrer ut info-only — top-risks viser reelle risker, ikke observability-noise const filtered = findings.filter(function (f) { return (f.severity || 'info').toLowerCase() !== 'info'; }); if (!filtered.length) return ''; const sorted = filtered.slice().sort(function (a, b) { return (sevOrder[a.severity] || 9) - (sevOrder[b.severity] || 9); }); const top = sorted.slice(0, max); const items = top.map(function (f, idx) { const sev = String(f.severity || 'info').toLowerCase(); const sevLabel = sev.toUpperCase(); const meta = [ f.file ? f.file + (f.line ? ':' + f.line : '') : '', f.id || '', f.owasp || '' ].filter(Boolean).join(' · '); const title = f.description || f.title || '—'; return ( '
  • ' + '
    ' + (idx + 1) + '
    ' + '
    ' + '
    ' + escapeHtml(title) + '
    ' + (meta ? '
    ' + escapeHtml(meta) + '
    ' : '') + '
    ' + '' + escapeHtml(sevLabel) + '' + '
  • ' ); }).join(''); return ( '
    ' + '

    Top ' + top.length + ' risks

    ' + '
      ' + items + '
    ' + '
    ' ); } // ============================================================ // 10 RENDERERS — én per høy-prio kommando. // ============================================================ function renderScan(data, slot) { const meterHtml = renderRiskMeter(data.risk_score, data.riskBand); const suppressedHtml = renderSuppressedGroup(data); const toxicHtml = renderToxicFlow(data.findings || []); const owaspHtml = (data.owasp && data.owasp.length) ? ( '

    OWASP-kategorier

    ' + '' + data.owasp.map(function (o) { return ''; }).join('') + '
    KategoriFunnMaks severitySkannere
    ' + escapeHtml(o.category) + '' + o.findings + '' + escapeHtml(o.max_severity) + '' + escapeHtml(o.scanners) + '
    ' + '
    ' ) : ''; const supplyHtml = (data.supply_chain && data.supply_chain.length) ? ( '

    Supply chain

    ' + '' + data.supply_chain.map(function (s) { return ''; }).join('') + '
    KomponentTypeKildeTrustNotater
    ' + escapeHtml(s.component) + '' + escapeHtml(s.type) + '' + escapeHtml(s.source) + '' + escapeHtml(s.trust) + '' + escapeHtml(s.notes) + '
    ' + '
    ' ) : ''; const topRisksHtml = renderTopRisks(data.findings || [], 5); const findingsHtml = renderFindingsBlock(data.findings || [], 'Funn'); const recHtml = renderRecommendationsList(data.recommendations || []); const body = meterHtml + suppressedHtml + toxicHtml + topRisksHtml + owaspHtml + supplyHtml + findingsHtml + recHtml; slot.innerHTML = renderPageShell({ eyebrow: 'SKANNING', title: data.title || 'Security Scan', lede: data.lede || (data.executive_summary ? data.executive_summary.split('\n')[0].slice(0, 220) : 'Skann av skills, MCP-konfig, kataloger eller GitHub-URL.'), verdict: data.verdict || inferVerdict(data, 'risk-score-meter'), keyStats: data.keyStats || inferKeyStats(data, 'risk-score-meter') }, body); } RENDERERS.renderScan = renderScan; function renderDeepScan(data, slot) { // Per-scanner small-multiples const sm = (data.scanners || []).map(function (s) { const okStatus = /ok/i.test(s.status || '') ? 'ok' : (s.status || 'unknown'); const grade = (s.findings === 0) ? 'A' : (s.findings <= 3) ? 'B' : (s.findings <= 8) ? 'C' : (s.findings <= 15) ? 'D' : 'F'; return { name: s.tag + ' · ' + s.name, score: Math.max(0, 5 - Math.min(5, Math.floor((s.findings || 0) / 3))), max: 5, grade: grade, status: s.findings + ' funn · ' + (s.duration_ms || 0) + 'ms · ' + okStatus }; }); const smHtml = renderSmallMultiples(sm); // Scanner Risk Matrix-tabell const matrixRows = (data.scanner_matrix || []).map(function (r) { return '' + escapeHtml(r.scanner) + '' + '' + r.critical + '' + '' + r.high + '' + '' + r.medium + '' + '' + r.low + '' + '' + r.info + ''; }).join(''); const matrixHtml = matrixRows ? ( '

    Scanner Risk Matrix

    ' + '' + matrixRows + '
    ScannerCRITHIGHMEDLOWINFO
    ' + '
    ' ) : ''; const meterHtml = (data.risk_score != null) ? renderRiskMeter(data.risk_score, data.riskBand) : ''; const topRisksHtml = renderTopRisks(data.findings || [], 5); const findingsHtml = renderFindingsBlock(data.findings || [], 'Findings (utvalg)'); const recHtml = renderRecommendationsList(data.recommendations || []); const suppressedHtml = renderSuppressedGroup(data); const toxicHtml = renderToxicFlow(data.findings || []); const body = meterHtml + suppressedHtml + toxicHtml + smHtml + matrixHtml + topRisksHtml + findingsHtml + recHtml; slot.innerHTML = renderPageShell({ eyebrow: 'DEEP-SCAN', title: data.title || 'Deterministisk deep-scan', lede: data.lede || '10 deterministiske Node.js-scannere, ingen LLM-invokasjon.', verdict: data.verdict || inferVerdict(data, 'findings-grade'), keyStats: data.keyStats || inferKeyStats(data, 'findings-grade') }, body); } RENDERERS.renderDeepScan = renderDeepScan; function renderPluginAudit(data, slot) { const meta = data.plugin_metadata || {}; const metaRows = Object.keys(meta).map(function (k) { return '' + escapeHtml(k.replace(/_/g, ' ')) + '' + escapeHtml(meta[k]) + ''; }).join(''); const metaHtml = metaRows ? '

    Plugin-metadata

    ' + metaRows + '
    ' : ''; const compHtml = (data.components && data.components.length) ? ( '

    Komponenter

    ' + '' + data.components.map(function (c) { return ''; }).join('') + '
    KomponentAntallNotater
    ' + escapeHtml(c.component) + '' + c.count + '' + escapeHtml(c.notes) + '
    ' + '
    ' ) : ''; const permHtml = (data.permissions && data.permissions.length) ? ( '

    Permission-matrise

    ' + '' + data.permissions.map(function (p) { const isYes = /^yes|^ja/i.test(p.justified); const isNo = /^no$|^nei/i.test(p.justified); const cls = isYes ? 'low' : (isNo ? 'critical' : 'medium'); return ''; }).join('') + '
    VerktøyKrevet avBegrunnet
    ' + escapeHtml(p.tool) + '' + escapeHtml(p.required_by) + '' + escapeHtml(p.justified) + '
    ' + '
    ' ) : ''; const trustSev = (function () { const t = String(data.trust_verdict_text || '').toLowerCase(); if (/block|fail|critical|do\s*not\s*install/i.test(t)) return 'critical'; if (/warn|caution|review|conditional/i.test(t)) return 'high'; if (/allow|trust|verified|pass/i.test(t)) return 'positive'; return 'medium'; })(); const trustHtml = data.trust_verdict_text ? ( '
    ' + 'Trust-verdict' + '

    ' + escapeHtml(data.trust_verdict_text).replace(/\n/g, '
    ') + '

    ' + '
    ' ) : ''; const topRisksHtml = renderTopRisks(data.findings || [], 5); const findingsHtml = renderFindingsBlock(data.findings || [], 'Funn'); const recHtml = renderRecommendationsList(data.recommendations || []); const body = renderRiskMeter(data.risk_score, data.riskBand) + metaHtml + compHtml + permHtml + trustHtml + topRisksHtml + findingsHtml + recHtml; slot.innerHTML = renderPageShell({ eyebrow: 'PLUGIN-AUDIT', title: data.title || 'Plugin trust-vurdering', lede: data.lede || 'Trust-verdikt basert på maintainer, lisens, permissions og MCP-deskripsjoner.', verdict: data.verdict || inferVerdict(data, 'risk-score-meter'), keyStats: data.keyStats || inferKeyStats(data, 'risk-score-meter') }, body); } RENDERERS.renderPluginAudit = renderPluginAudit; function renderMcpAudit(data, slot) { const landRows = (data.mcp_servers || []).map(function (s) { return '' + '' + escapeHtml(s.server) + '' + '' + escapeHtml(s.type) + '' + '' + escapeHtml(s.trust) + '' + '' + s.tools + '' + '' + (s.active ? 'aktiv' : 'dormant') + '' + ''; }).join(''); const landHtml = landRows ? ( '

    MCP-landskap

    ' + '' + landRows + '
    ServerTypeTrustToolsStatus
    ' + '
    ' ) : ''; // Per-server som critique-cards const psHtml = (data.per_server && data.per_server.length) ? ( '
    ' + data.per_server.map(function (p) { const sev = /(verdict:.*BLOCK|verdict:.*FAIL|critical)/i.test(p.body) ? 'critical' : /(verdict:.*WARNING|warn|medium|drift)/i.test(p.body) ? 'medium' : 'low'; const lines = p.body.split(/\r?\n/).slice(0, 6).join(' '); return '
    ' + '
    ' + '
    ' + escapeHtml(p.name) + '
    ' + (p.note ? '
    ' + escapeHtml(p.note) + '
    ' : '') + '
    ' + '
    ' + escapeHtml(lines.slice(0, 360)) + (lines.length > 360 ? '…' : '') + '
    ' + '
    '; }).join('') + '
    ' ) : ''; // Keep / Review / Remove kanban const buckets = data.buckets || { keep: [], review: [], remove: [] }; const cardFor = function (bucket, label) { const items = buckets[bucket] || []; const cards = items.length ? items.map(function (it) { return '
    ' + '
    ' + escapeHtml(it.server) + '
    ' + (it.reason ? '
    ' + escapeHtml(it.reason) + '
    ' : '') + '
    '; }).join('') : '
    Ingen
    '; return '
    ' + '
    ' + '' + escapeHtml(label) + '' + '' + items.length + '' + '
    ' + cards + '
    '; }; const kanbanHtml = '
    ' + cardFor('keep', 'Keep') + cardFor('review', 'Review') + cardFor('remove', 'Remove') + '
    '; const findingsHtml = renderFindingsBlock(data.findings || [], 'MCP-funn'); const recHtml = renderRecommendationsList(data.recommendations || []); const body = landHtml + psHtml + kanbanHtml + findingsHtml + recHtml; slot.innerHTML = renderPageShell({ eyebrow: 'MCP-AUDIT', title: data.title || 'MCP-konfig audit', lede: data.lede || 'Permissions, trust og deskripsjon-drift på tvers av installerte MCP-servere.', verdict: data.verdict || inferVerdict(data, 'findings'), keyStats: data.keyStats || inferKeyStats(data, 'findings') }, body); } RENDERERS.renderMcpAudit = renderMcpAudit; function renderIdeScan(data, slot) { const covRows = (data.coverage || []).map(function (c) { return '' + escapeHtml(c.ide) + '' + c.extensions + '' + c.findings + ''; }).join(''); const covHtml = covRows ? ( '

    Scan-dekning

    ' + '' + covRows + '
    IDEExtensionsFunn
    ' + '
    ' ) : ''; // Findings — bruk renderFindingsBlock men med extension+ide som meta const fs = (data.findings || []).map(function (f) { return Object.assign({}, f, { file: f.extension || f.file || '', category: f.ide || '' }); }); const findingsHtml = renderFindingsBlock(fs, 'IDE-extension funn'); const recHtml = renderRecommendationsList(data.recommendations || []); const body = covHtml + findingsHtml + recHtml; slot.innerHTML = renderPageShell({ eyebrow: 'IDE-SCAN', title: data.title || 'IDE-extension scan', lede: data.lede || 'VS Code + JetBrains supply-chain-sjekk, blocklist + typosquat + obfuskering.', verdict: data.verdict || inferVerdict(data, 'findings'), keyStats: data.keyStats || inferKeyStats(data, 'findings') }, body); } RENDERERS.renderIdeScan = renderIdeScan; function renderPosture(data, slot) { // Small-multiples per kategori const items = (data.categories || []).filter(function (c) { return c.status !== 'N-A' && c.status !== 'N/A'; }).map(function (c) { const score = c.status === 'PASS' ? 5 : c.status === 'PARTIAL' ? 3 : c.status === 'FAIL' ? 1 : 0; const grade = c.status === 'PASS' ? 'A' : c.status === 'PARTIAL' ? 'C' : c.status === 'FAIL' ? 'F' : ''; return { name: c.num + '. ' + c.name, score: score, max: 5, grade: grade, status: c.status + (c.findings ? ' · ' + c.findings + ' funn' : '') }; }); const smHtml = renderSmallMultiples(items); const ladderHtml = renderMatLadder(data.categories || [], data.posture_score, data.posture_applicable); // Quick wins const quickHtml = (data.quick_wins && data.quick_wins.length) ? ( '
    ' + 'Quick wins' + '
      ' + data.quick_wins.map(function (w) { return '
    1. ' + escapeHtml(w) + '
    2. '; }).join('') + '
    ' + '
    ' ) : ''; const topRisksHtml = renderTopRisks(data.findings || [], 5); const findingsHtml = renderFindingsBlock(data.findings || [], 'Top findings'); const recHtml = renderRecommendationsList(data.recommendations || []); const overall = data.posture_score != null ? ( '

    Overall score

    ' + data.posture_score + ' / ' + (data.posture_applicable || '?') + ' kategorier dekket — Grade ' + escapeHtml(data.grade || '?') + '.

    ' ) : ''; const body = overall + ladderHtml + smHtml + quickHtml + topRisksHtml + findingsHtml + recHtml; slot.innerHTML = renderPageShell({ eyebrow: 'POSTURE', title: data.title || 'Security posture', lede: data.lede || 'Rask scorecard, deterministisk scanner, <2s.', verdict: data.verdict || inferVerdict(data, 'posture-cards'), keyStats: data.keyStats || inferKeyStats(data, 'posture-cards') }, body); } RENDERERS.renderPosture = renderPosture; function renderAudit(data, slot) { const radarHtml = renderRadarSvg(data.radar_axes || []); // Category Assessment som expansion-kort const catHtml = (data.categories && data.categories.length) ? ( '

    Kategori-vurdering

    ' + '
    ' + data.categories.map(function (c) { const sev = c.status === 'FAIL' ? 'critical' : c.status === 'PARTIAL' ? 'medium' : c.status === 'PASS' ? 'low' : 'info'; const sevClass = 'card--severity-' + sev; return '
    ' + '
    ' + '
    ' + '
    Kat. ' + c.num + '
    ' + '
    ' + escapeHtml(c.name) + '
    ' + '
    Status: ' + escapeHtml(c.status || '—') + '
    ' + '
    ' + '
    '; }).join('') + '
    ' + '
    ' ) : ''; // Action Plan tre-tier const tierHtml = function (tier, label, sev) { const items = (data.action_plan && data.action_plan[tier]) || []; if (!items.length) return ''; return '
    ' + '' + escapeHtml(label) + '' + '
      ' + items.map(function (a) { return '
    1. ' + escapeHtml(a) + '
    2. '; }).join('') + '
    ' + '
    '; }; const actionHtml = tierHtml('immediate', 'Umiddelbar', 'critical') + tierHtml('high', 'Høy prioritet', 'high') + tierHtml('medium', 'Medium prioritet', 'medium'); const meterHtml = (data.risk_score != null) ? renderRiskMeter(data.risk_score, data.riskBand) : ''; const topRisksHtml = renderTopRisks(data.findings || [], 5); const findingsHtml = renderFindingsBlock(data.findings || [], 'Funn'); const body = meterHtml + radarHtml + catHtml + actionHtml + topRisksHtml + findingsHtml; slot.innerHTML = renderPageShell({ eyebrow: 'AUDIT', title: data.title || 'Full security audit', lede: data.lede || 'OWASP LLM Top 10-vurdering, A-F grading, action plan.', verdict: data.verdict || inferVerdict(data, 'findings-grade'), keyStats: data.keyStats || inferKeyStats(data, 'findings-grade') }, body); } RENDERERS.renderAudit = renderAudit; function renderDashboard(data, slot) { // Fleet-grid med fleet-tile per prosjekt const projects = data.projects || []; const sevForGrade = function (g) { const u = String(g || '').toUpperCase(); if (u === 'A') return 'low'; if (u === 'B') return 'low'; if (u === 'C') return 'medium'; if (u === 'D') return 'high'; if (u === 'F') return 'critical'; return 'info'; }; const tiles = projects.length ? projects.map(function (p) { const trend = (data.trends || []).find(function (t) { return t.name === p.name; }); const trendCls = trend ? ('fleet-tile__trend--' + trend.trend) : 'fleet-tile__trend--stable'; const fillPct = Math.max(0, Math.min(100, p.risk)); return ( '
    ' + '
    ' + '' + escapeHtml(p.name) + '' + '' + escapeHtml(p.grade || '?') + '' + '
    ' + '
    ' + '
    ' + 'Risk ' + p.risk + ' · ' + p.findings + ' funn' + (trend ? '' + escapeHtml(trend.d_risk) + '' : '') + '
    ' + (p.worst_category ? '
    Verst: ' + escapeHtml(p.worst_category) + '
    ' : '') + '
    ' ); }).join('') : ''; const gridHtml = tiles ? '
    ' + tiles + '
    ' : renderEmptyState('Ingen prosjekter funnet.'); // Errors const errorsHtml = (data.errors && data.errors.length) ? ( '

    Errors

    ' + '' + data.errors.map(function (e) { return ''; }).join('') + '
    ProsjektFeil
    ' + escapeHtml(e.project) + '' + escapeHtml(e.error) + '
    ' + '
    ' ) : ''; const recHtml = renderRecommendationsList(data.recommendations || []); const body = gridHtml + errorsHtml + recHtml; slot.innerHTML = renderPageShell({ eyebrow: 'DASHBOARD', title: data.title || 'Cross-project dashboard', lede: data.lede || 'Maskin-grade = svakeste lenke. Aggregert posture-skann per prosjekt.', verdict: data.verdict || inferVerdict(data, 'dashboard-fleet'), keyStats: data.keyStats || inferKeyStats(data, 'dashboard-fleet') }, body); } RENDERERS.renderDashboard = renderDashboard; function renderHarden(data, slot) { const recs = data.recommendations || []; // Diff-blokker per recommendation — DS Tier 3 recommendation-card med data-severity (v7.6.0 fase 5f). // CREATE → positive (ny grade A-fil), APPEND → medium (eksisterende fil utvides), // MERGE → low (allerede satt, kun normalisering), SKIP → low (ingen handling). const diffHtml = recs.map(function (r, idx) { const isCreate = /create/i.test(r.action); const isAppend = /append/i.test(r.action); const isMerge = /merge/i.test(r.action); const isNone = /none|skip/i.test(r.action); const actionLabel = isCreate ? 'CREATE' : isAppend ? 'APPEND' : isMerge ? 'MERGE' : 'SKIP'; const sev = mapSeverityToCardLevel(actionLabel); return ( '
    ' + '' + actionLabel + ' · ' + escapeHtml(String(r.num)) + '. ' + escapeHtml(r.category) + '' + '
    ' + '
    ' + escapeHtml(r.file) + '
    ' + (r.content_preview ? '
    ' + escapeHtml(r.content_preview).slice(0, 600) + (r.content_preview.length > 600 ? '…' : '') + '
    ' : '') + '
    ' + '
    ' ); }).join(''); // Diff summary footer const summaryRows = (data.diff_summary || []).map(function (d) { return '
    ' + escapeHtml(d.file) + '' + escapeHtml(d.action) + ' · ' + escapeHtml(d.lines) + '
    '; }).join(''); const summaryHtml = summaryRows ? '
    ' + summaryRows + '
    ' : ''; const introSev = (function () { const g = String(data.current_grade || '?').toUpperCase(); if (g === 'F' || g === 'D') return 'critical'; if (g === 'C') return 'high'; if (g === 'B') return 'medium'; if (g === 'A') return 'positive'; return 'medium'; })(); const intro = ( '
    ' + 'Snapshot · grade ' + escapeHtml(data.current_grade || '?') + '' + '

    Prosjekt-type: ' + escapeHtml(data.project_type || '?') + ' · ' + data.actionable + '/' + data.total + ' anbefalinger · Modus: ' + escapeHtml(data.mode || 'dry-run') + '

    ' + '
    ' ); const body = intro + (diffHtml || renderEmptyState('Ingen anbefalinger.')) + summaryHtml; slot.innerHTML = renderPageShell({ eyebrow: 'HARDEN', title: data.title || 'Grade A reference config', lede: data.lede || 'Diff-forhåndsvisning av settings.json, CLAUDE.md og .gitignore-endringer.', verdict: data.verdict || inferVerdict(data, 'diff-report'), keyStats: data.keyStats || [ { label: 'NÅ-GRADE', value: String(data.current_grade || '?') }, { label: 'AKSJONER', value: data.actionable + '/' + data.total }, { label: 'MODUS', value: data.mode || 'dry-run' } ] }, body); } RENDERERS.renderHarden = renderHarden; function renderRedTeam(data, slot) { const meterHtml = renderRiskMeter(100 - (data.defense_score || 0), data.riskBand); // Per-category small-multiples const cats = (data.categories || []).map(function (c) { const total = (c.pass || 0) + (c.fail || 0); const score = total ? Math.round((c.pass / total) * 5) : 0; const grade = total === 0 ? '?' : c.fail === 0 ? 'A' : c.fail <= 1 ? 'B' : c.fail <= 3 ? 'C' : 'D'; return { name: c.category, score: score, max: 5, grade: grade, status: c.pass + ' pass · ' + c.fail + ' fail' }; }); const smHtml = renderSmallMultiples(cats); // Failed scenarios som findings const scnFindings = (data.scenarios || []).map(function (s) { return { id: s.id, severity: s.severity, category: s.category, description: s.payload_class + ' — ' + s.reason, owasp: '' }; }); const findingsHtml = renderFindingsBlock(scnFindings, 'Failed scenarios'); // History const historyRows = (data.history || []).map(function (h) { return '' + escapeHtml(h.run) + '' + escapeHtml(h.date) + '' + h.defense_score + '%' + escapeHtml(h.delta) + ''; }).join(''); const historyHtml = historyRows ? ( '

    Defense score-historikk

    ' + '' + historyRows + '
    RunDatoScoreΔ
    ' + '
    ' ) : ''; const recHtml = renderRecommendationsList(data.recommendations || []); const body = meterHtml + smHtml + findingsHtml + historyHtml + recHtml; slot.innerHTML = renderPageShell({ eyebrow: 'RED-TEAM', title: data.title || 'Attack-simulasjon', lede: data.lede || (data.adaptive ? 'Adaptive mode aktiv (mutation-based evasion).' : 'Statisk mode — 64 deterministiske scenarios.'), verdict: data.verdict || inferVerdict(data, 'red-team-results'), keyStats: data.keyStats || inferKeyStats(data, 'red-team-results') }, body); } RENDERERS.renderRedTeam = renderRedTeam; // ============================================================ // FASE 3: 8 RENDERERS — én per gjenstående kommando. // ============================================================ function renderMcpInspect(data, slot) { const invRows = (data.server_inventory || []).map(function (s) { return '' + '' + escapeHtml(s.server) + '' + '' + escapeHtml(s.transport) + '' + '' + s.tools + '' + '' + escapeHtml(s.status) + '' + '' + (s.connected ? 'ja' : 'nei') + '' + ''; }).join(''); const invHtml = invRows ? ( '

    Server-inventar

    ' + '' + invRows + '
    ServerTransportToolsStatusConnected
    ' + '
    ' ) : ''; const cpHtml = renderCodepointReveal(data.codepoints || []); const fs = (data.findings || []).map(function (f) { return Object.assign({}, f, { file: f.server || f.file || '', category: f.category || '' }); }); const findingsHtml = renderFindingsBlock(fs, 'MCP-inspect funn'); const recHtml = renderRecommendationsList(data.recommendations || []); const body = invHtml + cpHtml + findingsHtml + recHtml; slot.innerHTML = renderPageShell({ eyebrow: 'MCP-INSPECT', title: data.title || 'MCP live-inspect', lede: data.lede || 'Runtime tool-deskripsjoner — drift, tool shadowing, codepoint reveal.', verdict: data.verdict || inferVerdict(data, 'findings'), keyStats: data.keyStats || inferKeyStats(data, 'findings') }, body); } RENDERERS.renderMcpInspect = renderMcpInspect; function renderSupplyCheck(data, slot) { // Ecosystem-cards (small-multiples-mønster) const ecos = (data.ecosystems || []).filter(function (e) { return Number(e.packages) > 0 || Number(e.osv_hits) > 0 || Number(e.typosquats) > 0; }); const ecoCards = ecos.length ? '
    ' + ecos.map(function (e) { const issues = (Number(e.osv_hits) || 0) + (Number(e.typosquats) || 0); const grade = issues === 0 ? 'A' : issues <= 1 ? 'B' : issues <= 3 ? 'C' : issues <= 6 ? 'D' : 'F'; const score = Math.max(0, 5 - Math.min(5, issues)); const fillPct = (score / 5) * 100; return '
    ' + '
    ' + '' + escapeHtml(e.ecosystem) + '' + '' + escapeHtml(grade) + '' + '
    ' + '
    ' + '' + e.packages + ' pakker · ' + e.osv_hits + ' OSV · ' + e.typosquats + ' typosquats' + '
    '; }).join('') + '
    ' : ''; const findingsHtml = renderFindingsBlock(data.findings || [], 'Supply-chain funn'); const recHtml = renderRecommendationsList(data.recommendations || []); const body = ecoCards + findingsHtml + recHtml; slot.innerHTML = renderPageShell({ eyebrow: 'SUPPLY-CHECK', title: data.title || 'Supply-chain recheck', lede: data.lede || 'Re-audit lockfiler mot blocklists, OSV.dev og typosquat-deteksjon.', verdict: data.verdict || inferVerdict(data, 'findings'), keyStats: data.keyStats || inferKeyStats(data, 'findings') }, body); } RENDERERS.renderSupplyCheck = renderSupplyCheck; function renderPreDeploy(data, slot) { const lights = data.traffic_lights || []; const sevForStatus = function (s) { const u = String(s || '').toUpperCase(); if (u === 'PASS' || u === 'GO') return 'low'; if (u === 'PASS-WITH-NOTES' || u === 'WARNING' || u === 'PARTIAL') return 'medium'; if (u === 'FAIL' || u === 'BLOCK' || u === 'NO-GO') return 'critical'; return 'info'; }; // v7.6.1 fix: sm-card__grade er fast 28×28 px (designet for én A-F-bokstav), så // "PASS"/"PASS-WITH-NOTES"/"FAIL" ble kuttet til "AS"/"PASS-WITH-..."/"FA". Bytt // til en bredde-tilpasset status-pill via inline styling (ingen DS-klasse-endring). const cards = lights.map(function (l) { const sev = sevForStatus(l.status); const pillBg = sev === 'low' ? 'var(--color-severity-low-soft)' : sev === 'medium' ? 'var(--color-severity-medium-soft)' : sev === 'critical' ? 'var(--color-severity-critical-soft)' : 'var(--color-bg-soft)'; const pillFg = sev === 'low' ? 'var(--color-severity-low-on)' : sev === 'medium' ? 'var(--color-severity-medium-on)' : sev === 'critical' ? 'var(--color-severity-critical-on)' : 'var(--color-text-secondary)'; const statusPill = '' + escapeHtml(l.status) + ''; return '
    ' + '
    ' + '' + escapeHtml(l.category) + '' + statusPill + '
    ' + (l.notes ? '' + escapeHtml(l.notes) + '' : '') + '
    '; }).join(''); const lightsHtml = cards ? '

    Traffic-light kategorier

    ' + cards + '
    ' : ''; const condHtml = (data.conditions && data.conditions.length) ? ( '
    ' + 'Vilkår å løse' + '
      ' + data.conditions.map(function (c) { return '
    1. ' + escapeHtml(c) + '
    2. '; }).join('') + '
    ' + '
    ' ) : ''; const apprRows = (data.approvals || []).map(function (a) { const isPending = /pending|—/i.test(a.approver) || !a.approver.trim(); return '' + escapeHtml(a.role) + '' + (isPending ? '(venter)' : escapeHtml(a.approver)) + '' + escapeHtml(a.date || '—') + '' + escapeHtml(a.notes) + ''; }).join(''); const apprHtml = apprRows ? ( '

    Godkjenninger

    ' + '' + apprRows + '
    RolleGodkjennerDatoNotater
    ' + '
    ' ) : ''; const findingsHtml = renderFindingsBlock(data.findings || [], 'Pre-deploy funn'); const recHtml = renderRecommendationsList(data.recommendations || []); const body = lightsHtml + condHtml + apprHtml + findingsHtml + recHtml; slot.innerHTML = renderPageShell({ eyebrow: 'PRE-DEPLOY', title: data.title || 'Pre-deploy security checklist', lede: data.lede || 'Enterprise-gate + production readiness — 13 kategorier.', verdict: data.verdict || inferVerdict(data, 'findings'), keyStats: data.keyStats || inferKeyStats(data, 'findings') }, body); } RENDERERS.renderPreDeploy = renderPreDeploy; function renderDiff(data, slot) { const newItems = data['new'] || []; const resolvedItems = data.resolved || []; const unchangedItems = data.unchanged || []; const movedItems = data.moved || []; const gradeBadge = function (g) { return g ? '' + escapeHtml(g) + '' : '?'; }; const headerHtml = ( '

    Grade-bevegelse

    ' + '
    ' + '
    ' + 'BASELINE ' + escapeHtml(data.baseline_date || '') + '' + '' + gradeBadge(data.baseline_grade) + '' + '
    ' + '' + '
    ' + '' + '' + gradeBadge(data.current_grade) + '' + '
    ' + '
    ' + '
    ' ); const renderRowItem = function (it, action) { const sev = it.severity || 'info'; const sevClass = 'card--severity-' + sev; const meta = [it.category, it.file, it.resolution, it.notes].filter(Boolean).join(' · '); const cellClass = action === 'new' ? 'diff__cell--added' : action === 'resolved' ? 'diff__cell--unchanged' : 'diff__cell--unchanged'; return '
    ' + '
    ' + '
    ' + '
    ' + '
    ' + '
    ' + escapeHtml(it.id || '—') + '
    ' + '
    ' + escapeHtml(it.description || it.resolution || it.notes || '') + '
    ' + (meta ? '
    ' + escapeHtml(meta) + '
    ' : '') + '
    ' + '
    ' + '
    ' + '
    '; }; const sectionFor = function (label, items, action) { if (!items.length) return ''; return '

    ' + escapeHtml(label) + ' (' + items.length + ')

    ' + '
    ' + items.map(function (it) { return renderRowItem(it, action); }).join('') + '
    ' + '
    '; }; const newHtml = sectionFor('Nye funn', newItems, 'new'); const resHtml = sectionFor('Løste funn', resolvedItems, 'resolved'); const unchHtml = sectionFor('Uendret', unchangedItems, 'unchanged'); const movHtml = (movedItems.length) ? sectionFor('Flyttet', movedItems.map(function (m) { return { id: m.id, severity: 'info', description: m.from + ' → ' + m.to }; }), 'moved') : ''; const recHtml = renderRecommendationsList(data.recommendations || []); const body = headerHtml + newHtml + resHtml + unchHtml + movHtml + recHtml; slot.innerHTML = renderPageShell({ eyebrow: 'DIFF', title: data.title || 'Scan diff mot baseline', lede: data.lede || 'Sammenligner nåværende scan mot lagret baseline.', verdict: data.verdict || inferVerdict(data, 'diff-report'), keyStats: data.keyStats || inferKeyStats(data, 'diff-report') }, body); } RENDERERS.renderDiff = renderDiff; function renderWatch(data, slot) { const meter = data.live_meter || {}; const meterRows = Object.keys(meter).map(function (k) { return '' + escapeHtml(k.replace(/_/g, ' ')) + '' + escapeHtml(meter[k]) + ''; }).join(''); const meterHtml = meterRows ? ( '

    Live-meter

    ' + '' + meterRows + '
    ' + '
    ' ) : ''; const histRows = (data.history || []).map(function (h) { const isCurrent = /^current/i.test(h.run); return '' + '' + escapeHtml(h.run) + '' + '' + escapeHtml(h.time) + '' + '' + escapeHtml(h.grade || '?') + '' + '' + h.risk_score + '' + '' + escapeHtml(h.delta || '—') + '' + ''; }).join(''); const histHtml = histRows ? ( '

    Siste runs

    ' + '' + histRows + '
    RunTidGradeRiskΔ
    ' + '
    ' ) : ''; const findingsHtml = renderFindingsBlock(data.findings || [], 'Funn (siste run)'); const notRows = (data.notify_events || []).map(function (n) { return '' + escapeHtml(n.time) + '' + escapeHtml(n.event) + '' + escapeHtml(n.channel) + '' + escapeHtml(n.status) + ''; }).join(''); const notHtml = notRows ? ( '

    Notify-eventer

    ' + '' + notRows + '
    TidEventChannelStatus
    ' + '
    ' ) : ''; const recHtml = renderRecommendationsList(data.recommendations || []); const body = meterHtml + histHtml + findingsHtml + notHtml + recHtml; slot.innerHTML = renderPageShell({ eyebrow: 'WATCH', title: data.title || 'Continuous monitoring', lede: data.lede || 'Kjører diff på rekursivt intervall via /loop. Notify ved nye funn.', verdict: data.verdict || inferVerdict(data, 'findings'), keyStats: data.keyStats || inferKeyStats(data, 'findings') }, body); } RENDERERS.renderWatch = renderWatch; function renderRegistry(data, slot) { const stats = data.stats || {}; const statsRows = Object.keys(stats).map(function (k) { return '' + escapeHtml(k.replace(/_/g, ' ')) + '' + escapeHtml(stats[k]) + ''; }).join(''); const statsHtml = statsRows ? ( '

    Registry-stats

    ' + '' + statsRows + '
    ' + '
    ' ) : ''; const sigRows = (data.signatures || []).map(function (s) { const isBad = /known-?bad|malicious/i.test(s.status); const isDrift = /drift/i.test(s.status); const isUnknown = /unknown/i.test(s.status); const sev = isBad ? 'critical' : isDrift ? 'medium' : isUnknown ? 'low' : 'info'; return '' + '' + escapeHtml(s.skill) + '' + '' + escapeHtml(s.source) + '' + '' + escapeHtml(s.fingerprint) + '' + '' + escapeHtml(s.status) + '' + '' + escapeHtml(s.first_seen) + '' + ''; }).join(''); const sigHtml = sigRows ? ( '

    Signaturer

    ' + '' + sigRows + '
    SkillKildeFingerprintStatusFørste sett
    ' + '
    ' ) : ''; const fs = (data.findings || []).map(function (f) { return Object.assign({}, f, { file: f.skill || f.file || '', category: f.category || '' }); }); const findingsHtml = renderFindingsBlock(fs, 'Registry-funn'); const recHtml = renderRecommendationsList(data.recommendations || []); const body = statsHtml + sigHtml + findingsHtml + recHtml; slot.innerHTML = renderPageShell({ eyebrow: 'REGISTRY', title: data.title || 'Skill-signature registry', lede: data.lede || 'Lokal fingerprint-database — kjente goder og kjente onde signaturer.', verdict: data.verdict || inferVerdict(data, 'findings'), keyStats: data.keyStats || inferKeyStats(data, 'findings') }, body); } RENDERERS.renderRegistry = renderRegistry; function renderClean(data, slot) { const buckets = data.buckets || { auto: [], 'semi-auto': [], manual: [], suppressed: [] }; const cardFor = function (bucket, label, sev) { const items = buckets[bucket] || []; const cards = items.length ? items.map(function (it) { return '
    ' + '
    ' + escapeHtml(it.id || '—') + ' — ' + escapeHtml(it.action || '') + '
    ' + (it.description ? '
    ' + escapeHtml(it.description) + '
    ' : '') + '
    '; }).join('') : '
    Ingen
    '; return '
    ' + '
    ' + '' + escapeHtml(label) + '' + '' + items.length + '' + '
    ' + cards + '
    '; }; const kanbanHtml = '
    ' + cardFor('auto', 'Auto', 'low') + cardFor('semi-auto', 'Semi-auto', 'medium') + cardFor('manual', 'Manual', 'high') + cardFor('suppressed', 'Undertrykt', 'info') + '
    '; // Advisory recommendation-cards per bucket — DS Tier 3 data-severity (v7.6.0 fase 5f). // Hver bucket med items > 0 får én recommendation-card med severity-tinted border + label. const bucketAdvisoryDefs = [ { key: 'auto', label: 'Auto-fixable', sev: 'positive', desc: 'Plugin kan fikse disse uten ekstra bekreftelse — deterministiske, lavrisiko-handlinger.' }, { key: 'semi-auto', label: 'Semi-auto — krever bekreftelse', sev: 'medium', desc: 'Foreslåtte tiltak vises som diff. Bruker bekrefter per finding før endring anvendes.' }, { key: 'manual', label: 'Manual remediation', sev: 'high', desc: 'Krever menneskelig vurdering — kontekst, scope eller side-effekter er ikke deterministisk avgjørbare.' }, { key: 'suppressed', label: 'Undertrykt', sev: 'low', desc: 'Allowlist-treff via .llm-security-ignore — ingen handling.' } ]; const advisoryHtml = bucketAdvisoryDefs.map(function (b) { const items = buckets[b.key] || []; if (!items.length) return ''; return ( '
    ' + '' + escapeHtml(b.label) + ' · ' + items.length + '' + '

    ' + escapeHtml(b.desc) + '

    ' + '
    ' ); }).join(''); const findingsHtml = renderFindingsBlock(data.findings || [], 'Tilknyttede funn'); const recHtml = renderRecommendationsList(data.recommendations || [], 'Anbefalinger', 'medium'); const isDry = ((data.mode || '').toLowerCase() === 'dry-run'); const intro = data.mode ? ( '
    ' + 'Modus · ' + escapeHtml(data.mode) + '' + '

    ' + (isDry ? 'Dry-run: ingen filer endres. Forhåndsvis tiltak før --apply.' : 'Fixes anvendes med automatisk backup i .llm-security-backup/.') + '

    ' + '
    ' ) : ''; const body = intro + advisoryHtml + kanbanHtml + findingsHtml + recHtml; slot.innerHTML = renderPageShell({ eyebrow: 'CLEAN', title: data.title || 'Remediation-kanban', lede: data.lede || 'Funn fordelt på Auto / Semi-auto / Manual / Undertrykt.', verdict: data.verdict || inferVerdict(data, 'kanban-buckets'), keyStats: data.keyStats || inferKeyStats(data, 'kanban-buckets') }, body); } RENDERERS.renderClean = renderClean; function renderThreatModel(data, slot) { // Matrix-rendering — 5×5 const cells = data.matrix_cells || []; const byPC = {}; cells.forEach(function (c) { const k = c.prob + '_' + c.cons; if (!byPC[k]) byPC[k] = []; byPC[k].push(c); }); const probSize = 5; const consMax = 5; let matrixHtml = '
    Konsekvens
    '; matrixHtml += '
    '; for (let cons = consMax; cons >= 1; cons--) { matrixHtml += '
    ' + cons + '
    '; for (let prob = 1; prob <= probSize; prob++) { const score = prob * cons; const items = byPC[prob + '_' + cons] || []; // v7.6.1 fix: bobler er nå '; }).join('') + (items.length > 3 ? '' : '') + '
    ' : ''; matrixHtml += '
    ' + '' + score + '' + bubblesHtml + '
    '; } } matrixHtml += '
    '; for (let prob = 1; prob <= probSize; prob++) { matrixHtml += '
    ' + prob + '
    '; } matrixHtml += '
    Sannsynlighet
    '; // Threats table const threatsRows = (data.threats || []).map(function (t) { return '' + '' + escapeHtml(t.id) + '' + '' + escapeHtml(t.description) + '' + ' ' + escapeHtml(t.severity) + '' + '' + escapeHtml(t.mitigation) + '' + ''; }).join(''); const threatsHtml = threatsRows ? ( '

    Trusler

    ' + '' + threatsRows + '
    IDBeskrivelseSeverityTiltak
    ' + '
    ' ) : ''; // STRIDE / MAESTRO coverage as side-by-side bar lists const coverageBlock = function (rows, label) { if (!rows || !rows.length) return ''; const max = Math.max.apply(null, rows.map(function (r) { return Number(r.count) || 0; })) || 1; const items = rows.map(function (r) { const pct = ((Number(r.count) || 0) / max) * 100; const labelKey = r.category || r.layer || ''; return '
    ' + '
    ' + '' + escapeHtml(labelKey) + '' + '' + r.count + '' + '
    ' + '
    ' + (r.notes ? '' + escapeHtml(r.notes) + '' : '') + '
    '; }).join(''); return '

    ' + escapeHtml(label) + '

    ' + items + '
    '; }; const strideHtml = coverageBlock(data.stride, 'STRIDE-dekning'); const maestroHtml = coverageBlock(data.maestro, 'MAESTRO-dekning'); // Roadmap const roadRows = (data.roadmap || []).map(function (r) { return '' + escapeHtml(r.priority) + '' + escapeHtml(r.threat_id) + '' + escapeHtml(r.mitigation) + '' + escapeHtml(r.owner) + '' + escapeHtml(r.eta) + ''; }).join(''); const roadHtml = roadRows ? ( '

    Mitigation roadmap

    ' + '' + roadRows + '
    PrioritetTrusselTiltakEierETA
    ' + '
    ' ) : ''; const recHtml = renderRecommendationsList(data.recommendations || []); const body = matrixHtml + threatsHtml + strideHtml + maestroHtml + roadHtml + recHtml; slot.innerHTML = renderPageShell({ eyebrow: 'THREAT-MODEL', title: data.title || 'Threat model · STRIDE + MAESTRO', lede: data.lede || 'Trusselmodellering med risikomatrise og mitigation-roadmap.', verdict: data.verdict || inferVerdict(data, 'matrix-risk'), keyStats: data.keyStats || inferKeyStats(data, 'matrix-risk') }, body); } RENDERERS.renderThreatModel = renderThreatModel; // ============================================================ // EXPORTS — single block; functions remain top-level declarations // for parity with the inline playground copy. // ============================================================ export { // escape escapeHtml, escapeAttr, // verdict + key-stats inference normalizeVerdict, inferVerdict, KEY_STATS_CONFIG, inferKeyStats, // page-shell helpers renderVerdictPill, renderKeyStatsGrid, renderPageShell, // parser helpers parseTableRow, parseTable, parseAllTables, parseSections, extractField, intOrZero, emptyInput, normalizeSeverity, normalizeVerdictText, gradeFromText, parseRiskDashboard, parseFindingsTables, parseRecommendations, safeOk, parseNarrativeAudit, // 18 parsers parseScan, parseDeepScan, parsePluginAudit, parseMcpAudit, parseMcpInspect, parseIdeScan, parseSupplyCheck, parsePosture, parseAudit, parseDashboard, parsePreDeploy, parseDiff, parseWatch, parseRegistry, parseClean, parseHarden, parseThreatModel, parseRedTeam, // routing maps PARSERS, RENDERERS, // renderer helpers renderEmptyState, renderFindingsBlock, renderRecommendationsList, mapSeverityToCardLevel, renderRiskMeter, renderSmallMultiples, renderRadarSvg, renderToxicFlow, renderMatLadder, renderSuppressedGroup, renderCodepointReveal, renderTopRisks, // 18 renderers renderScan, renderDeepScan, renderPluginAudit, renderMcpAudit, renderIdeScan, renderPosture, renderAudit, renderDashboard, renderHarden, renderRedTeam, renderMcpInspect, renderSupplyCheck, renderPreDeploy, renderDiff, renderWatch, renderRegistry, renderClean, renderThreatModel };