Files
f3nris f7c5e99cc5 feat(harfanglab): map the fields the alert and threat payloads actually carry (v1.4.0)
Replayed a real get_alerts and a real get_threats response through the
bundled mappers to see what came out.

get_alerts: a sigma alert matching a Windows event log carries the whole
record under `eventlog`, and none of it was mapped. On a Defender 1116
detection that left the malware, the file, the acting user and the
detecting product invisible — the incident indexed a hostname and
nothing else. The event data uses the provider's own field names, spaces
included, so the new entries quote them with JSONata backticks, and
Defender's "file:_" path prefix is stripped. Also added the agent IP
(placed before details_connection.SourceIp so a network alert's own
source still wins), the DNS domain, device type, agent version and
groups, the tenant, the alert subtype and threat type, the string
severity and the event time.

get_threats: `level` had no entry at all, so a threat's severity never
reached the OCSF document even though field_mappings derived the
incident's 1-5 from it. Added it, the event time, and the top agent's
reachability — whether the endpoint is online decides what response is
possible, and it was excluded without a reason.

Both trailers now record why each remaining field is left out, and the
event log's own SYSTEM writer and FQDN spelling are called out: indexing
them would create a junk user entity and a second spelling of one host.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-06 17:30:01 +02:00

65 lines
5.5 KiB
YAML

name: "HarfangLab Threats → OCSF"
description: "Maps a raw HarfangLab EDR threat (/api/data/alert/alert/Threat/, results_path = results) to OCSF. Uses native threat fields; top_agents/top_impacted_users/top_rules are ranked aggregates — the first (most impacted) entry feeds the endpoint/user/rule fields. mitre_tactics is a map keyed by tactic slug (e.g. attack.persistence); its keys are extracted with $keys()."
field_mappings:
title: "top_rules[0].rule_name ? top_rules[0].rule_name & ' — ' & slug : slug"
severity: "level = 'critical' ? 5 : (level = 'high' ? 4 : (level = 'medium' ? 3 : 2))"
description: "top_rules[0].rule_msg ? top_rules[0].rule_msg : status"
# results_path = results; source_path is JSONata over ONE threat object.
# Paths absent from a given threat are skipped at ingestion, so extra entries are safe.
ocsf:
# ── Finding ───────────────────────────────────────────────────────
- { source_path: "id", ocsf_field: "finding_info.uid" }
- { source_path: "slug", ocsf_field: "finding_info.title" }
- { source_path: "creation_date", ocsf_field: "finding_info.created_time" }
- { source_path: "creation_date", ocsf_field: "time" }
- { source_path: "first_seen", ocsf_field: "finding_info.first_seen_time" }
- { source_path: "last_seen", ocsf_field: "finding_info.last_seen_time" }
# x ? x guards: these are null on open/mono-tenant threats — null is falsy in
# JSONata, so the condition yields undefined and the entry is skipped instead
# of writing a literal null into the OCSF document.
- { source_path: "closed_date ? closed_date", ocsf_field: "end_time" }
- { source_path: "status", ocsf_field: "status" }
# field_mappings.severity turns this into the incident's 1-5 number; the string
# itself has no other home, so without this entry the threat's own level never
# reaches the OCSF document at all.
- { source_path: "level", ocsf_field: "severity" } # low | medium | high | critical
- { source_path: "linked_threat ? linked_threat", ocsf_field: "finding_info.uid_alt" }
- { source_path: "tenant ? tenant", ocsf_field: "metadata.tenant_uid" }
# ── Aggregate counts ──────────────────────────────────────────────
- { source_path: "total_security_event_count", ocsf_field: "finding_info.related_events_count" }
- { source_path: "agent_count", ocsf_field: "unmapped.agent_count" }
- { source_path: "impacted_user_count", ocsf_field: "unmapped.impacted_user_count" }
- { source_path: "rule_count", ocsf_field: "unmapped.rule_count" }
# ── MITRE ATT&CK — mitre_tactics is a MAP {"attack.persistence": {...}} ──
# [$keys(...)] keeps an array shape even for a single tactic (JSONata collapses
# singleton sequences to a scalar); the guard skips threats without tactics.
- { source_path: "mitre_tactics ? [$keys(mitre_tactics)]", ocsf_field: "attacks" }
# ── Top impacted endpoint (most security events first) ────────────
- { source_path: "top_agents[0].agent_hostname", ocsf_field: "src_endpoint.hostname" }
- { source_path: "top_agents[0].agent_id", ocsf_field: "device.uid" }
- { source_path: "top_agents[0].agent_osproducttype", ocsf_field: "src_endpoint.os.name" }
- { source_path: "top_agents[0].agent_ostype", ocsf_field: "src_endpoint.os.type" }
- { source_path: "top_agents[0].agent_osversion", ocsf_field: "device.os.version" }
# Whether the agent is still reachable decides what response is even possible
# (isolate, collect, kill) — the first thing asked of a threat, so it travels
# with the endpoint rather than being left to the console.
- { source_path: "top_agents[0].agent_status", ocsf_field: "unmapped.agent_status" }
# ── Top impacted user ─────────────────────────────────────────────
- { source_path: "top_impacted_users[0].user_name", ocsf_field: "user.name" }
- { source_path: "top_impacted_users[0].user_sid", ocsf_field: "user.uid" }
# ── Top rule ──────────────────────────────────────────────────────
- { source_path: "top_rules[0].rule_name", ocsf_field: "rule.name" }
- { source_path: "top_rules[0].rule_id", ocsf_field: "rule.uid" }
- { source_path: "top_rules[0].rule_type", ocsf_field: "finding_info.analytic.type" }
- { source_path: "top_rules[0].rule_msg", ocsf_field: "finding_info.desc" }
# Deliberately unmapped: old_id/origin_stack (legacy/console internals),
# top_rules[0].id (numeric console id — rule_id is the stable uid, mapped above),
# per-item security_event_count (equals the total while agent_count is 1, and the total
# is already mapped), top_rules[0].rule_level (the same value as the threat's own level,
# mapped to severity above), rule_os (the same value as the agent's ostype), the rule's
# creation_date/last_update (rule bookkeeping, not incident evidence),
# mitre_tactics[*].security_event_percentage (the tactic names are what pivot; the
# per-tactic weights have no OCSF home), top_rules[0].description (duplicate of rule_msg),
# and entries [1..] of the top_* arrays (OCSF detection_finding models a single primary
# endpoint/user/rule; counts convey scale).