feat(harfanglab): split image_name into the process path and the threat name (v1.4.1)

image_name was left unmapped because HarfangLab prefixes it with the
matched threat name — "[PUADlManager:Win32/OfferCore] C:\Windows\
explorer.exe" — and the clean path was available from the event data.
That reasoning only held for event-log alerts. On the yara, binary and
ioc subtypes there is neither a `process` object nor an `eventlog`
block, so those alerts reached the incident with no process at all.

Both halves are now pulled out, each guarded on the bracket actually
being there: $substringBefore and $substringAfter return the whole
string when the pattern is absent, so without the guard an unprefixed
path would land verbatim in malware.name.

The three entries lead the process block so the more specific sources
below — the matched process object, then the event log's own fields —
still overwrite them when present. malware.name consequently surfaces on
every subtype instead of only where Defender is the source.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-06 18:15:44 +02:00
parent f7c5e99cc5
commit 82f22fdbb3
2 changed files with 16 additions and 4 deletions
@@ -51,6 +51,18 @@ ocsf:
- { source_path: "tags", ocsf_field: "metadata.labels" }
- { source_path: "log_type", ocsf_field: "metadata.log_name" }
# ── Matched process — the actor ───────────────────────────────────
# image_name is the one process path present on EVERY alert subtype, and the
# only one on those carrying neither a `process` object nor an event log. When
# a threat matched, HarfangLab prefixes it with the threat name in brackets —
# "[PUADlManager:Win32/OfferCore] C:\Windows\explorer.exe" — so both halves are
# pulled out, each guarded on the bracket actually being there: without the
# $contains guard, $substringBefore/$substringAfter fall through on an
# unprefixed path and write the whole path into malware.name.
# These come first in the block so the more specific sources below (the matched
# process object, then the event log's own fields) overwrite them when present.
- { source_path: "$contains(image_name, '] ') ? $substringAfter(image_name, '] ') : image_name", ocsf_field: "actor.process.file.path" }
- { source_path: "$split($contains(image_name, '] ') ? $substringAfter(image_name, '] ') : image_name, '\\\\')[-1]", ocsf_field: "actor.process.name" }
- { source_path: "$contains(image_name, '[') ? $substringBefore($substringAfter(image_name, '['), ']')", ocsf_field: "malware.name" }
- { source_path: "process.process_name", ocsf_field: "actor.process.name" }
- { source_path: "process.image_name", ocsf_field: "actor.process.file.path" }
- { source_path: "process.commandline", ocsf_field: "actor.process.cmd_line" }
@@ -113,8 +125,8 @@ ocsf:
- { source_path: "eventlog.event_data.`Security intelligence Version`", ocsf_field: "unmapped.defender.signature_version" }
# Alert fields deliberately left unmapped: `username` (actor.user.name is taken by the
# more specific process.username), remaining details_* sub-objects (no clean OCSF home),
# `image_name` and eventlog.threat_process_name (both prefixed with the threat name —
# the clean path comes from the event data), `eventlog.user` (the account that WROTE the
# eventlog.threat_process_name (the same prefixed string as image_name, already split
# above), `eventlog.user` (the account that WROTE the
# record, usually SYSTEM — indexing it would create a junk user entity),
# `eventlog.computer_name` (the FQDN of the host already carried by agent.hostname; two
# spellings of one machine split correlation), `level_int` / `quarantine` / `execution`