diff --git a/integrations/joe-sandbox/manifest.yaml b/integrations/joe-sandbox/manifest.yaml new file mode 100644 index 0000000..4f97f61 --- /dev/null +++ b/integrations/joe-sandbox/manifest.yaml @@ -0,0 +1,99 @@ +id: joe_sandbox +name: Joe Sandbox +version: 1.0.0 +description: "Joe Sandbox Cloud (Web API v2) — detonate files and URLs, poll submission/analysis status, search analyses, download reports and read the account quota. API-key authentication; stdlib-only, no extra Python dependencies." +changelog: "1.0.0 — Initial release: file/URL submission, submission and analysis info, search, report download and account quota." +category: enrichment + +# Per-instance configuration. The default server is Joe Sandbox Cloud Pro; set +# server_url for an on-prem Joe Sandbox appliance. The API key is sent as a form +# field on every request. +config_schema: + properties: + api_key: + type: string + description: "Joe Sandbox API key" + x-soar-sensitive: true + server_url: + type: string + description: "Joe Sandbox base URL (default https://jbxcloud.joesecurity.org)" + default: https://jbxcloud.joesecurity.org + required: + - api_key + +commands: + - id: submit_sample + name: joe-sandbox-submit-sample + description: "Submit a file (base64) for analysis. Returns a submission_id; poll it with joe-sandbox-submission-info." + inputs_schema: + properties: + file_name: { type: string, description: "File name" } + content_base64: { type: string, description: "File content, base64-encoded" } + systems: { type: string, description: "Comma-separated analysis systems (e.g. w10x64). Omit for the account default." } + internet_access: { type: boolean, description: "Allow internet access during analysis (default true)" } + required: [file_name, content_base64] + outputs_schema: { properties: {} } + - id: submit_url + name: joe-sandbox-submit-url + description: "Submit a URL for analysis. Returns a submission_id." + inputs_schema: + properties: + url: { type: string, description: "URL to detonate" } + systems: { type: string, description: "Comma-separated analysis systems. Omit for the account default." } + internet_access: { type: boolean, description: "Allow internet access during analysis (default true)" } + required: [url] + outputs_schema: { properties: {} } + - id: submission_info + name: joe-sandbox-submission-info + description: "Get the status and details of a submission by submission_id." + risk: read + inputs_schema: + properties: + submission_id: { type: string, description: "Submission ID (from submit)" } + required: [submission_id] + outputs_schema: { properties: {} } + - id: analysis_info + name: joe-sandbox-analysis-info + description: "Get the details and verdict of an analysis by its web ID." + risk: read + inputs_schema: + properties: + web_id: { type: string, description: "Analysis web ID" } + required: [web_id] + outputs_schema: { properties: {} } + - id: search + name: joe-sandbox-search + description: "Search analyses by term (hash, URL, filename, …)." + risk: read + inputs_schema: + properties: + query: { type: string, description: "Search term" } + required: [query] + outputs_schema: { properties: {} } + - id: download_report + name: joe-sandbox-download-report + description: "Download an analysis report in a given format (returned as base64)." + risk: read + inputs_schema: + properties: + web_id: { type: string, description: "Analysis web ID" } + report_type: { type: string, description: "Report type: json, html, pdf, xml or misp (default json)" } + required: [web_id] + outputs_schema: { properties: {} } + - id: account_quota + name: joe-sandbox-account-quota + description: "Read the account's analysis quota." + risk: read + inputs_schema: + properties: {} + required: [] + outputs_schema: { properties: {} } + + - id: test_connection + name: joe-sandbox-test-connection + description: "Verify the API key and server availability (used by the Test button)." + risk: read + inputs_schema: + properties: {} + required: [] + outputs_schema: { properties: {} } diff --git a/integrations/joe-sandbox/scripts/account_quota.py b/integrations/joe-sandbox/scripts/account_quota.py new file mode 100644 index 0000000..20894a9 --- /dev/null +++ b/integrations/joe-sandbox/scripts/account_quota.py @@ -0,0 +1,37 @@ +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 "https://jbxcloud.joesecurity.org").rstrip("/") + "/api/v2" + + +def post_form(path, fields): + body = dict(fields) + body["apikey"] = str(_cfg().get("api_key") or "") + body["accept-tac"] = "1" + data = urllib.parse.urlencode({k: str(v) for k, v in body.items() if v not in (None, "")}).encode("utf-8") + req = urllib.request.Request(_base() + path, data=data, + headers={"Accept": "application/json", "Content-Type": "application/x-www-form-urlencoded"}, + method="POST") + with urllib.request.urlopen(req, timeout=90) as r: + raw = r.read() + return json.loads(raw) if raw else {} + + +def main(): + res = post_form("/account/info", {}) + print(json.dumps(res)) + + +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/joe-sandbox/scripts/analysis_info.py b/integrations/joe-sandbox/scripts/analysis_info.py new file mode 100644 index 0000000..01727f6 --- /dev/null +++ b/integrations/joe-sandbox/scripts/analysis_info.py @@ -0,0 +1,41 @@ +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 "https://jbxcloud.joesecurity.org").rstrip("/") + "/api/v2" + + +def post_form(path, fields): + body = dict(fields) + body["apikey"] = str(_cfg().get("api_key") or "") + body["accept-tac"] = "1" + data = urllib.parse.urlencode({k: str(v) for k, v in body.items() if v not in (None, "")}).encode("utf-8") + req = urllib.request.Request(_base() + path, data=data, + headers={"Accept": "application/json", "Content-Type": "application/x-www-form-urlencoded"}, + method="POST") + 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", "{}")) + web_id = inputs.get("web_id") + if not web_id: + raise Exception("web_id is required") + res = post_form("/analysis/info", {"webid": web_id}) + print(json.dumps(res)) + + +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/joe-sandbox/scripts/download_report.py b/integrations/joe-sandbox/scripts/download_report.py new file mode 100644 index 0000000..1c567d0 --- /dev/null +++ b/integrations/joe-sandbox/scripts/download_report.py @@ -0,0 +1,59 @@ +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 "https://jbxcloud.joesecurity.org").rstrip("/") + "/api/v2" + + +def post_form(path, fields): + body = dict(fields) + body["apikey"] = str(_cfg().get("api_key") or "") + body["accept-tac"] = "1" + data = urllib.parse.urlencode({k: str(v) for k, v in body.items() if v not in (None, "")}).encode("utf-8") + req = urllib.request.Request(_base() + path, data=data, + headers={"Accept": "application/json", "Content-Type": "application/x-www-form-urlencoded"}, + method="POST") + 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", "{}")) + web_id = inputs.get("web_id") + if not web_id: + raise Exception("web_id is required") + report_type = inputs.get("report_type") or "json" + + fields = { + "apikey": str(_cfg().get("api_key") or ""), + "accept-tac": "1", + "webid": web_id, + "type": report_type, + } + data = urllib.parse.urlencode({k: str(v) for k, v in fields.items() if v not in (None, "")}).encode("utf-8") + req = urllib.request.Request(_base() + "/analysis/download", data=data, + headers={"Accept": "application/json", "Content-Type": "application/x-www-form-urlencoded"}, + method="POST") + with urllib.request.urlopen(req, timeout=90) as r: + content = r.read() + + print(json.dumps({ + "web_id": web_id, + "report_type": report_type, + "content_base64": base64.b64encode(content).decode("ascii"), + })) + + +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/joe-sandbox/scripts/search.py b/integrations/joe-sandbox/scripts/search.py new file mode 100644 index 0000000..9b05d05 --- /dev/null +++ b/integrations/joe-sandbox/scripts/search.py @@ -0,0 +1,41 @@ +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 "https://jbxcloud.joesecurity.org").rstrip("/") + "/api/v2" + + +def post_form(path, fields): + body = dict(fields) + body["apikey"] = str(_cfg().get("api_key") or "") + body["accept-tac"] = "1" + data = urllib.parse.urlencode({k: str(v) for k, v in body.items() if v not in (None, "")}).encode("utf-8") + req = urllib.request.Request(_base() + path, data=data, + headers={"Accept": "application/json", "Content-Type": "application/x-www-form-urlencoded"}, + method="POST") + 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", "{}")) + query = inputs.get("query") + if not query: + raise Exception("query is required") + res = post_form("/analysis/search", {"q": query}) + print(json.dumps(res)) + + +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/joe-sandbox/scripts/submission_info.py b/integrations/joe-sandbox/scripts/submission_info.py new file mode 100644 index 0000000..f3ad1b1 --- /dev/null +++ b/integrations/joe-sandbox/scripts/submission_info.py @@ -0,0 +1,41 @@ +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 "https://jbxcloud.joesecurity.org").rstrip("/") + "/api/v2" + + +def post_form(path, fields): + body = dict(fields) + body["apikey"] = str(_cfg().get("api_key") or "") + body["accept-tac"] = "1" + data = urllib.parse.urlencode({k: str(v) for k, v in body.items() if v not in (None, "")}).encode("utf-8") + req = urllib.request.Request(_base() + path, data=data, + headers={"Accept": "application/json", "Content-Type": "application/x-www-form-urlencoded"}, + method="POST") + 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", "{}")) + submission_id = inputs.get("submission_id") + if not submission_id: + raise Exception("submission_id is required") + res = post_form("/submission/info", {"submission_id": submission_id}) + print(json.dumps(res)) + + +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/joe-sandbox/scripts/submit_sample.py b/integrations/joe-sandbox/scripts/submit_sample.py new file mode 100644 index 0000000..a082d4c --- /dev/null +++ b/integrations/joe-sandbox/scripts/submit_sample.py @@ -0,0 +1,78 @@ +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 "https://jbxcloud.joesecurity.org").rstrip("/") + "/api/v2" + + +def post_form(path, fields): + body = dict(fields) + body["apikey"] = str(_cfg().get("api_key") or "") + body["accept-tac"] = "1" + data = urllib.parse.urlencode({k: str(v) for k, v in body.items() if v not in (None, "")}).encode("utf-8") + req = urllib.request.Request(_base() + path, data=data, + headers={"Accept": "application/json", "Content-Type": "application/x-www-form-urlencoded"}, + method="POST") + 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", "{}")) + file_name = inputs.get("file_name") + content_base64 = inputs.get("content_base64") + if not file_name: + raise Exception("file_name is required") + if not content_base64: + raise Exception("content_base64 is required") + systems = inputs.get("systems") + internet_access = inputs.get("internet_access", True) + + fields = { + "apikey": str(_cfg().get("api_key") or ""), + "accept-tac": "1", + "internet-access": "1" if internet_access else "0", + } + if systems: + fields["systems"] = systems + + boundary = "----riposte" + uuid.uuid4().hex + parts = [] + for name, value in fields.items(): + parts.append(( + "--" + boundary + "\r\n" + 'Content-Disposition: form-data; name="' + str(name) + '"\r\n\r\n' + + str(value) + "\r\n" + ).encode("utf-8")) + parts.append(( + "--" + boundary + "\r\n" + 'Content-Disposition: form-data; name="sample"; filename="' + str(file_name) + '"\r\n' + "Content-Type: application/octet-stream\r\n\r\n" + ).encode("utf-8")) + parts.append(base64.b64decode(content_base64)) + parts.append(("\r\n--" + boundary + "--\r\n").encode("utf-8")) + data = b"".join(parts) + + req = urllib.request.Request(_base() + "/submission/new", data=data, + headers={"Accept": "application/json", + "Content-Type": "multipart/form-data; boundary=" + boundary}, + method="POST") + with urllib.request.urlopen(req, timeout=120) as r: + raw = r.read() + res = json.loads(raw) if raw else {} + print(json.dumps(res)) + + +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/joe-sandbox/scripts/submit_url.py b/integrations/joe-sandbox/scripts/submit_url.py new file mode 100644 index 0000000..c98160f --- /dev/null +++ b/integrations/joe-sandbox/scripts/submit_url.py @@ -0,0 +1,47 @@ +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 "https://jbxcloud.joesecurity.org").rstrip("/") + "/api/v2" + + +def post_form(path, fields): + body = dict(fields) + body["apikey"] = str(_cfg().get("api_key") or "") + body["accept-tac"] = "1" + data = urllib.parse.urlencode({k: str(v) for k, v in body.items() if v not in (None, "")}).encode("utf-8") + req = urllib.request.Request(_base() + path, data=data, + headers={"Accept": "application/json", "Content-Type": "application/x-www-form-urlencoded"}, + method="POST") + 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") + systems = inputs.get("systems") + internet_access = inputs.get("internet_access", True) + res = post_form("/submission/new", { + "url": url, + "internet-access": "1" if internet_access else "0", + "systems": systems or None, + }) + print(json.dumps(res)) + + +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/joe-sandbox/scripts/test_connection.py b/integrations/joe-sandbox/scripts/test_connection.py new file mode 100644 index 0000000..66b1c58 --- /dev/null +++ b/integrations/joe-sandbox/scripts/test_connection.py @@ -0,0 +1,39 @@ +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 "https://jbxcloud.joesecurity.org").rstrip("/") + "/api/v2" + + +def post_form(path, fields): + body = dict(fields) + body["apikey"] = str(_cfg().get("api_key") or "") + body["accept-tac"] = "1" + data = urllib.parse.urlencode({k: str(v) for k, v in body.items() if v not in (None, "")}).encode("utf-8") + req = urllib.request.Request(_base() + path, data=data, + headers={"Accept": "application/json", "Content-Type": "application/x-www-form-urlencoded"}, + method="POST") + with urllib.request.urlopen(req, timeout=90) as r: + raw = r.read() + return json.loads(raw) if raw else {} + + +def main(): + res = post_form("/server/online", {}) + if not isinstance(res, dict): + raise Exception("unexpected response") + print(json.dumps({"ok": True, "online": res.get("data", {}).get("online")})) + + +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)