feat(cra): CRA Compliance module Phase 1+2+3 (intake, scope, path, requirements, backlog, sbom, checks)
Phase 1 — Intake + Scope + Path: - Migration 119: compliance_cra_projects table (intake + classification + path + status state machine) - Backend service cra_routes.py: CRUD + scope-check + path-select - Deterministic Annex III/IV classifier (verbatim mapping from migration 059 wiki) - Path validation per classification (CRITICAL → notified_body mandatory) - Frontend: project list, dashboard, 3-step wizard (intake/scope/path) - Sidebar entry under "CRA Compliance" (red) Phase 2 — Annex I Requirements + Priorisierungs-Backlog: - cra_annex_i_data.py: 40 Annex-I requirements (8 categories), 9 measures (M540-M548), 3 CRA deadlines - Endpoints: /requirements (40 items), /backlog (priority-sorted with deadline pressure) - Frontend: requirements table with filters + expandable details, backlog with deadline banner + score-ranked table - Dashboard KPI cards (Critical count, days to CE deadline, etc.) + top-10 backlog snippet Phase 3 — SBOM Upload + Automated Checks: - Migration 120: compliance_cra_sboms (versioned uploads, CycloneDX + SPDX) - SBOM endpoints: POST /sbom/upload (format detection, summary extraction), GET /sboms - Checks reuse compliance_evidence_checks: init creates 6 default CRA checks, run executes - Real implementations: cra_security_txt (HTTP + Contact: line) and cra_tls_cert_check (TLS handshake) - Frontend: SBOM file upload + version list, Checks page with per-check URL input + Run button Backend-Reuse: gap_projects (intake pre-population), compliance_evidence_checks/_check_results. Tenant scoping via existing X-Tenant-ID header pattern. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,24 @@
|
||||
'use client'
|
||||
|
||||
type Classification = 'NOT_IN_SCOPE' | 'STANDARD' | 'IMPORTANT_I' | 'IMPORTANT_II' | 'CRITICAL'
|
||||
|
||||
const STYLES: Record<string, { bg: string; label: string }> = {
|
||||
NOT_IN_SCOPE: { bg: 'bg-gray-200 text-gray-700', label: 'Nicht im Scope' },
|
||||
STANDARD: { bg: 'bg-blue-100 text-blue-800', label: 'Standard' },
|
||||
IMPORTANT_I: { bg: 'bg-yellow-100 text-yellow-800', label: 'Important Class I' },
|
||||
IMPORTANT_II: { bg: 'bg-orange-100 text-orange-800', label: 'Important Class II' },
|
||||
CRITICAL: { bg: 'bg-red-100 text-red-800', label: 'Critical' },
|
||||
}
|
||||
|
||||
export function ClassificationBadge({ value, size = 'md' }: { value: string | null; size?: 'sm' | 'md' | 'lg' }) {
|
||||
if (!value) {
|
||||
return <span className="px-2 py-0.5 text-xs rounded-full bg-gray-100 text-gray-500">Unbewertet</span>
|
||||
}
|
||||
const style = STYLES[value] || { bg: 'bg-gray-100 text-gray-700', label: value }
|
||||
const sizeClasses = {
|
||||
sm: 'px-2 py-0.5 text-xs',
|
||||
md: 'px-3 py-1 text-sm font-medium',
|
||||
lg: 'px-4 py-2 text-base font-semibold',
|
||||
}[size]
|
||||
return <span className={`rounded-full ${sizeClasses} ${style.bg}`}>{style.label}</span>
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
'use client'
|
||||
|
||||
const STYLES: Record<string, { bg: string; label: string }> = {
|
||||
CRITICAL: { bg: 'bg-red-600 text-white', label: 'Kritisch' },
|
||||
HIGH: { bg: 'bg-orange-500 text-white', label: 'Hoch' },
|
||||
MEDIUM: { bg: 'bg-yellow-400 text-gray-900', label: 'Mittel' },
|
||||
LOW: { bg: 'bg-blue-100 text-blue-800', label: 'Niedrig' },
|
||||
}
|
||||
|
||||
export function SeverityBadge({ value }: { value: string }) {
|
||||
const s = STYLES[value] || { bg: 'bg-gray-200 text-gray-700', label: value }
|
||||
return <span className={`px-2 py-0.5 text-xs font-bold rounded ${s.bg}`}>{s.label}</span>
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
'use client'
|
||||
|
||||
const STEPS = [
|
||||
{ id: 'draft', label: 'Entwurf' },
|
||||
{ id: 'scoped', label: 'Intake' },
|
||||
{ id: 'classified', label: 'Klassifiziert' },
|
||||
{ id: 'path_selected', label: 'Pfad' },
|
||||
{ id: 'requirements_mapped', label: 'Requirements' },
|
||||
{ id: 'evidence_pending', label: 'Evidence' },
|
||||
{ id: 'ready_for_review', label: 'Review' },
|
||||
{ id: 'declaration_ready', label: 'DoC' },
|
||||
{ id: 'post_market', label: 'Post-Market' },
|
||||
]
|
||||
|
||||
export function StatusStepper({ current }: { current: string }) {
|
||||
const currentIdx = STEPS.findIndex(s => s.id === current)
|
||||
return (
|
||||
<div className="flex items-center gap-1 overflow-x-auto py-2">
|
||||
{STEPS.map((step, idx) => {
|
||||
const isPast = idx < currentIdx
|
||||
const isCurrent = idx === currentIdx
|
||||
return (
|
||||
<div key={step.id} className="flex items-center gap-1 flex-shrink-0">
|
||||
<div className={`w-7 h-7 rounded-full flex items-center justify-center text-xs font-medium ${
|
||||
isCurrent ? 'bg-blue-600 text-white' :
|
||||
isPast ? 'bg-green-500 text-white' :
|
||||
'bg-gray-200 text-gray-500'
|
||||
}`}>{idx + 1}</div>
|
||||
<span className={`text-xs ${isCurrent ? 'font-semibold text-blue-700' : isPast ? 'text-gray-700' : 'text-gray-400'}`}>
|
||||
{step.label}
|
||||
</span>
|
||||
{idx < STEPS.length - 1 && (
|
||||
<span className={`mx-1 ${isPast ? 'text-green-500' : 'text-gray-300'}`}>→</span>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user