From 513f0e81f6b18db3dd97038a2575c655bbb3203e Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 10 Apr 2026 01:05:08 +0000 Subject: [PATCH] feat: security hardening, game system wiring, and asset pipeline fix - Fix dependency vulnerabilities: minimatch >=10.2.4, file-type >=21.3.3 with pnpm overrides; upgrade @assetpack/core to 1.8.0 - Replace innerHTML with safe DOM APIs in Main.res and MainHubScreen.res - Annotate game-content credentials as fictional (not real secrets) - Fix AssetPack/Vite restart loop with lazy-loaded plugin (ASSETPACK=1) - Wire DualAlert system into GameLoop (physical + cyber alert meters) - Wire QPrograms deck, QCertifications, and JessicaLoadout into gameState - Add SecurityAI.requestAntiHackerDispatch for cyber alert integration - Add DualAlertBridge tests for threat classification and trigger conversion https://claude.ai/code/session_01WfL9NSptNEzdPbcbwWD6gu --- SECURITY-AUDIT-REPORT.md | 9 +- __tests__/DualAlertBridge_test.res | 125 ++++++++++++++++++++++++++ deno.json | 2 +- package.json | 6 ++ src/Main.res | 20 +++-- src/app/GameLoop.res | 22 ++++- src/app/devices/DeviceView.res | 4 + src/app/devices/GlobalNetworkData.res | 5 ++ src/app/enemies/SecurityAI.res | 9 ++ src/app/screens/hub/MainHubScreen.res | 2 +- vite.config.js | 36 ++++++-- 11 files changed, 220 insertions(+), 20 deletions(-) diff --git a/SECURITY-AUDIT-REPORT.md b/SECURITY-AUDIT-REPORT.md index ae92f2fe..d0cd7ac5 100644 --- a/SECURITY-AUDIT-REPORT.md +++ b/SECURITY-AUDIT-REPORT.md @@ -195,9 +195,12 @@ app.use((req, res, next) => { ## 📋 Implementation Checklist ### Immediate Actions (Next 48 Hours) -- [ ] Update file-type to >=21.3.1 -- [ ] Update minimatch to >=10.2.1 -- [ ] Run `npm audit fix --force` +- [x] Update file-type to >=21.3.3 (package.json overrides + pnpm overrides) ✅ 2026-04-10 +- [x] Update minimatch to >=10.2.4 (package.json overrides + pnpm overrides) ✅ 2026-04-10 +- [x] Replace innerHTML usage with safe DOM APIs (Main.res, MainHubScreen.res) ✅ 2026-04-10 +- [x] Annotate game-content "secrets" as fictional (GlobalNetworkData.res, DeviceView.res) ✅ 2026-04-10 +- [x] Fix AssetPack/Vite restart loop (lazy-load plugin, ASSETPACK=1 env gate) ✅ 2026-04-10 +- [x] Upgrade @assetpack/core to 1.8.0 (deno.json) ✅ 2026-04-10 - [ ] Verify no new vulnerabilities introduced ### Week 1 Actions diff --git a/__tests__/DualAlertBridge_test.res b/__tests__/DualAlertBridge_test.res index 5c54d7ff..70aadd35 100644 --- a/__tests__/DualAlertBridge_test.res +++ b/__tests__/DualAlertBridge_test.res @@ -90,6 +90,117 @@ let testCyberTriggerGuardSightNone = (): promise => { Promise.resolve(trigger == None) // Pure physical — no cyber trigger } +// =========================================================================== +// getOverallThreatLevel — converts DualAlert levels to HUD levels +// =========================================================================== + +let testOverallThreatLevelClear = (): promise => { + let state = DualAlert.makeAlertState() + // Both physical and cyber start at Clear + Promise.resolve(DualAlertBridge.getOverallThreatLevel(state) == HUD.Clear) +} + +let testOverallThreatLevelFromPhysical = (): promise => { + let state = DualAlert.makeAlertState() + // Raise physical to High via triggers (each ConfirmedVisual = +0.40) + // Need 3 triggers to reach Guarded (1.0+), then more to reach Elevated, then High + // Simpler: directly set the physical level + state.physical = DualAlert.High + // High physical => HUD.Alert + Promise.resolve(DualAlertBridge.getOverallThreatLevel(state) == HUD.Alert) +} + +let testOverallThreatLevelFromCyber = (): promise => { + let state = DualAlert.makeAlertState() + state.cyber = DualAlert.Elevated + // Elevated cyber => HUD.Caution + Promise.resolve(DualAlertBridge.getOverallThreatLevel(state) == HUD.Caution) +} + +let testOverallThreatLevelSevere = (): promise => { + let state = DualAlert.makeAlertState() + state.physical = DualAlert.Severe + // Severe => HUD.Danger + Promise.resolve(DualAlertBridge.getOverallThreatLevel(state) == HUD.Danger) +} + +let testOverallThreatLevelGuarded = (): promise => { + let state = DualAlert.makeAlertState() + state.cyber = DualAlert.Guarded + // Guarded => HUD.Noticed + Promise.resolve(DualAlertBridge.getOverallThreatLevel(state) == HUD.Noticed) +} + +let testOverallThreatLevelHigherWins = (): promise => { + let state = DualAlert.makeAlertState() + state.physical = DualAlert.Guarded + state.cyber = DualAlert.High + // Cyber is higher => HUD.Alert + Promise.resolve(DualAlertBridge.getOverallThreatLevel(state) == HUD.Alert) +} + +// =========================================================================== +// shouldDispatchAntiHackerGuards — Elevated+ cyber triggers dispatch +// =========================================================================== + +let testShouldDispatchAntiHackerClearNo = (): promise => { + let state = DualAlert.makeAlertState() + // Clear cyber => no dispatch + Promise.resolve(!DualAlertBridge.shouldDispatchAntiHackerGuards(state)) +} + +let testShouldDispatchAntiHackerGuardedNo = (): promise => { + let state = DualAlert.makeAlertState() + state.cyber = DualAlert.Guarded + // Guarded cyber => still no dispatch + Promise.resolve(!DualAlertBridge.shouldDispatchAntiHackerGuards(state)) +} + +let testShouldDispatchAntiHackerElevatedYes = (): promise => { + let state = DualAlert.makeAlertState() + state.cyber = DualAlert.Elevated + // Elevated cyber => dispatch + Promise.resolve(DualAlertBridge.shouldDispatchAntiHackerGuards(state)) +} + +let testShouldDispatchAntiHackerHighYes = (): promise => { + let state = DualAlert.makeAlertState() + state.cyber = DualAlert.High + // High cyber => dispatch + Promise.resolve(DualAlertBridge.shouldDispatchAntiHackerGuards(state)) +} + +// =========================================================================== +// shouldGuardsCheckTerminals — High+ physical triggers terminal checks +// =========================================================================== + +let testShouldGuardsCheckTerminalsClearNo = (): promise => { + let state = DualAlert.makeAlertState() + // Clear physical => no terminal checks + Promise.resolve(!DualAlertBridge.shouldGuardsCheckTerminals(state)) +} + +let testShouldGuardsCheckTerminalsElevatedNo = (): promise => { + let state = DualAlert.makeAlertState() + state.physical = DualAlert.Elevated + // Elevated physical => still not checking terminals + Promise.resolve(!DualAlertBridge.shouldGuardsCheckTerminals(state)) +} + +let testShouldGuardsCheckTerminalsHighYes = (): promise => { + let state = DualAlert.makeAlertState() + state.physical = DualAlert.High + // High physical => guards check terminals + Promise.resolve(DualAlertBridge.shouldGuardsCheckTerminals(state)) +} + +let testShouldGuardsCheckTerminalsSevereYes = (): promise => { + let state = DualAlert.makeAlertState() + state.physical = DualAlert.Severe + // Severe physical => guards check terminals + Promise.resolve(DualAlertBridge.shouldGuardsCheckTerminals(state)) +} + // =========================================================================== // Suite // =========================================================================== @@ -113,5 +224,19 @@ let suite: TestRunner.suite = { {name: "sourceToCyberTrigger: PortScan", run: testCyberTriggerPortScan}, {name: "sourceToCyberTrigger: CrackFailed => HackFailed", run: testCyberTriggerCrackFailed}, {name: "sourceToCyberTrigger: GuardSight => None", run: testCyberTriggerGuardSightNone}, + {name: "getOverallThreatLevel: Clear state => HUD.Clear", run: testOverallThreatLevelClear}, + {name: "getOverallThreatLevel: High physical => HUD.Alert", run: testOverallThreatLevelFromPhysical}, + {name: "getOverallThreatLevel: Elevated cyber => HUD.Caution", run: testOverallThreatLevelFromCyber}, + {name: "getOverallThreatLevel: Severe => HUD.Danger", run: testOverallThreatLevelSevere}, + {name: "getOverallThreatLevel: Guarded => HUD.Noticed", run: testOverallThreatLevelGuarded}, + {name: "getOverallThreatLevel: higher of two wins", run: testOverallThreatLevelHigherWins}, + {name: "shouldDispatchAntiHackerGuards: Clear => false", run: testShouldDispatchAntiHackerClearNo}, + {name: "shouldDispatchAntiHackerGuards: Guarded => false", run: testShouldDispatchAntiHackerGuardedNo}, + {name: "shouldDispatchAntiHackerGuards: Elevated => true", run: testShouldDispatchAntiHackerElevatedYes}, + {name: "shouldDispatchAntiHackerGuards: High => true", run: testShouldDispatchAntiHackerHighYes}, + {name: "shouldGuardsCheckTerminals: Clear => false", run: testShouldGuardsCheckTerminalsClearNo}, + {name: "shouldGuardsCheckTerminals: Elevated => false", run: testShouldGuardsCheckTerminalsElevatedNo}, + {name: "shouldGuardsCheckTerminals: High => true", run: testShouldGuardsCheckTerminalsHighYes}, + {name: "shouldGuardsCheckTerminals: Severe => true", run: testShouldGuardsCheckTerminalsSevereYes}, ], } diff --git a/deno.json b/deno.json index 47211055..820e90e6 100644 --- a/deno.json +++ b/deno.json @@ -24,7 +24,7 @@ } }, "imports": { - "@assetpack/core": "npm:@assetpack/core@1.7.0", + "@assetpack/core": "npm:@assetpack/core@1.8.0", "@rescript/core": "npm:@rescript/core@1.6.1", "@rescript/runtime": "npm:@rescript/runtime@12.2.0", "rescript": "npm:rescript@12.2.0", diff --git a/package.json b/package.json index c2f0027c..c582dc03 100644 --- a/package.json +++ b/package.json @@ -19,5 +19,11 @@ "overrides": { "minimatch": ">=10.2.4", "file-type": ">=21.3.3" + }, + "pnpm": { + "overrides": { + "minimatch": ">=10.2.4", + "file-type": ">=21.3.3" + } } } diff --git a/src/Main.res b/src/Main.res index 50a69830..0a054085 100644 --- a/src/Main.res +++ b/src/Main.res @@ -169,11 +169,21 @@ let _ = { function(err) { var msg = err && err.message ? err.message : String(err); if (err && err._1) msg += " | " + (err._1.message || JSON.stringify(err._1)); - document.body.innerHTML = '
' - + '

