Compare commits
3 Commits
d831d0973a
...
db9a45c9bc
| Author | SHA1 | Date | |
|---|---|---|---|
| db9a45c9bc | |||
| ece2c41340 | |||
| ea04ea586c |
@@ -23,11 +23,13 @@ ocsf:
|
|||||||
- { source_path: "alert_category", ocsf_field: "finding_info.analytic.category" }
|
- { source_path: "alert_category", ocsf_field: "finding_info.analytic.category" }
|
||||||
- { source_path: "alert_name", ocsf_field: "finding_info.analytic.name" }
|
- { source_path: "alert_name", ocsf_field: "finding_info.analytic.name" }
|
||||||
- { source_path: "matching_service_rule_id", ocsf_field: "finding_info.analytic.uid" }
|
- { source_path: "matching_service_rule_id", ocsf_field: "finding_info.analytic.uid" }
|
||||||
# ── Detection time: the MTTD anchor ───────────────────────────────
|
# ── Event time (OCSF `time`) ───────────────────────────────────────
|
||||||
# `time` is what Riposte measures detection-to-ingestion against. The grid
|
# Riposte no longer reads it to date detection: since migration 0256, MTTD
|
||||||
# exposes when the tenant took the alert in (source_insert_ts); a sensor-side
|
# starts when Riposte records the alert. It is the alert's own record of when
|
||||||
# detection timestamp, when the tenant sends one, is the better anchor and
|
# the source saw the event.
|
||||||
# comes last so it wins.
|
# The grid exposes when the tenant took the alert in (source_insert_ts); a
|
||||||
|
# sensor-side detection timestamp, when the tenant sends one, is closer to the
|
||||||
|
# event and comes last so it wins.
|
||||||
- { source_path: "source_insert_ts", ocsf_field: "time" }
|
- { source_path: "source_insert_ts", ocsf_field: "time" }
|
||||||
- { source_path: "detection_timestamp", ocsf_field: "time" }
|
- { source_path: "detection_timestamp", ocsf_field: "time" }
|
||||||
# ── Alert state ───────────────────────────────────────────────────
|
# ── Alert state ───────────────────────────────────────────────────
|
||||||
|
|||||||
@@ -18,8 +18,11 @@ ocsf:
|
|||||||
- { source_path: "modification_time", ocsf_field: "finding_info.modified_time" }
|
- { source_path: "modification_time", ocsf_field: "finding_info.modified_time" }
|
||||||
- { source_path: "xdr_url", ocsf_field: "finding_info.src_url" }
|
- { source_path: "xdr_url", ocsf_field: "finding_info.src_url" }
|
||||||
- { source_path: "status", ocsf_field: "status" }
|
- { source_path: "status", ocsf_field: "status" }
|
||||||
# `time` is the MTTD anchor — when XDR opened the incident, as opposed to when
|
# `time` is the OCSF event time — when XDR opened the incident, as opposed to
|
||||||
# Riposte ingested it. Without it the detection delay column stays empty.
|
# when Riposte ingested it.
|
||||||
|
# Riposte no longer reads it to date detection: since migration 0256, MTTD
|
||||||
|
# starts when Riposte records the alert. It is the alert's own record of when
|
||||||
|
# the source saw the event.
|
||||||
- { source_path: "creation_time", ocsf_field: "time" }
|
- { source_path: "creation_time", ocsf_field: "time" }
|
||||||
- { source_path: "alert_count", ocsf_field: "count" }
|
- { source_path: "alert_count", ocsf_field: "count" }
|
||||||
# ── MITRE ATT&CK (first aggregated tactic/technique) ──────────────
|
# ── MITRE ATT&CK (first aggregated tactic/technique) ──────────────
|
||||||
|
|||||||
@@ -35,7 +35,10 @@ ocsf:
|
|||||||
# The detection that opened the incident, so an analyst reading the aggregate
|
# The detection that opened the incident, so an analyst reading the aggregate
|
||||||
# can still pivot to the single alert in the XDR console.
|
# can still pivot to the single alert in the XDR console.
|
||||||
- { source_path: "alerts[0].alert_id", ocsf_field: "finding_info.uid_alt" }
|
- { source_path: "alerts[0].alert_id", ocsf_field: "finding_info.uid_alt" }
|
||||||
# ── Detection time: the MTTD anchor ───────────────────────────────
|
# ── Event time (OCSF `time`) ───────────────────────────────────────
|
||||||
|
# Riposte no longer reads it to date detection: since migration 0256, MTTD
|
||||||
|
# starts when Riposte records the alert. It is the alert's own record of when
|
||||||
|
# the source saw the event.
|
||||||
# Weakest first, strongest last. detection_time is often null on an XDR
|
# Weakest first, strongest last. detection_time is often null on an XDR
|
||||||
# incident, and then the first alert's own detection timestamp is the honest
|
# incident, and then the first alert's own detection timestamp is the honest
|
||||||
# anchor; incident creation is the last resort.
|
# anchor; incident creation is the last resort.
|
||||||
|
|||||||
@@ -27,11 +27,10 @@ ocsf:
|
|||||||
- { source_path: "name", ocsf_field: "finding_info.title" }
|
- { source_path: "name", ocsf_field: "finding_info.title" }
|
||||||
- { source_path: "description", ocsf_field: "finding_info.desc" }
|
- { source_path: "description", ocsf_field: "finding_info.desc" }
|
||||||
- { source_path: "created_timestamp", ocsf_field: "finding_info.created_time" }
|
- { source_path: "created_timestamp", ocsf_field: "finding_info.created_time" }
|
||||||
# `time` is the MTTD anchor: the ingestion engine reads THIS field and no other
|
# `time` is the OCSF event time: when Falcon raised the alert.
|
||||||
# to stamp detected_at. Without it the incident is anchored at ingestion, so the
|
# Riposte no longer reads it to date detection: since migration 0256, MTTD
|
||||||
# delay between Falcon detecting and Riposte collecting — over a day on a slow
|
# starts when Riposte records the alert. It is the alert's own record of when
|
||||||
# poll — is measured as zero. finding_info.created_time below does NOT serve
|
# the source saw the event.
|
||||||
# this: it is the finding's own record of the same instant, read by nothing.
|
|
||||||
- { source_path: "created_timestamp", ocsf_field: "time" }
|
- { source_path: "created_timestamp", ocsf_field: "time" }
|
||||||
- { source_path: "updated_timestamp", ocsf_field: "finding_info.modified_time" }
|
- { source_path: "updated_timestamp", ocsf_field: "finding_info.modified_time" }
|
||||||
- { source_path: "scenario", ocsf_field: "finding_info.analytic.name" }
|
- { source_path: "scenario", ocsf_field: "finding_info.analytic.name" }
|
||||||
|
|||||||
@@ -67,7 +67,10 @@ ocsf:
|
|||||||
- { source_path: "organization_id", ocsf_field: "cloud.org.uid" }
|
- { source_path: "organization_id", ocsf_field: "cloud.org.uid" }
|
||||||
- { source_path: "assignee.name", ocsf_field: "assignee.name" }
|
- { source_path: "assignee.name", ocsf_field: "assignee.name" }
|
||||||
- { source_path: "assignee.email", ocsf_field: "assignee.email_addr" }
|
- { source_path: "assignee.email", ocsf_field: "assignee.email_addr" }
|
||||||
# ── Detection time: the MTTD anchor ───────────────────────────────
|
# ── Event time (OCSF `time`) ───────────────────────────────────────
|
||||||
|
# Riposte no longer reads it to date detection: since migration 0256, MTTD
|
||||||
|
# starts when Riposte records the alert. It is the alert's own record of when
|
||||||
|
# the source saw the event.
|
||||||
# Weakest first, strongest last. The investigation is created after the alert that
|
# Weakest first, strongest last. The investigation is created after the alert that
|
||||||
# opened it; the alert's own alerted_at is when the event happened in the source system.
|
# opened it; the alert's own alerted_at is when the event happened in the source system.
|
||||||
- { source_path: "created_time", ocsf_field: "time" }
|
- { source_path: "created_time", ocsf_field: "time" }
|
||||||
|
|||||||
@@ -41,9 +41,11 @@ ocsf:
|
|||||||
- { source_path: "detection_name", ocsf_field: "finding_info.title" }
|
- { source_path: "detection_name", ocsf_field: "finding_info.title" }
|
||||||
- { source_path: "detection_name", ocsf_field: "finding_info.analytic.name" }
|
- { source_path: "detection_name", ocsf_field: "finding_info.analytic.name" }
|
||||||
- { source_path: "$fromMillis(timestamp)", ocsf_field: "finding_info.created_time" }
|
- { source_path: "$fromMillis(timestamp)", ocsf_field: "finding_info.created_time" }
|
||||||
# ── Detection time: the MTTD anchor ───────────────────────────────
|
# ── Event time (OCSF `time`) ───────────────────────────────────────
|
||||||
# The log's own timestamp, not the poll's. A feed that ran late must not make the
|
# Riposte no longer reads it to date detection: since migration 0256, MTTD
|
||||||
# detection look late with it.
|
# starts when Riposte records the alert. It is the alert's own record of when
|
||||||
|
# the source saw the event.
|
||||||
|
# The log's own timestamp, not the poll's.
|
||||||
- { source_path: "$fromMillis(timestamp)", ocsf_field: "time" }
|
- { source_path: "$fromMillis(timestamp)", ocsf_field: "time" }
|
||||||
# ── Product identity ──────────────────────────────────────────────
|
# ── Product identity ──────────────────────────────────────────────
|
||||||
- { source_path: "'Rapid7 InsightIDR'", ocsf_field: "metadata.product.name" }
|
- { source_path: "'Rapid7 InsightIDR'", ocsf_field: "metadata.product.name" }
|
||||||
|
|||||||
@@ -40,9 +40,11 @@ ocsf:
|
|||||||
- { source_path: "detection_name", ocsf_field: "finding_info.title" }
|
- { source_path: "detection_name", ocsf_field: "finding_info.title" }
|
||||||
- { source_path: "detection_name", ocsf_field: "finding_info.analytic.name" }
|
- { source_path: "detection_name", ocsf_field: "finding_info.analytic.name" }
|
||||||
- { source_path: "$fromMillis(timestamp)", ocsf_field: "finding_info.created_time" }
|
- { source_path: "$fromMillis(timestamp)", ocsf_field: "finding_info.created_time" }
|
||||||
# ── Detection time: the MTTD anchor ───────────────────────────────
|
# ── Event time (OCSF `time`) ───────────────────────────────────────
|
||||||
# The log's own timestamp, not the poll's. A feed that ran late must not make the
|
# Riposte no longer reads it to date detection: since migration 0256, MTTD
|
||||||
# detection look late with it.
|
# starts when Riposte records the alert. It is the alert's own record of when
|
||||||
|
# the source saw the event.
|
||||||
|
# The log's own timestamp, not the poll's.
|
||||||
- { source_path: "$fromMillis(timestamp)", ocsf_field: "time" }
|
- { source_path: "$fromMillis(timestamp)", ocsf_field: "time" }
|
||||||
# ── Product identity ──────────────────────────────────────────────
|
# ── Product identity ──────────────────────────────────────────────
|
||||||
- { source_path: "'Rapid7 InsightIDR'", ocsf_field: "metadata.product.name" }
|
- { source_path: "'Rapid7 InsightIDR'", ocsf_field: "metadata.product.name" }
|
||||||
|
|||||||
@@ -50,7 +50,10 @@ ocsf:
|
|||||||
- { source_path: "type", ocsf_field: "finding_info.analytic.name" }
|
- { source_path: "type", ocsf_field: "finding_info.analytic.name" }
|
||||||
- { source_path: "rule.rrn", ocsf_field: "finding_info.analytic.uid" }
|
- { source_path: "rule.rrn", ocsf_field: "finding_info.analytic.uid" }
|
||||||
- { source_path: "rule.version_rrn", ocsf_field: "finding_info.analytic.version" }
|
- { source_path: "rule.version_rrn", ocsf_field: "finding_info.analytic.version" }
|
||||||
# ── Detection time: the MTTD anchor ───────────────────────────────
|
# ── Event time (OCSF `time`) ───────────────────────────────────────
|
||||||
|
# Riposte no longer reads it to date detection: since migration 0256, MTTD
|
||||||
|
# starts when Riposte records the alert. It is the alert's own record of when
|
||||||
|
# the source saw the event.
|
||||||
# alerted_at is when the underlying event happened in the source system; created_at is
|
# alerted_at is when the underlying event happened in the source system; created_at is
|
||||||
# when Rapid7 finished building the alert. Weakest first, strongest last.
|
# when Rapid7 finished building the alert. Weakest first, strongest last.
|
||||||
- { source_path: "created_at", ocsf_field: "time" }
|
- { source_path: "created_at", ocsf_field: "time" }
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
id: sekoia_xdr
|
id: sekoia_xdr
|
||||||
name: Sekoia XDR
|
name: Sekoia XDR
|
||||||
version: 1.1.0
|
version: 1.2.0
|
||||||
description: "Sekoia Defend (XDR) REST API — alert ingestion and lifecycle (list/get/search, status workflow, comments), event search jobs (create/status/results + one-shot search), cases, asset management (get/list, attributes/keys add/remove), users, kill chains and a generic authenticated HTTP passthrough."
|
description: "Sekoia Defend (XDR) REST API — alert ingestion and lifecycle (list/get/search, status workflow, comments), event search jobs (create/status/results + one-shot search), cases, asset management (get/list, attributes/keys add/remove), users, kill chains and a generic authenticated HTTP passthrough."
|
||||||
changelog: "1.1.0 — Declare the risk tier on the 1 command that carried none, all of them destructive. An unset risk was already gated as destructive, so the approval prompt does not move; what changes is that the tier is a stated decision rather than a default, and a command added later cannot inherit it by omission. | 1.0.0 — Initial release: alert ingestion (list_alerts) with an exhaustive OCSF mapper, 20 commands across alerts, event search, cases, assets, users and kill chains."
|
changelog: "1.2.0 — Mapper reviewed on a real Entra ID alert. The incident source is now 'Sekoia XDR': it carried Sekoia's own `source` field, the alert's source address, which now goes to src_endpoint.ip (or .hostname for a host), and `target` to dst_endpoint instead of src_endpoint. The description is the alert's sentence with its {rule:<uuid>} placeholder replaced by the rule name, rather than the rule documentation (now finding_info.analytic.desc). Every TTP is mapped into attacks, not only the first; kill_chain_short_id, Sekoia's own kill chain, no longer poses as an ATT&CK tactic uid. Times are epoch milliseconds as OCSF expects (created_at/updated_at came in seconds, first/last seen as text); alert category in finding_info.types, detection type in analytic.category, product in metadata. | 1.1.0 — Declare the risk tier on the 1 command that carried none, all of them destructive. An unset risk was already gated as destructive, so the approval prompt does not move; what changes is that the tier is a stated decision rather than a default, and a command added later cannot inherit it by omission. | 1.0.0 — Initial release: alert ingestion (list_alerts) with an exhaustive OCSF mapper, 20 commands across alerts, event search, cases, assets, users and kill chains."
|
||||||
category: siem
|
category: siem
|
||||||
|
|
||||||
# Per-instance configuration. The API host defaults to the EU region; an
|
# Per-instance configuration. The API host defaults to the EU region; an
|
||||||
|
|||||||
@@ -1,43 +1,63 @@
|
|||||||
name: "Sekoia XDR Alerts → OCSF"
|
name: "Sekoia XDR Alerts → OCSF"
|
||||||
description: "Maps a Sekoia Defend (XDR) alert (/v1/sic/alerts, results_path = items) to OCSF Detection Finding fields. Alerts aggregate the matching events; use search_events for per-event detail."
|
description: "Maps a Sekoia Defend (XDR) alert (/v1/sic/alerts, results_path = items) to OCSF Detection Finding fields. Alerts aggregate the matching events; use search_events for per-event detail. Sekoia's `source` and `target` are the alert's main source and target (an IP address or a host), not the producing system."
|
||||||
field_mappings:
|
field_mappings:
|
||||||
title: "title"
|
title: "title"
|
||||||
description: "details"
|
# The alert's own sentence, with the rule placeholder Sekoia leaves in it
|
||||||
|
# ('{rule:<uuid>}') replaced by the rule's name. `details` is the rule's
|
||||||
|
# documentation — the same for every alert of the rule, MITRE prose and
|
||||||
|
# vendor links included — and goes to finding_info.analytic.desc instead.
|
||||||
|
description: "jsonata: description ? $replace(description, '{rule:' & rule.uuid & '}', rule.name) : title"
|
||||||
# urgency is a 0-100 score; bucket it onto the 1-5 incident scale. No parentheses (the engine chains
|
# urgency is a 0-100 score; bucket it onto the 1-5 incident scale. No parentheses (the engine chains
|
||||||
# right-associatively) and the lowest band uses '>= 0 ? 1' so the literal 1 sits in a then-branch —
|
# right-associatively) and the lowest band uses '>= 0 ? 1' so the literal 1 sits in a then-branch —
|
||||||
# a bare trailing else is read as a field path, not an integer, and would fall back to the default.
|
# a bare trailing else is read as a field path, not an integer, and would fall back to the default.
|
||||||
severity: "urgency.value >= 80 ? 5 : urgency.value >= 60 ? 4 : urgency.value >= 40 ? 3 : urgency.value >= 20 ? 2 : urgency.value >= 0 ? 1 : 1"
|
severity: "urgency.value >= 80 ? 5 : urgency.value >= 60 ? 4 : urgency.value >= 40 ? 3 : urgency.value >= 20 ? 2 : urgency.value >= 0 ? 1 : 1"
|
||||||
# How the alert was generated.
|
# The producing system. Sekoia's own `source` field is the alert's source
|
||||||
source: "source"
|
# address or host — it goes to src_endpoint below.
|
||||||
|
source: "jsonata: 'Sekoia XDR'"
|
||||||
# results_path = items; source_path is JSONata over ONE alert object.
|
# results_path = items; source_path is JSONata over ONE alert object.
|
||||||
# Paths absent from a given alert return nothing and are skipped.
|
# Paths absent from a given alert return nothing and are skipped.
|
||||||
ocsf:
|
ocsf:
|
||||||
# ── Finding ───────────────────────────────────────────────────────
|
# ── Finding ───────────────────────────────────────────────────────
|
||||||
- { source_path: "uuid", ocsf_field: "finding_info.uid" }
|
- { source_path: "uuid", ocsf_field: "finding_info.uid" }
|
||||||
- { source_path: "title", ocsf_field: "finding_info.title" }
|
- { source_path: "title", ocsf_field: "finding_info.title" }
|
||||||
- { source_path: "rule.description", ocsf_field: "finding_info.desc" }
|
- { source_path: "description ? $replace(description, '{rule:' & rule.uuid & '}', rule.name)", ocsf_field: "finding_info.desc" }
|
||||||
- { source_path: "created_at", ocsf_field: "finding_info.created_time" }
|
# OCSF times are epoch milliseconds: created_at/updated_at come in seconds,
|
||||||
- { source_path: "updated_at", ocsf_field: "finding_info.modified_time" }
|
# first/last seen as ISO 8601.
|
||||||
- { source_path: "first_seen_at", ocsf_field: "finding_info.first_seen_time" }
|
- { source_path: "created_at * 1000", ocsf_field: "finding_info.created_time" }
|
||||||
- { source_path: "last_seen_at", ocsf_field: "finding_info.last_seen_time" }
|
- { source_path: "updated_at * 1000", ocsf_field: "finding_info.modified_time" }
|
||||||
|
- { source_path: "$toMillis(first_seen_at)", ocsf_field: "finding_info.first_seen_time" }
|
||||||
|
- { source_path: "$toMillis(last_seen_at)", ocsf_field: "finding_info.last_seen_time" }
|
||||||
|
- { source_path: "alert_type.category ? [alert_type.category]", ocsf_field: "finding_info.types" }
|
||||||
# ── Detection rule (analytic) ─────────────────────────────────────
|
# ── Detection rule (analytic) ─────────────────────────────────────
|
||||||
- { source_path: "rule.name", ocsf_field: "finding_info.analytic.name" }
|
- { source_path: "rule.name", ocsf_field: "finding_info.analytic.name" }
|
||||||
- { source_path: "rule.uuid", ocsf_field: "finding_info.analytic.uid" }
|
- { source_path: "rule.uuid", ocsf_field: "finding_info.analytic.uid" }
|
||||||
- { source_path: "rule.description", ocsf_field: "finding_info.analytic.desc" }
|
|
||||||
- { source_path: "rule.type", ocsf_field: "finding_info.analytic.type" }
|
- { source_path: "rule.type", ocsf_field: "finding_info.analytic.type" }
|
||||||
|
- { source_path: "detection_type", ocsf_field: "finding_info.analytic.category" }
|
||||||
|
# The rule's documentation (Sekoia's `details`); rule objects carry no
|
||||||
|
# description of their own.
|
||||||
|
- { source_path: "details", ocsf_field: "finding_info.analytic.desc" }
|
||||||
# ── Incident state ────────────────────────────────────────────────
|
# ── Incident state ────────────────────────────────────────────────
|
||||||
- { source_path: "status.name", ocsf_field: "status" }
|
- { source_path: "status.name", ocsf_field: "status" }
|
||||||
- { source_path: "alert_type.value", ocsf_field: "activity_name" }
|
- { source_path: "alert_type.value", ocsf_field: "activity_name" }
|
||||||
- { source_path: "similar", ocsf_field: "count" }
|
- { source_path: "similar", ocsf_field: "count" }
|
||||||
- { source_path: "urgency.value", ocsf_field: "risk_score" }
|
- { source_path: "urgency.value", ocsf_field: "risk_score" }
|
||||||
# ── MITRE ATT&CK (first TTP / kill-chain step) ────────────────────
|
# ── MITRE ATT&CK — every TTP, not only the first ──────────────────
|
||||||
- { source_path: "ttps[0].name", ocsf_field: "attacks.technique.name" }
|
# Sekoia names techniques by their STIX id (attack-pattern--…), not their
|
||||||
- { source_path: "ttps[0].id", ocsf_field: "attacks.technique.uid" }
|
# ATT&CK id (T1583), and gives no tactic: kill_chain_short_id is Sekoia's
|
||||||
- { source_path: "kill_chain_short_id", ocsf_field: "attacks.tactic.uid" }
|
# own kill chain, not an ATT&CK tactic, so it is no longer written there.
|
||||||
# ── Target host ───────────────────────────────────────────────────
|
# The array is forced with [...] so a one-TTP alert still yields a list.
|
||||||
- { source_path: "target", ocsf_field: "src_endpoint.hostname" }
|
- { source_path: "ttps ? [ttps[type = 'attack-pattern'].{'technique': {'name': name, 'uid': id}}]", ocsf_field: "attacks" }
|
||||||
|
# ── Source and target ─────────────────────────────────────────────
|
||||||
|
# An address goes to .ip, anything else (a host name) to .hostname. The
|
||||||
|
# target is where the activity landed: dst_endpoint, not src_endpoint.
|
||||||
|
- { source_path: "source and ($contains(source, ':') or $contains(source, /^[0-9]{1,3}(\\.[0-9]{1,3}){3}$/)) ? source", ocsf_field: "src_endpoint.ip" }
|
||||||
|
- { source_path: "source and $not($contains(source, ':') or $contains(source, /^[0-9]{1,3}(\\.[0-9]{1,3}){3}$/)) ? source", ocsf_field: "src_endpoint.hostname" }
|
||||||
|
- { source_path: "target and ($contains(target, ':') or $contains(target, /^[0-9]{1,3}(\\.[0-9]{1,3}){3}$/)) ? target", ocsf_field: "dst_endpoint.ip" }
|
||||||
|
- { source_path: "target and $not($contains(target, ':') or $contains(target, /^[0-9]{1,3}(\\.[0-9]{1,3}){3}$/)) ? target", ocsf_field: "dst_endpoint.hostname" }
|
||||||
# ── Owning entity / tenant ────────────────────────────────────────
|
# ── Owning entity / tenant ────────────────────────────────────────
|
||||||
- { source_path: "entity.name", ocsf_field: "cloud.org.name" }
|
- { source_path: "entity.name", ocsf_field: "cloud.org.name" }
|
||||||
- { source_path: "entity.uuid", ocsf_field: "cloud.org.uid" }
|
- { source_path: "entity.uuid", ocsf_field: "cloud.org.uid" }
|
||||||
- { source_path: "short_id", ocsf_field: "metadata.uid" }
|
- { source_path: "short_id", ocsf_field: "metadata.uid" }
|
||||||
- { source_path: "community_uuid", ocsf_field: "metadata.tenant_uid" }
|
- { source_path: "community_uuid", ocsf_field: "metadata.tenant_uid" }
|
||||||
|
- { source_path: "'Sekoia XDR'", ocsf_field: "metadata.product.name" }
|
||||||
|
- { source_path: "'Sekoia'", ocsf_field: "metadata.product.vendor_name" }
|
||||||
|
|||||||
Reference in New Issue
Block a user