From a9119561bd011b15971f59575581dbe57c3cee8a Mon Sep 17 00:00:00 2001 From: Your Name Date: Sun, 24 May 2026 14:49:27 +0800 Subject: [PATCH] feat(web): add phased security rollout ladder --- apps/web/messages/en.json | 25 ++++++ apps/web/messages/zh-TW.json | 25 ++++++ .../app/[locale]/security-compliance/page.tsx | 90 +++++++++++++++++++ docs/LOGBOOK.md | 14 +++ .../security/SECURITY-MIRROR-STATUS-ROLLUP.md | 2 + .../SECURITY-SUPPLY-CHAIN-PROGRESS.md | 4 +- ...ecurity-mirror-status-rollup.snapshot.json | 28 ++++++ .../security-mirror-progress-guard.py | 44 +++++++++ 8 files changed, 231 insertions(+), 1 deletion(-) diff --git a/apps/web/messages/en.json b/apps/web/messages/en.json index a862f9dd6..eef73de8f 100644 --- a/apps/web/messages/en.json +++ b/apps/web/messages/en.json @@ -990,6 +990,9 @@ "routeRoleTitle": "前台入口角色對照", "routeRoleSubtitle": "從安全合規進來時,也能直接看懂每個資安入口該負責什麼;這些入口只導覽與說明,不提供執行按鈕。", "routeLabel": "入口", + "rolloutTitle": "低摩擦分階段收斂", + "rolloutSubtitle": "初期先建立框架、可視化與證據鏈;等負責人回覆、人工審查與執行期閘門都完成後,再逐步收嚴,不讓資安一開始拖慢產品流程。", + "phaseLabel": "階段", "items": { "routePreserved": { "label": "路由策略", @@ -1029,6 +1032,28 @@ "title": "AwoooP 看人控等待", "body": "查看審批與負責人回覆等待狀態;仍不代表資安執行期閘門已開。" } + }, + "rolloutPhases": { + "observe": { + "title": "觀測與盤點", + "body": "目前只整理入口、主機、專案、網站、監控與工具姿態,不阻擋使用者流程。" + }, + "evidence": { + "title": "補齊證據", + "body": "收斂脫敏證據、版本來源、負責人回覆與 AwoooP 只讀消費證明。" + }, + "humanReview": { + "title": "人工審查", + "body": "由負責人確認例外、風險接受、修復順序與是否進入執行期閘門。" + }, + "runtimeGate": { + "title": "批准後開閘", + "body": "只有明確批准後才允許掃描、修復、部署或主機變更的執行期流程。" + }, + "tightening": { + "title": "逐步收嚴", + "body": "依證據與影響範圍分批提高管控,不一次把整個產品流程鎖死。" + } } } }, diff --git a/apps/web/messages/zh-TW.json b/apps/web/messages/zh-TW.json index 268b8ac19..ecac7fe30 100644 --- a/apps/web/messages/zh-TW.json +++ b/apps/web/messages/zh-TW.json @@ -991,6 +991,9 @@ "routeRoleTitle": "前台入口角色對照", "routeRoleSubtitle": "從安全合規進來時,也能直接看懂每個資安入口該負責什麼;這些入口只導覽與說明,不提供執行按鈕。", "routeLabel": "入口", + "rolloutTitle": "低摩擦分階段收斂", + "rolloutSubtitle": "初期先建立框架、可視化與證據鏈;等負責人回覆、人工審查與執行期閘門都完成後,再逐步收嚴,不讓資安一開始拖慢產品流程。", + "phaseLabel": "階段", "items": { "routePreserved": { "label": "路由策略", @@ -1030,6 +1033,28 @@ "title": "AwoooP 看人控等待", "body": "查看審批與負責人回覆等待狀態;仍不代表資安執行期閘門已開。" } + }, + "rolloutPhases": { + "observe": { + "title": "觀測與盤點", + "body": "目前只整理入口、主機、專案、網站、監控與工具姿態,不阻擋使用者流程。" + }, + "evidence": { + "title": "補齊證據", + "body": "收斂脫敏證據、版本來源、負責人回覆與 AwoooP 只讀消費證明。" + }, + "humanReview": { + "title": "人工審查", + "body": "由負責人確認例外、風險接受、修復順序與是否進入執行期閘門。" + }, + "runtimeGate": { + "title": "批准後開閘", + "body": "只有明確批准後才允許掃描、修復、部署或主機變更的執行期流程。" + }, + "tightening": { + "title": "逐步收嚴", + "body": "依證據與影響範圍分批提高管控,不一次把整個產品流程鎖死。" + } } } }, diff --git a/apps/web/src/app/[locale]/security-compliance/page.tsx b/apps/web/src/app/[locale]/security-compliance/page.tsx index 0b95e6412..656943ac9 100644 --- a/apps/web/src/app/[locale]/security-compliance/page.tsx +++ b/apps/web/src/app/[locale]/security-compliance/page.tsx @@ -44,6 +44,26 @@ const routeRoleItems = [ { key: 'awooopApprovals', href: '/awooop/approvals', route: 'AwoooP', icon: Lock, color: '#6f6d66' }, ] as const +const rolloutPhaseItems = [ + { key: 'observe', phase: '01', state: '現在', icon: Radar, color: '#d97757' }, + { key: 'evidence', phase: '02', state: '補證', icon: ClipboardCheck, color: '#1f7a4d' }, + { key: 'humanReview', phase: '03', state: '人控', icon: CheckCircle2, color: '#1f7a4d' }, + { key: 'runtimeGate', phase: '04', state: '批准後', icon: ShieldCheck, color: '#6f6d66' }, + { key: 'tightening', phase: '05', state: '收斂', icon: Lock, color: '#6f6d66' }, +] as const + +const rolloutBoundaries = [ + 'security_compliance_rollout_phase_count=5', + 'security_compliance_rollout_current_phase=observe_first', + 'security_compliance_rollout_runtime_phase_enabled=false', + 'security_compliance_rollout_enforcement_enabled=false', + 'security_compliance_rollout_action_buttons_allowed=false', + 'runtime_execution_authorized=false', + 'active_runtime_gate_count=0', + 'action_buttons_allowed=false', + 'not_authorization=true', +] as const + function SecurityComplianceFrontStage({ locale }: { locale: string }) { const t = useTranslations('securityCompliance.frontStage') const textWrap = { overflowWrap: 'anywhere' as const, wordBreak: 'break-word' as const } @@ -167,6 +187,76 @@ function SecurityComplianceFrontStage({ locale }: { locale: string }) { })} +
+
+

