From 33e793bb765e97c3fd8e9aa3dcec82afc677abbc Mon Sep 17 00:00:00 2001 From: Your Name Date: Tue, 19 May 2026 21:46:44 +0800 Subject: [PATCH] feat(web): add IwoooS host collection order --- 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 | 71 +++++++++++ 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 | 115 +++++++++++++++++- ...ecurity-mirror-status-rollup.snapshot.json | 12 ++ .../security-mirror-progress-guard.py | 85 +++++++++++++ 11 files changed, 471 insertions(+), 6 deletions(-) diff --git a/apps/web/messages/en.json b/apps/web/messages/en.json index 840bc176..876c2d60 100644 --- a/apps/web/messages/en.json +++ b/apps/web/messages/en.json @@ -1361,6 +1361,49 @@ } } }, + "hostEvidenceCollection": { + "title": "Host Evidence Collection Order", + "subtitle": "Orders the seven host evidence items into a recommended collection sequence. Each step only names the next reviewable item and does not change received / accepted from 0.", + "stepLabel": "Collection step", + "dependencyLabel": "Dependency", + "items": { + "scopeFirst": { + "title": "Define scope boundary first", + "body": "Confirm allowed targets, exclusions, depth, and rate limits first. No scope means no scan.", + "dependency": "none; this is the first host collection step" + }, + "ownerSecond": { + "title": "Collect owner decision second", + "body": "Confirm who approves, the approved range, and the decision record; queue state cannot replace human control.", + "dependency": "requires readable scope boundary" + }, + "credentialThird": { + "title": "Isolate credential handling", + "body": "If future scans need credentials, define credential source, storage boundary, redaction, and rejection first.", + "dependency": "requires owner decision; plaintext credential collection remains forbidden" + }, + "maintenanceFourth": { + "title": "Schedule maintenance window", + "body": "Before updates, tuning, or SSH changes, confirm the window, impact scope, and notification.", + "dependency": "requires owner decision and change scope" + }, + "rollbackFifth": { + "title": "Add rollback plan", + "body": "Every host action needs recovery for packages, settings, services, and toolchain versions.", + "dependency": "requires maintenance window and change list" + }, + "validationSixth": { + "title": "Define validation metrics", + "body": "Define post-check metrics and failure handling lanes before execution is discussed.", + "dependency": "requires rollback plan" + }, + "redactedSeventh": { + "title": "Collect redacted ingestion last", + "body": "Findings / scan results enter mirror only as redacted summaries, never as raw payload.", + "dependency": "requires validation metrics; payloads_ingested=false" + } + } + }, "nextGate": { "title": "Next High-level Gate", "body": "S4.9 Gitea owner attestation response is the recommended next owner evidence. Headline progress should only increase after owner responses, redacted payload ingestion, active runtime gates, or GitHub primary readiness actually change." diff --git a/apps/web/messages/zh-TW.json b/apps/web/messages/zh-TW.json index e8c6bc63..fa56a90f 100644 --- a/apps/web/messages/zh-TW.json +++ b/apps/web/messages/zh-TW.json @@ -1362,6 +1362,49 @@ } } }, + "hostEvidenceCollection": { + "title": "主機 Evidence 收件順序", + "subtitle": "把七個主機 evidence 排成建議收件順序。每一步都只代表下一個可審項目,不會把 received / accepted 從 0 改掉。", + "stepLabel": "收件步驟", + "dependencyLabel": "前置依賴", + "items": { + "scopeFirst": { + "title": "先定義 scope boundary", + "body": "先確認允許目標、排除範圍、深度與速率。沒有 scope,不進 scan。", + "dependency": "無;這是主機收件第一步" + }, + "ownerSecond": { + "title": "再收 owner decision", + "body": "確認誰批准、批准範圍與決策紀錄,不用 queue 狀態替代人控決策。", + "dependency": "需要 scope boundary 可讀" + }, + "credentialThird": { + "title": "隔離 credential handling", + "body": "若未來要帶憑證掃描,先定義憑證來源、保存邊界、遮蔽與拒收。", + "dependency": "需要 owner decision;仍禁止收集憑證明文" + }, + "maintenanceFourth": { + "title": "安排 maintenance window", + "body": "更新、調校或 SSH 變更前先確認窗口、影響範圍與通知。", + "dependency": "需要 owner decision 與變更範圍" + }, + "rollbackFifth": { + "title": "補 rollback plan", + "body": "每個主機動作都要能回復套件、設定、服務與工具鏈版本。", + "dependency": "需要 maintenance window 與變更清單" + }, + "validationSixth": { + "title": "定義 validation metrics", + "body": "先定義 post-check 指標與失敗處理 lane,再談執行。", + "dependency": "需要 rollback plan" + }, + "redactedSeventh": { + "title": "最後才收 redacted ingestion", + "body": "finding / scan result 只用脫敏摘要進 mirror,不吃 raw payload。", + "dependency": "需要 validation metrics;payloads_ingested=false" + } + } + }, "nextGate": { "title": "下一個高層 Gate", "body": "S4.9 Gitea owner attestation response 是目前建議先收的 owner evidence。任何 headline 提升都要等 owner response、redacted payload ingestion、active runtime gate 或 GitHub primary readiness 有真實變化。" diff --git a/apps/web/src/app/[locale]/iwooos/page.tsx b/apps/web/src/app/[locale]/iwooos/page.tsx index 34f75785..f2bf5657 100644 --- a/apps/web/src/app/[locale]/iwooos/page.tsx +++ b/apps/web/src/app/[locale]/iwooos/page.tsx @@ -95,6 +95,13 @@ type HostEvidenceReadinessItem = { tone: 'steady' | 'warn' | 'locked' } +type HostEvidenceCollectionStep = { + key: string + step: string + icon: typeof ShieldCheck + tone: 'steady' | 'warn' | 'locked' +} + const postureMetrics: PostureMetric[] = [ { key: 'overall', value: '58%', tone: 'warn' }, { key: 'framework', value: '80-85%', tone: 'steady' }, @@ -216,6 +223,16 @@ const hostEvidenceReadinessItems: HostEvidenceReadinessItem[] = [ { key: 'redactedIngestion', gate: 'S1.6', icon: ShieldCheck, tone: 'locked' }, ] +const hostEvidenceCollectionSteps: HostEvidenceCollectionStep[] = [ + { key: 'scopeFirst', step: '01', icon: Radar, tone: 'warn' }, + { key: 'ownerSecond', step: '02', icon: ClipboardCheck, tone: 'warn' }, + { key: 'credentialThird', step: '03', icon: Lock, tone: 'locked' }, + { key: 'maintenanceFourth', step: '04', icon: Clock3, tone: 'warn' }, + { key: 'rollbackFifth', step: '05', icon: FileWarning, tone: 'warn' }, + { key: 'validationSixth', step: '06', icon: CheckCircle2, tone: 'warn' }, + { key: 'redactedSeventh', step: '07', icon: ShieldCheck, tone: 'locked' }, +] + const evidenceItems = [ 'iwooos-posture-projection.snapshot.json', 'security-rollout-policy.snapshot.json', @@ -546,6 +563,34 @@ function HostEvidenceReadinessCard({ item, index }: { item: HostEvidenceReadines ) } +function HostEvidenceCollectionCard({ item }: { item: HostEvidenceCollectionStep }) { + const t = useTranslations('iwooos.hostEvidenceCollection') + const Icon = item.icon + return ( +
+
+
+ + {t('stepLabel')} +
+ {item.step} +
+

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

+

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

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

{t('hostEvidenceCollection.title')}

+

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

+
+
+ {hostEvidenceCollectionSteps.map(item => ( + + ))} +
+
+
None: "s2_14_iwooos_host_coverage_view", "s2_15_iwooos_host_action_gate_matrix", "s2_16_iwooos_host_evidence_readiness_board", + "s2_17_iwooos_host_evidence_collection_order", ] assert_equal( "progress_delta_ledger.delta_ids", @@ -358,6 +359,15 @@ def validate(root: Path) -> None: "host_validation_metrics_evidence", "host_redacted_ingestion_evidence", ] + expected_iwooos_host_evidence_collection_step_ids = [ + "collect_scope_boundary_first", + "collect_owner_decision_second", + "collect_credential_handling_third", + "collect_maintenance_window_fourth", + "collect_rollback_plan_fifth", + "collect_validation_metrics_sixth", + "collect_redacted_ingestion_seventh", + ] assert_equal( "iwooos_projection.summary.frontend_surface_coverage_group_count", iwooos_projection["summary"]["frontend_surface_coverage_group_count"], @@ -393,6 +403,11 @@ def validate(root: Path) -> None: iwooos_projection["summary"]["host_evidence_readiness_item_count"], len(expected_iwooos_host_evidence_readiness_item_ids), ) + assert_equal( + "iwooos_projection.summary.host_evidence_collection_step_count", + iwooos_projection["summary"]["host_evidence_collection_step_count"], + len(expected_iwooos_host_evidence_collection_step_ids), + ) iwooos_progress = iwooos_projection["progress"] assert_equal("iwooos_projection.progress.overall_percent", iwooos_progress["overall_percent"], progress["overall_percent"]) assert_equal( @@ -732,6 +747,73 @@ def validate(root: Path) -> None: f"iwooos_projection.host_evidence_readiness_items.{item['item_id']}.not_authorization", item["not_authorization"], ) + iwooos_host_evidence_collection_order = iwooos_projection["host_evidence_collection_order"] + assert_equal( + "iwooos_projection.host_evidence_collection_order.ids", + [item["step_id"] for item in iwooos_host_evidence_collection_order], + expected_iwooos_host_evidence_collection_step_ids, + ) + assert_equal( + "iwooos_projection.host_evidence_collection_order.display_order", + [item["display_order"] for item in iwooos_host_evidence_collection_order], + list(range(1, len(expected_iwooos_host_evidence_collection_step_ids) + 1)), + ) + expected_iwooos_host_evidence_collection_source_ids = [ + "host_scope_boundary_evidence", + "host_owner_decision_record_evidence", + "host_credential_handling_evidence", + "host_maintenance_window_evidence", + "host_rollback_plan_evidence", + "host_validation_metrics_evidence", + "host_redacted_ingestion_evidence", + ] + assert_equal( + "iwooos_projection.host_evidence_collection_order.source_item_ids", + [item["source_item_id"] for item in iwooos_host_evidence_collection_order], + expected_iwooos_host_evidence_collection_source_ids, + ) + expected_iwooos_host_evidence_collection_dependencies = [ + [], + ["collect_scope_boundary_first"], + ["collect_owner_decision_second"], + ["collect_owner_decision_second"], + ["collect_maintenance_window_fourth"], + ["collect_rollback_plan_fifth"], + ["collect_validation_metrics_sixth"], + ] + assert_equal( + "iwooos_projection.host_evidence_collection_order.depends_on_step_ids", + [item["depends_on_step_ids"] for item in iwooos_host_evidence_collection_order], + expected_iwooos_host_evidence_collection_dependencies, + ) + for item in iwooos_host_evidence_collection_order: + assert_equal( + f"iwooos_projection.host_evidence_collection_order.{item['step_id']}.display_mode", + item["display_mode"], + "collection_order_only", + ) + assert_equal( + f"iwooos_projection.host_evidence_collection_order.{item['step_id']}.received_count", + item["received_count"], + 0, + ) + assert_equal( + f"iwooos_projection.host_evidence_collection_order.{item['step_id']}.accepted_count", + item["accepted_count"], + 0, + ) + assert_false( + f"iwooos_projection.host_evidence_collection_order.{item['step_id']}.runtime_execution_authorized", + item["runtime_execution_authorized"], + ) + assert_false( + f"iwooos_projection.host_evidence_collection_order.{item['step_id']}.action_buttons_allowed", + item["action_buttons_allowed"], + ) + assert_true( + f"iwooos_projection.host_evidence_collection_order.{item['step_id']}.not_authorization", + item["not_authorization"], + ) assert_equal( "iwooos_projection.non_blocking_lane_ids", iwooos_projection["non_blocking_lane_ids"], @@ -757,6 +839,7 @@ def validate(root: Path) -> None: "display_host_coverage_view", "display_host_action_gate_matrix", "display_host_evidence_readiness_board", + "display_host_evidence_collection_order", "display_evidence_refs", "display_forbidden_actions", ]: @@ -780,6 +863,8 @@ def validate(root: Path) -> None: "mark_host_evidence_received", "mark_host_evidence_accepted", "ingest_raw_host_evidence", + "advance_host_collection_state", + "skip_host_evidence_dependency", "apply_runtime_blocking_control", "switch_github_primary", "production_deploy",