feat(topology): scan-based creation wizard option (Pro contract + wiring)
Adds the @pro ScanImport contract (ProScanImportProps/ProScanImport) and a null community stub, and slots a third SCAN-BASED card into CreateTopologyWizard, gated on the pro panel being present so it tree-shakes out of the community build. The scan->topology importer itself ships in decnet/pro v1.2.0. CHANGELOG updated under [1.2.0].
This commit is contained in:
11
CHANGELOG.md
11
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,
|
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
|
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.
|
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
|
### Changed
|
||||||
- MITRE ATT&CK Enterprise bundle pinned 19.0 → **19.1**. The bundle and its
|
- MITRE ATT&CK Enterprise bundle pinned 19.0 → **19.1**. The bundle and its
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
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 api from '../../utils/api';
|
||||||
import { useEscapeKey } from '../../hooks/useEscapeKey';
|
import { useEscapeKey } from '../../hooks/useEscapeKey';
|
||||||
import { useFocusTrap } from '../../hooks/useFocusTrap';
|
import { useFocusTrap } from '../../hooks/useFocusTrap';
|
||||||
@@ -28,7 +29,7 @@ interface TopologySummary {
|
|||||||
status_changed_at: string | null;
|
status_changed_at: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
type Kind = 'blank' | 'seeded';
|
type Kind = 'blank' | 'seeded' | 'scan';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
@@ -103,15 +104,19 @@ const CreateTopologyWizard: React.FC<Props> = ({ open, onClose, onCreated }) =>
|
|||||||
[targetId, hosts],
|
[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 () => {
|
const handleCreate = async () => {
|
||||||
if (!targetId || !kind) return;
|
if (!targetId || !kind || kind === 'scan') return;
|
||||||
setSubmitting(true);
|
setSubmitting(true);
|
||||||
setErr(null);
|
setErr(null);
|
||||||
const isAgent = targetId !== LOCAL_CARD_ID;
|
|
||||||
const targetHostUuid = isAgent ? targetId : null;
|
|
||||||
const mode = isAgent ? 'agent' : 'unihost';
|
|
||||||
try {
|
try {
|
||||||
if (kind === 'blank') {
|
if (kind === 'blank') {
|
||||||
const { data } = await api.post<TopologySummary>('/topologies/blank', {
|
const { data } = await api.post<TopologySummary>('/topologies/blank', {
|
||||||
@@ -234,6 +239,22 @@ const CreateTopologyWizard: React.FC<Props> = ({ open, onClose, onCreated }) =>
|
|||||||
Runs the MazeNET generator with depth/branching/deckies parameters. Seed is optional — omit for a fresh roll.
|
Runs the MazeNET generator with depth/branching/deckies parameters. Seed is optional — omit for a fresh roll.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{ScanImport && (
|
||||||
|
<div
|
||||||
|
onClick={() => setKind('scan')}
|
||||||
|
className={`ctw-card ${kind === 'scan' ? 'selected' : ''}`}
|
||||||
|
>
|
||||||
|
<div className="ctw-card-head">
|
||||||
|
<Crosshair size={16} className="ctw-violet" />
|
||||||
|
<span className="ctw-card-name">SCAN-BASED</span>
|
||||||
|
</div>
|
||||||
|
<div className="ctw-card-sub">mirror an Nmap scan</div>
|
||||||
|
<div className="ctw-card-desc">
|
||||||
|
Import an Nmap XML scan and mirror the discovered hosts and services
|
||||||
|
as decoys. Review and pick targets before deploying.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -285,19 +306,25 @@ const CreateTopologyWizard: React.FC<Props> = ({ open, onClose, onCreated }) =>
|
|||||||
<div className="ctw-label">
|
<div className="ctw-label">
|
||||||
Target: <span className="ctw-violet">{targetLabel}</span> · pick a starting point.
|
Target: <span className="ctw-violet">{targetLabel}</span> · pick a starting point.
|
||||||
</div>
|
</div>
|
||||||
<div className="ctw-grid-2">{step1Cards}</div>
|
<div className={ScanImport ? 'ctw-grid-3' : 'ctw-grid-2'}>{step1Cards}</div>
|
||||||
|
|
||||||
<div className="ctw-field">
|
{kind !== 'scan' && (
|
||||||
<label>NAME</label>
|
<div className="ctw-field">
|
||||||
<input
|
<label>NAME</label>
|
||||||
autoFocus
|
<input
|
||||||
type="text"
|
autoFocus
|
||||||
value={name}
|
type="text"
|
||||||
onChange={(e) => setName(e.target.value)}
|
value={name}
|
||||||
placeholder="e.g. honeynet-dev"
|
onChange={(e) => setName(e.target.value)}
|
||||||
maxLength={64}
|
placeholder="e.g. honeynet-dev"
|
||||||
/>
|
maxLength={64}
|
||||||
</div>
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{kind === 'scan' && ScanImport && (
|
||||||
|
<ScanImport mode={mode} targetHostUuid={targetHostUuid} onCreated={onCreated} />
|
||||||
|
)}
|
||||||
|
|
||||||
{kind === 'seeded' && (
|
{kind === 'seeded' && (
|
||||||
<div className="ctw-grid-2">
|
<div className="ctw-grid-2">
|
||||||
@@ -371,7 +398,7 @@ const CreateTopologyWizard: React.FC<Props> = ({ open, onClose, onCreated }) =>
|
|||||||
NEXT →
|
NEXT →
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
{step === 1 && (
|
{step === 1 && kind !== 'scan' && (
|
||||||
<button className="ctw-btn" disabled={!canNext || submitting} onClick={handleCreate}>
|
<button className="ctw-btn" disabled={!canNext || submitting} onClick={handleCreate}>
|
||||||
{submitting ? 'CREATING…' : 'CREATE'}
|
{submitting ? 'CREATING…' : 'CREATE'}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
// 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,
|
// 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.
|
// 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', () => {
|
it('ships no pro routes', () => {
|
||||||
expect(proRoutes).toEqual([]);
|
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();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -3,6 +3,10 @@
|
|||||||
// sets VITE_DECNET_PRO=1 with decnet/pro/web/ present, in which case Vite
|
// 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
|
// aliases `@pro` to the real registry. proRoutes being empty lets the router
|
||||||
// and nav tree-shake the pro surface out of the community bundle.
|
// 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[] = [];
|
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;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
// Contract for Professional-tier UI pages. The pro build aliases `@pro` to the
|
// 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.
|
// 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 {
|
export interface ProRoute {
|
||||||
/** Router path, e.g. "/pro/intel". Convention: prefix pro routes with /pro. */
|
/** 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). */
|
/** Page element rendered at `path`. May be a lazy component (App wraps Suspense). */
|
||||||
element: ReactElement;
|
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<ProScanImportProps> | null;
|
||||||
|
|||||||
Reference in New Issue
Block a user