From b71771e52e34f3d012ce8923a0b0d5a0664f9918 Mon Sep 17 00:00:00 2001 From: Benjamin Admin Date: Sun, 28 Jun 2026 09:42:31 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20Customer=20Mission=20#4=20=E2=80=94=20a?= =?UTF-8?q?=20second,=20different=20contract=20target=20(no=20tender-speci?= =?UTF-8?q?al-logic)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit One contract example (Mission #3's public tender) is not enough to safely generalise: it risks baking tender-shaped assumptions into the later Scope→Journey selector. This mission runs TWO deliberately different contract sub-types against the same company through the IDENTICAL engine: - public tender (procurement: pentest report, references, support SLA, SBOM) -> delta 4 - private OEM spec (Lastenheft: CSMS, functional safety, SUMS, ASPICE) -> delta 3 The two deltas are completely DISJOINT (no shared missing capability), proving the contracts are genuinely different — yet there is no per-contract code: assess_transition treats each as a plain Required set, exactly like a regulation or a certification. Evidence-Relevance is target-relative even between two contracts (TISAX worth more to the automotive OEM than to the generic tender). Conclusion: "Contract" as a requirement source is now covered by >=2 diverse cases, so the later selector can treat any contract uniformly. Synthetic company + synthetic contracts (NO real names). Non-runtime -> no deploy. 5 tests pass. --- .../reference_scenarios/customer_mission_4.md | 39 +++++ .../mission_second_contract.py | 134 ++++++++++++++++++ .../tests/test_customer_mission_4.py | 57 ++++++++ 3 files changed, 230 insertions(+) create mode 100644 backend-compliance/reference_scenarios/customer_mission_4.md create mode 100644 backend-compliance/reference_scenarios/mission_second_contract.py create mode 100644 backend-compliance/tests/test_customer_mission_4.py diff --git a/backend-compliance/reference_scenarios/customer_mission_4.md b/backend-compliance/reference_scenarios/customer_mission_4.md new file mode 100644 index 00000000..2462df3b --- /dev/null +++ b/backend-compliance/reference_scenarios/customer_mission_4.md @@ -0,0 +1,39 @@ +# Customer Mission #4 — zwei verschiedene Verträge, eine Engine (kein Contract-Spezialfall) + +_Mission #3 zeigte EINEN Vertrag (öffentliche Ausschreibung) durch dieselbe Engine wie Gesetz/Zertifizierung. Ein Beispiel reicht nicht — sonst backen wir Tender-Annahmen in den späteren Selektor. Hier laufen ZWEI bewusst unterschiedliche Vertragsarten gegen dasselbe Unternehmen. Synthetischer Kunde + synthetische Verträge, keine echten Namen._ + +## Der Kunde (synthetisch) — EIN Profil +> **ISO 9001 · ISO 27001 · ISO 14001 · TISAX · CE-Prozess · PSIRT** · vernetzte Maschinen · Export EU + +## 1. Zwei Vertragsarten — dieselbe Engine `Profil − Required = Delta` + +| Vertrag | Art | geforderte Fähigkeiten | Delta (fehlt) | +|---|---|---|---| +| **Öffentliche Ausschreibung** | public tender | 10 | **4** | +| **OEM-Lastenheft** | private OEM spec | 11 | **3** | + +→ Beide Verträge sind nur ein `Required`-Satz; **es gibt keinen Contract-spezifischen Codepfad** — `assess_transition` behandelt sie identisch zu Gesetz und Zertifizierung. + +## 2. Verschiedene Verträge → verschiedene Deltas (Beweis: keine Tender-Speziallogik nötig) +- **Nur Ausschreibung:** `penetration_test_evidence`, `reference_project_evidence`, `sbom_creation`, `security_sla_and_support_commitment` +- **Nur OEM-Lastenheft:** `aspice_process_capability`, `functional_safety_evidence`, `software_update_management_system` +- **Beiden gemeinsam:** — + +→ Die zwei Verträge fordern **strukturell anderes** (Beschaffungsnachweise vs. Automotive-Engineering: CSMS/funktionale Sicherheit/SUMS/ASPICE). Trotzdem **ein** Mechanismus. Genau das brauchten wir vor dem Selektor: zwei diverse Contract-Ziele, kein Sonderpfad. + +## 3. Evidence-Relevanz(Vertrag) +| Zertifizierung (Evidence) | → Ausschreibung | → OEM-Lastenheft | +|---|---|---| +| **ISO27001** | hoch (4) | hoch (4) | +| **TISAX** | hoch (4) | hoch (6) | +| **PSIRT** | mittel (1) | mittel (2) | +| **ISO9001** | keine (0) | keine (0) | +| **ISO14001** | keine (0) | keine (0) | +| **CE** | keine (0) | keine (0) | + +→ **TISAX** zählt gegen den **Automotive-OEM** mehr als gegen die generische Ausschreibung (Prototype Protection, CSMS); **ISO 14001** ist gegen beide **keine**. Bestätigt: **Relevanz ist eine Funktion des Ziels** — auch zwischen zwei Verträgen. + +## Befund + +> **Zwei strukturell verschiedene Verträge, ein Mechanismus, keine Zeile Contract-Spezialcode.** Damit ist „Contract" als Anforderungsquelle abgesichert (≥2 diverse Fälle): der spätere Scope→Journey-Selektor kann **jeden** Vertrag als reinen `Required`-Satz behandeln, ohne Tender-Speziallogik. Nächste sinnvolle Diversität vor dem Selektor: ein Vertrag, der bewusst auf NICHT-Security-Fähigkeiten zielt (z. B. Umwelt-/Materialnachweise), um auch die zielrelative Evidence-Relevanz über Domänen hinweg zu prüfen. + diff --git a/backend-compliance/reference_scenarios/mission_second_contract.py b/backend-compliance/reference_scenarios/mission_second_contract.py new file mode 100644 index 00000000..8dc7d1a5 --- /dev/null +++ b/backend-compliance/reference_scenarios/mission_second_contract.py @@ -0,0 +1,134 @@ +# ruff: noqa +# mypy: ignore-errors +"""Customer Mission #4 — a SECOND, different contract target (no tender-special-logic). + +Mission #3 showed one contract (a public tender) runs through the same engine as a regulation and a +certification. One contract is not enough: with a single example we might accidentally bake +tender-shaped assumptions into the later Scope→Journey selector. So this mission runs TWO deliberately +different contract sub-types against the same company through the same engine: + + - a PUBLIC TENDER (öffentliche Ausschreibung — procurement: pentest report, references, SLA) + - a PRIVATE OEM SPEC (Lastenheft — automotive customer: CSMS, functional safety, SUMS, ASPICE) + +If both reduce to a plain `Required` set and produce DIFFERENT deltas with the IDENTICAL engine and NO +per-contract code, then „Contract" is not a special case — it is just another requirement source, and +the selector can treat any contract uniformly. + +Synthetic company + synthetic contracts (NO real names). Runs the REAL engine. Non-runtime -> no deploy. +Run: cd backend-compliance && PYTHONPATH=. python3 reference_scenarios/mission_second_contract.py +""" +from __future__ import annotations + +from compliance.company import ( + CompanyContext, Certification, CapabilityMappingEntry, build_company_profile, +) +from compliance.reasoning.enums import Confidence +from compliance.transition_reasoning import ( + TransitionContext, TransitionGoal, TargetRequirement, assess_transition, CoverageStatus, +) + +OUT = [] + + +def w(s=""): + OUT.append(s) + + +# ── Two contracts, each just a set of required capabilities (a contract has no parser — injected) ── +TENDER = [ # public procurement: security baseline + procurement-specific evidence + "information_security_management", "access_control_and_authentication", "incident_management", + "technical_vulnerability_management", "coordinated_vulnerability_disclosure", "sbom_creation", + "supplier_security", "penetration_test_evidence", "reference_project_evidence", + "security_sla_and_support_commitment", +] +OEM_SPEC = [ # private automotive customer Lastenheft: security + automotive-engineering-specific + "information_security_management", "access_control_and_authentication", "incident_management", + "supplier_security", "prototype_protection", "secure_signed_update_distribution", + "dedicated_security_contact", "cybersecurity_management_system", "software_update_management_system", + "functional_safety_evidence", "aspice_process_capability", +] +CONTRACTS = [ + ("Öffentliche Ausschreibung", "public tender", TENDER), + ("OEM-Lastenheft", "private OEM spec", OEM_SPEC), +] + +# ── ONE company profile (same multi-certified archetype as Missions #2/#3) ────────────────── +CERT_OBS = { + "ISO27001": ["information_security_management", "incident_management", "access_control_and_authentication", + "technical_vulnerability_management", "security_logging_and_monitoring", + "secure_development_lifecycle", "cybersecurity_management_system"], + "TISAX": ["information_security_management", "access_control_and_authentication", "incident_management", + "supplier_security", "prototype_protection", "cybersecurity_management_system"], + "PSIRT": ["coordinated_vulnerability_disclosure", "dedicated_security_contact", + "secure_signed_update_distribution"], + "ISO9001": ["ce_conformity_assessment_and_technical_documentation"], + "ISO14001": ["environmental_management_documentation"], + "CE": ["ce_conformity_assessment_and_technical_documentation"], +} +cmap = {k: CapabilityMappingEntry(capability_ids=v, confidence=Confidence.MEDIUM) for k, v in CERT_OBS.items()} +profile = build_company_profile( + CompanyContext(company_id="mc4", certifications=[Certification(certification_id=k) for k in CERT_OBS]), cmap) + + +def _delta(req_caps): + reqs = [TargetRequirement(capability_id=c) for c in req_caps] + a = assess_transition(TransitionContext(company_id="mc4", target=TransitionGoal(target_id="c")), reqs, profile) + return sorted({c.capability_id for c in a.coverage if c.status == CoverageStatus.MISSING}) + + +def _relevance(cert_caps, req_caps): + n = len(set(cert_caps) & set(req_caps)) + return n, ("hoch" if n >= 3 else "mittel" if n >= 1 else "keine") + + +w('# Customer Mission #4 — zwei verschiedene Verträge, eine Engine (kein Contract-Spezialfall)') +w("") +w('_Mission #3 zeigte EINEN Vertrag (öffentliche Ausschreibung) durch dieselbe Engine wie Gesetz/Zertifizierung. Ein Beispiel reicht nicht — sonst backen wir Tender-Annahmen in den späteren Selektor. Hier laufen ZWEI bewusst unterschiedliche Vertragsarten gegen dasselbe Unternehmen. Synthetischer Kunde + synthetische Verträge, keine echten Namen._') +w("") +w("## Der Kunde (synthetisch) — EIN Profil") +w("> **ISO 9001 · ISO 27001 · ISO 14001 · TISAX · CE-Prozess · PSIRT** · vernetzte Maschinen · Export EU") +w("") + +# ── 1. Zwei Verträge durch DIESELBE Engine ──────────────────────────────── +deltas = {name: _delta(caps) for name, _, caps in CONTRACTS} +w("## 1. Zwei Vertragsarten — dieselbe Engine `Profil − Required = Delta`") +w("") +w("| Vertrag | Art | geforderte Fähigkeiten | Delta (fehlt) |") +w("|---|---|---|---|") +for name, kind, caps in CONTRACTS: + w("| **%s** | %s | %d | **%d** |" % (name, kind, len(caps), len(deltas[name]))) +w("") +w("→ Beide Verträge sind nur ein `Required`-Satz; **es gibt keinen Contract-spezifischen Codepfad** — `assess_transition` behandelt sie identisch zu Gesetz und Zertifizierung.") +w("") + +# ── 2. Die Deltas sind UNTERSCHIEDLICH (also echte verschiedene Verträge) ── +only_t = sorted(set(deltas["Öffentliche Ausschreibung"]) - set(deltas["OEM-Lastenheft"])) +only_o = sorted(set(deltas["OEM-Lastenheft"]) - set(deltas["Öffentliche Ausschreibung"])) +shared = sorted(set(deltas["Öffentliche Ausschreibung"]) & set(deltas["OEM-Lastenheft"])) +w("## 2. Verschiedene Verträge → verschiedene Deltas (Beweis: keine Tender-Speziallogik nötig)") +w("- **Nur Ausschreibung:** %s" % (", ".join("`%s`" % c for c in only_t) or "—")) +w("- **Nur OEM-Lastenheft:** %s" % (", ".join("`%s`" % c for c in only_o) or "—")) +w("- **Beiden gemeinsam:** %s" % (", ".join("`%s`" % c for c in shared) or "—")) +w("") +w("→ Die zwei Verträge fordern **strukturell anderes** (Beschaffungsnachweise vs. Automotive-Engineering: CSMS/funktionale Sicherheit/SUMS/ASPICE). Trotzdem **ein** Mechanismus. Genau das brauchten wir vor dem Selektor: zwei diverse Contract-Ziele, kein Sonderpfad.") +w("") + +# ── 3. Evidence-Relevance(Vertrag) — dieselbe Evidence, anderer Wert je Vertrag ── +w("## 3. Evidence-Relevanz(Vertrag)") +w("| Zertifizierung (Evidence) | → Ausschreibung | → OEM-Lastenheft |") +w("|---|---|---|") +for cert, caps in CERT_OBS.items(): + nt, lt = _relevance(caps, TENDER) + no, lo = _relevance(caps, OEM_SPEC) + w("| **%s** | %s (%d) | %s (%d) |" % (cert, lt, nt, lo, no)) +w("") +w("→ **TISAX** zählt gegen den **Automotive-OEM** mehr als gegen die generische Ausschreibung (Prototype Protection, CSMS); **ISO 14001** ist gegen beide **keine**. Bestätigt: **Relevanz ist eine Funktion des Ziels** — auch zwischen zwei Verträgen.") +w("") + +# ── Befund ──────────────────────────────────────────────────────────────── +w("## Befund") +w("") +w('> **Zwei strukturell verschiedene Verträge, ein Mechanismus, keine Zeile Contract-Spezialcode.** Damit ist „Contract" als Anforderungsquelle abgesichert (≥2 diverse Fälle): der spätere Scope→Journey-Selektor kann **jeden** Vertrag als reinen `Required`-Satz behandeln, ohne Tender-Speziallogik. Nächste sinnvolle Diversität vor dem Selektor: ein Vertrag, der bewusst auf NICHT-Security-Fähigkeiten zielt (z. B. Umwelt-/Materialnachweise), um auch die zielrelative Evidence-Relevanz über Domänen hinweg zu prüfen.') +w("") + +print("\n".join(OUT)) diff --git a/backend-compliance/tests/test_customer_mission_4.py b/backend-compliance/tests/test_customer_mission_4.py new file mode 100644 index 00000000..dd3881a8 --- /dev/null +++ b/backend-compliance/tests/test_customer_mission_4.py @@ -0,0 +1,57 @@ +"""Customer Mission #4 — a second, different contract target (no tender-special-logic). + +Pins what Mission #4 guards: TWO structurally different contract sub-types (a public tender and a +private OEM Lastenheft) run through the identical engine and produce DIFFERENT, non-overlapping deltas +with no per-contract code. That is the evidence that the later Scope→Journey selector can treat any +contract as a plain Required set — no tender-shaped special case baked in. +""" + +from __future__ import annotations + +import os +import subprocess +import sys + + +def _run(): + root = os.path.join(os.path.dirname(__file__), "..") + r = subprocess.run( + [sys.executable, "reference_scenarios/mission_second_contract.py"], + cwd=root, env={**os.environ, "PYTHONPATH": "."}, capture_output=True, text=True, + ) + assert r.returncode == 0, r.stderr + return r.stdout + + +def test_runs_end_to_end(): + out = _run() + assert "Customer Mission #4" in out + assert "kein Contract-Spezialfall" in out + + +def test_two_distinct_contract_types_one_engine(): + out = _run() + assert "public tender" in out and "private OEM spec" in out + assert "keinen Contract-spezifischen Codepfad" in out + + +def test_contracts_produce_different_deltas(): + out = _run() + # the two contracts must be genuinely different: their deltas do not overlap + assert "**Beiden gemeinsam:** —" in out + # each carries its own distinctive missing capabilities + assert "penetration_test_evidence" in out # tender-only + assert "functional_safety_evidence" in out # OEM-only + + +def test_evidence_relevance_differs_between_contracts(): + out = _run() + # TISAX is worth more against the automotive OEM spec than the generic tender + assert "**TISAX** | hoch (4) | hoch (6) |" in out + assert "Relevanz ist eine Funktion des Ziels" in out + + +def test_no_real_company_names(): + out = _run().lower() + for name in ["eto", "owis", "winterhalter"]: + assert name not in out