Eventum Logo

Eventum

SIEM: Windows Security Events → OpenSearch

Generate realistic Windows Security Event Log data and index it into OpenSearch for SIEM rule development and detection testing.

Build a generator that produces Windows Security Event Log sessions — logon, privilege assignment, process creation, and logoff — with realistic timing and user behavior. Events are indexed into OpenSearch, 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:

  1. Logon (Event ID 4624) — a user logs in, session context is established.
  2. Privilege assignment (Event ID 4672) — special privileges are assigned.
  3. Process creation (Event ID 4688) — the user launches several processes. The FSM loops on this state.
  4. 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

eventum.yml
startup.yml
generator.yml
traffic.yml
logon.jinja
privilege.jinja
process.jinja
logoff.jinja
users.json

Build it

Create the project directory

mkdir -p eventum/generators/winevents/{patterns,templates,data}
cd eventum

Define 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).
generators/winevents/patterns/traffic.yml
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.0

This 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.

generators/winevents/data/users.json
[
  { "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.

generators/winevents/templates/logon.jinja
{% 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) %}
{
  "@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.

generators/winevents/templates/privilege.jinja
{
  "@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; after 4 iterations the FSM transitions to logoff.

generators/winevents/templates/process.jinja
{% do shared.set("process_count", shared.get("process_count", 0) + 1) %}
{% 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.

generators/winevents/templates/logoff.jinja
{
  "@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 key pattern: the logon template writes session context into shared state, and all subsequent templates read from it. This keeps the session consistent — 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:

generators/winevents/generator.yml
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: process
              when: { lt: { "shared.process_count": 4 } }
            - to: logoff
              when: { ge: { "shared.process_count": 4 } }
      - 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: false

Reading the transitions:

FromToConditionMeaning
logonprivilegealwaysEvery logon gets privileges
privilegeprocessalwaysStart creating processes
processprocessshared.process_count < 4Keep creating processes
processlogoffshared.process_count ≥ 4End the session
logofflogonalwaysStart a new session

Configure the application

eventum.yml
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: 100

All path.* values must be absolute paths. Adjust to match your actual project location.

The startup file registers the generator and passes connection parameters:

startup.yml
- id: winevents
  path: winevents/generator.yml
  params:
    opensearch_host: "https://localhost:9200"
    opensearch_user: admin

Replace the values with your actual OpenSearch connection details.

Store the OpenSearch password

Use the keyring to encrypt the password:

eventum-keyring set opensearch_password

You'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.yml

JSON 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.ratio in traffic.yml to 100+ and reduce the oscillator period for high-volume SIEM testing.
  • Add failed logons — create a logon_failed.jinja template for Event ID 4625 and add it as an FSM branch with a probability-based condition.
  • Vary session length — store a random target in shared during logon (module.rand.number.integer(2, 8)) and check against it in the process transition.
  • Multiple workstations — add a second generator in startup.yml with different parameters to simulate traffic from separate network segments.

What's next

On this page