diff --git a/apps/web/messages/en.json b/apps/web/messages/en.json index 4631fd31..6586f324 100644 --- a/apps/web/messages/en.json +++ b/apps/web/messages/en.json @@ -5447,6 +5447,113 @@ "boundaryTitle": "視覺模型邊界", "boundaryIntro": "這張圖是首屏理解模型,不是操作面板;所有高風險動作仍需人工批准與後續 runtime gate。" }, + "topologyAtlas": { + "eyebrow": "專業架構與拓樸圖譜", + "title": "用圖譜看攻擊面、資產關係與證據流", + "subtitle": "把主流資安產品常見的 graph、attack path、blast radius 與 evidence lane 濃縮成四個可切換視角;少文字、多圖表,仍維持 Gate 0。", + "tabsLabel": "架構拓樸圖譜視角", + "mapLabel": "圖譜視角", + "panelLabels": { + "evidence": "目前可見", + "next": "下一步", + "locked": "仍鎖住" + }, + "lenses": { + "architecture": { + "title": "架構分層", + "mapTitle": "Code → Asset → Host → Evidence → Gate", + "detail": "用五層結構看 IwoooS:產品與網站、版本來源、Kali / 開發主機、監控與 AwoooP、最後才是執行閘。", + "evidence": "8 類產品 / 網站、3 台主機、6 條工具鏈已進入同一張只讀圖譜。", + "next": "把 S4.9 owner response 與脫敏證據接成可驗證節點。", + "locked": "架構圖不是 runtime 授權,不代表可以掃描或修復。" + }, + "topology": { + "title": "主機拓樸", + "mapTitle": "112 / 111 / 168 Observe-only Fabric", + "detail": "把 Kali 112、開發主機 111 / 168、監控工具與 AwoooP 真相鏈放在同一張拓樸圖,而不是分散在長文件裡。", + "evidence": "目前只呈現觀測窗口、證據位置與人工 gate,沒有執行 SSH、掃描或主機設定變更。", + "next": "等 runtime gate 與掃描範圍批准後,才把 read-only evidence 轉入受控探測。", + "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 未授權。" + }, + "evidenceFlow": { + "title": "證據流", + "mapTitle": "Monitoring → AwoooP Truth Chain → Human Gate", + "detail": "用 evidence lane 表示資料如何被收集、脫敏、審查與回寫;這比單純列文件更接近 SOC / XDR 的操作體驗。", + "evidence": "AwoooP 跨 Session 狀態已接線,IwoooS 只讀鏡像與 progress guard 已有證據。", + "next": "補 reviewer 接受紀錄與 owner decision record,才可能進入下一個 runtime gate。", + "locked": "active_runtime_gate_count=0,沒有任何自動執行按鈕。" + } + }, + "nodes": { + "productSurface": { + "title": "產品 / 網站" + }, + "sourceControl": { + "title": "版本來源" + }, + "kali": { + "title": "Kali 112" + }, + "devHosts": { + "title": "開發主機" + }, + "monitoring": { + "title": "監控 / 工具" + }, + "awooopTruth": { + "title": "AwoooP 真相鏈" + }, + "runtimeGate": { + "title": "Gate 0" + } + }, + "layers": { + "externalSurface": { + "title": "外部資產面", + "body": "公開網站、產品入口與 VibeWork 先進入可理解範圍。" + }, + "codeSupply": { + "title": "版本來源面", + "body": "GitHub primary / Gitea 遷移仍等 S4.9 證據。" + }, + "hostFabric": { + "title": "主機拓樸面", + "body": "112、111、168 維持 observe-only 顯示。" + }, + "evidenceOps": { + "title": "證據營運面", + "body": "監控、KM、MCP、Ansible 與 AwoooP 對齊。" + }, + "gateControl": { + "title": "執行閘面", + "body": "runtime gate、掃描與修復仍全部鎖住。" + } + }, + "charts": { + "contextDepth": { + "label": "關聯深度", + "body": "已把 code-to-runtime 的理解路徑壓成四段。" + }, + "blastRadius": { + "label": "爆炸半徑", + "body": "未授權探測前維持 0,不誤導成已驗證攻擊半徑。" + }, + "evidenceFreshness": { + "label": "證據新鮮度", + "body": "目前真正卡點仍是 S4.9 owner evidence。" + } + }, + "boundaryTitle": "圖譜邊界", + "boundaryIntro": "這張圖是專業可視化與理解層,不是掃描拓樸、不是自動修復、不是 GitHub / Gitea 切換授權。" + }, "gateRadar": { "eyebrow": "執行閘雷達", "title": "一眼看懂哪裡可前進、哪裡不能碰", diff --git a/apps/web/messages/zh-TW.json b/apps/web/messages/zh-TW.json index 4631fd31..6586f324 100644 --- a/apps/web/messages/zh-TW.json +++ b/apps/web/messages/zh-TW.json @@ -5447,6 +5447,113 @@ "boundaryTitle": "視覺模型邊界", "boundaryIntro": "這張圖是首屏理解模型,不是操作面板;所有高風險動作仍需人工批准與後續 runtime gate。" }, + "topologyAtlas": { + "eyebrow": "專業架構與拓樸圖譜", + "title": "用圖譜看攻擊面、資產關係與證據流", + "subtitle": "把主流資安產品常見的 graph、attack path、blast radius 與 evidence lane 濃縮成四個可切換視角;少文字、多圖表,仍維持 Gate 0。", + "tabsLabel": "架構拓樸圖譜視角", + "mapLabel": "圖譜視角", + "panelLabels": { + "evidence": "目前可見", + "next": "下一步", + "locked": "仍鎖住" + }, + "lenses": { + "architecture": { + "title": "架構分層", + "mapTitle": "Code → Asset → Host → Evidence → Gate", + "detail": "用五層結構看 IwoooS:產品與網站、版本來源、Kali / 開發主機、監控與 AwoooP、最後才是執行閘。", + "evidence": "8 類產品 / 網站、3 台主機、6 條工具鏈已進入同一張只讀圖譜。", + "next": "把 S4.9 owner response 與脫敏證據接成可驗證節點。", + "locked": "架構圖不是 runtime 授權,不代表可以掃描或修復。" + }, + "topology": { + "title": "主機拓樸", + "mapTitle": "112 / 111 / 168 Observe-only Fabric", + "detail": "把 Kali 112、開發主機 111 / 168、監控工具與 AwoooP 真相鏈放在同一張拓樸圖,而不是分散在長文件裡。", + "evidence": "目前只呈現觀測窗口、證據位置與人工 gate,沒有執行 SSH、掃描或主機設定變更。", + "next": "等 runtime gate 與掃描範圍批准後,才把 read-only evidence 轉入受控探測。", + "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 未授權。" + }, + "evidenceFlow": { + "title": "證據流", + "mapTitle": "Monitoring → AwoooP Truth Chain → Human Gate", + "detail": "用 evidence lane 表示資料如何被收集、脫敏、審查與回寫;這比單純列文件更接近 SOC / XDR 的操作體驗。", + "evidence": "AwoooP 跨 Session 狀態已接線,IwoooS 只讀鏡像與 progress guard 已有證據。", + "next": "補 reviewer 接受紀錄與 owner decision record,才可能進入下一個 runtime gate。", + "locked": "active_runtime_gate_count=0,沒有任何自動執行按鈕。" + } + }, + "nodes": { + "productSurface": { + "title": "產品 / 網站" + }, + "sourceControl": { + "title": "版本來源" + }, + "kali": { + "title": "Kali 112" + }, + "devHosts": { + "title": "開發主機" + }, + "monitoring": { + "title": "監控 / 工具" + }, + "awooopTruth": { + "title": "AwoooP 真相鏈" + }, + "runtimeGate": { + "title": "Gate 0" + } + }, + "layers": { + "externalSurface": { + "title": "外部資產面", + "body": "公開網站、產品入口與 VibeWork 先進入可理解範圍。" + }, + "codeSupply": { + "title": "版本來源面", + "body": "GitHub primary / Gitea 遷移仍等 S4.9 證據。" + }, + "hostFabric": { + "title": "主機拓樸面", + "body": "112、111、168 維持 observe-only 顯示。" + }, + "evidenceOps": { + "title": "證據營運面", + "body": "監控、KM、MCP、Ansible 與 AwoooP 對齊。" + }, + "gateControl": { + "title": "執行閘面", + "body": "runtime gate、掃描與修復仍全部鎖住。" + } + }, + "charts": { + "contextDepth": { + "label": "關聯深度", + "body": "已把 code-to-runtime 的理解路徑壓成四段。" + }, + "blastRadius": { + "label": "爆炸半徑", + "body": "未授權探測前維持 0,不誤導成已驗證攻擊半徑。" + }, + "evidenceFreshness": { + "label": "證據新鮮度", + "body": "目前真正卡點仍是 S4.9 owner evidence。" + } + }, + "boundaryTitle": "圖譜邊界", + "boundaryIntro": "這張圖是專業可視化與理解層,不是掃描拓樸、不是自動修復、不是 GitHub / Gitea 切換授權。" + }, "gateRadar": { "eyebrow": "執行閘雷達", "title": "一眼看懂哪裡可前進、哪裡不能碰", diff --git a/apps/web/src/app/[locale]/iwooos/page.tsx b/apps/web/src/app/[locale]/iwooos/page.tsx index 8941a217..21c4537d 100644 --- a/apps/web/src/app/[locale]/iwooos/page.tsx +++ b/apps/web/src/app/[locale]/iwooos/page.tsx @@ -9,17 +9,25 @@ import { Activity, AlertTriangle, Bell, + Boxes, CheckCircle2, ClipboardCheck, + Cloud, + Code2, + Database, Clock3, FileText, FileWarning, GitBranch, ListChecks, Lock, + Network, Radar, + Route, SearchCheck, + Server, ShieldCheck, + Workflow, } from 'lucide-react' import Link from 'next/link' import { useTranslations } from 'next-intl' @@ -188,6 +196,39 @@ type IwoooSImmediateVisualMeshNode = { tone: 'steady' | 'warn' | 'locked' } +type IwoooSTopologyAtlasMode = 'architecture' | 'topology' | 'attackSurface' | 'evidenceFlow' + +type IwoooSTopologyAtlasLens = { + key: IwoooSTopologyAtlasMode + metric: string + icon: typeof ShieldCheck + tone: 'steady' | 'warn' | 'locked' +} + +type IwoooSTopologyAtlasNode = { + key: string + metric: string + x: number + y: number + icon: typeof ShieldCheck + tone: 'steady' | 'warn' | 'locked' +} + +type IwoooSTopologyAtlasLayer = { + key: string + metric: string + icon: typeof ShieldCheck + tone: 'steady' | 'warn' | 'locked' +} + +type IwoooSTopologyAtlasChart = { + key: string + value: string + percent: number + icon: typeof ShieldCheck + tone: 'steady' | 'warn' | 'locked' +} + type IwoooSGateRadarMode = 'visible' | 'blocker' | 'review' | 'locked' type IwoooSGateRadarLane = { @@ -2363,6 +2404,74 @@ const iwooosImmediateVisualMeshBoundaries = [ 'not_authorization=true', ] as const +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' }, +] + +const iwooosTopologyAtlasNodes: IwoooSTopologyAtlasNode[] = [ + { key: 'productSurface', metric: '8', x: 14, y: 30, icon: Boxes, tone: 'steady' }, + { key: 'sourceControl', metric: 'S4.9', x: 35, y: 18, icon: Code2, tone: 'warn' }, + { key: 'kali', metric: '112', x: 58, y: 30, icon: Server, tone: 'warn' }, + { key: 'devHosts', metric: '111 / 168', x: 77, y: 50, icon: Network, tone: 'warn' }, + { key: 'monitoring', metric: '6', x: 37, y: 66, icon: Radar, tone: 'steady' }, + { key: 'awooopTruth', metric: '已接線', x: 62, y: 78, icon: Workflow, tone: 'steady' }, + { key: 'runtimeGate', metric: '0', x: 88, y: 24, icon: Lock, tone: 'locked' }, +] + +const iwooosTopologyAtlasEdges = [ + ['productSurface', 'sourceControl'], + ['sourceControl', 'kali'], + ['sourceControl', 'monitoring'], + ['kali', 'devHosts'], + ['monitoring', 'awooopTruth'], + ['awooopTruth', 'runtimeGate'], +] as const + +const iwooosTopologyAtlasCompactPositions: Record = { + productSurface: { x: 28, y: 24 }, + sourceControl: { x: 72, y: 24 }, + kali: { x: 28, y: 43 }, + devHosts: { x: 72, y: 43 }, + monitoring: { x: 28, y: 63 }, + awooopTruth: { x: 72, y: 63 }, + runtimeGate: { x: 50, y: 82 }, +} + +const iwooosTopologyAtlasLayers: IwoooSTopologyAtlasLayer[] = [ + { key: 'externalSurface', metric: '8', icon: Cloud, tone: 'steady' }, + { key: 'codeSupply', metric: 'S4.9', icon: GitBranch, tone: 'warn' }, + { key: 'hostFabric', metric: '3', icon: Server, tone: 'warn' }, + { key: 'evidenceOps', metric: '6', icon: Database, tone: 'steady' }, + { key: 'gateControl', metric: '0', icon: Lock, tone: 'locked' }, +] + +const iwooosTopologyAtlasCharts: IwoooSTopologyAtlasChart[] = [ + { key: 'contextDepth', value: '4-hop', 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 iwooosTopologyAtlasBoundaries = [ + 'iwooos_topology_atlas_first_layer=true', + 'iwooos_topology_atlas_lens_count=4', + 'iwooos_topology_atlas_node_count=7', + '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_atlas_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', + 'runtime_execution_authorized=false', + 'active_runtime_gate_count=0', + 'action_buttons_allowed=false', + 'not_authorization=true', +] as const + const iwooosGateRadarLanes: IwoooSGateRadarLane[] = [ { key: 'visible', metric: '8/8', score: 86, icon: ShieldCheck, tone: 'steady' }, { key: 'blocker', metric: 'S4.9', score: 61, icon: FileWarning, tone: 'warn' }, @@ -9531,6 +9640,374 @@ function IwoooSImmediateVisualMeshBoard() { ) } +function IwoooSTopologyAtlasBoard() { + const t = useTranslations('iwooos.topologyAtlas') + const [activeKey, setActiveKey] = useState('architecture') + const activeLens = iwooosTopologyAtlasLenses.find(item => item.key === activeKey) ?? iwooosTopologyAtlasLenses[0] + const ActiveIcon = activeLens.icon + const textWrap = { overflowWrap: 'anywhere' as const, wordBreak: 'break-word' as const } + const atlasCanvasRef = useRef(null) + const [isCompactAtlas, setIsCompactAtlas] = useState(false) + const topologyAtlasNodes = iwooosTopologyAtlasNodes.map(item => { + if (!isCompactAtlas) return item + const compactPosition = iwooosTopologyAtlasCompactPositions[item.key] + return compactPosition ? { ...item, ...compactPosition } : item + }) + const nodeByKey = new Map(topologyAtlasNodes.map(item => [item.key, item])) + + useEffect(() => { + const node = atlasCanvasRef.current + if (!node) return + + const updateLayout = () => setIsCompactAtlas(node.getBoundingClientRect().width < 520) + updateLayout() + + const observer = new ResizeObserver(updateLayout) + observer.observe(node) + return () => observer.disconnect() + }, []) + + return ( +
+
+
+
+
+ + {t('eyebrow')} +
+

{t('title')}

+

+ {t('subtitle')} +

+
+ +
+ {iwooosTopologyAtlasLenses.map(item => { + const Icon = item.icon + const selected = activeKey === item.key + return ( + + ) + })} +
+
+ +
+
+ + + + + + + + + + + + + + + {iwooosTopologyAtlasEdges.map(([fromKey, toKey]) => { + const from = nodeByKey.get(fromKey) + const to = nodeByKey.get(toKey) + if (!from || !to) return null + return ( + + ) + })} + + +
+
+
{t('mapLabel')}
+
+ {t(`lenses.${activeLens.key}.mapTitle` as never)} +
+
+
+ +
+
+ + {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)) + return ( +
+
+ + {node.metric} +
+
+ {t(`nodes.${node.key}.title` as never)} +
+
+ ) + })} +
+ +
+
+
+ + {activeLens.metric} +
+

