fix(llm-security): playground v7.6.1 — visuelle bugs i v7.6.0

Seks bugs fanget av maintainer ved manuell verifisering i nettleser etter
v7.6.0-release. Alle skyldes mismatch mellom DS-klasser og hvordan
playground-rendrere brukte dem, eller manglende DS-implementasjoner av
klasser playground-rendrere antok eksisterte.

Fixes:
- renderFindingsBlock brukte .findings outer-class som DS har som
  2-kolonners grid (360px list + 1fr detail-panel) — headeren havnet
  i venstre kolonne, items i høyre, brutt layout i alle 18 rapporter
  med findings. Erstattet med .report-meta + h4 + findings__list >
  findings__group + findings__group-header + findings__items
  (korrekt DS-mønster, kun list-delen).
- .report-table manglet helt i DS men brukes i 7+ rendrere (OWASP,
  Supply chain, Scanner Risk Matrix, Plugin-meta, Permission-matrise,
  Live-meter, Siste runs, Godkjenninger, Mitigation roadmap). Lagt
  lokal CSS-implementasjon i playground-HTML style-blokk: border-
  collapse, zebra-hover, header-styling. Komplementerer DS-tokens
  uten å modifisere vendor.
- renderPreDeploy traffic-lights brukte .sm-card__grade som er fast
  28x28 px (én A-F-bokstav) — kuttet PASS til AS og PASS-WITH-NOTES
  til PASS-WITH-... i alle traffic-light-cards. Erstattet med
  bredde-tilpasset status-pill via inline styling (severity-soft +
  on tokens).
- Threat-model matrix-bobler ikke klikkbare. Erstattet span med
  button type=button data-threat-id + aria-label. Click-handler
  scroller til tilsvarende rad i Trusler-tabellen og fremhever
  den i 1.6 sek.
- Radar-labels overlappet ved 6+ akser fordi alle brukte
  text-anchor=middle. Økt SVG-størrelse 280 → 380, radius 105 → 125.
  Bytter text-anchor fra middle til start/end basert på horisontal-
  posisjon.
- recommendation-card__body tekstoverflyt på lange single-line tekster
  (vilkår, owner-tags, dato). Lagt overflow-wrap: anywhere;
  word-break: break-word i lokal style-blokk.

Verifisering:
- 4/4 fix-spesifikke smoke-tester passerer
- 18/18 renderere produserer fortsatt komplett HTML mot
  dft-komplett-demo (regresjons-test)
- Filendring playground.html 10677 → 10753 linjer (+76 netto)

Versjonsbump v7.6.0 → v7.6.1 (patch — bugfix-only, ingen scanner- eller
hook-atferdsendringer):
- plugins/llm-security/.claude-plugin/plugin.json
- plugins/llm-security/package.json
- plugins/llm-security/README.md (badge)
- plugins/llm-security/CHANGELOG.md ([7.6.1] entry)
- plugins/llm-security/playground/llm-security-playground.html (footer)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Kjell Tore Guttormsen 2026-05-06 14:33:19 +02:00
commit f9b555aa64
5 changed files with 142 additions and 15 deletions

View file

@ -1,5 +1,5 @@
{ {
"name": "llm-security", "name": "llm-security",
"description": "Security scanning, auditing, and threat modeling for Claude Code projects. Detects secrets, validates MCP servers, assesses security posture, and generates threat models aligned with OWASP LLM Top 10.", "description": "Security scanning, auditing, and threat modeling for Claude Code projects. Detects secrets, validates MCP servers, assesses security posture, and generates threat models aligned with OWASP LLM Top 10.",
"version": "7.6.0" "version": "7.6.1"
} }

View file

