diff --git a/plugins/ms-ai-architect/playground/ms-ai-architect-playground.html b/plugins/ms-ai-architect/playground/ms-ai-architect-playground.html index dc01074..00643d7 100644 --- a/plugins/ms-ai-architect/playground/ms-ai-architect-playground.html +++ b/plugins/ms-ai-architect/playground/ms-ai-architect-playground.html @@ -2632,10 +2632,65 @@ } } } - // _consumer-diskriminator (R15 forward-compat): Sesjon 4 setter denne til - // 'ros' når den oppdager Ros-spesifikk markdown (## Top-risikoer e.l.). - // Sesjon 3 lar den være null så Dpia-rendereren ikke trenger å skille. - return { ok: true, data: { matrix_cells: matrix_cells, threats: threats, radar_axes: radar_axes, residualPair: residualPair, _consumer: null } }; + // _consumer-diskriminator (R15): Settes til 'ros' når Ros-spesifikk + // markdown oppdages (## Top-risikoer eller ## Anbefaling). Dpia-fixturer + // har ingen av disse seksjonene → forblir null. + const hasTopRisks = /^##\s*Top.?risikoer\b/im.test(md); + const hasAnbefal = /^##\s*Anbefaling\b/im.test(md); + const consumer = (hasTopRisks || hasAnbefal) ? 'ros' : null; + // topRisks (R15, Ros-only): parse explicit ## Top-risikoer table, eller + // fallback til threats sortert på severity-rank (kritisk>høy>medium>lav). + // Felt er optional og brukes ikke av renderDpia. Tie-breaker: alfabetisk + // på description. + const sevRank = function (s) { + const v = String(s || '').toLowerCase(); + if (/crit|kritisk/.test(v)) return 4; + if (/høy|high/.test(v)) return 3; + if (/medium|moderat/.test(v)) return 2; + if (/lav|low/.test(v)) return 1; + return 0; + }; + let topRisks = []; + if (consumer === 'ros') { + const trTbl = parseTable(md, /##\s*Top.?risikoer/i); + if (trTbl && trTbl.rows.length) { + const idKey = trTbl.headers[0]; + const descKey = trTbl.headers.find(function (h) { return /trussel|risiko|description|beskrivelse/i.test(h); }) || trTbl.headers[1]; + const scKey = trTbl.headers.find(function (h) { return /score/i.test(h); }); + const sevKey = trTbl.headers.find(function (h) { return /severity|alvorlighet|nivå/i.test(h); }); + topRisks = trTbl.rows.map(function (row) { + return { + id: row[idKey] || '', + description: row[descKey] || row[idKey] || '', + score: scKey ? intOrZero(row[scKey] || '0') : 0, + severity: (sevKey && (row[sevKey] || '').toLowerCase().trim()) || '' + }; + }).slice(0, 5); + } else if (threats.length) { + topRisks = threats.slice().sort(function (a, b) { + const r = sevRank(b.severity) - sevRank(a.severity); + if (r !== 0) return r; + return String(a.description || '').localeCompare(String(b.description || '')); + }).slice(0, 5).map(function (t) { + return { id: t.id, description: t.description, score: 0, severity: t.severity }; + }); + } + } + // recommendation (Ros-only): første avsnitt under ## Anbefaling. + let recommendation = ''; + if (consumer === 'ros' && hasAnbefal) { + const m = md.match(/^##\s*Anbefaling\s*\n+([\s\S]*?)(?=\n##\s|\n$|$)/im); + if (m) recommendation = m[1].replace(/\n+$/, '').trim(); + } + return { ok: true, data: { + matrix_cells: matrix_cells, + threats: threats, + radar_axes: radar_axes, + residualPair: residualPair, + topRisks: topRisks, + recommendation: recommendation, + _consumer: consumer + } }; } function parseMatrixRisk6x5(md) { @@ -3513,9 +3568,80 @@ } function renderRos(data, slot) { + const sevForScore = function (s) { + const n = Number(s) || 0; + if (n >= 16) return 'critical'; + if (n >= 9) return 'high'; + if (n >= 4) return 'medium'; + return 'low'; + }; const matrixHtml = renderMatrixHtml(data, 5); const radarHtml = renderRadarSvg(data.radar_axes || []); - slot.innerHTML = matrixHtml + radarHtml + renderThreatsTable(data.threats); + // C4 top-risks-list (max 5). + const topRisks = (data.topRisks || []).slice(0, 5); + const topRisksHtml = topRisks.length ? '
' + + '

