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:
@@ -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 */}
|
||||
|
||||
Reference in New Issue
Block a user