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:
@@ -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)
|
||||||
Reference in New Issue
Block a user