import copy import importlib.util import sys from pathlib import Path import yaml TOOL_DIR = Path(__file__).resolve().parents[1] TOOL_PATH = TOOL_DIR / "security_scenario_composer.py" sys.path.insert(0, str(TOOL_DIR)) SPEC = importlib.util.spec_from_file_location("security_scenario_composer", TOOL_PATH) composer = importlib.util.module_from_spec(SPEC) assert SPEC.loader is not None sys.modules[SPEC.name] = composer SPEC.loader.exec_module(composer) def declaration_data( declaration_id="owner.c0", capability="c0.bootstrap-identity", tier="C0", requires=None, satisfies=None, ): requires = ["bare_host_trust"] if requires is None else requires satisfies = ["bootstrap_identity_trust"] if satisfies is None else satisfies return { "apiVersion": composer.contract.API_VERSION, "kind": composer.contract.KIND, "metadata": { "id": declaration_id, "name": declaration_id, "owner": "owner", "repo": "owner-repo", "domain": "infotech", "contract_version": "0.1", }, "spec": { "playbook": { "path": "playbooks/reference.yaml", "type": "reference", "invocation": "make reference", "description": "Reference entry point.", }, "capabilities": [ { "id": capability, "tier": tier, "resource_kinds": ["identities"], "description": "Reference capability.", } ], "parameters": [ { "name": "target", "type": "string", "required": True, "default": "reference", "sensitivity": "operational", "tuning_authority": "netkingdom_tunable", "description": "Reference target.", }, { "name": "secure_mode", "type": "boolean", "required": False, "default": True, "sensitivity": "security_sensitive", "tuning_authority": "platform_only", "description": "Security-sensitive reference switch.", }, ], "responsibilities": [ { "resource_kind": "identities", "owner": "owner", "resources": ["identity:reference"], "repo_owns": "Reference execution.", "netkingdom_orchestrates": "Reference selection.", } ], "trust": { "requires": [ {"state": state, "readiness_checks": []} for state in requires ], "satisfies": [ { "state": state, "readiness_checks": [ { "id": f"{state}-ready", "description": f"{state} is ready.", "evidence": "reference evidence", } ], } for state in satisfies ], }, "catalog": { "publish": f"capabilities/playbooks/{declaration_id}.yaml", "maturity": "reference", "consumers": ["netkingdom-security-scenario-composer"], }, }, } def declaration(tmp_path, data, filename="declaration.yaml"): path = tmp_path / filename path.write_text(yaml.safe_dump(data, sort_keys=False), encoding="utf-8") return composer.Declaration(path=path, data=data) def scenario(capabilities=None): capabilities = ["c0.bootstrap-identity"] if capabilities is None else capabilities return { "id": "scenario:test", "authority": "netkingdom", "initial_trust": ["bare_host_trust"], "requires": {"capabilities": capabilities}, "providers": {}, "parameter_overrides": {}, } def messages(issues): return [item.message for item in issues if item.level == "ERROR"] def test_single_provider_emits_plan_only_handoff(tmp_path): item = declaration(tmp_path, declaration_data()) issues, plan = composer.compose_scenario([item], scenario()) assert messages(issues) == [] assert plan["execution"] == { "mode": "plan-only", "permitted": False, "reason": "Composition does not authorize or perform provider execution.", } assert plan["execution_steps"][0]["executor_owner"] == "owner" assert plan["execution_steps"][0]["readiness_obligations"][0]["checks"] assert plan["responsibility_map"][0]["declaration_id"] == "owner.c0" assert plan["composition_digest"].startswith("sha256:") def test_ambiguous_provider_requires_pin(tmp_path): first = declaration(tmp_path, declaration_data("owner.c0-a"), "a.yaml") second = declaration(tmp_path, declaration_data("owner.c0-b"), "b.yaml") issues, plan = composer.compose_scenario([second, first], scenario()) assert plan is None assert any("ambiguous providers" in message for message in messages(issues)) def test_explicit_provider_pin_resolves_ambiguity(tmp_path): first = declaration(tmp_path, declaration_data("owner.c0-a"), "a.yaml") second = declaration(tmp_path, declaration_data("owner.c0-b"), "b.yaml") request = scenario() request["providers"] = {"c0.bootstrap-identity": "owner.c0-b"} issues, plan = composer.compose_scenario([second, first], request) assert messages(issues) == [] assert plan["capability_providers"] == {"c0.bootstrap-identity": "owner.c0-b"} def test_bad_provider_pin_fails_closed(tmp_path): item = declaration(tmp_path, declaration_data()) request = scenario() request["providers"] = {"c0.bootstrap-identity": "owner.missing"} issues, plan = composer.compose_scenario([item], request) assert plan is None assert any("does not provide" in message for message in messages(issues)) def test_override_for_unselected_declaration_fails(tmp_path): item = declaration(tmp_path, declaration_data()) request = scenario() request["parameter_overrides"] = {"owner.other": {"target": "wrong"}} issues, plan = composer.compose_scenario([item], request) assert plan is None assert "override targets an unselected declaration" in messages(issues) def test_tenant_cannot_override_platform_only_parameter(tmp_path): item = declaration(tmp_path, declaration_data()) request = scenario() request["authority"] = "tenant" request["parameter_overrides"] = {"owner.c0": {"secure_mode": False}} issues, plan = composer.compose_scenario([item], request) assert plan is None assert any("tenant authority cannot override" in message for message in messages(issues)) def test_trust_dependencies_override_request_order(tmp_path): c0 = declaration(tmp_path, declaration_data(), "c0.yaml") c1_data = declaration_data( "owner.c1", "c1.lightweight-sso", "C1", requires=["bootstrap_identity_trust"], satisfies=["runtime_identity_trust"], ) c1 = declaration(tmp_path, c1_data, "c1.yaml") request = scenario(["c1.lightweight-sso", "c0.bootstrap-identity"]) issues, plan = composer.compose_scenario([c1, c0], request) assert messages(issues) == [] assert [step["declaration_id"] for step in plan["execution_steps"]] == [ "owner.c0", "owner.c1", ] assert "runtime_identity_trust" in plan["planned_final_trust"] def test_unresolved_trust_fails_closed(tmp_path): item = declaration(tmp_path, declaration_data()) request = scenario() request["initial_trust"] = [] issues, plan = composer.compose_scenario([item], request) assert plan is None assert any("unresolved trust states" in message for message in messages(issues)) def test_duplicate_declaration_ids_fail_closed(tmp_path): data = declaration_data() first = declaration(tmp_path, data, "a.yaml") second = declaration(tmp_path, copy.deepcopy(data), "b.yaml") issues, plan = composer.compose_scenario([first, second], scenario()) assert plan is None assert any("duplicate declaration id" in message for message in messages(issues)) def test_checked_in_c0_scenario_composes(): repo_root = Path(__file__).resolve().parents[3] declaration_path = repo_root / "capabilities/playbooks/net-kingdom.local-identity.yaml" scenario_path = repo_root / "examples/security-scenarios/c0-local-identity.yaml" item = composer.Declaration( path=declaration_path, data=composer.contract.load_yaml(declaration_path), ) request = composer.contract.load_yaml(scenario_path) issues, plan = composer.compose_scenario([item], request) assert messages(issues) == [] assert plan["capability_providers"] == { "c0.bootstrap-identity": "net-kingdom.local-identity" } assert plan["execution_steps"][0]["repo"] == "net-kingdom" def test_checked_in_key_cape_scenario_sequences_c1_before_c2b(tmp_path): repo_root = Path(__file__).resolve().parents[3] scenario_path = repo_root / "examples/security-scenarios/c1-c2b-key-cape.yaml" c1_data = declaration_data( "key-cape.lightweight-sso", "c1.lightweight-sso", "C1", requires=["cluster_trust", "runtime_secret_trust"], satisfies=["runtime_identity_trust"], ) c1_data["metadata"]["owner"] = "key-cape" c1_data["metadata"]["repo"] = "key-cape" c2b_data = declaration_data( "key-cape.privacyidea-token-authority", "c2b.token-authority", "C2b", requires=["runtime_identity_trust", "runtime_secret_trust"], satisfies=["runtime_identity_trust"], ) c2b_data["metadata"]["owner"] = "key-cape" c2b_data["metadata"]["repo"] = "key-cape" declarations = [ declaration(tmp_path, c2b_data, "c2b.yaml"), declaration(tmp_path, c1_data, "c1.yaml"), ] request = composer.contract.load_yaml(scenario_path) issues, plan = composer.compose_scenario(declarations, request) assert messages(issues) == [] assert plan["capability_providers"] == { "c1.lightweight-sso": "key-cape.lightweight-sso", "c2b.token-authority": "key-cape.privacyidea-token-authority", } assert [step["declaration_id"] for step in plan["execution_steps"]] == [ "key-cape.lightweight-sso", "key-cape.privacyidea-token-authority", ] assert plan["execution"]["permitted"] is False