Compare commits

..

2 Commits

Author SHA1 Message Date
f3nris 12f836661b feat(sentinelone): full command coverage (70 commands, XSOAR parity)
Expand from 9 to 70 commands covering the official + DEV XSOAR SentinelOne V2
integrations: agents (actions/info/tags/count), threats & alerts (verdict/status/
notes/mitigate/fetch-file), hash blocklist & exclusions, IOCs, STAR rules,
Deep Visibility, remote scripts, endpoint tags, firewall rules and network
discovery. All script-based (urllib, INTEGRATION_SECRETS/INPUTS contract);
inputs prioritized from the DEV integration.
2026-06-22 12:48:43 +02:00
f3nris b52a5c3789 feat(sentinelone): curated EDR integration (9 analyst commands, API v2.1)
Endpoints derived from the official + custom XSOAR SentinelOne V2 integrations.
Script-based (urllib, INTEGRATION_SECRETS/INPUTS contract) to express S1's nested
filter bodies. Config: console url + api_token (ApiToken header).

Commands:
- enrich: get_threats, list_agents, get_agent, get_hash_verdict
- respond: isolate_agent (disconnect), reconnect_agent (connect), mitigate_threat
  (kill/quarantine/remediate/rollback), initiate_scan, write_threat_note
2026-06-22 12:31:45 +02:00
1838 changed files with 105 additions and 150589 deletions
+5 -215
View File
@@ -26,14 +26,8 @@ and walks the whole tree looking for files named exactly **`manifest.yaml`**.
integrations/
└── <integration-id>/
├── manifest.yaml # required — the integration definition
├── scripts/ # optional — only for script-based commands
│ └── <command-id>.py
├── mappers/ # optional — bundled mapper templates, one per command
│ └── <command-id>.yaml
├── incident-types/ # optional — bundled incident types
│ └── <anything>.yaml
└── agents/ # optional — the operator agent (Riposte 0.302.0+)
└── operator.yaml
└── scripts/ # optional — only for script-based commands
└── <command-id>.py
```
---
@@ -77,7 +71,6 @@ commands:
- id: get_ip_report
name: Get IP report
description: Reputation for an IP address.
risk: read # read | safe_write | destructive — see below
inputs_schema:
properties:
ip:
@@ -100,7 +93,6 @@ commands:
- id: enrich_custom
name: Custom enrichment
description: Runs scripts/enrich_custom.py in a sandbox.
risk: read
inputs_schema:
properties:
indicator:
@@ -115,206 +107,6 @@ A command is **request-based** when it has a `request:` block, or
**script-based** when a matching `scripts/<command-id>.py` exists. Prefer
request-based commands: they need no sandbox and are easier to audit.
### Inputs with a list of values
An input can offer its values in the playbook editor instead of a free-text box.
- **A fixed list** — `enum:`, when the values are the vendor's own and the same
everywhere (a verdict, a status). The editor shows a drop-down, and the
command bar refuses a value outside it.
- **A list read from the tenant** — `options_from:`, when the values belong to
each customer's tool (projects, issue types, priorities). The editor runs the
named command on the instance and lists what it returns; a typed value or an
expression is still accepted.
```yaml
issue_type_name:
type: string
options_from:
command: list_issue_types # a command of this integration, risk: read only
path: issue_types # where the rows are in its output (empty: the output itself)
value: name # row field written into the input
label: name # row field shown in the list (defaults to value)
depends_on: [project_key] # sibling inputs passed to the command, when filled with a literal
```
Riposte refuses an `options_from` command that is not `risk: read`: opening a
field must never change anything in the customer's tool.
### Multi-line inputs
`format: multiline` (or `format: markdown`, which says what the text is) makes
the playbook editor show a resizable text area instead of a one-line field
(Riposte 0.322+). Use it for a mail body, a comment, a markdown description:
a one-line field turns the line breaks of a pasted text into spaces. Older
Riposte versions ignore `format` and keep the one-line field.
```yaml
description: { type: string, format: markdown, description: "Body in markdown" }
```
### `risk:` — the agent approval gate
Every command declares a tier. It is what Riposte's agent approval gate reads:
`read` runs unattended, `safe_write` and `destructive` sit behind the operator's
configured threshold. An omitted `risk` is treated as **destructive** — safe, but
it makes a lookup as noisy as an isolation, so declare it rather than default it.
| Tier | What belongs there |
|------|--------------------|
| `read` | Only queries. Changes nothing outside Riposte: lookups, searches, telemetry, fetching a job's result. |
| `safe_write` | Additive or reversible, inside the tool's own case/enrichment plane: comments, notes, tags, tickets, alert statuses, threat-intel records, sandbox submissions, collection jobs, outbound messages. |
| `destructive` | Changes the estate or weakens a control: containment **and its removal**, blocklists and allowlists, exclusions and detection rules, every delete, identity and credential operations, running code on a host, generic API escape hatches. |
Two rules the catalog applies consistently, because they are not obvious:
- **Undoing a containment is tiered like the containment it lifts.** Un-isolating
a host or un-banning an IP mid-incident is its own decision, not a return to a
neutral state.
- **Retrieving a live secret is `destructive`.** It is a `GET`, but the tier
describes the stake, not the HTTP verb.
---
## Bundled mappers (`mappers/<command-id>.yaml`)
A mapper turns one raw element of a command's output into an incident: the
fields Riposte shows and routes on, and an OCSF projection for entity
indexing. Bundled mappers are registered as **read-only library templates**
on install and **replaced on every update** — an operator's clones are never
touched, which also means a clone never receives your fixes. Ship correct
templates; let operators clone only to diverge on purpose.
```yaml
name: "My Product Investigations → OCSF" # required — shown in the mapper library
description: |
Free text. Say what one element looks like and what the mapper reads.
# Incident fields. Target: title, severity, source, description, custom:<name>.
field_mappings:
title: "title" # dotted path into the element
severity: "priority" # values run through toSeverity (CRITICAL→5 … LOW→2)
"custom:vendor_status": "status" # lands in the declared custom field <name>
# OCSF projection. source_path is full JSONata; ocsf_field is the destination.
ocsf:
- { source_path: "rrn", ocsf_field: "finding_info.uid" }
- { source_path: "created_time", ocsf_field: "finding_info.created_time" }
```
### Expressions: dotted paths and `jsonata:`
A `field_mappings` expression is a dotted path (with an optional
`cond ? then : else` ternary). Prefixed with **`jsonata:`**, it is evaluated
by the full JSONata engine instead — the same one `ocsf.source_path` always
uses — which can index arrays, filter, and reshape:
```yaml
field_mappings:
"custom:evidence": "jsonata: $toTable(alerts.evidences.fields[])"
"custom:alert_name": "jsonata: $parseJson(alerts[0].evidences[0].data).source_json.AlertDisplayName"
severity: "jsonata: priority = 'HIGH' ? 4 : 2"
```
Two Riposte extensions exist alongside the standard JSONata functions:
| Function | What it does |
|----------|--------------|
| `$parseJson(str)` | Opens a JSON-encoded string — the original log line most SIEMs bury inside a JSON string field. |
| `$toTable(x [, cols])` | Turns an array of objects into rows for a `table` custom field (a single object becomes key/value rows). `cols` keeps only the named columns. |
Mind JSONata's singleton semantics: a path that finds **one** element unwraps
it from its array. End the path with `[]` (`alerts.evidences.fields[]`) so a
one-alert investigation still produces table rows instead of key/value pairs.
**Convention: reshape in the mapper, not in the script.** A command script
should return what the API gives, plus at most the flattening the API forces
on it. Building display structures in Python (pre-joined rows, markdown
summaries) welds them to the integration version — as expressions in the
mapper they are visible, testable against the rule's captured samples
(editor → Test), and an operator can adapt them without a package release.
Pre-1.11 integrations that ship pre-built fields keep working; don't extend
the pattern.
An expression that fails costs its own field and never the alert — silently,
by design. The mapper editor's **Test** button is where those errors surface,
named by field, evaluated against the rule's last captured samples.
---
## Bundled operator agents (`agents/*.yaml`)
An integration can ship the **operator agent** that knows how to use it.
Installing the integration in Riposte (0.302.0 and later) creates the agent in the
crew, named `<handle> · <integration name>`; older versions ignore the folder.
The sheet carries what took a real investigation to learn — the vendor's traps,
the order to call things in — so nobody has to rebuild it by hand.
```yaml
key: operator # stable id within the package; one agent per key
handle: Static # an invented netrunner handle, never an existing character
title: Rapid7 InsightIDR operator
role: analyst_l2 # must hold integrations.execute
approval_threshold: safe_write
max_tool_calls: 10
when_to_delegate: | # the only thing an orchestrator reads to route work to it
…
needs: |
…
returns: |
…
instructions: | # the vendor know-how: procedure and traps
…
commands: [list_investigation_alerts, get_alert_actors] # command ids of THIS manifest
riposte_tools: [get_incident, get_incident_context]
```
Riposte **refuses the sheet** — and still installs the integration — when it:
- lists a command this manifest does not declare, or one that is not `risk: read`
(an omitted risk counts as destructive);
- lists a Riposte tool that is not read-tier;
- names a role without `integrations.execute`. `viewer`, `auditor`,
`mcp_readonly` and `analyst_l1` lack it, and with them every integration
command would be invisible to the agent;
- has no `key`, `handle`, `role`, `when_to_delegate` or command.
The model is not part of the sheet: operators borrow Riposte's **Crew** role. A
sheet nobody edited follows the package's updates; an edited one keeps its edit
and offers the new version for review. Removing a sheet from the package retires
its agent on the next install.
## Well-known commands: the on-call router
Most command ids are an integration's own business. **Three are a contract**, and
declaring them is how an integration becomes something Riposte's pager can hand its
on-call ladder to — a telephony carrier that will ring the on-call team when a
client calls the number you published to them.
| Command | Inputs | What it must do |
|---|---|---|
| `set_oncall_ladder` | `numbers` (ordered, E.164), `ring_seconds` | Ring those numbers, **in that order**, one after another. A carrier that can only hold ONE destination takes `numbers[0]` and says so in its description — a single rung that follows the rota still beats a redirect a human has to edit. Must be idempotent: Riposte republishes on a schedule. |
| `get_oncall_ladder` | — | Answer `{ numbers: [...] }`: what the carrier **actually holds**, not what was last sent. Optional, and what makes Riposte's comparison honest. |
| `list_calls` | `since`, `limit` | Answer `{ calls: [...] }` with `from_number`, `did`, `started_at`, `duration`. Optional; it fills the pager journal after the fact. |
**The id is the whole contract.** There is no registry, no manifest field to set and
no vendor list anywhere in Riposte: the pager's settings screen offers whichever
instances declare `set_oncall_ladder`.
**Declarative, never reactive.** Riposte says *who* to ring; the carrier decides
*when*. A contract shaped the other way round — the carrier calling us as each call
arrives — is a webhook, and a self-hosted SOAR on a VM with no inbound path from the
internet cannot receive one.
Two things a router should be honest about in its description, because they decide
whether an operator should use it at all: whether the carrier can **ask the answerer
to press a key** before connecting the caller (most cannot, so a mobile's voicemail
ends the search as though a person had answered), and whether ringing a rung costs
anything on the operator's own trunk.
`integrations/ovhcloud` implements all three against a VoIP call queue; read it
before writing another.
---
## Adding this catalog to Riposte
@@ -337,11 +129,9 @@ Then **Sync**. Discovered integrations appear in the marketplace, ready to insta
1. Create `integrations/<id>/manifest.yaml` (one directory per integration).
2. Add `scripts/<command-id>.py` only for script-based commands.
3. Add `mappers/<command-id>.yaml` for every ingestion command — reshape with
`jsonata:` expressions there rather than in the script (see above).
4. Bump `version` (semver) on every change — Riposte tracks versions per source.
5. Validate the YAML parses and `id`/`name` are set.
6. Open a merge request.
3. Bump `version` (semver) on every change — Riposte tracks versions per source.
4. Validate the YAML parses and `id`/`name` are set.
5. Open a merge request.
See [`templates/manifest.example.yaml`](templates/manifest.example.yaml) for a
fully-commented starting point (that file is intentionally **not** ingested).
@@ -1,61 +0,0 @@
# The AbuseIPDB operator. Installed with the integration by Riposte 0.302.0
# and later (older versions ignore this folder). Read-only by design: it never
# reports an IP.
key: operator
handle: Tally
title: AbuseIPDB operator
role: analyst_l2 # needs integrations.execute — viewer would hide every command
approval_threshold: safe_write
max_tool_calls: 10
when_to_delegate: |
What AbuseIPDB's community reports say about an IP address or a small
network: abuse confidence score, number of reports and reporters, the
abuse categories reported, last reported date, ISP, usage type, country,
whether it is a Tor exit or whitelisted. IPs and CIDR blocks only. Not for
hashes, domains or URLs, and never for reporting an IP.
needs: |
The IP addresses (or a CIDR block, /24 at most) to check, and optionally
how far back reports should count (default 30 days). Or a Riposte alert
id, to pick the IPs from it.
returns: |
Per IP, facts from AbuseIPDB with the command that produced each:
abuseConfidenceScore, totalReports, numDistinctUsers, lastReportedAt,
isp, usageType, countryCode, isTor, isWhitelisted, and the most frequent
report categories when asked. "no reports" when totalReports is 0. The
score is attributed to AbuseIPDB, never your own verdict.
instructions: |
Your budget is 10 calls. Spend it in this order:
1. Only if you were given a Riposte alert id: get_incident_context (1 call)
and pick the public IPs from it. Otherwise skip.
2. Per IP: check_ip(ip=<ip>) with max_age_days 30 (default; at most 365).
Add verbose true only when the lead wants the report comments and
categories: it makes the answer much longer.
3. check_block(network=<CIDR>) only when the lead asks about the
neighbourhood of an IP. The free tier accepts /24 at most; write the
network as the lead gave it or as x.y.z.0/24 from an IP you were given.
Traps:
- A score of 0 with totalReports 0 means nobody reported it, not that it is
safe. isWhitelisted true means AbuseIPDB marks it as a known good
service. Report both as AbuseIPDB states them.
- usageType (e.g. "Data Center/Web Hosting/Transit") and isp often matter
more than the score: always include them.
- Private, loopback or link-local IPs are refused or meaningless: skip them
and say so.
- HTTP 422 means the IP or network was malformed: report it, do not adapt
the value. HTTP 429 is the daily quota: report it and stop.
- Copy IPs only from what you were given or a previous answer. Report
lastReportedAt exactly as the data gives it.
- On any other error: report the exact call and error, retry at most once.
commands:
- check_ip
- check_block
riposte_tools:
- get_incident
- get_incident_context
-68
View File
@@ -1,68 +0,0 @@
id: abuseipdb
name: AbuseIPDB
version: 1.1.1
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.1.0 — Declare the risk tier on the 1 command that carried none, all of them destructive. An unset risk was already gated as destructive, so the approval prompt does not move; what changes is that the tier is a stated decision rather than a default, and a command added later cannot inherit it by omission. | 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."
risk: destructive
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,64 +0,0 @@
# The Active Directory operator. Installed with the integration by Riposte
# 0.302.0 and later (older versions ignore this folder). Read-only by design:
# Riposte refuses a sheet that lists a write.
key: operator
handle: Ledger
title: Active Directory operator
role: analyst_l2 # needs integrations.execute — viewer would hide every command
approval_threshold: safe_write
max_tool_calls: 10
when_to_delegate: |
Who an on-prem Active Directory account or computer is: the user's
name, mail, manager, groups, and account flags (disabled, locked out,
password never expires...), last logon and password age; a computer's
groups and OS; who is in a group. It cannot show sign-in events: no
command does. Not for Entra ID (cloud) accounts, and not for changing any
account.
needs: |
The user's sAMAccountName (e.g. jdoe, without DOMAIN\) or e-mail, a
computer name, or a group. The Riposte alert id if the user or host must
be read from the alert.
returns: |
Facts from Active Directory, each with the command that produced it;
"not found" when a lookup came back empty; the exact call and error when
one failed. No verdict.
instructions: |
Your budget is 10 calls. Spend it in this order:
1. If you were given an alert: get_incident_context (1 call) and read the
user and host from it, exactly as written. DOMAIN\jdoe becomes jdoe;
jdoe@corp.com is an e-mail or UPN: use the email input.
2. ad_get_user (1 call) with username=<sAMAccountName>, and
attributes "lastLogonTimestamp,pwdLastSet,whenCreated,lockoutTime"
(commas, no spaces).
Prefer username or email over a DN. Read userAccountControlFields for
the flags (disabled, locked out, password never expires). Report the
time attributes exactly as returned; do not convert them.
0 users: try once with email or name, then report "not found".
3. memberOf lists the groups. Name the privileged ones apart (Domain
Admins, Enterprise Admins, Administrators, and any name with "admin").
4. ad_get_computer(name=<host>) (1 call) for the host of the alert, with
attributes "operatingSystem,lastLogonTimestamp,whenCreated".
5. Optional: ad_get_group_members with group-dn copied from a memberOf
value (never typed or adapted), limit 50 (1-2 calls). Always set
limit: without it the whole group comes back.
6. Optional: ad_search for what the other commands cannot ask, with an
LDAP filter in the form the manifest shows,
(&(objectCategory=person)(objectClass=user)), size-limit 20. Only
when you are sure of the filter; otherwise skip it.
Take DNs and names only from the alert or a previous answer. On an error
("LDAP error: ..."): report the exact call and error; retry at most once.
commands:
- ad_get_user
- ad_get_computer
- ad_get_group_members
- ad_search
riposte_tools:
- get_incident
- get_incident_context
-366
View File
@@ -1,366 +0,0 @@
id: active_directory
name: Active Directory
version: 1.1.1
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.1.0 — Declare the risk tier on the 18 commands that carried none: 3 safe_write, 15 destructive. An unset risk is read as destructive by the agent approval gate, so the 3 that only annotate sat behind the same prompt as the ones that change the estate. | 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."
risk: destructive
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."
risk: destructive
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."
risk: destructive
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)."
risk: destructive
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."
risk: destructive
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."
risk: destructive
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."
risk: destructive
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."
risk: destructive
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."
risk: destructive
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."
risk: destructive
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)."
risk: destructive
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."
risk: destructive
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."
risk: destructive
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."
risk: safe_write
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."
risk: safe_write
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."
risk: safe_write
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."
risk: destructive
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."
risk: destructive
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
-101
View File
@@ -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,113 +0,0 @@
id: anomali_threatstream
name: Anomali ThreatStream
version: 1.1.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.1.0 — Declare the risk tier on the 1 command that carried none: 1 safe_write. An unset risk is read as destructive by the agent approval gate, so the 1 that only annotate sat behind the same prompt as the ones that change the estate. | 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)."
risk: safe_write
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)
-62
View File
@@ -1,62 +0,0 @@
# The ANY.RUN operator. Installed with the integration by Riposte 0.302.0 and
# later (older versions ignore this folder). Read-only by design: it never
# detonates a file or URL and never deletes a task.
key: operator
handle: Gauntlet
title: ANY.RUN operator
role: analyst_l2 # needs integrations.execute — viewer would hide every command
approval_threshold: safe_write
max_tool_calls: 10
when_to_delegate: |
What our ANY.RUN account already found about a file or URL it analysed
before: the task, its verdict and threat level, tags, the processes,
network connections and MITRE techniques the report shows. Only tasks run
from this account. Not for detonating anything new — this operator cannot
submit — and not for reputation of IPs or domains.
needs: |
The ANY.RUN task id if known. Otherwise the file hash (SHA256 preferred),
file name or URL to look for in the account's history. Or a Riposte alert
id, to pick them from it.
returns: |
Per sample, facts from ANY.RUN with the command that produced each: the
task (id, date, name), verdict / threat level, tags, and from the report
the main processes, contacted domains and IPs, and techniques. "no existing
analysis in the account's history" when nothing matched — a new detonation
is a human decision. Verdicts are attributed to ANY.RUN, never your own.
instructions: |
Your budget is 10 calls. Spend it in this order:
1. Only if you were given a Riposte alert id: get_incident_context (1 call)
and pick the hashes, file names and URLs from it. Otherwise skip.
2. If the lead gave a task id: go to step 4.
3. Find the task: get_history(limit 50) (1 call, at most 2 with skip 50).
There is no hash search: look in each task for your SHA256 / MD5 in its
hashes, your file name in its name, or your URL. Match the hash exactly;
a matching name alone is weak: say so.
4. get_report(task_id=<uuid from the history entry or from the lead>) for
at most 2 tasks. Read the verdict / threat level and tags, then the
processes, the network (domains, IPs, HTTP requests) and the MITRE
techniques. Keep the top items; the report is long.
Traps:
- You cannot detonate. If nothing is found, say "no existing analysis" and
that a detonation would need a human; do not suggest you will do it.
- The history holds only this account's tasks, newest first, not ANY.RUN's
public submissions: "not in history" does not mean ANY.RUN never saw it.
- A task still running has no final verdict: report its status, do not read
it as clean.
- Take task ids only from a previous answer or the lead, never type or
adapt one (they are long UUIDs). Report dates exactly as given.
- HTTP 429 or a limits error: report it and stop.
- On any other error: report the exact call and error, retry at most once.
commands:
- get_history
- get_report
riposte_tools:
- get_incident
- get_incident_context
-86
View File
@@ -1,86 +0,0 @@
id: anyrun
name: ANY.RUN
version: 1.1.1
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.1.0 — Declare the risk tier on the 3 commands that carried none: 2 safe_write, 1 destructive. An unset risk is read as destructive by the agent approval gate, so the 2 that only annotate sat behind the same prompt as the ones that change the estate. | 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."
risk: safe_write
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."
risk: safe_write
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."
risk: destructive
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)
-53
View File
@@ -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)
-71
View File
@@ -1,71 +0,0 @@
id: armis
name: Armis
version: 1.1.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.1.0 — Declare the risk tier on the 1 command that carried none: 1 safe_write. An unset risk is read as destructive by the agent approval gate, so the 1 that only annotate sat behind the same prompt as the ones that change the estate. | 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."
risk: safe_write
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: {} }
-69
View File
@@ -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)
-67
View File
@@ -1,67 +0,0 @@
id: automox
name: Automox
version: 1.1.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.1.0 — Declare the risk tier on the 1 command that carried none, all of them destructive. An unset risk was already gated as destructive, so the approval prompt does not move; what changes is that the tier is a stated decision rather than a default, and a command added later cannot inherit it by omission. | 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)."
risk: destructive
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)
-131
View File
@@ -1,131 +0,0 @@
id: aws
name: AWS
version: 1.1.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.1.0 — Declare the risk tier on the 5 commands that carried none, all of them destructive. An unset risk was already gated as destructive, so the approval prompt does not move; what changes is that the tier is a stated decision rather than a default, and a command added later cannot inherit it by omission. | 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."
risk: destructive
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)."
risk: destructive
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)."
risk: destructive
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."
risk: destructive
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)."
risk: destructive
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)
-148
View File
@@ -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)
-130
View File
@@ -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)
-76
View File
@@ -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)
-123
View File
@@ -1,123 +0,0 @@
id: azure_security
name: Microsoft Azure
version: 1.1.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.1.0 — Declare the risk tier on the 3 commands that carried none: 1 safe_write, 2 destructive. An unset risk is read as destructive by the agent approval gate, so the 1 that only annotate sat behind the same prompt as the ones that change the estate. | 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)."
risk: safe_write
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)."
risk: destructive
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."
risk: destructive
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)

Some files were not shown because too many files have changed in this diff Show More