import importlib.util import json import sys from pathlib import Path import pytest # Dynamically import the console module (like other conformance tests) TOOL_PATH = Path(__file__).resolve().parents[1] / "security_bootstrap_console.py" SPEC = importlib.util.spec_from_file_location("security_bootstrap_console", TOOL_PATH) console = importlib.util.module_from_spec(SPEC) assert SPEC.loader is not None sys.modules[SPEC.name] = console SPEC.loader.exec_module(console) def test_metadata_template_has_core_fields(): tmpl = console.metadata_template() assert isinstance(tmpl, dict) core = [ "approval_scope", "bootstrap_mode", "custody_mode", "openbao_unseal_custody_model", "review_date", ] for f in core: assert f in tmpl assert tmpl["openbao_unseal_custody_model"] == console.DEFAULT_OPENBAO_UNSEAL_CUSTODY_MODEL def test_openbao_unseal_custody_model_gate_automation_default(): data = console.metadata_template() gate = console.openbao_unseal_custody_model_gate(data) assert gate.status == "done" init_gate = console.openbao_init_ceremony_gate(data) assert init_gate.status == "automation" def test_openbao_unseal_custody_production_models_selectable(): for model in ("attended-ceremony", "auto-unseal-transit"): data = console.metadata_template() data["openbao_unseal_custody_model"] = model gate = console.openbao_unseal_custody_model_gate(data) assert gate.status == "done" def test_attended_ceremony_init_gate_is_human_with_runbook(): data = console.metadata_template() data["openbao_unseal_custody_model"] = "attended-ceremony" init_gate = console.openbao_init_ceremony_gate(data) assert init_gate.status == "human" assert "openbao-attended-ceremony-runbook" in init_gate.reason def test_auto_unseal_transit_init_gate_requires_evidence(): data = console.metadata_template() data["openbao_unseal_custody_model"] = "auto-unseal-transit" gate = console.openbao_init_ceremony_gate(data) assert gate.status == "blocked" assert "openbao_transit_seal_configured" in gate.reason data["openbao_transit_seal_configured"] = True gate = console.openbao_init_ceremony_gate(data) assert gate.status == "blocked" assert "openbao_auto_unseal_verified" in gate.reason data["openbao_auto_unseal_verified"] = True gate = console.openbao_init_ceremony_gate(data) assert gate.status == "human" def test_production_profile_blocks_sops_held_automation(): data = console.metadata_template() data["deployment_profile"] = "production" gate = console.openbao_unseal_custody_model_gate(data) assert gate.status == "blocked" assert "production profile" in gate.reason.lower() init_gate = console.openbao_init_ceremony_gate(data) assert init_gate.status == "blocked" data["openbao_unseal_custody_model"] = "attended-ceremony" assert console.openbao_unseal_custody_model_gate(data).status == "done" def test_openbao_ceremony_record_template_and_validation(tmp_path): tmpl = console.openbao_ceremony_record_template() for key in ( "attended_init_completed", "unseal_shares_escrowed_out_of_band", "root_token_retired_or_escrowed", "post_unseal_verified", "no_secret_material_recorded", ): assert key in tmpl # a filled-in, non-secret record validates cleanly record = dict(tmpl) record.update( evidence_date="2026-07-02", ceremony_scope="Attended init on Railiance ThreePhoenix.", unseal_share_escrow_disposition="Shares to roster holders, offline packets.", root_token_disposition="revoked after configure-initial", witness="recovery-custodian", attended_init_completed=True, unseal_shares_escrowed_out_of_band=True, root_token_retired_or_escrowed=True, post_unseal_verified=True, no_secret_material_recorded=True, ) path = tmp_path / "record.json" path.write_text(json.dumps(record)) _, errors = console.load_evidence_json(path, "openbao-ceremony") assert errors == [] # a record leaking a token marker is refused record["root_token_disposition"] = "hvs.deadbeef" path.write_text(json.dumps(record)) _, errors = console.load_evidence_json(path, "openbao-ceremony") assert any("secret-looking" in e for e in errors) def test_onboarding_dry_run_template_has_required_fields(): tmpl = console.onboarding_dry_run_template() assert isinstance(tmpl, dict) required = [ "dry_run_date", "operator", "subject_reference", "actor_class", "groups", "effective_access_summary", "lock_offboard_result", "lldap_identity_verified", "keycape_oidc_claims_verified", "no_secret_material_recorded" ] for f in required: assert f in tmpl assert tmpl["actor_class"] != "king credential" # per guard # cross-check lifecycle template has the preview guards flow = console.lifecycle_flow_template() assert "shows_effective_access_before_save" in flow assert "prevents_platform_root_grant" in flow def test_lifecycle_flow_template(): tmpl = console.lifecycle_flow_template() assert "flow_version" in tmpl assert "onboard_user_supported" in tmpl assert tmpl["onboard_user_supported"] is True def test_runbook_payloads_includes_dry_run(): # minimal data data = {"openbao_initialized": True} payloads = console.runbook_payloads(data) names = [p["name"] for p in payloads] assert "User lifecycle dry-run (T06)" in names dry = next(p for p in payloads if "dry-run" in p["name"].lower()) assert "NET-WP-0019" in dry.get("location", "") or "dry-run-nonroot-user.sh" in dry.get("location", "") def test_audit_core_posture_ready_with_bootstrap_risk(): data = { "audit_core_production_sink_ready": False, "audit_core_bootstrap_risk_accepted": True, "audit_core_risk_owner": "role:platform-custodian", "audit_core_risk_review_date": "2026-07-02", "audit_core_risk_note": "Temporary bootstrap exception" } assert console.audit_core_posture_ready(data) is True reason = console.audit_core_posture_reason(data) assert "Temporary bootstrap audit-retention risk exception" in reason def test_audit_core_posture_ready_false_without_fields(): data = {"audit_core_production_sink_ready": False} assert console.audit_core_posture_ready(data) is False def test_runbook_payloads_taints(): data = { "openbao_initialized": True, "openbao_trial_material_exposed": True, "openbao_compromise_response_complete": False, "openbao_unseal_keys_rotated": False } payloads = console.runbook_payloads(data) # should have taint entries for compromise etc. taint_names = [p.get("name") for p in payloads if p.get("state") == "tainted" or "taint" in str(p).lower()] assert len([p for p in payloads if "compromised" in p["name"].lower() or "Emergency" in p["name"]]) > 0 def test_console_has_dry_run_commands_in_parser(): # crude check that subparsers include the 0019 commands # by looking at source or assuming from dispatch source = open(TOOL_PATH).read() assert "onboarding-dry-run" in source assert "lifecycle-cleanup-dryrun-users" in source assert "onboarding-dry-run-claims" in source # Note: full CLI dispatch and web-ui harder to unit without mocks; covered by fixture/acceptance in T08 later. # Syntax for sh covered in Makefile target.