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>

<!-- ============================================================
     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="leef,">
  <rule id="100000" level="3">
    <decoded_as>leef-kv</decoded_as>
    <description>leef: srcip=$(srcip) dstip=$(dstip)</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="srcip">^CHANGE_ME$</field>
    <description>leef: a value you care about from $(srcip)</description>
  </rule>
  -->
</group>
  • 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 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
# 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

Splunk

# props.conf  (search-time extraction)
[<REPLACE_WITH_SOURCETYPE>]
EXTRACT-logforge = 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]+))

# Quick search-time test in SPL:
# | rex field=_raw "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]+))"
  • note EXTRACT-<class> names must be unique within a sourcetype stanza — rename EXTRACT-logforge if you already use that class for this sourcetype
  • note a timestamp field was detected: this EXTRACT only makes it a searchable field. To set the event _time at index time, add TIME_PREFIX and TIME_FORMAT to this props.conf stanza (TIME_FORMAT uses Splunk strptime, e.g. %Y-%m-%dT%H:%M:%S) — this generator does not guess the strptime format.

ES ingest

PUT _ingest/pipeline/leef
{
  "description": "LogForge-generated ingest pipeline for leef",
  "processors": [
    {
      "kv": {
        "field": "message",
        "field_split": " ",
        "value_split": "="
      }
    }
  ]
}
  • note grok: kv-structured input — consider the Logstash kv filter instead of (or after) grok
  • note grok: constant field "leef_version" embedded as literal anchor "2.0" (varying=false)
  • note grok: custom patterns emitted — save the '# custom patterns' block to a file in your patterns_dir
  • 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/leef/_simulate (supply a docs[] array whose _source.message holds a sample line)

Graylog

# Grok patterns to add under System > Grok Patterns:
# (Graylog needs these custom patterns installed globally BEFORE the rule/extractor below will work.)
#   LEEF_EPOCH \d{10}(?:\d{3})?

# --- Graylog processing pipeline rule (primary) ---
# Paste under System > Pipelines > Manage rules, then attach the rule to a pipeline stage.
rule "leef-parse"
when
  has_field("message")
then
  let gp = grok(pattern: "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}", 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": "leef",
      "extractor_type": "grok",
      "converters": [],
      "order": 0,
      "cursor_strategy": "copy",
      "source_field": "message",
      "target_field": "",
      "extractor_config": {
        "grok_pattern": "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}",
        "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 grok: constant field "leef_version" embedded as literal anchor "2.0" (varying=false)
  • note grok: custom patterns emitted — save the '# custom patterns' block to a file in your patterns_dir
  • note 1 custom grok pattern(s) (LEEF_EPOCH) must be installed globally first under System > Grok Patterns — see the block at the top of the output
  • 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 LEEF:2\.0\|IBM\|QRadar\|7\.5\.0\|NewEvent\|\^\|devTime=%{data:devtime}\^src=%{ipv4:src}\^dst=%{ipv4:dst}\^sev=%{number:sev}\^cat=%{notSpace:cat}\^msg=%{notSpace:msg}
  • note emitted rule name is "logforge_rule"; rename it to match your "leef" 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 constant field "leef_version" embedded as literal anchor "2.0" (varying=false)
  • note field "devtime" (timestamp): could not derive a Joda/Java date format from the sample shape; using %{data} — add a date("…") format by hand if you need a parsed timestamp
  • 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        leef
    Format      regex
    Regex       ^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]+))$
    Time_Key    devtime
    # Time_Format <unrecognized timestamp shape — set a strptime format, e.g. %Y-%m-%dT%H:%M:%S>
# Fluentd <parse> block:
#   <parse>
#     @type regexp
#     expression /^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]+))$/
#     time_key devtime
#   </parse>
  • note Time_Key set to "devtime", but the timestamp shape was not recognized — fill in Time_Format (strptime) yourself; for epoch values consider Fluent Bit's time_as_integer / a Lua filter instead

Vector

[transforms.leef_parse]
type = "remap"
inputs = ["REPLACE_WITH_SOURCE"]
source = '''
. |= parse_regex!(.message, r'LEEF:2\.0\|IBM\|QRadar\|7\.5\.0\|NewEvent\|\^\|devTime=(?P<devtime>\d+)\^src=(?P<src>\d{1,3}(?:\.\d{1,3}){3})\^dst=(?P<dst>\d{1,3}(?:\.\d{1,3}){3})\^sev=(?P<sev>-?\d+(?:\.\d+)?)\^cat=(?P<cat>[A-Za-z]+)\^msg=(?P<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]+))')
'''
  • note kv input — parse_key_value!(.message) is the idiomatic Vector parser and is usually preferable to a regex

Loki

# promtail pipeline for "leef" (generated by LogForge)
# Add these stages under a scrape_config in your promtail config:
#   scrape_configs:
#     - job_name: leef
#       pipeline_stages:
# (the stages below are indented to sit under pipeline_stages)
pipeline_stages:
  - regex:
      expression: '^LEEF:2\.0\|IBM\|QRadar\|7\.5\.0\|NewEvent\|\^\|devTime=(?P<devtime>\d+)\^src=(?P<src>\d{1,3}(?:\.\d{1,3}){3})\^dst=(?P<dst>\d{1,3}(?:\.\d{1,3}){3})\^sev=(?P<sev>-?\d+(?:\.\d+)?)\^cat=(?P<cat>[A-Za-z]+)\^msg=(?P<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]+))$'
  • 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): devtime, src, dst, sev, cat, msg

syslog-ng

parser p_leef {
    regexp-parser(
        prefix(".leef.")
        patterns("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]+))")
    );
};
  • note captured fields are stored as name-value pairs under the prefix ".leef." (e.g. a group (?<srcip>…) becomes ".leef.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

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 →