Compare commits
183 Commits
306581e70b
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| bf273b959a | |||
| ba68d19e51 | |||
| a6e1245c79 | |||
| d06f8ea413 | |||
| 17318d4225 | |||
| c22982c8af | |||
| 1d475a6cf5 | |||
| 93309ac74d | |||
| a5095d3f0f | |||
| cebb3af796 | |||
| ba6bcd19f4 | |||
| aabe1a6837 | |||
| 50358ef8a1 | |||
| 82f22fdbb3 | |||
| f7c5e99cc5 | |||
| 6343a0c952 | |||
| 7efc873867 | |||
| 144089a099 | |||
| a4b5b44157 | |||
| 4ea3af6c1d | |||
| 952dc7efda | |||
| cba746a9f9 | |||
| 5d6f823189 | |||
| 098b90d6f3 | |||
| db902c35bc | |||
| 59e9211c0d | |||
| fcbd2ff135 | |||
| ef60cec0fa | |||
| 6d993f584f | |||
| 6d311740f9 | |||
| f6b8c84285 | |||
| 5ee7352652 | |||
| 534748f16a | |||
| a2c9f998c3 | |||
| ae7c494658 | |||
| 40252dadcb | |||
| ac2203f9a5 | |||
| 8248c60da8 | |||
| 4206b44e78 | |||
| 9cffefbb36 | |||
| f7791402a3 | |||
| c42a9cef03 | |||
| f6ff018f09 | |||
| c091800434 | |||
| 929b4166d6 | |||
| 381b42b6c7 | |||
| 5715f21603 | |||
| 7543a95711 | |||
| 7187e7b607 | |||
| 5bb465eb18 | |||
| a4dcd00cc7 | |||
| cf923301a7 | |||
| f833bfe3a5 | |||
| d6129f5d79 | |||
| c779c38dab | |||
| 4f94b59e17 | |||
| f09d00114f | |||
| a4241a5a5d | |||
| 232e310f77 | |||
| b231b648bc | |||
| 087b9abd19 | |||
| e91febd319 | |||
| 374e4184ff | |||
| 82353947d6 | |||
| ac47315b30 | |||
| 631292fcc7 | |||
| d8bb41b4c5 | |||
| 855f1b132c | |||
| 74bf6126fb | |||
| dd574e550b | |||
| 8a7e5a33a2 | |||
| 3c348b7601 | |||
| 2475c4b56e | |||
| 3be14f2f00 | |||
| 3a4d06441c | |||
| ce319c2fea | |||
| 8f3d9607df | |||
| a2f2b8efa0 | |||
| 3c282e8e53 | |||
| 771c90ef0d | |||
| 9fb5b65e4a | |||
| 16fe51ee99 | |||
| 5089e4466d | |||
| 8fa1ade945 | |||
| 54b077bfc9 | |||
| 6d38896892 | |||
| 223dfd34e3 | |||
| 83c1736d47 | |||
| bc100eef67 | |||
| 5f2e946c0a | |||
| 86fdd8483a | |||
| cd02c26b45 | |||
| ab68b3ae71 | |||
| 046fb79008 | |||
| 6548740890 | |||
| 16deec38f8 | |||
| cf950cf6e4 | |||
| 1949c98cf7 | |||
| addaeadae5 | |||
| b137e49be4 | |||
| c5699dd01a | |||
| 015131a2d1 | |||
| 82ad3a9369 | |||
| df8a2c1187 | |||
| 088694fdc2 | |||
| c1867a62a3 | |||
| c2cc4da675 | |||
| 2b5e7f37b4 | |||
| d469ed1d8f | |||
| 1ec065e91b | |||
| ee58358330 | |||
| 5bf679cfe0 | |||
| 70c66f7386 | |||
| 4020e13df9 | |||
| 2a1eb73115 | |||
| 81e39e8148 | |||
| d06ea406d3 | |||
| fe7dae8bd3 | |||
| 52356523c9 | |||
| a567558341 | |||
| 041cadade2 | |||
| 41e8aa8e7e | |||
| 0775500b15 | |||
| 9be28a2ff7 | |||
| 1292a8c798 | |||
| b6af7d1b68 | |||
| d5b7e38dd7 | |||
| 6de01f85bb | |||
| 86e7f11227 | |||
| 9f88891457 | |||
| 9892f2cf06 | |||
| be8bf9f822 | |||
| aca1db5cad | |||
| d0d72c5171 | |||
| fd64c245f4 | |||
| 6cc0bb61bc | |||
| 496ac6d8ce | |||
| 44d0836e1b | |||
| 796a95d5f8 | |||
| a5db725529 | |||
| ecc301d95d | |||
| 6b8dd13297 | |||
| 1c3936992b | |||
| 61da5ef145 | |||
| f34363b450 | |||
| fcf30da5fc | |||
| 847d18f79b | |||
| 5501297984 | |||
| 31d0a5938a | |||
| 62a44aea28 | |||
| 14e5e102a9 | |||
| 460dfaefba | |||
| f826181704 | |||
| 9da00ad7b9 | |||
| 7bd6ad6a69 | |||
| 26694b512a | |||
| e88805ccab | |||
| a279b58290 | |||
| df4cd98a9e | |||
| e3c15ae972 | |||
| 26cd02d77a | |||
| 53034f35da | |||
| fb82de7f93 | |||
| 8ee19ffe85 | |||
| bcc79598e6 | |||
| 7d0e4aa18a | |||
| 7bec81ad0a | |||
| 1bdc71abee | |||
| b8afff7678 | |||
| 239cc70672 | |||
| 2655a14bc3 | |||
| 9589b4f0d1 | |||
| 203273715c | |||
| 1294e3b330 | |||
| 79d870a4e8 | |||
| ac6a52cecd | |||
| 419f891267 | |||
| 2b3b9fc10c | |||
| 9ec84905be | |||
| 9504b22e04 | |||
| 1033388518 | |||
| 334ecac83e | |||
| 811a85424b |
@@ -0,0 +1,67 @@
|
||||
id: abuseipdb
|
||||
name: AbuseIPDB
|
||||
version: 1.0.0
|
||||
description: "AbuseIPDB (API v2) — check the abuse reputation of an IP, report abusive IPs, pull the blacklist and check a CIDR block. API-key authentication; stdlib-only, no extra Python dependencies."
|
||||
changelog: "1.0.0 — Initial release: IP reputation check, report, blacklist retrieval and CIDR-block check."
|
||||
category: enrichment
|
||||
|
||||
config_schema:
|
||||
properties:
|
||||
api_key:
|
||||
type: string
|
||||
description: "AbuseIPDB API key"
|
||||
x-soar-sensitive: true
|
||||
required:
|
||||
- api_key
|
||||
|
||||
commands:
|
||||
- id: check_ip
|
||||
name: abuseipdb-check-ip
|
||||
description: "Check the abuse-confidence reputation of an IP address."
|
||||
risk: read
|
||||
inputs_schema:
|
||||
properties:
|
||||
ip: { type: string, description: "IP address to check" }
|
||||
max_age_days: { type: number, description: "Only consider reports within this many days (default 30, max 365)" }
|
||||
verbose: { type: boolean, description: "Include the detailed report list" }
|
||||
required: [ip]
|
||||
outputs_schema: { properties: {} }
|
||||
- id: report_ip
|
||||
name: abuseipdb-report-ip
|
||||
description: "Report an abusive IP address to AbuseIPDB."
|
||||
inputs_schema:
|
||||
properties:
|
||||
ip: { type: string, description: "IP address to report" }
|
||||
categories: { type: string, description: "Comma-separated AbuseIPDB category IDs (e.g. 18,22)" }
|
||||
comment: { type: string, description: "Description of the abusive activity (avoid sensitive data)" }
|
||||
required: [ip, categories]
|
||||
outputs_schema: { properties: {} }
|
||||
- id: get_blacklist
|
||||
name: abuseipdb-get-blacklist
|
||||
description: "Retrieve the AbuseIPDB blacklist of the most-reported IPs."
|
||||
risk: read
|
||||
inputs_schema:
|
||||
properties:
|
||||
confidence_minimum: { type: number, description: "Minimum abuse-confidence score (default 100)" }
|
||||
limit: { type: number, description: "Maximum entries (default 100)" }
|
||||
required: []
|
||||
outputs_schema: { properties: {} }
|
||||
- id: check_block
|
||||
name: abuseipdb-check-block
|
||||
description: "Check the reports for every address in a CIDR block (max /24 on the free tier)."
|
||||
risk: read
|
||||
inputs_schema:
|
||||
properties:
|
||||
network: { type: string, description: "CIDR network, e.g. 192.0.2.0/24" }
|
||||
max_age_days: { type: number, description: "Only consider reports within this many days (default 30)" }
|
||||
required: [network]
|
||||
outputs_schema: { properties: {} }
|
||||
|
||||
- id: test_connection
|
||||
name: abuseipdb-test-connection
|
||||
description: "Verify the API key (used by the Test button)."
|
||||
risk: read
|
||||
inputs_schema:
|
||||
properties: {}
|
||||
required: []
|
||||
outputs_schema: { properties: {} }
|
||||
@@ -0,0 +1,45 @@
|
||||
import json, os, sys, urllib.parse, urllib.request, urllib.error
|
||||
|
||||
API = "https://api.abuseipdb.com/api/v2"
|
||||
|
||||
|
||||
def _cfg():
|
||||
return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||
|
||||
|
||||
def request(method, path, params=None, body=None):
|
||||
url = API + path
|
||||
q = {k: str(x) for k, x in (params or {}).items() if x not in (None, "")}
|
||||
if q:
|
||||
url += ("&" if "?" in url else "?") + urllib.parse.urlencode(q)
|
||||
data = urllib.parse.urlencode(body).encode("utf-8") if body is not None else None
|
||||
headers = {"Accept": "application/json", "Key": str(_cfg().get("api_key") or "")}
|
||||
if data is not None:
|
||||
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=60) as r:
|
||||
raw = r.read()
|
||||
return json.loads(raw) if raw else {}
|
||||
|
||||
|
||||
def main():
|
||||
inputs = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||
network = inputs.get("network")
|
||||
if not network:
|
||||
raise Exception("network is required")
|
||||
max_age_days = inputs.get("max_age_days")
|
||||
|
||||
params = {"network": network, "maxAgeInDays": max_age_days or 30}
|
||||
|
||||
result = request("GET", "/check-block", params=params)
|
||||
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)
|
||||
@@ -0,0 +1,48 @@
|
||||
import json, os, sys, urllib.parse, urllib.request, urllib.error
|
||||
|
||||
API = "https://api.abuseipdb.com/api/v2"
|
||||
|
||||
|
||||
def _cfg():
|
||||
return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||
|
||||
|
||||
def request(method, path, params=None, body=None):
|
||||
url = API + path
|
||||
q = {k: str(x) for k, x in (params or {}).items() if x not in (None, "")}
|
||||
if q:
|
||||
url += ("&" if "?" in url else "?") + urllib.parse.urlencode(q)
|
||||
data = urllib.parse.urlencode(body).encode("utf-8") if body is not None else None
|
||||
headers = {"Accept": "application/json", "Key": str(_cfg().get("api_key") or "")}
|
||||
if data is not None:
|
||||
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=60) as r:
|
||||
raw = r.read()
|
||||
return json.loads(raw) if raw else {}
|
||||
|
||||
|
||||
def main():
|
||||
inputs = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||
ip = inputs.get("ip")
|
||||
if not ip:
|
||||
raise Exception("ip is required")
|
||||
max_age_days = inputs.get("max_age_days")
|
||||
verbose = inputs.get("verbose")
|
||||
|
||||
params = {"ipAddress": ip, "maxAgeInDays": max_age_days or 30}
|
||||
if verbose:
|
||||
params["verbose"] = ""
|
||||
|
||||
result = request("GET", "/check", params=params)
|
||||
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)
|
||||
@@ -0,0 +1,46 @@
|
||||
import json, os, sys, urllib.parse, urllib.request, urllib.error
|
||||
|
||||
API = "https://api.abuseipdb.com/api/v2"
|
||||
|
||||
|
||||
def _cfg():
|
||||
return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||
|
||||
|
||||
def request(method, path, params=None, body=None):
|
||||
url = API + path
|
||||
q = {k: str(x) for k, x in (params or {}).items() if x not in (None, "")}
|
||||
if q:
|
||||
url += ("&" if "?" in url else "?") + urllib.parse.urlencode(q)
|
||||
data = urllib.parse.urlencode(body).encode("utf-8") if body is not None else None
|
||||
headers = {"Accept": "application/json", "Key": str(_cfg().get("api_key") or "")}
|
||||
if data is not None:
|
||||
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=60) as r:
|
||||
raw = r.read()
|
||||
return json.loads(raw) if raw else {}
|
||||
|
||||
|
||||
def main():
|
||||
inputs = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||
confidence_minimum = inputs.get("confidence_minimum")
|
||||
limit = inputs.get("limit")
|
||||
|
||||
params = {
|
||||
"confidenceMinimum": confidence_minimum or 100,
|
||||
"limit": limit or 100,
|
||||
}
|
||||
|
||||
result = request("GET", "/blacklist", params=params)
|
||||
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)
|
||||
@@ -0,0 +1,50 @@
|
||||
import json, os, sys, urllib.parse, urllib.request, urllib.error
|
||||
|
||||
API = "https://api.abuseipdb.com/api/v2"
|
||||
|
||||
|
||||
def _cfg():
|
||||
return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||
|
||||
|
||||
def request(method, path, params=None, body=None):
|
||||
url = API + path
|
||||
q = {k: str(x) for k, x in (params or {}).items() if x not in (None, "")}
|
||||
if q:
|
||||
url += ("&" if "?" in url else "?") + urllib.parse.urlencode(q)
|
||||
data = urllib.parse.urlencode(body).encode("utf-8") if body is not None else None
|
||||
headers = {"Accept": "application/json", "Key": str(_cfg().get("api_key") or "")}
|
||||
if data is not None:
|
||||
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=60) as r:
|
||||
raw = r.read()
|
||||
return json.loads(raw) if raw else {}
|
||||
|
||||
|
||||
def main():
|
||||
inputs = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||
ip = inputs.get("ip")
|
||||
if not ip:
|
||||
raise Exception("ip is required")
|
||||
categories = inputs.get("categories")
|
||||
if not categories:
|
||||
raise Exception("categories is required")
|
||||
comment = inputs.get("comment")
|
||||
|
||||
body = {"ip": ip, "categories": categories}
|
||||
if comment:
|
||||
body["comment"] = comment
|
||||
|
||||
result = request("POST", "/report", body=body)
|
||||
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)
|
||||
@@ -0,0 +1,40 @@
|
||||
import json, os, sys, urllib.parse, urllib.request, urllib.error
|
||||
|
||||
API = "https://api.abuseipdb.com/api/v2"
|
||||
|
||||
|
||||
def _cfg():
|
||||
return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||
|
||||
|
||||
def request(method, path, params=None, body=None):
|
||||
url = API + path
|
||||
q = {k: str(x) for k, x in (params or {}).items() if x not in (None, "")}
|
||||
if q:
|
||||
url += ("&" if "?" in url else "?") + urllib.parse.urlencode(q)
|
||||
data = urllib.parse.urlencode(body).encode("utf-8") if body is not None else None
|
||||
headers = {"Accept": "application/json", "Key": str(_cfg().get("api_key") or "")}
|
||||
if data is not None:
|
||||
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=60) as r:
|
||||
raw = r.read()
|
||||
return json.loads(raw) if raw else {}
|
||||
|
||||
|
||||
def main():
|
||||
params = {"ipAddress": "8.8.8.8", "maxAgeInDays": 1}
|
||||
result = request("GET", "/check", params=params)
|
||||
if "data" not in result:
|
||||
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)
|
||||
@@ -0,0 +1,101 @@
|
||||
id: alienvault_otx
|
||||
name: AlienVault OTX
|
||||
version: 1.0.0
|
||||
description: "AlienVault OTX (Open Threat Exchange, API v1) — reputation and threat context for IPs, domains, URLs and file hashes, pulse details and search, and passive DNS / related-URL pivots. API-key authentication; stdlib-only, no extra Python dependencies."
|
||||
changelog: "1.0.0 — Initial release: IP/domain/URL/file reputation, pulse details and search, passive DNS and related URLs."
|
||||
category: enrichment
|
||||
|
||||
config_schema:
|
||||
properties:
|
||||
api_key:
|
||||
type: string
|
||||
description: "AlienVault OTX API key (from your OTX account settings)"
|
||||
x-soar-sensitive: true
|
||||
required:
|
||||
- api_key
|
||||
|
||||
commands:
|
||||
- id: ip_reputation
|
||||
name: alienvault-otx-ip
|
||||
description: "Threat context for an IP address (IPv4 or IPv6)."
|
||||
risk: read
|
||||
inputs_schema:
|
||||
properties:
|
||||
ip: { type: string, description: "IP address" }
|
||||
required: [ip]
|
||||
outputs_schema: { properties: {} }
|
||||
- id: domain_reputation
|
||||
name: alienvault-otx-domain
|
||||
description: "Threat context for a domain."
|
||||
risk: read
|
||||
inputs_schema:
|
||||
properties:
|
||||
domain: { type: string, description: "Domain name" }
|
||||
required: [domain]
|
||||
outputs_schema: { properties: {} }
|
||||
- id: url_reputation
|
||||
name: alienvault-otx-url
|
||||
description: "Threat context for a URL."
|
||||
risk: read
|
||||
inputs_schema:
|
||||
properties:
|
||||
url: { type: string, description: "URL" }
|
||||
required: [url]
|
||||
outputs_schema: { properties: {} }
|
||||
- id: file_reputation
|
||||
name: alienvault-otx-file
|
||||
description: "Threat context for a file hash (MD5, SHA1 or SHA256)."
|
||||
risk: read
|
||||
inputs_schema:
|
||||
properties:
|
||||
file: { type: string, description: "File hash" }
|
||||
required: [file]
|
||||
outputs_schema: { properties: {} }
|
||||
- id: get_pulse
|
||||
name: alienvault-otx-get-pulse
|
||||
description: "Get the details of a pulse (threat report) by ID."
|
||||
risk: read
|
||||
inputs_schema:
|
||||
properties:
|
||||
pulse_id: { type: string, description: "Pulse ID" }
|
||||
required: [pulse_id]
|
||||
outputs_schema: { properties: {} }
|
||||
- id: search_pulses
|
||||
name: alienvault-otx-search-pulses
|
||||
description: "Search pulses by keyword."
|
||||
risk: read
|
||||
inputs_schema:
|
||||
properties:
|
||||
query: { type: string, description: "Search string" }
|
||||
limit: { type: number, description: "Maximum pulses (default 20)" }
|
||||
required: [query]
|
||||
outputs_schema: { properties: {} }
|
||||
- id: passive_dns
|
||||
name: alienvault-otx-passive-dns
|
||||
description: "Passive DNS records for an IP or domain indicator."
|
||||
risk: read
|
||||
inputs_schema:
|
||||
properties:
|
||||
indicator: { type: string, description: "IP or domain" }
|
||||
indicator_type: { type: string, description: "IPv4, IPv6 or domain (default auto-detected)" }
|
||||
required: [indicator]
|
||||
outputs_schema: { properties: {} }
|
||||
- id: related_urls
|
||||
name: alienvault-otx-related-urls
|
||||
description: "URLs associated with an IP or domain indicator."
|
||||
risk: read
|
||||
inputs_schema:
|
||||
properties:
|
||||
indicator: { type: string, description: "IP or domain" }
|
||||
indicator_type: { type: string, description: "IPv4, IPv6 or domain (default auto-detected)" }
|
||||
required: [indicator]
|
||||
outputs_schema: { properties: {} }
|
||||
|
||||
- id: test_connection
|
||||
name: alienvault-otx-test-connection
|
||||
description: "Verify the API key (used by the Test button)."
|
||||
risk: read
|
||||
inputs_schema:
|
||||
properties: {}
|
||||
required: []
|
||||
outputs_schema: { properties: {} }
|
||||
@@ -0,0 +1,41 @@
|
||||
import json, os, sys, urllib.parse, urllib.request, urllib.error
|
||||
|
||||
API = "https://otx.alienvault.com/api/v1"
|
||||
|
||||
|
||||
def _cfg():
|
||||
return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||
|
||||
|
||||
def request(method, path, params=None):
|
||||
url = API + path
|
||||
p = {k: str(x) for k, x in (params or {}).items() if x not in (None, "")}
|
||||
if p:
|
||||
url += ("&" if "?" in url else "?") + urllib.parse.urlencode(p)
|
||||
headers = {"Accept": "application/json", "X-OTX-API-KEY": str(_cfg().get("api_key") or "")}
|
||||
req = urllib.request.Request(url, headers=headers, method=method)
|
||||
with urllib.request.urlopen(req, timeout=60) as r:
|
||||
raw = r.read()
|
||||
return json.loads(raw) if raw else {}
|
||||
|
||||
|
||||
q = lambda v: urllib.parse.quote(str(v), safe="")
|
||||
|
||||
|
||||
def main():
|
||||
inputs = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||
domain = inputs.get("domain")
|
||||
if not domain:
|
||||
raise Exception("domain is required")
|
||||
res = request("GET", "/indicators/domain/%s/general" % q(domain))
|
||||
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)
|
||||
@@ -0,0 +1,41 @@
|
||||
import json, os, sys, urllib.parse, urllib.request, urllib.error
|
||||
|
||||
API = "https://otx.alienvault.com/api/v1"
|
||||
|
||||
|
||||
def _cfg():
|
||||
return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||
|
||||
|
||||
def request(method, path, params=None):
|
||||
url = API + path
|
||||
p = {k: str(x) for k, x in (params or {}).items() if x not in (None, "")}
|
||||
if p:
|
||||
url += ("&" if "?" in url else "?") + urllib.parse.urlencode(p)
|
||||
headers = {"Accept": "application/json", "X-OTX-API-KEY": str(_cfg().get("api_key") or "")}
|
||||
req = urllib.request.Request(url, headers=headers, method=method)
|
||||
with urllib.request.urlopen(req, timeout=60) as r:
|
||||
raw = r.read()
|
||||
return json.loads(raw) if raw else {}
|
||||
|
||||
|
||||
q = lambda v: urllib.parse.quote(str(v), safe="")
|
||||
|
||||
|
||||
def main():
|
||||
inputs = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||
file_hash = inputs.get("file")
|
||||
if not file_hash:
|
||||
raise Exception("file is required")
|
||||
res = request("GET", "/indicators/file/%s/general" % q(file_hash))
|
||||
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)
|
||||
@@ -0,0 +1,41 @@
|
||||
import json, os, sys, urllib.parse, urllib.request, urllib.error
|
||||
|
||||
API = "https://otx.alienvault.com/api/v1"
|
||||
|
||||
|
||||
def _cfg():
|
||||
return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||
|
||||
|
||||
def request(method, path, params=None):
|
||||
url = API + path
|
||||
p = {k: str(x) for k, x in (params or {}).items() if x not in (None, "")}
|
||||
if p:
|
||||
url += ("&" if "?" in url else "?") + urllib.parse.urlencode(p)
|
||||
headers = {"Accept": "application/json", "X-OTX-API-KEY": str(_cfg().get("api_key") or "")}
|
||||
req = urllib.request.Request(url, headers=headers, method=method)
|
||||
with urllib.request.urlopen(req, timeout=60) as r:
|
||||
raw = r.read()
|
||||
return json.loads(raw) if raw else {}
|
||||
|
||||
|
||||
q = lambda v: urllib.parse.quote(str(v), safe="")
|
||||
|
||||
|
||||
def main():
|
||||
inputs = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||
pulse_id = inputs.get("pulse_id")
|
||||
if not pulse_id:
|
||||
raise Exception("pulse_id is required")
|
||||
res = request("GET", "/pulses/%s" % q(pulse_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)
|
||||
@@ -0,0 +1,43 @@
|
||||
import json, os, sys, urllib.parse, urllib.request, urllib.error
|
||||
import re
|
||||
|
||||
API = "https://otx.alienvault.com/api/v1"
|
||||
|
||||
|
||||
def _cfg():
|
||||
return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||
|
||||
|
||||
def request(method, path, params=None):
|
||||
url = API + path
|
||||
p = {k: str(x) for k, x in (params or {}).items() if x not in (None, "")}
|
||||
if p:
|
||||
url += ("&" if "?" in url else "?") + urllib.parse.urlencode(p)
|
||||
headers = {"Accept": "application/json", "X-OTX-API-KEY": str(_cfg().get("api_key") or "")}
|
||||
req = urllib.request.Request(url, headers=headers, method=method)
|
||||
with urllib.request.urlopen(req, timeout=60) as r:
|
||||
raw = r.read()
|
||||
return json.loads(raw) if raw else {}
|
||||
|
||||
|
||||
q = lambda v: urllib.parse.quote(str(v), safe="")
|
||||
|
||||
|
||||
def main():
|
||||
inputs = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||
ip = inputs.get("ip")
|
||||
if not ip:
|
||||
raise Exception("ip is required")
|
||||
section = "IPv6" if ":" in ip else "IPv4"
|
||||
res = request("GET", "/indicators/%s/%s/general" % (section, q(ip)))
|
||||
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)
|
||||
@@ -0,0 +1,44 @@
|
||||
import json, os, sys, urllib.parse, urllib.request, urllib.error
|
||||
import re
|
||||
|
||||
API = "https://otx.alienvault.com/api/v1"
|
||||
|
||||
|
||||
def _cfg():
|
||||
return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||
|
||||
|
||||
def request(method, path, params=None):
|
||||
url = API + path
|
||||
p = {k: str(x) for k, x in (params or {}).items() if x not in (None, "")}
|
||||
if p:
|
||||
url += ("&" if "?" in url else "?") + urllib.parse.urlencode(p)
|
||||
headers = {"Accept": "application/json", "X-OTX-API-KEY": str(_cfg().get("api_key") or "")}
|
||||
req = urllib.request.Request(url, headers=headers, method=method)
|
||||
with urllib.request.urlopen(req, timeout=60) as r:
|
||||
raw = r.read()
|
||||
return json.loads(raw) if raw else {}
|
||||
|
||||
|
||||
q = lambda v: urllib.parse.quote(str(v), safe="")
|
||||
|
||||
|
||||
def main():
|
||||
inputs = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||
indicator = inputs.get("indicator")
|
||||
if not indicator:
|
||||
raise Exception("indicator is required")
|
||||
indicator_type = inputs.get("indicator_type")
|
||||
itype = indicator_type or ("IPv6" if ":" in indicator else ("IPv4" if re.match(r"^\d+\.\d+\.\d+\.\d+$", indicator) else "domain"))
|
||||
res = request("GET", "/indicators/%s/%s/passive_dns" % (itype, q(indicator)))
|
||||
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)
|
||||
@@ -0,0 +1,44 @@
|
||||
import json, os, sys, urllib.parse, urllib.request, urllib.error
|
||||
import re
|
||||
|
||||
API = "https://otx.alienvault.com/api/v1"
|
||||
|
||||
|
||||
def _cfg():
|
||||
return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||
|
||||
|
||||
def request(method, path, params=None):
|
||||
url = API + path
|
||||
p = {k: str(x) for k, x in (params or {}).items() if x not in (None, "")}
|
||||
if p:
|
||||
url += ("&" if "?" in url else "?") + urllib.parse.urlencode(p)
|
||||
headers = {"Accept": "application/json", "X-OTX-API-KEY": str(_cfg().get("api_key") or "")}
|
||||
req = urllib.request.Request(url, headers=headers, method=method)
|
||||
with urllib.request.urlopen(req, timeout=60) as r:
|
||||
raw = r.read()
|
||||
return json.loads(raw) if raw else {}
|
||||
|
||||
|
||||
q = lambda v: urllib.parse.quote(str(v), safe="")
|
||||
|
||||
|
||||
def main():
|
||||
inputs = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||
indicator = inputs.get("indicator")
|
||||
if not indicator:
|
||||
raise Exception("indicator is required")
|
||||
indicator_type = inputs.get("indicator_type")
|
||||
itype = indicator_type or ("IPv6" if ":" in indicator else ("IPv4" if re.match(r"^\d+\.\d+\.\d+\.\d+$", indicator) else "domain"))
|
||||
res = request("GET", "/indicators/%s/%s/url_list" % (itype, q(indicator)), {"limit": 100})
|
||||
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)
|
||||
@@ -0,0 +1,42 @@
|
||||
import json, os, sys, urllib.parse, urllib.request, urllib.error
|
||||
|
||||
API = "https://otx.alienvault.com/api/v1"
|
||||
|
||||
|
||||
def _cfg():
|
||||
return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||
|
||||
|
||||
def request(method, path, params=None):
|
||||
url = API + path
|
||||
p = {k: str(x) for k, x in (params or {}).items() if x not in (None, "")}
|
||||
if p:
|
||||
url += ("&" if "?" in url else "?") + urllib.parse.urlencode(p)
|
||||
headers = {"Accept": "application/json", "X-OTX-API-KEY": str(_cfg().get("api_key") or "")}
|
||||
req = urllib.request.Request(url, headers=headers, method=method)
|
||||
with urllib.request.urlopen(req, timeout=60) as r:
|
||||
raw = r.read()
|
||||
return json.loads(raw) if raw else {}
|
||||
|
||||
|
||||
q = lambda v: urllib.parse.quote(str(v), safe="")
|
||||
|
||||
|
||||
def main():
|
||||
inputs = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||
query = inputs.get("query")
|
||||
if not query:
|
||||
raise Exception("query is required")
|
||||
limit = inputs.get("limit")
|
||||
res = request("GET", "/search/pulses", {"q": query, "limit": limit or 20})
|
||||
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)
|
||||
@@ -0,0 +1,39 @@
|
||||
import json, os, sys, urllib.parse, urllib.request, urllib.error
|
||||
|
||||
API = "https://otx.alienvault.com/api/v1"
|
||||
|
||||
|
||||
def _cfg():
|
||||
return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||
|
||||
|
||||
def request(method, path, params=None):
|
||||
url = API + path
|
||||
p = {k: str(x) for k, x in (params or {}).items() if x not in (None, "")}
|
||||
if p:
|
||||
url += ("&" if "?" in url else "?") + urllib.parse.urlencode(p)
|
||||
headers = {"Accept": "application/json", "X-OTX-API-KEY": str(_cfg().get("api_key") or "")}
|
||||
req = urllib.request.Request(url, headers=headers, method=method)
|
||||
with urllib.request.urlopen(req, timeout=60) as r:
|
||||
raw = r.read()
|
||||
return json.loads(raw) if raw else {}
|
||||
|
||||
|
||||
q = lambda v: urllib.parse.quote(str(v), safe="")
|
||||
|
||||
|
||||
def main():
|
||||
res = request("GET", "/user/me")
|
||||
if "username" not in res:
|
||||
raise Exception("unexpected response")
|
||||
print(json.dumps({"ok": True, "user": res.get("username")}))
|
||||
|
||||
|
||||
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)
|
||||
@@ -0,0 +1,41 @@
|
||||
import json, os, sys, urllib.parse, urllib.request, urllib.error
|
||||
|
||||
API = "https://otx.alienvault.com/api/v1"
|
||||
|
||||
|
||||
def _cfg():
|
||||
return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||
|
||||
|
||||
def request(method, path, params=None):
|
||||
url = API + path
|
||||
p = {k: str(x) for k, x in (params or {}).items() if x not in (None, "")}
|
||||
if p:
|
||||
url += ("&" if "?" in url else "?") + urllib.parse.urlencode(p)
|
||||
headers = {"Accept": "application/json", "X-OTX-API-KEY": str(_cfg().get("api_key") or "")}
|
||||
req = urllib.request.Request(url, headers=headers, method=method)
|
||||
with urllib.request.urlopen(req, timeout=60) as r:
|
||||
raw = r.read()
|
||||
return json.loads(raw) if raw else {}
|
||||
|
||||
|
||||
q = lambda v: urllib.parse.quote(str(v), safe="")
|
||||
|
||||
|
||||
def main():
|
||||
inputs = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||
url = inputs.get("url")
|
||||
if not url:
|
||||
raise Exception("url is required")
|
||||
res = request("GET", "/indicators/url/%s/general" % q(url))
|
||||
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)
|
||||
@@ -0,0 +1,112 @@
|
||||
id: anomali_threatstream
|
||||
name: Anomali ThreatStream
|
||||
version: 1.0.0
|
||||
description: "Anomali ThreatStream (API v2/v1) — threat intelligence: reputation lookups for IPs, domains, file hashes and URLs, indicator search, passive DNS, threat-model listing, and indicator import (with or without approval). API-key authentication; stdlib-only, no extra Python dependencies."
|
||||
changelog: "1.0.0 — Initial release: ip/domain/file/url reputation, indicator search, passive DNS, threat models, import indicator."
|
||||
category: threat_intel
|
||||
|
||||
# Per-instance configuration. Auth header 'Authorization: apikey <username>:<api_key>'.
|
||||
config_schema:
|
||||
properties:
|
||||
url:
|
||||
type: string
|
||||
description: "ThreatStream API URL"
|
||||
default: "https://api.threatstream.com"
|
||||
username:
|
||||
type: string
|
||||
description: "ThreatStream username"
|
||||
api_key:
|
||||
type: string
|
||||
description: "ThreatStream API key"
|
||||
x-soar-sensitive: true
|
||||
required:
|
||||
- username
|
||||
- api_key
|
||||
|
||||
commands:
|
||||
- id: ip_reputation
|
||||
name: anomali-ip-reputation
|
||||
description: "Look up threat intelligence for an IP address."
|
||||
risk: read
|
||||
inputs_schema:
|
||||
properties:
|
||||
ip: { type: string, description: "IP address" }
|
||||
required: [ip]
|
||||
outputs_schema: { properties: {} }
|
||||
- id: domain_reputation
|
||||
name: anomali-domain-reputation
|
||||
description: "Look up threat intelligence for a domain."
|
||||
risk: read
|
||||
inputs_schema:
|
||||
properties:
|
||||
domain: { type: string, description: "Domain name" }
|
||||
required: [domain]
|
||||
outputs_schema: { properties: {} }
|
||||
- id: file_reputation
|
||||
name: anomali-file-reputation
|
||||
description: "Look up threat intelligence for a file hash."
|
||||
risk: read
|
||||
inputs_schema:
|
||||
properties:
|
||||
file_hash: { type: string, description: "MD5/SHA1/SHA256 hash" }
|
||||
required: [file_hash]
|
||||
outputs_schema: { properties: {} }
|
||||
- id: url_reputation
|
||||
name: anomali-url-reputation
|
||||
description: "Look up threat intelligence for a URL."
|
||||
risk: read
|
||||
inputs_schema:
|
||||
properties:
|
||||
url: { type: string, description: "URL" }
|
||||
required: [url]
|
||||
outputs_schema: { properties: {} }
|
||||
- id: get_indicators
|
||||
name: anomali-get-indicators
|
||||
description: "Search indicators with a free-text query."
|
||||
risk: read
|
||||
inputs_schema:
|
||||
properties:
|
||||
query: { type: string, description: "ThreatStream search query (q=)" }
|
||||
limit: { type: number, description: "Max indicators (default 20)" }
|
||||
required: []
|
||||
outputs_schema: { properties: {} }
|
||||
- id: passive_dns
|
||||
name: anomali-passive-dns
|
||||
description: "Get passive DNS records for an IP or domain."
|
||||
risk: read
|
||||
inputs_schema:
|
||||
properties:
|
||||
value: { type: string, description: "IP or domain" }
|
||||
type: { type: string, description: "ip or domain (default ip)" }
|
||||
required: [value]
|
||||
outputs_schema: { properties: {} }
|
||||
- id: get_threat_models
|
||||
name: anomali-get-threat-models
|
||||
description: "List threat models (actors, campaigns, incidents, ...)."
|
||||
risk: read
|
||||
inputs_schema:
|
||||
properties:
|
||||
query: { type: string, description: "Optional name search" }
|
||||
limit: { type: number, description: "Max models (default 20)" }
|
||||
required: []
|
||||
outputs_schema: { properties: {} }
|
||||
- id: import_indicator
|
||||
name: anomali-import-indicator
|
||||
description: "Import an observable as an indicator (optionally requiring approval)."
|
||||
inputs_schema:
|
||||
properties:
|
||||
value: { type: string, description: "Observable value (IP, domain, hash, URL)" }
|
||||
itype: { type: string, description: "Indicator type (e.g. mal_ip, mal_domain, apt_md5)" }
|
||||
confidence: { type: number, description: "Confidence 0-100 (default 50)" }
|
||||
approve: { type: boolean, description: "Import without approval (default false = requires approval)" }
|
||||
required: [value, itype]
|
||||
outputs_schema: { properties: {} }
|
||||
|
||||
- id: test_connection
|
||||
name: anomali-test-connection
|
||||
description: "Verify connectivity and the API key (used by the Test button)."
|
||||
risk: read
|
||||
inputs_schema:
|
||||
properties: {}
|
||||
required: []
|
||||
outputs_schema: { properties: {} }
|
||||
@@ -0,0 +1,54 @@
|
||||
import json, os, sys, urllib.parse, urllib.request, urllib.error
|
||||
|
||||
|
||||
def _cfg():
|
||||
return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||
|
||||
|
||||
def _inputs():
|
||||
return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||
|
||||
|
||||
def _base(cfg):
|
||||
return (str(cfg.get("url") or "https://api.threatstream.com")).rstrip("/")
|
||||
|
||||
|
||||
def _auth(cfg):
|
||||
return "apikey " + str(cfg.get("username", "")) + ":" + str(cfg.get("api_key", ""))
|
||||
|
||||
|
||||
def request(method, path, cfg, body=None, params=None):
|
||||
url = _base(cfg) + path
|
||||
if params:
|
||||
clean = {k: v for k, v in params.items() if v not in (None, "")}
|
||||
if clean:
|
||||
url += "?" + urllib.parse.urlencode(clean)
|
||||
data = json.dumps(body).encode("utf-8") if body is not None else None
|
||||
headers = {"Authorization": _auth(cfg), "Accept": "application/json"}
|
||||
if data is not None:
|
||||
headers["Content-Type"] = "application/json"
|
||||
req = urllib.request.Request(url, data=data, headers=headers, method=method)
|
||||
with urllib.request.urlopen(req, timeout=60) as r:
|
||||
raw = r.read()
|
||||
return json.loads(raw) if raw else {}
|
||||
|
||||
|
||||
def _run(fn):
|
||||
try:
|
||||
print(json.dumps(fn(_cfg(), _inputs())))
|
||||
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)
|
||||
|
||||
|
||||
def main(cfg, inputs):
|
||||
domain = inputs.get("domain")
|
||||
if not domain:
|
||||
raise Exception("domain is required")
|
||||
return request("GET", "/api/v2/intelligence/", cfg, params={"value": domain, "type": "domain", "limit": 50})
|
||||
|
||||
|
||||
_run(main)
|
||||
@@ -0,0 +1,54 @@
|
||||
import json, os, sys, urllib.parse, urllib.request, urllib.error
|
||||
|
||||
|
||||
def _cfg():
|
||||
return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||
|
||||
|
||||
def _inputs():
|
||||
return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||
|
||||
|
||||
def _base(cfg):
|
||||
return (str(cfg.get("url") or "https://api.threatstream.com")).rstrip("/")
|
||||
|
||||
|
||||
def _auth(cfg):
|
||||
return "apikey " + str(cfg.get("username", "")) + ":" + str(cfg.get("api_key", ""))
|
||||
|
||||
|
||||
def request(method, path, cfg, body=None, params=None):
|
||||
url = _base(cfg) + path
|
||||
if params:
|
||||
clean = {k: v for k, v in params.items() if v not in (None, "")}
|
||||
if clean:
|
||||
url += "?" + urllib.parse.urlencode(clean)
|
||||
data = json.dumps(body).encode("utf-8") if body is not None else None
|
||||
headers = {"Authorization": _auth(cfg), "Accept": "application/json"}
|
||||
if data is not None:
|
||||
headers["Content-Type"] = "application/json"
|
||||
req = urllib.request.Request(url, data=data, headers=headers, method=method)
|
||||
with urllib.request.urlopen(req, timeout=60) as r:
|
||||
raw = r.read()
|
||||
return json.loads(raw) if raw else {}
|
||||
|
||||
|
||||
def _run(fn):
|
||||
try:
|
||||
print(json.dumps(fn(_cfg(), _inputs())))
|
||||
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)
|
||||
|
||||
|
||||
def main(cfg, inputs):
|
||||
file_hash = inputs.get("file_hash")
|
||||
if not file_hash:
|
||||
raise Exception("file_hash is required")
|
||||
return request("GET", "/api/v2/intelligence/", cfg, params={"value": file_hash, "type": "md5", "limit": 50})
|
||||
|
||||
|
||||
_run(main)
|
||||
@@ -0,0 +1,53 @@
|
||||
import json, os, sys, urllib.parse, urllib.request, urllib.error
|
||||
|
||||
|
||||
def _cfg():
|
||||
return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||
|
||||
|
||||
def _inputs():
|
||||
return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||
|
||||
|
||||
def _base(cfg):
|
||||
return (str(cfg.get("url") or "https://api.threatstream.com")).rstrip("/")
|
||||
|
||||
|
||||
def _auth(cfg):
|
||||
return "apikey " + str(cfg.get("username", "")) + ":" + str(cfg.get("api_key", ""))
|
||||
|
||||
|
||||
def request(method, path, cfg, body=None, params=None):
|
||||
url = _base(cfg) + path
|
||||
if params:
|
||||
clean = {k: v for k, v in params.items() if v not in (None, "")}
|
||||
if clean:
|
||||
url += "?" + urllib.parse.urlencode(clean)
|
||||
data = json.dumps(body).encode("utf-8") if body is not None else None
|
||||
headers = {"Authorization": _auth(cfg), "Accept": "application/json"}
|
||||
if data is not None:
|
||||
headers["Content-Type"] = "application/json"
|
||||
req = urllib.request.Request(url, data=data, headers=headers, method=method)
|
||||
with urllib.request.urlopen(req, timeout=60) as r:
|
||||
raw = r.read()
|
||||
return json.loads(raw) if raw else {}
|
||||
|
||||
|
||||
def _run(fn):
|
||||
try:
|
||||
print(json.dumps(fn(_cfg(), _inputs())))
|
||||
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)
|
||||
|
||||
|
||||
def main(cfg, inputs):
|
||||
query = inputs.get("query")
|
||||
limit = inputs.get("limit")
|
||||
return request("GET", "/api/v2/intelligence/", cfg, params={"q": query, "limit": int(limit or 20)})
|
||||
|
||||
|
||||
_run(main)
|
||||
@@ -0,0 +1,53 @@
|
||||
import json, os, sys, urllib.parse, urllib.request, urllib.error
|
||||
|
||||
|
||||
def _cfg():
|
||||
return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||
|
||||
|
||||
def _inputs():
|
||||
return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||
|
||||
|
||||
def _base(cfg):
|
||||
return (str(cfg.get("url") or "https://api.threatstream.com")).rstrip("/")
|
||||
|
||||
|
||||
def _auth(cfg):
|
||||
return "apikey " + str(cfg.get("username", "")) + ":" + str(cfg.get("api_key", ""))
|
||||
|
||||
|
||||
def request(method, path, cfg, body=None, params=None):
|
||||
url = _base(cfg) + path
|
||||
if params:
|
||||
clean = {k: v for k, v in params.items() if v not in (None, "")}
|
||||
if clean:
|
||||
url += "?" + urllib.parse.urlencode(clean)
|
||||
data = json.dumps(body).encode("utf-8") if body is not None else None
|
||||
headers = {"Authorization": _auth(cfg), "Accept": "application/json"}
|
||||
if data is not None:
|
||||
headers["Content-Type"] = "application/json"
|
||||
req = urllib.request.Request(url, data=data, headers=headers, method=method)
|
||||
with urllib.request.urlopen(req, timeout=60) as r:
|
||||
raw = r.read()
|
||||
return json.loads(raw) if raw else {}
|
||||
|
||||
|
||||
def _run(fn):
|
||||
try:
|
||||
print(json.dumps(fn(_cfg(), _inputs())))
|
||||
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)
|
||||
|
||||
|
||||
def main(cfg, inputs):
|
||||
query = inputs.get("query")
|
||||
limit = inputs.get("limit")
|
||||
return request("GET", "/api/v1/threat_model_search/", cfg, params={"name": query, "limit": int(limit or 20)})
|
||||
|
||||
|
||||
_run(main)
|
||||
@@ -0,0 +1,63 @@
|
||||
import json, os, sys, urllib.parse, urllib.request, urllib.error
|
||||
|
||||
|
||||
def _cfg():
|
||||
return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||
|
||||
|
||||
def _inputs():
|
||||
return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||
|
||||
|
||||
def _base(cfg):
|
||||
return (str(cfg.get("url") or "https://api.threatstream.com")).rstrip("/")
|
||||
|
||||
|
||||
def _auth(cfg):
|
||||
return "apikey " + str(cfg.get("username", "")) + ":" + str(cfg.get("api_key", ""))
|
||||
|
||||
|
||||
def request(method, path, cfg, body=None, params=None):
|
||||
url = _base(cfg) + path
|
||||
if params:
|
||||
clean = {k: v for k, v in params.items() if v not in (None, "")}
|
||||
if clean:
|
||||
url += "?" + urllib.parse.urlencode(clean)
|
||||
data = json.dumps(body).encode("utf-8") if body is not None else None
|
||||
headers = {"Authorization": _auth(cfg), "Accept": "application/json"}
|
||||
if data is not None:
|
||||
headers["Content-Type"] = "application/json"
|
||||
req = urllib.request.Request(url, data=data, headers=headers, method=method)
|
||||
with urllib.request.urlopen(req, timeout=60) as r:
|
||||
raw = r.read()
|
||||
return json.loads(raw) if raw else {}
|
||||
|
||||
|
||||
def _run(fn):
|
||||
try:
|
||||
print(json.dumps(fn(_cfg(), _inputs())))
|
||||
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)
|
||||
|
||||
|
||||
def main(cfg, inputs):
|
||||
value = inputs.get("value")
|
||||
if not value:
|
||||
raise Exception("value is required")
|
||||
itype = inputs.get("itype")
|
||||
if not itype:
|
||||
raise Exception("itype is required")
|
||||
confidence = inputs.get("confidence")
|
||||
approve = inputs.get("approve")
|
||||
body = {"objects": [{"value": value, "itype": itype, "confidence": int(confidence or 50)}]}
|
||||
params = {}
|
||||
if approve:
|
||||
params["approve"] = "true"
|
||||
return request("POST", "/api/v2/intelligence/", cfg, body=body, params=params or None)
|
||||
|
||||
|
||||
_run(main)
|
||||
@@ -0,0 +1,54 @@
|
||||
import json, os, sys, urllib.parse, urllib.request, urllib.error
|
||||
|
||||
|
||||
def _cfg():
|
||||
return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||
|
||||
|
||||
def _inputs():
|
||||
return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||
|
||||
|
||||
def _base(cfg):
|
||||
return (str(cfg.get("url") or "https://api.threatstream.com")).rstrip("/")
|
||||
|
||||
|
||||
def _auth(cfg):
|
||||
return "apikey " + str(cfg.get("username", "")) + ":" + str(cfg.get("api_key", ""))
|
||||
|
||||
|
||||
def request(method, path, cfg, body=None, params=None):
|
||||
url = _base(cfg) + path
|
||||
if params:
|
||||
clean = {k: v for k, v in params.items() if v not in (None, "")}
|
||||
if clean:
|
||||
url += "?" + urllib.parse.urlencode(clean)
|
||||
data = json.dumps(body).encode("utf-8") if body is not None else None
|
||||
headers = {"Authorization": _auth(cfg), "Accept": "application/json"}
|
||||
if data is not None:
|
||||
headers["Content-Type"] = "application/json"
|
||||
req = urllib.request.Request(url, data=data, headers=headers, method=method)
|
||||
with urllib.request.urlopen(req, timeout=60) as r:
|
||||
raw = r.read()
|
||||
return json.loads(raw) if raw else {}
|
||||
|
||||
|
||||
def _run(fn):
|
||||
try:
|
||||
print(json.dumps(fn(_cfg(), _inputs())))
|
||||
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)
|
||||
|
||||
|
||||
def main(cfg, inputs):
|
||||
ip = inputs.get("ip")
|
||||
if not ip:
|
||||
raise Exception("ip is required")
|
||||
return request("GET", "/api/v2/intelligence/", cfg, params={"value": ip, "type": "ip", "limit": 50})
|
||||
|
||||
|
||||
_run(main)
|
||||
@@ -0,0 +1,57 @@
|
||||
import json, os, sys, urllib.parse, urllib.request, urllib.error
|
||||
|
||||
|
||||
def _cfg():
|
||||
return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||
|
||||
|
||||
def _inputs():
|
||||
return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||
|
||||
|
||||
def _base(cfg):
|
||||
return (str(cfg.get("url") or "https://api.threatstream.com")).rstrip("/")
|
||||
|
||||
|
||||
def _auth(cfg):
|
||||
return "apikey " + str(cfg.get("username", "")) + ":" + str(cfg.get("api_key", ""))
|
||||
|
||||
|
||||
def request(method, path, cfg, body=None, params=None):
|
||||
url = _base(cfg) + path
|
||||
if params:
|
||||
clean = {k: v for k, v in params.items() if v not in (None, "")}
|
||||
if clean:
|
||||
url += "?" + urllib.parse.urlencode(clean)
|
||||
data = json.dumps(body).encode("utf-8") if body is not None else None
|
||||
headers = {"Authorization": _auth(cfg), "Accept": "application/json"}
|
||||
if data is not None:
|
||||
headers["Content-Type"] = "application/json"
|
||||
req = urllib.request.Request(url, data=data, headers=headers, method=method)
|
||||
with urllib.request.urlopen(req, timeout=60) as r:
|
||||
raw = r.read()
|
||||
return json.loads(raw) if raw else {}
|
||||
|
||||
|
||||
def _run(fn):
|
||||
try:
|
||||
print(json.dumps(fn(_cfg(), _inputs())))
|
||||
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)
|
||||
|
||||
|
||||
def main(cfg, inputs):
|
||||
q = lambda v: urllib.parse.quote(str(v), safe="")
|
||||
value = inputs.get("value")
|
||||
if not value:
|
||||
raise Exception("value is required")
|
||||
itype = inputs.get("type") or "ip"
|
||||
path = "/api/v1/pdns/" + q(itype) + "/" + q(value) + "/"
|
||||
return request("GET", path, cfg)
|
||||
|
||||
|
||||
_run(main)
|
||||
@@ -0,0 +1,52 @@
|
||||
import json, os, sys, urllib.parse, urllib.request, urllib.error
|
||||
|
||||
|
||||
def _cfg():
|
||||
return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||
|
||||
|
||||
def _inputs():
|
||||
return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||
|
||||
|
||||
def _base(cfg):
|
||||
return (str(cfg.get("url") or "https://api.threatstream.com")).rstrip("/")
|
||||
|
||||
|
||||
def _auth(cfg):
|
||||
return "apikey " + str(cfg.get("username", "")) + ":" + str(cfg.get("api_key", ""))
|
||||
|
||||
|
||||
def request(method, path, cfg, body=None, params=None):
|
||||
url = _base(cfg) + path
|
||||
if params:
|
||||
clean = {k: v for k, v in params.items() if v not in (None, "")}
|
||||
if clean:
|
||||
url += "?" + urllib.parse.urlencode(clean)
|
||||
data = json.dumps(body).encode("utf-8") if body is not None else None
|
||||
headers = {"Authorization": _auth(cfg), "Accept": "application/json"}
|
||||
if data is not None:
|
||||
headers["Content-Type"] = "application/json"
|
||||
req = urllib.request.Request(url, data=data, headers=headers, method=method)
|
||||
with urllib.request.urlopen(req, timeout=60) as r:
|
||||
raw = r.read()
|
||||
return json.loads(raw) if raw else {}
|
||||
|
||||
|
||||
def _run(fn):
|
||||
try:
|
||||
print(json.dumps(fn(_cfg(), _inputs())))
|
||||
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)
|
||||
|
||||
|
||||
def main(cfg, inputs):
|
||||
request("GET", "/api/v2/intelligence/", cfg, params={"limit": 1})
|
||||
return {"ok": True}
|
||||
|
||||
|
||||
_run(main)
|
||||
@@ -0,0 +1,54 @@
|
||||
import json, os, sys, urllib.parse, urllib.request, urllib.error
|
||||
|
||||
|
||||
def _cfg():
|
||||
return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||
|
||||
|
||||
def _inputs():
|
||||
return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||
|
||||
|
||||
def _base(cfg):
|
||||
return (str(cfg.get("url") or "https://api.threatstream.com")).rstrip("/")
|
||||
|
||||
|
||||
def _auth(cfg):
|
||||
return "apikey " + str(cfg.get("username", "")) + ":" + str(cfg.get("api_key", ""))
|
||||
|
||||
|
||||
def request(method, path, cfg, body=None, params=None):
|
||||
url = _base(cfg) + path
|
||||
if params:
|
||||
clean = {k: v for k, v in params.items() if v not in (None, "")}
|
||||
if clean:
|
||||
url += "?" + urllib.parse.urlencode(clean)
|
||||
data = json.dumps(body).encode("utf-8") if body is not None else None
|
||||
headers = {"Authorization": _auth(cfg), "Accept": "application/json"}
|
||||
if data is not None:
|
||||
headers["Content-Type"] = "application/json"
|
||||
req = urllib.request.Request(url, data=data, headers=headers, method=method)
|
||||
with urllib.request.urlopen(req, timeout=60) as r:
|
||||
raw = r.read()
|
||||
return json.loads(raw) if raw else {}
|
||||
|
||||
|
||||
def _run(fn):
|
||||
try:
|
||||
print(json.dumps(fn(_cfg(), _inputs())))
|
||||
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)
|
||||
|
||||
|
||||
def main(cfg, inputs):
|
||||
url = inputs.get("url")
|
||||
if not url:
|
||||
raise Exception("url is required")
|
||||
return request("GET", "/api/v2/intelligence/", cfg, params={"value": url, "type": "url", "limit": 50})
|
||||
|
||||
|
||||
_run(main)
|
||||
@@ -0,0 +1,83 @@
|
||||
id: anyrun
|
||||
name: ANY.RUN
|
||||
version: 1.0.0
|
||||
description: "ANY.RUN (API v1) — interactive malware sandbox: detonate files and URLs on Windows/Linux, poll the analysis report and verdict, list analysis history, read user limits and delete tasks. API-key authentication; stdlib-only, no extra Python dependencies."
|
||||
changelog: "1.0.0 — Initial release: file/URL detonation, report and verdict retrieval, analysis history, user limits and task deletion."
|
||||
category: enrichment
|
||||
|
||||
# The API key is sent as 'Authorization: API-Key <key>' on every request.
|
||||
config_schema:
|
||||
properties:
|
||||
api_key:
|
||||
type: string
|
||||
description: "ANY.RUN API key"
|
||||
x-soar-sensitive: true
|
||||
required:
|
||||
- api_key
|
||||
|
||||
commands:
|
||||
- id: detonate_file
|
||||
name: anyrun-detonate-file
|
||||
description: "Detonate a file (base64) in the ANY.RUN sandbox. Returns a task_id; poll with anyrun-get-report."
|
||||
inputs_schema:
|
||||
properties:
|
||||
file_name: { type: string, description: "File name" }
|
||||
content_base64: { type: string, description: "File content, base64-encoded" }
|
||||
os: { type: string, description: "Sandbox OS: windows or linux (default windows)" }
|
||||
env_bitness: { type: number, description: "Windows bitness: 32 or 64 (default 64)" }
|
||||
required: [file_name, content_base64]
|
||||
outputs_schema: { properties: {} }
|
||||
- id: detonate_url
|
||||
name: anyrun-detonate-url
|
||||
description: "Detonate a URL in the ANY.RUN sandbox. Returns a task_id."
|
||||
inputs_schema:
|
||||
properties:
|
||||
url: { type: string, description: "URL to detonate" }
|
||||
os: { type: string, description: "Sandbox OS: windows or linux (default windows)" }
|
||||
env_bitness: { type: number, description: "Windows bitness: 32 or 64 (default 64)" }
|
||||
required: [url]
|
||||
outputs_schema: { properties: {} }
|
||||
- id: get_report
|
||||
name: anyrun-get-report
|
||||
description: "Get the full analysis report for a task (includes the verdict once the analysis completes)."
|
||||
risk: read
|
||||
inputs_schema:
|
||||
properties:
|
||||
task_id: { type: string, description: "Task ID (from a detonate command)" }
|
||||
required: [task_id]
|
||||
outputs_schema: { properties: {} }
|
||||
- id: get_history
|
||||
name: anyrun-get-history
|
||||
description: "List the analysis history for the account."
|
||||
risk: read
|
||||
inputs_schema:
|
||||
properties:
|
||||
limit: { type: number, description: "Maximum records (default 25)" }
|
||||
skip: { type: number, description: "Records to skip (pagination)" }
|
||||
required: []
|
||||
outputs_schema: { properties: {} }
|
||||
- id: get_user_limits
|
||||
name: anyrun-get-user-limits
|
||||
description: "Read the account's API usage limits."
|
||||
risk: read
|
||||
inputs_schema:
|
||||
properties: {}
|
||||
required: []
|
||||
outputs_schema: { properties: {} }
|
||||
- id: delete_task
|
||||
name: anyrun-delete-task
|
||||
description: "Delete an analysis task by ID."
|
||||
inputs_schema:
|
||||
properties:
|
||||
task_id: { type: string, description: "Task ID" }
|
||||
required: [task_id]
|
||||
outputs_schema: { properties: {} }
|
||||
|
||||
- id: test_connection
|
||||
name: anyrun-test-connection
|
||||
description: "Verify the API key (used by the Test button)."
|
||||
risk: read
|
||||
inputs_schema:
|
||||
properties: {}
|
||||
required: []
|
||||
outputs_schema: { properties: {} }
|
||||
@@ -0,0 +1,55 @@
|
||||
import base64, json, os, sys, urllib.parse, urllib.request, urllib.error
|
||||
|
||||
API = "https://api.any.run/v1"
|
||||
|
||||
|
||||
def _cfg():
|
||||
return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||
|
||||
|
||||
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 = API + path
|
||||
p = {k: str(x) for k, x in (params or {}).items() if x not in (None, "")}
|
||||
if p:
|
||||
url += ("&" if "?" in url else "?") + urllib.parse.urlencode(p)
|
||||
data = None
|
||||
headers = _headers()
|
||||
if form is not None:
|
||||
data = urllib.parse.urlencode({k: str(v) for k, v in form.items() if v not in (None, "")}).encode("utf-8")
|
||||
headers["Content-Type"] = "application/x-www-form-urlencoded"
|
||||
req = urllib.request.Request(url, data=data, headers=headers, method=method)
|
||||
with urllib.request.urlopen(req, timeout=90) as r:
|
||||
raw = r.read()
|
||||
return json.loads(raw) if raw else {}
|
||||
|
||||
|
||||
q = lambda v: urllib.parse.quote(str(v), safe="")
|
||||
|
||||
|
||||
def main():
|
||||
inputs = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||
task_id = inputs.get("task_id")
|
||||
if not task_id:
|
||||
raise Exception("task_id is required")
|
||||
|
||||
result = request("DELETE", "/analysis/" + q(task_id))
|
||||
if not result:
|
||||
result = {"ok": True, "task_id": task_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)
|
||||
@@ -0,0 +1,81 @@
|
||||
import base64, json, os, sys, urllib.parse, urllib.request, urllib.error
|
||||
import uuid
|
||||
|
||||
API = "https://api.any.run/v1"
|
||||
|
||||
|
||||
def _cfg():
|
||||
return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||
|
||||
|
||||
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 = API + path
|
||||
p = {k: str(x) for k, x in (params or {}).items() if x not in (None, "")}
|
||||
if p:
|
||||
url += ("&" if "?" in url else "?") + urllib.parse.urlencode(p)
|
||||
data = None
|
||||
headers = _headers()
|
||||
if form is not None:
|
||||
data = urllib.parse.urlencode({k: str(v) for k, v in form.items() if v not in (None, "")}).encode("utf-8")
|
||||
headers["Content-Type"] = "application/x-www-form-urlencoded"
|
||||
req = urllib.request.Request(url, data=data, headers=headers, method=method)
|
||||
with urllib.request.urlopen(req, timeout=90) as r:
|
||||
raw = r.read()
|
||||
return json.loads(raw) if raw else {}
|
||||
|
||||
|
||||
def multipart(fields, file_field, file_name, file_bytes):
|
||||
boundary = "----riposte" + uuid.uuid4().hex
|
||||
parts = []
|
||||
for name, value in fields.items():
|
||||
parts.append(("--" + boundary + "\r\n"
|
||||
+ 'Content-Disposition: form-data; name="' + name + '"\r\n\r\n'
|
||||
+ str(value) + "\r\n").encode("utf-8"))
|
||||
parts.append(("--" + boundary + "\r\n"
|
||||
+ 'Content-Disposition: form-data; name="' + file_field + '"; filename="' + file_name + '"\r\n'
|
||||
+ "Content-Type: application/octet-stream\r\n\r\n").encode("utf-8"))
|
||||
parts.append(file_bytes)
|
||||
parts.append(("\r\n--" + boundary + "--\r\n").encode("utf-8"))
|
||||
return boundary, b"".join(parts)
|
||||
|
||||
|
||||
def main():
|
||||
inputs = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||
file_name = inputs.get("file_name")
|
||||
if not file_name:
|
||||
raise Exception("file_name is required")
|
||||
content_base64 = inputs.get("content_base64")
|
||||
if not content_base64:
|
||||
raise Exception("content_base64 is required")
|
||||
env_os = inputs.get("os") or "windows"
|
||||
env_bitness = inputs.get("env_bitness") or 64
|
||||
|
||||
fields = {"obj_type": "file", "env_os": env_os, "env_bitness": str(env_bitness)}
|
||||
boundary, body = multipart(fields, "file", file_name, base64.b64decode(content_base64))
|
||||
req = urllib.request.Request(
|
||||
API + "/analysis",
|
||||
data=body,
|
||||
headers=_headers({"Content-Type": "multipart/form-data; boundary=" + boundary}),
|
||||
method="POST",
|
||||
)
|
||||
with urllib.request.urlopen(req, timeout=120) as r:
|
||||
raw = r.read()
|
||||
result = json.loads(raw) if raw else {}
|
||||
print(json.dumps(result))
|
||||
|
||||
|
||||
try:
|
||||
main()
|
||||
except urllib.error.HTTPError as e:
|
||||
print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")}))
|
||||
sys.exit(1)
|
||||
except Exception as e:
|
||||
print(json.dumps({"error": str(e)}))
|
||||
sys.exit(1)
|
||||
@@ -0,0 +1,52 @@
|
||||
import base64, json, os, sys, urllib.parse, urllib.request, urllib.error
|
||||
|
||||
API = "https://api.any.run/v1"
|
||||
|
||||
|
||||
def _cfg():
|
||||
return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||
|
||||
|
||||
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 = API + path
|
||||
p = {k: str(x) for k, x in (params or {}).items() if x not in (None, "")}
|
||||
if p:
|
||||
url += ("&" if "?" in url else "?") + urllib.parse.urlencode(p)
|
||||
data = None
|
||||
headers = _headers()
|
||||
if form is not None:
|
||||
data = urllib.parse.urlencode({k: str(v) for k, v in form.items() if v not in (None, "")}).encode("utf-8")
|
||||
headers["Content-Type"] = "application/x-www-form-urlencoded"
|
||||
req = urllib.request.Request(url, data=data, headers=headers, method=method)
|
||||
with urllib.request.urlopen(req, timeout=90) as r:
|
||||
raw = r.read()
|
||||
return json.loads(raw) if raw else {}
|
||||
|
||||
|
||||
def main():
|
||||
inputs = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||
url = inputs.get("url")
|
||||
if not url:
|
||||
raise Exception("url is required")
|
||||
env_os = inputs.get("os") or "windows"
|
||||
env_bitness = inputs.get("env_bitness") or 64
|
||||
|
||||
result = request("POST", "/analysis", form={"obj_type": "url", "obj_url": url, "env_os": env_os, "env_bitness": env_bitness})
|
||||
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)
|
||||
@@ -0,0 +1,49 @@
|
||||
import base64, json, os, sys, urllib.parse, urllib.request, urllib.error
|
||||
|
||||
API = "https://api.any.run/v1"
|
||||
|
||||
|
||||
def _cfg():
|
||||
return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||
|
||||
|
||||
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 = API + path
|
||||
p = {k: str(x) for k, x in (params or {}).items() if x not in (None, "")}
|
||||
if p:
|
||||
url += ("&" if "?" in url else "?") + urllib.parse.urlencode(p)
|
||||
data = None
|
||||
headers = _headers()
|
||||
if form is not None:
|
||||
data = urllib.parse.urlencode({k: str(v) for k, v in form.items() if v not in (None, "")}).encode("utf-8")
|
||||
headers["Content-Type"] = "application/x-www-form-urlencoded"
|
||||
req = urllib.request.Request(url, data=data, headers=headers, method=method)
|
||||
with urllib.request.urlopen(req, timeout=90) as r:
|
||||
raw = r.read()
|
||||
return json.loads(raw) if raw else {}
|
||||
|
||||
|
||||
def main():
|
||||
inputs = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||
limit = inputs.get("limit") or 25
|
||||
skip = inputs.get("skip")
|
||||
|
||||
result = request("GET", "/analysis", params={"limit": limit, "skip": skip})
|
||||
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)
|
||||
@@ -0,0 +1,53 @@
|
||||
import base64, json, os, sys, urllib.parse, urllib.request, urllib.error
|
||||
|
||||
API = "https://api.any.run/v1"
|
||||
|
||||
|
||||
def _cfg():
|
||||
return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||
|
||||
|
||||
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 = API + path
|
||||
p = {k: str(x) for k, x in (params or {}).items() if x not in (None, "")}
|
||||
if p:
|
||||
url += ("&" if "?" in url else "?") + urllib.parse.urlencode(p)
|
||||
data = None
|
||||
headers = _headers()
|
||||
if form is not None:
|
||||
data = urllib.parse.urlencode({k: str(v) for k, v in form.items() if v not in (None, "")}).encode("utf-8")
|
||||
headers["Content-Type"] = "application/x-www-form-urlencoded"
|
||||
req = urllib.request.Request(url, data=data, headers=headers, method=method)
|
||||
with urllib.request.urlopen(req, timeout=90) as r:
|
||||
raw = r.read()
|
||||
return json.loads(raw) if raw else {}
|
||||
|
||||
|
||||
q = lambda v: urllib.parse.quote(str(v), safe="")
|
||||
|
||||
|
||||
def main():
|
||||
inputs = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||
task_id = inputs.get("task_id")
|
||||
if not task_id:
|
||||
raise Exception("task_id is required")
|
||||
|
||||
result = request("GET", "/analysis/" + q(task_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)
|
||||
@@ -0,0 +1,45 @@
|
||||
import base64, json, os, sys, urllib.parse, urllib.request, urllib.error
|
||||
|
||||
API = "https://api.any.run/v1"
|
||||
|
||||
|
||||
def _cfg():
|
||||
return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||
|
||||
|
||||
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 = API + path
|
||||
p = {k: str(x) for k, x in (params or {}).items() if x not in (None, "")}
|
||||
if p:
|
||||
url += ("&" if "?" in url else "?") + urllib.parse.urlencode(p)
|
||||
data = None
|
||||
headers = _headers()
|
||||
if form is not None:
|
||||
data = urllib.parse.urlencode({k: str(v) for k, v in form.items() if v not in (None, "")}).encode("utf-8")
|
||||
headers["Content-Type"] = "application/x-www-form-urlencoded"
|
||||
req = urllib.request.Request(url, data=data, headers=headers, method=method)
|
||||
with urllib.request.urlopen(req, timeout=90) as r:
|
||||
raw = r.read()
|
||||
return json.loads(raw) if raw else {}
|
||||
|
||||
|
||||
def main():
|
||||
result = request("GET", "/user/limits")
|
||||
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)
|
||||
@@ -0,0 +1,47 @@
|
||||
import base64, json, os, sys, urllib.parse, urllib.request, urllib.error
|
||||
|
||||
API = "https://api.any.run/v1"
|
||||
|
||||
|
||||
def _cfg():
|
||||
return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||
|
||||
|
||||
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 = API + path
|
||||
p = {k: str(x) for k, x in (params or {}).items() if x not in (None, "")}
|
||||
if p:
|
||||
url += ("&" if "?" in url else "?") + urllib.parse.urlencode(p)
|
||||
data = None
|
||||
headers = _headers()
|
||||
if form is not None:
|
||||
data = urllib.parse.urlencode({k: str(v) for k, v in form.items() if v not in (None, "")}).encode("utf-8")
|
||||
headers["Content-Type"] = "application/x-www-form-urlencoded"
|
||||
req = urllib.request.Request(url, data=data, headers=headers, method=method)
|
||||
with urllib.request.urlopen(req, timeout=90) as r:
|
||||
raw = r.read()
|
||||
return json.loads(raw) if raw else {}
|
||||
|
||||
|
||||
def main():
|
||||
result = request("GET", "/user/limits")
|
||||
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)
|
||||
@@ -0,0 +1,70 @@
|
||||
id: armis
|
||||
name: Armis
|
||||
version: 1.0.0
|
||||
description: "Armis (API v1) — device and asset visibility: search devices and alerts with AQL, read a device, and update an alert's status. Secret-key (token exchange) authentication; stdlib-only, no extra Python dependencies."
|
||||
changelog: "1.0.0 — Initial release: search devices/alerts, get device, update alert status."
|
||||
category: asset_management
|
||||
|
||||
# Per-instance configuration. The secret key is exchanged for a short-lived
|
||||
# access token (sent as the 'Authorization' header).
|
||||
config_schema:
|
||||
properties:
|
||||
base_url:
|
||||
type: string
|
||||
description: "Armis instance URL (e.g. https://yourtenant.armis.com)"
|
||||
secret_key:
|
||||
type: string
|
||||
description: "Armis secret key"
|
||||
x-soar-sensitive: true
|
||||
required:
|
||||
- base_url
|
||||
- secret_key
|
||||
|
||||
commands:
|
||||
- id: search_devices
|
||||
name: armis-search-devices
|
||||
description: "Search devices with an AQL expression."
|
||||
risk: read
|
||||
inputs_schema:
|
||||
properties:
|
||||
aql: { type: string, description: "AQL filter appended to 'in:devices' (e.g. riskLevel:High)" }
|
||||
length: { type: number, description: "Max results (default 50)" }
|
||||
required: []
|
||||
outputs_schema: { properties: {} }
|
||||
- id: get_device
|
||||
name: armis-get-device
|
||||
description: "Get a single device by ID."
|
||||
risk: read
|
||||
inputs_schema:
|
||||
properties:
|
||||
device_id: { type: string, description: "Device ID" }
|
||||
required: [device_id]
|
||||
outputs_schema: { properties: {} }
|
||||
- id: search_alerts
|
||||
name: armis-search-alerts
|
||||
description: "Search alerts with an AQL expression."
|
||||
risk: read
|
||||
inputs_schema:
|
||||
properties:
|
||||
aql: { type: string, description: "AQL filter appended to 'in:alerts' (e.g. status:Unhandled)" }
|
||||
length: { type: number, description: "Max results (default 50)" }
|
||||
required: []
|
||||
outputs_schema: { properties: {} }
|
||||
- id: update_alert
|
||||
name: armis-update-alert
|
||||
description: "Update an alert's status."
|
||||
inputs_schema:
|
||||
properties:
|
||||
alert_id: { type: string, description: "Alert ID" }
|
||||
status: { type: string, description: "New status (UNHANDLED, SUPPRESSED, or RESOLVED)" }
|
||||
required: [alert_id, status]
|
||||
outputs_schema: { properties: {} }
|
||||
|
||||
- id: test_connection
|
||||
name: armis-test-connection
|
||||
description: "Verify the secret key via the token exchange (used by the Test button)."
|
||||
risk: read
|
||||
inputs_schema:
|
||||
properties: {}
|
||||
required: []
|
||||
outputs_schema: { properties: {} }
|
||||
@@ -0,0 +1,69 @@
|
||||
import json, os, sys, urllib.parse, urllib.request, urllib.error
|
||||
|
||||
|
||||
def _cfg():
|
||||
return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||
|
||||
|
||||
def _inputs():
|
||||
return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||
|
||||
|
||||
def _base(cfg):
|
||||
return str(cfg.get("base_url", "")).rstrip("/") + "/api/v1"
|
||||
|
||||
|
||||
def _token(cfg):
|
||||
form = urllib.parse.urlencode({"secret_key": str(cfg.get("secret_key", ""))}).encode("utf-8")
|
||||
req = urllib.request.Request(_base(cfg) + "/access_token/", data=form,
|
||||
headers={"Content-Type": "application/x-www-form-urlencoded",
|
||||
"Accept": "application/json"}, method="POST")
|
||||
with urllib.request.urlopen(req, timeout=60) as r:
|
||||
tok = json.loads(r.read())
|
||||
at = (tok.get("data") or {}).get("access_token")
|
||||
if not at:
|
||||
raise Exception("Token request failed: " + json.dumps(tok))
|
||||
return at
|
||||
|
||||
|
||||
def request(method, path, cfg, token, body=None, params=None):
|
||||
url = _base(cfg) + path
|
||||
if params:
|
||||
clean = {k: v for k, v in params.items() if v not in (None, "")}
|
||||
if clean:
|
||||
url += "?" + urllib.parse.urlencode(clean)
|
||||
data = json.dumps(body).encode("utf-8") if body is not None else None
|
||||
headers = {"Authorization": token, "Accept": "application/json"}
|
||||
if data is not None:
|
||||
headers["Content-Type"] = "application/json"
|
||||
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 _run(fn):
|
||||
try:
|
||||
cfg = _cfg()
|
||||
inputs = _inputs()
|
||||
token = _token(cfg)
|
||||
print(json.dumps(fn(cfg, token, inputs)))
|
||||
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)
|
||||
|
||||
|
||||
q = lambda v: urllib.parse.quote(str(v), safe="")
|
||||
|
||||
|
||||
def main(cfg, token, inputs):
|
||||
device_id = inputs.get("device_id")
|
||||
if not device_id:
|
||||
raise Exception("device_id is required")
|
||||
return request("GET", "/devices/" + q(device_id) + "/", cfg, token)
|
||||
|
||||
|
||||
_run(main)
|
||||
@@ -0,0 +1,66 @@
|
||||
import json, os, sys, urllib.parse, urllib.request, urllib.error
|
||||
|
||||
|
||||
def _cfg():
|
||||
return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||
|
||||
|
||||
def _inputs():
|
||||
return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||
|
||||
|
||||
def _base(cfg):
|
||||
return str(cfg.get("base_url", "")).rstrip("/") + "/api/v1"
|
||||
|
||||
|
||||
def _token(cfg):
|
||||
form = urllib.parse.urlencode({"secret_key": str(cfg.get("secret_key", ""))}).encode("utf-8")
|
||||
req = urllib.request.Request(_base(cfg) + "/access_token/", data=form,
|
||||
headers={"Content-Type": "application/x-www-form-urlencoded",
|
||||
"Accept": "application/json"}, method="POST")
|
||||
with urllib.request.urlopen(req, timeout=60) as r:
|
||||
tok = json.loads(r.read())
|
||||
at = (tok.get("data") or {}).get("access_token")
|
||||
if not at:
|
||||
raise Exception("Token request failed: " + json.dumps(tok))
|
||||
return at
|
||||
|
||||
|
||||
def request(method, path, cfg, token, body=None, params=None):
|
||||
url = _base(cfg) + path
|
||||
if params:
|
||||
clean = {k: v for k, v in params.items() if v not in (None, "")}
|
||||
if clean:
|
||||
url += "?" + urllib.parse.urlencode(clean)
|
||||
data = json.dumps(body).encode("utf-8") if body is not None else None
|
||||
headers = {"Authorization": token, "Accept": "application/json"}
|
||||
if data is not None:
|
||||
headers["Content-Type"] = "application/json"
|
||||
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 _run(fn):
|
||||
try:
|
||||
cfg = _cfg()
|
||||
inputs = _inputs()
|
||||
token = _token(cfg)
|
||||
print(json.dumps(fn(cfg, token, inputs)))
|
||||
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)
|
||||
|
||||
|
||||
def main(cfg, token, inputs):
|
||||
aql = inputs.get("aql")
|
||||
length = inputs.get("length")
|
||||
aql_str = "in:alerts" + ((" " + aql) if aql else "")
|
||||
return request("GET", "/search/", cfg, token, params={"aql": aql_str, "length": int(length or 50)})
|
||||
|
||||
|
||||
_run(main)
|
||||
@@ -0,0 +1,66 @@
|
||||
import json, os, sys, urllib.parse, urllib.request, urllib.error
|
||||
|
||||
|
||||
def _cfg():
|
||||
return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||
|
||||
|
||||
def _inputs():
|
||||
return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||
|
||||
|
||||
def _base(cfg):
|
||||
return str(cfg.get("base_url", "")).rstrip("/") + "/api/v1"
|
||||
|
||||
|
||||
def _token(cfg):
|
||||
form = urllib.parse.urlencode({"secret_key": str(cfg.get("secret_key", ""))}).encode("utf-8")
|
||||
req = urllib.request.Request(_base(cfg) + "/access_token/", data=form,
|
||||
headers={"Content-Type": "application/x-www-form-urlencoded",
|
||||
"Accept": "application/json"}, method="POST")
|
||||
with urllib.request.urlopen(req, timeout=60) as r:
|
||||
tok = json.loads(r.read())
|
||||
at = (tok.get("data") or {}).get("access_token")
|
||||
if not at:
|
||||
raise Exception("Token request failed: " + json.dumps(tok))
|
||||
return at
|
||||
|
||||
|
||||
def request(method, path, cfg, token, body=None, params=None):
|
||||
url = _base(cfg) + path
|
||||
if params:
|
||||
clean = {k: v for k, v in params.items() if v not in (None, "")}
|
||||
if clean:
|
||||
url += "?" + urllib.parse.urlencode(clean)
|
||||
data = json.dumps(body).encode("utf-8") if body is not None else None
|
||||
headers = {"Authorization": token, "Accept": "application/json"}
|
||||
if data is not None:
|
||||
headers["Content-Type"] = "application/json"
|
||||
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 _run(fn):
|
||||
try:
|
||||
cfg = _cfg()
|
||||
inputs = _inputs()
|
||||
token = _token(cfg)
|
||||
print(json.dumps(fn(cfg, token, inputs)))
|
||||
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)
|
||||
|
||||
|
||||
def main(cfg, token, inputs):
|
||||
aql = inputs.get("aql")
|
||||
length = inputs.get("length")
|
||||
aql_str = "in:devices" + ((" " + aql) if aql else "")
|
||||
return request("GET", "/search/", cfg, token, params={"aql": aql_str, "length": int(length or 50)})
|
||||
|
||||
|
||||
_run(main)
|
||||
@@ -0,0 +1,64 @@
|
||||
import json, os, sys, urllib.parse, urllib.request, urllib.error
|
||||
|
||||
|
||||
def _cfg():
|
||||
return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||
|
||||
|
||||
def _inputs():
|
||||
return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||
|
||||
|
||||
def _base(cfg):
|
||||
return str(cfg.get("base_url", "")).rstrip("/") + "/api/v1"
|
||||
|
||||
|
||||
def _token(cfg):
|
||||
form = urllib.parse.urlencode({"secret_key": str(cfg.get("secret_key", ""))}).encode("utf-8")
|
||||
req = urllib.request.Request(_base(cfg) + "/access_token/", data=form,
|
||||
headers={"Content-Type": "application/x-www-form-urlencoded",
|
||||
"Accept": "application/json"}, method="POST")
|
||||
with urllib.request.urlopen(req, timeout=60) as r:
|
||||
tok = json.loads(r.read())
|
||||
at = (tok.get("data") or {}).get("access_token")
|
||||
if not at:
|
||||
raise Exception("Token request failed: " + json.dumps(tok))
|
||||
return at
|
||||
|
||||
|
||||
def request(method, path, cfg, token, body=None, params=None):
|
||||
url = _base(cfg) + path
|
||||
if params:
|
||||
clean = {k: v for k, v in params.items() if v not in (None, "")}
|
||||
if clean:
|
||||
url += "?" + urllib.parse.urlencode(clean)
|
||||
data = json.dumps(body).encode("utf-8") if body is not None else None
|
||||
headers = {"Authorization": token, "Accept": "application/json"}
|
||||
if data is not None:
|
||||
headers["Content-Type"] = "application/json"
|
||||
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 _run(fn):
|
||||
try:
|
||||
cfg = _cfg()
|
||||
inputs = _inputs()
|
||||
token = _token(cfg)
|
||||
print(json.dumps(fn(cfg, token, inputs)))
|
||||
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)
|
||||
|
||||
|
||||
def main(cfg, token, inputs):
|
||||
request("GET", "/search/", cfg, token, params={"aql": "in:devices", "length": 1})
|
||||
return {"ok": True}
|
||||
|
||||
|
||||
_run(main)
|
||||
@@ -0,0 +1,75 @@
|
||||
import json, os, sys, urllib.parse, urllib.request, urllib.error
|
||||
|
||||
|
||||
def _cfg():
|
||||
return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||
|
||||
|
||||
def _inputs():
|
||||
return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||
|
||||
|
||||
def _base(cfg):
|
||||
return str(cfg.get("base_url", "")).rstrip("/") + "/api/v1"
|
||||
|
||||
|
||||
def _token(cfg):
|
||||
form = urllib.parse.urlencode({"secret_key": str(cfg.get("secret_key", ""))}).encode("utf-8")
|
||||
req = urllib.request.Request(_base(cfg) + "/access_token/", data=form,
|
||||
headers={"Content-Type": "application/x-www-form-urlencoded",
|
||||
"Accept": "application/json"}, method="POST")
|
||||
with urllib.request.urlopen(req, timeout=60) as r:
|
||||
tok = json.loads(r.read())
|
||||
at = (tok.get("data") or {}).get("access_token")
|
||||
if not at:
|
||||
raise Exception("Token request failed: " + json.dumps(tok))
|
||||
return at
|
||||
|
||||
|
||||
def request(method, path, cfg, token, body=None, params=None):
|
||||
url = _base(cfg) + path
|
||||
if params:
|
||||
clean = {k: v for k, v in params.items() if v not in (None, "")}
|
||||
if clean:
|
||||
url += "?" + urllib.parse.urlencode(clean)
|
||||
data = json.dumps(body).encode("utf-8") if body is not None else None
|
||||
headers = {"Authorization": token, "Accept": "application/json"}
|
||||
if data is not None:
|
||||
headers["Content-Type"] = "application/json"
|
||||
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 _run(fn):
|
||||
try:
|
||||
cfg = _cfg()
|
||||
inputs = _inputs()
|
||||
token = _token(cfg)
|
||||
print(json.dumps(fn(cfg, token, inputs)))
|
||||
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)
|
||||
|
||||
|
||||
q = lambda v: urllib.parse.quote(str(v), safe="")
|
||||
|
||||
|
||||
def main(cfg, token, inputs):
|
||||
alert_id = inputs.get("alert_id")
|
||||
status = inputs.get("status")
|
||||
if not alert_id:
|
||||
raise Exception("alert_id is required")
|
||||
if not status:
|
||||
raise Exception("status is required")
|
||||
resp = request("PATCH", "/alerts/" + q(alert_id) + "/", cfg, token, body={"status": status})
|
||||
if not resp:
|
||||
return {"ok": True, "alert_id": alert_id}
|
||||
return resp
|
||||
|
||||
|
||||
_run(main)
|
||||
@@ -0,0 +1,66 @@
|
||||
id: automox
|
||||
name: Automox
|
||||
version: 1.0.0
|
||||
description: "Automox (API) — endpoint patch and configuration management: list and read devices, list policies, and queue a command (e.g. install updates or run a policy) on a device. API-key authentication; stdlib-only, no extra Python dependencies."
|
||||
changelog: "1.0.0 — Initial release: list/get devices, list policies, run device command."
|
||||
category: endpoint
|
||||
|
||||
# Per-instance configuration. Auth header 'Authorization: Bearer <api_key>'.
|
||||
config_schema:
|
||||
properties:
|
||||
api_key:
|
||||
type: string
|
||||
description: "Automox API key"
|
||||
x-soar-sensitive: true
|
||||
org_id:
|
||||
type: string
|
||||
description: "Organization ID"
|
||||
required:
|
||||
- api_key
|
||||
- org_id
|
||||
|
||||
commands:
|
||||
- id: list_devices
|
||||
name: automox-list-devices
|
||||
description: "List devices."
|
||||
risk: read
|
||||
inputs_schema:
|
||||
properties:
|
||||
limit: { type: number, description: "Max devices (default 50)" }
|
||||
required: []
|
||||
outputs_schema: { properties: {} }
|
||||
- id: get_device
|
||||
name: automox-get-device
|
||||
description: "Get a single device by ID."
|
||||
risk: read
|
||||
inputs_schema:
|
||||
properties:
|
||||
device_id: { type: string, description: "Device (server) ID" }
|
||||
required: [device_id]
|
||||
outputs_schema: { properties: {} }
|
||||
- id: list_policies
|
||||
name: automox-list-policies
|
||||
description: "List policies."
|
||||
risk: read
|
||||
inputs_schema:
|
||||
properties: {}
|
||||
required: []
|
||||
outputs_schema: { properties: {} }
|
||||
- id: run_command
|
||||
name: automox-run-command
|
||||
description: "Queue a command on a device (e.g. InstallUpdate, Reboot)."
|
||||
inputs_schema:
|
||||
properties:
|
||||
device_id: { type: string, description: "Device (server) ID" }
|
||||
command_type: { type: string, description: "Command type (e.g. InstallUpdate, Reboot, GetOS)" }
|
||||
required: [device_id, command_type]
|
||||
outputs_schema: { properties: {} }
|
||||
|
||||
- id: test_connection
|
||||
name: automox-test-connection
|
||||
description: "Verify the API key (used by the Test button)."
|
||||
risk: read
|
||||
inputs_schema:
|
||||
properties: {}
|
||||
required: []
|
||||
outputs_schema: { properties: {} }
|
||||
@@ -0,0 +1,49 @@
|
||||
import json, os, sys, urllib.parse, urllib.request, urllib.error
|
||||
|
||||
BASE = "https://console.automox.com/api"
|
||||
|
||||
|
||||
def _cfg():
|
||||
return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||
|
||||
|
||||
def _inputs():
|
||||
return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||
|
||||
|
||||
def request(method, path, cfg, body=None, params=None):
|
||||
p = {k: v for k, v in (params or {}).items() if v not in (None, "")}
|
||||
p["o"] = str(cfg.get("org_id", ""))
|
||||
url = BASE + path + "?" + urllib.parse.urlencode(p)
|
||||
data = json.dumps(body).encode("utf-8") if body is not None else None
|
||||
headers = {"Authorization": "Bearer " + str(cfg.get("api_key", "")), "Accept": "application/json"}
|
||||
if data is not None:
|
||||
headers["Content-Type"] = "application/json"
|
||||
req = urllib.request.Request(url, data=data, headers=headers, method=method)
|
||||
with urllib.request.urlopen(req, timeout=60) as r:
|
||||
raw = r.read()
|
||||
return json.loads(raw) if raw else {}
|
||||
|
||||
|
||||
def _run(fn):
|
||||
try:
|
||||
print(json.dumps(fn(_cfg(), _inputs())))
|
||||
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)
|
||||
|
||||
|
||||
q = lambda v: urllib.parse.quote(str(v), safe="")
|
||||
|
||||
|
||||
def main(cfg, inputs):
|
||||
device_id = inputs.get("device_id")
|
||||
if not device_id:
|
||||
raise Exception("device_id is required")
|
||||
return request("GET", "/servers/" + q(device_id), cfg)
|
||||
|
||||
|
||||
_run(main)
|
||||
@@ -0,0 +1,48 @@
|
||||
import json, os, sys, urllib.parse, urllib.request, urllib.error
|
||||
|
||||
BASE = "https://console.automox.com/api"
|
||||
|
||||
|
||||
def _cfg():
|
||||
return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||
|
||||
|
||||
def _inputs():
|
||||
return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||
|
||||
|
||||
def request(method, path, cfg, body=None, params=None):
|
||||
p = {k: v for k, v in (params or {}).items() if v not in (None, "")}
|
||||
p["o"] = str(cfg.get("org_id", ""))
|
||||
url = BASE + path + "?" + urllib.parse.urlencode(p)
|
||||
data = json.dumps(body).encode("utf-8") if body is not None else None
|
||||
headers = {"Authorization": "Bearer " + str(cfg.get("api_key", "")), "Accept": "application/json"}
|
||||
if data is not None:
|
||||
headers["Content-Type"] = "application/json"
|
||||
req = urllib.request.Request(url, data=data, headers=headers, method=method)
|
||||
with urllib.request.urlopen(req, timeout=60) as r:
|
||||
raw = r.read()
|
||||
return json.loads(raw) if raw else {}
|
||||
|
||||
|
||||
def _run(fn):
|
||||
try:
|
||||
print(json.dumps(fn(_cfg(), _inputs())))
|
||||
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)
|
||||
|
||||
|
||||
q = lambda v: urllib.parse.quote(str(v), safe="")
|
||||
|
||||
|
||||
def main(cfg, inputs):
|
||||
limit = inputs.get("limit")
|
||||
limit = int(limit) if limit not in (None, "") else 50
|
||||
return request("GET", "/servers", cfg, params={"limit": limit})
|
||||
|
||||
|
||||
_run(main)
|
||||
@@ -0,0 +1,46 @@
|
||||
import json, os, sys, urllib.parse, urllib.request, urllib.error
|
||||
|
||||
BASE = "https://console.automox.com/api"
|
||||
|
||||
|
||||
def _cfg():
|
||||
return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||
|
||||
|
||||
def _inputs():
|
||||
return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||
|
||||
|
||||
def request(method, path, cfg, body=None, params=None):
|
||||
p = {k: v for k, v in (params or {}).items() if v not in (None, "")}
|
||||
p["o"] = str(cfg.get("org_id", ""))
|
||||
url = BASE + path + "?" + urllib.parse.urlencode(p)
|
||||
data = json.dumps(body).encode("utf-8") if body is not None else None
|
||||
headers = {"Authorization": "Bearer " + str(cfg.get("api_key", "")), "Accept": "application/json"}
|
||||
if data is not None:
|
||||
headers["Content-Type"] = "application/json"
|
||||
req = urllib.request.Request(url, data=data, headers=headers, method=method)
|
||||
with urllib.request.urlopen(req, timeout=60) as r:
|
||||
raw = r.read()
|
||||
return json.loads(raw) if raw else {}
|
||||
|
||||
|
||||
def _run(fn):
|
||||
try:
|
||||
print(json.dumps(fn(_cfg(), _inputs())))
|
||||
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)
|
||||
|
||||
|
||||
q = lambda v: urllib.parse.quote(str(v), safe="")
|
||||
|
||||
|
||||
def main(cfg, inputs):
|
||||
return request("GET", "/policies", cfg)
|
||||
|
||||
|
||||
_run(main)
|
||||
@@ -0,0 +1,55 @@
|
||||
import json, os, sys, urllib.parse, urllib.request, urllib.error
|
||||
|
||||
BASE = "https://console.automox.com/api"
|
||||
|
||||
|
||||
def _cfg():
|
||||
return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||
|
||||
|
||||
def _inputs():
|
||||
return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||
|
||||
|
||||
def request(method, path, cfg, body=None, params=None):
|
||||
p = {k: v for k, v in (params or {}).items() if v not in (None, "")}
|
||||
p["o"] = str(cfg.get("org_id", ""))
|
||||
url = BASE + path + "?" + urllib.parse.urlencode(p)
|
||||
data = json.dumps(body).encode("utf-8") if body is not None else None
|
||||
headers = {"Authorization": "Bearer " + str(cfg.get("api_key", "")), "Accept": "application/json"}
|
||||
if data is not None:
|
||||
headers["Content-Type"] = "application/json"
|
||||
req = urllib.request.Request(url, data=data, headers=headers, method=method)
|
||||
with urllib.request.urlopen(req, timeout=60) as r:
|
||||
raw = r.read()
|
||||
return json.loads(raw) if raw else {}
|
||||
|
||||
|
||||
def _run(fn):
|
||||
try:
|
||||
print(json.dumps(fn(_cfg(), _inputs())))
|
||||
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)
|
||||
|
||||
|
||||
q = lambda v: urllib.parse.quote(str(v), safe="")
|
||||
|
||||
|
||||
def main(cfg, inputs):
|
||||
device_id = inputs.get("device_id")
|
||||
if not device_id:
|
||||
raise Exception("device_id is required")
|
||||
command_type = inputs.get("command_type")
|
||||
if not command_type:
|
||||
raise Exception("command_type is required")
|
||||
resp = request("POST", "/servers/" + q(device_id) + "/queues", cfg, body={"command_type_name": command_type})
|
||||
if not resp:
|
||||
return {"ok": True, "device_id": device_id, "command_type": command_type}
|
||||
return resp
|
||||
|
||||
|
||||
_run(main)
|
||||
@@ -0,0 +1,47 @@
|
||||
import json, os, sys, urllib.parse, urllib.request, urllib.error
|
||||
|
||||
BASE = "https://console.automox.com/api"
|
||||
|
||||
|
||||
def _cfg():
|
||||
return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||
|
||||
|
||||
def _inputs():
|
||||
return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||
|
||||
|
||||
def request(method, path, cfg, body=None, params=None):
|
||||
p = {k: v for k, v in (params or {}).items() if v not in (None, "")}
|
||||
p["o"] = str(cfg.get("org_id", ""))
|
||||
url = BASE + path + "?" + urllib.parse.urlencode(p)
|
||||
data = json.dumps(body).encode("utf-8") if body is not None else None
|
||||
headers = {"Authorization": "Bearer " + str(cfg.get("api_key", "")), "Accept": "application/json"}
|
||||
if data is not None:
|
||||
headers["Content-Type"] = "application/json"
|
||||
req = urllib.request.Request(url, data=data, headers=headers, method=method)
|
||||
with urllib.request.urlopen(req, timeout=60) as r:
|
||||
raw = r.read()
|
||||
return json.loads(raw) if raw else {}
|
||||
|
||||
|
||||
def _run(fn):
|
||||
try:
|
||||
print(json.dumps(fn(_cfg(), _inputs())))
|
||||
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)
|
||||
|
||||
|
||||
q = lambda v: urllib.parse.quote(str(v), safe="")
|
||||
|
||||
|
||||
def main(cfg, inputs):
|
||||
request("GET", "/servers", cfg, params={"limit": 1})
|
||||
return {"ok": True}
|
||||
|
||||
|
||||
_run(main)
|
||||
@@ -0,0 +1,126 @@
|
||||
id: aws
|
||||
name: AWS
|
||||
version: 1.0.0
|
||||
description: "Amazon Web Services (EC2, IAM, STS) — cloud containment: describe instances and security groups, authorize/revoke security-group ingress rules, change an instance's security groups (isolate), stop instances, and deactivate a compromised IAM access key. AWS Signature V4 authentication; stdlib-only, no extra Python dependencies."
|
||||
changelog: "1.0.0 — Initial release: EC2 describe instances/security-groups, authorize/revoke ingress, modify instance security groups, stop instances; IAM list/update access keys; STS caller identity."
|
||||
category: cloud
|
||||
|
||||
# Per-instance configuration. Requests are signed with AWS Signature V4.
|
||||
# Use an IAM user/role access key with EC2 + IAM permissions. session_token is
|
||||
# only needed for temporary (STS) credentials.
|
||||
config_schema:
|
||||
properties:
|
||||
access_key_id:
|
||||
type: string
|
||||
description: "AWS access key ID"
|
||||
secret_access_key:
|
||||
type: string
|
||||
description: "AWS secret access key"
|
||||
x-soar-sensitive: true
|
||||
region:
|
||||
type: string
|
||||
description: "Default AWS region (e.g. eu-west-1)"
|
||||
default: "us-east-1"
|
||||
session_token:
|
||||
type: string
|
||||
description: "Optional STS session token (for temporary credentials)"
|
||||
x-soar-sensitive: true
|
||||
required:
|
||||
- access_key_id
|
||||
- secret_access_key
|
||||
|
||||
commands:
|
||||
- id: describe_instances
|
||||
name: aws-describe-instances
|
||||
description: "Describe EC2 instances (optionally a single instance by ID)."
|
||||
risk: read
|
||||
inputs_schema:
|
||||
properties:
|
||||
instance_id: { type: string, description: "Optional instance ID to fetch a single instance" }
|
||||
region: { type: string, description: "Region override" }
|
||||
required: []
|
||||
outputs_schema: { properties: {} }
|
||||
- id: describe_security_groups
|
||||
name: aws-describe-security-groups
|
||||
description: "Describe EC2 security groups (optionally a single group by ID)."
|
||||
risk: read
|
||||
inputs_schema:
|
||||
properties:
|
||||
group_id: { type: string, description: "Optional security group ID" }
|
||||
region: { type: string, description: "Region override" }
|
||||
required: []
|
||||
outputs_schema: { properties: {} }
|
||||
- id: authorize_security_group_ingress
|
||||
name: aws-authorize-security-group-ingress
|
||||
description: "Add an inbound rule to a security group."
|
||||
inputs_schema:
|
||||
properties:
|
||||
group_id: { type: string, description: "Security group ID" }
|
||||
protocol: { type: string, description: "IP protocol (tcp, udp, icmp, or -1 for all)" }
|
||||
from_port: { type: number, description: "Start port" }
|
||||
to_port: { type: number, description: "End port" }
|
||||
cidr: { type: string, description: "Source CIDR (e.g. 203.0.113.0/24)" }
|
||||
region: { type: string, description: "Region override" }
|
||||
required: [group_id, protocol, cidr]
|
||||
outputs_schema: { properties: {} }
|
||||
- id: revoke_security_group_ingress
|
||||
name: aws-revoke-security-group-ingress
|
||||
description: "Remove an inbound rule from a security group (containment)."
|
||||
inputs_schema:
|
||||
properties:
|
||||
group_id: { type: string, description: "Security group ID" }
|
||||
protocol: { type: string, description: "IP protocol (tcp, udp, icmp, or -1 for all)" }
|
||||
from_port: { type: number, description: "Start port" }
|
||||
to_port: { type: number, description: "End port" }
|
||||
cidr: { type: string, description: "Source CIDR to revoke" }
|
||||
region: { type: string, description: "Region override" }
|
||||
required: [group_id, protocol, cidr]
|
||||
outputs_schema: { properties: {} }
|
||||
- id: modify_instance_security_groups
|
||||
name: aws-modify-instance-security-groups
|
||||
description: "Replace the security groups attached to an instance (e.g. move it to an isolation group)."
|
||||
inputs_schema:
|
||||
properties:
|
||||
instance_id: { type: string, description: "Instance ID" }
|
||||
group_ids: { type: string, description: "Comma-separated security group IDs to set" }
|
||||
region: { type: string, description: "Region override" }
|
||||
required: [instance_id, group_ids]
|
||||
outputs_schema: { properties: {} }
|
||||
- id: stop_instances
|
||||
name: aws-stop-instances
|
||||
description: "Stop one or more EC2 instances."
|
||||
inputs_schema:
|
||||
properties:
|
||||
instance_ids: { type: string, description: "Comma-separated instance IDs" }
|
||||
force: { type: boolean, description: "Force stop (default false)" }
|
||||
region: { type: string, description: "Region override" }
|
||||
required: [instance_ids]
|
||||
outputs_schema: { properties: {} }
|
||||
- id: list_access_keys
|
||||
name: aws-list-access-keys
|
||||
description: "List a user's IAM access keys."
|
||||
risk: read
|
||||
inputs_schema:
|
||||
properties:
|
||||
user_name: { type: string, description: "IAM user name (omit to use the calling user)" }
|
||||
required: []
|
||||
outputs_schema: { properties: {} }
|
||||
- id: update_access_key
|
||||
name: aws-update-access-key
|
||||
description: "Activate or deactivate an IAM access key (deactivate to contain a compromised key)."
|
||||
inputs_schema:
|
||||
properties:
|
||||
access_key_id: { type: string, description: "The access key ID to update" }
|
||||
status: { type: string, description: "Active or Inactive" }
|
||||
user_name: { type: string, description: "IAM user name (omit to use the calling user)" }
|
||||
required: [access_key_id, status]
|
||||
outputs_schema: { properties: {} }
|
||||
|
||||
- id: test_connection
|
||||
name: aws-test-connection
|
||||
description: "Verify credentials via STS GetCallerIdentity (used by the Test button)."
|
||||
risk: read
|
||||
inputs_schema:
|
||||
properties: {}
|
||||
required: []
|
||||
outputs_schema: { properties: {} }
|
||||
@@ -0,0 +1,151 @@
|
||||
import json, os, sys, hmac, hashlib, datetime
|
||||
import urllib.parse, urllib.request, urllib.error
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
|
||||
def _cfg():
|
||||
return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||
|
||||
|
||||
def _inputs():
|
||||
return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||
|
||||
|
||||
def _sign_key(key, date_stamp, region, service):
|
||||
def _h(k, m):
|
||||
return hmac.new(k, m.encode("utf-8"), hashlib.sha256).digest()
|
||||
k_date = _h(("AWS4" + key).encode("utf-8"), date_stamp)
|
||||
k_region = _h(k_date, region)
|
||||
k_service = _h(k_region, service)
|
||||
return _h(k_service, "aws4_request")
|
||||
|
||||
|
||||
def _strip_ns(tag):
|
||||
return tag.split("}", 1)[1] if "}" in tag else tag
|
||||
|
||||
|
||||
def _xml_to_dict(elem):
|
||||
d = {}
|
||||
children = list(elem)
|
||||
if not children:
|
||||
return (elem.text or "").strip()
|
||||
for c in children:
|
||||
tag = _strip_ns(c.tag)
|
||||
val = _xml_to_dict(c)
|
||||
if tag in d:
|
||||
if not isinstance(d[tag], list):
|
||||
d[tag] = [d[tag]]
|
||||
d[tag].append(val)
|
||||
else:
|
||||
d[tag] = val
|
||||
return d
|
||||
|
||||
|
||||
def aws_query(service, host, region, action, version, params, cfg):
|
||||
# params: dict of extra query params for this Action
|
||||
body_params = {"Action": action, "Version": version}
|
||||
body_params.update({k: str(v) for k, v in params.items() if v is not None})
|
||||
body = urllib.parse.urlencode(sorted(body_params.items()))
|
||||
|
||||
access_key = str(cfg.get("access_key_id", ""))
|
||||
secret_key = str(cfg.get("secret_access_key", ""))
|
||||
session_token = cfg.get("session_token") or ""
|
||||
|
||||
now = datetime.datetime.utcnow()
|
||||
amz_date = now.strftime("%Y%m%dT%H%M%SZ")
|
||||
date_stamp = now.strftime("%Y%m%d")
|
||||
|
||||
method = "POST"
|
||||
canonical_uri = "/"
|
||||
canonical_querystring = ""
|
||||
payload_hash = hashlib.sha256(body.encode("utf-8")).hexdigest()
|
||||
canonical_headers = "content-type:application/x-www-form-urlencoded; charset=utf-8\n" \
|
||||
"host:" + host + "\n" \
|
||||
"x-amz-date:" + amz_date + "\n"
|
||||
signed_headers = "content-type;host;x-amz-date"
|
||||
if session_token:
|
||||
canonical_headers += "x-amz-security-token:" + session_token + "\n"
|
||||
signed_headers = "content-type;host;x-amz-date;x-amz-security-token"
|
||||
canonical_request = "\n".join([method, canonical_uri, canonical_querystring,
|
||||
canonical_headers, signed_headers, payload_hash])
|
||||
|
||||
algorithm = "AWS4-HMAC-SHA256"
|
||||
credential_scope = date_stamp + "/" + region + "/" + service + "/aws4_request"
|
||||
string_to_sign = "\n".join([algorithm, amz_date, credential_scope,
|
||||
hashlib.sha256(canonical_request.encode("utf-8")).hexdigest()])
|
||||
signing_key = _sign_key(secret_key, date_stamp, region, service)
|
||||
signature = hmac.new(signing_key, string_to_sign.encode("utf-8"), hashlib.sha256).hexdigest()
|
||||
|
||||
authorization = (algorithm + " Credential=" + access_key + "/" + credential_scope +
|
||||
", SignedHeaders=" + signed_headers + ", Signature=" + signature)
|
||||
headers = {
|
||||
"Content-Type": "application/x-www-form-urlencoded; charset=utf-8",
|
||||
"X-Amz-Date": amz_date,
|
||||
"Authorization": authorization,
|
||||
"Accept": "application/json",
|
||||
}
|
||||
if session_token:
|
||||
headers["X-Amz-Security-Token"] = session_token
|
||||
|
||||
req = urllib.request.Request("https://" + host + "/", data=body.encode("utf-8"), headers=headers, method="POST")
|
||||
with urllib.request.urlopen(req, timeout=60) as r:
|
||||
raw = r.read()
|
||||
root = ET.fromstring(raw)
|
||||
return {_strip_ns(root.tag): _xml_to_dict(root)}
|
||||
|
||||
|
||||
def _region(cfg, inputs):
|
||||
return (inputs.get("region") if inputs else None) or cfg.get("region") or "us-east-1"
|
||||
|
||||
|
||||
def ec2(action, params, cfg, inputs):
|
||||
region = _region(cfg, inputs)
|
||||
return aws_query("ec2", "ec2." + region + ".amazonaws.com", region, action, "2016-11-15", params, cfg)
|
||||
|
||||
|
||||
def iam(action, params, cfg):
|
||||
return aws_query("iam", "iam.amazonaws.com", "us-east-1", action, "2010-05-08", params, cfg)
|
||||
|
||||
|
||||
def sts(action, params, cfg):
|
||||
return aws_query("sts", "sts.amazonaws.com", "us-east-1", action, "2011-06-15", params, cfg)
|
||||
|
||||
|
||||
def _run(fn):
|
||||
try:
|
||||
print(json.dumps(fn(_cfg(), _inputs())))
|
||||
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)
|
||||
|
||||
|
||||
def main(cfg, inputs):
|
||||
group_id = inputs.get("group_id")
|
||||
if not group_id:
|
||||
raise Exception("group_id is required")
|
||||
protocol = inputs.get("protocol")
|
||||
if not protocol:
|
||||
raise Exception("protocol is required")
|
||||
cidr = inputs.get("cidr")
|
||||
if not cidr:
|
||||
raise Exception("cidr is required")
|
||||
|
||||
params = {
|
||||
"GroupId": group_id,
|
||||
"IpPermissions.1.IpProtocol": protocol,
|
||||
"IpPermissions.1.IpRanges.1.CidrIp": cidr,
|
||||
}
|
||||
from_port = inputs.get("from_port")
|
||||
if from_port is not None and str(from_port).strip() != "":
|
||||
params["IpPermissions.1.FromPort"] = int(from_port)
|
||||
to_port = inputs.get("to_port")
|
||||
if to_port is not None and str(to_port).strip() != "":
|
||||
params["IpPermissions.1.ToPort"] = int(to_port)
|
||||
|
||||
return ec2("AuthorizeSecurityGroupIngress", params, cfg, inputs)
|
||||
|
||||
|
||||
_run(main)
|
||||
@@ -0,0 +1,131 @@
|
||||
import json, os, sys, hmac, hashlib, datetime
|
||||
import urllib.parse, urllib.request, urllib.error
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
|
||||
def _cfg():
|
||||
return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||
|
||||
|
||||
def _inputs():
|
||||
return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||
|
||||
|
||||
def _sign_key(key, date_stamp, region, service):
|
||||
def _h(k, m):
|
||||
return hmac.new(k, m.encode("utf-8"), hashlib.sha256).digest()
|
||||
k_date = _h(("AWS4" + key).encode("utf-8"), date_stamp)
|
||||
k_region = _h(k_date, region)
|
||||
k_service = _h(k_region, service)
|
||||
return _h(k_service, "aws4_request")
|
||||
|
||||
|
||||
def _strip_ns(tag):
|
||||
return tag.split("}", 1)[1] if "}" in tag else tag
|
||||
|
||||
|
||||
def _xml_to_dict(elem):
|
||||
d = {}
|
||||
children = list(elem)
|
||||
if not children:
|
||||
return (elem.text or "").strip()
|
||||
for c in children:
|
||||
tag = _strip_ns(c.tag)
|
||||
val = _xml_to_dict(c)
|
||||
if tag in d:
|
||||
if not isinstance(d[tag], list):
|
||||
d[tag] = [d[tag]]
|
||||
d[tag].append(val)
|
||||
else:
|
||||
d[tag] = val
|
||||
return d
|
||||
|
||||
|
||||
def aws_query(service, host, region, action, version, params, cfg):
|
||||
# params: dict of extra query params for this Action
|
||||
body_params = {"Action": action, "Version": version}
|
||||
body_params.update({k: str(v) for k, v in params.items() if v is not None})
|
||||
body = urllib.parse.urlencode(sorted(body_params.items()))
|
||||
|
||||
access_key = str(cfg.get("access_key_id", ""))
|
||||
secret_key = str(cfg.get("secret_access_key", ""))
|
||||
session_token = cfg.get("session_token") or ""
|
||||
|
||||
now = datetime.datetime.utcnow()
|
||||
amz_date = now.strftime("%Y%m%dT%H%M%SZ")
|
||||
date_stamp = now.strftime("%Y%m%d")
|
||||
|
||||
method = "POST"
|
||||
canonical_uri = "/"
|
||||
canonical_querystring = ""
|
||||
payload_hash = hashlib.sha256(body.encode("utf-8")).hexdigest()
|
||||
canonical_headers = "content-type:application/x-www-form-urlencoded; charset=utf-8\n" \
|
||||
"host:" + host + "\n" \
|
||||
"x-amz-date:" + amz_date + "\n"
|
||||
signed_headers = "content-type;host;x-amz-date"
|
||||
if session_token:
|
||||
canonical_headers += "x-amz-security-token:" + session_token + "\n"
|
||||
signed_headers = "content-type;host;x-amz-date;x-amz-security-token"
|
||||
canonical_request = "\n".join([method, canonical_uri, canonical_querystring,
|
||||
canonical_headers, signed_headers, payload_hash])
|
||||
|
||||
algorithm = "AWS4-HMAC-SHA256"
|
||||
credential_scope = date_stamp + "/" + region + "/" + service + "/aws4_request"
|
||||
string_to_sign = "\n".join([algorithm, amz_date, credential_scope,
|
||||
hashlib.sha256(canonical_request.encode("utf-8")).hexdigest()])
|
||||
signing_key = _sign_key(secret_key, date_stamp, region, service)
|
||||
signature = hmac.new(signing_key, string_to_sign.encode("utf-8"), hashlib.sha256).hexdigest()
|
||||
|
||||
authorization = (algorithm + " Credential=" + access_key + "/" + credential_scope +
|
||||
", SignedHeaders=" + signed_headers + ", Signature=" + signature)
|
||||
headers = {
|
||||
"Content-Type": "application/x-www-form-urlencoded; charset=utf-8",
|
||||
"X-Amz-Date": amz_date,
|
||||
"Authorization": authorization,
|
||||
"Accept": "application/json",
|
||||
}
|
||||
if session_token:
|
||||
headers["X-Amz-Security-Token"] = session_token
|
||||
|
||||
req = urllib.request.Request("https://" + host + "/", data=body.encode("utf-8"), headers=headers, method="POST")
|
||||
with urllib.request.urlopen(req, timeout=60) as r:
|
||||
raw = r.read()
|
||||
root = ET.fromstring(raw)
|
||||
return {_strip_ns(root.tag): _xml_to_dict(root)}
|
||||
|
||||
|
||||
def _region(cfg, inputs):
|
||||
return (inputs.get("region") if inputs else None) or cfg.get("region") or "us-east-1"
|
||||
|
||||
|
||||
def ec2(action, params, cfg, inputs):
|
||||
region = _region(cfg, inputs)
|
||||
return aws_query("ec2", "ec2." + region + ".amazonaws.com", region, action, "2016-11-15", params, cfg)
|
||||
|
||||
|
||||
def iam(action, params, cfg):
|
||||
return aws_query("iam", "iam.amazonaws.com", "us-east-1", action, "2010-05-08", params, cfg)
|
||||
|
||||
|
||||
def sts(action, params, cfg):
|
||||
return aws_query("sts", "sts.amazonaws.com", "us-east-1", action, "2011-06-15", params, cfg)
|
||||
|
||||
|
||||
def _run(fn):
|
||||
try:
|
||||
print(json.dumps(fn(_cfg(), _inputs())))
|
||||
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)
|
||||
|
||||
|
||||
def main(cfg, inputs):
|
||||
instance_id = inputs.get("instance_id")
|
||||
params = {"InstanceId.1": instance_id} if instance_id else {}
|
||||
return ec2("DescribeInstances", params, cfg, inputs)
|
||||
|
||||
|
||||
_run(main)
|
||||
@@ -0,0 +1,131 @@
|
||||
import json, os, sys, hmac, hashlib, datetime
|
||||
import urllib.parse, urllib.request, urllib.error
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
|
||||
def _cfg():
|
||||
return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||
|
||||
|
||||
def _inputs():
|
||||
return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||
|
||||
|
||||
def _sign_key(key, date_stamp, region, service):
|
||||
def _h(k, m):
|
||||
return hmac.new(k, m.encode("utf-8"), hashlib.sha256).digest()
|
||||
k_date = _h(("AWS4" + key).encode("utf-8"), date_stamp)
|
||||
k_region = _h(k_date, region)
|
||||
k_service = _h(k_region, service)
|
||||
return _h(k_service, "aws4_request")
|
||||
|
||||
|
||||
def _strip_ns(tag):
|
||||
return tag.split("}", 1)[1] if "}" in tag else tag
|
||||
|
||||
|
||||
def _xml_to_dict(elem):
|
||||
d = {}
|
||||
children = list(elem)
|
||||
if not children:
|
||||
return (elem.text or "").strip()
|
||||
for c in children:
|
||||
tag = _strip_ns(c.tag)
|
||||
val = _xml_to_dict(c)
|
||||
if tag in d:
|
||||
if not isinstance(d[tag], list):
|
||||
d[tag] = [d[tag]]
|
||||
d[tag].append(val)
|
||||
else:
|
||||
d[tag] = val
|
||||
return d
|
||||
|
||||
|
||||
def aws_query(service, host, region, action, version, params, cfg):
|
||||
# params: dict of extra query params for this Action
|
||||
body_params = {"Action": action, "Version": version}
|
||||
body_params.update({k: str(v) for k, v in params.items() if v is not None})
|
||||
body = urllib.parse.urlencode(sorted(body_params.items()))
|
||||
|
||||
access_key = str(cfg.get("access_key_id", ""))
|
||||
secret_key = str(cfg.get("secret_access_key", ""))
|
||||
session_token = cfg.get("session_token") or ""
|
||||
|
||||
now = datetime.datetime.utcnow()
|
||||
amz_date = now.strftime("%Y%m%dT%H%M%SZ")
|
||||
date_stamp = now.strftime("%Y%m%d")
|
||||
|
||||
method = "POST"
|
||||
canonical_uri = "/"
|
||||
canonical_querystring = ""
|
||||
payload_hash = hashlib.sha256(body.encode("utf-8")).hexdigest()
|
||||
canonical_headers = "content-type:application/x-www-form-urlencoded; charset=utf-8\n" \
|
||||
"host:" + host + "\n" \
|
||||
"x-amz-date:" + amz_date + "\n"
|
||||
signed_headers = "content-type;host;x-amz-date"
|
||||
if session_token:
|
||||
canonical_headers += "x-amz-security-token:" + session_token + "\n"
|
||||
signed_headers = "content-type;host;x-amz-date;x-amz-security-token"
|
||||
canonical_request = "\n".join([method, canonical_uri, canonical_querystring,
|
||||
canonical_headers, signed_headers, payload_hash])
|
||||
|
||||
algorithm = "AWS4-HMAC-SHA256"
|
||||
credential_scope = date_stamp + "/" + region + "/" + service + "/aws4_request"
|
||||
string_to_sign = "\n".join([algorithm, amz_date, credential_scope,
|
||||
hashlib.sha256(canonical_request.encode("utf-8")).hexdigest()])
|
||||
signing_key = _sign_key(secret_key, date_stamp, region, service)
|
||||
signature = hmac.new(signing_key, string_to_sign.encode("utf-8"), hashlib.sha256).hexdigest()
|
||||
|
||||
authorization = (algorithm + " Credential=" + access_key + "/" + credential_scope +
|
||||
", SignedHeaders=" + signed_headers + ", Signature=" + signature)
|
||||
headers = {
|
||||
"Content-Type": "application/x-www-form-urlencoded; charset=utf-8",
|
||||
"X-Amz-Date": amz_date,
|
||||
"Authorization": authorization,
|
||||
"Accept": "application/json",
|
||||
}
|
||||
if session_token:
|
||||
headers["X-Amz-Security-Token"] = session_token
|
||||
|
||||
req = urllib.request.Request("https://" + host + "/", data=body.encode("utf-8"), headers=headers, method="POST")
|
||||
with urllib.request.urlopen(req, timeout=60) as r:
|
||||
raw = r.read()
|
||||
root = ET.fromstring(raw)
|
||||
return {_strip_ns(root.tag): _xml_to_dict(root)}
|
||||
|
||||
|
||||
def _region(cfg, inputs):
|
||||
return (inputs.get("region") if inputs else None) or cfg.get("region") or "us-east-1"
|
||||
|
||||
|
||||
def ec2(action, params, cfg, inputs):
|
||||
region = _region(cfg, inputs)
|
||||
return aws_query("ec2", "ec2." + region + ".amazonaws.com", region, action, "2016-11-15", params, cfg)
|
||||
|
||||
|
||||
def iam(action, params, cfg):
|
||||
return aws_query("iam", "iam.amazonaws.com", "us-east-1", action, "2010-05-08", params, cfg)
|
||||
|
||||
|
||||
def sts(action, params, cfg):
|
||||
return aws_query("sts", "sts.amazonaws.com", "us-east-1", action, "2011-06-15", params, cfg)
|
||||
|
||||
|
||||
def _run(fn):
|
||||
try:
|
||||
print(json.dumps(fn(_cfg(), _inputs())))
|
||||
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)
|
||||
|
||||
|
||||
def main(cfg, inputs):
|
||||
group_id = inputs.get("group_id")
|
||||
params = {"GroupId.1": group_id} if group_id else {}
|
||||
return ec2("DescribeSecurityGroups", params, cfg, inputs)
|
||||
|
||||
|
||||
_run(main)
|
||||
@@ -0,0 +1,131 @@
|
||||
import json, os, sys, hmac, hashlib, datetime
|
||||
import urllib.parse, urllib.request, urllib.error
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
|
||||
def _cfg():
|
||||
return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||
|
||||
|
||||
def _inputs():
|
||||
return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||
|
||||
|
||||
def _sign_key(key, date_stamp, region, service):
|
||||
def _h(k, m):
|
||||
return hmac.new(k, m.encode("utf-8"), hashlib.sha256).digest()
|
||||
k_date = _h(("AWS4" + key).encode("utf-8"), date_stamp)
|
||||
k_region = _h(k_date, region)
|
||||
k_service = _h(k_region, service)
|
||||
return _h(k_service, "aws4_request")
|
||||
|
||||
|
||||
def _strip_ns(tag):
|
||||
return tag.split("}", 1)[1] if "}" in tag else tag
|
||||
|
||||
|
||||
def _xml_to_dict(elem):
|
||||
d = {}
|
||||
children = list(elem)
|
||||
if not children:
|
||||
return (elem.text or "").strip()
|
||||
for c in children:
|
||||
tag = _strip_ns(c.tag)
|
||||
val = _xml_to_dict(c)
|
||||
if tag in d:
|
||||
if not isinstance(d[tag], list):
|
||||
d[tag] = [d[tag]]
|
||||
d[tag].append(val)
|
||||
else:
|
||||
d[tag] = val
|
||||
return d
|
||||
|
||||
|
||||
def aws_query(service, host, region, action, version, params, cfg):
|
||||
# params: dict of extra query params for this Action
|
||||
body_params = {"Action": action, "Version": version}
|
||||
body_params.update({k: str(v) for k, v in params.items() if v is not None})
|
||||
body = urllib.parse.urlencode(sorted(body_params.items()))
|
||||
|
||||
access_key = str(cfg.get("access_key_id", ""))
|
||||
secret_key = str(cfg.get("secret_access_key", ""))
|
||||
session_token = cfg.get("session_token") or ""
|
||||
|
||||
now = datetime.datetime.utcnow()
|
||||
amz_date = now.strftime("%Y%m%dT%H%M%SZ")
|
||||
date_stamp = now.strftime("%Y%m%d")
|
||||
|
||||
method = "POST"
|
||||
canonical_uri = "/"
|
||||
canonical_querystring = ""
|
||||
payload_hash = hashlib.sha256(body.encode("utf-8")).hexdigest()
|
||||
canonical_headers = "content-type:application/x-www-form-urlencoded; charset=utf-8\n" \
|
||||
"host:" + host + "\n" \
|
||||
"x-amz-date:" + amz_date + "\n"
|
||||
signed_headers = "content-type;host;x-amz-date"
|
||||
if session_token:
|
||||
canonical_headers += "x-amz-security-token:" + session_token + "\n"
|
||||
signed_headers = "content-type;host;x-amz-date;x-amz-security-token"
|
||||
canonical_request = "\n".join([method, canonical_uri, canonical_querystring,
|
||||
canonical_headers, signed_headers, payload_hash])
|
||||
|
||||
algorithm = "AWS4-HMAC-SHA256"
|
||||
credential_scope = date_stamp + "/" + region + "/" + service + "/aws4_request"
|
||||
string_to_sign = "\n".join([algorithm, amz_date, credential_scope,
|
||||
hashlib.sha256(canonical_request.encode("utf-8")).hexdigest()])
|
||||
signing_key = _sign_key(secret_key, date_stamp, region, service)
|
||||
signature = hmac.new(signing_key, string_to_sign.encode("utf-8"), hashlib.sha256).hexdigest()
|
||||
|
||||
authorization = (algorithm + " Credential=" + access_key + "/" + credential_scope +
|
||||
", SignedHeaders=" + signed_headers + ", Signature=" + signature)
|
||||
headers = {
|
||||
"Content-Type": "application/x-www-form-urlencoded; charset=utf-8",
|
||||
"X-Amz-Date": amz_date,
|
||||
"Authorization": authorization,
|
||||
"Accept": "application/json",
|
||||
}
|
||||
if session_token:
|
||||
headers["X-Amz-Security-Token"] = session_token
|
||||
|
||||
req = urllib.request.Request("https://" + host + "/", data=body.encode("utf-8"), headers=headers, method="POST")
|
||||
with urllib.request.urlopen(req, timeout=60) as r:
|
||||
raw = r.read()
|
||||
root = ET.fromstring(raw)
|
||||
return {_strip_ns(root.tag): _xml_to_dict(root)}
|
||||
|
||||
|
||||
def _region(cfg, inputs):
|
||||
return (inputs.get("region") if inputs else None) or cfg.get("region") or "us-east-1"
|
||||
|
||||
|
||||
def ec2(action, params, cfg, inputs):
|
||||
region = _region(cfg, inputs)
|
||||
return aws_query("ec2", "ec2." + region + ".amazonaws.com", region, action, "2016-11-15", params, cfg)
|
||||
|
||||
|
||||
def iam(action, params, cfg):
|
||||
return aws_query("iam", "iam.amazonaws.com", "us-east-1", action, "2010-05-08", params, cfg)
|
||||
|
||||
|
||||
def sts(action, params, cfg):
|
||||
return aws_query("sts", "sts.amazonaws.com", "us-east-1", action, "2011-06-15", params, cfg)
|
||||
|
||||
|
||||
def _run(fn):
|
||||
try:
|
||||
print(json.dumps(fn(_cfg(), _inputs())))
|
||||
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)
|
||||
|
||||
|
||||
def main(cfg, inputs):
|
||||
user_name = inputs.get("user_name")
|
||||
params = {"UserName": user_name} if user_name else {}
|
||||
return iam("ListAccessKeys", params, cfg)
|
||||
|
||||
|
||||
_run(main)
|
||||
@@ -0,0 +1,143 @@
|
||||
import json, os, sys, hmac, hashlib, datetime
|
||||
import urllib.parse, urllib.request, urllib.error
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
|
||||
def _cfg():
|
||||
return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||
|
||||
|
||||
def _inputs():
|
||||
return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||
|
||||
|
||||
def _sign_key(key, date_stamp, region, service):
|
||||
def _h(k, m):
|
||||
return hmac.new(k, m.encode("utf-8"), hashlib.sha256).digest()
|
||||
k_date = _h(("AWS4" + key).encode("utf-8"), date_stamp)
|
||||
k_region = _h(k_date, region)
|
||||
k_service = _h(k_region, service)
|
||||
return _h(k_service, "aws4_request")
|
||||
|
||||
|
||||
def _strip_ns(tag):
|
||||
return tag.split("}", 1)[1] if "}" in tag else tag
|
||||
|
||||
|
||||
def _xml_to_dict(elem):
|
||||
d = {}
|
||||
children = list(elem)
|
||||
if not children:
|
||||
return (elem.text or "").strip()
|
||||
for c in children:
|
||||
tag = _strip_ns(c.tag)
|
||||
val = _xml_to_dict(c)
|
||||
if tag in d:
|
||||
if not isinstance(d[tag], list):
|
||||
d[tag] = [d[tag]]
|
||||
d[tag].append(val)
|
||||
else:
|
||||
d[tag] = val
|
||||
return d
|
||||
|
||||
|
||||
def aws_query(service, host, region, action, version, params, cfg):
|
||||
# params: dict of extra query params for this Action
|
||||
body_params = {"Action": action, "Version": version}
|
||||
body_params.update({k: str(v) for k, v in params.items() if v is not None})
|
||||
body = urllib.parse.urlencode(sorted(body_params.items()))
|
||||
|
||||
access_key = str(cfg.get("access_key_id", ""))
|
||||
secret_key = str(cfg.get("secret_access_key", ""))
|
||||
session_token = cfg.get("session_token") or ""
|
||||
|
||||
now = datetime.datetime.utcnow()
|
||||
amz_date = now.strftime("%Y%m%dT%H%M%SZ")
|
||||
date_stamp = now.strftime("%Y%m%d")
|
||||
|
||||
method = "POST"
|
||||
canonical_uri = "/"
|
||||
canonical_querystring = ""
|
||||
payload_hash = hashlib.sha256(body.encode("utf-8")).hexdigest()
|
||||
canonical_headers = "content-type:application/x-www-form-urlencoded; charset=utf-8\n" \
|
||||
"host:" + host + "\n" \
|
||||
"x-amz-date:" + amz_date + "\n"
|
||||
signed_headers = "content-type;host;x-amz-date"
|
||||
if session_token:
|
||||
canonical_headers += "x-amz-security-token:" + session_token + "\n"
|
||||
signed_headers = "content-type;host;x-amz-date;x-amz-security-token"
|
||||
canonical_request = "\n".join([method, canonical_uri, canonical_querystring,
|
||||
canonical_headers, signed_headers, payload_hash])
|
||||
|
||||
algorithm = "AWS4-HMAC-SHA256"
|
||||
credential_scope = date_stamp + "/" + region + "/" + service + "/aws4_request"
|
||||
string_to_sign = "\n".join([algorithm, amz_date, credential_scope,
|
||||
hashlib.sha256(canonical_request.encode("utf-8")).hexdigest()])
|
||||
signing_key = _sign_key(secret_key, date_stamp, region, service)
|
||||
signature = hmac.new(signing_key, string_to_sign.encode("utf-8"), hashlib.sha256).hexdigest()
|
||||
|
||||
authorization = (algorithm + " Credential=" + access_key + "/" + credential_scope +
|
||||
", SignedHeaders=" + signed_headers + ", Signature=" + signature)
|
||||
headers = {
|
||||
"Content-Type": "application/x-www-form-urlencoded; charset=utf-8",
|
||||
"X-Amz-Date": amz_date,
|
||||
"Authorization": authorization,
|
||||
"Accept": "application/json",
|
||||
}
|
||||
if session_token:
|
||||
headers["X-Amz-Security-Token"] = session_token
|
||||
|
||||
req = urllib.request.Request("https://" + host + "/", data=body.encode("utf-8"), headers=headers, method="POST")
|
||||
with urllib.request.urlopen(req, timeout=60) as r:
|
||||
raw = r.read()
|
||||
root = ET.fromstring(raw)
|
||||
return {_strip_ns(root.tag): _xml_to_dict(root)}
|
||||
|
||||
|
||||
def _region(cfg, inputs):
|
||||
return (inputs.get("region") if inputs else None) or cfg.get("region") or "us-east-1"
|
||||
|
||||
|
||||
def ec2(action, params, cfg, inputs):
|
||||
region = _region(cfg, inputs)
|
||||
return aws_query("ec2", "ec2." + region + ".amazonaws.com", region, action, "2016-11-15", params, cfg)
|
||||
|
||||
|
||||
def iam(action, params, cfg):
|
||||
return aws_query("iam", "iam.amazonaws.com", "us-east-1", action, "2010-05-08", params, cfg)
|
||||
|
||||
|
||||
def sts(action, params, cfg):
|
||||
return aws_query("sts", "sts.amazonaws.com", "us-east-1", action, "2011-06-15", params, cfg)
|
||||
|
||||
|
||||
def _run(fn):
|
||||
try:
|
||||
print(json.dumps(fn(_cfg(), _inputs())))
|
||||
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)
|
||||
|
||||
|
||||
def main(cfg, inputs):
|
||||
instance_id = inputs.get("instance_id")
|
||||
if not instance_id:
|
||||
raise Exception("instance_id is required")
|
||||
group_ids_raw = inputs.get("group_ids")
|
||||
if not group_ids_raw or not str(group_ids_raw).strip():
|
||||
raise Exception("group_ids is required")
|
||||
group_ids = [s.strip() for s in str(group_ids_raw).split(",") if s.strip()]
|
||||
if not group_ids:
|
||||
raise Exception("group_ids is required")
|
||||
|
||||
params = {"InstanceId": instance_id}
|
||||
for i, gid in enumerate(group_ids, start=1):
|
||||
params["GroupId.%d" % i] = gid
|
||||
|
||||
return ec2("ModifyInstanceAttribute", params, cfg, inputs)
|
||||
|
||||
|
||||
_run(main)
|
||||
@@ -0,0 +1,151 @@
|
||||
import json, os, sys, hmac, hashlib, datetime
|
||||
import urllib.parse, urllib.request, urllib.error
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
|
||||
def _cfg():
|
||||
return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||
|
||||
|
||||
def _inputs():
|
||||
return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||
|
||||
|
||||
def _sign_key(key, date_stamp, region, service):
|
||||
def _h(k, m):
|
||||
return hmac.new(k, m.encode("utf-8"), hashlib.sha256).digest()
|
||||
k_date = _h(("AWS4" + key).encode("utf-8"), date_stamp)
|
||||
k_region = _h(k_date, region)
|
||||
k_service = _h(k_region, service)
|
||||
return _h(k_service, "aws4_request")
|
||||
|
||||
|
||||
def _strip_ns(tag):
|
||||
return tag.split("}", 1)[1] if "}" in tag else tag
|
||||
|
||||
|
||||
def _xml_to_dict(elem):
|
||||
d = {}
|
||||
children = list(elem)
|
||||
if not children:
|
||||
return (elem.text or "").strip()
|
||||
for c in children:
|
||||
tag = _strip_ns(c.tag)
|
||||
val = _xml_to_dict(c)
|
||||
if tag in d:
|
||||
if not isinstance(d[tag], list):
|
||||
d[tag] = [d[tag]]
|
||||
d[tag].append(val)
|
||||
else:
|
||||
d[tag] = val
|
||||
return d
|
||||
|
||||
|
||||
def aws_query(service, host, region, action, version, params, cfg):
|
||||
# params: dict of extra query params for this Action
|
||||
body_params = {"Action": action, "Version": version}
|
||||
body_params.update({k: str(v) for k, v in params.items() if v is not None})
|
||||
body = urllib.parse.urlencode(sorted(body_params.items()))
|
||||
|
||||
access_key = str(cfg.get("access_key_id", ""))
|
||||
secret_key = str(cfg.get("secret_access_key", ""))
|
||||
session_token = cfg.get("session_token") or ""
|
||||
|
||||
now = datetime.datetime.utcnow()
|
||||
amz_date = now.strftime("%Y%m%dT%H%M%SZ")
|
||||
date_stamp = now.strftime("%Y%m%d")
|
||||
|
||||
method = "POST"
|
||||
canonical_uri = "/"
|
||||
canonical_querystring = ""
|
||||
payload_hash = hashlib.sha256(body.encode("utf-8")).hexdigest()
|
||||
canonical_headers = "content-type:application/x-www-form-urlencoded; charset=utf-8\n" \
|
||||
"host:" + host + "\n" \
|
||||
"x-amz-date:" + amz_date + "\n"
|
||||
signed_headers = "content-type;host;x-amz-date"
|
||||
if session_token:
|
||||
canonical_headers += "x-amz-security-token:" + session_token + "\n"
|
||||
signed_headers = "content-type;host;x-amz-date;x-amz-security-token"
|
||||
canonical_request = "\n".join([method, canonical_uri, canonical_querystring,
|
||||
canonical_headers, signed_headers, payload_hash])
|
||||
|
||||
algorithm = "AWS4-HMAC-SHA256"
|
||||
credential_scope = date_stamp + "/" + region + "/" + service + "/aws4_request"
|
||||
string_to_sign = "\n".join([algorithm, amz_date, credential_scope,
|
||||
hashlib.sha256(canonical_request.encode("utf-8")).hexdigest()])
|
||||
signing_key = _sign_key(secret_key, date_stamp, region, service)
|
||||
signature = hmac.new(signing_key, string_to_sign.encode("utf-8"), hashlib.sha256).hexdigest()
|
||||
|
||||
authorization = (algorithm + " Credential=" + access_key + "/" + credential_scope +
|
||||
", SignedHeaders=" + signed_headers + ", Signature=" + signature)
|
||||
headers = {
|
||||
"Content-Type": "application/x-www-form-urlencoded; charset=utf-8",
|
||||
"X-Amz-Date": amz_date,
|
||||
"Authorization": authorization,
|
||||
"Accept": "application/json",
|
||||
}
|
||||
if session_token:
|
||||
headers["X-Amz-Security-Token"] = session_token
|
||||
|
||||
req = urllib.request.Request("https://" + host + "/", data=body.encode("utf-8"), headers=headers, method="POST")
|
||||
with urllib.request.urlopen(req, timeout=60) as r:
|
||||
raw = r.read()
|
||||
root = ET.fromstring(raw)
|
||||
return {_strip_ns(root.tag): _xml_to_dict(root)}
|
||||
|
||||
|
||||
def _region(cfg, inputs):
|
||||
return (inputs.get("region") if inputs else None) or cfg.get("region") or "us-east-1"
|
||||
|
||||
|
||||
def ec2(action, params, cfg, inputs):
|
||||
region = _region(cfg, inputs)
|
||||
return aws_query("ec2", "ec2." + region + ".amazonaws.com", region, action, "2016-11-15", params, cfg)
|
||||
|
||||
|
||||
def iam(action, params, cfg):
|
||||
return aws_query("iam", "iam.amazonaws.com", "us-east-1", action, "2010-05-08", params, cfg)
|
||||
|
||||
|
||||
def sts(action, params, cfg):
|
||||
return aws_query("sts", "sts.amazonaws.com", "us-east-1", action, "2011-06-15", params, cfg)
|
||||
|
||||
|
||||
def _run(fn):
|
||||
try:
|
||||
print(json.dumps(fn(_cfg(), _inputs())))
|
||||
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)
|
||||
|
||||
|
||||
def main(cfg, inputs):
|
||||
group_id = inputs.get("group_id")
|
||||
if not group_id:
|
||||
raise Exception("group_id is required")
|
||||
protocol = inputs.get("protocol")
|
||||
if not protocol:
|
||||
raise Exception("protocol is required")
|
||||
cidr = inputs.get("cidr")
|
||||
if not cidr:
|
||||
raise Exception("cidr is required")
|
||||
|
||||
params = {
|
||||
"GroupId": group_id,
|
||||
"IpPermissions.1.IpProtocol": protocol,
|
||||
"IpPermissions.1.IpRanges.1.CidrIp": cidr,
|
||||
}
|
||||
from_port = inputs.get("from_port")
|
||||
if from_port is not None and str(from_port).strip() != "":
|
||||
params["IpPermissions.1.FromPort"] = int(from_port)
|
||||
to_port = inputs.get("to_port")
|
||||
if to_port is not None and str(to_port).strip() != "":
|
||||
params["IpPermissions.1.ToPort"] = int(to_port)
|
||||
|
||||
return ec2("RevokeSecurityGroupIngress", params, cfg, inputs)
|
||||
|
||||
|
||||
_run(main)
|
||||
@@ -0,0 +1,148 @@
|
||||
import json, os, sys, hmac, hashlib, datetime
|
||||
import urllib.parse, urllib.request, urllib.error
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
|
||||
def _cfg():
|
||||
return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||
|
||||
|
||||
def _inputs():
|
||||
return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||
|
||||
|
||||
def _sign_key(key, date_stamp, region, service):
|
||||
def _h(k, m):
|
||||
return hmac.new(k, m.encode("utf-8"), hashlib.sha256).digest()
|
||||
k_date = _h(("AWS4" + key).encode("utf-8"), date_stamp)
|
||||
k_region = _h(k_date, region)
|
||||
k_service = _h(k_region, service)
|
||||
return _h(k_service, "aws4_request")
|
||||
|
||||
|
||||
def _strip_ns(tag):
|
||||
return tag.split("}", 1)[1] if "}" in tag else tag
|
||||
|
||||
|
||||
def _xml_to_dict(elem):
|
||||
d = {}
|
||||
children = list(elem)
|
||||
if not children:
|
||||
return (elem.text or "").strip()
|
||||
for c in children:
|
||||
tag = _strip_ns(c.tag)
|
||||
val = _xml_to_dict(c)
|
||||
if tag in d:
|
||||
if not isinstance(d[tag], list):
|
||||
d[tag] = [d[tag]]
|
||||
d[tag].append(val)
|
||||
else:
|
||||
d[tag] = val
|
||||
return d
|
||||
|
||||
|
||||
def aws_query(service, host, region, action, version, params, cfg):
|
||||
# params: dict of extra query params for this Action
|
||||
body_params = {"Action": action, "Version": version}
|
||||
body_params.update({k: str(v) for k, v in params.items() if v is not None})
|
||||
body = urllib.parse.urlencode(sorted(body_params.items()))
|
||||
|
||||
access_key = str(cfg.get("access_key_id", ""))
|
||||
secret_key = str(cfg.get("secret_access_key", ""))
|
||||
session_token = cfg.get("session_token") or ""
|
||||
|
||||
now = datetime.datetime.utcnow()
|
||||
amz_date = now.strftime("%Y%m%dT%H%M%SZ")
|
||||
date_stamp = now.strftime("%Y%m%d")
|
||||
|
||||
method = "POST"
|
||||
canonical_uri = "/"
|
||||
canonical_querystring = ""
|
||||
payload_hash = hashlib.sha256(body.encode("utf-8")).hexdigest()
|
||||
canonical_headers = "content-type:application/x-www-form-urlencoded; charset=utf-8\n" \
|
||||
"host:" + host + "\n" \
|
||||
"x-amz-date:" + amz_date + "\n"
|
||||
signed_headers = "content-type;host;x-amz-date"
|
||||
if session_token:
|
||||
canonical_headers += "x-amz-security-token:" + session_token + "\n"
|
||||
signed_headers = "content-type;host;x-amz-date;x-amz-security-token"
|
||||
canonical_request = "\n".join([method, canonical_uri, canonical_querystring,
|
||||
canonical_headers, signed_headers, payload_hash])
|
||||
|
||||
algorithm = "AWS4-HMAC-SHA256"
|
||||
credential_scope = date_stamp + "/" + region + "/" + service + "/aws4_request"
|
||||
string_to_sign = "\n".join([algorithm, amz_date, credential_scope,
|
||||
hashlib.sha256(canonical_request.encode("utf-8")).hexdigest()])
|
||||
signing_key = _sign_key(secret_key, date_stamp, region, service)
|
||||
signature = hmac.new(signing_key, string_to_sign.encode("utf-8"), hashlib.sha256).hexdigest()
|
||||
|
||||
authorization = (algorithm + " Credential=" + access_key + "/" + credential_scope +
|
||||
", SignedHeaders=" + signed_headers + ", Signature=" + signature)
|
||||
headers = {
|
||||
"Content-Type": "application/x-www-form-urlencoded; charset=utf-8",
|
||||
"X-Amz-Date": amz_date,
|
||||
"Authorization": authorization,
|
||||
"Accept": "application/json",
|
||||
}
|
||||
if session_token:
|
||||
headers["X-Amz-Security-Token"] = session_token
|
||||
|
||||
req = urllib.request.Request("https://" + host + "/", data=body.encode("utf-8"), headers=headers, method="POST")
|
||||
with urllib.request.urlopen(req, timeout=60) as r:
|
||||
raw = r.read()
|
||||
root = ET.fromstring(raw)
|
||||
return {_strip_ns(root.tag): _xml_to_dict(root)}
|
||||
|
||||
|
||||
def _region(cfg, inputs):
|
||||
return (inputs.get("region") if inputs else None) or cfg.get("region") or "us-east-1"
|
||||
|
||||
|
||||
def ec2(action, params, cfg, inputs):
|
||||
region = _region(cfg, inputs)
|
||||
return aws_query("ec2", "ec2." + region + ".amazonaws.com", region, action, "2016-11-15", params, cfg)
|
||||
|
||||
|
||||
def iam(action, params, cfg):
|
||||
return aws_query("iam", "iam.amazonaws.com", "us-east-1", action, "2010-05-08", params, cfg)
|
||||
|
||||
|
||||
def sts(action, params, cfg):
|
||||
return aws_query("sts", "sts.amazonaws.com", "us-east-1", action, "2011-06-15", params, cfg)
|
||||
|
||||
|
||||
def _run(fn):
|
||||
try:
|
||||
print(json.dumps(fn(_cfg(), _inputs())))
|
||||
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)
|
||||
|
||||
|
||||
def main(cfg, inputs):
|
||||
instance_ids_raw = inputs.get("instance_ids")
|
||||
if not instance_ids_raw or not str(instance_ids_raw).strip():
|
||||
raise Exception("instance_ids is required")
|
||||
instance_ids = [s.strip() for s in str(instance_ids_raw).split(",") if s.strip()]
|
||||
if not instance_ids:
|
||||
raise Exception("instance_ids is required")
|
||||
|
||||
force = inputs.get("force", False)
|
||||
if isinstance(force, str):
|
||||
force = force.strip().lower() in ("true", "1", "yes")
|
||||
else:
|
||||
force = bool(force)
|
||||
|
||||
params = {}
|
||||
for i, iid in enumerate(instance_ids, start=1):
|
||||
params["InstanceId.%d" % i] = iid
|
||||
if force:
|
||||
params["Force"] = "true"
|
||||
|
||||
return ec2("StopInstances", params, cfg, inputs)
|
||||
|
||||
|
||||
_run(main)
|
||||
@@ -0,0 +1,130 @@
|
||||
import json, os, sys, hmac, hashlib, datetime
|
||||
import urllib.parse, urllib.request, urllib.error
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
|
||||
def _cfg():
|
||||
return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||
|
||||
|
||||
def _inputs():
|
||||
return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||
|
||||
|
||||
def _sign_key(key, date_stamp, region, service):
|
||||
def _h(k, m):
|
||||
return hmac.new(k, m.encode("utf-8"), hashlib.sha256).digest()
|
||||
k_date = _h(("AWS4" + key).encode("utf-8"), date_stamp)
|
||||
k_region = _h(k_date, region)
|
||||
k_service = _h(k_region, service)
|
||||
return _h(k_service, "aws4_request")
|
||||
|
||||
|
||||
def _strip_ns(tag):
|
||||
return tag.split("}", 1)[1] if "}" in tag else tag
|
||||
|
||||
|
||||
def _xml_to_dict(elem):
|
||||
d = {}
|
||||
children = list(elem)
|
||||
if not children:
|
||||
return (elem.text or "").strip()
|
||||
for c in children:
|
||||
tag = _strip_ns(c.tag)
|
||||
val = _xml_to_dict(c)
|
||||
if tag in d:
|
||||
if not isinstance(d[tag], list):
|
||||
d[tag] = [d[tag]]
|
||||
d[tag].append(val)
|
||||
else:
|
||||
d[tag] = val
|
||||
return d
|
||||
|
||||
|
||||
def aws_query(service, host, region, action, version, params, cfg):
|
||||
# params: dict of extra query params for this Action
|
||||
body_params = {"Action": action, "Version": version}
|
||||
body_params.update({k: str(v) for k, v in params.items() if v is not None})
|
||||
body = urllib.parse.urlencode(sorted(body_params.items()))
|
||||
|
||||
access_key = str(cfg.get("access_key_id", ""))
|
||||
secret_key = str(cfg.get("secret_access_key", ""))
|
||||
session_token = cfg.get("session_token") or ""
|
||||
|
||||
now = datetime.datetime.utcnow()
|
||||
amz_date = now.strftime("%Y%m%dT%H%M%SZ")
|
||||
date_stamp = now.strftime("%Y%m%d")
|
||||
|
||||
method = "POST"
|
||||
canonical_uri = "/"
|
||||
canonical_querystring = ""
|
||||
payload_hash = hashlib.sha256(body.encode("utf-8")).hexdigest()
|
||||
canonical_headers = "content-type:application/x-www-form-urlencoded; charset=utf-8\n" \
|
||||
"host:" + host + "\n" \
|
||||
"x-amz-date:" + amz_date + "\n"
|
||||
signed_headers = "content-type;host;x-amz-date"
|
||||
if session_token:
|
||||
canonical_headers += "x-amz-security-token:" + session_token + "\n"
|
||||
signed_headers = "content-type;host;x-amz-date;x-amz-security-token"
|
||||
canonical_request = "\n".join([method, canonical_uri, canonical_querystring,
|
||||
canonical_headers, signed_headers, payload_hash])
|
||||
|
||||
algorithm = "AWS4-HMAC-SHA256"
|
||||
credential_scope = date_stamp + "/" + region + "/" + service + "/aws4_request"
|
||||
string_to_sign = "\n".join([algorithm, amz_date, credential_scope,
|
||||
hashlib.sha256(canonical_request.encode("utf-8")).hexdigest()])
|
||||
signing_key = _sign_key(secret_key, date_stamp, region, service)
|
||||
signature = hmac.new(signing_key, string_to_sign.encode("utf-8"), hashlib.sha256).hexdigest()
|
||||
|
||||
authorization = (algorithm + " Credential=" + access_key + "/" + credential_scope +
|
||||
", SignedHeaders=" + signed_headers + ", Signature=" + signature)
|
||||
headers = {
|
||||
"Content-Type": "application/x-www-form-urlencoded; charset=utf-8",
|
||||
"X-Amz-Date": amz_date,
|
||||
"Authorization": authorization,
|
||||
"Accept": "application/json",
|
||||
}
|
||||
if session_token:
|
||||
headers["X-Amz-Security-Token"] = session_token
|
||||
|
||||
req = urllib.request.Request("https://" + host + "/", data=body.encode("utf-8"), headers=headers, method="POST")
|
||||
with urllib.request.urlopen(req, timeout=60) as r:
|
||||
raw = r.read()
|
||||
root = ET.fromstring(raw)
|
||||
return {_strip_ns(root.tag): _xml_to_dict(root)}
|
||||
|
||||
|
||||
def _region(cfg, inputs):
|
||||
return (inputs.get("region") if inputs else None) or cfg.get("region") or "us-east-1"
|
||||
|
||||
|
||||
def ec2(action, params, cfg, inputs):
|
||||
region = _region(cfg, inputs)
|
||||
return aws_query("ec2", "ec2." + region + ".amazonaws.com", region, action, "2016-11-15", params, cfg)
|
||||
|
||||
|
||||
def iam(action, params, cfg):
|
||||
return aws_query("iam", "iam.amazonaws.com", "us-east-1", action, "2010-05-08", params, cfg)
|
||||
|
||||
|
||||
def sts(action, params, cfg):
|
||||
return aws_query("sts", "sts.amazonaws.com", "us-east-1", action, "2011-06-15", params, cfg)
|
||||
|
||||
|
||||
def _run(fn):
|
||||
try:
|
||||
print(json.dumps(fn(_cfg(), _inputs())))
|
||||
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)
|
||||
|
||||
|
||||
def main(cfg, inputs):
|
||||
identity = sts("GetCallerIdentity", {}, cfg)
|
||||
return {"ok": True, "identity": identity}
|
||||
|
||||
|
||||
_run(main)
|
||||
@@ -0,0 +1,143 @@
|
||||
import json, os, sys, hmac, hashlib, datetime
|
||||
import urllib.parse, urllib.request, urllib.error
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
|
||||
def _cfg():
|
||||
return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||
|
||||
|
||||
def _inputs():
|
||||
return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||
|
||||
|
||||
def _sign_key(key, date_stamp, region, service):
|
||||
def _h(k, m):
|
||||
return hmac.new(k, m.encode("utf-8"), hashlib.sha256).digest()
|
||||
k_date = _h(("AWS4" + key).encode("utf-8"), date_stamp)
|
||||
k_region = _h(k_date, region)
|
||||
k_service = _h(k_region, service)
|
||||
return _h(k_service, "aws4_request")
|
||||
|
||||
|
||||
def _strip_ns(tag):
|
||||
return tag.split("}", 1)[1] if "}" in tag else tag
|
||||
|
||||
|
||||
def _xml_to_dict(elem):
|
||||
d = {}
|
||||
children = list(elem)
|
||||
if not children:
|
||||
return (elem.text or "").strip()
|
||||
for c in children:
|
||||
tag = _strip_ns(c.tag)
|
||||
val = _xml_to_dict(c)
|
||||
if tag in d:
|
||||
if not isinstance(d[tag], list):
|
||||
d[tag] = [d[tag]]
|
||||
d[tag].append(val)
|
||||
else:
|
||||
d[tag] = val
|
||||
return d
|
||||
|
||||
|
||||
def aws_query(service, host, region, action, version, params, cfg):
|
||||
# params: dict of extra query params for this Action
|
||||
body_params = {"Action": action, "Version": version}
|
||||
body_params.update({k: str(v) for k, v in params.items() if v is not None})
|
||||
body = urllib.parse.urlencode(sorted(body_params.items()))
|
||||
|
||||
access_key = str(cfg.get("access_key_id", ""))
|
||||
secret_key = str(cfg.get("secret_access_key", ""))
|
||||
session_token = cfg.get("session_token") or ""
|
||||
|
||||
now = datetime.datetime.utcnow()
|
||||
amz_date = now.strftime("%Y%m%dT%H%M%SZ")
|
||||
date_stamp = now.strftime("%Y%m%d")
|
||||
|
||||
method = "POST"
|
||||
canonical_uri = "/"
|
||||
canonical_querystring = ""
|
||||
payload_hash = hashlib.sha256(body.encode("utf-8")).hexdigest()
|
||||
canonical_headers = "content-type:application/x-www-form-urlencoded; charset=utf-8\n" \
|
||||
"host:" + host + "\n" \
|
||||
"x-amz-date:" + amz_date + "\n"
|
||||
signed_headers = "content-type;host;x-amz-date"
|
||||
if session_token:
|
||||
canonical_headers += "x-amz-security-token:" + session_token + "\n"
|
||||
signed_headers = "content-type;host;x-amz-date;x-amz-security-token"
|
||||
canonical_request = "\n".join([method, canonical_uri, canonical_querystring,
|
||||
canonical_headers, signed_headers, payload_hash])
|
||||
|
||||
algorithm = "AWS4-HMAC-SHA256"
|
||||
credential_scope = date_stamp + "/" + region + "/" + service + "/aws4_request"
|
||||
string_to_sign = "\n".join([algorithm, amz_date, credential_scope,
|
||||
hashlib.sha256(canonical_request.encode("utf-8")).hexdigest()])
|
||||
signing_key = _sign_key(secret_key, date_stamp, region, service)
|
||||
signature = hmac.new(signing_key, string_to_sign.encode("utf-8"), hashlib.sha256).hexdigest()
|
||||
|
||||
authorization = (algorithm + " Credential=" + access_key + "/" + credential_scope +
|
||||
", SignedHeaders=" + signed_headers + ", Signature=" + signature)
|
||||
headers = {
|
||||
"Content-Type": "application/x-www-form-urlencoded; charset=utf-8",
|
||||
"X-Amz-Date": amz_date,
|
||||
"Authorization": authorization,
|
||||
"Accept": "application/json",
|
||||
}
|
||||
if session_token:
|
||||
headers["X-Amz-Security-Token"] = session_token
|
||||
|
||||
req = urllib.request.Request("https://" + host + "/", data=body.encode("utf-8"), headers=headers, method="POST")
|
||||
with urllib.request.urlopen(req, timeout=60) as r:
|
||||
raw = r.read()
|
||||
root = ET.fromstring(raw)
|
||||
return {_strip_ns(root.tag): _xml_to_dict(root)}
|
||||
|
||||
|
||||
def _region(cfg, inputs):
|
||||
return (inputs.get("region") if inputs else None) or cfg.get("region") or "us-east-1"
|
||||
|
||||
|
||||
def ec2(action, params, cfg, inputs):
|
||||
region = _region(cfg, inputs)
|
||||
return aws_query("ec2", "ec2." + region + ".amazonaws.com", region, action, "2016-11-15", params, cfg)
|
||||
|
||||
|
||||
def iam(action, params, cfg):
|
||||
return aws_query("iam", "iam.amazonaws.com", "us-east-1", action, "2010-05-08", params, cfg)
|
||||
|
||||
|
||||
def sts(action, params, cfg):
|
||||
return aws_query("sts", "sts.amazonaws.com", "us-east-1", action, "2011-06-15", params, cfg)
|
||||
|
||||
|
||||
def _run(fn):
|
||||
try:
|
||||
print(json.dumps(fn(_cfg(), _inputs())))
|
||||
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)
|
||||
|
||||
|
||||
def main(cfg, inputs):
|
||||
access_key_id = inputs.get("access_key_id")
|
||||
if not access_key_id:
|
||||
raise Exception("access_key_id is required")
|
||||
status = inputs.get("status")
|
||||
if not status:
|
||||
raise Exception("status is required")
|
||||
if status not in ("Active", "Inactive"):
|
||||
raise Exception("status must be Active or Inactive")
|
||||
|
||||
params = {"AccessKeyId": access_key_id, "Status": status}
|
||||
user_name = inputs.get("user_name")
|
||||
if user_name and str(user_name).strip():
|
||||
params["UserName"] = user_name
|
||||
|
||||
return iam("UpdateAccessKey", params, cfg)
|
||||
|
||||
|
||||
_run(main)
|
||||
@@ -0,0 +1,76 @@
|
||||
id: axonius
|
||||
name: Axonius
|
||||
version: 1.0.0
|
||||
description: "Axonius (REST API) — cybersecurity asset management: query devices and users with AQL filters, get a device by ID, and count devices matching a filter. API-key + API-secret authentication; stdlib-only, no extra Python dependencies."
|
||||
changelog: "1.0.0 — Initial release: list/get devices, device count, list users."
|
||||
category: asset_management
|
||||
|
||||
# Per-instance configuration. Auth uses the 'api-key' and 'api-secret' headers.
|
||||
config_schema:
|
||||
properties:
|
||||
base_url:
|
||||
type: string
|
||||
description: "Axonius instance URL (e.g. https://axonius.example.com)"
|
||||
api_key:
|
||||
type: string
|
||||
description: "API key"
|
||||
x-soar-sensitive: true
|
||||
api_secret:
|
||||
type: string
|
||||
description: "API secret"
|
||||
x-soar-sensitive: true
|
||||
required:
|
||||
- base_url
|
||||
- api_key
|
||||
- api_secret
|
||||
|
||||
commands:
|
||||
- id: list_devices
|
||||
name: axonius-list-devices
|
||||
description: "Query devices with an optional AQL filter."
|
||||
risk: read
|
||||
inputs_schema:
|
||||
properties:
|
||||
filter: { type: string, description: "AQL filter (e.g. specific_data.data.hostname == \"host01\")" }
|
||||
limit: { type: number, description: "Max devices (default 50)" }
|
||||
offset: { type: number, description: "Offset (default 0)" }
|
||||
required: []
|
||||
outputs_schema: { properties: {} }
|
||||
- id: get_device
|
||||
name: axonius-get-device
|
||||
description: "Get a single device by its internal Axonius ID."
|
||||
risk: read
|
||||
inputs_schema:
|
||||
properties:
|
||||
device_id: { type: string, description: "Internal Axonius device ID" }
|
||||
required: [device_id]
|
||||
outputs_schema: { properties: {} }
|
||||
- id: device_count
|
||||
name: axonius-device-count
|
||||
description: "Count devices matching an AQL filter."
|
||||
risk: read
|
||||
inputs_schema:
|
||||
properties:
|
||||
filter: { type: string, description: "AQL filter (empty = all devices)" }
|
||||
required: []
|
||||
outputs_schema: { properties: {} }
|
||||
- id: list_users
|
||||
name: axonius-list-users
|
||||
description: "Query users with an optional AQL filter."
|
||||
risk: read
|
||||
inputs_schema:
|
||||
properties:
|
||||
filter: { type: string, description: "AQL filter" }
|
||||
limit: { type: number, description: "Max users (default 50)" }
|
||||
offset: { type: number, description: "Offset (default 0)" }
|
||||
required: []
|
||||
outputs_schema: { properties: {} }
|
||||
|
||||
- id: test_connection
|
||||
name: axonius-test-connection
|
||||
description: "Verify connectivity and credentials (used by the Test button)."
|
||||
risk: read
|
||||
inputs_schema:
|
||||
properties: {}
|
||||
required: []
|
||||
outputs_schema: { properties: {} }
|
||||
@@ -0,0 +1,52 @@
|
||||
import json, os, sys, urllib.parse, urllib.request, urllib.error
|
||||
|
||||
|
||||
def _cfg():
|
||||
return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||
|
||||
|
||||
def _inputs():
|
||||
return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||
|
||||
|
||||
def request(method, path, cfg, body=None):
|
||||
url = str(cfg.get("base_url", "")).rstrip("/") + "/api" + path
|
||||
data = json.dumps(body).encode("utf-8") if body is not None else None
|
||||
headers = {
|
||||
"api-key": str(cfg.get("api_key", "")),
|
||||
"api-secret": str(cfg.get("api_secret", "")),
|
||||
"Accept": "application/json",
|
||||
}
|
||||
if data is not None:
|
||||
headers["Content-Type"] = "application/json"
|
||||
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 _run(fn):
|
||||
try:
|
||||
print(json.dumps(fn(_cfg(), _inputs())))
|
||||
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)
|
||||
|
||||
|
||||
q = lambda v: urllib.parse.quote(str(v), safe="")
|
||||
|
||||
|
||||
def main(cfg, inputs):
|
||||
filter_ = inputs.get("filter")
|
||||
|
||||
body = {"data": {}}
|
||||
if filter_:
|
||||
body["data"]["filter"] = filter_
|
||||
|
||||
return request("POST", "/devices/count", cfg, body=body)
|
||||
|
||||
|
||||
_run(main)
|
||||
@@ -0,0 +1,50 @@
|
||||
import json, os, sys, urllib.parse, urllib.request, urllib.error
|
||||
|
||||
|
||||
def _cfg():
|
||||
return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||
|
||||
|
||||
def _inputs():
|
||||
return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||
|
||||
|
||||
def request(method, path, cfg, body=None):
|
||||
url = str(cfg.get("base_url", "")).rstrip("/") + "/api" + path
|
||||
data = json.dumps(body).encode("utf-8") if body is not None else None
|
||||
headers = {
|
||||
"api-key": str(cfg.get("api_key", "")),
|
||||
"api-secret": str(cfg.get("api_secret", "")),
|
||||
"Accept": "application/json",
|
||||
}
|
||||
if data is not None:
|
||||
headers["Content-Type"] = "application/json"
|
||||
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 _run(fn):
|
||||
try:
|
||||
print(json.dumps(fn(_cfg(), _inputs())))
|
||||
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)
|
||||
|
||||
|
||||
q = lambda v: urllib.parse.quote(str(v), safe="")
|
||||
|
||||
|
||||
def main(cfg, inputs):
|
||||
device_id = inputs.get("device_id")
|
||||
if not device_id:
|
||||
raise Exception("device_id is required")
|
||||
|
||||
return request("GET", "/devices/" + q(device_id), cfg)
|
||||
|
||||
|
||||
_run(main)
|
||||
@@ -0,0 +1,61 @@
|
||||
import json, os, sys, urllib.parse, urllib.request, urllib.error
|
||||
|
||||
|
||||
def _cfg():
|
||||
return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||
|
||||
|
||||
def _inputs():
|
||||
return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||
|
||||
|
||||
def request(method, path, cfg, body=None):
|
||||
url = str(cfg.get("base_url", "")).rstrip("/") + "/api" + path
|
||||
data = json.dumps(body).encode("utf-8") if body is not None else None
|
||||
headers = {
|
||||
"api-key": str(cfg.get("api_key", "")),
|
||||
"api-secret": str(cfg.get("api_secret", "")),
|
||||
"Accept": "application/json",
|
||||
}
|
||||
if data is not None:
|
||||
headers["Content-Type"] = "application/json"
|
||||
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 _run(fn):
|
||||
try:
|
||||
print(json.dumps(fn(_cfg(), _inputs())))
|
||||
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)
|
||||
|
||||
|
||||
q = lambda v: urllib.parse.quote(str(v), safe="")
|
||||
|
||||
|
||||
def main(cfg, inputs):
|
||||
filter_ = inputs.get("filter")
|
||||
limit = inputs.get("limit")
|
||||
offset = inputs.get("offset")
|
||||
|
||||
body = {
|
||||
"data": {
|
||||
"page": {
|
||||
"limit": int(limit or 50),
|
||||
"offset": int(offset or 0),
|
||||
}
|
||||
}
|
||||
}
|
||||
if filter_:
|
||||
body["data"]["filter"] = filter_
|
||||
|
||||
return request("POST", "/devices", cfg, body=body)
|
||||
|
||||
|
||||
_run(main)
|
||||
@@ -0,0 +1,61 @@
|
||||
import json, os, sys, urllib.parse, urllib.request, urllib.error
|
||||
|
||||
|
||||
def _cfg():
|
||||
return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||
|
||||
|
||||
def _inputs():
|
||||
return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||
|
||||
|
||||
def request(method, path, cfg, body=None):
|
||||
url = str(cfg.get("base_url", "")).rstrip("/") + "/api" + path
|
||||
data = json.dumps(body).encode("utf-8") if body is not None else None
|
||||
headers = {
|
||||
"api-key": str(cfg.get("api_key", "")),
|
||||
"api-secret": str(cfg.get("api_secret", "")),
|
||||
"Accept": "application/json",
|
||||
}
|
||||
if data is not None:
|
||||
headers["Content-Type"] = "application/json"
|
||||
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 _run(fn):
|
||||
try:
|
||||
print(json.dumps(fn(_cfg(), _inputs())))
|
||||
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)
|
||||
|
||||
|
||||
q = lambda v: urllib.parse.quote(str(v), safe="")
|
||||
|
||||
|
||||
def main(cfg, inputs):
|
||||
filter_ = inputs.get("filter")
|
||||
limit = inputs.get("limit")
|
||||
offset = inputs.get("offset")
|
||||
|
||||
body = {
|
||||
"data": {
|
||||
"page": {
|
||||
"limit": int(limit or 50),
|
||||
"offset": int(offset or 0),
|
||||
}
|
||||
}
|
||||
}
|
||||
if filter_:
|
||||
body["data"]["filter"] = filter_
|
||||
|
||||
return request("POST", "/users", cfg, body=body)
|
||||
|
||||
|
||||
_run(main)
|
||||
@@ -0,0 +1,47 @@
|
||||
import json, os, sys, urllib.parse, urllib.request, urllib.error
|
||||
|
||||
|
||||
def _cfg():
|
||||
return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||
|
||||
|
||||
def _inputs():
|
||||
return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||
|
||||
|
||||
def request(method, path, cfg, body=None):
|
||||
url = str(cfg.get("base_url", "")).rstrip("/") + "/api" + path
|
||||
data = json.dumps(body).encode("utf-8") if body is not None else None
|
||||
headers = {
|
||||
"api-key": str(cfg.get("api_key", "")),
|
||||
"api-secret": str(cfg.get("api_secret", "")),
|
||||
"Accept": "application/json",
|
||||
}
|
||||
if data is not None:
|
||||
headers["Content-Type"] = "application/json"
|
||||
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 _run(fn):
|
||||
try:
|
||||
print(json.dumps(fn(_cfg(), _inputs())))
|
||||
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)
|
||||
|
||||
|
||||
q = lambda v: urllib.parse.quote(str(v), safe="")
|
||||
|
||||
|
||||
def main(cfg, inputs):
|
||||
request("POST", "/devices/count", cfg, body={"data": {}})
|
||||
return {"ok": True}
|
||||
|
||||
|
||||
_run(main)
|
||||
@@ -0,0 +1,120 @@
|
||||
id: azure_security
|
||||
name: Microsoft Azure
|
||||
version: 1.0.0
|
||||
description: "Microsoft Azure (Resource Manager: Defender for Cloud + Network) — cloud containment: list and read Defender for Cloud security alerts and update their state, read the secure score, list/read network security groups (NSGs), and add or delete NSG security rules (deny inbound to isolate). Azure AD OAuth2 client-credentials authentication; stdlib-only, no extra Python dependencies."
|
||||
changelog: "1.0.0 — Initial release: list/get security alerts, update alert state, secure score, list/get NSGs, create/delete NSG security rules."
|
||||
category: cloud
|
||||
|
||||
# Per-instance configuration. Uses application (client-credentials) OAuth2 with
|
||||
# a service principal that has Reader + Security Admin + Network Contributor on
|
||||
# the subscription.
|
||||
config_schema:
|
||||
properties:
|
||||
tenant_id:
|
||||
type: string
|
||||
description: "Azure AD tenant ID"
|
||||
client_id:
|
||||
type: string
|
||||
description: "Service principal (client) ID"
|
||||
client_secret:
|
||||
type: string
|
||||
description: "Service principal client secret"
|
||||
x-soar-sensitive: true
|
||||
subscription_id:
|
||||
type: string
|
||||
description: "Azure subscription ID"
|
||||
required:
|
||||
- tenant_id
|
||||
- client_id
|
||||
- client_secret
|
||||
- subscription_id
|
||||
|
||||
commands:
|
||||
- id: list_alerts
|
||||
name: azure-list-alerts
|
||||
description: "List Microsoft Defender for Cloud security alerts in the subscription."
|
||||
risk: read
|
||||
inputs_schema:
|
||||
properties: {}
|
||||
required: []
|
||||
outputs_schema: { properties: {} }
|
||||
- id: get_alert
|
||||
name: azure-get-alert
|
||||
description: "Get a single security alert by its full ARM resource ID."
|
||||
risk: read
|
||||
inputs_schema:
|
||||
properties:
|
||||
alert_id: { type: string, description: "Full ARM resource ID of the alert (from azure-list-alerts)" }
|
||||
required: [alert_id]
|
||||
outputs_schema: { properties: {} }
|
||||
- id: update_alert_state
|
||||
name: azure-update-alert-state
|
||||
description: "Change a security alert's state (dismiss, resolve, activate, or inProgress)."
|
||||
inputs_schema:
|
||||
properties:
|
||||
alert_id: { type: string, description: "Full ARM resource ID of the alert" }
|
||||
state: { type: string, description: "dismiss | resolve | activate | inProgress" }
|
||||
required: [alert_id, state]
|
||||
outputs_schema: { properties: {} }
|
||||
- id: get_secure_score
|
||||
name: azure-get-secure-score
|
||||
description: "Get the subscription's Defender for Cloud secure score."
|
||||
risk: read
|
||||
inputs_schema:
|
||||
properties: {}
|
||||
required: []
|
||||
outputs_schema: { properties: {} }
|
||||
- id: list_nsgs
|
||||
name: azure-list-nsgs
|
||||
description: "List network security groups in the subscription."
|
||||
risk: read
|
||||
inputs_schema:
|
||||
properties: {}
|
||||
required: []
|
||||
outputs_schema: { properties: {} }
|
||||
- id: get_nsg
|
||||
name: azure-get-nsg
|
||||
description: "Get a single network security group."
|
||||
risk: read
|
||||
inputs_schema:
|
||||
properties:
|
||||
resource_group: { type: string, description: "Resource group name" }
|
||||
nsg_name: { type: string, description: "NSG name" }
|
||||
required: [resource_group, nsg_name]
|
||||
outputs_schema: { properties: {} }
|
||||
- id: create_nsg_rule
|
||||
name: azure-create-nsg-rule
|
||||
description: "Create or update an NSG security rule (e.g. a Deny inbound rule to isolate a resource)."
|
||||
inputs_schema:
|
||||
properties:
|
||||
resource_group: { type: string, description: "Resource group name" }
|
||||
nsg_name: { type: string, description: "NSG name" }
|
||||
rule_name: { type: string, description: "Security rule name" }
|
||||
priority: { type: number, description: "Rule priority (100-4096)" }
|
||||
direction: { type: string, description: "Inbound or Outbound (default Inbound)" }
|
||||
access: { type: string, description: "Allow or Deny (default Deny)" }
|
||||
protocol: { type: string, description: "Tcp, Udp, or * (default *)" }
|
||||
source: { type: string, description: "Source address prefix (CIDR or *, default *)" }
|
||||
destination: { type: string, description: "Destination address prefix (default *)" }
|
||||
destination_port: { type: string, description: "Destination port range (default *)" }
|
||||
required: [resource_group, nsg_name, rule_name, priority]
|
||||
outputs_schema: { properties: {} }
|
||||
- id: delete_nsg_rule
|
||||
name: azure-delete-nsg-rule
|
||||
description: "Delete an NSG security rule."
|
||||
inputs_schema:
|
||||
properties:
|
||||
resource_group: { type: string, description: "Resource group name" }
|
||||
nsg_name: { type: string, description: "NSG name" }
|
||||
rule_name: { type: string, description: "Security rule name" }
|
||||
required: [resource_group, nsg_name, rule_name]
|
||||
outputs_schema: { properties: {} }
|
||||
|
||||
- id: test_connection
|
||||
name: azure-test-connection
|
||||
description: "Verify connectivity and the service-principal credentials (used by the Test button)."
|
||||
risk: read
|
||||
inputs_schema:
|
||||
properties: {}
|
||||
required: []
|
||||
outputs_schema: { properties: {} }
|
||||
@@ -0,0 +1,103 @@
|
||||
import json, os, sys, urllib.parse, urllib.request, urllib.error
|
||||
|
||||
ARM = "https://management.azure.com"
|
||||
|
||||
|
||||
def _cfg():
|
||||
return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||
|
||||
|
||||
def _inputs():
|
||||
return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||
|
||||
|
||||
def _token(cfg):
|
||||
url = "https://login.microsoftonline.com/" + str(cfg.get("tenant_id", "")) + "/oauth2/v2.0/token"
|
||||
form = urllib.parse.urlencode({
|
||||
"grant_type": "client_credentials",
|
||||
"client_id": str(cfg.get("client_id", "")),
|
||||
"client_secret": str(cfg.get("client_secret", "")),
|
||||
"scope": "https://management.azure.com/.default",
|
||||
}).encode("utf-8")
|
||||
req = urllib.request.Request(url, data=form, headers={"Content-Type": "application/x-www-form-urlencoded"}, method="POST")
|
||||
with urllib.request.urlopen(req, timeout=60) as r:
|
||||
tok = json.loads(r.read())
|
||||
if not tok.get("access_token"):
|
||||
raise Exception("Token request failed: " + json.dumps(tok))
|
||||
return tok["access_token"]
|
||||
|
||||
|
||||
def arm(method, path, token, api_version, body=None, params=None, full_url=None):
|
||||
url = (full_url if full_url else ARM + path)
|
||||
qp = {"api-version": api_version}
|
||||
if params:
|
||||
qp.update({k: v for k, v in params.items() if v not in (None, "")})
|
||||
url += ("&" if "?" in url else "?") + urllib.parse.urlencode(qp)
|
||||
data = json.dumps(body).encode("utf-8") if body is not None else None
|
||||
headers = {"Authorization": "Bearer " + token, "Accept": "application/json"}
|
||||
if data is not None:
|
||||
headers["Content-Type"] = "application/json"
|
||||
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 _run(fn):
|
||||
try:
|
||||
cfg = _cfg()
|
||||
inputs = _inputs()
|
||||
token = _token(cfg)
|
||||
print(json.dumps(fn(cfg, token, inputs)))
|
||||
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)
|
||||
|
||||
|
||||
sub = lambda cfg: str(cfg.get("subscription_id", ""))
|
||||
q = lambda v: urllib.parse.quote(str(v), safe="")
|
||||
|
||||
|
||||
def main(cfg, token, inputs):
|
||||
resource_group = inputs.get("resource_group")
|
||||
if not resource_group:
|
||||
raise Exception("resource_group is required")
|
||||
nsg_name = inputs.get("nsg_name")
|
||||
if not nsg_name:
|
||||
raise Exception("nsg_name is required")
|
||||
rule_name = inputs.get("rule_name")
|
||||
if not rule_name:
|
||||
raise Exception("rule_name is required")
|
||||
priority = inputs.get("priority")
|
||||
if priority in (None, ""):
|
||||
raise Exception("priority is required")
|
||||
|
||||
direction = inputs.get("direction")
|
||||
access = inputs.get("access")
|
||||
protocol = inputs.get("protocol")
|
||||
source = inputs.get("source")
|
||||
destination = inputs.get("destination")
|
||||
destination_port = inputs.get("destination_port")
|
||||
|
||||
path = ("/subscriptions/" + sub(cfg) + "/resourceGroups/" + q(resource_group) +
|
||||
"/providers/Microsoft.Network/networkSecurityGroups/" + q(nsg_name) +
|
||||
"/securityRules/" + q(rule_name))
|
||||
body = {
|
||||
"properties": {
|
||||
"priority": int(priority),
|
||||
"direction": direction or "Inbound",
|
||||
"access": access or "Deny",
|
||||
"protocol": protocol or "*",
|
||||
"sourceAddressPrefix": source or "*",
|
||||
"destinationAddressPrefix": destination or "*",
|
||||
"sourcePortRange": "*",
|
||||
"destinationPortRange": destination_port or "*",
|
||||
}
|
||||
}
|
||||
return arm("PUT", path, token, "2023-09-01", body=body)
|
||||
|
||||
|
||||
_run(main)
|
||||
@@ -0,0 +1,84 @@
|
||||
import json, os, sys, urllib.parse, urllib.request, urllib.error
|
||||
|
||||
ARM = "https://management.azure.com"
|
||||
|
||||
|
||||
def _cfg():
|
||||
return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||
|
||||
|
||||
def _inputs():
|
||||
return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||
|
||||
|
||||
def _token(cfg):
|
||||
url = "https://login.microsoftonline.com/" + str(cfg.get("tenant_id", "")) + "/oauth2/v2.0/token"
|
||||
form = urllib.parse.urlencode({
|
||||
"grant_type": "client_credentials",
|
||||
"client_id": str(cfg.get("client_id", "")),
|
||||
"client_secret": str(cfg.get("client_secret", "")),
|
||||
"scope": "https://management.azure.com/.default",
|
||||
}).encode("utf-8")
|
||||
req = urllib.request.Request(url, data=form, headers={"Content-Type": "application/x-www-form-urlencoded"}, method="POST")
|
||||
with urllib.request.urlopen(req, timeout=60) as r:
|
||||
tok = json.loads(r.read())
|
||||
if not tok.get("access_token"):
|
||||
raise Exception("Token request failed: " + json.dumps(tok))
|
||||
return tok["access_token"]
|
||||
|
||||
|
||||
def arm(method, path, token, api_version, body=None, params=None, full_url=None):
|
||||
url = (full_url if full_url else ARM + path)
|
||||
qp = {"api-version": api_version}
|
||||
if params:
|
||||
qp.update({k: v for k, v in params.items() if v not in (None, "")})
|
||||
url += ("&" if "?" in url else "?") + urllib.parse.urlencode(qp)
|
||||
data = json.dumps(body).encode("utf-8") if body is not None else None
|
||||
headers = {"Authorization": "Bearer " + token, "Accept": "application/json"}
|
||||
if data is not None:
|
||||
headers["Content-Type"] = "application/json"
|
||||
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 _run(fn):
|
||||
try:
|
||||
cfg = _cfg()
|
||||
inputs = _inputs()
|
||||
token = _token(cfg)
|
||||
print(json.dumps(fn(cfg, token, inputs)))
|
||||
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)
|
||||
|
||||
|
||||
sub = lambda cfg: str(cfg.get("subscription_id", ""))
|
||||
q = lambda v: urllib.parse.quote(str(v), safe="")
|
||||
|
||||
|
||||
def main(cfg, token, inputs):
|
||||
resource_group = inputs.get("resource_group")
|
||||
if not resource_group:
|
||||
raise Exception("resource_group is required")
|
||||
nsg_name = inputs.get("nsg_name")
|
||||
if not nsg_name:
|
||||
raise Exception("nsg_name is required")
|
||||
rule_name = inputs.get("rule_name")
|
||||
if not rule_name:
|
||||
raise Exception("rule_name is required")
|
||||
|
||||
path = ("/subscriptions/" + sub(cfg) + "/resourceGroups/" + q(resource_group) +
|
||||
"/providers/Microsoft.Network/networkSecurityGroups/" + q(nsg_name) +
|
||||
"/securityRules/" + q(rule_name))
|
||||
result = arm("DELETE", path, token, "2023-09-01")
|
||||
if not result:
|
||||
return {"ok": True, "deleted": rule_name}
|
||||
return result
|
||||
|
||||
|
||||
_run(main)
|
||||
@@ -0,0 +1,67 @@
|
||||
import json, os, sys, urllib.parse, urllib.request, urllib.error
|
||||
|
||||
ARM = "https://management.azure.com"
|
||||
|
||||
|
||||
def _cfg():
|
||||
return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||
|
||||
|
||||
def _inputs():
|
||||
return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||
|
||||
|
||||
def _token(cfg):
|
||||
url = "https://login.microsoftonline.com/" + str(cfg.get("tenant_id", "")) + "/oauth2/v2.0/token"
|
||||
form = urllib.parse.urlencode({
|
||||
"grant_type": "client_credentials",
|
||||
"client_id": str(cfg.get("client_id", "")),
|
||||
"client_secret": str(cfg.get("client_secret", "")),
|
||||
"scope": "https://management.azure.com/.default",
|
||||
}).encode("utf-8")
|
||||
req = urllib.request.Request(url, data=form, headers={"Content-Type": "application/x-www-form-urlencoded"}, method="POST")
|
||||
with urllib.request.urlopen(req, timeout=60) as r:
|
||||
tok = json.loads(r.read())
|
||||
if not tok.get("access_token"):
|
||||
raise Exception("Token request failed: " + json.dumps(tok))
|
||||
return tok["access_token"]
|
||||
|
||||
|
||||
def arm(method, path, token, api_version, body=None, params=None, full_url=None):
|
||||
url = (full_url if full_url else ARM + path)
|
||||
qp = {"api-version": api_version}
|
||||
if params:
|
||||
qp.update({k: v for k, v in params.items() if v not in (None, "")})
|
||||
url += ("&" if "?" in url else "?") + urllib.parse.urlencode(qp)
|
||||
data = json.dumps(body).encode("utf-8") if body is not None else None
|
||||
headers = {"Authorization": "Bearer " + token, "Accept": "application/json"}
|
||||
if data is not None:
|
||||
headers["Content-Type"] = "application/json"
|
||||
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 _run(fn):
|
||||
try:
|
||||
cfg = _cfg()
|
||||
inputs = _inputs()
|
||||
token = _token(cfg)
|
||||
print(json.dumps(fn(cfg, token, inputs)))
|
||||
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)
|
||||
|
||||
|
||||
def main(cfg, token, inputs):
|
||||
alert_id = inputs.get("alert_id")
|
||||
if not alert_id:
|
||||
raise Exception("alert_id is required")
|
||||
return arm("GET", None, token, "2022-01-01", full_url=ARM + str(alert_id))
|
||||
|
||||
|
||||
_run(main)
|
||||
@@ -0,0 +1,76 @@
|
||||
import json, os, sys, urllib.parse, urllib.request, urllib.error
|
||||
|
||||
ARM = "https://management.azure.com"
|
||||
|
||||
|
||||
def _cfg():
|
||||
return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||
|
||||
|
||||
def _inputs():
|
||||
return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||
|
||||
|
||||
def _token(cfg):
|
||||
url = "https://login.microsoftonline.com/" + str(cfg.get("tenant_id", "")) + "/oauth2/v2.0/token"
|
||||
form = urllib.parse.urlencode({
|
||||
"grant_type": "client_credentials",
|
||||
"client_id": str(cfg.get("client_id", "")),
|
||||
"client_secret": str(cfg.get("client_secret", "")),
|
||||
"scope": "https://management.azure.com/.default",
|
||||
}).encode("utf-8")
|
||||
req = urllib.request.Request(url, data=form, headers={"Content-Type": "application/x-www-form-urlencoded"}, method="POST")
|
||||
with urllib.request.urlopen(req, timeout=60) as r:
|
||||
tok = json.loads(r.read())
|
||||
if not tok.get("access_token"):
|
||||
raise Exception("Token request failed: " + json.dumps(tok))
|
||||
return tok["access_token"]
|
||||
|
||||
|
||||
def arm(method, path, token, api_version, body=None, params=None, full_url=None):
|
||||
url = (full_url if full_url else ARM + path)
|
||||
qp = {"api-version": api_version}
|
||||
if params:
|
||||
qp.update({k: v for k, v in params.items() if v not in (None, "")})
|
||||
url += ("&" if "?" in url else "?") + urllib.parse.urlencode(qp)
|
||||
data = json.dumps(body).encode("utf-8") if body is not None else None
|
||||
headers = {"Authorization": "Bearer " + token, "Accept": "application/json"}
|
||||
if data is not None:
|
||||
headers["Content-Type"] = "application/json"
|
||||
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 _run(fn):
|
||||
try:
|
||||
cfg = _cfg()
|
||||
inputs = _inputs()
|
||||
token = _token(cfg)
|
||||
print(json.dumps(fn(cfg, token, inputs)))
|
||||
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)
|
||||
|
||||
|
||||
sub = lambda cfg: str(cfg.get("subscription_id", ""))
|
||||
q = lambda v: urllib.parse.quote(str(v), safe="")
|
||||
|
||||
|
||||
def main(cfg, token, inputs):
|
||||
resource_group = inputs.get("resource_group")
|
||||
if not resource_group:
|
||||
raise Exception("resource_group is required")
|
||||
nsg_name = inputs.get("nsg_name")
|
||||
if not nsg_name:
|
||||
raise Exception("nsg_name is required")
|
||||
path = ("/subscriptions/" + sub(cfg) + "/resourceGroups/" + q(resource_group) +
|
||||
"/providers/Microsoft.Network/networkSecurityGroups/" + q(nsg_name))
|
||||
return arm("GET", path, token, "2023-09-01")
|
||||
|
||||
|
||||
_run(main)
|
||||
@@ -0,0 +1,68 @@
|
||||
import json, os, sys, urllib.parse, urllib.request, urllib.error
|
||||
|
||||
ARM = "https://management.azure.com"
|
||||
|
||||
|
||||
def _cfg():
|
||||
return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||
|
||||
|
||||
def _inputs():
|
||||
return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||
|
||||
|
||||
def _token(cfg):
|
||||
url = "https://login.microsoftonline.com/" + str(cfg.get("tenant_id", "")) + "/oauth2/v2.0/token"
|
||||
form = urllib.parse.urlencode({
|
||||
"grant_type": "client_credentials",
|
||||
"client_id": str(cfg.get("client_id", "")),
|
||||
"client_secret": str(cfg.get("client_secret", "")),
|
||||
"scope": "https://management.azure.com/.default",
|
||||
}).encode("utf-8")
|
||||
req = urllib.request.Request(url, data=form, headers={"Content-Type": "application/x-www-form-urlencoded"}, method="POST")
|
||||
with urllib.request.urlopen(req, timeout=60) as r:
|
||||
tok = json.loads(r.read())
|
||||
if not tok.get("access_token"):
|
||||
raise Exception("Token request failed: " + json.dumps(tok))
|
||||
return tok["access_token"]
|
||||
|
||||
|
||||
def arm(method, path, token, api_version, body=None, params=None, full_url=None):
|
||||
url = (full_url if full_url else ARM + path)
|
||||
qp = {"api-version": api_version}
|
||||
if params:
|
||||
qp.update({k: v for k, v in params.items() if v not in (None, "")})
|
||||
url += ("&" if "?" in url else "?") + urllib.parse.urlencode(qp)
|
||||
data = json.dumps(body).encode("utf-8") if body is not None else None
|
||||
headers = {"Authorization": "Bearer " + token, "Accept": "application/json"}
|
||||
if data is not None:
|
||||
headers["Content-Type"] = "application/json"
|
||||
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 _run(fn):
|
||||
try:
|
||||
cfg = _cfg()
|
||||
inputs = _inputs()
|
||||
token = _token(cfg)
|
||||
print(json.dumps(fn(cfg, token, inputs)))
|
||||
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)
|
||||
|
||||
|
||||
sub = lambda cfg: str(cfg.get("subscription_id", ""))
|
||||
|
||||
|
||||
def main(cfg, token, inputs):
|
||||
path = "/subscriptions/" + sub(cfg) + "/providers/Microsoft.Security/secureScores/ascScore"
|
||||
return arm("GET", path, token, "2020-01-01")
|
||||
|
||||
|
||||
_run(main)
|
||||
@@ -0,0 +1,68 @@
|
||||
import json, os, sys, urllib.parse, urllib.request, urllib.error
|
||||
|
||||
ARM = "https://management.azure.com"
|
||||
|
||||
|
||||
def _cfg():
|
||||
return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||
|
||||
|
||||
def _inputs():
|
||||
return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||
|
||||
|
||||
def _token(cfg):
|
||||
url = "https://login.microsoftonline.com/" + str(cfg.get("tenant_id", "")) + "/oauth2/v2.0/token"
|
||||
form = urllib.parse.urlencode({
|
||||
"grant_type": "client_credentials",
|
||||
"client_id": str(cfg.get("client_id", "")),
|
||||
"client_secret": str(cfg.get("client_secret", "")),
|
||||
"scope": "https://management.azure.com/.default",
|
||||
}).encode("utf-8")
|
||||
req = urllib.request.Request(url, data=form, headers={"Content-Type": "application/x-www-form-urlencoded"}, method="POST")
|
||||
with urllib.request.urlopen(req, timeout=60) as r:
|
||||
tok = json.loads(r.read())
|
||||
if not tok.get("access_token"):
|
||||
raise Exception("Token request failed: " + json.dumps(tok))
|
||||
return tok["access_token"]
|
||||
|
||||
|
||||
def arm(method, path, token, api_version, body=None, params=None, full_url=None):
|
||||
url = (full_url if full_url else ARM + path)
|
||||
qp = {"api-version": api_version}
|
||||
if params:
|
||||
qp.update({k: v for k, v in params.items() if v not in (None, "")})
|
||||
url += ("&" if "?" in url else "?") + urllib.parse.urlencode(qp)
|
||||
data = json.dumps(body).encode("utf-8") if body is not None else None
|
||||
headers = {"Authorization": "Bearer " + token, "Accept": "application/json"}
|
||||
if data is not None:
|
||||
headers["Content-Type"] = "application/json"
|
||||
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 _run(fn):
|
||||
try:
|
||||
cfg = _cfg()
|
||||
inputs = _inputs()
|
||||
token = _token(cfg)
|
||||
print(json.dumps(fn(cfg, token, inputs)))
|
||||
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)
|
||||
|
||||
|
||||
sub = lambda cfg: str(cfg.get("subscription_id", ""))
|
||||
|
||||
|
||||
def main(cfg, token, inputs):
|
||||
path = "/subscriptions/" + sub(cfg) + "/providers/Microsoft.Security/alerts"
|
||||
return arm("GET", path, token, "2022-01-01")
|
||||
|
||||
|
||||
_run(main)
|
||||
@@ -0,0 +1,68 @@
|
||||
import json, os, sys, urllib.parse, urllib.request, urllib.error
|
||||
|
||||
ARM = "https://management.azure.com"
|
||||
|
||||
|
||||
def _cfg():
|
||||
return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||
|
||||
|
||||
def _inputs():
|
||||
return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||
|
||||
|
||||
def _token(cfg):
|
||||
url = "https://login.microsoftonline.com/" + str(cfg.get("tenant_id", "")) + "/oauth2/v2.0/token"
|
||||
form = urllib.parse.urlencode({
|
||||
"grant_type": "client_credentials",
|
||||
"client_id": str(cfg.get("client_id", "")),
|
||||
"client_secret": str(cfg.get("client_secret", "")),
|
||||
"scope": "https://management.azure.com/.default",
|
||||
}).encode("utf-8")
|
||||
req = urllib.request.Request(url, data=form, headers={"Content-Type": "application/x-www-form-urlencoded"}, method="POST")
|
||||
with urllib.request.urlopen(req, timeout=60) as r:
|
||||
tok = json.loads(r.read())
|
||||
if not tok.get("access_token"):
|
||||
raise Exception("Token request failed: " + json.dumps(tok))
|
||||
return tok["access_token"]
|
||||
|
||||
|
||||
def arm(method, path, token, api_version, body=None, params=None, full_url=None):
|
||||
url = (full_url if full_url else ARM + path)
|
||||
qp = {"api-version": api_version}
|
||||
if params:
|
||||
qp.update({k: v for k, v in params.items() if v not in (None, "")})
|
||||
url += ("&" if "?" in url else "?") + urllib.parse.urlencode(qp)
|
||||
data = json.dumps(body).encode("utf-8") if body is not None else None
|
||||
headers = {"Authorization": "Bearer " + token, "Accept": "application/json"}
|
||||
if data is not None:
|
||||
headers["Content-Type"] = "application/json"
|
||||
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 _run(fn):
|
||||
try:
|
||||
cfg = _cfg()
|
||||
inputs = _inputs()
|
||||
token = _token(cfg)
|
||||
print(json.dumps(fn(cfg, token, inputs)))
|
||||
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)
|
||||
|
||||
|
||||
sub = lambda cfg: str(cfg.get("subscription_id", ""))
|
||||
|
||||
|
||||
def main(cfg, token, inputs):
|
||||
path = "/subscriptions/" + sub(cfg) + "/providers/Microsoft.Network/networkSecurityGroups"
|
||||
return arm("GET", path, token, "2023-09-01")
|
||||
|
||||
|
||||
_run(main)
|
||||
@@ -0,0 +1,69 @@
|
||||
import json, os, sys, urllib.parse, urllib.request, urllib.error
|
||||
|
||||
ARM = "https://management.azure.com"
|
||||
|
||||
|
||||
def _cfg():
|
||||
return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||
|
||||
|
||||
def _inputs():
|
||||
return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||
|
||||
|
||||
def _token(cfg):
|
||||
url = "https://login.microsoftonline.com/" + str(cfg.get("tenant_id", "")) + "/oauth2/v2.0/token"
|
||||
form = urllib.parse.urlencode({
|
||||
"grant_type": "client_credentials",
|
||||
"client_id": str(cfg.get("client_id", "")),
|
||||
"client_secret": str(cfg.get("client_secret", "")),
|
||||
"scope": "https://management.azure.com/.default",
|
||||
}).encode("utf-8")
|
||||
req = urllib.request.Request(url, data=form, headers={"Content-Type": "application/x-www-form-urlencoded"}, method="POST")
|
||||
with urllib.request.urlopen(req, timeout=60) as r:
|
||||
tok = json.loads(r.read())
|
||||
if not tok.get("access_token"):
|
||||
raise Exception("Token request failed: " + json.dumps(tok))
|
||||
return tok["access_token"]
|
||||
|
||||
|
||||
def arm(method, path, token, api_version, body=None, params=None, full_url=None):
|
||||
url = (full_url if full_url else ARM + path)
|
||||
qp = {"api-version": api_version}
|
||||
if params:
|
||||
qp.update({k: v for k, v in params.items() if v not in (None, "")})
|
||||
url += ("&" if "?" in url else "?") + urllib.parse.urlencode(qp)
|
||||
data = json.dumps(body).encode("utf-8") if body is not None else None
|
||||
headers = {"Authorization": "Bearer " + token, "Accept": "application/json"}
|
||||
if data is not None:
|
||||
headers["Content-Type"] = "application/json"
|
||||
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 _run(fn):
|
||||
try:
|
||||
cfg = _cfg()
|
||||
inputs = _inputs()
|
||||
token = _token(cfg)
|
||||
print(json.dumps(fn(cfg, token, inputs)))
|
||||
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)
|
||||
|
||||
|
||||
sub = lambda cfg: str(cfg.get("subscription_id", ""))
|
||||
|
||||
|
||||
def main(cfg, token, inputs):
|
||||
path = "/subscriptions/" + sub(cfg)
|
||||
arm("GET", path, token, "2022-12-01")
|
||||
return {"ok": True}
|
||||
|
||||
|
||||
_run(main)
|
||||
@@ -0,0 +1,78 @@
|
||||
import json, os, sys, urllib.parse, urllib.request, urllib.error
|
||||
|
||||
ARM = "https://management.azure.com"
|
||||
|
||||
|
||||
def _cfg():
|
||||
return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||
|
||||
|
||||
def _inputs():
|
||||
return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||
|
||||
|
||||
def _token(cfg):
|
||||
url = "https://login.microsoftonline.com/" + str(cfg.get("tenant_id", "")) + "/oauth2/v2.0/token"
|
||||
form = urllib.parse.urlencode({
|
||||
"grant_type": "client_credentials",
|
||||
"client_id": str(cfg.get("client_id", "")),
|
||||
"client_secret": str(cfg.get("client_secret", "")),
|
||||
"scope": "https://management.azure.com/.default",
|
||||
}).encode("utf-8")
|
||||
req = urllib.request.Request(url, data=form, headers={"Content-Type": "application/x-www-form-urlencoded"}, method="POST")
|
||||
with urllib.request.urlopen(req, timeout=60) as r:
|
||||
tok = json.loads(r.read())
|
||||
if not tok.get("access_token"):
|
||||
raise Exception("Token request failed: " + json.dumps(tok))
|
||||
return tok["access_token"]
|
||||
|
||||
|
||||
def arm(method, path, token, api_version, body=None, params=None, full_url=None):
|
||||
url = (full_url if full_url else ARM + path)
|
||||
qp = {"api-version": api_version}
|
||||
if params:
|
||||
qp.update({k: v for k, v in params.items() if v not in (None, "")})
|
||||
url += ("&" if "?" in url else "?") + urllib.parse.urlencode(qp)
|
||||
data = json.dumps(body).encode("utf-8") if body is not None else None
|
||||
headers = {"Authorization": "Bearer " + token, "Accept": "application/json"}
|
||||
if data is not None:
|
||||
headers["Content-Type"] = "application/json"
|
||||
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 _run(fn):
|
||||
try:
|
||||
cfg = _cfg()
|
||||
inputs = _inputs()
|
||||
token = _token(cfg)
|
||||
print(json.dumps(fn(cfg, token, inputs)))
|
||||
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)
|
||||
|
||||
|
||||
VALID_STATES = ("dismiss", "resolve", "activate", "inProgress")
|
||||
|
||||
|
||||
def main(cfg, token, inputs):
|
||||
alert_id = inputs.get("alert_id")
|
||||
if not alert_id:
|
||||
raise Exception("alert_id is required")
|
||||
state = inputs.get("state")
|
||||
if not state:
|
||||
raise Exception("state is required")
|
||||
if state not in VALID_STATES:
|
||||
raise Exception("state must be one of: " + ", ".join(VALID_STATES))
|
||||
result = arm("POST", None, token, "2022-01-01", body=None, full_url=ARM + str(alert_id) + "/" + state)
|
||||
if not result:
|
||||
return {"ok": True, "state": state}
|
||||
return result
|
||||
|
||||
|
||||
_run(main)
|
||||
@@ -0,0 +1,77 @@
|
||||
id: beyondtrust_password_safe
|
||||
name: BeyondTrust Password Safe
|
||||
version: 1.0.0
|
||||
description: "BeyondTrust Password Safe (Secrets Safe REST API v3) — privileged access and credential retrieval: list managed accounts and systems, request a credential release, and retrieve the credential. API-key (PS-Auth) session authentication; stdlib-only, no extra Python dependencies."
|
||||
changelog: "1.0.0 — Initial release: list managed accounts/systems, create release request, get credential."
|
||||
category: identity
|
||||
|
||||
# Per-instance configuration. Auth signs in with an API key + runas user
|
||||
# (header 'Authorization: PS-Auth key=<api_key>; runas=<runas_user>;'), which
|
||||
# establishes a session reused for the request.
|
||||
config_schema:
|
||||
properties:
|
||||
base_url:
|
||||
type: string
|
||||
description: "BeyondTrust URL (e.g. https://beyondtrust.example.com)"
|
||||
api_key:
|
||||
type: string
|
||||
description: "API registration key"
|
||||
x-soar-sensitive: true
|
||||
runas_user:
|
||||
type: string
|
||||
description: "Username to run as"
|
||||
insecure:
|
||||
type: boolean
|
||||
description: "Trust any TLS certificate (not secure)"
|
||||
default: false
|
||||
required:
|
||||
- base_url
|
||||
- api_key
|
||||
- runas_user
|
||||
|
||||
commands:
|
||||
- id: list_managed_accounts
|
||||
name: beyondtrust-list-managed-accounts
|
||||
description: "List managed accounts."
|
||||
risk: read
|
||||
inputs_schema:
|
||||
properties:
|
||||
account_name: { type: string, description: "Optional account name filter" }
|
||||
required: []
|
||||
outputs_schema: { properties: {} }
|
||||
- id: list_managed_systems
|
||||
name: beyondtrust-list-managed-systems
|
||||
description: "List managed systems."
|
||||
risk: read
|
||||
inputs_schema:
|
||||
properties: {}
|
||||
required: []
|
||||
outputs_schema: { properties: {} }
|
||||
- id: create_release_request
|
||||
name: beyondtrust-create-release-request
|
||||
description: "Request a credential release for a managed account."
|
||||
inputs_schema:
|
||||
properties:
|
||||
system_id: { type: string, description: "Managed system ID" }
|
||||
account_id: { type: string, description: "Managed account ID" }
|
||||
duration_minutes: { type: number, description: "Access duration in minutes (default 30)" }
|
||||
reason: { type: string, description: "Reason for the request" }
|
||||
required: [system_id, account_id]
|
||||
outputs_schema: { properties: {} }
|
||||
- id: get_credential
|
||||
name: beyondtrust-get-credential
|
||||
description: "Retrieve the credential for an approved request."
|
||||
inputs_schema:
|
||||
properties:
|
||||
request_id: { type: string, description: "Request ID (from create-release-request)" }
|
||||
required: [request_id]
|
||||
outputs_schema: { properties: {} }
|
||||
|
||||
- id: test_connection
|
||||
name: beyondtrust-test-connection
|
||||
description: "Verify the sign-in (used by the Test button)."
|
||||
risk: read
|
||||
inputs_schema:
|
||||
properties: {}
|
||||
required: []
|
||||
outputs_schema: { properties: {} }
|
||||
@@ -0,0 +1,103 @@
|
||||
import json, os, sys, ssl, http.cookiejar
|
||||
import urllib.parse, urllib.request, urllib.error
|
||||
|
||||
|
||||
def _cfg():
|
||||
return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||
|
||||
|
||||
def _inputs():
|
||||
return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||
|
||||
|
||||
def _ctx(cfg):
|
||||
if cfg.get("insecure"):
|
||||
c = ssl.create_default_context()
|
||||
c.check_hostname = False
|
||||
c.verify_mode = ssl.CERT_NONE
|
||||
return c
|
||||
return None
|
||||
|
||||
|
||||
class Client:
|
||||
def __init__(self, cfg):
|
||||
self.cfg = cfg
|
||||
self.base = str(cfg.get("base_url", "")).rstrip("/") + "/BeyondTrust/api/public/v3"
|
||||
ctx = _ctx(cfg)
|
||||
self.opener = urllib.request.build_opener(
|
||||
urllib.request.HTTPCookieProcessor(http.cookiejar.CookieJar()),
|
||||
urllib.request.HTTPSHandler(context=ctx) if ctx else urllib.request.HTTPSHandler(),
|
||||
)
|
||||
|
||||
def _auth_header(self):
|
||||
return "PS-Auth key=" + str(self.cfg.get("api_key", "")) + "; runas=" + str(self.cfg.get("runas_user", "")) + ";"
|
||||
|
||||
def signin(self):
|
||||
req = urllib.request.Request(self.base + "/Auth/SignAppin", data=b"",
|
||||
headers={"Authorization": self._auth_header(), "Accept": "application/json"}, method="POST")
|
||||
with self.opener.open(req, timeout=60) as r:
|
||||
raw = r.read()
|
||||
return json.loads(raw) if raw else {}
|
||||
|
||||
def call(self, method, path, body=None, params=None):
|
||||
url = self.base + path
|
||||
if params:
|
||||
clean = {k: v for k, v in params.items() if v not in (None, "")}
|
||||
if clean:
|
||||
url += "?" + urllib.parse.urlencode(clean)
|
||||
data = json.dumps(body).encode("utf-8") if body is not None else None
|
||||
headers = {"Accept": "application/json", "Authorization": self._auth_header()}
|
||||
if data is not None:
|
||||
headers["Content-Type"] = "application/json"
|
||||
req = urllib.request.Request(url, data=data, headers=headers, method=method)
|
||||
with self.opener.open(req, timeout=60) as r:
|
||||
raw = r.read()
|
||||
return json.loads(raw) if raw else {}
|
||||
|
||||
def signout(self):
|
||||
try:
|
||||
req = urllib.request.Request(self.base + "/Auth/Signout", data=b"", method="POST")
|
||||
self.opener.open(req, timeout=30).read()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def _run(fn):
|
||||
try:
|
||||
cfg = _cfg()
|
||||
inputs = _inputs()
|
||||
client = Client(cfg)
|
||||
client.signin()
|
||||
try:
|
||||
result = fn(client, inputs)
|
||||
finally:
|
||||
client.signout()
|
||||
print(json.dumps(result))
|
||||
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)
|
||||
|
||||
|
||||
def main(client, inputs):
|
||||
system_id = inputs.get("system_id")
|
||||
account_id = inputs.get("account_id")
|
||||
duration_minutes = inputs.get("duration_minutes")
|
||||
reason = inputs.get("reason")
|
||||
if not system_id:
|
||||
raise Exception("system_id is required")
|
||||
if not account_id:
|
||||
raise Exception("account_id is required")
|
||||
body = {
|
||||
"SystemId": int(system_id),
|
||||
"AccountId": int(account_id),
|
||||
"DurationMinutes": int(duration_minutes) if duration_minutes else 30,
|
||||
"Reason": reason or "Riposte SOAR",
|
||||
"AccessType": "View",
|
||||
}
|
||||
return client.call("POST", "/Requests", body=body)
|
||||
|
||||
|
||||
_run(main)
|
||||
@@ -0,0 +1,93 @@
|
||||
import json, os, sys, ssl, http.cookiejar
|
||||
import urllib.parse, urllib.request, urllib.error
|
||||
|
||||
|
||||
def _cfg():
|
||||
return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||
|
||||
|
||||
def _inputs():
|
||||
return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||
|
||||
|
||||
def _ctx(cfg):
|
||||
if cfg.get("insecure"):
|
||||
c = ssl.create_default_context()
|
||||
c.check_hostname = False
|
||||
c.verify_mode = ssl.CERT_NONE
|
||||
return c
|
||||
return None
|
||||
|
||||
|
||||
class Client:
|
||||
def __init__(self, cfg):
|
||||
self.cfg = cfg
|
||||
self.base = str(cfg.get("base_url", "")).rstrip("/") + "/BeyondTrust/api/public/v3"
|
||||
ctx = _ctx(cfg)
|
||||
self.opener = urllib.request.build_opener(
|
||||
urllib.request.HTTPCookieProcessor(http.cookiejar.CookieJar()),
|
||||
urllib.request.HTTPSHandler(context=ctx) if ctx else urllib.request.HTTPSHandler(),
|
||||
)
|
||||
|
||||
def _auth_header(self):
|
||||
return "PS-Auth key=" + str(self.cfg.get("api_key", "")) + "; runas=" + str(self.cfg.get("runas_user", "")) + ";"
|
||||
|
||||
def signin(self):
|
||||
req = urllib.request.Request(self.base + "/Auth/SignAppin", data=b"",
|
||||
headers={"Authorization": self._auth_header(), "Accept": "application/json"}, method="POST")
|
||||
with self.opener.open(req, timeout=60) as r:
|
||||
raw = r.read()
|
||||
return json.loads(raw) if raw else {}
|
||||
|
||||
def call(self, method, path, body=None, params=None):
|
||||
url = self.base + path
|
||||
if params:
|
||||
clean = {k: v for k, v in params.items() if v not in (None, "")}
|
||||
if clean:
|
||||
url += "?" + urllib.parse.urlencode(clean)
|
||||
data = json.dumps(body).encode("utf-8") if body is not None else None
|
||||
headers = {"Accept": "application/json", "Authorization": self._auth_header()}
|
||||
if data is not None:
|
||||
headers["Content-Type"] = "application/json"
|
||||
req = urllib.request.Request(url, data=data, headers=headers, method=method)
|
||||
with self.opener.open(req, timeout=60) as r:
|
||||
raw = r.read()
|
||||
return json.loads(raw) if raw else {}
|
||||
|
||||
def signout(self):
|
||||
try:
|
||||
req = urllib.request.Request(self.base + "/Auth/Signout", data=b"", method="POST")
|
||||
self.opener.open(req, timeout=30).read()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def _run(fn):
|
||||
try:
|
||||
cfg = _cfg()
|
||||
inputs = _inputs()
|
||||
client = Client(cfg)
|
||||
client.signin()
|
||||
try:
|
||||
result = fn(client, inputs)
|
||||
finally:
|
||||
client.signout()
|
||||
print(json.dumps(result))
|
||||
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)
|
||||
|
||||
|
||||
def main(client, inputs):
|
||||
request_id = inputs.get("request_id")
|
||||
if not request_id:
|
||||
raise Exception("request_id is required")
|
||||
q = lambda v: urllib.parse.quote(str(v), safe="")
|
||||
response = client.call("GET", "/Credentials/" + q(request_id))
|
||||
return {"request_id": request_id, "credential": response}
|
||||
|
||||
|
||||
_run(main)
|
||||
@@ -0,0 +1,89 @@
|
||||
import json, os, sys, ssl, http.cookiejar
|
||||
import urllib.parse, urllib.request, urllib.error
|
||||
|
||||
|
||||
def _cfg():
|
||||
return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||
|
||||
|
||||
def _inputs():
|
||||
return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||
|
||||
|
||||
def _ctx(cfg):
|
||||
if cfg.get("insecure"):
|
||||
c = ssl.create_default_context()
|
||||
c.check_hostname = False
|
||||
c.verify_mode = ssl.CERT_NONE
|
||||
return c
|
||||
return None
|
||||
|
||||
|
||||
class Client:
|
||||
def __init__(self, cfg):
|
||||
self.cfg = cfg
|
||||
self.base = str(cfg.get("base_url", "")).rstrip("/") + "/BeyondTrust/api/public/v3"
|
||||
ctx = _ctx(cfg)
|
||||
self.opener = urllib.request.build_opener(
|
||||
urllib.request.HTTPCookieProcessor(http.cookiejar.CookieJar()),
|
||||
urllib.request.HTTPSHandler(context=ctx) if ctx else urllib.request.HTTPSHandler(),
|
||||
)
|
||||
|
||||
def _auth_header(self):
|
||||
return "PS-Auth key=" + str(self.cfg.get("api_key", "")) + "; runas=" + str(self.cfg.get("runas_user", "")) + ";"
|
||||
|
||||
def signin(self):
|
||||
req = urllib.request.Request(self.base + "/Auth/SignAppin", data=b"",
|
||||
headers={"Authorization": self._auth_header(), "Accept": "application/json"}, method="POST")
|
||||
with self.opener.open(req, timeout=60) as r:
|
||||
raw = r.read()
|
||||
return json.loads(raw) if raw else {}
|
||||
|
||||
def call(self, method, path, body=None, params=None):
|
||||
url = self.base + path
|
||||
if params:
|
||||
clean = {k: v for k, v in params.items() if v not in (None, "")}
|
||||
if clean:
|
||||
url += "?" + urllib.parse.urlencode(clean)
|
||||
data = json.dumps(body).encode("utf-8") if body is not None else None
|
||||
headers = {"Accept": "application/json", "Authorization": self._auth_header()}
|
||||
if data is not None:
|
||||
headers["Content-Type"] = "application/json"
|
||||
req = urllib.request.Request(url, data=data, headers=headers, method=method)
|
||||
with self.opener.open(req, timeout=60) as r:
|
||||
raw = r.read()
|
||||
return json.loads(raw) if raw else {}
|
||||
|
||||
def signout(self):
|
||||
try:
|
||||
req = urllib.request.Request(self.base + "/Auth/Signout", data=b"", method="POST")
|
||||
self.opener.open(req, timeout=30).read()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def _run(fn):
|
||||
try:
|
||||
cfg = _cfg()
|
||||
inputs = _inputs()
|
||||
client = Client(cfg)
|
||||
client.signin()
|
||||
try:
|
||||
result = fn(client, inputs)
|
||||
finally:
|
||||
client.signout()
|
||||
print(json.dumps(result))
|
||||
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)
|
||||
|
||||
|
||||
def main(client, inputs):
|
||||
account_name = inputs.get("account_name")
|
||||
return client.call("GET", "/ManagedAccounts", params={"accountName": account_name})
|
||||
|
||||
|
||||
_run(main)
|
||||
@@ -0,0 +1,88 @@
|
||||
import json, os, sys, ssl, http.cookiejar
|
||||
import urllib.parse, urllib.request, urllib.error
|
||||
|
||||
|
||||
def _cfg():
|
||||
return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||
|
||||
|
||||
def _inputs():
|
||||
return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||
|
||||
|
||||
def _ctx(cfg):
|
||||
if cfg.get("insecure"):
|
||||
c = ssl.create_default_context()
|
||||
c.check_hostname = False
|
||||
c.verify_mode = ssl.CERT_NONE
|
||||
return c
|
||||
return None
|
||||
|
||||
|
||||
class Client:
|
||||
def __init__(self, cfg):
|
||||
self.cfg = cfg
|
||||
self.base = str(cfg.get("base_url", "")).rstrip("/") + "/BeyondTrust/api/public/v3"
|
||||
ctx = _ctx(cfg)
|
||||
self.opener = urllib.request.build_opener(
|
||||
urllib.request.HTTPCookieProcessor(http.cookiejar.CookieJar()),
|
||||
urllib.request.HTTPSHandler(context=ctx) if ctx else urllib.request.HTTPSHandler(),
|
||||
)
|
||||
|
||||
def _auth_header(self):
|
||||
return "PS-Auth key=" + str(self.cfg.get("api_key", "")) + "; runas=" + str(self.cfg.get("runas_user", "")) + ";"
|
||||
|
||||
def signin(self):
|
||||
req = urllib.request.Request(self.base + "/Auth/SignAppin", data=b"",
|
||||
headers={"Authorization": self._auth_header(), "Accept": "application/json"}, method="POST")
|
||||
with self.opener.open(req, timeout=60) as r:
|
||||
raw = r.read()
|
||||
return json.loads(raw) if raw else {}
|
||||
|
||||
def call(self, method, path, body=None, params=None):
|
||||
url = self.base + path
|
||||
if params:
|
||||
clean = {k: v for k, v in params.items() if v not in (None, "")}
|
||||
if clean:
|
||||
url += "?" + urllib.parse.urlencode(clean)
|
||||
data = json.dumps(body).encode("utf-8") if body is not None else None
|
||||
headers = {"Accept": "application/json", "Authorization": self._auth_header()}
|
||||
if data is not None:
|
||||
headers["Content-Type"] = "application/json"
|
||||
req = urllib.request.Request(url, data=data, headers=headers, method=method)
|
||||
with self.opener.open(req, timeout=60) as r:
|
||||
raw = r.read()
|
||||
return json.loads(raw) if raw else {}
|
||||
|
||||
def signout(self):
|
||||
try:
|
||||
req = urllib.request.Request(self.base + "/Auth/Signout", data=b"", method="POST")
|
||||
self.opener.open(req, timeout=30).read()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def _run(fn):
|
||||
try:
|
||||
cfg = _cfg()
|
||||
inputs = _inputs()
|
||||
client = Client(cfg)
|
||||
client.signin()
|
||||
try:
|
||||
result = fn(client, inputs)
|
||||
finally:
|
||||
client.signout()
|
||||
print(json.dumps(result))
|
||||
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)
|
||||
|
||||
|
||||
def main(client, inputs):
|
||||
return client.call("GET", "/ManagedSystems")
|
||||
|
||||
|
||||
_run(main)
|
||||
@@ -0,0 +1,89 @@
|
||||
import json, os, sys, ssl, http.cookiejar
|
||||
import urllib.parse, urllib.request, urllib.error
|
||||
|
||||
|
||||
def _cfg():
|
||||
return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||
|
||||
|
||||
def _inputs():
|
||||
return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||
|
||||
|
||||
def _ctx(cfg):
|
||||
if cfg.get("insecure"):
|
||||
c = ssl.create_default_context()
|
||||
c.check_hostname = False
|
||||
c.verify_mode = ssl.CERT_NONE
|
||||
return c
|
||||
return None
|
||||
|
||||
|
||||
class Client:
|
||||
def __init__(self, cfg):
|
||||
self.cfg = cfg
|
||||
self.base = str(cfg.get("base_url", "")).rstrip("/") + "/BeyondTrust/api/public/v3"
|
||||
ctx = _ctx(cfg)
|
||||
self.opener = urllib.request.build_opener(
|
||||
urllib.request.HTTPCookieProcessor(http.cookiejar.CookieJar()),
|
||||
urllib.request.HTTPSHandler(context=ctx) if ctx else urllib.request.HTTPSHandler(),
|
||||
)
|
||||
|
||||
def _auth_header(self):
|
||||
return "PS-Auth key=" + str(self.cfg.get("api_key", "")) + "; runas=" + str(self.cfg.get("runas_user", "")) + ";"
|
||||
|
||||
def signin(self):
|
||||
req = urllib.request.Request(self.base + "/Auth/SignAppin", data=b"",
|
||||
headers={"Authorization": self._auth_header(), "Accept": "application/json"}, method="POST")
|
||||
with self.opener.open(req, timeout=60) as r:
|
||||
raw = r.read()
|
||||
return json.loads(raw) if raw else {}
|
||||
|
||||
def call(self, method, path, body=None, params=None):
|
||||
url = self.base + path
|
||||
if params:
|
||||
clean = {k: v for k, v in params.items() if v not in (None, "")}
|
||||
if clean:
|
||||
url += "?" + urllib.parse.urlencode(clean)
|
||||
data = json.dumps(body).encode("utf-8") if body is not None else None
|
||||
headers = {"Accept": "application/json", "Authorization": self._auth_header()}
|
||||
if data is not None:
|
||||
headers["Content-Type"] = "application/json"
|
||||
req = urllib.request.Request(url, data=data, headers=headers, method=method)
|
||||
with self.opener.open(req, timeout=60) as r:
|
||||
raw = r.read()
|
||||
return json.loads(raw) if raw else {}
|
||||
|
||||
def signout(self):
|
||||
try:
|
||||
req = urllib.request.Request(self.base + "/Auth/Signout", data=b"", method="POST")
|
||||
self.opener.open(req, timeout=30).read()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def _run(fn):
|
||||
try:
|
||||
cfg = _cfg()
|
||||
inputs = _inputs()
|
||||
client = Client(cfg)
|
||||
client.signin()
|
||||
try:
|
||||
result = fn(client, inputs)
|
||||
finally:
|
||||
client.signout()
|
||||
print(json.dumps(result))
|
||||
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)
|
||||
|
||||
|
||||
def main(client, inputs):
|
||||
client.call("GET", "/ManagedSystems")
|
||||
return {"ok": True}
|
||||
|
||||
|
||||
_run(main)
|
||||
@@ -0,0 +1,74 @@
|
||||
id: binaryedge
|
||||
name: BinaryEdge
|
||||
version: 1.0.0
|
||||
description: "BinaryEdge (API v2) — internet exposure intelligence: query current and historical open ports/services for an IP, run a search, enumerate a domain's subdomains, check an email against data leaks, and read the subscription quota. API-key authentication; stdlib-only, no extra Python dependencies."
|
||||
changelog: "1.0.0 — Initial release: host lookup (current/historical), search, domain subdomains, data-leak email check, subscription."
|
||||
category: enrichment
|
||||
|
||||
# Per-instance configuration. The API key is sent as the 'X-Key' header.
|
||||
config_schema:
|
||||
properties:
|
||||
api_key:
|
||||
type: string
|
||||
description: "BinaryEdge API key"
|
||||
x-soar-sensitive: true
|
||||
required:
|
||||
- api_key
|
||||
|
||||
commands:
|
||||
- id: host
|
||||
name: binaryedge-host
|
||||
description: "Get the most recent open ports and services for an IP."
|
||||
risk: read
|
||||
inputs_schema:
|
||||
properties:
|
||||
ip: { type: string, description: "IP address" }
|
||||
required: [ip]
|
||||
outputs_schema: { properties: {} }
|
||||
- id: host_historical
|
||||
name: binaryedge-host-historical
|
||||
description: "Get historical open ports and services for an IP."
|
||||
risk: read
|
||||
inputs_schema:
|
||||
properties:
|
||||
ip: { type: string, description: "IP address" }
|
||||
required: [ip]
|
||||
outputs_schema: { properties: {} }
|
||||
- id: search
|
||||
name: binaryedge-search
|
||||
description: "Search hosts/services by a BinaryEdge query."
|
||||
risk: read
|
||||
inputs_schema:
|
||||
properties:
|
||||
query: { type: string, description: "BinaryEdge search query (e.g. type:elasticsearch)" }
|
||||
page: { type: number, description: "Page number (default 1)" }
|
||||
required: [query]
|
||||
outputs_schema: { properties: {} }
|
||||
- id: domain_subdomains
|
||||
name: binaryedge-domain-subdomains
|
||||
description: "List a domain's known subdomains."
|
||||
risk: read
|
||||
inputs_schema:
|
||||
properties:
|
||||
domain: { type: string, description: "Domain name" }
|
||||
page: { type: number, description: "Page number (default 1)" }
|
||||
required: [domain]
|
||||
outputs_schema: { properties: {} }
|
||||
- id: dataleaks_email
|
||||
name: binaryedge-dataleaks-email
|
||||
description: "Check whether an email appears in known data leaks."
|
||||
risk: read
|
||||
inputs_schema:
|
||||
properties:
|
||||
email: { type: string, description: "Email address" }
|
||||
required: [email]
|
||||
outputs_schema: { properties: {} }
|
||||
|
||||
- id: test_connection
|
||||
name: binaryedge-test-connection
|
||||
description: "Verify the API key via the subscription endpoint (used by the Test button)."
|
||||
risk: read
|
||||
inputs_schema:
|
||||
properties: {}
|
||||
required: []
|
||||
outputs_schema: { properties: {} }
|
||||
@@ -0,0 +1,47 @@
|
||||
import json, os, sys, urllib.parse, urllib.request, urllib.error
|
||||
|
||||
BASE = "https://api.binaryedge.io/v2"
|
||||
|
||||
|
||||
def _cfg():
|
||||
return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||
|
||||
|
||||
def _inputs():
|
||||
return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||
|
||||
|
||||
def request(path, cfg, params=None):
|
||||
url = BASE + path
|
||||
if params:
|
||||
clean = {k: v for k, v in params.items() if v not in (None, "")}
|
||||
if clean:
|
||||
url += "?" + urllib.parse.urlencode(clean)
|
||||
req = urllib.request.Request(url, headers={"X-Key": str(cfg.get("api_key", "")), "Accept": "application/json"}, method="GET")
|
||||
with urllib.request.urlopen(req, timeout=60) as r:
|
||||
raw = r.read()
|
||||
return json.loads(raw) if raw else {}
|
||||
|
||||
|
||||
def _run(fn):
|
||||
try:
|
||||
print(json.dumps(fn(_cfg(), _inputs())))
|
||||
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)
|
||||
|
||||
|
||||
q = lambda v: urllib.parse.quote(str(v), safe="")
|
||||
|
||||
|
||||
def main(cfg, inputs):
|
||||
email = inputs.get("email")
|
||||
if not email:
|
||||
raise Exception("email is required")
|
||||
return request("/query/dataleaks/email/" + q(email), cfg)
|
||||
|
||||
|
||||
_run(main)
|
||||
@@ -0,0 +1,48 @@
|
||||
import json, os, sys, urllib.parse, urllib.request, urllib.error
|
||||
|
||||
BASE = "https://api.binaryedge.io/v2"
|
||||
|
||||
|
||||
def _cfg():
|
||||
return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||
|
||||
|
||||
def _inputs():
|
||||
return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||
|
||||
|
||||
def request(path, cfg, params=None):
|
||||
url = BASE + path
|
||||
if params:
|
||||
clean = {k: v for k, v in params.items() if v not in (None, "")}
|
||||
if clean:
|
||||
url += "?" + urllib.parse.urlencode(clean)
|
||||
req = urllib.request.Request(url, headers={"X-Key": str(cfg.get("api_key", "")), "Accept": "application/json"}, method="GET")
|
||||
with urllib.request.urlopen(req, timeout=60) as r:
|
||||
raw = r.read()
|
||||
return json.loads(raw) if raw else {}
|
||||
|
||||
|
||||
def _run(fn):
|
||||
try:
|
||||
print(json.dumps(fn(_cfg(), _inputs())))
|
||||
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)
|
||||
|
||||
|
||||
q = lambda v: urllib.parse.quote(str(v), safe="")
|
||||
|
||||
|
||||
def main(cfg, inputs):
|
||||
domain = inputs.get("domain")
|
||||
if not domain:
|
||||
raise Exception("domain is required")
|
||||
page = inputs.get("page")
|
||||
return request("/query/domains/subdomain/" + q(domain), cfg, params={"page": int(page or 1)})
|
||||
|
||||
|
||||
_run(main)
|
||||
@@ -0,0 +1,47 @@
|
||||
import json, os, sys, urllib.parse, urllib.request, urllib.error
|
||||
|
||||
BASE = "https://api.binaryedge.io/v2"
|
||||
|
||||
|
||||
def _cfg():
|
||||
return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||
|
||||
|
||||
def _inputs():
|
||||
return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||
|
||||
|
||||
def request(path, cfg, params=None):
|
||||
url = BASE + path
|
||||
if params:
|
||||
clean = {k: v for k, v in params.items() if v not in (None, "")}
|
||||
if clean:
|
||||
url += "?" + urllib.parse.urlencode(clean)
|
||||
req = urllib.request.Request(url, headers={"X-Key": str(cfg.get("api_key", "")), "Accept": "application/json"}, method="GET")
|
||||
with urllib.request.urlopen(req, timeout=60) as r:
|
||||
raw = r.read()
|
||||
return json.loads(raw) if raw else {}
|
||||
|
||||
|
||||
def _run(fn):
|
||||
try:
|
||||
print(json.dumps(fn(_cfg(), _inputs())))
|
||||
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)
|
||||
|
||||
|
||||
q = lambda v: urllib.parse.quote(str(v), safe="")
|
||||
|
||||
|
||||
def main(cfg, inputs):
|
||||
ip = inputs.get("ip")
|
||||
if not ip:
|
||||
raise Exception("ip is required")
|
||||
return request("/query/ip/" + q(ip), cfg)
|
||||
|
||||
|
||||
_run(main)
|
||||
@@ -0,0 +1,47 @@
|
||||
import json, os, sys, urllib.parse, urllib.request, urllib.error
|
||||
|
||||
BASE = "https://api.binaryedge.io/v2"
|
||||
|
||||
|
||||
def _cfg():
|
||||
return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||
|
||||
|
||||
def _inputs():
|
||||
return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||
|
||||
|
||||
def request(path, cfg, params=None):
|
||||
url = BASE + path
|
||||
if params:
|
||||
clean = {k: v for k, v in params.items() if v not in (None, "")}
|
||||
if clean:
|
||||
url += "?" + urllib.parse.urlencode(clean)
|
||||
req = urllib.request.Request(url, headers={"X-Key": str(cfg.get("api_key", "")), "Accept": "application/json"}, method="GET")
|
||||
with urllib.request.urlopen(req, timeout=60) as r:
|
||||
raw = r.read()
|
||||
return json.loads(raw) if raw else {}
|
||||
|
||||
|
||||
def _run(fn):
|
||||
try:
|
||||
print(json.dumps(fn(_cfg(), _inputs())))
|
||||
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)
|
||||
|
||||
|
||||
q = lambda v: urllib.parse.quote(str(v), safe="")
|
||||
|
||||
|
||||
def main(cfg, inputs):
|
||||
ip = inputs.get("ip")
|
||||
if not ip:
|
||||
raise Exception("ip is required")
|
||||
return request("/query/ip/historical/" + q(ip), cfg)
|
||||
|
||||
|
||||
_run(main)
|
||||
@@ -0,0 +1,48 @@
|
||||
import json, os, sys, urllib.parse, urllib.request, urllib.error
|
||||
|
||||
BASE = "https://api.binaryedge.io/v2"
|
||||
|
||||
|
||||
def _cfg():
|
||||
return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||
|
||||
|
||||
def _inputs():
|
||||
return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||
|
||||
|
||||
def request(path, cfg, params=None):
|
||||
url = BASE + path
|
||||
if params:
|
||||
clean = {k: v for k, v in params.items() if v not in (None, "")}
|
||||
if clean:
|
||||
url += "?" + urllib.parse.urlencode(clean)
|
||||
req = urllib.request.Request(url, headers={"X-Key": str(cfg.get("api_key", "")), "Accept": "application/json"}, method="GET")
|
||||
with urllib.request.urlopen(req, timeout=60) as r:
|
||||
raw = r.read()
|
||||
return json.loads(raw) if raw else {}
|
||||
|
||||
|
||||
def _run(fn):
|
||||
try:
|
||||
print(json.dumps(fn(_cfg(), _inputs())))
|
||||
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)
|
||||
|
||||
|
||||
q = lambda v: urllib.parse.quote(str(v), safe="")
|
||||
|
||||
|
||||
def main(cfg, inputs):
|
||||
query = inputs.get("query")
|
||||
if not query:
|
||||
raise Exception("query is required")
|
||||
page = inputs.get("page")
|
||||
return request("/query/search", cfg, params={"query": query, "page": int(page or 1)})
|
||||
|
||||
|
||||
_run(main)
|
||||
@@ -0,0 +1,45 @@
|
||||
import json, os, sys, urllib.parse, urllib.request, urllib.error
|
||||
|
||||
BASE = "https://api.binaryedge.io/v2"
|
||||
|
||||
|
||||
def _cfg():
|
||||
return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||
|
||||
|
||||
def _inputs():
|
||||
return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||
|
||||
|
||||
def request(path, cfg, params=None):
|
||||
url = BASE + path
|
||||
if params:
|
||||
clean = {k: v for k, v in params.items() if v not in (None, "")}
|
||||
if clean:
|
||||
url += "?" + urllib.parse.urlencode(clean)
|
||||
req = urllib.request.Request(url, headers={"X-Key": str(cfg.get("api_key", "")), "Accept": "application/json"}, method="GET")
|
||||
with urllib.request.urlopen(req, timeout=60) as r:
|
||||
raw = r.read()
|
||||
return json.loads(raw) if raw else {}
|
||||
|
||||
|
||||
def _run(fn):
|
||||
try:
|
||||
print(json.dumps(fn(_cfg(), _inputs())))
|
||||
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)
|
||||
|
||||
|
||||
q = lambda v: urllib.parse.quote(str(v), safe="")
|
||||
|
||||
|
||||
def main(cfg, inputs):
|
||||
request("/user/subscription", cfg)
|
||||
return {"ok": True}
|
||||
|
||||
|
||||
_run(main)
|
||||
@@ -0,0 +1,74 @@
|
||||
id: box
|
||||
name: Box
|
||||
version: 1.0.0
|
||||
description: "Box (Content API v2) — evidence and file handling: search files, read file/folder metadata, list a folder's items, and create a shared link. Bearer-token authentication; stdlib-only, no extra Python dependencies."
|
||||
changelog: "1.0.0 — Initial release: search, get file/folder info, list folder items, create shared link."
|
||||
category: productivity
|
||||
|
||||
# Per-instance configuration. Auth header 'Authorization: Bearer <access_token>'.
|
||||
config_schema:
|
||||
properties:
|
||||
access_token:
|
||||
type: string
|
||||
description: "Box access token (developer token or OAuth2/JWT-issued token)"
|
||||
x-soar-sensitive: true
|
||||
required:
|
||||
- access_token
|
||||
|
||||
commands:
|
||||
- id: search
|
||||
name: box-search
|
||||
description: "Search for files and folders by keyword."
|
||||
risk: read
|
||||
inputs_schema:
|
||||
properties:
|
||||
query: { type: string, description: "Search query" }
|
||||
limit: { type: number, description: "Max results (default 30)" }
|
||||
required: [query]
|
||||
outputs_schema: { properties: {} }
|
||||
- id: get_file_info
|
||||
name: box-get-file-info
|
||||
description: "Get a file's metadata."
|
||||
risk: read
|
||||
inputs_schema:
|
||||
properties:
|
||||
file_id: { type: string, description: "File ID" }
|
||||
required: [file_id]
|
||||
outputs_schema: { properties: {} }
|
||||
- id: get_folder_info
|
||||
name: box-get-folder-info
|
||||
description: "Get a folder's metadata."
|
||||
risk: read
|
||||
inputs_schema:
|
||||
properties:
|
||||
folder_id: { type: string, description: "Folder ID (0 = root)" }
|
||||
required: [folder_id]
|
||||
outputs_schema: { properties: {} }
|
||||
- id: list_folder_items
|
||||
name: box-list-folder-items
|
||||
description: "List the items inside a folder."
|
||||
risk: read
|
||||
inputs_schema:
|
||||
properties:
|
||||
folder_id: { type: string, description: "Folder ID (0 = root)" }
|
||||
limit: { type: number, description: "Max items (default 100)" }
|
||||
required: [folder_id]
|
||||
outputs_schema: { properties: {} }
|
||||
- id: create_shared_link
|
||||
name: box-create-shared-link
|
||||
description: "Create a shared link for a file."
|
||||
inputs_schema:
|
||||
properties:
|
||||
file_id: { type: string, description: "File ID" }
|
||||
access: { type: string, description: "open, company, or collaborators (default company)" }
|
||||
required: [file_id]
|
||||
outputs_schema: { properties: {} }
|
||||
|
||||
- id: test_connection
|
||||
name: box-test-connection
|
||||
description: "Verify the access token (used by the Test button)."
|
||||
risk: read
|
||||
inputs_schema:
|
||||
properties: {}
|
||||
required: []
|
||||
outputs_schema: { properties: {} }
|
||||
@@ -0,0 +1,58 @@
|
||||
import json, os, sys, urllib.parse, urllib.request, urllib.error
|
||||
|
||||
BASE = "https://api.box.com/2.0"
|
||||
|
||||
|
||||
def _cfg():
|
||||
return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||
|
||||
|
||||
def _inputs():
|
||||
return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||
|
||||
|
||||
def request(method, path, cfg, body=None, params=None):
|
||||
url = BASE + path
|
||||
if params:
|
||||
clean = {k: v for k, v in params.items() if v not in (None, "")}
|
||||
if clean:
|
||||
url += "?" + urllib.parse.urlencode(clean)
|
||||
data = json.dumps(body).encode("utf-8") if body is not None else None
|
||||
headers = {"Authorization": "Bearer " + str(cfg.get("access_token", "")), "Accept": "application/json"}
|
||||
if data is not None:
|
||||
headers["Content-Type"] = "application/json"
|
||||
req = urllib.request.Request(url, data=data, headers=headers, method=method)
|
||||
with urllib.request.urlopen(req, timeout=60) as r:
|
||||
raw = r.read()
|
||||
return json.loads(raw) if raw else {}
|
||||
|
||||
|
||||
def _run(fn):
|
||||
try:
|
||||
print(json.dumps(fn(_cfg(), _inputs())))
|
||||
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)
|
||||
|
||||
|
||||
q = lambda v: urllib.parse.quote(str(v), safe="")
|
||||
|
||||
|
||||
def main(cfg, inputs):
|
||||
file_id = inputs.get("file_id")
|
||||
if not file_id:
|
||||
raise Exception("file_id is required")
|
||||
access = inputs.get("access")
|
||||
return request(
|
||||
"PUT",
|
||||
"/files/" + q(file_id),
|
||||
cfg,
|
||||
body={"shared_link": {"access": (access or "company")}},
|
||||
params={"fields": "shared_link"},
|
||||
)
|
||||
|
||||
|
||||
_run(main)
|
||||
@@ -0,0 +1,51 @@
|
||||
import json, os, sys, urllib.parse, urllib.request, urllib.error
|
||||
|
||||
BASE = "https://api.box.com/2.0"
|
||||
|
||||
|
||||
def _cfg():
|
||||
return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||
|
||||
|
||||
def _inputs():
|
||||
return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||
|
||||
|
||||
def request(method, path, cfg, body=None, params=None):
|
||||
url = BASE + path
|
||||
if params:
|
||||
clean = {k: v for k, v in params.items() if v not in (None, "")}
|
||||
if clean:
|
||||
url += "?" + urllib.parse.urlencode(clean)
|
||||
data = json.dumps(body).encode("utf-8") if body is not None else None
|
||||
headers = {"Authorization": "Bearer " + str(cfg.get("access_token", "")), "Accept": "application/json"}
|
||||
if data is not None:
|
||||
headers["Content-Type"] = "application/json"
|
||||
req = urllib.request.Request(url, data=data, headers=headers, method=method)
|
||||
with urllib.request.urlopen(req, timeout=60) as r:
|
||||
raw = r.read()
|
||||
return json.loads(raw) if raw else {}
|
||||
|
||||
|
||||
def _run(fn):
|
||||
try:
|
||||
print(json.dumps(fn(_cfg(), _inputs())))
|
||||
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)
|
||||
|
||||
|
||||
q = lambda v: urllib.parse.quote(str(v), safe="")
|
||||
|
||||
|
||||
def main(cfg, inputs):
|
||||
file_id = inputs.get("file_id")
|
||||
if not file_id:
|
||||
raise Exception("file_id is required")
|
||||
return request("GET", "/files/" + q(file_id), cfg)
|
||||
|
||||
|
||||
_run(main)
|
||||
@@ -0,0 +1,51 @@
|
||||
import json, os, sys, urllib.parse, urllib.request, urllib.error
|
||||
|
||||
BASE = "https://api.box.com/2.0"
|
||||
|
||||
|
||||
def _cfg():
|
||||
return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||
|
||||
|
||||
def _inputs():
|
||||
return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||
|
||||
|
||||
def request(method, path, cfg, body=None, params=None):
|
||||
url = BASE + path
|
||||
if params:
|
||||
clean = {k: v for k, v in params.items() if v not in (None, "")}
|
||||
if clean:
|
||||
url += "?" + urllib.parse.urlencode(clean)
|
||||
data = json.dumps(body).encode("utf-8") if body is not None else None
|
||||
headers = {"Authorization": "Bearer " + str(cfg.get("access_token", "")), "Accept": "application/json"}
|
||||
if data is not None:
|
||||
headers["Content-Type"] = "application/json"
|
||||
req = urllib.request.Request(url, data=data, headers=headers, method=method)
|
||||
with urllib.request.urlopen(req, timeout=60) as r:
|
||||
raw = r.read()
|
||||
return json.loads(raw) if raw else {}
|
||||
|
||||
|
||||
def _run(fn):
|
||||
try:
|
||||
print(json.dumps(fn(_cfg(), _inputs())))
|
||||
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)
|
||||
|
||||
|
||||
q = lambda v: urllib.parse.quote(str(v), safe="")
|
||||
|
||||
|
||||
def main(cfg, inputs):
|
||||
folder_id = inputs.get("folder_id")
|
||||
if not folder_id:
|
||||
raise Exception("folder_id is required")
|
||||
return request("GET", "/folders/" + q(folder_id), cfg)
|
||||
|
||||
|
||||
_run(main)
|
||||
@@ -0,0 +1,52 @@
|
||||
import json, os, sys, urllib.parse, urllib.request, urllib.error
|
||||
|
||||
BASE = "https://api.box.com/2.0"
|
||||
|
||||
|
||||
def _cfg():
|
||||
return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||
|
||||
|
||||
def _inputs():
|
||||
return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||
|
||||
|
||||
def request(method, path, cfg, body=None, params=None):
|
||||
url = BASE + path
|
||||
if params:
|
||||
clean = {k: v for k, v in params.items() if v not in (None, "")}
|
||||
if clean:
|
||||
url += "?" + urllib.parse.urlencode(clean)
|
||||
data = json.dumps(body).encode("utf-8") if body is not None else None
|
||||
headers = {"Authorization": "Bearer " + str(cfg.get("access_token", "")), "Accept": "application/json"}
|
||||
if data is not None:
|
||||
headers["Content-Type"] = "application/json"
|
||||
req = urllib.request.Request(url, data=data, headers=headers, method=method)
|
||||
with urllib.request.urlopen(req, timeout=60) as r:
|
||||
raw = r.read()
|
||||
return json.loads(raw) if raw else {}
|
||||
|
||||
|
||||
def _run(fn):
|
||||
try:
|
||||
print(json.dumps(fn(_cfg(), _inputs())))
|
||||
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)
|
||||
|
||||
|
||||
q = lambda v: urllib.parse.quote(str(v), safe="")
|
||||
|
||||
|
||||
def main(cfg, inputs):
|
||||
folder_id = inputs.get("folder_id")
|
||||
if not folder_id:
|
||||
raise Exception("folder_id is required")
|
||||
limit = inputs.get("limit")
|
||||
return request("GET", "/folders/" + q(folder_id) + "/items", cfg, params={"limit": int(limit or 100)})
|
||||
|
||||
|
||||
_run(main)
|
||||
@@ -0,0 +1,49 @@
|
||||
import json, os, sys, urllib.parse, urllib.request, urllib.error
|
||||
|
||||
BASE = "https://api.box.com/2.0"
|
||||
|
||||
|
||||
def _cfg():
|
||||
return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||
|
||||
|
||||
def _inputs():
|
||||
return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||
|
||||
|
||||
def request(method, path, cfg, body=None, params=None):
|
||||
url = BASE + path
|
||||
if params:
|
||||
clean = {k: v for k, v in params.items() if v not in (None, "")}
|
||||
if clean:
|
||||
url += "?" + urllib.parse.urlencode(clean)
|
||||
data = json.dumps(body).encode("utf-8") if body is not None else None
|
||||
headers = {"Authorization": "Bearer " + str(cfg.get("access_token", "")), "Accept": "application/json"}
|
||||
if data is not None:
|
||||
headers["Content-Type"] = "application/json"
|
||||
req = urllib.request.Request(url, data=data, headers=headers, method=method)
|
||||
with urllib.request.urlopen(req, timeout=60) as r:
|
||||
raw = r.read()
|
||||
return json.loads(raw) if raw else {}
|
||||
|
||||
|
||||
def _run(fn):
|
||||
try:
|
||||
print(json.dumps(fn(_cfg(), _inputs())))
|
||||
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)
|
||||
|
||||
|
||||
def main(cfg, inputs):
|
||||
query = inputs.get("query")
|
||||
if not query:
|
||||
raise Exception("query is required")
|
||||
limit = inputs.get("limit")
|
||||
return request("GET", "/search", cfg, params={"query": query, "limit": int(limit or 30)})
|
||||
|
||||
|
||||
_run(main)
|
||||
@@ -0,0 +1,46 @@
|
||||
import json, os, sys, urllib.parse, urllib.request, urllib.error
|
||||
|
||||
BASE = "https://api.box.com/2.0"
|
||||
|
||||
|
||||
def _cfg():
|
||||
return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||
|
||||
|
||||
def _inputs():
|
||||
return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||
|
||||
|
||||
def request(method, path, cfg, body=None, params=None):
|
||||
url = BASE + path
|
||||
if params:
|
||||
clean = {k: v for k, v in params.items() if v not in (None, "")}
|
||||
if clean:
|
||||
url += "?" + urllib.parse.urlencode(clean)
|
||||
data = json.dumps(body).encode("utf-8") if body is not None else None
|
||||
headers = {"Authorization": "Bearer " + str(cfg.get("access_token", "")), "Accept": "application/json"}
|
||||
if data is not None:
|
||||
headers["Content-Type"] = "application/json"
|
||||
req = urllib.request.Request(url, data=data, headers=headers, method=method)
|
||||
with urllib.request.urlopen(req, timeout=60) as r:
|
||||
raw = r.read()
|
||||
return json.loads(raw) if raw else {}
|
||||
|
||||
|
||||
def _run(fn):
|
||||
try:
|
||||
print(json.dumps(fn(_cfg(), _inputs())))
|
||||
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)
|
||||
|
||||
|
||||
def main(cfg, inputs):
|
||||
resp = request("GET", "/users/me", cfg)
|
||||
return {"ok": True, "login": resp.get("login")}
|
||||
|
||||
|
||||
_run(main)
|
||||
@@ -0,0 +1,79 @@
|
||||
id: cape
|
||||
name: CAPE Sandbox
|
||||
version: 1.0.0
|
||||
description: "CAPE Sandbox (APIv2) — dynamic malware analysis and config extraction: submit files and URLs, read task status and reports, and list tasks. Token authentication; stdlib-only, no extra Python dependencies."
|
||||
changelog: "1.0.0 — Initial release: submit file/URL, get task, get report, list tasks."
|
||||
category: enrichment
|
||||
|
||||
# Per-instance configuration. The token is sent as 'Authorization: Token <api_token>'.
|
||||
config_schema:
|
||||
properties:
|
||||
base_url:
|
||||
type: string
|
||||
description: "CAPE URL (e.g. https://cape.example.com)"
|
||||
api_token:
|
||||
type: string
|
||||
description: "CAPE API token"
|
||||
x-soar-sensitive: true
|
||||
insecure:
|
||||
type: boolean
|
||||
description: "Trust any TLS certificate (not secure)"
|
||||
default: false
|
||||
required:
|
||||
- base_url
|
||||
- api_token
|
||||
|
||||
commands:
|
||||
- id: submit_file
|
||||
name: cape-submit-file
|
||||
description: "Submit a file (base64) for analysis."
|
||||
inputs_schema:
|
||||
properties:
|
||||
file_name: { type: string, description: "File name" }
|
||||
content_base64: { type: string, description: "File content, base64-encoded" }
|
||||
required: [file_name, content_base64]
|
||||
outputs_schema: { properties: {} }
|
||||
- id: submit_url
|
||||
name: cape-submit-url
|
||||
description: "Submit a URL for analysis."
|
||||
inputs_schema:
|
||||
properties:
|
||||
url: { type: string, description: "URL to detonate" }
|
||||
required: [url]
|
||||
outputs_schema: { properties: {} }
|
||||
- id: get_task
|
||||
name: cape-get-task
|
||||
description: "Get a task's status and metadata."
|
||||
risk: read
|
||||
inputs_schema:
|
||||
properties:
|
||||
task_id: { type: string, description: "Task ID" }
|
||||
required: [task_id]
|
||||
outputs_schema: { properties: {} }
|
||||
- id: get_report
|
||||
name: cape-get-report
|
||||
description: "Get a task's full analysis report (JSON)."
|
||||
risk: read
|
||||
inputs_schema:
|
||||
properties:
|
||||
task_id: { type: string, description: "Task ID" }
|
||||
required: [task_id]
|
||||
outputs_schema: { properties: {} }
|
||||
- id: list_tasks
|
||||
name: cape-list-tasks
|
||||
description: "List recent tasks."
|
||||
risk: read
|
||||
inputs_schema:
|
||||
properties:
|
||||
limit: { type: number, description: "Max tasks (default 50)" }
|
||||
required: []
|
||||
outputs_schema: { properties: {} }
|
||||
|
||||
- id: test_connection
|
||||
name: cape-test-connection
|
||||
description: "Verify connectivity and the token (used by the Test button)."
|
||||
risk: read
|
||||
inputs_schema:
|
||||
properties: {}
|
||||
required: []
|
||||
outputs_schema: { properties: {} }
|
||||
@@ -0,0 +1,83 @@
|
||||
import json, os, sys, base64, ssl, uuid, urllib.parse, urllib.request, urllib.error
|
||||
|
||||
|
||||
def _cfg():
|
||||
return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||
|
||||
|
||||
def _inputs():
|
||||
return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||
|
||||
|
||||
def _ctx(cfg):
|
||||
if cfg.get("insecure"):
|
||||
c = ssl.create_default_context()
|
||||
c.check_hostname = False
|
||||
c.verify_mode = ssl.CERT_NONE
|
||||
return c
|
||||
return None
|
||||
|
||||
|
||||
def _headers(cfg, extra=None):
|
||||
h = {"Accept": "application/json", "Authorization": "Token " + str(cfg.get("api_token", ""))}
|
||||
if extra:
|
||||
h.update(extra)
|
||||
return h
|
||||
|
||||
|
||||
def request(method, path, cfg, form=None):
|
||||
url = str(cfg.get("base_url", "")).rstrip("/") + path
|
||||
data = None
|
||||
extra = {}
|
||||
if form is not None:
|
||||
data = urllib.parse.urlencode(form).encode("utf-8")
|
||||
extra["Content-Type"] = "application/x-www-form-urlencoded"
|
||||
req = urllib.request.Request(url, data=data, headers=_headers(cfg, extra), method=method)
|
||||
with urllib.request.urlopen(req, timeout=120, context=_ctx(cfg)) as r:
|
||||
raw = r.read()
|
||||
return json.loads(raw) if raw else {}
|
||||
|
||||
|
||||
def multipart(fields, file_field, file_name, file_bytes):
|
||||
boundary = "----riposte" + uuid.uuid4().hex
|
||||
parts = []
|
||||
for k, v in (fields or {}).items():
|
||||
parts.append(("--" + boundary + "\r\n"
|
||||
'Content-Disposition: form-data; name="' + k + '"\r\n\r\n' + str(v) + "\r\n").encode("utf-8"))
|
||||
parts.append(("--" + boundary + "\r\n"
|
||||
'Content-Disposition: form-data; name="' + file_field + '"; filename="' + file_name + '"\r\n'
|
||||
"Content-Type: application/octet-stream\r\n\r\n").encode("utf-8"))
|
||||
parts.append(file_bytes)
|
||||
parts.append(("\r\n--" + boundary + "--\r\n").encode("utf-8"))
|
||||
return b"".join(parts), "multipart/form-data; boundary=" + boundary
|
||||
|
||||
|
||||
def request_multipart(path, cfg, fields, file_field, file_name, file_bytes):
|
||||
url = str(cfg.get("base_url", "")).rstrip("/") + path
|
||||
data, content_type = multipart(fields, file_field, file_name, file_bytes)
|
||||
req = urllib.request.Request(url, data=data, headers=_headers(cfg, {"Content-Type": content_type}), method="POST")
|
||||
with urllib.request.urlopen(req, timeout=180, context=_ctx(cfg)) as r:
|
||||
raw = r.read()
|
||||
return json.loads(raw) if raw else {}
|
||||
|
||||
|
||||
def _run(fn):
|
||||
try:
|
||||
print(json.dumps(fn(_cfg(), _inputs())))
|
||||
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)
|
||||
|
||||
|
||||
def main(cfg, inputs):
|
||||
task_id = inputs.get("task_id")
|
||||
if not task_id:
|
||||
raise Exception("task_id is required")
|
||||
q = lambda v: urllib.parse.quote(str(v), safe="")
|
||||
return request("GET", "/apiv2/tasks/get/report/" + q(task_id) + "/", cfg)
|
||||
|
||||
|
||||
_run(main)
|
||||
@@ -0,0 +1,83 @@
|
||||
import json, os, sys, base64, ssl, uuid, urllib.parse, urllib.request, urllib.error
|
||||
|
||||
|
||||
def _cfg():
|
||||
return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||
|
||||
|
||||
def _inputs():
|
||||
return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||
|
||||
|
||||
def _ctx(cfg):
|
||||
if cfg.get("insecure"):
|
||||
c = ssl.create_default_context()
|
||||
c.check_hostname = False
|
||||
c.verify_mode = ssl.CERT_NONE
|
||||
return c
|
||||
return None
|
||||
|
||||
|
||||
def _headers(cfg, extra=None):
|
||||
h = {"Accept": "application/json", "Authorization": "Token " + str(cfg.get("api_token", ""))}
|
||||
if extra:
|
||||
h.update(extra)
|
||||
return h
|
||||
|
||||
|
||||
def request(method, path, cfg, form=None):
|
||||
url = str(cfg.get("base_url", "")).rstrip("/") + path
|
||||
data = None
|
||||
extra = {}
|
||||
if form is not None:
|
||||
data = urllib.parse.urlencode(form).encode("utf-8")
|
||||
extra["Content-Type"] = "application/x-www-form-urlencoded"
|
||||
req = urllib.request.Request(url, data=data, headers=_headers(cfg, extra), method=method)
|
||||
with urllib.request.urlopen(req, timeout=120, context=_ctx(cfg)) as r:
|
||||
raw = r.read()
|
||||
return json.loads(raw) if raw else {}
|
||||
|
||||
|
||||
def multipart(fields, file_field, file_name, file_bytes):
|
||||
boundary = "----riposte" + uuid.uuid4().hex
|
||||
parts = []
|
||||
for k, v in (fields or {}).items():
|
||||
parts.append(("--" + boundary + "\r\n"
|
||||
'Content-Disposition: form-data; name="' + k + '"\r\n\r\n' + str(v) + "\r\n").encode("utf-8"))
|
||||
parts.append(("--" + boundary + "\r\n"
|
||||
'Content-Disposition: form-data; name="' + file_field + '"; filename="' + file_name + '"\r\n'
|
||||
"Content-Type: application/octet-stream\r\n\r\n").encode("utf-8"))
|
||||
parts.append(file_bytes)
|
||||
parts.append(("\r\n--" + boundary + "--\r\n").encode("utf-8"))
|
||||
return b"".join(parts), "multipart/form-data; boundary=" + boundary
|
||||
|
||||
|
||||
def request_multipart(path, cfg, fields, file_field, file_name, file_bytes):
|
||||
url = str(cfg.get("base_url", "")).rstrip("/") + path
|
||||
data, content_type = multipart(fields, file_field, file_name, file_bytes)
|
||||
req = urllib.request.Request(url, data=data, headers=_headers(cfg, {"Content-Type": content_type}), method="POST")
|
||||
with urllib.request.urlopen(req, timeout=180, context=_ctx(cfg)) as r:
|
||||
raw = r.read()
|
||||
return json.loads(raw) if raw else {}
|
||||
|
||||
|
||||
def _run(fn):
|
||||
try:
|
||||
print(json.dumps(fn(_cfg(), _inputs())))
|
||||
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)
|
||||
|
||||
|
||||
def main(cfg, inputs):
|
||||
task_id = inputs.get("task_id")
|
||||
if not task_id:
|
||||
raise Exception("task_id is required")
|
||||
q = lambda v: urllib.parse.quote(str(v), safe="")
|
||||
return request("GET", "/apiv2/tasks/view/" + q(task_id) + "/", cfg)
|
||||
|
||||
|
||||
_run(main)
|
||||
@@ -0,0 +1,80 @@
|
||||
import json, os, sys, base64, ssl, uuid, urllib.parse, urllib.request, urllib.error
|
||||
|
||||
|
||||
def _cfg():
|
||||
return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||
|
||||
|
||||
def _inputs():
|
||||
return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||
|
||||
|
||||
def _ctx(cfg):
|
||||
if cfg.get("insecure"):
|
||||
c = ssl.create_default_context()
|
||||
c.check_hostname = False
|
||||
c.verify_mode = ssl.CERT_NONE
|
||||
return c
|
||||
return None
|
||||
|
||||
|
||||
def _headers(cfg, extra=None):
|
||||
h = {"Accept": "application/json", "Authorization": "Token " + str(cfg.get("api_token", ""))}
|
||||
if extra:
|
||||
h.update(extra)
|
||||
return h
|
||||
|
||||
|
||||
def request(method, path, cfg, form=None):
|
||||
url = str(cfg.get("base_url", "")).rstrip("/") + path
|
||||
data = None
|
||||
extra = {}
|
||||
if form is not None:
|
||||
data = urllib.parse.urlencode(form).encode("utf-8")
|
||||
extra["Content-Type"] = "application/x-www-form-urlencoded"
|
||||
req = urllib.request.Request(url, data=data, headers=_headers(cfg, extra), method=method)
|
||||
with urllib.request.urlopen(req, timeout=120, context=_ctx(cfg)) as r:
|
||||
raw = r.read()
|
||||
return json.loads(raw) if raw else {}
|
||||
|
||||
|
||||
def multipart(fields, file_field, file_name, file_bytes):
|
||||
boundary = "----riposte" + uuid.uuid4().hex
|
||||
parts = []
|
||||
for k, v in (fields or {}).items():
|
||||
parts.append(("--" + boundary + "\r\n"
|
||||
'Content-Disposition: form-data; name="' + k + '"\r\n\r\n' + str(v) + "\r\n").encode("utf-8"))
|
||||
parts.append(("--" + boundary + "\r\n"
|
||||
'Content-Disposition: form-data; name="' + file_field + '"; filename="' + file_name + '"\r\n'
|
||||
"Content-Type: application/octet-stream\r\n\r\n").encode("utf-8"))
|
||||
parts.append(file_bytes)
|
||||
parts.append(("\r\n--" + boundary + "--\r\n").encode("utf-8"))
|
||||
return b"".join(parts), "multipart/form-data; boundary=" + boundary
|
||||
|
||||
|
||||
def request_multipart(path, cfg, fields, file_field, file_name, file_bytes):
|
||||
url = str(cfg.get("base_url", "")).rstrip("/") + path
|
||||
data, content_type = multipart(fields, file_field, file_name, file_bytes)
|
||||
req = urllib.request.Request(url, data=data, headers=_headers(cfg, {"Content-Type": content_type}), method="POST")
|
||||
with urllib.request.urlopen(req, timeout=180, context=_ctx(cfg)) as r:
|
||||
raw = r.read()
|
||||
return json.loads(raw) if raw else {}
|
||||
|
||||
|
||||
def _run(fn):
|
||||
try:
|
||||
print(json.dumps(fn(_cfg(), _inputs())))
|
||||
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)
|
||||
|
||||
|
||||
def main(cfg, inputs):
|
||||
limit = inputs.get("limit")
|
||||
return request("GET", "/apiv2/tasks/list/" + str(int(limit or 50)) + "/", cfg)
|
||||
|
||||
|
||||
_run(main)
|
||||
@@ -0,0 +1,92 @@
|
||||
import json, os, sys, base64, ssl, uuid, urllib.parse, urllib.request, urllib.error
|
||||
|
||||
|
||||
def _cfg():
|
||||
return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||
|
||||
|
||||
def _inputs():
|
||||
return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||
|
||||
|
||||
def _ctx(cfg):
|
||||
if cfg.get("insecure"):
|
||||
c = ssl.create_default_context()
|
||||
c.check_hostname = False
|
||||
c.verify_mode = ssl.CERT_NONE
|
||||
return c
|
||||
return None
|
||||
|
||||
|
||||
def _headers(cfg, extra=None):
|
||||
h = {"Accept": "application/json", "Authorization": "Token " + str(cfg.get("api_token", ""))}
|
||||
if extra:
|
||||
h.update(extra)
|
||||
return h
|
||||
|
||||
|
||||
def request(method, path, cfg, form=None):
|
||||
url = str(cfg.get("base_url", "")).rstrip("/") + path
|
||||
data = None
|
||||
extra = {}
|
||||
if form is not None:
|
||||
data = urllib.parse.urlencode(form).encode("utf-8")
|
||||
extra["Content-Type"] = "application/x-www-form-urlencoded"
|
||||
req = urllib.request.Request(url, data=data, headers=_headers(cfg, extra), method=method)
|
||||
with urllib.request.urlopen(req, timeout=120, context=_ctx(cfg)) as r:
|
||||
raw = r.read()
|
||||
return json.loads(raw) if raw else {}
|
||||
|
||||
|
||||
def multipart(fields, file_field, file_name, file_bytes):
|
||||
boundary = "----riposte" + uuid.uuid4().hex
|
||||
parts = []
|
||||
for k, v in (fields or {}).items():
|
||||
parts.append(("--" + boundary + "\r\n"
|
||||
'Content-Disposition: form-data; name="' + k + '"\r\n\r\n' + str(v) + "\r\n").encode("utf-8"))
|
||||
parts.append(("--" + boundary + "\r\n"
|
||||
'Content-Disposition: form-data; name="' + file_field + '"; filename="' + file_name + '"\r\n'
|
||||
"Content-Type: application/octet-stream\r\n\r\n").encode("utf-8"))
|
||||
parts.append(file_bytes)
|
||||
parts.append(("\r\n--" + boundary + "--\r\n").encode("utf-8"))
|
||||
return b"".join(parts), "multipart/form-data; boundary=" + boundary
|
||||
|
||||
|
||||
def request_multipart(path, cfg, fields, file_field, file_name, file_bytes):
|
||||
url = str(cfg.get("base_url", "")).rstrip("/") + path
|
||||
data, content_type = multipart(fields, file_field, file_name, file_bytes)
|
||||
req = urllib.request.Request(url, data=data, headers=_headers(cfg, {"Content-Type": content_type}), method="POST")
|
||||
with urllib.request.urlopen(req, timeout=180, context=_ctx(cfg)) as r:
|
||||
raw = r.read()
|
||||
return json.loads(raw) if raw else {}
|
||||
|
||||
|
||||
def _run(fn):
|
||||
try:
|
||||
print(json.dumps(fn(_cfg(), _inputs())))
|
||||
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)
|
||||
|
||||
|
||||
def main(cfg, 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")
|
||||
return request_multipart(
|
||||
"/apiv2/tasks/create/file/",
|
||||
cfg,
|
||||
{},
|
||||
"file",
|
||||
file_name,
|
||||
base64.b64decode(content_base64),
|
||||
)
|
||||
|
||||
|
||||
_run(main)
|
||||
@@ -0,0 +1,82 @@
|
||||
import json, os, sys, base64, ssl, uuid, urllib.parse, urllib.request, urllib.error
|
||||
|
||||
|
||||
def _cfg():
|
||||
return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||
|
||||
|
||||
def _inputs():
|
||||
return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||
|
||||
|
||||
def _ctx(cfg):
|
||||
if cfg.get("insecure"):
|
||||
c = ssl.create_default_context()
|
||||
c.check_hostname = False
|
||||
c.verify_mode = ssl.CERT_NONE
|
||||
return c
|
||||
return None
|
||||
|
||||
|
||||
def _headers(cfg, extra=None):
|
||||
h = {"Accept": "application/json", "Authorization": "Token " + str(cfg.get("api_token", ""))}
|
||||
if extra:
|
||||
h.update(extra)
|
||||
return h
|
||||
|
||||
|
||||
def request(method, path, cfg, form=None):
|
||||
url = str(cfg.get("base_url", "")).rstrip("/") + path
|
||||
data = None
|
||||
extra = {}
|
||||
if form is not None:
|
||||
data = urllib.parse.urlencode(form).encode("utf-8")
|
||||
extra["Content-Type"] = "application/x-www-form-urlencoded"
|
||||
req = urllib.request.Request(url, data=data, headers=_headers(cfg, extra), method=method)
|
||||
with urllib.request.urlopen(req, timeout=120, context=_ctx(cfg)) as r:
|
||||
raw = r.read()
|
||||
return json.loads(raw) if raw else {}
|
||||
|
||||
|
||||
def multipart(fields, file_field, file_name, file_bytes):
|
||||
boundary = "----riposte" + uuid.uuid4().hex
|
||||
parts = []
|
||||
for k, v in (fields or {}).items():
|
||||
parts.append(("--" + boundary + "\r\n"
|
||||
'Content-Disposition: form-data; name="' + k + '"\r\n\r\n' + str(v) + "\r\n").encode("utf-8"))
|
||||
parts.append(("--" + boundary + "\r\n"
|
||||
'Content-Disposition: form-data; name="' + file_field + '"; filename="' + file_name + '"\r\n'
|
||||
"Content-Type: application/octet-stream\r\n\r\n").encode("utf-8"))
|
||||
parts.append(file_bytes)
|
||||
parts.append(("\r\n--" + boundary + "--\r\n").encode("utf-8"))
|
||||
return b"".join(parts), "multipart/form-data; boundary=" + boundary
|
||||
|
||||
|
||||
def request_multipart(path, cfg, fields, file_field, file_name, file_bytes):
|
||||
url = str(cfg.get("base_url", "")).rstrip("/") + path
|
||||
data, content_type = multipart(fields, file_field, file_name, file_bytes)
|
||||
req = urllib.request.Request(url, data=data, headers=_headers(cfg, {"Content-Type": content_type}), method="POST")
|
||||
with urllib.request.urlopen(req, timeout=180, context=_ctx(cfg)) as r:
|
||||
raw = r.read()
|
||||
return json.loads(raw) if raw else {}
|
||||
|
||||
|
||||
def _run(fn):
|
||||
try:
|
||||
print(json.dumps(fn(_cfg(), _inputs())))
|
||||
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)
|
||||
|
||||
|
||||
def main(cfg, inputs):
|
||||
url = inputs.get("url")
|
||||
if not url:
|
||||
raise Exception("url is required")
|
||||
return request("POST", "/apiv2/tasks/create/url/", cfg, form={"url": url})
|
||||
|
||||
|
||||
_run(main)
|
||||
@@ -0,0 +1,80 @@
|
||||
import json, os, sys, base64, ssl, uuid, urllib.parse, urllib.request, urllib.error
|
||||
|
||||
|
||||
def _cfg():
|
||||
return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||
|
||||
|
||||
def _inputs():
|
||||
return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||
|
||||
|
||||
def _ctx(cfg):
|
||||
if cfg.get("insecure"):
|
||||
c = ssl.create_default_context()
|
||||
c.check_hostname = False
|
||||
c.verify_mode = ssl.CERT_NONE
|
||||
return c
|
||||
return None
|
||||
|
||||
|
||||
def _headers(cfg, extra=None):
|
||||
h = {"Accept": "application/json", "Authorization": "Token " + str(cfg.get("api_token", ""))}
|
||||
if extra:
|
||||
h.update(extra)
|
||||
return h
|
||||
|
||||
|
||||
def request(method, path, cfg, form=None):
|
||||
url = str(cfg.get("base_url", "")).rstrip("/") + path
|
||||
data = None
|
||||
extra = {}
|
||||
if form is not None:
|
||||
data = urllib.parse.urlencode(form).encode("utf-8")
|
||||
extra["Content-Type"] = "application/x-www-form-urlencoded"
|
||||
req = urllib.request.Request(url, data=data, headers=_headers(cfg, extra), method=method)
|
||||
with urllib.request.urlopen(req, timeout=120, context=_ctx(cfg)) as r:
|
||||
raw = r.read()
|
||||
return json.loads(raw) if raw else {}
|
||||
|
||||
|
||||
def multipart(fields, file_field, file_name, file_bytes):
|
||||
boundary = "----riposte" + uuid.uuid4().hex
|
||||
parts = []
|
||||
for k, v in (fields or {}).items():
|
||||
parts.append(("--" + boundary + "\r\n"
|
||||
'Content-Disposition: form-data; name="' + k + '"\r\n\r\n' + str(v) + "\r\n").encode("utf-8"))
|
||||
parts.append(("--" + boundary + "\r\n"
|
||||
'Content-Disposition: form-data; name="' + file_field + '"; filename="' + file_name + '"\r\n'
|
||||
"Content-Type: application/octet-stream\r\n\r\n").encode("utf-8"))
|
||||
parts.append(file_bytes)
|
||||
parts.append(("\r\n--" + boundary + "--\r\n").encode("utf-8"))
|
||||
return b"".join(parts), "multipart/form-data; boundary=" + boundary
|
||||
|
||||
|
||||
def request_multipart(path, cfg, fields, file_field, file_name, file_bytes):
|
||||
url = str(cfg.get("base_url", "")).rstrip("/") + path
|
||||
data, content_type = multipart(fields, file_field, file_name, file_bytes)
|
||||
req = urllib.request.Request(url, data=data, headers=_headers(cfg, {"Content-Type": content_type}), method="POST")
|
||||
with urllib.request.urlopen(req, timeout=180, context=_ctx(cfg)) as r:
|
||||
raw = r.read()
|
||||
return json.loads(raw) if raw else {}
|
||||
|
||||
|
||||
def _run(fn):
|
||||
try:
|
||||
print(json.dumps(fn(_cfg(), _inputs())))
|
||||
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)
|
||||
|
||||
|
||||
def main(cfg, inputs):
|
||||
request("GET", "/apiv2/cuckoo/status/", cfg)
|
||||
return {"ok": True}
|
||||
|
||||
|
||||
_run(main)
|
||||
@@ -0,0 +1,129 @@
|
||||
id: carbon_black_cloud
|
||||
name: VMware Carbon Black Cloud
|
||||
version: 1.0.0
|
||||
description: "VMware Carbon Black Cloud (Platform API) — endpoint containment: search and read devices, quarantine/unquarantine an endpoint, trigger a background scan, update a device's policy, search alerts, and ban/unban a file hash (reputation override). API-token authentication; stdlib-only, no extra Python dependencies."
|
||||
changelog: "1.0.0 — Initial release: device search/get, quarantine/unquarantine, background scan, update policy, alert search, ban hash, delete reputation override."
|
||||
category: endpoint
|
||||
|
||||
# Per-instance configuration. The API token is sent as
|
||||
# 'X-Auth-Token: <api_secret_key>/<api_id>'. org_key identifies the org.
|
||||
config_schema:
|
||||
properties:
|
||||
base_url:
|
||||
type: string
|
||||
description: "Carbon Black Cloud URL (e.g. https://defense.conferdeploy.net)"
|
||||
api_id:
|
||||
type: string
|
||||
description: "API key ID"
|
||||
api_secret_key:
|
||||
type: string
|
||||
description: "API secret key"
|
||||
x-soar-sensitive: true
|
||||
org_key:
|
||||
type: string
|
||||
description: "Organization key"
|
||||
required:
|
||||
- base_url
|
||||
- api_id
|
||||
- api_secret_key
|
||||
- org_key
|
||||
|
||||
commands:
|
||||
- id: list_devices
|
||||
name: cbc-list-devices
|
||||
description: "Search devices (optionally by hostname or IP)."
|
||||
risk: read
|
||||
inputs_schema:
|
||||
properties:
|
||||
query: { type: string, description: "Free-text query (hostname, user, IP)" }
|
||||
rows: { type: number, description: "Max devices (default 50)" }
|
||||
required: []
|
||||
outputs_schema: { properties: {} }
|
||||
- id: get_device
|
||||
name: cbc-get-device
|
||||
description: "Get a single device by ID."
|
||||
risk: read
|
||||
inputs_schema:
|
||||
properties:
|
||||
device_id: { type: string, description: "Device ID" }
|
||||
required: [device_id]
|
||||
outputs_schema: { properties: {} }
|
||||
- id: quarantine_device
|
||||
name: cbc-quarantine-device
|
||||
description: "Quarantine (network-isolate) a device."
|
||||
inputs_schema:
|
||||
properties:
|
||||
device_id: { type: string, description: "Device ID" }
|
||||
required: [device_id]
|
||||
outputs_schema: { properties: {} }
|
||||
- id: unquarantine_device
|
||||
name: cbc-unquarantine-device
|
||||
description: "Remove a device from quarantine."
|
||||
inputs_schema:
|
||||
properties:
|
||||
device_id: { type: string, description: "Device ID" }
|
||||
required: [device_id]
|
||||
outputs_schema: { properties: {} }
|
||||
- id: background_scan
|
||||
name: cbc-background-scan
|
||||
description: "Start or stop a background scan on a device."
|
||||
inputs_schema:
|
||||
properties:
|
||||
device_id: { type: string, description: "Device ID" }
|
||||
toggle: { type: string, description: "ON or OFF (default ON)" }
|
||||
required: [device_id]
|
||||
outputs_schema: { properties: {} }
|
||||
- id: update_device_policy
|
||||
name: cbc-update-device-policy
|
||||
description: "Assign a device to a different policy."
|
||||
inputs_schema:
|
||||
properties:
|
||||
device_id: { type: string, description: "Device ID" }
|
||||
policy_id: { type: string, description: "Target policy ID" }
|
||||
required: [device_id, policy_id]
|
||||
outputs_schema: { properties: {} }
|
||||
- id: search_alerts
|
||||
name: cbc-search-alerts
|
||||
description: "Search alerts. Used for ingestion: results path = results."
|
||||
risk: read
|
||||
inputs_schema:
|
||||
properties:
|
||||
query: { type: string, description: "Free-text alert query" }
|
||||
rows: { type: number, description: "Max alerts (default 50)" }
|
||||
required: []
|
||||
outputs_schema: { properties: {} }
|
||||
ingest:
|
||||
results_path: results
|
||||
dedup_key: id
|
||||
- id: ban_hash
|
||||
name: cbc-ban-hash
|
||||
description: "Ban a file by SHA-256 hash (reputation deny-list override)."
|
||||
inputs_schema:
|
||||
properties:
|
||||
sha256: { type: string, description: "SHA-256 hash to ban" }
|
||||
filename: { type: string, description: "Optional associated file name" }
|
||||
description: { type: string, description: "Optional reason/description" }
|
||||
required: [sha256]
|
||||
outputs_schema: { properties: {} }
|
||||
- id: delete_reputation_override
|
||||
name: cbc-delete-reputation-override
|
||||
description: "Delete a reputation override (unban) by its ID."
|
||||
inputs_schema:
|
||||
properties:
|
||||
override_id: { type: string, description: "Reputation override ID" }
|
||||
required: [override_id]
|
||||
outputs_schema: { properties: {} }
|
||||
|
||||
- id: test_connection
|
||||
name: cbc-test-connection
|
||||
description: "Verify connectivity and the API token (used by the Test button)."
|
||||
risk: read
|
||||
inputs_schema:
|
||||
properties: {}
|
||||
required: []
|
||||
outputs_schema: { properties: {} }
|
||||
|
||||
ingestion:
|
||||
command: search_alerts
|
||||
mapper: search_alerts
|
||||
default_incident_type: "Carbon Black Cloud Alert"
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user