From aabe1a6837d70eb40ede602dcac8d1a3402ada10 Mon Sep 17 00:00:00 2001 From: Guillaume BOURGEOIS Date: Tue, 11 Aug 2026 09:04:30 +0200 Subject: [PATCH] feat(riposte-sextant): list the clients, and the identifier to deposit under (v1.1.0) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- integrations/riposte-sextant/manifest.yaml | 20 +++++++++- .../riposte-sextant/scripts/list_clients.py | 38 +++++++++++++++++++ 2 files changed, 56 insertions(+), 2 deletions(-) create mode 100644 integrations/riposte-sextant/scripts/list_clients.py diff --git a/integrations/riposte-sextant/manifest.yaml b/integrations/riposte-sextant/manifest.yaml index 18fd910..f7a9a4b 100644 --- a/integrations/riposte-sextant/manifest.yaml +++ b/integrations/riposte-sextant/manifest.yaml @@ -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." diff --git a/integrations/riposte-sextant/scripts/list_clients.py b/integrations/riposte-sextant/scripts/list_clients.py new file mode 100644 index 0000000..77c489d --- /dev/null +++ b/integrations/riposte-sextant/scripts/list_clients.py @@ -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)