Compare commits
232 Commits
d701284f46
...
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 | |||
| 306581e70b | |||
| da3909e79d | |||
| 84be770928 | |||
| 27615636e3 | |||
| 3f754d14e9 | |||
| dbb740f476 | |||
| 9f1df6ca7a | |||
| 527d6f79fd | |||
| 1ca5597453 | |||
| c406edb5e7 | |||
| 0263619e30 | |||
| b99de4af19 | |||
| 7e96048446 | |||
| 934f2c52d7 | |||
| 70dffb3b0a | |||
| fcf516ca82 | |||
| ef215daa88 | |||
| 8fd297169f | |||
| 729c339e2f | |||
| 6bccbb5a50 | |||
| 3608cbb74e | |||
| b98d315fe4 | |||
| 07fe4b50a7 | |||
| dd1ca83d7d | |||
| 76812ef128 | |||
| f3b0f269b8 | |||
| c068d9fa9a | |||
| 0f4852bb54 | |||
| 44694f6d9e | |||
| 096fa21808 | |||
| 2c4d40103e | |||
| be7d4a8b7c | |||
| b7f88b1a09 | |||
| 1157735be1 | |||
| 24e8c652ac | |||
| 587d7d1340 | |||
| 826aa63c74 | |||
| 52a966f4e5 | |||
| 5251441962 | |||
| fd6c92047a | |||
| 17faa01103 | |||
| d85a955e77 | |||
| b5f1254ee8 | |||
| d7df536eaf | |||
| e3e363c2f9 | |||
| 6b65c5f9df | |||
| e5237c0df6 | |||
| 9c04290a34 | |||
| a3ac1ee30d |
@@ -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,348 @@
|
||||
id: active_directory
|
||||
name: Active Directory
|
||||
version: 1.0.0
|
||||
description: "Active Directory over LDAP/LDAPS — query and manage users, computers, groups and contacts (search, enable/disable/unlock, set/expire password, group membership, OU moves, create/update/delete). Runs on a remote engine inside the AD network. Requires the Python 'ldap3' library on the engine host (pip install ldap3)."
|
||||
changelog: "1.0.0 — Initial release: full directory administration over LDAP (search, get user/computer/group-members, create/update/delete user/contact/group, enable/disable/unlock, set/expire password, password-never-expire, add/remove group membership, move user/computer OU, test credentials)."
|
||||
category: identity
|
||||
|
||||
# Per-instance configuration. The integration binds to a Domain Controller over
|
||||
# LDAP (389) or LDAPS (636). Secure connection (SSL/Start TLS) is required for
|
||||
# password operations. Run this integration on a remote engine that can reach the
|
||||
# Domain Controller. The bundled scripts require the Python 'ldap3' library to be
|
||||
# installed on the engine host: pip install ldap3
|
||||
config_schema:
|
||||
properties:
|
||||
server_ip:
|
||||
type: string
|
||||
description: "Domain Controller host or IP (e.g. dc01.company.com or 192.168.0.1)"
|
||||
port:
|
||||
type: string
|
||||
description: "LDAP port. Default 389 for LDAP / Start TLS, 636 for LDAPS."
|
||||
username:
|
||||
type: string
|
||||
description: "Bind username (e.g. DOMAIN\\\\user or user@company.com)"
|
||||
password:
|
||||
type: string
|
||||
description: "Bind password"
|
||||
x-soar-sensitive: true
|
||||
base_dn:
|
||||
type: string
|
||||
description: "Base DN (e.g. dc=company,dc=com)"
|
||||
secure_connection:
|
||||
type: string
|
||||
description: "Connection security: None, SSL (LDAPS), TLS, or Start TLS. Password operations require SSL or TLS."
|
||||
default: SSL
|
||||
ssl_version:
|
||||
type: string
|
||||
description: "SSL/TLS protocol: None, TLS, TLSv1, TLSv1_1, TLSv1_2, or TLS_CLIENT. Default None (let the library negotiate)."
|
||||
default: None
|
||||
ntlm:
|
||||
type: boolean
|
||||
description: "Use NTLM authentication for the bind"
|
||||
default: false
|
||||
unsecure:
|
||||
type: boolean
|
||||
description: "Trust any TLS certificate (not secure)"
|
||||
default: false
|
||||
page_size:
|
||||
type: string
|
||||
description: "LDAP paging size for searches"
|
||||
default: "500"
|
||||
default_base_query:
|
||||
type: string
|
||||
description: "Default LDAP filter used by ad-get-user when no query argument is given"
|
||||
default: "(&(objectClass=User)(objectCategory=person))"
|
||||
required:
|
||||
- server_ip
|
||||
- username
|
||||
- password
|
||||
- base_dn
|
||||
|
||||
commands:
|
||||
# ── Connectivity ──────────────────────────────────────────────────────────
|
||||
- id: test_connection
|
||||
name: ad-test-connection
|
||||
description: "Bind to the Domain Controller and verify the configured base DN (used by the Test button)."
|
||||
risk: read
|
||||
inputs_schema:
|
||||
properties: {}
|
||||
required: []
|
||||
outputs_schema: { properties: {} }
|
||||
|
||||
# ── Search / read ─────────────────────────────────────────────────────────
|
||||
- id: ad_search
|
||||
name: ad-search
|
||||
description: "Run a raw LDAP query and return the matching entries. Paging is handled internally up to size-limit."
|
||||
risk: read
|
||||
inputs_schema:
|
||||
properties:
|
||||
filter: { type: string, description: "LDAP search filter, e.g. (&(objectCategory=person)(objectClass=user))" }
|
||||
base-dn: { type: string, description: "Search base (defaults to the instance base_dn)" }
|
||||
attributes: { type: string, description: "CSV list of attributes to return, or ALL for every attribute" }
|
||||
size-limit: { type: number, description: "Maximum entries to return (default 50)" }
|
||||
time-limit: { type: number, description: "Maximum search time in seconds" }
|
||||
page-size: { type: number, description: "Paging size (overrides size-limit when set)" }
|
||||
required: [filter]
|
||||
outputs_schema: { properties: {} }
|
||||
- id: ad_get_user
|
||||
name: ad-get-user
|
||||
description: "Retrieve user accounts by DN, name, email, sAMAccountName or a custom attribute. Decodes userAccountControl flags."
|
||||
risk: read
|
||||
inputs_schema:
|
||||
properties:
|
||||
dn: { type: string, description: "Distinguished Name of the user" }
|
||||
name: { type: string, description: "Common name (cn) of the user" }
|
||||
email: { type: string, description: "User email (mail)" }
|
||||
username: { type: string, description: "sAMAccountName" }
|
||||
sAMAccountName: { type: string, description: "sAMAccountName (alias of username)" }
|
||||
custom-field-type: { type: string, description: "Attribute name to query by" }
|
||||
custom-field-data: { type: string, description: "Attribute value (required with custom-field-type)" }
|
||||
attributes: { type: string, description: "CSV list of extra attributes to add to the defaults" }
|
||||
attributes-to-exclude: { type: string, description: "CSV list of attributes to remove from the result" }
|
||||
limit: { type: number, description: "Maximum users to return (default 20)" }
|
||||
page-size: { type: number, description: "Paging size (overrides limit when set)" }
|
||||
required: []
|
||||
outputs_schema: { properties: {} }
|
||||
- id: ad_get_computer
|
||||
name: ad-get-computer
|
||||
description: "Retrieve computer accounts by DN, name or a custom attribute."
|
||||
risk: read
|
||||
inputs_schema:
|
||||
properties:
|
||||
dn: { type: string, description: "Distinguished Name of the computer" }
|
||||
name: { type: string, description: "Computer name" }
|
||||
custom-field-type: { type: string, description: "Attribute name to query by" }
|
||||
custom-field-data: { type: string, description: "Attribute value (required with custom-field-type)" }
|
||||
attributes: { type: string, description: "CSV list of extra attributes to add to the defaults" }
|
||||
limit: { type: number, description: "Maximum computers to return" }
|
||||
page-size: { type: number, description: "Paging size (overrides limit when set)" }
|
||||
required: []
|
||||
outputs_schema: { properties: {} }
|
||||
- id: ad_get_group_members
|
||||
name: ad-get-group-members
|
||||
description: "List the users, computers or nested groups that are members of a group (recursive by default)."
|
||||
risk: read
|
||||
inputs_schema:
|
||||
properties:
|
||||
group-dn: { type: string, description: "Distinguished Name of the group" }
|
||||
member-type: { type: string, description: "Member type to return: person, computer or group (default person)" }
|
||||
attributes: { type: string, description: "CSV list of extra attributes to add to the defaults" }
|
||||
time_limit: { type: number, description: "Search time limit in seconds (default 180)" }
|
||||
disable-nested-search: { type: string, description: "Set true to disable recursive membership resolution (default false)" }
|
||||
sAMAccountName: { type: string, description: "Filter members by sAMAccountName (default *)" }
|
||||
limit: { type: number, description: "Maximum members to return" }
|
||||
page-size: { type: number, description: "Paging size (overrides limit when set)" }
|
||||
required: [group-dn]
|
||||
outputs_schema: { properties: {} }
|
||||
|
||||
# ── Account state ─────────────────────────────────────────────────────────
|
||||
- id: ad_enable_account
|
||||
name: ad-enable-account
|
||||
description: "Enable a previously disabled user account."
|
||||
inputs_schema:
|
||||
properties:
|
||||
username: { type: string, description: "sAMAccountName of the account to enable" }
|
||||
base-dn: { type: string, description: "Search base (defaults to the instance base_dn)" }
|
||||
restore_user: { type: string, description: "Set true to restore the account's previous userAccountControl flags" }
|
||||
required: [username]
|
||||
outputs_schema: { properties: {} }
|
||||
- id: ad_disable_account
|
||||
name: ad-disable-account
|
||||
description: "Disable a user account."
|
||||
inputs_schema:
|
||||
properties:
|
||||
username: { type: string, description: "sAMAccountName of the account to disable" }
|
||||
base-dn: { type: string, description: "Search base (defaults to the instance base_dn)" }
|
||||
required: [username]
|
||||
outputs_schema: { properties: {} }
|
||||
- id: ad_unlock_account
|
||||
name: ad-unlock-account
|
||||
description: "Unlock a locked-out user account."
|
||||
inputs_schema:
|
||||
properties:
|
||||
username: { type: string, description: "sAMAccountName of the account to unlock" }
|
||||
base-dn: { type: string, description: "Search base (defaults to the instance base_dn)" }
|
||||
required: [username]
|
||||
outputs_schema: { properties: {} }
|
||||
- id: ad_set_new_password
|
||||
name: ad-set-new-password
|
||||
description: "Set a new password for a user. Requires a secure connection (SSL or TLS)."
|
||||
inputs_schema:
|
||||
properties:
|
||||
username: { type: string, description: "sAMAccountName whose password will be set" }
|
||||
password: { type: string, description: "New password", x-soar-sensitive: true }
|
||||
base-dn: { type: string, description: "Search base (defaults to the instance base_dn)" }
|
||||
required: [username, password]
|
||||
outputs_schema: { properties: {} }
|
||||
- id: ad_expire_password
|
||||
name: ad-expire-password
|
||||
description: "Force a user to change their password at next login."
|
||||
inputs_schema:
|
||||
properties:
|
||||
username: { type: string, description: "sAMAccountName of the user" }
|
||||
base-dn: { type: string, description: "Search base (defaults to the instance base_dn)" }
|
||||
required: [username]
|
||||
outputs_schema: { properties: {} }
|
||||
- id: ad_modify_password_never_expire
|
||||
name: ad-modify-password-never-expire
|
||||
description: "Set or clear the 'Password Never Expire' flag on a user account."
|
||||
inputs_schema:
|
||||
properties:
|
||||
username: { type: string, description: "sAMAccountName of the user" }
|
||||
value: { type: string, description: "true to set 'Password Never Expire', false to clear it" }
|
||||
required: [username, value]
|
||||
outputs_schema: { properties: {} }
|
||||
|
||||
# ── Group membership ──────────────────────────────────────────────────────
|
||||
- id: ad_add_to_group
|
||||
name: ad-add-to-group
|
||||
description: "Add a user, computer or nested group to a group."
|
||||
inputs_schema:
|
||||
properties:
|
||||
username: { type: string, description: "Username(s) to add (single or CSV). Mutually exclusive with computer-name." }
|
||||
computer-name: { type: string, description: "Computer name(s) to add (single or CSV)" }
|
||||
nested_group_cn: { type: string, description: "A group CN to add as a nested member" }
|
||||
group-cn: { type: string, description: "Target group CN" }
|
||||
base-dn: { type: string, description: "Search base (defaults to the instance base_dn)" }
|
||||
required: [group-cn]
|
||||
outputs_schema: { properties: {} }
|
||||
- id: ad_remove_from_group
|
||||
name: ad-remove-from-group
|
||||
description: "Remove a user or computer from a group."
|
||||
inputs_schema:
|
||||
properties:
|
||||
username: { type: string, description: "Username to remove. Mutually exclusive with computer-name." }
|
||||
computer-name: { type: string, description: "Computer name to remove" }
|
||||
group-cn: { type: string, description: "Target group CN" }
|
||||
base-dn: { type: string, description: "Search base (defaults to the instance base_dn)" }
|
||||
required: [group-cn]
|
||||
outputs_schema: { properties: {} }
|
||||
|
||||
# ── Organizational unit ───────────────────────────────────────────────────
|
||||
- id: ad_modify_user_ou
|
||||
name: ad-modify-user-ou
|
||||
description: "Move a user to a different organizational unit within the domain."
|
||||
inputs_schema:
|
||||
properties:
|
||||
user-name: { type: string, description: "Name of the user to move" }
|
||||
full-superior-dn: { type: string, description: "Target OU DN, e.g. OU=users,DC=domain,DC=com" }
|
||||
base-dn: { type: string, description: "Search base (defaults to the instance base_dn)" }
|
||||
required: [user-name, full-superior-dn]
|
||||
outputs_schema: { properties: {} }
|
||||
- id: ad_modify_computer_ou
|
||||
name: ad-modify-computer-ou
|
||||
description: "Move a computer to a different organizational unit within the domain."
|
||||
inputs_schema:
|
||||
properties:
|
||||
computer-name: { type: string, description: "Name of the computer to move" }
|
||||
full-superior-dn: { type: string, description: "Target OU DN, e.g. OU=computers,DC=domain,DC=com" }
|
||||
base-dn: { type: string, description: "Search base (defaults to the instance base_dn)" }
|
||||
required: [computer-name, full-superior-dn]
|
||||
outputs_schema: { properties: {} }
|
||||
|
||||
# ── Object lifecycle: users ───────────────────────────────────────────────
|
||||
- id: ad_create_user
|
||||
name: ad-create-user
|
||||
description: "Create a user account, set its initial password and enable it. Requires a secure connection (SSL or TLS)."
|
||||
inputs_schema:
|
||||
properties:
|
||||
username: { type: string, description: "sAMAccountName for the new user" }
|
||||
password: { type: string, description: "Initial password (user must change at next login)", x-soar-sensitive: true }
|
||||
user-dn: { type: string, description: "Full DN of the new user" }
|
||||
display-name: { type: string, description: "Display name" }
|
||||
description: { type: string, description: "Description" }
|
||||
email: { type: string, description: "Email (mail)" }
|
||||
telephone-number: { type: string, description: "Telephone number" }
|
||||
title: { type: string, description: "Job title" }
|
||||
custom-attributes: { type: string, description: "JSON object of extra attributes, e.g. {\"company\":\"ACME\"}" }
|
||||
required: [username, password, user-dn]
|
||||
outputs_schema: { properties: {} }
|
||||
- id: ad_update_user
|
||||
name: ad-update-user
|
||||
description: "Replace a single attribute on an existing user."
|
||||
inputs_schema:
|
||||
properties:
|
||||
username: { type: string, description: "sAMAccountName of the user" }
|
||||
attribute-name: { type: string, description: "Attribute to modify (e.g. sn, displayName, mail)" }
|
||||
attribute-value: { type: string, description: "New value" }
|
||||
base-dn: { type: string, description: "Search base (defaults to the instance base_dn)" }
|
||||
required: [username, attribute-name, attribute-value]
|
||||
outputs_schema: { properties: {} }
|
||||
- id: ad_delete_user
|
||||
name: ad-delete-user
|
||||
description: "Delete a user (or any object) by its DN."
|
||||
inputs_schema:
|
||||
properties:
|
||||
user-dn: { type: string, description: "DN of the object to delete" }
|
||||
required: [user-dn]
|
||||
outputs_schema: { properties: {} }
|
||||
|
||||
# ── Object lifecycle: contacts ────────────────────────────────────────────
|
||||
- id: ad_create_contact
|
||||
name: ad-create-contact
|
||||
description: "Create a contact object."
|
||||
inputs_schema:
|
||||
properties:
|
||||
contact-dn: { type: string, description: "Full DN of the new contact" }
|
||||
display-name: { type: string, description: "Display name" }
|
||||
description: { type: string, description: "Description" }
|
||||
email: { type: string, description: "Email (mail)" }
|
||||
telephone-number: { type: string, description: "Telephone number" }
|
||||
title: { type: string, description: "Job title" }
|
||||
custom-attributes: { type: string, description: "JSON object of extra attributes" }
|
||||
required: [contact-dn]
|
||||
outputs_schema: { properties: {} }
|
||||
- id: ad_update_contact
|
||||
name: ad-update-contact
|
||||
description: "Replace a single attribute on an existing contact."
|
||||
inputs_schema:
|
||||
properties:
|
||||
contact-dn: { type: string, description: "DN of the contact" }
|
||||
attribute-name: { type: string, description: "Attribute to modify" }
|
||||
attribute-value: { type: string, description: "New value" }
|
||||
required: [contact-dn, attribute-name, attribute-value]
|
||||
outputs_schema: { properties: {} }
|
||||
|
||||
# ── Object lifecycle: groups ──────────────────────────────────────────────
|
||||
- id: ad_create_group
|
||||
name: ad-create-group
|
||||
description: "Create a security or distribution group."
|
||||
inputs_schema:
|
||||
properties:
|
||||
name: { type: string, description: "Group name (sAMAccountName)" }
|
||||
group-type: { type: string, description: "security or distribution" }
|
||||
dn: { type: string, description: "Full DN of the new group" }
|
||||
members: { type: array, description: "DNs of initial members" }
|
||||
required: [name, group-type, dn]
|
||||
outputs_schema: { properties: {} }
|
||||
- id: ad_update_group
|
||||
name: ad-update-group
|
||||
description: "Replace a single attribute on an existing group."
|
||||
inputs_schema:
|
||||
properties:
|
||||
groupname: { type: string, description: "Group name (cn) to update" }
|
||||
attributename: { type: string, description: "Attribute to modify (e.g. description, displayName)" }
|
||||
attributevalue: { type: string, description: "New value" }
|
||||
basedn: { type: string, description: "Search base (defaults to the instance base_dn)" }
|
||||
required: [attributename, attributevalue]
|
||||
outputs_schema: { properties: {} }
|
||||
- id: ad_delete_group
|
||||
name: ad-delete-group
|
||||
description: "Delete a security or distribution group by its DN."
|
||||
inputs_schema:
|
||||
properties:
|
||||
dn: { type: string, description: "DN of the group" }
|
||||
required: [dn]
|
||||
outputs_schema: { properties: {} }
|
||||
|
||||
# ── Credentials ───────────────────────────────────────────────────────────
|
||||
- id: ad_test_credentials
|
||||
name: ad-test-credentials
|
||||
description: "Test whether a username/password can bind to the Domain Controller."
|
||||
risk: read
|
||||
inputs_schema:
|
||||
properties:
|
||||
username: { type: string, description: "Username to test (user or SERVER\\\\user)" }
|
||||
password: { type: string, description: "Password to test", x-soar-sensitive: true }
|
||||
required: [username, password]
|
||||
outputs_schema: { properties: {} }
|
||||
@@ -0,0 +1,240 @@
|
||||
import json, os, sys
|
||||
|
||||
S = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||
I = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||
|
||||
|
||||
def out(value):
|
||||
print(json.dumps(value, default=str))
|
||||
|
||||
|
||||
def fail(message, **extra):
|
||||
payload = {"error": message}
|
||||
payload.update(extra)
|
||||
print(json.dumps(payload, default=str))
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
try:
|
||||
import ssl
|
||||
from ldap3 import (
|
||||
Server, Connection, Tls, NTLM, SUBTREE, BASE, ALL_ATTRIBUTES,
|
||||
MODIFY_REPLACE, AUTO_BIND_NO_TLS, AUTO_BIND_TLS_BEFORE_BIND,
|
||||
)
|
||||
from ldap3.core.exceptions import LDAPException
|
||||
from ldap3.extend import microsoft
|
||||
from ldap3.utils.conv import escape_filter_chars
|
||||
except ImportError as _e:
|
||||
print(json.dumps({
|
||||
"error": "The 'ldap3' Python library is required for the Active Directory integration. "
|
||||
"Install it on the execution host (engine): pip install ldap3",
|
||||
"detail": str(_e),
|
||||
}))
|
||||
sys.exit(1)
|
||||
|
||||
SSL_VERSIONS = {
|
||||
"None": None,
|
||||
"TLS": ssl.PROTOCOL_TLS,
|
||||
"TLSv1": ssl.PROTOCOL_TLSv1,
|
||||
"TLSv1_1": ssl.PROTOCOL_TLSv1_1,
|
||||
"TLSv1_2": ssl.PROTOCOL_TLSv1_2,
|
||||
"TLS_CLIENT": ssl.PROTOCOL_TLS_CLIENT,
|
||||
}
|
||||
CIPHERS = (
|
||||
"@SECLEVEL=1:ECDHE+AESGCM:ECDHE+CHACHA20:DHE+AESGCM:DHE+CHACHA20:ECDH+AESGCM:"
|
||||
"DH+AESGCM:ECDH+AES:DH+AES:RSA+AESGCM:RSA+AES:!aNULL:!eNULL:!MD5:!DSS"
|
||||
)
|
||||
|
||||
CONN = None
|
||||
|
||||
|
||||
def _bool(v):
|
||||
if isinstance(v, bool):
|
||||
return v
|
||||
return str(v).lower() in ("1", "true", "yes")
|
||||
|
||||
|
||||
def _list(v):
|
||||
if isinstance(v, list):
|
||||
return v
|
||||
return [x.strip() for x in str(v).split(",") if x.strip()]
|
||||
|
||||
|
||||
def _cfg():
|
||||
return {
|
||||
"host": S.get("server_ip") or S.get("host") or "",
|
||||
"port": int(S["port"]) if S.get("port") else None,
|
||||
"user": S.get("username") or "",
|
||||
"password": S.get("password") or "",
|
||||
"base_dn": S.get("base_dn") or "",
|
||||
"secure": S.get("secure_connection") or "SSL",
|
||||
"ssl_version": S.get("ssl_version") or "None",
|
||||
"ntlm": _bool(S.get("ntlm")),
|
||||
"unsecure": _bool(S.get("unsecure")),
|
||||
"page_size": int(S["page_size"]) if S.get("page_size") else 500,
|
||||
}
|
||||
|
||||
|
||||
def _tls(unsecure, ssl_version):
|
||||
version = SSL_VERSIONS.get(ssl_version)
|
||||
if unsecure:
|
||||
return Tls(validate=ssl.CERT_NONE, ca_certs_file=None, ciphers=CIPHERS, version=version)
|
||||
return Tls(validate=ssl.CERT_REQUIRED, ca_certs_file=os.environ.get("SSL_CERT_FILE"), version=version)
|
||||
|
||||
|
||||
def _server(c):
|
||||
secure = c["secure"]
|
||||
if secure in ("SSL", "TLS"):
|
||||
return Server(host=c["host"], port=c["port"], use_ssl=True, tls=_tls(c["unsecure"], c["ssl_version"]), connect_timeout=120)
|
||||
if secure == "Start TLS":
|
||||
return Server(host=c["host"], port=c["port"], use_ssl=False, tls=_tls(c["unsecure"], c["ssl_version"]), connect_timeout=120)
|
||||
return Server(host=c["host"], port=c["port"], connect_timeout=120)
|
||||
|
||||
|
||||
def _auto_bind(secure):
|
||||
return AUTO_BIND_TLS_BEFORE_BIND if secure == "Start TLS" else AUTO_BIND_NO_TLS
|
||||
|
||||
|
||||
def connect():
|
||||
global CONN
|
||||
c = _cfg()
|
||||
server = _server(c)
|
||||
if c["ntlm"]:
|
||||
domain_user = c["user"] if "\\" in c["user"] else c["host"] + "\\" + c["user"]
|
||||
CONN = Connection(server, domain_user, password=c["password"], authentication=NTLM, auto_bind=_auto_bind(c["secure"]))
|
||||
else:
|
||||
CONN = Connection(server, user=c["user"], password=c["password"], auto_bind=_auto_bind(c["secure"]))
|
||||
return CONN, c
|
||||
|
||||
|
||||
def base_dn_arg():
|
||||
return I.get("base-dn") or I.get("base_dn") or _cfg()["base_dn"]
|
||||
|
||||
|
||||
def search_paging(conn, search_filter, search_base, attributes=None, page_size=100, size_limit=0, time_limit=0):
|
||||
entries = []
|
||||
cookie = None
|
||||
total = 0
|
||||
left = size_limit
|
||||
while True:
|
||||
ps = page_size
|
||||
if 0 < left < page_size:
|
||||
ps = left
|
||||
conn.search(search_base, search_filter, search_scope=SUBTREE, attributes=attributes,
|
||||
paged_size=ps, paged_cookie=cookie, time_limit=time_limit)
|
||||
left -= len(conn.entries)
|
||||
total += len(conn.entries)
|
||||
try:
|
||||
cookie = conn.result["controls"]["1.2.840.113556.1.4.319"]["value"]["cookie"]
|
||||
except (KeyError, TypeError):
|
||||
cookie = None
|
||||
entries.extend(conn.entries)
|
||||
if (size_limit and size_limit <= total) or not cookie:
|
||||
break
|
||||
flat = []
|
||||
for entry in entries:
|
||||
obj = json.loads(entry.entry_to_json())
|
||||
flat_entry = {"dn": obj["dn"]}
|
||||
for attr in obj.get("attributes", {}):
|
||||
flat_entry[attr] = obj["attributes"][attr]
|
||||
flat.append(flat_entry)
|
||||
return flat
|
||||
|
||||
|
||||
def user_dn(conn, sam, search_base):
|
||||
if "\\" in sam:
|
||||
sam = sam.split("\\", 1)[1]
|
||||
flat = search_paging(conn, f"(&(objectClass=user)(sAMAccountName={escape_filter_chars(sam)}))",
|
||||
search_base, page_size=1, size_limit=1)
|
||||
if not flat:
|
||||
raise Exception(f"Could not find user '{sam}'")
|
||||
return flat[0]["dn"]
|
||||
|
||||
|
||||
def computer_dn(conn, name, search_base):
|
||||
flat = search_paging(conn, f"(&(objectClass=user)(objectCategory=computer)(name={escape_filter_chars(name)}))",
|
||||
search_base, page_size=1, size_limit=1)
|
||||
if not flat:
|
||||
raise Exception(f"Could not find computer '{name}'")
|
||||
return flat[0]["dn"]
|
||||
|
||||
|
||||
def group_dn(conn, name, search_base):
|
||||
flat = search_paging(conn, f"(&(objectClass=group)(cn={escape_filter_chars(name)}))",
|
||||
search_base, page_size=1, size_limit=1)
|
||||
if not flat:
|
||||
raise Exception(f"Could not find group '{name}'")
|
||||
return flat[0]["dn"]
|
||||
|
||||
|
||||
def modify(conn, dn, modification):
|
||||
if not conn.modify(dn, modification):
|
||||
raise Exception(f"Failed to modify {dn}: {json.dumps(conn.result, default=str)}")
|
||||
|
||||
|
||||
def first_uac(values):
|
||||
if isinstance(values, list):
|
||||
return values[0] if values else 0
|
||||
return values or 0
|
||||
|
||||
|
||||
def uac_fields(value):
|
||||
value = int(value)
|
||||
flags = {
|
||||
"SCRIPT": 0x1, "ACCOUNTDISABLE": 0x2, "HOMEDIR_REQUIRED": 0x8, "LOCKOUT": 0x10,
|
||||
"PASSWD_NOTREQD": 0x20, "PASSWD_CANT_CHANGE": 0x40, "ENCRYPTED_TEXT_PWD_ALLOWED": 0x80,
|
||||
"TEMP_DUPLICATE_ACCOUNT": 0x100, "NORMAL_ACCOUNT": 0x200, "INTERDOMAIN_TRUST_ACCOUNT": 0x800,
|
||||
"WORKSTATION_TRUST_ACCOUNT": 0x1000, "SERVER_TRUST_ACCOUNT": 0x2000, "DONT_EXPIRE_PASSWORD": 0x10000,
|
||||
"MNS_LOGON_ACCOUNT": 0x20000, "SMARTCARD_REQUIRED": 0x40000, "TRUSTED_FOR_DELEGATION": 0x80000,
|
||||
"NOT_DELEGATED": 0x100000, "USE_DES_KEY_ONLY": 0x200000, "DONT_REQ_PREAUTH": 0x400000,
|
||||
"PASSWORD_EXPIRED": 0x800000, "TRUSTED_TO_AUTH_FOR_DELEGATION": 0x1000000, "PARTIAL_SECRETS_ACCOUNT": 0x4000000,
|
||||
}
|
||||
return {name: bool(value & mask) for name, mask in flags.items()}
|
||||
|
||||
|
||||
def current_uac(conn, search_base):
|
||||
sam = I.get("username") or I.get("sAMAccountName")
|
||||
query = "(&(objectClass=User)(objectCategory=person))"
|
||||
if sam:
|
||||
query = f"(&(objectClass=User)(objectCategory=person)(sAMAccountName={escape_filter_chars(sam)}))"
|
||||
flat = search_paging(conn, query, search_base, attributes=["userAccountControl"], size_limit=1, page_size=1)
|
||||
if flat and flat[0].get("userAccountControl"):
|
||||
return first_uac(flat[0]["userAccountControl"])
|
||||
return 512
|
||||
|
||||
|
||||
|
||||
def run():
|
||||
conn, c = connect()
|
||||
sb = base_dn_arg()
|
||||
if I.get("username") and I.get("computer-name"):
|
||||
fail("Provide either username or computer-name, not both")
|
||||
member_dns = []
|
||||
if I.get("username"):
|
||||
member_dns = [user_dn(conn, u, sb) for u in _list(I["username"])]
|
||||
elif I.get("computer-name"):
|
||||
member_dns = [computer_dn(conn, cn, sb) for cn in _list(I["computer-name"])]
|
||||
elif I.get("nested_group_cn"):
|
||||
member_dns = [group_dn(conn, I["nested_group_cn"], sb)]
|
||||
else:
|
||||
fail("Provide username, computer-name, or nested_group_cn")
|
||||
grp = group_dn(conn, I["group-cn"], sb)
|
||||
if not microsoft.addMembersToGroups.ad_add_members_to_groups(conn, member_dns, [grp], True):
|
||||
fail("Failed to add member(s) to group " + I["group-cn"])
|
||||
out({"added": member_dns, "group": I["group-cn"]})
|
||||
|
||||
|
||||
try:
|
||||
run()
|
||||
except SystemExit:
|
||||
raise
|
||||
except LDAPException as e:
|
||||
fail("LDAP error: " + str(e))
|
||||
except Exception as e:
|
||||
fail(str(e))
|
||||
finally:
|
||||
try:
|
||||
if CONN is not None:
|
||||
CONN.unbind()
|
||||
except Exception:
|
||||
pass
|
||||
@@ -0,0 +1,239 @@
|
||||
import json, os, sys
|
||||
|
||||
S = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||
I = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||
|
||||
|
||||
def out(value):
|
||||
print(json.dumps(value, default=str))
|
||||
|
||||
|
||||
def fail(message, **extra):
|
||||
payload = {"error": message}
|
||||
payload.update(extra)
|
||||
print(json.dumps(payload, default=str))
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
try:
|
||||
import ssl
|
||||
from ldap3 import (
|
||||
Server, Connection, Tls, NTLM, SUBTREE, BASE, ALL_ATTRIBUTES,
|
||||
MODIFY_REPLACE, AUTO_BIND_NO_TLS, AUTO_BIND_TLS_BEFORE_BIND,
|
||||
)
|
||||
from ldap3.core.exceptions import LDAPException
|
||||
from ldap3.extend import microsoft
|
||||
from ldap3.utils.conv import escape_filter_chars
|
||||
except ImportError as _e:
|
||||
print(json.dumps({
|
||||
"error": "The 'ldap3' Python library is required for the Active Directory integration. "
|
||||
"Install it on the execution host (engine): pip install ldap3",
|
||||
"detail": str(_e),
|
||||
}))
|
||||
sys.exit(1)
|
||||
|
||||
SSL_VERSIONS = {
|
||||
"None": None,
|
||||
"TLS": ssl.PROTOCOL_TLS,
|
||||
"TLSv1": ssl.PROTOCOL_TLSv1,
|
||||
"TLSv1_1": ssl.PROTOCOL_TLSv1_1,
|
||||
"TLSv1_2": ssl.PROTOCOL_TLSv1_2,
|
||||
"TLS_CLIENT": ssl.PROTOCOL_TLS_CLIENT,
|
||||
}
|
||||
CIPHERS = (
|
||||
"@SECLEVEL=1:ECDHE+AESGCM:ECDHE+CHACHA20:DHE+AESGCM:DHE+CHACHA20:ECDH+AESGCM:"
|
||||
"DH+AESGCM:ECDH+AES:DH+AES:RSA+AESGCM:RSA+AES:!aNULL:!eNULL:!MD5:!DSS"
|
||||
)
|
||||
|
||||
CONN = None
|
||||
|
||||
|
||||
def _bool(v):
|
||||
if isinstance(v, bool):
|
||||
return v
|
||||
return str(v).lower() in ("1", "true", "yes")
|
||||
|
||||
|
||||
def _list(v):
|
||||
if isinstance(v, list):
|
||||
return v
|
||||
return [x.strip() for x in str(v).split(",") if x.strip()]
|
||||
|
||||
|
||||
def _cfg():
|
||||
return {
|
||||
"host": S.get("server_ip") or S.get("host") or "",
|
||||
"port": int(S["port"]) if S.get("port") else None,
|
||||
"user": S.get("username") or "",
|
||||
"password": S.get("password") or "",
|
||||
"base_dn": S.get("base_dn") or "",
|
||||
"secure": S.get("secure_connection") or "SSL",
|
||||
"ssl_version": S.get("ssl_version") or "None",
|
||||
"ntlm": _bool(S.get("ntlm")),
|
||||
"unsecure": _bool(S.get("unsecure")),
|
||||
"page_size": int(S["page_size"]) if S.get("page_size") else 500,
|
||||
}
|
||||
|
||||
|
||||
def _tls(unsecure, ssl_version):
|
||||
version = SSL_VERSIONS.get(ssl_version)
|
||||
if unsecure:
|
||||
return Tls(validate=ssl.CERT_NONE, ca_certs_file=None, ciphers=CIPHERS, version=version)
|
||||
return Tls(validate=ssl.CERT_REQUIRED, ca_certs_file=os.environ.get("SSL_CERT_FILE"), version=version)
|
||||
|
||||
|
||||
def _server(c):
|
||||
secure = c["secure"]
|
||||
if secure in ("SSL", "TLS"):
|
||||
return Server(host=c["host"], port=c["port"], use_ssl=True, tls=_tls(c["unsecure"], c["ssl_version"]), connect_timeout=120)
|
||||
if secure == "Start TLS":
|
||||
return Server(host=c["host"], port=c["port"], use_ssl=False, tls=_tls(c["unsecure"], c["ssl_version"]), connect_timeout=120)
|
||||
return Server(host=c["host"], port=c["port"], connect_timeout=120)
|
||||
|
||||
|
||||
def _auto_bind(secure):
|
||||
return AUTO_BIND_TLS_BEFORE_BIND if secure == "Start TLS" else AUTO_BIND_NO_TLS
|
||||
|
||||
|
||||
def connect():
|
||||
global CONN
|
||||
c = _cfg()
|
||||
server = _server(c)
|
||||
if c["ntlm"]:
|
||||
domain_user = c["user"] if "\\" in c["user"] else c["host"] + "\\" + c["user"]
|
||||
CONN = Connection(server, domain_user, password=c["password"], authentication=NTLM, auto_bind=_auto_bind(c["secure"]))
|
||||
else:
|
||||
CONN = Connection(server, user=c["user"], password=c["password"], auto_bind=_auto_bind(c["secure"]))
|
||||
return CONN, c
|
||||
|
||||
|
||||
def base_dn_arg():
|
||||
return I.get("base-dn") or I.get("base_dn") or _cfg()["base_dn"]
|
||||
|
||||
|
||||
def search_paging(conn, search_filter, search_base, attributes=None, page_size=100, size_limit=0, time_limit=0):
|
||||
entries = []
|
||||
cookie = None
|
||||
total = 0
|
||||
left = size_limit
|
||||
while True:
|
||||
ps = page_size
|
||||
if 0 < left < page_size:
|
||||
ps = left
|
||||
conn.search(search_base, search_filter, search_scope=SUBTREE, attributes=attributes,
|
||||
paged_size=ps, paged_cookie=cookie, time_limit=time_limit)
|
||||
left -= len(conn.entries)
|
||||
total += len(conn.entries)
|
||||
try:
|
||||
cookie = conn.result["controls"]["1.2.840.113556.1.4.319"]["value"]["cookie"]
|
||||
except (KeyError, TypeError):
|
||||
cookie = None
|
||||
entries.extend(conn.entries)
|
||||
if (size_limit and size_limit <= total) or not cookie:
|
||||
break
|
||||
flat = []
|
||||
for entry in entries:
|
||||
obj = json.loads(entry.entry_to_json())
|
||||
flat_entry = {"dn": obj["dn"]}
|
||||
for attr in obj.get("attributes", {}):
|
||||
flat_entry[attr] = obj["attributes"][attr]
|
||||
flat.append(flat_entry)
|
||||
return flat
|
||||
|
||||
|
||||
def user_dn(conn, sam, search_base):
|
||||
if "\\" in sam:
|
||||
sam = sam.split("\\", 1)[1]
|
||||
flat = search_paging(conn, f"(&(objectClass=user)(sAMAccountName={escape_filter_chars(sam)}))",
|
||||
search_base, page_size=1, size_limit=1)
|
||||
if not flat:
|
||||
raise Exception(f"Could not find user '{sam}'")
|
||||
return flat[0]["dn"]
|
||||
|
||||
|
||||
def computer_dn(conn, name, search_base):
|
||||
flat = search_paging(conn, f"(&(objectClass=user)(objectCategory=computer)(name={escape_filter_chars(name)}))",
|
||||
search_base, page_size=1, size_limit=1)
|
||||
if not flat:
|
||||
raise Exception(f"Could not find computer '{name}'")
|
||||
return flat[0]["dn"]
|
||||
|
||||
|
||||
def group_dn(conn, name, search_base):
|
||||
flat = search_paging(conn, f"(&(objectClass=group)(cn={escape_filter_chars(name)}))",
|
||||
search_base, page_size=1, size_limit=1)
|
||||
if not flat:
|
||||
raise Exception(f"Could not find group '{name}'")
|
||||
return flat[0]["dn"]
|
||||
|
||||
|
||||
def modify(conn, dn, modification):
|
||||
if not conn.modify(dn, modification):
|
||||
raise Exception(f"Failed to modify {dn}: {json.dumps(conn.result, default=str)}")
|
||||
|
||||
|
||||
def first_uac(values):
|
||||
if isinstance(values, list):
|
||||
return values[0] if values else 0
|
||||
return values or 0
|
||||
|
||||
|
||||
def uac_fields(value):
|
||||
value = int(value)
|
||||
flags = {
|
||||
"SCRIPT": 0x1, "ACCOUNTDISABLE": 0x2, "HOMEDIR_REQUIRED": 0x8, "LOCKOUT": 0x10,
|
||||
"PASSWD_NOTREQD": 0x20, "PASSWD_CANT_CHANGE": 0x40, "ENCRYPTED_TEXT_PWD_ALLOWED": 0x80,
|
||||
"TEMP_DUPLICATE_ACCOUNT": 0x100, "NORMAL_ACCOUNT": 0x200, "INTERDOMAIN_TRUST_ACCOUNT": 0x800,
|
||||
"WORKSTATION_TRUST_ACCOUNT": 0x1000, "SERVER_TRUST_ACCOUNT": 0x2000, "DONT_EXPIRE_PASSWORD": 0x10000,
|
||||
"MNS_LOGON_ACCOUNT": 0x20000, "SMARTCARD_REQUIRED": 0x40000, "TRUSTED_FOR_DELEGATION": 0x80000,
|
||||
"NOT_DELEGATED": 0x100000, "USE_DES_KEY_ONLY": 0x200000, "DONT_REQ_PREAUTH": 0x400000,
|
||||
"PASSWORD_EXPIRED": 0x800000, "TRUSTED_TO_AUTH_FOR_DELEGATION": 0x1000000, "PARTIAL_SECRETS_ACCOUNT": 0x4000000,
|
||||
}
|
||||
return {name: bool(value & mask) for name, mask in flags.items()}
|
||||
|
||||
|
||||
def current_uac(conn, search_base):
|
||||
sam = I.get("username") or I.get("sAMAccountName")
|
||||
query = "(&(objectClass=User)(objectCategory=person))"
|
||||
if sam:
|
||||
query = f"(&(objectClass=User)(objectCategory=person)(sAMAccountName={escape_filter_chars(sam)}))"
|
||||
flat = search_paging(conn, query, search_base, attributes=["userAccountControl"], size_limit=1, page_size=1)
|
||||
if flat and flat[0].get("userAccountControl"):
|
||||
return first_uac(flat[0]["userAccountControl"])
|
||||
return 512
|
||||
|
||||
|
||||
|
||||
def run():
|
||||
conn, c = connect()
|
||||
contact_dn = I["contact-dn"]
|
||||
attributes = {}
|
||||
if I.get("custom-attributes"):
|
||||
try:
|
||||
attributes = json.loads(I["custom-attributes"])
|
||||
except Exception as e:
|
||||
fail("Failed to parse custom-attributes JSON: " + str(e))
|
||||
field_map = {"display-name": "displayName", "description": "description", "email": "mail",
|
||||
"telephone-number": "telephoneNumber", "title": "title"}
|
||||
for arg, attr in field_map.items():
|
||||
if I.get(arg):
|
||||
attributes[attr] = I[arg]
|
||||
if not conn.add(contact_dn, ["top", "person", "organizationalPerson", "contact"], attributes):
|
||||
fail("Failed to create contact: " + json.dumps(conn.result, default=str))
|
||||
out({"created": contact_dn})
|
||||
|
||||
|
||||
try:
|
||||
run()
|
||||
except SystemExit:
|
||||
raise
|
||||
except LDAPException as e:
|
||||
fail("LDAP error: " + str(e))
|
||||
except Exception as e:
|
||||
fail(str(e))
|
||||
finally:
|
||||
try:
|
||||
if CONN is not None:
|
||||
CONN.unbind()
|
||||
except Exception:
|
||||
pass
|
||||
@@ -0,0 +1,235 @@
|
||||
import json, os, sys
|
||||
|
||||
S = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||
I = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||
|
||||
|
||||
def out(value):
|
||||
print(json.dumps(value, default=str))
|
||||
|
||||
|
||||
def fail(message, **extra):
|
||||
payload = {"error": message}
|
||||
payload.update(extra)
|
||||
print(json.dumps(payload, default=str))
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
try:
|
||||
import ssl
|
||||
from ldap3 import (
|
||||
Server, Connection, Tls, NTLM, SUBTREE, BASE, ALL_ATTRIBUTES,
|
||||
MODIFY_REPLACE, AUTO_BIND_NO_TLS, AUTO_BIND_TLS_BEFORE_BIND,
|
||||
)
|
||||
from ldap3.core.exceptions import LDAPException
|
||||
from ldap3.extend import microsoft
|
||||
from ldap3.utils.conv import escape_filter_chars
|
||||
except ImportError as _e:
|
||||
print(json.dumps({
|
||||
"error": "The 'ldap3' Python library is required for the Active Directory integration. "
|
||||
"Install it on the execution host (engine): pip install ldap3",
|
||||
"detail": str(_e),
|
||||
}))
|
||||
sys.exit(1)
|
||||
|
||||
SSL_VERSIONS = {
|
||||
"None": None,
|
||||
"TLS": ssl.PROTOCOL_TLS,
|
||||
"TLSv1": ssl.PROTOCOL_TLSv1,
|
||||
"TLSv1_1": ssl.PROTOCOL_TLSv1_1,
|
||||
"TLSv1_2": ssl.PROTOCOL_TLSv1_2,
|
||||
"TLS_CLIENT": ssl.PROTOCOL_TLS_CLIENT,
|
||||
}
|
||||
CIPHERS = (
|
||||
"@SECLEVEL=1:ECDHE+AESGCM:ECDHE+CHACHA20:DHE+AESGCM:DHE+CHACHA20:ECDH+AESGCM:"
|
||||
"DH+AESGCM:ECDH+AES:DH+AES:RSA+AESGCM:RSA+AES:!aNULL:!eNULL:!MD5:!DSS"
|
||||
)
|
||||
|
||||
CONN = None
|
||||
|
||||
|
||||
def _bool(v):
|
||||
if isinstance(v, bool):
|
||||
return v
|
||||
return str(v).lower() in ("1", "true", "yes")
|
||||
|
||||
|
||||
def _list(v):
|
||||
if isinstance(v, list):
|
||||
return v
|
||||
return [x.strip() for x in str(v).split(",") if x.strip()]
|
||||
|
||||
|
||||
def _cfg():
|
||||
return {
|
||||
"host": S.get("server_ip") or S.get("host") or "",
|
||||
"port": int(S["port"]) if S.get("port") else None,
|
||||
"user": S.get("username") or "",
|
||||
"password": S.get("password") or "",
|
||||
"base_dn": S.get("base_dn") or "",
|
||||
"secure": S.get("secure_connection") or "SSL",
|
||||
"ssl_version": S.get("ssl_version") or "None",
|
||||
"ntlm": _bool(S.get("ntlm")),
|
||||
"unsecure": _bool(S.get("unsecure")),
|
||||
"page_size": int(S["page_size"]) if S.get("page_size") else 500,
|
||||
}
|
||||
|
||||
|
||||
def _tls(unsecure, ssl_version):
|
||||
version = SSL_VERSIONS.get(ssl_version)
|
||||
if unsecure:
|
||||
return Tls(validate=ssl.CERT_NONE, ca_certs_file=None, ciphers=CIPHERS, version=version)
|
||||
return Tls(validate=ssl.CERT_REQUIRED, ca_certs_file=os.environ.get("SSL_CERT_FILE"), version=version)
|
||||
|
||||
|
||||
def _server(c):
|
||||
secure = c["secure"]
|
||||
if secure in ("SSL", "TLS"):
|
||||
return Server(host=c["host"], port=c["port"], use_ssl=True, tls=_tls(c["unsecure"], c["ssl_version"]), connect_timeout=120)
|
||||
if secure == "Start TLS":
|
||||
return Server(host=c["host"], port=c["port"], use_ssl=False, tls=_tls(c["unsecure"], c["ssl_version"]), connect_timeout=120)
|
||||
return Server(host=c["host"], port=c["port"], connect_timeout=120)
|
||||
|
||||
|
||||
def _auto_bind(secure):
|
||||
return AUTO_BIND_TLS_BEFORE_BIND if secure == "Start TLS" else AUTO_BIND_NO_TLS
|
||||
|
||||
|
||||
def connect():
|
||||
global CONN
|
||||
c = _cfg()
|
||||
server = _server(c)
|
||||
if c["ntlm"]:
|
||||
domain_user = c["user"] if "\\" in c["user"] else c["host"] + "\\" + c["user"]
|
||||
CONN = Connection(server, domain_user, password=c["password"], authentication=NTLM, auto_bind=_auto_bind(c["secure"]))
|
||||
else:
|
||||
CONN = Connection(server, user=c["user"], password=c["password"], auto_bind=_auto_bind(c["secure"]))
|
||||
return CONN, c
|
||||
|
||||
|
||||
def base_dn_arg():
|
||||
return I.get("base-dn") or I.get("base_dn") or _cfg()["base_dn"]
|
||||
|
||||
|
||||
def search_paging(conn, search_filter, search_base, attributes=None, page_size=100, size_limit=0, time_limit=0):
|
||||
entries = []
|
||||
cookie = None
|
||||
total = 0
|
||||
left = size_limit
|
||||
while True:
|
||||
ps = page_size
|
||||
if 0 < left < page_size:
|
||||
ps = left
|
||||
conn.search(search_base, search_filter, search_scope=SUBTREE, attributes=attributes,
|
||||
paged_size=ps, paged_cookie=cookie, time_limit=time_limit)
|
||||
left -= len(conn.entries)
|
||||
total += len(conn.entries)
|
||||
try:
|
||||
cookie = conn.result["controls"]["1.2.840.113556.1.4.319"]["value"]["cookie"]
|
||||
except (KeyError, TypeError):
|
||||
cookie = None
|
||||
entries.extend(conn.entries)
|
||||
if (size_limit and size_limit <= total) or not cookie:
|
||||
break
|
||||
flat = []
|
||||
for entry in entries:
|
||||
obj = json.loads(entry.entry_to_json())
|
||||
flat_entry = {"dn": obj["dn"]}
|
||||
for attr in obj.get("attributes", {}):
|
||||
flat_entry[attr] = obj["attributes"][attr]
|
||||
flat.append(flat_entry)
|
||||
return flat
|
||||
|
||||
|
||||
def user_dn(conn, sam, search_base):
|
||||
if "\\" in sam:
|
||||
sam = sam.split("\\", 1)[1]
|
||||
flat = search_paging(conn, f"(&(objectClass=user)(sAMAccountName={escape_filter_chars(sam)}))",
|
||||
search_base, page_size=1, size_limit=1)
|
||||
if not flat:
|
||||
raise Exception(f"Could not find user '{sam}'")
|
||||
return flat[0]["dn"]
|
||||
|
||||
|
||||
def computer_dn(conn, name, search_base):
|
||||
flat = search_paging(conn, f"(&(objectClass=user)(objectCategory=computer)(name={escape_filter_chars(name)}))",
|
||||
search_base, page_size=1, size_limit=1)
|
||||
if not flat:
|
||||
raise Exception(f"Could not find computer '{name}'")
|
||||
return flat[0]["dn"]
|
||||
|
||||
|
||||
def group_dn(conn, name, search_base):
|
||||
flat = search_paging(conn, f"(&(objectClass=group)(cn={escape_filter_chars(name)}))",
|
||||
search_base, page_size=1, size_limit=1)
|
||||
if not flat:
|
||||
raise Exception(f"Could not find group '{name}'")
|
||||
return flat[0]["dn"]
|
||||
|
||||
|
||||
def modify(conn, dn, modification):
|
||||
if not conn.modify(dn, modification):
|
||||
raise Exception(f"Failed to modify {dn}: {json.dumps(conn.result, default=str)}")
|
||||
|
||||
|
||||
def first_uac(values):
|
||||
if isinstance(values, list):
|
||||
return values[0] if values else 0
|
||||
return values or 0
|
||||
|
||||
|
||||
def uac_fields(value):
|
||||
value = int(value)
|
||||
flags = {
|
||||
"SCRIPT": 0x1, "ACCOUNTDISABLE": 0x2, "HOMEDIR_REQUIRED": 0x8, "LOCKOUT": 0x10,
|
||||
"PASSWD_NOTREQD": 0x20, "PASSWD_CANT_CHANGE": 0x40, "ENCRYPTED_TEXT_PWD_ALLOWED": 0x80,
|
||||
"TEMP_DUPLICATE_ACCOUNT": 0x100, "NORMAL_ACCOUNT": 0x200, "INTERDOMAIN_TRUST_ACCOUNT": 0x800,
|
||||
"WORKSTATION_TRUST_ACCOUNT": 0x1000, "SERVER_TRUST_ACCOUNT": 0x2000, "DONT_EXPIRE_PASSWORD": 0x10000,
|
||||
"MNS_LOGON_ACCOUNT": 0x20000, "SMARTCARD_REQUIRED": 0x40000, "TRUSTED_FOR_DELEGATION": 0x80000,
|
||||
"NOT_DELEGATED": 0x100000, "USE_DES_KEY_ONLY": 0x200000, "DONT_REQ_PREAUTH": 0x400000,
|
||||
"PASSWORD_EXPIRED": 0x800000, "TRUSTED_TO_AUTH_FOR_DELEGATION": 0x1000000, "PARTIAL_SECRETS_ACCOUNT": 0x4000000,
|
||||
}
|
||||
return {name: bool(value & mask) for name, mask in flags.items()}
|
||||
|
||||
|
||||
def current_uac(conn, search_base):
|
||||
sam = I.get("username") or I.get("sAMAccountName")
|
||||
query = "(&(objectClass=User)(objectCategory=person))"
|
||||
if sam:
|
||||
query = f"(&(objectClass=User)(objectCategory=person)(sAMAccountName={escape_filter_chars(sam)}))"
|
||||
flat = search_paging(conn, query, search_base, attributes=["userAccountControl"], size_limit=1, page_size=1)
|
||||
if flat and flat[0].get("userAccountControl"):
|
||||
return first_uac(flat[0]["userAccountControl"])
|
||||
return 512
|
||||
|
||||
|
||||
|
||||
def run():
|
||||
conn, c = connect()
|
||||
dn = I["dn"]
|
||||
name = I["name"]
|
||||
type_map = {"security": "2147483650", "distribution": "2"}
|
||||
if I["group-type"] not in type_map:
|
||||
fail("group-type must be 'security' or 'distribution'")
|
||||
attributes = {"samAccountName": name, "groupType": type_map[I["group-type"]]}
|
||||
if I.get("members"):
|
||||
attributes["member"] = _list(I["members"])
|
||||
if not conn.add(dn, ["top", "group"], attributes):
|
||||
fail("Failed to create group: " + json.dumps(conn.result, default=str))
|
||||
out({"created": dn})
|
||||
|
||||
|
||||
try:
|
||||
run()
|
||||
except SystemExit:
|
||||
raise
|
||||
except LDAPException as e:
|
||||
fail("LDAP error: " + str(e))
|
||||
except Exception as e:
|
||||
fail(str(e))
|
||||
finally:
|
||||
try:
|
||||
if CONN is not None:
|
||||
CONN.unbind()
|
||||
except Exception:
|
||||
pass
|
||||
@@ -0,0 +1,246 @@
|
||||
import json, os, sys
|
||||
|
||||
S = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||
I = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||
|
||||
|
||||
def out(value):
|
||||
print(json.dumps(value, default=str))
|
||||
|
||||
|
||||
def fail(message, **extra):
|
||||
payload = {"error": message}
|
||||
payload.update(extra)
|
||||
print(json.dumps(payload, default=str))
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
try:
|
||||
import ssl
|
||||
from ldap3 import (
|
||||
Server, Connection, Tls, NTLM, SUBTREE, BASE, ALL_ATTRIBUTES,
|
||||
MODIFY_REPLACE, AUTO_BIND_NO_TLS, AUTO_BIND_TLS_BEFORE_BIND,
|
||||
)
|
||||
from ldap3.core.exceptions import LDAPException
|
||||
from ldap3.extend import microsoft
|
||||
from ldap3.utils.conv import escape_filter_chars
|
||||
except ImportError as _e:
|
||||
print(json.dumps({
|
||||
"error": "The 'ldap3' Python library is required for the Active Directory integration. "
|
||||
"Install it on the execution host (engine): pip install ldap3",
|
||||
"detail": str(_e),
|
||||
}))
|
||||
sys.exit(1)
|
||||
|
||||
SSL_VERSIONS = {
|
||||
"None": None,
|
||||
"TLS": ssl.PROTOCOL_TLS,
|
||||
"TLSv1": ssl.PROTOCOL_TLSv1,
|
||||
"TLSv1_1": ssl.PROTOCOL_TLSv1_1,
|
||||
"TLSv1_2": ssl.PROTOCOL_TLSv1_2,
|
||||
"TLS_CLIENT": ssl.PROTOCOL_TLS_CLIENT,
|
||||
}
|
||||
CIPHERS = (
|
||||
"@SECLEVEL=1:ECDHE+AESGCM:ECDHE+CHACHA20:DHE+AESGCM:DHE+CHACHA20:ECDH+AESGCM:"
|
||||
"DH+AESGCM:ECDH+AES:DH+AES:RSA+AESGCM:RSA+AES:!aNULL:!eNULL:!MD5:!DSS"
|
||||
)
|
||||
|
||||
CONN = None
|
||||
|
||||
|
||||
def _bool(v):
|
||||
if isinstance(v, bool):
|
||||
return v
|
||||
return str(v).lower() in ("1", "true", "yes")
|
||||
|
||||
|
||||
def _list(v):
|
||||
if isinstance(v, list):
|
||||
return v
|
||||
return [x.strip() for x in str(v).split(",") if x.strip()]
|
||||
|
||||
|
||||
def _cfg():
|
||||
return {
|
||||
"host": S.get("server_ip") or S.get("host") or "",
|
||||
"port": int(S["port"]) if S.get("port") else None,
|
||||
"user": S.get("username") or "",
|
||||
"password": S.get("password") or "",
|
||||
"base_dn": S.get("base_dn") or "",
|
||||
"secure": S.get("secure_connection") or "SSL",
|
||||
"ssl_version": S.get("ssl_version") or "None",
|
||||
"ntlm": _bool(S.get("ntlm")),
|
||||
"unsecure": _bool(S.get("unsecure")),
|
||||
"page_size": int(S["page_size"]) if S.get("page_size") else 500,
|
||||
}
|
||||
|
||||
|
||||
def _tls(unsecure, ssl_version):
|
||||
version = SSL_VERSIONS.get(ssl_version)
|
||||
if unsecure:
|
||||
return Tls(validate=ssl.CERT_NONE, ca_certs_file=None, ciphers=CIPHERS, version=version)
|
||||
return Tls(validate=ssl.CERT_REQUIRED, ca_certs_file=os.environ.get("SSL_CERT_FILE"), version=version)
|
||||
|
||||
|
||||
def _server(c):
|
||||
secure = c["secure"]
|
||||
if secure in ("SSL", "TLS"):
|
||||
return Server(host=c["host"], port=c["port"], use_ssl=True, tls=_tls(c["unsecure"], c["ssl_version"]), connect_timeout=120)
|
||||
if secure == "Start TLS":
|
||||
return Server(host=c["host"], port=c["port"], use_ssl=False, tls=_tls(c["unsecure"], c["ssl_version"]), connect_timeout=120)
|
||||
return Server(host=c["host"], port=c["port"], connect_timeout=120)
|
||||
|
||||
|
||||
def _auto_bind(secure):
|
||||
return AUTO_BIND_TLS_BEFORE_BIND if secure == "Start TLS" else AUTO_BIND_NO_TLS
|
||||
|
||||
|
||||
def connect():
|
||||
global CONN
|
||||
c = _cfg()
|
||||
server = _server(c)
|
||||
if c["ntlm"]:
|
||||
domain_user = c["user"] if "\\" in c["user"] else c["host"] + "\\" + c["user"]
|
||||
CONN = Connection(server, domain_user, password=c["password"], authentication=NTLM, auto_bind=_auto_bind(c["secure"]))
|
||||
else:
|
||||
CONN = Connection(server, user=c["user"], password=c["password"], auto_bind=_auto_bind(c["secure"]))
|
||||
return CONN, c
|
||||
|
||||
|
||||
def base_dn_arg():
|
||||
return I.get("base-dn") or I.get("base_dn") or _cfg()["base_dn"]
|
||||
|
||||
|
||||
def search_paging(conn, search_filter, search_base, attributes=None, page_size=100, size_limit=0, time_limit=0):
|
||||
entries = []
|
||||
cookie = None
|
||||
total = 0
|
||||
left = size_limit
|
||||
while True:
|
||||
ps = page_size
|
||||
if 0 < left < page_size:
|
||||
ps = left
|
||||
conn.search(search_base, search_filter, search_scope=SUBTREE, attributes=attributes,
|
||||
paged_size=ps, paged_cookie=cookie, time_limit=time_limit)
|
||||
left -= len(conn.entries)
|
||||
total += len(conn.entries)
|
||||
try:
|
||||
cookie = conn.result["controls"]["1.2.840.113556.1.4.319"]["value"]["cookie"]
|
||||
except (KeyError, TypeError):
|
||||
cookie = None
|
||||
entries.extend(conn.entries)
|
||||
if (size_limit and size_limit <= total) or not cookie:
|
||||
break
|
||||
flat = []
|
||||
for entry in entries:
|
||||
obj = json.loads(entry.entry_to_json())
|
||||
flat_entry = {"dn": obj["dn"]}
|
||||
for attr in obj.get("attributes", {}):
|
||||
flat_entry[attr] = obj["attributes"][attr]
|
||||
flat.append(flat_entry)
|
||||
return flat
|
||||
|
||||
|
||||
def user_dn(conn, sam, search_base):
|
||||
if "\\" in sam:
|
||||
sam = sam.split("\\", 1)[1]
|
||||
flat = search_paging(conn, f"(&(objectClass=user)(sAMAccountName={escape_filter_chars(sam)}))",
|
||||
search_base, page_size=1, size_limit=1)
|
||||
if not flat:
|
||||
raise Exception(f"Could not find user '{sam}'")
|
||||
return flat[0]["dn"]
|
||||
|
||||
|
||||
def computer_dn(conn, name, search_base):
|
||||
flat = search_paging(conn, f"(&(objectClass=user)(objectCategory=computer)(name={escape_filter_chars(name)}))",
|
||||
search_base, page_size=1, size_limit=1)
|
||||
if not flat:
|
||||
raise Exception(f"Could not find computer '{name}'")
|
||||
return flat[0]["dn"]
|
||||
|
||||
|
||||
def group_dn(conn, name, search_base):
|
||||
flat = search_paging(conn, f"(&(objectClass=group)(cn={escape_filter_chars(name)}))",
|
||||
search_base, page_size=1, size_limit=1)
|
||||
if not flat:
|
||||
raise Exception(f"Could not find group '{name}'")
|
||||
return flat[0]["dn"]
|
||||
|
||||
|
||||
def modify(conn, dn, modification):
|
||||
if not conn.modify(dn, modification):
|
||||
raise Exception(f"Failed to modify {dn}: {json.dumps(conn.result, default=str)}")
|
||||
|
||||
|
||||
def first_uac(values):
|
||||
if isinstance(values, list):
|
||||
return values[0] if values else 0
|
||||
return values or 0
|
||||
|
||||
|
||||
def uac_fields(value):
|
||||
value = int(value)
|
||||
flags = {
|
||||
"SCRIPT": 0x1, "ACCOUNTDISABLE": 0x2, "HOMEDIR_REQUIRED": 0x8, "LOCKOUT": 0x10,
|
||||
"PASSWD_NOTREQD": 0x20, "PASSWD_CANT_CHANGE": 0x40, "ENCRYPTED_TEXT_PWD_ALLOWED": 0x80,
|
||||
"TEMP_DUPLICATE_ACCOUNT": 0x100, "NORMAL_ACCOUNT": 0x200, "INTERDOMAIN_TRUST_ACCOUNT": 0x800,
|
||||
"WORKSTATION_TRUST_ACCOUNT": 0x1000, "SERVER_TRUST_ACCOUNT": 0x2000, "DONT_EXPIRE_PASSWORD": 0x10000,
|
||||
"MNS_LOGON_ACCOUNT": 0x20000, "SMARTCARD_REQUIRED": 0x40000, "TRUSTED_FOR_DELEGATION": 0x80000,
|
||||
"NOT_DELEGATED": 0x100000, "USE_DES_KEY_ONLY": 0x200000, "DONT_REQ_PREAUTH": 0x400000,
|
||||
"PASSWORD_EXPIRED": 0x800000, "TRUSTED_TO_AUTH_FOR_DELEGATION": 0x1000000, "PARTIAL_SECRETS_ACCOUNT": 0x4000000,
|
||||
}
|
||||
return {name: bool(value & mask) for name, mask in flags.items()}
|
||||
|
||||
|
||||
def current_uac(conn, search_base):
|
||||
sam = I.get("username") or I.get("sAMAccountName")
|
||||
query = "(&(objectClass=User)(objectCategory=person))"
|
||||
if sam:
|
||||
query = f"(&(objectClass=User)(objectCategory=person)(sAMAccountName={escape_filter_chars(sam)}))"
|
||||
flat = search_paging(conn, query, search_base, attributes=["userAccountControl"], size_limit=1, page_size=1)
|
||||
if flat and flat[0].get("userAccountControl"):
|
||||
return first_uac(flat[0]["userAccountControl"])
|
||||
return 512
|
||||
|
||||
|
||||
|
||||
def run():
|
||||
conn, c = connect()
|
||||
if c["secure"] not in ("SSL", "TLS"):
|
||||
fail("Creating a user requires a secure connection (SSL or TLS).")
|
||||
user_dn_val = I["user-dn"]
|
||||
object_classes = ["top", "person", "organizationalPerson", "user"]
|
||||
attributes = {"sAMAccountName": I["username"]}
|
||||
field_map = {"display-name": "displayName", "description": "description", "email": "mail",
|
||||
"telephone-number": "telephoneNumber", "title": "title"}
|
||||
for arg, attr in field_map.items():
|
||||
if I.get(arg):
|
||||
attributes[attr] = I[arg]
|
||||
if I.get("custom-attributes"):
|
||||
try:
|
||||
attributes.update(json.loads(I["custom-attributes"]))
|
||||
except Exception as e:
|
||||
fail("Failed to parse custom-attributes JSON: " + str(e))
|
||||
if not conn.add(user_dn_val, object_classes, attributes):
|
||||
fail("Failed to create user: " + json.dumps(conn.result, default=str))
|
||||
if not conn.extend.microsoft.modify_password(user_dn_val, I["password"]):
|
||||
fail("User created but failed to set password: " + json.dumps(conn.result, default=str))
|
||||
modify(conn, user_dn_val, {"userAccountControl": [(MODIFY_REPLACE, [512])],
|
||||
"pwdLastSet": [(MODIFY_REPLACE, ["0"])]})
|
||||
out({"created": user_dn_val})
|
||||
|
||||
|
||||
try:
|
||||
run()
|
||||
except SystemExit:
|
||||
raise
|
||||
except LDAPException as e:
|
||||
fail("LDAP error: " + str(e))
|
||||
except Exception as e:
|
||||
fail(str(e))
|
||||
finally:
|
||||
try:
|
||||
if CONN is not None:
|
||||
CONN.unbind()
|
||||
except Exception:
|
||||
pass
|
||||
@@ -0,0 +1,227 @@
|
||||
import json, os, sys
|
||||
|
||||
S = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||
I = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||
|
||||
|
||||
def out(value):
|
||||
print(json.dumps(value, default=str))
|
||||
|
||||
|
||||
def fail(message, **extra):
|
||||
payload = {"error": message}
|
||||
payload.update(extra)
|
||||
print(json.dumps(payload, default=str))
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
try:
|
||||
import ssl
|
||||
from ldap3 import (
|
||||
Server, Connection, Tls, NTLM, SUBTREE, BASE, ALL_ATTRIBUTES,
|
||||
MODIFY_REPLACE, AUTO_BIND_NO_TLS, AUTO_BIND_TLS_BEFORE_BIND,
|
||||
)
|
||||
from ldap3.core.exceptions import LDAPException
|
||||
from ldap3.extend import microsoft
|
||||
from ldap3.utils.conv import escape_filter_chars
|
||||
except ImportError as _e:
|
||||
print(json.dumps({
|
||||
"error": "The 'ldap3' Python library is required for the Active Directory integration. "
|
||||
"Install it on the execution host (engine): pip install ldap3",
|
||||
"detail": str(_e),
|
||||
}))
|
||||
sys.exit(1)
|
||||
|
||||
SSL_VERSIONS = {
|
||||
"None": None,
|
||||
"TLS": ssl.PROTOCOL_TLS,
|
||||
"TLSv1": ssl.PROTOCOL_TLSv1,
|
||||
"TLSv1_1": ssl.PROTOCOL_TLSv1_1,
|
||||
"TLSv1_2": ssl.PROTOCOL_TLSv1_2,
|
||||
"TLS_CLIENT": ssl.PROTOCOL_TLS_CLIENT,
|
||||
}
|
||||
CIPHERS = (
|
||||
"@SECLEVEL=1:ECDHE+AESGCM:ECDHE+CHACHA20:DHE+AESGCM:DHE+CHACHA20:ECDH+AESGCM:"
|
||||
"DH+AESGCM:ECDH+AES:DH+AES:RSA+AESGCM:RSA+AES:!aNULL:!eNULL:!MD5:!DSS"
|
||||
)
|
||||
|
||||
CONN = None
|
||||
|
||||
|
||||
def _bool(v):
|
||||
if isinstance(v, bool):
|
||||
return v
|
||||
return str(v).lower() in ("1", "true", "yes")
|
||||
|
||||
|
||||
def _list(v):
|
||||
if isinstance(v, list):
|
||||
return v
|
||||
return [x.strip() for x in str(v).split(",") if x.strip()]
|
||||
|
||||
|
||||
def _cfg():
|
||||
return {
|
||||
"host": S.get("server_ip") or S.get("host") or "",
|
||||
"port": int(S["port"]) if S.get("port") else None,
|
||||
"user": S.get("username") or "",
|
||||
"password": S.get("password") or "",
|
||||
"base_dn": S.get("base_dn") or "",
|
||||
"secure": S.get("secure_connection") or "SSL",
|
||||
"ssl_version": S.get("ssl_version") or "None",
|
||||
"ntlm": _bool(S.get("ntlm")),
|
||||
"unsecure": _bool(S.get("unsecure")),
|
||||
"page_size": int(S["page_size"]) if S.get("page_size") else 500,
|
||||
}
|
||||
|
||||
|
||||
def _tls(unsecure, ssl_version):
|
||||
version = SSL_VERSIONS.get(ssl_version)
|
||||
if unsecure:
|
||||
return Tls(validate=ssl.CERT_NONE, ca_certs_file=None, ciphers=CIPHERS, version=version)
|
||||
return Tls(validate=ssl.CERT_REQUIRED, ca_certs_file=os.environ.get("SSL_CERT_FILE"), version=version)
|
||||
|
||||
|
||||
def _server(c):
|
||||
secure = c["secure"]
|
||||
if secure in ("SSL", "TLS"):
|
||||
return Server(host=c["host"], port=c["port"], use_ssl=True, tls=_tls(c["unsecure"], c["ssl_version"]), connect_timeout=120)
|
||||
if secure == "Start TLS":
|
||||
return Server(host=c["host"], port=c["port"], use_ssl=False, tls=_tls(c["unsecure"], c["ssl_version"]), connect_timeout=120)
|
||||
return Server(host=c["host"], port=c["port"], connect_timeout=120)
|
||||
|
||||
|
||||
def _auto_bind(secure):
|
||||
return AUTO_BIND_TLS_BEFORE_BIND if secure == "Start TLS" else AUTO_BIND_NO_TLS
|
||||
|
||||
|
||||
def connect():
|
||||
global CONN
|
||||
c = _cfg()
|
||||
server = _server(c)
|
||||
if c["ntlm"]:
|
||||
domain_user = c["user"] if "\\" in c["user"] else c["host"] + "\\" + c["user"]
|
||||
CONN = Connection(server, domain_user, password=c["password"], authentication=NTLM, auto_bind=_auto_bind(c["secure"]))
|
||||
else:
|
||||
CONN = Connection(server, user=c["user"], password=c["password"], auto_bind=_auto_bind(c["secure"]))
|
||||
return CONN, c
|
||||
|
||||
|
||||
def base_dn_arg():
|
||||
return I.get("base-dn") or I.get("base_dn") or _cfg()["base_dn"]
|
||||
|
||||
|
||||
def search_paging(conn, search_filter, search_base, attributes=None, page_size=100, size_limit=0, time_limit=0):
|
||||
entries = []
|
||||
cookie = None
|
||||
total = 0
|
||||
left = size_limit
|
||||
while True:
|
||||
ps = page_size
|
||||
if 0 < left < page_size:
|
||||
ps = left
|
||||
conn.search(search_base, search_filter, search_scope=SUBTREE, attributes=attributes,
|
||||
paged_size=ps, paged_cookie=cookie, time_limit=time_limit)
|
||||
left -= len(conn.entries)
|
||||
total += len(conn.entries)
|
||||
try:
|
||||
cookie = conn.result["controls"]["1.2.840.113556.1.4.319"]["value"]["cookie"]
|
||||
except (KeyError, TypeError):
|
||||
cookie = None
|
||||
entries.extend(conn.entries)
|
||||
if (size_limit and size_limit <= total) or not cookie:
|
||||
break
|
||||
flat = []
|
||||
for entry in entries:
|
||||
obj = json.loads(entry.entry_to_json())
|
||||
flat_entry = {"dn": obj["dn"]}
|
||||
for attr in obj.get("attributes", {}):
|
||||
flat_entry[attr] = obj["attributes"][attr]
|
||||
flat.append(flat_entry)
|
||||
return flat
|
||||
|
||||
|
||||
def user_dn(conn, sam, search_base):
|
||||
if "\\" in sam:
|
||||
sam = sam.split("\\", 1)[1]
|
||||
flat = search_paging(conn, f"(&(objectClass=user)(sAMAccountName={escape_filter_chars(sam)}))",
|
||||
search_base, page_size=1, size_limit=1)
|
||||
if not flat:
|
||||
raise Exception(f"Could not find user '{sam}'")
|
||||
return flat[0]["dn"]
|
||||
|
||||
|
||||
def computer_dn(conn, name, search_base):
|
||||
flat = search_paging(conn, f"(&(objectClass=user)(objectCategory=computer)(name={escape_filter_chars(name)}))",
|
||||
search_base, page_size=1, size_limit=1)
|
||||
if not flat:
|
||||
raise Exception(f"Could not find computer '{name}'")
|
||||
return flat[0]["dn"]
|
||||
|
||||
|
||||
def group_dn(conn, name, search_base):
|
||||
flat = search_paging(conn, f"(&(objectClass=group)(cn={escape_filter_chars(name)}))",
|
||||
search_base, page_size=1, size_limit=1)
|
||||
if not flat:
|
||||
raise Exception(f"Could not find group '{name}'")
|
||||
return flat[0]["dn"]
|
||||
|
||||
|
||||
def modify(conn, dn, modification):
|
||||
if not conn.modify(dn, modification):
|
||||
raise Exception(f"Failed to modify {dn}: {json.dumps(conn.result, default=str)}")
|
||||
|
||||
|
||||
def first_uac(values):
|
||||
if isinstance(values, list):
|
||||
return values[0] if values else 0
|
||||
return values or 0
|
||||
|
||||
|
||||
def uac_fields(value):
|
||||
value = int(value)
|
||||
flags = {
|
||||
"SCRIPT": 0x1, "ACCOUNTDISABLE": 0x2, "HOMEDIR_REQUIRED": 0x8, "LOCKOUT": 0x10,
|
||||
"PASSWD_NOTREQD": 0x20, "PASSWD_CANT_CHANGE": 0x40, "ENCRYPTED_TEXT_PWD_ALLOWED": 0x80,
|
||||
"TEMP_DUPLICATE_ACCOUNT": 0x100, "NORMAL_ACCOUNT": 0x200, "INTERDOMAIN_TRUST_ACCOUNT": 0x800,
|
||||
"WORKSTATION_TRUST_ACCOUNT": 0x1000, "SERVER_TRUST_ACCOUNT": 0x2000, "DONT_EXPIRE_PASSWORD": 0x10000,
|
||||
"MNS_LOGON_ACCOUNT": 0x20000, "SMARTCARD_REQUIRED": 0x40000, "TRUSTED_FOR_DELEGATION": 0x80000,
|
||||
"NOT_DELEGATED": 0x100000, "USE_DES_KEY_ONLY": 0x200000, "DONT_REQ_PREAUTH": 0x400000,
|
||||
"PASSWORD_EXPIRED": 0x800000, "TRUSTED_TO_AUTH_FOR_DELEGATION": 0x1000000, "PARTIAL_SECRETS_ACCOUNT": 0x4000000,
|
||||
}
|
||||
return {name: bool(value & mask) for name, mask in flags.items()}
|
||||
|
||||
|
||||
def current_uac(conn, search_base):
|
||||
sam = I.get("username") or I.get("sAMAccountName")
|
||||
query = "(&(objectClass=User)(objectCategory=person))"
|
||||
if sam:
|
||||
query = f"(&(objectClass=User)(objectCategory=person)(sAMAccountName={escape_filter_chars(sam)}))"
|
||||
flat = search_paging(conn, query, search_base, attributes=["userAccountControl"], size_limit=1, page_size=1)
|
||||
if flat and flat[0].get("userAccountControl"):
|
||||
return first_uac(flat[0]["userAccountControl"])
|
||||
return 512
|
||||
|
||||
|
||||
|
||||
def run():
|
||||
conn, c = connect()
|
||||
if not conn.delete(I["dn"]):
|
||||
fail("Failed to delete group: " + json.dumps(conn.result, default=str))
|
||||
out({"deleted": I["dn"]})
|
||||
|
||||
|
||||
try:
|
||||
run()
|
||||
except SystemExit:
|
||||
raise
|
||||
except LDAPException as e:
|
||||
fail("LDAP error: " + str(e))
|
||||
except Exception as e:
|
||||
fail(str(e))
|
||||
finally:
|
||||
try:
|
||||
if CONN is not None:
|
||||
CONN.unbind()
|
||||
except Exception:
|
||||
pass
|
||||
@@ -0,0 +1,228 @@
|
||||
import json, os, sys
|
||||
|
||||
S = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||
I = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||
|
||||
|
||||
def out(value):
|
||||
print(json.dumps(value, default=str))
|
||||
|
||||
|
||||
def fail(message, **extra):
|
||||
payload = {"error": message}
|
||||
payload.update(extra)
|
||||
print(json.dumps(payload, default=str))
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
try:
|
||||
import ssl
|
||||
from ldap3 import (
|
||||
Server, Connection, Tls, NTLM, SUBTREE, BASE, ALL_ATTRIBUTES,
|
||||
MODIFY_REPLACE, AUTO_BIND_NO_TLS, AUTO_BIND_TLS_BEFORE_BIND,
|
||||
)
|
||||
from ldap3.core.exceptions import LDAPException
|
||||
from ldap3.extend import microsoft
|
||||
from ldap3.utils.conv import escape_filter_chars
|
||||
except ImportError as _e:
|
||||
print(json.dumps({
|
||||
"error": "The 'ldap3' Python library is required for the Active Directory integration. "
|
||||
"Install it on the execution host (engine): pip install ldap3",
|
||||
"detail": str(_e),
|
||||
}))
|
||||
sys.exit(1)
|
||||
|
||||
SSL_VERSIONS = {
|
||||
"None": None,
|
||||
"TLS": ssl.PROTOCOL_TLS,
|
||||
"TLSv1": ssl.PROTOCOL_TLSv1,
|
||||
"TLSv1_1": ssl.PROTOCOL_TLSv1_1,
|
||||
"TLSv1_2": ssl.PROTOCOL_TLSv1_2,
|
||||
"TLS_CLIENT": ssl.PROTOCOL_TLS_CLIENT,
|
||||
}
|
||||
CIPHERS = (
|
||||
"@SECLEVEL=1:ECDHE+AESGCM:ECDHE+CHACHA20:DHE+AESGCM:DHE+CHACHA20:ECDH+AESGCM:"
|
||||
"DH+AESGCM:ECDH+AES:DH+AES:RSA+AESGCM:RSA+AES:!aNULL:!eNULL:!MD5:!DSS"
|
||||
)
|
||||
|
||||
CONN = None
|
||||
|
||||
|
||||
def _bool(v):
|
||||
if isinstance(v, bool):
|
||||
return v
|
||||
return str(v).lower() in ("1", "true", "yes")
|
||||
|
||||
|
||||
def _list(v):
|
||||
if isinstance(v, list):
|
||||
return v
|
||||
return [x.strip() for x in str(v).split(",") if x.strip()]
|
||||
|
||||
|
||||
def _cfg():
|
||||
return {
|
||||
"host": S.get("server_ip") or S.get("host") or "",
|
||||
"port": int(S["port"]) if S.get("port") else None,
|
||||
"user": S.get("username") or "",
|
||||
"password": S.get("password") or "",
|
||||
"base_dn": S.get("base_dn") or "",
|
||||
"secure": S.get("secure_connection") or "SSL",
|
||||
"ssl_version": S.get("ssl_version") or "None",
|
||||
"ntlm": _bool(S.get("ntlm")),
|
||||
"unsecure": _bool(S.get("unsecure")),
|
||||
"page_size": int(S["page_size"]) if S.get("page_size") else 500,
|
||||
}
|
||||
|
||||
|
||||
def _tls(unsecure, ssl_version):
|
||||
version = SSL_VERSIONS.get(ssl_version)
|
||||
if unsecure:
|
||||
return Tls(validate=ssl.CERT_NONE, ca_certs_file=None, ciphers=CIPHERS, version=version)
|
||||
return Tls(validate=ssl.CERT_REQUIRED, ca_certs_file=os.environ.get("SSL_CERT_FILE"), version=version)
|
||||
|
||||
|
||||
def _server(c):
|
||||
secure = c["secure"]
|
||||
if secure in ("SSL", "TLS"):
|
||||
return Server(host=c["host"], port=c["port"], use_ssl=True, tls=_tls(c["unsecure"], c["ssl_version"]), connect_timeout=120)
|
||||
if secure == "Start TLS":
|
||||
return Server(host=c["host"], port=c["port"], use_ssl=False, tls=_tls(c["unsecure"], c["ssl_version"]), connect_timeout=120)
|
||||
return Server(host=c["host"], port=c["port"], connect_timeout=120)
|
||||
|
||||
|
||||
def _auto_bind(secure):
|
||||
return AUTO_BIND_TLS_BEFORE_BIND if secure == "Start TLS" else AUTO_BIND_NO_TLS
|
||||
|
||||
|
||||
def connect():
|
||||
global CONN
|
||||
c = _cfg()
|
||||
server = _server(c)
|
||||
if c["ntlm"]:
|
||||
domain_user = c["user"] if "\\" in c["user"] else c["host"] + "\\" + c["user"]
|
||||
CONN = Connection(server, domain_user, password=c["password"], authentication=NTLM, auto_bind=_auto_bind(c["secure"]))
|
||||
else:
|
||||
CONN = Connection(server, user=c["user"], password=c["password"], auto_bind=_auto_bind(c["secure"]))
|
||||
return CONN, c
|
||||
|
||||
|
||||
def base_dn_arg():
|
||||
return I.get("base-dn") or I.get("base_dn") or _cfg()["base_dn"]
|
||||
|
||||
|
||||
def search_paging(conn, search_filter, search_base, attributes=None, page_size=100, size_limit=0, time_limit=0):
|
||||
entries = []
|
||||
cookie = None
|
||||
total = 0
|
||||
left = size_limit
|
||||
while True:
|
||||
ps = page_size
|
||||
if 0 < left < page_size:
|
||||
ps = left
|
||||
conn.search(search_base, search_filter, search_scope=SUBTREE, attributes=attributes,
|
||||
paged_size=ps, paged_cookie=cookie, time_limit=time_limit)
|
||||
left -= len(conn.entries)
|
||||
total += len(conn.entries)
|
||||
try:
|
||||
cookie = conn.result["controls"]["1.2.840.113556.1.4.319"]["value"]["cookie"]
|
||||
except (KeyError, TypeError):
|
||||
cookie = None
|
||||
entries.extend(conn.entries)
|
||||
if (size_limit and size_limit <= total) or not cookie:
|
||||
break
|
||||
flat = []
|
||||
for entry in entries:
|
||||
obj = json.loads(entry.entry_to_json())
|
||||
flat_entry = {"dn": obj["dn"]}
|
||||
for attr in obj.get("attributes", {}):
|
||||
flat_entry[attr] = obj["attributes"][attr]
|
||||
flat.append(flat_entry)
|
||||
return flat
|
||||
|
||||
|
||||
def user_dn(conn, sam, search_base):
|
||||
if "\\" in sam:
|
||||
sam = sam.split("\\", 1)[1]
|
||||
flat = search_paging(conn, f"(&(objectClass=user)(sAMAccountName={escape_filter_chars(sam)}))",
|
||||
search_base, page_size=1, size_limit=1)
|
||||
if not flat:
|
||||
raise Exception(f"Could not find user '{sam}'")
|
||||
return flat[0]["dn"]
|
||||
|
||||
|
||||
def computer_dn(conn, name, search_base):
|
||||
flat = search_paging(conn, f"(&(objectClass=user)(objectCategory=computer)(name={escape_filter_chars(name)}))",
|
||||
search_base, page_size=1, size_limit=1)
|
||||
if not flat:
|
||||
raise Exception(f"Could not find computer '{name}'")
|
||||
return flat[0]["dn"]
|
||||
|
||||
|
||||
def group_dn(conn, name, search_base):
|
||||
flat = search_paging(conn, f"(&(objectClass=group)(cn={escape_filter_chars(name)}))",
|
||||
search_base, page_size=1, size_limit=1)
|
||||
if not flat:
|
||||
raise Exception(f"Could not find group '{name}'")
|
||||
return flat[0]["dn"]
|
||||
|
||||
|
||||
def modify(conn, dn, modification):
|
||||
if not conn.modify(dn, modification):
|
||||
raise Exception(f"Failed to modify {dn}: {json.dumps(conn.result, default=str)}")
|
||||
|
||||
|
||||
def first_uac(values):
|
||||
if isinstance(values, list):
|
||||
return values[0] if values else 0
|
||||
return values or 0
|
||||
|
||||
|
||||
def uac_fields(value):
|
||||
value = int(value)
|
||||
flags = {
|
||||
"SCRIPT": 0x1, "ACCOUNTDISABLE": 0x2, "HOMEDIR_REQUIRED": 0x8, "LOCKOUT": 0x10,
|
||||
"PASSWD_NOTREQD": 0x20, "PASSWD_CANT_CHANGE": 0x40, "ENCRYPTED_TEXT_PWD_ALLOWED": 0x80,
|
||||
"TEMP_DUPLICATE_ACCOUNT": 0x100, "NORMAL_ACCOUNT": 0x200, "INTERDOMAIN_TRUST_ACCOUNT": 0x800,
|
||||
"WORKSTATION_TRUST_ACCOUNT": 0x1000, "SERVER_TRUST_ACCOUNT": 0x2000, "DONT_EXPIRE_PASSWORD": 0x10000,
|
||||
"MNS_LOGON_ACCOUNT": 0x20000, "SMARTCARD_REQUIRED": 0x40000, "TRUSTED_FOR_DELEGATION": 0x80000,
|
||||
"NOT_DELEGATED": 0x100000, "USE_DES_KEY_ONLY": 0x200000, "DONT_REQ_PREAUTH": 0x400000,
|
||||
"PASSWORD_EXPIRED": 0x800000, "TRUSTED_TO_AUTH_FOR_DELEGATION": 0x1000000, "PARTIAL_SECRETS_ACCOUNT": 0x4000000,
|
||||
}
|
||||
return {name: bool(value & mask) for name, mask in flags.items()}
|
||||
|
||||
|
||||
def current_uac(conn, search_base):
|
||||
sam = I.get("username") or I.get("sAMAccountName")
|
||||
query = "(&(objectClass=User)(objectCategory=person))"
|
||||
if sam:
|
||||
query = f"(&(objectClass=User)(objectCategory=person)(sAMAccountName={escape_filter_chars(sam)}))"
|
||||
flat = search_paging(conn, query, search_base, attributes=["userAccountControl"], size_limit=1, page_size=1)
|
||||
if flat and flat[0].get("userAccountControl"):
|
||||
return first_uac(flat[0]["userAccountControl"])
|
||||
return 512
|
||||
|
||||
|
||||
|
||||
def run():
|
||||
conn, c = connect()
|
||||
dn = I["user-dn"]
|
||||
if not conn.delete(dn):
|
||||
fail("Failed to delete: " + json.dumps(conn.result, default=str))
|
||||
out({"deleted": dn})
|
||||
|
||||
|
||||
try:
|
||||
run()
|
||||
except SystemExit:
|
||||
raise
|
||||
except LDAPException as e:
|
||||
fail("LDAP error: " + str(e))
|
||||
except Exception as e:
|
||||
fail(str(e))
|
||||
finally:
|
||||
try:
|
||||
if CONN is not None:
|
||||
CONN.unbind()
|
||||
except Exception:
|
||||
pass
|
||||
@@ -0,0 +1,229 @@
|
||||
import json, os, sys
|
||||
|
||||
S = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||
I = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||
|
||||
|
||||
def out(value):
|
||||
print(json.dumps(value, default=str))
|
||||
|
||||
|
||||
def fail(message, **extra):
|
||||
payload = {"error": message}
|
||||
payload.update(extra)
|
||||
print(json.dumps(payload, default=str))
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
try:
|
||||
import ssl
|
||||
from ldap3 import (
|
||||
Server, Connection, Tls, NTLM, SUBTREE, BASE, ALL_ATTRIBUTES,
|
||||
MODIFY_REPLACE, AUTO_BIND_NO_TLS, AUTO_BIND_TLS_BEFORE_BIND,
|
||||
)
|
||||
from ldap3.core.exceptions import LDAPException
|
||||
from ldap3.extend import microsoft
|
||||
from ldap3.utils.conv import escape_filter_chars
|
||||
except ImportError as _e:
|
||||
print(json.dumps({
|
||||
"error": "The 'ldap3' Python library is required for the Active Directory integration. "
|
||||
"Install it on the execution host (engine): pip install ldap3",
|
||||
"detail": str(_e),
|
||||
}))
|
||||
sys.exit(1)
|
||||
|
||||
SSL_VERSIONS = {
|
||||
"None": None,
|
||||
"TLS": ssl.PROTOCOL_TLS,
|
||||
"TLSv1": ssl.PROTOCOL_TLSv1,
|
||||
"TLSv1_1": ssl.PROTOCOL_TLSv1_1,
|
||||
"TLSv1_2": ssl.PROTOCOL_TLSv1_2,
|
||||
"TLS_CLIENT": ssl.PROTOCOL_TLS_CLIENT,
|
||||
}
|
||||
CIPHERS = (
|
||||
"@SECLEVEL=1:ECDHE+AESGCM:ECDHE+CHACHA20:DHE+AESGCM:DHE+CHACHA20:ECDH+AESGCM:"
|
||||
"DH+AESGCM:ECDH+AES:DH+AES:RSA+AESGCM:RSA+AES:!aNULL:!eNULL:!MD5:!DSS"
|
||||
)
|
||||
|
||||
CONN = None
|
||||
|
||||
|
||||
def _bool(v):
|
||||
if isinstance(v, bool):
|
||||
return v
|
||||
return str(v).lower() in ("1", "true", "yes")
|
||||
|
||||
|
||||
def _list(v):
|
||||
if isinstance(v, list):
|
||||
return v
|
||||
return [x.strip() for x in str(v).split(",") if x.strip()]
|
||||
|
||||
|
||||
def _cfg():
|
||||
return {
|
||||
"host": S.get("server_ip") or S.get("host") or "",
|
||||
"port": int(S["port"]) if S.get("port") else None,
|
||||
"user": S.get("username") or "",
|
||||
"password": S.get("password") or "",
|
||||
"base_dn": S.get("base_dn") or "",
|
||||
"secure": S.get("secure_connection") or "SSL",
|
||||
"ssl_version": S.get("ssl_version") or "None",
|
||||
"ntlm": _bool(S.get("ntlm")),
|
||||
"unsecure": _bool(S.get("unsecure")),
|
||||
"page_size": int(S["page_size"]) if S.get("page_size") else 500,
|
||||
}
|
||||
|
||||
|
||||
def _tls(unsecure, ssl_version):
|
||||
version = SSL_VERSIONS.get(ssl_version)
|
||||
if unsecure:
|
||||
return Tls(validate=ssl.CERT_NONE, ca_certs_file=None, ciphers=CIPHERS, version=version)
|
||||
return Tls(validate=ssl.CERT_REQUIRED, ca_certs_file=os.environ.get("SSL_CERT_FILE"), version=version)
|
||||
|
||||
|
||||
def _server(c):
|
||||
secure = c["secure"]
|
||||
if secure in ("SSL", "TLS"):
|
||||
return Server(host=c["host"], port=c["port"], use_ssl=True, tls=_tls(c["unsecure"], c["ssl_version"]), connect_timeout=120)
|
||||
if secure == "Start TLS":
|
||||
return Server(host=c["host"], port=c["port"], use_ssl=False, tls=_tls(c["unsecure"], c["ssl_version"]), connect_timeout=120)
|
||||
return Server(host=c["host"], port=c["port"], connect_timeout=120)
|
||||
|
||||
|
||||
def _auto_bind(secure):
|
||||
return AUTO_BIND_TLS_BEFORE_BIND if secure == "Start TLS" else AUTO_BIND_NO_TLS
|
||||
|
||||
|
||||
def connect():
|
||||
global CONN
|
||||
c = _cfg()
|
||||
server = _server(c)
|
||||
if c["ntlm"]:
|
||||
domain_user = c["user"] if "\\" in c["user"] else c["host"] + "\\" + c["user"]
|
||||
CONN = Connection(server, domain_user, password=c["password"], authentication=NTLM, auto_bind=_auto_bind(c["secure"]))
|
||||
else:
|
||||
CONN = Connection(server, user=c["user"], password=c["password"], auto_bind=_auto_bind(c["secure"]))
|
||||
return CONN, c
|
||||
|
||||
|
||||
def base_dn_arg():
|
||||
return I.get("base-dn") or I.get("base_dn") or _cfg()["base_dn"]
|
||||
|
||||
|
||||
def search_paging(conn, search_filter, search_base, attributes=None, page_size=100, size_limit=0, time_limit=0):
|
||||
entries = []
|
||||
cookie = None
|
||||
total = 0
|
||||
left = size_limit
|
||||
while True:
|
||||
ps = page_size
|
||||
if 0 < left < page_size:
|
||||
ps = left
|
||||
conn.search(search_base, search_filter, search_scope=SUBTREE, attributes=attributes,
|
||||
paged_size=ps, paged_cookie=cookie, time_limit=time_limit)
|
||||
left -= len(conn.entries)
|
||||
total += len(conn.entries)
|
||||
try:
|
||||
cookie = conn.result["controls"]["1.2.840.113556.1.4.319"]["value"]["cookie"]
|
||||
except (KeyError, TypeError):
|
||||
cookie = None
|
||||
entries.extend(conn.entries)
|
||||
if (size_limit and size_limit <= total) or not cookie:
|
||||
break
|
||||
flat = []
|
||||
for entry in entries:
|
||||
obj = json.loads(entry.entry_to_json())
|
||||
flat_entry = {"dn": obj["dn"]}
|
||||
for attr in obj.get("attributes", {}):
|
||||
flat_entry[attr] = obj["attributes"][attr]
|
||||
flat.append(flat_entry)
|
||||
return flat
|
||||
|
||||
|
||||
def user_dn(conn, sam, search_base):
|
||||
if "\\" in sam:
|
||||
sam = sam.split("\\", 1)[1]
|
||||
flat = search_paging(conn, f"(&(objectClass=user)(sAMAccountName={escape_filter_chars(sam)}))",
|
||||
search_base, page_size=1, size_limit=1)
|
||||
if not flat:
|
||||
raise Exception(f"Could not find user '{sam}'")
|
||||
return flat[0]["dn"]
|
||||
|
||||
|
||||
def computer_dn(conn, name, search_base):
|
||||
flat = search_paging(conn, f"(&(objectClass=user)(objectCategory=computer)(name={escape_filter_chars(name)}))",
|
||||
search_base, page_size=1, size_limit=1)
|
||||
if not flat:
|
||||
raise Exception(f"Could not find computer '{name}'")
|
||||
return flat[0]["dn"]
|
||||
|
||||
|
||||
def group_dn(conn, name, search_base):
|
||||
flat = search_paging(conn, f"(&(objectClass=group)(cn={escape_filter_chars(name)}))",
|
||||
search_base, page_size=1, size_limit=1)
|
||||
if not flat:
|
||||
raise Exception(f"Could not find group '{name}'")
|
||||
return flat[0]["dn"]
|
||||
|
||||
|
||||
def modify(conn, dn, modification):
|
||||
if not conn.modify(dn, modification):
|
||||
raise Exception(f"Failed to modify {dn}: {json.dumps(conn.result, default=str)}")
|
||||
|
||||
|
||||
def first_uac(values):
|
||||
if isinstance(values, list):
|
||||
return values[0] if values else 0
|
||||
return values or 0
|
||||
|
||||
|
||||
def uac_fields(value):
|
||||
value = int(value)
|
||||
flags = {
|
||||
"SCRIPT": 0x1, "ACCOUNTDISABLE": 0x2, "HOMEDIR_REQUIRED": 0x8, "LOCKOUT": 0x10,
|
||||
"PASSWD_NOTREQD": 0x20, "PASSWD_CANT_CHANGE": 0x40, "ENCRYPTED_TEXT_PWD_ALLOWED": 0x80,
|
||||
"TEMP_DUPLICATE_ACCOUNT": 0x100, "NORMAL_ACCOUNT": 0x200, "INTERDOMAIN_TRUST_ACCOUNT": 0x800,
|
||||
"WORKSTATION_TRUST_ACCOUNT": 0x1000, "SERVER_TRUST_ACCOUNT": 0x2000, "DONT_EXPIRE_PASSWORD": 0x10000,
|
||||
"MNS_LOGON_ACCOUNT": 0x20000, "SMARTCARD_REQUIRED": 0x40000, "TRUSTED_FOR_DELEGATION": 0x80000,
|
||||
"NOT_DELEGATED": 0x100000, "USE_DES_KEY_ONLY": 0x200000, "DONT_REQ_PREAUTH": 0x400000,
|
||||
"PASSWORD_EXPIRED": 0x800000, "TRUSTED_TO_AUTH_FOR_DELEGATION": 0x1000000, "PARTIAL_SECRETS_ACCOUNT": 0x4000000,
|
||||
}
|
||||
return {name: bool(value & mask) for name, mask in flags.items()}
|
||||
|
||||
|
||||
def current_uac(conn, search_base):
|
||||
sam = I.get("username") or I.get("sAMAccountName")
|
||||
query = "(&(objectClass=User)(objectCategory=person))"
|
||||
if sam:
|
||||
query = f"(&(objectClass=User)(objectCategory=person)(sAMAccountName={escape_filter_chars(sam)}))"
|
||||
flat = search_paging(conn, query, search_base, attributes=["userAccountControl"], size_limit=1, page_size=1)
|
||||
if flat and flat[0].get("userAccountControl"):
|
||||
return first_uac(flat[0]["userAccountControl"])
|
||||
return 512
|
||||
|
||||
|
||||
|
||||
def run():
|
||||
conn, c = connect()
|
||||
sb = I.get("base-dn") or base_dn_arg()
|
||||
dn = user_dn(conn, I["username"], sb)
|
||||
options = current_uac(conn, sb)
|
||||
modify(conn, dn, {"userAccountControl": [(MODIFY_REPLACE, [options | 0x2])]})
|
||||
out({"disabled": I["username"]})
|
||||
|
||||
|
||||
try:
|
||||
run()
|
||||
except SystemExit:
|
||||
raise
|
||||
except LDAPException as e:
|
||||
fail("LDAP error: " + str(e))
|
||||
except Exception as e:
|
||||
fail(str(e))
|
||||
finally:
|
||||
try:
|
||||
if CONN is not None:
|
||||
CONN.unbind()
|
||||
except Exception:
|
||||
pass
|
||||
@@ -0,0 +1,232 @@
|
||||
import json, os, sys
|
||||
|
||||
S = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||
I = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||
|
||||
|
||||
def out(value):
|
||||
print(json.dumps(value, default=str))
|
||||
|
||||
|
||||
def fail(message, **extra):
|
||||
payload = {"error": message}
|
||||
payload.update(extra)
|
||||
print(json.dumps(payload, default=str))
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
try:
|
||||
import ssl
|
||||
from ldap3 import (
|
||||
Server, Connection, Tls, NTLM, SUBTREE, BASE, ALL_ATTRIBUTES,
|
||||
MODIFY_REPLACE, AUTO_BIND_NO_TLS, AUTO_BIND_TLS_BEFORE_BIND,
|
||||
)
|
||||
from ldap3.core.exceptions import LDAPException
|
||||
from ldap3.extend import microsoft
|
||||
from ldap3.utils.conv import escape_filter_chars
|
||||
except ImportError as _e:
|
||||
print(json.dumps({
|
||||
"error": "The 'ldap3' Python library is required for the Active Directory integration. "
|
||||
"Install it on the execution host (engine): pip install ldap3",
|
||||
"detail": str(_e),
|
||||
}))
|
||||
sys.exit(1)
|
||||
|
||||
SSL_VERSIONS = {
|
||||
"None": None,
|
||||
"TLS": ssl.PROTOCOL_TLS,
|
||||
"TLSv1": ssl.PROTOCOL_TLSv1,
|
||||
"TLSv1_1": ssl.PROTOCOL_TLSv1_1,
|
||||
"TLSv1_2": ssl.PROTOCOL_TLSv1_2,
|
||||
"TLS_CLIENT": ssl.PROTOCOL_TLS_CLIENT,
|
||||
}
|
||||
CIPHERS = (
|
||||
"@SECLEVEL=1:ECDHE+AESGCM:ECDHE+CHACHA20:DHE+AESGCM:DHE+CHACHA20:ECDH+AESGCM:"
|
||||
"DH+AESGCM:ECDH+AES:DH+AES:RSA+AESGCM:RSA+AES:!aNULL:!eNULL:!MD5:!DSS"
|
||||
)
|
||||
|
||||
CONN = None
|
||||
|
||||
|
||||
def _bool(v):
|
||||
if isinstance(v, bool):
|
||||
return v
|
||||
return str(v).lower() in ("1", "true", "yes")
|
||||
|
||||
|
||||
def _list(v):
|
||||
if isinstance(v, list):
|
||||
return v
|
||||
return [x.strip() for x in str(v).split(",") if x.strip()]
|
||||
|
||||
|
||||
def _cfg():
|
||||
return {
|
||||
"host": S.get("server_ip") or S.get("host") or "",
|
||||
"port": int(S["port"]) if S.get("port") else None,
|
||||
"user": S.get("username") or "",
|
||||
"password": S.get("password") or "",
|
||||
"base_dn": S.get("base_dn") or "",
|
||||
"secure": S.get("secure_connection") or "SSL",
|
||||
"ssl_version": S.get("ssl_version") or "None",
|
||||
"ntlm": _bool(S.get("ntlm")),
|
||||
"unsecure": _bool(S.get("unsecure")),
|
||||
"page_size": int(S["page_size"]) if S.get("page_size") else 500,
|
||||
}
|
||||
|
||||
|
||||
def _tls(unsecure, ssl_version):
|
||||
version = SSL_VERSIONS.get(ssl_version)
|
||||
if unsecure:
|
||||
return Tls(validate=ssl.CERT_NONE, ca_certs_file=None, ciphers=CIPHERS, version=version)
|
||||
return Tls(validate=ssl.CERT_REQUIRED, ca_certs_file=os.environ.get("SSL_CERT_FILE"), version=version)
|
||||
|
||||
|
||||
def _server(c):
|
||||
secure = c["secure"]
|
||||
if secure in ("SSL", "TLS"):
|
||||
return Server(host=c["host"], port=c["port"], use_ssl=True, tls=_tls(c["unsecure"], c["ssl_version"]), connect_timeout=120)
|
||||
if secure == "Start TLS":
|
||||
return Server(host=c["host"], port=c["port"], use_ssl=False, tls=_tls(c["unsecure"], c["ssl_version"]), connect_timeout=120)
|
||||
return Server(host=c["host"], port=c["port"], connect_timeout=120)
|
||||
|
||||
|
||||
def _auto_bind(secure):
|
||||
return AUTO_BIND_TLS_BEFORE_BIND if secure == "Start TLS" else AUTO_BIND_NO_TLS
|
||||
|
||||
|
||||
def connect():
|
||||
global CONN
|
||||
c = _cfg()
|
||||
server = _server(c)
|
||||
if c["ntlm"]:
|
||||
domain_user = c["user"] if "\\" in c["user"] else c["host"] + "\\" + c["user"]
|
||||
CONN = Connection(server, domain_user, password=c["password"], authentication=NTLM, auto_bind=_auto_bind(c["secure"]))
|
||||
else:
|
||||
CONN = Connection(server, user=c["user"], password=c["password"], auto_bind=_auto_bind(c["secure"]))
|
||||
return CONN, c
|
||||
|
||||
|
||||
def base_dn_arg():
|
||||
return I.get("base-dn") or I.get("base_dn") or _cfg()["base_dn"]
|
||||
|
||||
|
||||
def search_paging(conn, search_filter, search_base, attributes=None, page_size=100, size_limit=0, time_limit=0):
|
||||
entries = []
|
||||
cookie = None
|
||||
total = 0
|
||||
left = size_limit
|
||||
while True:
|
||||
ps = page_size
|
||||
if 0 < left < page_size:
|
||||
ps = left
|
||||
conn.search(search_base, search_filter, search_scope=SUBTREE, attributes=attributes,
|
||||
paged_size=ps, paged_cookie=cookie, time_limit=time_limit)
|
||||
left -= len(conn.entries)
|
||||
total += len(conn.entries)
|
||||
try:
|
||||
cookie = conn.result["controls"]["1.2.840.113556.1.4.319"]["value"]["cookie"]
|
||||
except (KeyError, TypeError):
|
||||
cookie = None
|
||||
entries.extend(conn.entries)
|
||||
if (size_limit and size_limit <= total) or not cookie:
|
||||
break
|
||||
flat = []
|
||||
for entry in entries:
|
||||
obj = json.loads(entry.entry_to_json())
|
||||
flat_entry = {"dn": obj["dn"]}
|
||||
for attr in obj.get("attributes", {}):
|
||||
flat_entry[attr] = obj["attributes"][attr]
|
||||
flat.append(flat_entry)
|
||||
return flat
|
||||
|
||||
|
||||
def user_dn(conn, sam, search_base):
|
||||
if "\\" in sam:
|
||||
sam = sam.split("\\", 1)[1]
|
||||
flat = search_paging(conn, f"(&(objectClass=user)(sAMAccountName={escape_filter_chars(sam)}))",
|
||||
search_base, page_size=1, size_limit=1)
|
||||
if not flat:
|
||||
raise Exception(f"Could not find user '{sam}'")
|
||||
return flat[0]["dn"]
|
||||
|
||||
|
||||
def computer_dn(conn, name, search_base):
|
||||
flat = search_paging(conn, f"(&(objectClass=user)(objectCategory=computer)(name={escape_filter_chars(name)}))",
|
||||
search_base, page_size=1, size_limit=1)
|
||||
if not flat:
|
||||
raise Exception(f"Could not find computer '{name}'")
|
||||
return flat[0]["dn"]
|
||||
|
||||
|
||||
def group_dn(conn, name, search_base):
|
||||
flat = search_paging(conn, f"(&(objectClass=group)(cn={escape_filter_chars(name)}))",
|
||||
search_base, page_size=1, size_limit=1)
|
||||
if not flat:
|
||||
raise Exception(f"Could not find group '{name}'")
|
||||
return flat[0]["dn"]
|
||||
|
||||
|
||||
def modify(conn, dn, modification):
|
||||
if not conn.modify(dn, modification):
|
||||
raise Exception(f"Failed to modify {dn}: {json.dumps(conn.result, default=str)}")
|
||||
|
||||
|
||||
def first_uac(values):
|
||||
if isinstance(values, list):
|
||||
return values[0] if values else 0
|
||||
return values or 0
|
||||
|
||||
|
||||
def uac_fields(value):
|
||||
value = int(value)
|
||||
flags = {
|
||||
"SCRIPT": 0x1, "ACCOUNTDISABLE": 0x2, "HOMEDIR_REQUIRED": 0x8, "LOCKOUT": 0x10,
|
||||
"PASSWD_NOTREQD": 0x20, "PASSWD_CANT_CHANGE": 0x40, "ENCRYPTED_TEXT_PWD_ALLOWED": 0x80,
|
||||
"TEMP_DUPLICATE_ACCOUNT": 0x100, "NORMAL_ACCOUNT": 0x200, "INTERDOMAIN_TRUST_ACCOUNT": 0x800,
|
||||
"WORKSTATION_TRUST_ACCOUNT": 0x1000, "SERVER_TRUST_ACCOUNT": 0x2000, "DONT_EXPIRE_PASSWORD": 0x10000,
|
||||
"MNS_LOGON_ACCOUNT": 0x20000, "SMARTCARD_REQUIRED": 0x40000, "TRUSTED_FOR_DELEGATION": 0x80000,
|
||||
"NOT_DELEGATED": 0x100000, "USE_DES_KEY_ONLY": 0x200000, "DONT_REQ_PREAUTH": 0x400000,
|
||||
"PASSWORD_EXPIRED": 0x800000, "TRUSTED_TO_AUTH_FOR_DELEGATION": 0x1000000, "PARTIAL_SECRETS_ACCOUNT": 0x4000000,
|
||||
}
|
||||
return {name: bool(value & mask) for name, mask in flags.items()}
|
||||
|
||||
|
||||
def current_uac(conn, search_base):
|
||||
sam = I.get("username") or I.get("sAMAccountName")
|
||||
query = "(&(objectClass=User)(objectCategory=person))"
|
||||
if sam:
|
||||
query = f"(&(objectClass=User)(objectCategory=person)(sAMAccountName={escape_filter_chars(sam)}))"
|
||||
flat = search_paging(conn, query, search_base, attributes=["userAccountControl"], size_limit=1, page_size=1)
|
||||
if flat and flat[0].get("userAccountControl"):
|
||||
return first_uac(flat[0]["userAccountControl"])
|
||||
return 512
|
||||
|
||||
|
||||
|
||||
def run():
|
||||
conn, c = connect()
|
||||
sb = I.get("base-dn") or base_dn_arg()
|
||||
dn = user_dn(conn, I["username"], sb)
|
||||
options = 512
|
||||
if str(I.get("restore_user")).lower() == "true":
|
||||
options = current_uac(conn, sb)
|
||||
options = options & ~0x2
|
||||
modify(conn, dn, {"userAccountControl": [(MODIFY_REPLACE, [options])]})
|
||||
out({"enabled": I["username"]})
|
||||
|
||||
|
||||
try:
|
||||
run()
|
||||
except SystemExit:
|
||||
raise
|
||||
except LDAPException as e:
|
||||
fail("LDAP error: " + str(e))
|
||||
except Exception as e:
|
||||
fail(str(e))
|
||||
finally:
|
||||
try:
|
||||
if CONN is not None:
|
||||
CONN.unbind()
|
||||
except Exception:
|
||||
pass
|
||||
@@ -0,0 +1,227 @@
|
||||
import json, os, sys
|
||||
|
||||
S = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||
I = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||
|
||||
|
||||
def out(value):
|
||||
print(json.dumps(value, default=str))
|
||||
|
||||
|
||||
def fail(message, **extra):
|
||||
payload = {"error": message}
|
||||
payload.update(extra)
|
||||
print(json.dumps(payload, default=str))
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
try:
|
||||
import ssl
|
||||
from ldap3 import (
|
||||
Server, Connection, Tls, NTLM, SUBTREE, BASE, ALL_ATTRIBUTES,
|
||||
MODIFY_REPLACE, AUTO_BIND_NO_TLS, AUTO_BIND_TLS_BEFORE_BIND,
|
||||
)
|
||||
from ldap3.core.exceptions import LDAPException
|
||||
from ldap3.extend import microsoft
|
||||
from ldap3.utils.conv import escape_filter_chars
|
||||
except ImportError as _e:
|
||||
print(json.dumps({
|
||||
"error": "The 'ldap3' Python library is required for the Active Directory integration. "
|
||||
"Install it on the execution host (engine): pip install ldap3",
|
||||
"detail": str(_e),
|
||||
}))
|
||||
sys.exit(1)
|
||||
|
||||
SSL_VERSIONS = {
|
||||
"None": None,
|
||||
"TLS": ssl.PROTOCOL_TLS,
|
||||
"TLSv1": ssl.PROTOCOL_TLSv1,
|
||||
"TLSv1_1": ssl.PROTOCOL_TLSv1_1,
|
||||
"TLSv1_2": ssl.PROTOCOL_TLSv1_2,
|
||||
"TLS_CLIENT": ssl.PROTOCOL_TLS_CLIENT,
|
||||
}
|
||||
CIPHERS = (
|
||||
"@SECLEVEL=1:ECDHE+AESGCM:ECDHE+CHACHA20:DHE+AESGCM:DHE+CHACHA20:ECDH+AESGCM:"
|
||||
"DH+AESGCM:ECDH+AES:DH+AES:RSA+AESGCM:RSA+AES:!aNULL:!eNULL:!MD5:!DSS"
|
||||
)
|
||||
|
||||
CONN = None
|
||||
|
||||
|
||||
def _bool(v):
|
||||
if isinstance(v, bool):
|
||||
return v
|
||||
return str(v).lower() in ("1", "true", "yes")
|
||||
|
||||
|
||||
def _list(v):
|
||||
if isinstance(v, list):
|
||||
return v
|
||||
return [x.strip() for x in str(v).split(",") if x.strip()]
|
||||
|
||||
|
||||
def _cfg():
|
||||
return {
|
||||
"host": S.get("server_ip") or S.get("host") or "",
|
||||
"port": int(S["port"]) if S.get("port") else None,
|
||||
"user": S.get("username") or "",
|
||||
"password": S.get("password") or "",
|
||||
"base_dn": S.get("base_dn") or "",
|
||||
"secure": S.get("secure_connection") or "SSL",
|
||||
"ssl_version": S.get("ssl_version") or "None",
|
||||
"ntlm": _bool(S.get("ntlm")),
|
||||
"unsecure": _bool(S.get("unsecure")),
|
||||
"page_size": int(S["page_size"]) if S.get("page_size") else 500,
|
||||
}
|
||||
|
||||
|
||||
def _tls(unsecure, ssl_version):
|
||||
version = SSL_VERSIONS.get(ssl_version)
|
||||
if unsecure:
|
||||
return Tls(validate=ssl.CERT_NONE, ca_certs_file=None, ciphers=CIPHERS, version=version)
|
||||
return Tls(validate=ssl.CERT_REQUIRED, ca_certs_file=os.environ.get("SSL_CERT_FILE"), version=version)
|
||||
|
||||
|
||||
def _server(c):
|
||||
secure = c["secure"]
|
||||
if secure in ("SSL", "TLS"):
|
||||
return Server(host=c["host"], port=c["port"], use_ssl=True, tls=_tls(c["unsecure"], c["ssl_version"]), connect_timeout=120)
|
||||
if secure == "Start TLS":
|
||||
return Server(host=c["host"], port=c["port"], use_ssl=False, tls=_tls(c["unsecure"], c["ssl_version"]), connect_timeout=120)
|
||||
return Server(host=c["host"], port=c["port"], connect_timeout=120)
|
||||
|
||||
|
||||
def _auto_bind(secure):
|
||||
return AUTO_BIND_TLS_BEFORE_BIND if secure == "Start TLS" else AUTO_BIND_NO_TLS
|
||||
|
||||
|
||||
def connect():
|
||||
global CONN
|
||||
c = _cfg()
|
||||
server = _server(c)
|
||||
if c["ntlm"]:
|
||||
domain_user = c["user"] if "\\" in c["user"] else c["host"] + "\\" + c["user"]
|
||||
CONN = Connection(server, domain_user, password=c["password"], authentication=NTLM, auto_bind=_auto_bind(c["secure"]))
|
||||
else:
|
||||
CONN = Connection(server, user=c["user"], password=c["password"], auto_bind=_auto_bind(c["secure"]))
|
||||
return CONN, c
|
||||
|
||||
|
||||
def base_dn_arg():
|
||||
return I.get("base-dn") or I.get("base_dn") or _cfg()["base_dn"]
|
||||
|
||||
|
||||
def search_paging(conn, search_filter, search_base, attributes=None, page_size=100, size_limit=0, time_limit=0):
|
||||
entries = []
|
||||
cookie = None
|
||||
total = 0
|
||||
left = size_limit
|
||||
while True:
|
||||
ps = page_size
|
||||
if 0 < left < page_size:
|
||||
ps = left
|
||||
conn.search(search_base, search_filter, search_scope=SUBTREE, attributes=attributes,
|
||||
paged_size=ps, paged_cookie=cookie, time_limit=time_limit)
|
||||
left -= len(conn.entries)
|
||||
total += len(conn.entries)
|
||||
try:
|
||||
cookie = conn.result["controls"]["1.2.840.113556.1.4.319"]["value"]["cookie"]
|
||||
except (KeyError, TypeError):
|
||||
cookie = None
|
||||
entries.extend(conn.entries)
|
||||
if (size_limit and size_limit <= total) or not cookie:
|
||||
break
|
||||
flat = []
|
||||
for entry in entries:
|
||||
obj = json.loads(entry.entry_to_json())
|
||||
flat_entry = {"dn": obj["dn"]}
|
||||
for attr in obj.get("attributes", {}):
|
||||
flat_entry[attr] = obj["attributes"][attr]
|
||||
flat.append(flat_entry)
|
||||
return flat
|
||||
|
||||
|
||||
def user_dn(conn, sam, search_base):
|
||||
if "\\" in sam:
|
||||
sam = sam.split("\\", 1)[1]
|
||||
flat = search_paging(conn, f"(&(objectClass=user)(sAMAccountName={escape_filter_chars(sam)}))",
|
||||
search_base, page_size=1, size_limit=1)
|
||||
if not flat:
|
||||
raise Exception(f"Could not find user '{sam}'")
|
||||
return flat[0]["dn"]
|
||||
|
||||
|
||||
def computer_dn(conn, name, search_base):
|
||||
flat = search_paging(conn, f"(&(objectClass=user)(objectCategory=computer)(name={escape_filter_chars(name)}))",
|
||||
search_base, page_size=1, size_limit=1)
|
||||
if not flat:
|
||||
raise Exception(f"Could not find computer '{name}'")
|
||||
return flat[0]["dn"]
|
||||
|
||||
|
||||
def group_dn(conn, name, search_base):
|
||||
flat = search_paging(conn, f"(&(objectClass=group)(cn={escape_filter_chars(name)}))",
|
||||
search_base, page_size=1, size_limit=1)
|
||||
if not flat:
|
||||
raise Exception(f"Could not find group '{name}'")
|
||||
return flat[0]["dn"]
|
||||
|
||||
|
||||
def modify(conn, dn, modification):
|
||||
if not conn.modify(dn, modification):
|
||||
raise Exception(f"Failed to modify {dn}: {json.dumps(conn.result, default=str)}")
|
||||
|
||||
|
||||
def first_uac(values):
|
||||
if isinstance(values, list):
|
||||
return values[0] if values else 0
|
||||
return values or 0
|
||||
|
||||
|
||||
def uac_fields(value):
|
||||
value = int(value)
|
||||
flags = {
|
||||
"SCRIPT": 0x1, "ACCOUNTDISABLE": 0x2, "HOMEDIR_REQUIRED": 0x8, "LOCKOUT": 0x10,
|
||||
"PASSWD_NOTREQD": 0x20, "PASSWD_CANT_CHANGE": 0x40, "ENCRYPTED_TEXT_PWD_ALLOWED": 0x80,
|
||||
"TEMP_DUPLICATE_ACCOUNT": 0x100, "NORMAL_ACCOUNT": 0x200, "INTERDOMAIN_TRUST_ACCOUNT": 0x800,
|
||||
"WORKSTATION_TRUST_ACCOUNT": 0x1000, "SERVER_TRUST_ACCOUNT": 0x2000, "DONT_EXPIRE_PASSWORD": 0x10000,
|
||||
"MNS_LOGON_ACCOUNT": 0x20000, "SMARTCARD_REQUIRED": 0x40000, "TRUSTED_FOR_DELEGATION": 0x80000,
|
||||
"NOT_DELEGATED": 0x100000, "USE_DES_KEY_ONLY": 0x200000, "DONT_REQ_PREAUTH": 0x400000,
|
||||
"PASSWORD_EXPIRED": 0x800000, "TRUSTED_TO_AUTH_FOR_DELEGATION": 0x1000000, "PARTIAL_SECRETS_ACCOUNT": 0x4000000,
|
||||
}
|
||||
return {name: bool(value & mask) for name, mask in flags.items()}
|
||||
|
||||
|
||||
def current_uac(conn, search_base):
|
||||
sam = I.get("username") or I.get("sAMAccountName")
|
||||
query = "(&(objectClass=User)(objectCategory=person))"
|
||||
if sam:
|
||||
query = f"(&(objectClass=User)(objectCategory=person)(sAMAccountName={escape_filter_chars(sam)}))"
|
||||
flat = search_paging(conn, query, search_base, attributes=["userAccountControl"], size_limit=1, page_size=1)
|
||||
if flat and flat[0].get("userAccountControl"):
|
||||
return first_uac(flat[0]["userAccountControl"])
|
||||
return 512
|
||||
|
||||
|
||||
|
||||
def run():
|
||||
conn, c = connect()
|
||||
dn = user_dn(conn, I["username"], I.get("base-dn") or base_dn_arg())
|
||||
modify(conn, dn, {"pwdLastSet": [(MODIFY_REPLACE, ["0"])]})
|
||||
out({"expired": I["username"]})
|
||||
|
||||
|
||||
try:
|
||||
run()
|
||||
except SystemExit:
|
||||
raise
|
||||
except LDAPException as e:
|
||||
fail("LDAP error: " + str(e))
|
||||
except Exception as e:
|
||||
fail(str(e))
|
||||
finally:
|
||||
try:
|
||||
if CONN is not None:
|
||||
CONN.unbind()
|
||||
except Exception:
|
||||
pass
|
||||
@@ -0,0 +1,243 @@
|
||||
import json, os, sys
|
||||
|
||||
S = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||
I = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||
|
||||
|
||||
def out(value):
|
||||
print(json.dumps(value, default=str))
|
||||
|
||||
|
||||
def fail(message, **extra):
|
||||
payload = {"error": message}
|
||||
payload.update(extra)
|
||||
print(json.dumps(payload, default=str))
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
try:
|
||||
import ssl
|
||||
from ldap3 import (
|
||||
Server, Connection, Tls, NTLM, SUBTREE, BASE, ALL_ATTRIBUTES,
|
||||
MODIFY_REPLACE, AUTO_BIND_NO_TLS, AUTO_BIND_TLS_BEFORE_BIND,
|
||||
)
|
||||
from ldap3.core.exceptions import LDAPException
|
||||
from ldap3.extend import microsoft
|
||||
from ldap3.utils.conv import escape_filter_chars
|
||||
except ImportError as _e:
|
||||
print(json.dumps({
|
||||
"error": "The 'ldap3' Python library is required for the Active Directory integration. "
|
||||
"Install it on the execution host (engine): pip install ldap3",
|
||||
"detail": str(_e),
|
||||
}))
|
||||
sys.exit(1)
|
||||
|
||||
SSL_VERSIONS = {
|
||||
"None": None,
|
||||
"TLS": ssl.PROTOCOL_TLS,
|
||||
"TLSv1": ssl.PROTOCOL_TLSv1,
|
||||
"TLSv1_1": ssl.PROTOCOL_TLSv1_1,
|
||||
"TLSv1_2": ssl.PROTOCOL_TLSv1_2,
|
||||
"TLS_CLIENT": ssl.PROTOCOL_TLS_CLIENT,
|
||||
}
|
||||
CIPHERS = (
|
||||
"@SECLEVEL=1:ECDHE+AESGCM:ECDHE+CHACHA20:DHE+AESGCM:DHE+CHACHA20:ECDH+AESGCM:"
|
||||
"DH+AESGCM:ECDH+AES:DH+AES:RSA+AESGCM:RSA+AES:!aNULL:!eNULL:!MD5:!DSS"
|
||||
)
|
||||
|
||||
CONN = None
|
||||
|
||||
|
||||
def _bool(v):
|
||||
if isinstance(v, bool):
|
||||
return v
|
||||
return str(v).lower() in ("1", "true", "yes")
|
||||
|
||||
|
||||
def _list(v):
|
||||
if isinstance(v, list):
|
||||
return v
|
||||
return [x.strip() for x in str(v).split(",") if x.strip()]
|
||||
|
||||
|
||||
def _cfg():
|
||||
return {
|
||||
"host": S.get("server_ip") or S.get("host") or "",
|
||||
"port": int(S["port"]) if S.get("port") else None,
|
||||
"user": S.get("username") or "",
|
||||
"password": S.get("password") or "",
|
||||
"base_dn": S.get("base_dn") or "",
|
||||
"secure": S.get("secure_connection") or "SSL",
|
||||
"ssl_version": S.get("ssl_version") or "None",
|
||||
"ntlm": _bool(S.get("ntlm")),
|
||||
"unsecure": _bool(S.get("unsecure")),
|
||||
"page_size": int(S["page_size"]) if S.get("page_size") else 500,
|
||||
}
|
||||
|
||||
|
||||
def _tls(unsecure, ssl_version):
|
||||
version = SSL_VERSIONS.get(ssl_version)
|
||||
if unsecure:
|
||||
return Tls(validate=ssl.CERT_NONE, ca_certs_file=None, ciphers=CIPHERS, version=version)
|
||||
return Tls(validate=ssl.CERT_REQUIRED, ca_certs_file=os.environ.get("SSL_CERT_FILE"), version=version)
|
||||
|
||||
|
||||
def _server(c):
|
||||
secure = c["secure"]
|
||||
if secure in ("SSL", "TLS"):
|
||||
return Server(host=c["host"], port=c["port"], use_ssl=True, tls=_tls(c["unsecure"], c["ssl_version"]), connect_timeout=120)
|
||||
if secure == "Start TLS":
|
||||
return Server(host=c["host"], port=c["port"], use_ssl=False, tls=_tls(c["unsecure"], c["ssl_version"]), connect_timeout=120)
|
||||
return Server(host=c["host"], port=c["port"], connect_timeout=120)
|
||||
|
||||
|
||||
def _auto_bind(secure):
|
||||
return AUTO_BIND_TLS_BEFORE_BIND if secure == "Start TLS" else AUTO_BIND_NO_TLS
|
||||
|
||||
|
||||
def connect():
|
||||
global CONN
|
||||
c = _cfg()
|
||||
server = _server(c)
|
||||
if c["ntlm"]:
|
||||
domain_user = c["user"] if "\\" in c["user"] else c["host"] + "\\" + c["user"]
|
||||
CONN = Connection(server, domain_user, password=c["password"], authentication=NTLM, auto_bind=_auto_bind(c["secure"]))
|
||||
else:
|
||||
CONN = Connection(server, user=c["user"], password=c["password"], auto_bind=_auto_bind(c["secure"]))
|
||||
return CONN, c
|
||||
|
||||
|
||||
def base_dn_arg():
|
||||
return I.get("base-dn") or I.get("base_dn") or _cfg()["base_dn"]
|
||||
|
||||
|
||||
def search_paging(conn, search_filter, search_base, attributes=None, page_size=100, size_limit=0, time_limit=0):
|
||||
entries = []
|
||||
cookie = None
|
||||
total = 0
|
||||
left = size_limit
|
||||
while True:
|
||||
ps = page_size
|
||||
if 0 < left < page_size:
|
||||
ps = left
|
||||
conn.search(search_base, search_filter, search_scope=SUBTREE, attributes=attributes,
|
||||
paged_size=ps, paged_cookie=cookie, time_limit=time_limit)
|
||||
left -= len(conn.entries)
|
||||
total += len(conn.entries)
|
||||
try:
|
||||
cookie = conn.result["controls"]["1.2.840.113556.1.4.319"]["value"]["cookie"]
|
||||
except (KeyError, TypeError):
|
||||
cookie = None
|
||||
entries.extend(conn.entries)
|
||||
if (size_limit and size_limit <= total) or not cookie:
|
||||
break
|
||||
flat = []
|
||||
for entry in entries:
|
||||
obj = json.loads(entry.entry_to_json())
|
||||
flat_entry = {"dn": obj["dn"]}
|
||||
for attr in obj.get("attributes", {}):
|
||||
flat_entry[attr] = obj["attributes"][attr]
|
||||
flat.append(flat_entry)
|
||||
return flat
|
||||
|
||||
|
||||
def user_dn(conn, sam, search_base):
|
||||
if "\\" in sam:
|
||||
sam = sam.split("\\", 1)[1]
|
||||
flat = search_paging(conn, f"(&(objectClass=user)(sAMAccountName={escape_filter_chars(sam)}))",
|
||||
search_base, page_size=1, size_limit=1)
|
||||
if not flat:
|
||||
raise Exception(f"Could not find user '{sam}'")
|
||||
return flat[0]["dn"]
|
||||
|
||||
|
||||
def computer_dn(conn, name, search_base):
|
||||
flat = search_paging(conn, f"(&(objectClass=user)(objectCategory=computer)(name={escape_filter_chars(name)}))",
|
||||
search_base, page_size=1, size_limit=1)
|
||||
if not flat:
|
||||
raise Exception(f"Could not find computer '{name}'")
|
||||
return flat[0]["dn"]
|
||||
|
||||
|
||||
def group_dn(conn, name, search_base):
|
||||
flat = search_paging(conn, f"(&(objectClass=group)(cn={escape_filter_chars(name)}))",
|
||||
search_base, page_size=1, size_limit=1)
|
||||
if not flat:
|
||||
raise Exception(f"Could not find group '{name}'")
|
||||
return flat[0]["dn"]
|
||||
|
||||
|
||||
def modify(conn, dn, modification):
|
||||
if not conn.modify(dn, modification):
|
||||
raise Exception(f"Failed to modify {dn}: {json.dumps(conn.result, default=str)}")
|
||||
|
||||
|
||||
def first_uac(values):
|
||||
if isinstance(values, list):
|
||||
return values[0] if values else 0
|
||||
return values or 0
|
||||
|
||||
|
||||
def uac_fields(value):
|
||||
value = int(value)
|
||||
flags = {
|
||||
"SCRIPT": 0x1, "ACCOUNTDISABLE": 0x2, "HOMEDIR_REQUIRED": 0x8, "LOCKOUT": 0x10,
|
||||
"PASSWD_NOTREQD": 0x20, "PASSWD_CANT_CHANGE": 0x40, "ENCRYPTED_TEXT_PWD_ALLOWED": 0x80,
|
||||
"TEMP_DUPLICATE_ACCOUNT": 0x100, "NORMAL_ACCOUNT": 0x200, "INTERDOMAIN_TRUST_ACCOUNT": 0x800,
|
||||
"WORKSTATION_TRUST_ACCOUNT": 0x1000, "SERVER_TRUST_ACCOUNT": 0x2000, "DONT_EXPIRE_PASSWORD": 0x10000,
|
||||
"MNS_LOGON_ACCOUNT": 0x20000, "SMARTCARD_REQUIRED": 0x40000, "TRUSTED_FOR_DELEGATION": 0x80000,
|
||||
"NOT_DELEGATED": 0x100000, "USE_DES_KEY_ONLY": 0x200000, "DONT_REQ_PREAUTH": 0x400000,
|
||||
"PASSWORD_EXPIRED": 0x800000, "TRUSTED_TO_AUTH_FOR_DELEGATION": 0x1000000, "PARTIAL_SECRETS_ACCOUNT": 0x4000000,
|
||||
}
|
||||
return {name: bool(value & mask) for name, mask in flags.items()}
|
||||
|
||||
|
||||
def current_uac(conn, search_base):
|
||||
sam = I.get("username") or I.get("sAMAccountName")
|
||||
query = "(&(objectClass=User)(objectCategory=person))"
|
||||
if sam:
|
||||
query = f"(&(objectClass=User)(objectCategory=person)(sAMAccountName={escape_filter_chars(sam)}))"
|
||||
flat = search_paging(conn, query, search_base, attributes=["userAccountControl"], size_limit=1, page_size=1)
|
||||
if flat and flat[0].get("userAccountControl"):
|
||||
return first_uac(flat[0]["userAccountControl"])
|
||||
return 512
|
||||
|
||||
|
||||
|
||||
def run():
|
||||
conn, c = connect()
|
||||
defaults = ["name", "memberOf"]
|
||||
query = "(&(objectClass=user)(objectCategory=computer))"
|
||||
esc = escape_filter_chars
|
||||
if I.get("dn"):
|
||||
query = f"(&(objectClass=user)(objectCategory=computer)(distinguishedName={esc(I['dn'])}))"
|
||||
elif I.get("name"):
|
||||
query = f"(&(objectClass=user)(objectCategory=computer)(name={esc(I['name'])}))"
|
||||
elif I.get("custom-field-type"):
|
||||
if not I.get("custom-field-data"):
|
||||
fail("custom-field-data is required when custom-field-type is given")
|
||||
query = f"(&(objectClass=user)(objectCategory=computer)({esc(I['custom-field-type'])}={esc(I['custom-field-data'])}))"
|
||||
custom = I["attributes"].split(",") if I.get("attributes") else []
|
||||
attributes = list(set(custom) | set(defaults))
|
||||
size_limit = int(I.get("limit") or 0)
|
||||
page_size = int(I.get("page-size") or c["page_size"])
|
||||
if I.get("page-size"):
|
||||
size_limit = page_size
|
||||
flat = search_paging(conn, query, base_dn_arg(), attributes=attributes, page_size=page_size, size_limit=size_limit)
|
||||
out({"computers": flat, "count": len(flat)})
|
||||
|
||||
|
||||
try:
|
||||
run()
|
||||
except SystemExit:
|
||||
raise
|
||||
except LDAPException as e:
|
||||
fail("LDAP error: " + str(e))
|
||||
except Exception as e:
|
||||
fail(str(e))
|
||||
finally:
|
||||
try:
|
||||
if CONN is not None:
|
||||
CONN.unbind()
|
||||
except Exception:
|
||||
pass
|
||||
@@ -0,0 +1,248 @@
|
||||
import json, os, sys
|
||||
|
||||
S = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||
I = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||
|
||||
|
||||
def out(value):
|
||||
print(json.dumps(value, default=str))
|
||||
|
||||
|
||||
def fail(message, **extra):
|
||||
payload = {"error": message}
|
||||
payload.update(extra)
|
||||
print(json.dumps(payload, default=str))
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
try:
|
||||
import ssl
|
||||
from ldap3 import (
|
||||
Server, Connection, Tls, NTLM, SUBTREE, BASE, ALL_ATTRIBUTES,
|
||||
MODIFY_REPLACE, AUTO_BIND_NO_TLS, AUTO_BIND_TLS_BEFORE_BIND,
|
||||
)
|
||||
from ldap3.core.exceptions import LDAPException
|
||||
from ldap3.extend import microsoft
|
||||
from ldap3.utils.conv import escape_filter_chars
|
||||
except ImportError as _e:
|
||||
print(json.dumps({
|
||||
"error": "The 'ldap3' Python library is required for the Active Directory integration. "
|
||||
"Install it on the execution host (engine): pip install ldap3",
|
||||
"detail": str(_e),
|
||||
}))
|
||||
sys.exit(1)
|
||||
|
||||
SSL_VERSIONS = {
|
||||
"None": None,
|
||||
"TLS": ssl.PROTOCOL_TLS,
|
||||
"TLSv1": ssl.PROTOCOL_TLSv1,
|
||||
"TLSv1_1": ssl.PROTOCOL_TLSv1_1,
|
||||
"TLSv1_2": ssl.PROTOCOL_TLSv1_2,
|
||||
"TLS_CLIENT": ssl.PROTOCOL_TLS_CLIENT,
|
||||
}
|
||||
CIPHERS = (
|
||||
"@SECLEVEL=1:ECDHE+AESGCM:ECDHE+CHACHA20:DHE+AESGCM:DHE+CHACHA20:ECDH+AESGCM:"
|
||||
"DH+AESGCM:ECDH+AES:DH+AES:RSA+AESGCM:RSA+AES:!aNULL:!eNULL:!MD5:!DSS"
|
||||
)
|
||||
|
||||
CONN = None
|
||||
|
||||
|
||||
def _bool(v):
|
||||
if isinstance(v, bool):
|
||||
return v
|
||||
return str(v).lower() in ("1", "true", "yes")
|
||||
|
||||
|
||||
def _list(v):
|
||||
if isinstance(v, list):
|
||||
return v
|
||||
return [x.strip() for x in str(v).split(",") if x.strip()]
|
||||
|
||||
|
||||
def _cfg():
|
||||
return {
|
||||
"host": S.get("server_ip") or S.get("host") or "",
|
||||
"port": int(S["port"]) if S.get("port") else None,
|
||||
"user": S.get("username") or "",
|
||||
"password": S.get("password") or "",
|
||||
"base_dn": S.get("base_dn") or "",
|
||||
"secure": S.get("secure_connection") or "SSL",
|
||||
"ssl_version": S.get("ssl_version") or "None",
|
||||
"ntlm": _bool(S.get("ntlm")),
|
||||
"unsecure": _bool(S.get("unsecure")),
|
||||
"page_size": int(S["page_size"]) if S.get("page_size") else 500,
|
||||
}
|
||||
|
||||
|
||||
def _tls(unsecure, ssl_version):
|
||||
version = SSL_VERSIONS.get(ssl_version)
|
||||
if unsecure:
|
||||
return Tls(validate=ssl.CERT_NONE, ca_certs_file=None, ciphers=CIPHERS, version=version)
|
||||
return Tls(validate=ssl.CERT_REQUIRED, ca_certs_file=os.environ.get("SSL_CERT_FILE"), version=version)
|
||||
|
||||
|
||||
def _server(c):
|
||||
secure = c["secure"]
|
||||
if secure in ("SSL", "TLS"):
|
||||
return Server(host=c["host"], port=c["port"], use_ssl=True, tls=_tls(c["unsecure"], c["ssl_version"]), connect_timeout=120)
|
||||
if secure == "Start TLS":
|
||||
return Server(host=c["host"], port=c["port"], use_ssl=False, tls=_tls(c["unsecure"], c["ssl_version"]), connect_timeout=120)
|
||||
return Server(host=c["host"], port=c["port"], connect_timeout=120)
|
||||
|
||||
|
||||
def _auto_bind(secure):
|
||||
return AUTO_BIND_TLS_BEFORE_BIND if secure == "Start TLS" else AUTO_BIND_NO_TLS
|
||||
|
||||
|
||||
def connect():
|
||||
global CONN
|
||||
c = _cfg()
|
||||
server = _server(c)
|
||||
if c["ntlm"]:
|
||||
domain_user = c["user"] if "\\" in c["user"] else c["host"] + "\\" + c["user"]
|
||||
CONN = Connection(server, domain_user, password=c["password"], authentication=NTLM, auto_bind=_auto_bind(c["secure"]))
|
||||
else:
|
||||
CONN = Connection(server, user=c["user"], password=c["password"], auto_bind=_auto_bind(c["secure"]))
|
||||
return CONN, c
|
||||
|
||||
|
||||
def base_dn_arg():
|
||||
return I.get("base-dn") or I.get("base_dn") or _cfg()["base_dn"]
|
||||
|
||||
|
||||
def search_paging(conn, search_filter, search_base, attributes=None, page_size=100, size_limit=0, time_limit=0):
|
||||
entries = []
|
||||
cookie = None
|
||||
total = 0
|
||||
left = size_limit
|
||||
while True:
|
||||
ps = page_size
|
||||
if 0 < left < page_size:
|
||||
ps = left
|
||||
conn.search(search_base, search_filter, search_scope=SUBTREE, attributes=attributes,
|
||||
paged_size=ps, paged_cookie=cookie, time_limit=time_limit)
|
||||
left -= len(conn.entries)
|
||||
total += len(conn.entries)
|
||||
try:
|
||||
cookie = conn.result["controls"]["1.2.840.113556.1.4.319"]["value"]["cookie"]
|
||||
except (KeyError, TypeError):
|
||||
cookie = None
|
||||
entries.extend(conn.entries)
|
||||
if (size_limit and size_limit <= total) or not cookie:
|
||||
break
|
||||
flat = []
|
||||
for entry in entries:
|
||||
obj = json.loads(entry.entry_to_json())
|
||||
flat_entry = {"dn": obj["dn"]}
|
||||
for attr in obj.get("attributes", {}):
|
||||
flat_entry[attr] = obj["attributes"][attr]
|
||||
flat.append(flat_entry)
|
||||
return flat
|
||||
|
||||
|
||||
def user_dn(conn, sam, search_base):
|
||||
if "\\" in sam:
|
||||
sam = sam.split("\\", 1)[1]
|
||||
flat = search_paging(conn, f"(&(objectClass=user)(sAMAccountName={escape_filter_chars(sam)}))",
|
||||
search_base, page_size=1, size_limit=1)
|
||||
if not flat:
|
||||
raise Exception(f"Could not find user '{sam}'")
|
||||
return flat[0]["dn"]
|
||||
|
||||
|
||||
def computer_dn(conn, name, search_base):
|
||||
flat = search_paging(conn, f"(&(objectClass=user)(objectCategory=computer)(name={escape_filter_chars(name)}))",
|
||||
search_base, page_size=1, size_limit=1)
|
||||
if not flat:
|
||||
raise Exception(f"Could not find computer '{name}'")
|
||||
return flat[0]["dn"]
|
||||
|
||||
|
||||
def group_dn(conn, name, search_base):
|
||||
flat = search_paging(conn, f"(&(objectClass=group)(cn={escape_filter_chars(name)}))",
|
||||
search_base, page_size=1, size_limit=1)
|
||||
if not flat:
|
||||
raise Exception(f"Could not find group '{name}'")
|
||||
return flat[0]["dn"]
|
||||
|
||||
|
||||
def modify(conn, dn, modification):
|
||||
if not conn.modify(dn, modification):
|
||||
raise Exception(f"Failed to modify {dn}: {json.dumps(conn.result, default=str)}")
|
||||
|
||||
|
||||
def first_uac(values):
|
||||
if isinstance(values, list):
|
||||
return values[0] if values else 0
|
||||
return values or 0
|
||||
|
||||
|
||||
def uac_fields(value):
|
||||
value = int(value)
|
||||
flags = {
|
||||
"SCRIPT": 0x1, "ACCOUNTDISABLE": 0x2, "HOMEDIR_REQUIRED": 0x8, "LOCKOUT": 0x10,
|
||||
"PASSWD_NOTREQD": 0x20, "PASSWD_CANT_CHANGE": 0x40, "ENCRYPTED_TEXT_PWD_ALLOWED": 0x80,
|
||||
"TEMP_DUPLICATE_ACCOUNT": 0x100, "NORMAL_ACCOUNT": 0x200, "INTERDOMAIN_TRUST_ACCOUNT": 0x800,
|
||||
"WORKSTATION_TRUST_ACCOUNT": 0x1000, "SERVER_TRUST_ACCOUNT": 0x2000, "DONT_EXPIRE_PASSWORD": 0x10000,
|
||||
"MNS_LOGON_ACCOUNT": 0x20000, "SMARTCARD_REQUIRED": 0x40000, "TRUSTED_FOR_DELEGATION": 0x80000,
|
||||
"NOT_DELEGATED": 0x100000, "USE_DES_KEY_ONLY": 0x200000, "DONT_REQ_PREAUTH": 0x400000,
|
||||
"PASSWORD_EXPIRED": 0x800000, "TRUSTED_TO_AUTH_FOR_DELEGATION": 0x1000000, "PARTIAL_SECRETS_ACCOUNT": 0x4000000,
|
||||
}
|
||||
return {name: bool(value & mask) for name, mask in flags.items()}
|
||||
|
||||
|
||||
def current_uac(conn, search_base):
|
||||
sam = I.get("username") or I.get("sAMAccountName")
|
||||
query = "(&(objectClass=User)(objectCategory=person))"
|
||||
if sam:
|
||||
query = f"(&(objectClass=User)(objectCategory=person)(sAMAccountName={escape_filter_chars(sam)}))"
|
||||
flat = search_paging(conn, query, search_base, attributes=["userAccountControl"], size_limit=1, page_size=1)
|
||||
if flat and flat[0].get("userAccountControl"):
|
||||
return first_uac(flat[0]["userAccountControl"])
|
||||
return 512
|
||||
|
||||
|
||||
|
||||
def run():
|
||||
conn, c = connect()
|
||||
member_type = I.get("member-type") or "person"
|
||||
group = I["group-dn"]
|
||||
nested = "" if str(I.get("disable-nested-search")).lower() == "true" else ":1.2.840.113556.1.4.1941:"
|
||||
account = I.get("sAMAccountName") or "*"
|
||||
time_limit = int(I.get("time_limit") or 180)
|
||||
defaults = {
|
||||
"person": ["name", "displayName", "memberOf", "mail", "sAMAccountName", "manager", "userAccountControl"],
|
||||
"group": ["name", "memberOf"],
|
||||
"computer": ["name", "memberOf"],
|
||||
}.get(member_type, ["name", "memberOf"])
|
||||
custom = I["attributes"].split(",") if I.get("attributes") else []
|
||||
attributes = list(set(custom) | set(defaults))
|
||||
if member_type == "group":
|
||||
query = f"(&(objectCategory={member_type})(memberOf{nested}={group})(sAMAccountName={account}))"
|
||||
else:
|
||||
query = f"(&(objectCategory={member_type})(objectClass=user)(memberOf{nested}={group})(sAMAccountName={account}))"
|
||||
size_limit = int(I.get("limit") or 0)
|
||||
page_size = int(I.get("page-size") or c["page_size"])
|
||||
if I.get("page-size"):
|
||||
size_limit = page_size
|
||||
flat = search_paging(conn, query, base_dn_arg(), attributes=attributes,
|
||||
page_size=page_size, size_limit=size_limit, time_limit=time_limit)
|
||||
members = [{"dn": e["dn"], "category": member_type} for e in flat]
|
||||
out({"group_dn": group, "members": members, "entries": flat, "count": len(flat)})
|
||||
|
||||
|
||||
try:
|
||||
run()
|
||||
except SystemExit:
|
||||
raise
|
||||
except LDAPException as e:
|
||||
fail("LDAP error: " + str(e))
|
||||
except Exception as e:
|
||||
fail(str(e))
|
||||
finally:
|
||||
try:
|
||||
if CONN is not None:
|
||||
CONN.unbind()
|
||||
except Exception:
|
||||
pass
|
||||
@@ -0,0 +1,254 @@
|
||||
import json, os, sys
|
||||
|
||||
S = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||
I = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||
|
||||
|
||||
def out(value):
|
||||
print(json.dumps(value, default=str))
|
||||
|
||||
|
||||
def fail(message, **extra):
|
||||
payload = {"error": message}
|
||||
payload.update(extra)
|
||||
print(json.dumps(payload, default=str))
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
try:
|
||||
import ssl
|
||||
from ldap3 import (
|
||||
Server, Connection, Tls, NTLM, SUBTREE, BASE, ALL_ATTRIBUTES,
|
||||
MODIFY_REPLACE, AUTO_BIND_NO_TLS, AUTO_BIND_TLS_BEFORE_BIND,
|
||||
)
|
||||
from ldap3.core.exceptions import LDAPException
|
||||
from ldap3.extend import microsoft
|
||||
from ldap3.utils.conv import escape_filter_chars
|
||||
except ImportError as _e:
|
||||
print(json.dumps({
|
||||
"error": "The 'ldap3' Python library is required for the Active Directory integration. "
|
||||
"Install it on the execution host (engine): pip install ldap3",
|
||||
"detail": str(_e),
|
||||
}))
|
||||
sys.exit(1)
|
||||
|
||||
SSL_VERSIONS = {
|
||||
"None": None,
|
||||
"TLS": ssl.PROTOCOL_TLS,
|
||||
"TLSv1": ssl.PROTOCOL_TLSv1,
|
||||
"TLSv1_1": ssl.PROTOCOL_TLSv1_1,
|
||||
"TLSv1_2": ssl.PROTOCOL_TLSv1_2,
|
||||
"TLS_CLIENT": ssl.PROTOCOL_TLS_CLIENT,
|
||||
}
|
||||
CIPHERS = (
|
||||
"@SECLEVEL=1:ECDHE+AESGCM:ECDHE+CHACHA20:DHE+AESGCM:DHE+CHACHA20:ECDH+AESGCM:"
|
||||
"DH+AESGCM:ECDH+AES:DH+AES:RSA+AESGCM:RSA+AES:!aNULL:!eNULL:!MD5:!DSS"
|
||||
)
|
||||
|
||||
CONN = None
|
||||
|
||||
|
||||
def _bool(v):
|
||||
if isinstance(v, bool):
|
||||
return v
|
||||
return str(v).lower() in ("1", "true", "yes")
|
||||
|
||||
|
||||
def _list(v):
|
||||
if isinstance(v, list):
|
||||
return v
|
||||
return [x.strip() for x in str(v).split(",") if x.strip()]
|
||||
|
||||
|
||||
def _cfg():
|
||||
return {
|
||||
"host": S.get("server_ip") or S.get("host") or "",
|
||||
"port": int(S["port"]) if S.get("port") else None,
|
||||
"user": S.get("username") or "",
|
||||
"password": S.get("password") or "",
|
||||
"base_dn": S.get("base_dn") or "",
|
||||
"secure": S.get("secure_connection") or "SSL",
|
||||
"ssl_version": S.get("ssl_version") or "None",
|
||||
"ntlm": _bool(S.get("ntlm")),
|
||||
"unsecure": _bool(S.get("unsecure")),
|
||||
"page_size": int(S["page_size"]) if S.get("page_size") else 500,
|
||||
}
|
||||
|
||||
|
||||
def _tls(unsecure, ssl_version):
|
||||
version = SSL_VERSIONS.get(ssl_version)
|
||||
if unsecure:
|
||||
return Tls(validate=ssl.CERT_NONE, ca_certs_file=None, ciphers=CIPHERS, version=version)
|
||||
return Tls(validate=ssl.CERT_REQUIRED, ca_certs_file=os.environ.get("SSL_CERT_FILE"), version=version)
|
||||
|
||||
|
||||
def _server(c):
|
||||
secure = c["secure"]
|
||||
if secure in ("SSL", "TLS"):
|
||||
return Server(host=c["host"], port=c["port"], use_ssl=True, tls=_tls(c["unsecure"], c["ssl_version"]), connect_timeout=120)
|
||||
if secure == "Start TLS":
|
||||
return Server(host=c["host"], port=c["port"], use_ssl=False, tls=_tls(c["unsecure"], c["ssl_version"]), connect_timeout=120)
|
||||
return Server(host=c["host"], port=c["port"], connect_timeout=120)
|
||||
|
||||
|
||||
def _auto_bind(secure):
|
||||
return AUTO_BIND_TLS_BEFORE_BIND if secure == "Start TLS" else AUTO_BIND_NO_TLS
|
||||
|
||||
|
||||
def connect():
|
||||
global CONN
|
||||
c = _cfg()
|
||||
server = _server(c)
|
||||
if c["ntlm"]:
|
||||
domain_user = c["user"] if "\\" in c["user"] else c["host"] + "\\" + c["user"]
|
||||
CONN = Connection(server, domain_user, password=c["password"], authentication=NTLM, auto_bind=_auto_bind(c["secure"]))
|
||||
else:
|
||||
CONN = Connection(server, user=c["user"], password=c["password"], auto_bind=_auto_bind(c["secure"]))
|
||||
return CONN, c
|
||||
|
||||
|
||||
def base_dn_arg():
|
||||
return I.get("base-dn") or I.get("base_dn") or _cfg()["base_dn"]
|
||||
|
||||
|
||||
def search_paging(conn, search_filter, search_base, attributes=None, page_size=100, size_limit=0, time_limit=0):
|
||||
entries = []
|
||||
cookie = None
|
||||
total = 0
|
||||
left = size_limit
|
||||
while True:
|
||||
ps = page_size
|
||||
if 0 < left < page_size:
|
||||
ps = left
|
||||
conn.search(search_base, search_filter, search_scope=SUBTREE, attributes=attributes,
|
||||
paged_size=ps, paged_cookie=cookie, time_limit=time_limit)
|
||||
left -= len(conn.entries)
|
||||
total += len(conn.entries)
|
||||
try:
|
||||
cookie = conn.result["controls"]["1.2.840.113556.1.4.319"]["value"]["cookie"]
|
||||
except (KeyError, TypeError):
|
||||
cookie = None
|
||||
entries.extend(conn.entries)
|
||||
if (size_limit and size_limit <= total) or not cookie:
|
||||
break
|
||||
flat = []
|
||||
for entry in entries:
|
||||
obj = json.loads(entry.entry_to_json())
|
||||
flat_entry = {"dn": obj["dn"]}
|
||||
for attr in obj.get("attributes", {}):
|
||||
flat_entry[attr] = obj["attributes"][attr]
|
||||
flat.append(flat_entry)
|
||||
return flat
|
||||
|
||||
|
||||
def user_dn(conn, sam, search_base):
|
||||
if "\\" in sam:
|
||||
sam = sam.split("\\", 1)[1]
|
||||
flat = search_paging(conn, f"(&(objectClass=user)(sAMAccountName={escape_filter_chars(sam)}))",
|
||||
search_base, page_size=1, size_limit=1)
|
||||
if not flat:
|
||||
raise Exception(f"Could not find user '{sam}'")
|
||||
return flat[0]["dn"]
|
||||
|
||||
|
||||
def computer_dn(conn, name, search_base):
|
||||
flat = search_paging(conn, f"(&(objectClass=user)(objectCategory=computer)(name={escape_filter_chars(name)}))",
|
||||
search_base, page_size=1, size_limit=1)
|
||||
if not flat:
|
||||
raise Exception(f"Could not find computer '{name}'")
|
||||
return flat[0]["dn"]
|
||||
|
||||
|
||||
def group_dn(conn, name, search_base):
|
||||
flat = search_paging(conn, f"(&(objectClass=group)(cn={escape_filter_chars(name)}))",
|
||||
search_base, page_size=1, size_limit=1)
|
||||
if not flat:
|
||||
raise Exception(f"Could not find group '{name}'")
|
||||
return flat[0]["dn"]
|
||||
|
||||
|
||||
def modify(conn, dn, modification):
|
||||
if not conn.modify(dn, modification):
|
||||
raise Exception(f"Failed to modify {dn}: {json.dumps(conn.result, default=str)}")
|
||||
|
||||
|
||||
def first_uac(values):
|
||||
if isinstance(values, list):
|
||||
return values[0] if values else 0
|
||||
return values or 0
|
||||
|
||||
|
||||
def uac_fields(value):
|
||||
value = int(value)
|
||||
flags = {
|
||||
"SCRIPT": 0x1, "ACCOUNTDISABLE": 0x2, "HOMEDIR_REQUIRED": 0x8, "LOCKOUT": 0x10,
|
||||
"PASSWD_NOTREQD": 0x20, "PASSWD_CANT_CHANGE": 0x40, "ENCRYPTED_TEXT_PWD_ALLOWED": 0x80,
|
||||
"TEMP_DUPLICATE_ACCOUNT": 0x100, "NORMAL_ACCOUNT": 0x200, "INTERDOMAIN_TRUST_ACCOUNT": 0x800,
|
||||
"WORKSTATION_TRUST_ACCOUNT": 0x1000, "SERVER_TRUST_ACCOUNT": 0x2000, "DONT_EXPIRE_PASSWORD": 0x10000,
|
||||
"MNS_LOGON_ACCOUNT": 0x20000, "SMARTCARD_REQUIRED": 0x40000, "TRUSTED_FOR_DELEGATION": 0x80000,
|
||||
"NOT_DELEGATED": 0x100000, "USE_DES_KEY_ONLY": 0x200000, "DONT_REQ_PREAUTH": 0x400000,
|
||||
"PASSWORD_EXPIRED": 0x800000, "TRUSTED_TO_AUTH_FOR_DELEGATION": 0x1000000, "PARTIAL_SECRETS_ACCOUNT": 0x4000000,
|
||||
}
|
||||
return {name: bool(value & mask) for name, mask in flags.items()}
|
||||
|
||||
|
||||
def current_uac(conn, search_base):
|
||||
sam = I.get("username") or I.get("sAMAccountName")
|
||||
query = "(&(objectClass=User)(objectCategory=person))"
|
||||
if sam:
|
||||
query = f"(&(objectClass=User)(objectCategory=person)(sAMAccountName={escape_filter_chars(sam)}))"
|
||||
flat = search_paging(conn, query, search_base, attributes=["userAccountControl"], size_limit=1, page_size=1)
|
||||
if flat and flat[0].get("userAccountControl"):
|
||||
return first_uac(flat[0]["userAccountControl"])
|
||||
return 512
|
||||
|
||||
|
||||
|
||||
def run():
|
||||
conn, c = connect()
|
||||
defaults = ["name", "displayName", "memberOf", "mail", "sAMAccountName", "manager", "userAccountControl"]
|
||||
query = S.get("default_base_query") or "(&(objectClass=User)(objectCategory=person))"
|
||||
esc = escape_filter_chars
|
||||
if I.get("dn"):
|
||||
query = f"(&(objectClass=User)(objectCategory=person)(distinguishedName={esc(I['dn'])}))"
|
||||
elif I.get("name"):
|
||||
query = f"(&(objectClass=User)(objectCategory=person)(cn={esc(I['name'])}))"
|
||||
elif I.get("email"):
|
||||
query = f"(&(objectClass=User)(objectCategory=person)(mail={esc(I['email'])}))"
|
||||
elif I.get("username") or I.get("sAMAccountName"):
|
||||
u = esc(I.get("username") or I.get("sAMAccountName"))
|
||||
query = f"(&(objectClass=User)(objectCategory=person)(sAMAccountName={u}))"
|
||||
elif I.get("custom-field-type"):
|
||||
if not I.get("custom-field-data"):
|
||||
fail("custom-field-data is required when custom-field-type is given")
|
||||
query = f"(&(objectClass=User)(objectCategory=person)({esc(I['custom-field-type'])}={esc(I['custom-field-data'])}))"
|
||||
custom = I["attributes"].split(",") if I.get("attributes") else []
|
||||
exclude = [a.strip() for a in (I.get("attributes-to-exclude") or "").split(",") if a.strip()]
|
||||
attributes = list((set(custom) | set(defaults)) - set(exclude))
|
||||
limit = int(I.get("limit") or 20)
|
||||
if limit <= 0:
|
||||
limit = 20
|
||||
page_size = int(I.get("page-size") or c["page_size"])
|
||||
if I.get("page-size"):
|
||||
limit = int(I["page-size"])
|
||||
flat = search_paging(conn, query, base_dn_arg(), attributes=attributes, size_limit=limit, page_size=page_size)
|
||||
for user in flat:
|
||||
if user.get("userAccountControl"):
|
||||
user["userAccountControlFields"] = uac_fields(first_uac(user["userAccountControl"]))
|
||||
out({"users": flat, "count": len(flat)})
|
||||
|
||||
|
||||
try:
|
||||
run()
|
||||
except SystemExit:
|
||||
raise
|
||||
except LDAPException as e:
|
||||
fail("LDAP error: " + str(e))
|
||||
except Exception as e:
|
||||
fail(str(e))
|
||||
finally:
|
||||
try:
|
||||
if CONN is not None:
|
||||
CONN.unbind()
|
||||
except Exception:
|
||||
pass
|
||||
@@ -0,0 +1,229 @@
|
||||
import json, os, sys
|
||||
|
||||
S = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||
I = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||
|
||||
|
||||
def out(value):
|
||||
print(json.dumps(value, default=str))
|
||||
|
||||
|
||||
def fail(message, **extra):
|
||||
payload = {"error": message}
|
||||
payload.update(extra)
|
||||
print(json.dumps(payload, default=str))
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
try:
|
||||
import ssl
|
||||
from ldap3 import (
|
||||
Server, Connection, Tls, NTLM, SUBTREE, BASE, ALL_ATTRIBUTES,
|
||||
MODIFY_REPLACE, AUTO_BIND_NO_TLS, AUTO_BIND_TLS_BEFORE_BIND,
|
||||
)
|
||||
from ldap3.core.exceptions import LDAPException
|
||||
from ldap3.extend import microsoft
|
||||
from ldap3.utils.conv import escape_filter_chars
|
||||
except ImportError as _e:
|
||||
print(json.dumps({
|
||||
"error": "The 'ldap3' Python library is required for the Active Directory integration. "
|
||||
"Install it on the execution host (engine): pip install ldap3",
|
||||
"detail": str(_e),
|
||||
}))
|
||||
sys.exit(1)
|
||||
|
||||
SSL_VERSIONS = {
|
||||
"None": None,
|
||||
"TLS": ssl.PROTOCOL_TLS,
|
||||
"TLSv1": ssl.PROTOCOL_TLSv1,
|
||||
"TLSv1_1": ssl.PROTOCOL_TLSv1_1,
|
||||
"TLSv1_2": ssl.PROTOCOL_TLSv1_2,
|
||||
"TLS_CLIENT": ssl.PROTOCOL_TLS_CLIENT,
|
||||
}
|
||||
CIPHERS = (
|
||||
"@SECLEVEL=1:ECDHE+AESGCM:ECDHE+CHACHA20:DHE+AESGCM:DHE+CHACHA20:ECDH+AESGCM:"
|
||||
"DH+AESGCM:ECDH+AES:DH+AES:RSA+AESGCM:RSA+AES:!aNULL:!eNULL:!MD5:!DSS"
|
||||
)
|
||||
|
||||
CONN = None
|
||||
|
||||
|
||||
def _bool(v):
|
||||
if isinstance(v, bool):
|
||||
return v
|
||||
return str(v).lower() in ("1", "true", "yes")
|
||||
|
||||
|
||||
def _list(v):
|
||||
if isinstance(v, list):
|
||||
return v
|
||||
return [x.strip() for x in str(v).split(",") if x.strip()]
|
||||
|
||||
|
||||
def _cfg():
|
||||
return {
|
||||
"host": S.get("server_ip") or S.get("host") or "",
|
||||
"port": int(S["port"]) if S.get("port") else None,
|
||||
"user": S.get("username") or "",
|
||||
"password": S.get("password") or "",
|
||||
"base_dn": S.get("base_dn") or "",
|
||||
"secure": S.get("secure_connection") or "SSL",
|
||||
"ssl_version": S.get("ssl_version") or "None",
|
||||
"ntlm": _bool(S.get("ntlm")),
|
||||
"unsecure": _bool(S.get("unsecure")),
|
||||
"page_size": int(S["page_size"]) if S.get("page_size") else 500,
|
||||
}
|
||||
|
||||
|
||||
def _tls(unsecure, ssl_version):
|
||||
version = SSL_VERSIONS.get(ssl_version)
|
||||
if unsecure:
|
||||
return Tls(validate=ssl.CERT_NONE, ca_certs_file=None, ciphers=CIPHERS, version=version)
|
||||
return Tls(validate=ssl.CERT_REQUIRED, ca_certs_file=os.environ.get("SSL_CERT_FILE"), version=version)
|
||||
|
||||
|
||||
def _server(c):
|
||||
secure = c["secure"]
|
||||
if secure in ("SSL", "TLS"):
|
||||
return Server(host=c["host"], port=c["port"], use_ssl=True, tls=_tls(c["unsecure"], c["ssl_version"]), connect_timeout=120)
|
||||
if secure == "Start TLS":
|
||||
return Server(host=c["host"], port=c["port"], use_ssl=False, tls=_tls(c["unsecure"], c["ssl_version"]), connect_timeout=120)
|
||||
return Server(host=c["host"], port=c["port"], connect_timeout=120)
|
||||
|
||||
|
||||
def _auto_bind(secure):
|
||||
return AUTO_BIND_TLS_BEFORE_BIND if secure == "Start TLS" else AUTO_BIND_NO_TLS
|
||||
|
||||
|
||||
def connect():
|
||||
global CONN
|
||||
c = _cfg()
|
||||
server = _server(c)
|
||||
if c["ntlm"]:
|
||||
domain_user = c["user"] if "\\" in c["user"] else c["host"] + "\\" + c["user"]
|
||||
CONN = Connection(server, domain_user, password=c["password"], authentication=NTLM, auto_bind=_auto_bind(c["secure"]))
|
||||
else:
|
||||
CONN = Connection(server, user=c["user"], password=c["password"], auto_bind=_auto_bind(c["secure"]))
|
||||
return CONN, c
|
||||
|
||||
|
||||
def base_dn_arg():
|
||||
return I.get("base-dn") or I.get("base_dn") or _cfg()["base_dn"]
|
||||
|
||||
|
||||
def search_paging(conn, search_filter, search_base, attributes=None, page_size=100, size_limit=0, time_limit=0):
|
||||
entries = []
|
||||
cookie = None
|
||||
total = 0
|
||||
left = size_limit
|
||||
while True:
|
||||
ps = page_size
|
||||
if 0 < left < page_size:
|
||||
ps = left
|
||||
conn.search(search_base, search_filter, search_scope=SUBTREE, attributes=attributes,
|
||||
paged_size=ps, paged_cookie=cookie, time_limit=time_limit)
|
||||
left -= len(conn.entries)
|
||||
total += len(conn.entries)
|
||||
try:
|
||||
cookie = conn.result["controls"]["1.2.840.113556.1.4.319"]["value"]["cookie"]
|
||||
except (KeyError, TypeError):
|
||||
cookie = None
|
||||
entries.extend(conn.entries)
|
||||
if (size_limit and size_limit <= total) or not cookie:
|
||||
break
|
||||
flat = []
|
||||
for entry in entries:
|
||||
obj = json.loads(entry.entry_to_json())
|
||||
flat_entry = {"dn": obj["dn"]}
|
||||
for attr in obj.get("attributes", {}):
|
||||
flat_entry[attr] = obj["attributes"][attr]
|
||||
flat.append(flat_entry)
|
||||
return flat
|
||||
|
||||
|
||||
def user_dn(conn, sam, search_base):
|
||||
if "\\" in sam:
|
||||
sam = sam.split("\\", 1)[1]
|
||||
flat = search_paging(conn, f"(&(objectClass=user)(sAMAccountName={escape_filter_chars(sam)}))",
|
||||
search_base, page_size=1, size_limit=1)
|
||||
if not flat:
|
||||
raise Exception(f"Could not find user '{sam}'")
|
||||
return flat[0]["dn"]
|
||||
|
||||
|
||||
def computer_dn(conn, name, search_base):
|
||||
flat = search_paging(conn, f"(&(objectClass=user)(objectCategory=computer)(name={escape_filter_chars(name)}))",
|
||||
search_base, page_size=1, size_limit=1)
|
||||
if not flat:
|
||||
raise Exception(f"Could not find computer '{name}'")
|
||||
return flat[0]["dn"]
|
||||
|
||||
|
||||
def group_dn(conn, name, search_base):
|
||||
flat = search_paging(conn, f"(&(objectClass=group)(cn={escape_filter_chars(name)}))",
|
||||
search_base, page_size=1, size_limit=1)
|
||||
if not flat:
|
||||
raise Exception(f"Could not find group '{name}'")
|
||||
return flat[0]["dn"]
|
||||
|
||||
|
||||
def modify(conn, dn, modification):
|
||||
if not conn.modify(dn, modification):
|
||||
raise Exception(f"Failed to modify {dn}: {json.dumps(conn.result, default=str)}")
|
||||
|
||||
|
||||
def first_uac(values):
|
||||
if isinstance(values, list):
|
||||
return values[0] if values else 0
|
||||
return values or 0
|
||||
|
||||
|
||||
def uac_fields(value):
|
||||
value = int(value)
|
||||
flags = {
|
||||
"SCRIPT": 0x1, "ACCOUNTDISABLE": 0x2, "HOMEDIR_REQUIRED": 0x8, "LOCKOUT": 0x10,
|
||||
"PASSWD_NOTREQD": 0x20, "PASSWD_CANT_CHANGE": 0x40, "ENCRYPTED_TEXT_PWD_ALLOWED": 0x80,
|
||||
"TEMP_DUPLICATE_ACCOUNT": 0x100, "NORMAL_ACCOUNT": 0x200, "INTERDOMAIN_TRUST_ACCOUNT": 0x800,
|
||||
"WORKSTATION_TRUST_ACCOUNT": 0x1000, "SERVER_TRUST_ACCOUNT": 0x2000, "DONT_EXPIRE_PASSWORD": 0x10000,
|
||||
"MNS_LOGON_ACCOUNT": 0x20000, "SMARTCARD_REQUIRED": 0x40000, "TRUSTED_FOR_DELEGATION": 0x80000,
|
||||
"NOT_DELEGATED": 0x100000, "USE_DES_KEY_ONLY": 0x200000, "DONT_REQ_PREAUTH": 0x400000,
|
||||
"PASSWORD_EXPIRED": 0x800000, "TRUSTED_TO_AUTH_FOR_DELEGATION": 0x1000000, "PARTIAL_SECRETS_ACCOUNT": 0x4000000,
|
||||
}
|
||||
return {name: bool(value & mask) for name, mask in flags.items()}
|
||||
|
||||
|
||||
def current_uac(conn, search_base):
|
||||
sam = I.get("username") or I.get("sAMAccountName")
|
||||
query = "(&(objectClass=User)(objectCategory=person))"
|
||||
if sam:
|
||||
query = f"(&(objectClass=User)(objectCategory=person)(sAMAccountName={escape_filter_chars(sam)}))"
|
||||
flat = search_paging(conn, query, search_base, attributes=["userAccountControl"], size_limit=1, page_size=1)
|
||||
if flat and flat[0].get("userAccountControl"):
|
||||
return first_uac(flat[0]["userAccountControl"])
|
||||
return 512
|
||||
|
||||
|
||||
|
||||
def run():
|
||||
conn, c = connect()
|
||||
name = I["computer-name"]
|
||||
dn = computer_dn(conn, name, I.get("base-dn") or base_dn_arg())
|
||||
if not conn.modify_dn(dn, f"CN={name}", new_superior=I["full-superior-dn"]):
|
||||
fail("Failed to move computer: " + json.dumps(conn.result, default=str))
|
||||
out({"moved": name, "to": I["full-superior-dn"]})
|
||||
|
||||
|
||||
try:
|
||||
run()
|
||||
except SystemExit:
|
||||
raise
|
||||
except LDAPException as e:
|
||||
fail("LDAP error: " + str(e))
|
||||
except Exception as e:
|
||||
fail(str(e))
|
||||
finally:
|
||||
try:
|
||||
if CONN is not None:
|
||||
CONN.unbind()
|
||||
except Exception:
|
||||
pass
|
||||
@@ -0,0 +1,236 @@
|
||||
import json, os, sys
|
||||
|
||||
S = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||
I = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||
|
||||
|
||||
def out(value):
|
||||
print(json.dumps(value, default=str))
|
||||
|
||||
|
||||
def fail(message, **extra):
|
||||
payload = {"error": message}
|
||||
payload.update(extra)
|
||||
print(json.dumps(payload, default=str))
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
try:
|
||||
import ssl
|
||||
from ldap3 import (
|
||||
Server, Connection, Tls, NTLM, SUBTREE, BASE, ALL_ATTRIBUTES,
|
||||
MODIFY_REPLACE, AUTO_BIND_NO_TLS, AUTO_BIND_TLS_BEFORE_BIND,
|
||||
)
|
||||
from ldap3.core.exceptions import LDAPException
|
||||
from ldap3.extend import microsoft
|
||||
from ldap3.utils.conv import escape_filter_chars
|
||||
except ImportError as _e:
|
||||
print(json.dumps({
|
||||
"error": "The 'ldap3' Python library is required for the Active Directory integration. "
|
||||
"Install it on the execution host (engine): pip install ldap3",
|
||||
"detail": str(_e),
|
||||
}))
|
||||
sys.exit(1)
|
||||
|
||||
SSL_VERSIONS = {
|
||||
"None": None,
|
||||
"TLS": ssl.PROTOCOL_TLS,
|
||||
"TLSv1": ssl.PROTOCOL_TLSv1,
|
||||
"TLSv1_1": ssl.PROTOCOL_TLSv1_1,
|
||||
"TLSv1_2": ssl.PROTOCOL_TLSv1_2,
|
||||
"TLS_CLIENT": ssl.PROTOCOL_TLS_CLIENT,
|
||||
}
|
||||
CIPHERS = (
|
||||
"@SECLEVEL=1:ECDHE+AESGCM:ECDHE+CHACHA20:DHE+AESGCM:DHE+CHACHA20:ECDH+AESGCM:"
|
||||
"DH+AESGCM:ECDH+AES:DH+AES:RSA+AESGCM:RSA+AES:!aNULL:!eNULL:!MD5:!DSS"
|
||||
)
|
||||
|
||||
CONN = None
|
||||
|
||||
|
||||
def _bool(v):
|
||||
if isinstance(v, bool):
|
||||
return v
|
||||
return str(v).lower() in ("1", "true", "yes")
|
||||
|
||||
|
||||
def _list(v):
|
||||
if isinstance(v, list):
|
||||
return v
|
||||
return [x.strip() for x in str(v).split(",") if x.strip()]
|
||||
|
||||
|
||||
def _cfg():
|
||||
return {
|
||||
"host": S.get("server_ip") or S.get("host") or "",
|
||||
"port": int(S["port"]) if S.get("port") else None,
|
||||
"user": S.get("username") or "",
|
||||
"password": S.get("password") or "",
|
||||
"base_dn": S.get("base_dn") or "",
|
||||
"secure": S.get("secure_connection") or "SSL",
|
||||
"ssl_version": S.get("ssl_version") or "None",
|
||||
"ntlm": _bool(S.get("ntlm")),
|
||||
"unsecure": _bool(S.get("unsecure")),
|
||||
"page_size": int(S["page_size"]) if S.get("page_size") else 500,
|
||||
}
|
||||
|
||||
|
||||
def _tls(unsecure, ssl_version):
|
||||
version = SSL_VERSIONS.get(ssl_version)
|
||||
if unsecure:
|
||||
return Tls(validate=ssl.CERT_NONE, ca_certs_file=None, ciphers=CIPHERS, version=version)
|
||||
return Tls(validate=ssl.CERT_REQUIRED, ca_certs_file=os.environ.get("SSL_CERT_FILE"), version=version)
|
||||
|
||||
|
||||
def _server(c):
|
||||
secure = c["secure"]
|
||||
if secure in ("SSL", "TLS"):
|
||||
return Server(host=c["host"], port=c["port"], use_ssl=True, tls=_tls(c["unsecure"], c["ssl_version"]), connect_timeout=120)
|
||||
if secure == "Start TLS":
|
||||
return Server(host=c["host"], port=c["port"], use_ssl=False, tls=_tls(c["unsecure"], c["ssl_version"]), connect_timeout=120)
|
||||
return Server(host=c["host"], port=c["port"], connect_timeout=120)
|
||||
|
||||
|
||||
def _auto_bind(secure):
|
||||
return AUTO_BIND_TLS_BEFORE_BIND if secure == "Start TLS" else AUTO_BIND_NO_TLS
|
||||
|
||||
|
||||
def connect():
|
||||
global CONN
|
||||
c = _cfg()
|
||||
server = _server(c)
|
||||
if c["ntlm"]:
|
||||
domain_user = c["user"] if "\\" in c["user"] else c["host"] + "\\" + c["user"]
|
||||
CONN = Connection(server, domain_user, password=c["password"], authentication=NTLM, auto_bind=_auto_bind(c["secure"]))
|
||||
else:
|
||||
CONN = Connection(server, user=c["user"], password=c["password"], auto_bind=_auto_bind(c["secure"]))
|
||||
return CONN, c
|
||||
|
||||
|
||||
def base_dn_arg():
|
||||
return I.get("base-dn") or I.get("base_dn") or _cfg()["base_dn"]
|
||||
|
||||
|
||||
def search_paging(conn, search_filter, search_base, attributes=None, page_size=100, size_limit=0, time_limit=0):
|
||||
entries = []
|
||||
cookie = None
|
||||
total = 0
|
||||
left = size_limit
|
||||
while True:
|
||||
ps = page_size
|
||||
if 0 < left < page_size:
|
||||
ps = left
|
||||
conn.search(search_base, search_filter, search_scope=SUBTREE, attributes=attributes,
|
||||
paged_size=ps, paged_cookie=cookie, time_limit=time_limit)
|
||||
left -= len(conn.entries)
|
||||
total += len(conn.entries)
|
||||
try:
|
||||
cookie = conn.result["controls"]["1.2.840.113556.1.4.319"]["value"]["cookie"]
|
||||
except (KeyError, TypeError):
|
||||
cookie = None
|
||||
entries.extend(conn.entries)
|
||||
if (size_limit and size_limit <= total) or not cookie:
|
||||
break
|
||||
flat = []
|
||||
for entry in entries:
|
||||
obj = json.loads(entry.entry_to_json())
|
||||
flat_entry = {"dn": obj["dn"]}
|
||||
for attr in obj.get("attributes", {}):
|
||||
flat_entry[attr] = obj["attributes"][attr]
|
||||
flat.append(flat_entry)
|
||||
return flat
|
||||
|
||||
|
||||
def user_dn(conn, sam, search_base):
|
||||
if "\\" in sam:
|
||||
sam = sam.split("\\", 1)[1]
|
||||
flat = search_paging(conn, f"(&(objectClass=user)(sAMAccountName={escape_filter_chars(sam)}))",
|
||||
search_base, page_size=1, size_limit=1)
|
||||
if not flat:
|
||||
raise Exception(f"Could not find user '{sam}'")
|
||||
return flat[0]["dn"]
|
||||
|
||||
|
||||
def computer_dn(conn, name, search_base):
|
||||
flat = search_paging(conn, f"(&(objectClass=user)(objectCategory=computer)(name={escape_filter_chars(name)}))",
|
||||
search_base, page_size=1, size_limit=1)
|
||||
if not flat:
|
||||
raise Exception(f"Could not find computer '{name}'")
|
||||
return flat[0]["dn"]
|
||||
|
||||
|
||||
def group_dn(conn, name, search_base):
|
||||
flat = search_paging(conn, f"(&(objectClass=group)(cn={escape_filter_chars(name)}))",
|
||||
search_base, page_size=1, size_limit=1)
|
||||
if not flat:
|
||||
raise Exception(f"Could not find group '{name}'")
|
||||
return flat[0]["dn"]
|
||||
|
||||
|
||||
def modify(conn, dn, modification):
|
||||
if not conn.modify(dn, modification):
|
||||
raise Exception(f"Failed to modify {dn}: {json.dumps(conn.result, default=str)}")
|
||||
|
||||
|
||||
def first_uac(values):
|
||||
if isinstance(values, list):
|
||||
return values[0] if values else 0
|
||||
return values or 0
|
||||
|
||||
|
||||
def uac_fields(value):
|
||||
value = int(value)
|
||||
flags = {
|
||||
"SCRIPT": 0x1, "ACCOUNTDISABLE": 0x2, "HOMEDIR_REQUIRED": 0x8, "LOCKOUT": 0x10,
|
||||
"PASSWD_NOTREQD": 0x20, "PASSWD_CANT_CHANGE": 0x40, "ENCRYPTED_TEXT_PWD_ALLOWED": 0x80,
|
||||
"TEMP_DUPLICATE_ACCOUNT": 0x100, "NORMAL_ACCOUNT": 0x200, "INTERDOMAIN_TRUST_ACCOUNT": 0x800,
|
||||
"WORKSTATION_TRUST_ACCOUNT": 0x1000, "SERVER_TRUST_ACCOUNT": 0x2000, "DONT_EXPIRE_PASSWORD": 0x10000,
|
||||
"MNS_LOGON_ACCOUNT": 0x20000, "SMARTCARD_REQUIRED": 0x40000, "TRUSTED_FOR_DELEGATION": 0x80000,
|
||||
"NOT_DELEGATED": 0x100000, "USE_DES_KEY_ONLY": 0x200000, "DONT_REQ_PREAUTH": 0x400000,
|
||||
"PASSWORD_EXPIRED": 0x800000, "TRUSTED_TO_AUTH_FOR_DELEGATION": 0x1000000, "PARTIAL_SECRETS_ACCOUNT": 0x4000000,
|
||||
}
|
||||
return {name: bool(value & mask) for name, mask in flags.items()}
|
||||
|
||||
|
||||
def current_uac(conn, search_base):
|
||||
sam = I.get("username") or I.get("sAMAccountName")
|
||||
query = "(&(objectClass=User)(objectCategory=person))"
|
||||
if sam:
|
||||
query = f"(&(objectClass=User)(objectCategory=person)(sAMAccountName={escape_filter_chars(sam)}))"
|
||||
flat = search_paging(conn, query, search_base, attributes=["userAccountControl"], size_limit=1, page_size=1)
|
||||
if flat and flat[0].get("userAccountControl"):
|
||||
return first_uac(flat[0]["userAccountControl"])
|
||||
return 512
|
||||
|
||||
|
||||
|
||||
def run():
|
||||
conn, c = connect()
|
||||
sam = I["username"]
|
||||
never = str(I["value"]).lower() == "true"
|
||||
sb = base_dn_arg()
|
||||
flat = search_paging(conn, f"(&(objectClass=User)(objectCategory=person)(sAMAccountName={escape_filter_chars(sam)}))",
|
||||
sb, attributes=["userAccountControl"], size_limit=1, page_size=1)
|
||||
if not flat:
|
||||
fail(f"User {sam} not found")
|
||||
uac = first_uac(flat[0].get("userAccountControl"))
|
||||
uac = (uac | (1 << 16)) if never else (uac & ~(1 << 16))
|
||||
dn = user_dn(conn, sam, sb)
|
||||
modify(conn, dn, {"userAccountControl": [(MODIFY_REPLACE, [uac])]})
|
||||
out({"username": sam, "password_never_expire": never})
|
||||
|
||||
|
||||
try:
|
||||
run()
|
||||
except SystemExit:
|
||||
raise
|
||||
except LDAPException as e:
|
||||
fail("LDAP error: " + str(e))
|
||||
except Exception as e:
|
||||
fail(str(e))
|
||||
finally:
|
||||
try:
|
||||
if CONN is not None:
|
||||
CONN.unbind()
|
||||
except Exception:
|
||||
pass
|
||||
@@ -0,0 +1,231 @@
|
||||
import json, os, sys
|
||||
|
||||
S = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||
I = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||
|
||||
|
||||
def out(value):
|
||||
print(json.dumps(value, default=str))
|
||||
|
||||
|
||||
def fail(message, **extra):
|
||||
payload = {"error": message}
|
||||
payload.update(extra)
|
||||
print(json.dumps(payload, default=str))
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
try:
|
||||
import ssl
|
||||
from ldap3 import (
|
||||
Server, Connection, Tls, NTLM, SUBTREE, BASE, ALL_ATTRIBUTES,
|
||||
MODIFY_REPLACE, AUTO_BIND_NO_TLS, AUTO_BIND_TLS_BEFORE_BIND,
|
||||
)
|
||||
from ldap3.core.exceptions import LDAPException
|
||||
from ldap3.extend import microsoft
|
||||
from ldap3.utils.conv import escape_filter_chars
|
||||
except ImportError as _e:
|
||||
print(json.dumps({
|
||||
"error": "The 'ldap3' Python library is required for the Active Directory integration. "
|
||||
"Install it on the execution host (engine): pip install ldap3",
|
||||
"detail": str(_e),
|
||||
}))
|
||||
sys.exit(1)
|
||||
|
||||
SSL_VERSIONS = {
|
||||
"None": None,
|
||||
"TLS": ssl.PROTOCOL_TLS,
|
||||
"TLSv1": ssl.PROTOCOL_TLSv1,
|
||||
"TLSv1_1": ssl.PROTOCOL_TLSv1_1,
|
||||
"TLSv1_2": ssl.PROTOCOL_TLSv1_2,
|
||||
"TLS_CLIENT": ssl.PROTOCOL_TLS_CLIENT,
|
||||
}
|
||||
CIPHERS = (
|
||||
"@SECLEVEL=1:ECDHE+AESGCM:ECDHE+CHACHA20:DHE+AESGCM:DHE+CHACHA20:ECDH+AESGCM:"
|
||||
"DH+AESGCM:ECDH+AES:DH+AES:RSA+AESGCM:RSA+AES:!aNULL:!eNULL:!MD5:!DSS"
|
||||
)
|
||||
|
||||
CONN = None
|
||||
|
||||
|
||||
def _bool(v):
|
||||
if isinstance(v, bool):
|
||||
return v
|
||||
return str(v).lower() in ("1", "true", "yes")
|
||||
|
||||
|
||||
def _list(v):
|
||||
if isinstance(v, list):
|
||||
return v
|
||||
return [x.strip() for x in str(v).split(",") if x.strip()]
|
||||
|
||||
|
||||
def _cfg():
|
||||
return {
|
||||
"host": S.get("server_ip") or S.get("host") or "",
|
||||
"port": int(S["port"]) if S.get("port") else None,
|
||||
"user": S.get("username") or "",
|
||||
"password": S.get("password") or "",
|
||||
"base_dn": S.get("base_dn") or "",
|
||||
"secure": S.get("secure_connection") or "SSL",
|
||||
"ssl_version": S.get("ssl_version") or "None",
|
||||
"ntlm": _bool(S.get("ntlm")),
|
||||
"unsecure": _bool(S.get("unsecure")),
|
||||
"page_size": int(S["page_size"]) if S.get("page_size") else 500,
|
||||
}
|
||||
|
||||
|
||||
def _tls(unsecure, ssl_version):
|
||||
version = SSL_VERSIONS.get(ssl_version)
|
||||
if unsecure:
|
||||
return Tls(validate=ssl.CERT_NONE, ca_certs_file=None, ciphers=CIPHERS, version=version)
|
||||
return Tls(validate=ssl.CERT_REQUIRED, ca_certs_file=os.environ.get("SSL_CERT_FILE"), version=version)
|
||||
|
||||
|
||||
def _server(c):
|
||||
secure = c["secure"]
|
||||
if secure in ("SSL", "TLS"):
|
||||
return Server(host=c["host"], port=c["port"], use_ssl=True, tls=_tls(c["unsecure"], c["ssl_version"]), connect_timeout=120)
|
||||
if secure == "Start TLS":
|
||||
return Server(host=c["host"], port=c["port"], use_ssl=False, tls=_tls(c["unsecure"], c["ssl_version"]), connect_timeout=120)
|
||||
return Server(host=c["host"], port=c["port"], connect_timeout=120)
|
||||
|
||||
|
||||
def _auto_bind(secure):
|
||||
return AUTO_BIND_TLS_BEFORE_BIND if secure == "Start TLS" else AUTO_BIND_NO_TLS
|
||||
|
||||
|
||||
def connect():
|
||||
global CONN
|
||||
c = _cfg()
|
||||
server = _server(c)
|
||||
if c["ntlm"]:
|
||||
domain_user = c["user"] if "\\" in c["user"] else c["host"] + "\\" + c["user"]
|
||||
CONN = Connection(server, domain_user, password=c["password"], authentication=NTLM, auto_bind=_auto_bind(c["secure"]))
|
||||
else:
|
||||
CONN = Connection(server, user=c["user"], password=c["password"], auto_bind=_auto_bind(c["secure"]))
|
||||
return CONN, c
|
||||
|
||||
|
||||
def base_dn_arg():
|
||||
return I.get("base-dn") or I.get("base_dn") or _cfg()["base_dn"]
|
||||
|
||||
|
||||
def search_paging(conn, search_filter, search_base, attributes=None, page_size=100, size_limit=0, time_limit=0):
|
||||
entries = []
|
||||
cookie = None
|
||||
total = 0
|
||||
left = size_limit
|
||||
while True:
|
||||
ps = page_size
|
||||
if 0 < left < page_size:
|
||||
ps = left
|
||||
conn.search(search_base, search_filter, search_scope=SUBTREE, attributes=attributes,
|
||||
paged_size=ps, paged_cookie=cookie, time_limit=time_limit)
|
||||
left -= len(conn.entries)
|
||||
total += len(conn.entries)
|
||||
try:
|
||||
cookie = conn.result["controls"]["1.2.840.113556.1.4.319"]["value"]["cookie"]
|
||||
except (KeyError, TypeError):
|
||||
cookie = None
|
||||
entries.extend(conn.entries)
|
||||
if (size_limit and size_limit <= total) or not cookie:
|
||||
break
|
||||
flat = []
|
||||
for entry in entries:
|
||||
obj = json.loads(entry.entry_to_json())
|
||||
flat_entry = {"dn": obj["dn"]}
|
||||
for attr in obj.get("attributes", {}):
|
||||
flat_entry[attr] = obj["attributes"][attr]
|
||||
flat.append(flat_entry)
|
||||
return flat
|
||||
|
||||
|
||||
def user_dn(conn, sam, search_base):
|
||||
if "\\" in sam:
|
||||
sam = sam.split("\\", 1)[1]
|
||||
flat = search_paging(conn, f"(&(objectClass=user)(sAMAccountName={escape_filter_chars(sam)}))",
|
||||
search_base, page_size=1, size_limit=1)
|
||||
if not flat:
|
||||
raise Exception(f"Could not find user '{sam}'")
|
||||
return flat[0]["dn"]
|
||||
|
||||
|
||||
def computer_dn(conn, name, search_base):
|
||||
flat = search_paging(conn, f"(&(objectClass=user)(objectCategory=computer)(name={escape_filter_chars(name)}))",
|
||||
search_base, page_size=1, size_limit=1)
|
||||
if not flat:
|
||||
raise Exception(f"Could not find computer '{name}'")
|
||||
return flat[0]["dn"]
|
||||
|
||||
|
||||
def group_dn(conn, name, search_base):
|
||||
flat = search_paging(conn, f"(&(objectClass=group)(cn={escape_filter_chars(name)}))",
|
||||
search_base, page_size=1, size_limit=1)
|
||||
if not flat:
|
||||
raise Exception(f"Could not find group '{name}'")
|
||||
return flat[0]["dn"]
|
||||
|
||||
|
||||
def modify(conn, dn, modification):
|
||||
if not conn.modify(dn, modification):
|
||||
raise Exception(f"Failed to modify {dn}: {json.dumps(conn.result, default=str)}")
|
||||
|
||||
|
||||
def first_uac(values):
|
||||
if isinstance(values, list):
|
||||
return values[0] if values else 0
|
||||
return values or 0
|
||||
|
||||
|
||||
def uac_fields(value):
|
||||
value = int(value)
|
||||
flags = {
|
||||
"SCRIPT": 0x1, "ACCOUNTDISABLE": 0x2, "HOMEDIR_REQUIRED": 0x8, "LOCKOUT": 0x10,
|
||||
"PASSWD_NOTREQD": 0x20, "PASSWD_CANT_CHANGE": 0x40, "ENCRYPTED_TEXT_PWD_ALLOWED": 0x80,
|
||||
"TEMP_DUPLICATE_ACCOUNT": 0x100, "NORMAL_ACCOUNT": 0x200, "INTERDOMAIN_TRUST_ACCOUNT": 0x800,
|
||||
"WORKSTATION_TRUST_ACCOUNT": 0x1000, "SERVER_TRUST_ACCOUNT": 0x2000, "DONT_EXPIRE_PASSWORD": 0x10000,
|
||||
"MNS_LOGON_ACCOUNT": 0x20000, "SMARTCARD_REQUIRED": 0x40000, "TRUSTED_FOR_DELEGATION": 0x80000,
|
||||
"NOT_DELEGATED": 0x100000, "USE_DES_KEY_ONLY": 0x200000, "DONT_REQ_PREAUTH": 0x400000,
|
||||
"PASSWORD_EXPIRED": 0x800000, "TRUSTED_TO_AUTH_FOR_DELEGATION": 0x1000000, "PARTIAL_SECRETS_ACCOUNT": 0x4000000,
|
||||
}
|
||||
return {name: bool(value & mask) for name, mask in flags.items()}
|
||||
|
||||
|
||||
def current_uac(conn, search_base):
|
||||
sam = I.get("username") or I.get("sAMAccountName")
|
||||
query = "(&(objectClass=User)(objectCategory=person))"
|
||||
if sam:
|
||||
query = f"(&(objectClass=User)(objectCategory=person)(sAMAccountName={escape_filter_chars(sam)}))"
|
||||
flat = search_paging(conn, query, search_base, attributes=["userAccountControl"], size_limit=1, page_size=1)
|
||||
if flat and flat[0].get("userAccountControl"):
|
||||
return first_uac(flat[0]["userAccountControl"])
|
||||
return 512
|
||||
|
||||
|
||||
|
||||
def run():
|
||||
conn, c = connect()
|
||||
name = I["user-name"]
|
||||
dn = user_dn(conn, name, I.get("base-dn") or base_dn_arg())
|
||||
cn = dn.split(",OU=", 1)[0].split(",DC=", 1)[0].replace("\\", "")
|
||||
dn_clean = dn.replace("\\", "")
|
||||
if not conn.modify_dn(dn_clean, cn, new_superior=I["full-superior-dn"]):
|
||||
fail("Failed to move user: " + json.dumps(conn.result, default=str))
|
||||
out({"moved": name, "to": I["full-superior-dn"]})
|
||||
|
||||
|
||||
try:
|
||||
run()
|
||||
except SystemExit:
|
||||
raise
|
||||
except LDAPException as e:
|
||||
fail("LDAP error: " + str(e))
|
||||
except Exception as e:
|
||||
fail(str(e))
|
||||
finally:
|
||||
try:
|
||||
if CONN is not None:
|
||||
CONN.unbind()
|
||||
except Exception:
|
||||
pass
|
||||
@@ -0,0 +1,237 @@
|
||||
import json, os, sys
|
||||
|
||||
S = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||
I = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||
|
||||
|
||||
def out(value):
|
||||
print(json.dumps(value, default=str))
|
||||
|
||||
|
||||
def fail(message, **extra):
|
||||
payload = {"error": message}
|
||||
payload.update(extra)
|
||||
print(json.dumps(payload, default=str))
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
try:
|
||||
import ssl
|
||||
from ldap3 import (
|
||||
Server, Connection, Tls, NTLM, SUBTREE, BASE, ALL_ATTRIBUTES,
|
||||
MODIFY_REPLACE, AUTO_BIND_NO_TLS, AUTO_BIND_TLS_BEFORE_BIND,
|
||||
)
|
||||
from ldap3.core.exceptions import LDAPException
|
||||
from ldap3.extend import microsoft
|
||||
from ldap3.utils.conv import escape_filter_chars
|
||||
except ImportError as _e:
|
||||
print(json.dumps({
|
||||
"error": "The 'ldap3' Python library is required for the Active Directory integration. "
|
||||
"Install it on the execution host (engine): pip install ldap3",
|
||||
"detail": str(_e),
|
||||
}))
|
||||
sys.exit(1)
|
||||
|
||||
SSL_VERSIONS = {
|
||||
"None": None,
|
||||
"TLS": ssl.PROTOCOL_TLS,
|
||||
"TLSv1": ssl.PROTOCOL_TLSv1,
|
||||
"TLSv1_1": ssl.PROTOCOL_TLSv1_1,
|
||||
"TLSv1_2": ssl.PROTOCOL_TLSv1_2,
|
||||
"TLS_CLIENT": ssl.PROTOCOL_TLS_CLIENT,
|
||||
}
|
||||
CIPHERS = (
|
||||
"@SECLEVEL=1:ECDHE+AESGCM:ECDHE+CHACHA20:DHE+AESGCM:DHE+CHACHA20:ECDH+AESGCM:"
|
||||
"DH+AESGCM:ECDH+AES:DH+AES:RSA+AESGCM:RSA+AES:!aNULL:!eNULL:!MD5:!DSS"
|
||||
)
|
||||
|
||||
CONN = None
|
||||
|
||||
|
||||
def _bool(v):
|
||||
if isinstance(v, bool):
|
||||
return v
|
||||
return str(v).lower() in ("1", "true", "yes")
|
||||
|
||||
|
||||
def _list(v):
|
||||
if isinstance(v, list):
|
||||
return v
|
||||
return [x.strip() for x in str(v).split(",") if x.strip()]
|
||||
|
||||
|
||||
def _cfg():
|
||||
return {
|
||||
"host": S.get("server_ip") or S.get("host") or "",
|
||||
"port": int(S["port"]) if S.get("port") else None,
|
||||
"user": S.get("username") or "",
|
||||
"password": S.get("password") or "",
|
||||
"base_dn": S.get("base_dn") or "",
|
||||
"secure": S.get("secure_connection") or "SSL",
|
||||
"ssl_version": S.get("ssl_version") or "None",
|
||||
"ntlm": _bool(S.get("ntlm")),
|
||||
"unsecure": _bool(S.get("unsecure")),
|
||||
"page_size": int(S["page_size"]) if S.get("page_size") else 500,
|
||||
}
|
||||
|
||||
|
||||
def _tls(unsecure, ssl_version):
|
||||
version = SSL_VERSIONS.get(ssl_version)
|
||||
if unsecure:
|
||||
return Tls(validate=ssl.CERT_NONE, ca_certs_file=None, ciphers=CIPHERS, version=version)
|
||||
return Tls(validate=ssl.CERT_REQUIRED, ca_certs_file=os.environ.get("SSL_CERT_FILE"), version=version)
|
||||
|
||||
|
||||
def _server(c):
|
||||
secure = c["secure"]
|
||||
if secure in ("SSL", "TLS"):
|
||||
return Server(host=c["host"], port=c["port"], use_ssl=True, tls=_tls(c["unsecure"], c["ssl_version"]), connect_timeout=120)
|
||||
if secure == "Start TLS":
|
||||
return Server(host=c["host"], port=c["port"], use_ssl=False, tls=_tls(c["unsecure"], c["ssl_version"]), connect_timeout=120)
|
||||
return Server(host=c["host"], port=c["port"], connect_timeout=120)
|
||||
|
||||
|
||||
def _auto_bind(secure):
|
||||
return AUTO_BIND_TLS_BEFORE_BIND if secure == "Start TLS" else AUTO_BIND_NO_TLS
|
||||
|
||||
|
||||
def connect():
|
||||
global CONN
|
||||
c = _cfg()
|
||||
server = _server(c)
|
||||
if c["ntlm"]:
|
||||
domain_user = c["user"] if "\\" in c["user"] else c["host"] + "\\" + c["user"]
|
||||
CONN = Connection(server, domain_user, password=c["password"], authentication=NTLM, auto_bind=_auto_bind(c["secure"]))
|
||||
else:
|
||||
CONN = Connection(server, user=c["user"], password=c["password"], auto_bind=_auto_bind(c["secure"]))
|
||||
return CONN, c
|
||||
|
||||
|
||||
def base_dn_arg():
|
||||
return I.get("base-dn") or I.get("base_dn") or _cfg()["base_dn"]
|
||||
|
||||
|
||||
def search_paging(conn, search_filter, search_base, attributes=None, page_size=100, size_limit=0, time_limit=0):
|
||||
entries = []
|
||||
cookie = None
|
||||
total = 0
|
||||
left = size_limit
|
||||
while True:
|
||||
ps = page_size
|
||||
if 0 < left < page_size:
|
||||
ps = left
|
||||
conn.search(search_base, search_filter, search_scope=SUBTREE, attributes=attributes,
|
||||
paged_size=ps, paged_cookie=cookie, time_limit=time_limit)
|
||||
left -= len(conn.entries)
|
||||
total += len(conn.entries)
|
||||
try:
|
||||
cookie = conn.result["controls"]["1.2.840.113556.1.4.319"]["value"]["cookie"]
|
||||
except (KeyError, TypeError):
|
||||
cookie = None
|
||||
entries.extend(conn.entries)
|
||||
if (size_limit and size_limit <= total) or not cookie:
|
||||
break
|
||||
flat = []
|
||||
for entry in entries:
|
||||
obj = json.loads(entry.entry_to_json())
|
||||
flat_entry = {"dn": obj["dn"]}
|
||||
for attr in obj.get("attributes", {}):
|
||||
flat_entry[attr] = obj["attributes"][attr]
|
||||
flat.append(flat_entry)
|
||||
return flat
|
||||
|
||||
|
||||
def user_dn(conn, sam, search_base):
|
||||
if "\\" in sam:
|
||||
sam = sam.split("\\", 1)[1]
|
||||
flat = search_paging(conn, f"(&(objectClass=user)(sAMAccountName={escape_filter_chars(sam)}))",
|
||||
search_base, page_size=1, size_limit=1)
|
||||
if not flat:
|
||||
raise Exception(f"Could not find user '{sam}'")
|
||||
return flat[0]["dn"]
|
||||
|
||||
|
||||
def computer_dn(conn, name, search_base):
|
||||
flat = search_paging(conn, f"(&(objectClass=user)(objectCategory=computer)(name={escape_filter_chars(name)}))",
|
||||
search_base, page_size=1, size_limit=1)
|
||||
if not flat:
|
||||
raise Exception(f"Could not find computer '{name}'")
|
||||
return flat[0]["dn"]
|
||||
|
||||
|
||||
def group_dn(conn, name, search_base):
|
||||
flat = search_paging(conn, f"(&(objectClass=group)(cn={escape_filter_chars(name)}))",
|
||||
search_base, page_size=1, size_limit=1)
|
||||
if not flat:
|
||||
raise Exception(f"Could not find group '{name}'")
|
||||
return flat[0]["dn"]
|
||||
|
||||
|
||||
def modify(conn, dn, modification):
|
||||
if not conn.modify(dn, modification):
|
||||
raise Exception(f"Failed to modify {dn}: {json.dumps(conn.result, default=str)}")
|
||||
|
||||
|
||||
def first_uac(values):
|
||||
if isinstance(values, list):
|
||||
return values[0] if values else 0
|
||||
return values or 0
|
||||
|
||||
|
||||
def uac_fields(value):
|
||||
value = int(value)
|
||||
flags = {
|
||||
"SCRIPT": 0x1, "ACCOUNTDISABLE": 0x2, "HOMEDIR_REQUIRED": 0x8, "LOCKOUT": 0x10,
|
||||
"PASSWD_NOTREQD": 0x20, "PASSWD_CANT_CHANGE": 0x40, "ENCRYPTED_TEXT_PWD_ALLOWED": 0x80,
|
||||
"TEMP_DUPLICATE_ACCOUNT": 0x100, "NORMAL_ACCOUNT": 0x200, "INTERDOMAIN_TRUST_ACCOUNT": 0x800,
|
||||
"WORKSTATION_TRUST_ACCOUNT": 0x1000, "SERVER_TRUST_ACCOUNT": 0x2000, "DONT_EXPIRE_PASSWORD": 0x10000,
|
||||
"MNS_LOGON_ACCOUNT": 0x20000, "SMARTCARD_REQUIRED": 0x40000, "TRUSTED_FOR_DELEGATION": 0x80000,
|
||||
"NOT_DELEGATED": 0x100000, "USE_DES_KEY_ONLY": 0x200000, "DONT_REQ_PREAUTH": 0x400000,
|
||||
"PASSWORD_EXPIRED": 0x800000, "TRUSTED_TO_AUTH_FOR_DELEGATION": 0x1000000, "PARTIAL_SECRETS_ACCOUNT": 0x4000000,
|
||||
}
|
||||
return {name: bool(value & mask) for name, mask in flags.items()}
|
||||
|
||||
|
||||
def current_uac(conn, search_base):
|
||||
sam = I.get("username") or I.get("sAMAccountName")
|
||||
query = "(&(objectClass=User)(objectCategory=person))"
|
||||
if sam:
|
||||
query = f"(&(objectClass=User)(objectCategory=person)(sAMAccountName={escape_filter_chars(sam)}))"
|
||||
flat = search_paging(conn, query, search_base, attributes=["userAccountControl"], size_limit=1, page_size=1)
|
||||
if flat and flat[0].get("userAccountControl"):
|
||||
return first_uac(flat[0]["userAccountControl"])
|
||||
return 512
|
||||
|
||||
|
||||
|
||||
def run():
|
||||
conn, c = connect()
|
||||
sb = base_dn_arg()
|
||||
if I.get("username") and I.get("computer-name"):
|
||||
fail("Provide either username or computer-name, not both")
|
||||
if I.get("username"):
|
||||
member_dn = user_dn(conn, I["username"], sb)
|
||||
elif I.get("computer-name"):
|
||||
member_dn = computer_dn(conn, I["computer-name"], sb)
|
||||
else:
|
||||
fail("Provide username or computer-name")
|
||||
grp = group_dn(conn, I["group-cn"], sb)
|
||||
if not microsoft.removeMembersFromGroups.ad_remove_members_from_groups(conn, [member_dn], [grp], True):
|
||||
fail("Failed to remove member from group " + I["group-cn"])
|
||||
out({"removed": member_dn, "group": I["group-cn"]})
|
||||
|
||||
|
||||
try:
|
||||
run()
|
||||
except SystemExit:
|
||||
raise
|
||||
except LDAPException as e:
|
||||
fail("LDAP error: " + str(e))
|
||||
except Exception as e:
|
||||
fail(str(e))
|
||||
finally:
|
||||
try:
|
||||
if CONN is not None:
|
||||
CONN.unbind()
|
||||
except Exception:
|
||||
pass
|
||||
@@ -0,0 +1,239 @@
|
||||
import json, os, sys
|
||||
|
||||
S = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||
I = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||
|
||||
|
||||
def out(value):
|
||||
print(json.dumps(value, default=str))
|
||||
|
||||
|
||||
def fail(message, **extra):
|
||||
payload = {"error": message}
|
||||
payload.update(extra)
|
||||
print(json.dumps(payload, default=str))
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
try:
|
||||
import ssl
|
||||
from ldap3 import (
|
||||
Server, Connection, Tls, NTLM, SUBTREE, BASE, ALL_ATTRIBUTES,
|
||||
MODIFY_REPLACE, AUTO_BIND_NO_TLS, AUTO_BIND_TLS_BEFORE_BIND,
|
||||
)
|
||||
from ldap3.core.exceptions import LDAPException
|
||||
from ldap3.extend import microsoft
|
||||
from ldap3.utils.conv import escape_filter_chars
|
||||
except ImportError as _e:
|
||||
print(json.dumps({
|
||||
"error": "The 'ldap3' Python library is required for the Active Directory integration. "
|
||||
"Install it on the execution host (engine): pip install ldap3",
|
||||
"detail": str(_e),
|
||||
}))
|
||||
sys.exit(1)
|
||||
|
||||
SSL_VERSIONS = {
|
||||
"None": None,
|
||||
"TLS": ssl.PROTOCOL_TLS,
|
||||
"TLSv1": ssl.PROTOCOL_TLSv1,
|
||||
"TLSv1_1": ssl.PROTOCOL_TLSv1_1,
|
||||
"TLSv1_2": ssl.PROTOCOL_TLSv1_2,
|
||||
"TLS_CLIENT": ssl.PROTOCOL_TLS_CLIENT,
|
||||
}
|
||||
CIPHERS = (
|
||||
"@SECLEVEL=1:ECDHE+AESGCM:ECDHE+CHACHA20:DHE+AESGCM:DHE+CHACHA20:ECDH+AESGCM:"
|
||||
"DH+AESGCM:ECDH+AES:DH+AES:RSA+AESGCM:RSA+AES:!aNULL:!eNULL:!MD5:!DSS"
|
||||
)
|
||||
|
||||
CONN = None
|
||||
|
||||
|
||||
def _bool(v):
|
||||
if isinstance(v, bool):
|
||||
return v
|
||||
return str(v).lower() in ("1", "true", "yes")
|
||||
|
||||
|
||||
def _list(v):
|
||||
if isinstance(v, list):
|
||||
return v
|
||||
return [x.strip() for x in str(v).split(",") if x.strip()]
|
||||
|
||||
|
||||
def _cfg():
|
||||
return {
|
||||
"host": S.get("server_ip") or S.get("host") or "",
|
||||
"port": int(S["port"]) if S.get("port") else None,
|
||||
"user": S.get("username") or "",
|
||||
"password": S.get("password") or "",
|
||||
"base_dn": S.get("base_dn") or "",
|
||||
"secure": S.get("secure_connection") or "SSL",
|
||||
"ssl_version": S.get("ssl_version") or "None",
|
||||
"ntlm": _bool(S.get("ntlm")),
|
||||
"unsecure": _bool(S.get("unsecure")),
|
||||
"page_size": int(S["page_size"]) if S.get("page_size") else 500,
|
||||
}
|
||||
|
||||
|
||||
def _tls(unsecure, ssl_version):
|
||||
version = SSL_VERSIONS.get(ssl_version)
|
||||
if unsecure:
|
||||
return Tls(validate=ssl.CERT_NONE, ca_certs_file=None, ciphers=CIPHERS, version=version)
|
||||
return Tls(validate=ssl.CERT_REQUIRED, ca_certs_file=os.environ.get("SSL_CERT_FILE"), version=version)
|
||||
|
||||
|
||||
def _server(c):
|
||||
secure = c["secure"]
|
||||
if secure in ("SSL", "TLS"):
|
||||
return Server(host=c["host"], port=c["port"], use_ssl=True, tls=_tls(c["unsecure"], c["ssl_version"]), connect_timeout=120)
|
||||
if secure == "Start TLS":
|
||||
return Server(host=c["host"], port=c["port"], use_ssl=False, tls=_tls(c["unsecure"], c["ssl_version"]), connect_timeout=120)
|
||||
return Server(host=c["host"], port=c["port"], connect_timeout=120)
|
||||
|
||||
|
||||
def _auto_bind(secure):
|
||||
return AUTO_BIND_TLS_BEFORE_BIND if secure == "Start TLS" else AUTO_BIND_NO_TLS
|
||||
|
||||
|
||||
def connect():
|
||||
global CONN
|
||||
c = _cfg()
|
||||
server = _server(c)
|
||||
if c["ntlm"]:
|
||||
domain_user = c["user"] if "\\" in c["user"] else c["host"] + "\\" + c["user"]
|
||||
CONN = Connection(server, domain_user, password=c["password"], authentication=NTLM, auto_bind=_auto_bind(c["secure"]))
|
||||
else:
|
||||
CONN = Connection(server, user=c["user"], password=c["password"], auto_bind=_auto_bind(c["secure"]))
|
||||
return CONN, c
|
||||
|
||||
|
||||
def base_dn_arg():
|
||||
return I.get("base-dn") or I.get("base_dn") or _cfg()["base_dn"]
|
||||
|
||||
|
||||
def search_paging(conn, search_filter, search_base, attributes=None, page_size=100, size_limit=0, time_limit=0):
|
||||
entries = []
|
||||
cookie = None
|
||||
total = 0
|
||||
left = size_limit
|
||||
while True:
|
||||
ps = page_size
|
||||
if 0 < left < page_size:
|
||||
ps = left
|
||||
conn.search(search_base, search_filter, search_scope=SUBTREE, attributes=attributes,
|
||||
paged_size=ps, paged_cookie=cookie, time_limit=time_limit)
|
||||
left -= len(conn.entries)
|
||||
total += len(conn.entries)
|
||||
try:
|
||||
cookie = conn.result["controls"]["1.2.840.113556.1.4.319"]["value"]["cookie"]
|
||||
except (KeyError, TypeError):
|
||||
cookie = None
|
||||
entries.extend(conn.entries)
|
||||
if (size_limit and size_limit <= total) or not cookie:
|
||||
break
|
||||
flat = []
|
||||
for entry in entries:
|
||||
obj = json.loads(entry.entry_to_json())
|
||||
flat_entry = {"dn": obj["dn"]}
|
||||
for attr in obj.get("attributes", {}):
|
||||
flat_entry[attr] = obj["attributes"][attr]
|
||||
flat.append(flat_entry)
|
||||
return flat
|
||||
|
||||
|
||||
def user_dn(conn, sam, search_base):
|
||||
if "\\" in sam:
|
||||
sam = sam.split("\\", 1)[1]
|
||||
flat = search_paging(conn, f"(&(objectClass=user)(sAMAccountName={escape_filter_chars(sam)}))",
|
||||
search_base, page_size=1, size_limit=1)
|
||||
if not flat:
|
||||
raise Exception(f"Could not find user '{sam}'")
|
||||
return flat[0]["dn"]
|
||||
|
||||
|
||||
def computer_dn(conn, name, search_base):
|
||||
flat = search_paging(conn, f"(&(objectClass=user)(objectCategory=computer)(name={escape_filter_chars(name)}))",
|
||||
search_base, page_size=1, size_limit=1)
|
||||
if not flat:
|
||||
raise Exception(f"Could not find computer '{name}'")
|
||||
return flat[0]["dn"]
|
||||
|
||||
|
||||
def group_dn(conn, name, search_base):
|
||||
flat = search_paging(conn, f"(&(objectClass=group)(cn={escape_filter_chars(name)}))",
|
||||
search_base, page_size=1, size_limit=1)
|
||||
if not flat:
|
||||
raise Exception(f"Could not find group '{name}'")
|
||||
return flat[0]["dn"]
|
||||
|
||||
|
||||
def modify(conn, dn, modification):
|
||||
if not conn.modify(dn, modification):
|
||||
raise Exception(f"Failed to modify {dn}: {json.dumps(conn.result, default=str)}")
|
||||
|
||||
|
||||
def first_uac(values):
|
||||
if isinstance(values, list):
|
||||
return values[0] if values else 0
|
||||
return values or 0
|
||||
|
||||
|
||||
def uac_fields(value):
|
||||
value = int(value)
|
||||
flags = {
|
||||
"SCRIPT": 0x1, "ACCOUNTDISABLE": 0x2, "HOMEDIR_REQUIRED": 0x8, "LOCKOUT": 0x10,
|
||||
"PASSWD_NOTREQD": 0x20, "PASSWD_CANT_CHANGE": 0x40, "ENCRYPTED_TEXT_PWD_ALLOWED": 0x80,
|
||||
"TEMP_DUPLICATE_ACCOUNT": 0x100, "NORMAL_ACCOUNT": 0x200, "INTERDOMAIN_TRUST_ACCOUNT": 0x800,
|
||||
"WORKSTATION_TRUST_ACCOUNT": 0x1000, "SERVER_TRUST_ACCOUNT": 0x2000, "DONT_EXPIRE_PASSWORD": 0x10000,
|
||||
"MNS_LOGON_ACCOUNT": 0x20000, "SMARTCARD_REQUIRED": 0x40000, "TRUSTED_FOR_DELEGATION": 0x80000,
|
||||
"NOT_DELEGATED": 0x100000, "USE_DES_KEY_ONLY": 0x200000, "DONT_REQ_PREAUTH": 0x400000,
|
||||
"PASSWORD_EXPIRED": 0x800000, "TRUSTED_TO_AUTH_FOR_DELEGATION": 0x1000000, "PARTIAL_SECRETS_ACCOUNT": 0x4000000,
|
||||
}
|
||||
return {name: bool(value & mask) for name, mask in flags.items()}
|
||||
|
||||
|
||||
def current_uac(conn, search_base):
|
||||
sam = I.get("username") or I.get("sAMAccountName")
|
||||
query = "(&(objectClass=User)(objectCategory=person))"
|
||||
if sam:
|
||||
query = f"(&(objectClass=User)(objectCategory=person)(sAMAccountName={escape_filter_chars(sam)}))"
|
||||
flat = search_paging(conn, query, search_base, attributes=["userAccountControl"], size_limit=1, page_size=1)
|
||||
if flat and flat[0].get("userAccountControl"):
|
||||
return first_uac(flat[0]["userAccountControl"])
|
||||
return 512
|
||||
|
||||
|
||||
|
||||
def run():
|
||||
conn, c = connect()
|
||||
search_filter = I["filter"]
|
||||
for a, b in {"\(": "\28", "\)": "\29", "\*": "\2a", "\/": "\2f", "\\\\": "\5c"}.items():
|
||||
search_filter = search_filter.replace(a, b)
|
||||
search_base = base_dn_arg()
|
||||
attributes = I.get("attributes")
|
||||
if attributes:
|
||||
attributes = ALL_ATTRIBUTES if attributes == "ALL" else [a.strip() for a in attributes.split(",")]
|
||||
size_limit = int(I.get("size-limit") or 50)
|
||||
time_limit = int(I.get("time-limit") or 0)
|
||||
page_size = int(I.get("page-size") or c["page_size"])
|
||||
if I.get("page-size"):
|
||||
size_limit = page_size
|
||||
flat = search_paging(conn, search_filter, search_base, attributes=attributes,
|
||||
page_size=page_size, size_limit=size_limit, time_limit=time_limit)
|
||||
out({"results": flat, "count": len(flat)})
|
||||
|
||||
|
||||
try:
|
||||
run()
|
||||
except SystemExit:
|
||||
raise
|
||||
except LDAPException as e:
|
||||
fail("LDAP error: " + str(e))
|
||||
except Exception as e:
|
||||
fail(str(e))
|
||||
finally:
|
||||
try:
|
||||
if CONN is not None:
|
||||
CONN.unbind()
|
||||
except Exception:
|
||||
pass
|
||||
@@ -0,0 +1,230 @@
|
||||
import json, os, sys
|
||||
|
||||
S = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||
I = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||
|
||||
|
||||
def out(value):
|
||||
print(json.dumps(value, default=str))
|
||||
|
||||
|
||||
def fail(message, **extra):
|
||||
payload = {"error": message}
|
||||
payload.update(extra)
|
||||
print(json.dumps(payload, default=str))
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
try:
|
||||
import ssl
|
||||
from ldap3 import (
|
||||
Server, Connection, Tls, NTLM, SUBTREE, BASE, ALL_ATTRIBUTES,
|
||||
MODIFY_REPLACE, AUTO_BIND_NO_TLS, AUTO_BIND_TLS_BEFORE_BIND,
|
||||
)
|
||||
from ldap3.core.exceptions import LDAPException
|
||||
from ldap3.extend import microsoft
|
||||
from ldap3.utils.conv import escape_filter_chars
|
||||
except ImportError as _e:
|
||||
print(json.dumps({
|
||||
"error": "The 'ldap3' Python library is required for the Active Directory integration. "
|
||||
"Install it on the execution host (engine): pip install ldap3",
|
||||
"detail": str(_e),
|
||||
}))
|
||||
sys.exit(1)
|
||||
|
||||
SSL_VERSIONS = {
|
||||
"None": None,
|
||||
"TLS": ssl.PROTOCOL_TLS,
|
||||
"TLSv1": ssl.PROTOCOL_TLSv1,
|
||||
"TLSv1_1": ssl.PROTOCOL_TLSv1_1,
|
||||
"TLSv1_2": ssl.PROTOCOL_TLSv1_2,
|
||||
"TLS_CLIENT": ssl.PROTOCOL_TLS_CLIENT,
|
||||
}
|
||||
CIPHERS = (
|
||||
"@SECLEVEL=1:ECDHE+AESGCM:ECDHE+CHACHA20:DHE+AESGCM:DHE+CHACHA20:ECDH+AESGCM:"
|
||||
"DH+AESGCM:ECDH+AES:DH+AES:RSA+AESGCM:RSA+AES:!aNULL:!eNULL:!MD5:!DSS"
|
||||
)
|
||||
|
||||
CONN = None
|
||||
|
||||
|
||||
def _bool(v):
|
||||
if isinstance(v, bool):
|
||||
return v
|
||||
return str(v).lower() in ("1", "true", "yes")
|
||||
|
||||
|
||||
def _list(v):
|
||||
if isinstance(v, list):
|
||||
return v
|
||||
return [x.strip() for x in str(v).split(",") if x.strip()]
|
||||
|
||||
|
||||
def _cfg():
|
||||
return {
|
||||
"host": S.get("server_ip") or S.get("host") or "",
|
||||
"port": int(S["port"]) if S.get("port") else None,
|
||||
"user": S.get("username") or "",
|
||||
"password": S.get("password") or "",
|
||||
"base_dn": S.get("base_dn") or "",
|
||||
"secure": S.get("secure_connection") or "SSL",
|
||||
"ssl_version": S.get("ssl_version") or "None",
|
||||
"ntlm": _bool(S.get("ntlm")),
|
||||
"unsecure": _bool(S.get("unsecure")),
|
||||
"page_size": int(S["page_size"]) if S.get("page_size") else 500,
|
||||
}
|
||||
|
||||
|
||||
def _tls(unsecure, ssl_version):
|
||||
version = SSL_VERSIONS.get(ssl_version)
|
||||
if unsecure:
|
||||
return Tls(validate=ssl.CERT_NONE, ca_certs_file=None, ciphers=CIPHERS, version=version)
|
||||
return Tls(validate=ssl.CERT_REQUIRED, ca_certs_file=os.environ.get("SSL_CERT_FILE"), version=version)
|
||||
|
||||
|
||||
def _server(c):
|
||||
secure = c["secure"]
|
||||
if secure in ("SSL", "TLS"):
|
||||
return Server(host=c["host"], port=c["port"], use_ssl=True, tls=_tls(c["unsecure"], c["ssl_version"]), connect_timeout=120)
|
||||
if secure == "Start TLS":
|
||||
return Server(host=c["host"], port=c["port"], use_ssl=False, tls=_tls(c["unsecure"], c["ssl_version"]), connect_timeout=120)
|
||||
return Server(host=c["host"], port=c["port"], connect_timeout=120)
|
||||
|
||||
|
||||
def _auto_bind(secure):
|
||||
return AUTO_BIND_TLS_BEFORE_BIND if secure == "Start TLS" else AUTO_BIND_NO_TLS
|
||||
|
||||
|
||||
def connect():
|
||||
global CONN
|
||||
c = _cfg()
|
||||
server = _server(c)
|
||||
if c["ntlm"]:
|
||||
domain_user = c["user"] if "\\" in c["user"] else c["host"] + "\\" + c["user"]
|
||||
CONN = Connection(server, domain_user, password=c["password"], authentication=NTLM, auto_bind=_auto_bind(c["secure"]))
|
||||
else:
|
||||
CONN = Connection(server, user=c["user"], password=c["password"], auto_bind=_auto_bind(c["secure"]))
|
||||
return CONN, c
|
||||
|
||||
|
||||
def base_dn_arg():
|
||||
return I.get("base-dn") or I.get("base_dn") or _cfg()["base_dn"]
|
||||
|
||||
|
||||
def search_paging(conn, search_filter, search_base, attributes=None, page_size=100, size_limit=0, time_limit=0):
|
||||
entries = []
|
||||
cookie = None
|
||||
total = 0
|
||||
left = size_limit
|
||||
while True:
|
||||
ps = page_size
|
||||
if 0 < left < page_size:
|
||||
ps = left
|
||||
conn.search(search_base, search_filter, search_scope=SUBTREE, attributes=attributes,
|
||||
paged_size=ps, paged_cookie=cookie, time_limit=time_limit)
|
||||
left -= len(conn.entries)
|
||||
total += len(conn.entries)
|
||||
try:
|
||||
cookie = conn.result["controls"]["1.2.840.113556.1.4.319"]["value"]["cookie"]
|
||||
except (KeyError, TypeError):
|
||||
cookie = None
|
||||
entries.extend(conn.entries)
|
||||
if (size_limit and size_limit <= total) or not cookie:
|
||||
break
|
||||
flat = []
|
||||
for entry in entries:
|
||||
obj = json.loads(entry.entry_to_json())
|
||||
flat_entry = {"dn": obj["dn"]}
|
||||
for attr in obj.get("attributes", {}):
|
||||
flat_entry[attr] = obj["attributes"][attr]
|
||||
flat.append(flat_entry)
|
||||
return flat
|
||||
|
||||
|
||||
def user_dn(conn, sam, search_base):
|
||||
if "\\" in sam:
|
||||
sam = sam.split("\\", 1)[1]
|
||||
flat = search_paging(conn, f"(&(objectClass=user)(sAMAccountName={escape_filter_chars(sam)}))",
|
||||
search_base, page_size=1, size_limit=1)
|
||||
if not flat:
|
||||
raise Exception(f"Could not find user '{sam}'")
|
||||
return flat[0]["dn"]
|
||||
|
||||
|
||||
def computer_dn(conn, name, search_base):
|
||||
flat = search_paging(conn, f"(&(objectClass=user)(objectCategory=computer)(name={escape_filter_chars(name)}))",
|
||||
search_base, page_size=1, size_limit=1)
|
||||
if not flat:
|
||||
raise Exception(f"Could not find computer '{name}'")
|
||||
return flat[0]["dn"]
|
||||
|
||||
|
||||
def group_dn(conn, name, search_base):
|
||||
flat = search_paging(conn, f"(&(objectClass=group)(cn={escape_filter_chars(name)}))",
|
||||
search_base, page_size=1, size_limit=1)
|
||||
if not flat:
|
||||
raise Exception(f"Could not find group '{name}'")
|
||||
return flat[0]["dn"]
|
||||
|
||||
|
||||
def modify(conn, dn, modification):
|
||||
if not conn.modify(dn, modification):
|
||||
raise Exception(f"Failed to modify {dn}: {json.dumps(conn.result, default=str)}")
|
||||
|
||||
|
||||
def first_uac(values):
|
||||
if isinstance(values, list):
|
||||
return values[0] if values else 0
|
||||
return values or 0
|
||||
|
||||
|
||||
def uac_fields(value):
|
||||
value = int(value)
|
||||
flags = {
|
||||
"SCRIPT": 0x1, "ACCOUNTDISABLE": 0x2, "HOMEDIR_REQUIRED": 0x8, "LOCKOUT": 0x10,
|
||||
"PASSWD_NOTREQD": 0x20, "PASSWD_CANT_CHANGE": 0x40, "ENCRYPTED_TEXT_PWD_ALLOWED": 0x80,
|
||||
"TEMP_DUPLICATE_ACCOUNT": 0x100, "NORMAL_ACCOUNT": 0x200, "INTERDOMAIN_TRUST_ACCOUNT": 0x800,
|
||||
"WORKSTATION_TRUST_ACCOUNT": 0x1000, "SERVER_TRUST_ACCOUNT": 0x2000, "DONT_EXPIRE_PASSWORD": 0x10000,
|
||||
"MNS_LOGON_ACCOUNT": 0x20000, "SMARTCARD_REQUIRED": 0x40000, "TRUSTED_FOR_DELEGATION": 0x80000,
|
||||
"NOT_DELEGATED": 0x100000, "USE_DES_KEY_ONLY": 0x200000, "DONT_REQ_PREAUTH": 0x400000,
|
||||
"PASSWORD_EXPIRED": 0x800000, "TRUSTED_TO_AUTH_FOR_DELEGATION": 0x1000000, "PARTIAL_SECRETS_ACCOUNT": 0x4000000,
|
||||
}
|
||||
return {name: bool(value & mask) for name, mask in flags.items()}
|
||||
|
||||
|
||||
def current_uac(conn, search_base):
|
||||
sam = I.get("username") or I.get("sAMAccountName")
|
||||
query = "(&(objectClass=User)(objectCategory=person))"
|
||||
if sam:
|
||||
query = f"(&(objectClass=User)(objectCategory=person)(sAMAccountName={escape_filter_chars(sam)}))"
|
||||
flat = search_paging(conn, query, search_base, attributes=["userAccountControl"], size_limit=1, page_size=1)
|
||||
if flat and flat[0].get("userAccountControl"):
|
||||
return first_uac(flat[0]["userAccountControl"])
|
||||
return 512
|
||||
|
||||
|
||||
|
||||
def run():
|
||||
conn, c = connect()
|
||||
if c["secure"] not in ("SSL", "TLS"):
|
||||
fail("Setting a password requires a secure connection (SSL or TLS).")
|
||||
dn = user_dn(conn, I["username"], I.get("base-dn") or base_dn_arg())
|
||||
if not conn.extend.microsoft.modify_password(dn, I["password"]):
|
||||
fail("Failed to set password: " + json.dumps(conn.result, default=str))
|
||||
out({"password_set": I["username"]})
|
||||
|
||||
|
||||
try:
|
||||
run()
|
||||
except SystemExit:
|
||||
raise
|
||||
except LDAPException as e:
|
||||
fail("LDAP error: " + str(e))
|
||||
except Exception as e:
|
||||
fail(str(e))
|
||||
finally:
|
||||
try:
|
||||
if CONN is not None:
|
||||
CONN.unbind()
|
||||
except Exception:
|
||||
pass
|
||||
@@ -0,0 +1,236 @@
|
||||
import json, os, sys
|
||||
|
||||
S = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||
I = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||
|
||||
|
||||
def out(value):
|
||||
print(json.dumps(value, default=str))
|
||||
|
||||
|
||||
def fail(message, **extra):
|
||||
payload = {"error": message}
|
||||
payload.update(extra)
|
||||
print(json.dumps(payload, default=str))
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
try:
|
||||
import ssl
|
||||
from ldap3 import (
|
||||
Server, Connection, Tls, NTLM, SUBTREE, BASE, ALL_ATTRIBUTES,
|
||||
MODIFY_REPLACE, AUTO_BIND_NO_TLS, AUTO_BIND_TLS_BEFORE_BIND,
|
||||
)
|
||||
from ldap3.core.exceptions import LDAPException
|
||||
from ldap3.extend import microsoft
|
||||
from ldap3.utils.conv import escape_filter_chars
|
||||
except ImportError as _e:
|
||||
print(json.dumps({
|
||||
"error": "The 'ldap3' Python library is required for the Active Directory integration. "
|
||||
"Install it on the execution host (engine): pip install ldap3",
|
||||
"detail": str(_e),
|
||||
}))
|
||||
sys.exit(1)
|
||||
|
||||
SSL_VERSIONS = {
|
||||
"None": None,
|
||||
"TLS": ssl.PROTOCOL_TLS,
|
||||
"TLSv1": ssl.PROTOCOL_TLSv1,
|
||||
"TLSv1_1": ssl.PROTOCOL_TLSv1_1,
|
||||
"TLSv1_2": ssl.PROTOCOL_TLSv1_2,
|
||||
"TLS_CLIENT": ssl.PROTOCOL_TLS_CLIENT,
|
||||
}
|
||||
CIPHERS = (
|
||||
"@SECLEVEL=1:ECDHE+AESGCM:ECDHE+CHACHA20:DHE+AESGCM:DHE+CHACHA20:ECDH+AESGCM:"
|
||||
"DH+AESGCM:ECDH+AES:DH+AES:RSA+AESGCM:RSA+AES:!aNULL:!eNULL:!MD5:!DSS"
|
||||
)
|
||||
|
||||
CONN = None
|
||||
|
||||
|
||||
def _bool(v):
|
||||
if isinstance(v, bool):
|
||||
return v
|
||||
return str(v).lower() in ("1", "true", "yes")
|
||||
|
||||
|
||||
def _list(v):
|
||||
if isinstance(v, list):
|
||||
return v
|
||||
return [x.strip() for x in str(v).split(",") if x.strip()]
|
||||
|
||||
|
||||
def _cfg():
|
||||
return {
|
||||
"host": S.get("server_ip") or S.get("host") or "",
|
||||
"port": int(S["port"]) if S.get("port") else None,
|
||||
"user": S.get("username") or "",
|
||||
"password": S.get("password") or "",
|
||||
"base_dn": S.get("base_dn") or "",
|
||||
"secure": S.get("secure_connection") or "SSL",
|
||||
"ssl_version": S.get("ssl_version") or "None",
|
||||
"ntlm": _bool(S.get("ntlm")),
|
||||
"unsecure": _bool(S.get("unsecure")),
|
||||
"page_size": int(S["page_size"]) if S.get("page_size") else 500,
|
||||
}
|
||||
|
||||
|
||||
def _tls(unsecure, ssl_version):
|
||||
version = SSL_VERSIONS.get(ssl_version)
|
||||
if unsecure:
|
||||
return Tls(validate=ssl.CERT_NONE, ca_certs_file=None, ciphers=CIPHERS, version=version)
|
||||
return Tls(validate=ssl.CERT_REQUIRED, ca_certs_file=os.environ.get("SSL_CERT_FILE"), version=version)
|
||||
|
||||
|
||||
def _server(c):
|
||||
secure = c["secure"]
|
||||
if secure in ("SSL", "TLS"):
|
||||
return Server(host=c["host"], port=c["port"], use_ssl=True, tls=_tls(c["unsecure"], c["ssl_version"]), connect_timeout=120)
|
||||
if secure == "Start TLS":
|
||||
return Server(host=c["host"], port=c["port"], use_ssl=False, tls=_tls(c["unsecure"], c["ssl_version"]), connect_timeout=120)
|
||||
return Server(host=c["host"], port=c["port"], connect_timeout=120)
|
||||
|
||||
|
||||
def _auto_bind(secure):
|
||||
return AUTO_BIND_TLS_BEFORE_BIND if secure == "Start TLS" else AUTO_BIND_NO_TLS
|
||||
|
||||
|
||||
def connect():
|
||||
global CONN
|
||||
c = _cfg()
|
||||
server = _server(c)
|
||||
if c["ntlm"]:
|
||||
domain_user = c["user"] if "\\" in c["user"] else c["host"] + "\\" + c["user"]
|
||||
CONN = Connection(server, domain_user, password=c["password"], authentication=NTLM, auto_bind=_auto_bind(c["secure"]))
|
||||
else:
|
||||
CONN = Connection(server, user=c["user"], password=c["password"], auto_bind=_auto_bind(c["secure"]))
|
||||
return CONN, c
|
||||
|
||||
|
||||
def base_dn_arg():
|
||||
return I.get("base-dn") or I.get("base_dn") or _cfg()["base_dn"]
|
||||
|
||||
|
||||
def search_paging(conn, search_filter, search_base, attributes=None, page_size=100, size_limit=0, time_limit=0):
|
||||
entries = []
|
||||
cookie = None
|
||||
total = 0
|
||||
left = size_limit
|
||||
while True:
|
||||
ps = page_size
|
||||
if 0 < left < page_size:
|
||||
ps = left
|
||||
conn.search(search_base, search_filter, search_scope=SUBTREE, attributes=attributes,
|
||||
paged_size=ps, paged_cookie=cookie, time_limit=time_limit)
|
||||
left -= len(conn.entries)
|
||||
total += len(conn.entries)
|
||||
try:
|
||||
cookie = conn.result["controls"]["1.2.840.113556.1.4.319"]["value"]["cookie"]
|
||||
except (KeyError, TypeError):
|
||||
cookie = None
|
||||
entries.extend(conn.entries)
|
||||
if (size_limit and size_limit <= total) or not cookie:
|
||||
break
|
||||
flat = []
|
||||
for entry in entries:
|
||||
obj = json.loads(entry.entry_to_json())
|
||||
flat_entry = {"dn": obj["dn"]}
|
||||
for attr in obj.get("attributes", {}):
|
||||
flat_entry[attr] = obj["attributes"][attr]
|
||||
flat.append(flat_entry)
|
||||
return flat
|
||||
|
||||
|
||||
def user_dn(conn, sam, search_base):
|
||||
if "\\" in sam:
|
||||
sam = sam.split("\\", 1)[1]
|
||||
flat = search_paging(conn, f"(&(objectClass=user)(sAMAccountName={escape_filter_chars(sam)}))",
|
||||
search_base, page_size=1, size_limit=1)
|
||||
if not flat:
|
||||
raise Exception(f"Could not find user '{sam}'")
|
||||
return flat[0]["dn"]
|
||||
|
||||
|
||||
def computer_dn(conn, name, search_base):
|
||||
flat = search_paging(conn, f"(&(objectClass=user)(objectCategory=computer)(name={escape_filter_chars(name)}))",
|
||||
search_base, page_size=1, size_limit=1)
|
||||
if not flat:
|
||||
raise Exception(f"Could not find computer '{name}'")
|
||||
return flat[0]["dn"]
|
||||
|
||||
|
||||
def group_dn(conn, name, search_base):
|
||||
flat = search_paging(conn, f"(&(objectClass=group)(cn={escape_filter_chars(name)}))",
|
||||
search_base, page_size=1, size_limit=1)
|
||||
if not flat:
|
||||
raise Exception(f"Could not find group '{name}'")
|
||||
return flat[0]["dn"]
|
||||
|
||||
|
||||
def modify(conn, dn, modification):
|
||||
if not conn.modify(dn, modification):
|
||||
raise Exception(f"Failed to modify {dn}: {json.dumps(conn.result, default=str)}")
|
||||
|
||||
|
||||
def first_uac(values):
|
||||
if isinstance(values, list):
|
||||
return values[0] if values else 0
|
||||
return values or 0
|
||||
|
||||
|
||||
def uac_fields(value):
|
||||
value = int(value)
|
||||
flags = {
|
||||
"SCRIPT": 0x1, "ACCOUNTDISABLE": 0x2, "HOMEDIR_REQUIRED": 0x8, "LOCKOUT": 0x10,
|
||||
"PASSWD_NOTREQD": 0x20, "PASSWD_CANT_CHANGE": 0x40, "ENCRYPTED_TEXT_PWD_ALLOWED": 0x80,
|
||||
"TEMP_DUPLICATE_ACCOUNT": 0x100, "NORMAL_ACCOUNT": 0x200, "INTERDOMAIN_TRUST_ACCOUNT": 0x800,
|
||||
"WORKSTATION_TRUST_ACCOUNT": 0x1000, "SERVER_TRUST_ACCOUNT": 0x2000, "DONT_EXPIRE_PASSWORD": 0x10000,
|
||||
"MNS_LOGON_ACCOUNT": 0x20000, "SMARTCARD_REQUIRED": 0x40000, "TRUSTED_FOR_DELEGATION": 0x80000,
|
||||
"NOT_DELEGATED": 0x100000, "USE_DES_KEY_ONLY": 0x200000, "DONT_REQ_PREAUTH": 0x400000,
|
||||
"PASSWORD_EXPIRED": 0x800000, "TRUSTED_TO_AUTH_FOR_DELEGATION": 0x1000000, "PARTIAL_SECRETS_ACCOUNT": 0x4000000,
|
||||
}
|
||||
return {name: bool(value & mask) for name, mask in flags.items()}
|
||||
|
||||
|
||||
def current_uac(conn, search_base):
|
||||
sam = I.get("username") or I.get("sAMAccountName")
|
||||
query = "(&(objectClass=User)(objectCategory=person))"
|
||||
if sam:
|
||||
query = f"(&(objectClass=User)(objectCategory=person)(sAMAccountName={escape_filter_chars(sam)}))"
|
||||
flat = search_paging(conn, query, search_base, attributes=["userAccountControl"], size_limit=1, page_size=1)
|
||||
if flat and flat[0].get("userAccountControl"):
|
||||
return first_uac(flat[0]["userAccountControl"])
|
||||
return 512
|
||||
|
||||
|
||||
|
||||
def run():
|
||||
conn, c = connect()
|
||||
server = _server(c)
|
||||
username = I["username"]
|
||||
try:
|
||||
if c["ntlm"]:
|
||||
domain_user = username if "\\" in username else c["host"] + "\\" + username
|
||||
test = Connection(server, domain_user, password=I["password"], authentication=NTLM, auto_bind=_auto_bind(c["secure"]))
|
||||
else:
|
||||
test = Connection(server, user=username, password=I["password"], auto_bind=_auto_bind(c["secure"]))
|
||||
test.unbind()
|
||||
except LDAPException:
|
||||
fail(f"Credential test for {username} failed")
|
||||
out({"valid": username})
|
||||
|
||||
|
||||
try:
|
||||
run()
|
||||
except SystemExit:
|
||||
raise
|
||||
except LDAPException as e:
|
||||
fail("LDAP error: " + str(e))
|
||||
except Exception as e:
|
||||
fail(str(e))
|
||||
finally:
|
||||
try:
|
||||
if CONN is not None:
|
||||
CONN.unbind()
|
||||
except Exception:
|
||||
pass
|
||||
@@ -0,0 +1,228 @@
|
||||
import json, os, sys
|
||||
|
||||
S = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||
I = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||
|
||||
|
||||
def out(value):
|
||||
print(json.dumps(value, default=str))
|
||||
|
||||
|
||||
def fail(message, **extra):
|
||||
payload = {"error": message}
|
||||
payload.update(extra)
|
||||
print(json.dumps(payload, default=str))
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
try:
|
||||
import ssl
|
||||
from ldap3 import (
|
||||
Server, Connection, Tls, NTLM, SUBTREE, BASE, ALL_ATTRIBUTES,
|
||||
MODIFY_REPLACE, AUTO_BIND_NO_TLS, AUTO_BIND_TLS_BEFORE_BIND,
|
||||
)
|
||||
from ldap3.core.exceptions import LDAPException
|
||||
from ldap3.extend import microsoft
|
||||
from ldap3.utils.conv import escape_filter_chars
|
||||
except ImportError as _e:
|
||||
print(json.dumps({
|
||||
"error": "The 'ldap3' Python library is required for the Active Directory integration. "
|
||||
"Install it on the execution host (engine): pip install ldap3",
|
||||
"detail": str(_e),
|
||||
}))
|
||||
sys.exit(1)
|
||||
|
||||
SSL_VERSIONS = {
|
||||
"None": None,
|
||||
"TLS": ssl.PROTOCOL_TLS,
|
||||
"TLSv1": ssl.PROTOCOL_TLSv1,
|
||||
"TLSv1_1": ssl.PROTOCOL_TLSv1_1,
|
||||
"TLSv1_2": ssl.PROTOCOL_TLSv1_2,
|
||||
"TLS_CLIENT": ssl.PROTOCOL_TLS_CLIENT,
|
||||
}
|
||||
CIPHERS = (
|
||||
"@SECLEVEL=1:ECDHE+AESGCM:ECDHE+CHACHA20:DHE+AESGCM:DHE+CHACHA20:ECDH+AESGCM:"
|
||||
"DH+AESGCM:ECDH+AES:DH+AES:RSA+AESGCM:RSA+AES:!aNULL:!eNULL:!MD5:!DSS"
|
||||
)
|
||||
|
||||
CONN = None
|
||||
|
||||
|
||||
def _bool(v):
|
||||
if isinstance(v, bool):
|
||||
return v
|
||||
return str(v).lower() in ("1", "true", "yes")
|
||||
|
||||
|
||||
def _list(v):
|
||||
if isinstance(v, list):
|
||||
return v
|
||||
return [x.strip() for x in str(v).split(",") if x.strip()]
|
||||
|
||||
|
||||
def _cfg():
|
||||
return {
|
||||
"host": S.get("server_ip") or S.get("host") or "",
|
||||
"port": int(S["port"]) if S.get("port") else None,
|
||||
"user": S.get("username") or "",
|
||||
"password": S.get("password") or "",
|
||||
"base_dn": S.get("base_dn") or "",
|
||||
"secure": S.get("secure_connection") or "SSL",
|
||||
"ssl_version": S.get("ssl_version") or "None",
|
||||
"ntlm": _bool(S.get("ntlm")),
|
||||
"unsecure": _bool(S.get("unsecure")),
|
||||
"page_size": int(S["page_size"]) if S.get("page_size") else 500,
|
||||
}
|
||||
|
||||
|
||||
def _tls(unsecure, ssl_version):
|
||||
version = SSL_VERSIONS.get(ssl_version)
|
||||
if unsecure:
|
||||
return Tls(validate=ssl.CERT_NONE, ca_certs_file=None, ciphers=CIPHERS, version=version)
|
||||
return Tls(validate=ssl.CERT_REQUIRED, ca_certs_file=os.environ.get("SSL_CERT_FILE"), version=version)
|
||||
|
||||
|
||||
def _server(c):
|
||||
secure = c["secure"]
|
||||
if secure in ("SSL", "TLS"):
|
||||
return Server(host=c["host"], port=c["port"], use_ssl=True, tls=_tls(c["unsecure"], c["ssl_version"]), connect_timeout=120)
|
||||
if secure == "Start TLS":
|
||||
return Server(host=c["host"], port=c["port"], use_ssl=False, tls=_tls(c["unsecure"], c["ssl_version"]), connect_timeout=120)
|
||||
return Server(host=c["host"], port=c["port"], connect_timeout=120)
|
||||
|
||||
|
||||
def _auto_bind(secure):
|
||||
return AUTO_BIND_TLS_BEFORE_BIND if secure == "Start TLS" else AUTO_BIND_NO_TLS
|
||||
|
||||
|
||||
def connect():
|
||||
global CONN
|
||||
c = _cfg()
|
||||
server = _server(c)
|
||||
if c["ntlm"]:
|
||||
domain_user = c["user"] if "\\" in c["user"] else c["host"] + "\\" + c["user"]
|
||||
CONN = Connection(server, domain_user, password=c["password"], authentication=NTLM, auto_bind=_auto_bind(c["secure"]))
|
||||
else:
|
||||
CONN = Connection(server, user=c["user"], password=c["password"], auto_bind=_auto_bind(c["secure"]))
|
||||
return CONN, c
|
||||
|
||||
|
||||
def base_dn_arg():
|
||||
return I.get("base-dn") or I.get("base_dn") or _cfg()["base_dn"]
|
||||
|
||||
|
||||
def search_paging(conn, search_filter, search_base, attributes=None, page_size=100, size_limit=0, time_limit=0):
|
||||
entries = []
|
||||
cookie = None
|
||||
total = 0
|
||||
left = size_limit
|
||||
while True:
|
||||
ps = page_size
|
||||
if 0 < left < page_size:
|
||||
ps = left
|
||||
conn.search(search_base, search_filter, search_scope=SUBTREE, attributes=attributes,
|
||||
paged_size=ps, paged_cookie=cookie, time_limit=time_limit)
|
||||
left -= len(conn.entries)
|
||||
total += len(conn.entries)
|
||||
try:
|
||||
cookie = conn.result["controls"]["1.2.840.113556.1.4.319"]["value"]["cookie"]
|
||||
except (KeyError, TypeError):
|
||||
cookie = None
|
||||
entries.extend(conn.entries)
|
||||
if (size_limit and size_limit <= total) or not cookie:
|
||||
break
|
||||
flat = []
|
||||
for entry in entries:
|
||||
obj = json.loads(entry.entry_to_json())
|
||||
flat_entry = {"dn": obj["dn"]}
|
||||
for attr in obj.get("attributes", {}):
|
||||
flat_entry[attr] = obj["attributes"][attr]
|
||||
flat.append(flat_entry)
|
||||
return flat
|
||||
|
||||
|
||||
def user_dn(conn, sam, search_base):
|
||||
if "\\" in sam:
|
||||
sam = sam.split("\\", 1)[1]
|
||||
flat = search_paging(conn, f"(&(objectClass=user)(sAMAccountName={escape_filter_chars(sam)}))",
|
||||
search_base, page_size=1, size_limit=1)
|
||||
if not flat:
|
||||
raise Exception(f"Could not find user '{sam}'")
|
||||
return flat[0]["dn"]
|
||||
|
||||
|
||||
def computer_dn(conn, name, search_base):
|
||||
flat = search_paging(conn, f"(&(objectClass=user)(objectCategory=computer)(name={escape_filter_chars(name)}))",
|
||||
search_base, page_size=1, size_limit=1)
|
||||
if not flat:
|
||||
raise Exception(f"Could not find computer '{name}'")
|
||||
return flat[0]["dn"]
|
||||
|
||||
|
||||
def group_dn(conn, name, search_base):
|
||||
flat = search_paging(conn, f"(&(objectClass=group)(cn={escape_filter_chars(name)}))",
|
||||
search_base, page_size=1, size_limit=1)
|
||||
if not flat:
|
||||
raise Exception(f"Could not find group '{name}'")
|
||||
return flat[0]["dn"]
|
||||
|
||||
|
||||
def modify(conn, dn, modification):
|
||||
if not conn.modify(dn, modification):
|
||||
raise Exception(f"Failed to modify {dn}: {json.dumps(conn.result, default=str)}")
|
||||
|
||||
|
||||
def first_uac(values):
|
||||
if isinstance(values, list):
|
||||
return values[0] if values else 0
|
||||
return values or 0
|
||||
|
||||
|
||||
def uac_fields(value):
|
||||
value = int(value)
|
||||
flags = {
|
||||
"SCRIPT": 0x1, "ACCOUNTDISABLE": 0x2, "HOMEDIR_REQUIRED": 0x8, "LOCKOUT": 0x10,
|
||||
"PASSWD_NOTREQD": 0x20, "PASSWD_CANT_CHANGE": 0x40, "ENCRYPTED_TEXT_PWD_ALLOWED": 0x80,
|
||||
"TEMP_DUPLICATE_ACCOUNT": 0x100, "NORMAL_ACCOUNT": 0x200, "INTERDOMAIN_TRUST_ACCOUNT": 0x800,
|
||||
"WORKSTATION_TRUST_ACCOUNT": 0x1000, "SERVER_TRUST_ACCOUNT": 0x2000, "DONT_EXPIRE_PASSWORD": 0x10000,
|
||||
"MNS_LOGON_ACCOUNT": 0x20000, "SMARTCARD_REQUIRED": 0x40000, "TRUSTED_FOR_DELEGATION": 0x80000,
|
||||
"NOT_DELEGATED": 0x100000, "USE_DES_KEY_ONLY": 0x200000, "DONT_REQ_PREAUTH": 0x400000,
|
||||
"PASSWORD_EXPIRED": 0x800000, "TRUSTED_TO_AUTH_FOR_DELEGATION": 0x1000000, "PARTIAL_SECRETS_ACCOUNT": 0x4000000,
|
||||
}
|
||||
return {name: bool(value & mask) for name, mask in flags.items()}
|
||||
|
||||
|
||||
def current_uac(conn, search_base):
|
||||
sam = I.get("username") or I.get("sAMAccountName")
|
||||
query = "(&(objectClass=User)(objectCategory=person))"
|
||||
if sam:
|
||||
query = f"(&(objectClass=User)(objectCategory=person)(sAMAccountName={escape_filter_chars(sam)}))"
|
||||
flat = search_paging(conn, query, search_base, attributes=["userAccountControl"], size_limit=1, page_size=1)
|
||||
if flat and flat[0].get("userAccountControl"):
|
||||
return first_uac(flat[0]["userAccountControl"])
|
||||
return 512
|
||||
|
||||
|
||||
|
||||
def run():
|
||||
conn, c = connect()
|
||||
dn = user_dn(conn, I["username"], I.get("base-dn") or base_dn_arg())
|
||||
if not microsoft.unlockAccount.ad_unlock_account(conn, dn):
|
||||
fail("Failed to unlock " + I["username"])
|
||||
out({"unlocked": I["username"]})
|
||||
|
||||
|
||||
try:
|
||||
run()
|
||||
except SystemExit:
|
||||
raise
|
||||
except LDAPException as e:
|
||||
fail("LDAP error: " + str(e))
|
||||
except Exception as e:
|
||||
fail(str(e))
|
||||
finally:
|
||||
try:
|
||||
if CONN is not None:
|
||||
CONN.unbind()
|
||||
except Exception:
|
||||
pass
|
||||
@@ -0,0 +1,226 @@
|
||||
import json, os, sys
|
||||
|
||||
S = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||
I = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||
|
||||
|
||||
def out(value):
|
||||
print(json.dumps(value, default=str))
|
||||
|
||||
|
||||
def fail(message, **extra):
|
||||
payload = {"error": message}
|
||||
payload.update(extra)
|
||||
print(json.dumps(payload, default=str))
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
try:
|
||||
import ssl
|
||||
from ldap3 import (
|
||||
Server, Connection, Tls, NTLM, SUBTREE, BASE, ALL_ATTRIBUTES,
|
||||
MODIFY_REPLACE, AUTO_BIND_NO_TLS, AUTO_BIND_TLS_BEFORE_BIND,
|
||||
)
|
||||
from ldap3.core.exceptions import LDAPException
|
||||
from ldap3.extend import microsoft
|
||||
from ldap3.utils.conv import escape_filter_chars
|
||||
except ImportError as _e:
|
||||
print(json.dumps({
|
||||
"error": "The 'ldap3' Python library is required for the Active Directory integration. "
|
||||
"Install it on the execution host (engine): pip install ldap3",
|
||||
"detail": str(_e),
|
||||
}))
|
||||
sys.exit(1)
|
||||
|
||||
SSL_VERSIONS = {
|
||||
"None": None,
|
||||
"TLS": ssl.PROTOCOL_TLS,
|
||||
"TLSv1": ssl.PROTOCOL_TLSv1,
|
||||
"TLSv1_1": ssl.PROTOCOL_TLSv1_1,
|
||||
"TLSv1_2": ssl.PROTOCOL_TLSv1_2,
|
||||
"TLS_CLIENT": ssl.PROTOCOL_TLS_CLIENT,
|
||||
}
|
||||
CIPHERS = (
|
||||
"@SECLEVEL=1:ECDHE+AESGCM:ECDHE+CHACHA20:DHE+AESGCM:DHE+CHACHA20:ECDH+AESGCM:"
|
||||
"DH+AESGCM:ECDH+AES:DH+AES:RSA+AESGCM:RSA+AES:!aNULL:!eNULL:!MD5:!DSS"
|
||||
)
|
||||
|
||||
CONN = None
|
||||
|
||||
|
||||
def _bool(v):
|
||||
if isinstance(v, bool):
|
||||
return v
|
||||
return str(v).lower() in ("1", "true", "yes")
|
||||
|
||||
|
||||
def _list(v):
|
||||
if isinstance(v, list):
|
||||
return v
|
||||
return [x.strip() for x in str(v).split(",") if x.strip()]
|
||||
|
||||
|
||||
def _cfg():
|
||||
return {
|
||||
"host": S.get("server_ip") or S.get("host") or "",
|
||||
"port": int(S["port"]) if S.get("port") else None,
|
||||
"user": S.get("username") or "",
|
||||
"password": S.get("password") or "",
|
||||
"base_dn": S.get("base_dn") or "",
|
||||
"secure": S.get("secure_connection") or "SSL",
|
||||
"ssl_version": S.get("ssl_version") or "None",
|
||||
"ntlm": _bool(S.get("ntlm")),
|
||||
"unsecure": _bool(S.get("unsecure")),
|
||||
"page_size": int(S["page_size"]) if S.get("page_size") else 500,
|
||||
}
|
||||
|
||||
|
||||
def _tls(unsecure, ssl_version):
|
||||
version = SSL_VERSIONS.get(ssl_version)
|
||||
if unsecure:
|
||||
return Tls(validate=ssl.CERT_NONE, ca_certs_file=None, ciphers=CIPHERS, version=version)
|
||||
return Tls(validate=ssl.CERT_REQUIRED, ca_certs_file=os.environ.get("SSL_CERT_FILE"), version=version)
|
||||
|
||||
|
||||
def _server(c):
|
||||
secure = c["secure"]
|
||||
if secure in ("SSL", "TLS"):
|
||||
return Server(host=c["host"], port=c["port"], use_ssl=True, tls=_tls(c["unsecure"], c["ssl_version"]), connect_timeout=120)
|
||||
if secure == "Start TLS":
|
||||
return Server(host=c["host"], port=c["port"], use_ssl=False, tls=_tls(c["unsecure"], c["ssl_version"]), connect_timeout=120)
|
||||
return Server(host=c["host"], port=c["port"], connect_timeout=120)
|
||||
|
||||
|
||||
def _auto_bind(secure):
|
||||
return AUTO_BIND_TLS_BEFORE_BIND if secure == "Start TLS" else AUTO_BIND_NO_TLS
|
||||
|
||||
|
||||
def connect():
|
||||
global CONN
|
||||
c = _cfg()
|
||||
server = _server(c)
|
||||
if c["ntlm"]:
|
||||
domain_user = c["user"] if "\\" in c["user"] else c["host"] + "\\" + c["user"]
|
||||
CONN = Connection(server, domain_user, password=c["password"], authentication=NTLM, auto_bind=_auto_bind(c["secure"]))
|
||||
else:
|
||||
CONN = Connection(server, user=c["user"], password=c["password"], auto_bind=_auto_bind(c["secure"]))
|
||||
return CONN, c
|
||||
|
||||
|
||||
def base_dn_arg():
|
||||
return I.get("base-dn") or I.get("base_dn") or _cfg()["base_dn"]
|
||||
|
||||
|
||||
def search_paging(conn, search_filter, search_base, attributes=None, page_size=100, size_limit=0, time_limit=0):
|
||||
entries = []
|
||||
cookie = None
|
||||
total = 0
|
||||
left = size_limit
|
||||
while True:
|
||||
ps = page_size
|
||||
if 0 < left < page_size:
|
||||
ps = left
|
||||
conn.search(search_base, search_filter, search_scope=SUBTREE, attributes=attributes,
|
||||
paged_size=ps, paged_cookie=cookie, time_limit=time_limit)
|
||||
left -= len(conn.entries)
|
||||
total += len(conn.entries)
|
||||
try:
|
||||
cookie = conn.result["controls"]["1.2.840.113556.1.4.319"]["value"]["cookie"]
|
||||
except (KeyError, TypeError):
|
||||
cookie = None
|
||||
entries.extend(conn.entries)
|
||||
if (size_limit and size_limit <= total) or not cookie:
|
||||
break
|
||||
flat = []
|
||||
for entry in entries:
|
||||
obj = json.loads(entry.entry_to_json())
|
||||
flat_entry = {"dn": obj["dn"]}
|
||||
for attr in obj.get("attributes", {}):
|
||||
flat_entry[attr] = obj["attributes"][attr]
|
||||
flat.append(flat_entry)
|
||||
return flat
|
||||
|
||||
|
||||
def user_dn(conn, sam, search_base):
|
||||
if "\\" in sam:
|
||||
sam = sam.split("\\", 1)[1]
|
||||
flat = search_paging(conn, f"(&(objectClass=user)(sAMAccountName={escape_filter_chars(sam)}))",
|
||||
search_base, page_size=1, size_limit=1)
|
||||
if not flat:
|
||||
raise Exception(f"Could not find user '{sam}'")
|
||||
return flat[0]["dn"]
|
||||
|
||||
|
||||
def computer_dn(conn, name, search_base):
|
||||
flat = search_paging(conn, f"(&(objectClass=user)(objectCategory=computer)(name={escape_filter_chars(name)}))",
|
||||
search_base, page_size=1, size_limit=1)
|
||||
if not flat:
|
||||
raise Exception(f"Could not find computer '{name}'")
|
||||
return flat[0]["dn"]
|
||||
|
||||
|
||||
def group_dn(conn, name, search_base):
|
||||
flat = search_paging(conn, f"(&(objectClass=group)(cn={escape_filter_chars(name)}))",
|
||||
search_base, page_size=1, size_limit=1)
|
||||
if not flat:
|
||||
raise Exception(f"Could not find group '{name}'")
|
||||
return flat[0]["dn"]
|
||||
|
||||
|
||||
def modify(conn, dn, modification):
|
||||
if not conn.modify(dn, modification):
|
||||
raise Exception(f"Failed to modify {dn}: {json.dumps(conn.result, default=str)}")
|
||||
|
||||
|
||||
def first_uac(values):
|
||||
if isinstance(values, list):
|
||||
return values[0] if values else 0
|
||||
return values or 0
|
||||
|
||||
|
||||
def uac_fields(value):
|
||||
value = int(value)
|
||||
flags = {
|
||||
"SCRIPT": 0x1, "ACCOUNTDISABLE": 0x2, "HOMEDIR_REQUIRED": 0x8, "LOCKOUT": 0x10,
|
||||
"PASSWD_NOTREQD": 0x20, "PASSWD_CANT_CHANGE": 0x40, "ENCRYPTED_TEXT_PWD_ALLOWED": 0x80,
|
||||
"TEMP_DUPLICATE_ACCOUNT": 0x100, "NORMAL_ACCOUNT": 0x200, "INTERDOMAIN_TRUST_ACCOUNT": 0x800,
|
||||
"WORKSTATION_TRUST_ACCOUNT": 0x1000, "SERVER_TRUST_ACCOUNT": 0x2000, "DONT_EXPIRE_PASSWORD": 0x10000,
|
||||
"MNS_LOGON_ACCOUNT": 0x20000, "SMARTCARD_REQUIRED": 0x40000, "TRUSTED_FOR_DELEGATION": 0x80000,
|
||||
"NOT_DELEGATED": 0x100000, "USE_DES_KEY_ONLY": 0x200000, "DONT_REQ_PREAUTH": 0x400000,
|
||||
"PASSWORD_EXPIRED": 0x800000, "TRUSTED_TO_AUTH_FOR_DELEGATION": 0x1000000, "PARTIAL_SECRETS_ACCOUNT": 0x4000000,
|
||||
}
|
||||
return {name: bool(value & mask) for name, mask in flags.items()}
|
||||
|
||||
|
||||
def current_uac(conn, search_base):
|
||||
sam = I.get("username") or I.get("sAMAccountName")
|
||||
query = "(&(objectClass=User)(objectCategory=person))"
|
||||
if sam:
|
||||
query = f"(&(objectClass=User)(objectCategory=person)(sAMAccountName={escape_filter_chars(sam)}))"
|
||||
flat = search_paging(conn, query, search_base, attributes=["userAccountControl"], size_limit=1, page_size=1)
|
||||
if flat and flat[0].get("userAccountControl"):
|
||||
return first_uac(flat[0]["userAccountControl"])
|
||||
return 512
|
||||
|
||||
|
||||
|
||||
def run():
|
||||
conn, c = connect()
|
||||
modify(conn, I["contact-dn"], {I["attribute-name"]: [(MODIFY_REPLACE, [I["attribute-value"]])]})
|
||||
out({"updated": I["contact-dn"], "attribute": I["attribute-name"], "value": I["attribute-value"]})
|
||||
|
||||
|
||||
try:
|
||||
run()
|
||||
except SystemExit:
|
||||
raise
|
||||
except LDAPException as e:
|
||||
fail("LDAP error: " + str(e))
|
||||
except Exception as e:
|
||||
fail(str(e))
|
||||
finally:
|
||||
try:
|
||||
if CONN is not None:
|
||||
CONN.unbind()
|
||||
except Exception:
|
||||
pass
|
||||
@@ -0,0 +1,227 @@
|
||||
import json, os, sys
|
||||
|
||||
S = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||
I = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||
|
||||
|
||||
def out(value):
|
||||
print(json.dumps(value, default=str))
|
||||
|
||||
|
||||
def fail(message, **extra):
|
||||
payload = {"error": message}
|
||||
payload.update(extra)
|
||||
print(json.dumps(payload, default=str))
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
try:
|
||||
import ssl
|
||||
from ldap3 import (
|
||||
Server, Connection, Tls, NTLM, SUBTREE, BASE, ALL_ATTRIBUTES,
|
||||
MODIFY_REPLACE, AUTO_BIND_NO_TLS, AUTO_BIND_TLS_BEFORE_BIND,
|
||||
)
|
||||
from ldap3.core.exceptions import LDAPException
|
||||
from ldap3.extend import microsoft
|
||||
from ldap3.utils.conv import escape_filter_chars
|
||||
except ImportError as _e:
|
||||
print(json.dumps({
|
||||
"error": "The 'ldap3' Python library is required for the Active Directory integration. "
|
||||
"Install it on the execution host (engine): pip install ldap3",
|
||||
"detail": str(_e),
|
||||
}))
|
||||
sys.exit(1)
|
||||
|
||||
SSL_VERSIONS = {
|
||||
"None": None,
|
||||
"TLS": ssl.PROTOCOL_TLS,
|
||||
"TLSv1": ssl.PROTOCOL_TLSv1,
|
||||
"TLSv1_1": ssl.PROTOCOL_TLSv1_1,
|
||||
"TLSv1_2": ssl.PROTOCOL_TLSv1_2,
|
||||
"TLS_CLIENT": ssl.PROTOCOL_TLS_CLIENT,
|
||||
}
|
||||
CIPHERS = (
|
||||
"@SECLEVEL=1:ECDHE+AESGCM:ECDHE+CHACHA20:DHE+AESGCM:DHE+CHACHA20:ECDH+AESGCM:"
|
||||
"DH+AESGCM:ECDH+AES:DH+AES:RSA+AESGCM:RSA+AES:!aNULL:!eNULL:!MD5:!DSS"
|
||||
)
|
||||
|
||||
CONN = None
|
||||
|
||||
|
||||
def _bool(v):
|
||||
if isinstance(v, bool):
|
||||
return v
|
||||
return str(v).lower() in ("1", "true", "yes")
|
||||
|
||||
|
||||
def _list(v):
|
||||
if isinstance(v, list):
|
||||
return v
|
||||
return [x.strip() for x in str(v).split(",") if x.strip()]
|
||||
|
||||
|
||||
def _cfg():
|
||||
return {
|
||||
"host": S.get("server_ip") or S.get("host") or "",
|
||||
"port": int(S["port"]) if S.get("port") else None,
|
||||
"user": S.get("username") or "",
|
||||
"password": S.get("password") or "",
|
||||
"base_dn": S.get("base_dn") or "",
|
||||
"secure": S.get("secure_connection") or "SSL",
|
||||
"ssl_version": S.get("ssl_version") or "None",
|
||||
"ntlm": _bool(S.get("ntlm")),
|
||||
"unsecure": _bool(S.get("unsecure")),
|
||||
"page_size": int(S["page_size"]) if S.get("page_size") else 500,
|
||||
}
|
||||
|
||||
|
||||
def _tls(unsecure, ssl_version):
|
||||
version = SSL_VERSIONS.get(ssl_version)
|
||||
if unsecure:
|
||||
return Tls(validate=ssl.CERT_NONE, ca_certs_file=None, ciphers=CIPHERS, version=version)
|
||||
return Tls(validate=ssl.CERT_REQUIRED, ca_certs_file=os.environ.get("SSL_CERT_FILE"), version=version)
|
||||
|
||||
|
||||
def _server(c):
|
||||
secure = c["secure"]
|
||||
if secure in ("SSL", "TLS"):
|
||||
return Server(host=c["host"], port=c["port"], use_ssl=True, tls=_tls(c["unsecure"], c["ssl_version"]), connect_timeout=120)
|
||||
if secure == "Start TLS":
|
||||
return Server(host=c["host"], port=c["port"], use_ssl=False, tls=_tls(c["unsecure"], c["ssl_version"]), connect_timeout=120)
|
||||
return Server(host=c["host"], port=c["port"], connect_timeout=120)
|
||||
|
||||
|
||||
def _auto_bind(secure):
|
||||
return AUTO_BIND_TLS_BEFORE_BIND if secure == "Start TLS" else AUTO_BIND_NO_TLS
|
||||
|
||||
|
||||
def connect():
|
||||
global CONN
|
||||
c = _cfg()
|
||||
server = _server(c)
|
||||
if c["ntlm"]:
|
||||
domain_user = c["user"] if "\\" in c["user"] else c["host"] + "\\" + c["user"]
|
||||
CONN = Connection(server, domain_user, password=c["password"], authentication=NTLM, auto_bind=_auto_bind(c["secure"]))
|
||||
else:
|
||||
CONN = Connection(server, user=c["user"], password=c["password"], auto_bind=_auto_bind(c["secure"]))
|
||||
return CONN, c
|
||||
|
||||
|
||||
def base_dn_arg():
|
||||
return I.get("base-dn") or I.get("base_dn") or _cfg()["base_dn"]
|
||||
|
||||
|
||||
def search_paging(conn, search_filter, search_base, attributes=None, page_size=100, size_limit=0, time_limit=0):
|
||||
entries = []
|
||||
cookie = None
|
||||
total = 0
|
||||
left = size_limit
|
||||
while True:
|
||||
ps = page_size
|
||||
if 0 < left < page_size:
|
||||
ps = left
|
||||
conn.search(search_base, search_filter, search_scope=SUBTREE, attributes=attributes,
|
||||
paged_size=ps, paged_cookie=cookie, time_limit=time_limit)
|
||||
left -= len(conn.entries)
|
||||
total += len(conn.entries)
|
||||
try:
|
||||
cookie = conn.result["controls"]["1.2.840.113556.1.4.319"]["value"]["cookie"]
|
||||
except (KeyError, TypeError):
|
||||
cookie = None
|
||||
entries.extend(conn.entries)
|
||||
if (size_limit and size_limit <= total) or not cookie:
|
||||
break
|
||||
flat = []
|
||||
for entry in entries:
|
||||
obj = json.loads(entry.entry_to_json())
|
||||
flat_entry = {"dn": obj["dn"]}
|
||||
for attr in obj.get("attributes", {}):
|
||||
flat_entry[attr] = obj["attributes"][attr]
|
||||
flat.append(flat_entry)
|
||||
return flat
|
||||
|
||||
|
||||
def user_dn(conn, sam, search_base):
|
||||
if "\\" in sam:
|
||||
sam = sam.split("\\", 1)[1]
|
||||
flat = search_paging(conn, f"(&(objectClass=user)(sAMAccountName={escape_filter_chars(sam)}))",
|
||||
search_base, page_size=1, size_limit=1)
|
||||
if not flat:
|
||||
raise Exception(f"Could not find user '{sam}'")
|
||||
return flat[0]["dn"]
|
||||
|
||||
|
||||
def computer_dn(conn, name, search_base):
|
||||
flat = search_paging(conn, f"(&(objectClass=user)(objectCategory=computer)(name={escape_filter_chars(name)}))",
|
||||
search_base, page_size=1, size_limit=1)
|
||||
if not flat:
|
||||
raise Exception(f"Could not find computer '{name}'")
|
||||
return flat[0]["dn"]
|
||||
|
||||
|
||||
def group_dn(conn, name, search_base):
|
||||
flat = search_paging(conn, f"(&(objectClass=group)(cn={escape_filter_chars(name)}))",
|
||||
search_base, page_size=1, size_limit=1)
|
||||
if not flat:
|
||||
raise Exception(f"Could not find group '{name}'")
|
||||
return flat[0]["dn"]
|
||||
|
||||
|
||||
def modify(conn, dn, modification):
|
||||
if not conn.modify(dn, modification):
|
||||
raise Exception(f"Failed to modify {dn}: {json.dumps(conn.result, default=str)}")
|
||||
|
||||
|
||||
def first_uac(values):
|
||||
if isinstance(values, list):
|
||||
return values[0] if values else 0
|
||||
return values or 0
|
||||
|
||||
|
||||
def uac_fields(value):
|
||||
value = int(value)
|
||||
flags = {
|
||||
"SCRIPT": 0x1, "ACCOUNTDISABLE": 0x2, "HOMEDIR_REQUIRED": 0x8, "LOCKOUT": 0x10,
|
||||
"PASSWD_NOTREQD": 0x20, "PASSWD_CANT_CHANGE": 0x40, "ENCRYPTED_TEXT_PWD_ALLOWED": 0x80,
|
||||
"TEMP_DUPLICATE_ACCOUNT": 0x100, "NORMAL_ACCOUNT": 0x200, "INTERDOMAIN_TRUST_ACCOUNT": 0x800,
|
||||
"WORKSTATION_TRUST_ACCOUNT": 0x1000, "SERVER_TRUST_ACCOUNT": 0x2000, "DONT_EXPIRE_PASSWORD": 0x10000,
|
||||
"MNS_LOGON_ACCOUNT": 0x20000, "SMARTCARD_REQUIRED": 0x40000, "TRUSTED_FOR_DELEGATION": 0x80000,
|
||||
"NOT_DELEGATED": 0x100000, "USE_DES_KEY_ONLY": 0x200000, "DONT_REQ_PREAUTH": 0x400000,
|
||||
"PASSWORD_EXPIRED": 0x800000, "TRUSTED_TO_AUTH_FOR_DELEGATION": 0x1000000, "PARTIAL_SECRETS_ACCOUNT": 0x4000000,
|
||||
}
|
||||
return {name: bool(value & mask) for name, mask in flags.items()}
|
||||
|
||||
|
||||
def current_uac(conn, search_base):
|
||||
sam = I.get("username") or I.get("sAMAccountName")
|
||||
query = "(&(objectClass=User)(objectCategory=person))"
|
||||
if sam:
|
||||
query = f"(&(objectClass=User)(objectCategory=person)(sAMAccountName={escape_filter_chars(sam)}))"
|
||||
flat = search_paging(conn, query, search_base, attributes=["userAccountControl"], size_limit=1, page_size=1)
|
||||
if flat and flat[0].get("userAccountControl"):
|
||||
return first_uac(flat[0]["userAccountControl"])
|
||||
return 512
|
||||
|
||||
|
||||
|
||||
def run():
|
||||
conn, c = connect()
|
||||
dn = group_dn(conn, I["groupname"], I.get("basedn") or base_dn_arg())
|
||||
modify(conn, dn, {I["attributename"]: [(MODIFY_REPLACE, [I["attributevalue"]])]})
|
||||
out({"updated": dn, "attribute": I["attributename"], "value": I["attributevalue"]})
|
||||
|
||||
|
||||
try:
|
||||
run()
|
||||
except SystemExit:
|
||||
raise
|
||||
except LDAPException as e:
|
||||
fail("LDAP error: " + str(e))
|
||||
except Exception as e:
|
||||
fail(str(e))
|
||||
finally:
|
||||
try:
|
||||
if CONN is not None:
|
||||
CONN.unbind()
|
||||
except Exception:
|
||||
pass
|
||||
@@ -0,0 +1,227 @@
|
||||
import json, os, sys
|
||||
|
||||
S = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||
I = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||
|
||||
|
||||
def out(value):
|
||||
print(json.dumps(value, default=str))
|
||||
|
||||
|
||||
def fail(message, **extra):
|
||||
payload = {"error": message}
|
||||
payload.update(extra)
|
||||
print(json.dumps(payload, default=str))
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
try:
|
||||
import ssl
|
||||
from ldap3 import (
|
||||
Server, Connection, Tls, NTLM, SUBTREE, BASE, ALL_ATTRIBUTES,
|
||||
MODIFY_REPLACE, AUTO_BIND_NO_TLS, AUTO_BIND_TLS_BEFORE_BIND,
|
||||
)
|
||||
from ldap3.core.exceptions import LDAPException
|
||||
from ldap3.extend import microsoft
|
||||
from ldap3.utils.conv import escape_filter_chars
|
||||
except ImportError as _e:
|
||||
print(json.dumps({
|
||||
"error": "The 'ldap3' Python library is required for the Active Directory integration. "
|
||||
"Install it on the execution host (engine): pip install ldap3",
|
||||
"detail": str(_e),
|
||||
}))
|
||||
sys.exit(1)
|
||||
|
||||
SSL_VERSIONS = {
|
||||
"None": None,
|
||||
"TLS": ssl.PROTOCOL_TLS,
|
||||
"TLSv1": ssl.PROTOCOL_TLSv1,
|
||||
"TLSv1_1": ssl.PROTOCOL_TLSv1_1,
|
||||
"TLSv1_2": ssl.PROTOCOL_TLSv1_2,
|
||||
"TLS_CLIENT": ssl.PROTOCOL_TLS_CLIENT,
|
||||
}
|
||||
CIPHERS = (
|
||||
"@SECLEVEL=1:ECDHE+AESGCM:ECDHE+CHACHA20:DHE+AESGCM:DHE+CHACHA20:ECDH+AESGCM:"
|
||||
"DH+AESGCM:ECDH+AES:DH+AES:RSA+AESGCM:RSA+AES:!aNULL:!eNULL:!MD5:!DSS"
|
||||
)
|
||||
|
||||
CONN = None
|
||||
|
||||
|
||||
def _bool(v):
|
||||
if isinstance(v, bool):
|
||||
return v
|
||||
return str(v).lower() in ("1", "true", "yes")
|
||||
|
||||
|
||||
def _list(v):
|
||||
if isinstance(v, list):
|
||||
return v
|
||||
return [x.strip() for x in str(v).split(",") if x.strip()]
|
||||
|
||||
|
||||
def _cfg():
|
||||
return {
|
||||
"host": S.get("server_ip") or S.get("host") or "",
|
||||
"port": int(S["port"]) if S.get("port") else None,
|
||||
"user": S.get("username") or "",
|
||||
"password": S.get("password") or "",
|
||||
"base_dn": S.get("base_dn") or "",
|
||||
"secure": S.get("secure_connection") or "SSL",
|
||||
"ssl_version": S.get("ssl_version") or "None",
|
||||
"ntlm": _bool(S.get("ntlm")),
|
||||
"unsecure": _bool(S.get("unsecure")),
|
||||
"page_size": int(S["page_size"]) if S.get("page_size") else 500,
|
||||
}
|
||||
|
||||
|
||||
def _tls(unsecure, ssl_version):
|
||||
version = SSL_VERSIONS.get(ssl_version)
|
||||
if unsecure:
|
||||
return Tls(validate=ssl.CERT_NONE, ca_certs_file=None, ciphers=CIPHERS, version=version)
|
||||
return Tls(validate=ssl.CERT_REQUIRED, ca_certs_file=os.environ.get("SSL_CERT_FILE"), version=version)
|
||||
|
||||
|
||||
def _server(c):
|
||||
secure = c["secure"]
|
||||
if secure in ("SSL", "TLS"):
|
||||
return Server(host=c["host"], port=c["port"], use_ssl=True, tls=_tls(c["unsecure"], c["ssl_version"]), connect_timeout=120)
|
||||
if secure == "Start TLS":
|
||||
return Server(host=c["host"], port=c["port"], use_ssl=False, tls=_tls(c["unsecure"], c["ssl_version"]), connect_timeout=120)
|
||||
return Server(host=c["host"], port=c["port"], connect_timeout=120)
|
||||
|
||||
|
||||
def _auto_bind(secure):
|
||||
return AUTO_BIND_TLS_BEFORE_BIND if secure == "Start TLS" else AUTO_BIND_NO_TLS
|
||||
|
||||
|
||||
def connect():
|
||||
global CONN
|
||||
c = _cfg()
|
||||
server = _server(c)
|
||||
if c["ntlm"]:
|
||||
domain_user = c["user"] if "\\" in c["user"] else c["host"] + "\\" + c["user"]
|
||||
CONN = Connection(server, domain_user, password=c["password"], authentication=NTLM, auto_bind=_auto_bind(c["secure"]))
|
||||
else:
|
||||
CONN = Connection(server, user=c["user"], password=c["password"], auto_bind=_auto_bind(c["secure"]))
|
||||
return CONN, c
|
||||
|
||||
|
||||
def base_dn_arg():
|
||||
return I.get("base-dn") or I.get("base_dn") or _cfg()["base_dn"]
|
||||
|
||||
|
||||
def search_paging(conn, search_filter, search_base, attributes=None, page_size=100, size_limit=0, time_limit=0):
|
||||
entries = []
|
||||
cookie = None
|
||||
total = 0
|
||||
left = size_limit
|
||||
while True:
|
||||
ps = page_size
|
||||
if 0 < left < page_size:
|
||||
ps = left
|
||||
conn.search(search_base, search_filter, search_scope=SUBTREE, attributes=attributes,
|
||||
paged_size=ps, paged_cookie=cookie, time_limit=time_limit)
|
||||
left -= len(conn.entries)
|
||||
total += len(conn.entries)
|
||||
try:
|
||||
cookie = conn.result["controls"]["1.2.840.113556.1.4.319"]["value"]["cookie"]
|
||||
except (KeyError, TypeError):
|
||||
cookie = None
|
||||
entries.extend(conn.entries)
|
||||
if (size_limit and size_limit <= total) or not cookie:
|
||||
break
|
||||
flat = []
|
||||
for entry in entries:
|
||||
obj = json.loads(entry.entry_to_json())
|
||||
flat_entry = {"dn": obj["dn"]}
|
||||
for attr in obj.get("attributes", {}):
|
||||
flat_entry[attr] = obj["attributes"][attr]
|
||||
flat.append(flat_entry)
|
||||
return flat
|
||||
|
||||
|
||||
def user_dn(conn, sam, search_base):
|
||||
if "\\" in sam:
|
||||
sam = sam.split("\\", 1)[1]
|
||||
flat = search_paging(conn, f"(&(objectClass=user)(sAMAccountName={escape_filter_chars(sam)}))",
|
||||
search_base, page_size=1, size_limit=1)
|
||||
if not flat:
|
||||
raise Exception(f"Could not find user '{sam}'")
|
||||
return flat[0]["dn"]
|
||||
|
||||
|
||||
def computer_dn(conn, name, search_base):
|
||||
flat = search_paging(conn, f"(&(objectClass=user)(objectCategory=computer)(name={escape_filter_chars(name)}))",
|
||||
search_base, page_size=1, size_limit=1)
|
||||
if not flat:
|
||||
raise Exception(f"Could not find computer '{name}'")
|
||||
return flat[0]["dn"]
|
||||
|
||||
|
||||
def group_dn(conn, name, search_base):
|
||||
flat = search_paging(conn, f"(&(objectClass=group)(cn={escape_filter_chars(name)}))",
|
||||
search_base, page_size=1, size_limit=1)
|
||||
if not flat:
|
||||
raise Exception(f"Could not find group '{name}'")
|
||||
return flat[0]["dn"]
|
||||
|
||||
|
||||
def modify(conn, dn, modification):
|
||||
if not conn.modify(dn, modification):
|
||||
raise Exception(f"Failed to modify {dn}: {json.dumps(conn.result, default=str)}")
|
||||
|
||||
|
||||
def first_uac(values):
|
||||
if isinstance(values, list):
|
||||
return values[0] if values else 0
|
||||
return values or 0
|
||||
|
||||
|
||||
def uac_fields(value):
|
||||
value = int(value)
|
||||
flags = {
|
||||
"SCRIPT": 0x1, "ACCOUNTDISABLE": 0x2, "HOMEDIR_REQUIRED": 0x8, "LOCKOUT": 0x10,
|
||||
"PASSWD_NOTREQD": 0x20, "PASSWD_CANT_CHANGE": 0x40, "ENCRYPTED_TEXT_PWD_ALLOWED": 0x80,
|
||||
"TEMP_DUPLICATE_ACCOUNT": 0x100, "NORMAL_ACCOUNT": 0x200, "INTERDOMAIN_TRUST_ACCOUNT": 0x800,
|
||||
"WORKSTATION_TRUST_ACCOUNT": 0x1000, "SERVER_TRUST_ACCOUNT": 0x2000, "DONT_EXPIRE_PASSWORD": 0x10000,
|
||||
"MNS_LOGON_ACCOUNT": 0x20000, "SMARTCARD_REQUIRED": 0x40000, "TRUSTED_FOR_DELEGATION": 0x80000,
|
||||
"NOT_DELEGATED": 0x100000, "USE_DES_KEY_ONLY": 0x200000, "DONT_REQ_PREAUTH": 0x400000,
|
||||
"PASSWORD_EXPIRED": 0x800000, "TRUSTED_TO_AUTH_FOR_DELEGATION": 0x1000000, "PARTIAL_SECRETS_ACCOUNT": 0x4000000,
|
||||
}
|
||||
return {name: bool(value & mask) for name, mask in flags.items()}
|
||||
|
||||
|
||||
def current_uac(conn, search_base):
|
||||
sam = I.get("username") or I.get("sAMAccountName")
|
||||
query = "(&(objectClass=User)(objectCategory=person))"
|
||||
if sam:
|
||||
query = f"(&(objectClass=User)(objectCategory=person)(sAMAccountName={escape_filter_chars(sam)}))"
|
||||
flat = search_paging(conn, query, search_base, attributes=["userAccountControl"], size_limit=1, page_size=1)
|
||||
if flat and flat[0].get("userAccountControl"):
|
||||
return first_uac(flat[0]["userAccountControl"])
|
||||
return 512
|
||||
|
||||
|
||||
|
||||
def run():
|
||||
conn, c = connect()
|
||||
dn = user_dn(conn, I["username"], I.get("base-dn") or base_dn_arg())
|
||||
modify(conn, dn, {I["attribute-name"]: [(MODIFY_REPLACE, [I["attribute-value"]])]})
|
||||
out({"updated": dn, "attribute": I["attribute-name"], "value": I["attribute-value"]})
|
||||
|
||||
|
||||
try:
|
||||
run()
|
||||
except SystemExit:
|
||||
raise
|
||||
except LDAPException as e:
|
||||
fail("LDAP error: " + str(e))
|
||||
except Exception as e:
|
||||
fail(str(e))
|
||||
finally:
|
||||
try:
|
||||
if CONN is not None:
|
||||
CONN.unbind()
|
||||
except Exception:
|
||||
pass
|
||||
@@ -0,0 +1,229 @@
|
||||
import json, os, sys
|
||||
|
||||
S = json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||
I = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||
|
||||
|
||||
def out(value):
|
||||
print(json.dumps(value, default=str))
|
||||
|
||||
|
||||
def fail(message, **extra):
|
||||
payload = {"error": message}
|
||||
payload.update(extra)
|
||||
print(json.dumps(payload, default=str))
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
try:
|
||||
import ssl
|
||||
from ldap3 import (
|
||||
Server, Connection, Tls, NTLM, SUBTREE, BASE, ALL_ATTRIBUTES,
|
||||
MODIFY_REPLACE, AUTO_BIND_NO_TLS, AUTO_BIND_TLS_BEFORE_BIND,
|
||||
)
|
||||
from ldap3.core.exceptions import LDAPException
|
||||
from ldap3.extend import microsoft
|
||||
from ldap3.utils.conv import escape_filter_chars
|
||||
except ImportError as _e:
|
||||
print(json.dumps({
|
||||
"error": "The 'ldap3' Python library is required for the Active Directory integration. "
|
||||
"Install it on the execution host (engine): pip install ldap3",
|
||||
"detail": str(_e),
|
||||
}))
|
||||
sys.exit(1)
|
||||
|
||||
SSL_VERSIONS = {
|
||||
"None": None,
|
||||
"TLS": ssl.PROTOCOL_TLS,
|
||||
"TLSv1": ssl.PROTOCOL_TLSv1,
|
||||
"TLSv1_1": ssl.PROTOCOL_TLSv1_1,
|
||||
"TLSv1_2": ssl.PROTOCOL_TLSv1_2,
|
||||
"TLS_CLIENT": ssl.PROTOCOL_TLS_CLIENT,
|
||||
}
|
||||
CIPHERS = (
|
||||
"@SECLEVEL=1:ECDHE+AESGCM:ECDHE+CHACHA20:DHE+AESGCM:DHE+CHACHA20:ECDH+AESGCM:"
|
||||
"DH+AESGCM:ECDH+AES:DH+AES:RSA+AESGCM:RSA+AES:!aNULL:!eNULL:!MD5:!DSS"
|
||||
)
|
||||
|
||||
CONN = None
|
||||
|
||||
|
||||
def _bool(v):
|
||||
if isinstance(v, bool):
|
||||
return v
|
||||
return str(v).lower() in ("1", "true", "yes")
|
||||
|
||||
|
||||
def _list(v):
|
||||
if isinstance(v, list):
|
||||
return v
|
||||
return [x.strip() for x in str(v).split(",") if x.strip()]
|
||||
|
||||
|
||||
def _cfg():
|
||||
return {
|
||||
"host": S.get("server_ip") or S.get("host") or "",
|
||||
"port": int(S["port"]) if S.get("port") else None,
|
||||
"user": S.get("username") or "",
|
||||
"password": S.get("password") or "",
|
||||
"base_dn": S.get("base_dn") or "",
|
||||
"secure": S.get("secure_connection") or "SSL",
|
||||
"ssl_version": S.get("ssl_version") or "None",
|
||||
"ntlm": _bool(S.get("ntlm")),
|
||||
"unsecure": _bool(S.get("unsecure")),
|
||||
"page_size": int(S["page_size"]) if S.get("page_size") else 500,
|
||||
}
|
||||
|
||||
|
||||
def _tls(unsecure, ssl_version):
|
||||
version = SSL_VERSIONS.get(ssl_version)
|
||||
if unsecure:
|
||||
return Tls(validate=ssl.CERT_NONE, ca_certs_file=None, ciphers=CIPHERS, version=version)
|
||||
return Tls(validate=ssl.CERT_REQUIRED, ca_certs_file=os.environ.get("SSL_CERT_FILE"), version=version)
|
||||
|
||||
|
||||
def _server(c):
|
||||
secure = c["secure"]
|
||||
if secure in ("SSL", "TLS"):
|
||||
return Server(host=c["host"], port=c["port"], use_ssl=True, tls=_tls(c["unsecure"], c["ssl_version"]), connect_timeout=120)
|
||||
if secure == "Start TLS":
|
||||
return Server(host=c["host"], port=c["port"], use_ssl=False, tls=_tls(c["unsecure"], c["ssl_version"]), connect_timeout=120)
|
||||
return Server(host=c["host"], port=c["port"], connect_timeout=120)
|
||||
|
||||
|
||||
def _auto_bind(secure):
|
||||
return AUTO_BIND_TLS_BEFORE_BIND if secure == "Start TLS" else AUTO_BIND_NO_TLS
|
||||
|
||||
|
||||
def connect():
|
||||
global CONN
|
||||
c = _cfg()
|
||||
server = _server(c)
|
||||
if c["ntlm"]:
|
||||
domain_user = c["user"] if "\\" in c["user"] else c["host"] + "\\" + c["user"]
|
||||
CONN = Connection(server, domain_user, password=c["password"], authentication=NTLM, auto_bind=_auto_bind(c["secure"]))
|
||||
else:
|
||||
CONN = Connection(server, user=c["user"], password=c["password"], auto_bind=_auto_bind(c["secure"]))
|
||||
return CONN, c
|
||||
|
||||
|
||||
def base_dn_arg():
|
||||
return I.get("base-dn") or I.get("base_dn") or _cfg()["base_dn"]
|
||||
|
||||
|
||||
def search_paging(conn, search_filter, search_base, attributes=None, page_size=100, size_limit=0, time_limit=0):
|
||||
entries = []
|
||||
cookie = None
|
||||
total = 0
|
||||
left = size_limit
|
||||
while True:
|
||||
ps = page_size
|
||||
if 0 < left < page_size:
|
||||
ps = left
|
||||
conn.search(search_base, search_filter, search_scope=SUBTREE, attributes=attributes,
|
||||
paged_size=ps, paged_cookie=cookie, time_limit=time_limit)
|
||||
left -= len(conn.entries)
|
||||
total += len(conn.entries)
|
||||
try:
|
||||
cookie = conn.result["controls"]["1.2.840.113556.1.4.319"]["value"]["cookie"]
|
||||
except (KeyError, TypeError):
|
||||
cookie = None
|
||||
entries.extend(conn.entries)
|
||||
if (size_limit and size_limit <= total) or not cookie:
|
||||
break
|
||||
flat = []
|
||||
for entry in entries:
|
||||
obj = json.loads(entry.entry_to_json())
|
||||
flat_entry = {"dn": obj["dn"]}
|
||||
for attr in obj.get("attributes", {}):
|
||||
flat_entry[attr] = obj["attributes"][attr]
|
||||
flat.append(flat_entry)
|
||||
return flat
|
||||
|
||||
|
||||
def user_dn(conn, sam, search_base):
|
||||
if "\\" in sam:
|
||||
sam = sam.split("\\", 1)[1]
|
||||
flat = search_paging(conn, f"(&(objectClass=user)(sAMAccountName={escape_filter_chars(sam)}))",
|
||||
search_base, page_size=1, size_limit=1)
|
||||
if not flat:
|
||||
raise Exception(f"Could not find user '{sam}'")
|
||||
return flat[0]["dn"]
|
||||
|
||||
|
||||
def computer_dn(conn, name, search_base):
|
||||
flat = search_paging(conn, f"(&(objectClass=user)(objectCategory=computer)(name={escape_filter_chars(name)}))",
|
||||
search_base, page_size=1, size_limit=1)
|
||||
if not flat:
|
||||
raise Exception(f"Could not find computer '{name}'")
|
||||
return flat[0]["dn"]
|
||||
|
||||
|
||||
def group_dn(conn, name, search_base):
|
||||
flat = search_paging(conn, f"(&(objectClass=group)(cn={escape_filter_chars(name)}))",
|
||||
search_base, page_size=1, size_limit=1)
|
||||
if not flat:
|
||||
raise Exception(f"Could not find group '{name}'")
|
||||
return flat[0]["dn"]
|
||||
|
||||
|
||||
def modify(conn, dn, modification):
|
||||
if not conn.modify(dn, modification):
|
||||
raise Exception(f"Failed to modify {dn}: {json.dumps(conn.result, default=str)}")
|
||||
|
||||
|
||||
def first_uac(values):
|
||||
if isinstance(values, list):
|
||||
return values[0] if values else 0
|
||||
return values or 0
|
||||
|
||||
|
||||
def uac_fields(value):
|
||||
value = int(value)
|
||||
flags = {
|
||||
"SCRIPT": 0x1, "ACCOUNTDISABLE": 0x2, "HOMEDIR_REQUIRED": 0x8, "LOCKOUT": 0x10,
|
||||
"PASSWD_NOTREQD": 0x20, "PASSWD_CANT_CHANGE": 0x40, "ENCRYPTED_TEXT_PWD_ALLOWED": 0x80,
|
||||
"TEMP_DUPLICATE_ACCOUNT": 0x100, "NORMAL_ACCOUNT": 0x200, "INTERDOMAIN_TRUST_ACCOUNT": 0x800,
|
||||
"WORKSTATION_TRUST_ACCOUNT": 0x1000, "SERVER_TRUST_ACCOUNT": 0x2000, "DONT_EXPIRE_PASSWORD": 0x10000,
|
||||
"MNS_LOGON_ACCOUNT": 0x20000, "SMARTCARD_REQUIRED": 0x40000, "TRUSTED_FOR_DELEGATION": 0x80000,
|
||||
"NOT_DELEGATED": 0x100000, "USE_DES_KEY_ONLY": 0x200000, "DONT_REQ_PREAUTH": 0x400000,
|
||||
"PASSWORD_EXPIRED": 0x800000, "TRUSTED_TO_AUTH_FOR_DELEGATION": 0x1000000, "PARTIAL_SECRETS_ACCOUNT": 0x4000000,
|
||||
}
|
||||
return {name: bool(value & mask) for name, mask in flags.items()}
|
||||
|
||||
|
||||
def current_uac(conn, search_base):
|
||||
sam = I.get("username") or I.get("sAMAccountName")
|
||||
query = "(&(objectClass=User)(objectCategory=person))"
|
||||
if sam:
|
||||
query = f"(&(objectClass=User)(objectCategory=person)(sAMAccountName={escape_filter_chars(sam)}))"
|
||||
flat = search_paging(conn, query, search_base, attributes=["userAccountControl"], size_limit=1, page_size=1)
|
||||
if flat and flat[0].get("userAccountControl"):
|
||||
return first_uac(flat[0]["userAccountControl"])
|
||||
return 512
|
||||
|
||||
|
||||
def run():
|
||||
conn, c = connect()
|
||||
if c["base_dn"]:
|
||||
found = conn.search(search_base=c["base_dn"], search_filter="(objectClass=*)",
|
||||
search_scope=BASE, size_limit=1, attributes=["1.1"])
|
||||
if not found:
|
||||
fail("Connected, but failed to verify base DN: " + json.dumps(conn.result, default=str))
|
||||
out({"ok": True})
|
||||
|
||||
|
||||
try:
|
||||
run()
|
||||
except SystemExit:
|
||||
raise
|
||||
except LDAPException as e:
|
||||
fail("LDAP error: " + str(e))
|
||||
except Exception as e:
|
||||
fail(str(e))
|
||||
finally:
|
||||
try:
|
||||
if CONN is not None:
|
||||
CONN.unbind()
|
||||
except Exception:
|
||||
pass
|
||||
@@ -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)
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user