Compare commits

...

3 Commits

Author SHA1 Message Date
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
23 changed files with 1518 additions and 0 deletions
+101
View File
@@ -0,0 +1,101 @@
id: infoblox
name: Infoblox NIOS
version: 1.0.0
description: "Infoblox NIOS (WAPI) — DNS-layer containment and DDI lookups: block a domain via an RPZ record (DNS sinkhole), list and delete RPZ records, search host records, look up networks, and add A records. HTTP Basic authentication; stdlib-only, no extra Python dependencies."
changelog: "1.0.0 — Initial release: add/list/delete RPZ records, search hosts, get networks, add A record."
category: network
# Per-instance configuration. HTTP Basic auth against the Grid Master WAPI.
config_schema:
properties:
base_url:
type: string
description: "Grid Master URL (e.g. https://infoblox.example.com)"
username:
type: string
description: "WAPI username"
password:
type: string
description: "WAPI password"
x-soar-sensitive: true
wapi_version:
type: string
description: "WAPI version (default v2.12)"
default: "v2.12"
insecure:
type: boolean
description: "Trust any TLS certificate (not secure)"
default: false
required:
- base_url
- username
- password
commands:
- id: add_rpz_domain
name: infoblox-add-rpz-domain
description: "Block a domain by adding an RPZ CNAME record (returns NXDOMAIN — DNS sinkhole)."
inputs_schema:
properties:
domain: { type: string, description: "Domain to block (e.g. bad.example.com)" }
rpz_zone: { type: string, description: "RPZ response-policy zone name (e.g. rpz.local)" }
view: { type: string, description: "DNS view (default 'default')" }
required: [domain, rpz_zone]
outputs_schema: { properties: {} }
- id: list_rpz_records
name: infoblox-list-rpz-records
description: "List RPZ CNAME records (optionally in a specific zone)."
risk: read
inputs_schema:
properties:
rpz_zone: { type: string, description: "Optional RPZ zone filter" }
max_results: { type: number, description: "Max records (default 100)" }
required: []
outputs_schema: { properties: {} }
- id: delete_rpz_record
name: infoblox-delete-rpz-record
description: "Delete an RPZ record (unblock) by its object reference."
inputs_schema:
properties:
ref: { type: string, description: "WAPI object reference (_ref) of the record" }
required: [ref]
outputs_schema: { properties: {} }
- id: search_host
name: infoblox-search-host
description: "Search host records by name (substring match)."
risk: read
inputs_schema:
properties:
name: { type: string, description: "Host name (or partial)" }
max_results: { type: number, description: "Max records (default 100)" }
required: [name]
outputs_schema: { properties: {} }
- id: get_network
name: infoblox-get-network
description: "Look up networks (optionally filtered by CIDR)."
risk: read
inputs_schema:
properties:
network: { type: string, description: "Optional network CIDR filter (e.g. 10.0.0.0/24)" }
max_results: { type: number, description: "Max networks (default 100)" }
required: []
outputs_schema: { properties: {} }
- id: add_a_record
name: infoblox-add-a-record
description: "Add a DNS A record."
inputs_schema:
properties:
name: { type: string, description: "FQDN of the record" }
ipv4addr: { type: string, description: "IPv4 address" }
view: { type: string, description: "DNS view (default 'default')" }
required: [name, ipv4addr]
outputs_schema: { properties: {} }
- id: test_connection
name: infoblox-test-connection
description: "Verify connectivity and credentials (used by the Test button)."
risk: read
inputs_schema:
properties: {}
required: []
outputs_schema: { properties: {} }
@@ -0,0 +1,78 @@
import json, os, sys, base64, ssl, 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 _base(cfg):
return str(cfg.get("base_url", "")).rstrip("/") + "/wapi/" + str(cfg.get("wapi_version") or "v2.12")
def _auth(cfg):
raw = str(cfg.get("username", "")) + ":" + str(cfg.get("password", ""))
return "Basic " + base64.b64encode(raw.encode("utf-8")).decode("utf-8")
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, context=_ctx(cfg)) as r:
raw = r.read()
try:
return json.loads(raw) if raw else {}
except Exception:
return {"result": raw.decode("utf-8", "replace")}
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):
name = inputs.get("name")
ipv4addr = inputs.get("ipv4addr")
if not name:
raise Exception("name is required")
if not ipv4addr:
raise Exception("ipv4addr is required")
view = inputs.get("view") or "default"
body = {
"name": name,
"ipv4addr": ipv4addr,
"view": view,
}
resp = request("POST", "/record:a", cfg, body=body)
return {"result": resp}
_run(main)
@@ -0,0 +1,79 @@
import json, os, sys, base64, ssl, 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 _base(cfg):
return str(cfg.get("base_url", "")).rstrip("/") + "/wapi/" + str(cfg.get("wapi_version") or "v2.12")
def _auth(cfg):
raw = str(cfg.get("username", "")) + ":" + str(cfg.get("password", ""))
return "Basic " + base64.b64encode(raw.encode("utf-8")).decode("utf-8")
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, context=_ctx(cfg)) as r:
raw = r.read()
try:
return json.loads(raw) if raw else {}
except Exception:
return {"result": raw.decode("utf-8", "replace")}
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")
rpz_zone = inputs.get("rpz_zone")
if not domain:
raise Exception("domain is required")
if not rpz_zone:
raise Exception("rpz_zone is required")
view = inputs.get("view") or "default"
body = {
"name": str(domain) + "." + str(rpz_zone),
"canonical": "",
"rp_zone": rpz_zone,
"view": view,
}
resp = request("POST", "/record:rpz:cname", cfg, body=body)
return {"result": resp}
_run(main)
@@ -0,0 +1,72 @@
import json, os, sys, base64, ssl, 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 _base(cfg):
return str(cfg.get("base_url", "")).rstrip("/") + "/wapi/" + str(cfg.get("wapi_version") or "v2.12")
def _auth(cfg):
raw = str(cfg.get("username", "")) + ":" + str(cfg.get("password", ""))
return "Basic " + base64.b64encode(raw.encode("utf-8")).decode("utf-8")
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, context=_ctx(cfg)) as r:
raw = r.read()
try:
return json.loads(raw) if raw else {}
except Exception:
return {"result": raw.decode("utf-8", "replace")}
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):
ref = inputs.get("ref")
if not ref:
raise Exception("ref is required")
resp = request("DELETE", "/" + q(ref), cfg)
return {"deleted": resp}
_run(main)
@@ -0,0 +1,74 @@
import json, os, sys, base64, ssl, 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 _base(cfg):
return str(cfg.get("base_url", "")).rstrip("/") + "/wapi/" + str(cfg.get("wapi_version") or "v2.12")
def _auth(cfg):
raw = str(cfg.get("username", "")) + ":" + str(cfg.get("password", ""))
return "Basic " + base64.b64encode(raw.encode("utf-8")).decode("utf-8")
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, context=_ctx(cfg)) as r:
raw = r.read()
try:
return json.loads(raw) if raw else {}
except Exception:
return {"result": raw.decode("utf-8", "replace")}
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):
network = inputs.get("network")
max_results = inputs.get("max_results")
params = {
"_max_results": int(max_results or 100),
}
if network:
params["network"] = network
resp = request("GET", "/network", cfg, params=params)
return {"networks": resp}
_run(main)
@@ -0,0 +1,75 @@
import json, os, sys, base64, ssl, 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 _base(cfg):
return str(cfg.get("base_url", "")).rstrip("/") + "/wapi/" + str(cfg.get("wapi_version") or "v2.12")
def _auth(cfg):
raw = str(cfg.get("username", "")) + ":" + str(cfg.get("password", ""))
return "Basic " + base64.b64encode(raw.encode("utf-8")).decode("utf-8")
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, context=_ctx(cfg)) as r:
raw = r.read()
try:
return json.loads(raw) if raw else {}
except Exception:
return {"result": raw.decode("utf-8", "replace")}
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):
rpz_zone = inputs.get("rpz_zone")
max_results = inputs.get("max_results")
params = {
"_max_results": int(max_results or 100),
"_return_fields+": "name,canonical,rp_zone,view",
}
if rpz_zone:
params["rp_zone"] = rpz_zone
resp = request("GET", "/record:rpz:cname", cfg, params=params)
return {"records": resp}
_run(main)
@@ -0,0 +1,74 @@
import json, os, sys, base64, ssl, 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 _base(cfg):
return str(cfg.get("base_url", "")).rstrip("/") + "/wapi/" + str(cfg.get("wapi_version") or "v2.12")
def _auth(cfg):
raw = str(cfg.get("username", "")) + ":" + str(cfg.get("password", ""))
return "Basic " + base64.b64encode(raw.encode("utf-8")).decode("utf-8")
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, context=_ctx(cfg)) as r:
raw = r.read()
try:
return json.loads(raw) if raw else {}
except Exception:
return {"result": raw.decode("utf-8", "replace")}
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):
name = inputs.get("name")
if not name:
raise Exception("name is required")
max_results = inputs.get("max_results")
params = {
"name~": name,
"_max_results": int(max_results or 100),
}
resp = request("GET", "/record:host", cfg, params=params)
return {"records": resp}
_run(main)
@@ -0,0 +1,65 @@
import json, os, sys, base64, ssl, 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 _base(cfg):
return str(cfg.get("base_url", "")).rstrip("/") + "/wapi/" + str(cfg.get("wapi_version") or "v2.12")
def _auth(cfg):
raw = str(cfg.get("username", "")) + ":" + str(cfg.get("password", ""))
return "Basic " + base64.b64encode(raw.encode("utf-8")).decode("utf-8")
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, context=_ctx(cfg)) as r:
raw = r.read()
try:
return json.loads(raw) if raw else {}
except Exception:
return {"result": raw.decode("utf-8", "replace")}
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", "/grid", cfg)
return {"ok": True}
_run(main)
+75
View File
@@ -0,0 +1,75 @@
id: netskope
name: Netskope
version: 1.0.0
description: "Netskope (SASE/CASB REST API v2) — web/cloud containment: list and read URL lists, replace a URL list (block list) and deploy it, and list enrolled users/clients. API-token authentication; stdlib-only, no extra Python dependencies."
changelog: "1.0.0 — Initial release: list/get URL lists, replace URL list, deploy URL lists, list users."
category: network
# Per-instance configuration. The token is sent as the 'Netskope-Api-Token' header.
config_schema:
properties:
base_url:
type: string
description: "Netskope tenant URL (e.g. https://yourtenant.goskope.com)"
api_token:
type: string
description: "Netskope API v2 token"
x-soar-sensitive: true
required:
- base_url
- api_token
commands:
- id: list_url_lists
name: netskope-list-url-lists
description: "List URL lists."
risk: read
inputs_schema:
properties: {}
required: []
outputs_schema: { properties: {} }
- id: get_url_list
name: netskope-get-url-list
description: "Get a single URL list by ID."
risk: read
inputs_schema:
properties:
list_id: { type: string, description: "URL list ID" }
required: [list_id]
outputs_schema: { properties: {} }
- id: replace_url_list
name: netskope-replace-url-list
description: "Replace the URLs of a URL list (e.g. update a block list)."
inputs_schema:
properties:
list_id: { type: string, description: "URL list ID" }
name: { type: string, description: "URL list name" }
urls: { type: string, description: "Comma-separated URLs/domains that become the list" }
list_type: { type: string, description: "exact or regex (default exact)" }
required: [list_id, name, urls]
outputs_schema: { properties: {} }
- id: deploy_url_lists
name: netskope-deploy-url-lists
description: "Deploy pending URL-list changes so they take effect."
inputs_schema:
properties: {}
required: []
outputs_schema: { properties: {} }
- id: list_users
name: netskope-list-users
description: "List enrolled users (SCIM)."
risk: read
inputs_schema:
properties:
limit: { type: number, description: "Max users (default 50)" }
required: []
outputs_schema: { properties: {} }
- id: test_connection
name: netskope-test-connection
description: "Verify connectivity and the API token (used by the Test button)."
risk: read
inputs_schema:
properties: {}
required: []
outputs_schema: { properties: {} }
@@ -0,0 +1,44 @@
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, params=None):
url = str(cfg.get("base_url", "")).rstrip("/") + 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 = {"Netskope-Api-Token": str(cfg.get("api_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):
result = request("POST", "/api/v2/policy/urllist/deploy", cfg, body={})
return result if result else {"ok": True}
_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, params=None):
url = str(cfg.get("base_url", "")).rstrip("/") + 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 = {"Netskope-Api-Token": str(cfg.get("api_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):
list_id = inputs.get("list_id")
if not list_id:
raise Exception("list_id is required")
q = lambda v: urllib.parse.quote(str(v), safe="")
return request("GET", "/api/v2/policy/urllist/" + q(list_id), cfg)
_run(main)
@@ -0,0 +1,43 @@
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, params=None):
url = str(cfg.get("base_url", "")).rstrip("/") + 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 = {"Netskope-Api-Token": str(cfg.get("api_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):
return request("GET", "/api/v2/policy/urllist", cfg)
_run(main)
@@ -0,0 +1,44 @@
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, params=None):
url = str(cfg.get("base_url", "")).rstrip("/") + 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 = {"Netskope-Api-Token": str(cfg.get("api_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):
limit = inputs.get("limit")
return request("GET", "/api/v2/scim/Users", cfg, params={"count": int(limit or 50)})
_run(main)
@@ -0,0 +1,58 @@
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, params=None):
url = str(cfg.get("base_url", "")).rstrip("/") + 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 = {"Netskope-Api-Token": str(cfg.get("api_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):
list_id = inputs.get("list_id")
if not list_id:
raise Exception("list_id is required")
name = inputs.get("name")
if not name:
raise Exception("name is required")
urls_raw = inputs.get("urls")
if not urls_raw:
raise Exception("urls is required")
urls_list = [s.strip() for s in str(urls_raw).split(",") if s.strip()]
if not urls_list:
raise Exception("urls is required")
list_type = inputs.get("list_type") or "exact"
q = lambda v: urllib.parse.quote(str(v), safe="")
body = {"name": name, "data": {"type": list_type, "urls": urls_list}}
return request("PUT", "/api/v2/policy/urllist/" + q(list_id), cfg, body=body)
_run(main)
@@ -0,0 +1,44 @@
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, params=None):
url = str(cfg.get("base_url", "")).rstrip("/") + 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 = {"Netskope-Api-Token": str(cfg.get("api_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):
request("GET", "/api/v2/policy/urllist", cfg)
return {"ok": True}
_run(main)
+93
View File
@@ -0,0 +1,93 @@
id: zscaler_zpa
name: Zscaler Private Access
version: 1.0.0
description: "Zscaler Private Access (ZPA config API) — zero-trust inventory and policy visibility: list and read application segments, list servers, server groups and segment groups, and list access policy rules. OAuth2 client-credentials authentication; stdlib-only, no extra Python dependencies."
changelog: "1.0.0 — Initial release: list/get application segments, list servers, server groups, segment groups, access policy rules."
category: network
# Per-instance configuration. Sign in with client_id + client_secret to obtain a
# bearer token; customer_id scopes the config API.
config_schema:
properties:
base_url:
type: string
description: "ZPA config API base URL"
default: "https://config.private.zscaler.com"
client_id:
type: string
description: "ZPA API client ID"
client_secret:
type: string
description: "ZPA API client secret"
x-soar-sensitive: true
customer_id:
type: string
description: "ZPA customer ID"
required:
- client_id
- client_secret
- customer_id
commands:
- id: list_application_segments
name: zpa-list-application-segments
description: "List application segments."
risk: read
inputs_schema:
properties:
page: { type: number, description: "Page number (default 1)" }
page_size: { type: number, description: "Page size (default 20)" }
required: []
outputs_schema: { properties: {} }
- id: get_application_segment
name: zpa-get-application-segment
description: "Get a single application segment by ID."
risk: read
inputs_schema:
properties:
segment_id: { type: string, description: "Application segment ID" }
required: [segment_id]
outputs_schema: { properties: {} }
- id: list_servers
name: zpa-list-servers
description: "List application servers."
risk: read
inputs_schema:
properties:
page: { type: number, description: "Page number (default 1)" }
page_size: { type: number, description: "Page size (default 20)" }
required: []
outputs_schema: { properties: {} }
- id: list_server_groups
name: zpa-list-server-groups
description: "List server groups."
risk: read
inputs_schema:
properties: {}
required: []
outputs_schema: { properties: {} }
- id: list_segment_groups
name: zpa-list-segment-groups
description: "List segment groups."
risk: read
inputs_schema:
properties: {}
required: []
outputs_schema: { properties: {} }
- id: list_access_policies
name: zpa-list-access-policies
description: "List access policy rules."
risk: read
inputs_schema:
properties: {}
required: []
outputs_schema: { properties: {} }
- id: test_connection
name: zpa-test-connection
description: "Verify the sign-in and API access (used by the Test button)."
risk: read
inputs_schema:
properties: {}
required: []
outputs_schema: { properties: {} }
@@ -0,0 +1,69 @@
import json, os, sys, urllib.parse, urllib.request, urllib.error
def _cfg():
return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
def _inputs():
return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
def _base(cfg):
return (str(cfg.get("base_url") or "https://config.private.zscaler.com")).rstrip("/")
def _token(cfg):
form = urllib.parse.urlencode({
"client_id": str(cfg.get("client_id", "")),
"client_secret": str(cfg.get("client_secret", "")),
}).encode("utf-8")
req = urllib.request.Request(_base(cfg) + "/signin", 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())
if not tok.get("access_token"):
raise Exception("Sign-in failed: " + json.dumps(tok))
return tok["access_token"]
def request(method, path, cfg, token, params=None):
# path is relative to the customer config base, e.g. /application
url = _base(cfg) + "/mgmtconfig/v1/admin/customers/" + str(cfg.get("customer_id", "")) + path
if params:
clean = {k: v for k, v in params.items() if v not in (None, "")}
if clean:
url += "?" + urllib.parse.urlencode(clean)
headers = {"Authorization": "Bearer " + token, "Accept": "application/json"}
req = urllib.request.Request(url, 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):
segment_id = inputs.get("segment_id")
if not segment_id:
raise Exception("segment_id is required")
return request("GET", "/application/" + q(segment_id), cfg, token)
_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("base_url") or "https://config.private.zscaler.com")).rstrip("/")
def _token(cfg):
form = urllib.parse.urlencode({
"client_id": str(cfg.get("client_id", "")),
"client_secret": str(cfg.get("client_secret", "")),
}).encode("utf-8")
req = urllib.request.Request(_base(cfg) + "/signin", 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())
if not tok.get("access_token"):
raise Exception("Sign-in failed: " + json.dumps(tok))
return tok["access_token"]
def request(method, path, cfg, token, params=None):
# path is relative to the customer config base, e.g. /application
url = _base(cfg) + "/mgmtconfig/v1/admin/customers/" + str(cfg.get("customer_id", "")) + path
if params:
clean = {k: v for k, v in params.items() if v not in (None, "")}
if clean:
url += "?" + urllib.parse.urlencode(clean)
headers = {"Authorization": "Bearer " + token, "Accept": "application/json"}
req = urllib.request.Request(url, 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):
return request("GET", "/policySet/rules/policyType/ACCESS_POLICY", cfg, token)
_run(main)
@@ -0,0 +1,65 @@
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") or "https://config.private.zscaler.com")).rstrip("/")
def _token(cfg):
form = urllib.parse.urlencode({
"client_id": str(cfg.get("client_id", "")),
"client_secret": str(cfg.get("client_secret", "")),
}).encode("utf-8")
req = urllib.request.Request(_base(cfg) + "/signin", 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())
if not tok.get("access_token"):
raise Exception("Sign-in failed: " + json.dumps(tok))
return tok["access_token"]
def request(method, path, cfg, token, params=None):
# path is relative to the customer config base, e.g. /application
url = _base(cfg) + "/mgmtconfig/v1/admin/customers/" + str(cfg.get("customer_id", "")) + path
if params:
clean = {k: v for k, v in params.items() if v not in (None, "")}
if clean:
url += "?" + urllib.parse.urlencode(clean)
headers = {"Authorization": "Bearer " + token, "Accept": "application/json"}
req = urllib.request.Request(url, 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):
page = inputs.get("page") or 1
page_size = inputs.get("page_size") or 20
return request("GET", "/application", cfg, token, params={"page": int(page), "pagesize": int(page_size)})
_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("base_url") or "https://config.private.zscaler.com")).rstrip("/")
def _token(cfg):
form = urllib.parse.urlencode({
"client_id": str(cfg.get("client_id", "")),
"client_secret": str(cfg.get("client_secret", "")),
}).encode("utf-8")
req = urllib.request.Request(_base(cfg) + "/signin", 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())
if not tok.get("access_token"):
raise Exception("Sign-in failed: " + json.dumps(tok))
return tok["access_token"]
def request(method, path, cfg, token, params=None):
# path is relative to the customer config base, e.g. /application
url = _base(cfg) + "/mgmtconfig/v1/admin/customers/" + str(cfg.get("customer_id", "")) + path
if params:
clean = {k: v for k, v in params.items() if v not in (None, "")}
if clean:
url += "?" + urllib.parse.urlencode(clean)
headers = {"Authorization": "Bearer " + token, "Accept": "application/json"}
req = urllib.request.Request(url, 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):
return request("GET", "/segmentGroup", cfg, token)
_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("base_url") or "https://config.private.zscaler.com")).rstrip("/")
def _token(cfg):
form = urllib.parse.urlencode({
"client_id": str(cfg.get("client_id", "")),
"client_secret": str(cfg.get("client_secret", "")),
}).encode("utf-8")
req = urllib.request.Request(_base(cfg) + "/signin", 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())
if not tok.get("access_token"):
raise Exception("Sign-in failed: " + json.dumps(tok))
return tok["access_token"]
def request(method, path, cfg, token, params=None):
# path is relative to the customer config base, e.g. /application
url = _base(cfg) + "/mgmtconfig/v1/admin/customers/" + str(cfg.get("customer_id", "")) + path
if params:
clean = {k: v for k, v in params.items() if v not in (None, "")}
if clean:
url += "?" + urllib.parse.urlencode(clean)
headers = {"Authorization": "Bearer " + token, "Accept": "application/json"}
req = urllib.request.Request(url, 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):
return request("GET", "/serverGroup", cfg, token)
_run(main)
@@ -0,0 +1,65 @@
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") or "https://config.private.zscaler.com")).rstrip("/")
def _token(cfg):
form = urllib.parse.urlencode({
"client_id": str(cfg.get("client_id", "")),
"client_secret": str(cfg.get("client_secret", "")),
}).encode("utf-8")
req = urllib.request.Request(_base(cfg) + "/signin", 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())
if not tok.get("access_token"):
raise Exception("Sign-in failed: " + json.dumps(tok))
return tok["access_token"]
def request(method, path, cfg, token, params=None):
# path is relative to the customer config base, e.g. /application
url = _base(cfg) + "/mgmtconfig/v1/admin/customers/" + str(cfg.get("customer_id", "")) + path
if params:
clean = {k: v for k, v in params.items() if v not in (None, "")}
if clean:
url += "?" + urllib.parse.urlencode(clean)
headers = {"Authorization": "Bearer " + token, "Accept": "application/json"}
req = urllib.request.Request(url, 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):
page = inputs.get("page") or 1
page_size = inputs.get("page_size") or 20
return request("GET", "/server", cfg, token, params={"page": int(page), "pagesize": int(page_size)})
_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") or "https://config.private.zscaler.com")).rstrip("/")
def _token(cfg):
form = urllib.parse.urlencode({
"client_id": str(cfg.get("client_id", "")),
"client_secret": str(cfg.get("client_secret", "")),
}).encode("utf-8")
req = urllib.request.Request(_base(cfg) + "/signin", 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())
if not tok.get("access_token"):
raise Exception("Sign-in failed: " + json.dumps(tok))
return tok["access_token"]
def request(method, path, cfg, token, params=None):
# path is relative to the customer config base, e.g. /application
url = _base(cfg) + "/mgmtconfig/v1/admin/customers/" + str(cfg.get("customer_id", "")) + path
if params:
clean = {k: v for k, v in params.items() if v not in (None, "")}
if clean:
url += "?" + urllib.parse.urlencode(clean)
headers = {"Authorization": "Bearer " + token, "Accept": "application/json"}
req = urllib.request.Request(url, 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", "/application", cfg, token, params={"page": 1, "pagesize": 1})
return {"ok": True}
_run(main)