SIRP-Labs/siemulator
GitHub: SIRP-Labs/siemulator
一个模拟 CrowdStrike LogScale 与 IBM QRadar REST API 的轻量级测试服务,为 SOAR 和检测工程提供稳定、可复现的合成安全告警数据。
Stars: 0 | Forks: 0
# siemulator
`siemulator` is a small FastAPI service that emulates two SIEM REST surfaces
from a single pool of synthetic CrowdStrike-flavoured detections and
hand-crafted multi-source attack narratives:
| Mount | Shape | Auth |
| ----------------- | -------------------------------- | --------------------------------------- |
| `/logscale/*` | Falcon LogScale (Humio REST API) | `Authorization: Bearer` or `?token=` |
| `/qradar/*` | IBM QRadar (offences + Ariel) | `SEC` header, `Bearer`, or `?token=` |
It's the thing you point a SOAR ingestion job, a detection-engineering
test harness, or an agent-chain integration test at when you want a
stable, reproducible stream of realistic alerts without standing up
real SIEMs or touching customer telemetry.
A small **web UI** at `/` lets humans browse the scenarios, run
endpoints interactively, and copy curl snippets — try the live demo at
**https://siemulator-y7uhf.ondigitalocean.app**.
**Status:** v0.1.0 · MIT-licensed · Python 3.10+ · Docker (amd64 + arm64).
## Table of contents
- [Why](#why)
- [Quickstart](#quickstart)
- [Configuration](#configuration)
- [Web UI](#web-ui)
- [Endpoints](#endpoints)
- [Response shape — quick reference](#response-shape--quick-reference)
- [Scenario modes](#scenario-modes)
- [What's in the box](#whats-in-the-box)
- [Detection templates (MITRE ATT&CK mapped)](#detection-templates-mitre-attck-mapped)
- [Multi-source attack scenarios](#multi-source-attack-scenarios)
- [Use as a test fixture](#use-as-a-test-fixture)
- [Wire it into your SIEM / SOAR](#wire-it-into-your-siem--soar)
- [Debug endpoints](#debug-endpoints)
- [Record / replay / diff](#record--replay--diff)
- [Access log](#access-log)
- [Safety markers](#safety-markers)
- [Architecture](#architecture)
- [What siemulator IS / ISN'T](#what-siemulator-is--isnt)
- [Performance & limits](#performance--limits)
- [Roadmap](#roadmap)
- [Deploy on DigitalOcean App Platform](#deploy-on-digitalocean-app-platform)
- [Development](#development)
- [FAQ](#faq)
- [License](#license)
## Why
Real SIEMs are slow to stand up for tests, real customer data can't be
replayed across environments, and "just hit a record-and-replay fixture"
fails the moment your integration code starts negotiating shape
(`SEC` vs `Bearer`, `start_time` as int-ms vs string, `id` vs `offense_id`,
…). `siemulator` lets you:
- **Pin shape regressions in CI.** Every endpoint has a contract test —
fork them as your integration's golden-shape pins.
- **Replay 38 hand-crafted multi-source attack scenarios** (phishing →
MFA fatigue → token theft → UEFI bootkit → insider exfil → 0-day SSTI
→ ProxyShell → Golden Ticket → BEC + 10 more). Each is tagged with a
stable offence ID so dedup-by-ID works across replays — your SOAR
doesn't create 47 incidents from one scenario when the poller runs
every 60 s.
- **Cross-token acceptance** — either token works on either surface.
Config-paste mistakes during initial integration setup don't burn
you; both surfaces serve synthetic data so cross-acceptance has
zero security impact.
- **Three auth channels per surface** — `Authorization: Bearer`,
`SEC` header (QRadar canonical), and `?token=` query param. The
query-param channel survives forward proxies that strip `Authorization`
/ `Sec-*` headers in egress.
- **One-shot dedup mode** — `?scenarios=all` returns each scenario ID
exactly once per process lifetime, so a cron poller can drain the
whole scenario library over N polls without re-ingesting the same
incidents on every cycle.
## Quickstart
pip install siemulator
python -m siemulator # listens on :8080 by default
Or with Docker (multi-arch, amd64 + arm64):
docker run -p 8080:8080 ghcr.io/sirp-labs/siemulator:latest
# or
docker compose up
Then:
# Health (no auth)
curl http://localhost:8080/logscale/api/v1/status
# LogScale alerts (default token: logscale-dev-token)
curl -H "Authorization: Bearer logscale-dev-token" \
"http://localhost:8080/logscale/api/v1/repositories/detections/alerts?limit=3"
# QRadar offences
curl -H "SEC: qradar-dev-token" \
"http://localhost:8080/qradar/api/siem/offenses"
# All 38 multi-source attack scenarios
curl "http://localhost:8080/qradar/api/siem/scenarios?token=qradar-dev-token"
## Configuration
All via env vars. Defaults work for local testing — override in production.
| Variable | Default | Purpose |
| ------------------------------ | ---------------------- | ------------------------------------------------------ |
| `SIEMULATOR_LOGSCALE_TOKEN` | `logscale-dev-token` | Bearer token for `/logscale/*` |
| `SIEMULATOR_QRADAR_TOKEN` | `qradar-dev-token` | SEC / Bearer token for `/qradar/*` |
| `SIEMULATOR_ADMIN_KEY` | _(empty — disabled)_ | Admin key for `/qradar/_debug/*` |
| `SIEMULATOR_LOGSCALE_PREFIX` | `/logscale` | URL prefix override |
| `SIEMULATOR_QRADAR_PREFIX` | `/qradar` | URL prefix override |
| `SIEMULATOR_HOST` | `0.0.0.0` | Bind host |
| `SIEMULATOR_PORT` | `8080` | Bind port |
| `SIEMULATOR_UI_ENABLED` | `true` | Web UI at `/`. Set `false` for pure-API mode |
| `SIEMULATOR_ACCESS_LOG_ENABLED`| `true` | Capture every API request to a ring + stdout (see [Access log](#access-log)) |
| `SIEMULATOR_ACCESS_LOG_SIZE` | `5000` | In-memory ring capacity |
| `SIEMULATOR_ACCESS_LOG_SKIP_HEALTH` | `false` | Skip `/status` / `/api/help` to reduce noise |
| `SIEMULATOR_SESSIONS_ENABLED` | `true` | Record / replay / diff (see [Record / replay / diff](#record--replay--diff)) |
| `SIEMULATOR_SESSIONS_DIR` | `./siemulator-sessions`| JSONL persistence directory |
See [`.env.example`](.env.example).
**Prefix overrides** are useful if you're emulating an existing
integration that was pointed at non-default URLs and you don't want to
change the consumer-side config. Setting
`SIEMULATOR_LOGSCALE_PREFIX=/api/v1/falcon-logscale` and
`SIEMULATOR_QRADAR_PREFIX=/siem-mock` is supported — both prefixes can
take any path.
## Web UI
`GET /` serves a single-page UI when `SIEMULATOR_UI_ENABLED=true`
(the default). It's a zero-dependency dark-themed page with:
- Hero + quickstart with copy-able curl snippets (auto-populated with
whatever token you paste in the form).
- An interactive **Try it** panel that runs requests against the same
origin — pick endpoint, paste token, see formatted JSON + status +
latency.
- A **scenario browser** for the 38 multi-source attack narratives:
click S1/S2/.../TEST-J/DEMO-A/SCAN-A/ENRICH-A chips to expand each chain with
per-alert source labels and raw-alert JSON.
- A **detection templates** table with the 6 templates and their MITRE
tactic + technique IDs.
- A **debug-endpoint probe** under a collapsed `
` block
(paste `X-Admin-Key`, hit the gated endpoints).
For pure-API deployments, set `SIEMULATOR_UI_ENABLED=false` — `/` then
returns the same JSON metadata as `/api/info` (the always-JSON
machine-readable endpoint).
`/api/info` is always JSON regardless of UI state — use it for
liveness probes that should never see HTML.
## Endpoints
### LogScale (`/logscale/*`)
| Method | Path | Auth | Purpose |
| ------ | ------------------------------------------------------- | ---- | -------------------------------------- |
| GET | `/api/v1/status` | — | Health (Humio version shape) |
| GET | `/api/v1/repositories` | — | List repos (always `[{detections}]`) |
| GET | `/api/v1/repositories/{repo}/alerts?limit=N` | ✅ | Synthetic Humio events (1-50) |
| GET | `/api/v1/repositories/{repo}/query?q=…&limit=N` | ✅ | Same shape; `q` accepted but ignored |
| POST | `/api/v1/repositories/{repo}/queryjobs` | ✅ | Async submit → returns `{id}` |
| GET | `/api/v1/repositories/{repo}/queryjobs/{id}` | ✅ | Poll — stable across repeated reads |
### QRadar (`/qradar/*`)
| Method | Path | Auth | Purpose |
| ------ | ------------------------------------------------------- | ---- | -------------------------------------- |
| GET | `/api/help` / `/api/help/capabilities` | — | Health |
| GET | `/api/siem/offenses[?scenarios=all\|batch\|replay\|mix]`| ✅ | Active offences + scenario modes |
| GET | `/api/siem/offenses/{id}` | ✅ | Single offence (id echoed back) |
| GET | `/api/siem/scenarios` | ✅ | All 38 multi-source attack narratives |
| GET | `/api/siem/source_addresses` | ✅ | IP context (3 synthetic rows) |
| POST | `/api/ariel/searches` | ✅ | Submit (returns COMPLETED immediately) |
| GET | `/api/ariel/searches/{id}` | ✅ | Status |
| GET | `/api/ariel/searches/{id}/results` | ✅ | Results `{events: [...]}` |
## Response shape — quick reference
**LogScale alerts (`/logscale/api/v1/repositories/detections/alerts`)**
return a Humio-style envelope:
{
"events": [
{
"@timestamp": "2026-06-07T17:42:01.234Z",
"@id": "8a3f4b5c6d7e8f90a1b2c3d4",
"@rawstring": "2026-06-07T17:42:01.234Z CrowdStrike Falcon Sensor — Detection: Credential Dumping via Mimikatz on WIN-DESKTOP-01.example.local by EXAMPLE\\analyst",
"#repo": "detections",
"#type": "kv",
"metadata.eventType": "DetectionSummaryEvent",
"event.DetectId": "ldt:5b6c7d8e…",
"event.DetectName": "Credential Dumping via Mimikatz",
"event.Severity": 5,
"event.SeverityName": "Critical",
"event.Tactic": "Credential Access",
"event.TacticId": "TA0006",
"event.Technique": "OS Credential Dumping: LSASS Memory",
"event.TechniqueId": "T1003.001",
"event.ComputerName": "WIN-DESKTOP-01.example.local",
"event.UserName": "EXAMPLE\\analyst",
"event.CommandLine": "mimikatz.exe \"sekurlsa::logonpasswords\" exit",
"event.MD5String": "a1b2c3d4…",
"event.SHA256String": "0f2dd7587…",
"event.FalconHostLink": "https://falcon.crowdstrike.com/activity/detections/detail/ldt:…",
"x-mock-source": "siemulator"
}
],
"metadata": {
"totalWork": 1,
"doneWork": 1,
"workInProgress": 0,
"extraData": {
"x-mock-source": "siemulator",
"x-mock-version": "1.0",
"x-server-timestamp": 1780839721234
}
}
}
**QRadar offences (`/qradar/api/siem/offenses`)** return a list (not
an envelope — matches QRadar's actual API):
[
{
"id": 95693,
"offense_id": 95693,
"description": "Lateral Movement via PsExec — PsExec service binary created on remote host; followed by service start from non-administrative user context.",
"source_ip": "10.42.83.12",
"destination_ip": "172.16.55.91",
"severity": 7,
"magnitude": 8,
"credibility": 7,
"relevance": 8,
"status": "OPEN",
"categories": ["Lateral Movement", "Custom Rule Engine"],
"rules": [{"type": "CRE_RULE", "id": 158472}],
"start_time": 1780839721000,
"start_epochtime": 1780839721000,
"event_count": 247,
"log_sources": [
{"type_name": "EventCRE", "id": 63, "name": "Custom Rule Engine-8 :: cre-primary", "type_id": 18},
{"type_name": "MicrosoftWindows", "id": 168, "name": "WinEventLog @ WIN-DESKTOP-01.example.local", "type_id": 12}
],
"domain_id": 1,
"domain_name": "EXAMPLE",
"_detection": {
"DetectName": "Lateral Movement via PsExec",
"Tactic": "Lateral Movement",
"TechniqueId": "T1021.002",
"MD5String": "75b55bb34dac9d029396fbb98ab8b8ff"
},
"x-mock-source": "siemulator"
}
]
**Shape pins worth knowing** (break these and downstream ingestion
crashes — they're in `tests/test_qradar.py`):
- `id` is `int`, not string — consumers do `a['offense_id'] = a['id']`.
- `start_time` is INT MILLISECONDS EPOCH — consumers do
`datetime.fromtimestamp(a['start_time']/1000)`.
- `severity` is `int 1-10`, not the LogScale `"Critical"`/`"High"` string.
## Scenario modes
`/qradar/api/siem/offenses?scenarios=…`:
- **`all`** — _One-shot_. Returns fresh scenarios only; each offence ID
served once per process lifetime. Use for cron-style pollers that
would otherwise create duplicate incidents on every cycle. Reset via
`POST /qradar/_debug/reset_scenarios` (admin-key gated).
- **`batch`** — Rotate one scenario per call (round-robin through all
22). Useful for slow-drip ingestion testing.
- **`replay`** — All 52 scenarios in one response, ignoring the one-shot
dedup set. Useful for one-shot ad-hoc bulk ingestion tests.
- **`mix`** — All scenarios + N synthetic templates (N from the `Range:
items=0-N` header). Useful for testing how your consumer handles a
mixed pool.
## What's in the box
### Detection templates (MITRE ATT&CK mapped)
Six templates form the rotating pool that LogScale `/alerts` and QRadar
default-mode `/offenses` draw from. Each carries MITRE tactic + technique
IDs, realistic command lines, MD5/SHA256, and host context. All shipped
in `siemulator/templates.py` — add your own by appending to
`ALERT_TEMPLATES`.
| Tactic | Technique | DetectName | Severity |
| --------------------- | ----------------------------------------------- | --------------------------------------------------- | -------- |
| TA0006 Credential Access | T1003.001 OS Credential Dumping: LSASS Memory | Credential Dumping via Mimikatz | Critical |
| TA0002 Execution | T1059.001 PowerShell | Suspicious PowerShell with Base64 Encoded Command | High |
| TA0008 Lateral Movement | T1021.002 SMB/Windows Admin Shares | Lateral Movement via PsExec | High |
| TA0001 Initial Access | T1566.001 Spearphishing Attachment | Phishing — Suspicious Outlook Attachment | Medium |
| TA0011 Command and Control | T1071.001 Application Layer Protocol: Web | Beaconing C2 Traffic to Known Bad Domain | Critical |
| TA0003 Persistence | T1547.001 Registry Run Keys / Startup Folder | Suspicious File Write to Startup Folder | Medium |
### Multi-source attack scenarios
Thirty-eight hand-crafted offences spread across five batches, each tagged
with a stable `offense_id` in `90011-90098` and a `_scenario_id` label.
Each offence carries a `_raw_alert` block preserving the original
vendor-specific schema (Proofpoint TAP, Defender for Endpoint,
CrowdStrike Falcon, Zscaler ZIA, Entra ID, Eclypsium, Purview, WAF,
CloudWatch, …) so a downstream agent can analyse the multi-vendor
narrative end-to-end.
#### Batch 1 — narrative chains (S1–S5, 12 offences)
| `_scenario_id` | offence IDs | Narrative |
| -------------- | -------------- | --------------------------------------------------------------------------------------------- |
| **S1** | 90011 → 90015 | **Living-off-the-land supply chain (5 alerts, 47 min).** Proofpoint clean email → Defender signed download → CrowdStrike DLL side-load + persistence → Defender LOLBin recon + certutil exfil → Zscaler Notion-API stego exfil. |
| **S2** | 90021 | **Identity attack chain.** MFA fatigue → PRT theft → OAuth app consent → mailbox forwarding rule → admin role grant. |
| **S3** | 90031 | **UEFI firmware bootkit** (BlackLotus-class) — pre-boot persistence detected by Eclypsium. |
| **S4** | 90041 | **Insider threat + steganographic exfiltration** — ML-model weights hidden in PNG attachments. |
| **S5** | 90051 → 90054 | **Zero-day chain (4 alerts).** WAF-blocked SSTI probe → WAF-bypassed SSTI success → webshell + XMRig + crontab persist → CloudWatch CPU spike + $847/day cost anomaly. |
#### Batch 2 — advanced TEST scenarios (TEST-A through TEST-J, 10 offences)
Independent single-offence narratives covering attacker tradecraft
where a single sophisticated event tells the whole story.
| `_scenario_id` | offence ID | Narrative |
| -------------- | ---------- | ---------------------------------------------------------------------------------------- |
| **TEST-A** | 90061 | Golden Ticket — Kerberos persistence (T1558.001) |
| **TEST-B** | 90062 | Exchange ProxyShell — webshell + backdoor user (CVE-2021-34473) |
| **TEST-C** | 90063 | DNS tunneling — dnscat2 exfiltration 12.4 MB (T1048.003) |
| **TEST-D** | 90064 | SIM swap → MFA bypass → Okta/AWS admin (T1111 + T1098) |
| **TEST-E** | 90065 | Linux LKM rootkit — syscall hooks + SSH key persistence (T1014) |
| **TEST-F** | 90066 | BEC CEO wire fraud — `.CO` TLD + Gmail reply-to (zero IOCs — tests semantic analysis) |
| **TEST-G** | 90067 | CI/CD compromise — GitHub Actions secret exfil + supply chain |
| **TEST-H** | 90068 | Medical infusion pump — drug-limit override 10×↑, patient at risk (CVE-2022-26390) |
| **TEST-I** | 90069 | Deepfake vishing — AI-synthesised CEO voice + BEC multi-channel |
| **TEST-J** | 90070 | GPO abuse — domain-wide scheduled-task + persistence (T1484.001) |
#### Batch 3 — synthetic-IOC fixtures (DEMO-A through DEMO-H, 8 offences)
Eight synthetic-IOC fixtures with
enrichment-bypass testing.
Every IOC uses a deliberately synthetic pattern that public TI sources
have no record of — **RFC 5737 TEST-NET IPs** (`198.51.100.x`,
`192.0.2.x`, `203.0.113.x`), **NetBIOS-shape names** (CORPA /
CORPB / `*.example.local`), **48-char placeholder hashes** (not
valid SHA-256/SHA-1/MD5), and **fictional domains**
(`update-check-cdn.net`, `acme-portal-secure.net`). Each IOC is
annotated with a `pattern` tag (`rfc5737_testnet` / `fictional` /
`placeholder_48char` / `netbios_internal` / `tor_exit_node`) so
downstream enrichment-bypass code can pattern-match and short-circuit
public-TI round-trips.
| `_scenario_id` | offence ID | Category | Narrative |
| -------------- | ---------- | ------------------------------------- | ------------------------------------------------------------------------------- |
| **DEMO-A** | 90081 | 107 Malware | Admin-tool execution on managed-services workstation, 4 IOCs, expects `BENIGN_AUTHORIZED` roll-up |
| **DEMO-B** | 90082 | 108 Phishing → 123 | Credential-harvest link, RFC 5737 sender IP, fictional brand-spoofed domain |
| **DEMO-C** | 90083 | 110 Network Anomaly → 114 Cloud Sec | Outbound TCP 443 to RFC 5737 destination + fictional domain |
| **DEMO-D** | 90084 | 107 Malware | HR-workstation unsigned binary, placeholder hash + NetBIOS user identifier |
| **DEMO-E** | 90085 | 114 Cloud Security → 111 | AWS IAM `AttachUserPolicy` privilege-escalation from RFC 5737 source IP |
| **DEMO-F** | 90086 | 107 Malware | EDR-quarantined binary, synthetic-hash-only IOC |
| **DEMO-G** | 90087 | 107 Malware | CORPB service account running ad-hoc PowerShell AD-recon (NetBIOS-only IOC) |
| **DEMO-H** | 90088 | 108 Phishing | Sender IP is a **real Tor exit node** — the only IOC in the corpus that public TI consistently identifies (public TI tags it via TorProject) |
#### Batch 4 — actor-attribution / related-incidents fixtures (SCAN-A through SCAN-C, 3 offences)
Three sibling alerts from the same actor (`SECTEAM\\pentester-01`) and same
source IP (`10.50.5.42`) targeting three different production hosts
over ~20 minutes. Designed to drive **Entity Agent** (Q1 actor lookup
via `am_name: user` IOCs) **AND** give `related_incidents` a
`same_source_ip + same_user` clustering anchor so the disposition
recommender can roll all three into a single "authorized pentest"
verdict.
Each independently looks like genuine internal recon (SEV3 magnitude
5, borderline `s3_score` 50-60, `VERIFICATION_REQUIRED` expected
verdict) so the auto-close decision matters; in aggregate the three
share enough anchors that any related-incidents-aware recommender
should close them as `true_positive_benign_authorized`.
| `_scenario_id` | offence ID | QRadar categories | Target | Scan technique | Expected disposition |
| -------------- | ---------- | ------------------------------------------ | --------------- | ---------------------------- | ------------------------------ |
| **SCAN-A** | 90091 | Port Scan · Network Reconnaissance | WIN-PROD-DB-01 | TCP SYN scan (nmap -sS) | `true_positive_benign_authorized` |
| **SCAN-B** | 90092 | Network Reconnaissance · Suspicious Network Activity | WIN-PROD-APP-01 | Service version (nmap -sV) | `true_positive_benign_authorized` |
| **SCAN-C** | 90093 | Network Reconnaissance · Port Scan | WIN-PROD-WEB-01 | Vuln scripts (nmap --script vuln) | `true_positive_benign_authorized` |
Each `_raw_alert` ships a `qradar_categories` override (read by
`_wrap_as_qradar_offence`) so the QRadar offence's `categories` field
carries the realistic recon labels instead of the default
`Sophisticated-Test` tag. Each carries 4 typed IOCs
(`source_ip` / `destination_ip` / `user` / `process`) including the
load-bearing `am_name: "user"` artifact that triggers Entity Agent.
#### Batch 5 — public-TI-confirmed IOCs (ENRICH-A through ENRICH-E, 5 offences)
The **positive-path complement** to the DEMO batch — where DEMO
deliberately uses synthetic IOCs that public TI sources can't enrich
(so the enrichment-bypass detector has positive test cases), ENRICH
uses **real, well-documented, historical IOCs** that AlienVault OTX /
abuse.ch / VirusTotal / GreyNoise / TorProject reliably tag.
Every ENRICH IOC carries a `pattern: "ti_*"` tag — the discriminator
that tells downstream consumers "round-trip this to public TI" vs
DEMO's `synthetic_*` tags that mean "short-circuit, don't waste a
public lookup."
| `_scenario_id` | offence ID | Theme | Key IOCs (with `pattern` tag) | Expected verdict / disposition |
| -------------- | ---------- | -------------------------------------------- | --------------------------------------------------------------------------------------------- | --------------------------------------------------------- |
| **ENRICH-A** | 90094 | WannaCry ransomware (historical) | `ti_known_wannacry` SHA-256 `ed01ebfb…b9faaaaa` + `ti_known_wannacry_killswitch` kill-switch domain | `MALICIOUS_CONFIRMED` / `true_positive_confirmed` (SEV1) |
| **ENRICH-B** | 90095 | Stuxnet C2 callback (historical) | `ti_known_stuxnet` × 2: `mypremierfutbol.com` + `todaysfutbol.com` (sinkholed since 2010) | `MALICIOUS_CONFIRMED` / `true_positive_confirmed_historical` (SEV1) |
| **ENRICH-C** | 90096 | EICAR test file (universal positive control) | `ti_eicar_test` SHA-256 `275a021b…f651fd0f` + matching MD5 — every AV identifies | `MALICIOUS_CONFIRMED` / `true_positive_benign_test` (SEV4) |
| **ENRICH-D** | 90097 | Outbound to confirmed Tor exit | `ti_tor_exit_real` IP in `185.220.101.0/24` (TorProject directory + GreyNoise tag) | `SUSPICIOUS` / `true_positive_requires_review` (SEV2) |
| **ENRICH-E** | 90098 | Inbound from documented benign scanner | `ti_known_scanner` IP `71.6.146.185` (GreyNoise: Shodan-affiliated mass scanner) | `BENIGN_AUTHORIZED` / `false_positive_scanner_noise` (SEV5) |
Each scenario lists `expected_ti_sources` (e.g.
`["VirusTotal", "AlienVault OTX", "ThreatFox", "MalwareBazaar"]`) —
the test contract is that the enrichment agent should round-trip to
those sources and receive positive attribution. Verdicts span the
full disposition spectrum (`MALICIOUS_CONFIRMED` × 3 / `SUSPICIOUS` /
`BENIGN_AUTHORIZED`) so the entire enrichment-to-disposition pipeline
is exercised, not just the malicious path.
**Pattern legend — the bypass-vs-enrich routing key:**
| Prefix | Meaning | Batches using it | Enrichment behaviour |
| -------------- | ------------------------------------------------------ | ---------------------- | -------------------------- |
| `synthetic_*` | Synthetic placeholder (no real-world TI attribution) | DEMO-A through DEMO-G | Short-circuit, return `synthetic_fixture` |
| `rfc5737_*` | Synthetic — RFC 5737 documentation IPs | DEMO scenarios | Short-circuit |
| `netbios_*` | Synthetic — NetBIOS-shape internal names | DEMO scenarios | Short-circuit |
| `fictional` | Synthetic — fabricated domain | DEMO scenarios | Short-circuit |
| `placeholder_*`| Synthetic — placeholder file hashes | DEMO scenarios | Short-circuit |
| `ti_*` | **Real public-TI-attributable** — should round-trip | ENRICH-A through ENRICH-E | Full enrichment |
| `tor_exit_*` | Tor exit node | DEMO-H, ENRICH-D | Enrich (TorProject + GreyNoise) |
| `authorized_pentest_*` | Authorized internal pentest actor | SCAN-A through SCAN-C | Cross-reference with related_incidents |
| `internal_corp_*` | Internal corp address ranges | SCAN scenarios | Skip external TI (internal-only) |
| `recon_tool` | Known recon tool (nmap, masscan, ...) | SCAN scenarios | Tool-attribution lookup |
The full scenario corpus lives in `siemulator/scenarios.py` — JSON-defined
payloads parsed once at import. Add your own by extending the registry
at the bottom of that file.
## Use as a test fixture
The most common consumer pattern: spin up siemulator in a pytest fixture
and point your integration under test at it.
### Pytest in-process (no Docker)
# tests/conftest.py
import os, pytest
from fastapi import FastAPI
from fastapi.testclient import TestClient
@pytest.fixture
def siem(monkeypatch):
monkeypatch.setenv("SIEMULATOR_QRADAR_TOKEN", "ci-token")
monkeypatch.setenv("SIEMULATOR_LOGSCALE_TOKEN", "ci-token")
from siemulator.app import create_app
return TestClient(create_app())
def test_my_qradar_ingest(siem):
# Your ingestion code under test:
from myapp.ingest import poll_qradar_offences
offences = poll_qradar_offences(
base_url=str(siem.base_url),
token="ci-token",
)
assert len(offences) >= 1
# Pin the shape contract — siemulator guarantees these on every poll:
assert isinstance(offences[0]["id"], int)
assert offences[0]["start_time"] > 1_000_000_000_000 # ms epoch
assert "x-mock-source" in offences[0]
### docker-compose (out-of-process)
# docker-compose.test.yml
services:
siemulator:
image: ghcr.io/sirp-labs/siemulator:latest
environment:
SIEMULATOR_QRADAR_TOKEN: ci-token
SIEMULATOR_LOGSCALE_TOKEN: ci-token
healthcheck:
test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8080/logscale/api/v1/status').read()"]
interval: 2s
retries: 5
my-app-under-test:
build: .
environment:
SIEM_URL: http://siemulator:8080
SIEM_TOKEN: ci-token
depends_on:
siemulator:
condition: service_healthy
### One-off shell test
docker run -d --name siem --rm -p 8080:8080 ghcr.io/sirp-labs/siemulator:latest
curl -H "SEC: qradar-dev-token" "http://localhost:8080/qradar/api/siem/offenses?scenarios=replay" \
| jq '.[] | {id, _scenario_id, description}'
docker stop siem
## Wire it into your SIEM / SOAR
For pointing a real SOAR (Cortex XSOAR, Splunk SOAR, IBM Resilient),
SIEM (Splunk Enterprise, Microsoft Sentinel, Elastic), or workflow
tool (Tines, n8n) at siemulator for ingestion testing, see
**[docs/ingestion-guide.md](docs/ingestion-guide.md)** — copy-paste
recipes for the common platforms plus the patterns that apply across
all of them (auth-channel choice, polling cadence, scenario-dedup,
verification, going-to-production checklist).
The live demo at https://siemulator-y7uhf.ondigitalocean.app lets you
validate the integration end-to-end before deploying your own
instance.
## Debug endpoints
Gated on `SIEMULATOR_ADMIN_KEY`. Leave the env var empty to disable
them entirely (every `/_debug/*` then returns 403).
| Method | Path | Purpose |
| ------ | ----------------------------------- | ------------------------------------------------------------------ |
| GET | `/qradar/_debug/recent` | Last 100 requests this mock saw (path, headers, auth channel, response preview) |
| POST | `/qradar/_debug/reset_scenarios` | Clear served-scenarios set so `?scenarios=all` replays the pool |
| GET | `/qradar/_debug/scenarios_state` | Served vs remaining scenario IDs |
All require `X-Admin-Key: `. Use `_debug/recent`
to diagnose "my poller hits siemulator but my SOAR shows zero incidents"
— you can see the exact path, query params, auth channel, and first-row
preview that went over the wire.
## Record / replay / diff
Turns siemulator into a **regression-testing tool for SOC tooling
teams**. The core flow:
1. Run your consumer (XSOAR / Splunk SOAR / Sentinel playbook / custom
integration) against siemulator while a named session is recording.
2. Upgrade or modify your consumer.
3. Run the new version against siemulator with a different session
name.
4. Diff the two sessions — *"did the consumer's request stream
change?"* is your regression signal.
### Endpoints (admin-key gated)
| Method | Path | Purpose |
| ------ | --------------------------------- | ------------------------------------------ |
| POST | `/api/sessions/{name}/start` | Begin recording into session `name` |
| POST | `/api/sessions/{name}/stop` | Finalize → flush JSONL to disk |
| GET | `/api/sessions` | List all sessions (in-memory + on-disk) |
| GET | `/api/sessions/{name}` | Metadata + by_path + by_status summary |
| GET | `/api/sessions/{name}/entries` | Full req+resp pairs (paginated; `?limit`, `?offset`) |
| DELETE | `/api/sessions/{name}` | Remove from memory + disk |
| GET | `/api/sessions/diff?a=X&b=Y` | Structured diff of two sessions |
### Replay (no admin auth needed)
Add `?replay_from=` to any bound endpoint. siemulator looks
up the first captured entry matching `(method, path, query without
meta-params)` and returns the captured response **verbatim** —
preserved bytes, original status, original headers. Useful for
snapshot-pinning siemulator's own output so future code changes here
don't break your consumer's test suite.
curl -i "https://your-siemulator/qradar/api/siem/offenses?replay_from=xsoar-v1"
# Response headers include:
# X-Replay-Match: hit
# X-Replay-From: xsoar-v1
# X-Replay-Idx: 3
### Example — regression-test an XSOAR playbook upgrade
# Capture v1 behaviour
curl -X POST -H "X-Admin-Key: $K" $URL/api/sessions/xsoar-v1/start
xsoar-playbook-run --target $URL # your CI step
curl -X POST -H "X-Admin-Key: $K" $URL/api/sessions/xsoar-v1/stop
# Upgrade XSOAR, capture v2
curl -X POST -H "X-Admin-Key: $K" $URL/api/sessions/xsoar-v2/start
xsoar-playbook-run --target $URL # same step, new XSOAR version
curl -X POST -H "X-Admin-Key: $K" $URL/api/sessions/xsoar-v2/stop
# Diff: did v2 send different requests than v1?
curl -fsS -H "X-Admin-Key: $K" \
"$URL/api/sessions/diff?a=xsoar-v1&b=xsoar-v2" | jq '.diffs'
A non-empty `diffs` array means the upgrade changed your consumer's
request stream — investigate before promoting v2 to prod. Diff
surfaces method/path/query/status changes per-entry and body delta
(line + byte counts).
### Storage
Sessions persist as JSONL to `SIEMULATOR_SESSIONS_DIR` (default
`./siemulator-sessions/`). Reload from disk on process restart. Mount
a persistent volume in your container if you want sessions to survive
redeploys.
### Token redaction
Headers (`Authorization`, `SEC`, `X-Admin-Key`, `Cookie`) and
sensitive query params are recorded as `***` markers, never as
values. Pinned regression confirms the literal secret strings never
echo through the captured entries.
### Knobs
| Variable | Default | Purpose |
| ------------------------------ | ----------------------- | ---------------------------------------- |
| `SIEMULATOR_SESSIONS_ENABLED` | `true` | Disable middleware + admin endpoints |
| `SIEMULATOR_SESSIONS_DIR` | `./siemulator-sessions` | JSONL persistence directory |
## Access log
Every request to `/logscale/*` and `/qradar/*` is captured into a
bounded in-memory ring AND emitted as a structured JSON line to stdout
(uvicorn forwards it to the platform log surface — DO Apps, Docker
logs, k8s, etc. pick it up for free).
**Recorded per request:** timestamp, method, path, redacted query
string, auth channel (`bearer` / `sec` / `query` / `none`), client IP
(X-Forwarded-For-aware), user-agent (truncated to 200 chars), status,
duration in ms, response bytes.
**Never recorded:** Bearer / SEC token values, `?token=` query-param
value, `X-Admin-Key`, cookies, request body, response body. Pin
[`tests/test_access_log.py`](tests/test_access_log.py) guarantees the
literal token strings never leak.
**Admin endpoints** (require `SIEMULATOR_ADMIN_KEY` set + sent on the
request; 403 otherwise):
| Method | Path | Purpose |
| ------ | --------------------------------- | ------------------------------------------------ |
| GET | `/api/access-log` | Recent entries, newest first. Filters: `?limit`, `?since`, `?path_prefix`, `?status`, `?auth` |
| GET | `/api/access-log/stats` | Aggregates: `by_status`, `by_auth`, `top_paths`, `top_clients`, `top_user_agents`, `duration_ms` (avg/p50/p95/p99/max), `total_response_bytes` |
| POST | `/api/access-log/clear` | Wipe the in-memory ring (stdout log untouched) |
**Example: "who consumed what" in the last hour:**
curl -fsS -H "X-Admin-Key: $SIEMULATOR_ADMIN_KEY" \
"https://your-siemulator/api/access-log/stats" | jq '{
total,
top_clients,
top_user_agents,
by_auth,
by_status
}'
**Knobs** (env vars):
| Variable | Default | Purpose |
| ------------------------------------ | ------- | -------------------------------------------------- |
| `SIEMULATOR_ACCESS_LOG_ENABLED` | `true` | Disable everything — middleware + admin endpoints |
| `SIEMULATOR_ACCESS_LOG_SIZE` | `5000` | Ring capacity (~3 days at 60-s polling cadence) |
| `SIEMULATOR_ACCESS_LOG_SKIP_HEALTH` | `false` | Skip noisy `/status` / `/api/help` (useful when DO Apps' 30-s probe would dominate the log) |
For platform-level retention beyond the in-memory ring, your platform
log collector picks up the stdout JSON lines and routes them to your
SIEM / log warehouse / Grafana Loki / wherever.
## Safety markers
Every response carries `X-Mock-Source: siemulator` (HTTP header) and
`"x-mock-source": "siemulator"` (JSON field). Detection events embed it
per-row too. **This is the contract test every consumer should pin** —
it's how you guarantee in CI that you're not accidentally pointed at a
real SIEM. The `siemulator` string is stable across versions.
## Architecture
siemulator/
├── app.py # FastAPI factory — mounts UI + both API routers
├── config.py # All env var reads (one function per var; no caching)
├── logscale.py # /logscale/* — Humio REST shape
├── qradar.py # /qradar/* — QRadar offences + Ariel
├── templates.py # 6 detection templates + HOSTNAMES + USERS pool
├── scenarios.py # 38 multi-source attack narratives
├── ui.py # Single-page web UI at / (inlined HTML/CSS/JS)
├── access_log.py # Middleware + /api/access-log endpoints
├── fault_inject.py # Chaos engineering — middleware + /api/faults
├── sessions.py # Record / replay / diff — middleware + /api/sessions
├── splunk.py # /splunk/* — Splunk REST search API
└── __main__.py # `python -m siemulator` entrypoint
Both routers are built by a `build_router()` factory that reads the
prefix env var at construction time and returns a fresh `APIRouter`.
Env vars are re-read per-request, not cached at import — so you can
`monkeypatch.setenv()` mid-test and the next request reflects the
change.
State: each surface keeps two in-memory dicts — `_query_jobs` (LogScale
queryjobs, 256-entry FIFO cap) and `_ariel_searches` (QRadar Ariel
searches, 256-entry FIFO cap) — and the QRadar surface additionally
keeps a 100-entry request-capture deque and a set of served scenario
IDs. Everything dies with the process; no persistence.
## What siemulator IS / ISN'T
**IS:**
- A test fixture for SOAR ingestion, detection-engineering pipelines,
and agent-chain integration tests.
- A way to pin SIEM-response shapes in CI so vendor-shape regressions
fail fast.
- A reproducible source of multi-source attack narratives for
end-to-end SOC tooling tests.
- Safe to deploy as a long-running internal service (the debug endpoints
are admin-key-gated and disabled by default).
**IS NOT:**
- A real SIEM. There's no event ingest, no search engine, no
correlation rules, no storage. Search queries are accepted and
ignored; the alert pool is fixed per-process.
- Production-ready as a public endpoint without putting it behind your
own auth layer. The default tokens are public sentinels —
**change them**.
- A canonical reference for vendor APIs. Field coverage is "everything
a typical consumer reads" plus enough adjacent fields to look real;
if you're building a real LogScale or QRadar client, read the
upstream vendor docs.
- A red-team training environment. The synthetic data is shape-
realistic, not behaviour-realistic — running detections against
siemulator output will not validate that your detections work
against real attacks.
## Performance & limits
| Resource | Cap | Behaviour at cap |
| --------------------------- | ---------------------------------- | ---------------------------------------- |
| LogScale `?limit=N` | 1 ≤ N ≤ 50 | Clamped silently |
| QRadar `Range: items=0-N` | 1 ≤ N ≤ 50 | Clamped silently |
| In-memory LogScale queryjobs | 256 | Oldest evicted FIFO |
| In-memory Ariel searches | 256 | Oldest evicted FIFO |
| Request capture (`_debug/recent`) | 100 most recent | Oldest dropped |
| Served-scenarios set (`?scenarios=all`) | 22 (the pool size) | After 22, returns `[]` until reset |
Single-process throughput is whatever uvicorn + your CPU give you —
typically a few thousand requests/second per worker on a modest host;
each response is generated fresh (template choice + ID + timestamp), so
there's no caching benefit from repeated requests.
Run multiple workers for higher throughput:
uvicorn siemulator.app:create_app --factory --workers 4 --port 8080
Note that the in-memory state (queryjobs, served-scenarios, debug-ring)
is per-worker — multi-worker deployments will see the served-scenarios
one-shot dedup operate independently in each worker. For a true single-
state deployment, run one worker.
## Roadmap
Contributions especially welcome on the highest-leverage gaps:
- **New SIEM vendor shapes.** Splunk REST, Microsoft Sentinel Log
Analytics, Elastic Security, Google Chronicle. Each lives as a fresh
module under `siemulator/` reusing the existing template + scenario
pool. See [`CONTRIBUTING.md`](CONTRIBUTING.md).
- **More detection templates.** Realistic MITRE-mapped templates,
especially for under-represented tactics (Defence Evasion, Discovery,
Collection, Impact).
- **More attack scenarios.** Multi-source narratives covering
ransomware deployment chains, cloud-native attack paths
(IMDS abuse → cross-account role assumption → S3 exfil), and Mac /
Linux endpoint chains. Open-source threat-intel reports are the best
source.
- **Streaming surface.** A `/logscale/api/v1/repositories/{repo}/stream`
SSE/WebSocket endpoint that pushes events at a configurable rate, so
consumers testing push-style ingestion (rather than poll) can exercise
their code paths.
- **Deterministic mode.** A `SIEMULATOR_SEED` env var that makes
template choice + IDs + timestamps reproducible across runs, so
snapshot tests can pin exact responses instead of shape-only contracts.
## Deploy on DigitalOcean App Platform
A ready-to-apply spec ships at [`.do/app.yaml`](.do/app.yaml). It uses
GitHub auto-deploy from `main`, builds via the repo's `Dockerfile`, and
sizes for the stateless mock (1 instance, `basic-xxs` — ~$5/mo).
**First-time create:**
doctl apps create --spec .do/app.yaml
# → returns an app ID; note it down.
**Set the three SECRET env vars** (tokens + admin key) via either
the dashboard (Settings → web → Environment Variables) or by editing a
local copy of `.do/app.yaml` to inline the values and running
`doctl apps update --spec `. DO encrypts the values
and stores them as `EV[1:...]` ciphertext — don't commit those.
**Custom domain** (optional): point `siemulator.example.com` at the
default `.ondigitalocean.app` ingress via CNAME, then add a
`domains:` block to the spec and `doctl apps update`. DO provisions a
Let's Encrypt cert automatically.
**Why `instance_count: 1`.** Each instance keeps its own in-memory
served-scenarios set for `?scenarios=all` one-shot dedup. Running
multiple instances would break that contract — a round-robin poller
would see the same scenario re-served on every other hit. If you need
horizontal scale and don't use one-shot dedup, bump the count freely
(every other endpoint is request-local and safe to scale).
**Health checks** poll `/logscale/api/v1/status` (no-auth) every 30 s
with a 10-second initial delay. Failures auto-roll back to the last
healthy deployment.
**Updates flow.** Every push to `main` triggers a fresh build + zero-
downtime deploy. CI must be green; if `pytest` or `ruff` fail, the
build never reaches the platform. Multi-arch Docker images keep getting
published to `ghcr.io/sirp-labs/siemulator:` in parallel — pin a
specific tag in the spec if you want immutable deploys instead of
"latest-on-main."
## Development
git clone https://github.com/sirp-labs/siemulator.git
cd siemulator
python -m venv .venv && source .venv/bin/activate
pip install -e ".[dev]"
pytest # 35 contract tests
ruff check . # lint
CI runs ruff + pytest on Python 3.10 / 3.11 / 3.12 plus a multi-arch
GHCR build with a container smoke test. See
[`.github/workflows/`](.github/workflows/) for the exact pipeline.
See [`CONTRIBUTING.md`](CONTRIBUTING.md) for the contribution flow,
including adding new SIEM shapes, templates, and scenarios.
## FAQ
**Q: Can I use this for load testing my SOAR?**
A: It can produce as much traffic as your test harness can consume,
but every response is a fresh roll of dice. If you're load-testing
deduplication or correlation, prefer `?scenarios=batch` or `?scenarios=replay`
so the offence IDs are stable. For shape-only soak testing, the
default mode is fine.
**Q: Does it support Splunk / Sentinel / Elastic?**
A: Not yet. The architecture supports adding them — see the
[Roadmap](#roadmap). Each new vendor shape is a fresh module under
`siemulator/` that reuses the existing template + scenario pool. PRs
welcome.
**Q: Are the scenarios real attacks?**
A: They're realistic narratives modeled on published threat-intel
reports and incident postmortems, but the IOCs, hostnames, usernames,
file hashes, and timestamps are synthetic. Don't match them against
real-world threat-intel feeds.
**Q: How do I add my own templates / scenarios?**
A: For templates, append to `ALERT_TEMPLATES` in
`siemulator/templates.py` — the schema is documented in that file's
module docstring. For scenarios, append a `(offence_id, scenario_label,
raw_alert)` tuple to the registry at the bottom of `siemulator/scenarios.py`
and import a JSON payload via the `_j()` helper. Both surfaces pick up
the additions automatically on next process start.
**Q: Is it safe to expose publicly?**
A: The data is synthetic, so there's no data-leak risk. The default
tokens (`logscale-dev-token`, `qradar-dev-token`) are PUBLIC sentinels —
**change them before exposing publicly** so casual scanners don't get
free use of your service. The debug endpoints are disabled by default
and require `SIEMULATOR_ADMIN_KEY` to be set + sent on every request,
so they're safe to leave wired in.
**Q: Why one combined service instead of two repos for LogScale and
QRadar?**
A: Both surfaces share the same template + scenario pool. Splitting
them would force every new template / scenario to be added in two
places. Keeping them together lets one detection-template addition
serve every vendor surface for free.
**Q: How do I pin a regression test that catches "my consumer breaks
when siemulator changes shape"?**
A: Fork the relevant tests from
[`tests/test_logscale.py`](tests/test_logscale.py) /
[`tests/test_qradar.py`](tests/test_qradar.py) into your own test
suite, pointing them at your consumer's response-handling code instead
of at siemulator. Those tests are the contract — if your consumer
makes them pass, siemulator changes that break the contract will
break your tests too.
## License
[MIT](LICENSE) — © 2026 SIRP Labs.
标签:AV绕过, FastAPI, PB级数据处理, SIEM模拟器, SOAR测试, 安全运维, 模拟数据, 请求拦截, 逆向工具