A SOC shift simulator for training analysts. One hour on the floor, an alert every two to three minutes, eleven initial-access scenarios spanning machines and identities, plus six short-form alerts so that no two shifts ever look alike. The student investigates in a fictional SIEM and EDR, decides, isolates, disables, or notifies the client, writes a short report at every closure, and gets a detailed score at the end of the shift.
The simulated analyst is the one who responds, not the one who escalates: there is no higher tier to hand the case off to. An escalated alert would be a dropped one.
Static application: three files opened in a browser are enough. No dependencies, no server, no network connection, no data transmitted.
🔗 Live demo: https://spellskite-coding.github.io/Open-SOC/
Companion app to Open-Forensics: where Open-SOC trains decision-making under pressure, Open-Forensics trains methodical reconstruction.
git clone <your-repo> Open-SOC
cd Open-SOCThen, either:
- open
index.htmldirectly in your browser (double-click), or - try it online right away: https://spellskite-coding.github.io/Open-SOC/, or
- serve the folder locally, recommended for classroom use:
python3 -m http.server 8000
# then http://localhost:8000/The test suite opens the same way: tests/run.html. It runs in the browser and displays
ALL TESTS PASS or the list of failures. No external tooling, no npm install.
The simulator targets precisely the response analyst's craft on initial access:
- Triage under pressure. Alerts land every two to three minutes, regardless of the chosen difficulty or shift length: the pace isn't a setting, it's the constraint of the job. Difficulty doesn't change the cadence, it changes the subtlety of the cases. Each alert's countdown starts when it's received, not when it's opened.
- Correlate. Scenarios arrive as several alerts interleaved with those of other scenarios and buried in realistic background noise. No case ID is provided: matching happens on entities (host, account, address).
- Investigate with internal sources only. SIEM, EDR, identity directory, and knowledge base. No OSINT, no external CTI: everything needed to decide is inside the app, including the internal intelligence sheets.
- Decide and remediate. Isolate the right host, disable and reset the right account, revoke MFA methods when identity persistence has been established.
- Escalate to the client when that's the correct response. Some situations admit neither inaction nor isolation: a brute force against a domain controller isn't addressed by cutting off the directory for the entire company, and perimeter blocking isn't the SOC's call to make. The Client Communication button is then the expected action, and it's scored as such. Playbook PB-005, inside the app, gives the two criteria: remediation doesn't belong to the SOC, or containment would have a major business impact.
- Know when to do nothing. Four cases out of seventeen are legitimate activity that looks like an attack (an authorized scan, a declared relocation, a contracted penetration test). Isolating a security scanner or disabling an executive's account on the strength of an unusual country is scored as a production error.
- Report. Every closure requires a verdict, a severity, an ATT&CK technique, and an
80-to-700-character report citing at least one factual element from the case. The
technique palette covers the full range observable from a SOC — 140 techniques
across 14 tactics, from reconnaissance to impact, including lateral movement,
collection, and exfiltration. It's grouped by tactic and filterable by keyboard
(
lsass,rdp,lateral,T1021…), because a list this size would be unusable under pressure otherwise. Picking the wrong tactic is part of the exercise: the palette isn't limited to the techniques actually used by the shipped scenarios.
Eleven complete scenarios ranked by difficulty (3 easy, 4 medium, 4 hard), each made of two or three alerts to correlate, including three benign cases spread across the levels. Families covered: phishing and user execution, password spraying, MFA fatigue and abusive OAuth consent, fake browser update, RDP brute force against an exposed service, webshell on an exposed application, trojanized installer and LSASS dumping, brute force against an exposed domain controller, authorized vulnerability scan, declared business travel, authorized internal penetration test.
Six short-form alerts are added on top: single-alert cases (antivirus test file outside the expected window, out-of-scope service account, mass email send, declared PsExec deployment, execution blocked from a USB drive, deletion of restore points). They fill the shift's remaining capacity, which removes idle stretches and means a replayed shift never quite looks like the previous one.
Two composition modes:
- Random draw — the shift pulls from the library to fill the chosen duration (15, 30, 60, or 90 minutes) according to the difficulty filter, with at least one benign case guaranteed. A 90-minute shift plays all eleven complete scenarios.
- Manual selection — you check exactly which cases to play, and the shift duration adapts to chain them at the floor's pace. Titles stay hidden by default to preserve the exercise; a checkbox reveals them to prepare a session.
A preview shown under the settings announces, before clocking in, the number of scenarios, alerts, the effective duration, and the actual gap between two alerts.
Instructors: ground truth (verdict, correct actions, debrief) lives in
js/data/scenarios.jsandjs/data/injects.js. Both files carry a warning at the top. Ask students not to open them before playing — it's the only way to cheat, and it's explicit.
Each closed alert is worth up to 12 points: verdict (4), severity (2), technique (2), report quality (2), meeting the deadline (2). Each scenario is additionally worth its remediation points: isolation (6), account disabling (5), client communication (5), password reset (3), MFA revocation (3), plus a containment-speed bonus (up to 4). A benign case correctly identified and left untouched earns 8 restraint points — but doing nothing at all earns nothing: restraint is only rewarded if the case was correctly identified.
Penalties target the damage caused:
| Fault | Cost |
|---|---|
| Explicitly forbidden action (isolating a domain controller, for example) | −8 |
| Action on a host or account unrelated to any case in the shift | −6 (−3 if undone before the end) |
| Unnecessary action on an entity from the case | −1.5, capped at −4.5 per scenario |
| Client communication about activity the client itself declared | −1.5 |
| Client communication about an entity outside the case | −2 |
Notifying the client of a real incident is never penalized, even when it wasn't required. And an action required by one case is never held against you under another case that happens to mention the same host: a faulty action is only charged once, at the highest rate it deserves.
The final debrief gives a score out of 100, a grade from A to E, five axes (detection, containment, accuracy, responsiveness, reporting), the real story of each scenario played, actions taken, missed, and unnecessary, the case's pitfalls, an alert-by-alert breakdown, the action log, and an exportable JSON report.
Results from the last twelve shifts are kept in the browser's localStorage, solely to
display progress. No written report is stored there.
The application is meant to be audited: it's a security-training tool, and it would be poor form for it to set a bad example.
- No dependencies. Zero third-party libraries, zero remote resources, zero external fonts. The whole repository can be read in one sitting.
- No network.
connect-src 'none': the page cannot issue a request, even if code were added to try. It works in an isolated environment. - Strict Content Security Policy, declared in
index.html:default-src 'none'; scripts and styles restricted to the origin; no inline script or style;object-src 'none';base-uri 'none';form-action 'none'. - No HTML injection possible. The DOM is built exclusively with
createElementandtextContent. There is not a single occurrence ofinnerHTML,outerHTML,insertAdjacentHTML,document.write,eval, ornew Functionin the code. Reports written by the analyst are rendered as plain text. - Locked-down element factory.
util.el()refuses by construction anyon*attribute, as well asstyle,href,src,srcdoc,action,formaction,ping, andbackground, none of which are used anywhere. - Local storage validated on read. History entries are filtered, typed, and bounded in length and count before being displayed; unavailable or corrupted storage does not prevent the app from working.
- Frozen data. The inventory and scenarios are recursively frozen with
Object.freezeat startup.
Quick check:
grep -rnE "innerHTML|outerHTML|insertAdjacentHTML|document\.write|eval\(|new Function" js/
# should return nothingNo organization, person, machine, or address in this repository exists. IP addresses are
drawn from the ranges reserved for documentation (RFC 5737: 192.0.2.0/24,
198.51.100.0/24, 203.0.113.0/24), and domains use the reserved TLDs .test,
.invalid, and .example (RFC 2606). No indicator of compromise from this simulator can
therefore point to a real resource, and the exercise logs won't pollute a threat
intelligence platform if a student accidentally copies them out.
index.html Single page, Content Security Policy
css/opensoc.css Single stylesheet, dark theme
js/util.js Safe DOM, deterministic randomness, formatting
js/data/org.js Fictional organization: fleet, directory, knowledge base
js/data/scenarios.js The eleven complete scenarios and their ground truth (SPOILERS)
js/data/injects.js The six short-form alerts and their ground truth (SPOILERS)
js/engine.js Shift generation, clock, remediation, scoring (no DOM)
js/ui.js Rendering and interactions
js/app.js Bootstrapping and safety net
tests/run.html Test suite runnable in the browser
tests/tests.js 85 assertions: data, engine, pacing, scoring, timing leaks
The engine never touches the DOM: that's what lets the tests simulate complete shifts — a perfect shift, a passive shift, a reckless shift — and verify that scoring separates them correctly.
The code is written in ES5 JavaScript: no modern syntax (no let, const, arrow
functions, template literals, or modules), no promises, no network calls. The only
slightly recent APIs are guarded: Math.imul has a built-in fallback,
crypto.getRandomValues and localStorage are wrapped in try/catch with fallback
behavior, globalThis is tested before use, and the escape key recognizes both Escape
and Esc as well as key code 27.
On the stylesheet side, :focus-visible is preceded by a :focus rule that serves as a
fallback, and the score gauge keeps a solid background if conic-gradient isn't
recognized. The application therefore works on Firefox, Chrome, Edge, Safari, and their
mobile counterparts, both on desktop and over file://.
This repository was validated under headless Firefox (rendering every view and running the test suite). If you notice a defect on another engine, open an issue with the browser and its version.
Add an object to js/data/scenarios.js — or to js/data/injects.js if it's a short
single-alert case meant to be mixed in with the others. The contract is described at the
top of the file; in short: alerts[] with their triggering logs, hosts{} and
accounts{} for telemetry revealed over time, involved for the case's entities, and
truth for the verdict, the required and tolerated actions, the narrative, and the
pitfalls. at offsets are in minutes relative to the scenario's start; a negative value is
an earlier event already present in the logs.
The technique palette in js/data/org.js is deliberately much broader than what the
scenarios actually use: a new case can almost always be written without touching it. If
you do need to extend it, follow the { id, tactic, name } format — the tactic must exist
in ORG.tactics and the label must start with the ID followed by a colon, both of which
the tests check.
Then open tests/run.html: the tests automatically verify that the entities cited exist
in the inventory, that required actions target entities from the case, that the expected
techniques appear in the closure selector, and that the two-to-three-minute pace holds
across every combination of duration and difficulty.
MIT — see LICENSE. Free to use in training centers, schools, or internally.