From f9b3585a004fbe4be6b658d0251e29d3bc8f71c8 Mon Sep 17 00:00:00 2001 From: Your Name Date: Mon, 1 Jun 2026 10:51:23 +0800 Subject: [PATCH] feat(web): add IwoooS topology drilldown --- apps/web/messages/en.json | 75 ++++-- apps/web/messages/zh-TW.json | 75 ++++-- apps/web/src/app/[locale]/iwooos/page.tsx | 220 +++++++++++++++++- docs/LOGBOOK.md | 27 +++ .../iwooos-posture-projection.snapshot.json | 124 +++++++++- ...ecurity-mirror-status-rollup.snapshot.json | 12 + .../security-mirror-progress-guard.py | 131 +++++++++++ 7 files changed, 607 insertions(+), 57 deletions(-) diff --git a/apps/web/messages/en.json b/apps/web/messages/en.json index 6586f324..26e0978b 100644 --- a/apps/web/messages/en.json +++ b/apps/web/messages/en.json @@ -5450,7 +5450,7 @@ "topologyAtlas": { "eyebrow": "專業架構與拓樸圖譜", "title": "用圖譜看攻擊面、資產關係與證據流", - "subtitle": "把主流資安產品常見的 graph、attack path、blast radius 與 evidence lane 濃縮成四個可切換視角;少文字、多圖表,仍維持 Gate 0。", + "subtitle": "把主流資安產品常見的圖譜、攻擊路徑、爆炸半徑與證據線濃縮成四個可切換視角;少文字、多圖表,仍維持執行閘 0。", "tabsLabel": "架構拓樸圖譜視角", "mapLabel": "圖譜視角", "panelLabels": { @@ -5458,38 +5458,71 @@ "next": "下一步", "locked": "仍鎖住" }, + "nodeDrilldown": { + "eyebrow": "節點下鑽", + "selectorLabel": "切換拓樸節點", + "fields": { + "relation": "關聯", + "evidence": "證據", + "next": "下一步", + "boundary": "邊界" + }, + "nodes": { + "productSurface": { + "body": "把 AwoooI、AwoooP、IwoooS、公開網站與 VibeWork 放在同一個資產面,先看關聯與責任邊界,不直接提高限制。" + }, + "sourceControl": { + "body": "GitHub / Gitea 是版本來源關鍵節點;目前只顯示 S4.9 負責人回覆卡點與決策紀錄缺口,不執行分支參照或工作流程變更。" + }, + "kali": { + "body": "Kali 192.168.0.112 已列為資安主機節點;這裡只呈現它與版本來源、開發主機、證據鏈的關係,不代表已授權掃描。" + }, + "devHosts": { + "body": "192.168.0.111 與 192.168.0.168 以開發主機群呈現,等主機維護窗口與負責人決策紀錄完整後才能進入執行期。" + }, + "monitoring": { + "body": "監控、MCP、Ansible、KM 與告警資料先進入證據線,讓 IwoooS 能解釋訊號來源與新鮮度。" + }, + "awooopTruth": { + "body": "AwoooP 是跨 Session 工作狀態與人工 gate 的真相鏈;IwoooS 只讀消費它,不把顯示狀態當批准。" + }, + "runtimeGate": { + "body": "所有掃描、修復、部署、主機更新與版本來源變更仍集中在執行閘 0;沒有正式決策前不產生執行按鈕。" + } + } + }, "lenses": { "architecture": { "title": "架構分層", - "mapTitle": "Code → Asset → Host → Evidence → Gate", + "mapTitle": "程式碼 → 資產 → 主機 → 證據 → 閘門", "detail": "用五層結構看 IwoooS:產品與網站、版本來源、Kali / 開發主機、監控與 AwoooP、最後才是執行閘。", "evidence": "8 類產品 / 網站、3 台主機、6 條工具鏈已進入同一張只讀圖譜。", - "next": "把 S4.9 owner response 與脫敏證據接成可驗證節點。", - "locked": "架構圖不是 runtime 授權,不代表可以掃描或修復。" + "next": "把 S4.9 負責人回覆與脫敏證據接成可驗證節點。", + "locked": "架構圖不是執行期授權,不代表可以掃描或修復。" }, "topology": { "title": "主機拓樸", - "mapTitle": "112 / 111 / 168 Observe-only Fabric", + "mapTitle": "112 / 111 / 168 只讀觀測主機網", "detail": "把 Kali 112、開發主機 111 / 168、監控工具與 AwoooP 真相鏈放在同一張拓樸圖,而不是分散在長文件裡。", "evidence": "目前只呈現觀測窗口、證據位置與人工 gate,沒有執行 SSH、掃描或主機設定變更。", - "next": "等 runtime gate 與掃描範圍批准後,才把 read-only evidence 轉入受控探測。", + "next": "等執行期閘門與掃描範圍批准後,才把只讀證據轉入受控探測。", "locked": "host_change_authorized=false,scan_authorized=false。" }, "attackSurface": { "title": "攻擊面路徑", - "mapTitle": "External Surface → Source Control → Runtime Boundary", - "detail": "把公開入口、產品、版本來源與 Gate 0 串成攻擊面圖;目標是優先看清可被利用的關聯,不先提高限制。", - "evidence": "目前可看見 8 類資產與 S4.9 版本來源卡點,但 blast radius 仍維持 0,避免誤導成已完成攻防驗證。", - "next": "先完成 GitHub primary / Gitea owner evidence,再讓風險路徑有可信來源。", - "locked": "source_control_mutation_authorized=false,GitHub primary switch 未授權。" + "mapTitle": "外部資產面 → 版本來源 → 執行期邊界", + "detail": "把公開入口、產品、版本來源與執行閘 0 串成攻擊面圖;目標是優先看清可被利用的關聯,不先提高限制。", + "evidence": "目前可看見 8 類資產與 S4.9 版本來源卡點,但爆炸半徑仍維持 0,避免誤導成已完成攻防驗證。", + "next": "先完成 GitHub 主來源 / Gitea 負責人證據,再讓風險路徑有可信來源。", + "locked": "版本來源變更未授權,GitHub 主來源切換未授權。" }, "evidenceFlow": { "title": "證據流", - "mapTitle": "Monitoring → AwoooP Truth Chain → Human Gate", - "detail": "用 evidence lane 表示資料如何被收集、脫敏、審查與回寫;這比單純列文件更接近 SOC / XDR 的操作體驗。", + "mapTitle": "監控 → AwoooP 真相鏈 → 人工閘門", + "detail": "用證據線表示資料如何被收集、脫敏、審查與回寫;這比單純列文件更接近 SOC / XDR 的操作體驗。", "evidence": "AwoooP 跨 Session 狀態已接線,IwoooS 只讀鏡像與 progress guard 已有證據。", - "next": "補 reviewer 接受紀錄與 owner decision record,才可能進入下一個 runtime gate。", - "locked": "active_runtime_gate_count=0,沒有任何自動執行按鈕。" + "next": "補審查者接受紀錄與負責人決策紀錄,才可能進入下一個執行期閘門。", + "locked": "執行期閘門數為 0,沒有任何自動執行按鈕。" } }, "nodes": { @@ -5512,7 +5545,7 @@ "title": "AwoooP 真相鏈" }, "runtimeGate": { - "title": "Gate 0" + "title": "執行閘 0" } }, "layers": { @@ -5522,11 +5555,11 @@ }, "codeSupply": { "title": "版本來源面", - "body": "GitHub primary / Gitea 遷移仍等 S4.9 證據。" + "body": "GitHub 主來源 / Gitea 遷移仍等 S4.9 證據。" }, "hostFabric": { "title": "主機拓樸面", - "body": "112、111、168 維持 observe-only 顯示。" + "body": "112、111、168 維持只讀觀測顯示。" }, "evidenceOps": { "title": "證據營運面", @@ -5534,13 +5567,13 @@ }, "gateControl": { "title": "執行閘面", - "body": "runtime gate、掃描與修復仍全部鎖住。" + "body": "執行期閘門、掃描與修復仍全部鎖住。" } }, "charts": { "contextDepth": { "label": "關聯深度", - "body": "已把 code-to-runtime 的理解路徑壓成四段。" + "body": "已把程式碼到執行期的理解路徑壓成四段。" }, "blastRadius": { "label": "爆炸半徑", @@ -5548,7 +5581,7 @@ }, "evidenceFreshness": { "label": "證據新鮮度", - "body": "目前真正卡點仍是 S4.9 owner evidence。" + "body": "目前真正卡點仍是 S4.9 負責人證據。" } }, "boundaryTitle": "圖譜邊界", diff --git a/apps/web/messages/zh-TW.json b/apps/web/messages/zh-TW.json index 6586f324..26e0978b 100644 --- a/apps/web/messages/zh-TW.json +++ b/apps/web/messages/zh-TW.json @@ -5450,7 +5450,7 @@ "topologyAtlas": { "eyebrow": "專業架構與拓樸圖譜", "title": "用圖譜看攻擊面、資產關係與證據流", - "subtitle": "把主流資安產品常見的 graph、attack path、blast radius 與 evidence lane 濃縮成四個可切換視角;少文字、多圖表,仍維持 Gate 0。", + "subtitle": "把主流資安產品常見的圖譜、攻擊路徑、爆炸半徑與證據線濃縮成四個可切換視角;少文字、多圖表,仍維持執行閘 0。", "tabsLabel": "架構拓樸圖譜視角", "mapLabel": "圖譜視角", "panelLabels": { @@ -5458,38 +5458,71 @@ "next": "下一步", "locked": "仍鎖住" }, + "nodeDrilldown": { + "eyebrow": "節點下鑽", + "selectorLabel": "切換拓樸節點", + "fields": { + "relation": "關聯", + "evidence": "證據", + "next": "下一步", + "boundary": "邊界" + }, + "nodes": { + "productSurface": { + "body": "把 AwoooI、AwoooP、IwoooS、公開網站與 VibeWork 放在同一個資產面,先看關聯與責任邊界,不直接提高限制。" + }, + "sourceControl": { + "body": "GitHub / Gitea 是版本來源關鍵節點;目前只顯示 S4.9 負責人回覆卡點與決策紀錄缺口,不執行分支參照或工作流程變更。" + }, + "kali": { + "body": "Kali 192.168.0.112 已列為資安主機節點;這裡只呈現它與版本來源、開發主機、證據鏈的關係,不代表已授權掃描。" + }, + "devHosts": { + "body": "192.168.0.111 與 192.168.0.168 以開發主機群呈現,等主機維護窗口與負責人決策紀錄完整後才能進入執行期。" + }, + "monitoring": { + "body": "監控、MCP、Ansible、KM 與告警資料先進入證據線,讓 IwoooS 能解釋訊號來源與新鮮度。" + }, + "awooopTruth": { + "body": "AwoooP 是跨 Session 工作狀態與人工 gate 的真相鏈;IwoooS 只讀消費它,不把顯示狀態當批准。" + }, + "runtimeGate": { + "body": "所有掃描、修復、部署、主機更新與版本來源變更仍集中在執行閘 0;沒有正式決策前不產生執行按鈕。" + } + } + }, "lenses": { "architecture": { "title": "架構分層", - "mapTitle": "Code → Asset → Host → Evidence → Gate", + "mapTitle": "程式碼 → 資產 → 主機 → 證據 → 閘門", "detail": "用五層結構看 IwoooS:產品與網站、版本來源、Kali / 開發主機、監控與 AwoooP、最後才是執行閘。", "evidence": "8 類產品 / 網站、3 台主機、6 條工具鏈已進入同一張只讀圖譜。", - "next": "把 S4.9 owner response 與脫敏證據接成可驗證節點。", - "locked": "架構圖不是 runtime 授權,不代表可以掃描或修復。" + "next": "把 S4.9 負責人回覆與脫敏證據接成可驗證節點。", + "locked": "架構圖不是執行期授權,不代表可以掃描或修復。" }, "topology": { "title": "主機拓樸", - "mapTitle": "112 / 111 / 168 Observe-only Fabric", + "mapTitle": "112 / 111 / 168 只讀觀測主機網", "detail": "把 Kali 112、開發主機 111 / 168、監控工具與 AwoooP 真相鏈放在同一張拓樸圖,而不是分散在長文件裡。", "evidence": "目前只呈現觀測窗口、證據位置與人工 gate,沒有執行 SSH、掃描或主機設定變更。", - "next": "等 runtime gate 與掃描範圍批准後,才把 read-only evidence 轉入受控探測。", + "next": "等執行期閘門與掃描範圍批准後,才把只讀證據轉入受控探測。", "locked": "host_change_authorized=false,scan_authorized=false。" }, "attackSurface": { "title": "攻擊面路徑", - "mapTitle": "External Surface → Source Control → Runtime Boundary", - "detail": "把公開入口、產品、版本來源與 Gate 0 串成攻擊面圖;目標是優先看清可被利用的關聯,不先提高限制。", - "evidence": "目前可看見 8 類資產與 S4.9 版本來源卡點,但 blast radius 仍維持 0,避免誤導成已完成攻防驗證。", - "next": "先完成 GitHub primary / Gitea owner evidence,再讓風險路徑有可信來源。", - "locked": "source_control_mutation_authorized=false,GitHub primary switch 未授權。" + "mapTitle": "外部資產面 → 版本來源 → 執行期邊界", + "detail": "把公開入口、產品、版本來源與執行閘 0 串成攻擊面圖;目標是優先看清可被利用的關聯,不先提高限制。", + "evidence": "目前可看見 8 類資產與 S4.9 版本來源卡點,但爆炸半徑仍維持 0,避免誤導成已完成攻防驗證。", + "next": "先完成 GitHub 主來源 / Gitea 負責人證據,再讓風險路徑有可信來源。", + "locked": "版本來源變更未授權,GitHub 主來源切換未授權。" }, "evidenceFlow": { "title": "證據流", - "mapTitle": "Monitoring → AwoooP Truth Chain → Human Gate", - "detail": "用 evidence lane 表示資料如何被收集、脫敏、審查與回寫;這比單純列文件更接近 SOC / XDR 的操作體驗。", + "mapTitle": "監控 → AwoooP 真相鏈 → 人工閘門", + "detail": "用證據線表示資料如何被收集、脫敏、審查與回寫;這比單純列文件更接近 SOC / XDR 的操作體驗。", "evidence": "AwoooP 跨 Session 狀態已接線,IwoooS 只讀鏡像與 progress guard 已有證據。", - "next": "補 reviewer 接受紀錄與 owner decision record,才可能進入下一個 runtime gate。", - "locked": "active_runtime_gate_count=0,沒有任何自動執行按鈕。" + "next": "補審查者接受紀錄與負責人決策紀錄,才可能進入下一個執行期閘門。", + "locked": "執行期閘門數為 0,沒有任何自動執行按鈕。" } }, "nodes": { @@ -5512,7 +5545,7 @@ "title": "AwoooP 真相鏈" }, "runtimeGate": { - "title": "Gate 0" + "title": "執行閘 0" } }, "layers": { @@ -5522,11 +5555,11 @@ }, "codeSupply": { "title": "版本來源面", - "body": "GitHub primary / Gitea 遷移仍等 S4.9 證據。" + "body": "GitHub 主來源 / Gitea 遷移仍等 S4.9 證據。" }, "hostFabric": { "title": "主機拓樸面", - "body": "112、111、168 維持 observe-only 顯示。" + "body": "112、111、168 維持只讀觀測顯示。" }, "evidenceOps": { "title": "證據營運面", @@ -5534,13 +5567,13 @@ }, "gateControl": { "title": "執行閘面", - "body": "runtime gate、掃描與修復仍全部鎖住。" + "body": "執行期閘門、掃描與修復仍全部鎖住。" } }, "charts": { "contextDepth": { "label": "關聯深度", - "body": "已把 code-to-runtime 的理解路徑壓成四段。" + "body": "已把程式碼到執行期的理解路徑壓成四段。" }, "blastRadius": { "label": "爆炸半徑", @@ -5548,7 +5581,7 @@ }, "evidenceFreshness": { "label": "證據新鮮度", - "body": "目前真正卡點仍是 S4.9 owner evidence。" + "body": "目前真正卡點仍是 S4.9 負責人證據。" } }, "boundaryTitle": "圖譜邊界", diff --git a/apps/web/src/app/[locale]/iwooos/page.tsx b/apps/web/src/app/[locale]/iwooos/page.tsx index 21c4537d..6b5783bc 100644 --- a/apps/web/src/app/[locale]/iwooos/page.tsx +++ b/apps/web/src/app/[locale]/iwooos/page.tsx @@ -197,6 +197,14 @@ type IwoooSImmediateVisualMeshNode = { } type IwoooSTopologyAtlasMode = 'architecture' | 'topology' | 'attackSurface' | 'evidenceFlow' +type IwoooSTopologyNodeKey = + | 'productSurface' + | 'sourceControl' + | 'kali' + | 'devHosts' + | 'monitoring' + | 'awooopTruth' + | 'runtimeGate' type IwoooSTopologyAtlasLens = { key: IwoooSTopologyAtlasMode @@ -206,7 +214,7 @@ type IwoooSTopologyAtlasLens = { } type IwoooSTopologyAtlasNode = { - key: string + key: IwoooSTopologyNodeKey metric: string x: number y: number @@ -229,6 +237,16 @@ type IwoooSTopologyAtlasChart = { tone: 'steady' | 'warn' | 'locked' } +type IwoooSTopologyDrilldownNode = { + key: IwoooSTopologyNodeKey + relationCode: string + evidenceCode: string + nextCode: string + boundaryCode: string + icon: typeof ShieldCheck + tone: 'steady' | 'warn' | 'locked' +} + type IwoooSGateRadarMode = 'visible' | 'blocker' | 'review' | 'locked' type IwoooSGateRadarLane = { @@ -2408,7 +2426,7 @@ const iwooosTopologyAtlasLenses: IwoooSTopologyAtlasLens[] = [ { key: 'architecture', metric: '5層', icon: Boxes, tone: 'steady' }, { key: 'topology', metric: '3主機', icon: Network, tone: 'warn' }, { key: 'attackSurface', metric: '8面', icon: Route, tone: 'warn' }, - { key: 'evidenceFlow', metric: 'Gate 0', icon: Workflow, tone: 'locked' }, + { key: 'evidenceFlow', metric: '閘門 0', icon: Workflow, tone: 'locked' }, ] const iwooosTopologyAtlasNodes: IwoooSTopologyAtlasNode[] = [ @@ -2421,7 +2439,7 @@ const iwooosTopologyAtlasNodes: IwoooSTopologyAtlasNode[] = [ { key: 'runtimeGate', metric: '0', x: 88, y: 24, icon: Lock, tone: 'locked' }, ] -const iwooosTopologyAtlasEdges = [ +const iwooosTopologyAtlasEdges: readonly (readonly [IwoooSTopologyNodeKey, IwoooSTopologyNodeKey])[] = [ ['productSurface', 'sourceControl'], ['sourceControl', 'kali'], ['sourceControl', 'monitoring'], @@ -2430,7 +2448,7 @@ const iwooosTopologyAtlasEdges = [ ['awooopTruth', 'runtimeGate'], ] as const -const iwooosTopologyAtlasCompactPositions: Record = { +const iwooosTopologyAtlasCompactPositions: Record = { productSurface: { x: 28, y: 24 }, sourceControl: { x: 72, y: 24 }, kali: { x: 28, y: 43 }, @@ -2449,23 +2467,97 @@ const iwooosTopologyAtlasLayers: IwoooSTopologyAtlasLayer[] = [ ] const iwooosTopologyAtlasCharts: IwoooSTopologyAtlasChart[] = [ - { key: 'contextDepth', value: '4-hop', percent: 74, icon: Route, tone: 'steady' }, + { key: 'contextDepth', value: '4段', percent: 74, icon: Route, tone: 'steady' }, { key: 'blastRadius', value: '0', percent: 0, icon: Lock, tone: 'locked' }, { key: 'evidenceFreshness', value: 'S4.9', percent: 61, icon: ClipboardCheck, tone: 'warn' }, ] +const iwooosTopologyDrilldownNodes: IwoooSTopologyDrilldownNode[] = [ + { + key: 'productSurface', + relationCode: '8 資產', + evidenceCode: '只讀', + nextCode: 'S4.9', + boundaryCode: '執行閘 0', + icon: Boxes, + tone: 'steady', + }, + { + key: 'sourceControl', + relationCode: 'GitHub / Gitea', + evidenceCode: '負責人回覆 0', + nextCode: '決策紀錄', + boundaryCode: '變更 0', + icon: Code2, + tone: 'warn', + }, + { + key: 'kali', + relationCode: '192.168.0.112', + evidenceCode: '只讀觀測', + nextCode: '範圍批准', + boundaryCode: '掃描 0', + icon: Server, + tone: 'warn', + }, + { + key: 'devHosts', + relationCode: '192.168.0.111 / 168', + evidenceCode: '主機窗口 0', + nextCode: '負責人窗口', + boundaryCode: '變更 0', + icon: Network, + tone: 'warn', + }, + { + key: 'monitoring', + relationCode: '6 工具線', + evidenceCode: '已鏡像', + nextCode: '新鮮度', + boundaryCode: '執行期 0', + icon: Radar, + tone: 'steady', + }, + { + key: 'awooopTruth', + relationCode: '跨 Session', + evidenceCode: '已接線', + nextCode: '審查者', + boundaryCode: '批准 0', + icon: Workflow, + tone: 'steady', + }, + { + key: 'runtimeGate', + relationCode: '人工閘', + evidenceCode: '關閉', + nextCode: '正式決策', + boundaryCode: '執行 0', + icon: Lock, + tone: 'locked', + }, +] + const iwooosTopologyAtlasBoundaries = [ 'iwooos_topology_atlas_first_layer=true', 'iwooos_topology_atlas_lens_count=4', 'iwooos_topology_atlas_node_count=7', + 'iwooos_topology_drilldown_node_count=7', + 'iwooos_topology_drilldown_default_node=productSurface', + 'iwooos_topology_drilldown_interactive_node_allowed=true', 'iwooos_topology_atlas_layer_count=5', 'iwooos_topology_atlas_technical_chart_count=3', 'iwooos_topology_atlas_interactive_lens_allowed=true', 'iwooos_topology_atlas_execution_action_buttons_allowed=false', + 'iwooos_topology_drilldown_execution_action_buttons_allowed=false', 'iwooos_topology_atlas_runtime_gate_count=0', + 'iwooos_topology_drilldown_runtime_gate_count=0', 'iwooos_topology_atlas_scan_authorized=false', 'iwooos_topology_atlas_host_change_authorized=false', 'iwooos_topology_atlas_source_control_mutation_authorized=false', + 'iwooos_topology_drilldown_scan_authorized=false', + 'iwooos_topology_drilldown_host_change_authorized=false', + 'iwooos_topology_drilldown_source_control_mutation_authorized=false', 'runtime_execution_authorized=false', 'active_runtime_gate_count=0', 'action_buttons_allowed=false', @@ -9643,8 +9735,11 @@ function IwoooSImmediateVisualMeshBoard() { function IwoooSTopologyAtlasBoard() { const t = useTranslations('iwooos.topologyAtlas') const [activeKey, setActiveKey] = useState('architecture') + const [activeNodeKey, setActiveNodeKey] = useState('productSurface') const activeLens = iwooosTopologyAtlasLenses.find(item => item.key === activeKey) ?? iwooosTopologyAtlasLenses[0] + const activeNode = iwooosTopologyDrilldownNodes.find(item => item.key === activeNodeKey) ?? iwooosTopologyDrilldownNodes[0] const ActiveIcon = activeLens.icon + const ActiveNodeIcon = activeNode.icon const textWrap = { overflowWrap: 'anywhere' as const, wordBreak: 'break-word' as const } const atlasCanvasRef = useRef(null) const [isCompactAtlas, setIsCompactAtlas] = useState(false) @@ -9654,6 +9749,11 @@ function IwoooSTopologyAtlasBoard() { return compactPosition ? { ...item, ...compactPosition } : item }) const nodeByKey = new Map(topologyAtlasNodes.map(item => [item.key, item])) + const connectedNodeKeys = new Set() + iwooosTopologyAtlasEdges.forEach(([fromKey, toKey]) => { + if (fromKey === activeNodeKey) connectedNodeKeys.add(toKey) + if (toKey === activeNodeKey) connectedNodeKeys.add(fromKey) + }) useEffect(() => { const node = atlasCanvasRef.current @@ -9784,6 +9884,7 @@ function IwoooSTopologyAtlasBoard() { const from = nodeByKey.get(fromKey) const to = nodeByKey.get(toKey) if (!from || !to) return null + const selectedPath = fromKey === activeNodeKey || toKey === activeNodeKey return ( ) })} @@ -9828,10 +9929,15 @@ function IwoooSTopologyAtlasBoard() { {topologyAtlasNodes.map(node => { const Icon = node.icon const active = activeKey === 'architecture' || (activeKey === 'topology' && ['kali', 'devHosts', 'monitoring'].includes(node.key)) || (activeKey === 'attackSurface' && ['productSurface', 'sourceControl', 'runtimeGate'].includes(node.key)) || (activeKey === 'evidenceFlow' && ['monitoring', 'awooopTruth', 'runtimeGate'].includes(node.key)) + const selectedNode = node.key === activeNodeKey + const connectedNode = connectedNodeKeys.has(node.key) return ( -
setActiveNodeKey(node.key)} style={{ position: 'absolute', left: `${node.x}%`, @@ -9839,12 +9945,14 @@ function IwoooSTopologyAtlasBoard() { transform: 'translate(-50%, -50%)', minWidth: 86, maxWidth: 116, - border: `1px solid ${active ? toneColors[node.tone] : 'rgba(255,255,255,0.2)'}`, + border: `1px solid ${selectedNode ? '#ffffff' : active || connectedNode ? toneColors[node.tone] : 'rgba(255,255,255,0.2)'}`, borderRadius: 8, padding: 8, - background: active ? 'rgba(255,255,255,0.95)' : 'rgba(255,255,255,0.78)', + background: selectedNode ? '#fffdf8' : active || connectedNode ? 'rgba(255,255,255,0.95)' : 'rgba(255,255,255,0.72)', color: '#141413', - boxShadow: active ? '0 12px 24px rgba(0,0,0,0.18)' : 'none', + boxShadow: selectedNode ? `0 0 0 3px ${toneColors[node.tone]}66, 0 14px 26px rgba(0,0,0,0.22)` : active || connectedNode ? '0 12px 24px rgba(0,0,0,0.18)' : 'none', + cursor: 'pointer', + textAlign: 'left', }} >
@@ -9854,7 +9962,7 @@ function IwoooSTopologyAtlasBoard() {
{t(`nodes.${node.key}.title` as never)}
-
+ ) })}
@@ -9899,6 +10007,94 @@ function IwoooSTopologyAtlasBoard() { +
+
+
+ + {t('nodeDrilldown.eyebrow')} +
+ + {activeNode.relationCode} + +
+

+ {t(`nodes.${activeNode.key}.title` as never)} +

+
+ {(['relation', 'evidence', 'next', 'boundary'] as const).map(key => ( +
+
+ {t(`nodeDrilldown.fields.${key}`)} +
+
+ {key === 'relation' ? activeNode.relationCode : key === 'evidence' ? activeNode.evidenceCode : key === 'next' ? activeNode.nextCode : activeNode.boundaryCode} +
+
+ ))} +
+

+ {t(`nodeDrilldown.nodes.${activeNode.key}.body` as never)} +

+
+ {iwooosTopologyDrilldownNodes.map(item => { + const Icon = item.icon + const selected = item.key === activeNodeKey + return ( + + ) + })} +
+
+
None: "s2_146_iwooos_immediate_visual_mesh", "s2_147_iwooos_gate_radar_first_layer", "s2_148_iwooos_professional_topology_atlas", + "s2_149_iwooos_topology_node_drilldown", ] assert_equal( "progress_delta_ledger.delta_ids", @@ -1580,6 +1581,20 @@ def validate(root: Path) -> None: iwooos_projection["summary"]["topology_atlas_node_count"], 7, ) + assert_equal( + "iwooos_projection.summary.topology_drilldown_node_count", + iwooos_projection["summary"]["topology_drilldown_node_count"], + 7, + ) + assert_equal( + "iwooos_projection.summary.topology_drilldown_default_node", + iwooos_projection["summary"]["topology_drilldown_default_node"], + "productSurface", + ) + assert_true( + "iwooos_projection.summary.topology_drilldown_interactive_node_allowed", + iwooos_projection["summary"]["topology_drilldown_interactive_node_allowed"], + ) assert_equal( "iwooos_projection.summary.topology_atlas_layer_count", iwooos_projection["summary"]["topology_atlas_layer_count"], @@ -1603,6 +1618,11 @@ def validate(root: Path) -> None: iwooos_projection["summary"]["topology_atlas_runtime_gate_count"], 0, ) + assert_equal( + "iwooos_projection.summary.topology_drilldown_runtime_gate_count", + iwooos_projection["summary"]["topology_drilldown_runtime_gate_count"], + 0, + ) assert_true( "iwooos_projection.summary.gate_radar_first_layer", iwooos_projection["summary"]["gate_radar_first_layer"], @@ -11999,12 +12019,18 @@ def validate(root: Path) -> None: 'data-testid="iwooos-topology-atlas-board"', 'data-testid="iwooos-topology-atlas-canvas"', 'data-testid="iwooos-topology-atlas-active-panel"', + 'data-testid="iwooos-topology-atlas-node-drilldown"', + 'data-testid="iwooos-topology-atlas-node-drilldown-metrics"', + 'data-testid="iwooos-topology-atlas-node-drilldown-selector"', 'data-testid="iwooos-topology-atlas-technical-charts"', 'data-testid="iwooos-topology-atlas-boundaries"', "IwoooSTopologyAtlasBoard", "IwoooSTopologyAtlasMode", + "IwoooSTopologyNodeKey", + "IwoooSTopologyDrilldownNode", "iwooosTopologyAtlasLenses", "iwooosTopologyAtlasNodes", + "iwooosTopologyDrilldownNodes", "iwooosTopologyAtlasLayers", "iwooosTopologyAtlasCharts", "iwooosTopologyAtlasBoundaries", @@ -12037,14 +12063,22 @@ def validate(root: Path) -> None: "iwooos_topology_atlas_first_layer=true", "iwooos_topology_atlas_lens_count=4", "iwooos_topology_atlas_node_count=7", + "iwooos_topology_drilldown_node_count=7", + "iwooos_topology_drilldown_default_node=productSurface", + "iwooos_topology_drilldown_interactive_node_allowed=true", "iwooos_topology_atlas_layer_count=5", "iwooos_topology_atlas_technical_chart_count=3", "iwooos_topology_atlas_interactive_lens_allowed=true", "iwooos_topology_atlas_execution_action_buttons_allowed=false", + "iwooos_topology_drilldown_execution_action_buttons_allowed=false", "iwooos_topology_atlas_runtime_gate_count=0", + "iwooos_topology_drilldown_runtime_gate_count=0", "iwooos_topology_atlas_scan_authorized=false", "iwooos_topology_atlas_host_change_authorized=false", "iwooos_topology_atlas_source_control_mutation_authorized=false", + "iwooos_topology_drilldown_scan_authorized=false", + "iwooos_topology_drilldown_host_change_authorized=false", + "iwooos_topology_drilldown_source_control_mutation_authorized=false", "runtime_execution_authorized=false", "active_runtime_gate_count=0", "action_buttons_allowed=false", @@ -12475,6 +12509,59 @@ def validate(root: Path) -> None: "iwooos_projection.topology_atlas_technical_charts.blastRadius.blast_radius_verified", blast_radius_chart["blast_radius_verified"], ) + expected_topology_drilldown_node_ids = [ + "productSurface", + "sourceControl", + "kali", + "devHosts", + "monitoring", + "awooopTruth", + "runtimeGate", + ] + topology_drilldown_nodes = iwooos_projection["topology_drilldown_nodes"] + assert_equal( + "iwooos_projection.topology_drilldown_nodes.ids", + [item["node_id"] for item in topology_drilldown_nodes], + expected_topology_drilldown_node_ids, + ) + assert_equal( + "iwooos_projection.topology_drilldown_nodes.display_order", + [item["display_order"] for item in topology_drilldown_nodes], + list(range(1, len(expected_topology_drilldown_node_ids) + 1)), + ) + for item in topology_drilldown_nodes: + assert_true( + f"iwooos_projection.topology_drilldown_nodes.{item['node_id']}.interactive_node_allowed", + item["interactive_node_allowed"], + ) + assert_false( + f"iwooos_projection.topology_drilldown_nodes.{item['node_id']}.execution_action_button_allowed", + item["execution_action_button_allowed"], + ) + assert_false( + f"iwooos_projection.topology_drilldown_nodes.{item['node_id']}.scan_authorized", + item["scan_authorized"], + ) + assert_false( + f"iwooos_projection.topology_drilldown_nodes.{item['node_id']}.host_change_authorized", + item["host_change_authorized"], + ) + assert_false( + f"iwooos_projection.topology_drilldown_nodes.{item['node_id']}.source_control_mutation_authorized", + item["source_control_mutation_authorized"], + ) + assert_false( + f"iwooos_projection.topology_drilldown_nodes.{item['node_id']}.runtime_gate_opened", + item["runtime_gate_opened"], + ) + assert_false( + f"iwooos_projection.topology_drilldown_nodes.{item['node_id']}.runtime_execution_authorized", + item["runtime_execution_authorized"], + ) + assert_true( + f"iwooos_projection.topology_drilldown_nodes.{item['node_id']}.not_authorization", + item["not_authorization"], + ) expected_gate_radar_lane_ids = [ "visible", "blocker", @@ -12808,6 +12895,7 @@ def validate(root: Path) -> None: "tabsLabel", "mapLabel", "panelLabels", + "nodeDrilldown", "lenses", "nodes", "layers", @@ -12836,6 +12924,49 @@ def validate(root: Path) -> None: list(web_messages_en["iwooos"]["topologyAtlas"]["panelLabels"].keys()), key, ) + for key in ["eyebrow", "selectorLabel", "fields", "nodes"]: + assert_contains( + "web_messages.zh-TW.iwooos.topologyAtlas.nodeDrilldown.keys", + list(web_messages_zh["iwooos"]["topologyAtlas"]["nodeDrilldown"].keys()), + key, + ) + assert_contains( + "web_messages.en.iwooos.topologyAtlas.nodeDrilldown.keys", + list(web_messages_en["iwooos"]["topologyAtlas"]["nodeDrilldown"].keys()), + key, + ) + for key in ["relation", "evidence", "next", "boundary"]: + assert_contains( + "web_messages.zh-TW.iwooos.topologyAtlas.nodeDrilldown.fields", + list(web_messages_zh["iwooos"]["topologyAtlas"]["nodeDrilldown"]["fields"].keys()), + key, + ) + assert_contains( + "web_messages.en.iwooos.topologyAtlas.nodeDrilldown.fields", + list(web_messages_en["iwooos"]["topologyAtlas"]["nodeDrilldown"]["fields"].keys()), + key, + ) + for key in ["productSurface", "sourceControl", "kali", "devHosts", "monitoring", "awooopTruth", "runtimeGate"]: + assert_contains( + "web_messages.zh-TW.iwooos.topologyAtlas.nodeDrilldown.nodes", + list(web_messages_zh["iwooos"]["topologyAtlas"]["nodeDrilldown"]["nodes"].keys()), + key, + ) + assert_contains( + "web_messages.en.iwooos.topologyAtlas.nodeDrilldown.nodes", + list(web_messages_en["iwooos"]["topologyAtlas"]["nodeDrilldown"]["nodes"].keys()), + key, + ) + assert_contains( + f"web_messages.zh-TW.iwooos.topologyAtlas.nodeDrilldown.nodes.{key}", + list(web_messages_zh["iwooos"]["topologyAtlas"]["nodeDrilldown"]["nodes"][key].keys()), + "body", + ) + assert_contains( + f"web_messages.en.iwooos.topologyAtlas.nodeDrilldown.nodes.{key}", + list(web_messages_en["iwooos"]["topologyAtlas"]["nodeDrilldown"]["nodes"][key].keys()), + "body", + ) for key in ["architecture", "topology", "attackSurface", "evidenceFlow"]: assert_contains( "web_messages.zh-TW.iwooos.topologyAtlas.lenses",