diff --git a/apps/web/messages/en.json b/apps/web/messages/en.json index 2b9fd3cf5..8d40a6c45 100644 --- a/apps/web/messages/en.json +++ b/apps/web/messages/en.json @@ -1672,6 +1672,49 @@ "decision": "display gate candidate only; action buttons=false" } } + }, + "hostOwnerDecisionReviewChecklist": { + "title": "Host Owner Decision Review Checklist", + "subtitle": "After owner decision candidate packets, every item still requires human review. This only displays review checks and does not create decision records, mark approval, or open runtime gates.", + "checkLabel": "Owner review", + "guardLabel": "Safety boundary", + "items": { + "scopeBoundaryReadable": { + "title": "Scope boundary readable", + "body": "Confirm the owner can read hosts, networks, services, exclusions, and observation purpose without exceeding the original scope.", + "guard": "scope review only; owner decision received=0" + }, + "scanModeNotAuthorization": { + "title": "Scan mode not authorization", + "body": "Confirm observe-only, future active scan, and credentialed scan are mode descriptions only, not scan authorization.", + "guard": "scan authorized=false" + }, + "credentialBoundaryMetadataOnly": { + "title": "Credential boundary metadata only", + "body": "Confirm credential handling keeps only metadata, owner, and retention boundary without requesting sensitive material.", + "guard": "secret collection=false" + }, + "maintenanceWindowNotChange": { + "title": "Maintenance window not change", + "body": "Confirm the maintenance window is only a future candidate condition and does not allow Kali updates or host tuning.", + "guard": "host update=false" + }, + "rollbackOwnerReadable": { + "title": "Rollback owner readable", + "body": "Confirm rollback owner, recovery route, and human contact point are readable, but no change is approved.", + "guard": "approval record=false" + }, + "validationMetricsPredefined": { + "title": "Validation metrics predefined", + "body": "Confirm post-check metrics, baseline, and evidence pointers are defined first for later gate review.", + "guard": "runtime gate opened=false" + }, + "runtimeGateStillSeparate": { + "title": "Runtime gate still separate", + "body": "Confirm owner decision checklist cannot execute any later host action and still needs a separate runtime gate.", + "guard": "action buttons=false" + } + } } }, "tickets": { diff --git a/apps/web/messages/zh-TW.json b/apps/web/messages/zh-TW.json index 4e1f8daf8..9cfa62ae8 100644 --- a/apps/web/messages/zh-TW.json +++ b/apps/web/messages/zh-TW.json @@ -1673,6 +1673,49 @@ "decision": "只顯示 gate 候選;action buttons=false" } } + }, + "hostOwnerDecisionReviewChecklist": { + "title": "主機 Owner Decision Review Checklist", + "subtitle": "Owner decision candidate packets 之後仍需人工逐項核對。這裡只顯示核對項,不建立決策紀錄、不標示 approved、不開 runtime gate。", + "checkLabel": "Owner 核對", + "guardLabel": "安全邊界", + "items": { + "scopeBoundaryReadable": { + "title": "Scope boundary readable", + "body": "確認 owner 能讀懂主機、網段、服務、排除範圍與觀察目的,且沒有超出原始 scope。", + "guard": "只核對 scope;owner decision received=0" + }, + "scanModeNotAuthorization": { + "title": "Scan mode not authorization", + "body": "確認 observe-only、future active scan、credentialed scan 都只是選項說明,不是掃描授權。", + "guard": "scan authorized=false" + }, + "credentialBoundaryMetadataOnly": { + "title": "Credential boundary metadata only", + "body": "確認 credential handling 只保留 metadata、owner 與保存邊界,不要求敏感素材。", + "guard": "secret collection=false" + }, + "maintenanceWindowNotChange": { + "title": "Maintenance window not change", + "body": "確認維護窗口只是未來候選條件,不代表可以更新 Kali 或調校主機。", + "guard": "host update=false" + }, + "rollbackOwnerReadable": { + "title": "Rollback owner readable", + "body": "確認 rollback owner、復原路徑與人工聯絡點可讀,但尚未批准任何 change。", + "guard": "approval record=false" + }, + "validationMetricsPredefined": { + "title": "Validation metrics predefined", + "body": "確認 post-check metrics、baseline 與 evidence pointer 已先定義,供未來 gate 判讀。", + "guard": "runtime gate opened=false" + }, + "runtimeGateStillSeparate": { + "title": "Runtime gate still separate", + "body": "確認 owner decision checklist 不能直接執行任何後續主機動作,仍需獨立 runtime gate。", + "guard": "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 63fd3ab48..ce31726d2 100644 --- a/apps/web/src/app/[locale]/iwooos/page.tsx +++ b/apps/web/src/app/[locale]/iwooos/page.tsx @@ -144,6 +144,13 @@ type HostOwnerDecisionCandidatePacket = { tone: 'steady' | 'warn' | 'locked' } +type HostOwnerDecisionReviewChecklistItem = { + 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' }, @@ -335,6 +342,16 @@ const hostOwnerDecisionCandidatePackets: HostOwnerDecisionCandidatePacket[] = [ { key: 'runtimeGateCandidate', packet: 'D7', icon: ShieldCheck, tone: 'locked' }, ] +const hostOwnerDecisionReviewChecklistItems: HostOwnerDecisionReviewChecklistItem[] = [ + { key: 'scopeBoundaryReadable', check: 'A1', icon: Radar, tone: 'warn' }, + { key: 'scanModeNotAuthorization', check: 'A2', icon: Activity, tone: 'locked' }, + { key: 'credentialBoundaryMetadataOnly', check: 'A3', icon: Lock, tone: 'locked' }, + { key: 'maintenanceWindowNotChange', check: 'A4', icon: Clock3, tone: 'warn' }, + { key: 'rollbackOwnerReadable', check: 'A5', icon: FileWarning, tone: 'warn' }, + { key: 'validationMetricsPredefined', check: 'A6', icon: CheckCircle2, tone: 'warn' }, + { key: 'runtimeGateStillSeparate', check: 'A7', icon: ShieldCheck, tone: 'locked' }, +] + const evidenceItems = [ 'iwooos-posture-projection.snapshot.json', 'security-rollout-policy.snapshot.json', @@ -861,6 +878,34 @@ function HostOwnerDecisionCandidateCard({ item }: { item: HostOwnerDecisionCandi ) } +function HostOwnerDecisionReviewChecklistCard({ item }: { item: HostOwnerDecisionReviewChecklistItem }) { + const t = useTranslations('iwooos.hostOwnerDecisionReviewChecklist') + 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') @@ -1133,6 +1178,26 @@ export default function IwoooSPage({ params }: { params: { locale: string } }) { +
+
+

{t('hostOwnerDecisionReviewChecklist.title')}

+

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

+
+
+ {hostOwnerDecisionReviewChecklistItems.map(item => ( + + ))} +
+
+
None: "s2_21_iwooos_host_evidence_reviewer_checklist", "s2_22_iwooos_host_evidence_reviewer_outcome_lanes", "s2_23_iwooos_host_owner_decision_candidate_packets", + "s2_24_iwooos_host_owner_decision_review_checklist", ] assert_equal( "progress_delta_ledger.delta_ids", @@ -428,6 +429,15 @@ def validate(root: Path) -> None: "host_validation_metrics_candidate_packet", "host_runtime_gate_candidate_packet", ] + expected_iwooos_host_owner_decision_review_checklist_item_ids = [ + "host_scope_boundary_readable_review_check", + "host_scan_mode_not_authorization_review_check", + "host_credential_boundary_metadata_only_review_check", + "host_maintenance_window_not_change_review_check", + "host_rollback_owner_readable_review_check", + "host_validation_metrics_predefined_review_check", + "host_runtime_gate_still_separate_review_check", + ] assert_equal( "iwooos_projection.summary.frontend_surface_coverage_group_count", iwooos_projection["summary"]["frontend_surface_coverage_group_count"], @@ -498,6 +508,11 @@ def validate(root: Path) -> None: iwooos_projection["summary"]["host_owner_decision_candidate_packet_count"], len(expected_iwooos_host_owner_decision_candidate_packet_ids), ) + assert_equal( + "iwooos_projection.summary.host_owner_decision_review_checklist_item_count", + iwooos_projection["summary"]["host_owner_decision_review_checklist_item_count"], + len(expected_iwooos_host_owner_decision_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( @@ -1299,6 +1314,75 @@ def validate(root: Path) -> None: f"iwooos_projection.host_owner_decision_candidate_packets.{item['packet_id']}.not_authorization", item["not_authorization"], ) + iwooos_host_owner_decision_review_checklist = iwooos_projection["host_owner_decision_review_checklist_items"] + assert_equal( + "iwooos_projection.host_owner_decision_review_checklist_items.ids", + [item["check_id"] for item in iwooos_host_owner_decision_review_checklist], + expected_iwooos_host_owner_decision_review_checklist_item_ids, + ) + assert_equal( + "iwooos_projection.host_owner_decision_review_checklist_items.display_order", + [item["display_order"] for item in iwooos_host_owner_decision_review_checklist], + list(range(1, len(expected_iwooos_host_owner_decision_review_checklist_item_ids) + 1)), + ) + expected_iwooos_host_owner_decision_review_guard_conditions = [ + "scope_review_only_owner_decision_received_zero", + "active_scan_and_credentialed_scan_authorized_false", + "secret_value_collection_allowed_false", + "host_update_authorized_false", + "owner_approval_record_created_false", + "runtime_gate_opened_false", + "action_buttons_allowed_false_runtime_gate_separate", + ] + assert_equal( + "iwooos_projection.host_owner_decision_review_checklist_items.guard_conditions", + [item["guard_condition"] for item in iwooos_host_owner_decision_review_checklist], + expected_iwooos_host_owner_decision_review_guard_conditions, + ) + for item in iwooos_host_owner_decision_review_checklist: + assert_equal( + f"iwooos_projection.host_owner_decision_review_checklist_items.{item['check_id']}.display_mode", + item["display_mode"], + "owner_decision_review_checklist_only", + ) + assert_equal( + f"iwooos_projection.host_owner_decision_review_checklist_items.{item['check_id']}.owner_decision_received_count", + item["owner_decision_received_count"], + 0, + ) + assert_equal( + f"iwooos_projection.host_owner_decision_review_checklist_items.{item['check_id']}.owner_decision_accepted_count", + item["owner_decision_accepted_count"], + 0, + ) + assert_false( + f"iwooos_projection.host_owner_decision_review_checklist_items.{item['check_id']}.owner_approval_record_created", + item["owner_approval_record_created"], + ) + assert_false( + f"iwooos_projection.host_owner_decision_review_checklist_items.{item['check_id']}.runtime_gate_opened", + item["runtime_gate_opened"], + ) + assert_false( + f"iwooos_projection.host_owner_decision_review_checklist_items.{item['check_id']}.raw_payload_allowed", + item["raw_payload_allowed"], + ) + assert_false( + f"iwooos_projection.host_owner_decision_review_checklist_items.{item['check_id']}.secret_value_collection_allowed", + item["secret_value_collection_allowed"], + ) + assert_false( + f"iwooos_projection.host_owner_decision_review_checklist_items.{item['check_id']}.runtime_execution_authorized", + item["runtime_execution_authorized"], + ) + assert_false( + f"iwooos_projection.host_owner_decision_review_checklist_items.{item['check_id']}.action_buttons_allowed", + item["action_buttons_allowed"], + ) + assert_true( + f"iwooos_projection.host_owner_decision_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"], @@ -1331,6 +1415,7 @@ def validate(root: Path) -> None: "display_host_evidence_reviewer_checklist", "display_host_evidence_reviewer_outcome_lanes", "display_host_owner_decision_candidate_packets", + "display_host_owner_decision_review_checklist", "display_evidence_refs", "display_forbidden_actions", ]: @@ -1375,6 +1460,9 @@ def validate(root: Path) -> None: "treat_host_owner_decision_candidate_as_approval", "mark_host_owner_decision_approved", "open_runtime_gate_from_owner_decision_candidate", + "treat_host_owner_decision_review_check_as_approval", + "mark_host_owner_decision_review_passed", + "open_runtime_gate_from_owner_decision_review_check", "apply_runtime_blocking_control", "switch_github_primary", "production_deploy",