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)