feat(rapid7-insightidr): InsightIDR integration (19 commands + OCSF ingestion)

REST API integration for Rapid7 InsightIDR. Investigation ingestion
(list_investigations) with an exhaustive OCSF mapper and a bundled default
incident type, plus 18 commands across investigations (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.

API v1/v2 selectable per instance (is_v2) and per command (api_version);
multi-customer query parameter supported on v2 calls.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Guillaume BOURGEOIS
2026-06-27 14:00:29 +02:00
parent 6bccbb5a50
commit 729c339e2f
22 changed files with 1597 additions and 0 deletions
@@ -0,0 +1,3 @@
name: "Rapid7 InsightIDR Investigation"
color: "#ff5630"
icon: "alert"
@@ -0,0 +1,331 @@
id: rapid7_insightidr
name: Rapid7 InsightIDR
version: 1.0.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.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
# (https://<region>.api.insight.rapid7.com). Authentication is an Organization
# API key sent in the X-Api-Key header.
config_schema:
properties:
region:
type: string
description: "Insight platform region: us, eu, ca, au or ap"
default: us
api_key:
type: string
description: "InsightIDR Organization API key (Read/Write)"
x-soar-sensitive: true
is_v2:
type: string
description: "Use API v2 by default for investigation commands (true/false). Can be overridden per command with api_version. Defaults to true (v2)."
default: "true"
is_multi_customer:
type: string
description: "Set to true if the API key has multi-customer access (adds the multi-customer query parameter on v2 calls)."
default: "false"
required:
- region
- api_key
auth:
- id: apikey
type: api_key
in: header
name: X-Api-Key
value_template: "{{secret}}"
secret_field: api_key
commands:
# ── Ingestion ───────────────────────────────────────────────────────────────
- id: list_investigations
name: rapid7-insight-idr-list-investigations
description: "List investigations, sorted by created_time descending. Used for ingestion: results path = data. Investigations aggregate the applicable alert data and are tied to alerts and detection rules."
risk: read
inputs_schema:
properties:
api_version: { type: string, description: "API version to use: V1, V2 or Default (uses the instance default)" }
statuses: { type: string, description: "Comma-separated statuses to include (open, investigating, closed)" }
start_time: { type: string, description: "Only investigations created after this ISO-8601 timestamp (e.g. 2018-07-01T00:00:00Z). Incremental fetch watermark; V2 only." }
end_time: { type: string, description: "Only investigations created before this ISO-8601 timestamp. V2 only." }
time_range: { type: string, description: "Relative time range string (e.g. 1 week, 1 day) instead of start_time/end_time" }
sources: { type: string, description: "Comma-separated sources to include (User, Alert). V2 only." }
priorities: { type: string, description: "Comma-separated priorities to include (Unspecified, Low, Medium, High, Critical). V2 only." }
assignee_email: { type: string, description: "Only investigations assigned to this user email" }
tags: { type: string, description: "Comma-separated tags; only investigations having all of them are returned. V2 only." }
sort_field: { type: string, description: "Field to sort by (Created time, Priority, RRN Last Created Alert, Last Detection Alert). V2 only." }
sort_direction: { type: string, description: "Sort direction (ASC, DESC). V2 only." }
index: { type: string, description: "0-based page index" }
page_size: { type: string, description: "Page size (1-1000)" }
limit: { type: number, description: "Maximum number of records to retrieve (default 50)" }
required: []
outputs_schema: { properties: {} }
ingest:
results_path: data
dedup_key: rrn
incremental_field: start_time
- id: get_investigation
name: rapid7-insight-idr-get-investigation
description: "Get a single investigation by ID or Rapid7 Resource Name (RRN). With api_version=V2 the ID must be in RRN format."
risk: read
inputs_schema:
properties:
api_version: { type: string, description: "API version to use: V1, V2 or Default" }
investigation_id: { type: string, description: "Investigation ID or RRN" }
required: [investigation_id]
outputs_schema: { properties: {} }
- id: search_investigation
name: rapid7-insight-idr-search-investigation
description: "Search investigations matching the given search/sort criteria (v2)."
risk: read
inputs_schema:
properties:
start_time: { type: string, description: "ISO-8601 lower bound on created_time (default 28 days ago)" }
end_time: { type: string, description: "ISO-8601 upper bound on created_time (default now)" }
actor_asset_hostname: { type: string, description: "Comma-separated values; CONTAINS match on actor_asset_hostname" }
actor_user_name: { type: string, description: "Comma-separated values; CONTAINS match on actor_user_name" }
alert_mitre_t_codes: { type: string, description: "Comma-separated values; EQUALS match on alert_mitre_t_codes" }
alert_rule_rrn: { type: string, description: "Comma-separated values; EQUALS match on alert_rule_rrn" }
assignee_id: { type: string, description: "Comma-separated values; EQUALS match on assignee_id" }
organization_id: { type: string, description: "Comma-separated values; EQUALS match on organization_id" }
priority: { type: string, description: "Comma-separated values; EQUALS match on priority (UNSPECIFIED, LOW, MEDIUM, HIGH, CRITICAL)" }
rrn: { type: string, description: "Comma-separated values; EQUALS match on rrn" }
source: { type: string, description: "Comma-separated values; EQUALS match on source (USER, ALERT)" }
status: { type: string, description: "Comma-separated values; EQUALS match on status (OPEN, CLOSED, INVESTIGATING, WAITING)" }
title: { type: string, description: "Comma-separated values; CONTAINS match on title" }
sort: { type: string, description: "Comma-separated fields to sort by (Created time, Priority, RRN, Alert created time, Alert detection created time)" }
sort_direction: { type: string, description: "Sort direction (asc, desc)" }
index: { type: string, description: "0-based page index" }
page_size: { type: string, description: "Page size (1-1000)" }
limit: { type: number, description: "Maximum number of records to retrieve (default 50)" }
required: []
outputs_schema: { properties: {} }
- id: create_investigation
name: rapid7-insight-idr-create-investigation
description: "Create a new investigation manually (v2)."
risk: safe_write
inputs_schema:
properties:
title: { type: string, description: "Name of the investigation" }
status: { type: string, description: "Open, Investigating or Closed (default Open)" }
priority: { type: string, description: "Unspecified, Low, Medium, High or Critical (default Unspecified)" }
disposition: { type: string, description: "Undecided, Benign, Malicious or Not Applicable (default Undecided)" }
user_email_address: { type: string, description: "Email of the user to assign the investigation to" }
required: [title]
outputs_schema: { properties: {} }
- id: update_investigation
name: rapid7-insight-idr-update-investigation
description: "Update fields of an investigation by ID or RRN (v2)."
risk: safe_write
inputs_schema:
properties:
investigation_id: { type: string, description: "Investigation ID or RRN to update" }
title: { type: string, description: "Name of the investigation" }
status: { type: string, description: "Open, Investigating or Closed" }
priority: { type: string, description: "Unspecified, Low, Medium, High or Critical" }
disposition: { type: string, description: "Undecided, Benign, Malicious or Not Applicable" }
user_email_address: { type: string, description: "Email of the user to assign the investigation to" }
threat_command_free_text: { type: string, description: "Additional text when closing an associated Threat Command alert (status=Closed)" }
threat_command_close_reason: { type: string, description: "Threat Command close reason (status=Closed)" }
required: [investigation_id]
outputs_schema: { properties: {} }
- id: assign_user
name: rapid7-insight-idr-assign-user
description: "Assign a user (by email) to one or more investigations. With api_version=V2 the IDs must be in RRN format."
risk: safe_write
inputs_schema:
properties:
api_version: { type: string, description: "API version to use: V1, V2 or Default" }
investigation_id: { type: string, description: "Comma-separated investigation IDs or RRNs" }
user_email_address: { type: string, description: "Email of the user to assign" }
required: [investigation_id, user_email_address]
outputs_schema: { properties: {} }
- id: set_status
name: rapid7-insight-idr-set-status
description: "Set the status of one or more investigations. Closing requires a disposition (v2). With api_version=V2 the IDs must be in RRN format."
risk: safe_write
inputs_schema:
properties:
api_version: { type: string, description: "API version to use: V1, V2 or Default" }
investigation_id: { type: string, description: "Comma-separated investigation IDs or RRNs" }
status: { type: string, description: "open, closed, investigating or waiting" }
disposition: { type: string, description: "benign, malicious or not_applicable (status=closed, V2 only)" }
threat_command_close_reason: { type: string, description: "Threat Command close reason (status=closed, V2 only)" }
threat_command_free_text: { type: string, description: "Additional text for a Threat Command alert (status=closed, V2 only)" }
required: [investigation_id, status]
outputs_schema: { properties: {} }
- id: close_investigations
name: rapid7-insight-idr-close-investigations
description: "Bulk-close all investigations matching the request parameters within a time window (v2)."
risk: safe_write
inputs_schema:
properties:
source: { type: string, description: "Investigation source to close: ALERT, MANUAL or HUNT. ALERT requires alert_type or detection_rule_rrn." }
start_time: { type: string, description: "ISO-8601 lower bound on createTime (e.g. 2018-07-01T00:00:00Z)" }
end_time: { type: string, description: "ISO-8601 upper bound on createTime (e.g. 2018-07-28T23:59:00Z)" }
alert_type: { type: string, description: "Alert category to close. Required when source=ALERT." }
disposition: { type: string, description: "Disposition to set: Undecided, Benign, Malicious or Not Applicable (default Not Applicable)" }
detection_rule_rrn: { type: string, description: "Detection rule RRN; only investigations linked to it are closed. Requires alert_type 'Attacker Behavior Detected'." }
max_investigations_to_close: { type: string, description: "Maximum number of investigations to close (no maximum if omitted)" }
required: [source, start_time, end_time]
outputs_schema: { properties: {} }
- id: list_investigation_alerts
name: rapid7-insight-idr-list-investigation-alerts
description: "List all alerts associated with an investigation, sorted by alert created time descending (v2)."
risk: read
inputs_schema:
properties:
investigation_id: { type: string, description: "Investigation ID or RRN (V2 RRN format)" }
all_results: { type: string, description: "Return all results, overriding the limit (true/false, default false)" }
limit: { type: number, description: "Maximum number of records to retrieve (default 50)" }
required: [investigation_id]
outputs_schema: { properties: {} }
- id: list_investigation_product_alerts
name: rapid7-insight-idr-list-investigation-product-alerts
description: "List all Rapid7 product alerts (from other Rapid7 products) associated with an investigation (v2)."
risk: read
inputs_schema:
properties:
investigation_id: { type: string, description: "Investigation ID or RRN (V2 RRN format)" }
all_results: { type: string, description: "Return all results, overriding the limit (true/false, default false)" }
limit: { type: number, description: "Maximum number of records to retrieve (default 50)" }
required: [investigation_id]
outputs_schema: { properties: {} }
- id: add_threat_indicators
name: rapid7-insight-idr-add-threat-indicators
description: "Add indicators (IPs, hashes, domains, URLs) to one or more custom threats by key."
risk: safe_write
inputs_schema:
properties:
key: { type: string, description: "Comma-separated threat keys to add indicators to" }
ip_addresses: { type: string, description: "Comma-separated IP address indicators" }
hashes: { type: string, description: "Comma-separated hash indicators" }
domain_names: { type: string, description: "Comma-separated domain indicators" }
url: { type: string, description: "Comma-separated URL indicators" }
required: [key]
outputs_schema: { properties: {} }
- id: replace_threat_indicators
name: rapid7-insight-idr-replace-threat-indicators
description: "Replace all indicators of one or more custom threats: deletes existing indicators and adds the supplied ones."
risk: safe_write
inputs_schema:
properties:
key: { type: string, description: "Comma-separated threat keys to replace indicators for" }
ip_addresses: { type: string, description: "Comma-separated IP address indicators" }
hashes: { type: string, description: "Comma-separated hash indicators" }
domain_names: { type: string, description: "Comma-separated domain indicators" }
url: { type: string, description: "Comma-separated URL indicators" }
required: [key]
outputs_schema: { properties: {} }
- id: list_logs
name: rapid7-insight-idr-list-logs
description: "List all logs available to the account."
risk: read
inputs_schema:
properties: {}
required: []
outputs_schema: { properties: {} }
- id: list_log_sets
name: rapid7-insight-idr-list-log-sets
description: "List all log sets configured for the InsightIDR instance."
risk: read
inputs_schema:
properties: {}
required: []
outputs_schema: { properties: {} }
- id: query_log
name: rapid7-insight-idr-query-log
description: "Run a LEQL query against a single log, following pagination links to gather all events."
risk: read
inputs_schema:
properties:
log_id: { type: string, description: "Log key to query" }
query: { type: string, description: "Valid LEQL query (no calculations)" }
time_range: { type: string, description: "Relative time range (e.g. 1 week, 1 day). When set, start_time/end_time are not needed." }
start_time: { type: string, description: "Lower bound as a UNIX timestamp in milliseconds" }
end_time: { type: string, description: "Upper bound as a UNIX timestamp in milliseconds" }
logs_per_page: { type: string, description: "Maximum log entries per page (default 50)" }
sequence_number: { type: string, description: "Earliest sequence number of a log entry to start from" }
required: [log_id, query]
outputs_schema: { properties: {} }
- id: query_log_set
name: rapid7-insight-idr-query-log-set
description: "Run a LEQL query against a log set, following pagination links to gather all events."
risk: read
inputs_schema:
properties:
log_set_id: { type: string, description: "Log set ID to query" }
query: { type: string, description: "Valid LEQL query (no calculations)" }
time_range: { type: string, description: "Relative time range (e.g. 1 week, 1 day). When set, start_time/end_time are not needed." }
start_time: { type: string, description: "Lower bound as a UNIX timestamp in milliseconds" }
end_time: { type: string, description: "Upper bound as a UNIX timestamp in milliseconds" }
logs_per_page: { type: string, description: "Maximum log entries per page (default 50)" }
sequence_number: { type: string, description: "Earliest sequence number of a log entry to start from" }
required: [log_set_id, query]
outputs_schema: { properties: {} }
- id: download_logs
name: rapid7-insight-idr-download-logs
description: "Download raw log events for up to 10 logs over a time window, optionally filtered by a LEQL query. Returns the events as text."
risk: read
inputs_schema:
properties:
log_ids: { type: string, description: "Comma-separated log IDs to download (up to 10)" }
time_range: { type: string, description: "Relative time range (e.g. Last 4 Days). Defaults to Last 3 days when no bounds are given." }
start_time: { type: string, description: "Lower bound as a UNIX timestamp in milliseconds" }
end_time: { type: string, description: "Upper bound as a UNIX timestamp in milliseconds" }
query: { type: string, description: "LEQL query to match desired events (no calculations)" }
limit: { type: string, description: "Maximum number of events to download (cannot exceed 20 million)" }
required: [log_ids]
outputs_schema: { properties: {} }
- id: list_users
name: rapid7-insight-idr-list-users
description: "List users matching the search/sort criteria, or retrieve a single user by RRN."
risk: read
inputs_schema:
properties:
rrn: { type: string, description: "RRN of a single user to retrieve. When set, the other filters are ignored." }
first_name: { type: string, description: "Comma-separated values to match on first_name (requires search_operator)" }
last_name: { type: string, description: "Comma-separated values to match on last_name (requires search_operator)" }
name: { type: string, description: "Comma-separated values to match on name (requires search_operator)" }
search_operator: { type: string, description: "Filter operator when first_name/last_name/name are used: contains or equals" }
sort: { type: string, description: "Comma-separated fields to sort by (first_name, last_name, name)" }
sort_direction: { type: string, description: "Sort direction (asc, desc)" }
index: { type: string, description: "0-based page index" }
page_size: { type: string, description: "Page size (1-1000)" }
limit: { type: number, description: "Maximum number of records to retrieve (default 50)" }
required: []
outputs_schema: { properties: {} }
# ── Connectivity test ─────────────────────────────────────────────────────
- id: test_connection
name: rapid7-insight-idr-test-connection
description: "Verify connectivity and credentials (used by the Test button)."
risk: read
inputs_schema:
properties: {}
required: []
outputs_schema: { properties: {} }
ingestion:
command: list_investigations
mapper: list_investigations
default_incident_type: "Rapid7 InsightIDR Investigation"
@@ -0,0 +1,33 @@
name: "Rapid7 InsightIDR Investigations → OCSF"
description: "Maps a Rapid7 InsightIDR investigation (idr/v2/investigations, results_path = data) to OCSF Incident Finding fields. Investigations aggregate the applicable alert data; use list_investigation_alerts for per-alert detail. v1 (id) and v2 (rrn) shapes are both covered via fallbacks."
field_mappings:
title: "title"
# toSeverity maps CRITICAL→5, HIGH→3, MEDIUM→2, LOW→1; UNSPECIFIED falls back to the rule default.
severity: "priority"
# How the investigation was generated (USER / ALERT).
source: "source"
# results_path = data; source_path is JSONata over ONE investigation object.
# Paths absent from a given investigation return nothing and are skipped, so v1/v2 fallbacks are safe.
ocsf:
# ── Finding ───────────────────────────────────────────────────────
- { source_path: "rrn ? rrn : id", ocsf_field: "finding_info.uid" }
- { source_path: "title", ocsf_field: "finding_info.title" }
- { source_path: "created_time", ocsf_field: "finding_info.created_time" }
- { source_path: "last_accessed", ocsf_field: "finding_info.modified_time" }
- { source_path: "first_alert_time ? first_alert_time : alert.first_event_time", ocsf_field: "finding_info.first_seen_time" }
- { source_path: "latest_alert_time", ocsf_field: "finding_info.last_seen_time" }
- { source_path: "tags", ocsf_field: "metadata.labels" }
# ── Incident state ────────────────────────────────────────────────
- { source_path: "status", ocsf_field: "status" }
- { source_path: "priority", ocsf_field: "priority" }
- { source_path: "disposition", ocsf_field: "disposition" }
- { source_path: "source", ocsf_field: "activity_name" }
- { source_path: "responsibility", ocsf_field: "comment" }
# ── Assignee (User) ───────────────────────────────────────────────
- { source_path: "assignee.name ? assignee.name : assignee_name", ocsf_field: "assignee.name" }
- { source_path: "assignee.email ? assignee.email : assignee_email", ocsf_field: "assignee.email_addr" }
# ── Owning organization ───────────────────────────────────────────
- { source_path: "organization_id", ocsf_field: "cloud.org.uid" }
# ── Originating alert (v1 shape) ──────────────────────────────────
- { source_path: "alert.type", ocsf_field: "finding_info.analytic.name" }
- { source_path: "alert.type_description", ocsf_field: "finding_info.analytic.desc" }
@@ -0,0 +1,73 @@
import json, os, sys, urllib.request, urllib.parse, urllib.error
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"}
return base, headers
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 _prune(d):
return {k: v for k, v in d.items() if v}
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 {}
# Endpoint suffix differs between add and replace; this script adds.
ACTION = "add"
def run():
inp = _inputs()
body = _prune({
"ips": _list(inp.get("ip_addresses")),
"hashes": _list(inp.get("hashes")),
"domain_names": _list(inp.get("domain_names")),
"urls": _list(inp.get("url")),
})
results = []
for key in _list(inp.get("key")):
out = request(
"POST",
"idr/v1/customthreats/key/" + urllib.parse.quote(key, safe="") + "/indicators/" + ACTION,
params={"format": "json"},
body=body,
)
results.append(out.get("threat", out))
print(json.dumps({"data": results}))
try:
run()
except urllib.error.HTTPError as e:
print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")}))
sys.exit(1)
except Exception as e:
print(json.dumps({"error": str(e)}))
sys.exit(1)
@@ -0,0 +1,69 @@
import json, os, sys, urllib.request, urllib.parse, urllib.error
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")
is_v2 = str(s.get("is_v2") or "true").strip().lower() in ("1", "true", "yes")
return base, headers, multi, is_v2
def _inputs():
return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
def _api_version(inp, is_v2):
v = str(inp.get("api_version") or "Default").strip()
return v if v in ("V1", "V2") else ("V2" if is_v2 else "V1")
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 {}
def run():
_, _, multi, is_v2 = _cfg()
inp = _inputs()
api_version = _api_version(inp, is_v2)
email = inp.get("user_email_address")
params = {"multi-customer": "true" if (multi and api_version == "V2") else None}
results = []
for inv_id in _list(inp.get("investigation_id")):
results.append(request(
"PUT",
"idr/" + api_version.lower() + "/investigations/" + urllib.parse.quote(inv_id, safe="") + "/assignee",
params=params,
body={"user_email_address": email},
))
print(json.dumps({"data": results}))
try:
run()
except urllib.error.HTTPError as e:
print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")}))
sys.exit(1)
except Exception as e:
print(json.dumps({"error": str(e)}))
sys.exit(1)
@@ -0,0 +1,54 @@
import json, os, sys, urllib.request, urllib.parse, urllib.error
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"}
return base, headers
def _inputs():
return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
def _prune(d):
return {k: v for k, v in d.items() if v not in (None, "", {}, [])}
def request(method, path, body=None):
base, headers = _cfg()
data = json.dumps(body).encode("utf-8") if body is not None else None
req = urllib.request.Request(base + path.lstrip("/"), 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 {}
def run():
inp = _inputs()
disposition = inp.get("disposition") or "Not Applicable"
max_close = inp.get("max_investigations_to_close")
body = _prune({
"source": inp.get("source"),
"alert_type": inp.get("alert_type"),
"disposition": disposition.replace(" ", "_"),
"detection_rule_rrn": inp.get("detection_rule_rrn"),
"from": inp.get("start_time"),
"to": inp.get("end_time"),
"max_investigations_to_close": int(max_close) if max_close not in (None, "") else None,
})
out = request("POST", "idr/v2/investigations/bulk_close", body=body)
ids = out.get("ids", [])
print(json.dumps({"ids": ids, "data": [{"id": i, "status": "CLOSED"} for i in ids]}))
try:
run()
except urllib.error.HTTPError as e:
print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")}))
sys.exit(1)
except Exception as e:
print(json.dumps({"error": str(e)}))
sys.exit(1)
@@ -0,0 +1,49 @@
import json, os, sys, urllib.request, urllib.parse, urllib.error
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"}
return base, headers
def _inputs():
return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
def _prune(d):
return {k: v for k, v in d.items() if v not in (None, "", {}, [])}
def request(method, path, body=None):
base, headers = _cfg()
data = json.dumps(body).encode("utf-8") if body is not None else None
req = urllib.request.Request(base + path.lstrip("/"), 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 {}
def run():
inp = _inputs()
disposition = str(inp.get("disposition") or "Undecided").replace(" ", "_")
body = _prune({
"assignee": _prune({"email": inp.get("user_email_address")}),
"disposition": disposition,
"priority": inp.get("priority") or "Unspecified",
"status": inp.get("status") or "Open",
"title": inp.get("title"),
})
print(json.dumps(request("POST", "idr/v2/investigations", body=body)))
try:
run()
except urllib.error.HTTPError as e:
print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")}))
sys.exit(1)
except Exception as e:
print(json.dumps({"error": str(e)}))
sys.exit(1)
@@ -0,0 +1,52 @@
import json, os, sys, urllib.request, urllib.parse, urllib.error
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", ""), "Accept": "application/json"}
return base, headers
def _inputs():
return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
def run():
base, headers = _cfg()
inp = _inputs()
start_time = inp.get("start_time")
end_time = inp.get("end_time")
time_range = inp.get("time_range")
if not (start_time or end_time or time_range):
time_range = "Last 3 days"
params = {
"from": start_time,
"to": end_time,
"time_range": time_range,
"query": inp.get("query"),
"limit": inp.get("limit"),
}
clean = {k: v for k, v in params.items() if v not in (None, "")}
# Up to 10 log IDs are joined with ':' in the path.
log_ids = str(inp.get("log_ids", "")).replace(",", ":")
url = base + "log_search/download/logs/" + urllib.parse.quote(log_ids, safe=":")
if clean:
url += "?" + urllib.parse.urlencode(clean, doseq=True)
dl_headers = dict(headers)
dl_headers["Accept-Encoding"] = ""
req = urllib.request.Request(url, headers=dl_headers, method="GET")
with urllib.request.urlopen(req, timeout=120) as r:
content = r.read().decode("utf-8", "replace")
print(json.dumps({"content": content, "log_ids": inp.get("log_ids")}))
try:
run()
except urllib.error.HTTPError as e:
print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")}))
sys.exit(1)
except Exception as e:
print(json.dumps({"error": str(e)}))
sys.exit(1)
@@ -0,0 +1,60 @@
import json, os, sys, urllib.request, urllib.parse, urllib.error
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")
is_v2 = str(s.get("is_v2") or "true").strip().lower() in ("1", "true", "yes")
return base, headers, multi, is_v2
def _inputs():
return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
def _api_version(inp, is_v2):
v = str(inp.get("api_version") or "Default").strip()
return v if v in ("V1", "V2") else ("V2" if is_v2 else "V1")
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 {}
def run():
_, _, multi, is_v2 = _cfg()
inp = _inputs()
api_version = _api_version(inp, is_v2)
inv_id = inp.get("investigation_id", "")
if api_version == "V2":
out = request("GET", "idr/v2/investigations/" + urllib.parse.quote(inv_id, safe=""),
params={"multi-customer": "true" if multi else None})
print(json.dumps(out if out.get("rrn") else {}))
return
# V1 has no get-by-id endpoint: list and match on id.
data = request("GET", "idr/v1/investigations", params={"size": 1000}).get("data", [])
match = next((i for i in data if i.get("id") == inv_id), {})
print(json.dumps(match))
try:
run()
except urllib.error.HTTPError as e:
print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")}))
sys.exit(1)
except Exception as e:
print(json.dumps({"error": str(e)}))
sys.exit(1)
@@ -0,0 +1,51 @@
import json, os, sys, urllib.request, urllib.parse, urllib.error
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 request(method, path, params=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)
req = urllib.request.Request(url, headers=headers, method=method)
with urllib.request.urlopen(req, timeout=90) as r:
raw = r.read()
return json.loads(raw) if raw else {}
def run():
_, _, multi = _cfg()
inp = _inputs()
inv_id = inp.get("investigation_id", "")
out = request("GET", "idr/v2/investigations/" + urllib.parse.quote(inv_id, safe="") + "/alerts",
params={"multi-customer": "true" if multi else None})
data = out.get("data", [])
all_results = str(inp.get("all_results") or "false").strip().lower() in ("1", "true", "yes")
if not all_results:
limit = int(inp.get("limit") or 50)
data = data[:limit]
print(json.dumps({"rrn": inv_id, "alert": data}))
try:
run()
except urllib.error.HTTPError as e:
print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")}))
sys.exit(1)
except Exception as e:
print(json.dumps({"error": str(e)}))
sys.exit(1)
@@ -0,0 +1,65 @@
import json, os, sys, urllib.request, urllib.parse, urllib.error
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 request(method, path, params=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)
req = urllib.request.Request(url, headers=headers, method=method)
with urllib.request.urlopen(req, timeout=90) as r:
raw = r.read()
return json.loads(raw) if raw else {}
def _flatten(response):
# Each entry holds a product 'type' plus one or more product alert lists/dicts.
rows = []
for result in response or []:
for product_name in list(result.keys()):
value = result[product_name]
if isinstance(value, list):
for alert in value:
rows.append(dict(alert, name=result.get("type")))
elif isinstance(value, dict):
rows.append(dict(value, name=result.get("type")))
return rows
def run():
_, _, multi = _cfg()
inp = _inputs()
inv_id = inp.get("investigation_id", "")
out = request("GET", "idr/v2/investigations/" + urllib.parse.quote(inv_id, safe="") + "/rapid7-product-alerts",
params={"multi-customer": "true" if multi else None})
data = _flatten(out if isinstance(out, list) else out.get("data", []))
all_results = str(inp.get("all_results") or "false").strip().lower() in ("1", "true", "yes")
if not all_results:
limit = int(inp.get("limit") or 50)
data = data[:limit]
print(json.dumps({"rrn": inv_id, "ProductAlert": data}))
try:
run()
except urllib.error.HTTPError as e:
print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")}))
sys.exit(1)
except Exception as e:
print(json.dumps({"error": str(e)}))
sys.exit(1)
@@ -0,0 +1,93 @@
import json, os, sys, urllib.request, urllib.parse, urllib.error
from datetime import datetime, timedelta, timezone
ISO = "%Y-%m-%dT%H:%M:%SZ"
_UNITS = {"second": 1, "minute": 60, "hour": 3600, "day": 86400, "week": 604800, "month": 2592000, "year": 31536000}
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")
is_v2 = str(s.get("is_v2") or "true").strip().lower() in ("1", "true", "yes")
return base, headers, multi, is_v2
def _inputs():
return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
def _api_version(inp, is_v2):
v = str(inp.get("api_version") or "Default").strip()
return v if v in ("V1", "V2") else ("V2" if is_v2 else "V1")
def _rel_seconds(text):
if not text:
return None
num = unit = None
for t in str(text).lower().replace("last", "").split():
if t.isdigit():
num = int(t)
elif t.rstrip("s") in _UNITS:
unit = t.rstrip("s")
return num * _UNITS[unit] if (num is not None and unit) else None
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 {}
def run():
_, _, multi, is_v2 = _cfg()
inp = _inputs()
api_version = _api_version(inp, is_v2)
limit = inp.get("limit") or 50
start_time = inp.get("start_time")
end_time = inp.get("end_time")
secs = _rel_seconds(inp.get("time_range"))
if secs:
now = datetime.now(timezone.utc)
start_time = (now - timedelta(seconds=secs)).strftime(ISO)
end_time = now.strftime(ISO)
params = {
"index": inp.get("index") or "0",
"size": inp.get("page_size") or limit,
"statuses": inp.get("statuses"),
"start_time": start_time,
"end_time": end_time,
}
if api_version == "V2":
params.update({
"sources": inp.get("sources"),
"priorities": inp.get("priorities"),
"assignee_email": inp.get("assignee_email"),
"sort_field": inp.get("sort_field"),
"sort_direction": inp.get("sort_direction"),
"tags": inp.get("tags"),
"multi-customer": "true" if multi else None,
})
endpoint = "idr/" + api_version.lower() + "/investigations"
print(json.dumps(request("GET", endpoint, params=params)))
try:
run()
except urllib.error.HTTPError as e:
print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")}))
sys.exit(1)
except Exception as e:
print(json.dumps({"error": str(e)}))
sys.exit(1)
@@ -0,0 +1,31 @@
import json, os, sys, urllib.request, urllib.parse, urllib.error
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"}
return base, headers
def request(method, path):
base, headers = _cfg()
req = urllib.request.Request(base + path.lstrip("/"), headers=headers, method=method)
with urllib.request.urlopen(req, timeout=90) as r:
raw = r.read()
return json.loads(raw) if raw else {}
def run():
print(json.dumps(request("GET", "log_search/management/logsets")))
try:
run()
except urllib.error.HTTPError as e:
print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")}))
sys.exit(1)
except Exception as e:
print(json.dumps({"error": str(e)}))
sys.exit(1)
@@ -0,0 +1,31 @@
import json, os, sys, urllib.request, urllib.parse, urllib.error
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"}
return base, headers
def request(method, path):
base, headers = _cfg()
req = urllib.request.Request(base + path.lstrip("/"), headers=headers, method=method)
with urllib.request.urlopen(req, timeout=90) as r:
raw = r.read()
return json.loads(raw) if raw else {}
def run():
print(json.dumps(request("GET", "log_search/management/logs")))
try:
run()
except urllib.error.HTTPError as e:
print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")}))
sys.exit(1)
except Exception as e:
print(json.dumps({"error": str(e)}))
sys.exit(1)
@@ -0,0 +1,72 @@
import json, os, sys, urllib.request, urllib.parse, urllib.error
USER_SEARCH = ["first_name", "last_name", "name"]
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"}
return base, headers
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 {}
def run():
inp = _inputs()
rrn = inp.get("rrn")
if rrn:
print(json.dumps(request("GET", "idr/v1/users/" + urllib.parse.quote(rrn, safe=""))))
return
operator = inp.get("search_operator")
search = []
for field in USER_SEARCH:
values = _list(inp.get(field))
if values and not operator:
raise ValueError("search_operator is required to use first_name/last_name/name filters.")
for value in values:
search.append({"field": field, "operator": str(operator).upper(), "value": value})
direction = str(inp.get("sort_direction") or "asc").upper()
sort = [{"field": f, "order": direction} for f in _list(inp.get("sort"))]
params = {"index": inp.get("index") or "0", "size": inp.get("page_size") or inp.get("limit") or 50}
body = {}
if search:
body["search"] = search
if sort:
body["sort"] = sort
print(json.dumps(request("POST", "idr/v1/users/_search", params=params, body=body)))
try:
run()
except urllib.error.HTTPError as e:
print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")}))
sys.exit(1)
except Exception as e:
print(json.dumps({"error": str(e)}))
sys.exit(1)
@@ -0,0 +1,85 @@
import json, os, sys, time, urllib.request, urllib.parse, urllib.error
_UNITS = {"second": 1, "minute": 60, "hour": 3600, "day": 86400, "week": 604800, "month": 2592000, "year": 31536000}
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"}
return base, headers
def _inputs():
return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
def _rel_ms_range(text):
if not text:
return None, None
num = unit = None
for t in str(text).lower().replace("last", "").split():
if t.isdigit():
num = int(t)
elif t.rstrip("s") in _UNITS:
unit = t.rstrip("s")
if num is None or not unit:
return None, None
now_ms = int(time.time() * 1000)
return now_ms - num * _UNITS[unit] * 1000, now_ms
def request(method, url, params=None):
base, headers = _cfg()
full = url if url.startswith("http") else base + url.lstrip("/")
if params:
clean = {k: v for k, v in params.items() if v not in (None, "")}
if clean:
full += ("&" if "?" in full else "?") + urllib.parse.urlencode(clean, doseq=True)
req = urllib.request.Request(full, headers=headers, method=method)
with urllib.request.urlopen(req, timeout=90) as r:
raw = r.read()
return json.loads(raw) if raw else {}
def _collect(first):
# Follow the query's pagination links until no more pages remain.
events = []
queue = [first]
while queue:
page = queue.pop(0)
events.extend(page.get("events", []) or [])
for link in page.get("links", []) or []:
href = link.get("href")
if href:
queue.append(request("GET", href))
return events
def run():
inp = _inputs()
start_time = inp.get("start_time")
end_time = inp.get("end_time")
if inp.get("time_range"):
start_time, end_time = _rel_ms_range(inp.get("time_range"))
params = {
"query": inp.get("query"),
"from": start_time,
"to": end_time,
"per_page": inp.get("logs_per_page"),
"sequence_number": inp.get("sequence_number"),
}
log_id = inp.get("log_id", "")
first = request("GET", "log_search/query/logs/" + urllib.parse.quote(log_id, safe=""), params=params)
print(json.dumps({"events": _collect(first)}))
try:
run()
except urllib.error.HTTPError as e:
print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")}))
sys.exit(1)
except Exception as e:
print(json.dumps({"error": str(e)}))
sys.exit(1)
@@ -0,0 +1,84 @@
import json, os, sys, time, urllib.request, urllib.parse, urllib.error
_UNITS = {"second": 1, "minute": 60, "hour": 3600, "day": 86400, "week": 604800, "month": 2592000, "year": 31536000}
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"}
return base, headers
def _inputs():
return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
def _rel_ms_range(text):
if not text:
return None, None
num = unit = None
for t in str(text).lower().replace("last", "").split():
if t.isdigit():
num = int(t)
elif t.rstrip("s") in _UNITS:
unit = t.rstrip("s")
if num is None or not unit:
return None, None
now_ms = int(time.time() * 1000)
return now_ms - num * _UNITS[unit] * 1000, now_ms
def request(method, url, params=None):
base, headers = _cfg()
full = url if url.startswith("http") else base + url.lstrip("/")
if params:
clean = {k: v for k, v in params.items() if v not in (None, "")}
if clean:
full += ("&" if "?" in full else "?") + urllib.parse.urlencode(clean, doseq=True)
req = urllib.request.Request(full, headers=headers, method=method)
with urllib.request.urlopen(req, timeout=90) as r:
raw = r.read()
return json.loads(raw) if raw else {}
def _collect(first):
events = []
queue = [first]
while queue:
page = queue.pop(0)
events.extend(page.get("events", []) or [])
for link in page.get("links", []) or []:
href = link.get("href")
if href:
queue.append(request("GET", href))
return events
def run():
inp = _inputs()
start_time = inp.get("start_time")
end_time = inp.get("end_time")
if inp.get("time_range"):
start_time, end_time = _rel_ms_range(inp.get("time_range"))
params = {
"query": inp.get("query"),
"from": start_time,
"to": end_time,
"per_page": inp.get("logs_per_page"),
"sequence_number": inp.get("sequence_number"),
}
log_set_id = inp.get("log_set_id", "")
first = request("GET", "log_search/query/logsets/" + urllib.parse.quote(log_set_id, safe=""), params=params)
print(json.dumps({"events": _collect(first)}))
try:
run()
except urllib.error.HTTPError as e:
print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")}))
sys.exit(1)
except Exception as e:
print(json.dumps({"error": str(e)}))
sys.exit(1)
@@ -0,0 +1,73 @@
import json, os, sys, urllib.request, urllib.parse, urllib.error
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"}
return base, headers
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 _prune(d):
return {k: v for k, v in d.items() if v}
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 {}
# replace deletes the threat's existing indicators before adding the supplied ones.
ACTION = "replace"
def run():
inp = _inputs()
body = _prune({
"ips": _list(inp.get("ip_addresses")),
"hashes": _list(inp.get("hashes")),
"domain_names": _list(inp.get("domain_names")),
"urls": _list(inp.get("url")),
})
results = []
for key in _list(inp.get("key")):
out = request(
"POST",
"idr/v1/customthreats/key/" + urllib.parse.quote(key, safe="") + "/indicators/" + ACTION,
params={"format": "json"},
body=body,
)
results.append(out.get("threat", out))
print(json.dumps({"data": results}))
try:
run()
except urllib.error.HTTPError as e:
print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")}))
sys.exit(1)
except Exception as e:
print(json.dumps({"error": str(e)}))
sys.exit(1)
@@ -0,0 +1,92 @@
import json, os, sys, urllib.request, urllib.parse, urllib.error
CONTAINS = "CONTAINS"
EQUALS = "EQUALS"
# (field, operator) pairs mirrored from the InsightIDR investigation search schema.
SEARCH = [
("actor_asset_hostname", CONTAINS),
("actor_user_name", CONTAINS),
("alert_mitre_t_codes", EQUALS),
("alert_rule_rrn", EQUALS),
("assignee_id", EQUALS),
("organization_id", EQUALS),
("priority", EQUALS),
("rrn", EQUALS),
("source", EQUALS),
("status", EQUALS),
("title", CONTAINS),
]
SORT_FIELDS = {
"Created time": "created_time",
"Priority": "priority",
"RRN": "rrn",
"Alert created time": "alerts_most_recent_created_time",
"Alert detection created time": "alerts_most_recent_detection_created_time",
}
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 {}
def run():
_, _, multi = _cfg()
inp = _inputs()
search = []
for field, op in SEARCH:
for value in _list(inp.get(field)):
search.append({"field": field, "operator": op, "value": value})
direction = str(inp.get("sort_direction") or "asc").upper()
sort = [{"field": SORT_FIELDS.get(f, f), "order": direction} for f in _list(inp.get("sort"))]
body = {"search": search, "sort": sort}
if inp.get("start_time"):
body["start_time"] = inp["start_time"]
if inp.get("end_time"):
body["end_time"] = inp["end_time"]
params = {
"index": inp.get("index") or "0",
"size": inp.get("page_size") or inp.get("limit") or 50,
"multi-customer": "true" if multi else None,
}
print(json.dumps(request("POST", "idr/v2/investigations/_search", params=params, body=body)))
try:
run()
except urllib.error.HTTPError as e:
print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")}))
sys.exit(1)
except Exception as e:
print(json.dumps({"error": str(e)}))
sys.exit(1)
@@ -0,0 +1,85 @@
import json, os, sys, urllib.request, urllib.parse, urllib.error
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")
is_v2 = str(s.get("is_v2") or "true").strip().lower() in ("1", "true", "yes")
return base, headers, multi, is_v2
def _inputs():
return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
def _api_version(inp, is_v2):
v = str(inp.get("api_version") or "Default").strip()
return v if v in ("V1", "V2") else ("V2" if is_v2 else "V1")
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 _prune(d):
return {k: v for k, v in d.items() if v not in (None, "", {}, [])}
def _camel(text):
return "".join(w.capitalize() for w in str(text).split()) if text else None
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 {}
def run():
_, _, multi, is_v2 = _cfg()
inp = _inputs()
api_version = _api_version(inp, is_v2)
status = inp.get("status", "")
body = {}
params = {}
if api_version == "V2":
body = _prune({
"disposition": inp.get("disposition"),
"threat_command_close_reason": _camel(inp.get("threat_command_close_reason")),
"threat_command_free_text": inp.get("threat_command_free_text"),
})
params = {"multi-customer": "true" if multi else None}
results = []
for inv_id in _list(inp.get("investigation_id")):
results.append(request(
"PUT",
"idr/" + api_version.lower() + "/investigations/" + urllib.parse.quote(inv_id, safe="") + "/status/" + urllib.parse.quote(status, safe=""),
params=params,
body=body or None,
))
print(json.dumps({"data": results}))
try:
run()
except urllib.error.HTTPError as e:
print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")}))
sys.exit(1)
except Exception as e:
print(json.dumps({"error": str(e)}))
sys.exit(1)
@@ -0,0 +1,46 @@
import json, os, sys, urllib.request, urllib.parse, urllib.error
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")
is_v2 = str(s.get("is_v2") or "true").strip().lower() in ("1", "true", "yes")
return base, headers, multi, is_v2
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=60) as r:
raw = r.read()
return json.loads(raw) if raw else {}
def run():
request("GET", "idr/v1/investigations", params={"size": 1})
print(json.dumps({"ok": True}))
try:
run()
except urllib.error.HTTPError as e:
detail = e.read().decode("utf-8", "replace")
if e.code == 401:
print(json.dumps({"ok": False, "error": "API key is not valid."}))
elif e.code == 500:
print(json.dumps({"ok": False, "error": "Wrong account region."}))
else:
print(json.dumps({"ok": False, "error": "HTTP " + str(e.code), "detail": detail}))
sys.exit(1)
except Exception as e:
print(json.dumps({"ok": False, "error": str(e)}))
sys.exit(1)
@@ -0,0 +1,65 @@
import json, os, sys, urllib.request, urllib.parse, urllib.error
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 _prune(d):
return {k: v for k, v in d.items() if v not in (None, "", {}, [])}
def _camel(text):
return "".join(w.capitalize() for w in str(text).split()) if text else None
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 {}
def run():
_, _, multi = _cfg()
inp = _inputs()
inv_id = inp.get("investigation_id", "")
disposition = inp.get("disposition")
body = _prune({
"assignee": _prune({"email": inp.get("user_email_address")}),
"disposition": disposition.replace(" ", "_") if disposition else None,
"priority": inp.get("priority"),
"status": inp.get("status"),
"title": inp.get("title"),
"threat_command_close_reason": _camel(inp.get("threat_command_close_reason")),
"threat_command_free_text": inp.get("threat_command_free_text"),
})
out = request("PATCH", "idr/v2/investigations/" + urllib.parse.quote(inv_id, safe=""),
params={"multi-customer": "true" if multi else None}, body=body)
print(json.dumps(out))
try:
run()
except urllib.error.HTTPError as e:
print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")}))
sys.exit(1)
except Exception as e:
print(json.dumps({"error": str(e)}))
sys.exit(1)