diff --git a/integrations/thinkst-canary/manifest.yaml b/integrations/thinkst-canary/manifest.yaml new file mode 100644 index 0000000..6542e6c --- /dev/null +++ b/integrations/thinkst-canary/manifest.yaml @@ -0,0 +1,66 @@ +id: thinkst_canary +name: Thinkst Canary +version: 1.0.0 +description: "Thinkst Canary (Console API v1) — deception/honeypot alerting: list and read incidents, acknowledge an incident, and list Canary devices. API-token authentication; stdlib-only, no extra Python dependencies." +changelog: "1.0.0 — Initial release: list/get incidents, acknowledge incident, list devices." +category: endpoint + +# Per-instance configuration. The auth token is sent as a request parameter; the +# console is reached at https://.canary.tools. +config_schema: + properties: + domain: + type: string + description: "Console subdomain (the X in https://X.canary.tools)" + auth_token: + type: string + description: "Console API auth token" + x-soar-sensitive: true + required: + - domain + - auth_token + +commands: + - id: list_incidents + name: canary-list-incidents + description: "List all incidents (alerts)." + risk: read + inputs_schema: + properties: + limit: { type: number, description: "Max incidents (default 50)" } + required: [] + outputs_schema: { properties: {} } + - id: get_incident + name: canary-get-incident + description: "Get a single incident by its ID/key." + risk: read + inputs_schema: + properties: + incident_id: { type: string, description: "Incident node/key ID" } + required: [incident_id] + outputs_schema: { properties: {} } + - id: acknowledge_incident + name: canary-acknowledge-incident + description: "Acknowledge an incident." + inputs_schema: + properties: + incident_id: { type: string, description: "Incident node/key ID" } + required: [incident_id] + outputs_schema: { properties: {} } + - id: list_devices + name: canary-list-devices + description: "List Canary devices." + risk: read + inputs_schema: + properties: {} + required: [] + outputs_schema: { properties: {} } + + - id: test_connection + name: canary-test-connection + description: "Verify the auth token (used by the Test button)." + risk: read + inputs_schema: + properties: {} + required: [] + outputs_schema: { properties: {} } diff --git a/integrations/thinkst-canary/scripts/acknowledge_incident.py b/integrations/thinkst-canary/scripts/acknowledge_incident.py new file mode 100644 index 0000000..22bded7 --- /dev/null +++ b/integrations/thinkst-canary/scripts/acknowledge_incident.py @@ -0,0 +1,55 @@ +import json, os, sys, 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 _base(cfg): + return "https://" + str(cfg.get("domain", "")) + ".canary.tools/api/v1" + + +def get(path, cfg, params=None): + p = {k: v for k, v in (params or {}).items() if v not in (None, "")} + p["auth_token"] = str(cfg.get("auth_token", "")) + url = _base(cfg) + path + "?" + urllib.parse.urlencode(p) + req = urllib.request.Request(url, headers={"Accept": "application/json"}, method="GET") + with urllib.request.urlopen(req, timeout=60) as r: + raw = r.read() + return json.loads(raw) if raw else {} + + +def post(path, cfg, fields=None): + p = {k: v for k, v in (fields or {}).items() if v not in (None, "")} + p["auth_token"] = str(cfg.get("auth_token", "")) + data = urllib.parse.urlencode(p).encode("utf-8") + req = urllib.request.Request(_base(cfg) + path, data=data, + headers={"Content-Type": "application/x-www-form-urlencoded", "Accept": "application/json"}, method="POST") + with urllib.request.urlopen(req, timeout=60) as r: + raw = r.read() + return json.loads(raw) if raw else {} + + +def _run(fn): + try: + print(json.dumps(fn(_cfg(), _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(cfg, inputs): + incident_id = inputs.get("incident_id") + if not incident_id: + raise Exception("incident_id is required") + return post("/incident/acknowledge", cfg, fields={"incident": incident_id}) + + +_run(main) diff --git a/integrations/thinkst-canary/scripts/get_incident.py b/integrations/thinkst-canary/scripts/get_incident.py new file mode 100644 index 0000000..038e5c6 --- /dev/null +++ b/integrations/thinkst-canary/scripts/get_incident.py @@ -0,0 +1,55 @@ +import json, os, sys, 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 _base(cfg): + return "https://" + str(cfg.get("domain", "")) + ".canary.tools/api/v1" + + +def get(path, cfg, params=None): + p = {k: v for k, v in (params or {}).items() if v not in (None, "")} + p["auth_token"] = str(cfg.get("auth_token", "")) + url = _base(cfg) + path + "?" + urllib.parse.urlencode(p) + req = urllib.request.Request(url, headers={"Accept": "application/json"}, method="GET") + with urllib.request.urlopen(req, timeout=60) as r: + raw = r.read() + return json.loads(raw) if raw else {} + + +def post(path, cfg, fields=None): + p = {k: v for k, v in (fields or {}).items() if v not in (None, "")} + p["auth_token"] = str(cfg.get("auth_token", "")) + data = urllib.parse.urlencode(p).encode("utf-8") + req = urllib.request.Request(_base(cfg) + path, data=data, + headers={"Content-Type": "application/x-www-form-urlencoded", "Accept": "application/json"}, method="POST") + with urllib.request.urlopen(req, timeout=60) as r: + raw = r.read() + return json.loads(raw) if raw else {} + + +def _run(fn): + try: + print(json.dumps(fn(_cfg(), _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(cfg, inputs): + incident_id = inputs.get("incident_id") + if not incident_id: + raise Exception("incident_id is required") + return get("/incident/fetch", cfg, params={"incident": incident_id}) + + +_run(main) diff --git a/integrations/thinkst-canary/scripts/list_devices.py b/integrations/thinkst-canary/scripts/list_devices.py new file mode 100644 index 0000000..4f3cacf --- /dev/null +++ b/integrations/thinkst-canary/scripts/list_devices.py @@ -0,0 +1,52 @@ +import json, os, sys, 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 _base(cfg): + return "https://" + str(cfg.get("domain", "")) + ".canary.tools/api/v1" + + +def get(path, cfg, params=None): + p = {k: v for k, v in (params or {}).items() if v not in (None, "")} + p["auth_token"] = str(cfg.get("auth_token", "")) + url = _base(cfg) + path + "?" + urllib.parse.urlencode(p) + req = urllib.request.Request(url, headers={"Accept": "application/json"}, method="GET") + with urllib.request.urlopen(req, timeout=60) as r: + raw = r.read() + return json.loads(raw) if raw else {} + + +def post(path, cfg, fields=None): + p = {k: v for k, v in (fields or {}).items() if v not in (None, "")} + p["auth_token"] = str(cfg.get("auth_token", "")) + data = urllib.parse.urlencode(p).encode("utf-8") + req = urllib.request.Request(_base(cfg) + path, data=data, + headers={"Content-Type": "application/x-www-form-urlencoded", "Accept": "application/json"}, method="POST") + with urllib.request.urlopen(req, timeout=60) as r: + raw = r.read() + return json.loads(raw) if raw else {} + + +def _run(fn): + try: + print(json.dumps(fn(_cfg(), _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(cfg, inputs): + return get("/devices/all", cfg) + + +_run(main) diff --git a/integrations/thinkst-canary/scripts/list_incidents.py b/integrations/thinkst-canary/scripts/list_incidents.py new file mode 100644 index 0000000..afdfaa2 --- /dev/null +++ b/integrations/thinkst-canary/scripts/list_incidents.py @@ -0,0 +1,53 @@ +import json, os, sys, 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 _base(cfg): + return "https://" + str(cfg.get("domain", "")) + ".canary.tools/api/v1" + + +def get(path, cfg, params=None): + p = {k: v for k, v in (params or {}).items() if v not in (None, "")} + p["auth_token"] = str(cfg.get("auth_token", "")) + url = _base(cfg) + path + "?" + urllib.parse.urlencode(p) + req = urllib.request.Request(url, headers={"Accept": "application/json"}, method="GET") + with urllib.request.urlopen(req, timeout=60) as r: + raw = r.read() + return json.loads(raw) if raw else {} + + +def post(path, cfg, fields=None): + p = {k: v for k, v in (fields or {}).items() if v not in (None, "")} + p["auth_token"] = str(cfg.get("auth_token", "")) + data = urllib.parse.urlencode(p).encode("utf-8") + req = urllib.request.Request(_base(cfg) + path, data=data, + headers={"Content-Type": "application/x-www-form-urlencoded", "Accept": "application/json"}, method="POST") + with urllib.request.urlopen(req, timeout=60) as r: + raw = r.read() + return json.loads(raw) if raw else {} + + +def _run(fn): + try: + print(json.dumps(fn(_cfg(), _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(cfg, inputs): + limit = inputs.get("limit") or 50 + return get("/incidents/all", cfg, params={"limit": int(limit)}) + + +_run(main) diff --git a/integrations/thinkst-canary/scripts/test_connection.py b/integrations/thinkst-canary/scripts/test_connection.py new file mode 100644 index 0000000..d4a7168 --- /dev/null +++ b/integrations/thinkst-canary/scripts/test_connection.py @@ -0,0 +1,53 @@ +import json, os, sys, 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 _base(cfg): + return "https://" + str(cfg.get("domain", "")) + ".canary.tools/api/v1" + + +def get(path, cfg, params=None): + p = {k: v for k, v in (params or {}).items() if v not in (None, "")} + p["auth_token"] = str(cfg.get("auth_token", "")) + url = _base(cfg) + path + "?" + urllib.parse.urlencode(p) + req = urllib.request.Request(url, headers={"Accept": "application/json"}, method="GET") + with urllib.request.urlopen(req, timeout=60) as r: + raw = r.read() + return json.loads(raw) if raw else {} + + +def post(path, cfg, fields=None): + p = {k: v for k, v in (fields or {}).items() if v not in (None, "")} + p["auth_token"] = str(cfg.get("auth_token", "")) + data = urllib.parse.urlencode(p).encode("utf-8") + req = urllib.request.Request(_base(cfg) + path, data=data, + headers={"Content-Type": "application/x-www-form-urlencoded", "Accept": "application/json"}, method="POST") + with urllib.request.urlopen(req, timeout=60) as r: + raw = r.read() + return json.loads(raw) if raw else {} + + +def _run(fn): + try: + print(json.dumps(fn(_cfg(), _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(cfg, inputs): + get("/ping", cfg) + return {"ok": True} + + +_run(main)