diff --git a/integrations/riposte-sextant/manifest.yaml b/integrations/riposte-sextant/manifest.yaml deleted file mode 100644 index f7a9a4b..0000000 --- a/integrations/riposte-sextant/manifest.yaml +++ /dev/null @@ -1,80 +0,0 @@ -id: riposte-sextant -name: Riposte Sextant -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.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: - properties: - base_url: - type: string - description: "Sextant's public URL, e.g. https://sextant.example.corp (no trailing path)" - token: - type: string - description: "Ingestion token, created in Sextant under Settings. It writes readings and nothing else: it carries no role and reads nothing." - x-soar-sensitive: true - required: - - base_url - - token - -commands: - - id: push_agent_stats - name: sextant-push-agent-stats - description: > - Deposit one agent reading for a client. Counters left empty stay UNKNOWN in - Sextant — never zero: "we did not measure how many agents are in error" and - "no agent is in error" are opposite pieces of news, and the second one - reassures wrongly. Map only what the console actually returned. - # safe_write, and not read: this writes a reading into another product. It - # changes nothing on the customer's estate, which is what "safe" means here. - risk: safe_write - inputs_schema: - properties: - client_id: - type: string - description: "The client's id IN THIS SOAR. Sextant resolves its own client through the pairing already recorded on the client sheet — no extra identifier to copy, so none to get wrong." - expected: - type: number - description: "Agents the contract commits to. A commitment, not a measurement — no console knows it. Without it the coverage ratio stays unknown and the raw counters still stand." - deployed: { type: number, description: "Agents installed on the estate" } - connected: { type: number, description: "Agents talking to the console" } - disconnected: { type: number, description: "Agents known but silent" } - errored: { type: number, description: "Agents reporting a fault" } - outdated: { type: number, description: "Agents running an old version" } - unmanaged: { type: number, description: "Machines with no agent" } - required: [client_id] - outputs_schema: - properties: - 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." - risk: read - inputs_schema: { properties: {} } - outputs_schema: - properties: - ok: { type: boolean } diff --git a/integrations/riposte-sextant/scripts/list_clients.py b/integrations/riposte-sextant/scripts/list_clients.py deleted file mode 100644 index 77c489d..0000000 --- a/integrations/riposte-sextant/scripts/list_clients.py +++ /dev/null @@ -1,38 +0,0 @@ -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) diff --git a/integrations/riposte-sextant/scripts/push_agent_stats.py b/integrations/riposte-sextant/scripts/push_agent_stats.py deleted file mode 100644 index d89037c..0000000 --- a/integrations/riposte-sextant/scripts/push_agent_stats.py +++ /dev/null @@ -1,85 +0,0 @@ -import json, os, sys, urllib.error, urllib.request - -# The six counters Sextant knows, plus the contractual expectation. The -# vocabulary is fixed on purpose: the steering portfolio compares clients to one -# another, and it can only do that if "connected" means the same thing whichever -# console produced it. -COUNTERS = ["expected", "deployed", "connected", "disconnected", "errored", "outdated", "unmanaged"] - - -def _cfg(): - return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) - - -def _post(path, body): - 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 + path, - data=json.dumps(body).encode("utf-8"), - headers={ - "Content-Type": "application/json", - "Accept": "application/json", - "Authorization": "Bearer " + str(cfg.get("token") or ""), - "User-Agent": "Riposte-SOAR/sextant", - }, - method="POST", - ) - with urllib.request.urlopen(req, timeout=60) as r: - raw = r.read() - return json.loads(raw) if raw else {} - - -def _counter(name, value): - """Return a counter, or None when it was not measured. - - An ABSENT value is dropped rather than sent as zero. That is the whole - contract: an unmapped field stays unknown in Sextant, and a zero in its - place would be a confident lie about a customer's estate. - - A value that is present but not a number is REFUSED instead, and the field - is named. That is a broken mapping in the calling playbook, and it must be - visible — silently dropping it would look exactly like "not measured". - """ - if value is None or value == "": - return None - try: - n = float(value) - except (TypeError, ValueError): - raise Exception(name + " is not a number: " + repr(value)) - if n != int(n): - raise Exception(name + " is not a whole number: " + repr(value)) - return int(n) - - -def main(): - inputs = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}")) - - client_id = str(inputs.get("client_id") or "").strip() - if not client_id: - raise Exception("client_id is required — it is the client's id in this SOAR") - - body = {"client_id": client_id} - for name in COUNTERS: - value = _counter(name, inputs.get(name)) - if value is not None: - body[name] = value - - result = _post("/api/ingest/agents", body) - sent = {k: v for k, v in body.items() if k != "client_id"} - print(json.dumps({"client_id": result.get("client_id", ""), "sent": sent})) - - -try: - main() -except urllib.error.HTTPError as e: - # Sextant names its refusals — an unpaired client says which identifier it - # did not recognise. Passing the body through is what turns "HTTP 422" into - # something an operator can act on. - 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) diff --git a/integrations/riposte-sextant/scripts/test_connection.py b/integrations/riposte-sextant/scripts/test_connection.py deleted file mode 100644 index f9b4186..0000000 --- a/integrations/riposte-sextant/scripts/test_connection.py +++ /dev/null @@ -1,38 +0,0 @@ -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/ping", - 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"{}") - # The probe deposits nothing and reads nothing back but an acknowledgement: - # an ingestion token must not become a way to enumerate clients. - if not result.get("ok"): - raise Exception("unexpected response from Sextant") - print(json.dumps({"ok": True})) - - -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)