feat(riposte-sextant): list the clients, and the identifier to deposit under (v1.1.0)

The refusal you get on the first run is "no Sextant client is paired with this
SOAR client", and it names the identifier it did not recognise — but nothing
told you which ones it would have recognised. This does.

Clients with no pairing appear with an empty identifier, and are surfaced again
under `unpaired`: they are the whole reason somebody runs this command twice,
and spotting them in a list is exactly what nobody does.

Verified against a running Sextant: the paired client comes back with its
identifier, and the route refuses a caller with no ingestion token.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-11 09:04:30 +02:00
parent 50358ef8a1
commit aabe1a6837
2 changed files with 56 additions and 2 deletions
+18 -2
View File
@@ -1,13 +1,13 @@
id: riposte-sextant
name: Riposte Sextant
version: 1.0.0
version: 1.1.0
description: >
Riposte Sextant — SOC steering. Push what this SOAR already knows about a
client's estate into their steering file: agent counters, measured on the
vendor console by another command and mapped in the playbook that calls this
one. Sextant never connects to anything itself; it receives. Bearer token
authentication, stdlib-only, no extra Python dependencies.
changelog: "1.0.0 — Initial release: push agent counters, and a connection test."
changelog: "1.1.0 — List the clients Sextant knows, and the identifier to deposit under.\n1.0.0 — Initial release: push agent counters, and a connection test."
category: reporting
config_schema:
@@ -54,6 +54,22 @@ commands:
client_id: { type: string, description: "The Sextant client the reading was filed under — the confirmation that the pairing pointed at the right one" }
sent: { type: object, description: "The counters actually deposited, so the run log shows what was left unmeasured" }
- id: list_clients
name: sextant-list-clients
description: >
List the clients Sextant knows and the identifier to deposit under. Answers
the question you have while writing the playbook, and the refusal you get
on the first run — "no Sextant client is paired with this SOAR client".
Clients with no pairing appear with an empty identifier: those are the ones
to go and pair, on their sheet in Sextant.
risk: read
inputs_schema: { properties: {} }
outputs_schema:
properties:
clients:
type: array
description: "code, name, soar_client_id (empty when unpaired) and whether the client is being steered"
- id: test_connection
name: sextant-test-connection
description: "Check the URL and the ingestion token, without depositing anything."
@@ -0,0 +1,38 @@
import json, os, sys, urllib.error, urllib.request
def _cfg():
return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
def main():
cfg = _cfg()
base = str(cfg.get("base_url") or "").rstrip("/")
if not base:
raise Exception("base_url is not configured")
req = urllib.request.Request(
base + "/api/ingest/clients",
headers={
"Accept": "application/json",
"Authorization": "Bearer " + str(cfg.get("token") or ""),
"User-Agent": "Riposte-SOAR/sextant",
},
method="GET",
)
with urllib.request.urlopen(req, timeout=30) as r:
result = json.loads(r.read() or b"{}")
clients = result.get("clients", [])
# The unpaired ones are surfaced separately rather than left to be spotted in
# a list: they are the whole reason somebody runs this command twice.
unpaired = [c.get("code") for c in clients if not c.get("soar_client_id")]
print(json.dumps({"clients": clients, "unpaired": unpaired}))
try:
main()
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)