generated from coulomb/repo-seed
T2: greenfield live proof against a fresh uninitialized OpenBao 2.5.5 — caught and fixed 'bao operator unseal -' not reading stdin (now 'bao write sys/unseal key=-'); init and reseal-replay paths proven. T3: attended-ceremony selectable — runbook, non-secret ceremony-record template + validator, and a lab/production deployment profile that blocks sops-held-automation in console selection, gates, and the init script. T4: console gate + evidence flags for auto-unseal-transit (Helm seal stanza prepared in railiance-platform). Also: SCOPE.md refreshed to current repo state; adhoc fix for the broken check-secrets Make target (unescaped $). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
186 lines
7.4 KiB
Python
186 lines
7.4 KiB
Python
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.
|