Files
breakpilot-compliance/backend-compliance/compliance/reasoning/implementation_engine.py
T
Benjamin Admin 1607c89459 feat(reasoning): Regulatory Reasoning Engine MVP (scope/obligations/implementation/interpretation)
Deterministic reasoning layer ON TOP of the Legal Knowledge Graph (obligation
registry) and the Compliance Execution Graph (control mapping/evidence). Answers
which regulations apply to a concrete product, which obligations follow, whether
the customer's implementation covers them, and whether a customer interpretation
is too narrow/broad/plausible.

- ProductProfile with tri-state facts (Optional[bool]=None => uncertain, never
  false security); safe predicate evaluator (no eval).
- 6 regulation triggers (CRA/MaschinenVO/RED/EMV/DataAct/NIS2) with missing-fact
  prompts; 24 obligation scope rules.
- CRA obligation_ids RE-USED verbatim from the registry (93 ids) — never re-minted
  (control_uuid trap); Machine/Data-Act flagged proposed=True.
- required_evidence constrained to the framework-agnostic shared evidence catalog;
  capabilities echo the planned Obligation->Capability layer.
- Overlap groups (CRA<->MaschinenVO cyber-safety) + evidence-for-multiple (USP).
- 4 endpoints POST /reasoning/{scope,obligations,implementation-assessment,
  interpretation-assessment}; thin handlers, registered in api/__init__.py.
- 22 tests (5 machine-builder scenarios + 10 acceptance questions). No DB
  migration, no RAG, no new controls.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-25 19:30:53 +02:00

143 lines
5.9 KiB
Python

"""Implementation reasoning engine (spec Modus 3).
Given a free-text claim ("Wir haben SBOMs und machen Updates, wenn Kunden Fehler
melden.") it maps the claimed capabilities onto the product's applicable
obligations and reports, per obligation, whether it is covered, partially
covered or not covered — plus the evidence that would close the gap.
"""
from __future__ import annotations
from typing import Dict, List
from .claim_normalizer import normalize_claim
from .enums import Confidence, CoverageStatus
from .obligation_engine import derive_obligations
from .schemas import (
CustomerImplementationClaim,
ImplementationAssessment,
ImplementationResponse,
ProductProfile,
)
from .taxonomy_claims import topics_for
# Typical sub-elements a capability still misses when only partially claimed.
STANDARD_GAPS: Dict[str, List[str]] = {
"software_bill_of_materials": [
"Vulnerability-Monitoring der Komponenten",
"Bewertung betroffener Komponenten",
"Lieferantenprozess",
],
"secure_updates": [
"aktive Schwachstellenüberwachung",
"Patch-Bewertung",
"Fristen und Verantwortlichkeiten",
"Nachweis der Updatefähigkeit",
],
"vulnerability_management": [
"definierter Vulnerability-Handling-Prozess",
"Priorisierung und Fristen",
],
"authentication": ["MFA für privilegierte Zugänge", "keine Standard-Zugangsdaten"],
"security_logging": ["Schutz der Logs vor Manipulation", "Monitoring/Alerting"],
"software_integrity": ["Signierung der Updates", "Verifikation der Update-Signatur"],
"secure_by_default": ["Härtung der Auslieferungskonfiguration", "Minimierung der Angriffsfläche"],
"secure_communication": ["verschlüsselte Übertragung", "Integritätsschutz der Verbindung"],
"risk_assessment": ["dokumentierte Risikobewertung", "Aufnahme in die technische Doku"],
"technical_documentation": ["vollständige technische Unterlagen", "Aktualisierung über den Lebenszyklus"],
}
def _missing_for(capabilities: List[str]) -> List[str]:
out: List[str] = []
for cap in capabilities:
for gap in STANDARD_GAPS.get(cap, []):
if gap not in out:
out.append(gap)
return out
def _coverage(required: List[str], claimed: List[str], qualifiers: List[str]) -> CoverageStatus:
req, have = set(required), set(claimed)
hit = req & have
if not hit:
return CoverageStatus.NOT_COVERED
if "absent" in qualifiers or "planned" in qualifiers:
return CoverageStatus.NOT_COVERED
if "reactive" in qualifiers and hit & {"secure_updates", "vulnerability_management"}:
return CoverageStatus.PARTIALLY_COVERED
if req <= have:
return CoverageStatus.COVERED
return CoverageStatus.PARTIALLY_COVERED
def assess_implementation(profile: ProductProfile, customer_claim: str) -> ImplementationResponse:
claim = normalize_claim(customer_claim)
obligations = derive_obligations(profile).applicable_obligations
claimed = claim.claimed_capability
claim_topics = set(claim.related_topics) | set(claimed)
assessments: List[ImplementationAssessment] = []
missing_evidence: List[str] = []
for ob in obligations:
from .rules_obligations import obligation_rule
rule = obligation_rule(ob.obligation_id)
required_caps = rule.required_capabilities if rule else []
ob_topics = set(topics_for(required_caps)) | set(required_caps)
directly_claimed = bool(set(required_caps) & set(claimed))
related = bool(ob_topics & claim_topics)
if not directly_claimed and not related:
continue # unrelated to the claim -> don't assess
status = _coverage(required_caps, claimed, claim.qualifiers)
missing = [] if status == CoverageStatus.COVERED else _missing_for(required_caps)
explanation = _explain(status, ob.title, claim.qualifiers)
if status != CoverageStatus.COVERED:
for ev in ob.required_evidence:
if ev not in missing_evidence:
missing_evidence.append(ev)
assessments.append(
ImplementationAssessment(
claim_id=claim.claim_id,
obligation_id=ob.obligation_id,
coverage_status=status,
missing_elements=missing,
required_evidence=ob.required_evidence,
explanation=explanation,
confidence=Confidence.MEDIUM,
)
)
return ImplementationResponse(
claim=claim,
assessments=assessments,
missing_evidence=missing_evidence,
summary=_summary(claim, assessments),
)
def _explain(status: CoverageStatus, title: str, qualifiers: List[str]) -> str:
if status == CoverageStatus.COVERED:
return "Die Pflicht '%s' wird durch die beschriebene Umsetzung plausibel abgedeckt." % title
if status == CoverageStatus.PARTIALLY_COVERED:
extra = " Der Prozess wirkt reaktiv." if "reactive" in qualifiers else ""
return "Die Pflicht '%s' ist nur teilweise abgedeckt.%s" % (title, extra)
return "Die Pflicht '%s' wird durch die Aussage nicht abgedeckt." % title
def _summary(claim: CustomerImplementationClaim, assessments: List[ImplementationAssessment]) -> str:
if not claim.claimed_capability:
return "Die Aussage ist zu unspezifisch — bitte konkretisieren, was umgesetzt wurde."
covered = sum(1 for a in assessments if a.coverage_status == CoverageStatus.COVERED)
partial = sum(1 for a in assessments if a.coverage_status == CoverageStatus.PARTIALLY_COVERED)
notc = sum(1 for a in assessments if a.coverage_status == CoverageStatus.NOT_COVERED)
if notc or partial:
head = "Teilweise erfüllt"
elif covered:
head = "Plausibel abgedeckt"
else:
head = "Nicht beurteilbar"
return "%s: %d abgedeckt, %d teilweise, %d offen." % (head, covered, partial, notc)