chore: retire the Riposte Sextant integration
It pushed agent readings over HTTP into a sibling product. That product was absorbed into the SOAR in 0.112.0, and the readings now go through the built-in SOC Reporting integration, which writes into the same database the server already owns — no base URL, no ingestion token, no second deployment to keep reachable. This integration is the seam the absorption spent five phases removing. Nothing here is worth keeping behind: the counters, the rule that an omitted counter stays unknown rather than zero, and the client-listing command all exist on the other side. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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 }
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
Reference in New Issue
Block a user