diff --git a/integrations/virustotal/manifest.yaml b/integrations/virustotal/manifest.yaml index f0c7e72..16ce87a 100644 --- a/integrations/virustotal/manifest.yaml +++ b/integrations/virustotal/manifest.yaml @@ -1,8 +1,8 @@ id: virustotal name: VirusTotal -version: 1.0.2 -description: VirusTotal API v3 — reputation lookups for IPs and domains. -changelog: "1.0.2 — Re-publish to regenerate form-based command scripts (fixes 'script not found' on install). 1.0.1 — Added test_connection for the instance Test button. 1.0.0 — Initial release: IP and domain reputation lookups." +version: 1.1.0 +description: "VirusTotal API v3 — reputation for IPs, domains, files and URLs, plus rescan/scan, intelligence search, analysis results, file behaviour (sandbox) reports, passive DNS and comments." +changelog: "1.1.0 — Full v3 coverage: added file and URL reputation, file rescan, URL scan, search, analysis-get, file sandbox (behaviour) report, passive DNS and comments (get/add/get-by-id/delete). Commands are now script-based (URL base64, form-encoded scans, comment resource routing). 1.0.2 — Re-publish to regenerate form-based command scripts. 1.0.1 — Added test_connection. 1.0.0 — Initial release: IP and domain reputation lookups." category: enrichment config_schema: @@ -27,40 +27,158 @@ auth: secret_field: api_key commands: + # ── Reputation ────────────────────────────────────────────────────────────── - id: get_ip_report - name: Get IP report - description: Reputation and last-analysis stats for an IP address. + name: virustotal-ip + description: "Reputation and last-analysis stats for an IP address." + risk: read inputs_schema: properties: - ip: - type: string - description: IP address to look up - required: - - ip - outputs_schema: - properties: {} - request: - method: GET - path: /ip_addresses/{ip} - auth_ref: apikey + ip: { type: string, description: "IP address to look up" } + required: [ip] + outputs_schema: { properties: {} } - id: get_domain_report - name: Get domain report - description: Reputation and last-analysis stats for a domain. + name: virustotal-domain + description: "Reputation and last-analysis stats for a domain." + risk: read inputs_schema: properties: - domain: - type: string - description: Domain to look up - required: - - domain - outputs_schema: - properties: {} - request: - method: GET - path: /domains/{domain} - auth_ref: apikey + domain: { type: string, description: "Domain to look up" } + required: [domain] + outputs_schema: { properties: {} } + - id: file + name: virustotal-file + description: "Reputation and analysis for a file hash (MD5, SHA1, SHA256)." + risk: read + inputs_schema: + properties: + file: { type: string, description: "File hash" } + required: [file] + outputs_schema: { properties: {} } + + - id: url + name: virustotal-url + description: "Reputation and analysis for a URL (base64-id resolved automatically)." + risk: read + inputs_schema: + properties: + url: { type: string, description: "URL to look up" } + required: [url] + outputs_schema: { properties: {} } + + # ── Scans ─────────────────────────────────────────────────────────────────── + - id: file_rescan + name: virustotal-file-rescan + description: "Re-analyse an already-submitted file by hash. Use analysis_get to fetch results." + risk: safe_write + inputs_schema: + properties: + file: { type: string, description: "File hash to rescan" } + required: [file] + outputs_schema: { properties: {} } + + - id: url_scan + name: virustotal-url-scan + description: "Submit a URL for scanning. Use analysis_get to fetch results." + risk: safe_write + inputs_schema: + properties: + url: { type: string, description: "URL to scan" } + required: [url] + outputs_schema: { properties: {} } + + - id: analysis_get + name: virustotal-analysis-get + description: "Get the result of an analysis (from file_rescan or url_scan) by ID." + risk: read + inputs_schema: + properties: + id: { type: string, description: "Analysis ID" } + required: [id] + outputs_schema: { properties: {} } + + # ── Intelligence / relationships ────────────────────────────────────────── + - id: search + name: virustotal-search + description: "Search VirusTotal for a file hash, URL, domain, IP, or tag." + risk: read + inputs_schema: + properties: + query: { type: string, description: "Search query" } + limit: { type: number, description: "Maximum number of results (default 10)" } + required: [query] + outputs_schema: { properties: {} } + + - id: file_sandbox_report + name: virustotal-file-sandbox-report + description: "Retrieve sandbox behaviour reports for a file hash." + risk: read + inputs_schema: + properties: + file: { type: string, description: "File hash" } + limit: { type: number, description: "Maximum number of reports (default 10)" } + required: [file] + outputs_schema: { properties: {} } + + - id: passive_dns + name: virustotal-passive-dns-data + description: "Passive DNS resolutions for an IP address or a domain." + risk: read + inputs_schema: + properties: + resource: { type: string, description: "IP address or domain" } + limit: { type: number, description: "Maximum number of resolutions (default 10)" } + required: [resource] + outputs_schema: { properties: {} } + + # ── Comments ────────────────────────────────────────────────────────────── + - id: comments_get + name: virustotal-comments-get + description: "Get comments for an IP, domain, URL or file." + risk: read + inputs_schema: + properties: + resource: { type: string, description: "The IP, domain, URL or file hash" } + resource_type: { type: string, description: "Resource type: ip, domain, url or file (auto-detected if omitted)" } + limit: { type: number, description: "Maximum number of comments (default 10)" } + required: [resource] + outputs_schema: { properties: {} } + + - id: comments_add + name: virustotal-comments-add + description: "Add a comment to an IP, domain, URL or file." + risk: safe_write + inputs_schema: + properties: + resource: { type: string, description: "The IP, domain, URL or file hash" } + resource_type: { type: string, description: "Resource type: ip, domain, url or file (auto-detected if omitted)" } + comment: { type: string, description: "Comment text" } + required: [resource, comment] + outputs_schema: { properties: {} } + + - id: comments_get_by_id + name: virustotal-comments-get-by-id + description: "Get a single comment by its ID." + risk: read + inputs_schema: + properties: + id: { type: string, description: "Comment ID" } + required: [id] + outputs_schema: { properties: {} } + + - id: comments_delete + name: virustotal-comments-delete + description: "Delete a comment by its ID." + risk: destructive + inputs_schema: + properties: + id: { type: string, description: "Comment ID" } + required: [id] + outputs_schema: { properties: {} } + + # ── Connectivity test ───────────────────────────────────────────────────── - id: test_connection name: virustotal-test-connection description: "Verify connectivity and credentials (used by the Test button)." @@ -68,9 +186,4 @@ commands: inputs_schema: properties: {} required: [] - outputs_schema: - properties: {} - request: - method: GET - path: /ip_addresses/8.8.8.8 - auth_ref: apikey + outputs_schema: { properties: {} } diff --git a/integrations/virustotal/scripts/analysis_get.py b/integrations/virustotal/scripts/analysis_get.py new file mode 100644 index 0000000..64c5137 --- /dev/null +++ b/integrations/virustotal/scripts/analysis_get.py @@ -0,0 +1,35 @@ +import json, os, sys, urllib.request, urllib.parse, urllib.error + + +def _cfg(): + s = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) + base = str(s.get("base_url") or "https://www.virustotal.com/api/v3").rstrip("/") + headers = {"x-apikey": s.get("api_key", ""), "Accept": "application/json"} + return base, headers + + +def _inputs(): + return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}")) + + +def request(path): + base, headers = _cfg() + req = urllib.request.Request(base + "/" + path.lstrip("/"), headers=headers, method="GET") + with urllib.request.urlopen(req, timeout=90) as r: + raw = r.read() + return json.loads(raw) if raw else {} + + +def run(): + analysis_id = urllib.parse.quote(str(_inputs().get("id", "")), safe="") + print(json.dumps(request("analyses/" + analysis_id))) + + +try: + run() +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/virustotal/scripts/comments_add.py b/integrations/virustotal/scripts/comments_add.py new file mode 100644 index 0000000..3858d44 --- /dev/null +++ b/integrations/virustotal/scripts/comments_add.py @@ -0,0 +1,65 @@ +import json, os, sys, base64, ipaddress, urllib.request, urllib.parse, urllib.error + + +def _cfg(): + s = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) + base = str(s.get("base_url") or "https://www.virustotal.com/api/v3").rstrip("/") + headers = {"x-apikey": s.get("api_key", ""), "Accept": "application/json"} + return base, headers + + +def _inputs(): + return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}")) + + +def _b64url(u): + return base64.urlsafe_b64encode(u.encode()).decode().strip("=") + + +def _resource_path(resource, rtype): + rtype = str(rtype or "").strip().lower() + if not rtype: + try: + ipaddress.ip_address(resource) + rtype = "ip" + except ValueError: + h = resource.strip() + if len(h) in (32, 40, 64) and all(c in "0123456789abcdefABCDEF" for c in h): + rtype = "file" + elif "://" in resource: + rtype = "url" + else: + rtype = "domain" + coll = {"ip": "ip_addresses", "domain": "domains", "file": "files", "hash": "files", "url": "urls"}.get(rtype, "files") + rid = _b64url(resource) if coll == "urls" else urllib.parse.quote(resource, safe="") + return coll + "/" + rid + + +def request(method, path, body=None): + base, headers = _cfg() + h = dict(headers) + data = None + if body is not None: + data = json.dumps(body).encode("utf-8") + h["Content-Type"] = "application/json" + req = urllib.request.Request(base + "/" + path.lstrip("/"), data=data, headers=h, method=method) + with urllib.request.urlopen(req, timeout=90) as r: + raw = r.read() + return json.loads(raw) if raw else {} + + +def run(): + inp = _inputs() + path = _resource_path(str(inp.get("resource", "")), inp.get("resource_type")) + body = {"data": {"type": "comment", "attributes": {"text": inp.get("comment", "")}}} + print(json.dumps(request("POST", path + "/comments", body=body))) + + +try: + run() +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/virustotal/scripts/comments_delete.py b/integrations/virustotal/scripts/comments_delete.py new file mode 100644 index 0000000..afa5201 --- /dev/null +++ b/integrations/virustotal/scripts/comments_delete.py @@ -0,0 +1,35 @@ +import json, os, sys, urllib.request, urllib.parse, urllib.error + + +def _cfg(): + s = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) + base = str(s.get("base_url") or "https://www.virustotal.com/api/v3").rstrip("/") + headers = {"x-apikey": s.get("api_key", ""), "Accept": "application/json"} + return base, headers + + +def _inputs(): + return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}")) + + +def request(method, path): + base, headers = _cfg() + req = urllib.request.Request(base + "/" + path.lstrip("/"), headers=headers, method=method) + with urllib.request.urlopen(req, timeout=90) as r: + return r.read().decode("utf-8", "replace") + + +def run(): + comment_id = str(_inputs().get("id", "")) + request("DELETE", "comments/" + urllib.parse.quote(comment_id, safe="")) + print(json.dumps({"deleted": True, "id": comment_id})) + + +try: + run() +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/virustotal/scripts/comments_get.py b/integrations/virustotal/scripts/comments_get.py new file mode 100644 index 0000000..fb46eed --- /dev/null +++ b/integrations/virustotal/scripts/comments_get.py @@ -0,0 +1,69 @@ +import json, os, sys, base64, ipaddress, urllib.request, urllib.parse, urllib.error + + +def _cfg(): + s = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) + base = str(s.get("base_url") or "https://www.virustotal.com/api/v3").rstrip("/") + headers = {"x-apikey": s.get("api_key", ""), "Accept": "application/json"} + return base, headers + + +def _inputs(): + return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}")) + + +def _b64url(u): + return base64.urlsafe_b64encode(u.encode()).decode().strip("=") + + +def _resource_path(resource, rtype): + rtype = str(rtype or "").strip().lower() + if not rtype: + try: + ipaddress.ip_address(resource) + rtype = "ip" + except ValueError: + h = resource.strip() + if len(h) in (32, 40, 64) and all(c in "0123456789abcdefABCDEF" for c in h): + rtype = "file" + elif "://" in resource: + rtype = "url" + else: + rtype = "domain" + coll = {"ip": "ip_addresses", "domain": "domains", "file": "files", "hash": "files", "url": "urls"}.get(rtype, "files") + rid = _b64url(resource) if coll == "urls" else urllib.parse.quote(resource, safe="") + return coll + "/" + rid + + +def request(method, path, params=None, body=None): + base, headers = _cfg() + url = base + "/" + path.lstrip("/") + if params: + clean = {k: v for k, v in params.items() if v not in (None, "")} + if clean: + url += "?" + urllib.parse.urlencode(clean, doseq=True) + h = dict(headers) + data = None + if body is not None: + data = json.dumps(body).encode("utf-8") + h["Content-Type"] = "application/json" + req = urllib.request.Request(url, data=data, headers=h, method=method) + with urllib.request.urlopen(req, timeout=90) as r: + raw = r.read() + return json.loads(raw) if raw else {} + + +def run(): + inp = _inputs() + path = _resource_path(str(inp.get("resource", "")), inp.get("resource_type")) + print(json.dumps(request("GET", path + "/comments", params={"limit": int(inp.get("limit") or 10)}))) + + +try: + run() +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/virustotal/scripts/comments_get_by_id.py b/integrations/virustotal/scripts/comments_get_by_id.py new file mode 100644 index 0000000..d434cbd --- /dev/null +++ b/integrations/virustotal/scripts/comments_get_by_id.py @@ -0,0 +1,35 @@ +import json, os, sys, urllib.request, urllib.parse, urllib.error + + +def _cfg(): + s = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) + base = str(s.get("base_url") or "https://www.virustotal.com/api/v3").rstrip("/") + headers = {"x-apikey": s.get("api_key", ""), "Accept": "application/json"} + return base, headers + + +def _inputs(): + return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}")) + + +def request(path): + base, headers = _cfg() + req = urllib.request.Request(base + "/" + path.lstrip("/"), headers=headers, method="GET") + with urllib.request.urlopen(req, timeout=90) as r: + raw = r.read() + return json.loads(raw) if raw else {} + + +def run(): + comment_id = urllib.parse.quote(str(_inputs().get("id", "")), safe="") + print(json.dumps(request("comments/" + comment_id))) + + +try: + run() +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/virustotal/scripts/file.py b/integrations/virustotal/scripts/file.py new file mode 100644 index 0000000..05230c6 --- /dev/null +++ b/integrations/virustotal/scripts/file.py @@ -0,0 +1,35 @@ +import json, os, sys, urllib.request, urllib.parse, urllib.error + + +def _cfg(): + s = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) + base = str(s.get("base_url") or "https://www.virustotal.com/api/v3").rstrip("/") + headers = {"x-apikey": s.get("api_key", ""), "Accept": "application/json"} + return base, headers + + +def _inputs(): + return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}")) + + +def request(path): + base, headers = _cfg() + req = urllib.request.Request(base + "/" + path.lstrip("/"), headers=headers, method="GET") + with urllib.request.urlopen(req, timeout=90) as r: + raw = r.read() + return json.loads(raw) if raw else {} + + +def run(): + file_hash = urllib.parse.quote(str(_inputs().get("file", "")), safe="") + print(json.dumps(request("files/" + file_hash))) + + +try: + run() +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/virustotal/scripts/file_rescan.py b/integrations/virustotal/scripts/file_rescan.py new file mode 100644 index 0000000..4ece1a6 --- /dev/null +++ b/integrations/virustotal/scripts/file_rescan.py @@ -0,0 +1,35 @@ +import json, os, sys, urllib.request, urllib.parse, urllib.error + + +def _cfg(): + s = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) + base = str(s.get("base_url") or "https://www.virustotal.com/api/v3").rstrip("/") + headers = {"x-apikey": s.get("api_key", ""), "Accept": "application/json"} + return base, headers + + +def _inputs(): + return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}")) + + +def request(method, path): + base, headers = _cfg() + req = urllib.request.Request(base + "/" + path.lstrip("/"), headers=headers, method=method) + with urllib.request.urlopen(req, timeout=90) as r: + raw = r.read() + return json.loads(raw) if raw else {} + + +def run(): + file_hash = urllib.parse.quote(str(_inputs().get("file", "")), safe="") + print(json.dumps(request("POST", "files/" + file_hash + "/analyse"))) + + +try: + run() +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/virustotal/scripts/file_sandbox_report.py b/integrations/virustotal/scripts/file_sandbox_report.py new file mode 100644 index 0000000..fb1adc9 --- /dev/null +++ b/integrations/virustotal/scripts/file_sandbox_report.py @@ -0,0 +1,38 @@ +import json, os, sys, urllib.request, urllib.parse, urllib.error + + +def _cfg(): + s = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) + base = str(s.get("base_url") or "https://www.virustotal.com/api/v3").rstrip("/") + headers = {"x-apikey": s.get("api_key", ""), "Accept": "application/json"} + return base, headers + + +def _inputs(): + return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}")) + + +def request(path, params): + base, headers = _cfg() + clean = {k: v for k, v in params.items() if v not in (None, "")} + url = base + "/" + path.lstrip("/") + ("?" + urllib.parse.urlencode(clean, doseq=True) if clean else "") + req = urllib.request.Request(url, headers=headers, method="GET") + with urllib.request.urlopen(req, timeout=90) as r: + raw = r.read() + return json.loads(raw) if raw else {} + + +def run(): + inp = _inputs() + file_hash = urllib.parse.quote(str(inp.get("file", "")), safe="") + print(json.dumps(request("files/" + file_hash + "/behaviours", {"limit": int(inp.get("limit") or 10)}))) + + +try: + run() +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/virustotal/scripts/get_domain_report.py b/integrations/virustotal/scripts/get_domain_report.py new file mode 100644 index 0000000..9653c34 --- /dev/null +++ b/integrations/virustotal/scripts/get_domain_report.py @@ -0,0 +1,35 @@ +import json, os, sys, urllib.request, urllib.parse, urllib.error + + +def _cfg(): + s = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) + base = str(s.get("base_url") or "https://www.virustotal.com/api/v3").rstrip("/") + headers = {"x-apikey": s.get("api_key", ""), "Accept": "application/json"} + return base, headers + + +def _inputs(): + return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}")) + + +def request(path): + base, headers = _cfg() + req = urllib.request.Request(base + "/" + path.lstrip("/"), headers=headers, method="GET") + with urllib.request.urlopen(req, timeout=90) as r: + raw = r.read() + return json.loads(raw) if raw else {} + + +def run(): + domain = urllib.parse.quote(str(_inputs().get("domain", "")), safe="") + print(json.dumps(request("domains/" + domain))) + + +try: + run() +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/virustotal/scripts/get_ip_report.py b/integrations/virustotal/scripts/get_ip_report.py new file mode 100644 index 0000000..385234a --- /dev/null +++ b/integrations/virustotal/scripts/get_ip_report.py @@ -0,0 +1,35 @@ +import json, os, sys, urllib.request, urllib.parse, urllib.error + + +def _cfg(): + s = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) + base = str(s.get("base_url") or "https://www.virustotal.com/api/v3").rstrip("/") + headers = {"x-apikey": s.get("api_key", ""), "Accept": "application/json"} + return base, headers + + +def _inputs(): + return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}")) + + +def request(path): + base, headers = _cfg() + req = urllib.request.Request(base + "/" + path.lstrip("/"), headers=headers, method="GET") + with urllib.request.urlopen(req, timeout=90) as r: + raw = r.read() + return json.loads(raw) if raw else {} + + +def run(): + ip = urllib.parse.quote(str(_inputs().get("ip", "")), safe="") + print(json.dumps(request("ip_addresses/" + ip))) + + +try: + run() +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/virustotal/scripts/passive_dns.py b/integrations/virustotal/scripts/passive_dns.py new file mode 100644 index 0000000..73f2db6 --- /dev/null +++ b/integrations/virustotal/scripts/passive_dns.py @@ -0,0 +1,44 @@ +import json, os, sys, ipaddress, urllib.request, urllib.parse, urllib.error + + +def _cfg(): + s = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) + base = str(s.get("base_url") or "https://www.virustotal.com/api/v3").rstrip("/") + headers = {"x-apikey": s.get("api_key", ""), "Accept": "application/json"} + return base, headers + + +def _inputs(): + return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}")) + + +def request(path, params): + base, headers = _cfg() + clean = {k: v for k, v in params.items() if v not in (None, "")} + url = base + "/" + path.lstrip("/") + ("?" + urllib.parse.urlencode(clean, doseq=True) if clean else "") + req = urllib.request.Request(url, headers=headers, method="GET") + with urllib.request.urlopen(req, timeout=90) as r: + raw = r.read() + return json.loads(raw) if raw else {} + + +def run(): + inp = _inputs() + resource = str(inp.get("resource", "")) + try: + ipaddress.ip_address(resource) + collection = "ip_addresses" + except ValueError: + collection = "domains" + rid = urllib.parse.quote(resource, safe="") + print(json.dumps(request(collection + "/" + rid + "/resolutions", {"limit": int(inp.get("limit") or 10)}))) + + +try: + run() +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/virustotal/scripts/search.py b/integrations/virustotal/scripts/search.py new file mode 100644 index 0000000..43123d4 --- /dev/null +++ b/integrations/virustotal/scripts/search.py @@ -0,0 +1,37 @@ +import json, os, sys, urllib.request, urllib.parse, urllib.error + + +def _cfg(): + s = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) + base = str(s.get("base_url") or "https://www.virustotal.com/api/v3").rstrip("/") + headers = {"x-apikey": s.get("api_key", ""), "Accept": "application/json"} + return base, headers + + +def _inputs(): + return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}")) + + +def request(path, params): + base, headers = _cfg() + clean = {k: v for k, v in params.items() if v not in (None, "")} + url = base + "/" + path.lstrip("/") + ("?" + urllib.parse.urlencode(clean, doseq=True) if clean else "") + req = urllib.request.Request(url, headers=headers, method="GET") + with urllib.request.urlopen(req, timeout=90) as r: + raw = r.read() + return json.loads(raw) if raw else {} + + +def run(): + inp = _inputs() + print(json.dumps(request("search", {"query": inp.get("query"), "limit": int(inp.get("limit") or 10)}))) + + +try: + run() +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/virustotal/scripts/test_connection.py b/integrations/virustotal/scripts/test_connection.py new file mode 100644 index 0000000..8eb14e4 --- /dev/null +++ b/integrations/virustotal/scripts/test_connection.py @@ -0,0 +1,33 @@ +import json, os, sys, urllib.request, urllib.parse, urllib.error + + +def _cfg(): + s = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) + base = str(s.get("base_url") or "https://www.virustotal.com/api/v3").rstrip("/") + headers = {"x-apikey": s.get("api_key", ""), "Accept": "application/json"} + return base, headers + + +def request(path): + base, headers = _cfg() + req = urllib.request.Request(base + "/" + path.lstrip("/"), headers=headers, method="GET") + with urllib.request.urlopen(req, timeout=60) as r: + raw = r.read() + return json.loads(raw) if raw else {} + + +def run(): + request("ip_addresses/8.8.8.8") + print(json.dumps({"ok": True})) + + +try: + run() +except urllib.error.HTTPError as e: + detail = e.read().decode("utf-8", "replace") + msg = "API key is not valid." if e.code in (401, 403) else "HTTP " + str(e.code) + print(json.dumps({"ok": False, "error": msg, "detail": detail})) + sys.exit(1) +except Exception as e: + print(json.dumps({"ok": False, "error": str(e)})) + sys.exit(1) diff --git a/integrations/virustotal/scripts/url.py b/integrations/virustotal/scripts/url.py new file mode 100644 index 0000000..a034d16 --- /dev/null +++ b/integrations/virustotal/scripts/url.py @@ -0,0 +1,39 @@ +import json, os, sys, base64, urllib.request, urllib.parse, urllib.error + + +def _cfg(): + s = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) + base = str(s.get("base_url") or "https://www.virustotal.com/api/v3").rstrip("/") + headers = {"x-apikey": s.get("api_key", ""), "Accept": "application/json"} + return base, headers + + +def _inputs(): + return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}")) + + +def _b64url(u): + return base64.urlsafe_b64encode(u.encode()).decode().strip("=") + + +def request(path): + base, headers = _cfg() + req = urllib.request.Request(base + "/" + path.lstrip("/"), headers=headers, method="GET") + with urllib.request.urlopen(req, timeout=90) as r: + raw = r.read() + return json.loads(raw) if raw else {} + + +def run(): + url_value = str(_inputs().get("url", "")) + print(json.dumps(request("urls/" + _b64url(url_value)))) + + +try: + run() +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/virustotal/scripts/url_scan.py b/integrations/virustotal/scripts/url_scan.py new file mode 100644 index 0000000..f5a4d51 --- /dev/null +++ b/integrations/virustotal/scripts/url_scan.py @@ -0,0 +1,35 @@ +import json, os, sys, urllib.request, urllib.parse, urllib.error + + +def _cfg(): + s = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) + base = str(s.get("base_url") or "https://www.virustotal.com/api/v3").rstrip("/") + headers = {"x-apikey": s.get("api_key", ""), "Accept": "application/json"} + return base, headers + + +def _inputs(): + return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}")) + + +def run(): + base, headers = _cfg() + url_value = str(_inputs().get("url", "")) + # VirusTotal expects the URL submission as form-encoded data. + data = urllib.parse.urlencode({"url": url_value}).encode("utf-8") + h = dict(headers) + h["Content-Type"] = "application/x-www-form-urlencoded" + req = urllib.request.Request(base + "/urls", data=data, headers=h, method="POST") + with urllib.request.urlopen(req, timeout=90) as r: + raw = r.read() + print(json.dumps(json.loads(raw) if raw else {})) + + +try: + run() +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)