"""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)