Three reference playbooks

Each of these is written against the real endpoints above. They are deliberately short — the value is in which call answers which question, not in the plumbing of any one platform.

Playbook A — Segmentation

Trigger. A rule tagged segmentation fires: an east-west conversation nobody expected. event_type is alert.fired, rule.tags contains segmentation.

1 — Read the pair off the alert. No parsing of positional data:

src  = matches.records[0].client_ip
dst  = matches.records[0].server_ip
port = matches.records[0].server_port

2 — Is it already allowed? POST /api/policy/check with {src, dst, port, protocol: "tcp"}.

  • covered: true → the flow matrix already permits this. Close the ticket as a detection-tuning item and stop: the rule and the matrix disagree, and that is a rule problem, not an incident.
  • covered: false → keep going, and carry nearest_rules[].why_not into the ticket verbatim. It is the sentence the network owner will reply to.

3 — Who are these machines? The alert’s entities[] usually answers this already. If entity context is off, or you want the 24-hour picture: GET /api/context/host?ip=<src>&window=24h&as_of=<fired_at>. new_peers_in_window is the interesting field: a source that has been talking to this destination for a week is a documentation gap, one that started an hour ago is an event.

4 — Open the ticket. Key it on dedup_key so the alert.resolved that follows finds it. Title it with rule.name, assign it from entities[].carto.group, and paste the checked_pair and the why_not.

5 — Acknowledge. POST /api/alerts/{id}/status with {"status": "ack"} so the console shows the alert as taken, and so anyone looking at obserae sees the ticket exists. Returns 204.

Playbook B — Threat intel

Trigger. An internal host talks to a destination a feed lists — a Tor exit node, a FireHOL entry. In a v2 alert you see it without asking: entities[].threat_intel[].verdict == "match".

1 — Confirm and expand. GET /api/enrichment/lookup?ip=<external>. The alert’s context is frozen at fire time — which is what you want for evidence — so this call tells you what the feeds say now, and threat_intel[].updated_at tells you how fresh each verdict is. A verdict: "unavailable" here means the enrichment database could not be consulted; it is not a clean bill of health, and a playbook that treats it as one will eventually close a real incident.

2 — Was this the only conversation? POST /api/context/peers with {"ip": "<internal>", "window": "24h", "direction": "out", "scope": "external", "as_of": "<fired_at>"}. One peer on one port is a click on a link. Twenty external peers, low volume, regular intervals, is a beacon — and the answer is sorted by volume, so read the tail as well as the head.

3 — Decide. Escalate on: a match verdict, plus either a second internal host reaching the same destination, or a peer count that has no business explanation. Otherwise annotate and close.

4 — Propose the indicator. obserae has already published it: the address is in GET /api/indicators?since=24h&min_severity=high from the moment the rule fired. Your playbook proposes it to whoever owns the blocklist — obserae does not push it, and obserae does not block. Send If-None-Match on that poll.

Playbook C — Exfiltration

Trigger. A volume spike. A threshold or anomaly rule fires with alert.observed_value and alert.threshold (threshold), or with the anomaly rule’s own scoring.

1 — Get the 24-hour picture. GET /api/context/host?ip=<host>&window=24h&as_of=<fired_at>. Read volume.out_bytes, talks_to[] sorted by bytes, and new_peers_in_window.

2 — Compare to the baseline. Same call, same host, as_of shifted back seven days:

GET /api/context/host?ip=10.0.0.50&window=24h&as_of=2026-05-28T12:00:00Z

Divide today’s volume.out_bytes by that one. A CI runner that egresses 4× its weekly norm to a destination in new_peers_in_window is the case; the same multiple spread over its usual peers is a release day.

3 — Name the destination. GET /api/enrichment/lookup?ip=<top peer>. A cloud region you already use is one conversation; an ASN nobody can account for is another.

4 — Page. On-call, with: the ratio you computed, the destination with its ASN and country, entities[].carto.host for the source, and alert.url. Key the incident on dedup_key — obserae will close it itself when the condition clears, so the on-call engineer is not left closing a page by hand at 04:00.

A note on windows. window accepts 1h, 24h and 7d only. To compare two periods, move as_of and keep window fixed, as step 2 does. A widened window is a widened scan, which is why the vocabulary is closed rather than free-form.