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>
This commit is contained in:
@@ -1,8 +1,8 @@
|
||||
id: harfanglab
|
||||
name: HarfangLab EDR
|
||||
version: 1.3.1
|
||||
version: 1.4.0
|
||||
description: "HarfangLab EDR — endpoint detection & response: alert/threat ingestion, endpoint enrichment, isolation, threat-intelligence (IOC/whitelist), telemetry hunting and forensic collection jobs."
|
||||
changelog: "1.3.1 — Flagged get_alerts as ingestion-ready (ingest hints: results_path/dedup/incremental on from_date) so it appears in the recommended ingestion commands when creating an instance. 1.3.0 — Response & triage expansion: kill/dump process, on-demand AV/YARA scans, file/service/scheduled-task deletion, quarantine management, threat-level isolation, job cancel/relaunch/wait; alert search (get_alerts) and full alert/threat triage (tag, comments, notes, status, level, history, details); exhaustive alert OCSF mappers; reworked get_threats mapper (fixed MITRE tactics map handling, full top-agent/user/rule and counts coverage, rule-based incident title/description). 1.2.2 — Documentation wording cleanup. 1.2.1 — Corrected the get_threats OCSF mapper to native threat fields (top_agents/top_impacted_users/top_rules/mitre_tactics) instead of enriched-only arrays; added agent.osversion to get_security_events. 1.2.0 — Added ingestion commands get_security_events (alerts) and get_threats with bundled OCSF mappers and ingest hints (results_path/dedup/incremental). 1.1.1 — Added test_connection for the instance Test button. 1.1.0 — Command names prefixed with 'harfanglab-' (e.g. harfanglab-isolate-endpoint) for easier toolbox search; command IDs unchanged. 1.0.0 — Initial release: endpoint/agent management, isolation, policy assignment, IOC & whitelist management, security-event triage, telemetry hunting (processes, network, DNS, authentications, binaries, event logs), threat hunting by hash, and forensic collection jobs (pipes, prefetch, run keys, scheduled tasks, drivers, services, processes, network, sessions, WMI, IOC scan, artifacts, RAM dump) with their result retrieval commands. Compatible with HarfangLab EDR 2.13.7+."
|
||||
changelog: "1.4.0 — Mapper coverage from real payloads: get_alerts now maps the Windows event-log payload sigma alerts carry (alert_subtype = eventlog), which was entirely unmapped — Defender 1116/1117 yields the malware name, classification, severity and provider, the detected file, the process that touched it and the acting user, plus the log provider and event code. Also added the agent IP, 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 gained the threat level (previously never reaching the OCSF document at all), the event time and the top agent's reachability. 1.3.1 — Flagged get_alerts as ingestion-ready (ingest hints: results_path/dedup/incremental on from_date) so it appears in the recommended ingestion commands when creating an instance. 1.3.0 — Response & triage expansion: kill/dump process, on-demand AV/YARA scans, file/service/scheduled-task deletion, quarantine management, threat-level isolation, job cancel/relaunch/wait; alert search (get_alerts) and full alert/threat triage (tag, comments, notes, status, level, history, details); exhaustive alert OCSF mappers; reworked get_threats mapper (fixed MITRE tactics map handling, full top-agent/user/rule and counts coverage, rule-based incident title/description). 1.2.2 — Documentation wording cleanup. 1.2.1 — Corrected the get_threats OCSF mapper to native threat fields (top_agents/top_impacted_users/top_rules/mitre_tactics) instead of enriched-only arrays; added agent.osversion to get_security_events. 1.2.0 — Added ingestion commands get_security_events (alerts) and get_threats with bundled OCSF mappers and ingest hints (results_path/dedup/incremental). 1.1.1 — Added test_connection for the instance Test button. 1.1.0 — Command names prefixed with 'harfanglab-' (e.g. harfanglab-isolate-endpoint) for easier toolbox search; command IDs unchanged. 1.0.0 — Initial release: endpoint/agent management, isolation, policy assignment, IOC & whitelist management, security-event triage, telemetry hunting (processes, network, DNS, authentications, binaries, event logs), threat hunting by hash, and forensic collection jobs (pipes, prefetch, run keys, scheduled tasks, drivers, services, processes, network, sessions, WMI, IOC scan, artifacts, RAM dump) with their result retrieval commands. Compatible with HarfangLab EDR 2.13.7+."
|
||||
category: endpoint
|
||||
|
||||
# Per-instance configuration. Scripts use <url> as the API base and call /api/... paths.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
name: "HarfangLab Alerts → OCSF"
|
||||
description: "Maps a HarfangLab EDR alert (/api/data/alert/alert/Alert/, results_path = results) to OCSF. The matched process is the actor (actor.*). Detail sub-objects are only present for their event kind; absent paths are skipped at ingestion."
|
||||
description: "Maps a HarfangLab EDR alert (/api/data/alert/alert/Alert/, results_path = results) to OCSF. The matched process is the actor (actor.*). Detail sub-objects and the eventlog payload are only present for their event kind; absent paths are skipped at ingestion."
|
||||
field_mappings:
|
||||
title: "rule_name"
|
||||
severity: "level = 'critical' ? 5 : (level = 'high' ? 4 : (level = 'medium' ? 3 : 2))"
|
||||
@@ -10,16 +10,23 @@ ocsf:
|
||||
# ── Endpoint ──────────────────────────────────────────────────────
|
||||
- { source_path: "agent.hostname", ocsf_field: "src_endpoint.hostname" }
|
||||
- { source_path: "agent.domainname", ocsf_field: "src_endpoint.domain" }
|
||||
# Before details_connection.SourceIp below, so a network alert's own source
|
||||
# address wins over the agent's; on every other alert this is the only IP.
|
||||
- { source_path: "agent.ipaddress", ocsf_field: "src_endpoint.ip" }
|
||||
- { source_path: "agent.osproducttype", ocsf_field: "src_endpoint.os.name" }
|
||||
- { source_path: "agent.ostype", ocsf_field: "src_endpoint.os.type" }
|
||||
- { source_path: "agent.osversion", ocsf_field: "device.os.version" }
|
||||
- { source_path: "agent.agentid", ocsf_field: "device.uid" }
|
||||
- { source_path: "agent.ipaddress", ocsf_field: "device.ip" }
|
||||
- { source_path: "agent.dnsdomainname", ocsf_field: "device.domain" }
|
||||
- { source_path: "agent.producttype", ocsf_field: "device.type" } # workstation | server
|
||||
# ── Finding ───────────────────────────────────────────────────────
|
||||
- { source_path: "id", ocsf_field: "finding_info.uid" }
|
||||
- { source_path: "alert_unique_id", ocsf_field: "metadata.uid" }
|
||||
- { source_path: "rule_name", ocsf_field: "finding_info.title" }
|
||||
- { source_path: "msg", ocsf_field: "finding_info.desc" }
|
||||
- { source_path: "alert_time", ocsf_field: "finding_info.created_time" }
|
||||
- { source_path: "alert_time", ocsf_field: "time" }
|
||||
- { source_path: "detection_date", ocsf_field: "finding_info.first_seen_time" }
|
||||
- { source_path: "last_update", ocsf_field: "finding_info.modified_time" }
|
||||
- { source_path: "ingestion_date", ocsf_field: "metadata.logged_time" }
|
||||
@@ -27,8 +34,16 @@ ocsf:
|
||||
- { source_path: "rule_id", ocsf_field: "finding_info.analytic.uid" }
|
||||
- { source_path: "rule_content", ocsf_field: "finding_info.analytic.desc" }
|
||||
- { source_path: "detection_origin", ocsf_field: "finding_info.analytic.category" }
|
||||
- { source_path: "alert_subtype", ocsf_field: "unmapped.alert_subtype" } # eventlog | connection | dns | file | registry | url_request
|
||||
- { source_path: "status", ocsf_field: "status" }
|
||||
- { source_path: "maturity", ocsf_field: "status_detail" }
|
||||
- { source_path: "threat_type", ocsf_field: "unmapped.threat_type" }
|
||||
- { source_path: "level", ocsf_field: "severity" } # low | medium | high | critical
|
||||
- { source_path: "tenant ? tenant", ocsf_field: "metadata.tenant_uid" }
|
||||
- { source_path: "agent.version", ocsf_field: "unmapped.agent_version" }
|
||||
# groups is an array of {id, name}; the [ ] keeps an array shape when a single
|
||||
# group matches, since JSONata collapses a singleton sequence to a scalar.
|
||||
- { source_path: "groups ? [groups.name]", ocsf_field: "unmapped.agent_groups" }
|
||||
- { source_path: "score", ocsf_field: "risk_score" }
|
||||
- { source_path: "confidence_int", ocsf_field: "confidence_score" }
|
||||
- { source_path: "threat_key", ocsf_field: "finding_info.uid_alt" } # threat aggregation key
|
||||
@@ -67,5 +82,43 @@ ocsf:
|
||||
- { source_path: "details_url_request.host", ocsf_field: "url.hostname" }
|
||||
- { source_path: "details_url_request.user_agent", ocsf_field: "http_request.user_agent" }
|
||||
- { source_path: "details_url_request.verb", ocsf_field: "http_request.http_method" }
|
||||
# ── Event details: Windows event log (alert_subtype = eventlog) ───
|
||||
# A sigma rule matching a Windows event log carries the whole record under
|
||||
# `eventlog`, and the provider's own field names — spaces included — under
|
||||
# eventlog.event_data. Backticks quote those names in JSONata. The block below
|
||||
# is Microsoft Defender (events 1116/1117), where the detection names the
|
||||
# malware, the file it was found in, the process that touched it and the user
|
||||
# it ran as. None of it exists on other subtypes, so it is skipped there.
|
||||
- { source_path: "eventlog.source_name", ocsf_field: "metadata.log_provider" }
|
||||
- { source_path: "eventlog.event_id", ocsf_field: "metadata.event_code" }
|
||||
- { source_path: "eventlog.event_data.`Product Name`", ocsf_field: "metadata.product.name" }
|
||||
- { source_path: "eventlog.event_data.`Product Version`", ocsf_field: "metadata.product.version" }
|
||||
- { source_path: "eventlog.event_data.`Threat Name`", ocsf_field: "malware.name" }
|
||||
- { source_path: "eventlog.event_data.`Threat ID`", ocsf_field: "malware.uid" }
|
||||
- { source_path: "eventlog.event_data.`Severity Name`", ocsf_field: "malware.severity" }
|
||||
- { source_path: "eventlog.event_data.`Product Name`", ocsf_field: "malware.provider" }
|
||||
- { source_path: "eventlog.event_data.`Category Name` ? [eventlog.event_data.`Category Name`]", ocsf_field: "malware.classifications" }
|
||||
# Defender prefixes the detected path with "file:_". $substringAfter returns the
|
||||
# whole string when the prefix is absent, so stripping it is safe either way.
|
||||
- { source_path: "$substringAfter(eventlog.event_data.`Path`, 'file:_')", ocsf_field: "file.path" }
|
||||
- { source_path: "$substringAfter(eventlog.event_data.`Path`, 'file:_')", ocsf_field: "malware.path" }
|
||||
- { source_path: "$split($substringAfter(eventlog.event_data.`Path`, 'file:_'), '\\\\')[-1]", ocsf_field: "file.name" }
|
||||
- { source_path: "eventlog.event_data.`Process Name`", ocsf_field: "actor.process.file.path" }
|
||||
- { source_path: "$split(eventlog.event_data.`Process Name`, '\\\\')[-1]", ocsf_field: "actor.process.name" }
|
||||
- { source_path: "eventlog.event_data.`Detection User`", ocsf_field: "actor.user.name" }
|
||||
- { source_path: "eventlog.event_data.`Action Name`", ocsf_field: "action" }
|
||||
- { source_path: "eventlog.event_data.`Detection ID`", ocsf_field: "unmapped.defender.detection_uid" }
|
||||
- { source_path: "eventlog.event_data.`Source Name`", ocsf_field: "unmapped.defender.detection_source" } # real-time protection, scheduled scan…
|
||||
- { source_path: "eventlog.event_data.`Execution Name`", ocsf_field: "unmapped.defender.execution_state" }
|
||||
- { 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).
|
||||
# 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
|
||||
# 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`
|
||||
# and Defender's Action ID / Severity ID / Category ID (console enums on scales OCSF does
|
||||
# not share), `detection_timestamp` and Defender's Detection Time (duplicates of
|
||||
# detection_date), `confidence` (the string form of confidence_int, already mapped), and
|
||||
# Defender's Error/Status/Origin/Remediation/Unused fields.
|
||||
|
||||
@@ -11,6 +11,7 @@ ocsf:
|
||||
- { 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
|
||||
@@ -18,6 +19,10 @@ ocsf:
|
||||
# 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 ──────────────────────────────────────────────
|
||||
@@ -35,6 +40,10 @@ ocsf:
|
||||
- { 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" }
|
||||
@@ -43,8 +52,13 @@ ocsf:
|
||||
- { 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), agent_status,
|
||||
# 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, rule_os/rule_level/rule creation dates (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).
|
||||
# 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).
|
||||
|
||||
Reference in New Issue
Block a user