diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 00000000..e1a9dbfa --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,10 @@ +{ + "permissions": { + "allow": [ + "Bash(*)", + "Write(*)", + "Edit(*)", + "Read(*)" + ] + } + } \ No newline at end of file diff --git a/app/views/break_escape/games/show.html.erb b/app/views/break_escape/games/show.html.erb index d1e4e14e..6c873f7c 100644 --- a/app/views/break_escape/games/show.html.erb +++ b/app/views/break_escape/games/show.html.erb @@ -51,6 +51,7 @@ + diff --git a/public/break_escape/css/log-filter-minigame.css b/public/break_escape/css/log-filter-minigame.css new file mode 100644 index 00000000..30f0ce6a --- /dev/null +++ b/public/break_escape/css/log-filter-minigame.css @@ -0,0 +1,892 @@ +/* ========================================================= + Log Filter Minigame — VM-02 / MG-06 + Used by: LogFilterMinigame (log-filter) + logType: "vpn" (MG-06 sis01_healthcare) + "ics_rdp" (VM-02 sis02_energy) + ========================================================= */ + +/* ── Wrapper / overlay ─────────────────────────────────── */ +.lf-wrapper { + position: absolute; + inset: 0; + background: #12121e; + display: flex; + flex-direction: column; + font-family: 'Courier New', Courier, monospace; + color: #d0d0e0; + overflow: hidden; + box-sizing: border-box; + border: 2px solid #ffffff; +} + +/* ── Header ─────────────────────────────────────────────── */ +.lf-header { + display: flex; + align-items: center; + justify-content: space-between; + background: #0d0d1a; + border-bottom: 2px solid #ffffff; + padding: 6px 10px; + flex-shrink: 0; +} + +.lf-header-title { + font-size: 13px; + font-weight: bold; + color: #ffffff; + letter-spacing: 0.05em; + text-transform: uppercase; +} + +.lf-close-btn { + background: #1e1e30; + border: 1px solid #ffffff; + color: #ffffff; + font-family: inherit; + font-size: 11px; + cursor: pointer; + padding: 2px 8px; + letter-spacing: 0.05em; +} + +.lf-close-btn:hover { + background: #3a3a50; +} + +/* ── Tab bar ─────────────────────────────────────────────── */ +.lf-tab-bar { + display: flex; + background: #0d0d1a; + border-bottom: 2px solid #444466; + flex-shrink: 0; + padding: 0 6px; + gap: 4px; +} + +.lf-tab-btn { + background: #1a1a2e; + border: 1px solid #444466; + border-bottom: none; + color: #8888aa; + font-family: inherit; + font-size: 11px; + cursor: pointer; + padding: 4px 10px; + letter-spacing: 0.03em; + position: relative; + margin-bottom: -2px; +} + +.lf-tab-btn:hover { + background: #22223a; + color: #ccccee; +} + +.lf-tab-active { + background: #12121e !important; + border-color: #8888ff !important; + color: #ffffff !important; +} + +/* Pulsing amber dot for unvisited additional tabs */ +.lf-tab-unread::after { + content: ' ●'; + color: #ffaa00; + animation: lf-pulse 1.2s ease-in-out infinite; +} + +@keyframes lf-pulse { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.3; } +} + +/* ── Body — two-pane layout ──────────────────────────────── */ +.lf-body { + display: flex; + flex: 1; + overflow: hidden; + position: relative; +} + +/* ── Left pane: filter builder ───────────────────────────── */ +.lf-filter-pane { + width: 220px; + min-width: 220px; + display: flex; + flex-direction: column; + border-right: 1px solid #333355; + background: #0e0e1c; + padding: 8px; + gap: 6px; + overflow-y: auto; +} + +.lf-filter-pane-label { + font-size: 10px; + color: #7777aa; + text-transform: uppercase; + letter-spacing: 0.08em; + border-bottom: 1px solid #333355; + padding-bottom: 4px; + margin-bottom: 2px; +} + +.lf-add-filter-btn { + background: #1a2a1a; + border: 1px solid #447744; + color: #66cc66; + font-family: inherit; + font-size: 11px; + cursor: pointer; + padding: 4px 8px; + text-align: left; + letter-spacing: 0.03em; +} + +.lf-add-filter-btn:hover { + background: #223322; + border-color: #66cc66; +} + +/* Filter picker dropdown */ +.lf-filter-picker { + background: #1a1a2e; + border: 1px solid #555577; + padding: 4px; + display: flex; + flex-direction: column; + gap: 2px; +} + +.lf-filter-category { + font-size: 10px; + color: #9999bb; + padding: 2px 4px; + cursor: pointer; + text-transform: uppercase; + letter-spacing: 0.05em; +} + +.lf-filter-category:hover { + background: #2a2a44; + color: #ffffff; +} + +.lf-filter-category-open { + color: #aaaaff; + background: #1e1e38; +} + +.lf-filter-values { + display: flex; + flex-direction: column; + gap: 1px; + padding-left: 10px; +} + +.lf-filter-value-item { + font-size: 10px; + color: #8888aa; + padding: 2px 4px; + cursor: pointer; +} + +.lf-filter-value-item:hover { + background: #2a2a44; + color: #ccccff; +} + +.lf-filter-text-input { + font-family: inherit; + font-size: 10px; + background: #111122; + border: 1px solid #444466; + color: #ccccee; + padding: 2px 4px; + width: calc(100% - 10px); + margin-left: 10px; + box-sizing: border-box; +} + +/* Active filter tokens */ +.lf-active-filters-label { + font-size: 10px; + color: #666688; + text-transform: uppercase; + letter-spacing: 0.06em; +} + +.lf-filter-tokens { + display: flex; + flex-direction: column; + gap: 3px; +} + +.lf-filter-token { + display: flex; + align-items: center; + justify-content: space-between; + background: #1a2a3a; + border: 1px solid #2255aa; + color: #88aaff; + font-size: 10px; + padding: 2px 5px; + letter-spacing: 0.03em; + gap: 4px; +} + +.lf-filter-token-remove { + background: none; + border: none; + color: #8899cc; + cursor: pointer; + font-size: 12px; + line-height: 1; + padding: 0; + flex-shrink: 0; +} + +.lf-filter-token-remove:hover { + color: #ff6666; +} + +/* Token colour variants by category */ +.lf-token-status { border-color: #aa6600; color: #ffaa44; background: #201400; } +.lf-token-access_level{ border-color: #226688; color: #66bbdd; background: #001820; } +.lf-token-account { border-color: #556655; color: #99cc99; background: #101a10; } +.lf-token-source_ip { border-color: #226688; color: #88bbdd; background: #001820; } +.lf-token-time { border-color: #553388; color: #bb88ff; background: #180028; } +.lf-token-country { border-color: #226688; color: #66bbdd; background: #001820; } +.lf-token-mfa { border-color: #553388; color: #bb88ff; background: #180028; } +.lf-token-result { border-color: #447744; color: #88cc88; background: #081408; } +.lf-token-user { border-color: #556655; color: #99cc99; background: #101a10; } + +/* Command preview */ +.lf-command-preview-label { + font-size: 10px; + color: #666688; + text-transform: uppercase; + letter-spacing: 0.06em; + margin-top: 4px; +} + +.lf-command-preview { + background: #070710; + border: 1px solid #225522; + color: #44ee44; + font-size: 10px; + padding: 5px 6px; + white-space: pre-wrap; + word-break: break-all; + line-height: 1.5; + min-height: 50px; +} + +.lf-clear-filters-btn { + background: #1a0a0a; + border: 1px solid #773333; + color: #cc6666; + font-family: inherit; + font-size: 10px; + cursor: pointer; + padding: 3px 6px; + text-align: left; + margin-top: auto; +} + +.lf-clear-filters-btn:hover { + background: #2a1010; + border-color: #cc6666; +} + +/* ── Right pane: log table ────────────────────────────────── */ +.lf-log-pane { + flex: 1; + display: flex; + flex-direction: column; + overflow: hidden; +} + +.lf-log-table-wrap { + flex: 1; + overflow-y: auto; + overflow-x: auto; + background: #0e0e1c; +} + +.lf-log-table { + width: 100%; + border-collapse: collapse; + font-size: 10px; + min-width: 600px; +} + +.lf-log-table th { + background: #111128; + color: #9999cc; + text-align: left; + padding: 4px 6px; + border-bottom: 1px solid #333355; + font-weight: bold; + position: sticky; + top: 0; + z-index: 1; + white-space: nowrap; + letter-spacing: 0.04em; +} + +.lf-log-table td { + padding: 3px 6px; + border-bottom: 1px solid #1a1a2e; + color: #b0b0cc; + white-space: nowrap; + cursor: pointer; +} + +.lf-log-row:hover td { + background: #1a1a32; +} + +.lf-row-dim td { + opacity: 0.28; +} + +.lf-row-dim:hover td { + opacity: 0.5; +} + +.lf-row-selected td { + background: #1e2244 !important; +} + +/* Anomaly row highlight */ +.lf-anomaly-row td { + background: #1e1200 !important; +} + +.lf-anomaly-row:hover td { + background: #2a1a00 !important; +} + +.lf-anomaly-row.lf-row-selected td { + background: #2e2200 !important; +} + +/* Status badges */ +.lf-badge { + display: inline-block; + padding: 1px 5px; + font-size: 9px; + font-weight: bold; + letter-spacing: 0.05em; +} + +.lf-status-active { + background: #cc7700; + color: #000000; +} + +.lf-status-closed { + color: #559944; +} + +.lf-status-failed { + color: #cc4444; +} + +.lf-status-accept { + color: #559944; +} + +.lf-status-reject { + color: #cc4444; +} + +/* Access level / type badges */ +.lf-level-engineer { color: #5599cc; } +.lf-level-contractor { color: #cc9944; } +.lf-level-admin { color: #cc5555; } + +/* Anomaly field highlights (within anomaly row) */ +.lf-anomaly-row .lf-field-account { color: #ffbb44; font-weight: bold; } +.lf-anomaly-row .lf-field-ip { color: #ffaa33; } +.lf-anomaly-row .lf-field-duration { color: #ffaa33; } + +/* Duration growing indicator */ +.lf-duration-growing { + color: #ffaa33; +} + +/* Session ID column */ +.lf-field-session-id { color: #5599bb; } +.lf-field-timestamp { color: #8888aa; } + +/* ── Session detail panel ────────────────────────────────── */ +.lf-session-detail { + background: #111122; + border-top: 1px solid #444466; + padding: 8px 10px; + flex-shrink: 0; + max-height: 160px; + overflow-y: auto; +} + +.lf-detail-header { + font-size: 11px; + color: #ffffff; + font-weight: bold; + text-transform: uppercase; + letter-spacing: 0.05em; + border-bottom: 1px solid #333355; + padding-bottom: 4px; + margin-bottom: 6px; +} + +.lf-detail-grid { + display: grid; + grid-template-columns: 130px 1fr; + gap: 2px 8px; + font-size: 10px; + margin-bottom: 8px; +} + +.lf-detail-label { + color: #7777aa; + white-space: nowrap; +} + +.lf-detail-value { + color: #ccccee; +} + +.lf-detail-actions { + display: flex; + gap: 6px; + flex-wrap: wrap; + margin-top: 4px; +} + +.lf-detail-btn { + background: #151530; + border: 1px solid #445566; + color: #88aacc; + font-family: inherit; + font-size: 10px; + cursor: pointer; + padding: 3px 8px; + letter-spacing: 0.03em; +} + +.lf-detail-btn:hover { + background: #1e2240; + border-color: #6688aa; + color: #ccddee; +} + +.lf-detail-btn-flag { + border-color: #885500; + color: #ffaa33; + background: #1e1200; +} + +.lf-detail-btn-flag:hover { + background: #2a1a00; + border-color: #cc8800; + color: #ffcc55; +} + +.lf-detail-btn-flag:disabled, +.lf-detail-btn-flag[disabled] { + opacity: 0.4; + cursor: default; +} + +.lf-session-flagged-banner { + color: #44cc44; + font-size: 10px; + padding: 3px 0; + display: flex; + align-items: center; + gap: 6px; +} + +.lf-tab2-prompt { + margin-top: 6px; + background: #1a1200; + border: 1px solid #aa7700; + color: #ffcc66; + font-size: 10px; + padding: 5px 8px; + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; +} + +.lf-tab2-prompt-btn { + background: #2a1e00; + border: 1px solid #aa7700; + color: #ffcc44; + font-family: inherit; + font-size: 10px; + cursor: pointer; + padding: 2px 7px; + white-space: nowrap; +} + +.lf-tab2-prompt-btn:hover { + background: #3a2e00; +} + +/* ── Overlay (threat intel, account history, flag confirm) ── */ +.lf-overlay { + position: absolute; + inset: 0; + background: rgba(0, 0, 0, 0.72); + display: flex; + align-items: center; + justify-content: center; + z-index: 10; +} + +.lf-overlay-panel { + background: #111122; + border: 2px solid #ffffff; + width: min(480px, 92%); + max-height: 85%; + overflow-y: auto; + display: flex; + flex-direction: column; +} + +.lf-overlay-header { + background: #0d0d1a; + border-bottom: 1px solid #444466; + padding: 6px 10px; + font-size: 11px; + font-weight: bold; + color: #ffffff; + text-transform: uppercase; + letter-spacing: 0.05em; + display: flex; + justify-content: space-between; + align-items: center; +} + +.lf-overlay-close { + background: none; + border: 1px solid #666688; + color: #9999bb; + font-family: inherit; + font-size: 10px; + cursor: pointer; + padding: 1px 6px; +} + +.lf-overlay-close:hover { + border-color: #ffffff; + color: #ffffff; +} + +.lf-overlay-body { + padding: 10px; + display: flex; + flex-direction: column; + gap: 6px; +} + +.lf-overlay-divider { + border: none; + border-top: 1px solid #333355; + margin: 4px 0; +} + +/* Threat intel */ +.lf-threat-grid { + display: grid; + grid-template-columns: 140px 1fr; + gap: 3px 8px; + font-size: 11px; +} + +.lf-threat-label { + color: #7777aa; +} + +.lf-threat-value { + color: #ccccee; +} + +.lf-threat-known-bad { + color: #ff4444; + font-weight: bold; + font-size: 12px; + letter-spacing: 0.04em; + margin-top: 4px; + border: 1px solid #cc2222; + padding: 3px 6px; + background: #200000; + align-self: flex-start; +} + +/* Account history */ +.lf-account-grid { + display: grid; + grid-template-columns: 160px 1fr; + gap: 3px 8px; + font-size: 11px; +} + +.lf-account-label { + color: #7777aa; +} + +.lf-account-value { + color: #ccccee; +} + +.lf-account-deprovisioned { + color: #ff4444; + font-weight: bold; +} + +.lf-account-gap-note { + color: #ffaa33; + font-weight: bold; +} + +.lf-account-anomaly-badge { + margin-top: 6px; + background: #200000; + border: 1px solid #cc2222; + color: #ff6666; + font-size: 11px; + font-weight: bold; + padding: 4px 8px; + text-align: center; + letter-spacing: 0.04em; +} + +/* Flag confirm modal */ +.lf-flag-confirm-body { + font-size: 11px; + line-height: 1.5; + color: #ccccee; +} + +.lf-flag-confirm-body strong { + color: #ffffff; +} + +.lf-flag-confirm-actions { + display: flex; + gap: 8px; + margin-top: 8px; + justify-content: flex-end; +} + +.lf-flag-confirm-btn { + background: #1a2a1a; + border: 1px solid #447744; + color: #66cc66; + font-family: inherit; + font-size: 11px; + cursor: pointer; + padding: 4px 12px; + letter-spacing: 0.03em; +} + +.lf-flag-confirm-btn:hover { + background: #223322; + border-color: #66cc66; +} + +.lf-flag-cancel-btn { + background: #1a1a28; + border: 1px solid #555577; + color: #8888aa; + font-family: inherit; + font-size: 11px; + cursor: pointer; + padding: 4px 12px; +} + +.lf-flag-cancel-btn:hover { + background: #22223a; + color: #aaaacc; +} + +/* ── Tab 2: Audit log ─────────────────────────────────────── */ +.lf-audit-pane { + flex: 1; + display: flex; + flex-direction: column; + overflow: hidden; +} + +.lf-audit-header-bar { + background: #0e0e1c; + border-bottom: 1px solid #333355; + padding: 6px 10px; + flex-shrink: 0; +} + +.lf-audit-title { + font-size: 11px; + color: #ffffff; + font-weight: bold; +} + +.lf-audit-subtitle { + font-size: 10px; + color: #7777aa; + margin-top: 2px; +} + +.lf-audit-table-wrap { + flex: 1; + overflow-y: auto; + overflow-x: auto; + background: #0e0e1c; +} + +.lf-audit-table { + width: 100%; + border-collapse: collapse; + font-size: 10px; + min-width: 560px; +} + +.lf-audit-table th { + background: #111128; + color: #9999cc; + text-align: left; + padding: 4px 6px; + border-bottom: 1px solid #333355; + font-weight: bold; + position: sticky; + top: 0; + z-index: 1; + letter-spacing: 0.04em; + white-space: nowrap; +} + +.lf-audit-table td { + padding: 3px 6px; + border-bottom: 1px solid #1a1a2e; + color: #b0b0cc; + white-space: nowrap; +} + +.lf-audit-row:hover td { + background: #1a1a32; +} + +.lf-audit-anomaly-row td { + background: #1e1200 !important; + cursor: pointer; +} + +.lf-audit-anomaly-row:hover td { + background: #2a1a00 !important; +} + +.lf-audit-anomaly-row .lf-audit-operator { + color: #ffbb44; + font-weight: bold; +} + +.lf-audit-chevron { + color: #ffaa33; + font-weight: bold; +} + +.lf-audit-error-row td { + background: #1e0000 !important; + color: #cc6666 !important; +} + +/* Audit detail overlay */ +.lf-audit-detail-grid { + display: grid; + grid-template-columns: 120px 1fr; + gap: 3px 8px; + font-size: 11px; +} + +.lf-audit-detail-label { + color: #7777aa; +} + +.lf-audit-detail-value { + color: #ccccee; +} + +.lf-audit-critical { + margin-top: 8px; + background: #1e0000; + border: 1px solid #cc2222; + padding: 6px 8px; + font-size: 11px; + line-height: 1.5; + color: #ff8888; + animation: lf-critical-pulse 2s ease-in-out infinite; +} + +@keyframes lf-critical-pulse { + 0%, 100% { border-color: #cc2222; } + 50% { border-color: #ff4444; } +} + +/* ── Status bar ───────────────────────────────────────────── */ +.lf-status-bar { + background: #080810; + border-top: 1px solid #333355; + padding: 4px 10px; + font-size: 10px; + color: #666688; + display: flex; + gap: 16px; + flex-shrink: 0; +} + +.lf-status-bar-count { + color: #9999bb; +} + +/* Completion banner */ +.lf-complete-banner { + background: #001a00; + border: 1px solid #44aa44; + color: #66cc66; + font-size: 11px; + font-weight: bold; + padding: 5px 10px; + text-align: center; + letter-spacing: 0.04em; + flex-shrink: 0; +} + +/* ── Scrollbar styling ────────────────────────────────────── */ +.lf-log-table-wrap::-webkit-scrollbar, +.lf-audit-table-wrap::-webkit-scrollbar, +.lf-filter-pane::-webkit-scrollbar, +.lf-session-detail::-webkit-scrollbar { + width: 6px; + height: 6px; +} + +.lf-log-table-wrap::-webkit-scrollbar-track, +.lf-audit-table-wrap::-webkit-scrollbar-track, +.lf-filter-pane::-webkit-scrollbar-track, +.lf-session-detail::-webkit-scrollbar-track { + background: #0a0a18; +} + +.lf-log-table-wrap::-webkit-scrollbar-thumb, +.lf-audit-table-wrap::-webkit-scrollbar-thumb, +.lf-filter-pane::-webkit-scrollbar-thumb, +.lf-session-detail::-webkit-scrollbar-thumb { + background: #333355; + border-radius: 3px; +} diff --git a/public/break_escape/js/minigames/index.js b/public/break_escape/js/minigames/index.js index 14c3e4f7..024d4c57 100644 --- a/public/break_escape/js/minigames/index.js +++ b/public/break_escape/js/minigames/index.js @@ -32,6 +32,7 @@ export { AlarmPanelMinigame } from './alarm-panel/alarm-panel-minigame.js'; export { ClaimsManagementSystemMinigame } from './claims-management-system/claims-management-system-minigame.js'; export { ForensicDataPlatformMinigame } from './forensic-data-platform/forensic-data-platform-minigame.js'; export { NcscBriefMinigame } from './ncsc-brief/ncsc-brief-minigame.js'; +export { LogFilterMinigame } from './log-filter/log-filter-minigame.js'; export { VpnLogViewerMinigame } from './vpn-log-viewer/vpn-log-viewer-minigame.js'; export { DrugLibraryIntegrityMinigame } from './drug-library-integrity/drug-library-integrity-minigame.js'; @@ -110,6 +111,7 @@ import { AlarmPanelMinigame } from './alarm-panel/alarm-panel-minigame.js'; import { ClaimsManagementSystemMinigame } from './claims-management-system/claims-management-system-minigame.js'; import { ForensicDataPlatformMinigame } from './forensic-data-platform/forensic-data-platform-minigame.js'; import { NcscBriefMinigame } from './ncsc-brief/ncsc-brief-minigame.js'; +import { LogFilterMinigame } from './log-filter/log-filter-minigame.js'; import { VpnLogViewerMinigame } from './vpn-log-viewer/vpn-log-viewer-minigame.js'; import { DrugLibraryIntegrityMinigame } from './drug-library-integrity/drug-library-integrity-minigame.js'; @@ -152,6 +154,7 @@ MinigameFramework.registerScene('alarm-panel', AlarmPanelMinigame); MinigameFramework.registerScene('claims-management-system', ClaimsManagementSystemMinigame); MinigameFramework.registerScene('forensic-data-platform', ForensicDataPlatformMinigame); MinigameFramework.registerScene('ncsc-brief', NcscBriefMinigame); +MinigameFramework.registerScene('log-filter', LogFilterMinigame); MinigameFramework.registerScene('vpn-log-viewer', VpnLogViewerMinigame); MinigameFramework.registerScene('drug-library-integrity', DrugLibraryIntegrityMinigame); diff --git a/public/break_escape/js/minigames/log-filter/log-filter-minigame.js b/public/break_escape/js/minigames/log-filter/log-filter-minigame.js new file mode 100644 index 00000000..2c427ef8 --- /dev/null +++ b/public/break_escape/js/minigames/log-filter/log-filter-minigame.js @@ -0,0 +1,1132 @@ +/** + * LogFilterMinigame — VM-02 / MG-06 + * + * Generic access-log analyser minigame. + * Controlled entirely by scenarioData — no scenario-specific code here. + * + * logType: "vpn" — VPN auth log (MG-06, sis01_healthcare) + * "ics_rdp" — Jump server session log (VM-02, sis02_energy) + * + * Expected scenarioData fields: + * title, logType, logEntries[], anomaly, threatIntel, accountHistory, + * flagActionLabel, flagConfirmTitle, flagConfirmBody, + * additionalTabs[], requireAllTabs, completionActions[], progressActions[] + */ + +import { MinigameScene } from '../framework/base-minigame.js'; + +export class LogFilterMinigame extends MinigameScene { + + // ── Column schemas ───────────────────────────────────────────────────── + + static LOG_FIELDS = { + vpn: [ + { key: 'timestamp', label: 'TIMESTAMP', width: '148px' }, + { key: 'user', label: 'USER', width: '120px' }, + { key: 'ip', label: 'IP', width: '120px' }, + { key: 'country', label: 'COUNTRY', width: '70px' }, + { key: 'mfa', label: 'MFA', width: '44px' }, + { key: 'result', label: 'RESULT', width: '70px' } + ], + ics_rdp: [ + { key: 'timestamp', label: 'TIMESTAMP', width: '140px' }, + { key: 'sessionId', label: 'SESSION_ID', width: '90px' }, + { key: 'account', label: 'ACCOUNT', width: '100px' }, + { key: 'sourceIp', label: 'SOURCE_IP', width: '130px' }, + { key: 'duration', label: 'DURATION', width: '72px' }, + { key: 'status', label: 'STATUS', width: '72px' }, + { key: 'accessLevel', label: 'ACCESS_LEVEL', width: '100px' } + ] + }; + + static FILTER_CATEGORIES = { + vpn: [ + { id: 'country', label: 'COUNTRY =', type: 'enum', values: ['UK', 'RO', 'US', 'DE', 'FR'] }, + { id: 'mfa', label: 'MFA =', type: 'enum', values: ['YES', 'NO'] }, + { id: 'result', label: 'RESULT =', type: 'enum', values: ['ACCEPT', 'REJECT'] }, + { id: 'user', label: 'USER =', type: 'text', placeholder: 'username or prefix' }, + { id: 'time', label: 'TIME =', type: 'enum', values: ['00–06', '06–12', '12–18', '18–24'] } + ], + ics_rdp: [ + { id: 'status', label: 'STATUS =', type: 'enum', values: ['ACTIVE', 'CLOSED', 'FAILED'] }, + { id: 'accessLevel', label: 'ACCESS_LEVEL =', type: 'enum', values: ['ENGINEER', 'CONTRACTOR', 'ADMIN'] }, + { id: 'account', label: 'ACCOUNT =', type: 'text', placeholder: 'account name or prefix' }, + { id: 'sourceIp', label: 'SOURCE_IP =', type: 'text', placeholder: 'IP prefix (e.g. 185.)' }, + { id: 'time', label: 'TIME =', type: 'enum', values: ['00–06', '06–12', '12–18', '18–24'] } + ] + }; + + // ── Constructor ──────────────────────────────────────────────────────── + + constructor(params) { + super(params); + + const sd = params.sprite?.scenarioData || {}; + + this._logType = sd.logType || 'vpn'; + this._title = sd.title || 'ACCESS LOG ANALYSER'; + this._logEntries = sd.logEntries || []; + this._anomaly = sd.anomaly || null; + this._threatIntel = sd.threatIntel || null; + this._accountHistory = sd.accountHistory || null; + this._flagActionLabel = sd.flagActionLabel || 'FLAG SESSION'; + this._flagConfirmTitle = sd.flagConfirmTitle || 'CONFIRM SESSION FLAG'; + this._flagConfirmBody = sd.flagConfirmBody || ''; + this._additionalTabs = sd.additionalTabs || []; + this._requireAllTabs = sd.requireAllTabs || false; + this._completionActions = sd.completionActions || []; + this._progressActions = sd.progressActions || []; + + // UI state + this._activeFilters = []; + this._sessionFlagged = false; + this._tabsVisited = new Set(); + this._currentTab = 'session_log'; + this._selectedEntry = null; + this._overlayMode = null; // null | 'threat_intel' | 'account_history' | 'flag_confirm' | 'audit_detail' + this._selectedAuditEntry = null; + this._filterPickerOpen = false; + this._filterPickerCat = null; // currently expanded category id + this._closeTimer = null; + this._completionFired = false; + + // DOM node references (set during render) + this._dom = {}; + } + + // ── Lifecycle ────────────────────────────────────────────────────────── + + start() { + super.start(); + this._resumeStateFromGlobals(); + this._renderLayout(); + this._switchTab('session_log'); + } + + /** Resume partial state if player previously visited and set globals. */ + _resumeStateFromGlobals() { + const globals = window.gameState?.globalVariables || {}; + if (this._anomaly) { + const confirmKey = 'jump_server_confirmed'; + if (globals[confirmKey] === true) { + this._sessionFlagged = true; + this._completionFired = true; + } + } + // Mark SIS audit as visited if already reviewed + if (globals['sis_audit_reviewed'] === true) { + this._tabsVisited.add('sis_audit'); + } + } + + // ── Top-level layout ─────────────────────────────────────────────────── + + _renderLayout() { + const c = this.container; + c.innerHTML = ''; + + // Outer wrapper + const wrap = this._el('div', 'lf-wrapper'); + c.appendChild(wrap); + this._dom.wrap = wrap; + + // Header + const header = this._el('div', 'lf-header'); + const titleEl = this._el('div', 'lf-header-title'); + titleEl.textContent = this._title; + const closeBtn = this._el('button', 'lf-close-btn'); + closeBtn.textContent = '[CLOSE]'; + closeBtn.addEventListener('click', () => this.close()); + header.appendChild(titleEl); + header.appendChild(closeBtn); + wrap.appendChild(header); + + // Tab bar + const tabBar = this._el('div', 'lf-tab-bar'); + wrap.appendChild(tabBar); + this._dom.tabBar = tabBar; + + // Body + const body = this._el('div', 'lf-body'); + wrap.appendChild(body); + this._dom.body = body; + + // Status bar + const statusBar = this._el('div', 'lf-status-bar'); + wrap.appendChild(statusBar); + this._dom.statusBar = statusBar; + + this._rebuildTabBar(); + } + + _rebuildTabBar() { + const bar = this._dom.tabBar; + bar.innerHTML = ''; + + // Session log tab + const btn0 = this._el('button', 'lf-tab-btn'); + btn0.textContent = 'SESSION LOG'; + btn0.dataset.tabId = 'session_log'; + btn0.addEventListener('click', () => this._switchTab('session_log')); + bar.appendChild(btn0); + + // Additional tabs + for (const tab of this._additionalTabs) { + const btn = this._el('button', 'lf-tab-btn'); + btn.textContent = tab.label; + btn.dataset.tabId = tab.id; + if (!this._tabsVisited.has(tab.id)) { + btn.classList.add('lf-tab-unread'); + } + btn.addEventListener('click', () => this._switchTab(tab.id)); + bar.appendChild(btn); + } + + this._updateTabActiveClass(); + } + + _updateTabActiveClass() { + const bar = this._dom.tabBar; + bar.querySelectorAll('.lf-tab-btn').forEach(btn => { + btn.classList.toggle('lf-tab-active', btn.dataset.tabId === this._currentTab); + }); + } + + _switchTab(tabId) { + this._currentTab = tabId; + this._overlayMode = null; + this._updateTabActiveClass(); + + const body = this._dom.body; + body.innerHTML = ''; + + if (tabId === 'session_log') { + this._renderSessionLogTab(body); + } else { + const tab = this._additionalTabs.find(t => t.id === tabId); + if (tab) { + this._onAdditionalTabVisited(tab); + if (tab.type === 'audit_log') { + this._renderAuditLogTab(body, tab); + } + } + } + + this._updateStatusBar(); + } + + // ── Session Log Tab ──────────────────────────────────────────────────── + + _renderSessionLogTab(body) { + // Left pane: filter builder + const filterPane = this._el('div', 'lf-filter-pane'); + body.appendChild(filterPane); + this._dom.filterPane = filterPane; + this._renderFilterPane(filterPane); + + // Right pane: log + const logPane = this._el('div', 'lf-log-pane'); + body.appendChild(logPane); + this._dom.logPane = logPane; + this._renderLogTable(logPane); + + // Session detail (rendered below log table inside logPane) + if (this._sessionFlagged && this._completionFired) { + const banner = this._el('div', 'lf-complete-banner'); + banner.textContent = '✓ INVESTIGATION COMPLETE — Jump server session flagged and SIS audit reviewed.'; + logPane.appendChild(banner); + } else if (this._selectedEntry) { + this._renderSessionDetail(logPane); + } + } + + // ── Filter Pane ──────────────────────────────────────────────────────── + + _renderFilterPane(pane) { + pane.innerHTML = ''; + + const label = this._el('div', 'lf-filter-pane-label'); + label.textContent = 'FILTER BUILDER'; + pane.appendChild(label); + + // ADD FILTER button + const addBtn = this._el('button', 'lf-add-filter-btn'); + addBtn.textContent = '[+ ADD FILTER]'; + addBtn.addEventListener('click', () => { + this._filterPickerOpen = !this._filterPickerOpen; + if (!this._filterPickerOpen) this._filterPickerCat = null; + this._renderFilterPane(pane); + }); + pane.appendChild(addBtn); + + // Picker dropdown + if (this._filterPickerOpen) { + this._renderFilterPicker(pane); + } + + // Active filters label + const activeLabel = this._el('div', 'lf-active-filters-label'); + activeLabel.textContent = `Active filters: (${this._activeFilters.length})`; + pane.appendChild(activeLabel); + + // Token pills + if (this._activeFilters.length > 0) { + const tokensWrap = this._el('div', 'lf-filter-tokens'); + for (let i = 0; i < this._activeFilters.length; i++) { + const f = this._activeFilters[i]; + const token = this._el('div', `lf-filter-token lf-token-${f.category.toLowerCase()}`); + const label2 = document.createTextNode(`${f.category.toUpperCase()}=${f.value}`); + const rmBtn = this._el('button', 'lf-filter-token-remove'); + rmBtn.textContent = '×'; + rmBtn.addEventListener('click', () => { + this._activeFilters.splice(i, 1); + this._onFiltersChanged(); + }); + token.appendChild(label2); + token.appendChild(rmBtn); + tokensWrap.appendChild(token); + } + pane.appendChild(tokensWrap); + } + + // Command preview + const previewLabel = this._el('div', 'lf-command-preview-label'); + previewLabel.textContent = 'COMMAND PREVIEW'; + pane.appendChild(previewLabel); + + const preview = this._el('div', 'lf-command-preview'); + preview.textContent = this._buildCommandPreview(); + pane.appendChild(preview); + this._dom.commandPreview = preview; + + // Clear all + const clearBtn = this._el('button', 'lf-clear-filters-btn'); + clearBtn.textContent = '[CLEAR ALL FILTERS]'; + clearBtn.addEventListener('click', () => { + this._activeFilters = []; + this._filterPickerOpen = false; + this._filterPickerCat = null; + this._onFiltersChanged(); + }); + pane.appendChild(clearBtn); + } + + _renderFilterPicker(pane) { + const picker = this._el('div', 'lf-filter-picker'); + const categories = LogFilterMinigame.FILTER_CATEGORIES[this._logType] || []; + + for (const cat of categories) { + const catEl = this._el('div', 'lf-filter-category'); + catEl.textContent = cat.label; + if (this._filterPickerCat === cat.id) { + catEl.classList.add('lf-filter-category-open'); + } + catEl.addEventListener('click', (e) => { + e.stopPropagation(); + this._filterPickerCat = (this._filterPickerCat === cat.id) ? null : cat.id; + this._renderFilterPane(pane); + }); + picker.appendChild(catEl); + + // Values panel for this category + if (this._filterPickerCat === cat.id) { + const valuesDiv = this._el('div', 'lf-filter-values'); + + if (cat.type === 'enum') { + for (const val of cat.values) { + const item = this._el('div', 'lf-filter-value-item'); + item.textContent = val; + item.addEventListener('click', (e) => { + e.stopPropagation(); + this._addFilter(cat.id, val); + }); + valuesDiv.appendChild(item); + } + } else if (cat.type === 'text') { + const input = this._el('input', 'lf-filter-text-input'); + input.type = 'text'; + input.placeholder = cat.placeholder || ''; + input.addEventListener('keydown', (e) => { + if (e.key === 'Enter' && input.value.trim()) { + this._addFilter(cat.id, input.value.trim()); + } + }); + input.addEventListener('click', e => e.stopPropagation()); + valuesDiv.appendChild(input); + } + + picker.appendChild(valuesDiv); + } + } + + pane.appendChild(picker); + } + + _addFilter(category, value) { + // Replace existing filter for same category (single-select per category) + this._activeFilters = this._activeFilters.filter(f => f.category !== category); + this._activeFilters.push({ category, value }); + this._filterPickerOpen = false; + this._filterPickerCat = null; + this._onFiltersChanged(); + } + + _onFiltersChanged() { + if (this._dom.filterPane) { + this._renderFilterPane(this._dom.filterPane); + } + if (this._dom.logPane) { + this._renderLogTable(this._dom.logPane); + if (this._selectedEntry) { + this._renderSessionDetail(this._dom.logPane); + } + } + this._updateStatusBar(); + } + + // ── Command Preview ──────────────────────────────────────────────────── + + _buildCommandPreview() { + if (this._activeFilters.length === 0) { + if (this._logType === 'ics_rdp') { + return '$ cat /var/log/js-albion-01/access.log'; + } + return '$ cat /var/log/vpn/auth.log'; + } + + const filters = this._activeFilters; + + if (this._logType === 'ics_rdp') { + // Column index map for awk (1-based) + const COL = { timestamp: 1, sessionId: 2, account: 3, sourceIp: 4, duration: 5, status: 6, accessLevel: 7 }; + const lines = []; + for (let i = 0; i < filters.length; i++) { + const f = filters[i]; + const isFirst = i === 0; + const prefix = isFirst ? '$ ' : ' | '; + + if (f.category === 'account') { + // Use grep for account text match + lines.push(`${prefix}grep "${f.value}" /var/log/js-albion-01/access.log`); + } else if (f.category === 'sourceIp') { + const escaped = f.value.replace('.', '\\.'); + lines.push(`${prefix}awk -F'|' '$${COL.sourceIp} ~ /^${escaped}/' /var/log/js-albion-01/access.log`); + } else if (f.category === 'time') { + const hourRange = this._timeRangeToHour(f.value); + lines.push(`${prefix}awk -F'|' '${hourRange}' /var/log/js-albion-01/access.log`); + } else { + const colIdx = COL[f.category]; + if (colIdx) { + lines.push(`${prefix}awk -F'|' '$${colIdx} == "${f.value}"' /var/log/js-albion-01/access.log`); + } + } + // Continuation lines use pipe + if (i === 0 && filters.length > 1) { + lines[lines.length - 1] += ' \\'; + } else if (i > 0 && i < filters.length - 1) { + lines[lines.length - 1] += ' \\'; + } + } + return lines.join('\n'); + } + + // VPN: grep-based + const lines = []; + for (let i = 0; i < filters.length; i++) { + const f = filters[i]; + const isFirst = i === 0; + const prefix = isFirst ? '$ grep ' : ' | grep '; + let term = ''; + if (f.category === 'country') term = `"COUNTRY=${f.value}"`; + else if (f.category === 'mfa') term = `"MFA=${f.value}"`; + else if (f.category === 'result') term = `"RESULT=${f.value}"`; + else if (f.category === 'user') term = `"${f.value}"`; + else if (f.category === 'time') { + const hourRange = this._timeRangeToHour(f.value); + term = `"${hourRange}"`; + } + const suffix = isFirst ? ` /var/log/vpn/auth.log` : ''; + const cont = (i < filters.length - 1) ? ' \\' : ''; + lines.push(`${prefix}${term}${suffix}${cont}`); + } + return lines.join('\n'); + } + + _timeRangeToHour(rangeStr) { + // "00–06" → awk condition or grep pattern + const map = { '00–06': '00|01|02|03|04|05', '06–12': '06|07|08|09|10|11', '12–18': '12|13|14|15|16|17', '18–24': '18|19|20|21|22|23' }; + if (this._logType === 'ics_rdp') { + // Return awk hour condition based on col 1 (timestamp) + const hours = (map[rangeStr] || '').split('|'); + return hours.map(h => `substr($1,12,2)=="${h}"`).join(' || '); + } + return map[rangeStr] || rangeStr; + } + + // ── Log Table ────────────────────────────────────────────────────────── + + _renderLogTable(pane) { + // Remove old table if present + const oldTable = pane.querySelector('.lf-log-table-wrap'); + if (oldTable) oldTable.remove(); + + const wrap = this._el('div', 'lf-log-table-wrap'); + // Insert before session-detail if it exists + const detail = pane.querySelector('.lf-session-detail'); + if (detail) { + pane.insertBefore(wrap, detail); + } else { + pane.appendChild(wrap); + } + this._dom.logTableWrap = wrap; + + const fields = LogFilterMinigame.LOG_FIELDS[this._logType] || []; + const filtered = this._applyFilters(this._logEntries); + + const table = this._el('table', 'lf-log-table'); + // Header + const thead = document.createElement('thead'); + const headerRow = document.createElement('tr'); + for (const f of fields) { + const th = document.createElement('th'); + th.textContent = f.label; + th.style.width = f.width; + headerRow.appendChild(th); + } + thead.appendChild(headerRow); + table.appendChild(thead); + + // Body + const tbody = document.createElement('tbody'); + const isAnomaly = (entry) => { + if (!this._anomaly) return false; + return entry.account === this._anomaly.account || + entry.user === this._anomaly.account || + (entry.status === this._anomaly.status && entry.sourceIp === this._anomaly.sourceIp); + }; + + for (const entry of this._logEntries) { + const tr = document.createElement('tr'); + tr.classList.add('lf-log-row'); + const visible = filtered.includes(entry); + if (!visible) tr.classList.add('lf-row-dim'); + if (isAnomaly(entry)) tr.classList.add('lf-anomaly-row'); + if (this._selectedEntry === entry) tr.classList.add('lf-row-selected'); + + for (const f of fields) { + const td = document.createElement('td'); + td.classList.add(`lf-field-${f.key.toLowerCase().replace('_', '-')}`); + this._renderCellValue(td, f.key, entry[f.key] || '', entry, isAnomaly(entry)); + tr.appendChild(td); + } + + tr.addEventListener('click', () => this._selectEntry(entry)); + tbody.appendChild(tr); + } + table.appendChild(tbody); + wrap.appendChild(table); + } + + _renderCellValue(td, key, val, entry, anomalyRow) { + if (key === 'status') { + if (val === 'ACTIVE') { + const span = this._el('span', 'lf-badge lf-status-active'); + span.textContent = val; + td.appendChild(span); + } else if (val === 'CLOSED') { + const span = this._el('span', 'lf-status-closed'); + span.textContent = val; + td.appendChild(span); + } else if (val === 'FAILED') { + const span = this._el('span', 'lf-status-failed'); + span.textContent = val; + td.appendChild(span); + } else if (val === 'ACCEPT') { + const span = this._el('span', 'lf-status-accept'); + span.textContent = val; + td.appendChild(span); + } else if (val === 'REJECT') { + const span = this._el('span', 'lf-status-reject'); + span.textContent = val; + td.appendChild(span); + } else { + td.textContent = val; + } + } else if (key === 'accessLevel') { + const cls = { ENGINEER: 'lf-level-engineer', CONTRACTOR: 'lf-level-contractor', ADMIN: 'lf-level-admin' }[val]; + if (cls) { + const span = this._el('span', cls); + span.textContent = val; + td.appendChild(span); + } else { + td.textContent = val; + } + } else if (key === 'duration' && anomalyRow && val.includes('+')) { + const span = this._el('span', 'lf-duration-growing'); + span.textContent = val; + td.appendChild(span); + } else if (key === 'sessionId') { + td.classList.add('lf-field-session-id'); + td.textContent = val; + } else if (key === 'timestamp') { + td.classList.add('lf-field-timestamp'); + td.textContent = val; + } else { + td.textContent = val; + } + } + + _applyFilters(entries) { + if (this._activeFilters.length === 0) return entries; + return entries.filter(entry => { + return this._activeFilters.every(f => { + const cat = f.category; + const val = f.value.toLowerCase(); + if (cat === 'status') return (entry.status || '').toLowerCase() === val; + if (cat === 'accessLevel') return (entry.accessLevel || '').toLowerCase() === val; + if (cat === 'account') return (entry.account || entry.user || '').toLowerCase().includes(val); + if (cat === 'user') return (entry.user || '').toLowerCase().includes(val); + if (cat === 'sourceIp') return (entry.sourceIp || entry.ip || '').toLowerCase().startsWith(val); + if (cat === 'country') return (entry.country || '').toLowerCase() === val; + if (cat === 'mfa') return (entry.mfa || '').toLowerCase() === val; + if (cat === 'result') return (entry.result || '').toLowerCase() === val; + if (cat === 'time') return this._matchesTimeRange(entry.timestamp || '', f.value); + return true; + }); + }); + } + + _matchesTimeRange(timestamp, range) { + const match = timestamp.match(/\d{2}:(\d{2})|\s(\d{2}):\d{2}/); + const hourStr = timestamp.slice(11, 13); + const hour = parseInt(hourStr, 10); + if (isNaN(hour)) return true; + const ranges = { '00–06': [0, 5], '06–12': [6, 11], '12–18': [12, 17], '18–24': [18, 23] }; + const [lo, hi] = ranges[range] || [0, 23]; + return hour >= lo && hour <= hi; + } + + _selectEntry(entry) { + this._selectedEntry = entry; + this._overlayMode = null; + + // Re-render log table selection highlights + if (this._dom.logTableWrap) { + this._dom.logTableWrap.querySelectorAll('.lf-log-row').forEach((tr, i) => { + tr.classList.toggle('lf-row-selected', this._logEntries[i] === entry); + }); + } + + // Remove existing session detail / banners + if (this._dom.logPane) { + const existing = this._dom.logPane.querySelectorAll('.lf-session-detail, .lf-complete-banner'); + existing.forEach(el => el.remove()); + this._renderSessionDetail(this._dom.logPane); + } + } + + // ── Session Detail Panel ─────────────────────────────────────────────── + + _renderSessionDetail(pane) { + const entry = this._selectedEntry; + if (!entry) return; + + const panel = this._el('div', 'lf-session-detail'); + this._dom.sessionDetail = panel; + + const header = this._el('div', 'lf-detail-header'); + header.textContent = 'SESSION DETAIL'; + panel.appendChild(header); + + const grid = this._el('div', 'lf-detail-grid'); + + const fields = LogFilterMinigame.LOG_FIELDS[this._logType] || []; + for (const f of fields) { + const lbl = this._el('div', 'lf-detail-label'); + lbl.textContent = f.label + ':'; + const val = this._el('div', 'lf-detail-value'); + val.textContent = entry[f.key] || '—'; + grid.appendChild(lbl); + grid.appendChild(val); + } + panel.appendChild(grid); + + // Action buttons + const actions = this._el('div', 'lf-detail-actions'); + panel.appendChild(actions); + + const anomalyEntry = this._isAnomalyEntry(entry); + + // [LOOK UP IP] — always shown + const ipBtn = this._el('button', 'lf-detail-btn'); + ipBtn.textContent = '[LOOK UP IP]'; + ipBtn.addEventListener('click', () => { + this._openOverlay('threat_intel'); + this._fireTriggerActions('threat_intel_opened'); + }); + actions.appendChild(ipBtn); + + // [INVESTIGATE ACCOUNT] — always shown + const accBtn = this._el('button', 'lf-detail-btn'); + accBtn.textContent = '[INVESTIGATE ACCOUNT]'; + accBtn.addEventListener('click', () => this._openOverlay('account_history')); + actions.appendChild(accBtn); + + // [FLAG SESSION] — only on anomaly entry + if (anomalyEntry) { + if (this._sessionFlagged) { + const flagged = this._el('div', 'lf-session-flagged-banner'); + flagged.textContent = '✓ SESSION FLAGGED'; + actions.appendChild(flagged); + + if (this._requireAllTabs && !this._allAddlTabsVisited()) { + this._renderTab2Prompt(panel); + } + } else { + const flagBtn = this._el('button', 'lf-detail-btn lf-detail-btn-flag'); + flagBtn.textContent = `[${this._flagActionLabel}]`; + flagBtn.addEventListener('click', () => this._openOverlay('flag_confirm')); + actions.appendChild(flagBtn); + } + } + + pane.appendChild(panel); + } + + _renderTab2Prompt(panel) { + const prompt = this._el('div', 'lf-tab2-prompt'); + const text = document.createTextNode( + '► Session flagged. Switch to the SIS Engineering Audit tab to complete your investigation.' + ); + const goBtn = this._el('button', 'lf-tab2-prompt-btn'); + goBtn.textContent = '[VIEW SIS ENGINEERING AUDIT →]'; + goBtn.addEventListener('click', () => { + if (this._additionalTabs.length > 0) { + this._switchTab(this._additionalTabs[0].id); + this._rebuildTabBar(); + } + }); + prompt.appendChild(text); + prompt.appendChild(goBtn); + panel.appendChild(prompt); + } + + _isAnomalyEntry(entry) { + if (!this._anomaly) return false; + const acc = entry.account || entry.user || ''; + return acc === this._anomaly.account; + } + + // ── Overlays ─────────────────────────────────────────────────────────── + + _openOverlay(mode) { + this._overlayMode = mode; + this._renderOverlay(); + } + + _closeOverlay() { + this._overlayMode = null; + const existing = this._dom.body.querySelector('.lf-overlay'); + if (existing) existing.remove(); + } + + _renderOverlay() { + // Remove existing overlay + const existing = this._dom.body.querySelector('.lf-overlay'); + if (existing) existing.remove(); + + const overlay = this._el('div', 'lf-overlay'); + this._dom.body.appendChild(overlay); + + const panel = this._el('div', 'lf-overlay-panel'); + overlay.appendChild(panel); + + switch (this._overlayMode) { + case 'threat_intel': this._renderThreatIntelOverlay(panel); break; + case 'account_history': this._renderAccountHistoryOverlay(panel); break; + case 'flag_confirm': this._renderFlagConfirmOverlay(panel); break; + case 'audit_detail': this._renderAuditDetailOverlay(panel); break; + } + } + + _overlayHeader(panel, title) { + const hdr = this._el('div', 'lf-overlay-header'); + const t = this._el('span'); + t.textContent = title; + const closeBtn = this._el('button', 'lf-overlay-close'); + closeBtn.textContent = '[✕]'; + closeBtn.addEventListener('click', () => this._closeOverlay()); + hdr.appendChild(t); + hdr.appendChild(closeBtn); + panel.appendChild(hdr); + const body = this._el('div', 'lf-overlay-body'); + panel.appendChild(body); + return body; + } + + _renderThreatIntelOverlay(panel) { + if (!this._threatIntel) return; + const body = this._overlayHeader(panel, 'THREAT INTELLIGENCE — IP LOOKUP'); + const ti = this._threatIntel; + + const grid = this._el('div', 'lf-threat-grid'); + const rows = [ + ['IP:', ti.ip], + ['ASN:', ti.asn], + ['Type:', ti.type], + ['Location:', ti.location], + ['Last flagged:', ti.lastFlagged] + ]; + for (const [lbl, val] of rows) { + const l = this._el('div', 'lf-threat-label'); l.textContent = lbl; + const v = this._el('div', 'lf-threat-value'); v.textContent = val; + grid.appendChild(l); grid.appendChild(v); + } + body.appendChild(grid); + + if (ti.knownBad) { + const badge = this._el('div', 'lf-threat-known-bad'); + badge.textContent = '⚠ KNOWN BAD: YES — Tor Exit Node'; + body.appendChild(badge); + } + } + + _renderAccountHistoryOverlay(panel) { + if (!this._accountHistory) return; + const ah = this._accountHistory; + const body = this._overlayHeader(panel, `ACCOUNT INVESTIGATION — ${ah.account}`); + + const divider1 = this._el('hr', 'lf-overlay-divider'); + body.appendChild(divider1); + + const grid = this._el('div', 'lf-account-grid'); + const rows = [ + ['Full name:', ah.fullName], + ['Contractor:', ah.contractor], + ['Role:', ah.role], + ['Access level:', ah.accessLevel], + ['Account status:', ah.status], + ]; + for (const [lbl, val] of rows) { + const l = this._el('div', 'lf-account-label'); l.textContent = lbl; + const v = this._el('div', 'lf-account-value'); + if (val === 'DEPROVISIONED' || (val && val.startsWith('DEPROVISIONED'))) { + v.classList.add('lf-account-deprovisioned'); + } + v.textContent = val; + grid.appendChild(l); grid.appendChild(v); + } + + // Deprovision note (special — shows gap) + if (ah.deprovisionNote) { + const lbl = this._el('div', 'lf-account-label'); lbl.textContent = ''; + const val = this._el('div', 'lf-account-value lf-account-gap-note'); + val.textContent = ah.deprovisionNote; + grid.appendChild(lbl); grid.appendChild(val); + } + body.appendChild(grid); + + const divider2 = this._el('hr', 'lf-overlay-divider'); + body.appendChild(divider2); + + const grid2 = this._el('div', 'lf-account-grid'); + const rows2 = [ + ['Last legit session:', ah.lastLegitimateSession], + ['Current session:', ah.currentSession] + ]; + for (const [lbl, val] of rows2) { + const l = this._el('div', 'lf-account-label'); l.textContent = lbl; + const v = this._el('div', 'lf-account-value'); v.textContent = val; + grid2.appendChild(l); grid2.appendChild(v); + } + body.appendChild(grid2); + + if (ah.anomalyBadge) { + const badge = this._el('div', 'lf-account-anomaly-badge'); + badge.textContent = `⚠ ${ah.anomalyBadge}`; + body.appendChild(badge); + } + } + + _renderFlagConfirmOverlay(panel) { + const body = this._overlayHeader(panel, this._flagConfirmTitle); + + const text = this._el('div', 'lf-flag-confirm-body'); + text.textContent = this._flagConfirmBody; + body.appendChild(text); + + const actions = this._el('div', 'lf-flag-confirm-actions'); + + const cancel = this._el('button', 'lf-flag-cancel-btn'); + cancel.textContent = '[CANCEL]'; + cancel.addEventListener('click', () => this._closeOverlay()); + + const confirm = this._el('button', 'lf-flag-confirm-btn'); + confirm.textContent = '[CONFIRM — FLAG ACTIVE SESSION]'; + confirm.addEventListener('click', () => this._onFlagConfirmed()); + + actions.appendChild(cancel); + actions.appendChild(confirm); + body.appendChild(actions); + } + + _onFlagConfirmed() { + this._sessionFlagged = true; + this._closeOverlay(); + + // Re-render session detail to show flagged state + if (this._dom.logPane) { + const existing = this._dom.logPane.querySelectorAll('.lf-session-detail'); + existing.forEach(el => el.remove()); + if (this._selectedEntry) { + this._renderSessionDetail(this._dom.logPane); + } + } + + this._checkCompletion(); + } + + // ── Additional Tabs ──────────────────────────────────────────────────── + + _onAdditionalTabVisited(tab) { + if (this._tabsVisited.has(tab.id)) return; + this._tabsVisited.add(tab.id); + + // Remove unread indicator + const btn = this._dom.tabBar.querySelector(`[data-tab-id="${tab.id}"]`); + if (btn) btn.classList.remove('lf-tab-unread'); + + // Fire onView setVariable + if (tab.onView?.setVariable) { + for (const [key, val] of Object.entries(tab.onView.setVariable)) { + this._setGlobalAndNotify(key, val); + } + } + + // Fire matching progressActions + this._fireTriggerActions('tab_viewed', tab.id); + + this._checkCompletion(); + } + + _allAddlTabsVisited() { + return this._additionalTabs.every(t => this._tabsVisited.has(t.id)); + } + + // ── Audit Log Tab ────────────────────────────────────────────────────── + + _renderAuditLogTab(body, tab) { + const pane = this._el('div', 'lf-audit-pane'); + body.appendChild(pane); + + // Header bar + const headerBar = this._el('div', 'lf-audit-header-bar'); + const titleEl = this._el('div', 'lf-audit-title'); + titleEl.textContent = tab.title || 'AUDIT LOG'; + const subtitleEl = this._el('div', 'lf-audit-subtitle'); + subtitleEl.textContent = tab.subtitle || ''; + headerBar.appendChild(titleEl); + headerBar.appendChild(subtitleEl); + pane.appendChild(headerBar); + + const tableWrap = this._el('div', 'lf-audit-table-wrap'); + pane.appendChild(tableWrap); + + const table = this._el('table', 'lf-audit-table'); + const thead = document.createElement('thead'); + const hRow = document.createElement('tr'); + const auditFields = [ + { key: 'timestamp', label: 'TIMESTAMP', width: '140px' }, + { key: 'operator', label: 'OPERATOR', width: '110px' }, + { key: 'command', label: 'COMMAND', width: '120px' }, + { key: 'parameter', label: 'PARAMETER', width: '160px' }, + { key: 'result', label: 'RESULT', width: '60px' } + ]; + for (const f of auditFields) { + const th = document.createElement('th'); + th.textContent = f.label; + th.style.width = f.width; + hRow.appendChild(th); + } + thead.appendChild(hRow); + table.appendChild(thead); + + const tbody = document.createElement('tbody'); + const entries = tab.auditEntries || []; + for (const entry of entries) { + const tr = document.createElement('tr'); + tr.classList.add('lf-audit-row'); + + const isAnomalyEntry = entry.operator && entry.operator !== '[SYSTEM]' && + this._anomaly && entry.operator === this._anomaly.account; + const isError = entry.errorClass === true || entry.result === 'ERR'; + + if (isAnomalyEntry) tr.classList.add('lf-audit-anomaly-row'); + if (isError) tr.classList.add('lf-audit-error-row'); + + for (const f of auditFields) { + const td = document.createElement('td'); + if (f.key === 'operator') { + td.classList.add('lf-audit-operator'); + td.textContent = entry[f.key] || '—'; + if (isAnomalyEntry && entry.command === 'WRITE_CONFIG') { + const chev = this._el('span', 'lf-audit-chevron'); + chev.textContent = ' ►'; + td.appendChild(chev); + } + } else { + td.textContent = entry[f.key] || '—'; + } + tr.appendChild(td); + } + + if (isAnomalyEntry && entry.detail) { + tr.style.cursor = 'pointer'; + tr.addEventListener('click', () => { + this._selectedAuditEntry = entry; + this._openOverlay('audit_detail'); + }); + } + + tbody.appendChild(tr); + } + table.appendChild(tbody); + tableWrap.appendChild(table); + } + + _renderAuditDetailOverlay(panel) { + const entry = this._selectedAuditEntry; + if (!entry) return; + + const body = this._overlayHeader(panel, `COMMAND DETAIL — ${entry.timestamp}`); + + const grid = this._el('div', 'lf-audit-detail-grid'); + const rows = [ + ['Operator:', entry.operator], + ['Command:', entry.command], + ['Parameter:', entry.parameter], + ['Old value:', entry.oldValue || '—'], + ['New value:', entry.newValue || '—'], + ['Result:', entry.result], + ['Session:', entry.sessionRef || '—'] + ]; + for (const [lbl, val] of rows) { + if (!val || val === '—') continue; + const l = this._el('div', 'lf-audit-detail-label'); l.textContent = lbl; + const v = this._el('div', 'lf-audit-detail-value'); v.textContent = val; + grid.appendChild(l); grid.appendChild(v); + } + body.appendChild(grid); + + if (entry.detail) { + const detail = this._el('div', 'lf-audit-critical'); + detail.textContent = entry.detail; + body.appendChild(detail); + } + } + + // ── Completion ───────────────────────────────────────────────────────── + + _checkCompletion() { + if (this._completionFired) return; + + const sessionDone = this._sessionFlagged; + const tabsDone = !this._requireAllTabs || this._allAddlTabsVisited(); + + if (sessionDone && tabsDone) { + this._completionFired = true; + this._onComplete(); + } + } + + _onComplete() { + this._executeActions(this._completionActions); + + // Show completion banner in session log + if (this._currentTab === 'session_log' && this._dom.logPane) { + const existing = this._dom.logPane.querySelectorAll('.lf-complete-banner, .lf-session-detail'); + existing.forEach(el => el.remove()); + const banner = this._el('div', 'lf-complete-banner'); + banner.textContent = '✓ INVESTIGATION COMPLETE — Session flagged. SIS audit reviewed.'; + this._dom.logPane.appendChild(banner); + } + + // Auto-close + this._closeTimer = setTimeout(() => this.close(), 1400); + } + + // ── Action execution ─────────────────────────────────────────────────── + + _executeActions(actions) { + if (!Array.isArray(actions)) return; + for (const action of actions) { + if (action.type === 'set_global') { + this._setGlobalAndNotify(action.key, action.value); + } else if (action.type === 'complete_task') { + window.objectivesManager?.completeTask(action.taskId); + } + } + } + + _fireTriggerActions(trigger, tabId) { + for (const action of this._progressActions) { + if (action.trigger !== trigger) continue; + if (trigger === 'tab_viewed' && action.tabId !== tabId) continue; + if (action.type === 'set_global') { + this._setGlobalAndNotify(action.key, action.value); + } + } + } + + // ── Global state ─────────────────────────────────────────────────────── + + _setGlobalAndNotify(name, value) { + if (window.npcManager?.setGlobalVariable) { + window.npcManager.setGlobalVariable(name, value); + return; + } + const oldValue = window.gameState?.globalVariables?.[name]; + if (window.gameState?.globalVariables) { + window.gameState.globalVariables[name] = value; + } + window.npcConversationStateManager?.broadcastGlobalVariableChange(name, value, null); + window.eventDispatcher?.emit(`global_variable_changed:${name}`, { name, value, oldValue }); + } + + // ── Status bar ───────────────────────────────────────────────────────── + + _updateStatusBar() { + if (!this._dom.statusBar) return; + if (this._currentTab !== 'session_log') { + this._dom.statusBar.textContent = ''; + return; + } + const filtered = this._applyFilters(this._logEntries); + const sb = this._dom.statusBar; + sb.innerHTML = ''; + const countEl = this._el('span', 'lf-status-bar-count'); + countEl.textContent = `RESULTS: ${filtered.length} entries visible`; + const filtersEl = document.createTextNode(` · ${this._activeFilters.length} filter${this._activeFilters.length !== 1 ? 's' : ''} active`); + sb.appendChild(countEl); + sb.appendChild(filtersEl); + } + + // ── DOM helpers ──────────────────────────────────────────────────────── + + _el(tag, classList) { + const el = document.createElement(tag); + if (classList) { + for (const cls of classList.split(' ')) { + if (cls) el.classList.add(cls); + } + } + return el; + } + + // ── Cleanup ──────────────────────────────────────────────────────────── + + cleanup() { + if (this._closeTimer) clearTimeout(this._closeTimer); + super.cleanup(); + } +} diff --git a/public/break_escape/js/systems/interactions.js b/public/break_escape/js/systems/interactions.js index 260b92cf..f3a9d7e4 100644 --- a/public/break_escape/js/systems/interactions.js +++ b/public/break_escape/js/systems/interactions.js @@ -1021,6 +1021,22 @@ export function handleObjectInteraction(sprite) { return; } + // Handle Log Filter Terminal (VM-02 sis02 / MG-06 sis01) + if (sprite.scenarioData?.type === 'log_filter_terminal' || + sprite.type === 'log_filter_terminal') { + const minigameId = sprite.scenarioData?.minigameId || 'log-filter'; + if (window.MinigameFramework) { + if (!window.MinigameFramework.mainGameScene) + window.MinigameFramework.init(window.game); + window.MinigameFramework.startMinigame(minigameId, null, { + title: sprite.scenarioData?.title || 'Access Log Analyser', + showCancel: true, + cancelText: 'Close', + sprite + }); + } + return; + } // Handle Drug Library Integrity Terminal (MG-09 sis01) if (sprite.scenarioData.type === 'drug_library_terminal' || sprite.type === 'drug_library_terminal') { console.log('Drug library dispatch firing, calling starter...', { fn: typeof window.startDrugLibraryIntegrityMinigame }); diff --git a/scenarios/sis02_energy/scenario.json.erb b/scenarios/sis02_energy/scenario.json.erb index 2a68a852..5add6453 100644 --- a/scenarios/sis02_energy/scenario.json.erb +++ b/scenarios/sis02_energy/scenario.json.erb @@ -46,7 +46,7 @@ # # NOT YET BUILT — required before scenario can fully run # VM: albion_scada_historian — Hacktivity VM for historian trend analysis (MG-02/VM-01) -# VM: albion_eng_workstation — Hacktivity VM for access log analysis (MG-04/VM-02) +# MG-VM02 hmi_eng_02 — LogFilterMinigame (log-filter) — IMPLEMENTED # INK: all four .ink files developed; require inklecate compilation + tag cross-check (TEST-02) # SPRITES: scada_control_room — room_office used as placeholder (ASSET-03) # SPRITES: battery_hall — room_servers used as placeholder (ASSET-04) @@ -250,13 +250,10 @@ hmi_password = @random_password # HMI-OPS-01 SCADA workstation password "status": "active" }, { - // TODO[VM]: albion_eng_workstation — jump server access log flag + // MG-VM02: hmi_eng_02 — LogFilterMinigame (log-filter), IMPLEMENTED "taskId": "identify_rdp_session", "title": "Identify the active attacker RDP session on the jump server", - "type": "submit_flags", - "targetFlags": ["albion_eng_workstation:jump_server_flag"], - "targetCount": 1, - "showProgress": true, + "type": "manual", "status": "active" } ] @@ -1006,38 +1003,152 @@ hmi_password = @random_password # HMI-OPS-01 SCADA workstation password "npcs": [], "objects": [ - // TODO[VM]: albion_eng_workstation — VM with jump server access logs, SIS engineering interface - // IMPLEMENTED: vm-launcher + flag-station infrastructure in place; VM content pending + // MG-VM02: hmi_eng_02 — LogFilterMinigame (log-filter), IMPLEMENTED { - "type": "vm-launcher", - "id": "vm_launcher_eng", + "type": "minigame", + "id": "hmi_eng_02", + "minigameId": "log-filter", "name": "HMI-ENG-02 Engineering Workstation", "sprite": "vm-launcher-desktop", "takeable": false, "puzzle_graph_role": "vm", "puzzle_graph_aim": "contact_marcus_investigate", - "puzzle_graph_links": [ - { "from": "vm_launcher_eng", "to": "eng_flag_station" } - ], "observations": "The engineering workstation. An active RDP session is visible on the screen — user: c.ellison, connected since 01:47. This account was deprovisioned eight months ago.", - "hacktivityMode": <%= vm_context && vm_context['hacktivity_mode'] ? 'true' : 'false' %>, - "vm": <%= vm_object('albion_eng_workstation', {"id": 2, "ip": "192.168.100.52"}) %> - }, - - // IMPLEMENTED: flag-station for engineering workstation — accepts access log analysis flag - { - "type": "flag-station", - "id": "eng_flag_station", - "name": "Jump Server Audit Terminal", - "takeable": false, - "puzzle_graph_role": "lock", - "puzzle_graph_aim": "contact_marcus_investigate", - "observations": "A secure terminal for submitting engineering workstation analysis findings.", - "acceptsVms": ["albion_eng_workstation"], - "flags": <%= flags_for_vm('albion_eng_workstation') %>, - "flagRewards": [ - { "type": "set_global", "key": "jump_server_confirmed", "value": true } - ] + "scenarioData": { + "title": "ALBION ENERGY — JUMP SERVER ACCESS LOG (JS-ALBION-01)", + "logType": "ics_rdp", + "logEntries": [ + { "timestamp": "2025-01-10 08:14", "sessionId": "SID-7801-A", "account": "j.nakamura", "sourceIp": "10.4.22.8", "duration": "00:47", "status": "CLOSED", "accessLevel": "ENGINEER" }, + { "timestamp": "2025-01-10 09:33", "sessionId": "SID-7802-B", "account": "m.patel", "sourceIp": "10.4.22.11", "duration": "01:15", "status": "CLOSED", "accessLevel": "ENGINEER" }, + { "timestamp": "2025-01-10 11:22", "sessionId": "SID-7817-Q", "account": "d.okonkwo", "sourceIp": "10.4.22.14", "duration": "00:34", "status": "CLOSED", "accessLevel": "ADMIN" }, + { "timestamp": "2025-01-10 14:22", "sessionId": "SID-7803-C", "account": "j.nakamura", "sourceIp": "10.4.22.8", "duration": "00:32", "status": "CLOSED", "accessLevel": "ENGINEER" }, + { "timestamp": "2025-01-10 16:05", "sessionId": "SID-7818-R", "account": "j.nakamura", "sourceIp": "10.4.22.8", "duration": "01:13", "status": "CLOSED", "accessLevel": "ENGINEER" }, + { "timestamp": "2025-01-11 08:33", "sessionId": "SID-7819-S", "account": "j.nakamura", "sourceIp": "10.4.22.8", "duration": "00:48", "status": "CLOSED", "accessLevel": "ENGINEER" }, + { "timestamp": "2025-01-11 09:01", "sessionId": "SID-7804-D", "account": "d.okonkwo", "sourceIp": "10.4.22.14", "duration": "02:08", "status": "CLOSED", "accessLevel": "ADMIN" }, + { "timestamp": "2025-01-11 10:47", "sessionId": "SID-7805-E", "account": "m.patel", "sourceIp": "10.4.22.11", "duration": "00:58", "status": "CLOSED", "accessLevel": "ENGINEER" }, + { "timestamp": "2025-01-11 15:49", "sessionId": "SID-7820-T", "account": "m.patel", "sourceIp": "10.4.22.11", "duration": "01:22", "status": "CLOSED", "accessLevel": "ENGINEER" }, + { "timestamp": "2025-01-12 08:30", "sessionId": "SID-7806-F", "account": "j.nakamura", "sourceIp": "10.4.22.8", "duration": "01:04", "status": "CLOSED", "accessLevel": "ENGINEER" }, + { "timestamp": "2025-01-12 10:07", "sessionId": "SID-7821-U", "account": "d.okonkwo", "sourceIp": "10.4.22.14", "duration": "00:45", "status": "CLOSED", "accessLevel": "ADMIN" }, + { "timestamp": "2025-01-12 13:15", "sessionId": "SID-7807-G", "account": "s.krishna", "sourceIp": "10.4.22.21", "duration": "00:43", "status": "CLOSED", "accessLevel": "CONTRACTOR" }, + { "timestamp": "2025-01-12 15:44", "sessionId": "SID-7822-V", "account": "m.patel", "sourceIp": "10.4.22.11", "duration": "00:37", "status": "CLOSED", "accessLevel": "ENGINEER" }, + { "timestamp": "2025-01-13 08:19", "sessionId": "SID-7808-H", "account": "j.nakamura", "sourceIp": "10.4.22.8", "duration": "01:22", "status": "CLOSED", "accessLevel": "ENGINEER" }, + { "timestamp": "2025-01-13 11:55", "sessionId": "SID-7823-W", "account": "m.patel", "sourceIp": "10.4.22.11", "duration": "01:08", "status": "CLOSED", "accessLevel": "ENGINEER" }, + { "timestamp": "2025-01-13 14:31", "sessionId": "SID-7809-I", "account": "d.okonkwo", "sourceIp": "10.4.22.14", "duration": "00:27", "status": "CLOSED", "accessLevel": "ADMIN" }, + { "timestamp": "2025-01-13 16:20", "sessionId": "SID-7824-X", "account": "j.nakamura", "sourceIp": "10.4.22.8", "duration": "00:55", "status": "CLOSED", "accessLevel": "ENGINEER" }, + { "timestamp": "2025-01-14 08:55", "sessionId": "SID-7810-J", "account": "m.patel", "sourceIp": "10.4.22.11", "duration": "01:37", "status": "CLOSED", "accessLevel": "ENGINEER" }, + { "timestamp": "2025-01-14 09:22", "sessionId": "SID-7825-Y", "account": "d.okonkwo", "sourceIp": "10.4.22.14", "duration": "00:52", "status": "CLOSED", "accessLevel": "ADMIN" }, + { "timestamp": "2025-01-14 11:43", "sessionId": "SID-7811-K", "account": "j.nakamura", "sourceIp": "10.4.22.8", "duration": "00:51", "status": "CLOSED", "accessLevel": "ENGINEER" }, + { "timestamp": "2025-01-14 14:11", "sessionId": "SID-7826-Z", "account": "s.krishna", "sourceIp": "10.4.22.21", "duration": "01:26", "status": "CLOSED", "accessLevel": "CONTRACTOR" }, + { "timestamp": "2025-01-15 09:08", "sessionId": "SID-7812-L", "account": "s.krishna", "sourceIp": "10.4.22.21", "duration": "02:14", "status": "CLOSED", "accessLevel": "CONTRACTOR" }, + { "timestamp": "2025-01-15 11:03", "sessionId": "SID-7827-AA","account": "j.nakamura", "sourceIp": "10.4.22.8", "duration": "00:44", "status": "CLOSED", "accessLevel": "ENGINEER" }, + { "timestamp": "2025-01-15 13:30", "sessionId": "SID-7828-AB","account": "d.okonkwo", "sourceIp": "10.4.22.14", "duration": "01:07", "status": "CLOSED", "accessLevel": "ADMIN" }, + { "timestamp": "2025-01-15 14:27", "sessionId": "SID-7813-M", "account": "m.patel", "sourceIp": "10.4.22.11", "duration": "01:03", "status": "CLOSED", "accessLevel": "ENGINEER" }, + { "timestamp": "2025-01-15 16:18", "sessionId": "SID-7814-N", "account": "d.okonkwo", "sourceIp": "10.4.22.14", "duration": "00:38", "status": "CLOSED", "accessLevel": "ADMIN" }, + { "timestamp": "2025-01-15 17:44", "sessionId": "SID-7815-O", "account": "j.nakamura", "sourceIp": "10.4.22.8", "duration": "02:01", "status": "CLOSED", "accessLevel": "ENGINEER" }, + { "timestamp": "2025-01-16 01:47", "sessionId": "SID-7816-P", "account": "c.ellison", "sourceIp": "185.220.101.45","duration": "04:46+","status": "ACTIVE", "accessLevel": "CONTRACTOR" }, + { "timestamp": "2025-01-16 08:10", "sessionId": "SID-7829-AC","account": "j.nakamura", "sourceIp": "10.4.22.8", "duration": "01:14", "status": "CLOSED", "accessLevel": "ENGINEER" }, + { "timestamp": "2025-01-16 09:44", "sessionId": "SID-7830-AD","account": "m.patel", "sourceIp": "10.4.22.11", "duration": "00:49", "status": "CLOSED", "accessLevel": "ENGINEER" }, + { "timestamp": "2025-01-16 10:22", "sessionId": "SID-7831-AE","account": "j.nakamura", "sourceIp": "10.4.22.8", "duration": "01:02", "status": "CLOSED", "accessLevel": "ENGINEER" }, + { "timestamp": "2025-01-16 11:08", "sessionId": "SID-7832-AF","account": "d.okonkwo", "sourceIp": "10.4.22.14", "duration": "01:33", "status": "CLOSED", "accessLevel": "ADMIN" }, + { "timestamp": "2025-01-16 11:55", "sessionId": "SID-7833-AG","account": "m.patel", "sourceIp": "10.4.22.11", "duration": "00:28", "status": "CLOSED", "accessLevel": "ENGINEER" }, + { "timestamp": "2025-01-16 13:17", "sessionId": "SID-7834-AH","account": "j.nakamura", "sourceIp": "10.4.22.8", "duration": "01:45", "status": "CLOSED", "accessLevel": "ENGINEER" }, + { "timestamp": "2025-01-16 14:32", "sessionId": "SID-7835-AI","account": "m.patel", "sourceIp": "10.4.22.11", "duration": "00:56", "status": "CLOSED", "accessLevel": "ENGINEER" }, + { "timestamp": "2025-01-16 15:04", "sessionId": "SID-7836-AJ","account": "d.okonkwo", "sourceIp": "10.4.22.14", "duration": "00:43", "status": "CLOSED", "accessLevel": "ADMIN" }, + { "timestamp": "2025-01-16 15:49", "sessionId": "SID-7837-AK","account": "j.nakamura", "sourceIp": "10.4.22.8", "duration": "01:11", "status": "CLOSED", "accessLevel": "ENGINEER" }, + { "timestamp": "2025-01-16 16:22", "sessionId": "SID-7838-AL","account": "m.patel", "sourceIp": "10.4.22.11", "duration": "00:34", "status": "CLOSED", "accessLevel": "ENGINEER" }, + { "timestamp": "2025-01-16 16:57", "sessionId": "SID-7839-AM","account": "j.nakamura", "sourceIp": "10.4.22.8", "duration": "00:47", "status": "CLOSED", "accessLevel": "ENGINEER" }, + { "timestamp": "2025-01-16 17:08", "sessionId": "SID-7840-AN","account": "s.krishna", "sourceIp": "10.4.22.21", "duration": "01:19", "status": "CLOSED", "accessLevel": "CONTRACTOR" } + ], + "anomaly": { + "account": "c.ellison", + "sourceIp": "185.220.101.45", + "timestamp": "2025-01-16 01:47", + "status": "ACTIVE", + "accessLevel": "CONTRACTOR", + "durationDisplay": "04:46+" + }, + "threatIntel": { + "ip": "185.220.101.45", + "asn": "AS60729 — Zwiebelfreunde e.V.", + "type": "Tor Exit Node", + "location": "Frankfurt, Germany (exit node)", + "lastFlagged": "2025-01-14 (credential stuffing activity)", + "knownBad": true + }, + "accountHistory": { + "account": "c.ellison", + "fullName": "C. Ellison (ICS Commissioning)", + "contractor": "CastleTech Engineering Ltd", + "role": "ICS/SCADA Commissioning Engineer", + "accessLevel": "CONTRACTOR — OT Network", + "status": "DEPROVISIONED", + "deprovisionedDate": "2024-05-09", + "deprovisionNote": "Account locked per leaver process. JUMP SERVER LOCAL AD: NOT REMOVED", + "lastLegitimateSession": "2024-04-28 09:15", + "currentSession": "2025-01-16 01:47 — ACTIVE (04:46+)", + "anomalyBadge": "DEPROVISIONED ACCOUNT — ACTIVE SESSION" + }, + "flagActionLabel": "FLAG SESSION", + "flagConfirmTitle": "CONFIRM SESSION FLAG", + "flagConfirmBody": "Account c.ellison was deprovisioned 2024-05-09. Active session since 01:47 from Tor exit node 185.220.101.45. Unauthorised access via dormant contractor account. Active attacker session in SCADA network.", + "additionalTabs": [ + { + "id": "sis_audit", + "label": "SIS ENGINEERING AUDIT", + "type": "audit_log", + "title": "SIS ENGINEERING AUDIT LOG — JS-ALBION-01 → SIS-ENG-PORT", + "subtitle": "2025-01-10 to 2025-01-16 · Showing: all commands", + "auditEntries": [ + { "timestamp": "2025-01-10 09:18", "operator": "j.nakamura", "command": "READ_CONFIG", "parameter": "ALL_SETPOINTS", "result": "OK" }, + { "timestamp": "2025-01-10 14:15", "operator": "m.patel", "command": "READ_CONFIG", "parameter": "ALL_SETPOINTS", "result": "OK" }, + { "timestamp": "2025-01-11 14:30", "operator": "m.patel", "command": "BACKUP_CONFIG", "parameter": "SIS_20250111", "result": "OK" }, + { "timestamp": "2025-01-12 10:33", "operator": "d.okonkwo", "command": "BACKUP_CONFIG", "parameter": "SIS_20250112", "result": "OK" }, + { "timestamp": "2025-01-13 08:47", "operator": "j.nakamura", "command": "READ_CONFIG", "parameter": "THERMAL_RUNAWAY_T", "result": "OK" }, + { "timestamp": "2025-01-13 09:12", "operator": "j.nakamura", "command": "READ_CONFIG", "parameter": "CHARGE_INHIBIT_TEMP", "result": "OK" }, + { "timestamp": "2025-01-14 09:17", "operator": "m.patel", "command": "READ_CONFIG", "parameter": "SIS_SYSTEM_STATUS", "result": "OK" }, + { "timestamp": "2025-01-14 11:05", "operator": "d.okonkwo", "command": "EXPORT_CONFIG", "parameter": "SIS_BASELINE_REF", "result": "OK" }, + { "timestamp": "2025-01-15 16:40", "operator": "j.nakamura", "command": "EXPORT_CONFIG", "parameter": "SAFETY_THRESHOLDS_REF", "result": "OK" }, + { "timestamp": "2025-01-16 02:04", "operator": "j.nakamura", "command": "READ_CONFIG", "parameter": "THERMAL_RUNAWAY_T", "result": "OK" }, + { "timestamp": "2025-01-16 02:17", "operator": "j.nakamura", "command": "READ_CONFIG", "parameter": "CHARGE_INHIBIT_TEMP", "result": "OK" }, + { + "timestamp": "2025-01-16 03:22", + "operator": "c.ellison", + "command": "WRITE_CONFIG", + "parameter": "THERMAL_RUNAWAY_T", + "oldValue": "55°C", + "newValue": "85°C", + "result": "OK", + "sessionRef": "SID-7816-P", + "detail": "CRITICAL: Thermal runaway protection threshold raised 30°C. IEC 61511 certified value: 55°C. No change approval recorded. Operator account is DEPROVISIONED." + }, + { + "timestamp": "2025-01-16 03:24", + "operator": "c.ellison", + "command": "WRITE_CONFIG", + "parameter": "SIS_HEARTBEAT_INTERVAL", + "oldValue": "5s", + "newValue": "30s", + "result": "OK", + "sessionRef": "SID-7816-P", + "detail": "Heartbeat interval increased 6×. Slows fault detection response by 25 seconds. No change approval recorded." + }, + { "timestamp": "2025-01-16 06:00", "operator": "[SYSTEM]", "command": "BACKUP_FAILED", "parameter": "SIS_CONFIG_NOW", "result": "ERR", "errorClass": true } + ], + "onView": { + "setVariable": { "sis_audit_reviewed": true } + } + } + ], + "requireAllTabs": true, + "completionActions": [ + { "type": "set_global", "key": "jump_server_confirmed", "value": true }, + { "type": "complete_task", "taskId": "identify_rdp_session" } + ], + "progressActions": [ + { "type": "set_global", "key": "sis_audit_reviewed", "value": true, "trigger": "tab_viewed", "tabId": "sis_audit" }, + { "type": "set_global", "key": "jump_server_threat_intel_viewed","value": true, "trigger": "threat_intel_opened" } + ] + } }, // IMPLEMENTED: Jump server rack — visual prop with readable session info diff --git a/test-alarm-panel-minigame.html b/test-alarm-panel-minigame.html new file mode 100644 index 00000000..a3e8f577 --- /dev/null +++ b/test-alarm-panel-minigame.html @@ -0,0 +1,406 @@ + + +
+ + +