bokamba / logforge / parse / LEEF / IBM QRadar

$ logforge parse leef

Parse LEEF / IBM QRadar logs → regex, Grok, Wazuh & rsyslog

LEEF, the Log Event Extended Format, is IBM QRadar's answer to CEF: a normalized envelope that products emit so QRadar (and other SIEMs) can ingest events without a bespoke DSM parser. You will encounter it from QRadar-integrated appliances and from vendors who ship a LEEF profile specifically to be QRadar-friendly. Like CEF it is a header-plus-attributes format, but the details differ enough that a CEF parser cannot read it. A LEEF 2.0 line begins with the literal 'LEEF:2.0' version marker followed by a pipe-delimited header — LEEF:2.0|Vendor|Product|Version|EventID — and then, crucially, an optional fifth pipe field that declares the delimiter used in the attributes section. In the example LEEF:2.0|IBM|QRadar|7.5.0|NewEvent|^| that trailing ^ says 'the key=value attributes are separated by a caret', not by tabs or spaces.

That configurable delimiter is the single most important thing to get right when parsing LEEF, and the biggest difference from CEF. LEEF 1.0 always separated attributes with a tab; LEEF 2.0 lets the emitter pick — tab, caret, or another character — and announces the choice in that fifth header slot. So devTime=1783085000000^src=203.0.113.66^dst=192.0.2.30^sev=9^cat=IPS^msg=Log4j RCE attempt blocked at perimeter is a caret-delimited attribute list, and a parser that assumes tabs (or splits on spaces) will read the whole thing as one field. QRadar also defines a set of normalized attribute keys — devTime, src, dst, srcPort, dstPort, sev, cat, usrName — that map onto QRadar's own event model, and it expects devTime in a parseable form (here an epoch-milliseconds value, 1783085000000).

For detection the QRadar-normalized keys are exactly the fields you want: src and dst for the connection, sev for QRadar's 1–10 severity, cat for the event category (IPS here), and msg for the human description ('Log4j RCE attempt blocked at perimeter'). devTime gives you the true event time independent of when it was received. Because LEEF's keys are standardized, correlation rules written against src, dst, sev, and cat work across every LEEF source feeding the SIEM — the parsing challenge is almost entirely about honoring the declared delimiter and normalizing devTime, not about the field semantics.

Open this in LogForge →

What a LEEF / IBM QRadar line looks like

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

LEEF:2.0|IBM|QRadar|7.5.0|NewEvent|^|devTime=1783085000000^src=203.0.113.66^dst=192.0.2.30^sev=9^cat=IPS^msg=Log4j RCE attempt blocked at perimeter
LEEF:2.0|IBM|QRadar|7.5.0|NewEvent|^|devTime=1783085062000^src=203.0.113.90^dst=192.0.2.31^sev=7^cat=Malware^msg=Suspicious outbound beacon detected

Detected fields

The engine classified this sample as kv and consolidated 12 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.

  • leef_version : number · literal
  • leef_vendor : literal · literal
  • leef_product : literal · literal
  • leef_device_version : literal · literal
  • leef_event_id : literal · literal
  • leef_delimiter : literal · literal
  • devtime : timestamp
  • src : ipv4
  • dst : ipv4
  • sev : number
  • cat : literal
  • msg : literal

Regex (named capture groups)

# sample: LEEF:2.0|IBM|QRadar|7.5.0|NewEvent|^|devTime=1783085000000^src=203.0.113.66^dst=192.0.2.30^sev=9^cat=IPS^msg=Log4j RCE attempt blocked at perimeter
# groups: devtime=1783085000000, src=203.0.113.66, dst=192.0.2.30, sev=9, cat=IPS, msg=Log4j RCE attempt blocked at perimeter
^LEEF:2\.0\|IBM\|QRadar\|7\.5\.0\|NewEvent\|\^\|devTime=(?<devtime>\d+)\^src=(?<src>\d{1,3}(?:\.\d{1,3}){3})\^dst=(?<dst>\d{1,3}(?:\.\d{1,3}){3})\^sev=(?<sev>-?\d+(?:\.\d+)?)\^cat=(?<cat>[A-Za-z]+)\^msg=(?<msg>(?:[A-Za-z]+\d+[A-Za-z]+ [A-Za-z]+ [A-Za-z]+ [A-Za-z]+ [A-Za-z]+ [A-Za-z]+|[A-Za-z]+ [A-Za-z]+ [A-Za-z]+ [A-Za-z]+))$

Grok pattern (Logstash / Elastic)

# custom patterns
LEEF_EPOCH \d{10}(?:\d{3})?

LEEF:2\.0\|IBM\|QRadar\|7\.5\.0\|NewEvent\|\^\|devTime=%{LEEF_EPOCH:devtime}\^src=%{IPV4:src}\^dst=%{IPV4:dst}\^sev=%{NUMBER:sev}\^cat=%{DATA:cat}\^msg=%{GREEDYDATA:msg}
  • note kv-structured input — consider the Logstash kv filter instead of (or after) grok
  • note constant field "leef_version" embedded as literal anchor "2.0" (varying=false)
  • note custom patterns emitted — save the '# custom patterns' block to a file in your patterns_dir

Wazuh decoder (OS_Regex XML)

