Compare commits
2 Commits
main
..
12f836661b
| Author | SHA1 | Date | |
|---|---|---|---|
| 12f836661b | |||
| b52a5c3789 |
@@ -1,67 +0,0 @@
|
|||||||
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: {} }
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
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)
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
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)
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
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)
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
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)
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
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)
|
|
||||||
@@ -1,348 +0,0 @@
|
|||||||
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: {} }
|
|
||||||
@@ -1,240 +0,0 @@
|
|||||||
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
|
|
||||||
@@ -1,239 +0,0 @@
|
|||||||
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
|
|
||||||
@@ -1,235 +0,0 @@
|
|||||||
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
|
|
||||||
@@ -1,246 +0,0 @@
|
|||||||
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
|
|
||||||
@@ -1,227 +0,0 @@
|
|||||||
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
|
|
||||||
@@ -1,228 +0,0 @@
|
|||||||
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
|
|
||||||
@@ -1,229 +0,0 @@
|
|||||||
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
|
|
||||||
@@ -1,232 +0,0 @@
|
|||||||
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
|
|
||||||
@@ -1,227 +0,0 @@
|
|||||||
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
|
|
||||||
@@ -1,243 +0,0 @@
|
|||||||
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
|
|
||||||
@@ -1,248 +0,0 @@
|
|||||||
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
|
|
||||||
@@ -1,254 +0,0 @@
|
|||||||
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
|
|
||||||
@@ -1,229 +0,0 @@
|
|||||||
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
|
|
||||||
@@ -1,236 +0,0 @@
|
|||||||
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
|
|
||||||
@@ -1,231 +0,0 @@
|
|||||||
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
|
|
||||||
@@ -1,237 +0,0 @@
|
|||||||
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
|
|
||||||
@@ -1,239 +0,0 @@
|
|||||||
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
|
|
||||||
@@ -1,230 +0,0 @@
|
|||||||
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
|
|
||||||
@@ -1,236 +0,0 @@
|
|||||||
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
|
|
||||||
@@ -1,228 +0,0 @@
|
|||||||
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
|
|
||||||
@@ -1,226 +0,0 @@
|
|||||||
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
|
|
||||||
@@ -1,227 +0,0 @@
|
|||||||
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
|
|
||||||
@@ -1,227 +0,0 @@
|
|||||||
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
|
|
||||||
@@ -1,229 +0,0 @@
|
|||||||
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
|
|
||||||
@@ -1,101 +0,0 @@
|
|||||||
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: {} }
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
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)
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
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)
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
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)
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
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)
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
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)
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
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)
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
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)
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
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)
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
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)
|
|
||||||
@@ -1,112 +0,0 @@
|
|||||||
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: {} }
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
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)
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
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)
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
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)
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
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)
|
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
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)
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
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)
|
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
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)
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
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)
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
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)
|
|
||||||
@@ -1,83 +0,0 @@
|
|||||||
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: {} }
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
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)
|
|
||||||
@@ -1,81 +0,0 @@
|
|||||||
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)
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
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)
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
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)
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
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)
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
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)
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
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)
|
|
||||||
@@ -1,70 +0,0 @@
|
|||||||
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: {} }
|
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
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)
|
|
||||||
@@ -1,66 +0,0 @@
|
|||||||
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)
|
|
||||||
@@ -1,66 +0,0 @@
|
|||||||
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)
|
|
||||||
@@ -1,64 +0,0 @@
|
|||||||
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)
|
|
||||||
@@ -1,75 +0,0 @@
|
|||||||
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)
|
|
||||||
@@ -1,66 +0,0 @@
|
|||||||
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: {} }
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
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)
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
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)
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
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)
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
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)
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
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)
|
|
||||||
@@ -1,126 +0,0 @@
|
|||||||
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: {} }
|
|
||||||
@@ -1,151 +0,0 @@
|
|||||||
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)
|
|
||||||
@@ -1,131 +0,0 @@
|
|||||||
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)
|
|
||||||
@@ -1,131 +0,0 @@
|
|||||||
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)
|
|
||||||
@@ -1,131 +0,0 @@
|
|||||||
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)
|
|
||||||
@@ -1,143 +0,0 @@
|
|||||||
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)
|
|
||||||
@@ -1,151 +0,0 @@
|
|||||||
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)
|
|
||||||
@@ -1,148 +0,0 @@
|
|||||||
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)
|
|
||||||
@@ -1,130 +0,0 @@
|
|||||||
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)
|
|
||||||
@@ -1,143 +0,0 @@
|
|||||||
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)
|
|
||||||
@@ -1,76 +0,0 @@
|
|||||||
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: {} }
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
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)
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
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)
|
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
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)
|
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
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)
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
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)
|
|
||||||
@@ -1,120 +0,0 @@
|
|||||||
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: {} }
|
|
||||||
@@ -1,103 +0,0 @@
|
|||||||
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)
|
|
||||||
@@ -1,84 +0,0 @@
|
|||||||
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)
|
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
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)
|
|
||||||
@@ -1,76 +0,0 @@
|
|||||||
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)
|
|
||||||
@@ -1,68 +0,0 @@
|
|||||||
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)
|
|
||||||
@@ -1,68 +0,0 @@
|
|||||||
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)
|
|
||||||
@@ -1,68 +0,0 @@
|
|||||||
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)
|
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
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)
|
|
||||||
@@ -1,78 +0,0 @@
|
|||||||
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)
|
|
||||||
@@ -1,77 +0,0 @@
|
|||||||
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: {} }
|
|
||||||
@@ -1,103 +0,0 @@
|
|||||||
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)
|
|
||||||
@@ -1,93 +0,0 @@
|
|||||||
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