From c845963fa23b0cdc5103db5c5a04257fc9cab412 Mon Sep 17 00:00:00 2001 From: Your Name Date: Tue, 19 May 2026 23:49:26 +0800 Subject: [PATCH] feat(web): add IwoooS decision draft review --- apps/web/messages/en.json | 43 ++++++ apps/web/messages/zh-TW.json | 43 ++++++ apps/web/src/app/[locale]/iwooos/page.tsx | 65 ++++++++ docs/LOGBOOK.md | 14 ++ .../iwooos_posture_projection_v1.schema.json | 94 ++++++++++++ docs/security/IWOOOS-POSTURE-PROJECTION.md | 22 ++- .../security/SECURITY-MIRROR-STATUS-ROLLUP.md | 3 +- .../SECURITY-SUPPLY-CHAIN-PROGRESS.md | 4 +- .../iwooos-posture-projection.snapshot.json | 140 +++++++++++++++++- ...ecurity-mirror-status-rollup.snapshot.json | 10 ++ .../security-mirror-progress-guard.py | 100 +++++++++++++ 11 files changed, 532 insertions(+), 6 deletions(-) diff --git a/apps/web/messages/en.json b/apps/web/messages/en.json index 2895b6d0..14bd0e41 100644 --- a/apps/web/messages/en.json +++ b/apps/web/messages/en.json @@ -1801,6 +1801,49 @@ "metadata": "runtime gate pointer; active gates=0" } } + }, + "hostOwnerDecisionRecordDraftReview": { + "title": "Host Owner Decision Record Draft Review Checklist", + "subtitle": "Decision record draft packets still require read-only review. This only shows whether drafts have the metadata needed for human decision and does not create formal decision records.", + "checkLabel": "Draft review", + "guardLabel": "No upgrade", + "items": { + "scopeStatementComplete": { + "title": "Scope statement complete", + "body": "Confirm the scope draft includes host, network, service, exclusions, and observation intent so decision record scope is not ambiguous.", + "guard": "draft review only; record created=false" + }, + "scanModeStillNotApproval": { + "title": "Scan mode still not approval", + "body": "Confirm scan mode remains a candidate description and is not read as active scan or credentialed scan authorization.", + "guard": "scan authorized=false" + }, + "credentialBoundaryMetadataOnly": { + "title": "Credential boundary metadata only", + "body": "Confirm credential boundary stays metadata-only and does not request or store sensitive material.", + "guard": "secret collection=false" + }, + "maintenanceConstraintsReadable": { + "title": "Maintenance constraints readable", + "body": "Confirm maintenance window, constraints, and impact boundary are readable without becoming host update approval.", + "guard": "host update=false" + }, + "rollbackOwnerReadable": { + "title": "Rollback owner readable", + "body": "Confirm rollback owner, recovery path, and human contact are readable while no approval record is created.", + "guard": "approval record=false" + }, + "validationMetricsLinked": { + "title": "Validation metrics linked", + "body": "Confirm post-check metrics, baseline, and evidence pointer are linked to the draft for later human review.", + "guard": "accepted=0" + }, + "runtimeGateStillClosed": { + "title": "Runtime gate still closed", + "body": "Confirm decision record draft review does not open runtime gates. Later execution still requires a separate gate.", + "guard": "active runtime gates=0; action buttons=false" + } + } } }, "tickets": { diff --git a/apps/web/messages/zh-TW.json b/apps/web/messages/zh-TW.json index 2b2406cd..68df91cd 100644 --- a/apps/web/messages/zh-TW.json +++ b/apps/web/messages/zh-TW.json @@ -1802,6 +1802,49 @@ "metadata": "runtime gate pointer;active gates=0" } } + }, + "hostOwnerDecisionRecordDraftReview": { + "title": "主機 Owner Decision Record Draft Review Checklist", + "subtitle": "Decision record 草稿包後仍需只讀核對。這裡只顯示草稿是否具備人工決策所需 metadata,不會建立正式 decision record。", + "checkLabel": "Draft review", + "guardLabel": "不可升級", + "items": { + "scopeStatementComplete": { + "title": "Scope statement complete", + "body": "確認 scope 草稿已包含主機、網段、服務、排除範圍與觀察目的,避免決策紀錄範圍不清。", + "guard": "只核對草稿;record created=false" + }, + "scanModeStillNotApproval": { + "title": "Scan mode still not approval", + "body": "確認 scan mode 仍只是候選描述,不被解讀成 active scan 或 credentialed scan 授權。", + "guard": "scan authorized=false" + }, + "credentialBoundaryMetadataOnly": { + "title": "Credential boundary metadata only", + "body": "確認 credential boundary 仍維持 metadata-only,沒有要求或保存敏感素材。", + "guard": "secret collection=false" + }, + "maintenanceConstraintsReadable": { + "title": "Maintenance constraints readable", + "body": "確認維護窗口、限制條件與不可影響範圍可讀,但不代表可以更新或調校主機。", + "guard": "host update=false" + }, + "rollbackOwnerReadable": { + "title": "Rollback owner readable", + "body": "確認 rollback owner、復原路徑與人工聯絡點可讀,但尚未建立 approval record。", + "guard": "approval record=false" + }, + "validationMetricsLinked": { + "title": "Validation metrics linked", + "body": "確認 post-check metrics、baseline 與 evidence pointer 已連到草稿,供後續人審使用。", + "guard": "accepted=0" + }, + "runtimeGateStillClosed": { + "title": "Runtime gate still closed", + "body": "確認 decision record 草稿審查不會開 runtime gate,後續執行仍需獨立 gate。", + "guard": "active runtime gates=0;action buttons=false" + } + } } }, "tickets": { diff --git a/apps/web/src/app/[locale]/iwooos/page.tsx b/apps/web/src/app/[locale]/iwooos/page.tsx index 4edbfbd0..8ef52ede 100644 --- a/apps/web/src/app/[locale]/iwooos/page.tsx +++ b/apps/web/src/app/[locale]/iwooos/page.tsx @@ -165,6 +165,13 @@ type HostOwnerDecisionRecordDraftPacket = { tone: 'steady' | 'warn' | 'locked' } +type HostOwnerDecisionRecordDraftReviewItem = { + key: string + check: string + icon: typeof ShieldCheck + tone: 'steady' | 'warn' | 'locked' +} + const postureMetrics: PostureMetric[] = [ { key: 'overall', value: '58%', tone: 'warn' }, { key: 'framework', value: '80-85%', tone: 'steady' }, @@ -386,6 +393,16 @@ const hostOwnerDecisionRecordDraftPackets: HostOwnerDecisionRecordDraftPacket[] { key: 'runtimeGateDraft', packet: 'DR7', icon: ShieldCheck, tone: 'locked' }, ] +const hostOwnerDecisionRecordDraftReviewItems: HostOwnerDecisionRecordDraftReviewItem[] = [ + { key: 'scopeStatementComplete', check: 'RV1', icon: Radar, tone: 'warn' }, + { key: 'scanModeStillNotApproval', check: 'RV2', icon: Activity, tone: 'locked' }, + { key: 'credentialBoundaryMetadataOnly', check: 'RV3', icon: Lock, tone: 'locked' }, + { key: 'maintenanceConstraintsReadable', check: 'RV4', icon: Clock3, tone: 'warn' }, + { key: 'rollbackOwnerReadable', check: 'RV5', icon: FileWarning, tone: 'warn' }, + { key: 'validationMetricsLinked', check: 'RV6', icon: CheckCircle2, tone: 'warn' }, + { key: 'runtimeGateStillClosed', check: 'RV7', icon: ShieldCheck, tone: 'locked' }, +] + const evidenceItems = [ 'iwooos-posture-projection.snapshot.json', 'security-rollout-policy.snapshot.json', @@ -996,6 +1013,34 @@ function HostOwnerDecisionRecordDraftCard({ item }: { item: HostOwnerDecisionRec ) } +function HostOwnerDecisionRecordDraftReviewCard({ item }: { item: HostOwnerDecisionRecordDraftReviewItem }) { + const t = useTranslations('iwooos.hostOwnerDecisionRecordDraftReview') + const Icon = item.icon + return ( +
+
+
+ + {t('checkLabel')} +
+ {item.check} +
+

+ {t(`items.${item.key}.title` as never)} +

+

+ {t(`items.${item.key}.body` as never)} +

+
+
{t('guardLabel')}
+
+ {t(`items.${item.key}.guard` as never)} +
+
+
+ ) +} + export default function IwoooSPage({ params }: { params: { locale: string } }) { const t = useTranslations('iwooos') @@ -1328,6 +1373,26 @@ export default function IwoooSPage({ params }: { params: { locale: string } }) { +
+
+

{t('hostOwnerDecisionRecordDraftReview.title')}

+

+ {t('hostOwnerDecisionRecordDraftReview.subtitle')} +

+
+
+ {hostOwnerDecisionRecordDraftReviewItems.map(item => ( + + ))} +
+
+
None: "s2_24_iwooos_host_owner_decision_review_checklist", "s2_25_iwooos_host_owner_decision_review_outcome_lanes", "s2_26_iwooos_host_owner_decision_record_draft_packets", + "s2_27_iwooos_host_owner_decision_record_draft_review_checklist", ] assert_equal( "progress_delta_ledger.delta_ids", @@ -458,6 +459,15 @@ def validate(root: Path) -> None: "host_decision_record_validation_metrics_draft_packet", "host_decision_record_runtime_gate_draft_packet", ] + expected_iwooos_host_owner_decision_record_draft_review_checklist_item_ids = [ + "host_decision_record_scope_statement_review_check", + "host_decision_record_scan_mode_review_check", + "host_decision_record_credential_boundary_review_check", + "host_decision_record_maintenance_constraints_review_check", + "host_decision_record_rollback_owner_review_check", + "host_decision_record_validation_metrics_review_check", + "host_decision_record_runtime_gate_review_check", + ] assert_equal( "iwooos_projection.summary.frontend_surface_coverage_group_count", iwooos_projection["summary"]["frontend_surface_coverage_group_count"], @@ -543,6 +553,11 @@ def validate(root: Path) -> None: iwooos_projection["summary"]["host_owner_decision_record_draft_packet_count"], len(expected_iwooos_host_owner_decision_record_draft_packet_ids), ) + assert_equal( + "iwooos_projection.summary.host_owner_decision_record_draft_review_checklist_item_count", + iwooos_projection["summary"]["host_owner_decision_record_draft_review_checklist_item_count"], + len(expected_iwooos_host_owner_decision_record_draft_review_checklist_item_ids), + ) iwooos_progress = iwooos_projection["progress"] assert_equal("iwooos_projection.progress.overall_percent", iwooos_progress["overall_percent"], progress["overall_percent"]) assert_equal( @@ -1562,6 +1577,86 @@ def validate(root: Path) -> None: f"iwooos_projection.host_owner_decision_record_draft_packets.{item['packet_id']}.not_authorization", item["not_authorization"], ) + iwooos_host_owner_decision_record_draft_review_checklist = iwooos_projection[ + "host_owner_decision_record_draft_review_checklist_items" + ] + assert_equal( + "iwooos_projection.host_owner_decision_record_draft_review_checklist_items.ids", + [item["check_id"] for item in iwooos_host_owner_decision_record_draft_review_checklist], + expected_iwooos_host_owner_decision_record_draft_review_checklist_item_ids, + ) + assert_equal( + "iwooos_projection.host_owner_decision_record_draft_review_checklist_items.display_order", + [item["display_order"] for item in iwooos_host_owner_decision_record_draft_review_checklist], + list(range(1, len(expected_iwooos_host_owner_decision_record_draft_review_checklist_item_ids) + 1)), + ) + expected_iwooos_host_owner_decision_record_draft_review_conditions = [ + "scope_statement_metadata_complete", + "scan_mode_not_authorization_confirmed", + "credential_boundary_metadata_only_confirmed", + "maintenance_constraints_no_change_confirmed", + "rollback_owner_recovery_pointer_readable", + "validation_metrics_baseline_linked", + "runtime_gate_separate_and_closed", + ] + assert_equal( + "iwooos_projection.host_owner_decision_record_draft_review_checklist_items.review_conditions", + [item["review_condition"] for item in iwooos_host_owner_decision_record_draft_review_checklist], + expected_iwooos_host_owner_decision_record_draft_review_conditions, + ) + for item in iwooos_host_owner_decision_record_draft_review_checklist: + assert_equal( + f"iwooos_projection.host_owner_decision_record_draft_review_checklist_items.{item['check_id']}.display_mode", + item["display_mode"], + "owner_decision_record_draft_review_checklist_only", + ) + assert_equal( + f"iwooos_projection.host_owner_decision_record_draft_review_checklist_items.{item['check_id']}.decision_record_review_passed_count", + item["decision_record_review_passed_count"], + 0, + ) + assert_false( + f"iwooos_projection.host_owner_decision_record_draft_review_checklist_items.{item['check_id']}.decision_record_created", + item["decision_record_created"], + ) + assert_equal( + f"iwooos_projection.host_owner_decision_record_draft_review_checklist_items.{item['check_id']}.owner_decision_received_count", + item["owner_decision_received_count"], + 0, + ) + assert_equal( + f"iwooos_projection.host_owner_decision_record_draft_review_checklist_items.{item['check_id']}.owner_decision_accepted_count", + item["owner_decision_accepted_count"], + 0, + ) + assert_false( + f"iwooos_projection.host_owner_decision_record_draft_review_checklist_items.{item['check_id']}.owner_approval_record_created", + item["owner_approval_record_created"], + ) + assert_false( + f"iwooos_projection.host_owner_decision_record_draft_review_checklist_items.{item['check_id']}.runtime_gate_opened", + item["runtime_gate_opened"], + ) + assert_false( + f"iwooos_projection.host_owner_decision_record_draft_review_checklist_items.{item['check_id']}.raw_payload_allowed", + item["raw_payload_allowed"], + ) + assert_false( + f"iwooos_projection.host_owner_decision_record_draft_review_checklist_items.{item['check_id']}.secret_value_collection_allowed", + item["secret_value_collection_allowed"], + ) + assert_false( + f"iwooos_projection.host_owner_decision_record_draft_review_checklist_items.{item['check_id']}.runtime_execution_authorized", + item["runtime_execution_authorized"], + ) + assert_false( + f"iwooos_projection.host_owner_decision_record_draft_review_checklist_items.{item['check_id']}.action_buttons_allowed", + item["action_buttons_allowed"], + ) + assert_true( + f"iwooos_projection.host_owner_decision_record_draft_review_checklist_items.{item['check_id']}.not_authorization", + item["not_authorization"], + ) assert_equal( "iwooos_projection.non_blocking_lane_ids", iwooos_projection["non_blocking_lane_ids"], @@ -1597,6 +1692,7 @@ def validate(root: Path) -> None: "display_host_owner_decision_review_checklist", "display_host_owner_decision_review_outcome_lanes", "display_host_owner_decision_record_draft_packets", + "display_host_owner_decision_record_draft_review_checklist", "display_evidence_refs", "display_forbidden_actions", ]: @@ -1650,6 +1746,10 @@ def validate(root: Path) -> None: "create_host_owner_decision_record_from_draft", "mark_host_owner_decision_record_created", "open_runtime_gate_from_owner_decision_record_draft", + "treat_host_owner_decision_record_draft_review_as_approval", + "mark_host_owner_decision_record_draft_review_passed", + "create_host_owner_decision_record_from_draft_review", + "open_runtime_gate_from_owner_decision_record_draft_review", "apply_runtime_blocking_control", "switch_github_primary", "production_deploy",