bokamba / logforge / parse / Windows Security Event

$ logforge parse windows-event

Parse Windows Security Event logs → regex, Grok, Wazuh & rsyslog

Windows Security events are the backbone of endpoint and domain detection, and they are natively a multi-line beast: on disk they live in the binary EVTX store and render as XML, with the human-readable message and a flat set of named EventData elements (SubjectUserName, TargetUserName, LogonType, IpAddress, and so on) rather than a single line of text. That XML/EVTX form is not what a text log parser sees. What THIS page targets is the flattened, single-line shape a SIEM pipeline actually ingests: a forwarder such as NXLog, Winlogbeat, or Snare reads the event and emits it as one line of key=value tokens (or JSON), collapsing the XML tree into EventID=4625 TargetUserName=admin LogonType=3 IpAddress=203.0.113.45 … . Be honest about this: if you are parsing raw EVTX you want an EVTX reader, not a regex — this parser is for the forwarded, line-oriented output.

The single most important field is EventID, because it names the event and dictates which other fields are present. 4624 is a successful logon and 4625 is a failed logon; 4634/4647 are logoff, 4672 is a privileged logon, 4720 is account creation, and 4688 is process creation. LogonType qualifies the 4624/4625 pair and is where a lot of the detection signal lives: 2 is an interactive (at-the-console) logon, 3 is a network logon (SMB, a share), 10 is RemoteInteractive (RDP), 4 is batch and 5 is service. TargetUserName is the account being authenticated, SubjectUserName is the account that requested it, and IpAddress plus WorkstationName give the origin. On a 4625 failure the Status/SubStatus carries an NTSTATUS code — 0xC000006D is a bad username or general logon failure, 0xC0000064 is 'user does not exist', 0xC000006A is a bad password, and 0xC0000234 is a locked-out account — which lets you distinguish password spraying from user enumeration.

Parsing traps are mostly artefacts of the flattening. The forwarder decides the key names and the delimiter, so IpAddress may be a dash ('-') for a local logon, WorkstationName can be blank, and free-text fields can themselves contain spaces or equals signs. The account fields are case-insensitive and machine accounts end in a '$'. For detection you correlate EventID + LogonType + TargetUserName + IpAddress: many 4625s from one IP against many usernames is spraying; repeated 4625 0xC0000064 is enumeration; a 4624 LogonType=10 from an unexpected source is suspicious RDP. Do not invent EventData field names the forwarder did not emit — extract what is actually on the line.

Open this in LogForge →

What a Windows Security Event line looks like

The key=value sample below is fed verbatim into the engine to produce every parser on this page.

EventID=4625 TargetUserName=admin LogonType=3 IpAddress=203.0.113.45 IpPort=51234 Status=0xC000006D WorkstationName=WKSTN-07
EventID=4624 TargetUserName=jdoe LogonType=2 IpAddress=192.0.2.10 IpPort=50022 Status=0xC0000064 WorkstationName=WKSTN-11

Detected fields

The engine classified this sample as kv and consolidated 7 fields across 2 lines. Fields marked literal were identical on every sample line, so they are baked into the pattern as anchors rather than captured.

  • eventid : number
  • targetusername : literal
  • logontype : number
  • ipaddress : ipv4
  • ipport : port
  • status : literal
  • workstationname : literal

Regex (named capture groups)

# sample: EventID=4625 TargetUserName=admin LogonType=3 IpAddress=203.0.113.45 IpPort=51234 Status=0xC000006D WorkstationName=WKSTN-07
# groups: eventid=4625, targetusername=admin, logontype=3, ipaddress=203.0.113.45, ipport=51234, status=0xC000006D, workstationname=WKSTN-07
^EventID=(?<eventid>-?\d+(?:\.\d+)?) TargetUserName=(?<targetusername>[A-Za-z]+) LogonType=(?<logontype>-?\d+(?:\.\d+)?) IpAddress=(?<ipaddress>\d{1,3}(?:\.\d{1,3}){3}) IpPort=(?<ipport>\d{1,5}) Status=(?<status>(?:\d+[A-Za-z]+\d+|\d+[A-Za-z]+\d+[A-Za-z]+)) WorkstationName=(?<workstationname>[A-Za-z]+-\d+)$

Grok pattern (Logstash / Elastic)

EventID=%{NUMBER:eventid} TargetUserName=%{NOTSPACE:targetusername} LogonType=%{NUMBER:logontype} IpAddress=%{IPV4:ipaddress} IpPort=%{INT:ipport} Status=%{NOTSPACE:status} WorkstationName=%{GREEDYDATA:workstationname}
  • note kv-structured input — consider the Logstash kv filter instead of (or after) grok

Wazuh decoder (OS_Regex XML)

