11089e9e80
add_comment (POST /idr/v1/comments) and list_comments (GET /idr/v1/comments) close the gap the Demisto integration never covered: the investigation comment thread is where an analyst's reasoning is recorded, and a playbook could neither write into it nor read it back. The Comments API is v1-only and has no multi-customer parameter, so neither command takes api_version. It addresses its target by RRN; the short investigation ID the other v1 endpoints accept is refused with a 400 that names no parameter, so both commands reject a non-RRN up front and say why. list_comments walks the pages (size capped at 100 by the API) up to `limit`, or all of them with all_results, with a 50-page backstop. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
81 lines
2.9 KiB
Python
81 lines
2.9 KiB
Python
import json, os, sys, urllib.request, urllib.parse, urllib.error
|
|
|
|
MAX_PAGE = 100 # the API caps `size` at 100
|
|
PAGE_CAP = 50 # backstop on the page walk, so a runaway target cannot loop forever
|
|
|
|
|
|
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 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():
|
|
inp = _inputs()
|
|
target = str(inp.get("investigation_id") or "").strip()
|
|
if not target:
|
|
raise ValueError("investigation_id is required: the RRN of the investigation to read comments from")
|
|
if not target.startswith("rrn:"):
|
|
raise ValueError(
|
|
"investigation_id must be an RRN (rrn:investigation:<region>:<org>:investigation:<id>), "
|
|
"not the short investigation ID: the Comments API targets a resource by RRN. Got " + repr(target)
|
|
)
|
|
sort_direction = str(inp.get("sort_direction") or "DESC").strip().upper()
|
|
if sort_direction not in ("ASC", "DESC"):
|
|
sort_direction = "DESC"
|
|
all_results = str(inp.get("all_results") or "false").strip().lower() in ("1", "true", "yes")
|
|
limit = int(inp.get("limit") or 50)
|
|
page_size = min(int(inp.get("page_size") or MAX_PAGE), MAX_PAGE)
|
|
index = int(inp.get("index") or 0)
|
|
|
|
data, metadata, pages = [], {}, 0
|
|
while pages < PAGE_CAP:
|
|
out = request("GET", "idr/v1/comments", params={
|
|
"target": target,
|
|
"index": index,
|
|
"size": page_size,
|
|
"sortDirection": sort_direction,
|
|
})
|
|
metadata = out.get("metadata") or {}
|
|
page = out.get("data") or []
|
|
data.extend(page)
|
|
pages += 1
|
|
index += 1
|
|
total_pages = int(metadata.get("total_pages") or 0)
|
|
if not page or index >= total_pages:
|
|
break
|
|
if not all_results and len(data) >= limit:
|
|
break
|
|
if not all_results:
|
|
data = data[:limit]
|
|
print(json.dumps({"target": target, "comment": data, "metadata": metadata}))
|
|
|
|
|
|
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)
|