feat(virustotal): complete v3 command coverage (script-based)

Expand the VirusTotal integration from 3 form-based commands to 15 script-based
commands covering the v3 API: ip/domain/file/url reputation (existing
get_ip_report/get_domain_report ids preserved), file rescan, URL scan,
analysis-get, intelligence search, file sandbox (behaviour) report, passive DNS,
and comments get/add/get-by-id/delete. Scripts handle URL base64 ids,
form-encoded URL submission and comment resource routing.

File-content upload (file-scan) and private scanning are intentionally omitted:
they require an XSOAR-style war-room file entry system Riposte does not have.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Guillaume BOURGEOIS
2026-06-27 15:53:22 +02:00
parent 70dffb3b0a
commit 934f2c52d7
16 changed files with 753 additions and 35 deletions
+148 -35
View File
@@ -1,8 +1,8 @@
id: virustotal id: virustotal
name: VirusTotal name: VirusTotal
version: 1.0.2 version: 1.1.0
description: VirusTotal API v3 — reputation lookups for IPs and domains. 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.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." 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 category: enrichment
config_schema: config_schema:
@@ -27,40 +27,158 @@ auth:
secret_field: api_key secret_field: api_key
commands: commands:
# ── Reputation ──────────────────────────────────────────────────────────────
- id: get_ip_report - id: get_ip_report
name: Get IP report name: virustotal-ip
description: Reputation and last-analysis stats for an IP address. description: "Reputation and last-analysis stats for an IP address."
risk: read
inputs_schema: inputs_schema:
properties: properties:
ip: ip: { type: string, description: "IP address to look up" }
type: string required: [ip]
description: IP address to look up outputs_schema: { properties: {} }
required:
- ip
outputs_schema:
properties: {}
request:
method: GET
path: /ip_addresses/{ip}
auth_ref: apikey
- id: get_domain_report - id: get_domain_report
name: Get domain report name: virustotal-domain
description: Reputation and last-analysis stats for a domain. description: "Reputation and last-analysis stats for a domain."
risk: read
inputs_schema: inputs_schema:
properties: properties:
domain: domain: { type: string, description: "Domain to look up" }
type: string required: [domain]
description: Domain to look up outputs_schema: { properties: {} }
required:
- domain
outputs_schema:
properties: {}
request:
method: GET
path: /domains/{domain}
auth_ref: apikey
- 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 - id: test_connection
name: virustotal-test-connection name: virustotal-test-connection
description: "Verify connectivity and credentials (used by the Test button)." description: "Verify connectivity and credentials (used by the Test button)."
@@ -68,9 +186,4 @@ commands:
inputs_schema: inputs_schema:
properties: {} properties: {}
required: [] required: []
outputs_schema: outputs_schema: { properties: {} }
properties: {}
request:
method: GET
path: /ip_addresses/8.8.8.8
auth_ref: apikey
@@ -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)
@@ -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)
@@ -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)
@@ -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)
@@ -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)
+35
View File
@@ -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)
@@ -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)
@@ -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)
@@ -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)
@@ -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)
@@ -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)
+37
View File
@@ -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)
@@ -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)
+39
View File
@@ -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)
@@ -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)