@ -6,6 +6,57 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
## [Unreleased] ## [Unreleased]
## [7.6.1] - 2026-05-06
Playground v7.6.0 visuell-patch. Seks bugs fanget under maintainer-
verifisering i nettleser; alle skyldes mismatch mellom DS-klasser og
hvordan playground-rendrere brukte dem (eller manglende DS-implementasjoner
av klasser playground-rendrere antok eksisterte). Ingen scanner- eller
hook-behavior-changes.
### Fixed
- **`renderFindingsBlock` brukte `.findings` outer-class** som DS har som
2-kolonners grid (`grid-template-columns: 360px 1fr`) for list+detail-
panel-layout. Resultat: findings-headeren havnet i venstre 360px-
kolonne og items i 1fr-kolonnen, brutt layout i alle 18 rapporter med
findings. Erstattet med `<section class="report-meta">` + `<h4>` +
`findings__list > findings__group > findings__group-header +
findings__items` (korrekt DS-mønster).
- **`.report-table` mangler i DS** men brukes i 7+ rendrere (OWASP-
kategorier, Supply chain, Scanner Risk Matrix, Plugin-meta, Permission-
matrise, Live-meter, Siste runs, Godkjenninger, Mitigation roadmap).
Lagt lokal CSS-implementasjon i playground-HTML `<style>`-blokk:
border-collapse, zebra-hover, header-styling, td-padding. Komplementerer
DS-tokens uten å modifisere vendor.
- **`renderPreDeploy` traffic-lights brukte `.sm-card__grade`** som er
fast `28×28 px` (designet for én A-F-bokstav) — kuttet "PASS" til "AS"
og "PASS-WITH-NOTES" til "PASS-WITH-..." i alle traffic-light-cards.
Erstattet med en bredde-tilpasset status-pill via inline styling
(severity-soft + on tokens).
- **Threat-model matrix-bobler ikke klikkbare**`<span>` uten event-
handler. Erstattet med `<button type="button" data-threat-id>` +
`aria-label`. Lagt til click-handler som scroller til tilsvarende rad
i Trusler-tabellen og fremhever den i 1.6 sek.
- **Radar-labels overlappet** ved 6+ akser fordi alle brukte
`text-anchor="middle"` med samme radius-offset. Økt SVG-størrelse fra
280×280 til 380×380, radius fra 105 til 125. Bytter `text-anchor` fra
`middle` til `start`/`end` basert på horisontal-posisjon (Math.cos(ang)
> 0.2 / < -0.2 / mellom).
- **`recommendation-card__body` tekstoverflyt** — lange single-line
tekster (vilkår, owner-tags, dato) ble klippet av container. Lagt
`overflow-wrap: anywhere; word-break: break-word` i lokal `<style>`-
blokk.
### Verification
- 4/4 fix-spesifikke smoke-tester passerer (`findings__list`,
`data-threat-id`-button, `viewBox="0 0 380 380"`, ingen `sm-card__grade
data-grade` i pre-deploy).
- 18/18 renderere produserer fortsatt komplett HTML-output mot
`dft-komplett-demo` (regresjons-test).
- Filendring: playground.html 10677 → 10753 linjer (+76 netto).
## [7.6.0] - 2026-05-06 ## [7.6.0] - 2026-05-06
Playground Tier 3-referanse-case. v7.6.0 hever playgroundet Playground Tier 3-referanse-case. v7.6.0 hever playgroundet

View file

