All checks were successful
CI/CD / go-lint (push) Has been skipped
CI/CD / python-lint (push) Has been skipped
CI/CD / nodejs-lint (push) Has been skipped
CI/CD / test-go-ai-compliance (push) Successful in 38s
CI/CD / test-python-backend-compliance (push) Successful in 31s
CI/CD / test-python-document-crawler (push) Successful in 19s
CI/CD / test-python-dsms-gateway (push) Successful in 17s
CI/CD / validate-canonical-controls (push) Successful in 10s
CI/CD / Deploy (push) Successful in 4s
Neues Feld auf canonical_controls klassifiziert, ob ein Control technisch im Source Code (code), organisatorisch via Dokumente (process) oder beides (hybrid) nachgewiesen wird. Inklusive Backfill-Endpoint, Frontend-Badge/Filter und MkDocs-Dokumentation. - Migration 079: evidence_type VARCHAR(20) + Index - Backend: Filter, Backfill-Endpoint mit Domain-Heuristik, CRUD - Frontend: EvidenceTypeBadge (sky/amber/violet), Nachweisart-Dropdown - Proxy: evidence_type Passthrough fuer controls + controls-count - Tests: 22 Tests fuer Klassifikations-Heuristik - Docs: Eigenes MkDocs-Kapitel mit Mermaid-Diagramm Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
386 lines
15 KiB
TypeScript
386 lines
15 KiB
TypeScript
import { AlertTriangle, CheckCircle2, Info } from 'lucide-react'
|
|
import React from 'react'
|
|
|
|
// =============================================================================
|
|
// TYPES
|
|
// =============================================================================
|
|
|
|
export interface OpenAnchor {
|
|
framework: string
|
|
ref: string
|
|
url: string
|
|
}
|
|
|
|
export interface EvidenceItem {
|
|
type: string
|
|
description: string
|
|
}
|
|
|
|
export interface CanonicalControl {
|
|
id: string
|
|
framework_id: string
|
|
control_id: string
|
|
title: string
|
|
objective: string
|
|
rationale: string
|
|
scope: {
|
|
platforms?: string[]
|
|
components?: string[]
|
|
data_classes?: string[]
|
|
}
|
|
requirements: string[]
|
|
test_procedure: string[]
|
|
evidence: (EvidenceItem | string)[]
|
|
severity: string
|
|
risk_score: number | null
|
|
implementation_effort: string | null
|
|
evidence_confidence: number | null
|
|
open_anchors: OpenAnchor[]
|
|
release_state: string
|
|
tags: string[]
|
|
license_rule?: number | null
|
|
source_original_text?: string | null
|
|
source_citation?: Record<string, string> | null
|
|
customer_visible?: boolean
|
|
verification_method: string | null
|
|
category: string | null
|
|
evidence_type: string | null
|
|
target_audience: string | string[] | null
|
|
generation_metadata?: Record<string, unknown> | null
|
|
generation_strategy?: string | null
|
|
parent_control_uuid?: string | null
|
|
parent_control_id?: string | null
|
|
parent_control_title?: string | null
|
|
decomposition_method?: string | null
|
|
created_at: string
|
|
updated_at: string
|
|
}
|
|
|
|
export interface Framework {
|
|
id: string
|
|
framework_id: string
|
|
name: string
|
|
version: string
|
|
description: string
|
|
release_state: string
|
|
}
|
|
|
|
// =============================================================================
|
|
// CONSTANTS
|
|
// =============================================================================
|
|
|
|
export const BACKEND_URL = '/api/sdk/v1/canonical'
|
|
|
|
export const SEVERITY_CONFIG: Record<string, { bg: string; label: string; icon: React.ComponentType<{ className?: string }> }> = {
|
|
critical: { bg: 'bg-red-100 text-red-800', label: 'Kritisch', icon: AlertTriangle },
|
|
high: { bg: 'bg-orange-100 text-orange-800', label: 'Hoch', icon: AlertTriangle },
|
|
medium: { bg: 'bg-yellow-100 text-yellow-800', label: 'Mittel', icon: Info },
|
|
low: { bg: 'bg-green-100 text-green-800', label: 'Niedrig', icon: CheckCircle2 },
|
|
}
|
|
|
|
export const EFFORT_LABELS: Record<string, string> = {
|
|
s: 'Klein (S)',
|
|
m: 'Mittel (M)',
|
|
l: 'Gross (L)',
|
|
xl: 'Sehr gross (XL)',
|
|
}
|
|
|
|
export const EMPTY_CONTROL = {
|
|
framework_id: 'bp_security_v1',
|
|
control_id: '',
|
|
title: '',
|
|
objective: '',
|
|
rationale: '',
|
|
scope: { platforms: [] as string[], components: [] as string[], data_classes: [] as string[] },
|
|
requirements: [''],
|
|
test_procedure: [''],
|
|
evidence: [{ type: '', description: '' }],
|
|
severity: 'medium',
|
|
risk_score: null as number | null,
|
|
implementation_effort: 'm' as string | null,
|
|
open_anchors: [{ framework: '', ref: '', url: '' }],
|
|
release_state: 'draft',
|
|
tags: [] as string[],
|
|
verification_method: null as string | null,
|
|
category: null as string | null,
|
|
evidence_type: null as string | null,
|
|
target_audience: null as string | null,
|
|
}
|
|
|
|
export const DOMAIN_OPTIONS = [
|
|
{ value: 'AUTH', label: 'AUTH — Authentifizierung' },
|
|
{ value: 'CRYPT', label: 'CRYPT — Kryptographie' },
|
|
{ value: 'NET', label: 'NET — Netzwerk' },
|
|
{ value: 'DATA', label: 'DATA — Datenschutz' },
|
|
{ value: 'LOG', label: 'LOG — Logging' },
|
|
{ value: 'ACC', label: 'ACC — Zugriffskontrolle' },
|
|
{ value: 'SEC', label: 'SEC — Sicherheit' },
|
|
{ value: 'INC', label: 'INC — Incident Response' },
|
|
{ value: 'AI', label: 'AI — Kuenstliche Intelligenz' },
|
|
{ value: 'COMP', label: 'COMP — Compliance' },
|
|
]
|
|
|
|
export const VERIFICATION_METHODS: Record<string, { bg: string; label: string }> = {
|
|
code_review: { bg: 'bg-blue-100 text-blue-700', label: 'Code Review' },
|
|
document: { bg: 'bg-amber-100 text-amber-700', label: 'Dokument' },
|
|
tool: { bg: 'bg-teal-100 text-teal-700', label: 'Tool' },
|
|
hybrid: { bg: 'bg-purple-100 text-purple-700', label: 'Hybrid' },
|
|
}
|
|
|
|
export const CATEGORY_OPTIONS = [
|
|
{ value: 'encryption', label: 'Verschluesselung & Kryptographie' },
|
|
{ value: 'authentication', label: 'Authentisierung & Zugriffskontrolle' },
|
|
{ value: 'network', label: 'Netzwerksicherheit' },
|
|
{ value: 'data_protection', label: 'Datenschutz & Datensicherheit' },
|
|
{ value: 'logging', label: 'Logging & Monitoring' },
|
|
{ value: 'incident', label: 'Vorfallmanagement' },
|
|
{ value: 'continuity', label: 'Notfall & Wiederherstellung' },
|
|
{ value: 'compliance', label: 'Compliance & Audit' },
|
|
{ value: 'supply_chain', label: 'Lieferkettenmanagement' },
|
|
{ value: 'physical', label: 'Physische Sicherheit' },
|
|
{ value: 'personnel', label: 'Personal & Schulung' },
|
|
{ value: 'application', label: 'Anwendungssicherheit' },
|
|
{ value: 'system', label: 'Systemhaertung & -betrieb' },
|
|
{ value: 'risk', label: 'Risikomanagement' },
|
|
{ value: 'governance', label: 'Sicherheitsorganisation' },
|
|
{ value: 'hardware', label: 'Hardware & Plattformsicherheit' },
|
|
{ value: 'identity', label: 'Identitaetsmanagement' },
|
|
]
|
|
|
|
export const EVIDENCE_TYPE_CONFIG: Record<string, { bg: string; label: string }> = {
|
|
code: { bg: 'bg-sky-100 text-sky-700', label: 'Code' },
|
|
process: { bg: 'bg-amber-100 text-amber-700', label: 'Prozess' },
|
|
hybrid: { bg: 'bg-violet-100 text-violet-700', label: 'Hybrid' },
|
|
}
|
|
|
|
export const EVIDENCE_TYPE_OPTIONS = [
|
|
{ value: 'code', label: 'Code — Technisch (Source Code, IaC, CI/CD)' },
|
|
{ value: 'process', label: 'Prozess — Organisatorisch (Dokumente, Policies)' },
|
|
{ value: 'hybrid', label: 'Hybrid — Code + Prozess' },
|
|
]
|
|
|
|
export const TARGET_AUDIENCE_OPTIONS: Record<string, { bg: string; label: string }> = {
|
|
// Legacy English keys
|
|
enterprise: { bg: 'bg-cyan-100 text-cyan-700', label: 'Unternehmen' },
|
|
authority: { bg: 'bg-rose-100 text-rose-700', label: 'Behoerden' },
|
|
provider: { bg: 'bg-violet-100 text-violet-700', label: 'Anbieter' },
|
|
all: { bg: 'bg-gray-100 text-gray-700', label: 'Alle' },
|
|
// German keys from LLM generation
|
|
unternehmen: { bg: 'bg-cyan-100 text-cyan-700', label: 'Unternehmen' },
|
|
behoerden: { bg: 'bg-rose-100 text-rose-700', label: 'Behoerden' },
|
|
entwickler: { bg: 'bg-sky-100 text-sky-700', label: 'Entwickler' },
|
|
datenschutzbeauftragte: { bg: 'bg-purple-100 text-purple-700', label: 'DSB' },
|
|
geschaeftsfuehrung: { bg: 'bg-amber-100 text-amber-700', label: 'GF' },
|
|
'it-abteilung': { bg: 'bg-blue-100 text-blue-700', label: 'IT' },
|
|
rechtsabteilung: { bg: 'bg-fuchsia-100 text-fuchsia-700', label: 'Recht' },
|
|
'compliance-officer': { bg: 'bg-indigo-100 text-indigo-700', label: 'Compliance' },
|
|
personalwesen: { bg: 'bg-pink-100 text-pink-700', label: 'Personal' },
|
|
einkauf: { bg: 'bg-lime-100 text-lime-700', label: 'Einkauf' },
|
|
produktion: { bg: 'bg-orange-100 text-orange-700', label: 'Produktion' },
|
|
vertrieb: { bg: 'bg-teal-100 text-teal-700', label: 'Vertrieb' },
|
|
gesundheitswesen: { bg: 'bg-red-100 text-red-700', label: 'Gesundheit' },
|
|
finanzwesen: { bg: 'bg-emerald-100 text-emerald-700', label: 'Finanzen' },
|
|
oeffentlicher_dienst: { bg: 'bg-rose-100 text-rose-700', label: 'Oeffentl. Dienst' },
|
|
}
|
|
|
|
export const COLLECTION_OPTIONS = [
|
|
{ value: 'bp_compliance_ce', label: 'CE (OWASP, ENISA, BSI)' },
|
|
{ value: 'bp_compliance_gesetze', label: 'Gesetze (EU, DE, BSI)' },
|
|
{ value: 'bp_compliance_datenschutz', label: 'Datenschutz' },
|
|
{ value: 'bp_compliance_recht', label: 'Recht' },
|
|
{ value: 'bp_dsfa_corpus', label: 'DSFA Corpus' },
|
|
{ value: 'bp_legal_templates', label: 'Legal Templates' },
|
|
]
|
|
|
|
// =============================================================================
|
|
// BADGE COMPONENTS
|
|
// =============================================================================
|
|
|
|
export function SeverityBadge({ severity }: { severity: string }) {
|
|
const config = SEVERITY_CONFIG[severity] || SEVERITY_CONFIG.medium
|
|
const Icon = config.icon
|
|
return (
|
|
<span className={`inline-flex items-center gap-1 px-2 py-0.5 rounded text-xs font-medium ${config.bg}`}>
|
|
<Icon className="w-3 h-3" />
|
|
{config.label}
|
|
</span>
|
|
)
|
|
}
|
|
|
|
export function StateBadge({ state }: { state: string }) {
|
|
const config: Record<string, string> = {
|
|
draft: 'bg-gray-100 text-gray-600',
|
|
review: 'bg-blue-100 text-blue-700',
|
|
approved: 'bg-green-100 text-green-700',
|
|
deprecated: 'bg-red-100 text-red-600',
|
|
needs_review: 'bg-yellow-100 text-yellow-800',
|
|
too_close: 'bg-red-100 text-red-700',
|
|
duplicate: 'bg-orange-100 text-orange-700',
|
|
}
|
|
const labels: Record<string, string> = {
|
|
needs_review: 'Review noetig',
|
|
too_close: 'Zu aehnlich',
|
|
duplicate: 'Duplikat',
|
|
}
|
|
return (
|
|
<span className={`inline-flex items-center px-2 py-0.5 rounded text-xs font-medium ${config[state] || config.draft}`}>
|
|
{labels[state] || state}
|
|
</span>
|
|
)
|
|
}
|
|
|
|
export function LicenseRuleBadge({ rule }: { rule: number | null | undefined }) {
|
|
if (!rule) return null
|
|
const config: Record<number, { bg: string; label: string }> = {
|
|
1: { bg: 'bg-green-100 text-green-700', label: 'Free Use' },
|
|
2: { bg: 'bg-blue-100 text-blue-700', label: 'Zitation' },
|
|
3: { bg: 'bg-amber-100 text-amber-700', label: 'Reformuliert' },
|
|
}
|
|
const c = config[rule]
|
|
if (!c) return null
|
|
return <span className={`inline-flex items-center px-2 py-0.5 rounded text-xs font-medium ${c.bg}`}>{c.label}</span>
|
|
}
|
|
|
|
export function VerificationMethodBadge({ method }: { method: string | null }) {
|
|
if (!method) return null
|
|
const config = VERIFICATION_METHODS[method]
|
|
if (!config) return null
|
|
return <span className={`inline-flex items-center px-2 py-0.5 rounded text-xs font-medium ${config.bg}`}>{config.label}</span>
|
|
}
|
|
|
|
export function CategoryBadge({ category }: { category: string | null }) {
|
|
if (!category) return null
|
|
const opt = CATEGORY_OPTIONS.find(c => c.value === category)
|
|
return (
|
|
<span className="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-indigo-50 text-indigo-700">
|
|
{opt?.label || category}
|
|
</span>
|
|
)
|
|
}
|
|
|
|
export function EvidenceTypeBadge({ type }: { type: string | null }) {
|
|
if (!type) return null
|
|
const config = EVIDENCE_TYPE_CONFIG[type]
|
|
if (!config) return null
|
|
return <span className={`inline-flex items-center px-2 py-0.5 rounded text-xs font-medium ${config.bg}`}>{config.label}</span>
|
|
}
|
|
|
|
export function TargetAudienceBadge({ audience }: { audience: string | string[] | null }) {
|
|
if (!audience) return null
|
|
|
|
// Parse JSON array string from DB (e.g. '["unternehmen", "einkauf"]')
|
|
let items: string[] = []
|
|
if (typeof audience === 'string') {
|
|
if (audience.startsWith('[')) {
|
|
try { items = JSON.parse(audience) } catch { items = [audience] }
|
|
} else {
|
|
items = [audience]
|
|
}
|
|
} else if (Array.isArray(audience)) {
|
|
items = audience
|
|
}
|
|
|
|
if (items.length === 0) return null
|
|
|
|
return (
|
|
<span className="inline-flex items-center gap-1 flex-wrap">
|
|
{items.map((item, i) => {
|
|
const config = TARGET_AUDIENCE_OPTIONS[item]
|
|
if (!config) return <span key={i} className="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-gray-100 text-gray-600">{item}</span>
|
|
return <span key={i} className={`inline-flex items-center px-2 py-0.5 rounded text-xs font-medium ${config.bg}`}>{config.label}</span>
|
|
})}
|
|
</span>
|
|
)
|
|
}
|
|
|
|
export function GenerationStrategyBadge({ strategy }: { strategy: string | null | undefined }) {
|
|
if (!strategy || strategy === 'ungrouped') {
|
|
return <span className="inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium bg-gray-100 text-gray-500">v1</span>
|
|
}
|
|
if (strategy === 'document_grouped') {
|
|
return <span className="inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium bg-emerald-100 text-emerald-700">v2</span>
|
|
}
|
|
if (strategy === 'phase74_gap_fill') {
|
|
return <span className="inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium bg-blue-100 text-blue-700">v5 Gap</span>
|
|
}
|
|
if (strategy === 'pass0b_atomic' || strategy === 'pass0b') {
|
|
return <span className="inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium bg-violet-100 text-violet-700">Atomar</span>
|
|
}
|
|
return <span className="inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium bg-gray-100 text-gray-500">{strategy}</span>
|
|
}
|
|
|
|
export const OBLIGATION_TYPE_CONFIG: Record<string, { bg: string; label: string }> = {
|
|
pflicht: { bg: 'bg-red-100 text-red-700', label: 'Pflicht' },
|
|
empfehlung: { bg: 'bg-amber-100 text-amber-700', label: 'Empfehlung' },
|
|
kann: { bg: 'bg-green-100 text-green-700', label: 'Kann' },
|
|
}
|
|
|
|
export function ObligationTypeBadge({ type }: { type: string | null | undefined }) {
|
|
if (!type) return null
|
|
const config = OBLIGATION_TYPE_CONFIG[type]
|
|
if (!config) return null
|
|
return <span className={`inline-flex items-center px-2 py-0.5 rounded text-xs font-medium ${config.bg}`}>{config.label}</span>
|
|
}
|
|
|
|
export function getDomain(controlId: string): string {
|
|
return controlId.split('-')[0] || ''
|
|
}
|
|
|
|
// =============================================================================
|
|
// PROVENANCE TYPES
|
|
// =============================================================================
|
|
|
|
export interface ObligationInfo {
|
|
candidate_id: string
|
|
obligation_text: string
|
|
action: string | null
|
|
object: string | null
|
|
normative_strength: string
|
|
release_state: string
|
|
}
|
|
|
|
export interface DocumentReference {
|
|
regulation_code: string
|
|
article: string | null
|
|
paragraph: string | null
|
|
extraction_method: string
|
|
confidence: number | null
|
|
}
|
|
|
|
export interface MergedDuplicate {
|
|
control_id: string
|
|
title: string
|
|
source_regulation: string | null
|
|
}
|
|
|
|
export interface RegulationSummary {
|
|
regulation_code: string
|
|
articles: string[]
|
|
link_types: string[]
|
|
}
|
|
|
|
// =============================================================================
|
|
// PROVENANCE BADGES
|
|
// =============================================================================
|
|
|
|
const EXTRACTION_METHOD_CONFIG: Record<string, { bg: string; label: string }> = {
|
|
exact_match: { bg: 'bg-green-100 text-green-700', label: 'Exakt' },
|
|
embedding_match: { bg: 'bg-blue-100 text-blue-700', label: 'Embedding' },
|
|
llm_extracted: { bg: 'bg-violet-100 text-violet-700', label: 'LLM' },
|
|
inferred: { bg: 'bg-gray-100 text-gray-600', label: 'Abgeleitet' },
|
|
}
|
|
|
|
export function ExtractionMethodBadge({ method }: { method: string }) {
|
|
const config = EXTRACTION_METHOD_CONFIG[method] || EXTRACTION_METHOD_CONFIG.inferred
|
|
return <span className={`inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium ${config.bg}`}>{config.label}</span>
|
|
}
|
|
|
|
export function RegulationCountBadge({ count }: { count: number }) {
|
|
if (count <= 0) return null
|
|
return (
|
|
<span className="inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium bg-violet-100 text-violet-700">
|
|
{count} {count === 1 ? 'Regulierung' : 'Regulierungen'}
|
|
</span>
|
|
)
|
|
}
|