"""Tests for Framework Decomposition Engine. Covers: - Registry loading - Routing classification (atomic / compound / framework_container) - Framework + domain matching - Subcontrol selection - Decomposition into sub-obligations - Quality rules (warnings, errors) - Inference helpers """ import pytest from compliance.services.framework_decomposition import ( classify_routing, decompose_framework_container, get_registry, registry_stats, reload_registry, DecomposedObligation, FrameworkDecompositionResult, RoutingResult, _detect_framework, _has_framework_keywords, _infer_action, _infer_object, _is_compound_obligation, _match_domain, _select_subcontrols, ) # --------------------------------------------------------------------------- # REGISTRY TESTS # --------------------------------------------------------------------------- class TestRegistryLoading: def test_registry_loads_successfully(self): reg = get_registry() assert len(reg) >= 3 def test_nist_in_registry(self): reg = get_registry() assert "NIST_SP800_53" in reg def test_owasp_asvs_in_registry(self): reg = get_registry() assert "OWASP_ASVS" in reg def test_csa_ccm_in_registry(self): reg = get_registry() assert "CSA_CCM" in reg def test_nist_has_domains(self): reg = get_registry() nist = reg["NIST_SP800_53"] assert len(nist["domains"]) >= 5 def test_nist_ac_has_subcontrols(self): reg = get_registry() nist = reg["NIST_SP800_53"] ac = next(d for d in nist["domains"] if d["domain_id"] == "AC") assert len(ac["subcontrols"]) >= 5 def test_registry_stats(self): stats = registry_stats() assert stats["frameworks"] >= 3 assert stats["total_domains"] >= 10 assert stats["total_subcontrols"] >= 30 def test_reload_registry(self): reg = reload_registry() assert len(reg) >= 3 # --------------------------------------------------------------------------- # ROUTING TESTS # --------------------------------------------------------------------------- class TestClassifyRouting: def test_atomic_simple_obligation(self): result = classify_routing( obligation_text="Multi-Faktor-Authentifizierung muss implementiert werden", action_raw="implementieren", object_raw="MFA", ) assert result.routing_type == "atomic" def test_framework_container_ccm_ais(self): result = classify_routing( obligation_text="Die CCM-Praktiken fuer Application and Interface Security (AIS) muessen implementiert werden", action_raw="implementieren", object_raw="CCM-Praktiken fuer AIS", ) assert result.routing_type == "framework_container" assert result.framework_ref == "CSA_CCM" assert result.framework_domain == "AIS" def test_framework_container_nist_800_53(self): result = classify_routing( obligation_text="Kontrollen gemaess NIST SP 800-53 umsetzen", action_raw="umsetzen", object_raw="Kontrollen gemaess NIST SP 800-53", ) assert result.routing_type == "framework_container" assert result.framework_ref == "NIST_SP800_53" def test_framework_container_owasp_asvs(self): result = classify_routing( obligation_text="OWASP ASVS Anforderungen muessen implementiert werden", action_raw="implementieren", object_raw="OWASP ASVS Anforderungen", ) assert result.routing_type == "framework_container" assert result.framework_ref == "OWASP_ASVS" def test_compound_obligation(self): result = classify_routing( obligation_text="Richtlinie erstellen und Schulungen durchfuehren", action_raw="erstellen und durchfuehren", object_raw="Richtlinie", ) assert result.routing_type == "compound" def test_no_split_phrase_not_compound(self): result = classify_routing( obligation_text="Richtlinie dokumentieren und pflegen", action_raw="dokumentieren und pflegen", object_raw="Richtlinie", ) assert result.routing_type == "atomic" def test_framework_keywords_in_object(self): result = classify_routing( obligation_text="Massnahmen umsetzen", action_raw="umsetzen", object_raw="Framework-Praktiken und Kontrollen", ) assert result.routing_type == "framework_container" def test_bsi_grundschutz_detected(self): result = classify_routing( obligation_text="BSI IT-Grundschutz Massnahmen umsetzen", action_raw="umsetzen", object_raw="BSI IT-Grundschutz Massnahmen", ) assert result.routing_type == "framework_container" # --------------------------------------------------------------------------- # FRAMEWORK DETECTION TESTS # --------------------------------------------------------------------------- class TestFrameworkDetection: def test_detect_csa_ccm_with_domain(self): result = _detect_framework( "CCM-Praktiken fuer AIS implementieren", "CCM-Praktiken", ) assert result.routing_type == "framework_container" assert result.framework_ref == "CSA_CCM" assert result.framework_domain == "AIS" def test_detect_nist_without_domain(self): result = _detect_framework( "NIST SP 800-53 Kontrollen implementieren", "Kontrollen", ) assert result.routing_type == "framework_container" assert result.framework_ref == "NIST_SP800_53" def test_no_framework_in_simple_text(self): result = _detect_framework( "Passwortrichtlinie dokumentieren", "Passwortrichtlinie", ) assert result.routing_type == "atomic" def test_csa_ccm_iam_domain(self): result = _detect_framework( "CSA CCM Identity and Access Management Kontrollen", "IAM-Kontrollen", ) assert result.routing_type == "framework_container" assert result.framework_ref == "CSA_CCM" assert result.framework_domain == "IAM" # --------------------------------------------------------------------------- # DOMAIN MATCHING TESTS # --------------------------------------------------------------------------- class TestDomainMatching: def test_match_ais_by_id(self): reg = get_registry() ccm = reg["CSA_CCM"] domain_id, title = _match_domain("AIS-Kontrollen implementieren", ccm) assert domain_id == "AIS" def test_match_by_full_title(self): reg = get_registry() ccm = reg["CSA_CCM"] domain_id, title = _match_domain( "Application and Interface Security Massnahmen", ccm, ) assert domain_id == "AIS" def test_match_nist_incident_response(self): reg = get_registry() nist = reg["NIST_SP800_53"] domain_id, title = _match_domain( "Vorfallreaktionsverfahren gemaess NIST IR", nist, ) assert domain_id == "IR" def test_no_match_generic_text(self): reg = get_registry() nist = reg["NIST_SP800_53"] domain_id, title = _match_domain("etwas Allgemeines", nist) assert domain_id is None # --------------------------------------------------------------------------- # SUBCONTROL SELECTION TESTS # --------------------------------------------------------------------------- class TestSubcontrolSelection: def test_keyword_based_selection(self): subcontrols = [ {"subcontrol_id": "SC-1", "title": "X", "keywords": ["api", "schnittstelle"], "object_hint": ""}, {"subcontrol_id": "SC-2", "title": "Y", "keywords": ["backup", "sicherung"], "object_hint": ""}, ] selected = _select_subcontrols("API-Schnittstellen schuetzen", subcontrols) assert len(selected) == 1 assert selected[0]["subcontrol_id"] == "SC-1" def test_no_keyword_match_returns_empty(self): subcontrols = [ {"subcontrol_id": "SC-1", "keywords": ["backup"], "title": "Backup", "object_hint": ""}, ] selected = _select_subcontrols("Passwort aendern", subcontrols) assert selected == [] def test_title_match_boosts_score(self): subcontrols = [ {"subcontrol_id": "SC-1", "title": "Password Security", "keywords": ["passwort"], "object_hint": ""}, {"subcontrol_id": "SC-2", "title": "Network Security", "keywords": ["netzwerk"], "object_hint": ""}, ] selected = _select_subcontrols("Password Security muss implementiert werden", subcontrols) assert len(selected) >= 1 assert selected[0]["subcontrol_id"] == "SC-1" # --------------------------------------------------------------------------- # DECOMPOSITION TESTS # --------------------------------------------------------------------------- class TestDecomposeFrameworkContainer: def test_decompose_ccm_ais(self): result = decompose_framework_container( obligation_candidate_id="OBL-001", parent_control_id="COMP-001", obligation_text="Die CCM-Praktiken fuer AIS muessen implementiert werden", framework_ref="CSA_CCM", framework_domain="AIS", ) assert result.release_state == "decomposed" assert result.framework_ref == "CSA_CCM" assert result.framework_domain == "AIS" assert len(result.decomposed_obligations) >= 3 assert len(result.matched_subcontrols) >= 3 def test_decomposed_obligations_have_ids(self): result = decompose_framework_container( obligation_candidate_id="OBL-001", parent_control_id="COMP-001", obligation_text="CCM-Praktiken fuer AIS", framework_ref="CSA_CCM", framework_domain="AIS", ) for d in result.decomposed_obligations: assert d.obligation_candidate_id.startswith("OBL-001-AIS-") assert d.parent_control_id == "COMP-001" assert d.source_ref_law == "Cloud Security Alliance CCM v4" assert d.routing_type == "atomic" assert d.release_state == "decomposed" def test_decomposed_have_action_and_object(self): result = decompose_framework_container( obligation_candidate_id="OBL-002", parent_control_id="COMP-002", obligation_text="CSA CCM AIS Massnahmen implementieren", framework_ref="CSA_CCM", framework_domain="AIS", ) for d in result.decomposed_obligations: assert d.action_raw, f"{d.subcontrol_id} missing action_raw" assert d.object_raw, f"{d.subcontrol_id} missing object_raw" def test_unknown_framework_returns_unmatched(self): result = decompose_framework_container( obligation_candidate_id="OBL-003", parent_control_id="COMP-003", obligation_text="XYZ-Framework Controls", framework_ref="NONEXISTENT", framework_domain="ABC", ) assert result.release_state == "unmatched" assert any("framework_not_matched" in i for i in result.issues) assert len(result.decomposed_obligations) == 0 def test_unknown_domain_falls_back_to_full(self): result = decompose_framework_container( obligation_candidate_id="OBL-004", parent_control_id="COMP-004", obligation_text="CSA CCM Kontrollen implementieren", framework_ref="CSA_CCM", framework_domain=None, ) # Should still decompose (falls back to keyword match or all domains) assert result.release_state in ("decomposed", "unmatched") def test_nist_incident_response_decomposition(self): result = decompose_framework_container( obligation_candidate_id="OBL-010", parent_control_id="COMP-010", obligation_text="NIST SP 800-53 Vorfallreaktionsmassnahmen implementieren", framework_ref="NIST_SP800_53", framework_domain="IR", ) assert result.release_state == "decomposed" assert len(result.decomposed_obligations) >= 3 sc_ids = [d.subcontrol_id for d in result.decomposed_obligations] assert any("IR-" in sc for sc in sc_ids) def test_confidence_high_with_full_match(self): result = decompose_framework_container( obligation_candidate_id="OBL-005", parent_control_id="COMP-005", obligation_text="CSA CCM AIS", framework_ref="CSA_CCM", framework_domain="AIS", ) assert result.decomposition_confidence >= 0.7 def test_confidence_low_without_framework(self): result = decompose_framework_container( obligation_candidate_id="OBL-006", parent_control_id="COMP-006", obligation_text="Unbekannte Massnahmen", framework_ref=None, framework_domain=None, ) assert result.decomposition_confidence <= 0.3 # --------------------------------------------------------------------------- # COMPOUND DETECTION TESTS # --------------------------------------------------------------------------- class TestCompoundDetection: def test_compound_verb(self): assert _is_compound_obligation( "erstellen und schulen", "Richtlinie erstellen und Schulungen durchfuehren", ) def test_no_split_phrase(self): assert not _is_compound_obligation( "dokumentieren und pflegen", "Richtlinie dokumentieren und pflegen", ) def test_no_split_define_and_maintain(self): assert not _is_compound_obligation( "define and maintain", "Define and maintain a security policy", ) def test_single_verb_not_compound(self): assert not _is_compound_obligation( "implementieren", "MFA implementieren", ) def test_empty_action_not_compound(self): assert not _is_compound_obligation("", "something") # --------------------------------------------------------------------------- # FRAMEWORK KEYWORD TESTS # --------------------------------------------------------------------------- class TestFrameworkKeywords: def test_two_keywords_detected(self): assert _has_framework_keywords("Framework-Praktiken implementieren") def test_single_keyword_not_enough(self): assert not _has_framework_keywords("Praktiken implementieren") def test_no_keywords(self): assert not _has_framework_keywords("MFA einrichten") # --------------------------------------------------------------------------- # INFERENCE HELPER TESTS # --------------------------------------------------------------------------- class TestInferAction: def test_infer_implementieren(self): assert _infer_action("Massnahmen muessen implementiert werden") == "implementieren" def test_infer_dokumentieren(self): assert _infer_action("Richtlinie muss dokumentiert werden") == "dokumentieren" def test_infer_testen(self): assert _infer_action("System wird getestet") == "testen" def test_infer_ueberwachen(self): assert _infer_action("Logs werden ueberwacht") == "ueberwachen" def test_infer_default(self): assert _infer_action("etwas passiert") == "implementieren" class TestInferObject: def test_infer_from_muessen_pattern(self): result = _infer_object("Zugriffsrechte muessen ueberprueft werden") assert "ueberprueft" in result or "Zugriffsrechte" in result def test_infer_fallback(self): result = _infer_object("Einfacher Satz ohne Modalverb") assert len(result) > 0