diff --git a/ansible/cloudflare/cloudflare_rules.py b/ansible/cloudflare/cloudflare_rules.py new file mode 100644 index 0000000..008794e --- /dev/null +++ b/ansible/cloudflare/cloudflare_rules.py @@ -0,0 +1,177 @@ +#!/usr/bin/env python3 + +""" +Cloudflare Rules Export / Import Script + +Usage: + Export rules: + python cloudflare_rules.py export + + Import rules: + python cloudflare_rules.py import + + Alternatively, set CLOUDFLARE_API_TOKEN and CLOUDFLARE_ZONE_ID + environment variables and run: + python cloudflare_rules.py export +""" + +import json +import os +import sys + +import requests + + +def usage(): + print("Usage: python cloudflare_rules.py {export|import} [API_TOKEN] [ZONE_ID]") + print() + print("Arguments:") + print(" export/import Operation to perform") + print(" API_TOKEN Cloudflare API Token (optional if CLOUDFLARE_API_TOKEN env var is set)") + print(" ZONE_ID Cloudflare Zone ID (optional if CLOUDFLARE_ZONE_ID env var is set)") + sys.exit(1) + + +def call_cloudflare(method, url, api_token, output_file=None, data_file=None, description=""): + print(f"{description}...") + + headers = { + "Authorization": f"Bearer {api_token}", + "Content-Type": "application/json", + } + + data = None + if data_file: + if not os.path.isfile(data_file): + print(f"Error: Data file {data_file} not found. Skipping {description}.") + return False + with open(data_file, "r") as f: + data = f.read() + + response = requests.request(method, url, headers=headers, data=data) + + if response.status_code < 200 or response.status_code > 299: + print(f"Error: {description} failed with HTTP status {response.status_code}") + if response.text: + print(f"Response: {response.text}") + return False + + if output_file: + with open(output_file, "w") as f: + f.write(response.text) + + return True + + +def export_rules(api_token, zone_id): + base_url = f"https://api.cloudflare.com/client/v4/zones/{zone_id}/rulesets" + pagerules_url = f"https://api.cloudflare.com/client/v4/zones/{zone_id}/pagerules" + + failed = False + if not call_cloudflare("GET", f"{base_url}/phases/http_request_firewall_custom/entrypoint", api_token, output_file="waf_rules.json", description="Exporting WAF Custom Rules"): + failed = True + if not call_cloudflare("GET", f"{base_url}/phases/http_ratelimit/entrypoint", api_token, output_file="rate_limit_rules.json", description="Exporting Rate Limiting Rules"): + failed = True + if not call_cloudflare("GET", f"{base_url}/phases/http_request_cache_settings/entrypoint", api_token, output_file="cache_rules.json", description="Exporting Cache Rules"): + failed = True + if not call_cloudflare("GET", f"{base_url}/phases/http_request_dynamic_redirect/entrypoint", api_token, output_file="redirect_rules.json", description="Exporting Redirect Rules"): + failed = True + if not call_cloudflare("GET", pagerules_url, api_token, output_file="page_rules.json", description="Exporting Page Rules"): + failed = True + + if not failed: + print("Export completed successfully. JSON files saved in current directory.") + else: + print("Export completed with errors.") + sys.exit(1) + + +def import_page_rules(api_token, zone_id): + pagerules_url = f"https://api.cloudflare.com/client/v4/zones/{zone_id}/pagerules" + + print("Importing Page Rules...") + if not os.path.isfile("page_rules.json"): + print("Error: Data file page_rules.json not found. Skipping Page Rules import.") + return False + + with open("page_rules.json", "r") as f: + data = json.load(f) + + rules = data.get("result", []) + if not rules: + print("No page rules found to import.") + return True + + headers = { + "Authorization": f"Bearer {api_token}", + "Content-Type": "application/json", + } + + page_failed = False + for i, rule in enumerate(rules): + payload = { + "targets": rule["targets"], + "actions": rule["actions"], + "priority": rule.get("priority", i + 1), + "status": rule.get("status", "active"), + } + + response = requests.post(pagerules_url, headers=headers, json=payload) + + if response.status_code < 200 or response.status_code > 299: + print(f"Error: Importing Page Rule {i} failed with HTTP status {response.status_code}") + if response.text: + print(f"Response: {response.text}") + page_failed = True + + if page_failed: + return False + + print("Page Rules imported successfully.") + return True + + +def import_rules(api_token, zone_id): + base_url = f"https://api.cloudflare.com/client/v4/zones/{zone_id}/rulesets" + + failed = False + if not call_cloudflare("PUT", f"{base_url}/phases/http_request_firewall_custom/entrypoint", api_token, data_file="waf_rules.json", description="Importing WAF Custom Rules"): + failed = True + if not call_cloudflare("PUT", f"{base_url}/phases/http_ratelimit/entrypoint", api_token, data_file="rate_limit_rules.json", description="Importing Rate Limiting Rules"): + failed = True + if not call_cloudflare("PUT", f"{base_url}/phases/http_request_cache_settings/entrypoint", api_token, data_file="cache_rules.json", description="Importing Cache Rules"): + failed = True + if not call_cloudflare("PUT", f"{base_url}/phases/http_request_dynamic_redirect/entrypoint", api_token, data_file="redirect_rules.json", description="Importing Redirect Rules"): + failed = True + if not import_page_rules(api_token, zone_id): + failed = True + + if not failed: + print("Import completed successfully.") + else: + print("Import completed with errors.") + sys.exit(1) + + +def main(): + if len(sys.argv) < 2: + usage() + + operation = sys.argv[1] + api_token = sys.argv[2] if len(sys.argv) > 2 else os.environ.get("CLOUDFLARE_API_TOKEN", "") + zone_id = sys.argv[3] if len(sys.argv) > 3 else os.environ.get("CLOUDFLARE_ZONE_ID", "") + + if not api_token or not zone_id: + print("Error: API_TOKEN and ZONE_ID must be provided either as arguments or environment variables.") + usage() + + if operation == "export": + export_rules(api_token, zone_id) + elif operation == "import": + import_rules(api_token, zone_id) + else: + usage() + + +if __name__ == "__main__": + main() diff --git a/ansible/cloudflare/cloudflare_rules/cache_rules.json b/ansible/cloudflare/cloudflare_rules/cache_rules.json new file mode 100644 index 0000000..bcb1286 --- /dev/null +++ b/ansible/cloudflare/cloudflare_rules/cache_rules.json @@ -0,0 +1,10 @@ +{ + "result": null, + "success": false, + "errors": [ + { + "message": "request is not authorized" + } + ], + "messages": [] +} diff --git a/ansible/cloudflare/cloudflare_rules/page_rules.json b/ansible/cloudflare/cloudflare_rules/page_rules.json new file mode 100644 index 0000000..ac5e026 --- /dev/null +++ b/ansible/cloudflare/cloudflare_rules/page_rules.json @@ -0,0 +1 @@ +{"result":[{"id":"380581670412fcfb02a75e0f049f84fe","targets":[{"target":"url","constraint":{"operator":"matches","value":"*doaj.org\/oai*"}}],"actions":[{"id":"browser_check","value":"off"},{"id":"security_level","value":"essentially_off"},{"id":"cache_level","value":"bypass"}],"priority":3,"status":"active","created_on":"2019-04-03T06:14:26.000000Z","modified_on":"2022-07-14T12:07:36.000000Z"},{"id":"56386c243bba2ac9c67cb21173da335e","targets":[{"target":"url","constraint":{"operator":"matches","value":"*doaj.org\/api*"}}],"actions":[{"id":"browser_check","value":"off"},{"id":"security_level","value":"essentially_off"},{"id":"cache_level","value":"bypass"}],"priority":2,"status":"active","created_on":"2019-02-26T15:20:15.000000Z","modified_on":"2022-07-15T09:47:38.000000Z"},{"id":"8ed1bac74f92fef5bada7e4091897453","targets":[{"target":"url","constraint":{"operator":"matches","value":"*doaj.org\/query*"}}],"actions":[{"id":"browser_cache_ttl","value":1800},{"id":"cache_level","value":"cache_everything"}],"priority":1,"status":"active","created_on":"2019-02-26T15:21:39.000000Z","modified_on":"2019-02-27T22:27:21.000000Z"}],"success":true,"errors":[],"messages":[]} \ No newline at end of file diff --git a/ansible/cloudflare/cloudflare_rules/rate_limit_rules.json b/ansible/cloudflare/cloudflare_rules/rate_limit_rules.json new file mode 100644 index 0000000..30fbbfa --- /dev/null +++ b/ansible/cloudflare/cloudflare_rules/rate_limit_rules.json @@ -0,0 +1,36 @@ +{ + "result": { + "description": "", + "id": "ffbc4d7fcdae4cd3a36aaaa9ac921483", + "kind": "zone", + "last_updated": "2025-07-09T09:18:41.981517Z", + "name": "default", + "phase": "http_ratelimit", + "rules": [ + { + "action": "block", + "description": "Leaked credential check", + "enabled": true, + "expression": "(cf.waf.credential_check.password_leaked)", + "id": "e254b99c19e746ce9b0d9e33eaff8992", + "last_updated": "2025-07-09T09:18:41.981517Z", + "ratelimit": { + "characteristics": [ + "ip.src", + "cf.colo.id" + ], + "mitigation_timeout": 3600, + "period": 10, + "requests_per_period": 5 + }, + "ref": "e254b99c19e746ce9b0d9e33eaff8992", + "version": "1" + } + ], + "source": "rate_limit", + "version": "1" + }, + "success": true, + "errors": [], + "messages": [] +} diff --git a/ansible/cloudflare/cloudflare_rules/redirect_rules.json b/ansible/cloudflare/cloudflare_rules/redirect_rules.json new file mode 100644 index 0000000..fc06c29 --- /dev/null +++ b/ansible/cloudflare/cloudflare_rules/redirect_rules.json @@ -0,0 +1,10 @@ +{ + "result": null, + "success": false, + "errors": [ + { + "message": "phase \"http_request_redirect\" not allowed at zone level" + } + ], + "messages": [] +} diff --git a/ansible/cloudflare/cloudflare_rules/waf_rules.json b/ansible/cloudflare/cloudflare_rules/waf_rules.json new file mode 100644 index 0000000..c066812 --- /dev/null +++ b/ansible/cloudflare/cloudflare_rules/waf_rules.json @@ -0,0 +1,35 @@ +{ + "result": { + "description": "", + "id": "3d79da9812e44c449dde54903717be8a", + "kind": "zone", + "last_updated": "2025-05-20T10:09:27.585167Z", + "name": "default", + "phase": "http_request_firewall_custom", + "rules": [ + { + "action": "skip", + "action_parameters": { + "phases": [ + "http_request_firewall_managed" + ] + }, + "description": "Allow Blog uploads", + "enabled": true, + "expression": "(http.request.full_uri contains \"blog.doaj.org/wp-admin/\")", + "id": "b47819cd21af43f09d0d9b972755b749", + "last_updated": "2025-05-20T10:09:27.585167Z", + "logging": { + "enabled": true + }, + "ref": "b47819cd21af43f09d0d9b972755b749", + "version": "1" + } + ], + "source": "firewall_custom", + "version": "1" + }, + "success": true, + "errors": [], + "messages": [] +}