/* ============================================================ CyberSOC Dashboard โ€” Main Application Controller ============================================================ */ // ============================================================ // StateStore โ€” single source of truth for all dashboard state // ============================================================ class StateStore { constructor() { this._state = StateStore._emptyState(); this._subscribers = new Map(); } static _emptyState() { return { episodeActive: false, episodeId: null, taskId: null, stepCount: 0, maxSteps: 30, totalReward: 0, reward: 0, done: false, alerts: [], topology: null, timeline: [], activeThreats: [], businessImpactScore: 0, liveScores: { threat_containment: 0, ioc_blocking: 0, forensic_investigation: 0, siem_correlation: 0, threat_intel_usage: 0, vuln_root_cause: 0, business_impact: 0, step_efficiency: 0, plan_coverage: 0, plan_evidence_quality: 0, }, threatGraph: { nodes: [], links: [] }, gradeBreakdown: null, finalScore: null, hostForensics: null, iocEnrichment: null, vulnerabilityResults: null, playbookResult: null, correlationResults: null, threatGraphSummary: null, activeTurn: null, }; } get(key) { return this._state[key]; } subscribe(event, callback) { if (!this._subscribers.has(event)) this._subscribers.set(event, []); this._subscribers.get(event).push(callback); return () => { const subs = this._subscribers.get(event); if (subs) { const i = subs.indexOf(callback); if (i >= 0) subs.splice(i, 1); } }; } emit(event, data) { (this._subscribers.get(event) || []).forEach(cb => { try { cb(data); } catch (e) { console.error(`[StateStore] "${event}" handler error:`, e); } }); } // Called by the API after every reset() or step() response. // Merges the parsed observation into state and fires events. applyObservation(obs, action) { if (!obs) return; const wasActive = this._state.episodeActive; Object.assign(this._state, { episodeActive: true, episodeId: obs.episode_id ?? this._state.episodeId, taskId: obs.task_id ?? this._state.taskId, stepCount: obs.step_count ?? this._state.stepCount, maxSteps: obs.max_steps ?? this._state.maxSteps, totalReward: obs.total_reward ?? this._state.totalReward, reward: obs.reward ?? 0, done: obs.done ?? false, alerts: obs.alert_queue ?? this._state.alerts, topology: obs.network_topology ?? this._state.topology, timeline: obs.timeline ?? this._state.timeline, activeThreats: obs.active_threats ?? this._state.activeThreats, businessImpactScore: obs.business_impact_score ?? this._state.businessImpactScore, gradeBreakdown: obs.grade_breakdown ?? this._state.gradeBreakdown, finalScore: obs.final_score ?? this._state.finalScore, hostForensics: obs.host_forensics ?? null, iocEnrichment: obs.ioc_enrichment ?? null, vulnerabilityResults: obs.vulnerability_results ?? null, playbookResult: obs.playbook_result ?? null, correlationResults: obs.correlation_results ?? null, threatGraphSummary: obs.threat_graph_summary ?? this._state.threatGraphSummary, activeTurn: obs.active_turn ?? this._state.activeTurn, }); if (!wasActive) this.emit('episode:started', { obs, action }); this.emit('episode:step', { obs, action }); if (this._state.done) this.emit('episode:ended', { obs, action }); } updateLiveScores(scores) { this._state.liveScores = { ...scores }; this.emit('state:scores', scores); } updateThreatGraph(graphData) { this._state.threatGraph = graphData; this.emit('state:threatGraph', graphData); } reset() { this._state = StateStore._emptyState(); this.emit('episode:reset', {}); } } // ============================================================ // RedTeamController โ€” manages the Red Team Toolkit UI panel // ============================================================ class RedTeamController { constructor(dashboard) { this.dashboard = dashboard; this._actionFields = ['lateral_pivot', 'deploy_payload', 'evade_detection']; this._bindEvents(); this._onActionTypeChange(); } _bindEvents() { const select = document.getElementById('red-action-select'); const btn = document.getElementById('btn-execute-red'); if (select) select.addEventListener('change', () => this._onActionTypeChange()); if (btn) btn.addEventListener('click', () => this._onExecuteClick()); } _onActionTypeChange() { const actionType = document.getElementById('red-action-select')?.value; this._actionFields.forEach(t => { const el = document.getElementById(`rt-fields-${t}`); if (el) el.classList.toggle('hidden', t !== actionType); }); } async _onExecuteClick() { const actionType = document.getElementById('red-action-select')?.value || 'pass_turn'; let action = { type: actionType }; if (actionType === 'lateral_pivot') { action.source_host = document.getElementById('red-src')?.value?.trim() || ''; action.target_host = document.getElementById('red-dst')?.value?.trim() || ''; } else if (actionType === 'deploy_payload') { action.hostname = document.getElementById('red-payload-host')?.value?.trim() || ''; action.payload_type = document.getElementById('red-payload-type')?.value || 'ransomware'; } else if (actionType === 'evade_detection') { action.hostname = document.getElementById('red-evade-host')?.value?.trim() || ''; action.technique = document.getElementById('red-evade-technique')?.value || 'migrate_pid'; } const btn = document.getElementById('btn-execute-red'); if (btn) btn.disabled = true; try { const obs = await this.dashboard.api.step(action); AnimationUtils.showNotification(`๐Ÿ”ด Red: ${actionType.replace(/_/g, ' ')}`, 'red', 2000); if (!obs.done && obs.active_turn !== 'red' && !this.dashboard.isPaused) { this.dashboard._scheduleNextStep(2000); } } catch (err) { AnimationUtils.showNotification(`โŒ Red action failed: ${err.message.substring(0, 60)}`, 'red', 3000); if (btn) btn.disabled = false; } } setEnabled(enabled) { const btn = document.getElementById('btn-execute-red'); const select = document.getElementById('red-action-select'); if (btn) btn.disabled = !enabled; if (select) select.disabled = !enabled; document.querySelectorAll('.rt-input').forEach(f => { f.disabled = !enabled; }); document.querySelectorAll('.rt-fields select').forEach(f => { f.disabled = !enabled; }); const section = document.querySelector('.red-team-section'); if (section) section.classList.toggle('rt-disabled', !enabled); } } // ============================================================ // CyberSOCDashboard โ€” orchestrates episode flow and UI bindings // ============================================================ class CyberSOCDashboard { constructor() { this.api = API; this.store = new StateStore(); this.threatGraph = new ClientThreatGraph(); this.graphViz = new ThreatGraphViz('threat-graph-container'); this.radarChart = new RadarChart('radar-chart'); this.rewardTimeline = new RewardTimeline('reward-timeline'); this.demoActions = []; this.currentStepIndex = 0; this.autoPlayTimer = null; this.episodeStartTime = null; this.timerInterval = null; this.isPaused = false; this.episodeRunning = false; this.prevAlertIds = new Set(); this.currentAction = null; this.redTeamController = null; } async init() { AnimationUtils.init(); this.graphViz.init(); this.radarChart.init(); this.rewardTimeline.init(); this.api.setStore(this.store); this._subscribeToStore(); this._bindButtons(); this.redTeamController = new RedTeamController(this); this._showConnectionOverlay('Connecting to CyberSOC Server...'); await this._waitForServer(); } // Wire UI components to store events โ€” the only place DOM updates are triggered. _subscribeToStore() { this.store.subscribe('episode:started', ({ obs }) => this._onEpisodeStarted(obs)); this.store.subscribe('episode:step', ({ obs, action }) => this._onStep(obs, action)); this.store.subscribe('episode:ended', ({ obs }) => this._handleEpisodeComplete(obs)); this.store.subscribe('episode:reset', () => this._resetUI()); this.store.subscribe('state:scores', scores => this.radarChart.update(scores)); this.store.subscribe('state:threatGraph', graphData => this.graphViz.update(graphData)); } async _waitForServer() { let attempts = 0; const maxAttempts = 30; const check = async () => { attempts++; const ok = await this.api.checkConnection(); if (ok) { this._hideConnectionOverlay(); AnimationUtils.showNotification('โœ… Connected to CyberSOC Server', 'green', 2000); document.getElementById('btn-start').disabled = false; } else if (attempts < maxAttempts) { this._showConnectionOverlay(`Connecting to CyberSOC Server... (${attempts}/${maxAttempts})`); setTimeout(check, 2000); } else { this._showConnectionOverlay('โš ๏ธ Server not available. Start with: uvicorn dashboard_server:app --port 8000'); document.getElementById('btn-start').disabled = false; } }; await check(); } _bindButtons() { document.getElementById('btn-start').addEventListener('click', () => this._onStartClick()); document.getElementById('btn-pause').addEventListener('click', () => this._onPauseClick()); document.getElementById('btn-next').addEventListener('click', () => this._onNextClick()); document.getElementById('btn-reset').addEventListener('click', () => this._onResetClick()); } async _onStartClick() { const taskId = document.getElementById('task-select').value; await this.startEpisode(taskId); } _onPauseClick() { this.isPaused = !this.isPaused; const btn = document.getElementById('btn-pause'); if (this.isPaused) { btn.textContent = 'โ–ถ Resume'; if (this.autoPlayTimer) { clearTimeout(this.autoPlayTimer); this.autoPlayTimer = null; } } else { btn.textContent = 'โธ Pause'; this._scheduleNextStep(); } } _onNextClick() { if (this.autoPlayTimer) { clearTimeout(this.autoPlayTimer); this.autoPlayTimer = null; } this.isPaused = true; document.getElementById('btn-pause').textContent = 'โ–ถ Resume'; this._executeNextStep(); } async _onResetClick() { this.isPaused = false; this.episodeRunning = false; if (this.autoPlayTimer) { clearTimeout(this.autoPlayTimer); this.autoPlayTimer = null; } if (this.timerInterval) { clearInterval(this.timerInterval); this.timerInterval = null; } document.getElementById('btn-pause').classList.add('hidden'); document.getElementById('btn-next').classList.add('hidden'); document.getElementById('btn-reset').classList.add('hidden'); // Resets data model โ†’ emits episode:reset โ†’ _resetUI() subscriber fires this.store.reset(); } // ============================================================ // Episode Management // ============================================================ async startEpisode(taskId) { document.getElementById('btn-start').classList.add('hidden'); document.getElementById('task-select').disabled = true; document.getElementById('btn-pause').classList.remove('hidden'); document.getElementById('btn-next').classList.remove('hidden'); document.getElementById('btn-reset').classList.remove('hidden'); AnimationUtils.showNotification(`๐Ÿš€ Starting ${taskId.toUpperCase()} episode...`, 'blue', 2000); // Reset alert tracking before api.reset() so _onEpisodeStarted starts clean this.prevAlertIds = new Set(); try { // api.reset() calls store.applyObservation() internally, which fires // episode:started โ†’ _onEpisodeStarted() and episode:step โ†’ _onStep(obs, null) const obs = await this.api.reset(taskId); this.episodeRunning = true; this.currentStepIndex = 0; this.episodeStartTime = Date.now(); if (this.timerInterval) clearInterval(this.timerInterval); this.timerInterval = setInterval(() => this._updateTimer(), 1000); this.demoActions = this.buildDemoActions(obs); this._scheduleNextStep(2500); } catch (err) { console.error('Episode start failed:', err); AnimationUtils.showNotification(`โŒ Failed to start episode: ${err.message}`, 'red', 5000); document.getElementById('btn-start').classList.remove('hidden'); document.getElementById('task-select').disabled = false; document.getElementById('btn-pause').classList.add('hidden'); document.getElementById('btn-next').classList.add('hidden'); document.getElementById('btn-reset').classList.add('hidden'); } } // ============================================================ // Store event handlers โ€” the reactive UI layer // ============================================================ // Fired once when a new episode observation arrives (wasActive was false). _onEpisodeStarted(obs) { this._resetUIForEpisode(); // Zero out scores โ€” scores must not carry over from a previous episode this.store.updateLiveScores({ threat_containment: 0, ioc_blocking: 0, forensic_investigation: 0, siem_correlation: 0, threat_intel_usage: 0, vuln_root_cause: 0, business_impact: 0, step_efficiency: 0, plan_coverage: 0, plan_evidence_quality: 0, }); this._updateHeader(obs); this._updatePhase(0, obs.max_steps); this._updateTurnIndicator(obs.active_turn); this._populateInitialAlerts(obs.alert_queue || []); // async fire-and-forget this._updateNetworkTopology(obs.network_topology); this._updateBusinessImpact(obs.business_impact_score ?? 0); this._updateActiveThreats(obs.active_threats); this._updateContainmentProgress(obs, obs.task_id); // Seed the client-side threat graph from the initial observation this.threatGraph = new ClientThreatGraph(); this.threatGraph.updateFromObservation(obs, null); this.store.updateThreatGraph(this.threatGraph.getGraphData()); (obs.alert_queue || []).forEach(a => this.prevAlertIds.add(a.alert_id)); } // Fired for every step response (including the initial reset). // When action is null this is the initial observation โ€” _onEpisodeStarted already handled it. _onStep(obs, action) { if (!action) return; this._updateHeader(obs); this._updatePhase(obs.step_count, obs.max_steps); this._updateTurnIndicator(obs.active_turn); this._updateActionLog(obs.timeline, action, obs); this._updateNetworkTopology(obs.network_topology); this._updateBusinessImpact(obs.business_impact_score ?? 0); this._updateActiveThreats(obs.active_threats); this._updateContainmentProgress(obs, obs.task_id); this._updateRewardBadge(obs.total_reward ?? 0); // Detect pivot alerts that just appeared const newPivots = []; (obs.alert_queue || []).forEach(alert => { if (alert.alert_id.startsWith('PIVOT-') && !this.prevAlertIds.has(alert.alert_id)) { newPivots.push(alert); } }); this._updateAlertQueue(obs.alert_queue || [], newPivots.length > 0); // async fire-and-forget // Update threat graph state and push to graphViz via store event this.threatGraph.updateFromObservation(obs, action); this.store.updateThreatGraph(this.threatGraph.getGraphData()); // Per-action graph animations if (action.type === 'run_forensics' && obs.host_forensics) { this.graphViz.flashNode(obs.host_forensics.hostname, obs.host_forensics.is_compromised ? 'red' : 'green'); } if (action.type === 'block_ioc') { this.graphViz.flashNode(action.ioc_value, 'green'); } if (action.type === 'kill_process') { const procId = `${action.hostname}:${action.process_name}`; this.graphViz.flashNode(procId, 'green'); AnimationUtils.showNotification(`๐Ÿ’€ Killed: ${action.process_name} on ${action.hostname}`, 'amber', 2000); } // Red team lateral pivot handling if (newPivots.length > 0) { this._handleRedTeamPivot(newPivots[0]); newPivots.forEach(p => { this.prevAlertIds.add(p.alert_id); const pivotLinks = this.threatGraph.links.filter(l => l.edgeType === 'pivoted_from'); if (pivotLinks.length > 0) { const pl = pivotLinks[pivotLinks.length - 1]; const srcId = pl.source.id || pl.source; const tgtId = pl.target.id || pl.target; this.graphViz.animatePivot(srcId, tgtId); } }); } if (obs.step_count > 0) { this.rewardTimeline.addPoint(obs.step_count, obs.total_reward ?? 0, action.type); } if (obs.threat_graph_summary) this._updateGraphSummary(obs.threat_graph_summary); // _updateLiveScores writes back to the store โ†’ emits state:scores โ†’ radarChart.update() this._updateLiveScores(obs, action); (obs.alert_queue || []).forEach(a => this.prevAlertIds.add(a.alert_id)); } buildDemoActions(obs) { const alerts = obs.alert_queue || []; const alertIds = alerts.map(a => a.alert_id); const taskId = obs.task_id; const allIocs = []; const ipIocs = []; const domainIocs = []; const hashIocs = []; const hostsSeen = new Set(); alerts.forEach(a => { if (!hostsSeen.has(a.source_host)) hostsSeen.add(a.source_host); (a.ioc_indicators || []).forEach(ioc => { if (!allIocs.includes(ioc)) { allIocs.push(ioc); if (/^\d{1,3}(\.\d{1,3}){3}$/.test(ioc)) ipIocs.push(ioc); else if (/[a-f0-9]{32,64}/i.test(ioc)) hashIocs.push(ioc); else domainIocs.push(ioc); } }); }); const hosts = [...hostsSeen]; if (taskId === 'hard') return this._hardDemoActions(alertIds, hosts, ipIocs, domainIocs, hashIocs); if (taskId === 'medium') return this._mediumDemoActions(alertIds, hosts, ipIocs, domainIocs, hashIocs); return this._easyDemoActions(alertIds, hosts, ipIocs, domainIocs, hashIocs); } _hardDemoActions(alertIds, _hosts, ipIocs, domainIocs, _hashIocs) { return [ { type: 'correlate_alerts', alert_ids: [alertIds[0] || 'ALERT-H001', alertIds[1] || 'ALERT-H002'] }, { type: 'query_host', hostname: 'EXEC-003' }, { type: 'run_forensics', hostname: 'EXEC-003' }, { type: 'query_host', hostname: 'WS-088' }, { type: 'run_forensics', hostname: 'WS-088' }, { type: 'enrich_ioc', ioc_value: ipIocs[0] || '198.51.100.77', ioc_type: 'ip' }, { type: 'scan_host_vulnerabilities', hostname: 'SRV-002' }, { type: 'run_forensics', hostname: 'SRV-002' }, { type: 'kill_process', hostname: 'EXEC-003', process_name: 'outlook_macro.exe' }, { type: 'kill_process', hostname: 'EXEC-003', process_name: 'svchost_c2.exe' }, { type: 'kill_process', hostname: 'WS-088', process_name: 'svchost_c2.exe' }, { type: 'block_ioc', ioc_value: ipIocs[0] || '198.51.100.77', ioc_type: 'ip' }, { type: 'block_ioc', ioc_value: domainIocs[0] || 'cdn-update.malware-c2.net', ioc_type: 'domain' }, { type: 'isolate_segment', subnet: 'executive', reason: 'APT lateral movement detected โ€” executive subnet compromised' }, { type: 'trigger_playbook', playbook_name: 'c2_disruption', target: 'EXEC-003' }, { type: 'kill_process', hostname: 'SRV-002', process_name: 'exploit_kernel.exe' }, { type: 'kill_process', hostname: 'SRV-002', process_name: 'data_pump.exe' }, { type: 'kill_process', hostname: 'FIN-008', process_name: 'data_pump.exe' }, { type: 'kill_process', hostname: 'SRV-010', process_name: 'blackcat_ransom.exe' }, { type: 'submit_containment_plan', plan: [ { threat_id: 'T-HARD-001', actions_taken: ['query_host', 'run_forensics', 'kill_process', 'correlate_alerts'], root_cause: 'Spear-phishing email with malicious macro attachment targeting executive VP. Macro executed outlook_macro.exe establishing initial foothold.', confidence: 0.95, }, { threat_id: 'T-HARD-002', actions_taken: ['run_forensics', 'kill_process', 'block_ioc', 'trigger_playbook', 'isolate_segment'], root_cause: 'C2 beacon (svchost_c2.exe) established on EXEC-003 and WS-088 communicating to 198.51.100.77 every 60s. C2 infrastructure disrupted.', confidence: 0.92, }, { threat_id: 'T-HARD-003', actions_taken: ['scan_host_vulnerabilities', 'run_forensics', 'kill_process'], root_cause: 'Kernel privilege escalation on SRV-002 via exploit_kernel.exe. CVE leveraged for SYSTEM-level access to database server.', confidence: 0.88, }, { threat_id: 'T-HARD-004', actions_taken: ['run_forensics', 'kill_process', 'block_ioc'], root_cause: 'Data exfiltration campaign using data_pump.exe on SRV-002 and FIN-008. 2.3GB customer PII transferred to 203.0.113.99. Exfil channel blocked.', confidence: 0.90, }, { threat_id: 'T-HARD-005', actions_taken: ['kill_process'], root_cause: 'BlackCat ransomware deployment on datacenter servers SRV-010 and SRV-015. Encryption halted before full deployment. Recovery recommended.', confidence: 0.85, }, ], executive_summary: 'APT campaign fully contained. C2 infrastructure at 198.51.100.77 disrupted and blocked. Executive subnet isolated. All 5 threat chains neutralized: phishing entry point, C2 beaconing, kernel privilege escalation, data exfiltration, and ransomware deployment. Estimated data exposure: 2.3GB PII. Recommend: patch SRV-002 kernel CVE, reset all executive credentials, deploy EDR signatures for BlackCat variants.', }, ]; } _mediumDemoActions(alertIds, hosts, ipIocs, domainIocs, _hashIocs) { return [ { type: 'correlate_alerts', alert_ids: alertIds.slice(0, 2) }, { type: 'query_host', hostname: hosts[0] || 'WS-017' }, { type: 'run_forensics', hostname: hosts[0] || 'WS-017' }, { type: 'enrich_ioc', ioc_value: domainIocs[0] || ipIocs[0] || '203.0.113.50', ioc_type: domainIocs.length > 0 ? 'domain' : 'ip' }, { type: 'query_host', hostname: hosts[1] || 'DEV-033' }, { type: 'run_forensics', hostname: hosts[1] || 'DEV-033' }, { type: 'kill_process', hostname: hosts[0] || 'WS-017', process_name: 'powershell.exe' }, { type: 'kill_process', hostname: hosts[0] || 'WS-017', process_name: 'mimikatz.exe' }, { type: 'block_ioc', ioc_value: domainIocs[0] || ipIocs[0] || '203.0.113.50', ioc_type: domainIocs.length > 0 ? 'domain' : 'ip' }, { type: 'kill_process', hostname: hosts[1] || 'DEV-033', process_name: 'svchost_backdoor.exe' }, { type: 'isolate_segment', subnet: 'corporate', reason: 'Credential theft and lateral movement detected' }, { type: 'trigger_playbook', playbook_name: 'phishing_response', target: hosts[0] || 'WS-017' }, { type: 'submit_containment_plan', plan: [ { threat_id: 'T-MED-001', actions_taken: ['query_host', 'run_forensics', 'kill_process'], root_cause: 'Phishing email led to PowerShell execution downloading payload from evil-login.example.com', confidence: 0.9 }, { threat_id: 'T-MED-002', actions_taken: ['run_forensics', 'kill_process', 'block_ioc'], root_cause: 'Credential dumping via Mimikatz. LSASS memory access detected.', confidence: 0.88 }, { threat_id: 'T-MED-003', actions_taken: ['run_forensics', 'kill_process', 'isolate_segment'], root_cause: 'Lateral movement via compromised admin credentials to DEV-033 and FIN-012', confidence: 0.85 }, ], executive_summary: 'Multi-stage phishing โ†’ credential theft โ†’ lateral movement campaign contained. Three compromised hosts remediated. Corporate subnet isolated pending investigation.', }, ]; } _easyDemoActions(alertIds, hosts, ipIocs, _domainIocs, hashIocs) { const host = hosts[0] || 'WS-042'; const hash = hashIocs[0] || ipIocs[0] || 'e99a18c428cb38d5f260853678922e03'; return [ { type: 'correlate_alerts', alert_ids: alertIds.slice(0, 2) }, { type: 'query_host', hostname: host }, { type: 'run_forensics', hostname: host }, { type: 'enrich_ioc', ioc_value: hash, ioc_type: hashIocs.length > 0 ? 'hash' : 'ip' }, { type: 'kill_process', hostname: host, process_name: 'cryptolocker.exe' }, { type: 'block_ioc', ioc_value: hash, ioc_type: hashIocs.length > 0 ? 'hash' : 'ip' }, { type: 'scan_host_vulnerabilities', hostname: host }, { type: 'trigger_playbook', playbook_name: 'ransomware_containment', target: host }, { type: 'submit_containment_plan', plan: [{ threat_id: 'T-EASY-001', actions_taken: ['query_host', 'run_forensics', 'kill_process', 'block_ioc'], root_cause: 'Ransomware (cryptolocker.exe) executing on WS-042, encrypting user documents. Delivered via phishing email attachment.', confidence: 0.95, }], executive_summary: 'Single ransomware endpoint contained. Process killed, IOC hash blocked. Recommend disk restore from backup for WS-042.', }, ]; } // ============================================================ // Step Execution // ============================================================ _scheduleNextStep(delay = 2000) { if (this.isPaused || !this.episodeRunning) return; if (this.autoPlayTimer) clearTimeout(this.autoPlayTimer); this.autoPlayTimer = setTimeout(() => this._executeNextStep(), delay); } async _executeNextStep() { if (!this.episodeRunning || this.store.get('done')) { this.episodeRunning = false; return; } if (this.currentStepIndex >= this.demoActions.length) { AnimationUtils.showNotification('โ„น๏ธ Demo sequence complete. Episode still running.', 'blue', 3000); return; } const action = this.demoActions[this.currentStepIndex]; this.currentAction = action; this.currentStepIndex++; try { // api.step() calls store.applyObservation() internally. // episode:step fires synchronously โ†’ _onStep() updates UI before we resume here. const obs = await this.api.step(action); if (obs.done) { this.episodeRunning = false; if (this.timerInterval) { clearInterval(this.timerInterval); this.timerInterval = null; } // episode:ended was already emitted by store โ†’ _handleEpisodeComplete() fired } else if (!this.isPaused && obs.active_turn !== 'red') { let delay = 2000; if (action.type === 'isolate_segment') delay = 3000; if (action.type === 'submit_containment_plan') delay = 500; this._scheduleNextStep(delay); } // When active_turn === 'red', auto-play pauses โ€” RedTeamController resumes it } catch (err) { console.error('Step failed:', err); AnimationUtils.showNotification(`โŒ Step failed: ${err.message.substring(0, 80)}`, 'red', 4000); if (!this.isPaused) this._scheduleNextStep(3000); } } // ============================================================ // Live Score Calculation โ€” writes back through store โ†’ radarChart // ============================================================ _updateLiveScores(obs, action) { if (!action) return; const scores = { ...this.store.get('liveScores') }; if (action.type === 'correlate_alerts') scores.siem_correlation = 1.0; if (action.type === 'run_forensics') { scores.forensic_investigation = Math.min(1, scores.forensic_investigation + 0.2); } if (action.type === 'enrich_ioc') { scores.threat_intel_usage = Math.min(1, scores.threat_intel_usage + 0.25); } if (action.type === 'scan_host_vulnerabilities' && (obs.vulnerability_results?.length ?? 0) > 0) { scores.vuln_root_cause = 1.0; } if (action.type === 'block_ioc') { scores.ioc_blocking = Math.min(1, scores.ioc_blocking + 0.2); } if (action.type === 'kill_process') { scores.threat_containment = Math.min(1, scores.threat_containment + 0.15); } if (action.type === 'submit_containment_plan') { scores.plan_coverage = 0.9; scores.plan_evidence_quality = 0.85; } scores.business_impact = Math.max(0, 1.0 - (obs.business_impact_score ?? 0)); const ratio = (obs.step_count ?? 0) / (obs.max_steps || 30); scores.step_efficiency = Math.max(0.3, 1.0 - Math.max(0, ratio - 0.5) * 1.5); // Emits state:scores โ†’ radarChart.update() subscriber fires this.store.updateLiveScores(scores); } // ============================================================ // UI Update Helpers // ============================================================ _updateTurnIndicator(activeTurn) { const el = document.getElementById('turn-indicator'); const text = document.getElementById('turn-text'); if (!el) return; if (!activeTurn) { el.classList.add('hidden'); this.redTeamController?.setEnabled(false); return; } el.classList.remove('hidden'); const isRed = activeTurn === 'red'; el.className = `turn-indicator ${isRed ? 'red-turn' : 'blue-turn'}`; if (text) text.textContent = isRed ? '๐Ÿ”ด RED TEAM TURN' : '๐Ÿ”ต BLUE TEAM TURN'; this.redTeamController?.setEnabled(isRed); } _updateHeader(obs) { const stepEl = document.getElementById('header-step'); const episodeEl = document.getElementById('header-episode'); const diffBadge = document.getElementById('difficulty-badge'); if (stepEl) { const old = parseInt(stepEl.textContent.split('/')[0]) || 0; AnimationUtils.countUpInt(stepEl, old, obs.step_count, 300); setTimeout(() => { if (stepEl) stepEl.textContent = `${obs.step_count}/${obs.max_steps}`; }, 350); } if (episodeEl && obs.episode_id) { episodeEl.textContent = obs.episode_id.substring(0, 8) + 'โ€ฆ'; } if (diffBadge && obs.task_id) { diffBadge.textContent = obs.task_id.toUpperCase(); diffBadge.className = `difficulty-badge ${obs.task_id}`; } } _updateTimer() { if (!this.episodeStartTime) return; const elapsed = Math.floor((Date.now() - this.episodeStartTime) / 1000); const mm = String(Math.floor(elapsed / 60)).padStart(2, '0'); const ss = String(elapsed % 60).padStart(2, '0'); const el = document.getElementById('header-timer'); if (el) el.textContent = `${mm}:${ss}`; } _updatePhase(_step, _maxSteps) { const dots = document.querySelectorAll('.phase-dot'); const connectors = document.querySelectorAll('.phase-connector'); let phaseIdx = 0; if (this.currentAction) { const t = this.currentAction.type; if (['correlate_alerts'].includes(t)) phaseIdx = 0; else if (['query_host', 'run_forensics', 'enrich_ioc', 'scan_host_vulnerabilities'].includes(t)) phaseIdx = 1; else if (['kill_process', 'block_ioc', 'isolate_segment', 'trigger_playbook'].includes(t)) phaseIdx = 2; else if (['submit_containment_plan'].includes(t)) phaseIdx = 3; } dots.forEach((dot, i) => { dot.classList.remove('active', 'completed'); if (i < phaseIdx) dot.classList.add('completed'); else if (i === phaseIdx) dot.classList.add('active'); }); connectors.forEach((conn, i) => conn.classList.toggle('completed', i < phaseIdx)); } async _populateInitialAlerts(alerts) { const list = document.getElementById('alert-list'); if (!list) return; list.innerHTML = ''; for (let i = 0; i < alerts.length; i++) { await new Promise(r => setTimeout(r, 180 * i)); this._addAlertCard(alerts[i], list, false); } this._updateAlertBadge(alerts.length); } async _updateAlertQueue(alerts, _hasPivot = false) { const list = document.getElementById('alert-list'); if (!list) return; const existingIds = new Set([...list.querySelectorAll('.alert-card')].map(c => c.dataset.alertId)); const newAlerts = alerts.filter(a => !existingIds.has(a.alert_id)); newAlerts.forEach(alert => this._addAlertCard(alert, list, alert.alert_id.startsWith('PIVOT-'))); const resolvedCount = alerts.filter(a => a.is_acknowledged).length; this._updateAlertBadge(alerts.length - resolvedCount); } _addAlertCard(alert, container, isPivot = false) { const card = document.createElement('div'); card.className = `alert-card${isPivot ? ' pivot' : ''}`; card.dataset.alertId = alert.alert_id; const severityClass = `severity-${alert.severity}`; const iocText = (alert.ioc_indicators || []).join(', '); const iocHtml = iocText ? `
IOCs: ${iocText}
` : ''; const pivotBadge = isPivot ? `โšก PIVOT` : ''; const correlBadge = alert.is_acknowledged ? `๐Ÿ”— CORR` : ''; card.innerHTML = `
${alert.severity.toUpperCase()} ${alert.source_host}
${alert.description}
${iocHtml} `; card.title = alert.description; container.insertBefore(card, container.firstChild); } _updateAlertBadge(count) { const badge = document.getElementById('alert-count-badge'); if (badge) badge.textContent = count; } _updateActionLog(timeline, _lastAction, obs) { const log = document.getElementById('action-log'); if (!log || !timeline?.length) return; const lastEntry = timeline[timeline.length - 1]; if (!lastEntry) return; const empty = log.querySelector('.empty-state'); if (empty) empty.remove(); const category = this._getActionCategory(lastEntry.action_type); const isPos = lastEntry.reward >= 0; const rewardText = `${isPos ? '+' : ''}${lastEntry.reward.toFixed(2)}`; const rewardClass = isPos ? 'positive' : 'negative'; const icon = this._getActionIcon(lastEntry.action_type); const detailsHtml = this._buildLogDetails(lastEntry.action_type, obs); const entry = document.createElement('div'); entry.className = `log-entry ${category}`; entry.innerHTML = `
[Step ${String(lastEntry.step).padStart(2, '0')}] ${icon} ${lastEntry.action_type} โ†’ ${lastEntry.target} ${isPos ? 'โœ…' : 'โŒ'} ${rewardText}
${lastEntry.result.substring(0, 120)}
${detailsHtml} `; log.insertBefore(entry, log.firstChild); this._updateRewardBadge(obs?.total_reward ?? 0); } _buildLogDetails(actionType, obs) { if (!obs) return ''; const lines = []; if (actionType === 'run_forensics' && obs.host_forensics) { const f = obs.host_forensics; const status = f.is_compromised ? `โš  COMPROMISED` : `โœ“ CLEAN`; lines.push(`Status: ${status}`); if (f.malicious_processes?.length) { lines.push(`Procs: ${f.malicious_processes.join(' ยท ')}`); } if (f.network_connections?.length) { lines.push(`Conns: ${f.network_connections.slice(0, 3).join(' ยท ')}`); } } if (actionType === 'enrich_ioc' && obs.ioc_enrichment) { const e = obs.ioc_enrichment; if (e.threat_actor) lines.push(`Actor: ${e.threat_actor}`); if (e.reputation != null) lines.push(`Reputation: ${e.reputation}`); if (e.mitre_ttps?.length) lines.push(`TTPs: ${e.mitre_ttps.slice(0, 3).join(' ยท ')}`); } if (actionType === 'scan_host_vulnerabilities' && obs.vulnerability_results?.length) { const vulns = obs.vulnerability_results; const critical = vulns.filter(v => (v.cvss_score ?? 0) >= 9).length; const cves = vulns.slice(0, 3).map(v => v.cve_id).join(' ยท '); lines.push(`Found ${vulns.length} CVEs${critical ? ` (${critical} critical)` : ''}`); if (cves) lines.push(`${cves}`); } if (actionType === 'trigger_playbook' && obs.playbook_result) { const p = obs.playbook_result; const success = p.success ?? p.status === 'success'; lines.push(`Result: ${success ? 'โœ“ Executed' : 'โœ— Failed'}`); if (p.actions_taken?.length) lines.push(`Steps: ${p.actions_taken.slice(0, 3).join(' โ†’ ')}`); } if (actionType === 'correlate_alerts' && obs.correlation_results) { const c = obs.correlation_results; const count = Array.isArray(c) ? c.length : (c.correlated_count ?? 1); lines.push(`Correlated ${count} alerts into chain`); if (c.threat_chain) lines.push(`Chain: ${c.threat_chain}`); } return lines.length ? `
${lines.join('
')}
` : ''; } _updateGraphSummary(summary) { const el = document.getElementById('graph-summary'); if (el && summary) el.textContent = summary; } _updateRewardBadge(totalReward) { const badge = document.getElementById('total-reward-badge'); if (!badge) return; const isPos = totalReward >= 0; badge.textContent = `${isPos ? '+' : ''}${totalReward.toFixed(2)}`; badge.className = `reward-badge${isPos ? '' : ' negative'}`; AnimationUtils.scaleBounce(badge); } _updateNetworkTopology(topology) { const container = document.getElementById('network-topology'); if (!container || !topology) return; const subnetConfig = { corporate: { label: 'Corporate', color: '#3b82f6' }, engineering: { label: 'Engineering', color: '#06b6d4' }, finance: { label: 'Finance', color: '#f59e0b' }, dmz: { label: 'DMZ', color: '#94a3b8' }, datacenter: { label: 'Datacenter', color: '#8b5cf6' }, executive: { label: 'Executive', color: '#ef4444' }, }; Object.entries(topology.subnets || {}).forEach(([subnet, count]) => { const cfg = subnetConfig[subnet] || { label: subnet, color: '#94a3b8' }; let section = document.getElementById(`subnet-${subnet}`); if (!section) { section = document.createElement('div'); section.className = 'subnet-section'; section.id = `subnet-${subnet}`; section.innerHTML = `
${cfg.label} ${count} hosts
`; container.appendChild(section); } const grid = document.getElementById(`grid-${subnet}`); if (grid && grid.children.length === 0) { for (let i = 0; i < count; i++) { const dot = document.createElement('div'); dot.className = 'host-dot online'; dot.dataset.idx = i; grid.appendChild(dot); } } }); const subnetHostStatus = {}; this.threatGraph.nodes.forEach((node) => { if (node.nodeType !== 'host' || !node.subnet) return; if (!subnetHostStatus[node.subnet]) subnetHostStatus[node.subnet] = []; subnetHostStatus[node.subnet].push({ hostname: node.id, status: node.status || 'online' }); }); Object.entries(subnetHostStatus).forEach(([subnet, hosts]) => { const grid = document.getElementById(`grid-${subnet}`); if (!grid) return; const dots = [...grid.querySelectorAll('.host-dot')]; hosts.forEach((h, i) => { if (i < dots.length) { dots[i].className = `host-dot ${h.status}`; dots[i].title = `${h.hostname} [${h.status}]`; dots[i].id = `hostdot-${h.hostname}`; } }); }); const topoStatComp = document.getElementById('topo-compromised'); const topoStatIso = document.getElementById('topo-isolated'); if (topoStatComp) topoStatComp.textContent = `${topology.compromised_count ?? 0} compromised`; if (topoStatIso) topoStatIso.textContent = `${topology.isolated_count ?? 0} isolated`; Object.keys(topology.subnets || {}).forEach(subnet => { const section = document.getElementById(`subnet-${subnet}`); if (!section) return; const hosts = subnetHostStatus[subnet] || []; const isIsolated = hosts.some(h => h.status === 'isolated'); const hasCompromised = hosts.some(h => h.status === 'compromised'); section.classList.toggle('isolated', isIsolated); section.classList.toggle('has-compromised', hasCompromised && !isIsolated); let overlay = section.querySelector('.isolated-overlay'); if (isIsolated && !overlay) { overlay = document.createElement('div'); overlay.className = 'isolated-overlay'; overlay.textContent = '๐Ÿ”’ ISOLATED'; section.appendChild(overlay); } else if (!isIsolated && overlay) { overlay.remove(); } }); } _updateBusinessImpact(score) { const marker = document.getElementById('impact-marker'); const value = document.getElementById('impact-value'); if (marker) marker.style.left = `${(score ?? 0) * 100}%`; if (value) { const prev = parseFloat(value.textContent) || 0; AnimationUtils.countUp(value, prev, score ?? 0, 400, 2); value.className = 'impact-value mono' + ((score ?? 0) > 0.6 ? ' critical' : (score ?? 0) > 0.35 ? ' high' : ''); } if ((score ?? 0) > 0.5) { const impactSection = document.querySelector('.impact-section'); if (impactSection) AnimationUtils.pulseGlow(impactSection, (score ?? 0) > 0.7 ? 'red' : 'amber', 2); } } _updateActiveThreats(threats) { const list = document.getElementById('active-threats-list'); if (!list) return; if (!threats || threats.length === 0) { list.innerHTML = 'โœ… All threats contained'; return; } list.innerHTML = threats.map(t => `${t}`).join(''); } _updateContainmentProgress(obs, taskId) { const container = document.getElementById('containment-bars'); if (!container) return; const timeline = obs.timeline || []; const killed = timeline.filter(t => t.action_type === 'kill_process'); const blocked = timeline.filter(t => t.action_type === 'block_ioc'); const forensics = timeline.filter(t => t.action_type === 'run_forensics'); const playbooks = timeline.filter(t => t.action_type === 'trigger_playbook'); const totals = { hard: { kill: 8, block: 6, forensics: 5, playbooks: 1 }, medium: { kill: 4, block: 5, forensics: 3, playbooks: 1 }, easy: { kill: 1, block: 1, forensics: 1, playbooks: 1 }, }; const t = totals[taskId] || totals.hard; const bars = [ { id: 'bar-kill', label: 'Processes Killed', current: Math.min(killed.length, t.kill), total: t.kill }, { id: 'bar-block', label: 'IOCs Blocked', current: Math.min(blocked.length, t.block), total: t.block }, { id: 'bar-forensics', label: 'Hosts Investigated', current: Math.min(forensics.length, t.forensics), total: t.forensics }, { id: 'bar-playbooks', label: 'Playbooks Triggered', current: Math.min(playbooks.length, t.playbooks), total: t.playbooks }, ]; bars.forEach(bar => { let item = document.getElementById(bar.id); if (!item) { item = document.createElement('div'); item.id = bar.id; item.className = 'containment-bar-item'; container.appendChild(item); } const pct = bar.total > 0 ? Math.round((bar.current / bar.total) * 100) : 0; const isComplete = bar.current >= bar.total; item.innerHTML = `
${bar.label} ${bar.current}/${bar.total}${isComplete ? ' โœ“' : ''}
`; }); } _handleRedTeamPivot(pivotAlert) { AnimationUtils.triggerRedTeamPivot(); const list = document.getElementById('alert-list'); if (list) { const card = document.createElement('div'); card.className = 'alert-card pivot'; card.dataset.alertId = pivotAlert.alert_id; card.innerHTML = `
CRITICAL ${pivotAlert.source_host || 'UNKNOWN'}
โšก ADAPTIVE THREAT โ€” LATERAL PIVOT DETECTED. Adversary moved to new host.
`; list.insertBefore(card, list.firstChild); } AnimationUtils.showNotification('โšก Red Team lateral pivot! Containment requirements updated.', 'red', 5000); } _handleEpisodeComplete(obs) { if (this.timerInterval) { clearInterval(this.timerInterval); this.timerInterval = null; } document.getElementById('btn-pause').classList.add('hidden'); document.getElementById('btn-next').classList.add('hidden'); this.currentAction = { type: 'submit_containment_plan' }; this._updatePhase(obs.step_count, obs.max_steps); if (obs.grade_breakdown) { this.store.updateLiveScores(obs.grade_breakdown); } setTimeout(() => { const finalScore = obs.final_score ?? obs.total_reward ?? 0; AnimationUtils.revealFinalScore(finalScore, obs.grade_breakdown, [], []); AnimationUtils.showNotification(`๐Ÿ† Episode complete! Score: ${finalScore.toFixed(3)}`, 'green', 6000); }, 1000); } // ============================================================ // Full UI reset โ€” called via episode:reset store event // ============================================================ _resetUI() { document.getElementById('alert-list').innerHTML = '
Awaiting alerts...
'; document.getElementById('action-log').innerHTML = '
Awaiting agent actions...
'; document.getElementById('active-threats-list').innerHTML = 'No threats detected'; document.getElementById('containment-bars').innerHTML = ''; document.getElementById('network-topology').innerHTML = ''; document.getElementById('network-topology').dataset.built = 'false'; document.getElementById('header-step').textContent = '0/30'; document.getElementById('header-episode').textContent = 'โ€”'; document.getElementById('header-timer').textContent = '00:00'; document.getElementById('impact-value').textContent = '0.00'; document.getElementById('impact-marker').style.left = '0%'; document.getElementById('total-reward-badge').textContent = '+0.00'; document.getElementById('alert-count-badge').textContent = '0'; document.getElementById('difficulty-badge').textContent = 'โ€”'; document.getElementById('difficulty-badge').className = 'difficulty-badge'; document.querySelectorAll('.phase-dot').forEach(d => d.classList.remove('active', 'completed')); document.querySelectorAll('.phase-connector').forEach(c => c.classList.remove('completed')); const turnEl = document.getElementById('turn-indicator'); if (turnEl) turnEl.classList.add('hidden'); this.redTeamController?.setEnabled(false); if (this.rewardTimeline) this.rewardTimeline.reset(); // store.reset() already zeroed liveScores; pass them to the chart directly if (this.radarChart) this.radarChart.update(this.store.get('liveScores')); this.threatGraph = new ClientThreatGraph(); if (this.graphViz._initialized) this.graphViz.update({ nodes: [], links: [] }); document.getElementById('btn-start').classList.remove('hidden'); document.getElementById('task-select').disabled = false; } // Clears dynamic panels at the start of a new episode (no chart reset โ€” scores stay zero // until _onEpisodeStarted explicitly zeroes them via the store). _resetUIForEpisode() { document.getElementById('alert-list').innerHTML = ''; document.getElementById('action-log').innerHTML = ''; document.getElementById('containment-bars').innerHTML = ''; document.getElementById('network-topology').innerHTML = ''; document.getElementById('network-topology').dataset.built = 'false'; if (this.rewardTimeline) this.rewardTimeline.reset(); } // ============================================================ // Helpers // ============================================================ _getActionCategory(actionType) { if (['correlate_alerts'].includes(actionType)) return 'triage'; if (['query_host', 'run_forensics', 'enrich_ioc', 'scan_host_vulnerabilities'].includes(actionType)) return 'investigation'; if (['kill_process', 'block_ioc', 'isolate_segment', 'trigger_playbook'].includes(actionType)) return 'remediation'; if (['submit_containment_plan'].includes(actionType)) return 'report'; return 'investigation'; } _getActionIcon(actionType) { const icons = { query_host: '๐Ÿ”', run_forensics: '๐Ÿงฌ', enrich_ioc: '๐Ÿ”Ž', scan_host_vulnerabilities: '๐Ÿฉบ', kill_process: 'โš”๏ธ', block_ioc: '๐Ÿšซ', isolate_segment: '๐Ÿ”’', trigger_playbook: 'โ–ถ๏ธ', correlate_alerts: '๐Ÿ”—', submit_containment_plan: '๐Ÿ“', }; return icons[actionType] || 'โžค'; } _showConnectionOverlay(msg) { const overlay = document.getElementById('connection-overlay'); const status = document.getElementById('connection-status'); if (overlay) overlay.classList.remove('hidden'); if (status) status.textContent = msg; } _hideConnectionOverlay() { const overlay = document.getElementById('connection-overlay'); if (overlay) { overlay.style.transition = 'opacity 0.5s ease'; overlay.style.opacity = '0'; setTimeout(() => { overlay.classList.add('hidden'); overlay.style.opacity = ''; overlay.style.transition = ''; }, 500); } } closeScoreOverlay() { const overlay = document.getElementById('final-score-overlay'); if (overlay) { overlay.style.transition = 'opacity 0.4s ease'; overlay.style.opacity = '0'; setTimeout(() => { overlay.classList.add('hidden'); overlay.style.opacity = ''; }, 400); } } } // ============================================================ // Bootstrap // ============================================================ let dashboard; document.addEventListener('DOMContentLoaded', () => { dashboard = new CyberSOCDashboard(); window.dashboard = dashboard; dashboard.init(); });