<!--
  Generated by LogForge - Wazuh decoder (OS_Regex dialect, not PCRE)
  sample: EventID=4625 TargetUserName=admin LogonType=3 IpAddress=203.0.113.45 IpPort=51234 Status=0xC000006D WorkstationName=WKSTN-07
  test with: /var/ossec/bin/wazuh-logtest
-->

<decoder name="windows-event-kv">
  <prematch>^EventID=</prematch>
</decoder>

<decoder name="windows-event-kv">
  <parent>windows-event-kv</parent>
  <regex offset="after_parent">^(\d+) TargetUserName=(\w+) LogonType=(\d+) IpAddress=(\d+.\d+.\d+.\d+) IpPort=(\d+) Status=(\w+) WorkstationName=(\w+)</regex>
  <order>eventid, targetusername, logontype, ipaddress, ipport, status, workstationname</order>
</decoder>

<!-- ============================================================
     ALERT RULE (starter) — put this in a RULES file, e.g.
     /var/ossec/etc/rules/local_rules.xml. Decoders and rules live
     in SEPARATE files. The rule matches the decoder above through
     <decoded_as>; set <level> and add <field>/<match> conditions so
     it alerts only on the events you care about. Rule ids 100000+
     are the user range — change them if they collide with yours.
     ============================================================ -->
<group name="windows-event,">
  <rule id="100000" level="3">
    <decoded_as>windows-event-kv</decoded_as>
    <description>windows-event: status=$(status)</description>
  </rule>

  <!-- Example — a higher-level alert gated on one field (uncomment and edit):
  <rule id="100001" level="10">
    <if_sid>100000</if_sid>
    <field name="status">^5\d\d$</field>
    <description>windows-event: a server-error response from $(status)</description>
  </rule>
  -->
</group>
  • note kv fields are extracted by same-named sibling decoders (offset="after_parent"), so per-line field order/absence is tolerated — the shared name is what makes Wazuh evaluate every sibling
  • note added a starter alert <rule> (level 3, matched to the decoder via <decoded_as>) — put it in a RULES file (not the decoders file), set the level, and add <field>/<match> conditions; the commented example child rule shows the pattern
  • note decoder order and prematch specificity may need site-specific tuning (other decoders in your ruleset can shadow these) — validate with /var/ossec/bin/wazuh-logtest

Wazuh's OS_Regex is not PCRE — a bare . is a literal dot and \. matches any character. Test Wazuh OS_Regex patterns →

rsyslog template / liblognorm rulebase

version=2
# windows_event — liblognorm v2 rulebase (generated by LogForge)
# Usage with rsyslog (mmnormalize runs liblognorm):
#   module(load="mmnormalize")
#   action(type="mmnormalize" rulebase="/etc/rsyslog.d/windows_event.rb" useRawMsg="on")
# Literal "%" is escaped as "%%"; raw tabs are written as \x09.
rule=windows_event:EventID=%eventid:number% TargetUserName=%targetusername:word% LogonType=%logontype:number% IpAddress=%ipaddress:ipv4% IpPort=%ipport:number% Status=%status:word% WorkstationName=%workstationname:word%
  • note kv structure: rsyslog offers mmfields (fast, fixed single-char separator, untyped) and mmnormalize (this rulebase, typed fields + literal anchors); mmnormalize was chosen for typed extraction
  • note chosen parser types: eventid=number, targetusername=word, logontype=number, ipaddress=ipv4, ipport=number, status=word, workstationname=word

Splunk

# props.conf  (search-time extraction)
[<REPLACE_WITH_SOURCETYPE>]
EXTRACT-logforge = EventID=(?<eventid>-?\d+(?:\.\d+)?) TargetUserName=(?<targetusername>[A-Za-z]+) LogonType=(?<logontype>-?\d+(?:\.\d+)?) IpAddress=(?<ipaddress>\d{1,3}(?:\.\d{1,3}){3}) IpPort=(?<ipport>\d{1,5}) Status=(?<status>(?:\d+[A-Za-z]+\d+|\d+[A-Za-z]+\d+[A-Za-z]+)) WorkstationName=(?<workstationname>[A-Za-z]+-\d+)

# Quick search-time test in SPL:
# | rex field=_raw "EventID=(?<eventid>-?\\d+(?:\\.\\d+)?) TargetUserName=(?<targetusername>[A-Za-z]+) LogonType=(?<logontype>-?\\d+(?:\\.\\d+)?) IpAddress=(?<ipaddress>\\d{1,3}(?:\\.\\d{1,3}){3}) IpPort=(?<ipport>\\d{1,5}) Status=(?<status>(?:\\d+[A-Za-z]+\\d+|\\d+[A-Za-z]+\\d+[A-Za-z]+)) WorkstationName=(?<workstationname>[A-Za-z]+-\\d+)"
  • note EXTRACT-<class> names must be unique within a sourcetype stanza — rename EXTRACT-logforge if you already use that class for this sourcetype

