Compare commits

...

189 Commits

Author SHA1 Message Date
f3nris bf273b959a feat(cortex-xdr): watermark incidents on modification_time, not creation
An XDR incident is not finished when it is created. Alerts keep joining it, an
analyst changes its status, its severity is raised. A creation_time watermark
fetches it once, the watermark moves past it, and nothing that happens
afterwards ever reaches Riposte — which is precisely the content the full fetch
exists to bring in.

modified_after filters and sorts on modification_time instead, so an incident
comes back on every change and dedup on incident_id turns the second visit into
an enrichment of the incident already there. It is now what the ingest hint
prefills; created_after stays for a one-shot backfill.

Worth knowing about that enrichment: it merges context and can fill a detection
anchor that was missing, but it does not restate the incident's severity or
status. An incident XDR later raises to critical stays at the severity it was
ingested with.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 23:50:24 +02:00
f3nris ba68d19e51 refactor(cortex-xdr): name the full fetch get-incidents-full (v1.3.1)
"fetch" said nothing next to a list of commands that all start with "get". The
command sits beside cortex-xdr-get-incidents in the picker, and the only thing
an operator needs to read there is which of the two carries everything — so the
name says it: get-incidents-full.

The id moves with it (fetch_incidents -> get_incidents_full), since the script
and the bundled mapper are bound to a command by filename. Anyone who created a
rule against the old id in the few minutes 1.3.0 was up has to point it at the
new command; the changelog says so.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 23:47:17 +02:00
f3nris a6e1245c79 feat(cortex-xdr): fetch incidents with their alerts, not a 21-field summary
incidents/get_incidents/ answers with a summary and nothing else: 21 fields,
no hosts, no users, no MITRE, no tags, and not one of the alerts the incident
aggregates. Ingesting through it leaves an incident whose raw payload says
almost nothing about what happened — and the shipped mapper had been written
for a richer shape than the endpoint ever returns, mapping hosts[0], users[0]
and mitre_* that simply are not in that response.

incidents/get_multiple_incidents_extra_data/ returns the same incidents with
39 fields, every alert in full — 156 fields each — and the file and network
artifacts. It is what the reference client fetches through (demisto/content,
CortexXDRIR.get_multiple_incidents_extra_data), and full_alert_fields must be
set or the nested alerts come back trimmed to a handful of fields.

Records arrive as {incident, alerts, network_artifacts, file_artifacts} with
each nested block wrapped as {total_count, data}. The script flattens them, so
every expression written against get_incidents keeps working — the summary's
21 fields are a subset of these 39 — while the alerts and artifacts land beside
them as plain lists, and their total_count says when a list is a sample rather
than the whole set. incident_sources is lifted into a scalar for the same
reason severity was on the alerts side: the incident-field mapper reads dotted
paths and cannot index a list.

get_incidents stays, for cheap polling, and now says in its description what it
does and does not carry.

The mapper maps the aggregate first and the first alert last, so the alert
fills in whatever the aggregate leaves silent — including the detection anchor,
since an XDR incident's detection_time is usually null while its alerts carry
theirs. Verified against the vendor's recorded response
(test_data/get_multiple_incidents_extra_data.json): 33 of 52 entries resolve,
severity critical lands on 5, source reads "XDR Agent", and the anchor falls
through to the alert's detection timestamp.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 23:41:27 +02:00
f3nris d06f8ea413 fix(cortex-xdr): the alerts endpoint never answered — wrong request dialect
get_alerts posted the incidents body, {filters, search_from, search_to, sort},
to alerts/get_alerts_by_filter_data/. That endpoint serves the alerts GRID and
speaks another dialect entirely, so every call — before this branch as much as
after it — came back a bare HTTP 500 with no hint as to why.

Shape taken from the reference client (demisto/content, Packs/ApiModules/
Scripts/CoreIRApiModule, get_alerts_by_filter_command):

  request_data.filter_data = {
    sort:   [{FIELD, ORDER}],          # a list, uppercase keys
    paging: {from, to},                # not search_from/search_to
    filter: {AND: [{SEARCH_FIELD, SEARCH_TYPE, SEARCH_VALUE}]},
  }

Severity is an enum there (SEV_040_HIGH), and several severities are OR'd, not
passed as a list. The watermark is a RANGE, since the grid has no gte operator;
its upper bound carries five minutes of slack, because our clock and the
tenant's are not the same clock. A filterless query is bounded to the last
thirty days rather than sent empty — the reference client refuses one outright,
and the grid is not meant to be asked for a whole retention.

The response needed as much work as the request. Rows arrive wrapped as
{alert_fields, incident_fields}, and mapping through that wrapper would put an
alert_fields. prefix on every expression an operator writes, so each row is
unwrapped. Two of its fields cannot be mapped as they stand: severity is the
enum code, and status.progress carries a dot INSIDE the key, which no mapping
path can express. Both are derived into severity_name and status_progress.

The mapper follows the grid's own vocabulary — internal_id, alert_name,
agent_hostname, agent_ip_addresses — and dedup moves to internal_id, since
alert_id belongs to the other API. case_id is kept as the correlation UID: it
is the join back to the incident feed.

Verified end to end against the vendor's own recorded response
(test_data/get_alerts_by_filter_results.json): 33 of 54 OCSF entries resolve on
it, severity lands on 3, the detection anchor is set, and the paging walks
0-100, 100-200, 200-250 with the truncation flag raised only when the ceiling,
not the window, ended the fetch.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 23:34:26 +02:00
f3nris 17318d4225 feat(cortex-xdr): ingest alerts, not only incidents (v1.3.0)
An XDR incident is an aggregate; the SOC works the detections under it. The
alerts endpoint was already exposed as a read command, but nothing could feed
an alert rule with it — no results path, no dedup key, no watermark, no mapper,
no incident type. All five are here now, so an alert rule can be pointed at
reply.alerts the same way it is pointed at reply.incidents.

get_alerts pages past the API's 100-results-per-call ceiling: an alert feed
carries far more than a hundred detections between two polls, and whatever a
single page leaves behind is never fetched again, because the next run's
watermark has already moved past it. On an incremental fetch it also sorts
oldest first, so a window larger than the limit drops its most recent alerts —
the only ones the next poll can still see — and says so via `truncated`.

Two fixes to the incident side while in the same files:

- The severity expression compared strings, which the mapping engine cannot do
  (it reads numeric comparisons only). Every test read as false, so every
  ingested incident silently took the alert rule's default severity. The bare
  field works: Riposte maps critical/high/medium/low onto 1-5 itself.
- The incident mapper carried no `time`, so the detection anchor was missing
  and MTTD stayed empty for the whole feed. creation_time fills it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 23:26:31 +02:00
f3nris c22982c8af chore: retire the Riposte Sextant integration
It pushed agent readings over HTTP into a sibling product. That product was
absorbed into the SOAR in 0.112.0, and the readings now go through the built-in
SOC Reporting integration, which writes into the same database the server
already owns — no base URL, no ingestion token, no second deployment to keep
reachable. This integration is the seam the absorption spent five phases
removing.

Nothing here is worth keeping behind: the counters, the rule that an omitted
counter stays unknown rather than zero, and the client-listing command all
exist on the other side.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 09:02:43 +02:00
f3nris 1d475a6cf5 fix(sentinelone): count-agents can be asked about the whole fleet again
Declared as a boolean, isActive rendered as a checkbox in the toolbox,
and a checkbox has no empty state: the count was forced to one half of
the fleet or the other, with no way to ask for both. As text, an empty
field means the filter is not sent.

