chore(llm-security): v7.7.2 — language consistency pass
~/.claude/CLAUDE.md specifies English for code and documentation, Norwegian for dialog only. Norwegian had crept into surface text across v7.5-v7.7. Translated to English in eight surfaces. No scanner, hook, or behavior changes — purely surface text. - 18 skill commands: the HTML Report-step now reads "HTML report: [Open in browser]" instead of "HTML-rapport: [Åpne i nettleser]" - scripts/lib/report-renderers.mjs: key-stat labels, lede defaults, table headers, maturity-ladder descriptions, action-tier labels, clean buckets, dry-run/apply copy, and JS comments. Regex alternations /^high|^høy/ and /resolution|løsning/i preserved. - playground/llm-security-playground.html: same renderer changes mirrored bit-identical, plus playground-only UI strings (catalog, breadcrumb aria-label, theme toggle, builder-modal hint, guide-panel "no projects yet", delete confirmation, alert/copy). Demo-state fixture content for dft-komplett-demo preserved (intentional Norwegian persona). - agents/skill-scanner-agent.md + agents/mcp-scanner-agent.md: Generaliseringsgrense + Parallell Read-strategi sections translated to Generalization boundary + Parallel Read strategy. - README.md: playground architecture prose + Recent versions table (v7.5.0 — v7.7.1). - CLAUDE.md: v7.7.1 highlights translated, new v7.7.2 highlights added. - ../../README.md: llm-security v7.5.0 — v7.7.1 bullets. - ../../CLAUDE.md: llm-security catalog entry. - docs/scanner-reference.md: six runnable-examples table cells. - docs/version-history.md: new v7.7.2 entry. v7.5-v7.7 narrative sections left in original language (deferred per operator). - Version bumped 7.7.1 → 7.7.2 in package.json, .claude-plugin/plugin.json, README badge + Recent versions, CLAUDE.md header + state, docs/version-history.md, playground renderHome hardcoded string, root README + CLAUDE.md llm-security entries. Tests: 1820/1820 green. CLI smoke-test: 18/18 commandIds produce >138 KB self-contained HTML. Browser-dogfood verified. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
4f6fc4a2a5
commit
03b8885b6e
31 changed files with 467 additions and 359 deletions
|
|
@ -79,7 +79,7 @@ function renderKeyStatsGrid(stats) {
|
|||
}
|
||||
|
||||
/**
|
||||
* Render page-shell — DS Tier 3 page__header-klyngen brukt på alle 4 overflater:
|
||||
* Render the page-shell — the DS Tier 3 page__header cluster used on all 4 surfaces:
|
||||
* - onboarding: page__eyebrow="ONBOARDING · n av 5 grupper komplette"
|
||||
* - home: page__eyebrow="HJEM" (m/ hero-modifier for editorial type-hierarki)
|
||||
* - catalog: page__eyebrow="KATALOG"
|
||||
|
|
@ -197,16 +197,16 @@ const KEY_STATS_CONFIG = {
|
|||
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 }
|
||||
{ label: 'TOTAL', value: fs.length },
|
||||
{ label: 'CRITICAL', value: crit, modifier: crit > 0 ? 'critical' : null },
|
||||
{ label: 'HIGH', 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 });
|
||||
if (d.findings) out.push({ label: 'FINDINGS', value: d.findings.length });
|
||||
return out;
|
||||
},
|
||||
'risk-score-meter': function (d) {
|
||||
|
|
@ -220,16 +220,16 @@ const KEY_STATS_CONFIG = {
|
|||
},
|
||||
'red-team-results': function (d) {
|
||||
return [
|
||||
{ label: 'TOTALT', value: d.total || 0 },
|
||||
{ label: 'TOTAL', 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 || '–' }
|
||||
{ label: 'PROJECTS', value: (d.projects || []).length },
|
||||
{ label: 'MACHINE GRADE', value: String(d.machine_grade || 'n/a').toUpperCase() },
|
||||
{ label: 'WEAKEST', value: d.weakest_link || '–' }
|
||||
];
|
||||
},
|
||||
'posture-cards': function (d) {
|
||||
|
|
@ -246,8 +246,8 @@ const KEY_STATS_CONFIG = {
|
|||
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: 'CURRENT GRADE', value: String(d.current_grade || '?').toUpperCase() },
|
||||
{ label: 'ACTIONS', value: newCount, modifier: newCount > 0 ? 'medium' : 'low' },
|
||||
{ label: 'SKIPPED', value: unchangedCount }
|
||||
];
|
||||
},
|
||||
|
|
@ -435,7 +435,7 @@ function gradeFromText(s) {
|
|||
return m ? m[1] : null;
|
||||
}
|
||||
|
||||
// Hjelper: parse Risk Dashboard-tabellen (fellesmønster)
|
||||
// Helper: parse the Risk Dashboard table (shared pattern)
|
||||
function parseRiskDashboard(md) {
|
||||
const out = {};
|
||||
const score = extractField(md, 'Risk Score');
|
||||
|
|
@ -486,7 +486,7 @@ function parseFindingsTables(md) {
|
|||
});
|
||||
if (!findingsSection) return findings;
|
||||
const body = findingsSection.body;
|
||||
// Splitt på ### -headere
|
||||
// Split on ### headers
|
||||
const subRe = /^###\s+(.+)$/gm;
|
||||
const matches = [];
|
||||
let m;
|
||||
|
|
@ -578,7 +578,7 @@ function parseNarrativeAudit(md) {
|
|||
}
|
||||
|
||||
// ============================================================
|
||||
// 10 PARSERS — én per høy-prio kommando.
|
||||
// 10 PARSERS — one per high-priority command.
|
||||
// Returner { ok: true, data: { ...domain-specific } } eller
|
||||
// { ok: false, errors: [{ section, reason }] }
|
||||
// ============================================================
|
||||
|
|
@ -1047,7 +1047,7 @@ const parseDashboard = safeOk(function (md) {
|
|||
});
|
||||
}
|
||||
}
|
||||
// Weakest link = første prosjekt sortert worst-first (allerede sortert i fixture)
|
||||
// Weakest link = first project, sorted worst-first (already sorted in the fixture)
|
||||
const weakest = projects.length ? projects[0].name : '';
|
||||
return { ok: true, data: Object.assign({}, dash, {
|
||||
machine_grade: machine_grade,
|
||||
|
|
@ -1195,8 +1195,8 @@ const parseRedTeam = safeOk(function (md) {
|
|||
});
|
||||
|
||||
// ============================================================
|
||||
// FASE 3: 8 PARSERS — én per gjenstående produces_report-kommando.
|
||||
// Mønstre gjenbrukes fra Fase 2 (parseRiskDashboard + parseFindingsTables
|
||||
// PHASE 3: 8 PARSERS — one per remaining produces_report command.
|
||||
// Patterns are reused from Phase 2 (parseRiskDashboard + parseFindingsTables
|
||||
// + safeOk). Matrix-risk-parsing er kopiert fra ms-ai-architect.
|
||||
// ============================================================
|
||||
const parseMcpInspect = safeOk(function (md) {
|
||||
|
|
@ -1614,7 +1614,7 @@ function renderEmptyState(message) {
|
|||
return '<div class="guide-panel guide-panel--info">' +
|
||||
'<div class="guide-panel__icon" aria-hidden="true">i</div>' +
|
||||
'<div class="guide-panel__body">' +
|
||||
'<p class="guide-panel__text">' + escapeHtml(message || 'Ingen data å vise.') + '</p>' +
|
||||
'<p class="guide-panel__text">' + escapeHtml(message || 'No data to display.') + '</p>' +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
}
|
||||
|
|
@ -1627,7 +1627,7 @@ function renderFindingsBlock(findings, label) {
|
|||
});
|
||||
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
|
||||
// DS Tier 3 (v7.6.0 phase 5h): card--severity-{level} modifier on the 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).
|
||||
|
|
@ -1649,8 +1649,8 @@ function renderFindingsBlock(findings, label) {
|
|||
);
|
||||
}).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.
|
||||
// the playground uses only the list part, so we wrap in .findings__list (no outer
|
||||
// .findings) to avoid the header landing in the left 360px column. v7.6.1 fix.
|
||||
return (
|
||||
'<section class="report-meta">' +
|
||||
'<h4>' + escapeHtml(label || 'Funn') + '</h4>' +
|
||||
|
|
@ -1684,7 +1684,7 @@ function renderRecommendationsList(recs, label, severity) {
|
|||
|
||||
/**
|
||||
* Map severity-string til DS-tier3 recommendation-card data-severity.
|
||||
* Aksepterer både severity-konvensjoner (critical/high/medium/low/info)
|
||||
* Accepts both severity conventions (critical/high/medium/low/info)
|
||||
* og action-types (CREATE/APPEND/MERGE/SKIP/NONE).
|
||||
*/
|
||||
function mapSeverityToCardLevel(input) {
|
||||
|
|
@ -1753,9 +1753,9 @@ function renderSmallMultiples(items) {
|
|||
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.
|
||||
// v7.6.1 fix: widen the SVG from 280 to 380 and r from 105 to 125 to give
|
||||
// labels more room. Use text-anchor based on horizontal position to keep
|
||||
// bottom labels from overlapping each other at 6+ axes.
|
||||
const size = 380, cx = size / 2, cy = size / 2, r = 125;
|
||||
const n = axes.length;
|
||||
const axisRows = axes.map(function (a) {
|
||||
|
|
@ -1766,7 +1766,7 @@ function renderRadarSvg(axes) {
|
|||
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.
|
||||
// Pick text-anchor based on position: left/right anchors flip.
|
||||
const dx = Math.cos(ang);
|
||||
const anchor = Math.abs(dx) < 0.2 ? 'middle' : (dx > 0 ? 'start' : 'end');
|
||||
return '<text class="radar__label" x="' + lx.toFixed(1) + '" y="' + ly.toFixed(1) + '" text-anchor="' + anchor + '" dominant-baseline="middle">' + escapeHtml(a.name) + '</text>';
|
||||
|
|
@ -1804,7 +1804,7 @@ function renderRadarSvg(axes) {
|
|||
|
||||
/**
|
||||
* Render tfa-flow + tfa-leg + tfa-arrow for et lethal trifecta-funn.
|
||||
* Brukes på scan + deep-scan-rapporter når findings inneholder
|
||||
* Used on scan + deep-scan reports when findings contain
|
||||
* en trifecta-pattern (f.eks. SCN-002 "Lethal trifecta: [Bash, Read, WebFetch]").
|
||||
* Synthesiserer 3-leddet kjede: untrusted-input → sensitive-access → exfil-sink.
|
||||
*/
|
||||
|
|
@ -1835,7 +1835,7 @@ function renderToxicFlow(findings) {
|
|||
}
|
||||
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: 'Sensitive access', name: tools[1], source: '.env / credentials / git-history', mit: 'unmitigated', mitText: 'No pre-write-pathguard on path' },
|
||||
{ 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) {
|
||||
|
|
@ -1874,13 +1874,13 @@ function renderMatLadder(categories, postureScore, postureApplicable) {
|
|||
? 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
|
||||
// 5 maturity steps — thresholds based on % 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.' }
|
||||
{ num: 1, name: 'Initial', threshold: 0, desc: 'Bare bones — no hooks or minimal posture.' },
|
||||
{ num: 2, name: 'Aware', threshold: 25, desc: 'Posture scanning is active and the risks are known.' },
|
||||
{ num: 3, name: 'Defensive', threshold: 50, desc: 'Hooks engaged on critical surfaces (PreToolUse, UserPromptSubmit).' },
|
||||
{ num: 4, name: 'Mature', threshold: 75, desc: 'Most of the 16 categories covered; trifecta detection on.' },
|
||||
{ num: 5, name: 'Optimized', threshold: 95, desc: 'Full coverage; A-grade on posture; active monitoring.' }
|
||||
];
|
||||
const currentIdx = steps.reduce(function (acc, s, i) {
|
||||
return pct >= s.threshold ? i : acc;
|
||||
|
|
@ -1890,7 +1890,7 @@ function renderMatLadder(categories, postureScore, postureApplicable) {
|
|||
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 pillText = state === 'current' ? 'You are here' : state === 'completed' ? 'Reached' : '';
|
||||
const pill = pillText ? '<span class="' + pillCls.trim() + '">' + escapeHtml(pillText) + '</span>' : '';
|
||||
const progress = state === 'current' ? (
|
||||
'<div class="mat-step__progress">' +
|
||||
|
|
@ -1912,7 +1912,7 @@ function renderMatLadder(categories, postureScore, postureApplicable) {
|
|||
return (
|
||||
'<section class="report-meta">' +
|
||||
'<h4>Modenhetsstige — posture-progresjon</h4>' +
|
||||
'<p style="font-size: var(--font-size-sm); opacity: 0.78; margin: 0 0 var(--space-3);">Posture-score på ' + passCount + ' av ' + total + ' kategorier (' + pct + '%) plasserer dette prosjektet på trinn ' + (currentIdx + 1) + ' av 5.</p>' +
|
||||
'<p style="font-size: var(--font-size-sm); opacity: 0.78; margin: 0 0 var(--space-3);">A posture score of ' + passCount + ' of ' + total + ' categories (' + pct + '%) places this project at step ' + (currentIdx + 1) + ' of 5.</p>' +
|
||||
'<div class="mat-ladder" role="list" aria-label="Posture-modenhet over 5 trinn">' + stepHtml + '</div>' +
|
||||
'</section>'
|
||||
);
|
||||
|
|
@ -1973,7 +1973,7 @@ function renderSuppressedGroup(data) {
|
|||
return (
|
||||
'<section class="report-meta">' +
|
||||
'<h4>Narrative audit — supprimerte signaler</h4>' +
|
||||
'<p class="suppressed-group__desc">' + 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.</p>' +
|
||||
'<p class="suppressed-group__desc">' + totalCount + ' signals were suppressed pre-report (v7.1.1 narrative_audit). These are not false-positives walked back in prose — they were auto-suppressed before classification.</p>' +
|
||||
groupsHtml +
|
||||
'</section>'
|
||||
);
|
||||
|
|
@ -1981,7 +1981,7 @@ function renderSuppressedGroup(data) {
|
|||
|
||||
/**
|
||||
* Render codepoint-reveal + cp-tag for Unicode-steganografi (UNI-funn).
|
||||
* Brukes på mcp-inspect-rapporter — bytter plain table mot side-by-side
|
||||
* Used on mcp-inspect reports — swaps a plain table for a side-by-side
|
||||
* "synlig vs. decoded codepoint"-visning per tool.
|
||||
*/
|
||||
function renderCodepointReveal(codepoints) {
|
||||
|
|
@ -1999,7 +1999,7 @@ function renderCodepointReveal(codepoints) {
|
|||
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
|
||||
// Highlight U+XXXX patterns
|
||||
const highlighted = cps.replace(/U\+[0-9A-Fa-f]{4,6}/g, function (m) {
|
||||
return '<span class="' + tagFor(m) + '">' + m + '</span>';
|
||||
});
|
||||
|
|
@ -2042,7 +2042,7 @@ function renderCodepointReveal(codepoints) {
|
|||
|
||||
/**
|
||||
* Render top-risks + top-risk for rangert top-funn-listing.
|
||||
* Tar de N (default 5) høyeste alvorlighetsnivåene fra findings og
|
||||
* Takes the N (default 5) highest-severity findings and
|
||||
* 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).
|
||||
|
|
@ -2089,7 +2089,7 @@ function renderTopRisks(findings, n) {
|
|||
}
|
||||
|
||||
// ============================================================
|
||||
// 10 RENDERERS — én per høy-prio kommando.
|
||||
// 10 RENDERERS — one per high-priority command.
|
||||
// ============================================================
|
||||
function renderScan(data, slot) {
|
||||
const meterHtml = renderRiskMeter(data.risk_score, data.riskBand);
|
||||
|
|
@ -2190,7 +2190,7 @@ function renderPluginAudit(data, slot) {
|
|||
) : '';
|
||||
const permHtml = (data.permissions && data.permissions.length) ? (
|
||||
'<section class="report-meta"><h4>Permission-matrise</h4>' +
|
||||
'<table class="report-table"><thead><tr><th>Verktøy</th><th>Krevet av</th><th>Begrunnet</th></tr></thead><tbody>' +
|
||||
'<table class="report-table"><thead><tr><th>Tool</th><th>Required by</th><th>Justified</th></tr></thead><tbody>' +
|
||||
data.permissions.map(function (p) {
|
||||
const isYes = /^yes|^ja/i.test(p.justified);
|
||||
const isNo = /^no$|^nei/i.test(p.justified);
|
||||
|
|
@ -2220,7 +2220,7 @@ function renderPluginAudit(data, slot) {
|
|||
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.',
|
||||
lede: data.lede || 'Trust verdict based on maintainer, license, permissions, and MCP descriptions.',
|
||||
verdict: data.verdict || inferVerdict(data, 'risk-score-meter'),
|
||||
keyStats: data.keyStats || inferKeyStats(data, 'risk-score-meter')
|
||||
}, body);
|
||||
|
|
@ -2285,7 +2285,7 @@ function renderMcpAudit(data, slot) {
|
|||
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.',
|
||||
lede: data.lede || 'Permissions, trust, and description drift across installed MCP servers.',
|
||||
verdict: data.verdict || inferVerdict(data, 'findings'),
|
||||
keyStats: data.keyStats || inferKeyStats(data, 'findings')
|
||||
}, body);
|
||||
|
|
@ -2392,7 +2392,7 @@ function renderAudit(data, slot) {
|
|||
'<ol class="recommendation-card__body">' + items.map(function (a) { return '<li>' + escapeHtml(a) + '</li>'; }).join('') + '</ol>' +
|
||||
'</section>';
|
||||
};
|
||||
const actionHtml = tierHtml('immediate', 'Umiddelbar', 'critical') + tierHtml('high', 'Høy prioritet', 'high') + tierHtml('medium', 'Medium prioritet', 'medium');
|
||||
const actionHtml = tierHtml('immediate', 'Immediate', 'critical') + tierHtml('high', 'High priority', 'high') + tierHtml('medium', 'Medium priority', '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');
|
||||
|
|
@ -2504,12 +2504,12 @@ function renderHarden(data, slot) {
|
|||
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.',
|
||||
lede: data.lede || 'Diff preview of settings.json, CLAUDE.md, and .gitignore changes.',
|
||||
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' }
|
||||
{ label: 'CURRENT GRADE', value: String(data.current_grade || '?') },
|
||||
{ label: 'ACTIONS', value: data.actionable + '/' + data.total },
|
||||
{ label: 'MODE', value: data.mode || 'dry-run' }
|
||||
]
|
||||
}, body);
|
||||
}
|
||||
|
|
@ -2564,7 +2564,7 @@ function renderRedTeam(data, slot) {
|
|||
RENDERERS.renderRedTeam = renderRedTeam;
|
||||
|
||||
// ============================================================
|
||||
// FASE 3: 8 RENDERERS — én per gjenstående kommando.
|
||||
// PHASE 3: 8 RENDERERS — one per remaining command.
|
||||
// ============================================================
|
||||
function renderMcpInspect(data, slot) {
|
||||
const invRows = (data.server_inventory || []).map(function (s) {
|
||||
|
|
@ -2602,7 +2602,7 @@ function renderMcpInspect(data, slot) {
|
|||
RENDERERS.renderMcpInspect = renderMcpInspect;
|
||||
|
||||
function renderSupplyCheck(data, slot) {
|
||||
// Ecosystem-cards (small-multiples-mønster)
|
||||
// Ecosystem cards (small-multiples pattern)
|
||||
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 ? '<div class="small-multiples">' + ecos.map(function (e) {
|
||||
const issues = (Number(e.osv_hits) || 0) + (Number(e.typosquats) || 0);
|
||||
|
|
@ -2640,7 +2640,7 @@ function renderPreDeploy(data, slot) {
|
|||
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å
|
||||
// v7.6.1 fix: sm-card__grade is fixed at 28×28 px (designed for one A-F letter), so
|
||||
// "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) {
|
||||
|
|
@ -2665,7 +2665,7 @@ function renderPreDeploy(data, slot) {
|
|||
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" data-severity="high">' +
|
||||
'<span class="recommendation-card__label">Vilkår å løse</span>' +
|
||||
'<span class="recommendation-card__label">Conditions to resolve</span>' +
|
||||
'<ol class="recommendation-card__body">' + data.conditions.map(function (c) { return '<li>' + escapeHtml(c) + '</li>'; }).join('') + '</ol>' +
|
||||
'</section>'
|
||||
) : '';
|
||||
|
|
@ -2708,7 +2708,7 @@ function renderDiff(data, slot) {
|
|||
'</div>' +
|
||||
'<div class="pair-before-after__arrow" aria-hidden="true"></div>' +
|
||||
'<div class="pair-before-after__cell">' +
|
||||
'<span class="pair-before-after__cell-label">NÅ</span>' +
|
||||
'<span class="pair-before-after__cell-label">NOW</span>' +
|
||||
'<span class="pair-before-after__cell-value">' + gradeBadge(data.current_grade) + '</span>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
|
|
@ -2741,7 +2741,7 @@ function renderDiff(data, slot) {
|
|||
'</section>';
|
||||
};
|
||||
const newHtml = sectionFor('Nye funn', newItems, 'new');
|
||||
const resHtml = sectionFor('Løste funn', resolvedItems, 'resolved');
|
||||
const resHtml = sectionFor('Resolved findings', 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 };
|
||||
|
|
@ -2751,7 +2751,7 @@ function renderDiff(data, slot) {
|
|||
slot.innerHTML = renderPageShell({
|
||||
eyebrow: 'DIFF',
|
||||
title: data.title || 'Scan diff mot baseline',
|
||||
lede: data.lede || 'Sammenligner nåværende scan mot lagret baseline.',
|
||||
lede: data.lede || 'Compares the current scan against the stored baseline.',
|
||||
verdict: data.verdict || inferVerdict(data, 'diff-report'),
|
||||
keyStats: data.keyStats || inferKeyStats(data, 'diff-report')
|
||||
}, body);
|
||||
|
|
@ -2797,7 +2797,7 @@ function renderWatch(data, slot) {
|
|||
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.',
|
||||
lede: data.lede || 'Runs diff on a recurring interval via /loop. Notifies on new findings.',
|
||||
verdict: data.verdict || inferVerdict(data, 'findings'),
|
||||
keyStats: data.keyStats || inferKeyStats(data, 'findings')
|
||||
}, body);
|
||||
|
|
@ -2829,7 +2829,7 @@ function renderRegistry(data, slot) {
|
|||
}).join('');
|
||||
const sigHtml = sigRows ? (
|
||||
'<section class="report-meta"><h4>Signaturer</h4>' +
|
||||
'<table class="report-table"><thead><tr><th>Skill</th><th>Kilde</th><th>Fingerprint</th><th>Status</th><th>Første sett</th></tr></thead><tbody>' + sigRows + '</tbody></table>' +
|
||||
'<table class="report-table"><thead><tr><th>Skill</th><th>Source</th><th>Fingerprint</th><th>Status</th><th>First seen</th></tr></thead><tbody>' + sigRows + '</tbody></table>' +
|
||||
'</section>'
|
||||
) : '';
|
||||
const fs = (data.findings || []).map(function (f) {
|
||||
|
|
@ -2874,11 +2874,11 @@ function renderClean(data, slot) {
|
|||
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.
|
||||
// Every bucket with items > 0 gets one recommendation-card with a 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: 'semi-auto', label: 'Semi-auto — requires confirmation', sev: 'medium', desc: 'Proposed changes are shown as a diff. The user confirms per finding before the change is applied.' },
|
||||
{ key: 'manual', label: 'Manual remediation', sev: 'high', desc: 'Requires human judgement — context, scope, or side-effects are not deterministically decidable.' },
|
||||
{ key: 'suppressed', label: 'Undertrykt', sev: 'low', desc: 'Allowlist-treff via .llm-security-ignore — ingen handling.' }
|
||||
];
|
||||
const advisoryHtml = bucketAdvisoryDefs.map(function (b) {
|
||||
|
|
@ -2897,14 +2897,14 @@ function renderClean(data, slot) {
|
|||
const intro = data.mode ? (
|
||||
'<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>' +
|
||||
'<p class="recommendation-card__body">' + (isDry ? 'Dry-run: no files are modified. Preview the actions before <code>--apply</code>.' : 'Fixes are applied with an automatic backup in <code>.llm-security-backup/</code>.') + '</p>' +
|
||||
'</section>'
|
||||
) : '';
|
||||
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.',
|
||||
lede: data.lede || 'Findings split across Auto / Semi-auto / Manual / Suppressed.',
|
||||
verdict: data.verdict || inferVerdict(data, 'kanban-buckets'),
|
||||
keyStats: data.keyStats || inferKeyStats(data, 'kanban-buckets')
|
||||
}, body);
|
||||
|
|
@ -2929,7 +2929,7 @@ function renderThreatModel(data, slot) {
|
|||
for (let prob = 1; prob <= probSize; prob++) {
|
||||
const score = prob * cons;
|
||||
const items = byPC[prob + '_' + cons] || [];
|
||||
// v7.6.1 fix: bobler er nå <button> så de er klikkbare og fokuserbare.
|
||||
// v7.6.1 fix: bubbles are now <button> so they are clickable and focusable.
|
||||
// data-threat-id lar event-handler senere mappe til detalj-modal.
|
||||
const bubblesHtml = items.length
|
||||
? '<div class="matrix__cell-bubbles">' +
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue