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>
This commit is contained in:
Guillaume BOURGEOIS
2026-07-12 21:59:07 +02:00
parent 232e310f77
commit a4241a5a5d
7 changed files with 483 additions and 0 deletions
+83
View File
@@ -0,0 +1,83 @@
id: cisco_ise
name: Cisco ISE
version: 1.0.0
description: "Cisco Identity Services Engine (ERS + ANC API) — network access containment: list and read endpoints, apply an Adaptive Network Control (ANC) quarantine policy to an endpoint, and clear it. HTTP Basic authentication; stdlib-only, no extra Python dependencies."
changelog: "1.0.0 — Initial release: list/get endpoints, apply/clear ANC policy, list ANC policies."
category: network
# Per-instance configuration. HTTP Basic auth against the ERS API (port 9060).
config_schema:
properties:
base_url:
type: string
description: "ISE URL including the ERS port (e.g. https://ise.example.com:9060)"
username:
type: string
description: "ERS admin username"
password:
type: string
description: "ERS admin password"
x-soar-sensitive: true
insecure:
type: boolean
description: "Trust any TLS certificate (not secure)"
default: false
required:
- base_url
- username
- password
commands:
- id: list_endpoints
name: ise-list-endpoints
description: "List endpoints (optionally filter by MAC)."
risk: read
inputs_schema:
properties:
mac: { type: string, description: "Optional MAC address filter" }
required: []
outputs_schema: { properties: {} }
- id: get_endpoint
name: ise-get-endpoint
description: "Get an endpoint by ID."
risk: read
inputs_schema:
properties:
endpoint_id: { type: string, description: "Endpoint ID" }
required: [endpoint_id]
outputs_schema: { properties: {} }
- id: apply_anc_policy
name: ise-apply-anc-policy
description: "Apply an ANC policy to an endpoint by MAC (quarantine — containment)."
inputs_schema:
properties:
mac: { type: string, description: "Endpoint MAC address" }
policy_name: { type: string, description: "ANC policy name (e.g. Quarantine)" }
required: [mac, policy_name]
outputs_schema: { properties: {} }
- id: clear_anc_policy
name: ise-clear-anc-policy
description: "Clear the ANC policy from an endpoint by MAC."
inputs_schema:
properties:
mac: { type: string, description: "Endpoint MAC address" }
policy_name: { type: string, description: "ANC policy name currently applied" }
required: [mac, policy_name]
outputs_schema: { properties: {} }
- id: list_anc_policies
name: ise-list-anc-policies
description: "List ANC policies."
risk: read
inputs_schema:
properties: {}
required: []
outputs_schema: { properties: {} }
- id: test_connection
name: ise-test-connection
description: "Verify connectivity and credentials (used by the Test button)."
risk: read
inputs_schema:
properties: {}
required: []
outputs_schema: { properties: {} }
@@ -0,0 +1,77 @@
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 _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 = str(cfg.get("base_url", "")).rstrip("/") + "/ers/config" + 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()
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):
mac = inputs.get("mac")
policy_name = inputs.get("policy_name")
if not mac:
raise Exception("mac is required")
if not policy_name:
raise Exception("policy_name is required")
body = {
"OperationAdditionalData": {
"additionalData": [
{"name": "macAddress", "value": mac},
{"name": "policyName", "value": policy_name},
]
}
}
result = request("PUT", "/ancendpoint/apply", cfg, body=body)
if not result:
return {"ok": True, "mac": mac, "policy": policy_name, "action": "apply"}
return result
_run(main)
@@ -0,0 +1,77 @@
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 _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 = str(cfg.get("base_url", "")).rstrip("/") + "/ers/config" + 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()
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):
mac = inputs.get("mac")
policy_name = inputs.get("policy_name")
if not mac:
raise Exception("mac is required")
if not policy_name:
raise Exception("policy_name is required")
body = {
"OperationAdditionalData": {
"additionalData": [
{"name": "macAddress", "value": mac},
{"name": "policyName", "value": policy_name},
]
}
}
result = request("PUT", "/ancendpoint/clear", cfg, body=body)
if not result:
return {"ok": True, "mac": mac, "policy": policy_name, "action": "clear"}
return result
_run(main)
@@ -0,0 +1,63 @@
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 _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 = str(cfg.get("base_url", "")).rstrip("/") + "/ers/config" + 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()
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):
endpoint_id = inputs.get("endpoint_id")
if not endpoint_id:
raise Exception("endpoint_id is required")
return request("GET", "/endpoint/" + q(endpoint_id), cfg)
_run(main)
@@ -0,0 +1,60 @@
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 _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 = str(cfg.get("base_url", "")).rstrip("/") + "/ers/config" + 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()
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", "/ancpolicy", cfg)
_run(main)
@@ -0,0 +1,62 @@
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 _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 = str(cfg.get("base_url", "")).rstrip("/") + "/ers/config" + 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()
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):
mac = inputs.get("mac")
params = {"filter": "mac.EQ." + mac} if mac else {}
return request("GET", "/endpoint", cfg, params=params)
_run(main)
@@ -0,0 +1,61 @@
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 _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 = str(cfg.get("base_url", "")).rstrip("/") + "/ers/config" + 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()
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", "/endpoint", cfg, params={"size": 1})
return {"ok": True}
_run(main)