diff --git a/CHANGELOG.md b/CHANGELOG.md index a7412ae4..53570619 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,17 @@ workers the in-process supervisor can't co-host. CoW-shared). ttp barely moved: its bulk is the privately-parsed ATT&CK bundle, which it alone consumes — so master-warming it was confirmed pointless and dropped. Lesson: prefork pays for base-floor-bound workers, not state-bound ones. +- **(Pro) Scan-based topology creation** — the MazeNET *New Topology* wizard + gains a third option alongside Blank and Seed-based: import an Nmap XML scan + and mirror its live hosts and services as decoys. Parses entirely in-browser + (native `DOMParser`, no new dependency), resolves discovered service + names/ports to DECNET services against the live catalog, groups hosts one LAN + per /24, and builds the topology through the existing CRUD APIs (blank → LANs + → deckies → edges) — no new backend. Hosts with no recognizable service + (e.g. `nmap -sn`) default to a bare SSH decoy. The XML parser is hardened + against XXE/SSRF and entity-expansion DoS, and scan values render as inert + text (no XSS). Professional-tier; tree-shaken out of the community build + (`decnet/pro` `v1.2.0`). ### Changed - MITRE ATT&CK Enterprise bundle pinned 19.0 → **19.1**. The bundle and its diff --git a/decnet_web/src/components/TopologyList/CreateTopologyWizard.tsx b/decnet_web/src/components/TopologyList/CreateTopologyWizard.tsx index eb30a149..4c7ef3a5 100644 --- a/decnet_web/src/components/TopologyList/CreateTopologyWizard.tsx +++ b/decnet_web/src/components/TopologyList/CreateTopologyWizard.tsx @@ -1,6 +1,7 @@ // SPDX-License-Identifier: AGPL-3.0-or-later import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; -import { X, Server, Cpu, FileText, Sparkles, Check } from '../../icons'; +import { X, Server, Cpu, FileText, Sparkles, Check, Crosshair } from '../../icons'; +import { ScanImport } from '@pro'; import api from '../../utils/api'; import { useEscapeKey } from '../../hooks/useEscapeKey'; import { useFocusTrap } from '../../hooks/useFocusTrap'; @@ -28,7 +29,7 @@ interface TopologySummary { status_changed_at: string | null; } -type Kind = 'blank' | 'seeded'; +type Kind = 'blank' | 'seeded' | 'scan'; interface Props { open: boolean; @@ -103,15 +104,19 @@ const CreateTopologyWizard: React.FC = ({ open, onClose, onCreated }) => [targetId, hosts], ); - const canNext = step === 0 ? !!targetId : !!kind && name.trim().length > 0; + const isAgent = !!targetId && targetId !== LOCAL_CARD_ID; + const targetHostUuid = isAgent ? targetId : null; + const mode = isAgent ? 'agent' : 'unihost'; + + // Scan import owns its own name/preview/create sub-flow inside the pro panel, + // so the wizard's name gate and CREATE button don't apply to it. + const canNext = + step === 0 ? !!targetId : kind === 'scan' || (!!kind && name.trim().length > 0); const handleCreate = async () => { - if (!targetId || !kind) return; + if (!targetId || !kind || kind === 'scan') return; setSubmitting(true); setErr(null); - const isAgent = targetId !== LOCAL_CARD_ID; - const targetHostUuid = isAgent ? targetId : null; - const mode = isAgent ? 'agent' : 'unihost'; try { if (kind === 'blank') { const { data } = await api.post('/topologies/blank', { @@ -234,6 +239,22 @@ const CreateTopologyWizard: React.FC = ({ open, onClose, onCreated }) => Runs the MazeNET generator with depth/branching/deckies parameters. Seed is optional — omit for a fresh roll. + {ScanImport && ( +
setKind('scan')} + className={`ctw-card ${kind === 'scan' ? 'selected' : ''}`} + > +
+ + SCAN-BASED +
+
mirror an Nmap scan
+
+ Import an Nmap XML scan and mirror the discovered hosts and services + as decoys. Review and pick targets before deploying. +
+
+ )} ); @@ -285,19 +306,25 @@ const CreateTopologyWizard: React.FC = ({ open, onClose, onCreated }) =>
Target: {targetLabel} · pick a starting point.
-
{step1Cards}
+
{step1Cards}
-
- - setName(e.target.value)} - placeholder="e.g. honeynet-dev" - maxLength={64} - /> -
+ {kind !== 'scan' && ( +
+ + setName(e.target.value)} + placeholder="e.g. honeynet-dev" + maxLength={64} + /> +
+ )} + + {kind === 'scan' && ScanImport && ( + + )} {kind === 'seeded' && (
@@ -371,7 +398,7 @@ const CreateTopologyWizard: React.FC = ({ open, onClose, onCreated }) => NEXT → )} - {step === 1 && ( + {step === 1 && kind !== 'scan' && ( diff --git a/decnet_web/src/pro/pro.test.ts b/decnet_web/src/pro/pro.test.ts index d2f970f6..0a659df9 100644 --- a/decnet_web/src/pro/pro.test.ts +++ b/decnet_web/src/pro/pro.test.ts @@ -1,5 +1,5 @@ // SPDX-License-Identifier: AGPL-3.0-or-later -import { proRoutes } from '@pro'; +import { proRoutes, ScanImport } from '@pro'; // In the community build, `@pro` resolves to the stub: no Professional pages, // so App's route map and Layout's nav group both tree-shake to nothing. @@ -7,4 +7,10 @@ describe('pro tier — community build', () => { it('ships no pro routes', () => { expect(proRoutes).toEqual([]); }); + + // null tree-shakes the wizard's third "SCAN-BASED" card out of the community + // bundle — the scan→topology importer is Professional-only. + it('ships no scan importer', () => { + expect(ScanImport).toBeNull(); + }); }); diff --git a/decnet_web/src/pro/stub.ts b/decnet_web/src/pro/stub.ts index a2e03e0a..33a5af43 100644 --- a/decnet_web/src/pro/stub.ts +++ b/decnet_web/src/pro/stub.ts @@ -3,6 +3,10 @@ // sets VITE_DECNET_PRO=1 with decnet/pro/web/ present, in which case Vite // aliases `@pro` to the real registry. proRoutes being empty lets the router // and nav tree-shake the pro surface out of the community bundle. -import type { ProRoute } from './types'; +import type { ProRoute, ProScanImport } from './types'; export const proRoutes: ProRoute[] = []; + +// No scan-based topology creation in the community build — the wizard's third +// card tree-shakes out when this is null. +export const ScanImport: ProScanImport = null; diff --git a/decnet_web/src/pro/types.ts b/decnet_web/src/pro/types.ts index 42b1cafc..d1086563 100644 --- a/decnet_web/src/pro/types.ts +++ b/decnet_web/src/pro/types.ts @@ -1,7 +1,7 @@ // SPDX-License-Identifier: AGPL-3.0-or-later // Contract for Professional-tier UI pages. The pro build aliases `@pro` to the // real registry in decnet/pro/web/; the community build resolves it to ./stub. -import type { ReactElement, ReactNode } from 'react'; +import type { ComponentType, ReactElement, ReactNode } from 'react'; export interface ProRoute { /** Router path, e.g. "/pro/intel". Convention: prefix pro routes with /pro. */ @@ -13,3 +13,35 @@ export interface ProRoute { /** Page element rendered at `path`. May be a lazy component (App wraps Suspense). */ element: ReactElement; } + +/** Created-topology summary handed back to the wizard. Mirrors the wizard's own + * TopologySummary (and GET /topologies rows) structurally so the wizard's + * onCreated handler is assignable without a cross-tree type import. */ +export interface ProTopologySummary { + id: string; + name: string; + mode: string; + target_host_uuid: string | null; + status: string; + version: number; + needs_resync?: boolean; + created_at: string; + status_changed_at: string | null; +} + +/** Props the CreateTopologyWizard passes to the pro scan-import panel. The pro + * build owns the entire scan→topology flow (file pick, parse, preview, create) + * and signals completion through `onCreated`; the community build never sees + * this surface. Kept structural — the pro tree implements the shape without + * importing it, mirroring how `ProRoute` crosses the trust boundary. */ +export interface ProScanImportProps { + /** "unihost" | "agent" — chosen in the wizard's TARGET step. */ + mode: string; + /** Agent host UUID, or null for local. */ + targetHostUuid: string | null; + /** Fires with the created topology summary; the wizard closes and navigates. */ + onCreated: (row: ProTopologySummary) => void; +} + +/** `null` in the community build (no scan import); a component in the pro build. */ +export type ProScanImport = ComponentType | null;