{t('rolloutTitle')}

+

+ {t('rolloutSubtitle')} +

+
+
+ {rolloutPhaseItems.map(item => { + const Icon = item.icon + return ( +
+
+ {t('phaseLabel')} {item.phase} + +
+
{item.state}
+

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

+

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

+
+ ) + })} +
+
+ {rolloutBoundaries.map(boundary => ( + + {boundary} + + ))} +
+
None: "s2_107_security_compliance_iwooos_frontstage_bridge", "s2_108_iwooos_frontstage_security_entry_roles", "s2_109_security_compliance_frontstage_route_role_map", + "s2_110_security_compliance_low_friction_rollout_ladder", ] assert_equal( "progress_delta_ledger.delta_ids", @@ -630,6 +631,11 @@ def validate(root: Path) -> None: [item["action_id"] for item in rollup["next_safe_actions"] if isinstance(item, dict)], "show_security_compliance_frontstage_route_role_map", ) + assert_contains( + "rollup.next_safe_actions.action_ids", + [item["action_id"] for item in rollup["next_safe_actions"] if isinstance(item, dict)], + "show_security_compliance_low_friction_rollout_ladder", + ) assert_contains( "rollup.next_safe_actions.action_ids", [item["action_id"] for item in rollup["next_safe_actions"] if isinstance(item, dict)], @@ -8009,6 +8015,11 @@ def validate(root: Path) -> None: security_compliance_page, 'data-testid="security-compliance-frontstage-route-role-map"', ) + assert_text_contains( + "security_compliance_page.low_friction_rollout_ladder_testid", + security_compliance_page, + 'data-testid="security-compliance-low-friction-rollout-ladder"', + ) assert_text_contains( "iwooos_page.security_compliance_frontstage_testid", iwooos_projection_page, @@ -8050,6 +8061,18 @@ def validate(root: Path) -> None: security_compliance_page, text, ) + for text in [ + "security_compliance_rollout_phase_count=5", + "security_compliance_rollout_current_phase=observe_first", + "security_compliance_rollout_runtime_phase_enabled=false", + "security_compliance_rollout_enforcement_enabled=false", + "security_compliance_rollout_action_buttons_allowed=false", + ]: + assert_text_contains( + "security_compliance_page.low_friction_rollout_boundary", + security_compliance_page, + text, + ) for text in [ "secret_value_collection_allowed=false", "repo_creation_authorized=false", @@ -8083,8 +8106,12 @@ def validate(root: Path) -> None: "routeRoleTitle", "routeRoleSubtitle", "routeLabel", + "rolloutTitle", + "rolloutSubtitle", + "phaseLabel", "items", "routeRoles", + "rolloutPhases", ]: assert_contains( "web_messages.zh-TW.securityCompliance.frontStage.keys", @@ -8124,6 +8151,23 @@ def validate(root: Path) -> None: list(web_messages_en["securityCompliance"]["frontStage"]["routeRoles"].keys()), key, ) + for key in [ + "observe", + "evidence", + "humanReview", + "runtimeGate", + "tightening", + ]: + assert_contains( + "web_messages.zh-TW.securityCompliance.frontStage.rolloutPhases", + list(web_messages_zh["securityCompliance"]["frontStage"]["rolloutPhases"].keys()), + key, + ) + assert_contains( + "web_messages.en.securityCompliance.frontStage.rolloutPhases", + list(web_messages_en["securityCompliance"]["frontStage"]["rolloutPhases"].keys()), + key, + ) assert_contains( "web_messages.zh-TW.iwooos.securityComplianceFrontStage", list(web_messages_zh["iwooos"].keys()),