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>
This commit is contained in:
@@ -0,0 +1,62 @@
|
|||||||
|
id: cisco_umbrella_enforcement
|
||||||
|
name: Cisco Umbrella Enforcement
|
||||||
|
version: 1.0.0
|
||||||
|
description: "Cisco Umbrella Enforcement API — DNS-layer containment: push malicious domains to the enforcement block list, list enforced domains, and remove them. Customer-key authentication; stdlib-only, no extra Python dependencies."
|
||||||
|
changelog: "1.0.0 — Initial release: add domain event (block), list domains, delete domain."
|
||||||
|
category: network
|
||||||
|
|
||||||
|
# Per-instance configuration. The customer key is appended as the 'customerKey'
|
||||||
|
# query parameter on every request.
|
||||||
|
config_schema:
|
||||||
|
properties:
|
||||||
|
api_url:
|
||||||
|
type: string
|
||||||
|
description: "Enforcement API base URL"
|
||||||
|
default: "https://s-platform.api.opendns.com"
|
||||||
|
api_key:
|
||||||
|
type: string
|
||||||
|
description: "Umbrella Enforcement customer key"
|
||||||
|
x-soar-sensitive: true
|
||||||
|
required:
|
||||||
|
- api_key
|
||||||
|
|
||||||
|
commands:
|
||||||
|
- id: add_domain
|
||||||
|
name: umbrella-add-domain
|
||||||
|
description: "Submit a security event that adds a domain to the Umbrella enforcement block list."
|
||||||
|
inputs_schema:
|
||||||
|
properties:
|
||||||
|
domain: { type: string, description: "Domain to block (e.g. malicious.example.com)" }
|
||||||
|
url: { type: string, description: "Destination URL (defaults to http://<domain>/)" }
|
||||||
|
device_id: { type: string, description: "Reporting device id (defaults to a fixed Riposte device id)" }
|
||||||
|
device_version: { type: string, description: "Reporting device version (default 1.0)" }
|
||||||
|
provider_name: { type: string, description: "Provider name (default 'Riposte SOAR')" }
|
||||||
|
required: [domain]
|
||||||
|
outputs_schema: { properties: {} }
|
||||||
|
- id: list_domains
|
||||||
|
name: umbrella-list-domains
|
||||||
|
description: "List the domains currently in the enforcement block list."
|
||||||
|
risk: read
|
||||||
|
inputs_schema:
|
||||||
|
properties:
|
||||||
|
page: { type: number, description: "Page number (default 1)" }
|
||||||
|
limit: { type: number, description: "Domains per page (default 200)" }
|
||||||
|
required: []
|
||||||
|
outputs_schema: { properties: {} }
|
||||||
|
- id: delete_domain
|
||||||
|
name: umbrella-delete-domain
|
||||||
|
description: "Remove a domain from the enforcement block list by its numeric id."
|
||||||
|
inputs_schema:
|
||||||
|
properties:
|
||||||
|
domain_id: { type: string, description: "Numeric id of the domain to remove (from umbrella-list-domains)" }
|
||||||
|
required: [domain_id]
|
||||||
|
outputs_schema: { properties: {} }
|
||||||
|
|
||||||
|
- id: test_connection
|
||||||
|
name: umbrella-test-connection
|
||||||
|
description: "Verify connectivity and the customer key (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
|
||||||
|
import datetime
|
||||||
|
|
||||||
|
|
||||||
|
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("api_url") or "https://s-platform.api.opendns.com")).rstrip("/")
|
||||||
|
|
||||||
|
|
||||||
|
def request(method, path, cfg, body=None, params=None):
|
||||||
|
p = {"customerKey": str(cfg.get("api_key", ""))}
|
||||||
|
if params:
|
||||||
|
p.update(params)
|
||||||
|
url = _base(cfg) + path + "?" + urllib.parse.urlencode(p)
|
||||||
|
data = json.dumps(body).encode("utf-8") if body is not None else None
|
||||||
|
headers = {"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 = str(inputs.get("domain") or "").strip()
|
||||||
|
if not domain:
|
||||||
|
raise Exception("domain is required")
|
||||||
|
|
||||||
|
url = str(inputs.get("url") or ("http://" + domain + "/"))
|
||||||
|
device_id = str(inputs.get("device_id") or "ba6a59f4-e692-4724-ba36-c28132c761de")
|
||||||
|
device_version = str(inputs.get("device_version") or "1.0")
|
||||||
|
provider_name = str(inputs.get("provider_name") or "Riposte SOAR")
|
||||||
|
|
||||||
|
ts = datetime.datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%S.0Z")
|
||||||
|
|
||||||
|
body = {
|
||||||
|
"alertTime": ts,
|
||||||
|
"deviceId": device_id,
|
||||||
|
"deviceVersion": device_version,
|
||||||
|
"dstDomain": domain,
|
||||||
|
"dstUrl": url,
|
||||||
|
"eventTime": ts,
|
||||||
|
"protocolVersion": "1.0a",
|
||||||
|
"providerName": provider_name,
|
||||||
|
}
|
||||||
|
|
||||||
|
return request("POST", "/1.0/events", cfg, body=body)
|
||||||
|
|
||||||
|
|
||||||
|
_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("api_url") or "https://s-platform.api.opendns.com")).rstrip("/")
|
||||||
|
|
||||||
|
|
||||||
|
def request(method, path, cfg, body=None, params=None):
|
||||||
|
p = {"customerKey": str(cfg.get("api_key", ""))}
|
||||||
|
if params:
|
||||||
|
p.update(params)
|
||||||
|
url = _base(cfg) + path + "?" + urllib.parse.urlencode(p)
|
||||||
|
data = json.dumps(body).encode("utf-8") if body is not None else None
|
||||||
|
headers = {"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_id = str(inputs.get("domain_id") or "").strip()
|
||||||
|
if not domain_id:
|
||||||
|
raise Exception("domain_id is required")
|
||||||
|
|
||||||
|
result = request("DELETE", "/1.0/domains/" + urllib.parse.quote(str(domain_id), safe=""), cfg)
|
||||||
|
if not result:
|
||||||
|
return {"ok": True, "deleted": domain_id}
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
_run(main)
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
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("api_url") or "https://s-platform.api.opendns.com")).rstrip("/")
|
||||||
|
|
||||||
|
|
||||||
|
def request(method, path, cfg, body=None, params=None):
|
||||||
|
p = {"customerKey": str(cfg.get("api_key", ""))}
|
||||||
|
if params:
|
||||||
|
p.update(params)
|
||||||
|
url = _base(cfg) + path + "?" + urllib.parse.urlencode(p)
|
||||||
|
data = json.dumps(body).encode("utf-8") if body is not None else None
|
||||||
|
headers = {"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):
|
||||||
|
page_raw = inputs.get("page")
|
||||||
|
limit_raw = inputs.get("limit")
|
||||||
|
|
||||||
|
try:
|
||||||
|
page = int(page_raw) if page_raw not in (None, "") else 1
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
page = 1
|
||||||
|
|
||||||
|
try:
|
||||||
|
limit = int(limit_raw) if limit_raw not in (None, "") else 200
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
limit = 200
|
||||||
|
|
||||||
|
return request("GET", "/1.0/domains", cfg, params={"page": page, "limit": limit})
|
||||||
|
|
||||||
|
|
||||||
|
_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 _base(cfg):
|
||||||
|
return (str(cfg.get("api_url") or "https://s-platform.api.opendns.com")).rstrip("/")
|
||||||
|
|
||||||
|
|
||||||
|
def request(method, path, cfg, body=None, params=None):
|
||||||
|
p = {"customerKey": str(cfg.get("api_key", ""))}
|
||||||
|
if params:
|
||||||
|
p.update(params)
|
||||||
|
url = _base(cfg) + path + "?" + urllib.parse.urlencode(p)
|
||||||
|
data = json.dumps(body).encode("utf-8") if body is not None else None
|
||||||
|
headers = {"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", "/1.0/domains", cfg, params={"page": 1, "limit": 1})
|
||||||
|
return {"ok": True}
|
||||||
|
|
||||||
|
|
||||||
|
_run(main)
|
||||||
Reference in New Issue
Block a user