<!--
  Generated by LogForge - Wazuh decoder (OS_Regex dialect, not PCRE)
  sample: LEEF:2.0|IBM|QRadar|7.5.0|NewEvent|^|devTime=1783085000000^src=203.0.113.66^dst=192.0.2.30^sev=9^cat=IPS^msg=Log4j RCE attempt blocked at perimeter
  test with: /var/ossec/bin/wazuh-logtest
-->

<decoder name="leef-kv">
  <prematch>^LEEF:\d+.\d+\|\w+\|\w+\|\w+.\w+.\w+\|\w+\|</prematch>
</decoder>

<decoder name="leef-kv">
  <parent>leef-kv</parent>
  <regex offset="after_parent">\|devTime=(\d+)</regex>
  <order>devtime</order>
</decoder>

<decoder name="leef-kv">
  <parent>leef-kv</parent>
  <regex offset="after_parent">src=(\d+.\d+.\d+.\d+)</regex>
  <order>srcip</order>
</decoder>

<decoder name="leef-kv">
  <parent>leef-kv</parent>
  <regex offset="after_parent">dst=(\d+.\d+.\d+.\d+)</regex>
  <order>dstip</order>
</decoder>

<decoder name="leef-kv">
  <parent>leef-kv</parent>
  <regex offset="after_parent">sev=(\d+)</regex>
  <order>sev</order>
</decoder>

<decoder name="leef-kv">
  <parent>leef-kv</parent>
  <regex offset="after_parent">cat=(\w+)</regex>
  <order>cat</order>
</decoder>

<decoder name="leef-kv">
  <parent>leef-kv</parent>
  <regex offset="after_parent">msg=(\.+)</regex>
  <order>msg</order>
</decoder>
  • note constant field "leef_version" skipped (identical in every line)
  • note constant field "leef_vendor" skipped (identical in every line)
  • note constant field "leef_product" skipped (identical in every line)
  • note constant field "leef_device_version" skipped (identical in every line)
  • note constant field "leef_event_id" skipped (identical in every line)
  • note constant field "leef_delimiter" skipped (identical in every line)
  • note field "src": '^' delimiter cannot start an OS_Regex (it would anchor); matching bare "src=" instead
  • note field "src" mapped to Wazuh conventional field "srcip"
  • note field "dst": '^' delimiter cannot start an OS_Regex (it would anchor); matching bare "dst=" instead
  • note field "dst" mapped to Wazuh conventional field "dstip"
  • note field "sev": '^' delimiter cannot start an OS_Regex (it would anchor); matching bare "sev=" instead
  • note field "cat": '^' delimiter cannot start an OS_Regex (it would anchor); matching bare "cat=" instead
  • note field "msg": '^' delimiter cannot start an OS_Regex (it would anchor); matching bare "msg=" instead
  • note field "msg": free-text capture (\.+) anchored at end of line — OS_Regex quantifiers are greedy, keep this field last
  • 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 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

rsyslog template / liblognorm rulebase

version=2
# leef — liblognorm v2 rulebase (generated by LogForge)
# Usage with rsyslog (mmnormalize runs liblognorm):
#   module(load="mmnormalize")
#   action(type="mmnormalize" rulebase="/etc/rsyslog.d/leef.rb" useRawMsg="on")
# Literal "%" is escaped as "%%"; raw tabs are written as \x09.
rule=leef:LEEF:2.0|IBM|QRadar|7.5.0|NewEvent|^|devTime=%devtime:number%^src=%src:ipv4%^dst=%dst:ipv4%^sev=%sev:number%^cat=%cat:char-to{"extradata":"^"}%^msg=%msg:rest%
  • 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: devtime=number, src=ipv4, dst=ipv4, sev=number, cat=char-to(^), msg=rest

FAQ

What is the delimiter in a LEEF log?
It depends on the version and the header. LEEF 1.0 always uses a tab between attributes. LEEF 2.0 lets the emitter choose and declares the choice in an optional fifth header field — in LEEF:2.0|IBM|QRadar|7.5.0|NewEvent|^| the ^ means attributes are caret-delimited. Always read that field before splitting; assuming tabs is the classic LEEF parsing bug.
How does LEEF differ from CEF?
Both are header + key=value envelopes for SIEM ingestion, but LEEF is IBM QRadar-oriented while CEF comes from ArcSight. LEEF has a version marker and a configurable attribute delimiter declared in the header; CEF has a six-field header and a space-separated extension split on key boundaries. Their header layouts and key dictionaries differ, so they need separate parsers.
Why is the LEEF devTime a big number instead of a date?
That is epoch time in milliseconds — 1783085000000 is a Unix timestamp times 1000. LEEF permits devTime either as an epoch value or as a formatted string with an accompanying devTimeFormat attribute. To make it human-readable, divide by 1000 and convert from Unix seconds, or parse the declared format if one is supplied.
Which LEEF attributes should I map for QRadar correlation?
The normalized keys QRadar understands: src and dst (IP addresses), srcPort and dstPort, sev (1–10 severity), cat (category), usrName (user), and devTime (event time). Mapping your source onto these standard keys is what lets QRadar apply built-in rules; msg carries the free-text description for context.

Try it on your own LEEF / IBM QRadar 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 →