Compare commits
3 Commits
157af87c17
...
220fa75b99
| Author | SHA1 | Date | |
|---|---|---|---|
| 220fa75b99 | |||
| 7e231c7c78 | |||
| b6903d4590 |
@@ -1,8 +1,8 @@
|
||||
id: rapid7_insightidr
|
||||
name: Rapid7 InsightIDR
|
||||
version: 1.1.1
|
||||
version: 1.3.0
|
||||
description: "Rapid7 InsightIDR (REST API) — investigation ingestion and full lifecycle (list/get/search/create/update/assign/set-status/bulk-close), investigation alerts and Rapid7 product alerts, custom threat indicators (add/replace), log management and LEQL log/log-set queries with downloads, and user directory search."
|
||||
changelog: "1.1.1 — list_alert_fields: default path_depth to 1 and expose the search filter. The V2 fields endpoint refuses an unbounded walk of the tree (\"root path with path depth greater than 1 is not supported for V2 without a search filter\"), so the bare call returned HTTP 400. | 1.1.0 — Add the Alert Triage API (/idr/at): search alerts, get an alert, its evidence, its actors and its process trees, plus the tenant's alert field catalogue. These carry the detection content the investigation endpoints do not — MITRE T-codes, matched rule keys, assets and accounts, command lines. MDR-gated. | 1.0.0 — Initial release: investigation ingestion (list_investigations) with an exhaustive OCSF mapper, 18 commands across investigations, alerts, custom threats, logs/LEQL search and users. API v1/v2 selectable per instance and per command."
|
||||
changelog: "1.3.0 — Add search_investigation_alerts: ingest only the alerts that belong to an investigation, with their full detection content, in 1+N+1 calls. Filtering search_alerts on investigation_rrn does not work — the term is accepted and silently matches nothing. | 1.2.1 — search_alerts: report a malformed terms_json with the input named and echoed, instead of json's bare \"Expecting value: line 1 column 1\". | 1.2.0 — Add the search_alerts OCSF mapper: rule_keys_of_interest carries the host, the process, its command line, its parent and the file hashes, so an alert-based feed indexes entities where the investigation feed indexed none. Validated against a live ABA alert. | 1.1.1 — list_alert_fields: default path_depth to 1 and expose the search filter. The V2 fields endpoint refuses an unbounded walk of the tree (\"root path with path depth greater than 1 is not supported for V2 without a search filter\"), so the bare call returned HTTP 400. | 1.1.0 — Add the Alert Triage API (/idr/at): search alerts, get an alert, its evidence, its actors and its process trees, plus the tenant's alert field catalogue. These carry the detection content the investigation endpoints do not — MITRE T-codes, matched rule keys, assets and accounts, command lines. MDR-gated. | 1.0.0 — Initial release: investigation ingestion (list_investigations) with an exhaustive OCSF mapper, 18 commands across investigations, alerts, custom threats, logs/LEQL search and users. API v1/v2 selectable per instance and per command."
|
||||
category: siem
|
||||
|
||||
# Per-instance configuration. The Insight platform region selects the API host
|
||||
@@ -337,7 +337,7 @@ commands:
|
||||
search_field: { type: string, description: "Comma-separated field ids for a single search term" }
|
||||
search_values: { type: string, description: "Comma-separated values the search_field must match (OR'ed together)" }
|
||||
search_operator: { type: string, description: "EQUALS, NOT_EQUALS, CONTAINS, GREATER_THAN or LESS_THAN (default EQUALS)" }
|
||||
terms_json: { type: string, description: "Raw JSON array of search terms, for criteria the simple triple cannot express. Takes precedence over search_field/search_values." }
|
||||
terms_json: { type: string, description: "Raw JSON array of search terms, for criteria the simple triple cannot express — e.g. [{\"field_ids\":[\"investigation_rrn\"],\"operator\":\"CONTAINS\",\"terms\":[\"rrn:investigation\"]}] to keep only alerts attached to an investigation. Paste the array alone, without the field name. Takes precedence over search_field/search_values." }
|
||||
sort_field: { type: string, description: "Field id to sort on" }
|
||||
sort_direction: { type: string, description: "ASC or DESC (default ASC)" }
|
||||
rrns_only: { type: string, description: "Return only alert RRNs instead of full details (true/false)" }
|
||||
@@ -414,6 +414,31 @@ commands:
|
||||
required: []
|
||||
outputs_schema: { properties: {} }
|
||||
|
||||
- id: search_investigation_alerts
|
||||
name: rapid7-insight-idr-search-investigation-alerts
|
||||
description: "Ingestion: the alerts that belong to an investigation, with their full detection content. Walks the investigations in a time window, collects their alert RRNs and retrieves the alerts in batches from the Alert Triage API — so an alert Rapid7 never folded into an investigation is never returned. Results path = alerts, dedup on rrn, incremental on start_time (the investigation's creation time). Use the search_alerts mapper: the alert objects are identical, with the parent investigation carried alongside under `investigation`."
|
||||
risk: read
|
||||
inputs_schema:
|
||||
properties:
|
||||
start_time: { type: string, description: "ISO-8601 lower bound on investigation created_time. Incremental fetch watermark." }
|
||||
end_time: { type: string, description: "ISO-8601 upper bound on investigation created_time" }
|
||||
default_days: { type: string, description: "Window size in days used when start_time is absent (default 1)" }
|
||||
statuses: { type: string, description: "Comma-separated investigation statuses to include (open, investigating, closed). Leaving out closed skips what the source has already triaged." }
|
||||
priorities: { type: string, description: "Comma-separated investigation priorities to include (Unspecified, Low, Medium, High, Critical)" }
|
||||
field_ids: { type: string, description: "Comma-separated additional alert field ids to include (see rapid7-insight-idr-list-alert-fields)" }
|
||||
include_investigation: { type: string, description: "Carry the parent investigation on each alert under `investigation` (true/false, default true)" }
|
||||
max_alerts: { type: string, description: "Ceiling on alert RRNs collected in one run (default 500)" }
|
||||
time_budget_seconds: { type: string, description: "Stop walking investigations after this many seconds and report truncated=true, rather than exceed the server's 90-second command budget and lose the whole run (default 60)" }
|
||||
index: { type: string, description: "0-based page index of investigations" }
|
||||
page_size: { type: string, description: "Investigation page size" }
|
||||
limit: { type: number, description: "Maximum number of investigations to walk (default 50)" }
|
||||
required: []
|
||||
outputs_schema: { properties: {} }
|
||||
ingest:
|
||||
results_path: alerts
|
||||
dedup_key: rrn
|
||||
incremental_field: start_time
|
||||
|
||||
# ── Connectivity test ─────────────────────────────────────────────────────
|
||||
- id: test_connection
|
||||
name: rapid7-insight-idr-test-connection
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
name: "Rapid7 InsightIDR Alerts → OCSF"
|
||||
description: |
|
||||
Maps one Rapid7 alert from the Alert Triage API to OCSF finding fields. Serves both
|
||||
search_alerts and search_investigation_alerts (results_path = alerts on either): the
|
||||
alert objects are identical, and the second command simply carries the parent
|
||||
investigation alongside under `investigation`.
|
||||
|
||||
This mapper exists because the investigation feed cannot carry a detection. An
|
||||
investigation, and the /investigations/{rrn}/alerts list behind it, hold a title, a
|
||||
priority, a status and timestamps — no host, no account, no process, no MITRE code. An
|
||||
incident built from one indexes zero entities, so it cannot be pivoted on, cannot be
|
||||
correlated, and gives an analyst nothing to read.
|
||||
|
||||
The alert carries all of it in rule_keys_of_interest: a list of {key, values} pairs
|
||||
whose keys are already dotted paths (process.cmd_line, parent_process.name, hostname).
|
||||
Reading them needs a JSONata predicate per key rather than a plain path, which is why
|
||||
every entry below looks heavier than in other mappers. A key a given rule does not emit
|
||||
resolves to nothing and is skipped, so the same file covers every detection rule.
|
||||
field_mappings:
|
||||
title: "title"
|
||||
# toSeverity maps CRITICAL→5, HIGH→4, MEDIUM→3, LOW→2, and anything else to the alert
|
||||
# rule's default. (The investigation mapper's comment on this is wrong — checked
|
||||
# against internal/incidenttypes/engine.go.)
|
||||
severity: "priority"
|
||||
source: "external_source"
|
||||
# Rapid7's own triage state. It cannot reach the incident's status or verdict columns —
|
||||
# MapFields only accepts title, severity, source, description and custom:* — so it is
|
||||
# kept as custom fields, where an analyst at least sees that the source already closed
|
||||
# this alert and what it concluded.
|
||||
"custom:rapid7_status": "status"
|
||||
"custom:rapid7_disposition": "disposition"
|
||||
# Rapid7's AI suggestion, exposed as a fact and never as a verdict.
|
||||
"custom:rapid7_ai_disposition": "ai_suggested_disposition"
|
||||
"custom:rapid7_responsibility": "responsibility"
|
||||
# The log identifiers a LEQL query needs to pull the raw event back out of Log Search.
|
||||
# Kept as custom fields rather than forced onto an OCSF field that means something else.
|
||||
"custom:rapid7_log_id": "log_details[0].log_id"
|
||||
"custom:rapid7_logset_id": "log_details[0].logset_id"
|
||||
ocsf:
|
||||
# ── Finding ───────────────────────────────────────────────────────
|
||||
- { source_path: "rrn", ocsf_field: "finding_info.uid" }
|
||||
- { source_path: "external_id", ocsf_field: "finding_info.uid_alt" }
|
||||
- { source_path: "title", ocsf_field: "finding_info.title" }
|
||||
- { source_path: "created_at", ocsf_field: "finding_info.created_time" }
|
||||
- { source_path: "updated_at", ocsf_field: "finding_info.modified_time" }
|
||||
- { source_path: "alerted_at", ocsf_field: "finding_info.first_seen_time" }
|
||||
- { source_path: "tags", ocsf_field: "metadata.labels" }
|
||||
# The detection rule. `rule.name` is documented but absent from live ABA alerts, so
|
||||
# `type` — which repeats the rule's display name — is the honest fallback.
|
||||
- { source_path: "type", ocsf_field: "finding_info.analytic.name" }
|
||||
- { source_path: "rule.rrn", ocsf_field: "finding_info.analytic.uid" }
|
||||
- { source_path: "rule.version_rrn", ocsf_field: "finding_info.analytic.version" }
|
||||
# ── Detection time: the MTTD anchor ───────────────────────────────
|
||||
# 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.
|
||||
- { source_path: "created_at", ocsf_field: "time" }
|
||||
- { source_path: "alerted_at", ocsf_field: "time" }
|
||||
# ── State, as the source sees it ──────────────────────────────────
|
||||
- { source_path: "status", ocsf_field: "status" }
|
||||
- { source_path: "disposition", ocsf_field: "disposition" }
|
||||
- { source_path: "priority", ocsf_field: "priority" }
|
||||
- { source_path: "responsibility", ocsf_field: "comment" }
|
||||
# ── Product identity ──────────────────────────────────────────────
|
||||
- { source_path: "'Rapid7 InsightIDR'", ocsf_field: "metadata.product.name" }
|
||||
- { source_path: "'Rapid7'", ocsf_field: "metadata.product.vendor_name" }
|
||||
- { source_path: "external_source", ocsf_field: "metadata.log_source" }
|
||||
- { source_path: "triggering_event_type", ocsf_field: "activity_name" }
|
||||
- { source_path: "triggering_event_sub_type", ocsf_field: "status_detail" }
|
||||
# The parent investigation, on the OCSF field meant for joining findings that belong
|
||||
# together. Every alert Rapid7 folded into one investigation carries the same value,
|
||||
# which is what lets the correlation engine rebuild that grouping as a case.
|
||||
- { source_path: "investigation_rrn", ocsf_field: "metadata.correlation_uid" }
|
||||
- { source_path: "organization.id", ocsf_field: "cloud.org.uid" }
|
||||
- { source_path: "organization.name", ocsf_field: "cloud.org.name" }
|
||||
# ── MITRE ATT&CK ──────────────────────────────────────────────────
|
||||
# mitre_tcodes mixes both kinds in one array: "TA0040" is a tactic, "T1490" a
|
||||
# technique. Splitting on the "TA" prefix keeps a tactic out of the technique field,
|
||||
# where it would name a technique that does not exist.
|
||||
- { source_path: "rule.mitre_tcodes[$substring($, 0, 2) = 'TA'][0]", ocsf_field: "attacks.tactic.uid" }
|
||||
- { source_path: "rule.mitre_tcodes[$substring($, 0, 2) != 'TA'][0]", ocsf_field: "attacks.technique.uid" }
|
||||
# ── The affected host ─────────────────────────────────────────────
|
||||
# device.hostname and src_endpoint.hostname both index the host as an entity; the
|
||||
# duplication is what other vendor mappers do and what the pivot graph reads.
|
||||
- { source_path: "rule_keys_of_interest[key='hostname'].values[0]", ocsf_field: "device.hostname" }
|
||||
- { source_path: "rule_keys_of_interest[key='hostname'].values[0]", ocsf_field: "src_endpoint.hostname" }
|
||||
# ── The process the alert is about ────────────────────────────────
|
||||
- { source_path: "rule_keys_of_interest[key='process.name'].values[0]", ocsf_field: "process.name" }
|
||||
- { source_path: "rule_keys_of_interest[key='process.name'].values[0]", ocsf_field: "process.file.name" }
|
||||
- { source_path: "rule_keys_of_interest[key='process.cmd_line'].values[0]", ocsf_field: "process.cmd_line" }
|
||||
- { source_path: "rule_keys_of_interest[key='process.exe_path'].values[0]", ocsf_field: "process.file.path" }
|
||||
- { source_path: "rule_keys_of_interest[key='process.exe_file.hashes.md5'].values[0]", ocsf_field: "process.file.hashes.md5" }
|
||||
- { source_path: "rule_keys_of_interest[key='process.exe_file.hashes.sha256'].values[0]", ocsf_field: "process.file.hashes.sha256" }
|
||||
- { source_path: "rule_keys_of_interest[key='process.pid'].values[0]", ocsf_field: "process.pid" }
|
||||
# ── Its parent, which is what says how it was launched ────────────
|
||||
- { source_path: "rule_keys_of_interest[key='parent_process.name'].values[0]", ocsf_field: "actor.process.parent_process.name" }
|
||||
- { source_path: "rule_keys_of_interest[key='parent_process.cmd_line'].values[0]", ocsf_field: "actor.process.parent_process.cmd_line" }
|
||||
- { source_path: "rule_keys_of_interest[key='parent_process.exe_path'].values[0]", ocsf_field: "actor.process.parent_process.file.path" }
|
||||
- { source_path: "rule_keys_of_interest[key='parent_process.exe_file.hashes.md5'].values[0]", ocsf_field: "actor.process.parent_process.file.hashes.md5" }
|
||||
- { source_path: "rule_keys_of_interest[key='parent_process.exe_file.hashes.sha256'].values[0]", ocsf_field: "actor.process.parent_process.file.hashes.sha256" }
|
||||
# ── The acting account ────────────────────────────────────────────
|
||||
# There is no canonical user namespace in this tenant's field catalogue, so the account
|
||||
# only appears when a rule names it among its keys of interest. Both spellings are
|
||||
# mapped because rules are not consistent about which one they emit.
|
||||
- { source_path: "rule_keys_of_interest[key='user'].values[0]", ocsf_field: "actor.user.name" }
|
||||
- { source_path: "rule_keys_of_interest[key='username'].values[0]", ocsf_field: "actor.user.name" }
|
||||
- { source_path: "rule_keys_of_interest[key='account'].values[0]", ocsf_field: "actor.user.account.name" }
|
||||
# ── Network, when the rule is about a flow ────────────────────────
|
||||
- { source_path: "rule_keys_of_interest[key='source_address'].values[0]", ocsf_field: "src_endpoint.ip" }
|
||||
- { source_path: "rule_keys_of_interest[key='destination_address'].values[0]", ocsf_field: "dst_endpoint.ip" }
|
||||
- { source_path: "rule_keys_of_interest[key='destination_port'].values[0]", ocsf_field: "dst_endpoint.port" }
|
||||
# ── Who owns it at the source ─────────────────────────────────────
|
||||
- { source_path: "assignee.first_name & ' ' & assignee.last_name", ocsf_field: "assignee.name" }
|
||||
- { source_path: "assignee.email", ocsf_field: "assignee.email_addr" }
|
||||
# ── The log entry behind the alert ────────────────────────────────
|
||||
# Not investigable on its own, but it is the handle a LEQL query needs to pull the raw
|
||||
# event out of Log Search — the only bridge from an alert back to the line that made it.
|
||||
- { source_path: "log_details[0].log_entry_id", ocsf_field: "metadata.uid" }
|
||||
# ── The parent investigation ──────────────────────────────────────
|
||||
# Present only on the search_investigation_alerts feed, which carries the container
|
||||
# beside the alert. Absent on search_alerts, where these resolve to nothing and are
|
||||
# skipped — the same file serves both.
|
||||
- { source_path: "investigation.title", ocsf_field: "finding_info.related_events.title" }
|
||||
- { source_path: "investigation.status", ocsf_field: "finding_info.related_events.status" }
|
||||
- { source_path: "investigation.priority", ocsf_field: "finding_info.related_events.priority" }
|
||||
- { source_path: "investigation.disposition", ocsf_field: "finding_info.related_events.disposition" }
|
||||
- { source_path: "investigation.rrn", ocsf_field: "finding_info.related_events.uid" }
|
||||
@@ -54,8 +54,20 @@ def _terms(inp):
|
||||
"""
|
||||
raw = inp.get("terms_json")
|
||||
if raw:
|
||||
parsed = json.loads(raw) if isinstance(raw, str) else raw
|
||||
return parsed if isinstance(parsed, list) else [parsed]
|
||||
if isinstance(raw, str):
|
||||
try:
|
||||
parsed = json.loads(raw)
|
||||
except ValueError as e:
|
||||
# json's own message ("Expecting value: line 1 column 1") names
|
||||
# neither the input nor what it received, so an operator who
|
||||
# pasted the field name along with the value — the obvious
|
||||
# mistake, since both are shown together — gets a parser error
|
||||
# with nothing to act on. Echo the head of what arrived.
|
||||
head = raw[:80] + ("…" if len(raw) > 80 else "")
|
||||
raise ValueError("terms_json is not valid JSON (" + str(e) + "). "
|
||||
"Expected a JSON array of search terms, got: " + head)
|
||||
return parsed if isinstance(parsed, list) else [parsed]
|
||||
return raw if isinstance(raw, list) else [raw]
|
||||
field_ids = _list(inp.get("search_field"))
|
||||
values = _list(inp.get("search_values"))
|
||||
if not field_ids or not values:
|
||||
|
||||
@@ -0,0 +1,156 @@
|
||||
import json, os, sys, urllib.request, urllib.parse, urllib.error
|
||||
|
||||
# The Alert Triage API lives under /idr/at, a base of its own — not /idr/v1 or
|
||||
# /idr/v2 like the investigation endpoints. Same host, same API key.
|
||||
AT = "idr/at/"
|
||||
|
||||
|
||||
def _cfg():
|
||||
s = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||
region = str(s.get("region") or "us").strip().lower()
|
||||
base = "https://" + region + ".api.insight.rapid7.com/"
|
||||
headers = {"X-Api-Key": s.get("api_key", ""), "Content-Type": "application/json", "Accept": "application/json"}
|
||||
multi = str(s.get("is_multi_customer") or "").strip().lower() in ("1", "true", "yes")
|
||||
return base, headers, multi
|
||||
|
||||
|
||||
def _inputs():
|
||||
return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||
|
||||
|
||||
def _list(v):
|
||||
if v in (None, ""):
|
||||
return []
|
||||
if isinstance(v, list):
|
||||
return [str(x).strip() for x in v if str(x).strip()]
|
||||
return [p.strip() for p in str(v).split(",") if p.strip()]
|
||||
|
||||
|
||||
def request(method, path, params=None, body=None):
|
||||
base, headers, _ = _cfg()
|
||||
url = base + path.lstrip("/")
|
||||
if params:
|
||||
clean = {k: v for k, v in params.items() if v not in (None, "")}
|
||||
if clean:
|
||||
url += ("&" if "?" in url else "?") + urllib.parse.urlencode(clean, doseq=True)
|
||||
data = json.dumps(body).encode("utf-8") if body is not None else None
|
||||
req = urllib.request.Request(url, data=data, headers=headers, method=method)
|
||||
with urllib.request.urlopen(req, timeout=90) as r:
|
||||
raw = r.read()
|
||||
return json.loads(raw) if raw else {}
|
||||
|
||||
import time
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
ISO = "%Y-%m-%dT%H:%M:%SZ"
|
||||
# /alerts/ops/rrns takes a list; batching keeps one URL and one body reasonable.
|
||||
RRN_BATCH = 100
|
||||
# The server gives a command 90 seconds. Stop walking investigations well before
|
||||
# that and report the truncation, rather than have the whole run killed and every
|
||||
# alert in the window lost — a partial fetch that says so beats a silent zero.
|
||||
DEFAULT_BUDGET_S = 60
|
||||
|
||||
|
||||
def run():
|
||||
_, _, multi = _cfg()
|
||||
inp = _inputs()
|
||||
limit = int(inp.get("limit") or 50)
|
||||
max_alerts = int(inp.get("max_alerts") or 500)
|
||||
budget = float(inp.get("time_budget_seconds") or DEFAULT_BUDGET_S)
|
||||
started = time.monotonic()
|
||||
|
||||
start_time = inp.get("start_time")
|
||||
if not start_time:
|
||||
days = int(inp.get("default_days") or 1)
|
||||
start_time = (datetime.now(timezone.utc) - timedelta(days=days)).strftime(ISO)
|
||||
|
||||
# 1) The investigations in the window. This is the unit that decides what is
|
||||
# worth ingesting: an alert Rapid7 never folded into one is not returned by
|
||||
# this command at all, which is the whole point of it.
|
||||
inv_page = request("GET", "idr/v2/investigations", params={
|
||||
"index": inp.get("index") or "0",
|
||||
"size": inp.get("page_size") or limit,
|
||||
"statuses": inp.get("statuses"),
|
||||
"priorities": inp.get("priorities"),
|
||||
"start_time": start_time,
|
||||
"end_time": inp.get("end_time"),
|
||||
"sort_field": "Created time",
|
||||
"sort_direction": "ASC",
|
||||
"multi-customer": "true" if multi else None,
|
||||
})
|
||||
investigations = (inv_page.get("data") or [])[:limit]
|
||||
|
||||
# 2) Their alert RRNs. One call per investigation — there is no bulk form —
|
||||
# so this is the step the time budget guards.
|
||||
order, by_rrn, truncated = [], {}, False
|
||||
for inv in investigations:
|
||||
if time.monotonic() - started > budget or len(order) >= max_alerts:
|
||||
truncated = True
|
||||
break
|
||||
inv_rrn = inv.get("rrn") or ""
|
||||
if not inv_rrn:
|
||||
continue
|
||||
try:
|
||||
page = request("GET", "idr/v2/investigations/" + urllib.parse.quote(inv_rrn, safe="") + "/alerts",
|
||||
params={"multi-customer": "true" if multi else None})
|
||||
except urllib.error.HTTPError:
|
||||
# One unreadable investigation must not cost the whole window. The
|
||||
# alert simply does not appear; the counts below say how many did.
|
||||
continue
|
||||
for a in (page.get("data") or []):
|
||||
rrn = a.get("id") or a.get("rrn")
|
||||
if rrn and rrn not in by_rrn:
|
||||
order.append(rrn)
|
||||
by_rrn[rrn] = inv
|
||||
if len(order) >= max_alerts:
|
||||
truncated = True
|
||||
break
|
||||
|
||||
# 3) The alerts themselves, in batches. This is where the detection content
|
||||
# lives — rule_keys_of_interest, mitre_tcodes, disposition — none of which
|
||||
# the investigation or its alert list carries.
|
||||
field_ids = _list(inp.get("field_ids"))
|
||||
alerts = []
|
||||
for i in range(0, len(order), RRN_BATCH):
|
||||
batch = order[i:i + RRN_BATCH]
|
||||
body = {"rrns": batch}
|
||||
if field_ids:
|
||||
body["field_ids"] = field_ids
|
||||
out = request("POST", AT + "alerts/ops/rrns", body=body)
|
||||
alerts.extend(out.get("alerts") or [])
|
||||
|
||||
# The investigation each alert came from, carried alongside so a mapper can
|
||||
# read the container's own title, priority and disposition without a second
|
||||
# feed. Alert fields are left untouched: `investigation_rrn` already exists
|
||||
# on the alert and nothing here may shadow it.
|
||||
if str(inp.get("include_investigation") or "true").strip().lower() in ("1", "true", "yes"):
|
||||
for a in alerts:
|
||||
inv = by_rrn.get(a.get("rrn"))
|
||||
if inv:
|
||||
a["investigation"] = inv
|
||||
|
||||
print(json.dumps({
|
||||
"alerts": alerts,
|
||||
"metadata": {
|
||||
"investigations_read": len(investigations),
|
||||
"alert_rrns_collected": len(order),
|
||||
"alerts_returned": len(alerts),
|
||||
"truncated": truncated,
|
||||
"elapsed_seconds": round(time.monotonic() - started, 1),
|
||||
},
|
||||
}))
|
||||
|
||||
|
||||
try:
|
||||
run()
|
||||
except urllib.error.HTTPError as e:
|
||||
detail = e.read().decode("utf-8", "replace")
|
||||
if e.code in (401, 403):
|
||||
detail += (" — the Alert Triage API (/idr/at) is only available to Managed Detection and"
|
||||
" Response customers with access to the Alerts experience; a key that works on"
|
||||
" the investigation endpoints can still be refused here")
|
||||
print(json.dumps({"error": "HTTP " + str(e.code), "detail": detail}))
|
||||
sys.exit(1)
|
||||
except Exception as e:
|
||||
print(json.dumps({"error": str(e)}))
|
||||
sys.exit(1)
|
||||
Reference in New Issue
Block a user