f7c5e99cc5
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>
65 lines
5.5 KiB
YAML
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).
|