6673c8052b
"vollständig" still implied fulfillment. potentially_addresses now reads "… adressiert N Pflichten direkt und M teilweise; K werden durch die Aussage nicht berührt. … Dies ist keine Konformitätsaussage." Enum value kept (potentially_addresses chosen over addresses_claimed for product clarity). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
159 lines
7.0 KiB
Python
159 lines
7.0 KiB
Python
"""Implementation reasoning (spec Modus 3) — Welt 1 only.
|
|
|
|
Maps a free-text claim ("Wir haben SBOMs und machen Updates, wenn Kunden Fehler
|
|
melden.") onto the product's applicable obligations and reports, per obligation,
|
|
whether the *claim* potentially/partially/does-not address it — plus the
|
|
evidence that WOULD be needed to prove real implementation.
|
|
|
|
This is NOT a conformity verdict. It judges the customer's statement, never
|
|
whether the obligation is met. The real verdict (ComplianceStatus: erfüllt/
|
|
offen/unklar from verified evidence) lives in the Compliance Execution Graph.
|
|
The four reasoning layers: claim -> interpretation (capabilities/topics on the
|
|
claim) -> potential obligation coverage (`claim_coverage`) -> evidence required.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import Dict, List
|
|
|
|
from .claim_normalizer import normalize_claim
|
|
from .enums import ClaimCoverage, Confidence
|
|
from .obligation_engine import derive_obligations
|
|
from .schemas import (
|
|
ClaimObligationMapping,
|
|
CustomerImplementationClaim,
|
|
ImplementationReasoningResponse,
|
|
ProductProfile,
|
|
)
|
|
from .taxonomy_claims import topics_for
|
|
|
|
DISCLAIMER = (
|
|
"Diese Auswertung interpretiert ausschließlich die Kundenaussage (ClaimCoverage, Welt 1). "
|
|
"Sie ist KEINE Konformitätsaussage — der tatsächliche Compliance-Status (ComplianceStatus, "
|
|
"Welt 2) ergibt sich erst aus geprüften Nachweisen im Compliance Execution Graph."
|
|
)
|
|
|
|
# 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]) -> ClaimCoverage:
|
|
if not required:
|
|
return ClaimCoverage.INSUFFICIENT_INFORMATION
|
|
req, have = set(required), set(claimed)
|
|
hit = req & have
|
|
if not hit:
|
|
return ClaimCoverage.DOES_NOT_ADDRESS
|
|
if "absent" in qualifiers or "planned" in qualifiers:
|
|
return ClaimCoverage.DOES_NOT_ADDRESS
|
|
if "reactive" in qualifiers and hit & {"secure_updates", "vulnerability_management"}:
|
|
return ClaimCoverage.PARTIALLY_ADDRESSES
|
|
if req <= have:
|
|
return ClaimCoverage.POTENTIALLY_ADDRESSES
|
|
return ClaimCoverage.PARTIALLY_ADDRESSES
|
|
|
|
|
|
def reason_implementation_claim(
|
|
profile: ProductProfile, customer_claim: str
|
|
) -> ImplementationReasoningResponse:
|
|
claim = normalize_claim(customer_claim)
|
|
obligations = derive_obligations(profile).applicable_obligations
|
|
claimed = claim.claimed_capability
|
|
claim_topics = set(claim.related_topics) | set(claimed)
|
|
|
|
mappings: List[ClaimObligationMapping] = []
|
|
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 reason about it
|
|
|
|
coverage = _coverage(required_caps, claimed, claim.qualifiers)
|
|
missing = [] if coverage == ClaimCoverage.POTENTIALLY_ADDRESSES else _missing_for(required_caps)
|
|
if coverage != ClaimCoverage.POTENTIALLY_ADDRESSES:
|
|
for ev in ob.required_evidence:
|
|
if ev not in missing_evidence:
|
|
missing_evidence.append(ev)
|
|
mappings.append(
|
|
ClaimObligationMapping(
|
|
claim_id=claim.claim_id,
|
|
obligation_id=ob.obligation_id,
|
|
claim_coverage=coverage,
|
|
missing_elements=missing,
|
|
required_evidence=ob.required_evidence,
|
|
explanation=_explain(coverage, ob.title, claim.qualifiers),
|
|
confidence=Confidence.MEDIUM,
|
|
)
|
|
)
|
|
|
|
return ImplementationReasoningResponse(
|
|
claim=claim,
|
|
mappings=mappings,
|
|
missing_evidence=missing_evidence,
|
|
summary=_summary(claim, mappings),
|
|
disclaimer=DISCLAIMER,
|
|
)
|
|
|
|
|
|
def _explain(coverage: ClaimCoverage, title: str, qualifiers: List[str]) -> str:
|
|
if coverage == ClaimCoverage.POTENTIALLY_ADDRESSES:
|
|
return "Die Aussage adressiert die Pflicht '%s' direkt — Nachweise erforderlich für eine Bewertung der Umsetzung." % title
|
|
if coverage == ClaimCoverage.PARTIALLY_ADDRESSES:
|
|
extra = " Der beschriebene Prozess wirkt reaktiv." if "reactive" in qualifiers else ""
|
|
return "Die Aussage adressiert die Pflicht '%s' nur teilweise.%s" % (title, extra)
|
|
if coverage == ClaimCoverage.DOES_NOT_ADDRESS:
|
|
return "Die Aussage adressiert die Pflicht '%s' nicht." % title
|
|
return "Zur Pflicht '%s' liegen zu wenige Angaben für eine Einordnung vor." % title
|
|
|
|
|
|
def _summary(claim: CustomerImplementationClaim, mappings: List[ClaimObligationMapping]) -> str:
|
|
if not claim.claimed_capability:
|
|
return "Die Aussage ist zu unspezifisch — bitte konkretisieren, was umgesetzt wurde."
|
|
full = sum(1 for m in mappings if m.claim_coverage == ClaimCoverage.POTENTIALLY_ADDRESSES)
|
|
partial = sum(1 for m in mappings if m.claim_coverage == ClaimCoverage.PARTIALLY_ADDRESSES)
|
|
none = sum(1 for m in mappings if m.claim_coverage == ClaimCoverage.DOES_NOT_ADDRESS)
|
|
return (
|
|
"Die beschriebene Maßnahme adressiert wahrscheinlich %d Pflicht(en) direkt und %d "
|
|
"teilweise; %d werden durch die Aussage nicht berührt. Für eine Bewertung der tatsächlichen "
|
|
"Umsetzung sind Nachweise erforderlich. Dies ist keine Konformitätsaussage." % (full, partial, none)
|
|
)
|