Merge origin/main (Compliance endpoint+graph-loader 2341bda6) in capability-model
This commit is contained in:
@@ -33,6 +33,12 @@ COPY migrations/ ./migrations/
|
|||||||
# Copy policy files (YAML rules)
|
# Copy policy files (YAML rules)
|
||||||
COPY policies/ ./policies/
|
COPY policies/ ./policies/
|
||||||
|
|
||||||
|
# Copy Compliance Execution Graph data (file-backed: Registry join-key copy + accepted control
|
||||||
|
# mappings + evidence requirements) consumed by GET /sdk/v1/compliance/obligation-status.
|
||||||
|
COPY data/control_mappings/ ./data/control_mappings/
|
||||||
|
COPY data/evidence_requirements/ ./data/evidence_requirements/
|
||||||
|
COPY data/obligations/ ./data/obligations/
|
||||||
|
|
||||||
# Create non-root user
|
# Create non-root user
|
||||||
RUN adduser -D -u 1000 appuser
|
RUN adduser -D -u 1000 appuser
|
||||||
USER appuser
|
USER appuser
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
// Control-Mapping: CRA Annex I -> NIST SP 800-53 Rev. 5. Eine Zeile = ein Mapping (Schema: ControlMapping).
|
||||||
|
// Reviewt 2026-06-25 (benjamin): 3 accepted, mapping_type=primary_implementation (kanonische Primaer-Control je Anforderung).
|
||||||
|
// Heimat der OWASP-Rejects (2)(e)/(2)(l)/(2)(i): dort war OWASP nicht der Zielstandard ("Mapping ueber NIST/BSI erforderlich").
|
||||||
|
// related-Controls (SC-3(3), RA-5, AC-6, SI-16, ...) folgen separat als mapping_type=supports — hier nur der kanonische Einstieg.
|
||||||
|
// obligation_id ADOPTIERT (2026-06-25, Registry-Handoff #4): SI-7->signed_update_integrity, SI-2->provide_security_updates, CM-7->remote_access_attack_surface_min. Join jetzt exakt (nicht citation_unit). CM-7 ist breiter als remote_access — Registry waehlte die vorhandene Obligation; Verfeinerung via Capability/cross_domain spaeter.
|
||||||
|
{"source_norm": "CRA Annex I Part I (2)(e) — Integritaet", "source_role": "operational_requirement", "target_framework": "NIST SP 800-53", "target_control": "SI-7", "mapping_type": "primary_implementation", "mapping_status": "accepted", "provenance": "human_curated", "rationale": "NIST SI-7 = Software, Firmware, and Information Integrity — kanonische Integritaetskontrolle (Signaturpruefung, Manipulationserkennung).", "reviewed_by": "benjamin", "review_date": "2026-06-25", "review_reason": "Primaere Implementierung der CRA-Integritaetsanforderung; OWASP war hier kein passender Treffer. Related (spaeter, supports): SA-10, CM-14.", "version": "2026-06-25", "obligation_id": "signed_update_integrity"}
|
||||||
|
{"source_norm": "CRA Annex I Part I (2)(l) — Sichere Updates", "source_role": "operational_requirement", "target_framework": "NIST SP 800-53", "target_control": "SI-2", "mapping_type": "primary_implementation", "mapping_status": "accepted", "provenance": "human_curated", "rationale": "NIST SI-2 = Flaw Remediation — kanonische Update-/Patch-Kontrolle.", "reviewed_by": "benjamin", "review_date": "2026-06-25", "review_reason": "Primaere Implementierung der CRA-Update-Anforderung. Related (spaeter, supports): RA-5, CM-3, SA-11.", "version": "2026-06-25", "obligation_id": "provide_security_updates"}
|
||||||
|
{"source_norm": "CRA Annex I Part I (2)(i) — Angriffsflaeche minimieren", "source_role": "operational_requirement", "target_framework": "NIST SP 800-53", "target_control": "CM-7", "mapping_type": "primary_implementation", "mapping_status": "accepted", "provenance": "human_curated", "rationale": "NIST CM-7 = Least Functionality — Deaktivierung nicht benoetigter Ports/Dienste/Funktionen.", "reviewed_by": "benjamin", "review_date": "2026-06-25", "review_reason": "CM-7 als Primaer-Control fuer Angriffsflaeche (nicht SC-3(3)). Related (spaeter, supports): SC-3(3), AC-6, SI-16.", "version": "2026-06-25", "obligation_id": "remote_access_attack_surface_min"}
|
||||||
@@ -2,13 +2,13 @@
|
|||||||
// Reviewt 2026-06-25 (benjamin): 7 accepted, 13 rejected. accepted = Audit-Wahrheit (Advisor nutzt acceptedOnly).
|
// Reviewt 2026-06-25 (benjamin): 7 accepted, 13 rejected. accepted = Audit-Wahrheit (Advisor nutzt acceptedOnly).
|
||||||
// rejected bleiben als Audit-Spur ("warum verworfen"). KEIN confidence — kuratiert = fachliche Feststellung.
|
// rejected bleiben als Audit-Spur ("warum verworfen"). KEIN confidence — kuratiert = fachliche Feststellung.
|
||||||
// Architekturbeweis: CRA -> OWASP fuer AppSec/Auth/Crypto/Logging; Ops/Update/Attack-Surface/Integritaet -> NIST/BSI.
|
// Architekturbeweis: CRA -> OWASP fuer AppSec/Auth/Crypto/Logging; Ops/Update/Attack-Surface/Integritaet -> NIST/BSI.
|
||||||
{"source_norm": "CRA Annex I Part I (2)(c) — Schutz vor unbefugtem Zugriff", "source_role": "operational_requirement", "target_framework": "OWASP ASVS", "target_control": "V6.3.1", "mapping_type": "supports", "mapping_status": "accepted", "provenance": "human_curated", "rationale": "V6 = Authentication.", "reviewed_by": "benjamin", "review_date": "2026-06-25", "review_reason": "V6 = Authentication, sauberer Treffer fuer Zugriffsschutz/Authentisierung.", "version": "2026-06-25"}
|
{"source_norm": "CRA Annex I Part I (2)(c) — Schutz vor unbefugtem Zugriff", "source_role": "operational_requirement", "target_framework": "OWASP ASVS", "target_control": "V6.3.1", "mapping_type": "supports", "mapping_status": "accepted", "provenance": "human_curated", "rationale": "V6 = Authentication.", "reviewed_by": "benjamin", "review_date": "2026-06-25", "review_reason": "V6 = Authentication, sauberer Treffer fuer Zugriffsschutz/Authentisierung.", "version": "2026-06-25", "obligation_id": "user_authentication_required"}
|
||||||
{"source_norm": "CRA Annex I Part I (2)(c) — Schutz vor unbefugtem Zugriff", "source_role": "operational_requirement", "target_framework": "OWASP ASVS", "target_control": "V6.1.1", "mapping_type": "supports", "mapping_status": "accepted", "provenance": "human_curated", "rationale": "V6 = Authentication.", "reviewed_by": "benjamin", "review_date": "2026-06-25", "review_reason": "V6 = Authentication, sauberer Treffer fuer Zugriffsschutz/Authentisierung.", "version": "2026-06-25"}
|
{"source_norm": "CRA Annex I Part I (2)(c) — Schutz vor unbefugtem Zugriff", "source_role": "operational_requirement", "target_framework": "OWASP ASVS", "target_control": "V6.1.1", "mapping_type": "supports", "mapping_status": "accepted", "provenance": "human_curated", "rationale": "V6 = Authentication.", "reviewed_by": "benjamin", "review_date": "2026-06-25", "review_reason": "V6 = Authentication, sauberer Treffer fuer Zugriffsschutz/Authentisierung.", "version": "2026-06-25", "obligation_id": "user_authentication_required"}
|
||||||
{"source_norm": "CRA Annex I Part I (2)(d) — Vertraulichkeit / Verschluesselung", "source_role": "operational_requirement", "target_framework": "OWASP ASVS", "target_control": "V11.2.1", "mapping_type": "supports", "mapping_status": "accepted", "provenance": "human_curated", "rationale": "V11 = Cryptography.", "reviewed_by": "benjamin", "review_date": "2026-06-25", "review_reason": "Korrektur von V14: V11 = Cryptography, richtiger Bereich fuer Verschluesselung.", "version": "2026-06-25"}
|
{"source_norm": "CRA Annex I Part I (2)(d) — Vertraulichkeit / Verschluesselung", "source_role": "operational_requirement", "target_framework": "OWASP ASVS", "target_control": "V11.2.1", "mapping_type": "supports", "mapping_status": "accepted", "provenance": "human_curated", "rationale": "V11 = Cryptography.", "reviewed_by": "benjamin", "review_date": "2026-06-25", "review_reason": "Korrektur von V14: V11 = Cryptography, richtiger Bereich fuer Verschluesselung.", "version": "2026-06-25", "obligation_id": "credential_confidentiality_protection"}
|
||||||
{"source_norm": "CRA Annex I Part I (2)(d) — Vertraulichkeit / Verschluesselung", "source_role": "operational_requirement", "target_framework": "OWASP ASVS", "target_control": "V11.7.1", "mapping_type": "supports", "mapping_status": "accepted", "provenance": "human_curated", "rationale": "V11.7 = Key Management.", "reviewed_by": "benjamin", "review_date": "2026-06-25", "review_reason": "Korrektur von V14: V11.7 = Key Management fuer Verschluesselung/Schluesselverwaltung.", "version": "2026-06-25"}
|
{"source_norm": "CRA Annex I Part I (2)(d) — Vertraulichkeit / Verschluesselung", "source_role": "operational_requirement", "target_framework": "OWASP ASVS", "target_control": "V11.7.1", "mapping_type": "supports", "mapping_status": "accepted", "provenance": "human_curated", "rationale": "V11.7 = Key Management.", "reviewed_by": "benjamin", "review_date": "2026-06-25", "review_reason": "Korrektur von V14: V11.7 = Key Management fuer Verschluesselung/Schluesselverwaltung.", "version": "2026-06-25", "obligation_id": "auth_key_management"}
|
||||||
{"source_norm": "CRA Annex I Part I (2)(k) — Sicherheitsrelevante Ereignisse / Logging", "source_role": "operational_requirement", "target_framework": "OWASP ASVS", "target_control": "V16.3.3", "mapping_type": "supports", "mapping_status": "accepted", "provenance": "human_curated", "rationale": "V16 = Security Logging.", "reviewed_by": "benjamin", "review_date": "2026-06-25", "review_reason": "V16 = Logging, sauberer Treffer fuer sicherheitsrelevante Ereignisse.", "version": "2026-06-25"}
|
{"source_norm": "CRA Annex I Part I (2)(k) — Sicherheitsrelevante Ereignisse / Logging", "source_role": "operational_requirement", "target_framework": "OWASP ASVS", "target_control": "V16.3.3", "mapping_type": "supports", "mapping_status": "accepted", "provenance": "human_curated", "rationale": "V16 = Security Logging.", "reviewed_by": "benjamin", "review_date": "2026-06-25", "review_reason": "V16 = Logging, sauberer Treffer fuer sicherheitsrelevante Ereignisse.", "version": "2026-06-25", "obligation_id": "event_logging_security_events"}
|
||||||
{"source_norm": "CRA Annex I Part I (2)(k) — Sicherheitsrelevante Ereignisse / Logging", "source_role": "operational_requirement", "target_framework": "OWASP ASVS", "target_control": "V16.3.4", "mapping_type": "supports", "mapping_status": "accepted", "provenance": "human_curated", "rationale": "V16 = Security Logging.", "reviewed_by": "benjamin", "review_date": "2026-06-25", "review_reason": "V16 = Logging, sauberer Treffer fuer sicherheitsrelevante Ereignisse.", "version": "2026-06-25"}
|
{"source_norm": "CRA Annex I Part I (2)(k) — Sicherheitsrelevante Ereignisse / Logging", "source_role": "operational_requirement", "target_framework": "OWASP ASVS", "target_control": "V16.3.4", "mapping_type": "supports", "mapping_status": "accepted", "provenance": "human_curated", "rationale": "V16 = Security Logging.", "reviewed_by": "benjamin", "review_date": "2026-06-25", "review_reason": "V16 = Logging, sauberer Treffer fuer sicherheitsrelevante Ereignisse.", "version": "2026-06-25", "obligation_id": "event_logging_security_events"}
|
||||||
{"source_norm": "CRA Annex I Part I (2)(k) — Sicherheitsrelevante Ereignisse / Logging", "source_role": "operational_requirement", "target_framework": "OWASP ASVS", "target_control": "V16.1.1", "mapping_type": "supports", "mapping_status": "accepted", "provenance": "human_curated", "rationale": "V16 = Security Logging.", "reviewed_by": "benjamin", "review_date": "2026-06-25", "review_reason": "V16 = Logging, sauberer Treffer fuer sicherheitsrelevante Ereignisse.", "version": "2026-06-25"}
|
{"source_norm": "CRA Annex I Part I (2)(k) — Sicherheitsrelevante Ereignisse / Logging", "source_role": "operational_requirement", "target_framework": "OWASP ASVS", "target_control": "V16.1.1", "mapping_type": "supports", "mapping_status": "accepted", "provenance": "human_curated", "rationale": "V16 = Security Logging.", "reviewed_by": "benjamin", "review_date": "2026-06-25", "review_reason": "V16 = Logging, sauberer Treffer fuer sicherheitsrelevante Ereignisse.", "version": "2026-06-25", "obligation_id": "event_logging_security_events"}
|
||||||
{"source_norm": "CRA Annex I Part I (2)(c) — Schutz vor unbefugtem Zugriff", "source_role": "operational_requirement", "target_framework": "OWASP ASVS", "target_control": "V14.2.4", "mapping_type": "related", "mapping_status": "rejected", "provenance": "human_curated", "rationale": "Retriever-Kandidat.", "reviewed_by": "benjamin", "review_date": "2026-06-25", "review_reason": "V14 = Config, kein Auth — verworfen.", "version": "2026-06-25"}
|
{"source_norm": "CRA Annex I Part I (2)(c) — Schutz vor unbefugtem Zugriff", "source_role": "operational_requirement", "target_framework": "OWASP ASVS", "target_control": "V14.2.4", "mapping_type": "related", "mapping_status": "rejected", "provenance": "human_curated", "rationale": "Retriever-Kandidat.", "reviewed_by": "benjamin", "review_date": "2026-06-25", "review_reason": "V14 = Config, kein Auth — verworfen.", "version": "2026-06-25"}
|
||||||
{"source_norm": "CRA Annex I Part I (2)(d) — Vertraulichkeit / Verschluesselung", "source_role": "operational_requirement", "target_framework": "OWASP ASVS", "target_control": "V14.2.4", "mapping_type": "related", "mapping_status": "rejected", "provenance": "human_curated", "rationale": "Retriever-Kandidat.", "reviewed_by": "benjamin", "review_date": "2026-06-25", "review_reason": "V14 = Config, Crypto gehoert zu V11 — verworfen.", "version": "2026-06-25"}
|
{"source_norm": "CRA Annex I Part I (2)(d) — Vertraulichkeit / Verschluesselung", "source_role": "operational_requirement", "target_framework": "OWASP ASVS", "target_control": "V14.2.4", "mapping_type": "related", "mapping_status": "rejected", "provenance": "human_curated", "rationale": "Retriever-Kandidat.", "reviewed_by": "benjamin", "review_date": "2026-06-25", "review_reason": "V14 = Config, Crypto gehoert zu V11 — verworfen.", "version": "2026-06-25"}
|
||||||
{"source_norm": "CRA Annex I Part I (2)(d) — Vertraulichkeit / Verschluesselung", "source_role": "operational_requirement", "target_framework": "OWASP ASVS", "target_control": "V14.3.2", "mapping_type": "related", "mapping_status": "rejected", "provenance": "human_curated", "rationale": "Retriever-Kandidat.", "reviewed_by": "benjamin", "review_date": "2026-06-25", "review_reason": "V14 = Config, Crypto gehoert zu V11 — verworfen.", "version": "2026-06-25"}
|
{"source_norm": "CRA Annex I Part I (2)(d) — Vertraulichkeit / Verschluesselung", "source_role": "operational_requirement", "target_framework": "OWASP ASVS", "target_control": "V14.3.2", "mapping_type": "related", "mapping_status": "rejected", "provenance": "human_curated", "rationale": "Retriever-Kandidat.", "reviewed_by": "benjamin", "review_date": "2026-06-25", "review_reason": "V14 = Config, Crypto gehoert zu V11 — verworfen.", "version": "2026-06-25"}
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
// Evidence-Requirements je NIST-SP-800-53-Control (Schema: EvidenceRequirement). Eine Zeile = eine geforderte Evidenz.
|
||||||
|
// WICHTIG: evidence_type ist FRAMEWORK-AGNOSTISCH (geteilter Katalog config_export/test_report/repo_scan/sbom/...) —
|
||||||
|
// dieselben Typen tragen CRA, NIST, ISO 27001, IEC 62443, BSI. (framework, control) ist nur der Verweis, nicht der Typ.
|
||||||
|
// Stand 2026-06-25, Basis: die 3 accepted CRA->NIST primary_implementation-Mappings (SI-7 Integritaet, SI-2 Updates, CM-7 Angriffsflaeche).
|
||||||
|
{"framework": "NIST SP 800-53", "control": "SI-7", "evidence_type": "config_export", "evidence_source": "github", "freshness_requirement": "per_release", "required": true, "rationale": "Secure-Boot-/Code-Signing-Konfiguration als Nachweis der Integritaetspruefung.", "version": "2026-06-25"}
|
||||||
|
{"framework": "NIST SP 800-53", "control": "SI-7", "evidence_type": "test_report", "evidence_source": "ci", "freshness_requirement": "per_release", "required": true, "rationale": "Signatur-/Integritaets-Verifikationstest (CI) belegt funktionierende Manipulationserkennung.", "version": "2026-06-25"}
|
||||||
|
{"framework": "NIST SP 800-53", "control": "SI-2", "evidence_type": "repo_scan", "evidence_source": "scanner", "freshness_requirement": "continuous", "required": true, "rationale": "Fortlaufender Dependency-/Vuln-Scan weist Behebung bekannter ausnutzbarer Schwachstellen nach.", "version": "2026-06-25"}
|
||||||
|
{"framework": "NIST SP 800-53", "control": "SI-2", "evidence_type": "config_export", "evidence_source": "github", "freshness_requirement": "per_release", "required": true, "rationale": "Konfiguration des sicheren Update-/Patch-Mechanismus als technischer Nachweis.", "version": "2026-06-25"}
|
||||||
|
{"framework": "NIST SP 800-53", "control": "CM-7", "evidence_type": "config_export", "evidence_source": "github", "freshness_requirement": "per_release", "required": true, "rationale": "Konfiguration deaktivierter Ports/Dienste/Funktionen als Nachweis minimierter Angriffsflaeche.", "version": "2026-06-25"}
|
||||||
|
{"framework": "NIST SP 800-53", "control": "CM-7", "evidence_type": "repo_scan", "evidence_source": "scanner", "freshness_requirement": "quarterly", "required": false, "rationale": "Angriffsflaechen-Scan (offene Ports/Dienste) — vertiefend, nicht Pflicht je Release.", "version": "2026-06-25"}
|
||||||
@@ -0,0 +1,826 @@
|
|||||||
|
{
|
||||||
|
"schema_version": "obligation_join_keys_v1",
|
||||||
|
"contract": "obligation_id ist der stabile Join-Key. Legal Knowledge Graph haengt citation_spans an obligation_id; Compliance Execution Graph mappt control_mapping.source_norm -> obligation_id. Interim-Bruecke = citation_units. obligation_id NIE neu vergeben (re-link).",
|
||||||
|
"count": 93,
|
||||||
|
"obligation_ids": [
|
||||||
|
{
|
||||||
|
"obligation_id": "sbom_creation",
|
||||||
|
"regulation": "CRA",
|
||||||
|
"family": "sbom",
|
||||||
|
"tier": "LEGAL_MINIMUM",
|
||||||
|
"citation_units": [
|
||||||
|
"Annex I Part II (1)"
|
||||||
|
],
|
||||||
|
"source_role": "LEGAL_BASIS"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"obligation_id": "sbom_dependency_coverage",
|
||||||
|
"regulation": "CRA",
|
||||||
|
"family": "sbom",
|
||||||
|
"tier": "LEGAL_MINIMUM",
|
||||||
|
"citation_units": [
|
||||||
|
"Art. 3(36) i.V.m. Annex I Part II (1)"
|
||||||
|
],
|
||||||
|
"source_role": "LEGAL_BASIS"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"obligation_id": "sbom_format_standard",
|
||||||
|
"regulation": "CRA",
|
||||||
|
"family": "sbom",
|
||||||
|
"tier": "LEGAL_MINIMUM",
|
||||||
|
"citation_units": [
|
||||||
|
"Annex I Part II (1)"
|
||||||
|
],
|
||||||
|
"source_role": "LEGAL_BASIS"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"obligation_id": "sbom_maintenance_update",
|
||||||
|
"regulation": "CRA",
|
||||||
|
"family": "sbom",
|
||||||
|
"tier": "LEGAL_MINIMUM",
|
||||||
|
"citation_units": [
|
||||||
|
"Annex I Part II (1)"
|
||||||
|
],
|
||||||
|
"source_role": "LEGAL_BASIS"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"obligation_id": "sbom_completeness_verification",
|
||||||
|
"regulation": "CRA",
|
||||||
|
"family": "sbom",
|
||||||
|
"tier": "BEST_PRACTICE",
|
||||||
|
"citation_units": [],
|
||||||
|
"source_role": "GUIDANCE"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"obligation_id": "sbom_tooling_automation",
|
||||||
|
"regulation": "CRA",
|
||||||
|
"family": "sbom",
|
||||||
|
"tier": "BEST_PRACTICE",
|
||||||
|
"citation_units": [],
|
||||||
|
"source_role": "IMPLEMENTATION"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"obligation_id": "sbom_access_provision",
|
||||||
|
"regulation": "CRA",
|
||||||
|
"family": "sbom",
|
||||||
|
"tier": "BEST_PRACTICE",
|
||||||
|
"citation_units": [],
|
||||||
|
"source_role": "GUIDANCE"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"obligation_id": "sbom_authority_provision",
|
||||||
|
"regulation": "CRA",
|
||||||
|
"family": "sbom",
|
||||||
|
"tier": "LEGAL_MINIMUM",
|
||||||
|
"citation_units": [
|
||||||
|
"Art. 31 / Annex I Part II (1)"
|
||||||
|
],
|
||||||
|
"source_role": "LEGAL_BASIS"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"obligation_id": "sbom_confidentiality",
|
||||||
|
"regulation": "CRA",
|
||||||
|
"family": "sbom",
|
||||||
|
"tier": "LEGAL_MINIMUM",
|
||||||
|
"citation_units": [
|
||||||
|
"Art. 31(4)"
|
||||||
|
],
|
||||||
|
"source_role": "LEGAL_BASIS"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"obligation_id": "sbom_supply_chain_contracts",
|
||||||
|
"regulation": "CRA",
|
||||||
|
"family": "sbom",
|
||||||
|
"tier": "BEST_PRACTICE",
|
||||||
|
"citation_units": [],
|
||||||
|
"source_role": "GUIDANCE"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"obligation_id": "sbom_technical_documentation",
|
||||||
|
"regulation": "CRA",
|
||||||
|
"family": "sbom",
|
||||||
|
"tier": "LEGAL_MINIMUM",
|
||||||
|
"citation_units": [
|
||||||
|
"Art. 31 i.V.m. Annex VII"
|
||||||
|
],
|
||||||
|
"source_role": "EVIDENCE"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"obligation_id": "vuln_identification_inventory",
|
||||||
|
"regulation": "CRA",
|
||||||
|
"family": "vuln",
|
||||||
|
"tier": "LEGAL_MINIMUM",
|
||||||
|
"citation_units": [
|
||||||
|
"Annex I Part II (1)"
|
||||||
|
],
|
||||||
|
"source_role": "LEGAL_BASIS"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"obligation_id": "vuln_assessment_prioritization",
|
||||||
|
"regulation": "CRA",
|
||||||
|
"family": "vuln",
|
||||||
|
"tier": "LEGAL_MINIMUM",
|
||||||
|
"citation_units": [
|
||||||
|
"Annex I Part II (1)"
|
||||||
|
],
|
||||||
|
"source_role": "LEGAL_BASIS"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"obligation_id": "vuln_remediation_patching",
|
||||||
|
"regulation": "CRA",
|
||||||
|
"family": "vuln",
|
||||||
|
"tier": "LEGAL_MINIMUM",
|
||||||
|
"citation_units": [
|
||||||
|
"Annex I Part II (2) & (8)"
|
||||||
|
],
|
||||||
|
"source_role": "LEGAL_BASIS"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"obligation_id": "vuln_handling_process",
|
||||||
|
"regulation": "CRA",
|
||||||
|
"family": "vuln",
|
||||||
|
"tier": "LEGAL_MINIMUM",
|
||||||
|
"citation_units": [
|
||||||
|
"Article 13(8) & Annex VII"
|
||||||
|
],
|
||||||
|
"source_role": "LEGAL_BASIS"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"obligation_id": "coordinated_vulnerability_disclosure",
|
||||||
|
"regulation": "CRA",
|
||||||
|
"family": "vuln",
|
||||||
|
"tier": "LEGAL_MINIMUM",
|
||||||
|
"citation_units": [
|
||||||
|
"Annex I Part II (5)"
|
||||||
|
],
|
||||||
|
"source_role": "LEGAL_BASIS"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"obligation_id": "exploited_vuln_reporting_authorities",
|
||||||
|
"regulation": "CRA",
|
||||||
|
"family": "vuln",
|
||||||
|
"tier": "LEGAL_MINIMUM",
|
||||||
|
"citation_units": [
|
||||||
|
"Article 14 & Article 16"
|
||||||
|
],
|
||||||
|
"source_role": "LEGAL_BASIS"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"obligation_id": "vuln_info_dissemination_users",
|
||||||
|
"regulation": "CRA",
|
||||||
|
"family": "vuln",
|
||||||
|
"tier": "LEGAL_MINIMUM",
|
||||||
|
"citation_units": [
|
||||||
|
"Annex I Part II (4) & (6)"
|
||||||
|
],
|
||||||
|
"source_role": "LEGAL_BASIS"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"obligation_id": "user_authentication_required",
|
||||||
|
"regulation": "CRA",
|
||||||
|
"family": "authentication",
|
||||||
|
"tier": "LEGAL_MINIMUM",
|
||||||
|
"citation_units": [
|
||||||
|
"Annex I (2)(d)"
|
||||||
|
],
|
||||||
|
"source_role": "LEGAL_BASIS"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"obligation_id": "authentication_policy_documented",
|
||||||
|
"regulation": "CRA",
|
||||||
|
"family": "authentication",
|
||||||
|
"tier": "BEST_PRACTICE",
|
||||||
|
"citation_units": [],
|
||||||
|
"source_role": "GUIDANCE"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"obligation_id": "auth_exceptions_documented",
|
||||||
|
"regulation": "CRA",
|
||||||
|
"family": "authentication",
|
||||||
|
"tier": "BEST_PRACTICE",
|
||||||
|
"citation_units": [],
|
||||||
|
"source_role": "GUIDANCE"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"obligation_id": "mfa_required",
|
||||||
|
"regulation": "CRA",
|
||||||
|
"family": "authentication",
|
||||||
|
"tier": "BEST_PRACTICE",
|
||||||
|
"citation_units": [],
|
||||||
|
"source_role": "GUIDANCE"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"obligation_id": "step_up_authentication",
|
||||||
|
"regulation": "CRA",
|
||||||
|
"family": "authentication",
|
||||||
|
"tier": "BEST_PRACTICE",
|
||||||
|
"citation_units": [],
|
||||||
|
"source_role": "GUIDANCE"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"obligation_id": "privileged_op_reauth",
|
||||||
|
"regulation": "CRA",
|
||||||
|
"family": "authentication",
|
||||||
|
"tier": "BEST_PRACTICE",
|
||||||
|
"citation_units": [],
|
||||||
|
"source_role": "GUIDANCE"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"obligation_id": "strong_crypto_authentication",
|
||||||
|
"regulation": "CRA",
|
||||||
|
"family": "authentication",
|
||||||
|
"tier": "LEGAL_MINIMUM",
|
||||||
|
"citation_units": [
|
||||||
|
"Annex I (2)(e)"
|
||||||
|
],
|
||||||
|
"source_role": "LEGAL_BASIS"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"obligation_id": "credential_lifecycle_management",
|
||||||
|
"regulation": "CRA",
|
||||||
|
"family": "authentication",
|
||||||
|
"tier": "BEST_PRACTICE",
|
||||||
|
"citation_units": [],
|
||||||
|
"source_role": "GUIDANCE"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"obligation_id": "credential_confidentiality_protection",
|
||||||
|
"regulation": "CRA",
|
||||||
|
"family": "authentication",
|
||||||
|
"tier": "LEGAL_MINIMUM",
|
||||||
|
"citation_units": [
|
||||||
|
"Annex I (2)(e)"
|
||||||
|
],
|
||||||
|
"source_role": "LEGAL_BASIS"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"obligation_id": "password_policy",
|
||||||
|
"regulation": "CRA",
|
||||||
|
"family": "authentication",
|
||||||
|
"tier": "BEST_PRACTICE",
|
||||||
|
"citation_units": [],
|
||||||
|
"source_role": "GUIDANCE"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"obligation_id": "no_default_credentials",
|
||||||
|
"regulation": "CRA",
|
||||||
|
"family": "authentication",
|
||||||
|
"tier": "LEGAL_MINIMUM",
|
||||||
|
"citation_units": [
|
||||||
|
"Annex I (2)(a)"
|
||||||
|
],
|
||||||
|
"source_role": "LEGAL_BASIS"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"obligation_id": "account_lockout_failed_attempts",
|
||||||
|
"regulation": "CRA",
|
||||||
|
"family": "authentication",
|
||||||
|
"tier": "BEST_PRACTICE",
|
||||||
|
"citation_units": [],
|
||||||
|
"source_role": "GUIDANCE"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"obligation_id": "server_side_validation",
|
||||||
|
"regulation": "CRA",
|
||||||
|
"family": "authentication",
|
||||||
|
"tier": "BEST_PRACTICE",
|
||||||
|
"citation_units": [],
|
||||||
|
"source_role": "GUIDANCE"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"obligation_id": "session_binding_management",
|
||||||
|
"regulation": "CRA",
|
||||||
|
"family": "authentication",
|
||||||
|
"tier": "BEST_PRACTICE",
|
||||||
|
"citation_units": [],
|
||||||
|
"source_role": "GUIDANCE"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"obligation_id": "reauth_after_inactivity",
|
||||||
|
"regulation": "CRA",
|
||||||
|
"family": "authentication",
|
||||||
|
"tier": "BEST_PRACTICE",
|
||||||
|
"citation_units": [],
|
||||||
|
"source_role": "GUIDANCE"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"obligation_id": "token_validation_lifecycle",
|
||||||
|
"regulation": "CRA",
|
||||||
|
"family": "authentication",
|
||||||
|
"tier": "BEST_PRACTICE",
|
||||||
|
"citation_units": [],
|
||||||
|
"source_role": "GUIDANCE"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"obligation_id": "mutual_authentication",
|
||||||
|
"regulation": "CRA",
|
||||||
|
"family": "authentication",
|
||||||
|
"tier": "BEST_PRACTICE",
|
||||||
|
"citation_units": [],
|
||||||
|
"source_role": "GUIDANCE"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"obligation_id": "revocation_check",
|
||||||
|
"regulation": "CRA",
|
||||||
|
"family": "authentication",
|
||||||
|
"tier": "BEST_PRACTICE",
|
||||||
|
"citation_units": [],
|
||||||
|
"source_role": "GUIDANCE"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"obligation_id": "encrypted_auth_channel",
|
||||||
|
"regulation": "CRA",
|
||||||
|
"family": "authentication",
|
||||||
|
"tier": "LEGAL_MINIMUM",
|
||||||
|
"citation_units": [
|
||||||
|
"Annex I (2)(e)"
|
||||||
|
],
|
||||||
|
"source_role": "LEGAL_BASIS"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"obligation_id": "tls_certificate_auth",
|
||||||
|
"regulation": "CRA",
|
||||||
|
"family": "authentication",
|
||||||
|
"tier": "BEST_PRACTICE",
|
||||||
|
"citation_units": [],
|
||||||
|
"source_role": "GUIDANCE"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"obligation_id": "service_to_service_auth",
|
||||||
|
"regulation": "CRA",
|
||||||
|
"family": "authentication",
|
||||||
|
"tier": "BEST_PRACTICE",
|
||||||
|
"citation_units": [],
|
||||||
|
"source_role": "GUIDANCE"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"obligation_id": "auth_key_management",
|
||||||
|
"regulation": "CRA",
|
||||||
|
"family": "authentication",
|
||||||
|
"tier": "BEST_PRACTICE",
|
||||||
|
"citation_units": [],
|
||||||
|
"source_role": "GUIDANCE"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"obligation_id": "biometric_authentication",
|
||||||
|
"regulation": "CRA",
|
||||||
|
"family": "authentication",
|
||||||
|
"tier": "BEST_PRACTICE",
|
||||||
|
"citation_units": [],
|
||||||
|
"source_role": "GUIDANCE"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"obligation_id": "federated_auth_assertions",
|
||||||
|
"regulation": "CRA",
|
||||||
|
"family": "authentication",
|
||||||
|
"tier": "BEST_PRACTICE",
|
||||||
|
"citation_units": [],
|
||||||
|
"source_role": "GUIDANCE"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"obligation_id": "separate_authn_authz",
|
||||||
|
"regulation": "CRA",
|
||||||
|
"family": "authentication",
|
||||||
|
"tier": "BEST_PRACTICE",
|
||||||
|
"citation_units": [],
|
||||||
|
"source_role": "GUIDANCE"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"obligation_id": "remote_access_authentication",
|
||||||
|
"regulation": "CRA",
|
||||||
|
"family": "authentication",
|
||||||
|
"tier": "BEST_PRACTICE",
|
||||||
|
"citation_units": [],
|
||||||
|
"source_role": "GUIDANCE"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"obligation_id": "supplier_access_auth",
|
||||||
|
"regulation": "CRA",
|
||||||
|
"family": "authentication",
|
||||||
|
"tier": "BEST_PRACTICE",
|
||||||
|
"citation_units": [],
|
||||||
|
"source_role": "GUIDANCE"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"obligation_id": "personal_admin_accounts",
|
||||||
|
"regulation": "CRA",
|
||||||
|
"family": "authentication",
|
||||||
|
"tier": "BEST_PRACTICE",
|
||||||
|
"citation_units": [],
|
||||||
|
"source_role": "GUIDANCE"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"obligation_id": "firmware_software_authentication",
|
||||||
|
"regulation": "CRA",
|
||||||
|
"family": "authentication",
|
||||||
|
"tier": "LEGAL_MINIMUM",
|
||||||
|
"citation_units": [
|
||||||
|
"Annex I (2)(c)"
|
||||||
|
],
|
||||||
|
"source_role": "LEGAL_BASIS"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"obligation_id": "event_logging_security_events",
|
||||||
|
"regulation": "CRA",
|
||||||
|
"family": "logging",
|
||||||
|
"tier": "LEGAL_MINIMUM",
|
||||||
|
"citation_units": [
|
||||||
|
"Annex I Part I (2)(k)"
|
||||||
|
],
|
||||||
|
"source_role": "LEGAL_BASIS"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"obligation_id": "access_control_event_logging",
|
||||||
|
"regulation": "CRA",
|
||||||
|
"family": "logging",
|
||||||
|
"tier": "LEGAL_MINIMUM",
|
||||||
|
"citation_units": [
|
||||||
|
"Annex I Part I (2)(k)"
|
||||||
|
],
|
||||||
|
"source_role": "LEGAL_BASIS"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"obligation_id": "audit_trail_admin_actions",
|
||||||
|
"regulation": "CRA",
|
||||||
|
"family": "logging",
|
||||||
|
"tier": "LEGAL_MINIMUM",
|
||||||
|
"citation_units": [
|
||||||
|
"Annex I Part I (2)(k)"
|
||||||
|
],
|
||||||
|
"source_role": "LEGAL_BASIS"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"obligation_id": "log_integrity_immutability",
|
||||||
|
"regulation": "CRA",
|
||||||
|
"family": "logging",
|
||||||
|
"tier": "LEGAL_MINIMUM",
|
||||||
|
"citation_units": [
|
||||||
|
"Annex I Part I (2)(k)"
|
||||||
|
],
|
||||||
|
"source_role": "LEGAL_BASIS"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"obligation_id": "log_access_control_protection",
|
||||||
|
"regulation": "CRA",
|
||||||
|
"family": "logging",
|
||||||
|
"tier": "LEGAL_MINIMUM",
|
||||||
|
"citation_units": [
|
||||||
|
"Annex I Part I (2)(k)"
|
||||||
|
],
|
||||||
|
"source_role": "LEGAL_BASIS"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"obligation_id": "log_retention_archival",
|
||||||
|
"regulation": "CRA",
|
||||||
|
"family": "logging",
|
||||||
|
"tier": "BEST_PRACTICE",
|
||||||
|
"citation_units": [],
|
||||||
|
"source_role": "GUIDANCE"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"obligation_id": "centralized_log_management",
|
||||||
|
"regulation": "CRA",
|
||||||
|
"family": "logging",
|
||||||
|
"tier": "BEST_PRACTICE",
|
||||||
|
"citation_units": [],
|
||||||
|
"source_role": "GUIDANCE"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"obligation_id": "log_monitoring_alerting",
|
||||||
|
"regulation": "CRA",
|
||||||
|
"family": "logging",
|
||||||
|
"tier": "LEGAL_MINIMUM",
|
||||||
|
"citation_units": [
|
||||||
|
"Annex I Part I (2)(k)"
|
||||||
|
],
|
||||||
|
"source_role": "LEGAL_BASIS"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"obligation_id": "log_data_minimization_privacy",
|
||||||
|
"regulation": "CRA",
|
||||||
|
"family": "logging",
|
||||||
|
"tier": "BEST_PRACTICE",
|
||||||
|
"citation_units": [],
|
||||||
|
"source_role": "GUIDANCE"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"obligation_id": "log_format_standardization",
|
||||||
|
"regulation": "CRA",
|
||||||
|
"family": "logging",
|
||||||
|
"tier": "BEST_PRACTICE",
|
||||||
|
"citation_units": [],
|
||||||
|
"source_role": "GUIDANCE"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"obligation_id": "log_timestamp_synchronization",
|
||||||
|
"regulation": "CRA",
|
||||||
|
"family": "logging",
|
||||||
|
"tier": "BEST_PRACTICE",
|
||||||
|
"citation_units": [],
|
||||||
|
"source_role": "GUIDANCE"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"obligation_id": "logging_availability_resilience",
|
||||||
|
"regulation": "CRA",
|
||||||
|
"family": "logging",
|
||||||
|
"tier": "BEST_PRACTICE",
|
||||||
|
"citation_units": [],
|
||||||
|
"source_role": "GUIDANCE"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"obligation_id": "logging_thread_safety_correctness",
|
||||||
|
"regulation": "CRA",
|
||||||
|
"family": "logging",
|
||||||
|
"tier": "BEST_PRACTICE",
|
||||||
|
"citation_units": [],
|
||||||
|
"source_role": "IMPLEMENTATION"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"obligation_id": "logging_library_supply_chain",
|
||||||
|
"regulation": "CRA",
|
||||||
|
"family": "logging",
|
||||||
|
"tier": "BEST_PRACTICE",
|
||||||
|
"citation_units": [],
|
||||||
|
"source_role": "GUIDANCE"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"obligation_id": "logging_config_management",
|
||||||
|
"regulation": "CRA",
|
||||||
|
"family": "logging",
|
||||||
|
"tier": "BEST_PRACTICE",
|
||||||
|
"citation_units": [],
|
||||||
|
"source_role": "GUIDANCE"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"obligation_id": "logging_governance_roles",
|
||||||
|
"regulation": "CRA",
|
||||||
|
"family": "logging",
|
||||||
|
"tier": "BEST_PRACTICE",
|
||||||
|
"citation_units": [],
|
||||||
|
"source_role": "GUIDANCE"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"obligation_id": "incident_response_logging",
|
||||||
|
"regulation": "CRA",
|
||||||
|
"family": "logging",
|
||||||
|
"tier": "BEST_PRACTICE",
|
||||||
|
"citation_units": [],
|
||||||
|
"source_role": "GUIDANCE"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"obligation_id": "log_transmission_security",
|
||||||
|
"regulation": "CRA",
|
||||||
|
"family": "logging",
|
||||||
|
"tier": "BEST_PRACTICE",
|
||||||
|
"citation_units": [],
|
||||||
|
"source_role": "GUIDANCE"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"obligation_id": "network_traffic_logging",
|
||||||
|
"regulation": "CRA",
|
||||||
|
"family": "logging",
|
||||||
|
"tier": "BEST_PRACTICE",
|
||||||
|
"citation_units": [],
|
||||||
|
"source_role": "GUIDANCE"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"obligation_id": "remote_access_control_least_privilege",
|
||||||
|
"regulation": "CRA",
|
||||||
|
"family": "remote_access",
|
||||||
|
"tier": "LEGAL_MINIMUM",
|
||||||
|
"citation_units": [
|
||||||
|
"Annex I (1)(2)(d)"
|
||||||
|
],
|
||||||
|
"source_role": "LEGAL_BASIS"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"obligation_id": "remote_access_confidentiality_integrity",
|
||||||
|
"regulation": "CRA",
|
||||||
|
"family": "remote_access",
|
||||||
|
"tier": "LEGAL_MINIMUM",
|
||||||
|
"citation_units": [
|
||||||
|
"Annex I (1)(2)(b)(c)"
|
||||||
|
],
|
||||||
|
"source_role": "LEGAL_BASIS"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"obligation_id": "remote_session_management",
|
||||||
|
"regulation": "CRA",
|
||||||
|
"family": "remote_access",
|
||||||
|
"tier": "BEST_PRACTICE",
|
||||||
|
"citation_units": [],
|
||||||
|
"source_role": "GUIDANCE"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"obligation_id": "remote_access_mfa",
|
||||||
|
"regulation": "CRA",
|
||||||
|
"family": "remote_access",
|
||||||
|
"tier": "BEST_PRACTICE",
|
||||||
|
"citation_units": [],
|
||||||
|
"source_role": "GUIDANCE"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"obligation_id": "remote_access_encryption",
|
||||||
|
"regulation": "CRA",
|
||||||
|
"family": "remote_access",
|
||||||
|
"tier": "BEST_PRACTICE",
|
||||||
|
"citation_units": [],
|
||||||
|
"source_role": "GUIDANCE"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"obligation_id": "reject_insecure_remote_protocols",
|
||||||
|
"regulation": "CRA",
|
||||||
|
"family": "remote_access",
|
||||||
|
"tier": "BEST_PRACTICE",
|
||||||
|
"citation_units": [],
|
||||||
|
"source_role": "GUIDANCE"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"obligation_id": "remote_access_logging_audit",
|
||||||
|
"regulation": "CRA",
|
||||||
|
"family": "remote_access",
|
||||||
|
"tier": "LEGAL_MINIMUM",
|
||||||
|
"citation_units": [
|
||||||
|
"Annex I (1)(2)(g)"
|
||||||
|
],
|
||||||
|
"source_role": "LEGAL_BASIS"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"obligation_id": "remote_access_user_validation_ot",
|
||||||
|
"regulation": "CRA",
|
||||||
|
"family": "remote_access",
|
||||||
|
"tier": "BEST_PRACTICE",
|
||||||
|
"citation_units": [],
|
||||||
|
"source_role": "GUIDANCE"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"obligation_id": "remote_access_training",
|
||||||
|
"regulation": "CRA",
|
||||||
|
"family": "remote_access",
|
||||||
|
"tier": "BEST_PRACTICE",
|
||||||
|
"citation_units": [],
|
||||||
|
"source_role": "GUIDANCE"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"obligation_id": "remote_access_architecture_design",
|
||||||
|
"regulation": "CRA",
|
||||||
|
"family": "remote_access",
|
||||||
|
"tier": "BEST_PRACTICE",
|
||||||
|
"citation_units": [],
|
||||||
|
"source_role": "GUIDANCE"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"obligation_id": "remote_access_attack_surface_min",
|
||||||
|
"regulation": "CRA",
|
||||||
|
"family": "remote_access",
|
||||||
|
"tier": "LEGAL_MINIMUM",
|
||||||
|
"citation_units": [
|
||||||
|
"Annex I (1)(2)(a)"
|
||||||
|
],
|
||||||
|
"source_role": "LEGAL_BASIS"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"obligation_id": "remote_access_vuln_patch_mgmt",
|
||||||
|
"regulation": "CRA",
|
||||||
|
"family": "remote_access",
|
||||||
|
"tier": "LEGAL_MINIMUM",
|
||||||
|
"citation_units": [
|
||||||
|
"Annex I (2)(1)"
|
||||||
|
],
|
||||||
|
"source_role": "LEGAL_BASIS"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"obligation_id": "remote_access_threat_detection",
|
||||||
|
"regulation": "CRA",
|
||||||
|
"family": "remote_access",
|
||||||
|
"tier": "BEST_PRACTICE",
|
||||||
|
"citation_units": [],
|
||||||
|
"source_role": "GUIDANCE"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"obligation_id": "remote_maintenance_governance",
|
||||||
|
"regulation": "CRA",
|
||||||
|
"family": "remote_access",
|
||||||
|
"tier": "BEST_PRACTICE",
|
||||||
|
"citation_units": [],
|
||||||
|
"source_role": "GUIDANCE"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"obligation_id": "temporary_remote_access_mgmt",
|
||||||
|
"regulation": "CRA",
|
||||||
|
"family": "remote_access",
|
||||||
|
"tier": "BEST_PRACTICE",
|
||||||
|
"citation_units": [],
|
||||||
|
"source_role": "GUIDANCE"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"obligation_id": "remote_access_data_export_protection",
|
||||||
|
"regulation": "CRA",
|
||||||
|
"family": "remote_access",
|
||||||
|
"tier": "BEST_PRACTICE",
|
||||||
|
"citation_units": [],
|
||||||
|
"source_role": "GUIDANCE"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"obligation_id": "component_remote_interface_security",
|
||||||
|
"regulation": "CRA",
|
||||||
|
"family": "remote_access",
|
||||||
|
"tier": "BEST_PRACTICE",
|
||||||
|
"citation_units": [],
|
||||||
|
"source_role": "GUIDANCE"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"obligation_id": "remote_access_fallback_concept",
|
||||||
|
"regulation": "CRA",
|
||||||
|
"family": "remote_access",
|
||||||
|
"tier": "BEST_PRACTICE",
|
||||||
|
"citation_units": [],
|
||||||
|
"source_role": "GUIDANCE"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"obligation_id": "provide_security_updates",
|
||||||
|
"regulation": "CRA",
|
||||||
|
"family": "updates",
|
||||||
|
"tier": "LEGAL_MINIMUM",
|
||||||
|
"citation_units": [
|
||||||
|
"Annex I (2)(c)",
|
||||||
|
"Art. 13"
|
||||||
|
],
|
||||||
|
"source_role": "LEGAL_BASIS"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"obligation_id": "support_period_maintenance",
|
||||||
|
"regulation": "CRA",
|
||||||
|
"family": "updates",
|
||||||
|
"tier": "LEGAL_MINIMUM",
|
||||||
|
"citation_units": [
|
||||||
|
"Art. 13(8)"
|
||||||
|
],
|
||||||
|
"source_role": "LEGAL_BASIS"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"obligation_id": "signed_update_integrity",
|
||||||
|
"regulation": "CRA",
|
||||||
|
"family": "updates",
|
||||||
|
"tier": "LEGAL_MINIMUM",
|
||||||
|
"citation_units": [
|
||||||
|
"Annex I (1)(3)(f)"
|
||||||
|
],
|
||||||
|
"source_role": "LEGAL_BASIS"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"obligation_id": "trusted_update_source",
|
||||||
|
"regulation": "CRA",
|
||||||
|
"family": "updates",
|
||||||
|
"tier": "LEGAL_MINIMUM",
|
||||||
|
"citation_units": [
|
||||||
|
"Annex I (1)(3)(d)"
|
||||||
|
],
|
||||||
|
"source_role": "LEGAL_BASIS"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"obligation_id": "update_testing_validation",
|
||||||
|
"regulation": "CRA",
|
||||||
|
"family": "updates",
|
||||||
|
"tier": "BEST_PRACTICE",
|
||||||
|
"citation_units": [],
|
||||||
|
"source_role": "GUIDANCE"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"obligation_id": "update_rollback",
|
||||||
|
"regulation": "CRA",
|
||||||
|
"family": "updates",
|
||||||
|
"tier": "BEST_PRACTICE",
|
||||||
|
"citation_units": [],
|
||||||
|
"source_role": "GUIDANCE"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"obligation_id": "automatic_updates_optout",
|
||||||
|
"regulation": "CRA",
|
||||||
|
"family": "updates",
|
||||||
|
"tier": "LEGAL_MINIMUM",
|
||||||
|
"citation_units": [
|
||||||
|
"Annex I (2)(c)"
|
||||||
|
],
|
||||||
|
"source_role": "LEGAL_BASIS"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"obligation_id": "update_risk_assessment",
|
||||||
|
"regulation": "CRA",
|
||||||
|
"family": "updates",
|
||||||
|
"tier": "LEGAL_MINIMUM",
|
||||||
|
"citation_units": [
|
||||||
|
"Annex I (1)(2)"
|
||||||
|
],
|
||||||
|
"source_role": "LEGAL_BASIS"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"obligation_id": "secure_modification_control",
|
||||||
|
"regulation": "CRA",
|
||||||
|
"family": "updates",
|
||||||
|
"tier": "BEST_PRACTICE",
|
||||||
|
"citation_units": [],
|
||||||
|
"source_role": "IMPLEMENTATION"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,126 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
|
||||||
|
"github.com/breakpilot/ai-compliance-sdk/internal/ucca"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ComplianceGraphHandlers serves the read-only Compliance Execution Graph
|
||||||
|
// (Regulation -> Obligation -> Control -> Evidence) over the file-backed bridge artifacts.
|
||||||
|
// It is intentionally SEPARATE from the DB-backed ObligationsHandlers: this is the curated
|
||||||
|
// cross-session graph (Registry join keys + accepted control mappings + evidence requirements),
|
||||||
|
// loaded once at startup. Fail-closed: if the graph could not load, every request answers 503.
|
||||||
|
type ComplianceGraphHandlers struct {
|
||||||
|
joins *ucca.ObligationJoinKeys
|
||||||
|
mappings *ucca.ControlMappingSet
|
||||||
|
evidence *ucca.EvidenceRequirementSet
|
||||||
|
loadErr error
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewComplianceGraphHandlers loads the graph once. Construction never fails; a load error is
|
||||||
|
// retained and surfaced as 503 per request (matches the codebase's load-warn-continue startup).
|
||||||
|
func NewComplianceGraphHandlers() *ComplianceGraphHandlers {
|
||||||
|
joins, mappings, evidence, err := ucca.LoadComplianceGraph()
|
||||||
|
return &ComplianceGraphHandlers{joins: joins, mappings: mappings, evidence: evidence, loadErr: err}
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadError exposes a startup load failure so the wiring can log a warning.
|
||||||
|
func (h *ComplianceGraphHandlers) LoadError() error { return h.loadErr }
|
||||||
|
|
||||||
|
// RegisterRoutes mounts the compliance-graph routes under /compliance.
|
||||||
|
func (h *ComplianceGraphHandlers) RegisterRoutes(r *gin.RouterGroup) {
|
||||||
|
g := r.Group("/compliance")
|
||||||
|
g.GET("/obligation-status", h.ObligationStatus)
|
||||||
|
}
|
||||||
|
|
||||||
|
type cgControlDTO struct {
|
||||||
|
Framework string `json:"framework"`
|
||||||
|
Control string `json:"control"`
|
||||||
|
MappingType string `json:"mapping_type"`
|
||||||
|
EvidenceRequired []string `json:"evidence_required"`
|
||||||
|
EvidenceStatus string `json:"evidence_status"` // missing | partial | present | none_required
|
||||||
|
}
|
||||||
|
|
||||||
|
type cgStatusResponse struct {
|
||||||
|
ObligationID string `json:"obligation_id"`
|
||||||
|
OverallStatus string `json:"overall_status"` // unknown_obligation | unmapped | not_assessed | open | met
|
||||||
|
LegalBasis []string `json:"legal_basis,omitempty"`
|
||||||
|
CitationSpans string `json:"citation_spans"` // "pending" until the Legal-KG attaches spans
|
||||||
|
Controls []cgControlDTO `json:"controls"`
|
||||||
|
Note string `json:"note,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ObligationStatus answers GET /sdk/v1/compliance/obligation-status?obligation_id=...
|
||||||
|
//
|
||||||
|
// It NEVER asserts fulfillment automatically. With no evidence collection wired (MVP), a mapped
|
||||||
|
// obligation is "not_assessed" and every required evidence is "missing" — the honest picture is
|
||||||
|
// "required vs present evidence", not "a document exists". Fail-closed otherwise:
|
||||||
|
// - no obligation_id -> 400
|
||||||
|
// - graph not loaded -> 503
|
||||||
|
// - id not in the Registry -> 200 overall_status=unknown_obligation
|
||||||
|
// - mapped but no control yet -> 200 overall_status=unmapped
|
||||||
|
func (h *ComplianceGraphHandlers) ObligationStatus(c *gin.Context) {
|
||||||
|
if h.loadErr != nil {
|
||||||
|
c.JSON(http.StatusServiceUnavailable, gin.H{"error": "compliance graph unavailable", "detail": h.loadErr.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
obID := strings.TrimSpace(c.Query("obligation_id"))
|
||||||
|
if obID == "" {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "obligation_id query parameter required"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resp := cgStatusResponse{ObligationID: obID, CitationSpans: "pending", Controls: []cgControlDTO{}}
|
||||||
|
|
||||||
|
if h.joins.FindObligation(obID) == nil {
|
||||||
|
resp.OverallStatus = "unknown_obligation"
|
||||||
|
resp.Note = "obligation_id not in the Registry join-key contract"
|
||||||
|
c.JSON(http.StatusOK, resp)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// MVP: hasEvidence=nil -> no collection wired -> all required evidence counts as missing.
|
||||||
|
st := ucca.AssessObligationStatus(h.joins, h.mappings, h.evidence, obID, nil)
|
||||||
|
resp.LegalBasis = st.LegalBasis
|
||||||
|
|
||||||
|
if len(st.Controls) == 0 {
|
||||||
|
resp.OverallStatus = "unmapped"
|
||||||
|
resp.Note = "no accepted control maps to this obligation yet"
|
||||||
|
c.JSON(http.StatusOK, resp)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, cs := range st.Controls {
|
||||||
|
types := make([]string, 0, len(cs.RequiredEvidence))
|
||||||
|
for _, e := range cs.RequiredEvidence {
|
||||||
|
types = append(types, e.EvidenceType)
|
||||||
|
}
|
||||||
|
resp.Controls = append(resp.Controls, cgControlDTO{
|
||||||
|
Framework: cs.Framework,
|
||||||
|
Control: cs.Control,
|
||||||
|
MappingType: cs.MappingType,
|
||||||
|
EvidenceRequired: types,
|
||||||
|
EvidenceStatus: cgEvidenceStatus(len(cs.RequiredEvidence), len(cs.MissingEvidence)),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
// No fulfillment claim without real evidence collection.
|
||||||
|
resp.OverallStatus = "not_assessed"
|
||||||
|
resp.Note = "evidence collection not wired (MVP) — fulfillment not asserted"
|
||||||
|
c.JSON(http.StatusOK, resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
func cgEvidenceStatus(required, missing int) string {
|
||||||
|
switch {
|
||||||
|
case required == 0:
|
||||||
|
return "none_required"
|
||||||
|
case missing == 0:
|
||||||
|
return "present"
|
||||||
|
case missing == required:
|
||||||
|
return "missing"
|
||||||
|
default:
|
||||||
|
return "partial"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
func newComplianceGraphTestRouter(t *testing.T) *gin.Engine {
|
||||||
|
t.Helper()
|
||||||
|
gin.SetMode(gin.TestMode)
|
||||||
|
h := NewComplianceGraphHandlers()
|
||||||
|
if err := h.LoadError(); err != nil {
|
||||||
|
t.Fatalf("compliance graph failed to load (candidate paths): %v", err)
|
||||||
|
}
|
||||||
|
r := gin.New()
|
||||||
|
h.RegisterRoutes(r.Group("/sdk/v1"))
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
func getObligationStatus(t *testing.T, r *gin.Engine, query string) (int, cgStatusResponse) {
|
||||||
|
t.Helper()
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
req, _ := http.NewRequest(http.MethodGet, "/sdk/v1/compliance/obligation-status"+query, nil)
|
||||||
|
r.ServeHTTP(w, req)
|
||||||
|
var resp cgStatusResponse
|
||||||
|
if w.Code == http.StatusOK {
|
||||||
|
if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
|
||||||
|
t.Fatalf("decode body %q: %v", w.Body.String(), err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return w.Code, resp
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestObligationStatus(t *testing.T) {
|
||||||
|
r := newComplianceGraphTestRouter(t)
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
query string
|
||||||
|
wantHTTP int
|
||||||
|
wantOverall string
|
||||||
|
wantControls bool // expect >=1 control
|
||||||
|
}{
|
||||||
|
{"missing param -> 400", "", http.StatusBadRequest, "", false},
|
||||||
|
{"unknown id -> unknown_obligation", "?obligation_id=does_not_exist", http.StatusOK, "unknown_obligation", false},
|
||||||
|
{"mapped (OWASP V6) -> not_assessed", "?obligation_id=user_authentication_required", http.StatusOK, "not_assessed", true},
|
||||||
|
{"NIST adopted (SI-2) -> not_assessed", "?obligation_id=provide_security_updates", http.StatusOK, "not_assessed", true},
|
||||||
|
{"in registry, no control -> unmapped", "?obligation_id=sbom_creation", http.StatusOK, "unmapped", false},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
code, resp := getObligationStatus(t, r, tt.query)
|
||||||
|
if code != tt.wantHTTP {
|
||||||
|
t.Fatalf("http %d, want %d", code, tt.wantHTTP)
|
||||||
|
}
|
||||||
|
if tt.wantHTTP != http.StatusOK {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if resp.OverallStatus != tt.wantOverall {
|
||||||
|
t.Errorf("overall_status=%q, want %q", resp.OverallStatus, tt.wantOverall)
|
||||||
|
}
|
||||||
|
if tt.wantControls && len(resp.Controls) == 0 {
|
||||||
|
t.Error("expected >=1 control")
|
||||||
|
}
|
||||||
|
if !tt.wantControls && len(resp.Controls) != 0 {
|
||||||
|
t.Errorf("expected 0 controls, got %d", len(resp.Controls))
|
||||||
|
}
|
||||||
|
if resp.CitationSpans != "pending" {
|
||||||
|
t.Errorf("citation_spans=%q, want pending", resp.CitationSpans)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The MVP must NEVER auto-assert fulfillment: with no evidence collection wired, every required
|
||||||
|
// evidence is "missing" and the overall status stays "not_assessed".
|
||||||
|
func TestObligationStatus_NoFulfillmentClaim(t *testing.T) {
|
||||||
|
r := newComplianceGraphTestRouter(t)
|
||||||
|
code, resp := getObligationStatus(t, r, "?obligation_id=user_authentication_required")
|
||||||
|
if code != http.StatusOK {
|
||||||
|
t.Fatalf("http %d", code)
|
||||||
|
}
|
||||||
|
if resp.OverallStatus == "met" || resp.OverallStatus == "erfuellt" {
|
||||||
|
t.Fatalf("MVP must not assert fulfillment, got overall_status=%q", resp.OverallStatus)
|
||||||
|
}
|
||||||
|
for _, ctl := range resp.Controls {
|
||||||
|
if len(ctl.EvidenceRequired) > 0 && ctl.EvidenceStatus != "missing" {
|
||||||
|
t.Errorf("control %s/%s evidence_status=%q, want missing (no collection wired)", ctl.Framework, ctl.Control, ctl.EvidenceStatus)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -153,6 +153,12 @@ func buildRouter(cfg *config.Config, pool *pgxpool.Pool) *gin.Engine {
|
|||||||
ragHandlers := handlers.NewRAGHandlers(corpusVersionStore)
|
ragHandlers := handlers.NewRAGHandlers(corpusVersionStore)
|
||||||
obligationsHandlers := handlers.NewObligationsHandlersWithStore(obligationsStore)
|
obligationsHandlers := handlers.NewObligationsHandlersWithStore(obligationsStore)
|
||||||
|
|
||||||
|
// Compliance Execution Graph (file-backed: Registry join keys + accepted control mappings + evidence)
|
||||||
|
complianceGraphHandlers := handlers.NewComplianceGraphHandlers()
|
||||||
|
if err := complianceGraphHandlers.LoadError(); err != nil {
|
||||||
|
log.Printf("WARNING: compliance graph not loaded (obligation-status -> 503): %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
// Regulatory News
|
// Regulatory News
|
||||||
allV2Regs, err := ucca.LoadAllV2Regulations()
|
allV2Regs, err := ucca.LoadAllV2Regulations()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -201,7 +207,8 @@ func buildRouter(cfg *config.Config, pool *pgxpool.Pool) *gin.Engine {
|
|||||||
uccaHandlers, escalationHandlers, obligationsHandlers, ragHandlers,
|
uccaHandlers, escalationHandlers, obligationsHandlers, ragHandlers,
|
||||||
roadmapHandlers, workshopHandlers, portfolioHandlers,
|
roadmapHandlers, workshopHandlers, portfolioHandlers,
|
||||||
academyHandlers, trainingHandlers, whistleblowerHandlers, iaceHandler,
|
academyHandlers, trainingHandlers, whistleblowerHandlers, iaceHandler,
|
||||||
gapHandler, maximizerHandlers, regulatoryNewsHandlers, useCaseHandler)
|
gapHandler, maximizerHandlers, regulatoryNewsHandlers, useCaseHandler,
|
||||||
|
complianceGraphHandlers)
|
||||||
|
|
||||||
return router
|
return router
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ func registerRoutes(
|
|||||||
maximizerHandlers *handlers.MaximizerHandlers,
|
maximizerHandlers *handlers.MaximizerHandlers,
|
||||||
regulatoryNewsHandlers *handlers.RegulatoryNewsHandlers,
|
regulatoryNewsHandlers *handlers.RegulatoryNewsHandlers,
|
||||||
useCaseHandler *handlers.UseCaseHandler,
|
useCaseHandler *handlers.UseCaseHandler,
|
||||||
|
complianceGraphHandlers *handlers.ComplianceGraphHandlers,
|
||||||
) {
|
) {
|
||||||
v1 := router.Group("/sdk/v1")
|
v1 := router.Group("/sdk/v1")
|
||||||
{
|
{
|
||||||
@@ -54,6 +55,7 @@ func registerRoutes(
|
|||||||
registerMaximizerRoutes(v1, maximizerHandlers)
|
registerMaximizerRoutes(v1, maximizerHandlers)
|
||||||
registerUseCaseRoutes(v1, useCaseHandler)
|
registerUseCaseRoutes(v1, useCaseHandler)
|
||||||
v1.GET("/regulatory-news", regulatoryNewsHandlers.GetNews)
|
v1.GET("/regulatory-news", regulatoryNewsHandlers.GetNews)
|
||||||
|
complianceGraphHandlers.RegisterRoutes(v1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,89 @@
|
|||||||
|
package ucca
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
|
)
|
||||||
|
|
||||||
|
// graphCallerRel resolves a path relative to THIS source file (build-time location), so the
|
||||||
|
// graph data is findable under `go test` (cwd = package dir) regardless of working directory.
|
||||||
|
// In a built container the source is gone, so cwd-relative candidates carry the load instead.
|
||||||
|
func graphCallerRel(rel string) string {
|
||||||
|
_, file, _, ok := runtime.Caller(0)
|
||||||
|
if !ok {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return filepath.Join(filepath.Dir(file), rel)
|
||||||
|
}
|
||||||
|
|
||||||
|
// firstExisting returns the first candidate path that exists with the requested kind (dir vs
|
||||||
|
// file). Empty candidates (e.g. unset env overrides) are skipped.
|
||||||
|
func firstExisting(candidates []string, wantDir bool) string {
|
||||||
|
for _, p := range candidates {
|
||||||
|
if p == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
info, err := os.Stat(p)
|
||||||
|
if err != nil || info.IsDir() != wantDir {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadComplianceGraph loads the file-backed Compliance Execution Graph: the Registry join-key
|
||||||
|
// contract (obligations/obligation_join_keys.json — owned by the Obligation session) + our
|
||||||
|
// curated, accepted control mappings + evidence requirements. Locations are resolved across
|
||||||
|
// three layouts: dev (cwd = ai-compliance-sdk/, canonical contract at ../obligations), container
|
||||||
|
// (WORKDIR /app, data/ copied in incl. a synced data/obligations/ copy) and `go test`
|
||||||
|
// (cwd = package dir, via graphCallerRel). Fail-closed: a missing/invalid source returns an
|
||||||
|
// error so the handler serves 503 — never a half-built graph.
|
||||||
|
//
|
||||||
|
// NOTE: data/obligations/obligation_join_keys.json is a SYNCED COPY of the repo-root contract
|
||||||
|
// (the canonical owner is the Obligation session). Re-sync it when the Registry grows; dev/test
|
||||||
|
// prefer the canonical repo-root path, only the container falls back to the copy.
|
||||||
|
func LoadComplianceGraph() (*ObligationJoinKeys, *ControlMappingSet, *EvidenceRequirementSet, error) {
|
||||||
|
joinPath := firstExisting([]string{
|
||||||
|
os.Getenv("BP_OBLIGATION_JOIN_KEYS"),
|
||||||
|
"../obligations/obligation_join_keys.json",
|
||||||
|
graphCallerRel("../../../obligations/obligation_join_keys.json"),
|
||||||
|
"data/obligations/obligation_join_keys.json",
|
||||||
|
graphCallerRel("../../data/obligations/obligation_join_keys.json"),
|
||||||
|
}, false)
|
||||||
|
if joinPath == "" {
|
||||||
|
return nil, nil, nil, fmt.Errorf("obligation_join_keys.json not found in any candidate path")
|
||||||
|
}
|
||||||
|
mapDir := firstExisting([]string{
|
||||||
|
os.Getenv("BP_CONTROL_MAPPINGS_DIR"),
|
||||||
|
"data/control_mappings",
|
||||||
|
graphCallerRel("../../data/control_mappings"),
|
||||||
|
}, true)
|
||||||
|
if mapDir == "" {
|
||||||
|
return nil, nil, nil, fmt.Errorf("control_mappings dir not found in any candidate path")
|
||||||
|
}
|
||||||
|
evDir := firstExisting([]string{
|
||||||
|
os.Getenv("BP_EVIDENCE_DIR"),
|
||||||
|
"data/evidence_requirements",
|
||||||
|
graphCallerRel("../../data/evidence_requirements"),
|
||||||
|
}, true)
|
||||||
|
if evDir == "" {
|
||||||
|
return nil, nil, nil, fmt.Errorf("evidence_requirements dir not found in any candidate path")
|
||||||
|
}
|
||||||
|
|
||||||
|
joins, err := LoadObligationJoinKeys(joinPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, nil, fmt.Errorf("load join keys (%s): %w", joinPath, err)
|
||||||
|
}
|
||||||
|
mappings, err := LoadControlMappings(mapDir)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, nil, fmt.Errorf("load control mappings (%s): %w", mapDir, err)
|
||||||
|
}
|
||||||
|
evidence, err := LoadEvidenceRequirements(evDir)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, nil, fmt.Errorf("load evidence (%s): %w", evDir, err)
|
||||||
|
}
|
||||||
|
return joins, mappings, evidence, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
package ucca
|
||||||
|
|
||||||
|
// ObligationStatus is the Advisor's vertical slice over the compliance graph for ONE legal
|
||||||
|
// obligation: which accepted controls satisfy it, what evidence they require, what's missing,
|
||||||
|
// and the resulting status. The point is "the required evidence is (not) present", not "a
|
||||||
|
// document exists". citation_spans is pending until the Legal-Knowledge-Graph session attaches
|
||||||
|
// them to the obligation (the upper half of the bridge).
|
||||||
|
type ObligationStatus struct {
|
||||||
|
ObligationID string `json:"obligation_id"`
|
||||||
|
LegalBasis []string `json:"legal_basis"` // the obligation's citation_units
|
||||||
|
Status string `json:"status"` // erfuellt | offen | unklar
|
||||||
|
Controls []ObligationControlStatus `json:"controls"`
|
||||||
|
CitationSpans string `json:"citation_spans"` // "pending" until the registry fills them
|
||||||
|
}
|
||||||
|
|
||||||
|
// ObligationControlStatus is one control under an obligation with its evidence picture.
|
||||||
|
type ObligationControlStatus struct {
|
||||||
|
Framework string `json:"framework"`
|
||||||
|
Control string `json:"control"`
|
||||||
|
MappingType string `json:"mapping_type"`
|
||||||
|
RequiredEvidence []EvidenceRequirement `json:"required_evidence"`
|
||||||
|
MissingEvidence []EvidenceRequirement `json:"missing_evidence"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// AssessObligationStatus traverses obligation_id -> (citation_unit) -> accepted Controls ->
|
||||||
|
// required Evidence -> Status. hasEvidence reports whether a given (framework, control,
|
||||||
|
// evidence_type) is already collected; pass nil in the MVP (no collection yet) -> everything
|
||||||
|
// required is missing and the status is "offen". Unknown or unmapped obligation -> "unklar".
|
||||||
|
func AssessObligationStatus(joins *ObligationJoinKeys, mappings *ControlMappingSet, evidence *EvidenceRequirementSet, obligationID string, hasEvidence func(framework, control, evidenceType string) bool) ObligationStatus {
|
||||||
|
ob := joins.FindObligation(obligationID)
|
||||||
|
if ob == nil {
|
||||||
|
return ObligationStatus{ObligationID: obligationID, Status: "unklar", CitationSpans: "pending"}
|
||||||
|
}
|
||||||
|
st := ObligationStatus{
|
||||||
|
ObligationID: obligationID,
|
||||||
|
LegalBasis: ob.CitationUnits,
|
||||||
|
CitationSpans: "pending",
|
||||||
|
Controls: []ObligationControlStatus{},
|
||||||
|
}
|
||||||
|
ctrls := AcceptedControlsForObligation(*ob, mappings)
|
||||||
|
if len(ctrls) == 0 {
|
||||||
|
st.Status = "unklar" // no accepted control reaches it — we cannot assess
|
||||||
|
return st
|
||||||
|
}
|
||||||
|
anyMissing := false
|
||||||
|
for _, m := range ctrls {
|
||||||
|
req := evidence.RequiredFor(m.TargetFramework, m.TargetControl)
|
||||||
|
missing := make([]EvidenceRequirement, 0, len(req))
|
||||||
|
for _, e := range req {
|
||||||
|
if hasEvidence == nil || !hasEvidence(e.Framework, e.Control, e.EvidenceType) {
|
||||||
|
missing = append(missing, e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(missing) > 0 {
|
||||||
|
anyMissing = true
|
||||||
|
}
|
||||||
|
st.Controls = append(st.Controls, ObligationControlStatus{
|
||||||
|
Framework: m.TargetFramework,
|
||||||
|
Control: m.TargetControl,
|
||||||
|
MappingType: m.MappingType,
|
||||||
|
RequiredEvidence: req,
|
||||||
|
MissingEvidence: missing,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if anyMissing {
|
||||||
|
st.Status = "offen"
|
||||||
|
} else {
|
||||||
|
st.Status = "erfuellt"
|
||||||
|
}
|
||||||
|
return st
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
package ucca
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func loadGraph(t *testing.T) (*ObligationJoinKeys, *ControlMappingSet, *EvidenceRequirementSet) {
|
||||||
|
t.Helper()
|
||||||
|
joins, err := LoadObligationJoinKeys("../../../obligations/obligation_join_keys.json")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("join keys: %v", err)
|
||||||
|
}
|
||||||
|
maps, err := LoadControlMappings("../../data/control_mappings")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("mappings: %v", err)
|
||||||
|
}
|
||||||
|
ev, err := LoadEvidenceRequirements("../../data/evidence_requirements")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("evidence: %v", err)
|
||||||
|
}
|
||||||
|
return joins, maps, ev
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAssessObligationStatus(t *testing.T) {
|
||||||
|
joins, maps, ev := loadGraph(t)
|
||||||
|
|
||||||
|
// covered obligation, no evidence collected yet (MVP) -> offen
|
||||||
|
st := AssessObligationStatus(joins, maps, ev, "user_authentication_required", nil)
|
||||||
|
if st.Status != "offen" {
|
||||||
|
t.Errorf("want offen, got %q", st.Status)
|
||||||
|
}
|
||||||
|
if len(st.Controls) == 0 {
|
||||||
|
t.Fatal("expected controls for a covered obligation")
|
||||||
|
}
|
||||||
|
for _, c := range st.Controls {
|
||||||
|
if len(c.MissingEvidence) != len(c.RequiredEvidence) {
|
||||||
|
t.Error("MVP: all required evidence should be missing")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
t.Logf("DURCHSTICH user_authentication_required: status=%s legal_basis=%v citation_spans=%s",
|
||||||
|
st.Status, st.LegalBasis, st.CitationSpans)
|
||||||
|
for _, c := range st.Controls {
|
||||||
|
t.Logf(" %s %s (%s): %d required evidence, %d missing", c.Framework, c.Control, c.MappingType, len(c.RequiredEvidence), len(c.MissingEvidence))
|
||||||
|
}
|
||||||
|
|
||||||
|
// all evidence present -> erfuellt
|
||||||
|
st2 := AssessObligationStatus(joins, maps, ev, "user_authentication_required", func(f, c, et string) bool { return true })
|
||||||
|
if st2.Status != "erfuellt" {
|
||||||
|
t.Errorf("want erfuellt with all evidence present, got %q", st2.Status)
|
||||||
|
}
|
||||||
|
|
||||||
|
// uncovered obligation (no accepted control reaches it) -> unklar
|
||||||
|
if st3 := AssessObligationStatus(joins, maps, ev, "sbom_creation", nil); st3.Status != "unklar" {
|
||||||
|
t.Errorf("uncovered sbom_creation: want unklar, got %q", st3.Status)
|
||||||
|
}
|
||||||
|
|
||||||
|
// unknown obligation_id -> unklar
|
||||||
|
if st4 := AssessObligationStatus(joins, maps, ev, "does_not_exist", nil); st4.Status != "unklar" {
|
||||||
|
t.Errorf("unknown obligation: want unklar, got %q", st4.Status)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -23,9 +23,10 @@ type ControlMapping struct {
|
|||||||
SourceRole string `json:"source_role"` // source_role of the norm (operational_requirement, ...)
|
SourceRole string `json:"source_role"` // source_role of the norm (operational_requirement, ...)
|
||||||
TargetFramework string `json:"target_framework"` // e.g. "OWASP ASVS"
|
TargetFramework string `json:"target_framework"` // e.g. "OWASP ASVS"
|
||||||
TargetControl string `json:"target_control"` // e.g. "V6.3.1"
|
TargetControl string `json:"target_control"` // e.g. "V6.3.1"
|
||||||
MappingType string `json:"mapping_type"` // supports | partially_supports | implements | related | contradicts
|
MappingType string `json:"mapping_type"` // primary_implementation | implements | supports | partially_supports | related | contradicts
|
||||||
MappingStatus string `json:"mapping_status"` // candidate | accepted | rejected | superseded
|
MappingStatus string `json:"mapping_status"` // candidate | accepted | rejected | superseded
|
||||||
Provenance string `json:"provenance"` // retriever_candidate | human_curated | rule_based
|
Provenance string `json:"provenance"` // retriever_candidate | human_curated | rule_based
|
||||||
|
ObligationID string `json:"obligation_id,omitempty"` // stable cross-session join key (Obligation Registry); empty until adopted, citation_unit is the interim bridge
|
||||||
Rationale string `json:"rationale"`
|
Rationale string `json:"rationale"`
|
||||||
ReviewedBy string `json:"reviewed_by,omitempty"` // who decided (human or rule id)
|
ReviewedBy string `json:"reviewed_by,omitempty"` // who decided (human or rule id)
|
||||||
ReviewDate string `json:"review_date,omitempty"` // YYYY-MM-DD
|
ReviewDate string `json:"review_date,omitempty"` // YYYY-MM-DD
|
||||||
@@ -35,7 +36,7 @@ type ControlMapping struct {
|
|||||||
|
|
||||||
// Allowed enum values — the deterministic "rule" layer that keeps the curated store clean.
|
// Allowed enum values — the deterministic "rule" layer that keeps the curated store clean.
|
||||||
var (
|
var (
|
||||||
mappingTypeValues = map[string]bool{"supports": true, "partially_supports": true, "implements": true, "related": true, "contradicts": true}
|
mappingTypeValues = map[string]bool{"primary_implementation": true, "implements": true, "supports": true, "partially_supports": true, "related": true, "contradicts": true}
|
||||||
mappingStatusValues = map[string]bool{"candidate": true, "accepted": true, "rejected": true, "superseded": true}
|
mappingStatusValues = map[string]bool{"candidate": true, "accepted": true, "rejected": true, "superseded": true}
|
||||||
provenanceValues = map[string]bool{"retriever_candidate": true, "human_curated": true, "rule_based": true}
|
provenanceValues = map[string]bool{"retriever_candidate": true, "human_curated": true, "rule_based": true}
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,172 @@
|
|||||||
|
package ucca
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"os"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ObligationKey is one entry of the Obligation Registry's cross-session contract
|
||||||
|
// (obligations/obligation_join_keys.json). obligation_id is the STABLE join key — assigned
|
||||||
|
// only by the Registry, never minted here. citation_units are the interim bridge until our
|
||||||
|
// ControlMapping adopts obligation_id directly.
|
||||||
|
type ObligationKey struct {
|
||||||
|
ObligationID string `json:"obligation_id"`
|
||||||
|
Regulation string `json:"regulation"`
|
||||||
|
Family string `json:"family"`
|
||||||
|
Tier string `json:"tier"`
|
||||||
|
CitationUnits []string `json:"citation_units"`
|
||||||
|
SourceRole string `json:"source_role"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ObligationJoinKeys is the loaded contract + a citation-unit index for the interim join.
|
||||||
|
type ObligationJoinKeys struct {
|
||||||
|
SchemaVersion string `json:"schema_version"`
|
||||||
|
Count int `json:"count"`
|
||||||
|
ObligationIDs []ObligationKey `json:"obligation_ids"`
|
||||||
|
byCitationKey map[string][]string
|
||||||
|
}
|
||||||
|
|
||||||
|
var citationRefRe = regexp.MustCompile(`\(([0-9a-zA-Z]+)\)`)
|
||||||
|
|
||||||
|
// citationUnitKey normalizes a CRA Annex I reference for the INTERIM citation_unit join, so
|
||||||
|
// our "CRA Annex I Part I (2)(c)" and the Registry's "Annex I (2)(c)" collapse to the same
|
||||||
|
// key ("i:2.c"). Interim only — superseded by the stable obligation_id once adopted.
|
||||||
|
func citationUnitKey(cu string) string {
|
||||||
|
low := strings.ToLower(cu)
|
||||||
|
part := ""
|
||||||
|
switch {
|
||||||
|
case strings.Contains(low, "part ii"):
|
||||||
|
part = "ii"
|
||||||
|
case strings.Contains(low, "part i"), strings.Contains(low, "(2)"):
|
||||||
|
part = "i" // CRA Annex I Part I = the (2)(x) essential requirements
|
||||||
|
}
|
||||||
|
var refs []string
|
||||||
|
for _, m := range citationRefRe.FindAllStringSubmatch(cu, -1) {
|
||||||
|
refs = append(refs, strings.ToLower(m[1]))
|
||||||
|
}
|
||||||
|
return part + ":" + strings.Join(refs, ".")
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadObligationJoinKeys reads the Registry contract and indexes it by citation-unit key.
|
||||||
|
func LoadObligationJoinKeys(path string) (*ObligationJoinKeys, error) {
|
||||||
|
raw, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var o ObligationJoinKeys
|
||||||
|
if err := json.Unmarshal(raw, &o); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
o.byCitationKey = map[string][]string{}
|
||||||
|
for _, ob := range o.ObligationIDs {
|
||||||
|
for _, cu := range ob.CitationUnits {
|
||||||
|
k := citationUnitKey(cu)
|
||||||
|
o.byCitationKey[k] = append(o.byCitationKey[k], ob.ObligationID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return &o, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ObligationsForCitation returns the obligation_ids that join (interim) to a citation
|
||||||
|
// reference such as a control_mapping.source_norm.
|
||||||
|
func (o *ObligationJoinKeys) ObligationsForCitation(citationRef string) []string {
|
||||||
|
return o.byCitationKey[citationUnitKey(citationRef)]
|
||||||
|
}
|
||||||
|
|
||||||
|
// FindObligation returns the registry entry for an obligation_id (nil if unknown).
|
||||||
|
func (o *ObligationJoinKeys) FindObligation(obligationID string) *ObligationKey {
|
||||||
|
for i := range o.ObligationIDs {
|
||||||
|
if o.ObligationIDs[i].ObligationID == obligationID {
|
||||||
|
return &o.ObligationIDs[i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// mappingReaches reports whether a control mapping reaches an obligation — EXACT via the
|
||||||
|
// adopted obligation_id (semantic, preferred), else via the interim citation_unit join (for
|
||||||
|
// not-yet-adopted rows). Once obligation_id is set, the coarse citation_unit match is ignored:
|
||||||
|
// that is how the semantic join replaces the structural one (e.g. V11.2.1 crypto no longer
|
||||||
|
// rides (2)(d) into user_authentication_required — it goes to credential_confidentiality_protection).
|
||||||
|
func mappingReaches(m ControlMapping, ob ObligationKey, citationKeys map[string]bool) bool {
|
||||||
|
if m.ObligationID != "" {
|
||||||
|
return m.ObligationID == ob.ObligationID
|
||||||
|
}
|
||||||
|
return citationKeys[citationUnitKey(m.SourceNorm)]
|
||||||
|
}
|
||||||
|
|
||||||
|
// AcceptedControlsForObligation returns our accepted control mappings that reach an obligation
|
||||||
|
// (deduped by target control), obligation_id-exact where adopted, citation_unit otherwise.
|
||||||
|
func AcceptedControlsForObligation(ob ObligationKey, mappings *ControlMappingSet) []ControlMapping {
|
||||||
|
keys := make(map[string]bool, len(ob.CitationUnits))
|
||||||
|
for _, cu := range ob.CitationUnits {
|
||||||
|
keys[citationUnitKey(cu)] = true
|
||||||
|
}
|
||||||
|
out := []ControlMapping{}
|
||||||
|
seen := map[string]bool{}
|
||||||
|
for _, m := range mappings.All {
|
||||||
|
if !m.IsAccepted() || !mappingReaches(m, ob, keys) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
ck := m.TargetFramework + ":" + m.TargetControl
|
||||||
|
if seen[ck] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen[ck] = true
|
||||||
|
out = append(out, m)
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// ObligationCoverage is one row of the cross-session coverage report.
|
||||||
|
type ObligationCoverage struct {
|
||||||
|
ObligationID string `json:"obligation_id"`
|
||||||
|
Family string `json:"family"`
|
||||||
|
Status string `json:"status"` // covered | mapped_rejected | uncovered
|
||||||
|
AcceptedControls []string `json:"accepted_controls"`
|
||||||
|
EvidenceCount int `json:"evidence_count"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ComputeObligationCoverage joins the Registry obligations to our control mappings — exact via
|
||||||
|
// obligation_id where adopted, else via the interim citation_unit join — and reports per
|
||||||
|
// obligation: covered (>=1 accepted control reaches it), mapped_rejected (only rejected
|
||||||
|
// mappings reach it), or uncovered. The signal back to the Obligation session.
|
||||||
|
func ComputeObligationCoverage(joins *ObligationJoinKeys, mappings *ControlMappingSet, evidence *EvidenceRequirementSet) []ObligationCoverage {
|
||||||
|
out := make([]ObligationCoverage, 0, len(joins.ObligationIDs))
|
||||||
|
for _, ob := range joins.ObligationIDs {
|
||||||
|
keys := make(map[string]bool, len(ob.CitationUnits))
|
||||||
|
for _, cu := range ob.CitationUnits {
|
||||||
|
keys[citationUnitKey(cu)] = true
|
||||||
|
}
|
||||||
|
cov := ObligationCoverage{ObligationID: ob.ObligationID, Family: ob.Family}
|
||||||
|
seen := map[string]bool{}
|
||||||
|
rejected := false
|
||||||
|
for _, m := range mappings.All {
|
||||||
|
if !mappingReaches(m, ob, keys) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if m.IsAccepted() {
|
||||||
|
ck := m.TargetFramework + ":" + m.TargetControl
|
||||||
|
if !seen[ck] {
|
||||||
|
seen[ck] = true
|
||||||
|
cov.AcceptedControls = append(cov.AcceptedControls, ck)
|
||||||
|
cov.EvidenceCount += len(evidence.RequiredFor(m.TargetFramework, m.TargetControl))
|
||||||
|
}
|
||||||
|
} else if m.MappingStatus == "rejected" {
|
||||||
|
rejected = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
switch {
|
||||||
|
case len(cov.AcceptedControls) > 0:
|
||||||
|
cov.Status = "covered"
|
||||||
|
case rejected:
|
||||||
|
cov.Status = "mapped_rejected"
|
||||||
|
default:
|
||||||
|
cov.Status = "uncovered"
|
||||||
|
}
|
||||||
|
out = append(out, cov)
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
package ucca
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestCitationUnitKey_Join(t *testing.T) {
|
||||||
|
// our source_norm and the registry citation_unit must collapse to the SAME key.
|
||||||
|
if citationUnitKey("CRA Annex I Part I (2)(c) — Schutz vor unbefugtem Zugriff") != citationUnitKey("Annex I (2)(c)") {
|
||||||
|
t.Errorf("interim join broken: %q vs %q",
|
||||||
|
citationUnitKey("CRA Annex I Part I (2)(c)"), citationUnitKey("Annex I (2)(c)"))
|
||||||
|
}
|
||||||
|
// Part II must NOT collide with Part I.
|
||||||
|
if citationUnitKey("Annex I Part II (1)") == citationUnitKey("CRA Annex I Part I (2)(c)") {
|
||||||
|
t.Error("Part II must not join to Part I")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLoadObligationJoinKeys(t *testing.T) {
|
||||||
|
o, err := LoadObligationJoinKeys("../../../obligations/obligation_join_keys.json")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("load: %v", err)
|
||||||
|
}
|
||||||
|
if o.Count != len(o.ObligationIDs) {
|
||||||
|
t.Errorf("count %d != len %d", o.Count, len(o.ObligationIDs))
|
||||||
|
}
|
||||||
|
if len(o.ObligationIDs) == 0 {
|
||||||
|
t.Fatal("empty contract")
|
||||||
|
}
|
||||||
|
if got := o.ObligationsForCitation("CRA Annex I Part I (2)(c)"); len(got) == 0 {
|
||||||
|
t.Error("expected an obligation joined to (2)(c)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestObligationCoverage_Report(t *testing.T) {
|
||||||
|
joins, err := LoadObligationJoinKeys("../../../obligations/obligation_join_keys.json")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("join keys: %v", err)
|
||||||
|
}
|
||||||
|
maps, err := LoadControlMappings("../../data/control_mappings")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("mappings: %v", err)
|
||||||
|
}
|
||||||
|
ev, err := LoadEvidenceRequirements("../../data/evidence_requirements")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("evidence: %v", err)
|
||||||
|
}
|
||||||
|
cov := ComputeObligationCoverage(joins, maps, ev)
|
||||||
|
if len(cov) == 0 {
|
||||||
|
t.Fatal("no coverage computed")
|
||||||
|
}
|
||||||
|
byStatus := map[string]int{}
|
||||||
|
for _, c := range cov {
|
||||||
|
byStatus[c.Status]++
|
||||||
|
}
|
||||||
|
t.Logf("COVERAGE: %d Obligations | covered=%d mapped_rejected=%d uncovered=%d",
|
||||||
|
len(cov), byStatus["covered"], byStatus["mapped_rejected"], byStatus["uncovered"])
|
||||||
|
for _, c := range cov {
|
||||||
|
if c.Status != "uncovered" {
|
||||||
|
t.Logf(" %-15s %-36s controls=%v evidence=%d", c.Status, c.ObligationID, c.AcceptedControls, c.EvidenceCount)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user