From 729c339e2f8d1a1f58204e2460126737d2c98f7c Mon Sep 17 00:00:00 2001 From: Guillaume BOURGEOIS Date: Sat, 27 Jun 2026 14:00:29 +0200 Subject: [PATCH] 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 --- .../rapid7-insightidr-investigation.yaml | 3 + integrations/rapid7-insightidr/manifest.yaml | 331 ++++++++++++++++++ .../mappers/list_investigations.yaml | 33 ++ .../scripts/add_threat_indicators.py | 73 ++++ .../rapid7-insightidr/scripts/assign_user.py | 69 ++++ .../scripts/close_investigations.py | 54 +++ .../scripts/create_investigation.py | 49 +++ .../scripts/download_logs.py | 52 +++ .../scripts/get_investigation.py | 60 ++++ .../scripts/list_investigation_alerts.py | 51 +++ .../list_investigation_product_alerts.py | 65 ++++ .../scripts/list_investigations.py | 93 +++++ .../scripts/list_log_sets.py | 31 ++ .../rapid7-insightidr/scripts/list_logs.py | 31 ++ .../rapid7-insightidr/scripts/list_users.py | 72 ++++ .../rapid7-insightidr/scripts/query_log.py | 85 +++++ .../scripts/query_log_set.py | 84 +++++ .../scripts/replace_threat_indicators.py | 73 ++++ .../scripts/search_investigation.py | 92 +++++ .../rapid7-insightidr/scripts/set_status.py | 85 +++++ .../scripts/test_connection.py | 46 +++ .../scripts/update_investigation.py | 65 ++++ 22 files changed, 1597 insertions(+) create mode 100644 integrations/rapid7-insightidr/incident-types/rapid7-insightidr-investigation.yaml create mode 100644 integrations/rapid7-insightidr/manifest.yaml create mode 100644 integrations/rapid7-insightidr/mappers/list_investigations.yaml create mode 100644 integrations/rapid7-insightidr/scripts/add_threat_indicators.py create mode 100644 integrations/rapid7-insightidr/scripts/assign_user.py create mode 100644 integrations/rapid7-insightidr/scripts/close_investigations.py create mode 100644 integrations/rapid7-insightidr/scripts/create_investigation.py create mode 100644 integrations/rapid7-insightidr/scripts/download_logs.py create mode 100644 integrations/rapid7-insightidr/scripts/get_investigation.py create mode 100644 integrations/rapid7-insightidr/scripts/list_investigation_alerts.py create mode 100644 integrations/rapid7-insightidr/scripts/list_investigation_product_alerts.py create mode 100644 integrations/rapid7-insightidr/scripts/list_investigations.py create mode 100644 integrations/rapid7-insightidr/scripts/list_log_sets.py create mode 100644 integrations/rapid7-insightidr/scripts/list_logs.py create mode 100644 integrations/rapid7-insightidr/scripts/list_users.py create mode 100644 integrations/rapid7-insightidr/scripts/query_log.py create mode 100644 integrations/rapid7-insightidr/scripts/query_log_set.py create mode 100644 integrations/rapid7-insightidr/scripts/replace_threat_indicators.py create mode 100644 integrations/rapid7-insightidr/scripts/search_investigation.py create mode 100644 integrations/rapid7-insightidr/scripts/set_status.py create mode 100644 integrations/rapid7-insightidr/scripts/test_connection.py create mode 100644 integrations/rapid7-insightidr/scripts/update_investigation.py diff --git a/integrations/rapid7-insightidr/incident-types/rapid7-insightidr-investigation.yaml b/integrations/rapid7-insightidr/incident-types/rapid7-insightidr-investigation.yaml new file mode 100644 index 0000000..a3f38f0 --- /dev/null +++ b/integrations/rapid7-insightidr/incident-types/rapid7-insightidr-investigation.yaml @@ -0,0 +1,3 @@ +name: "Rapid7 InsightIDR Investigation" +color: "#ff5630" +icon: "alert" diff --git a/integrations/rapid7-insightidr/manifest.yaml b/integrations/rapid7-insightidr/manifest.yaml new file mode 100644 index 0000000..e137394 --- /dev/null +++ b/integrations/rapid7-insightidr/manifest.yaml @@ -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://.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" diff --git a/integrations/rapid7-insightidr/mappers/list_investigations.yaml b/integrations/rapid7-insightidr/mappers/list_investigations.yaml new file mode 100644 index 0000000..2a9f0f4 --- /dev/null +++ b/integrations/rapid7-insightidr/mappers/list_investigations.yaml @@ -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" } diff --git a/integrations/rapid7-insightidr/scripts/add_threat_indicators.py b/integrations/rapid7-insightidr/scripts/add_threat_indicators.py new file mode 100644 index 0000000..00f3554 --- /dev/null +++ b/integrations/rapid7-insightidr/scripts/add_threat_indicators.py @@ -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) diff --git a/integrations/rapid7-insightidr/scripts/assign_user.py b/integrations/rapid7-insightidr/scripts/assign_user.py new file mode 100644 index 0000000..f3b2721 --- /dev/null +++ b/integrations/rapid7-insightidr/scripts/assign_user.py @@ -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) diff --git a/integrations/rapid7-insightidr/scripts/close_investigations.py b/integrations/rapid7-insightidr/scripts/close_investigations.py new file mode 100644 index 0000000..d96edab --- /dev/null +++ b/integrations/rapid7-insightidr/scripts/close_investigations.py @@ -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) diff --git a/integrations/rapid7-insightidr/scripts/create_investigation.py b/integrations/rapid7-insightidr/scripts/create_investigation.py new file mode 100644 index 0000000..6d03bcb --- /dev/null +++ b/integrations/rapid7-insightidr/scripts/create_investigation.py @@ -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) diff --git a/integrations/rapid7-insightidr/scripts/download_logs.py b/integrations/rapid7-insightidr/scripts/download_logs.py new file mode 100644 index 0000000..31eaab1 --- /dev/null +++ b/integrations/rapid7-insightidr/scripts/download_logs.py @@ -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) diff --git a/integrations/rapid7-insightidr/scripts/get_investigation.py b/integrations/rapid7-insightidr/scripts/get_investigation.py new file mode 100644 index 0000000..3081269 --- /dev/null +++ b/integrations/rapid7-insightidr/scripts/get_investigation.py @@ -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) diff --git a/integrations/rapid7-insightidr/scripts/list_investigation_alerts.py b/integrations/rapid7-insightidr/scripts/list_investigation_alerts.py new file mode 100644 index 0000000..94dfe01 --- /dev/null +++ b/integrations/rapid7-insightidr/scripts/list_investigation_alerts.py @@ -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) diff --git a/integrations/rapid7-insightidr/scripts/list_investigation_product_alerts.py b/integrations/rapid7-insightidr/scripts/list_investigation_product_alerts.py new file mode 100644 index 0000000..16a46f9 --- /dev/null +++ b/integrations/rapid7-insightidr/scripts/list_investigation_product_alerts.py @@ -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) diff --git a/integrations/rapid7-insightidr/scripts/list_investigations.py b/integrations/rapid7-insightidr/scripts/list_investigations.py new file mode 100644 index 0000000..e79cc05 --- /dev/null +++ b/integrations/rapid7-insightidr/scripts/list_investigations.py @@ -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) diff --git a/integrations/rapid7-insightidr/scripts/list_log_sets.py b/integrations/rapid7-insightidr/scripts/list_log_sets.py new file mode 100644 index 0000000..70fea96 --- /dev/null +++ b/integrations/rapid7-insightidr/scripts/list_log_sets.py @@ -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) diff --git a/integrations/rapid7-insightidr/scripts/list_logs.py b/integrations/rapid7-insightidr/scripts/list_logs.py new file mode 100644 index 0000000..a13a1ae --- /dev/null +++ b/integrations/rapid7-insightidr/scripts/list_logs.py @@ -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) diff --git a/integrations/rapid7-insightidr/scripts/list_users.py b/integrations/rapid7-insightidr/scripts/list_users.py new file mode 100644 index 0000000..f9af554 --- /dev/null +++ b/integrations/rapid7-insightidr/scripts/list_users.py @@ -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) diff --git a/integrations/rapid7-insightidr/scripts/query_log.py b/integrations/rapid7-insightidr/scripts/query_log.py new file mode 100644 index 0000000..96d2de3 --- /dev/null +++ b/integrations/rapid7-insightidr/scripts/query_log.py @@ -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) diff --git a/integrations/rapid7-insightidr/scripts/query_log_set.py b/integrations/rapid7-insightidr/scripts/query_log_set.py new file mode 100644 index 0000000..7e7d2da --- /dev/null +++ b/integrations/rapid7-insightidr/scripts/query_log_set.py @@ -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) diff --git a/integrations/rapid7-insightidr/scripts/replace_threat_indicators.py b/integrations/rapid7-insightidr/scripts/replace_threat_indicators.py new file mode 100644 index 0000000..7816acb --- /dev/null +++ b/integrations/rapid7-insightidr/scripts/replace_threat_indicators.py @@ -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) diff --git a/integrations/rapid7-insightidr/scripts/search_investigation.py b/integrations/rapid7-insightidr/scripts/search_investigation.py new file mode 100644 index 0000000..b7c1ca3 --- /dev/null +++ b/integrations/rapid7-insightidr/scripts/search_investigation.py @@ -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) diff --git a/integrations/rapid7-insightidr/scripts/set_status.py b/integrations/rapid7-insightidr/scripts/set_status.py new file mode 100644 index 0000000..2c1186d --- /dev/null +++ b/integrations/rapid7-insightidr/scripts/set_status.py @@ -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) diff --git a/integrations/rapid7-insightidr/scripts/test_connection.py b/integrations/rapid7-insightidr/scripts/test_connection.py new file mode 100644 index 0000000..1beb1f4 --- /dev/null +++ b/integrations/rapid7-insightidr/scripts/test_connection.py @@ -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) diff --git a/integrations/rapid7-insightidr/scripts/update_investigation.py b/integrations/rapid7-insightidr/scripts/update_investigation.py new file mode 100644 index 0000000..4636ee4 --- /dev/null +++ b/integrations/rapid7-insightidr/scripts/update_investigation.py @@ -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)