diff --git a/integrations/thehive/manifest.yaml b/integrations/thehive/manifest.yaml new file mode 100644 index 0000000..5ffc927 --- /dev/null +++ b/integrations/thehive/manifest.yaml @@ -0,0 +1,125 @@ +id: thehive +name: TheHive +version: 1.0.0 +description: "TheHive (case-management API v1) — SOC case handling: create/read/update/search cases, create tasks, create and read alerts, promote an alert to a case, and add observables. Bearer (API key) authentication; stdlib-only, no extra Python dependencies. Targets TheHive 5." +changelog: "1.0.0 — Initial release: case create/get/update/search, create task, alert create/get/promote, add observable." +category: case_management + +# Per-instance configuration. The API key is sent as 'Authorization: Bearer '. +config_schema: + properties: + base_url: + type: string + description: "TheHive URL (e.g. https://thehive.example.com:9000)" + api_key: + type: string + description: "TheHive API key" + x-soar-sensitive: true + required: + - base_url + - api_key + +commands: + - id: create_case + name: thehive-create-case + description: "Create a case." + inputs_schema: + properties: + title: { type: string, description: "Case title" } + description: { type: string, description: "Case description" } + severity: { type: number, description: "1 (low) - 4 (critical), default 2" } + tlp: { type: number, description: "TLP 0 (white) - 4 (red), default 2" } + tags: { type: string, description: "Comma-separated tags" } + required: [title, description] + outputs_schema: { properties: {} } + - id: get_case + name: thehive-get-case + description: "Get a case by ID." + risk: read + inputs_schema: + properties: + case_id: { type: string, description: "Case ID" } + required: [case_id] + outputs_schema: { properties: {} } + - id: update_case + name: thehive-update-case + description: "Update a case (status, severity, or assignee)." + inputs_schema: + properties: + case_id: { type: string, description: "Case ID" } + status: { type: string, description: "New status (e.g. Open, Resolved)" } + severity: { type: number, description: "1-4" } + assignee: { type: string, description: "Assignee login" } + required: [case_id] + outputs_schema: { properties: {} } + - id: search_cases + name: thehive-search-cases + description: "Search cases with a free-text query." + risk: read + inputs_schema: + properties: + query: { type: string, description: "Free-text keyword to match in title/description" } + limit: { type: number, description: "Max cases (default 50)" } + required: [] + outputs_schema: { properties: {} } + - id: create_task + name: thehive-create-task + description: "Add a task to a case." + inputs_schema: + properties: + case_id: { type: string, description: "Case ID" } + title: { type: string, description: "Task title" } + description: { type: string, description: "Task description" } + required: [case_id, title] + outputs_schema: { properties: {} } + - id: create_alert + name: thehive-create-alert + description: "Create an alert." + inputs_schema: + properties: + title: { type: string, description: "Alert title" } + description: { type: string, description: "Alert description" } + type: { type: string, description: "Alert type (e.g. external)" } + source: { type: string, description: "Alert source" } + source_ref: { type: string, description: "Unique source reference" } + severity: { type: number, description: "1-4, default 2" } + required: [title, description, type, source, source_ref] + outputs_schema: { properties: {} } + - id: get_alert + name: thehive-get-alert + description: "Get an alert by ID." + risk: read + inputs_schema: + properties: + alert_id: { type: string, description: "Alert ID" } + required: [alert_id] + outputs_schema: { properties: {} } + - id: promote_alert + name: thehive-promote-alert + description: "Promote an alert to a case." + inputs_schema: + properties: + alert_id: { type: string, description: "Alert ID" } + required: [alert_id] + outputs_schema: { properties: {} } + - id: add_observable + name: thehive-add-observable + description: "Add an observable to a case." + inputs_schema: + properties: + case_id: { type: string, description: "Case ID" } + data_type: { type: string, description: "Observable type (ip, domain, url, hash, mail, ...)" } + data: { type: string, description: "Observable value" } + message: { type: string, description: "Optional note" } + ioc: { type: boolean, description: "Mark as IOC (default false)" } + required: [case_id, data_type, data] + outputs_schema: { properties: {} } + + - id: test_connection + name: thehive-test-connection + description: "Verify connectivity and the API key (used by the Test button)." + risk: read + inputs_schema: + properties: {} + required: [] + outputs_schema: { properties: {} } diff --git a/integrations/thehive/scripts/add_observable.py b/integrations/thehive/scripts/add_observable.py new file mode 100644 index 0000000..05fdfdf --- /dev/null +++ b/integrations/thehive/scripts/add_observable.py @@ -0,0 +1,62 @@ +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 request(method, path, cfg, body=None): + url = str(cfg.get("base_url", "")).rstrip("/") + path + data = json.dumps(body).encode("utf-8") if body is not None else None + headers = {"Authorization": "Bearer " + str(cfg.get("api_key", "")), "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 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): + case_id = str(inputs.get("case_id", "")).strip() + data_type = str(inputs.get("data_type", "")).strip() + data = str(inputs.get("data", "")).strip() + + if not case_id: + raise Exception("case_id is required") + if not data_type: + raise Exception("data_type is required") + if not data: + raise Exception("data is required") + + message = inputs.get("message") + ioc = inputs.get("ioc", False) + + body = { + "dataType": data_type, + "data": data, + "ioc": bool(ioc), + } + if message not in (None, ""): + body["message"] = str(message) + + q = lambda v: urllib.parse.quote(str(v), safe="") + return request("POST", "/api/v1/case/" + q(case_id) + "/observable", cfg, body) + + +_run(main) diff --git a/integrations/thehive/scripts/create_alert.py b/integrations/thehive/scripts/create_alert.py new file mode 100644 index 0000000..7c7a2e5 --- /dev/null +++ b/integrations/thehive/scripts/create_alert.py @@ -0,0 +1,67 @@ +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 request(method, path, cfg, body=None): + url = str(cfg.get("base_url", "")).rstrip("/") + path + data = json.dumps(body).encode("utf-8") if body is not None else None + headers = {"Authorization": "Bearer " + str(cfg.get("api_key", "")), "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 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): + title = str(inputs.get("title", "")).strip() + description = str(inputs.get("description", "")).strip() + type_ = str(inputs.get("type", "")).strip() + source = str(inputs.get("source", "")).strip() + source_ref = str(inputs.get("source_ref", "")).strip() + + if not title: + raise Exception("title is required") + if not description: + raise Exception("description is required") + if not type_: + raise Exception("type is required") + if not source: + raise Exception("source is required") + if not source_ref: + raise Exception("source_ref is required") + + severity = inputs.get("severity") + + body = { + "title": title, + "description": description, + "type": type_, + "source": source, + "sourceRef": source_ref, + "severity": int(severity) if severity not in (None, "") else 2, + } + + return request("POST", "/api/v1/alert", cfg, body) + + +_run(main) diff --git a/integrations/thehive/scripts/create_case.py b/integrations/thehive/scripts/create_case.py new file mode 100644 index 0000000..006d978 --- /dev/null +++ b/integrations/thehive/scripts/create_case.py @@ -0,0 +1,62 @@ +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 request(method, path, cfg, body=None): + url = str(cfg.get("base_url", "")).rstrip("/") + path + data = json.dumps(body).encode("utf-8") if body is not None else None + headers = {"Authorization": "Bearer " + str(cfg.get("api_key", "")), "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 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): + title = str(inputs.get("title", "")).strip() + description = str(inputs.get("description", "")).strip() + if not title: + raise Exception("title is required") + if not description: + raise Exception("description is required") + + severity = inputs.get("severity") + tlp = inputs.get("tlp") + tags_raw = inputs.get("tags") + + body = { + "title": title, + "description": description, + "severity": int(severity) if severity not in (None, "") else 2, + "tlp": int(tlp) if tlp not in (None, "") else 2, + } + + if tags_raw not in (None, ""): + tags = [s.strip() for s in str(tags_raw).split(",") if s.strip()] + if tags: + body["tags"] = tags + + return request("POST", "/api/v1/case", cfg, body) + + +_run(main) diff --git a/integrations/thehive/scripts/create_task.py b/integrations/thehive/scripts/create_task.py new file mode 100644 index 0000000..067b7ba --- /dev/null +++ b/integrations/thehive/scripts/create_task.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 request(method, path, cfg, body=None): + url = str(cfg.get("base_url", "")).rstrip("/") + path + data = json.dumps(body).encode("utf-8") if body is not None else None + headers = {"Authorization": "Bearer " + str(cfg.get("api_key", "")), "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 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): + case_id = str(inputs.get("case_id", "")).strip() + title = str(inputs.get("title", "")).strip() + if not case_id: + raise Exception("case_id is required") + if not title: + raise Exception("title is required") + + description = inputs.get("description") + + body = {"title": title} + if description not in (None, ""): + body["description"] = str(description) + + q = lambda v: urllib.parse.quote(str(v), safe="") + return request("POST", "/api/v1/case/" + q(case_id) + "/task", cfg, body) + + +_run(main) diff --git a/integrations/thehive/scripts/get_alert.py b/integrations/thehive/scripts/get_alert.py new file mode 100644 index 0000000..6c9da82 --- /dev/null +++ b/integrations/thehive/scripts/get_alert.py @@ -0,0 +1,44 @@ +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 request(method, path, cfg, body=None): + url = str(cfg.get("base_url", "")).rstrip("/") + path + data = json.dumps(body).encode("utf-8") if body is not None else None + headers = {"Authorization": "Bearer " + str(cfg.get("api_key", "")), "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 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): + alert_id = str(inputs.get("alert_id", "")).strip() + if not alert_id: + raise Exception("alert_id is required") + + q = lambda v: urllib.parse.quote(str(v), safe="") + return request("GET", "/api/v1/alert/" + q(alert_id), cfg) + + +_run(main) diff --git a/integrations/thehive/scripts/get_case.py b/integrations/thehive/scripts/get_case.py new file mode 100644 index 0000000..19f9c59 --- /dev/null +++ b/integrations/thehive/scripts/get_case.py @@ -0,0 +1,44 @@ +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 request(method, path, cfg, body=None): + url = str(cfg.get("base_url", "")).rstrip("/") + path + data = json.dumps(body).encode("utf-8") if body is not None else None + headers = {"Authorization": "Bearer " + str(cfg.get("api_key", "")), "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 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): + case_id = str(inputs.get("case_id", "")).strip() + if not case_id: + raise Exception("case_id is required") + + q = lambda v: urllib.parse.quote(str(v), safe="") + return request("GET", "/api/v1/case/" + q(case_id), cfg) + + +_run(main) diff --git a/integrations/thehive/scripts/promote_alert.py b/integrations/thehive/scripts/promote_alert.py new file mode 100644 index 0000000..5275269 --- /dev/null +++ b/integrations/thehive/scripts/promote_alert.py @@ -0,0 +1,44 @@ +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 request(method, path, cfg, body=None): + url = str(cfg.get("base_url", "")).rstrip("/") + path + data = json.dumps(body).encode("utf-8") if body is not None else None + headers = {"Authorization": "Bearer " + str(cfg.get("api_key", "")), "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 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): + alert_id = str(inputs.get("alert_id", "")).strip() + if not alert_id: + raise Exception("alert_id is required") + + q = lambda v: urllib.parse.quote(str(v), safe="") + return request("POST", "/api/v1/alert/" + q(alert_id) + "/case", cfg, {}) + + +_run(main) diff --git a/integrations/thehive/scripts/search_cases.py b/integrations/thehive/scripts/search_cases.py new file mode 100644 index 0000000..75f777e --- /dev/null +++ b/integrations/thehive/scripts/search_cases.py @@ -0,0 +1,50 @@ +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 request(method, path, cfg, body=None): + url = str(cfg.get("base_url", "")).rstrip("/") + path + data = json.dumps(body).encode("utf-8") if body is not None else None + headers = {"Authorization": "Bearer " + str(cfg.get("api_key", "")), "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 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): + query = inputs.get("query") + limit = inputs.get("limit") + limit_val = int(limit) if limit not in (None, "") else 50 + + ql = [{"_name": "listCase"}] + if query not in (None, ""): + ql.append({"_name": "filter", "_like": {"_field": "title", "_value": str(query)}}) + ql.append({"_name": "page", "_from": 0, "_to": limit_val}) + + body = {"query": ql} + resp = request("POST", "/api/v1/query", cfg, body) + return {"results": resp} + + +_run(main) diff --git a/integrations/thehive/scripts/test_connection.py b/integrations/thehive/scripts/test_connection.py new file mode 100644 index 0000000..e1f2634 --- /dev/null +++ b/integrations/thehive/scripts/test_connection.py @@ -0,0 +1,40 @@ +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 request(method, path, cfg, body=None): + url = str(cfg.get("base_url", "")).rstrip("/") + path + data = json.dumps(body).encode("utf-8") if body is not None else None + headers = {"Authorization": "Bearer " + str(cfg.get("api_key", "")), "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 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): + request("GET", "/api/v1/user/current", cfg) + return {"ok": True} + + +_run(main) diff --git a/integrations/thehive/scripts/update_case.py b/integrations/thehive/scripts/update_case.py new file mode 100644 index 0000000..e022795 --- /dev/null +++ b/integrations/thehive/scripts/update_case.py @@ -0,0 +1,62 @@ +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 request(method, path, cfg, body=None): + url = str(cfg.get("base_url", "")).rstrip("/") + path + data = json.dumps(body).encode("utf-8") if body is not None else None + headers = {"Authorization": "Bearer " + str(cfg.get("api_key", "")), "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 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): + case_id = str(inputs.get("case_id", "")).strip() + if not case_id: + raise Exception("case_id is required") + + status = inputs.get("status") + severity = inputs.get("severity") + assignee = inputs.get("assignee") + + body = {} + if status not in (None, ""): + body["status"] = str(status).strip() + if severity not in (None, ""): + body["severity"] = int(severity) + if assignee not in (None, ""): + body["assignee"] = str(assignee).strip() + + if not body: + raise Exception("at least one field to update is required") + + q = lambda v: urllib.parse.quote(str(v), safe="") + resp = request("PATCH", "/api/v1/case/" + q(case_id), cfg, body) + if not resp: + return {"ok": True, "case_id": case_id} + return resp + + +_run(main)