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>
125 lines
11 KiB
YAML
125 lines
11 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 ───────────────────────────────────
|
|
- { 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),
|
|
# `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.
|