From 5e9cab6ab5cdba4aa23c3ec68395c49a6b7966ea Mon Sep 17 00:00:00 2001 From: Benjamin Admin Date: Wed, 25 Mar 2026 21:53:40 +0100 Subject: [PATCH] feat: evidence_type Feld (code/process/hybrid) fuer Controls 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 --- .../app/api/sdk/v1/canonical/route.ts | 4 +- .../components/ControlDetail.tsx | 5 +- .../control-library/components/helpers.tsx | 21 +++ .../app/sdk/control-library/page.tsx | 21 ++- .../api/canonical_control_routes.py | 122 +++++++++++++++- .../migrations/079_evidence_type.sql | 16 +++ .../tests/test_evidence_type.py | 79 +++++++++++ .../services/sdk-modules/evidence-type.md | 132 ++++++++++++++++++ mkdocs.yml | 1 + 9 files changed, 390 insertions(+), 11 deletions(-) create mode 100644 backend-compliance/migrations/079_evidence_type.sql create mode 100644 backend-compliance/tests/test_evidence_type.py create mode 100644 docs-src/services/sdk-modules/evidence-type.md diff --git a/admin-compliance/app/api/sdk/v1/canonical/route.ts b/admin-compliance/app/api/sdk/v1/canonical/route.ts index 5b1b608..06c87c7 100644 --- a/admin-compliance/app/api/sdk/v1/canonical/route.ts +++ b/admin-compliance/app/api/sdk/v1/canonical/route.ts @@ -26,7 +26,7 @@ export async function GET(request: NextRequest) { case 'controls': { const controlParams = new URLSearchParams() - const passthrough = ['severity', 'domain', 'release_state', 'verification_method', 'category', + const passthrough = ['severity', 'domain', 'release_state', 'verification_method', 'category', 'evidence_type', 'target_audience', 'source', 'search', 'control_type', 'exclude_duplicates', 'sort', 'order', 'limit', 'offset'] for (const key of passthrough) { const val = searchParams.get(key) @@ -39,7 +39,7 @@ export async function GET(request: NextRequest) { case 'controls-count': { const countParams = new URLSearchParams() - const countPassthrough = ['severity', 'domain', 'release_state', 'verification_method', 'category', + const countPassthrough = ['severity', 'domain', 'release_state', 'verification_method', 'category', 'evidence_type', 'target_audience', 'source', 'search', 'control_type', 'exclude_duplicates'] for (const key of countPassthrough) { const val = searchParams.get(key) diff --git a/admin-compliance/app/sdk/control-library/components/ControlDetail.tsx b/admin-compliance/app/sdk/control-library/components/ControlDetail.tsx index d4a047f..b0386c6 100644 --- a/admin-compliance/app/sdk/control-library/components/ControlDetail.tsx +++ b/admin-compliance/app/sdk/control-library/components/ControlDetail.tsx @@ -8,10 +8,10 @@ import { } from 'lucide-react' import { CanonicalControl, EFFORT_LABELS, BACKEND_URL, - SeverityBadge, StateBadge, LicenseRuleBadge, VerificationMethodBadge, CategoryBadge, TargetAudienceBadge, + SeverityBadge, StateBadge, LicenseRuleBadge, VerificationMethodBadge, CategoryBadge, EvidenceTypeBadge, TargetAudienceBadge, ObligationTypeBadge, GenerationStrategyBadge, ExtractionMethodBadge, RegulationCountBadge, - VERIFICATION_METHODS, CATEGORY_OPTIONS, + VERIFICATION_METHODS, CATEGORY_OPTIONS, EVIDENCE_TYPE_OPTIONS, ObligationInfo, DocumentReference, MergedDuplicate, RegulationSummary, } from './helpers' @@ -185,6 +185,7 @@ export function ControlDetail({ + diff --git a/admin-compliance/app/sdk/control-library/components/helpers.tsx b/admin-compliance/app/sdk/control-library/components/helpers.tsx index cc460c3..dd50c58 100644 --- a/admin-compliance/app/sdk/control-library/components/helpers.tsx +++ b/admin-compliance/app/sdk/control-library/components/helpers.tsx @@ -44,6 +44,7 @@ export interface CanonicalControl { customer_visible?: boolean verification_method: string | null category: string | null + evidence_type: string | null target_audience: string | string[] | null generation_metadata?: Record | null generation_strategy?: string | null @@ -102,6 +103,7 @@ export const EMPTY_CONTROL = { 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, } @@ -145,6 +147,18 @@ export const CATEGORY_OPTIONS = [ { value: 'identity', label: 'Identitaetsmanagement' }, ] +export const EVIDENCE_TYPE_CONFIG: Record = { + 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 = { // Legacy English keys enterprise: { bg: 'bg-cyan-100 text-cyan-700', label: 'Unternehmen' }, @@ -244,6 +258,13 @@ export function CategoryBadge({ category }: { category: string | null }) { ) } +export function EvidenceTypeBadge({ type }: { type: string | null }) { + if (!type) return null + const config = EVIDENCE_TYPE_CONFIG[type] + if (!config) return null + return {config.label} +} + export function TargetAudienceBadge({ audience }: { audience: string | string[] | null }) { if (!audience) return null diff --git a/admin-compliance/app/sdk/control-library/page.tsx b/admin-compliance/app/sdk/control-library/page.tsx index 07549c9..3fe398d 100644 --- a/admin-compliance/app/sdk/control-library/page.tsx +++ b/admin-compliance/app/sdk/control-library/page.tsx @@ -8,9 +8,9 @@ import { } from 'lucide-react' import { CanonicalControl, Framework, BACKEND_URL, EMPTY_CONTROL, - SeverityBadge, StateBadge, LicenseRuleBadge, VerificationMethodBadge, CategoryBadge, TargetAudienceBadge, + SeverityBadge, StateBadge, LicenseRuleBadge, VerificationMethodBadge, CategoryBadge, EvidenceTypeBadge, TargetAudienceBadge, GenerationStrategyBadge, ObligationTypeBadge, - VERIFICATION_METHODS, CATEGORY_OPTIONS, TARGET_AUDIENCE_OPTIONS, + VERIFICATION_METHODS, CATEGORY_OPTIONS, EVIDENCE_TYPE_OPTIONS, TARGET_AUDIENCE_OPTIONS, } from './components/helpers' import { ControlForm } from './components/ControlForm' import { ControlDetail } from './components/ControlDetail' @@ -51,6 +51,7 @@ export default function ControlLibraryPage() { const [stateFilter, setStateFilter] = useState('') const [verificationFilter, setVerificationFilter] = useState('') const [categoryFilter, setCategoryFilter] = useState('') + const [evidenceTypeFilter, setEvidenceTypeFilter] = useState('') const [audienceFilter, setAudienceFilter] = useState('') const [sourceFilter, setSourceFilter] = useState('') const [typeFilter, setTypeFilter] = useState('') @@ -94,6 +95,7 @@ export default function ControlLibraryPage() { if (stateFilter) p.set('release_state', stateFilter) if (verificationFilter) p.set('verification_method', verificationFilter) if (categoryFilter) p.set('category', categoryFilter) + if (evidenceTypeFilter) p.set('evidence_type', evidenceTypeFilter) if (audienceFilter) p.set('target_audience', audienceFilter) if (sourceFilter) p.set('source', sourceFilter) if (typeFilter) p.set('control_type', typeFilter) @@ -101,7 +103,7 @@ export default function ControlLibraryPage() { if (debouncedSearch) p.set('search', debouncedSearch) if (extra) for (const [k, v] of Object.entries(extra)) p.set(k, v) return p.toString() - }, [severityFilter, domainFilter, stateFilter, verificationFilter, categoryFilter, audienceFilter, sourceFilter, typeFilter, hideDuplicates, debouncedSearch]) + }, [severityFilter, domainFilter, stateFilter, verificationFilter, categoryFilter, evidenceTypeFilter, audienceFilter, sourceFilter, typeFilter, hideDuplicates, debouncedSearch]) // Load metadata (domains, sources — once + on refresh) const loadMeta = useCallback(async () => { @@ -169,7 +171,7 @@ export default function ControlLibraryPage() { useEffect(() => { loadControls() }, [loadControls]) // Reset page when filters change - useEffect(() => { setCurrentPage(1) }, [severityFilter, domainFilter, stateFilter, verificationFilter, categoryFilter, audienceFilter, sourceFilter, typeFilter, hideDuplicates, debouncedSearch, sortBy]) + useEffect(() => { setCurrentPage(1) }, [severityFilter, domainFilter, stateFilter, verificationFilter, categoryFilter, evidenceTypeFilter, audienceFilter, sourceFilter, typeFilter, hideDuplicates, debouncedSearch, sortBy]) // Pagination const totalPages = Math.max(1, Math.ceil(totalCount / PAGE_SIZE)) @@ -654,6 +656,16 @@ export default function ControlLibraryPage() { ))} +