SIEM Test Data: Index Windows Security Events in OpenSearch
Generate SIEM test data — Windows Security Event Log sessions indexed into OpenSearch — for detection development, parser validation, and analyst training.
A detection rule tuned against a handful of hand-written events, a SIEM parser wired to a new log source, an analyst who has never seen an incident play out — each needs the same thing before anyone trusts it: security telemetry that behaves like a real environment's. Logons, privilege use, and process activity, in realistic sequences and volume, with no live attack and no production system anywhere in the test.
The generator below produces Windows Security Event Log sessions — logon, privilege assignment, process creation, and logoff — with realistic timing and user behavior, indexed into OpenSearch and ready for dashboards and detection rules.
No OpenSearch? Replace the opensearch output with stdout: {} in the generator config. Everything else stays the same.
What you'll build
The generator uses a finite state machine (FSM) to model user sessions. Each session follows a fixed flow:
- Logon (Event ID 4624) — a user logs in, session context is established.
- Privilege assignment (Event ID 4672) — special privileges are assigned.
- Process creation (Event ID 4688) — the user launches several processes. The FSM loops on this state.
- Logoff (Event ID 4634) — the session ends, and the cycle restarts.
A time-patterns input controls the arrival rate, producing timestamps at natural intervals. Each timestamp advances the FSM by one step.
Prerequisites
- Eventum installed
- An OpenSearch instance accessible over HTTP(S) (optional — stdout works for local testing)
Project structure
Build it
Create the project directory
mkdir -p eventum/generators/winevents/{patterns,templates,data}
cd eventumDefine the traffic pattern
The time-patterns plugin generates timestamps using a statistical model. The pattern file has four components:
- Oscillator — the repeating time window (every 10 seconds).
- Multiplier — how many timestamps per period (7 ≈ one full session).
- Randomizer — adds ±20% variance to the count.
- Spreader — distributes timestamps within each period (uniform = evenly spaced).
label: Security events
oscillator:
start: "now"
end: "never"
period: 10
unit: seconds
multiplier:
ratio: 7
randomizer:
deviation: 0.2
direction: mixed
spreader:
distribution: uniform
parameters:
low: 0.0
high: 1.0This produces roughly 7 timestamps every 10 seconds — enough to complete one user session per cycle.
Create sample data
The user pool is a JSON file loaded as a sample. Each session randomly picks a user from this list. JSON sample rows support named access via object keys — user.name, user.domain.
[
{ "name": "jsmith", "domain": "CORP" },
{ "name": "ajohnson", "domain": "CORP" },
{ "name": "mwilliams", "domain": "CORP" },
{ "name": "kbrown", "domain": "CORP" },
{ "name": "admin", "domain": "CORP" },
{ "name": "svc-backup", "domain": "CORP" }
]Write the session templates
Each template produces one JSON event following Elastic Common Schema field naming — a widely used format for SIEM ingestion.
Logon — initializes the session. Picks a random user, generates a session ID, and stores everything in shared state so subsequent templates can access it.
{% set user = module.rand.choice(samples.users) %}
{% set session_id = module.rand.crypto.uuid4() %}
{% set src_ip = module.faker.locale.en.ipv4_private() %}
{% set workstation = "WS-" ~ module.rand.number.integer(1000, 9999) %}
{% set logon_type = module.rand.weighted_choice(["Interactive", "Network", "RemoteInteractive"], [0.5, 0.3, 0.2]) %}
{% do shared.set("session_id", session_id) %}
{% do shared.set("username", user.name) %}
{% do shared.set("domain", user.domain) %}
{% do shared.set("src_ip", src_ip) %}
{% do shared.set("workstation", workstation) %}
{% do shared.set("process_count", 0) %}
{% do shared.pop("logoff_ready", None) %}
{
"@timestamp": "{{ timestamp.isoformat() }}",
"event": {
"code": 4624,
"action": "logged-in",
"provider": "Microsoft-Windows-Security-Auditing"
},
"host": { "name": "{{ workstation }}" },
"source": { "ip": "{{ src_ip }}" },
"user": { "name": "{{ user.name }}", "domain": "{{ user.domain }}" },
"winlog": {
"logon": { "id": "{{ session_id }}", "type": "{{ logon_type }}" }
},
"message": "An account was successfully logged on."
}Privilege assignment — follows logon, reads session context from shared.
{
"@timestamp": "{{ timestamp.isoformat() }}",
"event": {
"code": 4672,
"action": "assigned-special-privileges",
"provider": "Microsoft-Windows-Security-Auditing"
},
"host": { "name": "{{ shared.get('workstation') }}" },
"user": { "name": "{{ shared.get('username') }}", "domain": "{{ shared.get('domain') }}" },
"winlog": {
"logon": { "id": "{{ shared.get('session_id') }}" }
},
"message": "Special privileges assigned to new logon."
}Process creation — the FSM loops on this state. Each call increments a counter in shared; once it reaches 4, the template itself sets a flag that tells the FSM the session is ready for logoff.
{% set process_count = shared.get("process_count", 0) + 1 %}
{% do shared.set("process_count", process_count) %}
{% if process_count >= 4 %}
{% do shared.set("logoff_ready", true) %}
{% endif %}
{% set proc = module.rand.choice(["cmd.exe", "powershell.exe", "notepad.exe", "chrome.exe", "svchost.exe", "taskmgr.exe"]) %}
{
"@timestamp": "{{ timestamp.isoformat() }}",
"event": {
"code": 4688,
"action": "created-process",
"provider": "Microsoft-Windows-Security-Auditing"
},
"host": { "name": "{{ shared.get('workstation') }}" },
"user": { "name": "{{ shared.get('username') }}", "domain": "{{ shared.get('domain') }}" },
"process": {
"name": "{{ proc }}",
"executable": "C:\\Windows\\System32\\{{ proc }}",
"pid": {{ module.rand.number.integer(1000, 65535) }}
},
"winlog": {
"logon": { "id": "{{ shared.get('session_id') }}" }
},
"message": "A new process has been created."
}Logoff — ends the session. The FSM transitions back to logon, starting a new cycle.
{
"@timestamp": "{{ timestamp.isoformat() }}",
"event": {
"code": 4634,
"action": "logged-off",
"provider": "Microsoft-Windows-Security-Auditing"
},
"host": { "name": "{{ shared.get('workstation') }}" },
"user": { "name": "{{ shared.get('username') }}", "domain": "{{ shared.get('domain') }}" },
"winlog": {
"logon": { "id": "{{ shared.get('session_id') }}" }
},
"message": "An account was logged off."
}The logon template writes session context into shared state, and every later template reads from it — the same user, workstation, and session ID appear across every event in the session.
Configure the generator
The generator config wires the pipeline together. The FSM transitions define the session flow:
input:
- time_patterns:
patterns:
- patterns/traffic.yml
event:
template:
mode: fsm
samples:
users:
type: json
source: data/users.json
templates:
- logon:
template: templates/logon.jinja
initial: true
transitions:
- to: privilege
when: { always: }
- privilege:
template: templates/privilege.jinja
transitions:
- to: process
when: { always: }
- process:
template: templates/process.jinja
transitions:
- to: logoff
when: { defined: shared.logoff_ready }
- to: process
when: { always: }
- logoff:
template: templates/logoff.jinja
transitions:
- to: logon
when: { always: }
output:
- stdout:
formatter:
format: json
- opensearch:
hosts:
- ${params.opensearch_host}
username: ${params.opensearch_user}
password: ${secrets.opensearch_password}
index: winevents
verify: falseReading the transitions:
| From | To | Condition | Meaning |
|---|---|---|---|
| logon | privilege | always | Every logon gets privileges |
| privilege | process | always | Start creating processes |
| process | logoff | defined: shared.logoff_ready | End the session after 4 processes |
| process | process | always (fallback) | Keep creating processes |
| logoff | logon | always | Start a new session |
Transitions are evaluated in order, and the first one whose condition holds wins. On process, the logoff_ready check is listed before the always fallback — reversed, the fallback would fire first every time, since always never fails, and the FSM would never reach the flag check.
Configure the application
server:
host: "0.0.0.0"
port: 9474
path:
startup: /home/user/eventum/startup.yml
generators_dir: /home/user/eventum/generators
logs: /home/user/eventum/logs
keyring_cryptfile: /home/user/eventum/cryptfile.cfg
generation:
timezone: UTC
batch:
size: 100All path.* values must be absolute paths. Adjust to match your actual project location.
The startup file registers the generator and passes connection parameters:
- id: winevents
path: winevents/generator.yml
params:
opensearch_host: "https://localhost:9200"
opensearch_user: adminReplace the values with your actual OpenSearch connection details.
Store the OpenSearch password
Use the keyring to encrypt the password:
eventum-keyring set opensearch_passwordYou'll be prompted for the keyring password and the secret value. The generator config references it as ${secrets.opensearch_password}.
Using stdout only? Skip this step and remove the opensearch block from generator.yml.
Run it
eventum run -c eventum.ymlJSON events stream to stdout — each one is a step in a user session:
{"@timestamp":"2025-06-15T14:23:01+00:00","event":{"code":4624,"action":"logged-in","provider":"Microsoft-Windows-Security-Auditing"},"host":{"name":"WS-4821"},"source":{"ip":"10.12.44.198"},"user":{"name":"jsmith","domain":"CORP"},"winlog":{"logon":{"id":"a1b2c3d4-e5f6-7890-abcd-ef1234567890","type":"Interactive"}},"message":"An account was successfully logged on."}
{"@timestamp":"2025-06-15T14:23:02+00:00","event":{"code":4672,"action":"assigned-special-privileges", ...}}
{"@timestamp":"2025-06-15T14:23:03+00:00","event":{"code":4688,"action":"created-process", ...},"process":{"name":"powershell.exe", ...}}
{"@timestamp":"2025-06-15T14:23:05+00:00","event":{"code":4688,"action":"created-process", ...},"process":{"name":"chrome.exe", ...}}
{"@timestamp":"2025-06-15T14:23:06+00:00","event":{"code":4688,"action":"created-process", ...},"process":{"name":"cmd.exe", ...}}
{"@timestamp":"2025-06-15T14:23:07+00:00","event":{"code":4688,"action":"created-process", ...},"process":{"name":"svchost.exe", ...}}
{"@timestamp":"2025-06-15T14:23:09+00:00","event":{"code":4634,"action":"logged-off", ...}}A complete session: logon → privilege → 4 processes → logoff. Then a new session starts with a different user.
Going further
- Scale up traffic — increase
multiplier.ratiointraffic.ymlto 100+ and reduce the oscillator period for high-volume SIEM testing. - Add failed logons — create a
logon_failed.jinjatemplate for Event ID 4625 and add it as an FSM branch with a probability-based condition. - Vary session length — store a random target in
sharedduring logon (module.rand.number.integer(2, 8)) and have the process template compare its counter against that target instead of the fixed 4, settinglogoff_readyonce it's reached. - Multiple workstations — add a second generator in
startup.ymlwith different parameters to simulate traffic from separate network segments.
What's next
time-patterns reference
Full parameter docs for the time-patterns input plugin.
FSM picking mode
How finite state machine transitions work.
OpenSearch output
Connection, auth, and TLS options for the OpenSearch plugin.
FAQ
Related
- The scenarios track for the wider applied picture of synthetic test data
- The detection testing lesson for turning telemetry like this into Sigma-rule tests
- The Windows Event Log & Sysmon format lesson for the EVTX event model and EventID structure behind these templates
- The OpenSearch delivery lesson for indexing, the bulk API, and TLS/authentication in depth
- The windows-security generator in the Eventum Hub for a production-tuned, ready-made version of this build
Test Data Pipeline End to End
Test data pipeline delivery end to end — generate a realistic event stream, deliver it the way production does, and confirm the backend actually received it.
Test Sigma Rules with Synthetic Attack Telemetry
Generate benign and attack-shaped telemetry to test Sigma rules and ATT&CK-mapped detections, without running a real attack or touching production.