diff --git a/apps/web/messages/en.json b/apps/web/messages/en.json index ca7f38c39..8f2eafc5b 100644 --- a/apps/web/messages/en.json +++ b/apps/web/messages/en.json @@ -1282,6 +1282,43 @@ } } }, + "hostActionGates": { + "title": "Host Action Gate Matrix", + "subtitle": "Breaks host-related high-risk actions into read-only gates. This only explains what is locked and what human decision is required; it does not provide execution entry points.", + "gateLabel": "Required gate", + "items": { + "activeScan": { + "title": "Active scan", + "body": "Active scans for Kali 112 and development hosts 168 / 111 are not approved and cannot be triggered from IwoooS.", + "gate": "requires S1.6 scan scope approval plus a follow-up runtime gate" + }, + "credentialedScan": { + "title": "Credentialed scan", + "body": "Any credentialed scan requires scope, credential handling, and redacted evidence rules before it can proceed.", + "gate": "requires S1.6 scope approval; credentialed scan remains false" + }, + "kaliExecute": { + "title": "Kali /execute", + "body": "The Kali execution endpoint remains a block candidate and is not opened just because hosts are visible.", + "gate": "requires a human decision record and S3.4 follow-up runtime gate" + }, + "sshChange": { + "title": "SSH / host change", + "body": "Logging into hosts, changing settings, tuning services, restarting services, or changing SSH settings is outside the frontend authority.", + "gate": "requires explicit human approval, a change plan, and rollback evidence" + }, + "kaliUpdate": { + "title": "Kali host update", + "body": "Kali updates and host tuning affect scan results and toolchain stability, so they must be approved separately from posture display.", + "gate": "requires maintenance window, update list, validation metrics, and rollback plan" + }, + "runtimeBlocking": { + "title": "Runtime blocking control", + "body": "Turning findings into product blocking or runtime enforcement still waits for owner evidence and a human decision.", + "gate": "requires an accepted decision record; active runtime gates remain 0" + } + } + }, "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 30742937a..6a12004c6 100644 --- a/apps/web/messages/zh-TW.json +++ b/apps/web/messages/zh-TW.json @@ -1283,6 +1283,43 @@ } } }, + "hostActionGates": { + "title": "主機動作 Gate 矩陣", + "subtitle": "把主機相關高風險動作拆成只讀 gate。這裡只說明哪些動作仍被鎖住,以及需要哪一類人工決策;不提供任何執行入口。", + "gateLabel": "需要 Gate", + "items": { + "activeScan": { + "title": "Active scan", + "body": "對 Kali 112、開發主機 168 / 111 的主動掃描仍未批准,不能由 IwoooS 直接觸發。", + "gate": "需要 S1.6 scan scope approval 與後續 runtime gate" + }, + "credentialedScan": { + "title": "Credentialed scan", + "body": "任何帶憑證的掃描都必須先有 scope、credential handling 與脫敏 evidence 規範。", + "gate": "需要 S1.6 scope approval;目前 credentialed scan=false" + }, + "kaliExecute": { + "title": "Kali /execute", + "body": "Kali 執行端點仍是 block candidate,不能因主機已納入視野就開放。", + "gate": "需要人工 decision record 與 S3.4 follow-up runtime gate" + }, + "sshChange": { + "title": "SSH / host change", + "body": "登入主機、改設定、套用調校、重啟服務或變更 SSH 設定都不屬於目前前端權限。", + "gate": "需要明確人工批准、變更計畫與 rollback evidence" + }, + "kaliUpdate": { + "title": "Kali host update", + "body": "Kali 更新與主機調校會影響掃描結果與工具鏈穩定性,必須和資安 gate 分開批准。", + "gate": "需要維護窗口、更新清單、驗證指標與 rollback 計畫" + }, + "runtimeBlocking": { + "title": "Runtime blocking control", + "body": "把 finding 變成產品阻擋或 runtime enforcement 仍要等 owner evidence 與人工決策。", + "gate": "需要 accepted decision record;active runtime gates 仍為 0" + } + } + }, "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 01dd5316f..5932dd88c 100644 --- a/apps/web/src/app/[locale]/iwooos/page.tsx +++ b/apps/web/src/app/[locale]/iwooos/page.tsx @@ -81,6 +81,13 @@ type HostCoverageItem = { tone: 'steady' | 'warn' | 'locked' } +type HostActionGateItem = { + key: string + gate: string + icon: typeof ShieldCheck + tone: 'steady' | 'warn' | 'locked' +} + const postureMetrics: PostureMetric[] = [ { key: 'overall', value: '58%', tone: 'warn' }, { key: 'framework', value: '80-85%', tone: 'steady' }, @@ -183,6 +190,15 @@ const hostCoverageItems: HostCoverageItem[] = [ { key: 'dev111', address: '192.168.0.111', icon: Radar, tone: 'warn' }, ] +const hostActionGateItems: HostActionGateItem[] = [ + { key: 'activeScan', gate: 'S1.6', icon: Radar, tone: 'locked' }, + { key: 'credentialedScan', gate: 'S1.6', icon: Lock, tone: 'locked' }, + { key: 'kaliExecute', gate: 'S3.4', icon: Activity, tone: 'locked' }, + { key: 'sshChange', gate: 'S3.4', icon: Lock, tone: 'locked' }, + { key: 'kaliUpdate', gate: 'S3.4', icon: FileWarning, tone: 'warn' }, + { key: 'runtimeBlocking', gate: 'S3.4', icon: ShieldCheck, tone: 'locked' }, +] + const evidenceItems = [ 'iwooos-posture-projection.snapshot.json', 'security-rollout-policy.snapshot.json', @@ -457,6 +473,34 @@ function HostCoverageCard({ item, index }: { item: HostCoverageItem; index: numb ) } +function HostActionGateCard({ item, index }: { item: HostActionGateItem; index: number }) { + const t = useTranslations('iwooos.hostActionGates') + const Icon = item.icon + return ( +
+
+
+ + {item.gate} +
+ {String(index + 1).padStart(2, '0')} +
+

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

+

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

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

{t('hostActionGates.title')}

+

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

+
+
+ {hostActionGateItems.map((item, index) => ( + + ))} +
+
+
None: "s2_12_iwooos_operator_journey_projection", "s2_13_iwooos_owner_evidence_readiness_board", "s2_14_iwooos_host_coverage_view", + "s2_15_iwooos_host_action_gate_matrix", ] assert_equal( "progress_delta_ledger.delta_ids", @@ -339,6 +340,14 @@ def validate(root: Path) -> None: "dev_168_development_host", "dev_111_development_host", ] + expected_iwooos_host_action_gate_item_ids = [ + "host_active_scan_gate", + "host_credentialed_scan_gate", + "kali_execute_gate", + "ssh_host_change_gate", + "kali_host_update_gate", + "runtime_blocking_control_gate", + ] assert_equal( "iwooos_projection.summary.frontend_surface_coverage_group_count", iwooos_projection["summary"]["frontend_surface_coverage_group_count"], @@ -364,6 +373,11 @@ def validate(root: Path) -> None: iwooos_projection["summary"]["host_coverage_item_count"], len(expected_iwooos_host_coverage_item_ids), ) + assert_equal( + "iwooos_projection.summary.host_action_gate_item_count", + iwooos_projection["summary"]["host_action_gate_item_count"], + len(expected_iwooos_host_action_gate_item_ids), + ) iwooos_progress = iwooos_projection["progress"] assert_equal("iwooos_projection.progress.overall_percent", iwooos_progress["overall_percent"], progress["overall_percent"]) assert_equal( @@ -603,6 +617,51 @@ def validate(root: Path) -> None: f"iwooos_projection.host_coverage_items.{item['host_id']}.not_authorization", item["not_authorization"], ) + iwooos_host_action_gates = iwooos_projection["host_action_gate_items"] + assert_equal( + "iwooos_projection.host_action_gate_items.ids", + [item["action_id"] for item in iwooos_host_action_gates], + expected_iwooos_host_action_gate_item_ids, + ) + assert_equal( + "iwooos_projection.host_action_gate_items.display_order", + [item["display_order"] for item in iwooos_host_action_gates], + list(range(1, len(expected_iwooos_host_action_gate_item_ids) + 1)), + ) + for item in iwooos_host_action_gates: + assert_equal( + f"iwooos_projection.host_action_gate_items.{item['action_id']}.display_mode", + item["display_mode"], + "gate_only", + ) + assert_false( + f"iwooos_projection.host_action_gate_items.{item['action_id']}.active_scan_authorized", + item["active_scan_authorized"], + ) + assert_false( + f"iwooos_projection.host_action_gate_items.{item['action_id']}.credentialed_scan_authorized", + item["credentialed_scan_authorized"], + ) + assert_false( + f"iwooos_projection.host_action_gate_items.{item['action_id']}.ssh_change_authorized", + item["ssh_change_authorized"], + ) + assert_false( + f"iwooos_projection.host_action_gate_items.{item['action_id']}.host_update_authorized", + item["host_update_authorized"], + ) + assert_false( + f"iwooos_projection.host_action_gate_items.{item['action_id']}.runtime_execution_authorized", + item["runtime_execution_authorized"], + ) + assert_false( + f"iwooos_projection.host_action_gate_items.{item['action_id']}.action_buttons_allowed", + item["action_buttons_allowed"], + ) + assert_true( + f"iwooos_projection.host_action_gate_items.{item['action_id']}.not_authorization", + item["not_authorization"], + ) assert_equal( "iwooos_projection.non_blocking_lane_ids", iwooos_projection["non_blocking_lane_ids"], @@ -626,6 +685,7 @@ def validate(root: Path) -> None: "display_operator_journey_steps", "display_owner_evidence_readiness_board", "display_host_coverage_view", + "display_host_action_gate_matrix", "display_evidence_refs", "display_forbidden_actions", ]: @@ -641,8 +701,12 @@ def validate(root: Path) -> None: "modify_workflow_or_secret", "enable_runner", "ssh_to_host", + "open_ssh_session", "update_kali_host", + "auto_update_host", + "run_host_package_upgrade", "credentialed_scan_host", + "apply_runtime_blocking_control", "switch_github_primary", "production_deploy", "treat_progress_as_authorization",