Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c5da537434 | |||
| 4a339fda24 |
@@ -30,8 +30,10 @@ integrations/
|
||||
│ └── <command-id>.py
|
||||
├── mappers/ # optional — bundled mapper templates, one per command
|
||||
│ └── <command-id>.yaml
|
||||
└── incident-types/ # optional — bundled incident types
|
||||
└── <anything>.yaml
|
||||
├── incident-types/ # optional — bundled incident types
|
||||
│ └── <anything>.yaml
|
||||
└── agents/ # optional — the operator agent (Riposte 0.302.0+)
|
||||
└── operator.yaml
|
||||
```
|
||||
|
||||
---
|
||||
@@ -228,6 +230,48 @@ named by field, evaluated against the rule's last captured samples.
|
||||
|
||||
---
|
||||
|
||||
## Bundled operator agents (`agents/*.yaml`)
|
||||
|
||||
An integration can ship the **operator agent** that knows how to use it.
|
||||
Installing the integration in Riposte (0.302.0 and later) creates the agent in the
|
||||
crew, named `<handle> · <integration name>`; older versions ignore the folder.
|
||||
The sheet carries what took a real investigation to learn — the vendor's traps,
|
||||
the order to call things in — so nobody has to rebuild it by hand.
|
||||
|
||||
```yaml
|
||||
key: operator # stable id within the package; one agent per key
|
||||
handle: Static # an invented netrunner handle, never an existing character
|
||||
title: Rapid7 InsightIDR operator
|
||||
role: analyst_l2 # must hold integrations.execute
|
||||
approval_threshold: safe_write
|
||||
max_tool_calls: 10
|
||||
when_to_delegate: | # the only thing an orchestrator reads to route work to it
|
||||
…
|
||||
needs: |
|
||||
…
|
||||
returns: |
|
||||
…
|
||||
instructions: | # the vendor know-how: procedure and traps
|
||||
…
|
||||
commands: [list_investigation_alerts, get_alert_actors] # command ids of THIS manifest
|
||||
riposte_tools: [get_incident, get_incident_context]
|
||||
```
|
||||
|
||||
Riposte **refuses the sheet** — and still installs the integration — when it:
|
||||
|
||||
- lists a command this manifest does not declare, or one that is not `risk: read`
|
||||
(an omitted risk counts as destructive);
|
||||
- lists a Riposte tool that is not read-tier;
|
||||
- names a role without `integrations.execute`. `viewer`, `auditor`,
|
||||
`mcp_readonly` and `analyst_l1` lack it, and with them every integration
|
||||
command would be invisible to the agent;
|
||||
- has no `key`, `handle`, `role`, `when_to_delegate` or command.
|
||||
|
||||
The model is not part of the sheet: operators borrow Riposte's **Crew** role. A
|
||||
sheet nobody edited follows the package's updates; an edited one keeps its edit
|
||||
and offers the new version for review. Removing a sheet from the package retires
|
||||
its agent on the next install.
|
||||
|
||||
## Well-known commands: the on-call router
|
||||
|
||||
Most command ids are an integration's own business. **Three are a contract**, and
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
# The Rapid7 InsightIDR operator. Installed with the integration by Riposte
|
||||
# 0.302.0 and later (older versions ignore this folder). Read-only by design:
|
||||
# Riposte refuses a sheet that lists a write.
|
||||
key: operator
|
||||
handle: Static
|
||||
title: Rapid7 InsightIDR operator
|
||||
role: analyst_l2 # needs integrations.execute — viewer would hide every command
|
||||
approval_threshold: safe_write
|
||||
max_tool_calls: 10
|
||||
|
||||
when_to_delegate: |
|
||||
Anything an InsightIDR investigation can answer: the alerts it holds, the
|
||||
assets, accounts and IPs involved, the evidence that triggered them, process
|
||||
trees, other alerts or investigations for the same user or host, and log
|
||||
searches in InsightIDR log sets. Not for other vendors' alerts.
|
||||
|
||||
needs: |
|
||||
The Riposte alert id or the InsightIDR investigation RRN
|
||||
(rrn:investigation:...). For a log search: the user, host or IP, and a time
|
||||
window.
|
||||
|
||||
returns: |
|
||||
Facts from InsightIDR, each with the command that produced it; "not found"
|
||||
when a lookup came back empty; the exact query and error when one failed.
|
||||
No verdict.
|
||||
|
||||
instructions: |
|
||||
ALWAYS start here, whatever you were asked:
|
||||
1. Read the Riposte alert with get_incident_context. Its source_ref is the
|
||||
investigation RRN (rrn:investigation:...), never an alert RRN.
|
||||
2. list_investigation_alerts(investigation_id=<that RRN>) gives the
|
||||
investigation's alerts.
|
||||
3. An alert whose id or rrn starts with rrn:alerts: -> get_alert_actors, then
|
||||
get_alert_evidence, with that RRN. get_alert_process_trees only for
|
||||
endpoint (process) alerts.
|
||||
4. An alert with a plain UUID id has no RRN: it is a behaviour (UBA) alert
|
||||
such as "attempted to remotely access N new assets". The Alert Triage
|
||||
commands answer 404 for it — do not call them with a UUID or with the
|
||||
investigation RRN. Go to the logs instead (below).
|
||||
Only then answer the question you were asked.
|
||||
|
||||
Log searches, only when the alert's own evidence does not answer:
|
||||
- Take the log set id from list_log_sets; never invent one.
|
||||
- LEQL keyword search, no field names: where("<user, host or IP>"),
|
||||
e.g. where("jdoe")
|
||||
Always parentheses, always quotes. "where x = y" without parentheses is
|
||||
rejected as Invalid Query Syntax. No groupby or calculate.
|
||||
- Window: the alert's first and last seen times, widened by 30 minutes on
|
||||
each side (start_time / end_time, ISO-8601).
|
||||
- If a query returns "Invalid Query Syntax", do not try variants: report the
|
||||
exact query and the error.
|
||||
|
||||
Same user or host elsewhere: search_investigation with actor_user_name or
|
||||
actor_asset_hostname. It takes start_time / end_time, not time_range; left
|
||||
empty it covers the last 28 days.
|
||||
|
||||
commands:
|
||||
- list_investigation_alerts
|
||||
- get_investigation
|
||||
- search_investigation
|
||||
- get_alert
|
||||
- get_alert_actors
|
||||
- get_alert_evidence
|
||||
- get_alert_process_trees
|
||||
- search_alerts
|
||||
- list_users
|
||||
- list_log_sets
|
||||
- query_log_set
|
||||
|
||||
riposte_tools:
|
||||
- get_incident
|
||||
- get_incident_context
|
||||
File diff suppressed because one or more lines are too long
@@ -131,6 +131,13 @@ def run():
|
||||
end_time = _to_ms(inp.get("end_time"))
|
||||
if inp.get("time_range"):
|
||||
start_time, end_time = _rel_ms_range(inp.get("time_range"))
|
||||
# Log Search wants both bounds. A scheduled run hands over start_time alone
|
||||
# (the watermark), and the very first run may hand over nothing at all.
|
||||
now_ms = int(time.time() * 1000)
|
||||
if end_time is None:
|
||||
end_time = now_ms
|
||||
if start_time is None:
|
||||
start_time = end_time - 24 * 3600 * 1000
|
||||
try:
|
||||
limit = int(inp.get("limit") or 1000)
|
||||
except (TypeError, ValueError):
|
||||
@@ -142,7 +149,11 @@ def run():
|
||||
"per_page": inp.get("logs_per_page"),
|
||||
"sequence_number": inp.get("sequence_number"),
|
||||
}
|
||||
log_id = inp.get("log_id", "")
|
||||
# A pasted ID often drags a quote or a space along; quoted into the path it
|
||||
# names no log, and the API answers a bare 404.
|
||||
log_id = str(inp.get("log_id") or "").strip().strip("\"'").strip()
|
||||
if not log_id:
|
||||
raise ValueError("log_id is required (list_log_sets gives it)")
|
||||
first = request("GET", "log_search/query/logs/" + urllib.parse.quote(log_id, safe=""), params=params)
|
||||
events = _identify(_collect(first, limit), log_id, inp.get("detection_name"))
|
||||
print(json.dumps({"events": events, "count": len(events), "from": start_time, "to": end_time}))
|
||||
@@ -151,7 +162,10 @@ def run():
|
||||
try:
|
||||
run()
|
||||
except urllib.error.HTTPError as e:
|
||||
print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")}))
|
||||
out = {"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")}
|
||||
if e.code == 404:
|
||||
out["hint"] = "No log with this log_id in the configured region. Check the ID against list_log_sets, and the instance region."
|
||||
print(json.dumps(out))
|
||||
sys.exit(1)
|
||||
except Exception as e:
|
||||
print(json.dumps({"error": str(e)}))
|
||||
|
||||
@@ -131,6 +131,13 @@ def run():
|
||||
end_time = _to_ms(inp.get("end_time"))
|
||||
if inp.get("time_range"):
|
||||
start_time, end_time = _rel_ms_range(inp.get("time_range"))
|
||||
# Log Search wants both bounds. A scheduled run hands over start_time alone
|
||||
# (the watermark), and the very first run may hand over nothing at all.
|
||||
now_ms = int(time.time() * 1000)
|
||||
if end_time is None:
|
||||
end_time = now_ms
|
||||
if start_time is None:
|
||||
start_time = end_time - 24 * 3600 * 1000
|
||||
try:
|
||||
limit = int(inp.get("limit") or 1000)
|
||||
except (TypeError, ValueError):
|
||||
@@ -142,7 +149,11 @@ def run():
|
||||
"per_page": inp.get("logs_per_page"),
|
||||
"sequence_number": inp.get("sequence_number"),
|
||||
}
|
||||
log_set_id = inp.get("log_set_id", "")
|
||||
# A pasted ID often drags a quote or a space along; quoted into the path it
|
||||
# names no log set, and the API answers a bare 404.
|
||||
log_set_id = str(inp.get("log_set_id") or "").strip().strip("\"'").strip()
|
||||
if not log_set_id:
|
||||
raise ValueError("log_set_id is required (list_log_sets gives it)")
|
||||
first = request("GET", "log_search/query/logsets/" + urllib.parse.quote(log_set_id, safe=""), params=params)
|
||||
# No fallback log id: a log set spans several logs and every event names its own.
|
||||
events = _identify(_collect(first, limit), "", inp.get("detection_name"))
|
||||
@@ -152,7 +163,10 @@ def run():
|
||||
try:
|
||||
run()
|
||||
except urllib.error.HTTPError as e:
|
||||
print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")}))
|
||||
out = {"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")}
|
||||
if e.code == 404:
|
||||
out["hint"] = "No log set with this log_set_id in the configured region. Check the ID against list_log_sets, and the instance region."
|
||||
print(json.dumps(out))
|
||||
sys.exit(1)
|
||||
except Exception as e:
|
||||
print(json.dumps({"error": str(e)}))
|
||||
|
||||
Reference in New Issue
Block a user