diff --git a/app/views/break_escape/games/show.html.erb b/app/views/break_escape/games/show.html.erb index 2253d4c0..8c9f6ebe 100644 --- a/app/views/break_escape/games/show.html.erb +++ b/app/views/break_escape/games/show.html.erb @@ -50,6 +50,7 @@ + diff --git a/index.html b/index.html index 880cc260..97dc2bba 100644 --- a/index.html +++ b/index.html @@ -46,6 +46,7 @@ + diff --git a/planning_notes/siem/MG01_siem_alignment_audit.md b/planning_notes/siem/MG01_siem_alignment_audit.md new file mode 100644 index 00000000..414575e1 --- /dev/null +++ b/planning_notes/siem/MG01_siem_alignment_audit.md @@ -0,0 +1,96 @@ +# MG01 SIEM Dashboard - Alignment Audit Against game_design + +## Purpose + +This document audits the existing SIEM planning docs against game_design source documents. + +Reviewed source set: + +- game_design/minigame_design_guide.md +- game_design/minigame_planning.md +- game_design/new_objects_planning.md +- game_design/healthcare_draft/gdd.md +- game_design/healthcare_draft/gdd_walkthrough.md + +Reviewed SIEM planning docs: + +- planning_notes/siem/MG01_siem_implementation_plan.md +- planning_notes/siem/MG01_siem_ui_ideas_and_visual_direction.md + +## Non-Negotiable MG01 Requirements (From game_design) + +1. Minigame category: HTML/CSS. +2. Scenario position: Room 2, Ravi Anand's laptop. +3. Core mechanic: mixed alert stream with row actions DISMISS or ESCALATE. +4. Correct critical IoCs include: + +- Encoded PowerShell execution. +- LSASS access. +- Anomalous SMB write volumes. +- Cross-zone RDP sessions. + +5. Outcome variables: + +- Success path writes siem_escalated = true. +- Missed critical path writes siem_missed_alerts = true. + +6. Re-entry behavior: minigame state must persist on reopen. +7. Runtime injection support: siem_new_alert event can add alerts. +8. Required visual structure: + +- Header text: NORTHGATE TRUST // SIEM CONSOLE. +- Dark charcoal panel background (#1a1a2e). +- Left alert pane (~70%) and right escalation queue (~30%). +- Bottom status bar with ALERTS PENDING and TIME REMAINING. +- Result banner text includes full failure wording: CRITICAL ALERTS MISSED - INCIDENT ESCALATED. + +9. Required visual behavior: + +- CRIT flashes at 1 Hz. +- Dismissed rows fade and shift left. +- Escalated rows show green left border and move to queue. + +## What Is Already Correct in Current SIEM Plans + +1. Framework approach is correct: MinigameScene-based HTML/CSS minigame. +2. Interaction model is correct: per-row DISMISS/ESCALATE triage. +3. Scenario integration intent is correct: Ravi laptop in Room 2. +4. Global variable writebacks use the correct names (siem_escalated / siem_missed_alerts). +5. Event model includes siem_new_alert and ransomware-driven alert surge behavior. +6. Persistence is explicitly planned (save/restore state on reopen). +7. Critical IoC examples match the game_design narrative. + +## Mismatches Found + +1. UI plan header branding is not aligned: + +- Uses SAFETYNET/ops-suite style header instead of required NORTHGATE TRUST // SIEM CONSOLE. + +2. UI plan layout departs from required two-pane structure: + +- Introduces a dedicated third telemetry column. +- game_design specifies two-pane composition (left log + right escalation queue). + +3. UI plan color tokens diverge from specified background: + +- Uses alternate dark values instead of required #1a1a2e base direction. + +4. UI plan severity naming drifts from game_design naming: + +- Adds INFO and uses MEDIUM/CRITICAL wording instead of MED/CRIT naming in visual labels. + +5. Failure banner text is abbreviated in the UI plan: + +- Must retain full message: CRITICAL ALERTS MISSED - INCIDENT ESCALATED. + +6. UI plan does not explicitly lock button color mapping: + +- game_design explicitly calls for DISMISS dark grey and ESCALATE amber. + +## Audit Conclusion + +The implementation plan is mostly aligned on mechanics and integration. +The UI ideas plan needs a strict alignment pass for naming, structure, and exact visual callouts from game_design. + +No expansion of feature scope is required. +No new gameplay systems are required. diff --git a/planning_notes/siem/MG01_siem_implementation_plan.md b/planning_notes/siem/MG01_siem_implementation_plan.md new file mode 100644 index 00000000..a6165682 --- /dev/null +++ b/planning_notes/siem/MG01_siem_implementation_plan.md @@ -0,0 +1,311 @@ +# MG01 SIEM Dashboard - Implementation Plan + +## Scope and Intent + +MG01 is an interactive SIEM triage minigame where the player classifies alerts as DISMISS or ESCALATE under time pressure. + +Primary learning objective: + +- Teach alert fatigue and false-positive discrimination in a mixed stream of migration noise and true attack indicators. + +Scenario objective: + +- Correctly escalate the critical indicators to set `siem_escalated = true`. +- Incorrect handling (especially dismissing critical alerts) sets `siem_missed_alerts = true`. + +Design objective: + +- Build as a normal minigame pane inside the existing Break Escape minigame framework, while visually reading as another panel in the same software family as the SAFETYNET visualiser SIEM mode. + +## Constraints Gathered From Existing Docs/Code + +From game design docs: + +- MG01 is minigame #1 in the healthcare flow and occurs in Room 2 (Ravi laptop). +- It must include a scrollable live event log and player triage actions. +- Correct critical set includes: encoded PowerShell, LSASS access, anomalous SMB write volume, cross-zone RDP session. +- It should support state persistence while open/close and allow injected alerts via events. + +From runtime architecture: + +- Minigames are classes extending `MinigameScene`. +- Registration is in `public/break_escape/js/minigames/index.js`. +- Trigger entry points are typically from `public/break_escape/js/systems/minigame-starters.js` and `unlock-system.js` lockType routing. +- Event bus is `window.eventDispatcher` (on/emit/off). +- Cross-system reactive state should be written via `window.npcManager.setGlobalVariable(...)`. + +From visual references: + +- Existing SIEM-like visual language in `public/break_escape/js/music/bond-visualiser.js` and `public/break_escape/css/bond-visualiser.css`. +- Strong candidate visual tokens to reuse: + - Palette: green/gold/red/cyan on near-black. + - Pixel corners, framed panels, scanline/noise treatment. + - Press Start 2P and VT323 hierarchy. + - Dense telemetry pane composition. + +## Recommended Technical Approach + +Implement MG01 as an HTML/CSS minigame (not Phaser) extending `MinigameScene`. + +Reasoning: + +- Existing docs explicitly classify SIEM dashboard as HTML/CSS. +- UI behavior is list + controls + queue + timer, which is DOM-friendly and easier to theme. +- Faster iteration for text-heavy interaction and accessibility. + +## File Plan + +1. Add minigame scene class: + +- `public/break_escape/js/minigames/siem/siem-dashboard-minigame.js` + +2. Add styles: + +- `public/break_escape/css/siem-dashboard-minigame.css` + +3. Register scene: + +- Update `public/break_escape/js/minigames/index.js` +- Register key: `siem-dashboard` (or `siem` if no naming collision risk) + +4. Add starter helper: + +- Update `public/break_escape/js/systems/minigame-starters.js` +- Add `startSiemDashboardMinigame(...)` + +5. Route from interaction layer: + +- Preferred: add lockType handling in `public/break_escape/js/systems/unlock-system.js` for `lockType: "siem_dashboard"`. +- Alternative (if object is non-lock terminal): trigger via object interaction handler directly. + +6. Ensure CSS load path: + +- If engine uses centralized CSS includes, add stylesheet there. +- If minigames self-inject styles, follow existing project pattern used by other minigames. + +## Scenario Contract + +For scenario objects representing the SIEM laptop, use scenarioData such as: + +```json +{ + "locked": true, + "lockType": "siem_dashboard", + "name": "Ravi Anand Laptop", + "scenarioData": { + "mgId": "MG01", + "timeLimitSec": 180, + "requiredCriticalIds": ["ALRT-001", "ALRT-005", "ALRT-012", "ALRT-018"], + "autoInject": true, + "seed": "northgate_day1" + } +} +``` + +Notes: + +- Keep server authority for final scenario gating if applicable. +- Client minigame is UX and progression signaling, but backend validation should remain authoritative for unlock-critical paths. + +## Runtime Data Model (In-Minigame) + +```js +state = { + startedAt: number, + remainingSec: number, + alerts: Array, + escalatedIds: Set, + dismissedIds: Set, + pendingIds: Set, + criticalIds: Set, + triageHistory: Array<{id, action, ts}>, + finished: boolean +} +``` + +Alert shape: + +```js +{ + id: string, + severity: 'INFO' | 'LOW' | 'MEDIUM' | 'HIGH' | 'CRITICAL', + timestamp: string, + source: string, + description: string, + iocType: string, + critical: boolean, + status: 'pending' | 'dismissed' | 'escalated' +} +``` + +## Completion Logic + +Suggested rule set: + +- Success if all critical alerts are escalated before timer expiry. +- Failure if timer expires with any critical un-escalated. +- Optional immediate fail mode: dismissing any critical alert marks hard failure. + +Recommended for narrative consistency: + +- Do not hard-fail immediately; allow continued triage, then evaluate at timer end or when all critical handled. +- This supports "alerts were there but missed" messaging without abrupt modal interruption. + +Outcome writeback: + +- On success: + - `window.npcManager.setGlobalVariable('siem_escalated', true)` + - `window.npcManager.setGlobalVariable('siem_missed_alerts', false)` +- On failure: + - `window.npcManager.setGlobalVariable('siem_missed_alerts', true)` + +Also emit event for hooks: + +```js +window.eventDispatcher.emit("siem_triage_completed", { + success, + escalated: [...state.escalatedIds], + dismissed: [...state.dismissedIds], + missedCritical: [...missedCriticalIds], +}); +``` + +Then call `this.complete(success)`. + +## Event Integration + +Subscribe inside `start()` using `this.addEventListener(...)`: + +- `siem_new_alert`: append dynamically injected alert rows. +- `global_variable_changed:ransomware_deployed`: border pulse + burst of CRITICAL events. + +Example handling: + +- `siem_new_alert` payload normalized and inserted at top of stream. +- Debounce render and cap max rows (e.g., 200) to avoid DOM slowdown. + +## Persistence Behavior + +Required by design docs: reopen should continue prior state. + +Implementation options: + +1. Session-only in `window.gameState.globalVariables['mg01_siem_state']`. +2. Server sync piggyback through existing state-sync (preferred if scenario requires cross-reload continuity). + +Minimum viable persistence: + +- Save timer offset, triage actions, and alert statuses on each action. +- Restore in constructor/init when state exists and not completed. + +## UX Flow + +1. Open panel with seeded alerts and active timer. +2. Player triages rows via DISMISS/ESCALATE. +3. Escalated queue updates right pane and counters. +4. Alerts can arrive in real time. +5. Completion banner appears when conditions met. +6. Variables/events written; minigame closes via framework callback. + +## Integration With NPC/Objective Systems + +Expected downstream effects based on design docs: + +- Ravi dialogue branch unlock tied to `siem_escalated = true`. +- Command board timeline appends SIEM finding events. +- Failure route (`siem_missed_alerts = true`) influences later consequence dialogue. + +Therefore ensure: + +- Variable names exactly match design docs. +- Result payload includes enough detail for optional NPC flavor reactions. + +## Testing Plan + +Unit-like checks (manual + script-level): + +- Correct triage set -> success path writes expected globals. +- Missed critical -> failure path writes expected globals. +- Reopen state restore works (timer and row statuses). +- `siem_new_alert` injection works while panel is open. +- Event listeners cleaned on close (no duplicate listeners after reopen). + +Gameplay checks: + +- Room 2 laptop launches MG01 reliably. +- Ravi dialogue changes after success/failure. +- Command board reflects outcome. + +Visual checks: + +- Readable at target resolutions. +- Button affordance clear under pressure. +- Severity color and flashing behavior obvious but not noisy. + +## Implementation Milestones + +1. Scaffolding + +- Create scene class, styles, registration, starter. + +2. Core mechanics + +- Seed data, triage actions, queue pane, timer, evaluation. + +3. Integration + +- Global variables + event emissions + scenario trigger route. + +4. Persistence + +- Save/restore state on reopen. + +5. Visual pass + +- Apply visualiser-compatible styling cues. + +6. QA and balancing + +- Tune timer, alert count, critical/noise ratio, readability. + +## Balancing Defaults (Initial) + +- Time limit: 180 seconds. +- Total alerts shown: 18 to 24. +- Critical alerts: 4 fixed (the canonical attack chain). +- Noise ratio: roughly 70% benign. +- Auto-injection: 1 to 3 additional alerts during play. + +## Risks and Mitigations + +Risk: player treats all alerts as ESCALATE spam. + +- Mitigation: optional scoring penalty for over-escalation and Ravi feedback on triage quality. + +Risk: visual overload harms comprehension. + +- Mitigation: strict typography hierarchy, row spacing, severity anchors. + +Risk: state desync with scenario progression. + +- Mitigation: one authoritative completion write path and explicit completion event payload. + +Risk: event listener leaks on reopen. + +- Mitigation: register via `this.addEventListener` only, rely on base cleanup. + +## Nice-to-Have Extensions + +- Scorecard (precision/recall style) in post-result panel. +- Difficulty profiles (easy/normal/hard) by alert ambiguity. +- Context drawer: clicking an alert opens richer forensic details. +- Minor SFX tied to escalate/dismiss and critical arrival. + +## Definition of Done + +- MG01 launches from the intended in-world terminal. +- Player can complete triage loop with clear success/fail outcomes. +- `siem_escalated` / `siem_missed_alerts` update correctly. +- Re-entry persistence works in-session. +- Visual style reads as the same software ecosystem as existing SIEM visualiser mode. +- No regressions in existing minigame lifecycle behavior. diff --git a/planning_notes/siem/MG01_siem_revision_plan_strict_alignment.md b/planning_notes/siem/MG01_siem_revision_plan_strict_alignment.md new file mode 100644 index 00000000..fd43e91b --- /dev/null +++ b/planning_notes/siem/MG01_siem_revision_plan_strict_alignment.md @@ -0,0 +1,124 @@ +# MG01 SIEM Dashboard - Strict Alignment Revision Plan (No Scope Expansion) + +## Objective + +Apply only minimal corrections so SIEM planning matches game_design exactly. + +This plan does not add new features. +This plan does not change scenario flow. +This plan only resolves mismatches. + +## Revision Set A - Required Corrections + +### A1. Title and Header Identity + +Change UI direction to use the exact in-world title: + +- NORTHGATE TRUST // SIEM CONSOLE + +Keep optional visual cues from the existing visualiser only where they do not alter this identity. + +### A2. Layout Structure + +Use the exact two-pane arrangement from game_design: + +- Left pane (~70%): alert log stream. +- Right pane (~30%): escalated-for-review queue. + +Remove the extra telemetry column from the primary layout. + +### A3. Background and Base Tone + +Use dark charcoal direction centered on: + +- #1a1a2e + +Other accent colors remain severity-driven and consistent with game_design. + +### A4. Severity Labels and Badge Mapping + +Use game_design label set in the UI: + +- LOW, MED, HIGH, CRIT + +Badge colors: + +- LOW = grey +- MED = amber +- HIGH = orange +- CRIT = red (1 Hz flash) + +### A5. Button Color Contract + +Lock button visuals to game_design: + +- DISMISS = dark grey +- ESCALATE = amber + +### A6. Result Banner Copy + +Use exact copy contract: + +- Success: INCIDENT TEAM NOTIFIED +- Failure: CRITICAL ALERTS MISSED - INCIDENT ESCALATED + +### A7. Status Bar Copy + +Use exact status line structure: + +- ALERTS PENDING: X | TIME REMAINING: MM:SS + +## Revision Set B - Clarifications (Still No New Features) + +### B1. Time Presentation Clarification + +Preserve both game_design time elements without adding mechanics: + +1. Header includes live system clock (top-right). +2. Bottom status bar includes countdown TIME REMAINING. + +### B2. Event and Variable Name Lock + +Keep exact interaction names for scenario compatibility: + +- Event input: siem_new_alert +- Global outputs: siem_escalated, siem_missed_alerts + +No aliasing or renaming. + +### B3. Critical IoC Canonical Set + +Keep the fixed canonical critical set in all examples and logic references: + +- Encoded PowerShell execution +- LSASS access +- Anomalous SMB write volumes +- Cross-zone RDP sessions + +## Revision Set C - Keep As-Is + +The following existing planning choices should remain unchanged: + +1. HTML/CSS implementation approach. +2. MinigameScene lifecycle integration and cleanup model. +3. Re-entry state persistence requirement. +4. Dynamic alert injection support while open. +5. Outcome signaling to downstream NPC/objective/command-board flow via existing global variable names. + +## Acceptance Checklist + +A revised SIEM planning package is aligned when all are true: + +1. Header text exactly matches NORTHGATE TRUST // SIEM CONSOLE. +2. Primary layout is two-pane 70/30 as specified. +3. Required background direction uses #1a1a2e. +4. Severity labels and colors match LOW/MED/HIGH/CRIT mapping. +5. DISMISS/ESCALATE colors match dark grey/amber. +6. Result banner failure copy includes INCIDENT ESCALATED. +7. Global variable and event names match game_design exactly. +8. No additional features beyond game_design are introduced. + +## Implementation Note + +When updating the existing two SIEM planning docs in a later edit pass, apply only A1-A7 and B1-B3. +Do not modify scenario mechanics beyond those alignment corrections. diff --git a/planning_notes/siem/MG01_siem_ui_ideas_and_visual_direction.md b/planning_notes/siem/MG01_siem_ui_ideas_and_visual_direction.md new file mode 100644 index 00000000..f6a05c15 --- /dev/null +++ b/planning_notes/siem/MG01_siem_ui_ideas_and_visual_direction.md @@ -0,0 +1,202 @@ +# MG01 SIEM Dashboard - UI Ideas and Visual Direction + +## Goal + +Make MG01 feel like a sibling pane of the existing SAFETYNET visualiser app, not a separate unrelated tool. + +Desired player perception: + +- "I am still inside the same operations software stack." + +## Visual Reference Synthesis + +Borrow cues from the visualiser SIEM mode and shell: + +- Color language: + - Green for baseline telemetry. + - Gold for metadata/highlight. + - Red for criticals. + - Cyan for network/source detail. +- Framing language: + - Pixel corner brackets. + - Thin technical panel borders. + - Scanline/noise subtle overlays. +- Type hierarchy: + - Press Start 2P for labels/headings/status chips. + - VT323 for dense row data and counters. + +Do not duplicate the full music overlay chrome. +Instead, present MG01 as a dedicated module tab in that ecosystem. + +## "Same App, Different Pane" Concept + +Use a shell title pattern like: + +- `SAFETYNET // INCIDENT OPS SUITE` + +Module title: + +- `MODULE: SIEM TRIAGE (MG01)` + +This keeps continuity while communicating functional context shift. + +## Layout Blueprint + +Three-column operational layout: + +1. Left rail (telemetry) + +- Threat score block (0-100). +- Timer block. +- Severity counts. +- Tiny sparkline for incoming event pressure. + +2. Center main stream (triage list) + +- Scrollable event rows with fixed row height. +- Columns: SEV, TIME, SRC, EVENT, ACTIONS. +- Actions per row: DISMISS and ESCALATE. +- Current selection highlight for keyboard support. + +3. Right rail (escalation queue) + +- Header: `ESCALATED FOR REVIEW`. +- Queue list with criticals pinned top. +- Live count and optional checklist of required critical IDs. + +Bottom status strip: + +- `PENDING: X | ESCALATED: Y | DISMISSED: Z | TIME: MM:SS` + +## Severity and Motion Language + +Severity treatments: + +- INFO: subdued green text, low-contrast row background. +- LOW: cyan accent. +- MEDIUM: gold accent. +- HIGH: orange accent and border pulse every ~1.5s. +- CRITICAL: red badge with 1Hz blink and stronger left border. + +Motion rules: + +- Dismiss action: row fades to 35% and shifts left 4px. +- Escalate action: row slides right into queue and receives green "routed" check flash. +- New incoming alerts: drop-in from top with a 120-180ms transition. + +Keep animations intentional and brief; avoid noisy perpetual movement. + +## Component Ideas + +### Header Cluster + +- App title + module title. +- Live clock. +- Connection state chip: `SIEM CORE: LIVE`. + +### Event Row Template + +- Severity chip. +- Timestamp (monospace). +- Source short code. +- Event text line. +- Right-aligned actions. + +Optional tiny metadata hover details: + +- Country, MITRE tactic, confidence, host/user. + +### Queue Row Template + +- Severity + short event text. +- Escalation timestamp. +- Mini source badge. + +### Result Banner + +- Success: green bar `INCIDENT TEAM NOTIFIED`. +- Failure: red bar `CRITICAL ALERTS MISSED`. + +## Design Tokens (Proposed) + +```css +--siem-bg: #03070c; +--siem-panel: #07111a; +--siem-grid: rgba(0, 255, 65, 0.08); +--siem-text: #a9f7c8; +--siem-green: #00ff41; +--siem-cyan: #00ffff; +--siem-gold: #ffd700; +--siem-orange: #ff6600; +--siem-red: #ff003c; +--siem-border: #0f3b20; +``` + +Typography: + +- Labels/headings: Press Start 2P, 8-10px. +- Data rows: VT323, 16-19px. +- Timer/threat numerics: VT323, 26-34px. + +## Micro-Interactions + +- Keyboard shortcuts: + - Up/Down to move row focus. + - E to escalate selected row. + - D to dismiss selected row. +- Button states: + - Hover border glow. + - Disabled style after decision. +- Audio (optional): + - Soft click for row action. + - Distinct alert chirp for new CRITICAL. + +## Accessibility and Readability + +- Keep contrast above practical readability threshold for low-light scenes. +- Never encode severity by color alone; include explicit severity text chip. +- Provide reduced-motion fallback class if needed. +- Avoid text below 8px in Press Start 2P; for dense data use VT323. + +## Mobile/Small View Fallback + +When horizontal space is constrained: + +- Collapse right queue into a bottom drawer tab (`Escalations`). +- Keep center stream full width. +- Move telemetry stats to compact top strip. + +Even if not primary target, this keeps panel robust in varied test setups. + +## Thematic Flavor Ideas + +- Header operation codename rotates from an approved list (same visualiser concept). +- Top-right hint line from Ravi context: + - `RAVI: "Filter migration noise, find true IoCs."` +- Subtle map/radar ghost graphic in background at 4-6% opacity. + +## Copy Tone + +Short, operational, non-cinematic. +Examples: + +- `ALERT INGEST ACTIVE` +- `ANALYST ACTION REQUIRED` +- `QUEUE FOR INCIDENT REVIEW` + +Avoid dramatic hacker cliches for this module because educational clarity matters. + +## Concrete UI Deliverable Checklist + +- Shared-shell look aligned with visualiser palette and framing language. +- Distinct SIEM module title and clear triage affordances. +- Three-pane desktop layout plus compact fallback. +- Clear state transitions for pending/dismissed/escalated. +- Result banner and bottom status strip. +- Minimal, meaningful animation set. + +## Optional Future Visual Cohesion Step + +Create a lightweight shared style file (example: `ops-suite-theme.css`) for reusable tokens and chrome. + +MG01 can consume that theme now, and future tools (network map, command board) can migrate later to strengthen suite-level consistency. diff --git a/public/break_escape/css/siem-dashboard-minigame.css b/public/break_escape/css/siem-dashboard-minigame.css new file mode 100644 index 00000000..fed0908f --- /dev/null +++ b/public/break_escape/css/siem-dashboard-minigame.css @@ -0,0 +1,521 @@ +.siem-minigame-container { + background: #0d1324; + border: 2px solid #2f3a59; +} + +.siem-minigame-game-container { + padding: 14px; + box-sizing: border-box; + overflow: visible; +} + +.siem-panel { + position: relative; + display: flex; + flex-direction: column; + width: 100%; + height: 100%; + margin: 0; + box-sizing: border-box; + min-height: 520px; + background: #1a1a2e; + border: 2px solid #3a4a6d; + overflow: hidden; +} + +.siem-result-banner { + position: absolute; + top: -60px; + left: 0; + right: 0; + z-index: 3; + height: 56px; + display: block; + font-family: 'Press Start 2P', monospace; + font-size: 12px; + letter-spacing: 1px; + color: #ffffff; + transition: top 0.25s ease; + padding: 0 72px 0 16px; + box-sizing: border-box; + text-align: center; + line-height: 56px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.siem-result-banner.show { + top: 0; +} + +.siem-result-banner.success { + background: #1f7a3b; + border-bottom: 2px solid #39c164; +} + +.siem-result-banner.failure { + background: #7a1f2e; + border-bottom: 2px solid #ff3a5a; +} + +.siem-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 12px; + border-bottom: 2px solid #364261; + background: #11192b; +} + +.siem-title { + font-family: 'Press Start 2P', monospace; + font-size: 12px; + color: #c8d8ff; + letter-spacing: 1px; +} + +.siem-clock { + font-family: 'VT323', monospace; + font-size: 32px; + color: #ffd27f; + margin-right: 64px; +} + +.siem-body { + display: grid; + grid-template-columns: 7fr 3fr; + gap: 8px; + flex: 1; + min-height: 0; + padding: 10px; +} + +.siem-alert-pane, +.siem-queue-pane { + border: 2px solid #2e3957; + background: #131f38; + display: flex; + flex-direction: column; + min-height: 0; +} + +.siem-pane-title { + padding: 10px; + border-bottom: 2px solid #2e3957; + font-family: 'Press Start 2P', monospace; + font-size: 11px; + color: #9ac1ff; + background: #0f1730; +} + +.siem-alert-list, +.siem-queue-list { + padding: 8px; + overflow-y: auto; +} + +.siem-alert-list { + display: flex; + flex-direction: column; + gap: 6px; +} + +.siem-alert-row { + display: grid; + grid-template-columns: 70px 86px 140px minmax(0, 1fr) 200px; + gap: 10px; + align-items: center; + min-height: 60px; + padding: 8px; + border: 2px solid #32466f; + background: #1a2745; + transition: transform 0.18s ease, opacity 0.18s ease; +} + +.siem-alert-row.status-dismissed { + opacity: 0.3; + transform: translateX(-6px); +} + +.siem-alert-row.status-escalated { + border-left: 4px solid #47c46a; +} + +.siem-severity { + display: inline-flex; + align-items: center; + justify-content: center; + height: 32px; + font-family: 'Press Start 2P', monospace; + font-size: 11px; + color: #ffffff; + border: 2px solid rgba(255, 255, 255, 0.2); +} + +.siem-severity.sev-LOW { + background: #6e7583; +} + +.siem-severity.sev-MED { + background: #c28733; +} + +.siem-severity.sev-HIGH { + background: #d86a29; +} + +.siem-severity.sev-CRIT { + background: #cf2b45; + animation: siem-crit-flash 1s steps(1, end) infinite; +} + +@keyframes siem-crit-flash { + 0% { opacity: 1; } + 50% { opacity: 0.35; } + 100% { opacity: 1; } +} + +.siem-time, +.siem-source, +.siem-description, +.siem-queue-text, +.siem-queue-count, +.siem-status-bar { + font-family: 'VT323', monospace; +} + +.siem-time, +.siem-source, +.siem-description { + font-size: 20px; + color: #d0dbf0; +} + +.siem-source { + color: #8bd0f0; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.siem-description { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.siem-actions { + display: inline-flex; + gap: 6px; + justify-content: flex-end; +} + +.siem-btn { + min-width: 90px; + height: 36px; + border: 2px solid #2d3856; + font-family: 'Press Start 2P', monospace; + font-size: 16px; + color: #ffffff; + cursor: pointer; +} + +.siem-btn.dismiss { + background: #4b5260; +} + +.siem-btn.escalate { + background: #c28733; +} + +.siem-btn:disabled { + opacity: 0.55; + cursor: default; +} + +.siem-queue-count { + padding: 10px; + font-size: 22px; + color: #d2e0ff; + border-bottom: 2px solid #2e3957; +} + +.siem-queue-list { + display: flex; + flex-direction: column; + gap: 6px; +} + +.siem-queue-item { + display: grid; + grid-template-columns: 56px minmax(0, 1fr); + gap: 8px; + align-items: center; + min-height: 42px; + padding: 6px; + border: 2px solid #32466f; + background: #17243f; +} + +.siem-queue-sev { + display: inline-flex; + align-items: center; + justify-content: center; + height: 28px; + border: 2px solid rgba(255, 255, 255, 0.2); + font-family: 'Press Start 2P', monospace; + font-size: 10px; + color: #ffffff; +} + +.siem-queue-sev.sev-LOW { + background: #6e7583; +} + +.siem-queue-sev.sev-MED { + background: #c28733; +} + +.siem-queue-sev.sev-HIGH { + background: #d86a29; +} + +.siem-queue-sev.sev-CRIT { + background: #cf2b45; +} + +.siem-queue-text { + font-size: 19px; + color: #d6e2ff; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.siem-status-bar { + display: flex; + justify-content: space-between; + align-items: center; + min-height: 48px; + padding: 10px 12px; + border-top: 2px solid #364261; + background: #101729; + font-size: 24px; + color: #d2e0ff; +} + +.siem-panel.ransomware-pulse { + animation: siem-panel-pulse 0.8s steps(1, end) infinite; +} + +@keyframes siem-panel-pulse { + 0% { + border-color: #3a4a6d; + box-shadow: 0 0 0 0 rgba(255, 70, 94, 0); + } + 50% { + border-color: #ff4f67; + box-shadow: 0 0 28px 6px rgba(255, 70, 94, 0.6); + } + 100% { + border-color: #3a4a6d; + box-shadow: 0 0 0 0 rgba(255, 70, 94, 0); + } +} + +/* ── Severity Breakdown Section ───────────────────────────────────────── */ + +.siem-queue-section-title { + padding: 10px 8px 6px; + font-family: 'Press Start 2P', monospace; + font-size: 10px; + color: #ffd700; + text-transform: uppercase; + letter-spacing: 0.5px; + border-top: 2px solid #2e3957; + background: #0f1730; + margin-top: 8px; +} + +.siem-severity-chart { + padding: 8px; + display: flex; + align-items: stretch; + gap: 3px; + height: 20px; + min-height: 20px; + flex: 0 0 auto; + box-sizing: content-box; + margin-bottom: 6px; + border: 2px solid #2e3957; + background: #131f38; +} + +.siem-severity-bar { + flex: 1; + height: 100%; + min-height: 100%; + box-sizing: border-box; + border: 1px solid rgba(255, 255, 255, 0.1); + min-width: 4px; +} + +.siem-severity-bar.sev-LOW { + background: #6e7583; +} + +.siem-severity-bar.sev-MED { + background: #c28733; +} + +.siem-severity-bar.sev-HIGH { + background: #d86a29; +} + +.siem-severity-bar.sev-CRIT { + background: #cf2b45; +} + +.siem-severity-legend { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 6px; + padding: 6px; +} + +.siem-severity-item { + display: flex; + align-items: center; + gap: 6px; + font-family: 'Press Start 2P', monospace; + font-size: 9px; + color: #d2e0ff; +} + +.siem-severity-item-color { + display: inline-block; + width: 14px; + height: 14px; + border: 1px solid rgba(255, 255, 255, 0.2); +} + +.siem-severity-item-color.sev-LOW { + background: #6e7583; +} + +.siem-severity-item-color.sev-MED { + background: #c28733; +} + +.siem-severity-item-color.sev-HIGH { + background: #d86a29; +} + +.siem-severity-item-color.sev-CRIT { + background: #cf2b45; +} + +.siem-severity-item-count { + margin-left: auto; + color: #a8b8d8; + font-size: 10px; +} + +.siem-alert-score-box { + padding: 8px; + margin-top: 6px; + border: 2px solid #2e3957; + background: #131f38; + text-align: center; +} + +.siem-alert-score-label { + font-family: 'Press Start 2P', monospace; + font-size: 10px; + color: #00ffff; + text-transform: uppercase; + letter-spacing: 0.5px; + margin-bottom: 4px; + display: block; +} + +.siem-alert-score-value { + font-family: 'VT323', monospace; + font-size: 24px; + color: #00ff41; +} + +/* ── Top Sources Section ───────────────────────────────────────── */ + +.siem-sources-box { + padding: 8px; + border: 2px solid #2e3957; + background: #131f38; + margin-top: 6px; +} + +.siem-sources-empty { + font-family: 'VT323', monospace; + font-size: 12px; + color: #6e7583; + text-align: center; + padding: 12px; +} + +.siem-sources-list { + display: flex; + flex-direction: column; + gap: 6px; +} + +.siem-source-row { + display: grid; + grid-template-columns: 80px minmax(0, 1fr) 28px; + gap: 6px; + align-items: center; + min-height: 24px; + padding: 4px; +} + +.siem-source-name { + font-family: 'Press Start 2P', monospace; + font-size: 9px; + color: #d2e0ff; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.siem-source-bar-container { + background: rgba(0, 255, 255, 0.1); + border: 1px solid rgba(0, 255, 255, 0.3); + height: 12px; + position: relative; +} + +.siem-source-bar { + background: #00ffff; + height: 100%; + transition: width 0.3s ease; +} + +.siem-source-count { + font-family: 'VT323', monospace; + font-size: 10px; + color: #00ffff; + text-align: right; + min-width: 28px; +} + +@media (max-width: 1100px) { + .siem-alert-row { + grid-template-columns: 56px 72px 105px minmax(0, 1fr) 168px; + } + + .siem-time, + .siem-source, + .siem-description { + font-size: 16px; + } +} diff --git a/public/break_escape/js/minigames/index.js b/public/break_escape/js/minigames/index.js index ef453dea..d8443a20 100644 --- a/public/break_escape/js/minigames/index.js +++ b/public/break_escape/js/minigames/index.js @@ -18,6 +18,7 @@ export { TitleScreenMinigame, startTitleScreenMinigame } from './title-screen/ti export { RFIDMinigame, startRFIDMinigame, returnToConversationAfterRFID } from './rfid/rfid-minigame.js'; export { VmLauncherMinigame } from './vm-launcher/vm-launcher-minigame.js'; export { FlagStationMinigame } from './flag-station/flag-station-minigame.js?v=6'; +export { SiemDashboardMinigame } from './siem/siem-dashboard-minigame.js'; // Initialize the global minigame framework for backward compatibility import { MinigameFramework } from './framework/minigame-manager.js'; @@ -84,6 +85,7 @@ import { VmLauncherMinigame } from './vm-launcher/vm-launcher-minigame.js'; // Import the flag station minigame import { FlagStationMinigame } from './flag-station/flag-station-minigame.js?v=6'; +import { SiemDashboardMinigame } from './siem/siem-dashboard-minigame.js'; // Register minigames MinigameFramework.registerScene('lockpicking', LockpickingMinigamePhaser); // Use Phaser version as default @@ -102,6 +104,7 @@ MinigameFramework.registerScene('title-screen', TitleScreenMinigame); MinigameFramework.registerScene('rfid', RFIDMinigame); MinigameFramework.registerScene('vm-launcher', VmLauncherMinigame); MinigameFramework.registerScene('flag-station', FlagStationMinigame); +MinigameFramework.registerScene('siem-dashboard', SiemDashboardMinigame); // Make minigame functions available globally window.startNotesMinigame = startNotesMinigame; diff --git a/public/break_escape/js/minigames/siem/siem-dashboard-minigame.js b/public/break_escape/js/minigames/siem/siem-dashboard-minigame.js new file mode 100644 index 00000000..6453d0cd --- /dev/null +++ b/public/break_escape/js/minigames/siem/siem-dashboard-minigame.js @@ -0,0 +1,932 @@ +import { MinigameScene } from '../framework/base-minigame.js'; + +const STATE_KEY = 'mg01_siem_state'; + +const SEVERITY_ORDER = { + CRIT: 4, + HIGH: 3, + MED: 2, + LOW: 1 +}; + +function normalizeSeverity(severity) { + const value = String(severity || '').toUpperCase(); + if (value === 'CRITICAL' || value === 'CRIT') return 'CRIT'; + if (value === 'HIGH') return 'HIGH'; + if (value === 'MEDIUM' || value === 'MED') return 'MED'; + return 'LOW'; +} + +function formatClock(date) { + const hh = String(date.getHours()).padStart(2, '0'); + const mm = String(date.getMinutes()).padStart(2, '0'); + const ss = String(date.getSeconds()).padStart(2, '0'); + return `${hh}:${mm}:${ss}`; +} + +function formatTimer(totalSeconds) { + const safeSeconds = Math.max(0, totalSeconds); + const mm = String(Math.floor(safeSeconds / 60)).padStart(2, '0'); + const ss = String(safeSeconds % 60).padStart(2, '0'); + return `${mm}:${ss}`; +} + +function parseClockToSeconds(clockText) { + const parts = String(clockText || '').split(':').map((part) => Number(part)); + if (parts.length !== 3 || parts.some((part) => Number.isNaN(part))) return 0; + return ((parts[0] * 3600) + (parts[1] * 60) + parts[2]) % 86400; +} + +function formatSecondsToClock(totalSeconds) { + const wrapped = ((Math.floor(totalSeconds) % 86400) + 86400) % 86400; + const hh = String(Math.floor(wrapped / 3600)).padStart(2, '0'); + const mm = String(Math.floor((wrapped % 3600) / 60)).padStart(2, '0'); + const ss = String(wrapped % 60).padStart(2, '0'); + return `${hh}:${mm}:${ss}`; +} + +function getLatestAlertSecond(alerts = []) { + if (!Array.isArray(alerts) || alerts.length === 0) return 0; + return alerts.reduce((latest, alert) => { + const seconds = parseClockToSeconds(alert?.timestamp); + return Math.max(latest, seconds); + }, 0); +} + +function createSeededAlerts() { + return [ + { + id: 'ALRT-001', + severity: 'CRIT', + timestamp: '07:12:21', + source: 'FINWKS-047', + description: 'Encoded PowerShell execution chain detected', + critical: true, + status: 'pending' + }, + { + id: 'ALRT-002', + severity: 'LOW', + timestamp: '07:12:42', + source: 'NETOPS-MIG', + description: 'Expected VLAN migration route update applied', + critical: false, + status: 'pending' + }, + { + id: 'ALRT-003', + severity: 'MED', + timestamp: '07:13:15', + source: 'BKP-SCH-02', + description: 'Scheduled backup verification window started', + critical: false, + status: 'pending' + }, + { + id: 'ALRT-004', + severity: 'LOW', + timestamp: '07:13:38', + source: 'SWITCH-W7', + description: 'Legacy switch spanning-tree recalculation', + critical: false, + status: 'pending' + }, + { + id: 'ALRT-005', + severity: 'CRIT', + timestamp: '07:14:03', + source: 'DC01', + description: 'LSASS process memory access behavior flagged', + critical: true, + status: 'pending' + }, + { + id: 'ALRT-006', + severity: 'MED', + timestamp: '07:14:25', + source: 'FW-CORE', + description: 'Temporary migration allowlist entry consumed', + critical: false, + status: 'pending' + }, + { + id: 'ALRT-007', + severity: 'LOW', + timestamp: '07:14:54', + source: 'VPN-GW', + description: 'Known contractor access window opened', + critical: false, + status: 'pending' + }, + { + id: 'ALRT-008', + severity: 'HIGH', + timestamp: '07:15:16', + source: 'FILE-SRV-03', + description: 'Elevated SMB write activity during patch staging', + critical: false, + status: 'pending' + }, + { + id: 'ALRT-009', + severity: 'LOW', + timestamp: '07:15:44', + source: 'NMS-POOL', + description: 'Monitoring probe restart completed', + critical: false, + status: 'pending' + }, + { + id: 'ALRT-010', + severity: 'MED', + timestamp: '07:16:11', + source: 'DNS-INFRA', + description: 'Expected resolver policy sync from migration', + critical: false, + status: 'pending' + }, + { + id: 'ALRT-011', + severity: 'LOW', + timestamp: '07:16:39', + source: 'NETOPS-MIG', + description: 'Clinical subnet route verification succeeded', + critical: false, + status: 'pending' + }, + { + id: 'ALRT-012', + severity: 'CRIT', + timestamp: '07:17:02', + source: 'SMB-AUDIT', + description: 'Anomalous SMB write volume spike across DC shares', + critical: true, + status: 'pending' + }, + { + id: 'ALRT-013', + severity: 'LOW', + timestamp: '07:17:31', + source: 'PATCH-ORCH', + description: 'Planned update batch completed in enterprise zone', + critical: false, + status: 'pending' + }, + { + id: 'ALRT-014', + severity: 'MED', + timestamp: '07:17:57', + source: 'ROUTER-EDGE', + description: 'Perimeter route flap self-corrected', + critical: false, + status: 'pending' + }, + { + id: 'ALRT-015', + severity: 'LOW', + timestamp: '07:18:21', + source: 'BKP-SCH-02', + description: 'Backup retention policy check complete', + critical: false, + status: 'pending' + }, + { + id: 'ALRT-018', + severity: 'CRIT', + timestamp: '07:18:48', + source: 'RDP-MON', + description: 'Cross-zone RDP session from enterprise into clinical host', + critical: true, + status: 'pending' + } + ]; +} + +export class SiemDashboardMinigame extends MinigameScene { + constructor(container, params = {}) { + super(container, { + ...params, + title: 'SIEM Dashboard', + showCancel: true, + cancelText: 'Close Console' + }); + + this.timeLimitSec = Number(params.timeLimitSec) > 0 ? Number(params.timeLimitSec) : 180; + this.remainingSec = this.timeLimitSec; + this.alerts = createSeededAlerts(); + this.alertTimelineSec = getLatestAlertSecond(this.alerts); + this.finished = false; + this.isFinalized = false; + this.ransomwareFlooded = false; + this._tickerId = null; + this._eventSubs = []; + this._scheduledAlertTimeouts = []; + + this.alertsListEl = null; + this.queueListEl = null; + this.queueCountEl = null; + this.pendingCountEl = null; + this.timerEl = null; + this.systemClockEl = null; + this.resultBannerEl = null; + this.panelEl = null; + } + + init() { + super.init(); + + if (this.headerElement) { + this.headerElement.style.display = 'none'; + } + + this.container.classList.add('siem-minigame-container'); + this.gameContainer.classList.add('siem-minigame-game-container'); + + this.restoreState(); + this.renderLayout(); + this.renderAll(); + } + + start() { + super.start(); + + this.startTickers(); + this.subscribeScenarioEvents(); + } + + complete(success) { + if (!this.isFinalized) { + this.persistState(); + if (window.MinigameFramework) { + window.MinigameFramework.endMinigame(false, { + aborted: true, + minigameName: 'siem-dashboard' + }); + } + return; + } + + super.complete(success); + } + + cleanup() { + if (this._tickerId) { + clearInterval(this._tickerId); + this._tickerId = null; + } + + if (this._scheduledAlertTimeouts.length) { + this._scheduledAlertTimeouts.forEach((timeoutId) => clearTimeout(timeoutId)); + this._scheduledAlertTimeouts = []; + } + + this.unsubscribeScenarioEvents(); + super.cleanup(); + } + + nextAlertTimestamp(stepSec = 1) { + const increment = Math.max(1, Math.floor(Number(stepSec) || 1)); + this.alertTimelineSec = (this.alertTimelineSec + increment) % 86400; + return formatSecondsToClock(this.alertTimelineSec); + } + + startTickers() { + this.updateHeaderClock(); + this.updateStatusBar(); + + this._tickerId = setInterval(() => { + if (!this.finished) { + this.remainingSec = Math.max(0, this.remainingSec - 1); + this.updateStatusBar(); + + // Passive alerts every 7-10 seconds (random) + if (Math.random() < 0.15) { + this.injectPassiveAlert(); + } + + if (this.remainingSec === 0) { + this.finalizeOutcome(); + } + } + + this.updateHeaderClock(); + }, 1000); + } + + subscribeScenarioEvents() { + if (!window.eventDispatcher) return; + + const newAlertHandler = (payload) => { + this.handleInjectedAlert(payload); + }; + + const ransomwareHandler = (payload) => { + if (payload?.value === true) { + this.injectRansomwareCriticalFlood(); + } + }; + + window.eventDispatcher.on('siem_new_alert', newAlertHandler); + window.eventDispatcher.on('global_variable_changed:ransomware_deployed', ransomwareHandler); + + this._eventSubs.push({ event: 'siem_new_alert', handler: newAlertHandler }); + this._eventSubs.push({ event: 'global_variable_changed:ransomware_deployed', handler: ransomwareHandler }); + } + + unsubscribeScenarioEvents() { + if (!window.eventDispatcher || !this._eventSubs.length) return; + + this._eventSubs.forEach((sub) => { + window.eventDispatcher.off(sub.event, sub.handler); + }); + + this._eventSubs = []; + } + + renderLayout() { + this.gameContainer.innerHTML = ` +
+
+
+
NORTHGATE TRUST // SIEM CONSOLE
+
00:00:00
+
+
+
+
ALERT STREAM
+
+
+
+
ESCALATED FOR REVIEW
+
0 alerts queued
+
+
+
+
+ ALERTS PENDING: 0 + TIME REMAINING: 00:00 +
+
+ `; + + this.panelEl = this.gameContainer.querySelector('#siem-panel'); + this.alertsListEl = this.gameContainer.querySelector('#siem-alert-list'); + this.queueListEl = this.gameContainer.querySelector('#siem-queue-list'); + this.queueCountEl = this.gameContainer.querySelector('#siem-queue-count'); + this.pendingCountEl = this.gameContainer.querySelector('#siem-pending-count'); + this.timerEl = this.gameContainer.querySelector('#siem-time-remaining'); + this.systemClockEl = this.gameContainer.querySelector('#siem-system-clock'); + this.resultBannerEl = this.gameContainer.querySelector('#siem-result-banner'); + } + + renderAll() { + this.renderAlerts(); + this.renderQueue(); + this.updateStatusBar(); + } + + renderAlerts() { + if (!this.alertsListEl) return; + + const previousScrollTop = this.alertsListEl.scrollTop; + this.alertsListEl.innerHTML = ''; + + this.alerts.forEach((alert) => { + const row = document.createElement('div'); + row.className = `siem-alert-row status-${alert.status}`; + row.dataset.alertId = alert.id; + + const severity = document.createElement('span'); + severity.className = `siem-severity sev-${alert.severity}`; + severity.textContent = alert.severity; + + const time = document.createElement('span'); + time.className = 'siem-time'; + time.textContent = alert.timestamp; + + const source = document.createElement('span'); + source.className = 'siem-source'; + source.textContent = alert.source; + + const description = document.createElement('span'); + description.className = 'siem-description'; + description.textContent = alert.description; + + const actions = document.createElement('span'); + actions.className = 'siem-actions'; + + const dismissBtn = document.createElement('button'); + dismissBtn.className = 'siem-btn dismiss'; + dismissBtn.textContent = 'DISMISS'; + dismissBtn.disabled = alert.status !== 'pending' || this.finished; + dismissBtn.addEventListener('click', () => this.handleAction(alert.id, 'dismissed')); + + const escalateBtn = document.createElement('button'); + escalateBtn.className = 'siem-btn escalate'; + escalateBtn.textContent = 'ESCALATE'; + escalateBtn.disabled = alert.status !== 'pending' || this.finished; + escalateBtn.addEventListener('click', () => this.handleAction(alert.id, 'escalated')); + + const undoBtn = document.createElement('button'); + undoBtn.className = 'siem-btn dismiss'; + undoBtn.textContent = 'UNDO'; + undoBtn.disabled = alert.status !== 'dismissed' || this.finished; + undoBtn.addEventListener('click', () => this.handleAction(alert.id, 'pending')); + + // Show dismiss/escalate buttons for pending alerts, undo button for dismissed alerts + if (alert.status === 'pending') { + actions.appendChild(dismissBtn); + actions.appendChild(escalateBtn); + } else if (alert.status === 'dismissed') { + actions.appendChild(undoBtn); + } else if (alert.status === 'escalated') { + // Escalated alerts show no buttons + } + + row.appendChild(severity); + row.appendChild(time); + row.appendChild(source); + row.appendChild(description); + row.appendChild(actions); + + this.alertsListEl.appendChild(row); + }); + + this.alertsListEl.scrollTop = previousScrollTop; + } + + renderQueue() { + if (!this.queueListEl || !this.queueCountEl) return; + + const escalated = this.alerts + .filter((alert) => alert.status === 'escalated') + .sort((a, b) => { + const sevDelta = SEVERITY_ORDER[b.severity] - SEVERITY_ORDER[a.severity]; + if (sevDelta !== 0) return sevDelta; + return a.timestamp.localeCompare(b.timestamp); + }); + + this.queueListEl.innerHTML = ''; + + escalated.forEach((alert) => { + const item = document.createElement('div'); + item.className = 'siem-queue-item'; + item.innerHTML = ` + ${alert.severity} + ${alert.source} - ${alert.description} + `; + this.queueListEl.appendChild(item); + }); + + // Add severity breakdown section + const breakdownSection = document.createElement('div'); + breakdownSection.className = 'siem-queue-section-title'; + breakdownSection.textContent = '▸ SEVERITY BREAKDOWN'; + + const severityChart = this.renderSeverityChart(); + const severityLegend = this.renderSeverityLegend(); + + // Add alert score section + const scoreSection = document.createElement('div'); + scoreSection.className = 'siem-queue-section-title'; + scoreSection.textContent = '▸ ALERTS SCORE'; + + const scoreBox = document.createElement('div'); + scoreBox.className = 'siem-alert-score-box'; + scoreBox.innerHTML = ` + TRIAGE SCORE + ${this.calculateAlertScore()} + `; + + // Append all to queue list + this.queueListEl.appendChild(breakdownSection); + this.queueListEl.appendChild(severityChart); + this.queueListEl.appendChild(severityLegend); + this.queueListEl.appendChild(scoreSection); + this.queueListEl.appendChild(scoreBox); + + // Add top sources section + const sourcesSection = document.createElement('div'); + sourcesSection.className = 'siem-queue-section-title'; + sourcesSection.textContent = '▸ TOP SOURCES'; + + const sourcesBox = this.renderTopSources(); + + this.queueListEl.appendChild(sourcesSection); + this.queueListEl.appendChild(sourcesBox); + + this.queueCountEl.textContent = `${escalated.length} alerts queued`; + } + + renderSeverityChart() { + const chartBox = document.createElement('div'); + chartBox.className = 'siem-severity-chart'; + + const severities = ['LOW', 'MED', 'HIGH', 'CRIT']; + const total = Math.max(1, this.alerts.length); + + severities.forEach(sev => { + const count = this.alerts.filter(a => a.severity === sev).length; + const percentage = (count / total) * 100; + const bar = document.createElement('div'); + bar.className = `siem-severity-bar sev-${sev}`; + bar.style.flex = Math.max(percentage, 1) || 0.1; + chartBox.appendChild(bar); + }); + + return chartBox; + } + + renderSeverityLegend() { + const legend = document.createElement('div'); + legend.className = 'siem-severity-legend'; + + const severities = ['CRIT', 'HIGH', 'MED', 'LOW']; + severities.forEach(sev => { + const count = this.alerts.filter(a => a.severity === sev).length; + const item = document.createElement('div'); + item.className = 'siem-severity-item'; + item.innerHTML = ` + + ${sev} + ${count} + `; + legend.appendChild(item); + }); + + return legend; + } + + calculateAlertScore() { + // Score based on escalated critical alerts vs total critical alerts + const critical = this.alerts.filter(a => a.critical); + const criticalEscalated = critical.filter(a => a.status === 'escalated').length; + + if (critical.length === 0) return '0'; + + const percentage = Math.floor((criticalEscalated / critical.length) * 100); + return `${percentage}%`; + } + + renderTopSources() { + const box = document.createElement('div'); + box.className = 'siem-sources-box'; + + // Count alerts by source + const sourceCounts = {}; + this.alerts.forEach(alert => { + sourceCounts[alert.source] = (sourceCounts[alert.source] || 0) + 1; + }); + + // Get top 5 sources sorted by count + const topSources = Object.entries(sourceCounts) + .sort((a, b) => b[1] - a[1]) + .slice(0, 5); + + if (topSources.length === 0) { + box.innerHTML = '
No sources yet
'; + return box; + } + + const maxCount = Math.max(...topSources.map(s => s[1])); + + const list = document.createElement('div'); + list.className = 'siem-sources-list'; + + topSources.forEach(([source, count]) => { + const row = document.createElement('div'); + row.className = 'siem-source-row'; + + const name = document.createElement('span'); + name.className = 'siem-source-name'; + name.textContent = source; + + const barContainer = document.createElement('div'); + barContainer.className = 'siem-source-bar-container'; + + const bar = document.createElement('div'); + bar.className = 'siem-source-bar'; + const barWidth = (count / maxCount) * 100; + bar.style.width = `${barWidth}%`; + + barContainer.appendChild(bar); + + const countSpan = document.createElement('span'); + countSpan.className = 'siem-source-count'; + countSpan.textContent = count.toString(); + + row.appendChild(name); + row.appendChild(barContainer); + row.appendChild(countSpan); + list.appendChild(row); + }); + + box.appendChild(list); + return box; + } + + updateStatusBar() { + if (this.pendingCountEl) { + const pending = this.alerts.filter((alert) => alert.status === 'pending').length; + this.pendingCountEl.textContent = `ALERTS PENDING: ${pending}`; + } + + if (this.timerEl) { + this.timerEl.textContent = `TIME REMAINING: ${formatTimer(this.remainingSec)}`; + } + } + + updateHeaderClock() { + if (this.systemClockEl) { + this.systemClockEl.textContent = formatClock(new Date()); + } + } + + handleAction(alertId, newStatus) { + if (this.finished) return; + + const alert = this.alerts.find((entry) => entry.id === alertId); + if (!alert) return; + + // Allow status changes: + // pending → dismissed, pending → escalated + // dismissed → pending (undo dismiss), escalated stays escalated + if (alert.status === 'escalated') return; // Can't change escalated status + if (alert.status !== 'pending' && newStatus !== 'pending') return; // Can only go back to pending + + const wasCritical = alert.critical; + const wasStatusDismissed = alert.status === 'dismissed'; + alert.status = newStatus; + + // If a critical alert is dismissed, mark that alerts were missed + if (wasCritical && newStatus === 'dismissed') { + this.setScenarioGlobal('siem_missed_alerts', true); + } + + // If an alert is undone, check if we should clear the missed_alerts flag + // (only if no critical alerts remain dismissed) + if (wasCritical && wasStatusDismissed && newStatus === 'pending') { + const anyCriticalDismissed = this.alerts + .filter((entry) => entry.critical) + .some((entry) => entry.status === 'dismissed'); + if (!anyCriticalDismissed) { + this.setScenarioGlobal('siem_missed_alerts', false); + } + } + + this.renderAll(); + this.persistState(); + + // Check if all critical alerts are now handled (escalated only) + const allCriticalEscalated = this.alerts + .filter((entry) => entry.critical) + .every((entry) => entry.status === 'escalated'); + + if (allCriticalEscalated && this.alerts.filter((entry) => entry.critical).length > 0) { + this.finalizeOutcome(); + } + } + + injectPassiveAlert() { + if (this.finished) return; + + // Passive alerts: mostly LOW and MED severity, occasionally HIGH + const passiveAlertPool = [ + { severity: 'LOW', source: 'NETMON', description: 'Routine DNS query volume pattern detected' }, + { severity: 'LOW', source: 'FW-LOG', description: 'Blocked port scan from external range' }, + { severity: 'LOW', source: 'SYSLOG', description: 'User session timeout on workstation' }, + { severity: 'MED', source: 'IDS-CORE', description: 'Unusual traffic pattern on port 445' }, + { severity: 'MED', source: 'PKI', description: 'Certificate authority audit log rotation' }, + { severity: 'MED', source: 'VPN-GW', description: 'VPN session disconnected abnormally' }, + { severity: 'HIGH', source: 'AUTH-SRV', description: 'Failed authentication attempts threshold' }, + { severity: 'LOW', source: 'DHCP', description: 'DHCP lease renewal processed' }, + { severity: 'LOW', source: 'DNS-PIX', description: 'Known malware domain access blocked' }, + { severity: 'MED', source: 'PROXY', description: 'SSL/TLS certificate validation warning' } + ]; + + const randomAlert = passiveAlertPool[Math.floor(Math.random() * passiveAlertPool.length)]; + this.handleInjectedAlert(randomAlert); + } + + handleInjectedAlert(payload = {}) { + if (this.finished) return; + + const severity = normalizeSeverity(payload.severity); + const stepSec = Number(payload.stepSec) > 0 ? Number(payload.stepSec) : 1; + const alert = { + id: payload.id || `ALRT-EXT-${Date.now()}-${Math.floor(Math.random() * 9999)}`, + severity, + timestamp: this.nextAlertTimestamp(stepSec), + source: payload.source || 'SIEM-CORE', + description: payload.description || 'External alert injected into SIEM stream', + critical: severity === 'CRIT', + status: 'pending' + }; + + // Keep the player's visible alert rows stable while new alerts are prepended. + const previousScrollTop = this.alertsListEl ? this.alertsListEl.scrollTop : 0; + const previousScrollHeight = this.alertsListEl ? this.alertsListEl.scrollHeight : 0; + + this.alerts.unshift(alert); + this.renderAll(); + + if (this.alertsListEl) { + const scrollDelta = Math.max(0, this.alertsListEl.scrollHeight - previousScrollHeight); + this.alertsListEl.scrollTop = previousScrollTop + scrollDelta; + } + + this.persistState(); + } + + injectRansomwareCriticalFlood() { + if (this.ransomwareFlooded || this.finished) return; + + this.ransomwareFlooded = true; + if (this.panelEl) { + this.panelEl.classList.add('ransomware-pulse'); + } + + const ransomwareSequence = [ + { + severity: 'CRIT', + source: 'EHR-CORE', + description: 'Ransomware encryption behavior detected in clinical data plane' + }, + { + severity: 'MED', + source: 'BKP-SCH-02', + description: 'Backup verification jobs failing across multiple nodes' + }, + { + severity: 'CRIT', + source: 'AD-MONITOR', + description: 'Mass credential abuse and privilege escalation chain observed' + }, + { + severity: 'HIGH', + source: 'FILE-SRV-02', + description: 'Rapid rename operations with encrypted extension patterns' + }, + { + severity: 'CRIT', + source: 'FW-CORE', + description: 'Lateral movement burst crossing enterprise and clinical segments' + }, + { + severity: 'LOW', + source: 'NETMON', + description: 'Unusual heartbeat jitter observed on monitoring collectors' + }, + { + severity: 'HIGH', + source: 'IAM-SVC', + description: 'Service account token misuse detected in domain operations' + }, + { + severity: 'CRIT', + source: 'SMB-AUDIT', + description: 'Emergency threshold exceeded for encrypted SMB write operations' + } + ]; + + // Spread burst over 10-20 seconds with mixed severities between critical hits. + const durationSec = 10 + Math.floor(Math.random() * 11); + const slotGapSec = durationSec / Math.max(1, ransomwareSequence.length - 1); + + ransomwareSequence.forEach((entry, index) => { + const delayMs = Math.round(slotGapSec * index * 1000); + const timeoutId = setTimeout(() => { + if (this.finished) return; + this.handleInjectedAlert({ + ...entry, + stepSec: Math.max(1, Math.round(slotGapSec)) + }); + }, delayMs); + this._scheduledAlertTimeouts.push(timeoutId); + }); + } + + finalizeOutcome() { + if (this.finished) return; + + this.finished = true; + + const criticalAlerts = this.alerts.filter((entry) => entry.critical); + const criticalEscalated = criticalAlerts.filter((entry) => entry.status === 'escalated').length; + const success = criticalEscalated === criticalAlerts.length && criticalAlerts.length > 0; + + this.isFinalized = true; + + if (success) { + this.setScenarioGlobal('siem_escalated', true); + this.setScenarioGlobal('siem_missed_alerts', false); + this.showResultBanner('INCIDENT TEAM NOTIFIED', true); + } else { + this.setScenarioGlobal('siem_escalated', false); + this.setScenarioGlobal('siem_missed_alerts', true); + this.showResultBanner('CRITICAL ALERTS MISSED - INCIDENT ESCALATED', false); + } + + this.gameResult = { + success, + escalated: this.alerts.filter((entry) => entry.status === 'escalated').map((entry) => entry.id), + dismissed: this.alerts.filter((entry) => entry.status === 'dismissed').map((entry) => entry.id), + missedCritical: criticalAlerts.filter((entry) => entry.status !== 'escalated').map((entry) => entry.id) + }; + + if (window.eventDispatcher) { + window.eventDispatcher.emit('siem_triage_completed', this.gameResult); + } + + this.clearState(); + this.renderAll(); + + setTimeout(() => { + super.complete(success); + }, 1300); + } + + showResultBanner(message, success) { + if (!this.resultBannerEl) return; + + this.resultBannerEl.textContent = message; + this.resultBannerEl.classList.remove('success', 'failure', 'show'); + this.resultBannerEl.classList.add(success ? 'success' : 'failure'); + + requestAnimationFrame(() => { + this.resultBannerEl.classList.add('show'); + }); + } + + setScenarioGlobal(name, value) { + if (window.npcManager?.setGlobalVariable) { + window.npcManager.setGlobalVariable(name, value); + return; + } + + if (!window.gameState) { + window.gameState = {}; + } + if (!window.gameState.globalVariables) { + window.gameState.globalVariables = {}; + } + + const oldValue = window.gameState.globalVariables[name]; + window.gameState.globalVariables[name] = value; + + if (window.eventDispatcher) { + window.eventDispatcher.emit(`global_variable_changed:${name}`, { + name, + value, + oldValue + }); + } + } + + persistState() { + if (!window.gameState) window.gameState = {}; + if (!window.gameState.globalVariables) window.gameState.globalVariables = {}; + + window.gameState.globalVariables[STATE_KEY] = { + remainingSec: this.remainingSec, + alerts: this.alerts.map((entry) => ({ + id: entry.id, + severity: entry.severity, + timestamp: entry.timestamp, + source: entry.source, + description: entry.description, + critical: entry.critical, + status: entry.status + })), + ransomwareFlooded: this.ransomwareFlooded + }; + } + + restoreState() { + const persisted = window.gameState?.globalVariables?.[STATE_KEY]; + if (!persisted) return; + + if (Array.isArray(persisted.alerts) && persisted.alerts.length > 0) { + this.alerts = persisted.alerts.map((entry) => ({ + ...entry, + severity: normalizeSeverity(entry.severity), + status: entry.status || 'pending', + critical: entry.critical === true + })); + } + + this.alertTimelineSec = getLatestAlertSecond(this.alerts); + + if (typeof persisted.remainingSec === 'number') { + this.remainingSec = Math.max(0, Math.floor(persisted.remainingSec)); + } + + this.ransomwareFlooded = persisted.ransomwareFlooded === true; + } + + clearState() { + if (window.gameState?.globalVariables) { + delete window.gameState.globalVariables[STATE_KEY]; + } + } +} diff --git a/public/break_escape/js/systems/minigame-starters.js b/public/break_escape/js/systems/minigame-starters.js index 15b2f885..8e51edc1 100644 --- a/public/break_escape/js/systems/minigame-starters.js +++ b/public/break_escape/js/systems/minigame-starters.js @@ -581,9 +581,43 @@ export function startPasswordMinigame(lockable, type, correctPassword, callback, }); } +export function startSiemMinigame(lockable, callback, options = {}) { + console.log('Starting SIEM minigame', { lockable, options }); + + if (!window.MinigameFramework) { + console.error('MinigameFramework not available'); + window.gameAlert('SIEM minigame unavailable.', 'error', 'Error', 3000); + if (callback) callback(false, { reason: 'framework_unavailable' }); + return; + } + + if (!window.MinigameFramework.mainGameScene) { + window.MinigameFramework.init(window.game); + } + + const scenarioData = lockable?.scenarioData || {}; + const params = { + title: 'SIEM Dashboard', + lockable, + showCancel: true, + cancelText: 'Close Console', + timeLimitSec: options.timeLimitSec || scenarioData.timeLimitSec, + onComplete: (success, result) => { + if (result?.aborted) { + callback?.(false, result); + return; + } + callback?.(success, result); + } + }; + + window.MinigameFramework.startMinigame('siem-dashboard', null, params); +} + // Export for global access window.startLockpickingMinigame = startLockpickingMinigame; window.startKeySelectionMinigame = startKeySelectionMinigame; window.startPinMinigame = startPinMinigame; window.startPasswordMinigame = startPasswordMinigame; +window.startSiemMinigame = startSiemMinigame; diff --git a/public/break_escape/js/systems/unlock-system.js b/public/break_escape/js/systems/unlock-system.js index 9b7a0631..5c0b62cd 100644 --- a/public/break_escape/js/systems/unlock-system.js +++ b/public/break_escape/js/systems/unlock-system.js @@ -12,7 +12,7 @@ import { DOOR_ALIGN_OVERLAP } from '../utils/constants.js'; // create separate module instances with separate rooms objects, causing state to diverge. import { rooms } from '../core/rooms.js?v=25'; import { unlockDoor } from './doors.js?v=6'; -import { startLockpickingMinigame, startKeySelectionMinigame, startPinMinigame, startPasswordMinigame } from './minigame-starters.js'; +import { startLockpickingMinigame, startKeySelectionMinigame, startPinMinigame, startPasswordMinigame, startSiemMinigame } from './minigame-starters.js'; import { playUISound } from './ui-sounds.js?v=1'; // Helper function to notify server of unlock and get room/container data @@ -487,6 +487,15 @@ export function handleUnlock(lockable, type) { } break; + case 'siem_dashboard': + console.log('SIEM DASHBOARD MINIGAME REQUESTED'); + startSiemMinigame(lockable, () => { + console.log('SIEM minigame closed'); + }, { + timeLimitSec: lockable?.scenarioData?.timeLimitSec + }); + break; + default: window.gameAlert(`This ${type} requires ${lockRequirements.lockType} to unlock.`, 'info', 'Locked', 4000); break; diff --git a/scenarios/test-siem.json b/scenarios/test-siem.json new file mode 100644 index 00000000..a7d5efc4 --- /dev/null +++ b/scenarios/test-siem.json @@ -0,0 +1,38 @@ +{ + "scenario_brief": "Test SIEM Dashboard Minigame", + "endGoal": "Complete the SIEM alert triage challenge", + "startRoom": "test_room", + "rooms": { + "test_room": { + "type": "office", + "name": "Test Lab", + "connections": {}, + "objects": [ + { + "id": "siem_terminal", + "type": "pc", + "name": "SIEM Dashboard Terminal", + "texture": { + "key": "pc" + }, + "x": 400, + "y": 300, + "width": 64, + "height": 64, + "takeable": false, + "interactable": true, + "active": true, + "locked": true, + "lockType": "siem_dashboard", + "scenarioData": { + "id": "siem_terminal", + "name": "SIEM Dashboard Terminal", + "locked": true, + "lockType": "siem_dashboard", + "timeLimitSec": 180 + } + } + ] + } + } +} diff --git a/scenarios/test-siem/mission.json b/scenarios/test-siem/mission.json new file mode 100644 index 00000000..245f2025 --- /dev/null +++ b/scenarios/test-siem/mission.json @@ -0,0 +1,14 @@ +{ + "display_name": "Test SIEM Dashboard", + "description": "Quick test scenario for the SIEM Dashboard minigame. Review security alerts and identify critical indicators of compromise.", + "difficulty_level": 1, + "secgen_scenario": null, + "collection": "testing", + "cybok": [ + { + "ka": "NS", + "topic": "Network Security", + "keywords": ["SIEM", "Alert triage", "Security monitoring", "Incident response"] + } + ] +} diff --git a/scenarios/test-siem/scenario.json.erb b/scenarios/test-siem/scenario.json.erb new file mode 100644 index 00000000..26e27a90 --- /dev/null +++ b/scenarios/test-siem/scenario.json.erb @@ -0,0 +1,57 @@ +{ + "scenario_brief": "Test the SIEM Dashboard minigame. A security team has detected an ongoing attack. Review the alert stream, identify critical indicators of compromise, and escalate all critical-severity alerts to prevent a breach.", + "endGoal": "Successfully triage all critical-severity alerts and escalate them to the incident response team.", + "startRoom": "soc", + "startItemsInInventory": [], + "objectives": [ + { + "aimId": "test_siem_triage", + "title": "Test SIEM Dashboard", + "description": "Use the SIEM console to identify and escalate critical security alerts", + "status": "active", + "order": 0, + "tasks": [ + { + "taskId": "access_siem", + "title": "Access the SIEM Dashboard", + "type": "unlock_object", + "targetObject": "siem_console", + "status": "active" + }, + { + "taskId": "triage_alerts", + "title": "Complete alert triage", + "type": "manual", + "status": "active", + "description": "Escalate all critical-severity alerts to complete the exercise" + } + ] + } + ], + "rooms": { + "soc": { + "type": "room_office", + "connections": {}, + "objects": [ + { + "type": "pc", + "id": "siem_console", + "name": "SIEM Console", + "takeable": false, + "locked": true, + "lockType": "siem_dashboard", + "interactable": true, + "active": true, + "scenarioData": { + "id": "siem_console", + "name": "SIEM Dashboard", + "locked": true, + "lockType": "siem_dashboard", + "timeLimitSec": 180 + }, + "observations": "The main SIEM monitoring console — locked and waiting for authentication" + } + ] + } + } +}