rapid7-insightidr 1.13.0: comment an investigation and read the thread back
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>
This commit is contained in:
@@ -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"}
|
||||
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 _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, 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()
|
||||
# The Comments API addresses its target by RRN only. The short investigation ID
|
||||
# the v1 endpoints accept is rejected here, and the API answers 400 without
|
||||
# naming the parameter — so refuse it up front with the reason.
|
||||
target = str(inp.get("investigation_id") or "").strip()
|
||||
if not target:
|
||||
raise ValueError("investigation_id is required: the RRN of the investigation to comment on")
|
||||
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)
|
||||
)
|
||||
comment = inp.get("comment")
|
||||
attachments = _list(inp.get("attachments"))
|
||||
if comment in (None, "") and not attachments:
|
||||
raise ValueError("comment is required (or attachments, to post a comment carrying only files)")
|
||||
body = _prune({
|
||||
"target": target,
|
||||
"body": comment,
|
||||
"attachments": attachments,
|
||||
})
|
||||
print(json.dumps(request("POST", "idr/v1/comments", 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,80 @@
|
||||
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)
|
||||
Reference in New Issue
Block a user