diff --git a/integrations/cloudflare/manifest.yaml b/integrations/cloudflare/manifest.yaml new file mode 100644 index 0000000..5ebfced --- /dev/null +++ b/integrations/cloudflare/manifest.yaml @@ -0,0 +1,108 @@ +id: cloudflare +name: Cloudflare +version: 1.0.0 +description: "Cloudflare (client API v4) — network containment at the edge: list zones, block/unblock IPs (firewall access rules), list access rules, and manage DNS records (list/create/delete). API-token (Bearer) authentication; stdlib-only, no extra Python dependencies." +changelog: "1.0.0 — Initial release: list/get zones, block IP, list/delete access rules, list/create/delete DNS records." +category: network + +# Per-instance configuration. The API token is sent as 'Authorization: Bearer '. +config_schema: + properties: + api_token: + type: string + description: "Cloudflare API token (scoped to the needed zones/permissions)" + x-soar-sensitive: true + required: + - api_token + +commands: + - id: list_zones + name: cloudflare-list-zones + description: "List zones (domains) in the account." + risk: read + inputs_schema: + properties: + name: { type: string, description: "Optional zone name filter" } + required: [] + outputs_schema: { properties: {} } + - id: get_zone + name: cloudflare-get-zone + description: "Get a single zone by ID." + risk: read + inputs_schema: + properties: + zone_id: { type: string, description: "Zone ID" } + required: [zone_id] + outputs_schema: { properties: {} } + - id: block_ip + name: cloudflare-block-ip + description: "Create a firewall access rule to block (or challenge) an IP, range, ASN, or country." + inputs_schema: + properties: + zone_id: { type: string, description: "Zone ID" } + value: { type: string, description: "IP, CIDR, ASN (AS####), or 2-letter country code" } + target: { type: string, description: "ip, ip_range, asn, or country (default ip)" } + mode: { type: string, description: "block, challenge, js_challenge, or whitelist (default block)" } + notes: { type: string, description: "Optional note" } + required: [zone_id, value] + outputs_schema: { properties: {} } + - id: list_access_rules + name: cloudflare-list-access-rules + description: "List firewall access rules for a zone." + risk: read + inputs_schema: + properties: + zone_id: { type: string, description: "Zone ID" } + required: [zone_id] + outputs_schema: { properties: {} } + - id: delete_access_rule + name: cloudflare-delete-access-rule + description: "Delete a firewall access rule (unblock)." + inputs_schema: + properties: + zone_id: { type: string, description: "Zone ID" } + rule_id: { type: string, description: "Access rule ID" } + required: [zone_id, rule_id] + outputs_schema: { properties: {} } + - id: list_dns_records + name: cloudflare-list-dns-records + description: "List DNS records for a zone." + risk: read + inputs_schema: + properties: + zone_id: { type: string, description: "Zone ID" } + record_type: { type: string, description: "Optional record type filter (A, AAAA, CNAME, TXT, ...)" } + name: { type: string, description: "Optional record name filter" } + required: [zone_id] + outputs_schema: { properties: {} } + - id: create_dns_record + name: cloudflare-create-dns-record + description: "Create a DNS record (e.g. sinkhole a malicious host)." + inputs_schema: + properties: + zone_id: { type: string, description: "Zone ID" } + record_type: { type: string, description: "Record type (A, AAAA, CNAME, TXT, ...)" } + name: { type: string, description: "Record name" } + content: { type: string, description: "Record content (IP or target)" } + ttl: { type: number, description: "TTL seconds (1 = automatic, default 1)" } + proxied: { type: boolean, description: "Proxy through Cloudflare (default false)" } + required: [zone_id, record_type, name, content] + outputs_schema: { properties: {} } + - id: delete_dns_record + name: cloudflare-delete-dns-record + description: "Delete a DNS record." + inputs_schema: + properties: + zone_id: { type: string, description: "Zone ID" } + record_id: { type: string, description: "DNS record ID" } + required: [zone_id, record_id] + outputs_schema: { properties: {} } + + - id: test_connection + name: cloudflare-test-connection + description: "Verify the API token (used by the Test button)." + risk: read + inputs_schema: + properties: {} + required: [] + outputs_schema: { properties: {} } diff --git a/integrations/cloudflare/scripts/block_ip.py b/integrations/cloudflare/scripts/block_ip.py new file mode 100644 index 0000000..cd8199b --- /dev/null +++ b/integrations/cloudflare/scripts/block_ip.py @@ -0,0 +1,60 @@ +import json, os, sys, urllib.parse, urllib.request, urllib.error + +BASE = "https://api.cloudflare.com/client/v4" + + +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("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) + + +q = lambda v: urllib.parse.quote(str(v), safe="") + + +def main(cfg, inputs): + zone_id = inputs.get("zone_id") + if not zone_id: + raise Exception("zone_id is required") + value = inputs.get("value") + if not value: + raise Exception("value is required") + target = inputs.get("target") or "ip" + mode = inputs.get("mode") or "block" + notes = inputs.get("notes") + body = {"mode": mode, "configuration": {"target": target, "value": value}} + if notes: + body["notes"] = notes + return request("POST", "/zones/" + q(zone_id) + "/firewall/access_rules/rules", cfg, body=body) + + +_run(main) diff --git a/integrations/cloudflare/scripts/create_dns_record.py b/integrations/cloudflare/scripts/create_dns_record.py new file mode 100644 index 0000000..35e6c52 --- /dev/null +++ b/integrations/cloudflare/scripts/create_dns_record.py @@ -0,0 +1,69 @@ +import json, os, sys, urllib.parse, urllib.request, urllib.error + +BASE = "https://api.cloudflare.com/client/v4" + + +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("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) + + +q = lambda v: urllib.parse.quote(str(v), safe="") + + +def main(cfg, inputs): + zone_id = inputs.get("zone_id") + if not zone_id: + raise Exception("zone_id is required") + record_type = inputs.get("record_type") + if not record_type: + raise Exception("record_type is required") + name = inputs.get("name") + if not name: + raise Exception("name is required") + content = inputs.get("content") + if not content: + raise Exception("content is required") + ttl = inputs.get("ttl") + proxied = inputs.get("proxied") + body = { + "type": record_type, + "name": name, + "content": content, + "ttl": int(ttl or 1), + "proxied": bool(proxied), + } + return request("POST", "/zones/" + q(zone_id) + "/dns_records", cfg, body=body) + + +_run(main) diff --git a/integrations/cloudflare/scripts/delete_access_rule.py b/integrations/cloudflare/scripts/delete_access_rule.py new file mode 100644 index 0000000..0db4150 --- /dev/null +++ b/integrations/cloudflare/scripts/delete_access_rule.py @@ -0,0 +1,54 @@ +import json, os, sys, urllib.parse, urllib.request, urllib.error + +BASE = "https://api.cloudflare.com/client/v4" + + +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("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) + + +q = lambda v: urllib.parse.quote(str(v), safe="") + + +def main(cfg, inputs): + zone_id = inputs.get("zone_id") + if not zone_id: + raise Exception("zone_id is required") + rule_id = inputs.get("rule_id") + if not rule_id: + raise Exception("rule_id is required") + return request("DELETE", "/zones/" + q(zone_id) + "/firewall/access_rules/rules/" + q(rule_id), cfg) + + +_run(main) diff --git a/integrations/cloudflare/scripts/delete_dns_record.py b/integrations/cloudflare/scripts/delete_dns_record.py new file mode 100644 index 0000000..e2d9d91 --- /dev/null +++ b/integrations/cloudflare/scripts/delete_dns_record.py @@ -0,0 +1,54 @@ +import json, os, sys, urllib.parse, urllib.request, urllib.error + +BASE = "https://api.cloudflare.com/client/v4" + + +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("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) + + +q = lambda v: urllib.parse.quote(str(v), safe="") + + +def main(cfg, inputs): + zone_id = inputs.get("zone_id") + if not zone_id: + raise Exception("zone_id is required") + record_id = inputs.get("record_id") + if not record_id: + raise Exception("record_id is required") + return request("DELETE", "/zones/" + q(zone_id) + "/dns_records/" + q(record_id), cfg) + + +_run(main) diff --git a/integrations/cloudflare/scripts/get_zone.py b/integrations/cloudflare/scripts/get_zone.py new file mode 100644 index 0000000..d11da49 --- /dev/null +++ b/integrations/cloudflare/scripts/get_zone.py @@ -0,0 +1,51 @@ +import json, os, sys, urllib.parse, urllib.request, urllib.error + +BASE = "https://api.cloudflare.com/client/v4" + + +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("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) + + +q = lambda v: urllib.parse.quote(str(v), safe="") + + +def main(cfg, inputs): + zone_id = inputs.get("zone_id") + if not zone_id: + raise Exception("zone_id is required") + return request("GET", "/zones/" + q(zone_id), cfg) + + +_run(main) diff --git a/integrations/cloudflare/scripts/list_access_rules.py b/integrations/cloudflare/scripts/list_access_rules.py new file mode 100644 index 0000000..ff77448 --- /dev/null +++ b/integrations/cloudflare/scripts/list_access_rules.py @@ -0,0 +1,51 @@ +import json, os, sys, urllib.parse, urllib.request, urllib.error + +BASE = "https://api.cloudflare.com/client/v4" + + +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("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) + + +q = lambda v: urllib.parse.quote(str(v), safe="") + + +def main(cfg, inputs): + zone_id = inputs.get("zone_id") + if not zone_id: + raise Exception("zone_id is required") + return request("GET", "/zones/" + q(zone_id) + "/firewall/access_rules/rules", cfg, params={"per_page": 50}) + + +_run(main) diff --git a/integrations/cloudflare/scripts/list_dns_records.py b/integrations/cloudflare/scripts/list_dns_records.py new file mode 100644 index 0000000..eaf4102 --- /dev/null +++ b/integrations/cloudflare/scripts/list_dns_records.py @@ -0,0 +1,58 @@ +import json, os, sys, urllib.parse, urllib.request, urllib.error + +BASE = "https://api.cloudflare.com/client/v4" + + +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("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) + + +q = lambda v: urllib.parse.quote(str(v), safe="") + + +def main(cfg, inputs): + zone_id = inputs.get("zone_id") + if not zone_id: + raise Exception("zone_id is required") + record_type = inputs.get("record_type") + name = inputs.get("name") + return request( + "GET", + "/zones/" + q(zone_id) + "/dns_records", + cfg, + params={"type": record_type, "name": name, "per_page": 100}, + ) + + +_run(main) diff --git a/integrations/cloudflare/scripts/list_zones.py b/integrations/cloudflare/scripts/list_zones.py new file mode 100644 index 0000000..3e7b918 --- /dev/null +++ b/integrations/cloudflare/scripts/list_zones.py @@ -0,0 +1,46 @@ +import json, os, sys, urllib.parse, urllib.request, urllib.error + +BASE = "https://api.cloudflare.com/client/v4" + + +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("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): + name = inputs.get("name") + return request("GET", "/zones", cfg, params={"name": name, "per_page": 50}) + + +_run(main) diff --git a/integrations/cloudflare/scripts/test_connection.py b/integrations/cloudflare/scripts/test_connection.py new file mode 100644 index 0000000..1e229ab --- /dev/null +++ b/integrations/cloudflare/scripts/test_connection.py @@ -0,0 +1,46 @@ +import json, os, sys, urllib.parse, urllib.request, urllib.error + +BASE = "https://api.cloudflare.com/client/v4" + + +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("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", "/user/tokens/verify", cfg) + return {"ok": True} + + +_run(main)