@ -6,7 +6,7 @@
*AI-generated: all code produced by Claude Code through dialog-driven development. [Full disclosure →](../../README.md#ai-generated-code-disclosure)* *AI-generated: all code produced by Claude Code through dialog-driven development. [Full disclosure →](../../README.md#ai-generated-code-disclosure)*
![Version](https://img.shields.io/badge/version-7.6.0-blue) ![Version](https://img.shields.io/badge/version-7.6.1-blue)
![Platform](https://img.shields.io/badge/platform-Claude_Code_Plugin-purple) ![Platform](https://img.shields.io/badge/platform-Claude_Code_Plugin-purple)
![Commands](https://img.shields.io/badge/commands-20-orange) ![Commands](https://img.shields.io/badge/commands-20-orange)
![Agents](https://img.shields.io/badge/agents-6-orange) ![Agents](https://img.shields.io/badge/agents-6-orange)

View file

@ -1,6 +1,6 @@
{ {
"name": "llm-security", "name": "llm-security",
"version": "7.6.0", "version": "7.6.1",
"description": "Security scanning, auditing, and threat modeling for Claude Code projects", "description": "Security scanning, auditing, and threat modeling for Claude Code projects",
"type": "module", "type": "module",
"bin": { "bin": {

View file

@ -138,6 +138,29 @@
overstyrer). Gjengis av renderVerdictPill() med data-verdict-mapping. */ overstyrer). Gjengis av renderVerdictPill() med data-verdict-mapping. */
/* v7.6.0 fase 4: lokal form-progress-stegblokk fjernet — DS Tier 3 sup /* v7.6.0 fase 4: lokal form-progress-stegblokk fjernet — DS Tier 3 sup
leverer .form-progress__steps + .fp-step + .fp-step__num/__name. */ leverer .form-progress__steps + .fp-step + .fp-step__num/__name. */
/* v7.6.1 fix: .report-table — DS har ikke implementert denne klassen, men
playground-rendrere bruker den i 7+ rapporter (OWASP-kategorier, Supply
chain, Scanner Risk Matrix, Plugin-meta, Permission-matrise, Live-meter,
Siste runs, Godkjenninger, Mitigation roadmap). Lokal styling som
komplementerer DS-tokens. */
.report-table { width: 100%; border-collapse: collapse; margin: var(--space-3) 0; font-size: var(--font-size-sm); }
.report-table th { text-align: left; padding: 8px 12px; border-bottom: 2px solid var(--color-border-moderate); background: var(--color-bg-soft); font-weight: var(--font-weight-semibold); color: var(--color-text-secondary); text-transform: uppercase; font-size: 11px; letter-spacing: 0.04em; }
.report-table td { padding: 8px 12px; border-bottom: 1px solid var(--color-border-subtle); vertical-align: top; color: var(--color-text-primary); }
.report-table tr:last-child td { border-bottom: none; }
.report-table tbody tr:hover { background: var(--color-bg-soft); }
.report-table code { font-family: var(--font-family-mono); font-size: 12px; background: var(--color-surface-sunken); padding: 1px 6px; border-radius: var(--radius-sm); }
/* v7.6.1 fix: recommendation-card body kan inneholde lange single-line
tekster (vilkår, owner-tags, dato). Tving word-wrap så grid-celle
(auto + 1fr) ikke skubber innhold utenfor viewport. */
.recommendation-card__body { overflow-wrap: anywhere; word-break: break-word; }
/* v7.6.1 fix: matrix-bobler skal være klikkbare. DS har hover på cellene,
men bobler er <span> uten cursor. Gjør bubble til cursor:pointer + focus. */
.matrix__bubble { cursor: pointer; transition: transform var(--duration-fast) var(--ease-default); }
.matrix__bubble:hover { transform: scale(1.15); }
.matrix__bubble:focus-visible { outline: 2px solid var(--color-primary-500); outline-offset: 2px; }
</style> </style>
</head> </head>
<body> <body>
@ -6818,7 +6841,7 @@
verdict: 'n-a', verdict: 'n-a',
hero: true, hero: true,
meta: [ meta: [
'Plugin v7.6.0', 'Plugin v7.6.1',
projects.length + ' prosjekt' + (projects.length === 1 ? '' : 'er'), projects.length + ' prosjekt' + (projects.length === 1 ? '' : 'er'),
CATALOG.commands.length + ' kommandoer' CATALOG.commands.length + ' kommandoer'
], ],
@ -8820,10 +8843,18 @@
'</div>' '</div>'
); );
}).join(''); }).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.
return ( return (
'<section class="findings">' + '<section class="report-meta">' +
'<div class="findings__group-header">' + escapeHtml(label || 'Funn') + ' (' + findings.length + ')</div>' + '<h4>' + escapeHtml(label || 'Funn') + '</h4>' +
'<div class="findings__items">' + items + '</div>' + '<div class="findings__list" style="max-height: none;">' +
'<div class="findings__group">' +
'<div class="findings__group-header">' + escapeHtml(label || 'Funn') + ' (' + findings.length + ')</div>' +
'<div class="findings__items">' + items + '</div>' +
'</div>' +
'</div>' +
'</section>' '</section>'
); );
} }
@ -8917,7 +8948,10 @@
function renderRadarSvg(axes) { function renderRadarSvg(axes) {
// axes: [{ name, score (0-5) }] // axes: [{ name, score (0-5) }]
if (!axes || axes.length < 3) return ''; if (!axes || axes.length < 3) return '';
const size = 280, cx = size / 2, cy = size / 2, r = 105; // 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.
const size = 380, cx = size / 2, cy = size / 2, r = 125;
const n = axes.length; const n = axes.length;
const axisRows = axes.map(function (a) { const axisRows = axes.map(function (a) {
return '<div class="radar__score-row"><span>' + escapeHtml(a.name) + '</span><strong>' + escapeHtml(String(a.score || 0)) + '/5</strong></div>'; return '<div class="radar__score-row"><span>' + escapeHtml(a.name) + '</span><strong>' + escapeHtml(String(a.score || 0)) + '/5</strong></div>';
@ -8925,9 +8959,12 @@
const angle = function (i) { return -Math.PI / 2 + (i * 2 * Math.PI / n); }; const angle = function (i) { return -Math.PI / 2 + (i * 2 * Math.PI / n); };
const labelHtml = axes.map(function (a, i) { const labelHtml = axes.map(function (a, i) {
const ang = angle(i); const ang = angle(i);
const lx = cx + Math.cos(ang) * (r + 22); const lx = cx + Math.cos(ang) * (r + 28);
const ly = cy + Math.sin(ang) * (r + 22); const ly = cy + Math.sin(ang) * (r + 28);
return '<text class="radar__label" x="' + lx.toFixed(1) + '" y="' + ly.toFixed(1) + '" text-anchor="middle" dominant-baseline="middle">' + escapeHtml(a.name) + '</text>'; // Velg text-anchor basert på posisjon: ankerene til venstre/høyre snur.
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>';
}).join(''); }).join('');
const grids = [1, 2, 3, 4, 5].map(function (k) { const grids = [1, 2, 3, 4, 5].map(function (k) {
const rk = (r * k) / 5; const rk = (r * k) / 5;
@ -9800,12 +9837,24 @@
if (u === 'FAIL' || u === 'BLOCK' || u === 'NO-GO') return 'critical'; if (u === 'FAIL' || u === 'BLOCK' || u === 'NO-GO') return 'critical';
return 'info'; return 'info';
}; };
// v7.6.1 fix: sm-card__grade er fast 28×28 px (designet for én A-F-bokstav), så
// "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) { const cards = lights.map(function (l) {
const sev = sevForStatus(l.status); const sev = sevForStatus(l.status);
return '<div class="sm-card" data-severity="' + escapeAttr(sev) + '" style="border-left: 3px solid var(--color-' + sev + '); padding-left: var(--space-3);">' + const pillBg = sev === 'low' ? 'var(--color-severity-low-soft)'
: sev === 'medium' ? 'var(--color-severity-medium-soft)'
: sev === 'critical' ? 'var(--color-severity-critical-soft)'
: 'var(--color-bg-soft)';
const pillFg = sev === 'low' ? 'var(--color-severity-low-on)'
: sev === 'medium' ? 'var(--color-severity-medium-on)'
: sev === 'critical' ? 'var(--color-severity-critical-on)'
: 'var(--color-text-secondary)';
const statusPill = '<span style="font-family: var(--font-family-mono); font-size: 11px; font-weight: var(--font-weight-bold); letter-spacing: 0.04em; padding: 3px 8px; border-radius: var(--radius-sm); background: ' + pillBg + '; color: ' + pillFg + '; white-space: nowrap;">' + escapeHtml(l.status) + '</span>';
return '<div class="sm-card" data-severity="' + escapeAttr(sev) + '" style="border-left: 3px solid var(--color-severity-' + (sev === 'low' ? 'low' : sev === 'medium' ? 'medium' : sev === 'critical' ? 'critical' : 'low') + '); padding-left: var(--space-3);">' +
'<div class="sm-card__header">' + '<div class="sm-card__header">' +
'<span class="sm-card__name">' + escapeHtml(l.category) + '</span>' + '<span class="sm-card__name">' + escapeHtml(l.category) + '</span>' +
'<span class="sm-card__grade" data-grade="' + (sev === 'low' ? 'A' : sev === 'medium' ? 'C' : sev === 'critical' ? 'F' : '?') + '">' + escapeHtml(l.status) + '</span>' + statusPill +
'</div>' + '</div>' +
(l.notes ? '<span class="sm-card__status">' + escapeHtml(l.notes) + '</span>' : '') + (l.notes ? '<span class="sm-card__status">' + escapeHtml(l.notes) + '</span>' : '') +
'</div>'; '</div>';
@ -10077,12 +10126,14 @@
for (let prob = 1; prob <= probSize; prob++) { for (let prob = 1; prob <= probSize; prob++) {
const score = prob * cons; const score = prob * cons;
const items = byPC[prob + '_' + cons] || []; const items = byPC[prob + '_' + cons] || [];
// v7.6.1 fix: bobler er nå <button> så de er klikkbare og fokuserbare.
// data-threat-id lar event-handler senere mappe til detalj-modal.
const bubblesHtml = items.length const bubblesHtml = items.length
? '<div class="matrix__cell-bubbles">' + ? '<div class="matrix__cell-bubbles">' +
items.slice(0, 3).map(function (it, i) { items.slice(0, 3).map(function (it, i) {
return '<span class="matrix__bubble" title="' + escapeAttr(it.label || '') + '">' + (i + 1) + '</span>'; return '<button type="button" class="matrix__bubble" data-threat-id="' + escapeAttr(it.id || it.label || '') + '" title="' + escapeAttr(it.label || '') + '" aria-label="Trussel: ' + escapeAttr(it.label || it.id || '') + '">' + (i + 1) + '</button>';
}).join('') + }).join('') +
(items.length > 3 ? '<span class="matrix__bubble matrix__bubble--count">+' + (items.length - 3) + '</span>' : '') + (items.length > 3 ? '<button type="button" class="matrix__bubble matrix__bubble--count" aria-label="' + (items.length - 3) + ' flere trusler">+' + (items.length - 3) + '</button>' : '') +
'</div>' '</div>'
: ''; : '';
matrixHtml += '<div class="matrix__cell" data-score="' + score + '">' + matrixHtml += '<div class="matrix__cell" data-score="' + score + '">' +
@ -10368,6 +10419,31 @@
} }
function attachActionHandlers() { function attachActionHandlers() {
// v7.6.1 fix: matrix-bobler klikkbare. Klikk scroller til tilsvarende rad
// i Trusler-tabellen og fremhever den kort. Bruker data-threat-id som anker.
document.addEventListener('click', function (ev) {
const bubble = ev.target.closest('.matrix__bubble[data-threat-id]');
if (!bubble) return;
const threatId = bubble.getAttribute('data-threat-id');
if (!threatId) return;
// Finn raden i Trusler-tabellen (TM-XXX i første kolonne)
const tables = document.querySelectorAll('table.report-table');
for (let t = 0; t < tables.length; t++) {
const rows = tables[t].querySelectorAll('tbody tr');
for (let r = 0; r < rows.length; r++) {
const firstCell = rows[r].querySelector('td');
if (firstCell && firstCell.textContent.trim() === threatId) {
rows[r].scrollIntoView({ behavior: 'smooth', block: 'center' });
const orig = rows[r].style.background;
rows[r].style.background = 'var(--color-primary-100, var(--color-bg-soft))';
rows[r].style.transition = 'background var(--duration-base) var(--ease-default)';
setTimeout(function () { rows[r].style.background = orig; }, 1600);
return;
}
}
}
});
document.addEventListener('click', function (ev) { document.addEventListener('click', function (ev) {
const target = ev.target.closest('[data-action]'); const target = ev.target.closest('[data-action]');
if (!target) return; if (!target) return;