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 →