Compare commits
2 Commits
main
..
12f836661b
| Author | SHA1 | Date | |
|---|---|---|---|
| 12f836661b | |||
| b52a5c3789 |
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -1,53 +0,0 @@
|
||||
import base64, json, os, sys, urllib.parse, urllib.request, urllib.error
|
||||
|
||||
API = "https://api.any.run/v1"
|
||||
|
||||
|
||||
def _cfg():
|
||||
return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||
|
||||
|
||||
def _headers(extra=None):
|
||||
h = {"Accept": "application/json", "Authorization": "API-Key " + str(_cfg().get("api_key") or "")}
|
||||
if extra:
|
||||
h.update(extra)
|
||||
return h
|
||||
|
||||
|
||||
def request(method, path, params=None, form=None):
|
||||
url = API + path
|
||||
p = {k: str(x) for k, x in (params or {}).items() if x not in (None, "")}
|
||||
if p:
|
||||
url += ("&" if "?" in url else "?") + urllib.parse.urlencode(p)
|
||||
data = None
|
||||
headers = _headers()
|
||||
if form is not None:
|
||||
data = urllib.parse.urlencode({k: str(v) for k, v in form.items() if v not in (None, "")}).encode("utf-8")
|
||||
headers["Content-Type"] = "application/x-www-form-urlencoded"
|
||||
req = urllib.request.Request(url, data=data, headers=headers, method=method)
|
||||
with urllib.request.urlopen(req, timeout=90) as r:
|
||||
raw = r.read()
|
||||
return json.loads(raw) if raw else {}
|
||||
|
||||
|
||||
q = lambda v: urllib.parse.quote(str(v), safe="")
|
||||
|
||||
|
||||
def main():
|
||||
inputs = json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||
task_id = inputs.get("task_id")
|
||||
if not task_id:
|
||||
raise Exception("task_id is required")
|
||||
|
||||
result = request("GET", "/analysis/" + q(task_id))
|
||||
print(json.dumps(result))
|
||||
|
||||
|
||||
try:
|
||||
main()
|
||||
except urllib.error.HTTPError as e:
|
||||
print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")}))
|
||||
sys.exit(1)
|
||||
except Exception as e:
|
||||
print(json.dumps({"error": str(e)}))
|
||||
sys.exit(1)
|
||||
@@ -1,45 +0,0 @@
|
||||
import base64, json, os, sys, urllib.parse, urllib.request, urllib.error
|
||||
|
||||
API = "https://api.any.run/v1"
|
||||
|
||||
|
||||
def _cfg():
|
||||
return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||
|
||||
|
||||
def _headers(extra=None):
|
||||
h = {"Accept": "application/json", "Authorization": "API-Key " + str(_cfg().get("api_key") or "")}
|
||||
if extra:
|
||||
h.update(extra)
|
||||
return h
|
||||
|
||||
|
||||
def request(method, path, params=None, form=None):
|
||||
url = API + path
|
||||
p = {k: str(x) for k, x in (params or {}).items() if x not in (None, "")}
|
||||
if p:
|
||||
url += ("&" if "?" in url else "?") + urllib.parse.urlencode(p)
|
||||
data = None
|
||||
headers = _headers()
|
||||
if form is not None:
|
||||
data = urllib.parse.urlencode({k: str(v) for k, v in form.items() if v not in (None, "")}).encode("utf-8")
|
||||
headers["Content-Type"] = "application/x-www-form-urlencoded"
|
||||
req = urllib.request.Request(url, data=data, headers=headers, method=method)
|
||||
with urllib.request.urlopen(req, timeout=90) as r:
|
||||
raw = r.read()
|
||||
return json.loads(raw) if raw else {}
|
||||
|
||||
|
||||
def main():
|
||||
result = request("GET", "/user/limits")
|
||||
print(json.dumps(result))
|
||||
|
||||
|
||||
try:
|
||||
main()
|
||||
except urllib.error.HTTPError as e:
|
||||
print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")}))
|
||||
sys.exit(1)
|
||||
except Exception as e:
|
||||
print(json.dumps({"error": str(e)}))
|
||||
sys.exit(1)
|
||||
@@ -1,47 +0,0 @@
|
||||
import base64, json, os, sys, urllib.parse, urllib.request, urllib.error
|
||||
|
||||
API = "https://api.any.run/v1"
|
||||
|
||||
|
||||
def _cfg():
|
||||
return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||
|
||||
|
||||
def _headers(extra=None):
|
||||
h = {"Accept": "application/json", "Authorization": "API-Key " + str(_cfg().get("api_key") or "")}
|
||||
if extra:
|
||||
h.update(extra)
|
||||
return h
|
||||
|
||||
|
||||
def request(method, path, params=None, form=None):
|
||||
url = API + path
|
||||
p = {k: str(x) for k, x in (params or {}).items() if x not in (None, "")}
|
||||
if p:
|
||||
url += ("&" if "?" in url else "?") + urllib.parse.urlencode(p)
|
||||
data = None
|
||||
headers = _headers()
|
||||
if form is not None:
|
||||
data = urllib.parse.urlencode({k: str(v) for k, v in form.items() if v not in (None, "")}).encode("utf-8")
|
||||
headers["Content-Type"] = "application/x-www-form-urlencoded"
|
||||
req = urllib.request.Request(url, data=data, headers=headers, method=method)
|
||||
with urllib.request.urlopen(req, timeout=90) as r:
|
||||
raw = r.read()
|
||||
return json.loads(raw) if raw else {}
|
||||
|
||||
|
||||
def main():
|
||||
result = request("GET", "/user/limits")
|
||||
if not isinstance(result, dict):
|
||||
raise Exception("unexpected response")
|
||||
print(json.dumps({"ok": True}))
|
||||
|
||||
|
||||
try:
|
||||
main()
|
||||
except urllib.error.HTTPError as e:
|
||||
print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")}))
|
||||
sys.exit(1)
|
||||
except Exception as e:
|
||||
print(json.dumps({"error": str(e)}))
|
||||
sys.exit(1)
|
||||
@@ -1,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: {} }
|
||||
@@ -1,69 +0,0 @@
|
||||
import json, os, sys, urllib.parse, urllib.request, urllib.error
|
||||
|
||||
|
||||
def _cfg():
|
||||
return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||
|
||||
|
||||
def _inputs():
|
||||
return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||
|
||||
|
||||
def _base(cfg):
|
||||
return str(cfg.get("base_url", "")).rstrip("/") + "/api/v1"
|
||||
|
||||
|
||||
def _token(cfg):
|
||||
form = urllib.parse.urlencode({"secret_key": str(cfg.get("secret_key", ""))}).encode("utf-8")
|
||||
req = urllib.request.Request(_base(cfg) + "/access_token/", data=form,
|
||||
headers={"Content-Type": "application/x-www-form-urlencoded",
|
||||
"Accept": "application/json"}, method="POST")
|
||||
with urllib.request.urlopen(req, timeout=60) as r:
|
||||
tok = json.loads(r.read())
|
||||
at = (tok.get("data") or {}).get("access_token")
|
||||
if not at:
|
||||
raise Exception("Token request failed: " + json.dumps(tok))
|
||||
return at
|
||||
|
||||
|
||||
def request(method, path, cfg, token, body=None, params=None):
|
||||
url = _base(cfg) + path
|
||||
if params:
|
||||
clean = {k: v for k, v in params.items() if v not in (None, "")}
|
||||
if clean:
|
||||
url += "?" + urllib.parse.urlencode(clean)
|
||||
data = json.dumps(body).encode("utf-8") if body is not None else None
|
||||
headers = {"Authorization": token, "Accept": "application/json"}
|
||||
if data is not None:
|
||||
headers["Content-Type"] = "application/json"
|
||||
req = urllib.request.Request(url, data=data, headers=headers, method=method)
|
||||
with urllib.request.urlopen(req, timeout=90) as r:
|
||||
raw = r.read()
|
||||
return json.loads(raw) if raw else {}
|
||||
|
||||
|
||||
def _run(fn):
|
||||
try:
|
||||
cfg = _cfg()
|
||||
inputs = _inputs()
|
||||
token = _token(cfg)
|
||||
print(json.dumps(fn(cfg, token, inputs)))
|
||||
except urllib.error.HTTPError as e:
|
||||
print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")}))
|
||||
sys.exit(1)
|
||||
except Exception as e:
|
||||
print(json.dumps({"error": str(e)}))
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
q = lambda v: urllib.parse.quote(str(v), safe="")
|
||||
|
||||
|
||||
def main(cfg, token, inputs):
|
||||
device_id = inputs.get("device_id")
|
||||
if not device_id:
|
||||
raise Exception("device_id is required")
|
||||
return request("GET", "/devices/" + q(device_id) + "/", cfg, token)
|
||||
|
||||
|
||||
_run(main)
|
||||
@@ -1,66 +0,0 @@
|
||||
import json, os, sys, urllib.parse, urllib.request, urllib.error
|
||||
|
||||
|
||||
def _cfg():
|
||||
return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||
|
||||
|
||||
def _inputs():
|
||||
return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||
|
||||
|
||||
def _base(cfg):
|
||||
return str(cfg.get("base_url", "")).rstrip("/") + "/api/v1"
|
||||
|
||||
|
||||
def _token(cfg):
|
||||
form = urllib.parse.urlencode({"secret_key": str(cfg.get("secret_key", ""))}).encode("utf-8")
|
||||
req = urllib.request.Request(_base(cfg) + "/access_token/", data=form,
|
||||
headers={"Content-Type": "application/x-www-form-urlencoded",
|
||||
"Accept": "application/json"}, method="POST")
|
||||
with urllib.request.urlopen(req, timeout=60) as r:
|
||||
tok = json.loads(r.read())
|
||||
at = (tok.get("data") or {}).get("access_token")
|
||||
if not at:
|
||||
raise Exception("Token request failed: " + json.dumps(tok))
|
||||
return at
|
||||
|
||||
|
||||
def request(method, path, cfg, token, body=None, params=None):
|
||||
url = _base(cfg) + path
|
||||
if params:
|
||||
clean = {k: v for k, v in params.items() if v not in (None, "")}
|
||||
if clean:
|
||||
url += "?" + urllib.parse.urlencode(clean)
|
||||
data = json.dumps(body).encode("utf-8") if body is not None else None
|
||||
headers = {"Authorization": token, "Accept": "application/json"}
|
||||
if data is not None:
|
||||
headers["Content-Type"] = "application/json"
|
||||
req = urllib.request.Request(url, data=data, headers=headers, method=method)
|
||||
with urllib.request.urlopen(req, timeout=90) as r:
|
||||
raw = r.read()
|
||||
return json.loads(raw) if raw else {}
|
||||
|
||||
|
||||
def _run(fn):
|
||||
try:
|
||||
cfg = _cfg()
|
||||
inputs = _inputs()
|
||||
token = _token(cfg)
|
||||
print(json.dumps(fn(cfg, token, inputs)))
|
||||
except urllib.error.HTTPError as e:
|
||||
print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")}))
|
||||
sys.exit(1)
|
||||
except Exception as e:
|
||||
print(json.dumps({"error": str(e)}))
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def main(cfg, token, inputs):
|
||||
aql = inputs.get("aql")
|
||||
length = inputs.get("length")
|
||||
aql_str = "in:alerts" + ((" " + aql) if aql else "")
|
||||
return request("GET", "/search/", cfg, token, params={"aql": aql_str, "length": int(length or 50)})
|
||||
|
||||
|
||||
_run(main)
|
||||
@@ -1,66 +0,0 @@
|
||||
import json, os, sys, urllib.parse, urllib.request, urllib.error
|
||||
|
||||
|
||||
def _cfg():
|
||||
return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||
|
||||
|
||||
def _inputs():
|
||||
return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||
|
||||
|
||||
def _base(cfg):
|
||||
return str(cfg.get("base_url", "")).rstrip("/") + "/api/v1"
|
||||
|
||||
|
||||
def _token(cfg):
|
||||
form = urllib.parse.urlencode({"secret_key": str(cfg.get("secret_key", ""))}).encode("utf-8")
|
||||
req = urllib.request.Request(_base(cfg) + "/access_token/", data=form,
|
||||
headers={"Content-Type": "application/x-www-form-urlencoded",
|
||||
"Accept": "application/json"}, method="POST")
|
||||
with urllib.request.urlopen(req, timeout=60) as r:
|
||||
tok = json.loads(r.read())
|
||||
at = (tok.get("data") or {}).get("access_token")
|
||||
if not at:
|
||||
raise Exception("Token request failed: " + json.dumps(tok))
|
||||
return at
|
||||
|
||||
|
||||
def request(method, path, cfg, token, body=None, params=None):
|
||||
url = _base(cfg) + path
|
||||
if params:
|
||||
clean = {k: v for k, v in params.items() if v not in (None, "")}
|
||||
if clean:
|
||||
url += "?" + urllib.parse.urlencode(clean)
|
||||
data = json.dumps(body).encode("utf-8") if body is not None else None
|
||||
headers = {"Authorization": token, "Accept": "application/json"}
|
||||
if data is not None:
|
||||
headers["Content-Type"] = "application/json"
|
||||
req = urllib.request.Request(url, data=data, headers=headers, method=method)
|
||||
with urllib.request.urlopen(req, timeout=90) as r:
|
||||
raw = r.read()
|
||||
return json.loads(raw) if raw else {}
|
||||
|
||||
|
||||
def _run(fn):
|
||||
try:
|
||||
cfg = _cfg()
|
||||
inputs = _inputs()
|
||||
token = _token(cfg)
|
||||
print(json.dumps(fn(cfg, token, inputs)))
|
||||
except urllib.error.HTTPError as e:
|
||||
print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")}))
|
||||
sys.exit(1)
|
||||
except Exception as e:
|
||||
print(json.dumps({"error": str(e)}))
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def main(cfg, token, inputs):
|
||||
aql = inputs.get("aql")
|
||||
length = inputs.get("length")
|
||||
aql_str = "in:devices" + ((" " + aql) if aql else "")
|
||||
return request("GET", "/search/", cfg, token, params={"aql": aql_str, "length": int(length or 50)})
|
||||
|
||||
|
||||
_run(main)
|
||||
@@ -1,64 +0,0 @@
|
||||
import json, os, sys, urllib.parse, urllib.request, urllib.error
|
||||
|
||||
|
||||
def _cfg():
|
||||
return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||
|
||||
|
||||
def _inputs():
|
||||
return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||
|
||||
|
||||
def _base(cfg):
|
||||
return str(cfg.get("base_url", "")).rstrip("/") + "/api/v1"
|
||||
|
||||
|
||||
def _token(cfg):
|
||||
form = urllib.parse.urlencode({"secret_key": str(cfg.get("secret_key", ""))}).encode("utf-8")
|
||||
req = urllib.request.Request(_base(cfg) + "/access_token/", data=form,
|
||||
headers={"Content-Type": "application/x-www-form-urlencoded",
|
||||
"Accept": "application/json"}, method="POST")
|
||||
with urllib.request.urlopen(req, timeout=60) as r:
|
||||
tok = json.loads(r.read())
|
||||
at = (tok.get("data") or {}).get("access_token")
|
||||
if not at:
|
||||
raise Exception("Token request failed: " + json.dumps(tok))
|
||||
return at
|
||||
|
||||
|
||||
def request(method, path, cfg, token, body=None, params=None):
|
||||
url = _base(cfg) + path
|
||||
if params:
|
||||
clean = {k: v for k, v in params.items() if v not in (None, "")}
|
||||
if clean:
|
||||
url += "?" + urllib.parse.urlencode(clean)
|
||||
data = json.dumps(body).encode("utf-8") if body is not None else None
|
||||
headers = {"Authorization": token, "Accept": "application/json"}
|
||||
if data is not None:
|
||||
headers["Content-Type"] = "application/json"
|
||||
req = urllib.request.Request(url, data=data, headers=headers, method=method)
|
||||
with urllib.request.urlopen(req, timeout=90) as r:
|
||||
raw = r.read()
|
||||
return json.loads(raw) if raw else {}
|
||||
|
||||
|
||||
def _run(fn):
|
||||
try:
|
||||
cfg = _cfg()
|
||||
inputs = _inputs()
|
||||
token = _token(cfg)
|
||||
print(json.dumps(fn(cfg, token, inputs)))
|
||||
except urllib.error.HTTPError as e:
|
||||
print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")}))
|
||||
sys.exit(1)
|
||||
except Exception as e:
|
||||
print(json.dumps({"error": str(e)}))
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def main(cfg, token, inputs):
|
||||
request("GET", "/search/", cfg, token, params={"aql": "in:devices", "length": 1})
|
||||
return {"ok": True}
|
||||
|
||||
|
||||
_run(main)
|
||||
@@ -1,75 +0,0 @@
|
||||
import json, os, sys, urllib.parse, urllib.request, urllib.error
|
||||
|
||||
|
||||
def _cfg():
|
||||
return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||
|
||||
|
||||
def _inputs():
|
||||
return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||
|
||||
|
||||
def _base(cfg):
|
||||
return str(cfg.get("base_url", "")).rstrip("/") + "/api/v1"
|
||||
|
||||
|
||||
def _token(cfg):
|
||||
form = urllib.parse.urlencode({"secret_key": str(cfg.get("secret_key", ""))}).encode("utf-8")
|
||||
req = urllib.request.Request(_base(cfg) + "/access_token/", data=form,
|
||||
headers={"Content-Type": "application/x-www-form-urlencoded",
|
||||
"Accept": "application/json"}, method="POST")
|
||||
with urllib.request.urlopen(req, timeout=60) as r:
|
||||
tok = json.loads(r.read())
|
||||
at = (tok.get("data") or {}).get("access_token")
|
||||
if not at:
|
||||
raise Exception("Token request failed: " + json.dumps(tok))
|
||||
return at
|
||||
|
||||
|
||||
def request(method, path, cfg, token, body=None, params=None):
|
||||
url = _base(cfg) + path
|
||||
if params:
|
||||
clean = {k: v for k, v in params.items() if v not in (None, "")}
|
||||
if clean:
|
||||
url += "?" + urllib.parse.urlencode(clean)
|
||||
data = json.dumps(body).encode("utf-8") if body is not None else None
|
||||
headers = {"Authorization": token, "Accept": "application/json"}
|
||||
if data is not None:
|
||||
headers["Content-Type"] = "application/json"
|
||||
req = urllib.request.Request(url, data=data, headers=headers, method=method)
|
||||
with urllib.request.urlopen(req, timeout=90) as r:
|
||||
raw = r.read()
|
||||
return json.loads(raw) if raw else {}
|
||||
|
||||
|
||||
def _run(fn):
|
||||
try:
|
||||
cfg = _cfg()
|
||||
inputs = _inputs()
|
||||
token = _token(cfg)
|
||||
print(json.dumps(fn(cfg, token, inputs)))
|
||||
except urllib.error.HTTPError as e:
|
||||
print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")}))
|
||||
sys.exit(1)
|
||||
except Exception as e:
|
||||
print(json.dumps({"error": str(e)}))
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
q = lambda v: urllib.parse.quote(str(v), safe="")
|
||||
|
||||
|
||||
def main(cfg, token, inputs):
|
||||
alert_id = inputs.get("alert_id")
|
||||
status = inputs.get("status")
|
||||
if not alert_id:
|
||||
raise Exception("alert_id is required")
|
||||
if not status:
|
||||
raise Exception("status is required")
|
||||
resp = request("PATCH", "/alerts/" + q(alert_id) + "/", cfg, token, body={"status": status})
|
||||
if not resp:
|
||||
return {"ok": True, "alert_id": alert_id}
|
||||
return resp
|
||||
|
||||
|
||||
_run(main)
|
||||
@@ -1,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)
|
||||
@@ -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)
|
||||
@@ -1,148 +0,0 @@
|
||||
import json, os, sys, hmac, hashlib, datetime
|
||||
import urllib.parse, urllib.request, urllib.error
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
|
||||
def _cfg():
|
||||
return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||
|
||||
|
||||
def _inputs():
|
||||
return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||
|
||||
|
||||
def _sign_key(key, date_stamp, region, service):
|
||||
def _h(k, m):
|
||||
return hmac.new(k, m.encode("utf-8"), hashlib.sha256).digest()
|
||||
k_date = _h(("AWS4" + key).encode("utf-8"), date_stamp)
|
||||
k_region = _h(k_date, region)
|
||||
k_service = _h(k_region, service)
|
||||
return _h(k_service, "aws4_request")
|
||||
|
||||
|
||||
def _strip_ns(tag):
|
||||
return tag.split("}", 1)[1] if "}" in tag else tag
|
||||
|
||||
|
||||
def _xml_to_dict(elem):
|
||||
d = {}
|
||||
children = list(elem)
|
||||
if not children:
|
||||
return (elem.text or "").strip()
|
||||
for c in children:
|
||||
tag = _strip_ns(c.tag)
|
||||
val = _xml_to_dict(c)
|
||||
if tag in d:
|
||||
if not isinstance(d[tag], list):
|
||||
d[tag] = [d[tag]]
|
||||
d[tag].append(val)
|
||||
else:
|
||||
d[tag] = val
|
||||
return d
|
||||
|
||||
|
||||
def aws_query(service, host, region, action, version, params, cfg):
|
||||
# params: dict of extra query params for this Action
|
||||
body_params = {"Action": action, "Version": version}
|
||||
body_params.update({k: str(v) for k, v in params.items() if v is not None})
|
||||
body = urllib.parse.urlencode(sorted(body_params.items()))
|
||||
|
||||
access_key = str(cfg.get("access_key_id", ""))
|
||||
secret_key = str(cfg.get("secret_access_key", ""))
|
||||
session_token = cfg.get("session_token") or ""
|
||||
|
||||
now = datetime.datetime.utcnow()
|
||||
amz_date = now.strftime("%Y%m%dT%H%M%SZ")
|
||||
date_stamp = now.strftime("%Y%m%d")
|
||||
|
||||
method = "POST"
|
||||
canonical_uri = "/"
|
||||
canonical_querystring = ""
|
||||
payload_hash = hashlib.sha256(body.encode("utf-8")).hexdigest()
|
||||
canonical_headers = "content-type:application/x-www-form-urlencoded; charset=utf-8\n" \
|
||||
"host:" + host + "\n" \
|
||||
"x-amz-date:" + amz_date + "\n"
|
||||
signed_headers = "content-type;host;x-amz-date"
|
||||
if session_token:
|
||||
canonical_headers += "x-amz-security-token:" + session_token + "\n"
|
||||
signed_headers = "content-type;host;x-amz-date;x-amz-security-token"
|
||||
canonical_request = "\n".join([method, canonical_uri, canonical_querystring,
|
||||
canonical_headers, signed_headers, payload_hash])
|
||||
|
||||
algorithm = "AWS4-HMAC-SHA256"
|
||||
credential_scope = date_stamp + "/" + region + "/" + service + "/aws4_request"
|
||||
string_to_sign = "\n".join([algorithm, amz_date, credential_scope,
|
||||
hashlib.sha256(canonical_request.encode("utf-8")).hexdigest()])
|
||||
signing_key = _sign_key(secret_key, date_stamp, region, service)
|
||||
signature = hmac.new(signing_key, string_to_sign.encode("utf-8"), hashlib.sha256).hexdigest()
|
||||
|
||||
authorization = (algorithm + " Credential=" + access_key + "/" + credential_scope +
|
||||
", SignedHeaders=" + signed_headers + ", Signature=" + signature)
|
||||
headers = {
|
||||
"Content-Type": "application/x-www-form-urlencoded; charset=utf-8",
|
||||
"X-Amz-Date": amz_date,
|
||||
"Authorization": authorization,
|
||||
"Accept": "application/json",
|
||||
}
|
||||
if session_token:
|
||||
headers["X-Amz-Security-Token"] = session_token
|
||||
|
||||
req = urllib.request.Request("https://" + host + "/", data=body.encode("utf-8"), headers=headers, method="POST")
|
||||
with urllib.request.urlopen(req, timeout=60) as r:
|
||||
raw = r.read()
|
||||
root = ET.fromstring(raw)
|
||||
return {_strip_ns(root.tag): _xml_to_dict(root)}
|
||||
|
||||
|
||||
def _region(cfg, inputs):
|
||||
return (inputs.get("region") if inputs else None) or cfg.get("region") or "us-east-1"
|
||||
|
||||
|
||||
def ec2(action, params, cfg, inputs):
|
||||
region = _region(cfg, inputs)
|
||||
return aws_query("ec2", "ec2." + region + ".amazonaws.com", region, action, "2016-11-15", params, cfg)
|
||||
|
||||
|
||||
def iam(action, params, cfg):
|
||||
return aws_query("iam", "iam.amazonaws.com", "us-east-1", action, "2010-05-08", params, cfg)
|
||||
|
||||
|
||||
def sts(action, params, cfg):
|
||||
return aws_query("sts", "sts.amazonaws.com", "us-east-1", action, "2011-06-15", params, cfg)
|
||||
|
||||
|
||||
def _run(fn):
|
||||
try:
|
||||
print(json.dumps(fn(_cfg(), _inputs())))
|
||||
except urllib.error.HTTPError as e:
|
||||
print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")}))
|
||||
sys.exit(1)
|
||||
except Exception as e:
|
||||
print(json.dumps({"error": str(e)}))
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def main(cfg, inputs):
|
||||
instance_ids_raw = inputs.get("instance_ids")
|
||||
if not instance_ids_raw or not str(instance_ids_raw).strip():
|
||||
raise Exception("instance_ids is required")
|
||||
instance_ids = [s.strip() for s in str(instance_ids_raw).split(",") if s.strip()]
|
||||
if not instance_ids:
|
||||
raise Exception("instance_ids is required")
|
||||
|
||||
force = inputs.get("force", False)
|
||||
if isinstance(force, str):
|
||||
force = force.strip().lower() in ("true", "1", "yes")
|
||||
else:
|
||||
force = bool(force)
|
||||
|
||||
params = {}
|
||||
for i, iid in enumerate(instance_ids, start=1):
|
||||
params["InstanceId.%d" % i] = iid
|
||||
if force:
|
||||
params["Force"] = "true"
|
||||
|
||||
return ec2("StopInstances", params, cfg, inputs)
|
||||
|
||||
|
||||
_run(main)
|
||||
@@ -1,130 +0,0 @@
|
||||
import json, os, sys, hmac, hashlib, datetime
|
||||
import urllib.parse, urllib.request, urllib.error
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
|
||||
def _cfg():
|
||||
return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||
|
||||
|
||||
def _inputs():
|
||||
return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||
|
||||
|
||||
def _sign_key(key, date_stamp, region, service):
|
||||
def _h(k, m):
|
||||
return hmac.new(k, m.encode("utf-8"), hashlib.sha256).digest()
|
||||
k_date = _h(("AWS4" + key).encode("utf-8"), date_stamp)
|
||||
k_region = _h(k_date, region)
|
||||
k_service = _h(k_region, service)
|
||||
return _h(k_service, "aws4_request")
|
||||
|
||||
|
||||
def _strip_ns(tag):
|
||||
return tag.split("}", 1)[1] if "}" in tag else tag
|
||||
|
||||
|
||||
def _xml_to_dict(elem):
|
||||
d = {}
|
||||
children = list(elem)
|
||||
if not children:
|
||||
return (elem.text or "").strip()
|
||||
for c in children:
|
||||
tag = _strip_ns(c.tag)
|
||||
val = _xml_to_dict(c)
|
||||
if tag in d:
|
||||
if not isinstance(d[tag], list):
|
||||
d[tag] = [d[tag]]
|
||||
d[tag].append(val)
|
||||
else:
|
||||
d[tag] = val
|
||||
return d
|
||||
|
||||
|
||||
def aws_query(service, host, region, action, version, params, cfg):
|
||||
# params: dict of extra query params for this Action
|
||||
body_params = {"Action": action, "Version": version}
|
||||
body_params.update({k: str(v) for k, v in params.items() if v is not None})
|
||||
body = urllib.parse.urlencode(sorted(body_params.items()))
|
||||
|
||||
access_key = str(cfg.get("access_key_id", ""))
|
||||
secret_key = str(cfg.get("secret_access_key", ""))
|
||||
session_token = cfg.get("session_token") or ""
|
||||
|
||||
now = datetime.datetime.utcnow()
|
||||
amz_date = now.strftime("%Y%m%dT%H%M%SZ")
|
||||
date_stamp = now.strftime("%Y%m%d")
|
||||
|
||||
method = "POST"
|
||||
canonical_uri = "/"
|
||||
canonical_querystring = ""
|
||||
payload_hash = hashlib.sha256(body.encode("utf-8")).hexdigest()
|
||||
canonical_headers = "content-type:application/x-www-form-urlencoded; charset=utf-8\n" \
|
||||
"host:" + host + "\n" \
|
||||
"x-amz-date:" + amz_date + "\n"
|
||||
signed_headers = "content-type;host;x-amz-date"
|
||||
if session_token:
|
||||
canonical_headers += "x-amz-security-token:" + session_token + "\n"
|
||||
signed_headers = "content-type;host;x-amz-date;x-amz-security-token"
|
||||
canonical_request = "\n".join([method, canonical_uri, canonical_querystring,
|
||||
canonical_headers, signed_headers, payload_hash])
|
||||
|
||||
algorithm = "AWS4-HMAC-SHA256"
|
||||
credential_scope = date_stamp + "/" + region + "/" + service + "/aws4_request"
|
||||
string_to_sign = "\n".join([algorithm, amz_date, credential_scope,
|
||||
hashlib.sha256(canonical_request.encode("utf-8")).hexdigest()])
|
||||
signing_key = _sign_key(secret_key, date_stamp, region, service)
|
||||
signature = hmac.new(signing_key, string_to_sign.encode("utf-8"), hashlib.sha256).hexdigest()
|
||||
|
||||
authorization = (algorithm + " Credential=" + access_key + "/" + credential_scope +
|
||||
", SignedHeaders=" + signed_headers + ", Signature=" + signature)
|
||||
headers = {
|
||||
"Content-Type": "application/x-www-form-urlencoded; charset=utf-8",
|
||||
"X-Amz-Date": amz_date,
|
||||
"Authorization": authorization,
|
||||
"Accept": "application/json",
|
||||
}
|
||||
if session_token:
|
||||
headers["X-Amz-Security-Token"] = session_token
|
||||
|
||||
req = urllib.request.Request("https://" + host + "/", data=body.encode("utf-8"), headers=headers, method="POST")
|
||||
with urllib.request.urlopen(req, timeout=60) as r:
|
||||
raw = r.read()
|
||||
root = ET.fromstring(raw)
|
||||
return {_strip_ns(root.tag): _xml_to_dict(root)}
|
||||
|
||||
|
||||
def _region(cfg, inputs):
|
||||
return (inputs.get("region") if inputs else None) or cfg.get("region") or "us-east-1"
|
||||
|
||||
|
||||
def ec2(action, params, cfg, inputs):
|
||||
region = _region(cfg, inputs)
|
||||
return aws_query("ec2", "ec2." + region + ".amazonaws.com", region, action, "2016-11-15", params, cfg)
|
||||
|
||||
|
||||
def iam(action, params, cfg):
|
||||
return aws_query("iam", "iam.amazonaws.com", "us-east-1", action, "2010-05-08", params, cfg)
|
||||
|
||||
|
||||
def sts(action, params, cfg):
|
||||
return aws_query("sts", "sts.amazonaws.com", "us-east-1", action, "2011-06-15", params, cfg)
|
||||
|
||||
|
||||
def _run(fn):
|
||||
try:
|
||||
print(json.dumps(fn(_cfg(), _inputs())))
|
||||
except urllib.error.HTTPError as e:
|
||||
print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")}))
|
||||
sys.exit(1)
|
||||
except Exception as e:
|
||||
print(json.dumps({"error": str(e)}))
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def main(cfg, inputs):
|
||||
identity = sts("GetCallerIdentity", {}, cfg)
|
||||
return {"ok": True, "identity": identity}
|
||||
|
||||
|
||||
_run(main)
|
||||
@@ -1,143 +0,0 @@
|
||||
import json, os, sys, hmac, hashlib, datetime
|
||||
import urllib.parse, urllib.request, urllib.error
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
|
||||
def _cfg():
|
||||
return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||
|
||||
|
||||
def _inputs():
|
||||
return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||
|
||||
|
||||
def _sign_key(key, date_stamp, region, service):
|
||||
def _h(k, m):
|
||||
return hmac.new(k, m.encode("utf-8"), hashlib.sha256).digest()
|
||||
k_date = _h(("AWS4" + key).encode("utf-8"), date_stamp)
|
||||
k_region = _h(k_date, region)
|
||||
k_service = _h(k_region, service)
|
||||
return _h(k_service, "aws4_request")
|
||||
|
||||
|
||||
def _strip_ns(tag):
|
||||
return tag.split("}", 1)[1] if "}" in tag else tag
|
||||
|
||||
|
||||
def _xml_to_dict(elem):
|
||||
d = {}
|
||||
children = list(elem)
|
||||
if not children:
|
||||
return (elem.text or "").strip()
|
||||
for c in children:
|
||||
tag = _strip_ns(c.tag)
|
||||
val = _xml_to_dict(c)
|
||||
if tag in d:
|
||||
if not isinstance(d[tag], list):
|
||||
d[tag] = [d[tag]]
|
||||
d[tag].append(val)
|
||||
else:
|
||||
d[tag] = val
|
||||
return d
|
||||
|
||||
|
||||
def aws_query(service, host, region, action, version, params, cfg):
|
||||
# params: dict of extra query params for this Action
|
||||
body_params = {"Action": action, "Version": version}
|
||||
body_params.update({k: str(v) for k, v in params.items() if v is not None})
|
||||
body = urllib.parse.urlencode(sorted(body_params.items()))
|
||||
|
||||
access_key = str(cfg.get("access_key_id", ""))
|
||||
secret_key = str(cfg.get("secret_access_key", ""))
|
||||
session_token = cfg.get("session_token") or ""
|
||||
|
||||
now = datetime.datetime.utcnow()
|
||||
amz_date = now.strftime("%Y%m%dT%H%M%SZ")
|
||||
date_stamp = now.strftime("%Y%m%d")
|
||||
|
||||
method = "POST"
|
||||
canonical_uri = "/"
|
||||
canonical_querystring = ""
|
||||
payload_hash = hashlib.sha256(body.encode("utf-8")).hexdigest()
|
||||
canonical_headers = "content-type:application/x-www-form-urlencoded; charset=utf-8\n" \
|
||||
"host:" + host + "\n" \
|
||||
"x-amz-date:" + amz_date + "\n"
|
||||
signed_headers = "content-type;host;x-amz-date"
|
||||
if session_token:
|
||||
canonical_headers += "x-amz-security-token:" + session_token + "\n"
|
||||
signed_headers = "content-type;host;x-amz-date;x-amz-security-token"
|
||||
canonical_request = "\n".join([method, canonical_uri, canonical_querystring,
|
||||
canonical_headers, signed_headers, payload_hash])
|
||||
|
||||
algorithm = "AWS4-HMAC-SHA256"
|
||||
credential_scope = date_stamp + "/" + region + "/" + service + "/aws4_request"
|
||||
string_to_sign = "\n".join([algorithm, amz_date, credential_scope,
|
||||
hashlib.sha256(canonical_request.encode("utf-8")).hexdigest()])
|
||||
signing_key = _sign_key(secret_key, date_stamp, region, service)
|
||||
signature = hmac.new(signing_key, string_to_sign.encode("utf-8"), hashlib.sha256).hexdigest()
|
||||
|
||||
authorization = (algorithm + " Credential=" + access_key + "/" + credential_scope +
|
||||
", SignedHeaders=" + signed_headers + ", Signature=" + signature)
|
||||
headers = {
|
||||
"Content-Type": "application/x-www-form-urlencoded; charset=utf-8",
|
||||
"X-Amz-Date": amz_date,
|
||||
"Authorization": authorization,
|
||||
"Accept": "application/json",
|
||||
}
|
||||
if session_token:
|
||||
headers["X-Amz-Security-Token"] = session_token
|
||||
|
||||
req = urllib.request.Request("https://" + host + "/", data=body.encode("utf-8"), headers=headers, method="POST")
|
||||
with urllib.request.urlopen(req, timeout=60) as r:
|
||||
raw = r.read()
|
||||
root = ET.fromstring(raw)
|
||||
return {_strip_ns(root.tag): _xml_to_dict(root)}
|
||||
|
||||
|
||||
def _region(cfg, inputs):
|
||||
return (inputs.get("region") if inputs else None) or cfg.get("region") or "us-east-1"
|
||||
|
||||
|
||||
def ec2(action, params, cfg, inputs):
|
||||
region = _region(cfg, inputs)
|
||||
return aws_query("ec2", "ec2." + region + ".amazonaws.com", region, action, "2016-11-15", params, cfg)
|
||||
|
||||
|
||||
def iam(action, params, cfg):
|
||||
return aws_query("iam", "iam.amazonaws.com", "us-east-1", action, "2010-05-08", params, cfg)
|
||||
|
||||
|
||||
def sts(action, params, cfg):
|
||||
return aws_query("sts", "sts.amazonaws.com", "us-east-1", action, "2011-06-15", params, cfg)
|
||||
|
||||
|
||||
def _run(fn):
|
||||
try:
|
||||
print(json.dumps(fn(_cfg(), _inputs())))
|
||||
except urllib.error.HTTPError as e:
|
||||
print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")}))
|
||||
sys.exit(1)
|
||||
except Exception as e:
|
||||
print(json.dumps({"error": str(e)}))
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def main(cfg, inputs):
|
||||
access_key_id = inputs.get("access_key_id")
|
||||
if not access_key_id:
|
||||
raise Exception("access_key_id is required")
|
||||
status = inputs.get("status")
|
||||
if not status:
|
||||
raise Exception("status is required")
|
||||
if status not in ("Active", "Inactive"):
|
||||
raise Exception("status must be Active or Inactive")
|
||||
|
||||
params = {"AccessKeyId": access_key_id, "Status": status}
|
||||
user_name = inputs.get("user_name")
|
||||
if user_name and str(user_name).strip():
|
||||
params["UserName"] = user_name
|
||||
|
||||
return iam("UpdateAccessKey", params, cfg)
|
||||
|
||||
|
||||
_run(main)
|
||||
@@ -1,76 +0,0 @@
|
||||
id: axonius
|
||||
name: Axonius
|
||||
version: 1.0.0
|
||||
description: "Axonius (REST API) — cybersecurity asset management: query devices and users with AQL filters, get a device by ID, and count devices matching a filter. API-key + API-secret authentication; stdlib-only, no extra Python dependencies."
|
||||
changelog: "1.0.0 — Initial release: list/get devices, device count, list users."
|
||||
category: asset_management
|
||||
|
||||
# Per-instance configuration. Auth uses the 'api-key' and 'api-secret' headers.
|
||||
config_schema:
|
||||
properties:
|
||||
base_url:
|
||||
type: string
|
||||
description: "Axonius instance URL (e.g. https://axonius.example.com)"
|
||||
api_key:
|
||||
type: string
|
||||
description: "API key"
|
||||
x-soar-sensitive: true
|
||||
api_secret:
|
||||
type: string
|
||||
description: "API secret"
|
||||
x-soar-sensitive: true
|
||||
required:
|
||||
- base_url
|
||||
- api_key
|
||||
- api_secret
|
||||
|
||||
commands:
|
||||
- id: list_devices
|
||||
name: axonius-list-devices
|
||||
description: "Query devices with an optional AQL filter."
|
||||
risk: read
|
||||
inputs_schema:
|
||||
properties:
|
||||
filter: { type: string, description: "AQL filter (e.g. specific_data.data.hostname == \"host01\")" }
|
||||
limit: { type: number, description: "Max devices (default 50)" }
|
||||
offset: { type: number, description: "Offset (default 0)" }
|
||||
required: []
|
||||
outputs_schema: { properties: {} }
|
||||
- id: get_device
|
||||
name: axonius-get-device
|
||||
description: "Get a single device by its internal Axonius ID."
|
||||
risk: read
|
||||
inputs_schema:
|
||||
properties:
|
||||
device_id: { type: string, description: "Internal Axonius device ID" }
|
||||
required: [device_id]
|
||||
outputs_schema: { properties: {} }
|
||||
- id: device_count
|
||||
name: axonius-device-count
|
||||
description: "Count devices matching an AQL filter."
|
||||
risk: read
|
||||
inputs_schema:
|
||||
properties:
|
||||
filter: { type: string, description: "AQL filter (empty = all devices)" }
|
||||
required: []
|
||||
outputs_schema: { properties: {} }
|
||||
- id: list_users
|
||||
name: axonius-list-users
|
||||
description: "Query users with an optional AQL filter."
|
||||
risk: read
|
||||
inputs_schema:
|
||||
properties:
|
||||
filter: { type: string, description: "AQL filter" }
|
||||
limit: { type: number, description: "Max users (default 50)" }
|
||||
offset: { type: number, description: "Offset (default 0)" }
|
||||
required: []
|
||||
outputs_schema: { properties: {} }
|
||||
|
||||
- id: test_connection
|
||||
name: axonius-test-connection
|
||||
description: "Verify connectivity and credentials (used by the Test button)."
|
||||
risk: read
|
||||
inputs_schema:
|
||||
properties: {}
|
||||
required: []
|
||||
outputs_schema: { properties: {} }
|
||||
@@ -1,52 +0,0 @@
|
||||
import json, os, sys, urllib.parse, urllib.request, urllib.error
|
||||
|
||||
|
||||
def _cfg():
|
||||
return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||
|
||||
|
||||
def _inputs():
|
||||
return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||
|
||||
|
||||
def request(method, path, cfg, body=None):
|
||||
url = str(cfg.get("base_url", "")).rstrip("/") + "/api" + path
|
||||
data = json.dumps(body).encode("utf-8") if body is not None else None
|
||||
headers = {
|
||||
"api-key": str(cfg.get("api_key", "")),
|
||||
"api-secret": str(cfg.get("api_secret", "")),
|
||||
"Accept": "application/json",
|
||||
}
|
||||
if data is not None:
|
||||
headers["Content-Type"] = "application/json"
|
||||
req = urllib.request.Request(url, data=data, headers=headers, method=method)
|
||||
with urllib.request.urlopen(req, timeout=90) as r:
|
||||
raw = r.read()
|
||||
return json.loads(raw) if raw else {}
|
||||
|
||||
|
||||
def _run(fn):
|
||||
try:
|
||||
print(json.dumps(fn(_cfg(), _inputs())))
|
||||
except urllib.error.HTTPError as e:
|
||||
print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")}))
|
||||
sys.exit(1)
|
||||
except Exception as e:
|
||||
print(json.dumps({"error": str(e)}))
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
q = lambda v: urllib.parse.quote(str(v), safe="")
|
||||
|
||||
|
||||
def main(cfg, inputs):
|
||||
filter_ = inputs.get("filter")
|
||||
|
||||
body = {"data": {}}
|
||||
if filter_:
|
||||
body["data"]["filter"] = filter_
|
||||
|
||||
return request("POST", "/devices/count", cfg, body=body)
|
||||
|
||||
|
||||
_run(main)
|
||||
@@ -1,50 +0,0 @@
|
||||
import json, os, sys, urllib.parse, urllib.request, urllib.error
|
||||
|
||||
|
||||
def _cfg():
|
||||
return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||
|
||||
|
||||
def _inputs():
|
||||
return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||
|
||||
|
||||
def request(method, path, cfg, body=None):
|
||||
url = str(cfg.get("base_url", "")).rstrip("/") + "/api" + path
|
||||
data = json.dumps(body).encode("utf-8") if body is not None else None
|
||||
headers = {
|
||||
"api-key": str(cfg.get("api_key", "")),
|
||||
"api-secret": str(cfg.get("api_secret", "")),
|
||||
"Accept": "application/json",
|
||||
}
|
||||
if data is not None:
|
||||
headers["Content-Type"] = "application/json"
|
||||
req = urllib.request.Request(url, data=data, headers=headers, method=method)
|
||||
with urllib.request.urlopen(req, timeout=90) as r:
|
||||
raw = r.read()
|
||||
return json.loads(raw) if raw else {}
|
||||
|
||||
|
||||
def _run(fn):
|
||||
try:
|
||||
print(json.dumps(fn(_cfg(), _inputs())))
|
||||
except urllib.error.HTTPError as e:
|
||||
print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")}))
|
||||
sys.exit(1)
|
||||
except Exception as e:
|
||||
print(json.dumps({"error": str(e)}))
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
q = lambda v: urllib.parse.quote(str(v), safe="")
|
||||
|
||||
|
||||
def main(cfg, inputs):
|
||||
device_id = inputs.get("device_id")
|
||||
if not device_id:
|
||||
raise Exception("device_id is required")
|
||||
|
||||
return request("GET", "/devices/" + q(device_id), cfg)
|
||||
|
||||
|
||||
_run(main)
|
||||
@@ -1,61 +0,0 @@
|
||||
import json, os, sys, urllib.parse, urllib.request, urllib.error
|
||||
|
||||
|
||||
def _cfg():
|
||||
return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||
|
||||
|
||||
def _inputs():
|
||||
return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||
|
||||
|
||||
def request(method, path, cfg, body=None):
|
||||
url = str(cfg.get("base_url", "")).rstrip("/") + "/api" + path
|
||||
data = json.dumps(body).encode("utf-8") if body is not None else None
|
||||
headers = {
|
||||
"api-key": str(cfg.get("api_key", "")),
|
||||
"api-secret": str(cfg.get("api_secret", "")),
|
||||
"Accept": "application/json",
|
||||
}
|
||||
if data is not None:
|
||||
headers["Content-Type"] = "application/json"
|
||||
req = urllib.request.Request(url, data=data, headers=headers, method=method)
|
||||
with urllib.request.urlopen(req, timeout=90) as r:
|
||||
raw = r.read()
|
||||
return json.loads(raw) if raw else {}
|
||||
|
||||
|
||||
def _run(fn):
|
||||
try:
|
||||
print(json.dumps(fn(_cfg(), _inputs())))
|
||||
except urllib.error.HTTPError as e:
|
||||
print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")}))
|
||||
sys.exit(1)
|
||||
except Exception as e:
|
||||
print(json.dumps({"error": str(e)}))
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
q = lambda v: urllib.parse.quote(str(v), safe="")
|
||||
|
||||
|
||||
def main(cfg, inputs):
|
||||
filter_ = inputs.get("filter")
|
||||
limit = inputs.get("limit")
|
||||
offset = inputs.get("offset")
|
||||
|
||||
body = {
|
||||
"data": {
|
||||
"page": {
|
||||
"limit": int(limit or 50),
|
||||
"offset": int(offset or 0),
|
||||
}
|
||||
}
|
||||
}
|
||||
if filter_:
|
||||
body["data"]["filter"] = filter_
|
||||
|
||||
return request("POST", "/devices", cfg, body=body)
|
||||
|
||||
|
||||
_run(main)
|
||||
@@ -1,61 +0,0 @@
|
||||
import json, os, sys, urllib.parse, urllib.request, urllib.error
|
||||
|
||||
|
||||
def _cfg():
|
||||
return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||
|
||||
|
||||
def _inputs():
|
||||
return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||
|
||||
|
||||
def request(method, path, cfg, body=None):
|
||||
url = str(cfg.get("base_url", "")).rstrip("/") + "/api" + path
|
||||
data = json.dumps(body).encode("utf-8") if body is not None else None
|
||||
headers = {
|
||||
"api-key": str(cfg.get("api_key", "")),
|
||||
"api-secret": str(cfg.get("api_secret", "")),
|
||||
"Accept": "application/json",
|
||||
}
|
||||
if data is not None:
|
||||
headers["Content-Type"] = "application/json"
|
||||
req = urllib.request.Request(url, data=data, headers=headers, method=method)
|
||||
with urllib.request.urlopen(req, timeout=90) as r:
|
||||
raw = r.read()
|
||||
return json.loads(raw) if raw else {}
|
||||
|
||||
|
||||
def _run(fn):
|
||||
try:
|
||||
print(json.dumps(fn(_cfg(), _inputs())))
|
||||
except urllib.error.HTTPError as e:
|
||||
print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")}))
|
||||
sys.exit(1)
|
||||
except Exception as e:
|
||||
print(json.dumps({"error": str(e)}))
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
q = lambda v: urllib.parse.quote(str(v), safe="")
|
||||
|
||||
|
||||
def main(cfg, inputs):
|
||||
filter_ = inputs.get("filter")
|
||||
limit = inputs.get("limit")
|
||||
offset = inputs.get("offset")
|
||||
|
||||
body = {
|
||||
"data": {
|
||||
"page": {
|
||||
"limit": int(limit or 50),
|
||||
"offset": int(offset or 0),
|
||||
}
|
||||
}
|
||||
}
|
||||
if filter_:
|
||||
body["data"]["filter"] = filter_
|
||||
|
||||
return request("POST", "/users", cfg, body=body)
|
||||
|
||||
|
||||
_run(main)
|
||||
@@ -1,47 +0,0 @@
|
||||
import json, os, sys, urllib.parse, urllib.request, urllib.error
|
||||
|
||||
|
||||
def _cfg():
|
||||
return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||
|
||||
|
||||
def _inputs():
|
||||
return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||
|
||||
|
||||
def request(method, path, cfg, body=None):
|
||||
url = str(cfg.get("base_url", "")).rstrip("/") + "/api" + path
|
||||
data = json.dumps(body).encode("utf-8") if body is not None else None
|
||||
headers = {
|
||||
"api-key": str(cfg.get("api_key", "")),
|
||||
"api-secret": str(cfg.get("api_secret", "")),
|
||||
"Accept": "application/json",
|
||||
}
|
||||
if data is not None:
|
||||
headers["Content-Type"] = "application/json"
|
||||
req = urllib.request.Request(url, data=data, headers=headers, method=method)
|
||||
with urllib.request.urlopen(req, timeout=90) as r:
|
||||
raw = r.read()
|
||||
return json.loads(raw) if raw else {}
|
||||
|
||||
|
||||
def _run(fn):
|
||||
try:
|
||||
print(json.dumps(fn(_cfg(), _inputs())))
|
||||
except urllib.error.HTTPError as e:
|
||||
print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")}))
|
||||
sys.exit(1)
|
||||
except Exception as e:
|
||||
print(json.dumps({"error": str(e)}))
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
q = lambda v: urllib.parse.quote(str(v), safe="")
|
||||
|
||||
|
||||
def main(cfg, inputs):
|
||||
request("POST", "/devices/count", cfg, body={"data": {}})
|
||||
return {"ok": True}
|
||||
|
||||
|
||||
_run(main)
|
||||
@@ -1,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
Reference in New Issue
Block a user