From 239cc70672fc66ccd3fb4c32e12125b6793d94c3 Mon Sep 17 00:00:00 2001 From: Guillaume BOURGEOIS Date: Sat, 11 Jul 2026 22:48:49 +0200 Subject: [PATCH] feat(hybrid-analysis): new Hybrid Analysis (Falcon Sandbox) integration 7 commands: file/URL detonation, report summary + state, hash search, environment listing. API-key auth, stdlib-only. Co-Authored-By: Claude Fable 5 --- integrations/hybrid-analysis/manifest.yaml | 82 ++++++++++++++++++ .../scripts/get_environments.py | 46 ++++++++++ .../hybrid-analysis/scripts/get_result.py | 54 ++++++++++++ .../hybrid-analysis/scripts/get_state.py | 54 ++++++++++++ .../hybrid-analysis/scripts/search_hash.py | 51 ++++++++++++ .../hybrid-analysis/scripts/submit_file.py | 83 +++++++++++++++++++ .../hybrid-analysis/scripts/submit_url.py | 54 ++++++++++++ .../scripts/test_connection.py | 48 +++++++++++ 8 files changed, 472 insertions(+) create mode 100644 integrations/hybrid-analysis/manifest.yaml create mode 100644 integrations/hybrid-analysis/scripts/get_environments.py create mode 100644 integrations/hybrid-analysis/scripts/get_result.py create mode 100644 integrations/hybrid-analysis/scripts/get_state.py create mode 100644 integrations/hybrid-analysis/scripts/search_hash.py create mode 100644 integrations/hybrid-analysis/scripts/submit_file.py create mode 100644 integrations/hybrid-analysis/scripts/submit_url.py create mode 100644 integrations/hybrid-analysis/scripts/test_connection.py diff --git a/integrations/hybrid-analysis/manifest.yaml b/integrations/hybrid-analysis/manifest.yaml new file mode 100644 index 0000000..af2483c --- /dev/null +++ b/integrations/hybrid-analysis/manifest.yaml @@ -0,0 +1,82 @@ +id: hybrid_analysis +name: Hybrid Analysis +version: 1.0.0 +description: "Hybrid Analysis / CrowdStrike Falcon Sandbox (API v2) — detonate files and URLs, poll analysis state, retrieve report summaries, search by hash, and list sandbox environments. API-key authentication; stdlib-only, no extra Python dependencies." +changelog: "1.0.0 — Initial release: file/URL submission, report summary and state, hash search and environment listing." +category: enrichment + +# The API key is sent in the 'api-key' header; Hybrid Analysis also requires a +# 'User-Agent: Falcon Sandbox' header (the scripts set it). +config_schema: + properties: + api_key: + type: string + description: "Hybrid Analysis API key" + x-soar-sensitive: true + required: + - api_key + +commands: + - id: submit_file + name: hybrid-analysis-submit-file + description: "Submit a file (base64) for analysis in a sandbox environment. Returns a job/sha256; poll with hybrid-analysis-get-state." + inputs_schema: + properties: + file_name: { type: string, description: "File name" } + content_base64: { type: string, description: "File content, base64-encoded" } + environment_id: { type: number, description: "Sandbox environment ID (see hybrid-analysis-get-environments)" } + required: [file_name, content_base64, environment_id] + outputs_schema: { properties: {} } + - id: submit_url + name: hybrid-analysis-submit-url + description: "Submit a URL for analysis in a sandbox environment." + inputs_schema: + properties: + url: { type: string, description: "URL to detonate" } + environment_id: { type: number, description: "Sandbox environment ID" } + required: [url, environment_id] + outputs_schema: { properties: {} } + - id: get_result + name: hybrid-analysis-get-result + description: "Get the analysis report summary (verdict, threat score, signatures) by job/sha256 ID." + risk: read + inputs_schema: + properties: + job_id: { type: string, description: "Job ID or sha256:environment_id identifier" } + required: [job_id] + outputs_schema: { properties: {} } + - id: get_state + name: hybrid-analysis-get-state + description: "Get the current state of an analysis (IN_QUEUE, IN_PROGRESS, SUCCESS, ERROR)." + risk: read + inputs_schema: + properties: + job_id: { type: string, description: "Job ID or sha256:environment_id identifier" } + required: [job_id] + outputs_schema: { properties: {} } + - id: search_hash + name: hybrid-analysis-search-hash + description: "Look up existing analyses for a file hash (MD5, SHA1 or SHA256)." + risk: read + inputs_schema: + properties: + hash: { type: string, description: "File hash" } + required: [hash] + outputs_schema: { properties: {} } + - id: get_environments + name: hybrid-analysis-get-environments + description: "List the available sandbox environments and their IDs." + risk: read + inputs_schema: + properties: {} + required: [] + outputs_schema: { properties: {} } + + - id: test_connection + name: hybrid-analysis-test-connection + description: "Verify the API key (used by the Test button)." + risk: read + inputs_schema: + properties: {} + required: [] + outputs_schema: { properties: {} } diff --git a/integrations/hybrid-analysis/scripts/get_environments.py b/integrations/hybrid-analysis/scripts/get_environments.py new file mode 100644 index 0000000..df6d6bf --- /dev/null +++ b/integrations/hybrid-analysis/scripts/get_environments.py @@ -0,0 +1,46 @@ +import base64, json, os, sys, urllib.parse, urllib.request, urllib.error + +API = "https://www.hybrid-analysis.com/api/v2" + + +def _cfg(): + return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) + + +def _headers(extra=None): + h = {"Accept": "application/json", "User-Agent": "Falcon Sandbox", + "api-key": str(_cfg().get("api_key") or "")} + if extra: + h.update(extra) + return h + + +def request(method, path, params=None, form=None): + url = API + path + p = {k: str(x) for k, x in (params or {}).items() if x not in (None, "")} + if p: + url += ("&" if "?" in url else "?") + urllib.parse.urlencode(p) + data = None + headers = _headers() + if form is not None: + data = urllib.parse.urlencode({k: str(v) for k, v in form.items() if v not in (None, "")}).encode("utf-8") + headers["Content-Type"] = "application/x-www-form-urlencoded" + req = urllib.request.Request(url, data=data, headers=headers, method=method) + with urllib.request.urlopen(req, timeout=90) as r: + raw = r.read() + return json.loads(raw) if raw else {} + + +def main(): + result = request("GET", "/system/environments") + print(json.dumps(result)) + + +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) diff --git a/integrations/hybrid-analysis/scripts/get_result.py b/integrations/hybrid-analysis/scripts/get_result.py new file mode 100644 index 0000000..bfd8f39 --- /dev/null +++ b/integrations/hybrid-analysis/scripts/get_result.py @@ -0,0 +1,54 @@ +import base64, json, os, sys, urllib.parse, urllib.request, urllib.error + +API = "https://www.hybrid-analysis.com/api/v2" + + +def _cfg(): + return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) + + +def _headers(extra=None): + h = {"Accept": "application/json", "User-Agent": "Falcon Sandbox", + "api-key": str(_cfg().get("api_key") or "")} + if extra: + h.update(extra) + return h + + +def request(method, path, params=None, form=None): + url = API + path + p = {k: str(x) for k, x in (params or {}).items() if x not in (None, "")} + if p: + url += ("&" if "?" in url else "?") + urllib.parse.urlencode(p) + data = None + headers = _headers() + if form is not None: + data = urllib.parse.urlencode({k: str(v) for k, v in form.items() if v not in (None, "")}).encode("utf-8") + headers["Content-Type"] = "application/x-www-form-urlencoded" + req = urllib.request.Request(url, data=data, headers=headers, method=method) + with urllib.request.urlopen(req, timeout=90) as r: + raw = r.read() + return json.loads(raw) if raw else {} + + +q = lambda v: urllib.parse.quote(str(v), safe="") + + +def main(): + inputs = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}")) + job_id = inputs.get("job_id") + if not job_id: + raise Exception("job_id is required") + + result = request("GET", "/report/" + q(job_id) + "/summary") + print(json.dumps(result)) + + +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) diff --git a/integrations/hybrid-analysis/scripts/get_state.py b/integrations/hybrid-analysis/scripts/get_state.py new file mode 100644 index 0000000..5c23220 --- /dev/null +++ b/integrations/hybrid-analysis/scripts/get_state.py @@ -0,0 +1,54 @@ +import base64, json, os, sys, urllib.parse, urllib.request, urllib.error + +API = "https://www.hybrid-analysis.com/api/v2" + + +def _cfg(): + return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) + + +def _headers(extra=None): + h = {"Accept": "application/json", "User-Agent": "Falcon Sandbox", + "api-key": str(_cfg().get("api_key") or "")} + if extra: + h.update(extra) + return h + + +def request(method, path, params=None, form=None): + url = API + path + p = {k: str(x) for k, x in (params or {}).items() if x not in (None, "")} + if p: + url += ("&" if "?" in url else "?") + urllib.parse.urlencode(p) + data = None + headers = _headers() + if form is not None: + data = urllib.parse.urlencode({k: str(v) for k, v in form.items() if v not in (None, "")}).encode("utf-8") + headers["Content-Type"] = "application/x-www-form-urlencoded" + req = urllib.request.Request(url, data=data, headers=headers, method=method) + with urllib.request.urlopen(req, timeout=90) as r: + raw = r.read() + return json.loads(raw) if raw else {} + + +q = lambda v: urllib.parse.quote(str(v), safe="") + + +def main(): + inputs = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}")) + job_id = inputs.get("job_id") + if not job_id: + raise Exception("job_id is required") + + result = request("GET", "/report/" + q(job_id) + "/state") + print(json.dumps(result)) + + +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) diff --git a/integrations/hybrid-analysis/scripts/search_hash.py b/integrations/hybrid-analysis/scripts/search_hash.py new file mode 100644 index 0000000..665a29e --- /dev/null +++ b/integrations/hybrid-analysis/scripts/search_hash.py @@ -0,0 +1,51 @@ +import base64, json, os, sys, urllib.parse, urllib.request, urllib.error + +API = "https://www.hybrid-analysis.com/api/v2" + + +def _cfg(): + return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) + + +def _headers(extra=None): + h = {"Accept": "application/json", "User-Agent": "Falcon Sandbox", + "api-key": str(_cfg().get("api_key") or "")} + if extra: + h.update(extra) + return h + + +def request(method, path, params=None, form=None): + url = API + path + p = {k: str(x) for k, x in (params or {}).items() if x not in (None, "")} + if p: + url += ("&" if "?" in url else "?") + urllib.parse.urlencode(p) + data = None + headers = _headers() + if form is not None: + data = urllib.parse.urlencode({k: str(v) for k, v in form.items() if v not in (None, "")}).encode("utf-8") + headers["Content-Type"] = "application/x-www-form-urlencoded" + req = urllib.request.Request(url, data=data, headers=headers, method=method) + with urllib.request.urlopen(req, timeout=90) as r: + raw = r.read() + return json.loads(raw) if raw else {} + + +def main(): + inputs = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}")) + hash_ = inputs.get("hash") + if not hash_: + raise Exception("hash is required") + + result = request("POST", "/search/hash", form={"hash": hash_}) + print(json.dumps(result)) + + +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) diff --git a/integrations/hybrid-analysis/scripts/submit_file.py b/integrations/hybrid-analysis/scripts/submit_file.py new file mode 100644 index 0000000..9748c69 --- /dev/null +++ b/integrations/hybrid-analysis/scripts/submit_file.py @@ -0,0 +1,83 @@ +import base64, json, os, sys, urllib.parse, urllib.request, urllib.error +import uuid + +API = "https://www.hybrid-analysis.com/api/v2" + + +def _cfg(): + return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) + + +def _headers(extra=None): + h = {"Accept": "application/json", "User-Agent": "Falcon Sandbox", + "api-key": str(_cfg().get("api_key") or "")} + if extra: + h.update(extra) + return h + + +def request(method, path, params=None, form=None): + url = API + path + p = {k: str(x) for k, x in (params or {}).items() if x not in (None, "")} + if p: + url += ("&" if "?" in url else "?") + urllib.parse.urlencode(p) + data = None + headers = _headers() + if form is not None: + data = urllib.parse.urlencode({k: str(v) for k, v in form.items() if v not in (None, "")}).encode("utf-8") + headers["Content-Type"] = "application/x-www-form-urlencoded" + req = urllib.request.Request(url, data=data, headers=headers, method=method) + with urllib.request.urlopen(req, timeout=90) as r: + raw = r.read() + return json.loads(raw) if raw else {} + + +def multipart(fields, file_field, file_name, file_bytes): + boundary = "----riposte" + uuid.uuid4().hex + parts = [] + for name, value in fields.items(): + parts.append(("--" + boundary + "\r\n" + + 'Content-Disposition: form-data; name="' + name + '"\r\n\r\n' + + str(value) + "\r\n").encode("utf-8")) + parts.append(("--" + boundary + "\r\n" + + 'Content-Disposition: form-data; name="' + file_field + '"; filename="' + file_name + '"\r\n' + + "Content-Type: application/octet-stream\r\n\r\n").encode("utf-8")) + parts.append(file_bytes) + parts.append(("\r\n--" + boundary + "--\r\n").encode("utf-8")) + return boundary, b"".join(parts) + + +def main(): + inputs = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}")) + file_name = inputs.get("file_name") + if not file_name: + raise Exception("file_name is required") + content_base64 = inputs.get("content_base64") + if not content_base64: + raise Exception("content_base64 is required") + environment_id = inputs.get("environment_id") + if not environment_id: + raise Exception("environment_id is required") + + fields = {"environment_id": str(environment_id)} + boundary, body = multipart(fields, "file", file_name, base64.b64decode(content_base64)) + req = urllib.request.Request( + API + "/submit/file", + data=body, + headers=_headers({"Content-Type": "multipart/form-data; boundary=" + boundary}), + method="POST", + ) + with urllib.request.urlopen(req, timeout=120) as r: + raw = r.read() + result = json.loads(raw) if raw else {} + print(json.dumps(result)) + + +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) diff --git a/integrations/hybrid-analysis/scripts/submit_url.py b/integrations/hybrid-analysis/scripts/submit_url.py new file mode 100644 index 0000000..0023294 --- /dev/null +++ b/integrations/hybrid-analysis/scripts/submit_url.py @@ -0,0 +1,54 @@ +import base64, json, os, sys, urllib.parse, urllib.request, urllib.error + +API = "https://www.hybrid-analysis.com/api/v2" + + +def _cfg(): + return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) + + +def _headers(extra=None): + h = {"Accept": "application/json", "User-Agent": "Falcon Sandbox", + "api-key": str(_cfg().get("api_key") or "")} + if extra: + h.update(extra) + return h + + +def request(method, path, params=None, form=None): + url = API + path + p = {k: str(x) for k, x in (params or {}).items() if x not in (None, "")} + if p: + url += ("&" if "?" in url else "?") + urllib.parse.urlencode(p) + data = None + headers = _headers() + if form is not None: + data = urllib.parse.urlencode({k: str(v) for k, v in form.items() if v not in (None, "")}).encode("utf-8") + headers["Content-Type"] = "application/x-www-form-urlencoded" + req = urllib.request.Request(url, data=data, headers=headers, method=method) + with urllib.request.urlopen(req, timeout=90) as r: + raw = r.read() + return json.loads(raw) if raw else {} + + +def main(): + inputs = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}")) + url = inputs.get("url") + if not url: + raise Exception("url is required") + environment_id = inputs.get("environment_id") + if not environment_id: + raise Exception("environment_id is required") + + result = request("POST", "/submit/url-for-analysis", form={"url": url, "environment_id": environment_id}) + print(json.dumps(result)) + + +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) diff --git a/integrations/hybrid-analysis/scripts/test_connection.py b/integrations/hybrid-analysis/scripts/test_connection.py new file mode 100644 index 0000000..4e22e60 --- /dev/null +++ b/integrations/hybrid-analysis/scripts/test_connection.py @@ -0,0 +1,48 @@ +import base64, json, os, sys, urllib.parse, urllib.request, urllib.error + +API = "https://www.hybrid-analysis.com/api/v2" + + +def _cfg(): + return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) + + +def _headers(extra=None): + h = {"Accept": "application/json", "User-Agent": "Falcon Sandbox", + "api-key": str(_cfg().get("api_key") or "")} + if extra: + h.update(extra) + return h + + +def request(method, path, params=None, form=None): + url = API + path + p = {k: str(x) for k, x in (params or {}).items() if x not in (None, "")} + if p: + url += ("&" if "?" in url else "?") + urllib.parse.urlencode(p) + data = None + headers = _headers() + if form is not None: + data = urllib.parse.urlencode({k: str(v) for k, v in form.items() if v not in (None, "")}).encode("utf-8") + headers["Content-Type"] = "application/x-www-form-urlencoded" + req = urllib.request.Request(url, data=data, headers=headers, method=method) + with urllib.request.urlopen(req, timeout=90) as r: + raw = r.read() + return json.loads(raw) if raw else {} + + +def main(): + result = request("GET", "/key/current") + if not isinstance(result, dict): + raise Exception("unexpected response") + 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)