diff --git a/docs/LOGBOOK.md b/docs/LOGBOOK.md index 3d4b8ecd..4deed359 100644 --- a/docs/LOGBOOK.md +++ b/docs/LOGBOOK.md @@ -49378,3 +49378,18 @@ production browser smoke: **未做**: - 沒有讀 secret、token、`.env`、raw sessions / SQLite / auth;沒有寫 credential marker;沒有 host / Docker / Nginx / firewall / K3s / DB 操作;沒有使用 GitHub。 + +## 2026-06-29 — 14:04 P0-005 placeholder preflight guard hardening + +**完成內容**: +- 修正 `scripts/reboot-recovery/dr-escrow-evidence-checklist.py`,owner response skeleton 改用既有 preflight 可辨識的 fail-closed placeholder,不再輸出可能被誤當真實 ref 的 ``。 +- 強化 `scripts/reboot-recovery/post-reboot-owner-response-preflight.py`:拒收 `<...>`、`YYYY-MM-DD` 與 fake example ref prefix,避免 placeholder 被改成 `accepted` 後誤過。 +- 補測 `scripts/reboot-recovery/tests/test_dr_escrow_evidence_checklist.py`,覆蓋未填 skeleton fail-closed 與 angle-bracket placeholder ref 不可 accepted。 + +**本地驗證結果**: +- `python3.11 -m py_compile scripts/reboot-recovery/dr-escrow-evidence-checklist.py scripts/reboot-recovery/post-reboot-owner-response-preflight.py`:通過。 +- `python3.11 -m pytest scripts/reboot-recovery/tests/test_dr_escrow_evidence_checklist.py scripts/reboot-recovery/tests/test_post_reboot_owner_response_template.py -q`:`7 passed`。 +- `python3 scripts/reboot-recovery/dr-escrow-evidence-checklist.py --output /tmp/awoooi-dr-escrow-evidence-checklist-20260629-fixed.json` + `jq empty`:通過。 + +**仍維持**: +- 未寫 credential marker、未讀 secret / token / `.env` / raw sessions / SQLite / auth,未做 host / Docker / Nginx / firewall / K3s / DB 操作,未使用 GitHub。 diff --git a/docs/operations/awoooi-priority-work-order-readback.snapshot.json b/docs/operations/awoooi-priority-work-order-readback.snapshot.json index cda956f0..f91c4c91 100644 --- a/docs/operations/awoooi-priority-work-order-readback.snapshot.json +++ b/docs/operations/awoooi-priority-work-order-readback.snapshot.json @@ -1,6 +1,6 @@ { "schema_version": "awoooi_priority_work_order_readback_v1", - "generated_at": "2026-06-29T14:00:19+08:00", + "generated_at": "2026-06-29T14:05:53+08:00", "status": "p0_005_dr_escrow_checklist_ready_waiting_redacted_refs", "source_refs": { "global_scorecard": "~/.codex/product-runtime-governance-completion-scorecard.snapshot.json", @@ -13,7 +13,7 @@ "dr_escrow_evidence_checklist_generator": "scripts/reboot-recovery/dr-escrow-evidence-checklist.py" }, "current_head": { - "gitea_main_sha": "bd55386e6edc46ce0b188011b171191b7773c5ba", + "gitea_main_sha": "c98e6d5e5309dad9f0be4399a3d2e1fe2ec669ca", "latest_successful_deploy_marker": "9362588ce chore(cd): deploy a423301 [skip ci]", "latest_successful_deployed_source_sha": "a4233017ad5fd03977233f3db6a4bb45d71507ed", "latest_source_readiness_commit_sha": "0c8d4e88c39157b92322fa41a92e6b15c317ac49", @@ -118,7 +118,8 @@ "credential_marker_write_authorized_count": 0, "secret_value_collection_allowed": false, "checklist_generator_present": true, - "checklist_schema_version": "awoooi_dr_escrow_evidence_checklist_v1" + "checklist_schema_version": "awoooi_dr_escrow_evidence_checklist_v1", + "placeholder_preflight_guard_present": true }, "missing_items": [ "restic_repository_password", diff --git a/scripts/reboot-recovery/dr-escrow-evidence-checklist.py b/scripts/reboot-recovery/dr-escrow-evidence-checklist.py index e70e8842..b2d887d7 100644 --- a/scripts/reboot-recovery/dr-escrow-evidence-checklist.py +++ b/scripts/reboot-recovery/dr-escrow-evidence-checklist.py @@ -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": [""], - "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"", - "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 diff --git a/scripts/reboot-recovery/post-reboot-owner-response-preflight.py b/scripts/reboot-recovery/post-reboot-owner-response-preflight.py index bc858d38..bb100c6d 100755 --- a/scripts/reboot-recovery/post-reboot-owner-response-preflight.py +++ b/scripts/reboot-recovery/post-reboot-owner-response-preflight.py @@ -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]]: diff --git a/scripts/reboot-recovery/tests/test_dr_escrow_evidence_checklist.py b/scripts/reboot-recovery/tests/test_dr_escrow_evidence_checklist.py index 76f9735e..34b0b3ea 100644 --- a/scripts/reboot-recovery/tests/test_dr_escrow_evidence_checklist.py +++ b/scripts/reboot-recovery/tests/test_dr_escrow_evidence_checklist.py @@ -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 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"", + "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