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