From 394a74908ccdd07b494da91d5b62a2e405b556e8 Mon Sep 17 00:00:00 2001 From: Your Name Date: Tue, 19 May 2026 21:57:32 +0800 Subject: [PATCH] feat(web): add IwoooS host intake preflight --- 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 | 81 ++++++++++ docs/security/IWOOOS-POSTURE-PROJECTION.md | 23 ++- .../security/SECURITY-MIRROR-STATUS-ROLLUP.md | 3 +- .../SECURITY-SUPPLY-CHAIN-PROGRESS.md | 4 +- .../iwooos-posture-projection.snapshot.json | 145 +++++++++++++++++- ...ecurity-mirror-status-rollup.snapshot.json | 12 ++ .../security-mirror-progress-guard.py | 81 ++++++++++ 11 files changed, 508 insertions(+), 6 deletions(-) diff --git a/apps/web/messages/en.json b/apps/web/messages/en.json index 876c2d609..185a0a31b 100644 --- a/apps/web/messages/en.json +++ b/apps/web/messages/en.json @@ -1404,6 +1404,49 @@ } } }, + "hostEvidenceIntake": { + "title": "Host Evidence Intake Preflight", + "subtitle": "Before future host evidence enters human review, this read-only preflight checks whether it is safe to review. It does not accept raw payloads, plaintext credentials, or change received / accepted.", + "checkLabel": "Preflight", + "rejectLabel": "Reject / quarantine condition", + "items": { + "metadataPointer": { + "title": "Metadata pointer only", + "body": "Host evidence only accepts redacted metadata pointers, source steps, and summaries, not full scan output.", + "reject": "reject when redacted metadata pointer is missing" + }, + "dependencyOrder": { + "title": "Collection order match", + "body": "Submitted evidence must follow the S2.17 collection order and cannot skip scope or owner decision.", + "reject": "quarantine when prerequisite dependencies are skipped" + }, + "scopeBeforeScan": { + "title": "Scope before scan", + "body": "Any scan-related evidence must map to scope boundary before it can enter human review.", + "reject": "reject scan evidence without scope" + }, + "ownerBeforeChange": { + "title": "Owner before host change", + "body": "SSH, updates, tuning, or blocking-control evidence requires an owner decision pointer.", + "reject": "reject host-change evidence without decision record" + }, + "credentialPlaintext": { + "title": "Credential plaintext blocked", + "body": "Passwords, tokens, private keys, sessions, or plaintext credentials cannot enter IwoooS mirror.", + "reject": "reject and quarantine when plaintext credential material is detected" + }, + "rawPayload": { + "title": "Raw payload blocked", + "body": "Full raw scan output, unredacted findings, host dumps, or log bundles do not enter projection.", + "reject": "raw payload is always rejected" + }, + "counterFreeze": { + "title": "Frontend counters frozen", + "body": "The frontend can display preflight state only and cannot move received / accepted away from 0.", + "reject": "block frontend attempts to advance counters" + } + } + }, "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 fa56a90f4..3f2b3d383 100644 --- a/apps/web/messages/zh-TW.json +++ b/apps/web/messages/zh-TW.json @@ -1405,6 +1405,49 @@ } } }, + "hostEvidenceIntake": { + "title": "主機 Evidence Intake Preflight", + "subtitle": "未來收到主機 evidence 前,先用這組只讀預檢規則判斷能否進人工 review。預檢不會收 raw payload、憑證明文,也不會改 received / accepted。", + "checkLabel": "預檢", + "rejectLabel": "拒收 / 隔離條件", + "items": { + "metadataPointer": { + "title": "Metadata pointer only", + "body": "主機 evidence 只接受脫敏 metadata pointer、來源步驟與摘要,不直接吃完整掃描輸出。", + "reject": "缺 redacted metadata pointer 就拒收" + }, + "dependencyOrder": { + "title": "Collection order match", + "body": "提交的 evidence 必須符合 S2.17 收件順序,不能跳過 scope 或 owner decision。", + "reject": "跳過前置依賴就隔離" + }, + "scopeBeforeScan": { + "title": "Scope before scan", + "body": "任何 scan 相關 evidence 都要先能對到 scope boundary;沒有 scope 不進人工審。", + "reject": "scan evidence 沒有 scope 就拒收" + }, + "ownerBeforeChange": { + "title": "Owner before host change", + "body": "SSH、更新、調校或 blocking control 的 evidence 需要 owner decision pointer。", + "reject": "主機變更缺 decision record 就拒收" + }, + "credentialPlaintext": { + "title": "Credential plaintext blocked", + "body": "任何帳密、token、private key、session 或憑證明文都不能進 IwoooS mirror。", + "reject": "偵測到憑證明文就拒收並隔離" + }, + "rawPayload": { + "title": "Raw payload blocked", + "body": "完整掃描 raw output、未脫敏 finding、主機 dump 或 log bundle 不進 projection。", + "reject": "raw payload 一律拒收" + }, + "counterFreeze": { + "title": "Frontend counters frozen", + "body": "前端只能顯示預檢狀態,不能把 received / accepted 從 0 推進。", + "reject": "前端嘗試推 counter 就阻擋" + } + } + }, "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 f2bf56573..41f3f9841 100644 --- a/apps/web/src/app/[locale]/iwooos/page.tsx +++ b/apps/web/src/app/[locale]/iwooos/page.tsx @@ -102,6 +102,13 @@ type HostEvidenceCollectionStep = { tone: 'steady' | 'warn' | 'locked' } +type HostEvidenceIntakeCheck = { + 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' }, @@ -233,6 +240,16 @@ const hostEvidenceCollectionSteps: HostEvidenceCollectionStep[] = [ { key: 'redactedSeventh', step: '07', icon: ShieldCheck, tone: 'locked' }, ] +const hostEvidenceIntakeChecks: HostEvidenceIntakeCheck[] = [ + { key: 'metadataPointer', check: 'P1', icon: ListChecks, tone: 'warn' }, + { key: 'dependencyOrder', check: 'P2', icon: GitBranch, tone: 'warn' }, + { key: 'scopeBeforeScan', check: 'P3', icon: Radar, tone: 'locked' }, + { key: 'ownerBeforeChange', check: 'P4', icon: ClipboardCheck, tone: 'locked' }, + { key: 'credentialPlaintext', check: 'P5', icon: Lock, tone: 'locked' }, + { key: 'rawPayload', check: 'P6', icon: FileWarning, tone: 'locked' }, + { key: 'counterFreeze', check: 'P7', icon: ShieldCheck, tone: 'locked' }, +] + const evidenceItems = [ 'iwooos-posture-projection.snapshot.json', 'security-rollout-policy.snapshot.json', @@ -591,6 +608,34 @@ function HostEvidenceCollectionCard({ item }: { item: HostEvidenceCollectionStep ) } +function HostEvidenceIntakeCard({ item }: { item: HostEvidenceIntakeCheck }) { + const t = useTranslations('iwooos.hostEvidenceIntake') + const Icon = item.icon + return ( +
+
+
+ + {t('checkLabel')} +
+ {item.check} +
+

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

+

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

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

{t('hostEvidenceIntake.title')}

+

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

+
+
+ {hostEvidenceIntakeChecks.map(item => ( + + ))} +
+
+
None: "s2_15_iwooos_host_action_gate_matrix", "s2_16_iwooos_host_evidence_readiness_board", "s2_17_iwooos_host_evidence_collection_order", + "s2_18_iwooos_host_evidence_intake_preflight", ] assert_equal( "progress_delta_ledger.delta_ids", @@ -368,6 +369,15 @@ def validate(root: Path) -> None: "collect_validation_metrics_sixth", "collect_redacted_ingestion_seventh", ] + expected_iwooos_host_evidence_intake_preflight_check_ids = [ + "host_metadata_pointer_shape_check", + "host_collection_dependency_order_check", + "host_scope_boundary_before_scan_check", + "host_owner_decision_before_change_check", + "host_credential_plaintext_rejection_check", + "host_raw_payload_rejection_check", + "host_counter_transition_freeze_check", + ] assert_equal( "iwooos_projection.summary.frontend_surface_coverage_group_count", iwooos_projection["summary"]["frontend_surface_coverage_group_count"], @@ -408,6 +418,11 @@ def validate(root: Path) -> None: iwooos_projection["summary"]["host_evidence_collection_step_count"], len(expected_iwooos_host_evidence_collection_step_ids), ) + assert_equal( + "iwooos_projection.summary.host_evidence_intake_preflight_check_count", + iwooos_projection["summary"]["host_evidence_intake_preflight_check_count"], + len(expected_iwooos_host_evidence_intake_preflight_check_ids), + ) iwooos_progress = iwooos_projection["progress"] assert_equal("iwooos_projection.progress.overall_percent", iwooos_progress["overall_percent"], progress["overall_percent"]) assert_equal( @@ -814,6 +829,67 @@ def validate(root: Path) -> None: f"iwooos_projection.host_evidence_collection_order.{item['step_id']}.not_authorization", item["not_authorization"], ) + iwooos_host_evidence_intake_preflight = iwooos_projection["host_evidence_intake_preflight_checks"] + assert_equal( + "iwooos_projection.host_evidence_intake_preflight_checks.ids", + [item["check_id"] for item in iwooos_host_evidence_intake_preflight], + expected_iwooos_host_evidence_intake_preflight_check_ids, + ) + assert_equal( + "iwooos_projection.host_evidence_intake_preflight_checks.display_order", + [item["display_order"] for item in iwooos_host_evidence_intake_preflight], + list(range(1, len(expected_iwooos_host_evidence_intake_preflight_check_ids) + 1)), + ) + expected_iwooos_host_evidence_intake_preflight_rejection_lanes = [ + "reject_missing_redacted_metadata_pointer", + "quarantine_dependency_skip", + "reject_scan_without_scope", + "reject_change_without_owner_decision", + "reject_plaintext_credential_or_secret_value", + "reject_raw_payload_ingestion", + "reject_frontend_counter_transition", + ] + assert_equal( + "iwooos_projection.host_evidence_intake_preflight_checks.rejection_lanes", + [item["rejection_lane"] for item in iwooos_host_evidence_intake_preflight], + expected_iwooos_host_evidence_intake_preflight_rejection_lanes, + ) + for item in iwooos_host_evidence_intake_preflight: + assert_equal( + f"iwooos_projection.host_evidence_intake_preflight_checks.{item['check_id']}.display_mode", + item["display_mode"], + "intake_preflight_only", + ) + assert_equal( + f"iwooos_projection.host_evidence_intake_preflight_checks.{item['check_id']}.received_count", + item["received_count"], + 0, + ) + assert_equal( + f"iwooos_projection.host_evidence_intake_preflight_checks.{item['check_id']}.accepted_count", + item["accepted_count"], + 0, + ) + assert_false( + f"iwooos_projection.host_evidence_intake_preflight_checks.{item['check_id']}.raw_payload_allowed", + item["raw_payload_allowed"], + ) + assert_false( + f"iwooos_projection.host_evidence_intake_preflight_checks.{item['check_id']}.secret_value_collection_allowed", + item["secret_value_collection_allowed"], + ) + assert_false( + f"iwooos_projection.host_evidence_intake_preflight_checks.{item['check_id']}.runtime_execution_authorized", + item["runtime_execution_authorized"], + ) + assert_false( + f"iwooos_projection.host_evidence_intake_preflight_checks.{item['check_id']}.action_buttons_allowed", + item["action_buttons_allowed"], + ) + assert_true( + f"iwooos_projection.host_evidence_intake_preflight_checks.{item['check_id']}.not_authorization", + item["not_authorization"], + ) assert_equal( "iwooos_projection.non_blocking_lane_ids", iwooos_projection["non_blocking_lane_ids"], @@ -840,6 +916,7 @@ def validate(root: Path) -> None: "display_host_action_gate_matrix", "display_host_evidence_readiness_board", "display_host_evidence_collection_order", + "display_host_evidence_intake_preflight_checks", "display_evidence_refs", "display_forbidden_actions", ]: @@ -865,6 +942,10 @@ def validate(root: Path) -> None: "ingest_raw_host_evidence", "advance_host_collection_state", "skip_host_evidence_dependency", + "accept_host_evidence_without_preflight", + "ingest_host_evidence_raw_payload", + "collect_host_credential_plaintext", + "advance_host_evidence_counters_from_frontend", "apply_runtime_blocking_control", "switch_github_primary", "production_deploy",