1607c89459
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>
143 lines
5.9 KiB
Python
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)
|