true/yes/1 and false/no/0 are read in any case, a real JSON boolean from
a playbook still works, and anything else is refused rather than folded
into false — that would answer a different question than the one asked.
2026-08-14 23:48:40 +02:00
f3nris 93309ac74d feat(sentinelone): count-agents can be scoped to an account
A multi-tenant console answered one number for the whole estate.
accountIds narrows the count to the accounts asked for, the way
get_threats and get_alerts already do at fetch time.
2026-08-14 23:40:13 +02:00
f3nris a5095d3f0f feat(sentinelone): count-agents can tell the active fleet from the dormant one
isActive splits a count that until now lumped both together. It is sent
only when the caller sets it: an unset boolean must not reach the console
as isActive=false and quietly count the dormant agents instead of all of
them.
2026-08-14 23:30:47 +02:00
f3nris cebb3af796 feat(sentinelone): count-agents can ask about versions, state and hardware
The count only knew where an agent was, not what it was running or what
it runs on. Four filters carry the console's own vocabulary through:
agentVersion__gt to count the fleet still below a target build,
networkStatuses to keep only the endpoints in a given connection state,
operationalStatesNin to leave states out of the tally, and machineTypes
to count servers apart from laptops.

The list filters are split and re-joined so a hand-typed
"connected, disconnected" does not reach the API with the space inside
the value.
2026-08-14 22:39:29 +02:00
f3nris ba6bcd19f4 fix(cortex-xdr): say what went wrong when the API answers something other than JSON (v1.2.1)
A tenant URL that is not the API host answers 200 with the console's HTML
shell, and the client turned that into "Expecting value: line 1 column 1
(char 0)" — a JSON parse error that names neither the URL nor the response.

The shared client now reports the status, content type and first bytes of any
non-JSON reply, and points at the API host the operator was meant to copy.
Around it, the URL is reduced to scheme + host so a pasted /public_api/v1 or
console path cannot double the API root, a missing key or key ID is refused
before the request, and 401/403 recalls the three usual causes (auth mode,
revoked key, clock skew).

Nonce and timestamp now ride along in standard mode too, matching the
reference client, and test_connection probes get_incidents — the read that
ingestion actually depends on — instead of the distribution versions.
2026-08-13 14:35:54 +02:00
f3nris aabe1a6837 feat(riposte-sextant): list the clients, and the identifier to deposit under (v1.1.0)
The refusal you get on the first run is "no Sextant client is paired with this
SOAR client", and it names the identifier it did not recognise — but nothing
told you which ones it would have recognised. This does.

Clients with no pairing appear with an empty identifier, and are surfaced again
under `unpaired`: they are the whole reason somebody runs this command twice,
and spotting them in a list is exactly what nobody does.

Verified against a running Sextant: the paired client comes back with its
identifier, and the route refuses a caller with no ingestion token.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 09:04:30 +02:00
f3nris 50358ef8a1 feat(riposte-sextant): push agent readings into the SOC steering file
Sextant counts nothing itself: it receives. Until now the opposite was true —
it reached into this SOAR to run a command, which meant SOAR had to host a
whitelist table, a screen to fill it, a dedicated permission and a catalogue
route. Configuration belonging to another product, living here.

As an integration, none of that is needed: an operator creates an instance with
a URL and a token like for any other tool, and a scheduled playbook counts on
the vendor console, maps, then calls push_agent_stats.

The mapping lives in that playbook, which is where it belongs — next to the
connector that produced the numbers, and per client rather than per product.

WHAT THE SCRIPT REFUSES TO DO, and why it matters more than what it does: a
counter left empty is OMITTED, never sent as zero. "We did not measure how many
agents are in error" and "no agent is in error" are opposite pieces of news, and
the second reassures wrongly — the whole steering file exists not to say it. A
counter that is present but not a number is refused instead, naming the field:
that is a broken mapping in the calling playbook, and dropping it silently would
look exactly like "not measured".

Verified against a running Sextant rather than assumed: the deposit files under
the paired client with the empty counter absent, a broken mapping is refused by
name, and an unpaired client comes back saying which identifier was not
recognised.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 08:46:10 +02:00
f3nris 82f22fdbb3 feat(harfanglab): split image_name into the process path and the threat name (v1.4.1)
image_name was left unmapped because HarfangLab prefixes it with the
matched threat name — "[PUADlManager:Win32/OfferCore] C:\Windows\
explorer.exe" — and the clean path was available from the event data.
That reasoning only held for event-log alerts. On the yara, binary and
ioc subtypes there is neither a `process` object nor an `eventlog`
block, so those alerts reached the incident with no process at all.

Both halves are now pulled out, each guarded on the bracket actually
being there: $substringBefore and $substringAfter return the whole
string when the pattern is absent, so without the guard an unprefixed
path would land verbatim in malware.name.

The three entries lead the process block so the more specific sources
below — the matched process object, then the event log's own fields —
still overwrite them when present. malware.name consequently surfaces on
every subtype instead of only where Defender is the source.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-06 18:15:44 +02:00
f3nris f7c5e99cc5 feat(harfanglab): map the fields the alert and threat payloads actually carry (v1.4.0)
Replayed a real get_alerts and a real get_threats response through the
bundled mappers to see what came out.

get_alerts: a sigma alert matching a Windows event log carries the whole
record under `eventlog`, and none of it was mapped. On a Defender 1116
detection that left the malware, the file, the acting user and the
detecting product invisible — the incident indexed a hostname and
nothing else. The event data uses the provider's own field names, spaces
included, so the new entries quote them with JSONata backticks, and
Defender's "file:_" path prefix is stripped. Also added the agent IP
(placed before details_connection.SourceIp so a network alert's own
source still wins), the DNS domain, device type, agent version and
groups, the tenant, the alert subtype and threat type, the string
severity and the event time.

get_threats: `level` had no entry at all, so a threat's severity never
reached the OCSF document even though field_mappings derived the
incident's 1-5 from it. Added it, the event time, and the top agent's
reachability — whether the endpoint is online decides what response is
possible, and it was excluded without a reason.