ES ingest

PUT _ingest/pipeline/windows-event
{
  "description": "LogForge-generated ingest pipeline for windows-event",
  "processors": [
    {
      "kv": {
        "field": "message",
        "field_split": " ",
        "value_split": "="
      }
    }
  ]
}
  • note grok: kv-structured input — consider the Logstash kv filter instead of (or after) grok
  • note kv structure: emitted a { kv: { field: "message", field_split: " ", value_split: "=" } } processor — it extracts key=value pairs natively; adjust field_split/value_split if your delimiter differs
  • note a grok alternative is available too (the reused grok pattern is shown in the notes above); prefer the kv processor unless you need typed/anchored extraction
  • note test in Kibana Dev Tools with: POST _ingest/pipeline/windows-event/_simulate (supply a docs[] array whose _source.message holds a sample line)

Graylog

# --- Graylog processing pipeline rule (primary) ---
# Paste under System > Pipelines > Manage rules, then attach the rule to a pipeline stage.
rule "windows-event-parse"
when
  has_field("message")
then
  let gp = grok(pattern: "EventID=%{NUMBER:eventid} TargetUserName=%{NOTSPACE:targetusername} LogonType=%{NUMBER:logontype} IpAddress=%{IPV4:ipaddress} IpPort=%{INT:ipport} Status=%{NOTSPACE:status} WorkstationName=%{GREEDYDATA:workstationname}", value: to_string($message.message), only_named_captures: true);
  set_fields(gp);
end

# --- Graylog import-ready extractor JSON (secondary) ---
# Save as a .json file and import under System > Inputs > (input) > Manage extractors > Actions > Import extractors.
{
  "extractors": [
    {
      "title": "windows-event",
      "extractor_type": "grok",
      "converters": [],
      "order": 0,
      "cursor_strategy": "copy",
      "source_field": "message",
      "target_field": "",
      "extractor_config": {
        "grok_pattern": "EventID=%{NUMBER:eventid} TargetUserName=%{NOTSPACE:targetusername} LogonType=%{NUMBER:logontype} IpAddress=%{IPV4:ipaddress} IpPort=%{INT:ipport} Status=%{NOTSPACE:status} WorkstationName=%{GREEDYDATA:workstationname}",
        "named_captures_only": true
      },
      "condition_type": "none",
      "condition_value": ""
    }
  ],
  "version": "5.0.0"
}
  • note grok: kv-structured input — consider the Logstash kv filter instead of (or after) grok
  • note primary artifact is the processing-pipeline rule; the extractor JSON is an equivalent import-ready alternative for the classic extractor UI

Datadog

logforge_rule EventID=%{number:eventid} TargetUserName=%{notSpace:targetusername} LogonType=%{number:logontype} IpAddress=%{ipv4:ipaddress} IpPort=%{integer:ipport} Status=%{notSpace:status} WorkstationName=%{notSpace:workstationname}
  • note emitted rule name is "logforge_rule"; rename it to match your "windows-event" convention if desired
  • note kv input — consider Datadog's key-value/`keyvalue()` filter in the Grok Parser instead of anchoring each key by hand
  • note paste this line into a Grok Parser processor in a Datadog Log Pipeline; matchers are anchored left-to-right and rule whitespace matches log whitespace. Complex or multi-shape logs may need Helper Rules.

Fluent Bit

[PARSER]
    Name        windows-event
    Format      regex
    Regex       ^EventID=(?<eventid>-?\d+(?:\.\d+)?) TargetUserName=(?<targetusername>[A-Za-z]+) LogonType=(?<logontype>-?\d+(?:\.\d+)?) IpAddress=(?<ipaddress>\d{1,3}(?:\.\d{1,3}){3}) IpPort=(?<ipport>\d{1,5}) Status=(?<status>(?:\d+[A-Za-z]+\d+|\d+[A-Za-z]+\d+[A-Za-z]+)) WorkstationName=(?<workstationname>[A-Za-z]+-\d+)$
    # Time_Key    <name of a timestamp capture group, if any>
    # Time_Format <strptime format, e.g. %Y-%m-%dT%H:%M:%S>
# Fluentd <parse> block:
#   <parse>
#     @type regexp
#     expression /^EventID=(?<eventid>-?\d+(?:\.\d+)?) TargetUserName=(?<targetusername>[A-Za-z]+) LogonType=(?<logontype>-?\d+(?:\.\d+)?) IpAddress=(?<ipaddress>\d{1,3}(?:\.\d{1,3}){3}) IpPort=(?<ipport>\d{1,5}) Status=(?<status>(?:\d+[A-Za-z]+\d+|\d+[A-Za-z]+\d+[A-Za-z]+)) WorkstationName=(?<workstationname>[A-Za-z]+-\d+)$/
#   </parse>

Vector

