diff --git a/apps/web/messages/en.json b/apps/web/messages/en.json index b3b4d2709..8797e3ba7 100644 --- a/apps/web/messages/en.json +++ b/apps/web/messages/en.json @@ -1181,6 +1181,43 @@ } } }, + "journey": { + "title": "Security Handling Journey", + "subtitle": "Pins the visible security mesh flow into a read-only status map: read posture, inspect existing surfaces, then move through owner evidence, human decisions, and follow-up gates.", + "outputLabel": "Output", + "steps": { + "readPosture": { + "title": "Read Current Posture", + "body": "Start from the 58% headline, framework / runtime landing, active gates, and next high-level gate.", + "output": "read-only posture, not authorization" + }, + "openSurface": { + "title": "Open Existing Surfaces", + "body": "Enter the original page by security, alert, authorization, governance, audit, or code review responsibility.", + "output": "preserve original owner and data boundary" + }, + "triageLane": { + "title": "Triage Non-blocking Lanes", + "body": "LOW / MEDIUM, missing owner response, partial mirror, and Kali observe findings stay observe / warn first.", + "output": "follow-up, not blocking" + }, + "collectEvidence": { + "title": "Collect Owner Evidence", + "body": "The next recommended collection item remains S4.9 Gitea owner attestation response, accepting redacted evidence only.", + "output": "update received / accepted state, no execution" + }, + "humanDecision": { + "title": "Wait for Human Decision", + "body": "Security gates need decision records; AwoooP approval, Code Review, or progress numbers cannot replace that.", + "output": "human decision, not runtime" + }, + "runtimeGate": { + "title": "Follow-up Runtime Gate", + "body": "Only after human approval can work move into follow-up runtime gate templates; active runtime gates remain 0.", + "output": "separate gate after approval" + } + } + }, "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 a5723aa5b..f01c6e0bb 100644 --- a/apps/web/messages/zh-TW.json +++ b/apps/web/messages/zh-TW.json @@ -1182,6 +1182,43 @@ } } }, + "journey": { + "title": "資安處理旅程", + "subtitle": "把目前資安網的可見流程固定成只讀狀態圖:先看態勢,再查既有頁面,最後才進入 owner evidence、人工決策與後續 gate。", + "outputLabel": "輸出", + "steps": { + "readPosture": { + "title": "讀取目前態勢", + "body": "先看 58% headline、framework / runtime landing、active gates 與下一個高層 gate。", + "output": "只讀 posture,不代表授權" + }, + "openSurface": { + "title": "開啟既有頁面", + "body": "依照 security、alert、authorization、governance、audit、code review 的責任面進入原頁。", + "output": "保留原頁 owner 與資料邊界" + }, + "triageLane": { + "title": "判讀非阻擋分流", + "body": "LOW / MEDIUM、缺 owner response、partial mirror 與 Kali observe finding 先維持 observe / warn。", + "output": "follow-up,不升 blocking" + }, + "collectEvidence": { + "title": "收 owner evidence", + "body": "下一個建議收件仍是 S4.9 Gitea owner attestation response,且只接受脫敏 evidence。", + "output": "更新 received / accepted 狀態,不執行" + }, + "humanDecision": { + "title": "等待人工決策", + "body": "資安 gate 需要 decision record;AwoooP approval、Code Review 或進度數字都不能自動替代。", + "output": "人控決策,不是 runtime" + }, + "runtimeGate": { + "title": "後續 runtime gate", + "body": "只有人工批准後,才進入 follow-up runtime gate template;目前 active runtime gates 仍為 0。", + "output": "待批准後另開 gate" + } + } + }, "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 32dfa0af0..a1aa1b0ad 100644 --- a/apps/web/src/app/[locale]/iwooos/page.tsx +++ b/apps/web/src/app/[locale]/iwooos/page.tsx @@ -61,6 +61,12 @@ type ConflictControl = { tone: 'steady' | 'warn' | 'locked' } +type JourneyStep = { + key: string + icon: typeof ShieldCheck + tone: 'steady' | 'warn' | 'locked' +} + const postureMetrics: PostureMetric[] = [ { key: 'overall', value: '58%', tone: 'warn' }, { key: 'framework', value: '80-85%', tone: 'steady' }, @@ -138,6 +144,15 @@ const conflictControls: ConflictControl[] = [ { key: 'kaliNotCalled', tone: 'locked' }, ] +const journeySteps: JourneyStep[] = [ + { key: 'readPosture', icon: Radar, tone: 'warn' }, + { key: 'openSurface', icon: ListChecks, tone: 'steady' }, + { key: 'triageLane', icon: Activity, tone: 'warn' }, + { key: 'collectEvidence', icon: ClipboardCheck, tone: 'warn' }, + { key: 'humanDecision', icon: Lock, tone: 'locked' }, + { key: 'runtimeGate', icon: ShieldCheck, tone: 'locked' }, +] + const evidenceItems = [ 'iwooos-posture-projection.snapshot.json', 'security-rollout-policy.snapshot.json', @@ -333,6 +348,29 @@ function ConflictRow({ item, index }: { item: ConflictControl; index: number }) ) } +function JourneyStepCard({ item, index }: { item: JourneyStep; index: number }) { + const t = useTranslations('iwooos.journey') + const Icon = item.icon + return ( +
+
+ + {String(index + 1).padStart(2, '0')} +
+

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

+

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

+
+
{t('outputLabel')}
+
+ {t(`steps.${item.key}.output` as never)} +
+
+
+ ) +} + export default function IwoooSPage({ params }: { params: { locale: string } }) { const t = useTranslations('iwooos') @@ -367,6 +405,24 @@ export default function IwoooSPage({ params }: { params: { locale: string } }) { +
+
+

{t('journey.title')}

+

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

+
+
+ {journeySteps.map((item, index) => )} +
+
+
None: "s2_9_iwooos_posture_projection_contract", "s2_10_iwooos_existing_frontend_surface_integration", "s2_11_iwooos_surface_coverage_boundary_matrix", + "s2_12_iwooos_operator_journey_projection", ] assert_equal( "progress_delta_ledger.delta_ids", @@ -314,6 +315,14 @@ def validate(root: Path) -> None: "awooop_approval_not_security_approval", "frontend_index_does_not_call_kali", ] + expected_iwooos_journey_step_ids = [ + "read_current_posture", + "open_existing_security_surface", + "triage_non_blocking_lane", + "collect_owner_evidence", + "wait_for_human_decision", + "prepare_followup_runtime_gate", + ] assert_equal( "iwooos_projection.summary.frontend_surface_coverage_group_count", iwooos_projection["summary"]["frontend_surface_coverage_group_count"], @@ -324,6 +333,11 @@ def validate(root: Path) -> None: iwooos_projection["summary"]["frontend_surface_conflict_control_count"], len(expected_iwooos_conflict_control_ids), ) + assert_equal( + "iwooos_projection.summary.operator_journey_step_count", + iwooos_projection["summary"]["operator_journey_step_count"], + len(expected_iwooos_journey_step_ids), + ) iwooos_progress = iwooos_projection["progress"] assert_equal("iwooos_projection.progress.overall_percent", iwooos_progress["overall_percent"], progress["overall_percent"]) assert_equal( @@ -458,6 +472,35 @@ def validate(root: Path) -> None: f"iwooos_projection.frontend_surface_conflict_controls.{item['control_id']}.not_authorization", item["not_authorization"], ) + iwooos_journey_steps = iwooos_projection["operator_journey_steps"] + assert_equal( + "iwooos_projection.operator_journey_steps.ids", + [item["step_id"] for item in iwooos_journey_steps], + expected_iwooos_journey_step_ids, + ) + assert_equal( + "iwooos_projection.operator_journey_steps.display_order", + [item["display_order"] for item in iwooos_journey_steps], + list(range(1, len(expected_iwooos_journey_step_ids) + 1)), + ) + for item in iwooos_journey_steps: + assert_equal( + f"iwooos_projection.operator_journey_steps.{item['step_id']}.display_mode", + item["display_mode"], + "journey_only", + ) + assert_false( + f"iwooos_projection.operator_journey_steps.{item['step_id']}.runtime_execution_authorized", + item["runtime_execution_authorized"], + ) + assert_false( + f"iwooos_projection.operator_journey_steps.{item['step_id']}.action_buttons_allowed", + item["action_buttons_allowed"], + ) + assert_true( + f"iwooos_projection.operator_journey_steps.{item['step_id']}.not_authorization", + item["not_authorization"], + ) assert_equal( "iwooos_projection.non_blocking_lane_ids", iwooos_projection["non_blocking_lane_ids"], @@ -478,6 +521,7 @@ def validate(root: Path) -> None: "display_existing_frontend_security_surfaces", "display_frontend_surface_coverage_matrix", "display_frontend_surface_conflict_controls", + "display_operator_journey_steps", "display_evidence_refs", "display_forbidden_actions", ]: