82f22fdbb3
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>
137 lines
12 KiB
YAML
137 lines
12 KiB
YAML
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 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))"
|
|
description: "alert_type"
|
|
# results_path = results; source_path is JSONata over ONE alert object.
|
|
# Paths absent from a given alert are skipped at ingestion, so extra entries are safe.
|
|
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" }
|
|
- { source_path: "alert_type", ocsf_field: "finding_info.analytic.type" }
|
|
- { 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
|
|
- { source_path: "mitre_cells", ocsf_field: "attacks" } # ["TAxxxx:Tyyyy", ...] best-effort
|
|
- { 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" }
|
|
- { source_path: "process.username", ocsf_field: "actor.user.name" }
|
|
- { source_path: "process.integrity_level", ocsf_field: "actor.process.integrity" }
|
|
- { source_path: "process.pid", ocsf_field: "actor.process.pid" }
|
|
- { source_path: "process.hashes.sha256", ocsf_field: "actor.process.file.hashes.sha256" }
|
|
- { source_path: "process.hashes.sha1", ocsf_field: "actor.process.file.hashes.sha1" }
|
|
- { source_path: "process.hashes.md5", ocsf_field: "actor.process.file.hashes.md5" }
|
|
- { source_path: "process.parent_image", ocsf_field: "actor.process.parent_process.name" }
|
|
- { source_path: "process.parent_commandline", ocsf_field: "actor.process.parent_process.cmd_line" }
|
|
# ── Event details: network connection ─────────────────────────────
|
|
- { source_path: "details_connection.SourceIp", ocsf_field: "src_endpoint.ip" }
|
|
- { source_path: "details_connection.SourcePort", ocsf_field: "src_endpoint.port" }
|
|
- { source_path: "details_connection.DestinationIp", ocsf_field: "dst_endpoint.ip" }
|
|
- { source_path: "details_connection.DestinationPort", ocsf_field: "dst_endpoint.port" }
|
|
- { source_path: "details_connection.Protocol", ocsf_field: "connection_info.protocol_name" }
|
|
# ── Event details: DNS ────────────────────────────────────────────
|
|
- { source_path: "details_dns_resolution.requested_name", ocsf_field: "query.hostname" }
|
|
- { source_path: "details_dns_resolution.query_type", ocsf_field: "query.type" }
|
|
# ── Event details: file ───────────────────────────────────────────
|
|
- { source_path: "details_file.target_filename", ocsf_field: "file.path" }
|
|
# ── Event details: registry ───────────────────────────────────────
|
|
- { source_path: "details_registry.target_object", ocsf_field: "reg_key.path" }
|
|
- { source_path: "details_registry.registry_value_type", ocsf_field: "reg_value.type" }
|
|
- { source_path: "details_registry.data_string_added", ocsf_field: "reg_value.data" }
|
|
# ── Event details: URL request ────────────────────────────────────
|
|
- { source_path: "details_url_request.url", ocsf_field: "url.url_string" }
|
|
- { 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),
|
|
# 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`
|
|
# 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.
|