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 ( + '' + ); + } + + /** + * 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' ? ( + '' + escapeHtml(c.tool || '—') + '' +
+ headRisk +
+ '