diff --git a/plugins/llm-security/playground/llm-security-playground.html b/plugins/llm-security/playground/llm-security-playground.html index d827f0a..7653917 100644 --- a/plugins/llm-security/playground/llm-security-playground.html +++ b/plugins/llm-security/playground/llm-security-playground.html @@ -8798,14 +8798,20 @@ 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 || '') + '
' + @@ -8822,17 +8828,46 @@ ); } - function renderRecommendationsList(recs, label) { + /** + * 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 = [ @@ -9163,6 +9198,54 @@ ); } + /** + * 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. // ============================================================ @@ -9189,9 +9272,10 @@ '' + '
    ' ) : ''; + const topRisksHtml = renderTopRisks(data.findings || [], 5); const findingsHtml = renderFindingsBlock(data.findings || [], 'Funn'); const recHtml = renderRecommendationsList(data.recommendations || []); - const body = meterHtml + suppressedHtml + toxicHtml + owaspHtml + supplyHtml + findingsHtml + recHtml; + const body = meterHtml + suppressedHtml + toxicHtml + topRisksHtml + owaspHtml + supplyHtml + findingsHtml + recHtml; slot.innerHTML = renderPageShell({ eyebrow: 'SKANNING', title: data.title || 'Security Scan', @@ -9231,11 +9315,13 @@ matrixRows + '' + '' ) : ''; + 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 = suppressedHtml + toxicHtml + smHtml + matrixHtml + findingsHtml + recHtml; + const body = meterHtml + suppressedHtml + toxicHtml + smHtml + matrixHtml + topRisksHtml + findingsHtml + recHtml; slot.innerHTML = renderPageShell({ eyebrow: 'DEEP-SCAN', title: data.title || 'Deterministisk deep-scan', @@ -9273,15 +9359,23 @@ '' + '' ) : ''; + 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 + findingsHtml + recHtml; + 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', @@ -9405,19 +9499,20 @@ 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 + findingsHtml + recHtml; + const body = overall + ladderHtml + smHtml + quickHtml + topRisksHtml + findingsHtml + recHtml; slot.innerHTML = renderPageShell({ eyebrow: 'POSTURE', title: data.title || 'Security posture', @@ -9435,7 +9530,8 @@ '

    Kategori-vurdering

    ' + '
    ' + data.categories.map(function (c) { const sev = c.status === 'FAIL' ? 'critical' : c.status === 'PARTIAL' ? 'medium' : c.status === 'PASS' ? 'low' : 'info'; - return '
    ' + + const sevClass = 'card--severity-' + sev; + return '
    ' + '
    ' + '
    ' + '
    Kat. ' + c.num + '
    ' + @@ -9447,17 +9543,19 @@ '
    ' ) : ''; // Action Plan tre-tier - const tierHtml = function (tier, label) { + const tierHtml = function (tier, label, sev) { const items = (data.action_plan && data.action_plan[tier]) || []; if (!items.length) return ''; - return '
    ' + + return '
    ' + '' + escapeHtml(label) + '' + '
      ' + items.map(function (a) { return '
    1. ' + escapeHtml(a) + '
    2. '; }).join('') + '
    ' + '
    '; }; - const actionHtml = tierHtml('immediate', 'Umiddelbar') + tierHtml('high', 'Høy prioritet') + tierHtml('medium', 'Medium prioritet'); + 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 = radarHtml + catHtml + actionHtml + findingsHtml; + const body = meterHtml + radarHtml + catHtml + actionHtml + topRisksHtml + findingsHtml; slot.innerHTML = renderPageShell({ eyebrow: 'AUDIT', title: data.title || 'Full security audit', @@ -9522,22 +9620,24 @@ function renderHarden(data, slot) { const recs = data.recommendations || []; - // Diff-blokker per recommendation + // 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 cellClass = isCreate ? 'diff__cell--added' : (isAppend ? 'diff__cell--added' : (isMerge ? 'diff__cell--unchanged' : 'diff__cell--unchanged')); const actionLabel = isCreate ? 'CREATE' : isAppend ? 'APPEND' : isMerge ? 'MERGE' : 'SKIP'; + const sev = mapSeverityToCardLevel(actionLabel); return ( - '
    ' + - '
    ' + - '' + r.num + '. ' + escapeHtml(r.category) + '' + escapeHtml(r.file) + '' + - '
    Action: ' + actionLabel + '
    ' + + '
    ' + + '' + 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 @@ -9545,12 +9645,21 @@ 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

    ' + - '

    Prosjekt-type: ' + escapeHtml(data.project_type || '?') + ' · Nåværende grade: ' + escapeHtml(data.current_grade || '?') + ' · ' + data.actionable + '/' + data.total + ' anbefalinger · Modus: ' + escapeHtml(data.mode || 'dry-run') + '

    ' + + '
    ' + + '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 ? '
    ' + diffHtml + '
    ' : renderEmptyState('Ingen anbefalinger.')) + summaryHtml; + const body = intro + (diffHtml || renderEmptyState('Ingen anbefalinger.')) + summaryHtml; slot.innerHTML = renderPageShell({ eyebrow: 'HARDEN', title: data.title || 'Grade A reference config', @@ -9703,7 +9812,7 @@ }).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('') + '
    ' + '
    ' @@ -9755,13 +9864,14 @@ ); 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 || '—') + '
    ' + @@ -9911,12 +10021,34 @@ 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 || []); + const recHtml = renderRecommendationsList(data.recommendations || [], 'Anbefalinger', 'medium'); + const isDry = ((data.mode || '').toLowerCase() === 'dry-run'); const intro = data.mode ? ( - '

    Modus

    ' + escapeHtml(data.mode) + ' — ' + ((data.mode || '').toLowerCase() === 'dry-run' ? 'ingen filer endres.' : 'fixes anvendes med backup.') + '

    ' + '
    ' + + '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 + kanbanHtml + findingsHtml + recHtml; + const body = intro + advisoryHtml + kanbanHtml + findingsHtml + recHtml; slot.innerHTML = renderPageShell({ eyebrow: 'CLEAN', title: data.title || 'Remediation-kanban',