Top-risikoer

' + + topRisks.map(function (r, i) { + const sev = r.severity || sevForScore(r.score); + const scoreLabel = r.score ? String(r.score) : (r.severity || '—').toUpperCase(); + return '
' + + '' + (i + 1) + '' + + '' + escapeHtml(r.description || '') + '' + + '' + escapeHtml(scoreLabel) + '' + + '
'; + }).join('') + '
' : ''; + // B6 residual-pair (gjenbruker mønster fra Dpia / Security). + const rp = data.residualPair; + let residualHtml = ''; + if (rp && rp.before && rp.after) { + const labelOf = function (cell) { + if (cell.score != null) return cell.prob + '×' + cell.cons + ' = ' + cell.score; + return cell.label || '—'; + }; + const sevBefore = rp.before.score != null ? sevForScore(rp.before.score) : ''; + const sevAfter = rp.after.score != null ? sevForScore(rp.after.score) : ''; + const attrBefore = sevBefore ? ' data-severity="' + sevBefore + '"' : ''; + const attrAfter = sevAfter ? ' data-severity="' + sevAfter + '"' : ''; + residualHtml = '
' + + '
' + + 'FØR TILTAK' + + '' + escapeHtml(labelOf(rp.before)) + '' + + 'Sannsynlighet × konsekvens' + + '
' + + '' + + '
' + + 'ETTER TILTAK' + + '' + escapeHtml(labelOf(rp.after)) + '' + + 'Restrisiko' + + '
' + + '
'; + } + // D5 recommendation-card. + const rec = data.recommendation || ''; + const recommendationHtml = rec ? '' : ''; + const threatsHtml = renderThreatsTable(data.threats); + const body = matrixHtml + radarHtml + topRisksHtml + residualHtml + threatsHtml + recommendationHtml; + // Utvid matrix-risk-keyStats med RESTRISIKO når residualPair finnes + // (samme mønster som renderDpia). + const baseStats = inferKeyStats(data, 'matrix-risk'); + const stats = (data.keyStats || (rp && rp.after + ? baseStats.concat([{ + label: 'RESTRISIKO', + value: rp.after.score != null ? String(rp.after.score) : (rp.after.label || '—'), + modifier: rp.after.score != null && rp.after.score >= 9 ? 'high' : 'low', + hint: 'etter tiltak' + }]) + : baseStats)); + slot.innerHTML = renderPageShell({ + eyebrow: 'ROS', + title: data.title || 'ROS-analyse (5×5)', + lede: data.lede || 'Risiko- og sårbarhetsanalyse iht. NS 5814 / ISO 31000 med AI-trusselbibliotek.', + verdict: data.verdict || inferVerdict(data, 'matrix-risk'), + keyStats: stats + }, body); } function renderReview(data, slot) { diff --git a/plugins/ms-ai-architect/playground/test-fixtures/ros.md b/plugins/ms-ai-architect/playground/test-fixtures/ros.md index 9882f1e..ae0020b 100644 --- a/plugins/ms-ai-architect/playground/test-fixtures/ros.md +++ b/plugins/ms-ai-architect/playground/test-fixtures/ros.md @@ -48,6 +48,22 @@ Metodikk: NS 5814 / ISO 31000 + AI-trusselbibliotek | M-104 | Multi-region failover testet | done | Drift | | M-105 | Batching-logikk implementert | in-progress | Tech Lead | +## Top-risikoer + +| ID | Trussel | Score | Severity | +|----|---------|-------|----------| +| T-101 | Modell-drift over tid | 12 | high | +| T-105 | Saksbehandlings-overbelastning | 12 | high | +| T-107 | Misbruk av AI-forklaring som bevis | 12 | high | +| T-108 | Kjedevirkning ved feil i objektregister | 10 | high | +| T-103 | Bias mot småbiler/MC | 9 | medium | + +Restrisiko: 4×3 → 2×2 + +## Anbefaling + +ROS godkjent av seksjonsleder 2026-04-25 forutsatt at M-103 (robusthetstest) ferdigstilles innen 2026-06-15. Re-evaluering ved hver modell-release eller ved endring i sak-volum > 20%. + ## Konklusjon Restrisiko etter tiltak: medium. ROS godkjent av seksjonsleder 2026-04-25.