[transforms.windows_event_parse]
type = "remap"
inputs = ["REPLACE_WITH_SOURCE"]
source = '''
. |= parse_regex!(.message, r'EventID=(?P<eventid>-?\d+(?:\.\d+)?) TargetUserName=(?P<targetusername>[A-Za-z]+) LogonType=(?P<logontype>-?\d+(?:\.\d+)?) IpAddress=(?P<ipaddress>\d{1,3}(?:\.\d{1,3}){3}) IpPort=(?P<ipport>\d{1,5}) Status=(?P<status>(?:\d+[A-Za-z]+\d+|\d+[A-Za-z]+\d+[A-Za-z]+)) WorkstationName=(?P<workstationname>[A-Za-z]+-\d+)')
'''
  • note kv input — parse_key_value!(.message) is the idiomatic Vector parser and is usually preferable to a regex

Loki

# promtail pipeline for "windows-event" (generated by LogForge)
# Add these stages under a scrape_config in your promtail config:
#   scrape_configs:
#     - job_name: windows-event
#       pipeline_stages:
# (the stages below are indented to sit under pipeline_stages)
pipeline_stages:
  - regex:
      expression: '^EventID=(?P<eventid>-?\d+(?:\.\d+)?) TargetUserName=(?P<targetusername>[A-Za-z]+) LogonType=(?P<logontype>-?\d+(?:\.\d+)?) IpAddress=(?P<ipaddress>\d{1,3}(?:\.\d{1,3}){3}) IpPort=(?P<ipport>\d{1,5}) Status=(?P<status>(?:\d+[A-Za-z]+\d+|\d+[A-Za-z]+\d+[A-Za-z]+)) WorkstationName=(?P<workstationname>[A-Za-z]+-\d+)$'
  • note no low-cardinality field found to promote to a Loki label — omitted the `- labels:` stage; every captured field stays in the extracted map for later stages
  • note left in the extracted map (NOT promoted to labels — high cardinality would explode Loki streams): eventid, targetusername, logontype, ipaddress, ipport, status, workstationname

syslog-ng

parser p_windows_event {
    regexp-parser(
        prefix(".windows_event.")
        patterns("EventID=(?<eventid>-?\\d+(?:\\.\\d+)?) TargetUserName=(?<targetusername>[A-Za-z]+) LogonType=(?<logontype>-?\\d+(?:\\.\\d+)?) IpAddress=(?<ipaddress>\\d{1,3}(?:\\.\\d{1,3}){3}) IpPort=(?<ipport>\\d{1,5}) Status=(?<status>(?:\\d+[A-Za-z]+\\d+|\\d+[A-Za-z]+\\d+[A-Za-z]+)) WorkstationName=(?<workstationname>[A-Za-z]+-\\d+)")
    );
};
  • note captured fields are stored as name-value pairs under the prefix ".windows_event." (e.g. a group (?<srcip>…) becomes ".windows_event.srcip")
  • note kv structure: syslog-ng has a dedicated kv-parser() that is simpler and more robust than regexp-parser for key=value logs — consider kv-parser(prefix(".logforge.")) instead of the emitted regexp-parser

FAQ

Why does this parser target single-line Windows events instead of EVTX/XML?
Because raw Windows Security logs are binary EVTX that render as multi-line XML — a text regex cannot parse that reliably, and you should use an EVTX reader for it. In practice a SIEM ingests events after a forwarder (NXLog, Winlogbeat, Snare) has flattened each one to a single line of key=value or JSON. This page parses that forwarded single-line form, which is what actually hits your pipeline.
What does LogonType mean in event 4624 / 4625?
LogonType classifies how the logon happened: 2 is interactive (console), 3 is network (e.g. SMB/share access), 10 is RemoteInteractive (RDP), 4 is batch (scheduled task), and 5 is service. It is central to detection — a network or RDP logon from an unexpected source, or a burst of type-3 failures, reads very differently from a normal interactive login.
How do I tell password spraying from user enumeration in 4625 events?
Look at the NTSTATUS Status/SubStatus code plus the username spread. 0xC0000064 means the user does not exist (enumeration), 0xC000006A means the password was wrong for a real account, and 0xC000006D is a generic bad-username/logon failure. Many 4625s from one IpAddress across many distinct TargetUserNames indicates spraying; repeated 0xC0000064 against different names indicates enumeration.
Which fields matter most for Windows logon detection?
EventID (4624 success vs 4625 failure), LogonType, TargetUserName, SubjectUserName, IpAddress, and WorkstationName, plus Status/SubStatus on failures. Correlating EventID + LogonType + IpAddress + TargetUserName covers brute force, spraying, enumeration, and anomalous RDP or network logons.

Try it on your own Windows Security Event lines

Paste a few real lines, review the detected fields, and copy whichever format your stack needs. Free, no account, nothing uploaded.

Open this sample in LogForge →