feat(llm-security): playground v7.6.0 fase 5e-h — Tier 3 spesialkomponenter (del 2) [skip-docs]
- top-risks + top-risk: rangert top-funn-listing per rapport
(renderTopRisks helper, integrert i renderScan, renderDeepScan,
renderPluginAudit, renderPosture, renderAudit — ekskluderer info-funn,
default 5 toppfunn med data-severity-tinted left-border)
- recommendation-card: data-severity-attributtet utvidet på alle
inline-bruk (Trust-verdict, Quick wins, Action plan tiers, Vilkår)
pluss /security clean (per-bucket advisory-cards) og /security harden
(intro snapshot + per-recommendation diff-cards med action-type-mapping
CREATE→positive / APPEND→medium / MERGE→low / SKIP→low)
- risk-meter: lagt til på renderDeepScan og renderAudit conditional på
data.risk_score — utvider eksisterende bruk (renderScan, renderPluginAudit,
renderRedTeam) til 5 archetypes
- card--severity-{level}: severity-color border-modifier på .findings__item
i renderFindingsBlock (delt helper) pluss inline-bruk i renderAudit
category-cards og renderDiff row-items
Ny helper-funksjon mapSeverityToCardLevel(input) normaliserer severity-
strenger og action-types til DS Tier 3-konvensjonene
(critical/high/medium/low/positive). renderRecommendationsList får valgfri
severity-param som default fall-back til 'low'.
Verifisering bekreftet:
- top-risks: 5 forekomster (≥1 ✓)
- recommendation-card: 32 (≥1 ✓ — utvidet fra 4)
- risk-meter: 7 (≥3 ✓ — 5 archetypes bruker helper)
- card--severity-: 4 (≥4 ✓ — findings__item + 2 inline-steder)
- Sesjon 2-3 anker intakte (verdict-pill-lg 20, fp-step 12,
badge--scope-security 5, tfa-flow 3, mat-ladder 2, suppressed-group 8,
codepoint-reveal 12)
- Window-globaler intakt
- JS parse: OK (node --check på ekstrahert main JS)
- demo-state JSON parse: OK (3 prosjekter, 18 rapporter)
- HTML-balanse: 3 script / 3 /script / 1 style
- Smoke-test mot demo-data: 5/7 renderere viser komplett markup;
renderDeepScan og renderAudit har tomme findings-arrays i demo så
top-risks/card--severity rendrer korrekt tomt (defensiv design,
bevisst per Sesjon 3 observasjon 2)
Filendring: 10545 → 10677 linjer (+132 netto).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
fbda041522
commit
e9e5ceebfb
1 changed files with 162 additions and 30 deletions
|
|
@ -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 (
|
||||
'<div class="findings__item">' +
|
||||
'<div class="findings__item-severity-dot" data-severity="' + escapeAttr(f.severity || 'info') + '"></div>' +
|
||||
'<div class="findings__item ' + sevClass + '" data-severity="' + escapeAttr(sev) + '">' +
|
||||
'<div class="findings__item-severity-dot" data-severity="' + escapeAttr(sev) + '"></div>' +
|
||||
'<div>' +
|
||||
'<div class="findings__item-id">' + escapeHtml(f.id || '—') + '</div>' +
|
||||
'<div class="findings__item-title">' + escapeHtml(f.description || f.title || '') + '</div>' +
|
||||
|
|
@ -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 '<li>' + escapeHtml(r) + '</li>'; }).join('');
|
||||
return (
|
||||
'<section class="recommendation-card">' +
|
||||
'<section class="recommendation-card" data-severity="' + escapeAttr(sev) + '">' +
|
||||
'<span class="recommendation-card__label">' + escapeHtml(label || 'Anbefalinger') + '</span>' +
|
||||
'<ol class="recommendation-card__body">' + items + '</ol>' +
|
||||
'</section>'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 (
|
||||
'<li class="top-risk" data-severity="' + escapeAttr(sev) + '">' +
|
||||
'<div class="top-risk__rank">' + (idx + 1) + '</div>' +
|
||||
'<div class="top-risk__desc">' +
|
||||
'<div>' + escapeHtml(title) + '</div>' +
|
||||
(meta ? '<div style="font-family: var(--font-family-mono); font-size: 11px; color: var(--color-text-tertiary); margin-top: 2px;">' + escapeHtml(meta) + '</div>' : '') +
|
||||
'</div>' +
|
||||
'<span class="top-risk__score" data-severity="' + escapeAttr(sev) + '">' + escapeHtml(sevLabel) + '</span>' +
|
||||
'</li>'
|
||||
);
|
||||
}).join('');
|
||||
return (
|
||||
'<section class="report-meta">' +
|
||||
'<h4 class="top-risks__heading">Top ' + top.length + ' risks</h4>' +
|
||||
'<ol class="top-risks">' + items + '</ol>' +
|
||||
'</section>'
|
||||
);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 10 RENDERERS — én per høy-prio kommando.
|
||||
// ============================================================
|
||||
|
|
@ -9189,9 +9272,10 @@
|
|||
'</tbody></table>' +
|
||||
'</section>'
|
||||
) : '';
|
||||
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 + '</tbody></table>' +
|
||||
'</section>'
|
||||
) : '';
|
||||
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 @@
|
|||
'</tbody></table>' +
|
||||
'</section>'
|
||||
) : '';
|
||||
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 ? (
|
||||
'<section class="recommendation-card">' +
|
||||
'<section class="recommendation-card" data-severity="' + escapeAttr(trustSev) + '">' +
|
||||
'<span class="recommendation-card__label">Trust-verdict</span>' +
|
||||
'<p class="recommendation-card__body">' + escapeHtml(data.trust_verdict_text).replace(/\n/g, '<br>') + '</p>' +
|
||||
'</section>'
|
||||
) : '';
|
||||
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) ? (
|
||||
'<section class="recommendation-card">' +
|
||||
'<section class="recommendation-card" data-severity="positive">' +
|
||||
'<span class="recommendation-card__label">Quick wins</span>' +
|
||||
'<ol class="recommendation-card__body">' +
|
||||
data.quick_wins.map(function (w) { return '<li>' + escapeHtml(w) + '</li>'; }).join('') +
|
||||
'</ol>' +
|
||||
'</section>'
|
||||
) : '';
|
||||
const topRisksHtml = renderTopRisks(data.findings || [], 5);
|
||||
const findingsHtml = renderFindingsBlock(data.findings || [], 'Top findings');
|
||||
const recHtml = renderRecommendationsList(data.recommendations || []);
|
||||
const overall = data.posture_score != null ? (
|
||||
'<section class="report-meta"><h4>Overall score</h4><p><strong>' + data.posture_score + ' / ' + (data.posture_applicable || '?') + ' kategorier dekket</strong> — Grade ' + escapeHtml(data.grade || '?') + '.</p></section>'
|
||||
) : '';
|
||||
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 @@
|
|||
'<section class="report-meta"><h4>Kategori-vurdering</h4>' +
|
||||
'<div class="findings__items">' + data.categories.map(function (c) {
|
||||
const sev = c.status === 'FAIL' ? 'critical' : c.status === 'PARTIAL' ? 'medium' : c.status === 'PASS' ? 'low' : 'info';
|
||||
return '<div class="findings__item">' +
|
||||
const sevClass = 'card--severity-' + sev;
|
||||
return '<div class="findings__item ' + sevClass + '" data-severity="' + escapeAttr(sev) + '">' +
|
||||
'<div class="findings__item-severity-dot" data-severity="' + escapeAttr(sev) + '"></div>' +
|
||||
'<div>' +
|
||||
'<div class="findings__item-id">Kat. ' + c.num + '</div>' +
|
||||
|
|
@ -9447,17 +9543,19 @@
|
|||
'</section>'
|
||||
) : '';
|
||||
// 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 '<section class="recommendation-card">' +
|
||||
return '<section class="recommendation-card" data-severity="' + escapeAttr(sev) + '">' +
|
||||
'<span class="recommendation-card__label">' + escapeHtml(label) + '</span>' +
|
||||
'<ol class="recommendation-card__body">' + items.map(function (a) { return '<li>' + escapeHtml(a) + '</li>'; }).join('') + '</ol>' +
|
||||
'</section>';
|
||||
};
|
||||
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 (
|
||||
'<div class="diff__row">' +
|
||||
'<div class="diff__cell ' + cellClass + '">' +
|
||||
'<strong>' + r.num + '. ' + escapeHtml(r.category) + '</strong> — <code>' + escapeHtml(r.file) + '</code>' +
|
||||
'<div class="diff__cell"><strong>Action:</strong> ' + actionLabel + '</div>' +
|
||||
'<section class="recommendation-card" data-severity="' + escapeAttr(sev) + '">' +
|
||||
'<span class="recommendation-card__label">' + actionLabel + ' · ' + escapeHtml(String(r.num)) + '. ' + escapeHtml(r.category) + '</span>' +
|
||||
'<div class="recommendation-card__body">' +
|
||||
'<div><code>' + escapeHtml(r.file) + '</code></div>' +
|
||||
(r.content_preview ? '<pre style="margin: var(--space-2) 0; font-size: var(--font-size-sm); white-space: pre-wrap; opacity: ' + (isNone ? '0.6' : '0.9') + '">' + escapeHtml(r.content_preview).slice(0, 600) + (r.content_preview.length > 600 ? '…' : '') + '</pre>' : '') +
|
||||
'</div>' +
|
||||
'</div>'
|
||||
'</section>'
|
||||
);
|
||||
}).join('');
|
||||
// Diff summary footer
|
||||
|
|
@ -9545,12 +9645,21 @@
|
|||
return '<div class="diff__summary-item"><span>' + escapeHtml(d.file) + '</span><span class="diff__summary-count">' + escapeHtml(d.action) + ' · ' + escapeHtml(d.lines) + '</span></div>';
|
||||
}).join('');
|
||||
const summaryHtml = summaryRows ? '<div class="diff__summary">' + summaryRows + '</div>' : '';
|
||||
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 = (
|
||||
'<section class="report-meta"><h4>Snapshot</h4>' +
|
||||
'<p>Prosjekt-type: <strong>' + escapeHtml(data.project_type || '?') + '</strong> · Nåværende grade: <strong>' + escapeHtml(data.current_grade || '?') + '</strong> · ' + data.actionable + '/' + data.total + ' anbefalinger · Modus: <em>' + escapeHtml(data.mode || 'dry-run') + '</em></p>' +
|
||||
'<section class="recommendation-card" data-severity="' + escapeAttr(introSev) + '">' +
|
||||
'<span class="recommendation-card__label">Snapshot · grade ' + escapeHtml(data.current_grade || '?') + '</span>' +
|
||||
'<p class="recommendation-card__body">Prosjekt-type: <strong>' + escapeHtml(data.project_type || '?') + '</strong> · ' + data.actionable + '/' + data.total + ' anbefalinger · Modus: <em>' + escapeHtml(data.mode || 'dry-run') + '</em></p>' +
|
||||
'</section>'
|
||||
);
|
||||
const body = intro + (diffHtml ? '<div class="diff">' + diffHtml + '</div>' : 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 ? '<section class="report-meta"><h4>Traffic-light kategorier</h4><div class="small-multiples">' + cards + '</div></section>' : '';
|
||||
const condHtml = (data.conditions && data.conditions.length) ? (
|
||||
'<section class="recommendation-card">' +
|
||||
'<section class="recommendation-card" data-severity="high">' +
|
||||
'<span class="recommendation-card__label">Vilkår å løse</span>' +
|
||||
'<ol class="recommendation-card__body">' + data.conditions.map(function (c) { return '<li>' + escapeHtml(c) + '</li>'; }).join('') + '</ol>' +
|
||||
'</section>'
|
||||
|
|
@ -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 '<div class="diff__row">' +
|
||||
'<div class="diff__cell ' + cellClass + '">' +
|
||||
'<div class="findings__item">' +
|
||||
'<div class="findings__item ' + sevClass + '" data-severity="' + escapeAttr(sev) + '">' +
|
||||
'<div class="findings__item-severity-dot" data-severity="' + escapeAttr(sev) + '"></div>' +
|
||||
'<div>' +
|
||||
'<div class="findings__item-id">' + escapeHtml(it.id || '—') + '</div>' +
|
||||
|
|
@ -9911,12 +10021,34 @@
|
|||
cardFor('manual', 'Manual', 'high') +
|
||||
cardFor('suppressed', 'Undertrykt', 'info') +
|
||||
'</div>';
|
||||
// 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 (
|
||||
'<section class="recommendation-card" data-severity="' + escapeAttr(b.sev) + '">' +
|
||||
'<span class="recommendation-card__label">' + escapeHtml(b.label) + ' · ' + items.length + '</span>' +
|
||||
'<p class="recommendation-card__body">' + escapeHtml(b.desc) + '</p>' +
|
||||
'</section>'
|
||||
);
|
||||
}).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 ? (
|
||||
'<section class="report-meta"><h4>Modus</h4><p><strong>' + escapeHtml(data.mode) + '</strong> — ' + ((data.mode || '').toLowerCase() === 'dry-run' ? 'ingen filer endres.' : 'fixes anvendes med backup.') + '</p></section>'
|
||||
'<section class="recommendation-card" data-severity="' + (isDry ? 'low' : 'medium') + '">' +
|
||||
'<span class="recommendation-card__label">Modus · ' + escapeHtml(data.mode) + '</span>' +
|
||||
'<p class="recommendation-card__body">' + (isDry ? 'Dry-run: ingen filer endres. Forhåndsvis tiltak før <code>--apply</code>.' : 'Fixes anvendes med automatisk backup i <code>.llm-security-backup/</code>.') + '</p>' +
|
||||
'</section>'
|
||||
) : '';
|
||||
const body = intro + kanbanHtml + findingsHtml + recHtml;
|
||||
const body = intro + advisoryHtml + kanbanHtml + findingsHtml + recHtml;
|
||||
slot.innerHTML = renderPageShell({
|
||||
eyebrow: 'CLEAN',
|
||||
title: data.title || 'Remediation-kanban',
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue