From 6548740890e3bb130d25cc59f0fa7633d35f1ec8 Mon Sep 17 00:00:00 2001 From: Guillaume BOURGEOIS Date: Sun, 12 Jul 2026 15:08:51 +0200 Subject: [PATCH] feat(sumologic): new Sumo Logic log-search integration Sumo Logic REST API, 4 commands: search (job create+poll+messages, cookie session), list/get collectors. Access-key (Basic) auth, stdlib-only. Co-Authored-By: Claude Opus 4.8 (1M context) --- integrations/sumologic/manifest.yaml | 65 +++++++++++++ .../sumologic/scripts/get_collector.py | 65 +++++++++++++ .../sumologic/scripts/list_collectors.py | 64 +++++++++++++ integrations/sumologic/scripts/search.py | 96 +++++++++++++++++++ .../sumologic/scripts/test_connection.py | 60 ++++++++++++ 5 files changed, 350 insertions(+) create mode 100644 integrations/sumologic/manifest.yaml create mode 100644 integrations/sumologic/scripts/get_collector.py create mode 100644 integrations/sumologic/scripts/list_collectors.py create mode 100644 integrations/sumologic/scripts/search.py create mode 100644 integrations/sumologic/scripts/test_connection.py diff --git a/integrations/sumologic/manifest.yaml b/integrations/sumologic/manifest.yaml new file mode 100644 index 0000000..8c465f4 --- /dev/null +++ b/integrations/sumologic/manifest.yaml @@ -0,0 +1,65 @@ +id: sumologic +name: Sumo Logic +version: 1.0.0 +description: "Sumo Logic (REST API) — log search and collector inventory: run a search job (create, poll, and return messages) and list/read collectors. Access-ID/key (Basic) authentication; stdlib-only, no extra Python dependencies." +changelog: "1.0.0 — Initial release: search (job create+poll+messages), list/get collectors." +category: siem + +# Per-instance configuration. HTTP Basic auth with the access ID + access key. +config_schema: + properties: + base_url: + type: string + description: "Sumo Logic API endpoint (region-specific, e.g. https://api.us2.sumologic.com)" + access_id: + type: string + description: "Access ID" + access_key: + type: string + description: "Access key" + x-soar-sensitive: true + required: + - base_url + - access_id + - access_key + +commands: + - id: search + name: sumologic-search + description: "Run a search: create a search job, poll until complete, and return the messages." + risk: read + inputs_schema: + properties: + query: { type: string, description: "Sumo Logic search query" } + from_time: { type: string, description: "ISO-8601 start time (e.g. 2024-01-01T00:00:00)" } + to_time: { type: string, description: "ISO-8601 end time" } + limit: { type: number, description: "Max messages (default 100)" } + required: [query, from_time, to_time] + outputs_schema: { properties: {} } + - id: list_collectors + name: sumologic-list-collectors + description: "List collectors." + risk: read + inputs_schema: + properties: + limit: { type: number, description: "Max collectors (default 100)" } + required: [] + outputs_schema: { properties: {} } + - id: get_collector + name: sumologic-get-collector + description: "Get a single collector by ID." + risk: read + inputs_schema: + properties: + collector_id: { type: string, description: "Collector ID" } + required: [collector_id] + outputs_schema: { properties: {} } + + - id: test_connection + name: sumologic-test-connection + description: "Verify connectivity and credentials (used by the Test button)." + risk: read + inputs_schema: + properties: {} + required: [] + outputs_schema: { properties: {} } diff --git a/integrations/sumologic/scripts/get_collector.py b/integrations/sumologic/scripts/get_collector.py new file mode 100644 index 0000000..18ff0c2 --- /dev/null +++ b/integrations/sumologic/scripts/get_collector.py @@ -0,0 +1,65 @@ +import json, os, sys, time, base64, http.cookiejar +import urllib.parse, urllib.request, urllib.error + + +def _cfg(): + return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) + + +def _inputs(): + return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}")) + + +def _auth(cfg): + raw = str(cfg.get("access_id", "")) + ":" + str(cfg.get("access_key", "")) + return "Basic " + base64.b64encode(raw.encode("utf-8")).decode("utf-8") + + +def _base(cfg): + return str(cfg.get("base_url", "")).rstrip("/") + "/api/v1" + + +class Client: + def __init__(self, cfg): + self.cfg = cfg + self.opener = urllib.request.build_opener(urllib.request.HTTPCookieProcessor(http.cookiejar.CookieJar())) + + def call(self, method, path, body=None, params=None): + url = _base(self.cfg) + path + if params: + clean = {k: v for k, v in params.items() if v not in (None, "")} + if clean: + url += "?" + urllib.parse.urlencode(clean) + data = json.dumps(body).encode("utf-8") if body is not None else None + headers = {"Authorization": _auth(self.cfg), "Accept": "application/json"} + if data is not None: + headers["Content-Type"] = "application/json" + req = urllib.request.Request(url, data=data, headers=headers, method=method) + with self.opener.open(req, timeout=60) as r: + raw = r.read() + return json.loads(raw) if raw else {} + + +def _run(fn): + try: + client = Client(_cfg()) + print(json.dumps(fn(client, _inputs()))) + 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) + + +q = lambda v: urllib.parse.quote(str(v), safe="") + + +def main(client, inputs): + collector_id = inputs.get("collector_id") + if not collector_id: + raise Exception("collector_id is required") + return client.call("GET", "/collectors/" + q(collector_id)) + + +_run(main) diff --git a/integrations/sumologic/scripts/list_collectors.py b/integrations/sumologic/scripts/list_collectors.py new file mode 100644 index 0000000..a74e5eb --- /dev/null +++ b/integrations/sumologic/scripts/list_collectors.py @@ -0,0 +1,64 @@ +import json, os, sys, time, base64, http.cookiejar +import urllib.parse, urllib.request, urllib.error + + +def _cfg(): + return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) + + +def _inputs(): + return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}")) + + +def _auth(cfg): + raw = str(cfg.get("access_id", "")) + ":" + str(cfg.get("access_key", "")) + return "Basic " + base64.b64encode(raw.encode("utf-8")).decode("utf-8") + + +def _base(cfg): + return str(cfg.get("base_url", "")).rstrip("/") + "/api/v1" + + +class Client: + def __init__(self, cfg): + self.cfg = cfg + self.opener = urllib.request.build_opener(urllib.request.HTTPCookieProcessor(http.cookiejar.CookieJar())) + + def call(self, method, path, body=None, params=None): + url = _base(self.cfg) + path + if params: + clean = {k: v for k, v in params.items() if v not in (None, "")} + if clean: + url += "?" + urllib.parse.urlencode(clean) + data = json.dumps(body).encode("utf-8") if body is not None else None + headers = {"Authorization": _auth(self.cfg), "Accept": "application/json"} + if data is not None: + headers["Content-Type"] = "application/json" + req = urllib.request.Request(url, data=data, headers=headers, method=method) + with self.opener.open(req, timeout=60) as r: + raw = r.read() + return json.loads(raw) if raw else {} + + +def _run(fn): + try: + client = Client(_cfg()) + print(json.dumps(fn(client, _inputs()))) + 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) + + +q = lambda v: urllib.parse.quote(str(v), safe="") + + +def main(client, inputs): + limit = inputs.get("limit") + limit = int(limit) if limit not in (None, "") else 100 + return client.call("GET", "/collectors", params={"limit": limit}) + + +_run(main) diff --git a/integrations/sumologic/scripts/search.py b/integrations/sumologic/scripts/search.py new file mode 100644 index 0000000..300db18 --- /dev/null +++ b/integrations/sumologic/scripts/search.py @@ -0,0 +1,96 @@ +import json, os, sys, time, base64, http.cookiejar +import urllib.parse, urllib.request, urllib.error + + +def _cfg(): + return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) + + +def _inputs(): + return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}")) + + +def _auth(cfg): + raw = str(cfg.get("access_id", "")) + ":" + str(cfg.get("access_key", "")) + return "Basic " + base64.b64encode(raw.encode("utf-8")).decode("utf-8") + + +def _base(cfg): + return str(cfg.get("base_url", "")).rstrip("/") + "/api/v1" + + +class Client: + def __init__(self, cfg): + self.cfg = cfg + self.opener = urllib.request.build_opener(urllib.request.HTTPCookieProcessor(http.cookiejar.CookieJar())) + + def call(self, method, path, body=None, params=None): + url = _base(self.cfg) + path + if params: + clean = {k: v for k, v in params.items() if v not in (None, "")} + if clean: + url += "?" + urllib.parse.urlencode(clean) + data = json.dumps(body).encode("utf-8") if body is not None else None + headers = {"Authorization": _auth(self.cfg), "Accept": "application/json"} + if data is not None: + headers["Content-Type"] = "application/json" + req = urllib.request.Request(url, data=data, headers=headers, method=method) + with self.opener.open(req, timeout=60) as r: + raw = r.read() + return json.loads(raw) if raw else {} + + +def _run(fn): + try: + client = Client(_cfg()) + print(json.dumps(fn(client, _inputs()))) + 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) + + +q = lambda v: urllib.parse.quote(str(v), safe="") + + +def main(client, inputs): + query = inputs.get("query") + if not query: + raise Exception("query is required") + from_time = inputs.get("from_time") + if not from_time: + raise Exception("from_time is required") + to_time = inputs.get("to_time") + if not to_time: + raise Exception("to_time is required") + limit = inputs.get("limit") + limit = int(limit) if limit not in (None, "") else 100 + + job = client.call("POST", "/search/jobs", body={"query": query, "from": from_time, "to": to_time, "timeZone": "UTC"}) + job_id = job.get("id") + if not job_id: + raise Exception("failed to create search job: " + json.dumps(job)) + + state = None + for _ in range(60): + status = client.call("GET", "/search/jobs/" + q(job_id)) + state = status.get("state") + if state == "DONE GATHERING RESULTS": + break + if state == "CANCELLED": + raise Exception("search job cancelled") + time.sleep(2) + + messages = client.call("GET", "/search/jobs/" + q(job_id) + "/messages", params={"offset": 0, "limit": limit}) + + try: + client.call("DELETE", "/search/jobs/" + q(job_id)) + except Exception: + pass + + return {"job_id": job_id, "state": state, "messages": messages} + + +_run(main) diff --git a/integrations/sumologic/scripts/test_connection.py b/integrations/sumologic/scripts/test_connection.py new file mode 100644 index 0000000..01161ac --- /dev/null +++ b/integrations/sumologic/scripts/test_connection.py @@ -0,0 +1,60 @@ +import json, os, sys, time, base64, http.cookiejar +import urllib.parse, urllib.request, urllib.error + + +def _cfg(): + return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) + + +def _inputs(): + return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}")) + + +def _auth(cfg): + raw = str(cfg.get("access_id", "")) + ":" + str(cfg.get("access_key", "")) + return "Basic " + base64.b64encode(raw.encode("utf-8")).decode("utf-8") + + +def _base(cfg): + return str(cfg.get("base_url", "")).rstrip("/") + "/api/v1" + + +class Client: + def __init__(self, cfg): + self.cfg = cfg + self.opener = urllib.request.build_opener(urllib.request.HTTPCookieProcessor(http.cookiejar.CookieJar())) + + def call(self, method, path, body=None, params=None): + url = _base(self.cfg) + path + if params: + clean = {k: v for k, v in params.items() if v not in (None, "")} + if clean: + url += "?" + urllib.parse.urlencode(clean) + data = json.dumps(body).encode("utf-8") if body is not None else None + headers = {"Authorization": _auth(self.cfg), "Accept": "application/json"} + if data is not None: + headers["Content-Type"] = "application/json" + req = urllib.request.Request(url, data=data, headers=headers, method=method) + with self.opener.open(req, timeout=60) as r: + raw = r.read() + return json.loads(raw) if raw else {} + + +def _run(fn): + try: + client = Client(_cfg()) + print(json.dumps(fn(client, _inputs()))) + 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) + + +def main(client, inputs): + client.call("GET", "/collectors", params={"limit": 1}) + return {"ok": True} + + +_run(main)