refactor(cra): NIST/OWASP as collapsible per-finding detail, not a flat column

Per the Co-Pilot-calm principle: the findings table stays compact (Befund /
CRA-Anforderung / Risiko / Maßnahmen) and the NIST/OWASP + ISO 27001
best-practice depth is revealed per finding via a "NIST/OWASP" toggle. Keeps
the 3-layer model (CRA obligation -> measure -> best-practice depth) tidy.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Benjamin Admin
2026-06-13 22:11:33 +02:00
parent a4b405077f
commit f8de5a6dff
@@ -1,6 +1,7 @@
'use client'
import { CRADemo } from '../_hooks/useCRADemo'
import { Fragment, useState } from 'react'
import { CRADemo, CRAFinding } from '../_hooks/useCRADemo'
const RISK_BADGE: Record<string, string> = {
CRITICAL: 'bg-red-100 text-red-700 dark:bg-red-900/40 dark:text-red-300',
@@ -17,6 +18,82 @@ function RiskBadge({ level }: { level: string }) {
)
}
function FindingsTable({ findings }: { findings: CRAFinding[] }) {
const [open, setOpen] = useState<Record<string, boolean>>({})
const toggle = (id: string) => setOpen((o) => ({ ...o, [id]: !o[id] }))
return (
<div className="overflow-x-auto">
<table className="w-full text-xs">
<thead>
<tr className="text-gray-500 border-b border-gray-200 dark:border-gray-700 text-left">
<th className="py-2 px-4">Cyber-Befund</th>
<th className="py-2 px-3">CRA-Anforderung</th>
<th className="py-2 px-3">Risiko</th>
<th className="py-2 px-3">Maßnahmen</th>
<th className="py-2 px-3 text-right">Best Practice</th>
</tr>
</thead>
<tbody>
{findings.map((f) => (
<Fragment key={f.id}>
<tr className="border-b border-gray-100 dark:border-gray-700/50 align-top">
<td className="py-2 px-4 max-w-xs">
<div className="text-gray-800 dark:text-gray-200">{f.title}</div>
<div className="text-[10px] text-gray-400">{f.id} · {f.cwe} · {f.location}</div>
</td>
<td className="py-2 px-3 text-gray-600 dark:text-gray-300">
<span className="font-medium">{f.primary_requirement}</span> {f.requirement_title}
{f.requirement_ids.length > 1 && (
<span className="text-[10px] text-gray-400"> +{f.requirement_ids.length - 1}</span>
)}
<div className="text-[10px] text-gray-400">{f.annex_anchor}</div>
</td>
<td className="py-2 px-3"><RiskBadge level={f.risk_level} /></td>
<td className="py-2 px-3 text-gray-600 dark:text-gray-300">
{f.measures.length ? f.measures.join(', ') : <span className="text-gray-400"></span>}
</td>
<td className="py-2 px-3 text-right">
<button
onClick={() => toggle(f.id)}
className="text-[11px] text-purple-600 hover:underline whitespace-nowrap"
>
NIST/OWASP {open[f.id] ? '▲' : '▼'}
</button>
</td>
</tr>
{open[f.id] && (
<tr className="border-b border-gray-100 dark:border-gray-700/50 bg-gray-50/60 dark:bg-gray-900/30">
<td colSpan={5} className="px-4 py-2">
<p className="text-[10px] text-gray-400 mb-1">Best-Practice-Tiefe (Golden-Set-Crosswalk)</p>
<div className="flex flex-wrap gap-1 items-center">
<span className="text-[10px] text-gray-500 mr-1">NIST 800-53:</span>
{f.nist_refs.map((n) => (
<span key={n} className="inline-block rounded bg-slate-100 text-slate-600 dark:bg-slate-700 dark:text-slate-300 px-1.5 py-0.5 text-[10px] font-mono">{n}</span>
))}
<span className="text-[10px] text-gray-500 mx-1">OWASP:</span>
{f.owasp_refs.map((o) => (
<span key={o.code} className="inline-block rounded bg-blue-100 text-blue-700 dark:bg-blue-900/40 dark:text-blue-300 px-1.5 py-0.5 text-[10px] font-medium">{o.code} · {o.label}</span>
))}
{f.iso27001_ref.length > 0 && (
<>
<span className="text-[10px] text-gray-500 mx-1">ISO 27001:</span>
{f.iso27001_ref.map((iso) => (
<span key={iso} className="inline-block rounded bg-gray-100 text-gray-500 dark:bg-gray-700 dark:text-gray-300 px-1.5 py-0.5 text-[10px]">{iso}</span>
))}
</>
)}
</div>
</td>
</tr>
)}
</Fragment>
))}
</tbody>
</table>
</div>
)
}
export function CRACyberView({ data }: { data: CRADemo }) {
return (
<div className="space-y-6">
@@ -86,54 +163,7 @@ export function CRACyberView({ data }: { data: CRADemo }) {
<div className="px-4 py-3 border-b border-gray-100 dark:border-gray-700">
<h2 className="text-sm font-semibold text-gray-800 dark:text-gray-200">Befunde CRA-Anforderung</h2>
</div>
<div className="overflow-x-auto">
<table className="w-full text-xs">
<thead>
<tr className="text-gray-500 border-b border-gray-200 dark:border-gray-700 text-left">
<th className="py-2 px-4">Cyber-Befund</th>
<th className="py-2 px-3">CRA-Anforderung</th>
<th className="py-2 px-3">Best Practice (NIST / OWASP)</th>
<th className="py-2 px-3">Risiko</th>
<th className="py-2 px-4">Maßnahmen</th>
</tr>
</thead>
<tbody>
{data.findings.map((f) => (
<tr key={f.id} className="border-b border-gray-100 dark:border-gray-700/50 align-top">
<td className="py-2 px-4 max-w-xs">
<div className="text-gray-800 dark:text-gray-200">{f.title}</div>
<div className="text-[10px] text-gray-400">{f.id} · {f.cwe} · {f.location}</div>
</td>
<td className="py-2 px-3 text-gray-600 dark:text-gray-300">
<span className="font-medium">{f.primary_requirement}</span> {f.requirement_title}
{f.requirement_ids.length > 1 && (
<span className="text-[10px] text-gray-400"> +{f.requirement_ids.length - 1}</span>
)}
<div className="text-[10px] text-gray-400">{f.annex_anchor}</div>
</td>
<td className="py-2 px-3">
<div className="flex flex-wrap gap-1">
{f.nist_refs.map((n) => (
<span key={n} className="inline-block rounded bg-slate-100 text-slate-600 dark:bg-slate-700 dark:text-slate-300 px-1.5 py-0.5 text-[10px] font-mono">
{n}
</span>
))}
{f.owasp_refs.map((o) => (
<span key={o.code} title={o.label} className="inline-block rounded bg-blue-100 text-blue-700 dark:bg-blue-900/40 dark:text-blue-300 px-1.5 py-0.5 text-[10px] font-medium">
{o.code}
</span>
))}
</div>
</td>
<td className="py-2 px-3"><RiskBadge level={f.risk_level} /></td>
<td className="py-2 px-4 text-gray-600 dark:text-gray-300">
{f.measures.length ? f.measures.join(', ') : <span className="text-gray-400"></span>}
</td>
</tr>
))}
</tbody>
</table>
</div>
<FindingsTable findings={data.findings} />
</div>
{/* Recommended measures — full curated text + norm references */}