feat(llm-security): playground v7.6.0 fase 5a-d — Tier 3 spesialkomponenter (del 1) [skip-docs]
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 <noreply@anthropic.com>
This commit is contained in:
parent
2481133515
commit
fbda041522
1 changed files with 290 additions and 13 deletions
|
|
@ -7717,6 +7717,39 @@
|
||||||
// { ok: false, errors: [{ section, reason }] }
|
// { 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 parseScan = safeOk(function (md) {
|
||||||
const dash = parseRiskDashboard(md);
|
const dash = parseRiskDashboard(md);
|
||||||
const findings = parseFindingsTables(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 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, {
|
return { ok: true, data: Object.assign({}, dash, {
|
||||||
findings: findings,
|
findings: findings,
|
||||||
owasp: owasp,
|
owasp: owasp,
|
||||||
supply_chain: supply_chain,
|
supply_chain: supply_chain,
|
||||||
executive_summary: exec ? exec.body.split(/\n##/)[0].trim() : '',
|
executive_summary: exec ? exec.body.split(/\n##/)[0].trim() : '',
|
||||||
|
narrative_audit: suppressed ? { suppressed_findings: suppressed } : undefined,
|
||||||
recommendations: parseRecommendations(md)
|
recommendations: parseRecommendations(md)
|
||||||
}) };
|
}) };
|
||||||
});
|
});
|
||||||
|
|
@ -7790,11 +7825,15 @@
|
||||||
info: intOrZero(row[matrixTbl.headers[5]] || '0')
|
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, {
|
return { ok: true, data: Object.assign({}, dash, {
|
||||||
scanners: scannerBlocks,
|
scanners: scannerBlocks,
|
||||||
scanner_matrix: scanner_matrix,
|
scanner_matrix: scanner_matrix,
|
||||||
score: dash.risk_score,
|
score: dash.risk_score,
|
||||||
findings: parseFindingsTables(md),
|
findings: parseFindingsTables(md),
|
||||||
|
executive_summary: exec ? exec.body.split(/\n##/)[0].trim() : '',
|
||||||
|
narrative_audit: suppressed ? { suppressed_findings: suppressed } : undefined,
|
||||||
recommendations: parseRecommendations(md)
|
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 (
|
||||||
|
'<button class="tfa-leg" type="button" data-severity="' + escapeAttr(sev) + '" aria-label="' + escapeAttr(leg.label + ': ' + leg.name) + '">' +
|
||||||
|
'<span class="tfa-leg__label">' + escapeHtml(leg.label) + '</span>' +
|
||||||
|
'<span class="tfa-leg__name">' + escapeHtml(leg.name) + '</span>' +
|
||||||
|
'<span class="tfa-leg__source">' + escapeHtml(leg.source) + '</span>' +
|
||||||
|
'<span class="tfa-leg__status" data-mit="' + escapeAttr(leg.mit) + '">' + escapeHtml(leg.mitText) + '</span>' +
|
||||||
|
'</button>'
|
||||||
|
);
|
||||||
|
};
|
||||||
|
const arrowHtml = '<div class="tfa-arrow" data-severity="' + escapeAttr(sev) + '" aria-hidden="true"><div class="tfa-arrow__line"></div></div>';
|
||||||
|
return (
|
||||||
|
'<section class="report-meta">' +
|
||||||
|
'<h4>Toxic flow — Lethal trifecta-kjede</h4>' +
|
||||||
|
'<p style="font-size: var(--font-size-sm); opacity: 0.78; margin: 0 0 var(--space-3);">Den fulle 3-leddete kjeden som overskrider Rule of Two. Hver leg er umitigert — ingen hook bryter kjeden.</p>' +
|
||||||
|
'<div class="tfa-flow" role="group" aria-label="Lethal trifecta-kjede">' +
|
||||||
|
'<div class="tfa-flow__verdict" data-verdict="' + escapeAttr(verdict) + '">' + escapeHtml(verdict) + '</div>' +
|
||||||
|
legHtml(legs[0]) + arrowHtml + legHtml(legs[1]) + arrowHtml + legHtml(legs[2]) +
|
||||||
|
'</div>' +
|
||||||
|
'</section>'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 ? '<span class="' + pillCls.trim() + '">' + escapeHtml(pillText) + '</span>' : '';
|
||||||
|
const progress = state === 'current' ? (
|
||||||
|
'<div class="mat-step__progress">' +
|
||||||
|
'<div class="mat-step__progress-bar"><div class="mat-step__progress-fill" style="width: ' + pct + '%"></div></div>' +
|
||||||
|
'<span>' + passCount + ' / ' + total + ' kategorier</span>' +
|
||||||
|
'</div>'
|
||||||
|
) : '';
|
||||||
|
return (
|
||||||
|
'<div class="mat-step" data-state="' + escapeAttr(state) + '">' +
|
||||||
|
'<div class="mat-step__icon" aria-hidden="true">' + escapeHtml(icon) + '</div>' +
|
||||||
|
'<div>' +
|
||||||
|
'<div class="mat-step__name">' + escapeHtml(s.name) + pill + '</div>' +
|
||||||
|
'<div class="mat-step__desc">' + escapeHtml(s.desc) + '</div>' +
|
||||||
|
progress +
|
||||||
|
'</div>' +
|
||||||
|
'</div>'
|
||||||
|
);
|
||||||
|
}).join('');
|
||||||
|
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>' +
|
||||||
|
'<div class="mat-ladder" role="list" aria-label="Posture-modenhet over 5 trinn">' + stepHtml + '</div>' +
|
||||||
|
'</section>'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 ? (
|
||||||
|
'<div class="suppressed-group__examples">' +
|
||||||
|
'<span class="suppressed-group__example">' + escapeHtml(g.example) + '</span>' +
|
||||||
|
'</div>'
|
||||||
|
) : '';
|
||||||
|
return (
|
||||||
|
'<div class="suppressed-group">' +
|
||||||
|
'<div class="suppressed-group__head">' +
|
||||||
|
'<span class="suppressed-group__reason">' + escapeHtml(g.reason) + '</span>' +
|
||||||
|
'<span class="suppressed-group__count">' + g.count + ' ' + (g.count === 1 ? 'forekomst' : 'forekomster') + '</span>' +
|
||||||
|
'</div>' +
|
||||||
|
example +
|
||||||
|
'</div>'
|
||||||
|
);
|
||||||
|
}).join('');
|
||||||
|
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>' +
|
||||||
|
groupsHtml +
|
||||||
|
'</section>'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 '<span class="' + tagFor(m) + '">' + m + '</span>';
|
||||||
|
});
|
||||||
|
const headRisk = isClean
|
||||||
|
? '<span style="font-size: 11px; color: var(--color-state-success);">Ren — ingen non-ASCII</span>'
|
||||||
|
: '<span style="font-size: 11px; font-weight: var(--font-weight-semibold); color: var(--color-severity-' + sev + ');">' + escapeHtml(risk) + ' risk</span>';
|
||||||
|
const visibleCol = isClean
|
||||||
|
? '<div class="codepoint-reveal__source">' + escapeHtml(c.tool || '—') + '</div>'
|
||||||
|
: '<div class="codepoint-reveal__source">' + escapeHtml(c.tool || '—') + ' <span style="opacity: 0.6;">(rendert visuelt)</span></div>';
|
||||||
|
const decodedCol = isClean
|
||||||
|
? '<div class="codepoint-reveal__decoded">(ingen suspekte codepoints)</div>'
|
||||||
|
: '<div class="codepoint-reveal__decoded">' + highlighted + '</div>';
|
||||||
|
return (
|
||||||
|
'<div class="codepoint-reveal">' +
|
||||||
|
'<div class="codepoint-reveal__head">' +
|
||||||
|
'<strong>' + escapeHtml(c.server || '—') + ' · <code>' + escapeHtml(c.tool || '—') + '</code></strong>' +
|
||||||
|
headRisk +
|
||||||
|
'</div>' +
|
||||||
|
'<div class="codepoint-reveal__body">' +
|
||||||
|
'<div class="codepoint-reveal__col">' +
|
||||||
|
'<span class="codepoint-reveal__col-label">Synlig (rendret tekst)</span>' +
|
||||||
|
visibleCol +
|
||||||
|
'</div>' +
|
||||||
|
'<div class="codepoint-reveal__col">' +
|
||||||
|
'<span class="codepoint-reveal__col-label">Decoded (codepoints)</span>' +
|
||||||
|
decodedCol +
|
||||||
|
'</div>' +
|
||||||
|
'</div>' +
|
||||||
|
'</div>'
|
||||||
|
);
|
||||||
|
}).join('');
|
||||||
|
return (
|
||||||
|
'<section class="report-meta">' +
|
||||||
|
'<h4>Codepoint-reveal — Unicode-steganografi</h4>' +
|
||||||
|
'<p style="font-size: var(--font-size-sm); opacity: 0.78; margin: 0 0 var(--space-3);">Tools med non-ASCII codepoints i deskripsjoner — zero-width / homoglyph / bidi-override. Side-ved-side: synlig form vs. dekoded codepoints.</p>' +
|
||||||
|
'<div style="display: flex; flex-direction: column; gap: var(--space-3);">' + blocks + '</div>' +
|
||||||
|
'</section>'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// 10 RENDERERS — én per høy-prio kommando.
|
// 10 RENDERERS — én per høy-prio kommando.
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
function renderScan(data, slot) {
|
function renderScan(data, slot) {
|
||||||
const meterHtml = renderRiskMeter(data.risk_score, data.riskBand);
|
const meterHtml = renderRiskMeter(data.risk_score, data.riskBand);
|
||||||
|
const suppressedHtml = renderSuppressedGroup(data);
|
||||||
|
const toxicHtml = renderToxicFlow(data.findings || []);
|
||||||
const owaspHtml = (data.owasp && data.owasp.length) ? (
|
const owaspHtml = (data.owasp && data.owasp.length) ? (
|
||||||
'<section class="report-meta"><h4>OWASP-kategorier</h4>' +
|
'<section class="report-meta"><h4>OWASP-kategorier</h4>' +
|
||||||
'<table class="report-table"><thead><tr><th>Kategori</th><th>Funn</th><th>Maks severity</th><th>Skannere</th></tr></thead><tbody>' +
|
'<table class="report-table"><thead><tr><th>Kategori</th><th>Funn</th><th>Maks severity</th><th>Skannere</th></tr></thead><tbody>' +
|
||||||
|
|
@ -8908,7 +9191,7 @@
|
||||||
) : '';
|
) : '';
|
||||||
const findingsHtml = renderFindingsBlock(data.findings || [], 'Funn');
|
const findingsHtml = renderFindingsBlock(data.findings || [], 'Funn');
|
||||||
const recHtml = renderRecommendationsList(data.recommendations || []);
|
const recHtml = renderRecommendationsList(data.recommendations || []);
|
||||||
const body = meterHtml + owaspHtml + supplyHtml + findingsHtml + recHtml;
|
const body = meterHtml + suppressedHtml + toxicHtml + owaspHtml + supplyHtml + findingsHtml + recHtml;
|
||||||
slot.innerHTML = renderPageShell({
|
slot.innerHTML = renderPageShell({
|
||||||
eyebrow: 'SKANNING',
|
eyebrow: 'SKANNING',
|
||||||
title: data.title || 'Security Scan',
|
title: data.title || 'Security Scan',
|
||||||
|
|
@ -8950,7 +9233,9 @@
|
||||||
) : '';
|
) : '';
|
||||||
const findingsHtml = renderFindingsBlock(data.findings || [], 'Findings (utvalg)');
|
const findingsHtml = renderFindingsBlock(data.findings || [], 'Findings (utvalg)');
|
||||||
const recHtml = renderRecommendationsList(data.recommendations || []);
|
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({
|
slot.innerHTML = renderPageShell({
|
||||||
eyebrow: 'DEEP-SCAN',
|
eyebrow: 'DEEP-SCAN',
|
||||||
title: data.title || 'Deterministisk deep-scan',
|
title: data.title || 'Deterministisk deep-scan',
|
||||||
|
|
@ -9117,6 +9402,7 @@
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
const smHtml = renderSmallMultiples(items);
|
const smHtml = renderSmallMultiples(items);
|
||||||
|
const ladderHtml = renderMatLadder(data.categories || [], data.posture_score, data.posture_applicable);
|
||||||
// Quick wins
|
// Quick wins
|
||||||
const quickHtml = (data.quick_wins && data.quick_wins.length) ? (
|
const quickHtml = (data.quick_wins && data.quick_wins.length) ? (
|
||||||
'<section class="recommendation-card">' +
|
'<section class="recommendation-card">' +
|
||||||
|
|
@ -9131,7 +9417,7 @@
|
||||||
const overall = data.posture_score != null ? (
|
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>'
|
'<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 + smHtml + quickHtml + findingsHtml + recHtml;
|
const body = overall + ladderHtml + smHtml + quickHtml + findingsHtml + recHtml;
|
||||||
slot.innerHTML = renderPageShell({
|
slot.innerHTML = renderPageShell({
|
||||||
eyebrow: 'POSTURE',
|
eyebrow: 'POSTURE',
|
||||||
title: data.title || 'Security posture',
|
title: data.title || 'Security posture',
|
||||||
|
|
@ -9346,16 +9632,7 @@
|
||||||
'<table class="report-table"><thead><tr><th>Server</th><th>Transport</th><th>Tools</th><th>Status</th><th>Connected</th></tr></thead><tbody>' + invRows + '</tbody></table>' +
|
'<table class="report-table"><thead><tr><th>Server</th><th>Transport</th><th>Tools</th><th>Status</th><th>Connected</th></tr></thead><tbody>' + invRows + '</tbody></table>' +
|
||||||
'</section>'
|
'</section>'
|
||||||
) : '';
|
) : '';
|
||||||
const cpRows = (data.codepoints || []).map(function (c) {
|
const cpHtml = renderCodepointReveal(data.codepoints || []);
|
||||||
const sev = /high/i.test(c.risk) ? 'critical' : /medium/i.test(c.risk) ? 'medium' : 'low';
|
|
||||||
return '<tr><td>' + escapeHtml(c.server) + '</td><td><code>' + escapeHtml(c.tool) + '</code></td><td>' + escapeHtml(c.codepoints) + '</td><td><span class="key-stat__value" style="color: var(--color-' + sev + ')">' + escapeHtml(c.risk) + '</span></td></tr>';
|
|
||||||
}).join('');
|
|
||||||
const cpHtml = cpRows ? (
|
|
||||||
'<section class="report-meta"><h4>Codepoint-reveal</h4>' +
|
|
||||||
'<p style="font-size: var(--font-size-sm); opacity: 0.78;">Tools med non-ASCII codepoints i deskripsjoner — zero-width / homoglyph-kandidater.</p>' +
|
|
||||||
'<table class="report-table"><thead><tr><th>Server</th><th>Tool</th><th>Codepoints</th><th>Risk</th></tr></thead><tbody>' + cpRows + '</tbody></table>' +
|
|
||||||
'</section>'
|
|
||||||
) : '';
|
|
||||||
const fs = (data.findings || []).map(function (f) {
|
const fs = (data.findings || []).map(function (f) {
|
||||||
return Object.assign({}, f, {
|
return Object.assign({}, f, {
|
||||||
file: f.server || f.file || '',
|
file: f.server || f.file || '',
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue