fix(recovery): fail closed dr escrow placeholders
Some checks failed
CD Pipeline / workflow-shape (push) Successful in 0s
CD Pipeline / cancel-stale-cd (push) Has been skipped
CD Pipeline / tests (push) Failing after 2m53s
CD Pipeline / build-and-deploy (push) Has been skipped
CD Pipeline / post-deploy-checks (push) Has been skipped

This commit is contained in:
Your Name
2026-06-29 14:05:44 +08:00
parent c98e6d5e53
commit b8f8f05c6e
5 changed files with 126 additions and 18 deletions

View File

@@ -41,11 +41,11 @@ def evidence_item(item_id: str) -> dict[str, Any]:
"last_reviewed_at",
"contains_secret_value=false",
],
"accepted_ref_examples": [
f"vault-item-id-for-{item_id}",
f"sealed-envelope-id-for-{item_id}",
f"recovery-checklist-id-for-{item_id}",
f"ticket-id-for-{item_id}",
"accepted_ref_classes": [
"password_manager_item_id",
"sealed_envelope_id",
"recovery_checklist_id",
"review_ticket_id",
],
"rejected_values": [
"passwords",
@@ -84,13 +84,13 @@ def owner_response_skeleton() -> dict[str, Any]:
"responses": [
{
"gate_id": GATE_ID,
"owner_role": "backup_dr_owner",
"owner_team": "platform_security",
"owner_role": "owner_role_here",
"owner_team": "owner_team_here",
"decision": "pending",
"decision_reason": "fill_only_after_all_redacted_refs_are_present",
"decision_reason": "decision_reason_here",
"affected_scope": "P0-005 DR credential escrow evidence",
"redacted_evidence_refs": ["<ONE_PARENT_TICKET_OR_REVIEW_REF>"],
"followup_owner": "backup_dr_owner",
"redacted_evidence_refs": ["redacted_evidence_ref_here"],
"followup_owner": "followup_owner_here",
"runtime_action_requested": False,
"runtime_action_authorized": False,
"host_write_requested": False,
@@ -102,10 +102,10 @@ def owner_response_skeleton() -> dict[str, Any]:
"escrow_items": [
{
"item_id": item_id,
"non_secret_evidence_ref": f"<NON_SECRET_REF_FOR_{item_id}>",
"recovery_owner": "backup_dr_owner",
"reviewer": "security_reviewer",
"last_reviewed_at": "YYYY-MM-DD",
"non_secret_evidence_ref": "non_secret_evidence_ref_here",
"recovery_owner": "owner_role_here",
"reviewer": "reviewer_here",
"last_reviewed_at": "pending",
"contains_secret_value": False,
}
for item_id in ITEMS

View File

@@ -172,7 +172,22 @@ def normalized(value: Any) -> str:
def is_placeholder(value: Any) -> bool:
return normalized(value).lower() in PLACEHOLDER_VALUES
text = normalized(value)
lower = text.lower()
if lower in PLACEHOLDER_VALUES:
return True
if re.fullmatch(r"<[^<>]+>", text):
return True
if lower in {"yyyy-mm-dd", "yyyy/mm/dd"}:
return True
return lower.startswith(
(
"vault-item-id-for-",
"sealed-envelope-id-for-",
"recovery-checklist-id-for-",
"ticket-id-for-",
)
)
def collect_strings(value: Any, path: str = "$") -> list[tuple[str, str]]:

View File

@@ -8,6 +8,7 @@ from pathlib import Path
ROOT = Path(__file__).resolve().parents[3]
SCRIPT = ROOT / "scripts" / "reboot-recovery" / "dr-escrow-evidence-checklist.py"
PREFLIGHT_SCRIPT = ROOT / "scripts" / "reboot-recovery" / "post-reboot-owner-response-preflight.py"
ITEMS = {
"restic_repository_password",
@@ -28,6 +29,28 @@ def load_checklist() -> dict:
return json.loads(result.stdout)
def run_preflight(tmp_path: Path, owner_packet: dict, response: dict) -> dict:
packet_path = tmp_path / "owner-packet.json"
response_path = tmp_path / "owner-response.json"
packet_path.write_text(json.dumps(owner_packet, indent=2) + "\n", encoding="utf-8")
response_path.write_text(json.dumps(response, indent=2) + "\n", encoding="utf-8")
result = subprocess.run(
[
sys.executable,
str(PREFLIGHT_SCRIPT),
"--owner-packet-file",
str(packet_path),
"--response-file",
str(response_path),
"--json",
],
text=True,
capture_output=True,
check=True,
)
return json.loads(result.stdout)
def test_checklist_is_single_p0_005_intake_packet() -> None:
payload = load_checklist()
@@ -73,3 +96,57 @@ def test_checklist_outputs_marker_dry_run_commands_only() -> None:
assert " --evidence-id <NON_SECRET_REF_FOR_" in command
assert "--token" not in command
assert "password=" not in command.lower()
def test_unfilled_skeleton_fails_closed_against_preflight(tmp_path: Path) -> None:
payload = load_checklist()
preflight = run_preflight(
tmp_path,
payload["owner_packet"],
payload["owner_response_skeleton"],
)
assert preflight["status"] == "blocked_waiting_owner_response_content"
assert preflight["owner_response_received_count"] == 0
assert preflight["owner_response_accepted_count"] == 0
assert preflight["runtime_action_authorized"] is False
assert preflight["secret_value_collection_allowed"] is False
blockers = "\n".join(preflight["blockers"])
assert "credential_escrow_evidence.owner_role_missing" in blockers
assert "credential_escrow_evidence.restic_repository_password.non_secret_evidence_ref_missing" in blockers
def test_angle_bracket_placeholder_refs_cannot_be_accepted(tmp_path: Path) -> None:
payload = load_checklist()
response = payload["owner_response_skeleton"]
item = response["responses"][0]
item.update(
{
"owner_role": "backup_dr_owner",
"owner_team": "platform_security",
"decision": "accepted",
"decision_reason": "reviewed redacted evidence refs only",
"redacted_evidence_refs": ["review-ticket-20260629"],
"followup_owner": "backup_dr_owner",
}
)
for escrow_item in item["escrow_items"]:
item_id = escrow_item["item_id"]
escrow_item.update(
{
"non_secret_evidence_ref": f"<NON_SECRET_REF_FOR_{item_id}>",
"recovery_owner": "backup_dr_owner",
"reviewer": "security_reviewer",
"last_reviewed_at": "2026-06-29",
}
)
preflight = run_preflight(tmp_path, payload["owner_packet"], response)
assert preflight["status"] == "blocked_waiting_owner_response_content"
assert preflight["owner_response_received_count"] == 0
assert preflight["owner_response_accepted_count"] == 0
blockers = "\n".join(preflight["blockers"])
for item_id in ITEMS:
assert f"credential_escrow_evidence.{item_id}.non_secret_evidence_ref_missing" in blockers