feat: evidence_type Feld (code/process/hybrid) fuer Controls
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
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>
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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({
|
||||
<LicenseRuleBadge rule={ctrl.license_rule} />
|
||||
<VerificationMethodBadge method={ctrl.verification_method} />
|
||||
<CategoryBadge category={ctrl.category} />
|
||||
<EvidenceTypeBadge type={ctrl.evidence_type} />
|
||||
<TargetAudienceBadge audience={ctrl.target_audience} />
|
||||
<GenerationStrategyBadge strategy={ctrl.generation_strategy} />
|
||||
<ObligationTypeBadge type={ctrl.generation_metadata?.obligation_type as string} />
|
||||
|
||||
@@ -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<string, unknown> | 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<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' },
|
||||
@@ -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 <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
|
||||
|
||||
|
||||
@@ -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<string>('')
|
||||
const [verificationFilter, setVerificationFilter] = useState<string>('')
|
||||
const [categoryFilter, setCategoryFilter] = useState<string>('')
|
||||
const [evidenceTypeFilter, setEvidenceTypeFilter] = useState<string>('')
|
||||
const [audienceFilter, setAudienceFilter] = useState<string>('')
|
||||
const [sourceFilter, setSourceFilter] = useState<string>('')
|
||||
const [typeFilter, setTypeFilter] = useState<string>('')
|
||||
@@ -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() {
|
||||
<option key={c.value} value={c.value}>{c.label}</option>
|
||||
))}
|
||||
</select>
|
||||
<select
|
||||
value={evidenceTypeFilter}
|
||||
onChange={e => setEvidenceTypeFilter(e.target.value)}
|
||||
className="text-sm border border-gray-300 rounded-lg px-2 py-1.5 focus:outline-none focus:ring-2 focus:ring-purple-500"
|
||||
>
|
||||
<option value="">Nachweisart</option>
|
||||
{EVIDENCE_TYPE_OPTIONS.map(c => (
|
||||
<option key={c.value} value={c.value}>{c.label}</option>
|
||||
))}
|
||||
</select>
|
||||
<select
|
||||
value={audienceFilter}
|
||||
onChange={e => setAudienceFilter(e.target.value)}
|
||||
@@ -792,6 +804,7 @@ export default function ControlLibraryPage() {
|
||||
<LicenseRuleBadge rule={ctrl.license_rule} />
|
||||
<VerificationMethodBadge method={ctrl.verification_method} />
|
||||
<CategoryBadge category={ctrl.category} />
|
||||
<EvidenceTypeBadge type={ctrl.evidence_type} />
|
||||
<TargetAudienceBadge audience={ctrl.target_audience} />
|
||||
<GenerationStrategyBadge strategy={ctrl.generation_strategy} />
|
||||
<ObligationTypeBadge type={ctrl.generation_metadata?.obligation_type as string} />
|
||||
|
||||
@@ -80,6 +80,7 @@ class ControlResponse(BaseModel):
|
||||
customer_visible: Optional[bool] = None
|
||||
verification_method: Optional[str] = None
|
||||
category: Optional[str] = None
|
||||
evidence_type: Optional[str] = None
|
||||
target_audience: Optional[str] = None
|
||||
generation_metadata: Optional[dict] = None
|
||||
generation_strategy: Optional[str] = "ungrouped"
|
||||
@@ -113,6 +114,7 @@ class ControlCreateRequest(BaseModel):
|
||||
customer_visible: Optional[bool] = True
|
||||
verification_method: Optional[str] = None
|
||||
category: Optional[str] = None
|
||||
evidence_type: Optional[str] = None
|
||||
target_audience: Optional[str] = None
|
||||
generation_metadata: Optional[dict] = None
|
||||
applicable_industries: Optional[list] = None
|
||||
@@ -141,6 +143,7 @@ class ControlUpdateRequest(BaseModel):
|
||||
customer_visible: Optional[bool] = None
|
||||
verification_method: Optional[str] = None
|
||||
category: Optional[str] = None
|
||||
evidence_type: Optional[str] = None
|
||||
target_audience: Optional[str] = None
|
||||
generation_metadata: Optional[dict] = None
|
||||
applicable_industries: Optional[list] = None
|
||||
@@ -172,7 +175,7 @@ _CONTROL_COLS = """id, framework_id, control_id, title, objective, rationale,
|
||||
severity, risk_score, implementation_effort,
|
||||
evidence_confidence, open_anchors, release_state, tags,
|
||||
license_rule, source_original_text, source_citation,
|
||||
customer_visible, verification_method, category,
|
||||
customer_visible, verification_method, category, evidence_type,
|
||||
target_audience, generation_metadata, generation_strategy,
|
||||
applicable_industries, applicable_company_size, scope_conditions,
|
||||
parent_control_uuid, decomposition_method, pipeline_version,
|
||||
@@ -312,6 +315,7 @@ async def list_controls(
|
||||
release_state: Optional[str] = Query(None),
|
||||
verification_method: Optional[str] = Query(None),
|
||||
category: Optional[str] = Query(None),
|
||||
evidence_type: Optional[str] = Query(None, description="Filter: code, process, hybrid"),
|
||||
target_audience: Optional[str] = Query(None),
|
||||
source: Optional[str] = Query(None, description="Filter by source_citation->source"),
|
||||
search: Optional[str] = Query(None, description="Full-text search in control_id, title, objective"),
|
||||
@@ -348,6 +352,9 @@ async def list_controls(
|
||||
if category:
|
||||
query += " AND category = :cat"
|
||||
params["cat"] = category
|
||||
if evidence_type:
|
||||
query += " AND evidence_type = :et"
|
||||
params["et"] = evidence_type
|
||||
if target_audience:
|
||||
query += " AND target_audience LIKE :ta_pattern"
|
||||
params["ta_pattern"] = f'%"{target_audience}"%'
|
||||
@@ -398,6 +405,7 @@ async def count_controls(
|
||||
release_state: Optional[str] = Query(None),
|
||||
verification_method: Optional[str] = Query(None),
|
||||
category: Optional[str] = Query(None),
|
||||
evidence_type: Optional[str] = Query(None),
|
||||
target_audience: Optional[str] = Query(None),
|
||||
source: Optional[str] = Query(None),
|
||||
search: Optional[str] = Query(None),
|
||||
@@ -426,6 +434,9 @@ async def count_controls(
|
||||
if category:
|
||||
query += " AND category = :cat"
|
||||
params["cat"] = category
|
||||
if evidence_type:
|
||||
query += " AND evidence_type = :et"
|
||||
params["et"] = evidence_type
|
||||
if target_audience:
|
||||
query += " AND target_audience LIKE :ta_pattern"
|
||||
params["ta_pattern"] = f'%"{target_audience}"%'
|
||||
@@ -998,6 +1009,109 @@ async def backfill_normative_strength(
|
||||
}
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# EVIDENCE TYPE BACKFILL
|
||||
# =============================================================================
|
||||
|
||||
# Domains that are primarily technical (code-verifiable)
|
||||
_CODE_DOMAINS = frozenset({
|
||||
"SEC", "AUTH", "CRYPT", "CRYP", "CRY", "NET", "LOG", "ACC", "APP", "SYS",
|
||||
"CI", "CONT", "API", "CLOUD", "IAC", "SAST", "DAST", "DEP", "SBOM",
|
||||
"WEB", "DEV", "SDL", "PKI", "HSM", "TEE", "TPM", "CRX", "CRF",
|
||||
"FWU", "STO", "RUN", "VUL", "MAL", "PLT", "AUT",
|
||||
})
|
||||
|
||||
# Domains that are primarily process-based (document-verifiable)
|
||||
_PROCESS_DOMAINS = frozenset({
|
||||
"GOV", "ORG", "COMP", "LEGAL", "HR", "TRAIN", "AML", "FIN",
|
||||
"RISK", "AUDIT", "AUD", "PROC", "DOC", "PHYS", "PHY", "PRIV", "DPO",
|
||||
"BCDR", "BCP", "VENDOR", "SUPPLY", "SUP", "CERT", "POLICY",
|
||||
"ENV", "HLT", "TRD", "LAB", "PER", "REL", "ISM", "COM",
|
||||
"GAM", "RIS", "PCA", "GNT", "HCA", "RES", "ISS",
|
||||
})
|
||||
|
||||
# Domains that are typically hybrid
|
||||
_HYBRID_DOMAINS = frozenset({
|
||||
"DATA", "AI", "INC", "ID", "IAM", "IDF", "IDP", "IDA", "IDN",
|
||||
"OPS", "MNT", "INT", "BCK",
|
||||
})
|
||||
|
||||
|
||||
def _classify_evidence_type(control_id: str, category: str | None) -> str:
|
||||
"""Heuristic: classify a control as code/process/hybrid based on domain prefix."""
|
||||
domain = control_id.split("-")[0].upper() if control_id else ""
|
||||
|
||||
if domain in _CODE_DOMAINS:
|
||||
return "code"
|
||||
if domain in _PROCESS_DOMAINS:
|
||||
return "process"
|
||||
if domain in _HYBRID_DOMAINS:
|
||||
return "hybrid"
|
||||
|
||||
# Fallback: use category if available
|
||||
code_categories = {"encryption", "authentication", "network", "application", "system", "identity"}
|
||||
process_categories = {"compliance", "personnel", "physical", "governance", "risk"}
|
||||
if category in code_categories:
|
||||
return "code"
|
||||
if category in process_categories:
|
||||
return "process"
|
||||
|
||||
return "process" # Conservative default
|
||||
|
||||
|
||||
@router.post("/controls/backfill-evidence-type")
|
||||
async def backfill_evidence_type(
|
||||
dry_run: bool = Query(True, description="Nur zaehlen, nicht aendern"),
|
||||
):
|
||||
"""
|
||||
Klassifiziert Controls als code/process/hybrid basierend auf Domain-Prefix.
|
||||
|
||||
Heuristik:
|
||||
- SEC, AUTH, CRYPT, NET, LOG, ... → code
|
||||
- GOV, ORG, COMP, LEGAL, HR, ... → process
|
||||
- DATA, AI, INC → hybrid
|
||||
"""
|
||||
with SessionLocal() as db:
|
||||
rows = db.execute(text("""
|
||||
SELECT id, control_id, category, evidence_type
|
||||
FROM canonical_controls
|
||||
WHERE release_state NOT IN ('rejected', 'merged')
|
||||
ORDER BY control_id
|
||||
""")).fetchall()
|
||||
|
||||
changes = []
|
||||
stats = {"total": len(rows), "already_set": 0, "code": 0, "process": 0, "hybrid": 0}
|
||||
|
||||
for row in rows:
|
||||
if row.evidence_type is not None:
|
||||
stats["already_set"] += 1
|
||||
continue
|
||||
|
||||
new_type = _classify_evidence_type(row.control_id, row.category)
|
||||
stats[new_type] += 1
|
||||
changes.append({
|
||||
"id": str(row.id),
|
||||
"control_id": row.control_id,
|
||||
"evidence_type": new_type,
|
||||
})
|
||||
|
||||
if not dry_run and changes:
|
||||
for change in changes:
|
||||
db.execute(text("""
|
||||
UPDATE canonical_controls
|
||||
SET evidence_type = :et
|
||||
WHERE id = CAST(:cid AS uuid)
|
||||
"""), {"et": change["evidence_type"], "cid": change["id"]})
|
||||
db.commit()
|
||||
|
||||
return {
|
||||
"dry_run": dry_run,
|
||||
"stats": stats,
|
||||
"total_changes": len(changes),
|
||||
"sample_changes": changes[:20],
|
||||
}
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# CONTROL CRUD (CREATE / UPDATE / DELETE)
|
||||
# =============================================================================
|
||||
@@ -1040,7 +1154,7 @@ async def create_control(body: ControlCreateRequest):
|
||||
severity, risk_score, implementation_effort, evidence_confidence,
|
||||
open_anchors, release_state, tags,
|
||||
license_rule, source_original_text, source_citation,
|
||||
customer_visible, verification_method, category,
|
||||
customer_visible, verification_method, category, evidence_type,
|
||||
target_audience, generation_metadata,
|
||||
applicable_industries, applicable_company_size, scope_conditions
|
||||
) VALUES (
|
||||
@@ -1051,7 +1165,7 @@ async def create_control(body: ControlCreateRequest):
|
||||
CAST(:anchors AS jsonb), :release_state, CAST(:tags AS jsonb),
|
||||
:license_rule, :source_original_text,
|
||||
CAST(:source_citation AS jsonb),
|
||||
:customer_visible, :verification_method, :category,
|
||||
:customer_visible, :verification_method, :category, :evidence_type,
|
||||
:target_audience, CAST(:generation_metadata AS jsonb),
|
||||
CAST(:applicable_industries AS jsonb),
|
||||
CAST(:applicable_company_size AS jsonb),
|
||||
@@ -1082,6 +1196,7 @@ async def create_control(body: ControlCreateRequest):
|
||||
"customer_visible": body.customer_visible,
|
||||
"verification_method": body.verification_method,
|
||||
"category": body.category,
|
||||
"evidence_type": body.evidence_type,
|
||||
"target_audience": body.target_audience,
|
||||
"generation_metadata": _json.dumps(body.generation_metadata) if body.generation_metadata else None,
|
||||
"applicable_industries": _json.dumps(body.applicable_industries) if body.applicable_industries else None,
|
||||
@@ -1312,6 +1427,7 @@ def _control_row(r) -> dict:
|
||||
"customer_visible": r.customer_visible,
|
||||
"verification_method": r.verification_method,
|
||||
"category": r.category,
|
||||
"evidence_type": getattr(r, "evidence_type", None),
|
||||
"target_audience": r.target_audience,
|
||||
"generation_metadata": r.generation_metadata,
|
||||
"generation_strategy": getattr(r, "generation_strategy", "ungrouped"),
|
||||
|
||||
16
backend-compliance/migrations/079_evidence_type.sql
Normal file
16
backend-compliance/migrations/079_evidence_type.sql
Normal file
@@ -0,0 +1,16 @@
|
||||
-- Migration 079: Add evidence_type to canonical_controls
|
||||
-- Classifies HOW a control is evidenced:
|
||||
-- code = Technical control, verifiable in source code / IaC / CI-CD
|
||||
-- process = Organizational / governance control, verified via documents / policies
|
||||
-- hybrid = Both code and process evidence required
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
IF EXISTS (SELECT 1 FROM information_schema.tables
|
||||
WHERE table_schema = 'compliance' AND table_name = 'canonical_controls') THEN
|
||||
ALTER TABLE canonical_controls ADD COLUMN IF NOT EXISTS
|
||||
evidence_type VARCHAR(20) DEFAULT NULL
|
||||
CHECK (evidence_type IN ('code', 'process', 'hybrid'));
|
||||
CREATE INDEX IF NOT EXISTS idx_cc_evidence_type ON canonical_controls(evidence_type);
|
||||
END IF;
|
||||
END $$;
|
||||
79
backend-compliance/tests/test_evidence_type.py
Normal file
79
backend-compliance/tests/test_evidence_type.py
Normal file
@@ -0,0 +1,79 @@
|
||||
"""Tests for evidence_type classification heuristic."""
|
||||
import sys
|
||||
sys.path.insert(0, ".")
|
||||
|
||||
from compliance.api.canonical_control_routes import _classify_evidence_type
|
||||
|
||||
|
||||
class TestClassifyEvidenceType:
|
||||
"""Tests for _classify_evidence_type()."""
|
||||
|
||||
# --- Code domains ---
|
||||
def test_sec_is_code(self):
|
||||
assert _classify_evidence_type("SEC-042", None) == "code"
|
||||
|
||||
def test_auth_is_code(self):
|
||||
assert _classify_evidence_type("AUTH-001", None) == "code"
|
||||
|
||||
def test_crypt_is_code(self):
|
||||
assert _classify_evidence_type("CRYPT-003", None) == "code"
|
||||
|
||||
def test_cryp_is_code(self):
|
||||
assert _classify_evidence_type("CRYP-010", None) == "code"
|
||||
|
||||
def test_net_is_code(self):
|
||||
assert _classify_evidence_type("NET-015", None) == "code"
|
||||
|
||||
def test_log_is_code(self):
|
||||
assert _classify_evidence_type("LOG-007", None) == "code"
|
||||
|
||||
def test_acc_is_code(self):
|
||||
assert _classify_evidence_type("ACC-012", None) == "code"
|
||||
|
||||
def test_api_is_code(self):
|
||||
assert _classify_evidence_type("API-001", None) == "code"
|
||||
|
||||
# --- Process domains ---
|
||||
def test_gov_is_process(self):
|
||||
assert _classify_evidence_type("GOV-001", None) == "process"
|
||||
|
||||
def test_comp_is_process(self):
|
||||
assert _classify_evidence_type("COMP-001", None) == "process"
|
||||
|
||||
def test_fin_is_process(self):
|
||||
assert _classify_evidence_type("FIN-001", None) == "process"
|
||||
|
||||
def test_hr_is_process(self):
|
||||
assert _classify_evidence_type("HR-001", None) == "process"
|
||||
|
||||
def test_org_is_process(self):
|
||||
assert _classify_evidence_type("ORG-001", None) == "process"
|
||||
|
||||
def test_env_is_process(self):
|
||||
assert _classify_evidence_type("ENV-001", None) == "process"
|
||||
|
||||
# --- Hybrid domains ---
|
||||
def test_data_is_hybrid(self):
|
||||
assert _classify_evidence_type("DATA-005", None) == "hybrid"
|
||||
|
||||
def test_ai_is_hybrid(self):
|
||||
assert _classify_evidence_type("AI-001", None) == "hybrid"
|
||||
|
||||
def test_inc_is_hybrid(self):
|
||||
assert _classify_evidence_type("INC-003", None) == "hybrid"
|
||||
|
||||
def test_iam_is_hybrid(self):
|
||||
assert _classify_evidence_type("IAM-001", None) == "hybrid"
|
||||
|
||||
# --- Category fallback ---
|
||||
def test_unknown_domain_encryption_category(self):
|
||||
assert _classify_evidence_type("XYZ-001", "encryption") == "code"
|
||||
|
||||
def test_unknown_domain_governance_category(self):
|
||||
assert _classify_evidence_type("XYZ-001", "governance") == "process"
|
||||
|
||||
def test_unknown_domain_no_category(self):
|
||||
assert _classify_evidence_type("XYZ-001", None) == "process"
|
||||
|
||||
def test_empty_control_id(self):
|
||||
assert _classify_evidence_type("", None) == "process"
|
||||
132
docs-src/services/sdk-modules/evidence-type.md
Normal file
132
docs-src/services/sdk-modules/evidence-type.md
Normal file
@@ -0,0 +1,132 @@
|
||||
# Evidence Type — Code vs. Prozess Controls
|
||||
|
||||
## Uebersicht
|
||||
|
||||
Nicht jedes Control kann gleich nachgewiesen werden. Ein Verschluesselungs-Control
|
||||
ist im Quellcode pruefbar, ein Risikomanagement-Control erfordert Dokumente und
|
||||
Nachweise. Diese Unterscheidung bestimmt, **wie** die Compliance-Plattform den
|
||||
Nachweis automatisiert oder den Nutzer unterstuetzt.
|
||||
|
||||
Das Feld `evidence_type` auf `canonical_controls` klassifiziert jeden Control
|
||||
in eine von drei Kategorien.
|
||||
|
||||
## Die drei Typen
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
subgraph "code"
|
||||
C1["Source Code"]
|
||||
C2["IaC / Terraform"]
|
||||
C3["CI/CD Pipeline"]
|
||||
C4["Cloud Config"]
|
||||
end
|
||||
|
||||
subgraph "process"
|
||||
P1["Policies"]
|
||||
P2["Schulungsnachweise"]
|
||||
P3["Vertraege"]
|
||||
P4["Screenshots / Audits"]
|
||||
end
|
||||
|
||||
subgraph "hybrid"
|
||||
H1["Code + Doku"]
|
||||
H2["Config + Schulung"]
|
||||
end
|
||||
```
|
||||
|
||||
### Code Controls
|
||||
|
||||
| Eigenschaft | Beschreibung |
|
||||
|---|---|
|
||||
| **evidence_type** | `code` |
|
||||
| **Nachweis** | Automatisiert pruefbar: Source Code, IaC, CI/CD, Cloud-Konfiguration |
|
||||
| **Automatisierung** | Hoch — SAST, Dependency Scan, Config Audit |
|
||||
| **Beispiel-Domains** | SEC, AUTH, CRYPT, NET, LOG, ACC, API |
|
||||
| **Beispiel** | "AES-256 Verschluesselung at rest" → pruefbar via Code Review / IaC Scan |
|
||||
|
||||
### Prozess Controls
|
||||
|
||||
| Eigenschaft | Beschreibung |
|
||||
|---|---|
|
||||
| **evidence_type** | `process` |
|
||||
| **Nachweis** | Dokumente, Policies, Schulungsnachweise, Vertraege, Screenshots |
|
||||
| **Automatisierung** | Gering — erfordert manuelle Uploads oder MCP-Dokumenten-Scan |
|
||||
| **Beispiel-Domains** | GOV, ORG, COMP, LEGAL, HR, FIN, RISK, AUDIT, ENV |
|
||||
| **Beispiel** | "Reallabor-Zugang fuer KMUs bereitstellen" → Nachweis ueber Programm-Dokumentation |
|
||||
|
||||
!!! info "Governance & Regulatorische Controls"
|
||||
Controls wie "Behoerde muss KMUs Zugang zu Reallaboren geben" sind Prozess-Controls.
|
||||
Der Nachweis erfolgt ueber Dokumente — nicht im Source Code.
|
||||
Auch regulatorische Umsetzungspflichten (GOV-Domain) fallen hierunter.
|
||||
|
||||
### Hybrid Controls
|
||||
|
||||
| Eigenschaft | Beschreibung |
|
||||
|---|---|
|
||||
| **evidence_type** | `hybrid` |
|
||||
| **Nachweis** | Sowohl Code als auch Dokumente erforderlich |
|
||||
| **Automatisierung** | Teilweise — Code-Teil automatisiert, Prozess-Teil manuell |
|
||||
| **Beispiel-Domains** | DATA, AI, INC, IAM |
|
||||
| **Beispiel** | "MFA implementieren" → Config pruefbar (code) + Nutzer-Schulung noetig (process) |
|
||||
|
||||
## Backfill-Heuristik
|
||||
|
||||
Der Backfill klassifiziert Controls automatisch anhand des Domain-Prefix:
|
||||
|
||||
```
|
||||
POST /api/compliance/v1/canonical/controls/backfill-evidence-type?dry_run=true
|
||||
```
|
||||
|
||||
**Algorithmus:**
|
||||
|
||||
1. Domain-Prefix extrahieren (z.B. `SEC` aus `SEC-042`)
|
||||
2. Gegen vordefinierte Domain-Sets pruefen (code / process / hybrid)
|
||||
3. Falls Domain unbekannt: Category als Fallback nutzen
|
||||
4. Falls auch keine Category: `process` (konservativ)
|
||||
|
||||
### Domain-Zuordnung
|
||||
|
||||
| Typ | Domains |
|
||||
|---|---|
|
||||
| **code** | SEC, AUTH, CRYPT, CRYP, NET, LOG, ACC, APP, SYS, API, WEB, DEV, SDL, PKI, HSM, TEE, TPM, VUL, ... |
|
||||
| **process** | GOV, ORG, COMP, LEGAL, HR, FIN, RISK, AUDIT, ENV, HLT, TRD, LAB, PHYS, PRIV, DPO, ... |
|
||||
| **hybrid** | DATA, AI, INC, IAM, OPS, MNT, INT, ... |
|
||||
|
||||
## Frontend-Anzeige
|
||||
|
||||
In der Control-Library werden Controls mit farbcodierten Badges angezeigt:
|
||||
|
||||
| evidence_type | Badge | Farbe | Bedeutung |
|
||||
|---|---|---|---|
|
||||
| `code` | **Code** | Blau (sky) | Technisch, im Source Code pruefbar |
|
||||
| `process` | **Prozess** | Amber/Orange | Organisatorisch, Dokument-basiert |
|
||||
| `hybrid` | **Hybrid** | Violett | Beides erforderlich |
|
||||
|
||||
Zusaetzlich steht ein Dropdown-Filter "Nachweisart" zur Verfuegung.
|
||||
|
||||
## Zusammenspiel mit anderen Feldern
|
||||
|
||||
| Feld | Zweck | Beispiel |
|
||||
|---|---|---|
|
||||
| `evidence_type` | **WAS** wird nachgewiesen (Code oder Prozess) | `code` |
|
||||
| `verification_method` | **WIE** wird verifiziert | `code_review`, `document`, `tool`, `hybrid` |
|
||||
| `evidence_confidence` | **WIE SICHER** ist der Nachweis (0.0 - 1.0) | `0.92` |
|
||||
| `normative_strength` | **WIE VERBINDLICH** ist das Control | `must`, `should`, `may` |
|
||||
|
||||
!!! warning "evidence_type vs. verification_method"
|
||||
`evidence_type` sagt, ob ein Control technisch oder organisatorisch ist.
|
||||
`verification_method` sagt, mit welcher Methode es geprueft wird.
|
||||
Ein `process`-Control kann trotzdem `verification_method = tool` haben
|
||||
(z.B. wenn ein MCP-Scan Dokumente automatisch prueft).
|
||||
|
||||
## Kuenftige Automatisierung
|
||||
|
||||
### Code Controls
|
||||
- **Git-Repository-Scan**: SAST, Secret Detection, Dependency Check
|
||||
- **IaC-Analyse**: Terraform/Pulumi/CloudFormation Policies
|
||||
- **CI/CD-Integration**: Pipeline-Ergebnisse als Evidence sammeln
|
||||
|
||||
### Prozess Controls
|
||||
- **MCP-Dokumenten-Scan**: Kunden-Laufwerk anbinden, Dokumente automatisch pruefen
|
||||
- **Screenshot-Analyse**: OCR + LLM-Validierung von Screenshots
|
||||
- **Interview-Protokolle**: Strukturierte Audit-Checklisten
|
||||
@@ -110,6 +110,7 @@ nav:
|
||||
- Deduplizierungs-Engine: services/sdk-modules/dedup-engine.md
|
||||
- Control Provenance Wiki: services/sdk-modules/control-provenance.md
|
||||
- Normative Verbindlichkeit (Dreistufenmodell): services/sdk-modules/normative-verbindlichkeit.md
|
||||
- Evidence Type (Code vs. Prozess): services/sdk-modules/evidence-type.md
|
||||
- Anti-Fake-Evidence Architektur: services/sdk-modules/anti-fake-evidence.md
|
||||
- Strategie:
|
||||
- Wettbewerbsanalyse & Roadmap: strategy/wettbewerbsanalyse.md
|
||||
|
||||
Reference in New Issue
Block a user