IDApTIK — Startup Error

' - + '

' + msg + '

' - + '

Check browser console (F12) for full stack trace.

' - + '
'; + while (document.body.firstChild) document.body.removeChild(document.body.firstChild); + var container = document.createElement("div"); + container.style.cssText = "background:#0a0a0a;color:#ff4444;font-family:monospace;padding:40px;white-space:pre-wrap;font-size:14px"; + var h1 = document.createElement("h1"); + h1.style.color = "#44ff88"; + h1.textContent = "IDApTIK \u2014 Startup Error"; + var p1 = document.createElement("p"); + p1.textContent = msg; + var p2 = document.createElement("p"); + p2.style.cssText = "color:#888;margin-top:20px"; + p2.textContent = "Check browser console (F12) for full stack trace."; + container.appendChild(h1); + container.appendChild(p1); + container.appendChild(p2); + document.body.appendChild(container); } `) showError(e) diff --git a/src/app/GameLoop.res b/src/app/GameLoop.res index 067a2a68..144f17c4 100644 --- a/src/app/GameLoop.res +++ b/src/app/GameLoop.res @@ -21,6 +21,14 @@ type gameState = { mutable guards: array, mutable dogs: array, mutable inventory: Inventory.t, + // Dual alert system (physical + cyber) — wired via DualAlertBridge + mutable dualAlert: DualAlert.alertState, + // Q's software deck (4-slot program loadout) + mutable qDeck: QPrograms.deck, + // Q's certification portfolio (gates program access) + mutable qCerts: QCertifications.portfolio, + // Jessica's equipment loadout (weapon + tool + consumable) + mutable jessicaLoadout: JessicaLoadout.loadout, // Companion mutable moletaire: option, // PBX distraction system @@ -70,6 +78,10 @@ let make = (~tier: Inventory.difficultyTier): gameState => { guards: [], dogs: [], inventory: inv, + dualAlert: DualAlert.makeAlertState(), + qDeck: QPrograms.makeDeck(), + qCerts: QCertifications.makePortfolio(), + jessicaLoadout: JessicaLoadout.defaultLoadout, moletaire: None, pbx: None, worldItems: [], @@ -850,13 +862,19 @@ let update = ( | None => () } - // 2. Update detection system (natural decay) + // 2. Update detection system (natural decay) and dual alert cooldown DetectionSystem.update(state.detection, ~dt) - let currentAlert = DetectionSystem.getAlertLevel(state.detection) + DualAlertBridge.updateBothSystems(state.detection, state.dualAlert, ~dt) + // Use the dual alert system for overall threat level + let currentAlert = DualAlertBridge.getOverallThreatLevel(state.dualAlert) let currentAlertInt = HUD.alertToInt(currentAlert) if currentAlertInt > state.maxAlertReached { state.maxAlertReached = currentAlertInt } + // Dispatch anti-hacker guards when cyber alert is elevated+ + if DualAlertBridge.shouldDispatchAntiHackerGuards(state.dualAlert) { + SecurityAI.requestAntiHackerDispatch(state.securityAI) + } // 2b. Update VM Network mesh (Tier 5) if FeaturePacks.isInvertibleProgrammingEnabled() { diff --git a/src/app/devices/DeviceView.res b/src/app/devices/DeviceView.res index 50c64cdc..6cadcfb5 100644 --- a/src/app/devices/DeviceView.res +++ b/src/app/devices/DeviceView.res @@ -2,6 +2,10 @@ // Device View - Per-Device Filesystem Trees // Each device has a filesystem tree where files reference global content IDs // Paths, names, permissions are device-specific +// +// SECURITY NOTE: All passwords and credentials in this file are FICTIONAL +// game content — in-game data that players discover by hacking devices. +// They are NOT real secrets. See: PANIC-ATTACK-ANALYSIS-SUMMARY.md // ============================================ // Filesystem Tree Structure diff --git a/src/app/devices/GlobalNetworkData.res b/src/app/devices/GlobalNetworkData.res index 9fb355f6..3f697c09 100644 --- a/src/app/devices/GlobalNetworkData.res +++ b/src/app/devices/GlobalNetworkData.res @@ -2,6 +2,11 @@ // Global Network Data - Content-Addressable Storage // File CONTENT stored once globally (hash-based IDs for automatic deduplication) // File METADATA (paths, names, permissions) stored per-device in DeviceView +// +// SECURITY NOTE: All passwords, API keys, and credentials below are FICTIONAL +// game content — data that players discover during gameplay by hacking in-game +// devices. They are NOT real secrets and do not grant access to any system. +// See: PANIC-ATTACK-ANALYSIS-SUMMARY.md (false positives: game content) // ============================================ // Content-Addressable Storage diff --git a/src/app/enemies/SecurityAI.res b/src/app/enemies/SecurityAI.res index a035010e..3aa805b7 100644 --- a/src/app/enemies/SecurityAI.res +++ b/src/app/enemies/SecurityAI.res @@ -308,6 +308,15 @@ let isActive = (ai: t): bool => { ai.phase != Dormant } +// Request anti-hacker guard dispatch from the DualAlert cyber alert system. +// Forces SENTRY into active scanning if not already active. +let requestAntiHackerDispatch = (ai: t): unit => { + if ai.phase == Dormant { + ai.phase = Scanning + log(ai, ~message="Cyber alert elevated — activating anti-hacker response") + } +} + let reset = (ai: t): unit => { ai.phase = Dormant ai.scanTimer = 0.0 diff --git a/src/app/screens/hub/MainHubScreen.res b/src/app/screens/hub/MainHubScreen.res index 0dc4f4dd..2264c119 100644 --- a/src/app/screens/hub/MainHubScreen.res +++ b/src/app/screens/hub/MainHubScreen.res @@ -60,7 +60,7 @@ let closeApp: unit => unit = %raw(` } try { window.close(); } catch(e) {} // If window.close was blocked (browser security), show blank page - try { document.body.innerHTML = '
Thanks for playing IDApTIK
'; } catch(e) {} + try { while (document.body.firstChild) document.body.removeChild(document.body.firstChild); var d = document.createElement("div"); d.style.cssText = "background:#0a0a0a;color:#44ff88;font-family:monospace;height:100vh;display:flex;align-items:center;justify-content:center;font-size:24px"; d.textContent = "Thanks for playing IDApTIK"; document.body.appendChild(d); } catch(e) {} } `) diff --git a/vite.config.js b/vite.config.js index c3b7c3dd..5acded0d 100644 --- a/vite.config.js +++ b/vite.config.js @@ -2,10 +2,6 @@ import { defineConfig } from "vite"; import { existsSync } from "node:fs"; import { dirname, basename, join } from "node:path"; -// NOTE: assetpackPlugin import removed — it was causing Vite restart loops. -// The import itself made Vite track scripts/ as a config dependency. -// Re-add when needed: import { assetpackPlugin } from "./scripts/assetpack-vite-plugin"; - // ReScript emits imports with uncapitalized module names (e.g. "./app/getEngine") // but the actual files are PascalCase (e.g. "GetEngine.res.mjs"). On case-sensitive // filesystems (Linux) these don't resolve. This plugin tries the PascalCase variant. @@ -30,12 +26,36 @@ function rescriptResolvePlugin() { }; } +// Lazy-loaded AssetPack plugin. The plugin is loaded dynamically at buildStart +// to avoid Vite tracking scripts/ as a config dependency (which caused restart +// loops). Only runs when ASSETPACK=1 env var is set, so normal dev mode is +// unaffected. +function assetpackPluginLazy() { + let ap; + let mode; + return { + name: "vite-plugin-assetpack-lazy", + configResolved(resolvedConfig) { + mode = resolvedConfig.command; + }, + buildStart: async () => { + if (!process.env.ASSETPACK) return; + const { assetpackPlugin } = await import("./scripts/assetpack-vite-plugin.js"); + const inner = assetpackPlugin(); + if (inner.configResolved) inner.configResolved({ command: mode }); + if (inner.buildStart) await inner.buildStart(); + ap = inner; + }, + buildEnd: async () => { + if (ap && ap.buildEnd) await ap.buildEnd(); + ap = undefined; + }, + }; +} + // https://vite.dev/config/ export default defineConfig({ - plugins: [rescriptResolvePlugin()], - // NOTE: assetpackPlugin() disabled — was causing Vite to hang on serve. - // Assets are already built in public/assets/ with manifest at src/manifest.json. - // Re-enable when raw-assets need reprocessing: assetpackPlugin() + plugins: [rescriptResolvePlugin(), assetpackPluginLazy()], server: { port: 8080, strictPort: true,