From f6b8c84285cd41110a2e4fd8e459c1ad4dc1a17c Mon Sep 17 00:00:00 2001 From: Guillaume BOURGEOIS Date: Wed, 15 Jul 2026 22:37:07 +0200 Subject: [PATCH] feat(edr): OCSF mappers + ingestion for 6 EDR integrations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every EDR (category: endpoint) integration is now audited for an OCSF mapper. Six had a fetch command but no mapper — add a bundled OCSF mapper for each, wire the fetch command as an ingestion source (ingest: results_path/dedup_key [/incremental_field]) and add the top-level ingestion block. Field mappings were derived from each vendor's fetch output and cross-referenced against the demisto/content reference for that vendor. - carbon-black-cloud: search_alerts (results_path results, dedup id) — 35 fields - cisco-secure-endpoint: list_events (data, id) — 21 fields - cybereason: query_malops (malops, guid, incr start_time) — 6 fields - sophos-central: list_alerts (items, id) — 14 fields - trend-vision-one: list_alerts (items, id, incr start_datetime) — 19 fields - withsecure: list_incidents (items, incidentId) — 7 fields Validated: every manifest passes ParseManifest+ValidateManifest and every mapper passes ParseMapperFile with all JSONata source_path/field expressions compiling. Co-Authored-By: Claude Opus 4.8 (1M context) --- integrations/carbon-black-cloud/manifest.yaml | 10 +++- .../mappers/search_alerts.yaml | 52 +++++++++++++++++++ .../cisco-secure-endpoint/manifest.yaml | 10 +++- .../mappers/list_events.yaml | 35 +++++++++++++ integrations/cybereason/manifest.yaml | 11 +++- .../cybereason/mappers/query_malops.yaml | 13 +++++ integrations/sophos-central/manifest.yaml | 10 +++- .../sophos-central/mappers/list_alerts.yaml | 32 ++++++++++++ integrations/trend-vision-one/manifest.yaml | 11 +++- .../trend-vision-one/mappers/list_alerts.yaml | 35 +++++++++++++ integrations/withsecure/manifest.yaml | 10 +++- .../withsecure/mappers/list_incidents.yaml | 19 +++++++ 12 files changed, 242 insertions(+), 6 deletions(-) create mode 100644 integrations/carbon-black-cloud/mappers/search_alerts.yaml create mode 100644 integrations/cisco-secure-endpoint/mappers/list_events.yaml create mode 100644 integrations/cybereason/mappers/query_malops.yaml create mode 100644 integrations/sophos-central/mappers/list_alerts.yaml create mode 100644 integrations/trend-vision-one/mappers/list_alerts.yaml create mode 100644 integrations/withsecure/mappers/list_incidents.yaml diff --git a/integrations/carbon-black-cloud/manifest.yaml b/integrations/carbon-black-cloud/manifest.yaml index d028f4a..fe11415 100644 --- a/integrations/carbon-black-cloud/manifest.yaml +++ b/integrations/carbon-black-cloud/manifest.yaml @@ -84,7 +84,7 @@ commands: outputs_schema: { properties: {} } - id: search_alerts name: cbc-search-alerts - description: "Search alerts." + description: "Search alerts. Used for ingestion: results path = results." risk: read inputs_schema: properties: @@ -92,6 +92,9 @@ commands: rows: { type: number, description: "Max alerts (default 50)" } required: [] outputs_schema: { properties: {} } + ingest: + results_path: results + dedup_key: id - id: ban_hash name: cbc-ban-hash description: "Ban a file by SHA-256 hash (reputation deny-list override)." @@ -119,3 +122,8 @@ commands: properties: {} required: [] outputs_schema: { properties: {} } + +ingestion: + command: search_alerts + mapper: search_alerts + default_incident_type: "Carbon Black Cloud Alert" diff --git a/integrations/carbon-black-cloud/mappers/search_alerts.yaml b/integrations/carbon-black-cloud/mappers/search_alerts.yaml new file mode 100644 index 0000000..e132c21 --- /dev/null +++ b/integrations/carbon-black-cloud/mappers/search_alerts.yaml @@ -0,0 +1,52 @@ +name: "VMware Carbon Black Cloud Alerts → OCSF" +description: "Exhaustive map of a Carbon Black Cloud Alert Search v7 alert (POST /api/alerts/v7/orgs/{org_key}/alerts/_search, results_path = results) to OCSF. Field paths validated against the CBC Platform Alerts v7 API. The alerting process = actor (actor.*); the child/target process spawned as a result = process.*; parent lineage under actor.process.parent_process.*." +field_mappings: + title: "reason" + severity: "severity >= 9 ? 5 : (severity >= 7 ? 4 : (severity >= 5 ? 3 : (severity >= 3 ? 2 : 1)))" + description: "type" +# results_path = results; source_path is JSONata over ONE alert object. +# Paths absent from a given alert (e.g. no childproc.* on a non-process alert type) are +# skipped at ingestion, so extra entries are safe. +ocsf: + # ── Finding / alert identity ─────────────────────────────────────── + - { source_path: "id", ocsf_field: "finding_info.uid" } + - { source_path: "reason", ocsf_field: "finding_info.title" } + - { source_path: "type", ocsf_field: "finding_info.desc" } + - { source_path: "first_event_timestamp", ocsf_field: "finding_info.created_time" } + - { source_path: "backend_update_timestamp", ocsf_field: "finding_info.modified_time" } + - { source_path: "alert_url", ocsf_field: "finding_info.src_url" } + - { source_path: "workflow.status", ocsf_field: "status" } + - { source_path: "determination.value", ocsf_field: "status_detail" } + # ── Endpoint / device ────────────────────────────────────────────── + - { source_path: "device_id", ocsf_field: "device.uid" } + - { source_path: "device_os_version", ocsf_field: "device.os.build" } + - { source_path: "device_name", ocsf_field: "src_endpoint.hostname" } + - { source_path: "device_os", ocsf_field: "src_endpoint.os.name" } + - { source_path: "device_internal_ip", ocsf_field: "src_endpoint.ip" } + - { source_path: "device_username", ocsf_field: "user.name" } + - { source_path: "org_key", ocsf_field: "cloud.account.uid" } + # ── Triggering process — the actor ───────────────────────────────── + - { source_path: "process_name", ocsf_field: "actor.process.name" } + - { source_path: "process_pid", ocsf_field: "actor.process.pid" } + - { source_path: "process_cmdline", ocsf_field: "actor.process.cmd_line" } + - { source_path: "process_sha256", ocsf_field: "actor.process.file.hashes.sha256" } + - { source_path: "process_md5", ocsf_field: "actor.process.file.hashes.md5" } + - { source_path: "process_guid", ocsf_field: "metadata.original_event_uid" } + - { source_path: "process_username", ocsf_field: "actor.user.name" } + # ── Parent process ────────────────────────────────────────────────── + - { source_path: "parent_name", ocsf_field: "actor.process.parent_process.name" } + - { source_path: "parent_pid", ocsf_field: "actor.process.parent_process.pid" } + - { source_path: "parent_cmdline", ocsf_field: "actor.process.parent_process.cmd_line" } + - { source_path: "parent_sha256", ocsf_field: "actor.process.parent_process.file.hashes.sha256" } + # ── Child / target process ────────────────────────────────────────── + - { source_path: "childproc_name", ocsf_field: "process.name" } + - { source_path: "childproc_pid", ocsf_field: "process.pid" } + - { source_path: "childproc_cmdline", ocsf_field: "process.cmd_line" } + - { source_path: "childproc_guid", ocsf_field: "process.uid" } + # ── Detection rule / watchlist / report ───────────────────────────── + - { source_path: "watchlists[0].name", ocsf_field: "rule.name" } + - { source_path: "watchlists[0].id", ocsf_field: "rule.uid" } + - { source_path: "report_name", ocsf_field: "rule.desc" } + # ── IOC / observable ────────────────────────────────────────────── + - { source_path: "ioc_hit", ocsf_field: "observables.value" } + - { source_path: "ioc_field", ocsf_field: "observables.type" } diff --git a/integrations/cisco-secure-endpoint/manifest.yaml b/integrations/cisco-secure-endpoint/manifest.yaml index c76be35..c0605ce 100644 --- a/integrations/cisco-secure-endpoint/manifest.yaml +++ b/integrations/cisco-secure-endpoint/manifest.yaml @@ -62,7 +62,7 @@ commands: outputs_schema: { properties: {} } - id: list_events name: cisco-amp-list-events - description: "List events." + description: "List events. Used for ingestion: results path = data." risk: read inputs_schema: properties: @@ -70,6 +70,9 @@ commands: limit: { type: number, description: "Max events (default 50)" } required: [] outputs_schema: { properties: {} } + ingest: + results_path: data + dedup_key: id - id: get_trajectory name: cisco-amp-get-trajectory description: "Get a computer's device trajectory." @@ -88,3 +91,8 @@ commands: properties: {} required: [] outputs_schema: { properties: {} } + +ingestion: + command: list_events + mapper: list_events + default_incident_type: "Cisco Secure Endpoint Event" diff --git a/integrations/cisco-secure-endpoint/mappers/list_events.yaml b/integrations/cisco-secure-endpoint/mappers/list_events.yaml new file mode 100644 index 0000000..3fc8a57 --- /dev/null +++ b/integrations/cisco-secure-endpoint/mappers/list_events.yaml @@ -0,0 +1,35 @@ +name: "Cisco Secure Endpoint Events → OCSF" +description: "Maps a Cisco AMP for Endpoints event object (GET /v1/events, results_path = data) to OCSF. The detected file is the subject (file.*/malware.*); the process that dropped/ran it (file.parent.*) is the actor (actor.process.*). Field paths validated against the AMP API v1 event schema (Cisco docs + demisto/content CiscoAMP pack README)." +field_mappings: + title: "detection ? detection : event_type" + severity: "severity = 'Critical' ? 5 : (severity = 'High' ? 4 : (severity = 'Medium' ? 3 : 2))" + description: "event_type" +# results_path = data; source_path is JSONata over ONE event object. +# Paths absent from a given event are skipped at ingestion, so extra entries are safe. +ocsf: + # ── Endpoint / device ────────────────────────────────────────────── + - { source_path: "computer.hostname", ocsf_field: "src_endpoint.hostname" } + - { source_path: "computer.external_ip", ocsf_field: "src_endpoint.ip" } + - { source_path: "computer.connector_guid", ocsf_field: "device.uid" } + - { source_path: "computer.user", ocsf_field: "user.name" } + # ── Finding / analytic ───────────────────────────────────────────── + - { source_path: "id", ocsf_field: "finding_info.uid" } + - { source_path: "timestamp", ocsf_field: "finding_info.created_time" } + - { source_path: "event_type", ocsf_field: "finding_info.analytic.name" } + - { source_path: "event_type_id", ocsf_field: "finding_info.analytic.uid" } + - { source_path: "file.disposition", ocsf_field: "disposition" } + # ── Malware / detection ──────────────────────────────────────────── + - { source_path: "detection", ocsf_field: "malware.name" } + - { source_path: "detection_id", ocsf_field: "malware.uid" } + # ── Detected file — the subject ──────────────────────────────────── + - { source_path: "file.file_name", ocsf_field: "file.name" } + - { source_path: "file.file_path", ocsf_field: "file.path" } + - { source_path: "file.identity.sha256", ocsf_field: "file.hashes.sha256" } + - { source_path: "file.identity.sha1", ocsf_field: "file.hashes.sha1" } + - { source_path: "file.identity.md5", ocsf_field: "file.hashes.md5" } + # ── Parent process (dropped/ran the file) — the actor ────────────── + - { source_path: "file.parent.file_name", ocsf_field: "actor.process.name" } + - { source_path: "file.parent.process_id", ocsf_field: "actor.process.pid" } + - { source_path: "file.parent.identity.sha256", ocsf_field: "actor.process.file.hashes.sha256" } + - { source_path: "file.parent.identity.sha1", ocsf_field: "actor.process.file.hashes.sha1" } + - { source_path: "file.parent.identity.md5", ocsf_field: "actor.process.file.hashes.md5" } diff --git a/integrations/cybereason/manifest.yaml b/integrations/cybereason/manifest.yaml index c8bbde6..96eb53f 100644 --- a/integrations/cybereason/manifest.yaml +++ b/integrations/cybereason/manifest.yaml @@ -31,7 +31,7 @@ config_schema: commands: - id: query_malops name: cybereason-query-malops - description: "Query Malops (malicious operations) in a time window." + description: "Query Malops (malicious operations) in a time window. Used for ingestion: results path = malops." risk: read inputs_schema: properties: @@ -39,6 +39,10 @@ commands: end_time: { type: number, description: "End time as Unix epoch milliseconds" } required: [] outputs_schema: { properties: {} } + ingest: + results_path: malops + dedup_key: guid + incremental_field: start_time - id: list_sensors name: cybereason-list-sensors description: "List sensors (endpoints)." @@ -93,3 +97,8 @@ commands: properties: {} required: [] outputs_schema: { properties: {} } + +ingestion: + command: query_malops + mapper: query_malops + default_incident_type: "Cybereason Malop" diff --git a/integrations/cybereason/mappers/query_malops.yaml b/integrations/cybereason/mappers/query_malops.yaml new file mode 100644 index 0000000..4e9ff05 --- /dev/null +++ b/integrations/cybereason/mappers/query_malops.yaml @@ -0,0 +1,13 @@ +name: "Cybereason Malops → OCSF" +description: "Maps the raw items of the 'malops' array returned by POST /rest/detection/inbox (results_path = malops) to OCSF finding fields. Each element is the flat, non-nested Malop-inbox shape (guid, edr, displayName, status, severity, creationTime, machines[], users[]) — not the enriched elementValues/simpleValues shape returned by /rest/crimes/unified." +field_mappings: + title: "displayName" + severity: "severity = 'Critical' ? 5 : (severity = 'High' ? 4 : (severity = 'Medium' ? 3 : (severity = 'Low' ? 2 : 3)))" + description: "\"Cybereason Malop — severity: \" & severity & \", status: \" & status" +ocsf: + - { source_path: "guid", ocsf_field: "finding_info.uid" } + - { source_path: "displayName", ocsf_field: "finding_info.title" } + - { source_path: "creationTime", ocsf_field: "finding_info.created_time" } + - { source_path: "status", ocsf_field: "status" } + - { source_path: "machines[0].displayName", ocsf_field: "src_endpoint.hostname" } + - { source_path: "$count(machines)", ocsf_field: "count" } diff --git a/integrations/sophos-central/manifest.yaml b/integrations/sophos-central/manifest.yaml index 642d43a..7dd12ba 100644 --- a/integrations/sophos-central/manifest.yaml +++ b/integrations/sophos-central/manifest.yaml @@ -67,13 +67,16 @@ commands: outputs_schema: { properties: {} } - id: list_alerts name: sophos-list-alerts - description: "List alerts." + description: "List alerts. Used for ingestion: results path = items." risk: read inputs_schema: properties: limit: { type: number, description: "Max alerts (default 50)" } required: [] outputs_schema: { properties: {} } + ingest: + results_path: items + dedup_key: id - id: test_connection name: sophos-test-connection @@ -83,3 +86,8 @@ commands: properties: {} required: [] outputs_schema: { properties: {} } + +ingestion: + command: list_alerts + mapper: list_alerts + default_incident_type: "Sophos Central Alert" diff --git a/integrations/sophos-central/mappers/list_alerts.yaml b/integrations/sophos-central/mappers/list_alerts.yaml new file mode 100644 index 0000000..6cdfaf2 --- /dev/null +++ b/integrations/sophos-central/mappers/list_alerts.yaml @@ -0,0 +1,32 @@ +name: "Sophos Central Alerts → OCSF" +description: "Maps a Sophos Central alert (GET /common/v1/alerts, results_path = items) to OCSF Detection Finding fields. Sophos severity is a string enum (high/medium/low); managedAgent is the affected endpoint." +field_mappings: + title: "description" + description: "description" + # Sophos severity enum → 1-5 incident scale. Right-associative chaining, no parentheses; + # the last band uses a literal else so 'low' and anything unknown land on 2. + severity: "severity = 'high' ? 4 : severity = 'medium' ? 3 : 2" + # Product that raised the alert (e.g. endpoint, server, mobile). + source: "product" +# results_path = items; source_path is JSONata over ONE alert object using RAW field names. +# Fields absent from a given alert return nothing and are skipped. +ocsf: + # ── Finding ─────────────────────────────────────────────────────── + - { source_path: "id", ocsf_field: "finding_info.uid" } + - { source_path: "description", ocsf_field: "finding_info.title" } + - { source_path: "groupKey", ocsf_field: "finding_info.desc" } + - { source_path: "raisedAt", ocsf_field: "finding_info.created_time" } + - { source_path: "type", ocsf_field: "finding_info.analytic.type" } + - { source_path: "severity", ocsf_field: "severity" } + # ── Alert classification ────────────────────────────────────────── + - { source_path: "category", ocsf_field: "rule.category" } + - { source_path: "type", ocsf_field: "rule.name" } + # ── Affected endpoint (managedAgent) ────────────────────────────── + - { source_path: "managedAgent.name", ocsf_field: "src_endpoint.hostname" } + - { source_path: "managedAgent.id", ocsf_field: "device.uid" } + - { source_path: "managedAgent.type", ocsf_field: "device.type" } + # ── Referenced person ───────────────────────────────────────────── + - { source_path: "person.name", ocsf_field: "user.name" } + # ── Provenance ──────────────────────────────────────────────────── + - { source_path: "product", ocsf_field: "metadata.product.name" } + - { source_path: "tenant.id", ocsf_field: "metadata.tenant_uid" } diff --git a/integrations/trend-vision-one/manifest.yaml b/integrations/trend-vision-one/manifest.yaml index eeda3cb..1c2fd9d 100644 --- a/integrations/trend-vision-one/manifest.yaml +++ b/integrations/trend-vision-one/manifest.yaml @@ -64,7 +64,7 @@ commands: outputs_schema: { properties: {} } - id: list_alerts name: tmv1-list-alerts - description: "List Workbench alerts in a time window." + description: "List Workbench alerts in a time window. Used for ingestion: results path = items." risk: read inputs_schema: properties: @@ -73,6 +73,10 @@ commands: top: { type: number, description: "Max alerts (default 50)" } required: [] outputs_schema: { properties: {} } + ingest: + results_path: items + dedup_key: id + incremental_field: start_datetime - id: get_alert name: tmv1-get-alert description: "Get a single Workbench alert by ID." @@ -110,3 +114,8 @@ commands: properties: {} required: [] outputs_schema: { properties: {} } + +ingestion: + command: list_alerts + mapper: list_alerts + default_incident_type: "Trend Vision One Alert" diff --git a/integrations/trend-vision-one/mappers/list_alerts.yaml b/integrations/trend-vision-one/mappers/list_alerts.yaml new file mode 100644 index 0000000..eb371d4 --- /dev/null +++ b/integrations/trend-vision-one/mappers/list_alerts.yaml @@ -0,0 +1,35 @@ +name: "Trend Micro Vision One Workbench Alerts → OCSF" +description: "Maps a Vision One Workbench alert (GET /v3.0/workbench/alerts, results_path = items) to OCSF finding fields. Field names verified against the v3.0 Workbench alert schema (demisto/content TrendMicroVisionOneV3 + trendmicro/tm-v1-api-cookbook). Vision One severity is a string (critical/high/medium/low/info); impactScope entities are typed (host/account/emailAddress/…) — first host → src_endpoint, first account → user." +field_mappings: + title: "model" + severity: "severity = 'critical' ? 5 : (severity = 'high' ? 4 : (severity = 'medium' ? 3 : (severity = 'low' ? 2 : 1)))" + description: "description" +# results_path = items; source_path is JSONata over ONE alert object. +# Paths absent from a given alert are skipped at ingestion, so extra entries are safe. +ocsf: + # ── Finding / analytic ──────────────────────────────────────────── + - { source_path: "id", ocsf_field: "finding_info.uid" } + - { source_path: "model", ocsf_field: "finding_info.title" } + - { source_path: "description", ocsf_field: "finding_info.desc" } + - { source_path: "createdDateTime", ocsf_field: "finding_info.created_time" } + - { source_path: "updatedDateTime", ocsf_field: "finding_info.modified_time" } + - { source_path: "workbenchLink", ocsf_field: "finding_info.src_url" } + - { source_path: "score", ocsf_field: "confidence_score" } + - { source_path: "investigationStatus", ocsf_field: "status" } + # ── Detection model as the rule ─────────────────────────────────── + - { source_path: "model", ocsf_field: "rule.name" } + - { source_path: "modelId", ocsf_field: "rule.uid" } + # ── MITRE ATT&CK (nested under matchedRules[].matchedFilters[]) ──── + - { source_path: "matchedRules[0].matchedFilters[0].mitreTechniqueIds[0]", ocsf_field: "attacks.technique.uid" } + # ── Impacted host — first 'host' entity → src_endpoint / device ──── + - { source_path: "impactScope.entities[entityType='host'][0].entityValue.name", ocsf_field: "src_endpoint.hostname" } + - { source_path: "impactScope.entities[entityType='host'][0].entityValue.ips[0]", ocsf_field: "src_endpoint.ip" } + - { source_path: "impactScope.entities[entityType='host'][0].entityValue.guid", ocsf_field: "device.uid" } + # ── Impacted account — first 'account' entity → user ────────────── + - { source_path: "impactScope.entities[entityType='account'][0].entityValue", ocsf_field: "user.name" } + # ── IOC / observable (first indicator) ──────────────────────────── + - { source_path: "indicators[0].value", ocsf_field: "observables.value" } + - { source_path: "indicators[0].type", ocsf_field: "observables.type" } + - { source_path: "indicators[0].field", ocsf_field: "observables.name" } + # ── Product provenance ──────────────────────────────────────────── + - { source_path: "alertProvider", ocsf_field: "metadata.product.name" } diff --git a/integrations/withsecure/manifest.yaml b/integrations/withsecure/manifest.yaml index b0ed952..a4c6c49 100644 --- a/integrations/withsecure/manifest.yaml +++ b/integrations/withsecure/manifest.yaml @@ -70,13 +70,16 @@ commands: outputs_schema: { properties: {} } - id: list_incidents name: withsecure-list-incidents - description: "List incidents (detections)." + description: "List incidents (detections). Used for ingestion: results path = items." risk: read inputs_schema: properties: limit: { type: number, description: "Max incidents (default 50)" } required: [] outputs_schema: { properties: {} } + ingest: + results_path: items + dedup_key: incidentId - id: test_connection name: withsecure-test-connection @@ -86,3 +89,8 @@ commands: properties: {} required: [] outputs_schema: { properties: {} } + +ingestion: + command: list_incidents + mapper: list_incidents + default_incident_type: "WithSecure Elements Incident" diff --git a/integrations/withsecure/mappers/list_incidents.yaml b/integrations/withsecure/mappers/list_incidents.yaml new file mode 100644 index 0000000..06bc1ab --- /dev/null +++ b/integrations/withsecure/mappers/list_incidents.yaml @@ -0,0 +1,19 @@ +name: "WithSecure Elements Incidents → OCSF" +description: "Maps a WithSecure Elements incident (Broad Context Detection / BCD) from GET /incidents/v1/incidents (results_path = items) to OCSF finding fields. Field paths validated against the WithSecure Connect Elements API OpenAPI spec (connect.withsecure.com/specs/elements). The incidents list object carries only incident-level metadata — device / user / process / file / MITRE fields live on the separate /incidents/v1/detections endpoint and are not present here." +field_mappings: + title: "name" + severity: "severity = 'critical' ? 5 : (severity = 'high' ? 4 : (severity = 'medium' ? 3 : (severity = 'low' ? 2 : 1)))" + description: "$join(categories, ', ')" +# results_path = items; source_path is JSONata over ONE incident (BCD) object. +# Paths absent from a given incident are skipped at ingestion, so extra entries are safe. +ocsf: + # ── Finding identity ────────────────────────────────────────────── + - { source_path: "incidentId", ocsf_field: "finding_info.uid" } + - { source_path: "name", ocsf_field: "finding_info.title" } + # ── Timestamps ──────────────────────────────────────────────────── + - { source_path: "createdTimestamp", ocsf_field: "finding_info.created_time" } + - { source_path: "updatedTimestamp", ocsf_field: "finding_info.modified_time" } + # ── State ───────────────────────────────────────────────────────── + - { source_path: "severity", ocsf_field: "severity" } + - { source_path: "status", ocsf_field: "status" } + - { source_path: "resolution", ocsf_field: "finding_info.desc" }