From fbda041522a510208bf8ade4d47910bd1010c654 Mon Sep 17 00:00:00 2001 From: Kjell Tore Guttormsen Date: Wed, 6 May 2026 13:25:35 +0200 Subject: [PATCH] =?UTF-8?q?feat(llm-security):=20playground=20v7.6.0=20fas?= =?UTF-8?q?e=205a-d=20=E2=80=94=20Tier=203=20spesialkomponenter=20(del=201?= =?UTF-8?q?)=20[skip-docs]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Integrer fire llm-security-spesifikke Tier 3-komponenter: - tfa-flow + tfa-leg + tfa-arrow: visualiserer lethal-trifecta-kjede i toxic-flow-rapport (untrusted-input → sensitive-access → exfil-sink) - mat-ladder + mat-step: posture-modenhet over kategorier i posture-rapport - suppressed-group: narrative-audit (v7.1.1) i scan-rapport executive summary - codepoint-reveal + cp-tag: side-ved-side reveal for Unicode-steganografi i mcp-inspect-rapport (visible vs decoded) Endringer: - Fire nye render-helpers (renderToxicFlow, renderMatLadder, renderSuppressedGroup, renderCodepointReveal) i hovedscriptet, plassert før renderScan/Deep/Posture/MCP-Inspect. - parseScan + parseDeepScan utvidet med narrative_audit-felt via ny parseNarrativeAudit-helper som ekstraherer "**Suppressed signals:**"- blokken fra raw_markdown. - renderScan: meterHtml + suppressedHtml + toxicHtml + owaspHtml + ... - renderDeepScan: suppressedHtml + toxicHtml + smHtml + matrixHtml + ... - renderPosture: overall + ladderHtml + smHtml + quickHtml + ... - renderMcpInspect: invHtml + cpHtml (rebuilt via renderCodepointReveal) Verifisert: - tfa-flow=3, mat-ladder=2, suppressed-group=8, codepoint-reveal=12 i HTML - verdict-pill-lg=20, fp-step=12, scope-security=5 (Sesjon 2-kriterier intakte) - form-progress__step strict singular=0 (DS canonical bevart) - Window-globaler intakt (24 unike __-prefiksede globaler) - JS parse OK (node --check), JSON-state parse OK (3 prosjekter, 18 rapporter) - HTML-balanse OK (3 script-tags, 1 style-blokk) - Smoke-test mot demo-data: alle 4 helpers rendrer non-empty HTML med forventede DS-klasser Master-plan: plugins/llm-security/playground/V7.6.0-PLAN.local.md (Sesjon 3 av 5). Sesjon 4 (fase 5e-h: top-risks, recommendation-card, risk-meter, card--severity-*) neste, deretter Sesjon 5 (verifisering, docs, release). Co-Authored-By: Claude Opus 4.7 --- .../playground/llm-security-playground.html | 303 +++++++++++++++++- 1 file changed, 290 insertions(+), 13 deletions(-) diff --git a/plugins/llm-security/playground/llm-security-playground.html b/plugins/llm-security/playground/llm-security-playground.html index cf40e51..d827f0a 100644 --- a/plugins/llm-security/playground/llm-security-playground.html +++ b/plugins/llm-security/playground/llm-security-playground.html @@ -7717,6 +7717,39 @@ // { ok: false, errors: [{ section, reason }] } // ============================================================ + /** + * 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 }; + } + const parseScan = safeOk(function (md) { const dash = parseRiskDashboard(md); const findings = parseFindingsTables(md); @@ -7740,11 +7773,13 @@ }; }) : []; 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) }) }; }); @@ -7790,11 +7825,15 @@ 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) }) }; }); @@ -8882,12 +8921,256 @@ ); } + // ============================================================ + // 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 + '
' + + '
' + ); + } + // ============================================================ // 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

' + '' + @@ -8908,7 +9191,7 @@ ) : ''; const findingsHtml = renderFindingsBlock(data.findings || [], 'Funn'); const recHtml = renderRecommendationsList(data.recommendations || []); - const body = meterHtml + owaspHtml + supplyHtml + findingsHtml + recHtml; + const body = meterHtml + suppressedHtml + toxicHtml + owaspHtml + supplyHtml + findingsHtml + recHtml; slot.innerHTML = renderPageShell({ eyebrow: 'SKANNING', title: data.title || 'Security Scan', @@ -8950,7 +9233,9 @@ ) : ''; const findingsHtml = renderFindingsBlock(data.findings || [], 'Findings (utvalg)'); const recHtml = renderRecommendationsList(data.recommendations || []); - const body = smHtml + matrixHtml + findingsHtml + recHtml; + const suppressedHtml = renderSuppressedGroup(data); + const toxicHtml = renderToxicFlow(data.findings || []); + const body = suppressedHtml + toxicHtml + smHtml + matrixHtml + findingsHtml + recHtml; slot.innerHTML = renderPageShell({ eyebrow: 'DEEP-SCAN', title: data.title || 'Deterministisk deep-scan', @@ -9117,6 +9402,7 @@ }; }); 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) ? ( '
' + @@ -9131,7 +9417,7 @@ const overall = data.posture_score != null ? ( '

Overall score

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

' ) : ''; - const body = overall + smHtml + quickHtml + findingsHtml + recHtml; + const body = overall + ladderHtml + smHtml + quickHtml + findingsHtml + recHtml; slot.innerHTML = renderPageShell({ eyebrow: 'POSTURE', title: data.title || 'Security posture', @@ -9346,16 +9632,7 @@ '
KategoriFunnMaks severitySkannere
' + invRows + '
ServerTransportToolsStatusConnected
' + '
' ) : ''; - const cpRows = (data.codepoints || []).map(function (c) { - const sev = /high/i.test(c.risk) ? 'critical' : /medium/i.test(c.risk) ? 'medium' : 'low'; - return '' + escapeHtml(c.server) + '' + escapeHtml(c.tool) + '' + escapeHtml(c.codepoints) + '' + escapeHtml(c.risk) + ''; - }).join(''); - const cpHtml = cpRows ? ( - '

Codepoint-reveal

' + - '

Tools med non-ASCII codepoints i deskripsjoner — zero-width / homoglyph-kandidater.

' + - '' + cpRows + '
ServerToolCodepointsRisk
' + - '
' - ) : ''; + const cpHtml = renderCodepointReveal(data.codepoints || []); const fs = (data.findings || []).map(function (f) { return Object.assign({}, f, { file: f.server || f.file || '',