+ {t(`lenses.${activeLens.key}.title` as never)} +

+

+ {t(`lenses.${activeLens.key}.detail` as never)} +

+
+ {(['evidence', 'next', 'locked'] as const).map(key => ( +
+
{t(`panelLabels.${key}`)}
+
+ {t(`lenses.${activeLens.key}.${key}` as never)} +
+
+ ))} +
+
+ +
+ {iwooosTopologyAtlasLayers.map(item => { + const Icon = item.icon + return ( +
+
+ + {item.metric} +
+
+ {t(`layers.${item.key}.title` as never)} +
+
+ {t(`layers.${item.key}.body` as never)} +
+
+ ) + })} +
+
+
+ +
+ {iwooosTopologyAtlasCharts.map(item => { + const Icon = item.icon + return ( +
+
+
+ + + {t(`charts.${item.key}.label` as never)} + +
+ {item.value} +
+
+
+
+

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

+
+ ) + })} +
+ +
+ + {t('boundaryTitle')} + +

+ {t('boundaryIntro')} +

+
+ {iwooosTopologyAtlasBoundaries.map(item => ( + + {item} + + ))} +
+
+
+
+ ) +} + function IwoooSGateRadarBoard() { const t = useTranslations('iwooos.gateRadar') const [activeKey, setActiveKey] = useState('visible') @@ -14655,6 +15132,7 @@ export default function IwoooSPage({ params }: { params: { locale: string } }) { + diff --git a/docs/LOGBOOK.md b/docs/LOGBOOK.md index 980f7171..e7f29ca9 100644 --- a/docs/LOGBOOK.md +++ b/docs/LOGBOOK.md @@ -1,3 +1,31 @@ +## 2026-06-01|IwoooS 專業架構與拓樸圖譜 + +**背景**: + +- 使用者指出 IwoooS 不能只用大量文字或交差式圖卡呈現資安工作。 +- 本輪參考主流資安產品常見的 graph / attack path / blast radius / evidence lane 體驗,將架構圖、拓樸圖與技術圖表做得更細緻。 + +**本次調整**: + +- `apps/web/src/app/[locale]/iwooos/page.tsx`: + - 新增 `IwoooSTopologyAtlasBoard`,放在首屏資安網視覺模型之後、執行閘雷達之前。 + - 提供四個可切換視角:架構分層、主機拓樸、攻擊面路徑、證據流。 + - 新增 graph canvas,呈現產品 / 網站、版本來源、Kali 112、開發主機 111 / 168、監控工具、AwoooP 真相鏈與 Gate 0 的關係。 + - 新增 5 個圖層卡與 3 個技術圖表:關聯深度、爆炸半徑、證據新鮮度。 +- `apps/web/messages/zh-TW.json` / `apps/web/messages/en.json`: + - 新增 `iwooos.topologyAtlas` 繁中文案;`en.json` 維持繁中鏡像,確保網站內容全部以繁體中文呈現。 +- `docs/security/iwooos-posture-projection.snapshot.json` / `security-mirror-status-rollup.snapshot.json`: + - 補上 `topology_atlas_first_layer=true`、`topology_atlas_lens_count=4`、`topology_atlas_node_count=7`、`topology_atlas_technical_chart_count=3`。 + - 新增 `S2.148` 進度 ledger;headline 不增加,因為這是 framework / UX / 可理解度提升,不是 runtime gate。 +- `scripts/security/security-mirror-progress-guard.py`: + - 新增 guard,鎖住圖譜位置、四個視角、7 個節點、3 個技術圖表、繁中文案與 runtime false 邊界。 + +**進度邊界**: + +- 整體維持 `61%`。 +- `active_runtime_gate_count=0`、`runtime_execution_authorized=false`。 +- 未執行 Kali 掃描、未 SSH 變更 112 / 111 / 168、未開啟自動修復、未做 GitHub primary / Gitea 切換。 + ## 2026-06-01|IwoooS 執行閘雷達 **背景**: diff --git a/docs/security/iwooos-posture-projection.snapshot.json b/docs/security/iwooos-posture-projection.snapshot.json index 25eff043..12c413dd 100644 --- a/docs/security/iwooos-posture-projection.snapshot.json +++ b/docs/security/iwooos-posture-projection.snapshot.json @@ -145,7 +145,15 @@ "progress_acceleration_lane_count": 6, "owner_response_next_action_focus_item_count": 4, "s4_9_owner_response_preflight_check_count": 6, - "s4_9_owner_response_request_template_count": 5 + "s4_9_owner_response_request_template_count": 5, + "topology_atlas_first_layer": true, + "topology_atlas_lens_count": 4, + "topology_atlas_node_count": 7, + "topology_atlas_layer_count": 5, + "topology_atlas_technical_chart_count": 3, + "topology_atlas_interactive_lens_allowed": true, + "topology_atlas_above_gate_radar": true, + "topology_atlas_runtime_gate_count": 0 }, "progress": { "overall_percent": 61, @@ -7437,5 +7445,171 @@ "action_buttons_allowed": false, "not_authorization": true } + ], + "topology_atlas_lenses": [ + { + "lens_id": "architecture", + "display_order": 1, + "display_mode": "first_screen_professional_topology_atlas", + "metric": "5層", + "graph_pattern": "code_to_asset_to_host_to_evidence_to_gate", + "interactive_lens_allowed": true, + "execution_action_button_allowed": false, + "runtime_gate_opened": false, + "runtime_execution_authorized": false, + "not_authorization": true + }, + { + "lens_id": "topology", + "display_order": 2, + "display_mode": "first_screen_professional_topology_atlas", + "metric": "3主機", + "graph_pattern": "observe_only_host_fabric", + "interactive_lens_allowed": true, + "execution_action_button_allowed": false, + "scan_authorized": false, + "host_change_authorized": false, + "runtime_gate_opened": false, + "runtime_execution_authorized": false, + "not_authorization": true + }, + { + "lens_id": "attackSurface", + "display_order": 3, + "display_mode": "first_screen_professional_topology_atlas", + "metric": "8面", + "graph_pattern": "external_surface_to_source_control_to_runtime_boundary", + "interactive_lens_allowed": true, + "execution_action_button_allowed": false, + "source_control_mutation_authorized": false, + "github_primary_switch_authorized": false, + "blast_radius_verified": false, + "runtime_gate_opened": false, + "runtime_execution_authorized": false, + "not_authorization": true + }, + { + "lens_id": "evidenceFlow", + "display_order": 4, + "display_mode": "first_screen_professional_topology_atlas", + "metric": "Gate 0", + "graph_pattern": "monitoring_to_awooop_truth_chain_to_human_gate", + "interactive_lens_allowed": true, + "execution_action_button_allowed": false, + "owner_response_received_count": 0, + "reviewer_accepted_count": 0, + "runtime_gate_opened": false, + "runtime_execution_authorized": false, + "not_authorization": true + } + ], + "topology_atlas_nodes": [ + { + "node_id": "productSurface", + "display_order": 1, + "node_type": "asset_surface", + "metric": "8", + "read_only": true, + "runtime_gate_opened": false, + "runtime_execution_authorized": false, + "not_authorization": true + }, + { + "node_id": "sourceControl", + "display_order": 2, + "node_type": "source_control", + "metric": "S4.9", + "read_only": true, + "source_control_mutation_authorized": false, + "runtime_gate_opened": false, + "runtime_execution_authorized": false, + "not_authorization": true + }, + { + "node_id": "kali", + "display_order": 3, + "node_type": "kali_host", + "metric": "112", + "read_only": true, + "scan_authorized": false, + "host_change_authorized": false, + "runtime_gate_opened": false, + "runtime_execution_authorized": false, + "not_authorization": true + }, + { + "node_id": "devHosts", + "display_order": 4, + "node_type": "developer_hosts", + "metric": "111/168", + "read_only": true, + "scan_authorized": false, + "host_change_authorized": false, + "runtime_gate_opened": false, + "runtime_execution_authorized": false, + "not_authorization": true + }, + { + "node_id": "monitoring", + "display_order": 5, + "node_type": "monitoring_tools", + "metric": "6", + "read_only": true, + "runtime_gate_opened": false, + "runtime_execution_authorized": false, + "not_authorization": true + }, + { + "node_id": "awooopTruth", + "display_order": 6, + "node_type": "truth_chain", + "metric": "已接線", + "read_only": true, + "runtime_gate_opened": false, + "runtime_execution_authorized": false, + "not_authorization": true + }, + { + "node_id": "runtimeGate", + "display_order": 7, + "node_type": "runtime_gate", + "metric": "0", + "read_only": true, + "scan_authorized": false, + "runtime_gate_opened": false, + "runtime_execution_authorized": false, + "not_authorization": true + } + ], + "topology_atlas_technical_charts": [ + { + "chart_id": "contextDepth", + "display_order": 1, + "display_mode": "first_screen_professional_topology_atlas", + "value": "4-hop", + "percent": 74, + "runtime_delta": false, + "not_authorization": true + }, + { + "chart_id": "blastRadius", + "display_order": 2, + "display_mode": "first_screen_professional_topology_atlas", + "value": "0", + "percent": 0, + "runtime_delta": false, + "blast_radius_verified": false, + "not_authorization": true + }, + { + "chart_id": "evidenceFreshness", + "display_order": 3, + "display_mode": "first_screen_professional_topology_atlas", + "value": "S4.9", + "percent": 61, + "runtime_delta": false, + "owner_response_received_count": 0, + "not_authorization": true + } ] } diff --git a/docs/security/security-mirror-status-rollup.snapshot.json b/docs/security/security-mirror-status-rollup.snapshot.json index 2d0fc77b..2a6430dc 100644 --- a/docs/security/security-mirror-status-rollup.snapshot.json +++ b/docs/security/security-mirror-status-rollup.snapshot.json @@ -2245,6 +2245,18 @@ "runtime_delta": false, "execution_authorized": false, "not_authorization": true + }, + { + "delta_id": "s2_148_iwooos_professional_topology_atlas", + "display_order": 177, + "completed_stage": "S2.148 IwoooS 專業架構與拓樸圖譜", + "progress_axis": "framework_detail", + "headline_percent_delta": 0, + "framework_delta_visible": true, + "why_headline_unchanged": "IwoooS 只新增專業架構與拓樸圖譜,把架構分層、主機拓樸、攻擊面路徑與證據流做成四個 graph lens,搭配 7 個節點、5 個圖層與 3 個技術圖表;iwooos_topology_atlas_lens_count=4、iwooos_topology_atlas_node_count=7、iwooos_topology_atlas_layer_count=5、iwooos_topology_atlas_technical_chart_count=3、iwooos_topology_atlas_execution_action_buttons_allowed=false、iwooos_topology_atlas_runtime_gate_count=0、runtime_execution_authorized=false、active_runtime_gate_count=0,不把圖譜專業化當掃描、修復、部署、主機更新、source-control mutation、GitHub primary 切換、Gitea 停用或 runtime gate。", + "runtime_delta": false, + "execution_authorized": false, + "not_authorization": true } ], "next_safe_actions": [ diff --git a/scripts/security/security-mirror-progress-guard.py b/scripts/security/security-mirror-progress-guard.py index 6cba0760..5cc4bf85 100755 --- a/scripts/security/security-mirror-progress-guard.py +++ b/scripts/security/security-mirror-progress-guard.py @@ -612,6 +612,7 @@ def validate(root: Path) -> None: "s2_145_iwooos_first_layer_focus_deck", "s2_146_iwooos_immediate_visual_mesh", "s2_147_iwooos_gate_radar_first_layer", + "s2_148_iwooos_professional_topology_atlas", ] assert_equal( "progress_delta_ledger.delta_ids", @@ -1565,6 +1566,43 @@ def validate(root: Path) -> None: iwooos_projection["summary"]["immediate_visual_mesh_runtime_gate_count"], 0, ) + assert_true( + "iwooos_projection.summary.topology_atlas_first_layer", + iwooos_projection["summary"]["topology_atlas_first_layer"], + ) + assert_equal( + "iwooos_projection.summary.topology_atlas_lens_count", + iwooos_projection["summary"]["topology_atlas_lens_count"], + 4, + ) + assert_equal( + "iwooos_projection.summary.topology_atlas_node_count", + iwooos_projection["summary"]["topology_atlas_node_count"], + 7, + ) + assert_equal( + "iwooos_projection.summary.topology_atlas_layer_count", + iwooos_projection["summary"]["topology_atlas_layer_count"], + 5, + ) + assert_equal( + "iwooos_projection.summary.topology_atlas_technical_chart_count", + iwooos_projection["summary"]["topology_atlas_technical_chart_count"], + 3, + ) + assert_true( + "iwooos_projection.summary.topology_atlas_interactive_lens_allowed", + iwooos_projection["summary"]["topology_atlas_interactive_lens_allowed"], + ) + assert_true( + "iwooos_projection.summary.topology_atlas_above_gate_radar", + iwooos_projection["summary"]["topology_atlas_above_gate_radar"], + ) + assert_equal( + "iwooos_projection.summary.topology_atlas_runtime_gate_count", + iwooos_projection["summary"]["topology_atlas_runtime_gate_count"], + 0, + ) assert_true( "iwooos_projection.summary.gate_radar_first_layer", iwooos_projection["summary"]["gate_radar_first_layer"], @@ -11957,6 +11995,66 @@ def validate(root: Path) -> None: iwooos_projection_page, text, ) + for text in [ + 'data-testid="iwooos-topology-atlas-board"', + 'data-testid="iwooos-topology-atlas-canvas"', + 'data-testid="iwooos-topology-atlas-active-panel"', + 'data-testid="iwooos-topology-atlas-technical-charts"', + 'data-testid="iwooos-topology-atlas-boundaries"', + "IwoooSTopologyAtlasBoard", + "IwoooSTopologyAtlasMode", + "iwooosTopologyAtlasLenses", + "iwooosTopologyAtlasNodes", + "iwooosTopologyAtlasLayers", + "iwooosTopologyAtlasCharts", + "iwooosTopologyAtlasBoundaries", + "iwooos-topology-grid", + ]: + assert_text_contains( + "iwooos_page.topology_atlas", + iwooos_projection_page, + text, + ) + assert_text_before( + "iwooos_page.immediate_visual_mesh_before_topology_atlas", + iwooos_projection_page, + "", + "", + ) + assert_text_before( + "iwooos_page.topology_atlas_before_gate_radar", + iwooos_projection_page, + "", + "", + ) + assert_text_before( + "iwooos_page.topology_atlas_before_command_map", + iwooos_projection_page, + "", + "", + ) + for text in [ + "iwooos_topology_atlas_first_layer=true", + "iwooos_topology_atlas_lens_count=4", + "iwooos_topology_atlas_node_count=7", + "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_atlas_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", + "runtime_execution_authorized=false", + "active_runtime_gate_count=0", + "action_buttons_allowed=false", + "not_authorization=true", + ]: + assert_text_contains( + "iwooos_page.topology_atlas_boundary", + iwooos_projection_page, + text, + ) for text in [ 'data-testid="iwooos-gate-radar-board"', 'data-testid="iwooos-gate-radar-canvas"', @@ -12228,6 +12326,155 @@ def validate(root: Path) -> None: "iwooos_projection.immediate_visual_mesh_nodes.runtimeGate.scan_authorized", runtime_gate_mesh_node["scan_authorized"], ) + expected_topology_atlas_lens_ids = [ + "architecture", + "topology", + "attackSurface", + "evidenceFlow", + ] + topology_atlas_lenses = iwooos_projection["topology_atlas_lenses"] + assert_equal( + "iwooos_projection.topology_atlas_lenses.ids", + [item["lens_id"] for item in topology_atlas_lenses], + expected_topology_atlas_lens_ids, + ) + assert_equal( + "iwooos_projection.topology_atlas_lenses.display_order", + [item["display_order"] for item in topology_atlas_lenses], + list(range(1, len(expected_topology_atlas_lens_ids) + 1)), + ) + for item in topology_atlas_lenses: + assert_equal( + f"iwooos_projection.topology_atlas_lenses.{item['lens_id']}.display_mode", + item["display_mode"], + "first_screen_professional_topology_atlas", + ) + assert_true( + f"iwooos_projection.topology_atlas_lenses.{item['lens_id']}.interactive_lens_allowed", + item["interactive_lens_allowed"], + ) + assert_false( + f"iwooos_projection.topology_atlas_lenses.{item['lens_id']}.execution_action_button_allowed", + item["execution_action_button_allowed"], + ) + assert_false( + f"iwooos_projection.topology_atlas_lenses.{item['lens_id']}.runtime_gate_opened", + item["runtime_gate_opened"], + ) + assert_false( + f"iwooos_projection.topology_atlas_lenses.{item['lens_id']}.runtime_execution_authorized", + item["runtime_execution_authorized"], + ) + assert_true( + f"iwooos_projection.topology_atlas_lenses.{item['lens_id']}.not_authorization", + item["not_authorization"], + ) + topology_lens = next(item for item in topology_atlas_lenses if item["lens_id"] == "topology") + for flag in ["scan_authorized", "host_change_authorized"]: + assert_false( + f"iwooos_projection.topology_atlas_lenses.topology.{flag}", + topology_lens[flag], + ) + attack_surface_lens = next(item for item in topology_atlas_lenses if item["lens_id"] == "attackSurface") + for flag in ["source_control_mutation_authorized", "github_primary_switch_authorized", "blast_radius_verified"]: + assert_false( + f"iwooos_projection.topology_atlas_lenses.attackSurface.{flag}", + attack_surface_lens[flag], + ) + evidence_flow_lens = next(item for item in topology_atlas_lenses if item["lens_id"] == "evidenceFlow") + for count_key in ["owner_response_received_count", "reviewer_accepted_count"]: + assert_equal( + f"iwooos_projection.topology_atlas_lenses.evidenceFlow.{count_key}", + evidence_flow_lens[count_key], + 0, + ) + expected_topology_atlas_node_ids = [ + "productSurface", + "sourceControl", + "kali", + "devHosts", + "monitoring", + "awooopTruth", + "runtimeGate", + ] + topology_atlas_nodes = iwooos_projection["topology_atlas_nodes"] + assert_equal( + "iwooos_projection.topology_atlas_nodes.ids", + [item["node_id"] for item in topology_atlas_nodes], + expected_topology_atlas_node_ids, + ) + assert_equal( + "iwooos_projection.topology_atlas_nodes.display_order", + [item["display_order"] for item in topology_atlas_nodes], + list(range(1, len(expected_topology_atlas_node_ids) + 1)), + ) + for item in topology_atlas_nodes: + assert_true( + f"iwooos_projection.topology_atlas_nodes.{item['node_id']}.read_only", + item["read_only"], + ) + assert_false( + f"iwooos_projection.topology_atlas_nodes.{item['node_id']}.runtime_gate_opened", + item["runtime_gate_opened"], + ) + assert_false( + f"iwooos_projection.topology_atlas_nodes.{item['node_id']}.runtime_execution_authorized", + item["runtime_execution_authorized"], + ) + assert_true( + f"iwooos_projection.topology_atlas_nodes.{item['node_id']}.not_authorization", + item["not_authorization"], + ) + for node_id in ["kali", "devHosts"]: + node = next(item for item in topology_atlas_nodes if item["node_id"] == node_id) + assert_false( + f"iwooos_projection.topology_atlas_nodes.{node_id}.scan_authorized", + node["scan_authorized"], + ) + assert_false( + f"iwooos_projection.topology_atlas_nodes.{node_id}.host_change_authorized", + node["host_change_authorized"], + ) + source_control_topology_node = next(item for item in topology_atlas_nodes if item["node_id"] == "sourceControl") + assert_false( + "iwooos_projection.topology_atlas_nodes.sourceControl.source_control_mutation_authorized", + source_control_topology_node["source_control_mutation_authorized"], + ) + expected_topology_atlas_chart_ids = [ + "contextDepth", + "blastRadius", + "evidenceFreshness", + ] + topology_atlas_charts = iwooos_projection["topology_atlas_technical_charts"] + assert_equal( + "iwooos_projection.topology_atlas_technical_charts.ids", + [item["chart_id"] for item in topology_atlas_charts], + expected_topology_atlas_chart_ids, + ) + assert_equal( + "iwooos_projection.topology_atlas_technical_charts.display_order", + [item["display_order"] for item in topology_atlas_charts], + list(range(1, len(expected_topology_atlas_chart_ids) + 1)), + ) + for item in topology_atlas_charts: + assert_equal( + f"iwooos_projection.topology_atlas_technical_charts.{item['chart_id']}.display_mode", + item["display_mode"], + "first_screen_professional_topology_atlas", + ) + assert_false( + f"iwooos_projection.topology_atlas_technical_charts.{item['chart_id']}.runtime_delta", + item["runtime_delta"], + ) + assert_true( + f"iwooos_projection.topology_atlas_technical_charts.{item['chart_id']}.not_authorization", + item["not_authorization"], + ) + blast_radius_chart = next(item for item in topology_atlas_charts if item["chart_id"] == "blastRadius") + assert_false( + "iwooos_projection.topology_atlas_technical_charts.blastRadius.blast_radius_verified", + blast_radius_chart["blast_radius_verified"], + ) expected_gate_radar_lane_ids = [ "visible", "blocker", @@ -12544,6 +12791,138 @@ def validate(root: Path) -> None: list(web_messages_en["iwooos"]["immediateVisualMesh"]["nodes"][key].keys()), field, ) + assert_contains( + "web_messages.zh-TW.iwooos.topologyAtlas", + list(web_messages_zh["iwooos"].keys()), + "topologyAtlas", + ) + assert_contains( + "web_messages.en.iwooos.topologyAtlas", + list(web_messages_en["iwooos"].keys()), + "topologyAtlas", + ) + for key in [ + "eyebrow", + "title", + "subtitle", + "tabsLabel", + "mapLabel", + "panelLabels", + "lenses", + "nodes", + "layers", + "charts", + "boundaryTitle", + "boundaryIntro", + ]: + assert_contains( + "web_messages.zh-TW.iwooos.topologyAtlas.keys", + list(web_messages_zh["iwooos"]["topologyAtlas"].keys()), + key, + ) + assert_contains( + "web_messages.en.iwooos.topologyAtlas.keys", + list(web_messages_en["iwooos"]["topologyAtlas"].keys()), + key, + ) + for key in ["evidence", "next", "locked"]: + assert_contains( + "web_messages.zh-TW.iwooos.topologyAtlas.panelLabels", + list(web_messages_zh["iwooos"]["topologyAtlas"]["panelLabels"].keys()), + key, + ) + assert_contains( + "web_messages.en.iwooos.topologyAtlas.panelLabels", + list(web_messages_en["iwooos"]["topologyAtlas"]["panelLabels"].keys()), + key, + ) + for key in ["architecture", "topology", "attackSurface", "evidenceFlow"]: + assert_contains( + "web_messages.zh-TW.iwooos.topologyAtlas.lenses", + list(web_messages_zh["iwooos"]["topologyAtlas"]["lenses"].keys()), + key, + ) + assert_contains( + "web_messages.en.iwooos.topologyAtlas.lenses", + list(web_messages_en["iwooos"]["topologyAtlas"]["lenses"].keys()), + key, + ) + for field in ["title", "mapTitle", "detail", "evidence", "next", "locked"]: + assert_contains( + f"web_messages.zh-TW.iwooos.topologyAtlas.lenses.{key}", + list(web_messages_zh["iwooos"]["topologyAtlas"]["lenses"][key].keys()), + field, + ) + assert_contains( + f"web_messages.en.iwooos.topologyAtlas.lenses.{key}", + list(web_messages_en["iwooos"]["topologyAtlas"]["lenses"][key].keys()), + field, + ) + for key in ["productSurface", "sourceControl", "kali", "devHosts", "monitoring", "awooopTruth", "runtimeGate"]: + assert_contains( + "web_messages.zh-TW.iwooos.topologyAtlas.nodes", + list(web_messages_zh["iwooos"]["topologyAtlas"]["nodes"].keys()), + key, + ) + assert_contains( + "web_messages.en.iwooos.topologyAtlas.nodes", + list(web_messages_en["iwooos"]["topologyAtlas"]["nodes"].keys()), + key, + ) + assert_contains( + f"web_messages.zh-TW.iwooos.topologyAtlas.nodes.{key}", + list(web_messages_zh["iwooos"]["topologyAtlas"]["nodes"][key].keys()), + "title", + ) + assert_contains( + f"web_messages.en.iwooos.topologyAtlas.nodes.{key}", + list(web_messages_en["iwooos"]["topologyAtlas"]["nodes"][key].keys()), + "title", + ) + for key in ["externalSurface", "codeSupply", "hostFabric", "evidenceOps", "gateControl"]: + assert_contains( + "web_messages.zh-TW.iwooos.topologyAtlas.layers", + list(web_messages_zh["iwooos"]["topologyAtlas"]["layers"].keys()), + key, + ) + assert_contains( + "web_messages.en.iwooos.topologyAtlas.layers", + list(web_messages_en["iwooos"]["topologyAtlas"]["layers"].keys()), + key, + ) + for field in ["title", "body"]: + assert_contains( + f"web_messages.zh-TW.iwooos.topologyAtlas.layers.{key}", + list(web_messages_zh["iwooos"]["topologyAtlas"]["layers"][key].keys()), + field, + ) + assert_contains( + f"web_messages.en.iwooos.topologyAtlas.layers.{key}", + list(web_messages_en["iwooos"]["topologyAtlas"]["layers"][key].keys()), + field, + ) + for key in ["contextDepth", "blastRadius", "evidenceFreshness"]: + assert_contains( + "web_messages.zh-TW.iwooos.topologyAtlas.charts", + list(web_messages_zh["iwooos"]["topologyAtlas"]["charts"].keys()), + key, + ) + assert_contains( + "web_messages.en.iwooos.topologyAtlas.charts", + list(web_messages_en["iwooos"]["topologyAtlas"]["charts"].keys()), + key, + ) + for field in ["label", "body"]: + assert_contains( + f"web_messages.zh-TW.iwooos.topologyAtlas.charts.{key}", + list(web_messages_zh["iwooos"]["topologyAtlas"]["charts"][key].keys()), + field, + ) + assert_contains( + f"web_messages.en.iwooos.topologyAtlas.charts.{key}", + list(web_messages_en["iwooos"]["topologyAtlas"]["charts"][key].keys()), + field, + ) assert_contains( "web_messages.zh-TW.iwooos.gateRadar", list(web_messages_zh["iwooos"].keys()),