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).