Both trailers now record why each remaining field is left out, and the
event log's own SYSTEM writer and FQDN spelling are called out: indexing
them would create a junk user entity and a second spelling of one host.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-06 17:30:01 +02:00
f3nris 6343a0c952 feat(ipinfo): support the Lite API for free/Lite plan tokens (v1.1.0)
The integration only exposed the Core API (/lookup/*), which returns 403
for accounts on the free/Lite plan — including the instance Test button.

Add the Lite endpoints: lite_lookup_ip (GET /lite/{ip}), lite_lookup_me
(GET /lite/me) and lite_batch (POST /batch/lite, code-first because the
endpoint takes a raw JSON array; chunked at the documented 1000-IP cap).

Point test_connection at /lite/me: every plan can reach it, so the token
check now succeeds on Lite-only accounts while still validating Core ones.
Core commands keep their paths and state the plan requirement up front.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-05 21:01:43 +02:00
Guillaume BOURGEOIS 7efc873867 feat(sentinelone): scope threat and alert fetches to given accounts (v1.4.0)
A multi-tenant console returns every account's threats and alerts at once.
get_threats and get_alerts now take an optional account_ids input, passed
through as the accountIds query parameter, so an ingestion can be pinned to
the accounts the SOC actually watches.

The input is optional, so existing instances and running ingestions keep
their current behaviour.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 15:56:47 +02:00
f3nris 144089a099 fix(harfanglab): flag get_alerts as ingestion-ready (v1.3.1)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-21 23:19:58 +02:00
f3nris a4b5b44157 fix(harfanglab): null guards and stable attacks array in get_threats mapper
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-21 22:52:11 +02:00
f3nris 4ea3af6c1d fix(harfanglab): rework get_threats OCSF mapper from live threat sample
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-21 22:49:31 +02:00
f3nris 952dc7efda feat(harfanglab): v1.3.0 — response & triage expansion (27 commands + alert mappers)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-21 22:33:59 +02:00
f3nris cba746a9f9 feat(harfanglab): exhaustive alert OCSF mappers (get_alerts + upgraded get_security_events)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-21 22:26:19 +02:00
f3nris 5d6f823189 feat(harfanglab): threat triage commands (status, level, comment, note, history)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-21 22:20:54 +02:00
f3nris 098b90d6f3 feat(harfanglab): alert triage commands (tag, comment, details, history)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-21 22:18:01 +02:00
f3nris db902c35bc feat(harfanglab): alert search and single-threat read commands
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-21 22:14:25 +02:00
f3nris 59e9211c0d feat(harfanglab): threat-level isolation and job lifecycle commands
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-21 22:09:56 +02:00
f3nris fcbd2ff135 feat(harfanglab): quarantine management commands
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-21 22:04:47 +02:00
f3nris ef60cec0fa feat(harfanglab): file/service/scheduled-task deletion commands
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-21 22:01:22 +02:00
f3nris 6d993f584f feat(harfanglab): on-demand AV and YARA scan commands
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-21 21:57:37 +02:00
f3nris 6d311740f9 feat(harfanglab): kill/dump process response commands
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-21 21:52:39 +02:00
f3nris f6b8c84285 feat(edr): OCSF mappers + ingestion for 6 EDR integrations
Every EDR (category: endpoint) integration is now audited for an OCSF mapper.
Six had a fetch command but no mapper — add a bundled OCSF mapper for each,
wire the fetch command as an ingestion source (ingest: results_path/dedup_key
[/incremental_field]) and add the top-level ingestion block. Field mappings
were derived from each vendor's fetch output and cross-referenced against the
demisto/content reference for that vendor.

- carbon-black-cloud: search_alerts (results_path results, dedup id) — 35 fields
- cisco-secure-endpoint: list_events (data, id) — 21 fields
- cybereason: query_malops (malops, guid, incr start_time) — 6 fields
- sophos-central: list_alerts (items, id) — 14 fields
- trend-vision-one: list_alerts (items, id, incr start_datetime) — 19 fields
- withsecure: list_incidents (items, incidentId) — 7 fields

Validated: every manifest passes ParseManifest+ValidateManifest and every mapper
passes ParseMapperFile with all JSONata source_path/field expressions compiling.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 22:37:07 +02:00
f3nris 5ee7352652 fix(ransomware-live): keyless — drop optional API key, no auth required
Ransomware.live's v2 API is free and needs no authentication. Remove the
optional api_key config field and the X-API-KEY header logic from all commands
so instance creation requires no credentials. Only an optional base_url override
remains.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 19:18:49 +02:00
f3nris 534748f16a feat(ransomware-live): add Ransomware.live OSINT integration
Ransomware.live API v2 — OSINT tracking of ransomware/extortion groups and
their claimed victims. 9 read-only commands: recent victims, list groups,
group profile, group victims, and search victims by keyword, country, or date,
plus recent cyberattacks and a test-connection check. Useful for third-party /
supply-chain exposure checks (is a partner or domain listed as a victim?).

API key is optional (sent as X-API-KEY when configured; keyless otherwise).
stdlib-only Python (urllib), no extra dependencies. category: threat_intel.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 18:56:50 +02:00
Guillaume BOURGEOIS a2c9f998c3 feat(feed): add Botvrij.eu OSINT feed connector (IOC lists by type) 2026-07-13 12:15:50 +02:00
Guillaume BOURGEOIS ae7c494658 feat(feed): add Microsoft 365 endpoints feed connector (allowlist / known infra) 2026-07-13 12:15:50 +02:00
Guillaume BOURGEOIS 40252dadcb feat(feed): add generic RSS/Atom IOC feed connector (URL/IP/hash extraction, defang-aware) 2026-07-13 12:15:49 +02:00
Guillaume BOURGEOIS ac2203f9a5 feat(feed): add FIRST EPSS feed connector (exploit-prediction scores) 2026-07-13 12:12:35 +02:00
Guillaume BOURGEOIS 8248c60da8 feat(feed): add NVD CVE feed connector (recent CVEs, NVD 2.0 API) 2026-07-13 12:12:34 +02:00
Guillaume BOURGEOIS 4206b44e78 feat(feed): add CISA KEV feed connector (known exploited vulnerabilities) 2026-07-13 12:12:34 +02:00
Guillaume BOURGEOIS 9cffefbb36 feat(feed): add GCP IP ranges feed connector (allowlist / known infra) 2026-07-13 12:08:49 +02:00
Guillaume BOURGEOIS f7791402a3 feat(feed): add Cloudflare IPs feed connector (allowlist / known infra) 2026-07-13 12:08:49 +02:00
Guillaume BOURGEOIS c42a9cef03 feat(feed): add AWS IP ranges feed connector (allowlist / known infra) 2026-07-13 12:08:48 +02:00
Guillaume BOURGEOIS f6ff018f09 feat(feed): add OpenPhish feed connector (community phishing URLs) 2026-07-12 23:02:11 +02:00
Guillaume BOURGEOIS c091800434 feat(feed): add Emerging Threats feed connector (ET Open reputation lists) 2026-07-12 23:02:11 +02:00
Guillaume BOURGEOIS 929b4166d6 feat(feed): add generic JSON feed connector (configurable field mapping) 2026-07-12 23:02:10 +02:00
Guillaume BOURGEOIS 381b42b6c7 feat(feed): add CINS Army feed connector (CI Army bad-guys list) 2026-07-12 22:59:20 +02:00
Guillaume BOURGEOIS 5715f21603 feat(feed): add Blocklist.de feed connector (fail2ban attacker lists) 2026-07-12 22:59:19 +02:00
Guillaume BOURGEOIS 7543a95711 feat(feed): add DShield block list feed connector (SANS ISC) 2026-07-12 22:59:18 +02:00
Guillaume BOURGEOIS 7187e7b607 feat(feed): add SSL Blacklist feed connector (abuse.ch C2 IP/fingerprint lists) 2026-07-12 22:56:36 +02:00
Guillaume BOURGEOIS 5bb465eb18 feat(feed): add ThreatFox feed connector (abuse.ch IOC export) 2026-07-12 22:56:36 +02:00
Guillaume BOURGEOIS a4dcd00cc7 feat(feed): add URLhaus feed connector (abuse.ch malware URL feed) 2026-07-12 22:56:35 +02:00
Guillaume BOURGEOIS cf923301a7 feat(feed): add Spamhaus DROP feed connector (DROP/DROPv6/ASN-DROP) 2026-07-12 22:49:30 +02:00
Guillaume BOURGEOIS f833bfe3a5 feat(feed): add Tor exit nodes feed connector 2026-07-12 22:49:29 +02:00
Guillaume BOURGEOIS d6129f5d79 feat(feed): add Feodo Tracker feed connector (abuse.ch C2 IP blocklist) 2026-07-12 22:49:28 +02:00
Guillaume BOURGEOIS c779c38dab feat(feed-taxii2): new generic TAXII 2.1 / STIX feed connector
Pulls STIX 2.x indicators from any TAXII 2.1 server, parses the STIX patterns
into normalized IOCs {value,type} for TIM import. list-collections + fetch.
Basic/bearer/none auth, stdlib-only. py_compile clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 22:43:17 +02:00
Guillaume BOURGEOIS 4f94b59e17 feat(feed-plaintext): new generic plain-text threat-intel feed connector
Fetches a line-per-indicator blocklist URL and emits normalized IOCs {value,type}
for TIM import. Comment-char/field-index/type config, auto type-detection.
Optional bearer auth, stdlib-only. py_compile clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 22:43:17 +02:00
Guillaume BOURGEOIS f09d00114f feat(feed-csv): new generic CSV threat-intel feed connector
Fetches a delimited feed URL and emits normalized IOCs {value,type} for TIM
import (works with the /indicators/feed-extract -> /indicators/bulk flow).
Column/delimiter/comment/type config, auto type-detection. New 'feed' category.
Optional bearer auth, stdlib-only. py_compile clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 22:43:16 +02:00
Guillaume BOURGEOIS a4241a5a5d feat(cisco-ise): new Cisco ISE NAC integration
ISE ERS + ANC API, 6 commands: list/get endpoints, apply/clear ANC policy
(quarantine containment), list ANC policies. HTTP Basic auth, stdlib-only.
py_compile clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 21:59:07 +02:00
Guillaume BOURGEOIS 232e310f77 feat(automox): new Automox patch-management integration
Automox API, 5 commands: list/get devices, list policies, run device command.
API-key auth, stdlib-only. py_compile clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 21:59:06 +02:00
Guillaume BOURGEOIS b231b648bc feat(domaintools): new DomainTools DNS-intel integration
DomainTools API, 5 commands: whois, whois history, reputation, reverse IP.
API-key auth, stdlib-only. py_compile clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 21:59:05 +02:00
Guillaume BOURGEOIS 087b9abd19 feat(thinkst-canary): new Thinkst Canary deception integration
Canary Console API v1, 5 commands: list/get incidents, acknowledge incident,
list devices. Auth-token auth, stdlib-only. py_compile clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 21:53:59 +02:00
Guillaume BOURGEOIS e91febd319 feat(intune): new Microsoft Intune MDM integration
Microsoft Graph device management, 6 commands: list/get managed devices,
retire/wipe/remote-lock device (containment). OAuth2 client-credentials auth,
stdlib-only. py_compile clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 21:53:59 +02:00
Guillaume BOURGEOIS 374e4184ff feat(meraki): new Cisco Meraki network integration
Meraki Dashboard API v1, 5 commands: list organizations/networks/devices, list
network clients. API-key auth, stdlib-only. py_compile clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 21:53:58 +02:00
Guillaume BOURGEOIS 82353947d6 feat(mailinblack): new Mailinblack email-security integration (French vendor)
Mailinblack API, 5 commands: list quarantine, release message, block/allow
sender. Bearer auth, stdlib-only. py_compile clean. NOTE: API paths best-effort
— verify against vendor docs before production.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 21:37:32 +02:00
Guillaume BOURGEOIS ac47315b30 feat(cybelangel): new CybelAngel attack-surface integration (French vendor)
CybelAngel platform API, 4 commands: list/get reports, update report status.
OAuth2 client-credentials auth, stdlib-only. py_compile clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 21:37:31 +02:00
Guillaume BOURGEOIS 631292fcc7 feat(scaleway): new Scaleway cloud integration (French vendor)
Scaleway Cloud API, 5 commands: list/get instances, list security groups, list
projects. Secret-key (X-Auth-Token) auth, stdlib-only. py_compile clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 21:37:31 +02:00
Guillaume BOURGEOIS d8bb41b4c5 feat(vade): new Vade email-security integration (French vendor)
Vade API, 4 commands: search messages, get message, remediate message. Bearer
auth, stdlib-only. py_compile clean. NOTE: API paths best-effort — verify
against vendor docs before production.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 21:13:24 +02:00
Guillaume BOURGEOIS 855f1b132c feat(ovhcloud): new OVHcloud integration (French vendor)
OVH API v1, 5 commands: get account, list cloud projects, list/get dedicated
servers. SHA-1 signed application-key auth, stdlib-only. py_compile clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 21:13:23 +02:00
Guillaume BOURGEOIS 74bf6126fb feat(efficientip): new EfficientIP SOLIDserver DNS/DDI integration (French vendor)
SOLIDserver REST API, 5 commands: add RPZ record (DNS block/sinkhole), list DNS
zones, search IP, list networks. HTTP Basic auth, stdlib-only. py_compile clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 21:13:22 +02:00
Guillaume BOURGEOIS dd574e550b feat(gatewatcher): new Gatewatcher AionIQ NDR integration (French vendor)
Gatewatcher AionIQ, 4 commands: list/get alerts, search alerts. API-KEY auth,
stdlib-only. py_compile clean. NOTE: API paths best-effort — verify against
vendor docs before production.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 16:12:38 +02:00
Guillaume BOURGEOIS 8a7e5a33a2 feat(tehtris): new TEHTRIS XDR integration (French vendor)
TEHTRIS XDR, 5 commands: list/get alerts, list agents, isolate agent
(containment). Bearer auth, stdlib-only. py_compile clean. NOTE: API paths
best-effort — verify against vendor docs before production.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 16:12:37 +02:00
Guillaume BOURGEOIS 3c348b7601 feat(glimps): new GLIMPS Detect malware-analysis integration (French vendor)
GDetect API lite v2, 4 commands: submit file (multipart), get result by UUID,
search by SHA-256. Token auth, stdlib-only. py_compile clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 16:12:37 +02:00
Guillaume BOURGEOIS 2475c4b56e feat(stormshield): new Stormshield SNS firewall integration (French vendor)
Stormshield SNS API, 5 commands: add/remove block host-group member (containment),
list hosts, monitor query. Session (login) auth, stdlib-only. py_compile clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 16:07:38 +02:00
Guillaume BOURGEOIS 3be14f2f00 feat(wallix-bastion): new WALLIX Bastion PAM integration (French vendor)
WALLIX Bastion REST API, 6 commands: list devices/accounts/authorizations,
list/get sessions. X-Auth-User/X-Auth-Key auth, stdlib-only. py_compile clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 16:07:38 +02:00
Guillaume BOURGEOIS 3a4d06441c feat(crowdsec): new CrowdSec CTI integration (French vendor)
CrowdSec CTI API v2, 4 commands: IP reputation, multi-IP search, fire list.
API-key auth, stdlib-only. py_compile clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 16:07:37 +02:00
Guillaume BOURGEOIS ce319c2fea feat(intel471): new Intel 471 threat-intel integration
Intel 471 Titan API v1, 5 commands: search reports/indicators, get report,
list CVE reports. HTTP Basic auth, stdlib-only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 15:49:17 +02:00
Guillaume BOURGEOIS 8f3d9607df feat(threatq): new ThreatQ threat-intel integration
ThreatQ REST API, 5 commands: search/get indicators, create indicator, list
events. OAuth2 password-grant auth, stdlib-only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 15:49:17 +02:00
Guillaume BOURGEOIS a2f2b8efa0 feat(eclecticiq): new EclecticIQ threat-intel integration
EclecticIQ Intelligence Center API v2, 4 commands: search entities/observables,
get entity. Bearer-token auth, stdlib-only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 15:49:16 +02:00
Guillaume BOURGEOIS 3c282e8e53 feat(signalsciences): new Signal Sciences (Fastly NGWAF) integration
Signal Sciences API v0, 5 commands: list sites, get suspicious IPs, list events,
add IP to list (block). API-token auth, stdlib-only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 15:44:30 +02:00
Guillaume BOURGEOIS 771c90ef0d feat(imperva): new Imperva Cloud WAF integration
Cloud Application Security API v1, 5 commands: list sites, get site status,
block/whitelist IPs (site ACL). API-ID/key auth, stdlib-only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 15:44:29 +02:00
Guillaume BOURGEOIS 9fb5b65e4a feat(f5-bigip): new F5 BIG-IP integration
iControl REST, 6 commands: list virtual servers/pools, get pool members,
disable pool member, add data-group record (block). HTTP Basic auth, stdlib-only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 15:44:28 +02:00
Guillaume BOURGEOIS 16fe51ee99 feat(sailpoint): new SailPoint IdentityNow IGA integration
IdentityNow API v3, 6 commands: list/get identities, list accounts, disable/
enable account (containment). OAuth2 client-credentials auth, stdlib-only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 15:39:19 +02:00
Guillaume BOURGEOIS 5089e4466d feat(beyondtrust-password-safe): new BeyondTrust Password Safe PAM integration
Password Safe REST API v3, 5 commands: list managed accounts/systems, create
release request, get credential. PS-Auth session auth, stdlib-only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 15:39:18 +02:00
Guillaume BOURGEOIS 8fa1ade945 feat(delinea-secret-server): new Delinea Secret Server PAM integration
Secret Server REST API v1, 5 commands: search secrets, get secret, get secret
field, list folders. OAuth2 password-grant auth, stdlib-only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 15:39:18 +02:00
Guillaume BOURGEOIS 54b077bfc9 feat(confluence): new Confluence runbook/knowledge integration
Confluence REST API, 6 commands: CQL search, get page, create/update page,
list spaces. Basic (email + API token) auth, stdlib-only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 15:23:56 +02:00
Guillaume BOURGEOIS 6d38896892 feat(dropbox): new Dropbox file/evidence integration
Dropbox API v2, 5 commands: search, get metadata, list folder, create shared
link. Bearer-token auth, stdlib-only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 15:23:56 +02:00
Guillaume BOURGEOIS 223dfd34e3 feat(box): new Box file/evidence integration
Box Content API v2, 6 commands: search, get file/folder info, list folder
items, create shared link. Bearer-token auth, stdlib-only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 15:23:56 +02:00
Guillaume BOURGEOIS 83c1736d47 feat(snyk): new Snyk appsec-posture integration
Snyk API v1 + REST, 5 commands: list/get projects, list project issues, list
org issues. API-token auth, stdlib-only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 15:18:48 +02:00
Guillaume BOURGEOIS bc100eef67 feat(gitlab): new GitLab project-security integration
GitLab API v4, 6 commands: list vulnerabilities, list/create issues, get
project, list audit events. Private-token auth, stdlib-only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 15:18:48 +02:00
Guillaume BOURGEOIS 5f2e946c0a feat(github): new GitHub code-security integration
GitHub REST API, 6 commands: list secret/code/Dependabot scanning alerts,
get repo, create issue. PAT (Bearer) auth, stdlib-only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 15:18:47 +02:00
Guillaume BOURGEOIS 86fdd8483a feat(pingone): new PingOne identity-containment integration
PingOne Platform API, 6 commands: list/get users, enable/disable user, list
populations. OAuth2 client-credentials auth, stdlib-only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 15:14:01 +02:00
Guillaume BOURGEOIS cd02c26b45 feat(jumpcloud): new JumpCloud identity integration
JumpCloud API v1, 6 commands: list/get users, suspend/activate user, list
systems. API-key auth, stdlib-only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 15:14:01 +02:00
Guillaume BOURGEOIS ab68b3ae71 feat(onelogin): new OneLogin identity-containment integration
OneLogin API v1/v2, 6 commands: list/get users, lock user, logout user (revoke
sessions), list events. OAuth2 client-credentials auth, stdlib-only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 15:14:00 +02:00
Guillaume BOURGEOIS 046fb79008 feat(datadog): new Datadog observability integration
Datadog API v1/v2, 6 commands: search logs, list/get monitors, submit event,
list events. API-key + Application-key auth, stdlib-only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 15:08:51 +02:00
Guillaume BOURGEOIS 6548740890 feat(sumologic): new Sumo Logic log-search integration
Sumo Logic REST API, 4 commands: search (job create+poll+messages, cookie
session), list/get collectors. Access-key (Basic) auth, stdlib-only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 15:08:51 +02:00
Guillaume BOURGEOIS 16deec38f8 feat(graylog): new Graylog log-search integration
Graylog REST API, 5 commands: relative message search, list/get streams, get
message. API-token (Basic) auth, stdlib-only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 15:08:50 +02:00
Guillaume BOURGEOIS cf950cf6e4 feat(withsecure): new WithSecure Elements endpoint integration
WithSecure Elements Connect API, 7 commands: list/get devices, isolate/release
(containment), scan, list incidents. OAuth2 client-credentials auth, stdlib-only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 15:04:02 +02:00
Guillaume BOURGEOIS 1949c98cf7 feat(sophos-central): new Sophos Central endpoint integration
Sophos Central Endpoint API, 7 commands: list/get endpoints, isolate/de-isolate
(containment), scan, list alerts. OAuth2 client-credentials + tenant discovery
(whoami), stdlib-only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 15:04:02 +02:00
Guillaume BOURGEOIS addaeadae5 feat(cisco-secure-endpoint): new Cisco Secure Endpoint (AMP) integration
AMP for Endpoints API v1, 7 commands: list/get computers, isolate/stop-isolation
(containment), list events, get trajectory. HTTP Basic auth, stdlib-only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 15:04:01 +02:00
Guillaume BOURGEOIS b137e49be4 feat(yaraify): new YARAify file/YARA-intel integration
YARAify (abuse.ch) API v1, 4 commands: lookup hash, get YARA rule, recent
matches. Auth-Key auth, stdlib-only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 14:58:08 +02:00
Guillaume BOURGEOIS c5699dd01a feat(threatfox): new ThreatFox IOC-intel integration
ThreatFox (abuse.ch) API v1, 5 commands: search IOC, recent IOCs, search hash,
tag IOCs. Auth-Key auth, stdlib-only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 14:58:07 +02:00
Guillaume BOURGEOIS 015131a2d1 feat(malwarebazaar): new MalwareBazaar sample-intel integration
MalwareBazaar (abuse.ch) API v1, 5 commands: sample info by hash, recent
samples, tag info, signature info. Auth-Key auth, stdlib-only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 14:58:07 +02:00
Guillaume BOURGEOIS 82ad3a9369 feat(armis): new Armis device-visibility integration
Armis API v1, 5 commands: search devices/alerts (AQL), get device, update alert
status. Secret-key token-exchange auth, stdlib-only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 14:41:43 +02:00
Guillaume BOURGEOIS df8a2c1187 feat(axonius): new Axonius asset-management integration
Axonius REST API, 5 commands: list/get devices, device count, list users (AQL
filters). API-key + API-secret auth, stdlib-only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 14:41:43 +02:00
Guillaume BOURGEOIS 088694fdc2 feat(runzero): new runZero asset-inventory integration
runZero API v1.0, 6 commands: search/get assets, search services, list sites,
list tasks. Bearer-token auth, stdlib-only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 14:41:42 +02:00
Guillaume BOURGEOIS c1867a62a3 feat(rocketchat): new Rocket.Chat ChatOps integration
Rocket.Chat REST API v1, 6 commands: post message, get channel info, list
channels, create channel, get user. X-Auth-Token/X-User-Id auth, stdlib-only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 14:22:06 +02:00
Guillaume BOURGEOIS c2cc4da675 feat(mattermost): new Mattermost ChatOps integration
Mattermost REST API v4, 6 commands: create post, get channel by name, search
posts, create channel, get user. Bearer-token auth, stdlib-only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 14:22:05 +02:00
Guillaume BOURGEOIS 2b5e7f37b4 feat(discord): new Discord notification/ChatOps integration
Discord webhook + bot API, 5 commands: webhook message, bot channel message,
list channel messages, get channel. Webhook + bot-token auth, stdlib-only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 14:22:04 +02:00
Guillaume BOURGEOIS d469ed1d8f feat(urlhaus): new URLhaus malicious-URL intel integration
URLhaus (abuse.ch) API v1, 5 commands: URL/host/payload lookup, recent URLs.
Auth-Key auth, stdlib-only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 14:15:54 +02:00
Guillaume BOURGEOIS 1ec065e91b feat(binaryedge): new BinaryEdge exposure-intel integration
BinaryEdge API v2, 6 commands: host lookup (current/historical), search, domain
subdomains, data-leak email check, subscription. API-key auth, stdlib-only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 14:15:54 +02:00
Guillaume BOURGEOIS ee58358330 feat(hunter): new Hunter.io email-OSINT integration
Hunter.io API v2, 5 commands: domain search, email finder, email verifier,
account. API-key auth, stdlib-only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 14:15:53 +02:00
Guillaume BOURGEOIS 5bf679cfe0 feat(symantec-dlp): new Symantec DLP integration
Symantec DLP Enforce REST API v2, 5 commands: list/get incidents, get incident
history, update incident (status/severity). OAuth2 password-grant auth,
stdlib-only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 14:10:27 +02:00
Guillaume BOURGEOIS 70c66f7386 feat(forcepoint-dlp): new Forcepoint DLP integration
Forcepoint DLP REST API v1, 4 commands: list incidents, get incident, update
incident (status/severity/assignee). Token auth, stdlib-only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 14:10:26 +02:00
Guillaume BOURGEOIS 4020e13df9 feat(microsoft-purview): new Microsoft Purview DLP integration
DLP alerts via Microsoft Graph Security API, 6 commands: list/get DLP alerts,
update alert (status/assignee/determination), list/get incidents. Azure AD
OAuth2 client-credentials auth, stdlib-only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 14:10:25 +02:00
Guillaume BOURGEOIS 2a1eb73115 feat(hibp): new Have I Been Pwned breach-enrichment integration
HIBP API v3 + Pwned Passwords, 6 commands: account breaches/pastes, list/get
breach, password pwned check (k-anonymity, password never transmitted).
API-key auth, stdlib-only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 00:57:25 +02:00
Guillaume BOURGEOIS 81e39e8148 feat(telegram): new Telegram bot alerting integration
Telegram Bot API, 5 commands: send message, send photo, get chat, get updates.
Bot-token auth, stdlib-only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 00:57:24 +02:00
Guillaume BOURGEOIS d06ea406d3 feat(twilio): new Twilio SMS/voice alerting integration
Twilio REST API, 5 commands: send SMS, make voice call (TwiML), get/list
messages. HTTP Basic auth, stdlib-only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 00:57:23 +02:00
Guillaume BOURGEOIS fe7dae8bd3 feat(wiz): new Wiz CNAPP integration
Wiz GraphQL API, 6 commands: list/get issues, update issue status, list
vulnerabilities, list cloud resources. OAuth2 client-credentials auth,
stdlib-only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 00:53:02 +02:00
Guillaume BOURGEOIS 52356523c9 feat(prisma-cloud): new Prisma Cloud CSPM integration
Prisma Cloud REST API, 7 commands: list/get alerts, dismiss alerts, list
policies, list cloud accounts, config (RQL) search. Access-key login auth,
stdlib-only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 00:53:01 +02:00
Guillaume BOURGEOIS a567558341 feat(vault): new HashiCorp Vault secrets integration
Vault KV v2 + sys API, 7 commands: read/write/delete/list secrets, list mounts,
lookup token. Vault-token auth, stdlib-only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 00:53:01 +02:00
Guillaume BOURGEOIS 041cadade2 feat(extrahop): new ExtraHop Reveal(x) NDR integration
ExtraHop REST API v1, 6 commands: search/get detections, list/search devices,
get device. ESA API-key auth, stdlib-only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 00:48:03 +02:00
Guillaume BOURGEOIS 41e8aa8e7e feat(vectra): new Vectra AI NDR integration
Vectra Detect API v2.3, 8 commands: list/get detections, mark detection fixed,
add note, list/get hosts, list accounts. Token auth, stdlib-only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 00:48:01 +02:00
Guillaume BOURGEOIS 0775500b15 feat(darktrace): new Darktrace NDR integration
Darktrace Threat Visualizer API, 7 commands: get/acknowledge model breaches,
get devices, get/activate Antigena actions (autonomous-response containment).
HMAC-SHA1 signed auth, stdlib-only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 00:48:00 +02:00
Guillaume BOURGEOIS 9be28a2ff7 feat(triage): new Hatching Triage sandbox integration
Triage API v0, 7 commands: submit file (multipart _json+file) / URL, get
sample, get report overview, list samples, search. Bearer-token auth,
stdlib-only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 00:42:02 +02:00
Guillaume BOURGEOIS 1292a8c798 feat(cape): new CAPE Sandbox integration
CAPE APIv2, 6 commands: submit file (multipart) / URL, get task, get report,
list tasks. Token auth, stdlib-only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 00:42:01 +02:00
Guillaume BOURGEOIS b6af7d1b68 feat(cuckoo): new Cuckoo Sandbox integration
Cuckoo REST API, 7 commands: submit file (multipart) / URL, get task, get
report, list tasks, delete task. Optional Bearer-token auth, stdlib-only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 00:42:00 +02:00
Guillaume BOURGEOIS d5b7e38dd7 feat(infoblox): new Infoblox NIOS DNS-containment integration
Infoblox WAPI, 7 commands: add RPZ domain (DNS sinkhole/block), list/delete RPZ
records, search hosts, get networks, add A record. HTTP Basic auth, stdlib-only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 00:37:12 +02:00
Guillaume BOURGEOIS 6de01f85bb feat(zscaler-zpa): new Zscaler Private Access integration
ZPA config API, 7 commands: list/get application segments, list servers,
server groups, segment groups, access policy rules. OAuth2 client-credentials
(signin) auth, stdlib-only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 00:37:11 +02:00
Guillaume BOURGEOIS 86e7f11227 feat(netskope): new Netskope SASE/CASB integration
Netskope REST API v2, 6 commands: list/get URL lists, replace URL list (block
list), deploy URL lists, list users. API-token auth, stdlib-only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 00:37:10 +02:00
Guillaume BOURGEOIS 9f88891457 feat(freshservice): new Freshservice ITSM integration
Freshservice API v2, 7 commands: create/get/update/list tickets, add note
(public/private), list agents. API-key (Basic) auth, stdlib-only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 00:31:45 +02:00
Guillaume BOURGEOIS 9892f2cf06 feat(zendesk): new Zendesk ticketing integration
Zendesk Support API v2, 7 commands: create/get/update/search tickets, add
comment (public/internal), list users. API-token (Basic) auth, stdlib-only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 00:31:44 +02:00
Guillaume BOURGEOIS be8bf9f822 feat(thehive): new TheHive case-management integration
TheHive 5 API v1, 10 commands: case create/get/update/search, create task,
alert create/get/promote-to-case, add observable. Bearer (API key) auth,
stdlib-only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 00:31:43 +02:00
Guillaume BOURGEOIS aca1db5cad feat(chronicle): new Google Chronicle SIEM integration
Chronicle Backstory API, 5 commands: list IOCs, list alerts, list assets,
get IOC details. Service-account RS256 JWT auth (remote engine, PyJWT +
cryptography).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 00:25:51 +02:00
Guillaume BOURGEOIS d0d72c5171 feat(elasticsearch): new Elasticsearch log-search integration
Elasticsearch REST API, 7 commands: search (DSL or query_string), count, get/
index document, list indices, delete by query. API-key auth, stdlib-only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 00:25:50 +02:00
Guillaume BOURGEOIS fd64c245f4 feat(qradar): new IBM QRadar SIEM integration
QRadar REST API, 8 commands: list/get offenses, update offense (close/assign),
list reference sets, add/remove reference-set value (block lists), list assets.
SEC-token auth, stdlib-only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 00:25:49 +02:00
Guillaume BOURGEOIS 6cc0bb61bc feat(cisco-fmc): new Cisco Firepower Management Center integration
FMC REST API, 7 commands: list/create/delete host objects, list access
policies/rules, create block access rule. Token auth (generatetoken, token +
domain UUID from response headers), stdlib-only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 00:20:41 +02:00
Guillaume BOURGEOIS 496ac6d8ce feat(checkpoint): new Check Point firewall-containment integration
Check Point Management Web API, 7 commands: show/add hosts, show access
rulebase, add access rule (drop), publish, install policy. Session (login)
auth with X-chkp-sid, stdlib-only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 00:20:41 +02:00
Guillaume BOURGEOIS 44d0836e1b feat(cloudflare): new Cloudflare edge-containment integration
Cloudflare client API v4, 9 commands: list/get zones, block IP (firewall access
rule), list/delete access rules, list/create/delete DNS records. API-token
(Bearer) auth, stdlib-only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 00:20:40 +02:00
Guillaume BOURGEOIS 796a95d5f8 feat(mandiant): new Mandiant Advantage threat-intel integration
Mandiant TI API v4, 8 commands: search/get indicator, get actor/malware/
vulnerability, list/get reports. Key+secret OAuth2 token exchange, stdlib-only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 00:15:12 +02:00
Guillaume BOURGEOIS a5db725529 feat(anomali-threatstream): new Anomali ThreatStream threat-intel integration
ThreatStream API, 9 commands: ip/domain/file/url reputation, indicator search,
passive DNS, threat models, import indicator. apikey auth, stdlib-only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 00:15:11 +02:00
Guillaume BOURGEOIS ecc301d95d feat(threatconnect): new ThreatConnect threat-intel integration
ThreatConnect API v3, 9 commands: list/get/create/delete indicators, list/get/
create groups, add indicator tag. HMAC-SHA256 signed auth, stdlib-only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 00:15:10 +02:00
Guillaume BOURGEOIS 6b8dd13297 feat(trend-vision-one): new Trend Vision One XDR integration
Vision One API v3.0, 9 commands: list endpoints, isolate/restore endpoint
(containment), terminate process, list/get Workbench alerts, add/remove
suspicious object (block hash/URL/IP/domain). Bearer-token auth, stdlib-only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 00:06:32 +02:00
Guillaume BOURGEOIS 1c3936992b feat(cybereason): new Cybereason EDR integration
Cybereason API, 7 commands: query Malops, list sensors, get machine details,
isolate/un-isolate machine (containment), block file hash. Session (login)
auth, stdlib-only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 00:06:32 +02:00
Guillaume BOURGEOIS 61da5ef145 feat(carbon-black-cloud): new VMware Carbon Black Cloud EDR integration
Platform API, 10 commands: device search/get, quarantine/unquarantine
(network-isolate), background scan, update device policy, alert search,
ban/unban file hash (reputation override). API-token auth, stdlib-only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 00:06:31 +02:00
Guillaume BOURGEOIS f34363b450 feat(rapid7-insightvm): new Rapid7 InsightVM vulnerability integration
InsightVM/Nexpose Console API v3, 9 commands: list/get assets, asset
vulnerabilities, list/get vulnerabilities, list sites, start site scan,
get scan. HTTP Basic auth, stdlib-only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-11 23:59:39 +02:00
Guillaume BOURGEOIS fcf30da5fc feat(qualys): new Qualys VMDR vulnerability integration
Qualys API v2 (XML), 6 commands: host list, host detections, knowledge-base
vulnerability details, scan list, launch scan. HTTP Basic auth, stdlib-only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-11 23:59:38 +02:00
Guillaume BOURGEOIS 847d18f79b feat(tenable-io): new Tenable Vulnerability Management integration
Tenable.io API, 9 commands: list/get assets, list vulnerabilities, per-asset
vulnerabilities, vulnerability (plugin) details, list scans, scan status,
launch scan. API-key (access/secret) auth, stdlib-only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-11 23:59:37 +02:00
Guillaume BOURGEOIS 5501297984 feat(gcp-security): new Google Cloud cloud-containment integration
Compute Engine + Security Command Center, 7 commands: list/create/delete VPC
firewall rules (deny to isolate), list/stop instances, list SCC findings.
Service-account RS256 JWT auth (remote engine, PyJWT + cryptography).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-11 23:54:02 +02:00
Guillaume BOURGEOIS 31d0a5938a feat(azure-security): new Microsoft Azure cloud-containment integration
Azure Resource Manager (Defender for Cloud + Network), 9 commands: list/get
security alerts, update alert state, secure score, list/get NSGs, create/delete
NSG security rules (deny inbound to isolate). OAuth2 client-credentials, stdlib-only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-11 23:54:01 +02:00
Guillaume BOURGEOIS 62a44aea28 feat(aws): new AWS cloud-containment integration
EC2 + IAM + STS via AWS Query protocol (Signature V4), 9 commands: describe
instances/security-groups, authorize/revoke security-group ingress, modify
instance security groups (isolate), stop instances, list/update IAM access keys
(deactivate compromised key), STS caller identity. Stdlib-only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-11 23:54:00 +02:00
Guillaume BOURGEOIS 14e5e102a9 feat(microsoft-defender-o365): new O365 email-remediation integration
Microsoft Graph mail APIs, 7 commands: search mailbox messages, get message,
list attachments, list folders, move message, hard-delete (purge) message.
Azure AD OAuth2 client-credentials auth (Mail.ReadWrite), stdlib-only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-11 23:46:36 +02:00
Guillaume BOURGEOIS 460dfaefba feat(mimecast): new Mimecast anti-phishing integration
Email Security API, 11 commands: held-message list/release/reject, message
search + info, managed URL create/list (block), block sender, create
remediation, URL decode. HMAC-SHA1 signed auth, stdlib-only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-11 23:46:35 +02:00
Guillaume BOURGEOIS f826181704 feat(proofpoint-tap): new Proofpoint TAP email-threat integration
TAP SIEM API v2, 11 commands: SIEM events (all/messages/clicks blocked+delivered+
permitted), threat/campaign forensics, campaign get/list, URL Defense decode,
top clickers. HTTP Basic (service principal) auth, stdlib-only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-11 23:46:35 +02:00
Guillaume BOURGEOIS 9da00ad7b9 feat(cyberark-pas): new CyberArk PAS privileged-access integration
PVWA REST API, 11 commands: list/get accounts, change/verify/reconcile
credentials (CPM rotation), retrieve password, add/delete account, list safes,
list users. Session (logon) auth, stdlib-only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-11 23:37:54 +02:00
Guillaume BOURGEOIS 7bd6ad6a69 feat(duo): new Cisco Duo MFA-containment integration
Duo Admin API, 9 commands: get users/user, modify user status
(disable/enable/bypass), delete user, bypass codes, devices, user devices,
authentication logs. HMAC-SHA1 signed auth, stdlib-only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-11 23:37:53 +02:00
Guillaume BOURGEOIS 26694b512a feat(okta): new Okta identity-containment integration
Okta Core API, 14 commands: user lifecycle (suspend/unsuspend/deactivate/unlock),
clear sessions + tokens, expire password, reset MFA factors, get/list users,
get factors, system log, add/remove group membership. SSWS-token auth, stdlib-only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-11 23:37:52 +02:00
Guillaume BOURGEOIS e88805ccab feat(opsgenie): new Opsgenie alerting integration
Alert API v2, 13 commands: alert create/close/acknowledge/get/list, add note/tags,
assign, snooze, delete, list schedules, on-calls. GenieKey auth (US/EU), stdlib-only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-11 23:28:26 +02:00
Guillaume BOURGEOIS a279b58290 feat(pagerduty): new PagerDuty alerting integration
Events API v2 + REST API v2, 10 commands: trigger/acknowledge/resolve incident,
list/get incident, add note, list on-calls/users/services. Token + routing-key
auth, stdlib-only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-11 23:28:26 +02:00
Guillaume BOURGEOIS df4cd98a9e feat(microsoft-teams): new Microsoft Teams notification integration
Incoming-webhook / Power Automate Workflows, 3 commands: send message,
send Adaptive Card (title/text/facts/action button). Webhook-URL auth, stdlib-only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-11 23:28:25 +02:00
Guillaume BOURGEOIS e3c15ae972 feat(slack): new Slack notification/ChatOps integration
Slack Web API, 8 commands: send message (text/Block Kit), list/create channel,
invite to channel, set topic, get user, add reaction. Bot-token auth, stdlib-only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-11 23:28:25 +02:00
Guillaume BOURGEOIS 26cd02d77a feat(panos): new PAN-OS / Panorama firewall-containment integration
PAN-OS XML API, 10 commands: register/unregister IP tag (Dynamic Address
Group fast-block, no commit), address create/delete, custom URL category
create/edit, EDL list/refresh, commit. API-key auth, targets a vsys, stdlib-only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-11 23:13:35 +02:00
Guillaume BOURGEOIS 53034f35da feat(zscaler): new Zscaler Internet Access containment integration
ZIA REST API, 12 commands: URL/IP block-list add/remove, block-list and
allow-list read, allow-list add, category URL add, Sandbox report, activate
changes. Session-based obfuscated-API-key auth, stdlib-only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-11 23:13:34 +02:00
Guillaume BOURGEOIS fb82de7f93 feat(cisco-umbrella-enforcement): new Cisco Umbrella Enforcement integration
Umbrella Enforcement API, 4 commands: add domain (DNS-layer block),
list enforced domains, delete domain. Customer-key auth, stdlib-only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-11 23:13:24 +02:00
Guillaume BOURGEOIS 8ee19ffe85 feat(fortigate): new FortiGate network-containment integration
FortiOS REST API v2, 11 commands: ban/unban/list banned IPs (quarantine),
firewall address object create/delete/list, address-group update/list,
firewall policy and service listing. Bearer-token auth, multi-VDOM, stdlib-only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-11 23:13:24 +02:00
Guillaume BOURGEOIS bcc79598e6 feat(ipqualityscore): new IPQualityScore enrichment integration
6 commands: IP/URL/email/phone fraud reputation, leaked-email check.
API-key auth, stdlib-only.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 22:58:17 +02:00
Guillaume BOURGEOIS 7d0e4aa18a feat(misp): new MISP threat-intel integration
10 commands: event/attribute search, event read/create/publish/delete,
attribute add, event tagging, sightings. API-key auth, stdlib-only.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 22:58:16 +02:00
Guillaume BOURGEOIS 7bec81ad0a feat(vmray): new VMRay sandbox integration
8 commands: file/URL detonation, sample/submission/analysis retrieval,
sample-by-hash lookup, IOCs. API-key auth, stdlib-only.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 22:58:15 +02:00
Guillaume BOURGEOIS 1bdc71abee feat(intezer): new Intezer enrichment integration
9 commands: analyze by file/hash/URL, analysis result, IOCs, metadata
and sub-analyses. API-key (token-exchange) auth, stdlib-only.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 22:58:15 +02:00
Guillaume BOURGEOIS b8afff7678 feat(anyrun): new ANY.RUN sandbox integration
7 commands: file/URL detonation, report + verdict retrieval, analysis
history, user limits, delete task. API-key auth, stdlib-only.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 22:48:50 +02:00
Guillaume BOURGEOIS 239cc70672 feat(hybrid-analysis): new Hybrid Analysis (Falcon Sandbox) integration
7 commands: file/URL detonation, report summary + state, hash search,
environment listing. API-key auth, stdlib-only.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 22:48:49 +02:00
Guillaume BOURGEOIS 2655a14bc3 feat(joe-sandbox): new Joe Sandbox integration
8 commands: file/URL detonation, submission + analysis info, search,
report download, account quota. API-key auth, stdlib-only.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 22:48:49 +02:00
Guillaume BOURGEOIS 9589b4f0d1 feat(securitytrails): new SecurityTrails enrichment integration
8 commands: domain details, subdomains, WHOIS, DNS history, associated
domains, IP neighbors, domain search. API-key auth, stdlib-only.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 22:48:48 +02:00
Guillaume BOURGEOIS 203273715c feat(censys): new Censys enrichment integration
3 commands: host lookup by IP and Censys Search Language host query.
API ID + secret (Basic) auth, stdlib-only.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 22:39:29 +02:00
Guillaume BOURGEOIS 1294e3b330 feat(maltiverse): new Maltiverse enrichment integration
5 commands: IP/domain/URL/file threat-intel reputation. Bearer-token
auth, stdlib-only.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 22:39:29 +02:00
Guillaume BOURGEOIS 79d870a4e8 feat(emailrep): new EmailRep.io enrichment integration
3 commands: email reputation lookup, report malicious address. API-key
auth, stdlib-only.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 22:39:29 +02:00
Guillaume BOURGEOIS ac6a52cecd feat(pulsedive): new Pulsedive enrichment integration
4 commands: indicator lookup, scan submission, scan-result retrieval.
API-key auth, stdlib-only.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 22:39:28 +02:00
Guillaume BOURGEOIS 419f891267 feat(urlscan): new urlscan.io enrichment integration
4 commands: submit URL scan, retrieve result, search historical scans.
API-key auth, stdlib-only.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 22:33:10 +02:00
Guillaume BOURGEOIS 2b3b9fc10c feat(alienvault-otx): new AlienVault OTX enrichment integration
9 commands: IP/domain/URL/file reputation, pulse details + search,
passive DNS and related URLs. API-key auth, stdlib-only.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 22:33:10 +02:00
Guillaume BOURGEOIS 9ec84905be feat(greynoise): new GreyNoise enrichment integration
7 commands: IP context, quick check, RIOT, GNQL query + stats, IP
timeline. API-key auth, stdlib-only.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 22:33:09 +02:00
Guillaume BOURGEOIS 9504b22e04 feat(abuseipdb): new AbuseIPDB enrichment integration
5 commands: IP abuse-reputation check, report abusive IP, blacklist
retrieval, CIDR-block check. API-key auth, stdlib-only.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 22:33:03 +02:00
Guillaume BOURGEOIS 1033388518 feat(microsoft-entra-id): new Microsoft Entra ID integration
14 commands (Microsoft Graph users): user get/list/create/update/delete,
account disable/enable, revoke sign-in sessions, reset password, assign
manager, and group/manager/auth-method reads. Azure AD OAuth 2.0
client-credentials, stdlib-only.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 22:22:46 +02:00
Guillaume BOURGEOIS 334ecac83e feat(microsoft-sentinel): new Microsoft Sentinel integration
19 commands (Azure Resource Manager API): incident ingestion + CRUD,
comments, related alerts/entities/relations, watchlists, and threat
indicators. Azure AD OAuth 2.0 client-credentials, stdlib-only.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 22:22:46 +02:00
Guillaume BOURGEOIS 811a85424b feat(microsoft-defender-endpoint): new Defender for Endpoint integration
22 commands (Security Center API): alert ingestion + triage, machine
isolate/unisolate, restrict/unrestrict app execution, AV scan, stop &
quarantine file, collect investigation package, offboard, tag, list
machine actions, advanced hunting (KQL), and custom indicators. Azure
AD OAuth 2.0 client-credentials, stdlib-only.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 22:22:45 +02:00
Guillaume BOURGEOIS 306581e70b feat(jira): new Atlassian Jira integration
Jira Cloud and On-Prem/Data Center (27 commands): JQL ingestion with an
OCSF mapper and JQL query, full issue lifecycle (create/get/edit/delete,
transitions, assign), comments, remote web links and issue links,
attachments (upload/download/delete), field and user lookups, and agile
boards/sprints/epics. Basic (email + API token) or Personal Access Token
authentication; API v3 + ADF bodies on Cloud, v2 on On-Prem.
Stdlib-only, no extra Python dependencies.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 21:56:52 +02:00
Guillaume BOURGEOIS da3909e79d feat(mail-sender): new SMTP mail sender integration
Send email over SMTP: plain-text and HTML bodies with inline data-URI
images, base64 file attachment, CC/BCC, Reply-To and custom headers.
Plain / STARTTLS / SSL-TLS with optional authentication. Stdlib-only
(smtplib), no extra Python dependencies.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 23:48:23 +02:00
Guillaume BOURGEOIS 84be770928 feat(gmail-single-user): new single-mailbox Gmail integration
Gmail for one mailbox over OAuth 2.0 (no service account / delegation):
auth-link + exchange-code to obtain a refresh token, connectivity test,
message search/get, send/reply with attachments, attachment retrieval,
and get_incidents ingestion with an OCSF mapper. Refresh-token grant,
stdlib-only (no extra Python dependencies).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 23:29:12 +02:00
Guillaume BOURGEOIS 27615636e3 feat(gmail): new Gmail integration
Gmail API + Directory API (27 commands): mailbox search/read/send/
trash/label/move, attachments retrieval, vacation auto-reply, filters,
forwarding addresses, delegates, and Workspace user administration;
message ingestion (get_incidents) with a bundled OCSF mapper.
Service-account auth with domain-wide delegation (JWT RS256), runs on
a remote engine (requires PyJWT + cryptography).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 23:18:07 +02:00
Guillaume BOURGEOIS 3f754d14e9 feat(google-drive): new Google Drive integration
Drive API v3 (25 commands): shared drive management, change tracking,
Drive activity queries, file search/get/create/upload/download/copy/
move/delete, permission list/create/update/delete, and Drive labels.
Service-account auth with domain-wide delegation (JWT RS256), runs on
a remote engine (requires PyJWT + cryptography).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 23:17:58 +02:00
Guillaume BOURGEOIS dbb740f476 feat(google-calendar): new Google Calendar integration
Calendar API v3 ACL management: create access control rules (grant a
role to a user/group/domain/public) and list a calendar's ACL rules.
Service-account auth with domain-wide delegation (JWT RS256), runs on
a remote engine (requires PyJWT + cryptography).

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

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