From 7bec81ad0af7c04ee0b219be2f53c6bbccd22016 Mon Sep 17 00:00:00 2001 From: Guillaume BOURGEOIS Date: Sat, 11 Jul 2026 22:58:15 +0200 Subject: [PATCH] feat(vmray): new VMRay sandbox integration 8 commands: file/URL detonation, sample/submission/analysis retrieval, sample-by-hash lookup, IOCs. API-key auth, stdlib-only. Co-Authored-By: Claude Fable 5 --- integrations/vmray/manifest.yaml | 95 +++++++++++++++++++ .../vmray/scripts/get_analysis_by_sample.py | 54 +++++++++++ integrations/vmray/scripts/get_iocs.py | 54 +++++++++++ integrations/vmray/scripts/get_sample.py | 54 +++++++++++ .../vmray/scripts/get_sample_by_hash.py | 63 ++++++++++++ integrations/vmray/scripts/get_submission.py | 54 +++++++++++ integrations/vmray/scripts/test_connection.py | 49 ++++++++++ integrations/vmray/scripts/upload_sample.py | 85 +++++++++++++++++ integrations/vmray/scripts/upload_url.py | 52 ++++++++++ 9 files changed, 560 insertions(+) create mode 100644 integrations/vmray/manifest.yaml create mode 100644 integrations/vmray/scripts/get_analysis_by_sample.py create mode 100644 integrations/vmray/scripts/get_iocs.py create mode 100644 integrations/vmray/scripts/get_sample.py create mode 100644 integrations/vmray/scripts/get_sample_by_hash.py create mode 100644 integrations/vmray/scripts/get_submission.py create mode 100644 integrations/vmray/scripts/test_connection.py create mode 100644 integrations/vmray/scripts/upload_sample.py create mode 100644 integrations/vmray/scripts/upload_url.py diff --git a/integrations/vmray/manifest.yaml b/integrations/vmray/manifest.yaml new file mode 100644 index 0000000..43f540f --- /dev/null +++ b/integrations/vmray/manifest.yaml @@ -0,0 +1,95 @@ +id: vmray +name: VMRay +version: 1.0.0 +description: "VMRay (REST API) — detonate files and URLs in the VMRay sandbox, retrieve samples/submissions/analyses (by ID or hash), and pull IOCs. API-key authentication; stdlib-only, no extra Python dependencies. Works with VMRay Cloud or an on-prem appliance." +changelog: "1.0.0 — Initial release: file/URL submission, sample/submission/analysis retrieval, sample-by-hash lookup and IOCs." +category: enrichment + +# Per-instance configuration. The API key is sent as 'Authorization: api_key '. +config_schema: + properties: + server_url: + type: string + description: "VMRay server URL (e.g. https://cloud.vmray.com)" + api_key: + type: string + description: "VMRay API key" + x-soar-sensitive: true + required: + - server_url + - api_key + +commands: + - id: upload_sample + name: vmray-upload-sample + description: "Submit a file (base64) for analysis. Returns submission and sample IDs." + inputs_schema: + properties: + file_name: { type: string, description: "File name" } + content_base64: { type: string, description: "File content, base64-encoded" } + tags: { type: string, description: "Comma-separated tags" } + required: [file_name, content_base64] + outputs_schema: { properties: {} } + - id: upload_url + name: vmray-upload-url + description: "Submit a URL for analysis." + inputs_schema: + properties: + url: { type: string, description: "URL to detonate" } + tags: { type: string, description: "Comma-separated tags" } + required: [url] + outputs_schema: { properties: {} } + - id: get_sample + name: vmray-get-sample + description: "Get a sample's details and verdict by sample ID." + risk: read + inputs_schema: + properties: + sample_id: { type: string, description: "Sample ID" } + required: [sample_id] + outputs_schema: { properties: {} } + - id: get_sample_by_hash + name: vmray-get-sample-by-hash + description: "Look up samples by file hash (MD5, SHA1 or SHA256)." + risk: read + inputs_schema: + properties: + hash: { type: string, description: "File hash" } + required: [hash] + outputs_schema: { properties: {} } + - id: get_submission + name: vmray-get-submission + description: "Get a submission's status by submission ID." + risk: read + inputs_schema: + properties: + submission_id: { type: string, description: "Submission ID" } + required: [submission_id] + outputs_schema: { properties: {} } + - id: get_analysis_by_sample + name: vmray-get-analysis-by-sample + description: "List the analyses for a sample." + risk: read + inputs_schema: + properties: + sample_id: { type: string, description: "Sample ID" } + required: [sample_id] + outputs_schema: { properties: {} } + - id: get_iocs + name: vmray-get-iocs + description: "Get the IOCs extracted for a sample." + risk: read + inputs_schema: + properties: + sample_id: { type: string, description: "Sample ID" } + required: [sample_id] + outputs_schema: { properties: {} } + + - id: test_connection + name: vmray-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/vmray/scripts/get_analysis_by_sample.py b/integrations/vmray/scripts/get_analysis_by_sample.py new file mode 100644 index 0000000..18bab78 --- /dev/null +++ b/integrations/vmray/scripts/get_analysis_by_sample.py @@ -0,0 +1,54 @@ +import base64, json, os, sys, urllib.parse, urllib.request, urllib.error + + +def _cfg(): + return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) + + +def _base(): + return str(_cfg().get("server_url") or "").rstrip("/") + "/rest" + + +def _headers(extra=None): + h = {"Accept": "application/json", "Authorization": "api_key " + str(_cfg().get("api_key") or "")} + if extra: + h.update(extra) + return h + + +def request(method, path, params=None, form=None): + url = _base() + 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(): + args = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}")) + sample_id = args.get("sample_id") + if not sample_id: + raise Exception("sample_id is required") + result = request("GET", "/analysis/sample/" + q(sample_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/vmray/scripts/get_iocs.py b/integrations/vmray/scripts/get_iocs.py new file mode 100644 index 0000000..0466b77 --- /dev/null +++ b/integrations/vmray/scripts/get_iocs.py @@ -0,0 +1,54 @@ +import base64, json, os, sys, urllib.parse, urllib.request, urllib.error + + +def _cfg(): + return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) + + +def _base(): + return str(_cfg().get("server_url") or "").rstrip("/") + "/rest" + + +def _headers(extra=None): + h = {"Accept": "application/json", "Authorization": "api_key " + str(_cfg().get("api_key") or "")} + if extra: + h.update(extra) + return h + + +def request(method, path, params=None, form=None): + url = _base() + 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(): + args = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}")) + sample_id = args.get("sample_id") + if not sample_id: + raise Exception("sample_id is required") + result = request("GET", "/sample/" + q(sample_id) + "/iocs") + 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/vmray/scripts/get_sample.py b/integrations/vmray/scripts/get_sample.py new file mode 100644 index 0000000..a770e8a --- /dev/null +++ b/integrations/vmray/scripts/get_sample.py @@ -0,0 +1,54 @@ +import base64, json, os, sys, urllib.parse, urllib.request, urllib.error + + +def _cfg(): + return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) + + +def _base(): + return str(_cfg().get("server_url") or "").rstrip("/") + "/rest" + + +def _headers(extra=None): + h = {"Accept": "application/json", "Authorization": "api_key " + str(_cfg().get("api_key") or "")} + if extra: + h.update(extra) + return h + + +def request(method, path, params=None, form=None): + url = _base() + 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(): + args = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}")) + sample_id = args.get("sample_id") + if not sample_id: + raise Exception("sample_id is required") + result = request("GET", "/sample/" + q(sample_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/vmray/scripts/get_sample_by_hash.py b/integrations/vmray/scripts/get_sample_by_hash.py new file mode 100644 index 0000000..9d561b1 --- /dev/null +++ b/integrations/vmray/scripts/get_sample_by_hash.py @@ -0,0 +1,63 @@ +import base64, json, os, sys, urllib.parse, urllib.request, urllib.error + + +def _cfg(): + return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) + + +def _base(): + return str(_cfg().get("server_url") or "").rstrip("/") + "/rest" + + +def _headers(extra=None): + h = {"Accept": "application/json", "Authorization": "api_key " + str(_cfg().get("api_key") or "")} + if extra: + h.update(extra) + return h + + +def request(method, path, params=None, form=None): + url = _base() + 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(): + args = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}")) + hash_value = args.get("hash") + if not hash_value: + raise Exception("hash is required") + length = len(str(hash_value)) + if length == 32: + hash_type = "md5" + elif length == 40: + hash_type = "sha1" + elif length == 64: + hash_type = "sha256" + else: + raise Exception("unrecognized hash length") + result = request("GET", "/sample/" + hash_type + "/" + q(hash_value)) + 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/vmray/scripts/get_submission.py b/integrations/vmray/scripts/get_submission.py new file mode 100644 index 0000000..b9abc6f --- /dev/null +++ b/integrations/vmray/scripts/get_submission.py @@ -0,0 +1,54 @@ +import base64, json, os, sys, urllib.parse, urllib.request, urllib.error + + +def _cfg(): + return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) + + +def _base(): + return str(_cfg().get("server_url") or "").rstrip("/") + "/rest" + + +def _headers(extra=None): + h = {"Accept": "application/json", "Authorization": "api_key " + str(_cfg().get("api_key") or "")} + if extra: + h.update(extra) + return h + + +def request(method, path, params=None, form=None): + url = _base() + 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(): + args = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}")) + submission_id = args.get("submission_id") + if not submission_id: + raise Exception("submission_id is required") + result = request("GET", "/submission/" + q(submission_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/vmray/scripts/test_connection.py b/integrations/vmray/scripts/test_connection.py new file mode 100644 index 0000000..89af406 --- /dev/null +++ b/integrations/vmray/scripts/test_connection.py @@ -0,0 +1,49 @@ +import base64, json, os, sys, urllib.parse, urllib.request, urllib.error + + +def _cfg(): + return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) + + +def _base(): + return str(_cfg().get("server_url") or "").rstrip("/") + "/rest" + + +def _headers(extra=None): + h = {"Accept": "application/json", "Authorization": "api_key " + str(_cfg().get("api_key") or "")} + if extra: + h.update(extra) + return h + + +def request(method, path, params=None, form=None): + url = _base() + 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", "/analysis", params={"_limit": 1}) + 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) diff --git a/integrations/vmray/scripts/upload_sample.py b/integrations/vmray/scripts/upload_sample.py new file mode 100644 index 0000000..3689690 --- /dev/null +++ b/integrations/vmray/scripts/upload_sample.py @@ -0,0 +1,85 @@ +import base64, json, os, sys, urllib.parse, urllib.request, urllib.error, uuid + + +def _cfg(): + return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) + + +def _base(): + return str(_cfg().get("server_url") or "").rstrip("/") + "/rest" + + +def _headers(extra=None): + h = {"Accept": "application/json", "Authorization": "api_key " + str(_cfg().get("api_key") or "")} + if extra: + h.update(extra) + return h + + +def request(method, path, params=None, form=None): + url = _base() + 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(): + args = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}")) + file_name = args.get("file_name") + content_base64 = args.get("content_base64") + tags = args.get("tags") + if not file_name: + raise Exception("file_name is required") + if not content_base64: + raise Exception("content_base64 is required") + + boundary = "----riposte" + uuid.uuid4().hex + parts = [] + + fields = {"tags": tags} + for name, value in fields.items(): + if value not in (None, ""): + parts.append( + ( + "--" + boundary + "\r\n" + + 'Content-Disposition: form-data; name="' + name + '"\r\n\r\n' + + str(value) + "\r\n" + ).encode("utf-8") + ) + + file_bytes = base64.b64decode(content_base64) + file_header = ( + "--" + boundary + "\r\n" + + 'Content-Disposition: form-data; name="sample_file"; filename="' + file_name + '"\r\n' + + "Content-Type: application/octet-stream\r\n\r\n" + ).encode("utf-8") + parts.append(file_header + file_bytes + b"\r\n") + parts.append(("--" + boundary + "--\r\n").encode("utf-8")) + + body = b"".join(parts) + url = _base() + "/sample/submit" + headers = _headers({"Content-Type": "multipart/form-data; boundary=" + boundary}) + req = urllib.request.Request(url, data=body, headers=headers, 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/vmray/scripts/upload_url.py b/integrations/vmray/scripts/upload_url.py new file mode 100644 index 0000000..63beb62 --- /dev/null +++ b/integrations/vmray/scripts/upload_url.py @@ -0,0 +1,52 @@ +import base64, json, os, sys, urllib.parse, urllib.request, urllib.error + + +def _cfg(): + return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) + + +def _base(): + return str(_cfg().get("server_url") or "").rstrip("/") + "/rest" + + +def _headers(extra=None): + h = {"Accept": "application/json", "Authorization": "api_key " + str(_cfg().get("api_key") or "")} + if extra: + h.update(extra) + return h + + +def request(method, path, params=None, form=None): + url = _base() + 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(): + args = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}")) + url = args.get("url") + tags = args.get("tags") + if not url: + raise Exception("url is required") + result = request("POST", "/sample/submit", form={"sample_url": url, "tags": tags or None}) + 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)