@olegkoval/agent-skills 1.5.1 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "olko-agent-skills",
3
3
  "description": "Agent-agnostic skill catalog for Codex, Claude, Cursor, and other skill-aware tools.",
4
- "version": "1.5.0",
4
+ "version": "1.5.1",
5
5
  "author": {
6
6
  "name": "Oleg Koval"
7
7
  },
@@ -23,6 +23,7 @@
23
23
  "./packages/software-development/starter-rules",
24
24
  "./packages/software-development/open-source-publisher",
25
25
  "./packages/marketing/viral-launch",
26
- "./packages/marketing/search-console-indexing-audit"
26
+ "./packages/marketing/search-console-indexing-audit",
27
+ "./packages/software-development/cloudflare-block-countries"
27
28
  ]
28
29
  }
@@ -75,6 +75,11 @@
75
75
  "name": "olko:search-console-indexing-audit",
76
76
  "source": "./packages/marketing/search-console-indexing-audit/adapters/cursor",
77
77
  "description": "Analyze Google Search Console Coverage CSV exports and correlate them with sitemap, robots, canonical, redirect, and noindex signals."
78
+ },
79
+ {
80
+ "name": "olko:cloudflare-block-countries",
81
+ "source": "./packages/software-development/cloudflare-block-countries/adapters/cursor",
82
+ "description": "Block specific countries via Cloudflare WAF Custom Rules using the Cloudflare API. Handles creating new rulesets and updating existing ones across single or multiple zones."
78
83
  }
79
84
  ]
80
85
  }
@@ -306,6 +306,26 @@
306
306
  "cursor",
307
307
  "copilot"
308
308
  ]
309
+ },
310
+ {
311
+ "name": "cloudflare-block-countries",
312
+ "lookupName": "olko:cloudflare-block-countries",
313
+ "category": "software-development",
314
+ "path": "packages/software-development/cloudflare-block-countries",
315
+ "description": "Block specific countries via Cloudflare WAF Custom Rules using the Cloudflare API. Handles creating new rulesets and updating existing ones across single or multiple zones.",
316
+ "tags": [
317
+ "cloudflare",
318
+ "waf",
319
+ "security",
320
+ "geo-blocking",
321
+ "firewall"
322
+ ],
323
+ "adapters": [
324
+ "codex",
325
+ "claude",
326
+ "cursor",
327
+ "copilot"
328
+ ]
309
329
  }
310
330
  ]
311
331
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@olegkoval/agent-skills",
3
- "version": "1.5.1",
3
+ "version": "1.6.0",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -0,0 +1,175 @@
1
+ ---
2
+ name: cloudflare-block-countries
3
+ description: 'Block specific countries via Cloudflare WAF Custom Rules using the Cloudflare API. Use when user wants to geo-block traffic, block countries in Cloudflare, set up WAF country rules, or mentions blocking regions. Handles both creating new rulesets and updating existing ones.'
4
+ license: MIT
5
+ allowed-tools: Bash
6
+ compatibility: Codex, Claude Code, Cursor, and other Agent Skills compatible tools. Requires curl and jq.
7
+ metadata:
8
+ author: Oleg Koval
9
+ tags:
10
+ - cloudflare
11
+ - waf
12
+ - security
13
+ - geo-blocking
14
+ - firewall
15
+ ---
16
+
17
+ # Cloudflare Country Block via WAF Custom Rules
18
+
19
+ Block traffic from specific countries using Cloudflare WAF Custom Rules and the Cloudflare API.
20
+
21
+ ## Prerequisites
22
+
23
+ - Cloudflare API Token with `Zone:Rulesets:Edit` permission
24
+ - Zone ID for the target domain
25
+ - `curl` and `jq` installed
26
+
27
+ ## Workflow
28
+
29
+ ### 1. Get Zone IDs
30
+
31
+ ```bash
32
+ curl -s -X GET \
33
+ "https://api.cloudflare.com/client/v4/zones?account.id=YOUR_ACCOUNT_ID&status=active" \
34
+ -H "Authorization: Bearer YOUR_API_TOKEN" \
35
+ -H "Content-Type: application/json" | jq '.result[] | {name: .name, id: .id}'
36
+ ```
37
+
38
+ ### 2. Check Existing Custom Firewall Rulesets
39
+
40
+ ```bash
41
+ curl -s -X GET \
42
+ "https://api.cloudflare.com/client/v4/zones/YOUR_ZONE_ID/rulesets" \
43
+ -H "Authorization: Bearer YOUR_API_TOKEN" \
44
+ -H "Content-Type: application/json" | \
45
+ jq '.result[] | select(.phase == "http_request_firewall_custom")'
46
+ ```
47
+
48
+ If the output is empty — no existing ruleset. Go to **3a**. If a ruleset exists, note its `id` and go to **3b**.
49
+
50
+ ### 3a. Create NEW Ruleset with Block Rule
51
+
52
+ ```bash
53
+ curl -s -X POST \
54
+ "https://api.cloudflare.com/client/v4/zones/YOUR_ZONE_ID/rulesets" \
55
+ -H "Authorization: Bearer YOUR_API_TOKEN" \
56
+ -H "Content-Type: application/json" \
57
+ --data '{
58
+ "name": "default",
59
+ "description": "WAF Custom Rules",
60
+ "kind": "zone",
61
+ "phase": "http_request_firewall_custom",
62
+ "rules": [
63
+ {
64
+ "action": "block",
65
+ "description": "Block traffic from sanctioned countries",
66
+ "enabled": true,
67
+ "expression": "(ip.src.country in {\"RU\" \"BY\" \"IR\" \"KP\"})"
68
+ }
69
+ ]
70
+ }' | jq '.success'
71
+ ```
72
+
73
+ ### 3b. Update EXISTING Ruleset
74
+
75
+ ```bash
76
+ curl -s -X PUT \
77
+ "https://api.cloudflare.com/client/v4/zones/YOUR_ZONE_ID/rulesets/RULESET_ID" \
78
+ -H "Authorization: Bearer YOUR_API_TOKEN" \
79
+ -H "Content-Type: application/json" \
80
+ --data '{
81
+ "description": "WAF Custom Rules",
82
+ "rules": [
83
+ {
84
+ "action": "block",
85
+ "description": "Block traffic from sanctioned countries",
86
+ "enabled": true,
87
+ "expression": "(ip.src.country in {\"RU\" \"BY\" \"IR\" \"KP\"})"
88
+ }
89
+ ]
90
+ }' | jq '.success'
91
+ ```
92
+
93
+ ### 4. Verify the Rule is Active
94
+
95
+ ```bash
96
+ curl -s -X GET \
97
+ "https://api.cloudflare.com/client/v4/zones/YOUR_ZONE_ID/rulesets/RULESET_ID" \
98
+ -H "Authorization: Bearer YOUR_API_TOKEN" \
99
+ -H "Content-Type: application/json" | jq '.result.rules'
100
+ ```
101
+
102
+ ## Country Codes Reference
103
+
104
+ | Code | Country |
105
+ |------|---------|
106
+ | `RU` | Russia |
107
+ | `BY` | Belarus |
108
+ | `IR` | Iran |
109
+ | `KP` | North Korea |
110
+ | `CN` | China |
111
+ | `CU` | Cuba |
112
+ | `SY` | Syria |
113
+ | `VE` | Venezuela |
114
+
115
+ ## WAF Expression Examples
116
+
117
+ ```
118
+ # Block multiple countries
119
+ (ip.src.country in {"RU" "BY" "IR" "KP"})
120
+
121
+ # Block country + specific ASN
122
+ (ip.src.country eq "RU") or (ip.geoip.asnum eq 12345)
123
+
124
+ # Block all except allowlisted IPs
125
+ (ip.src.country in {"RU" "BY"}) and not (ip.src in {1.2.3.4/32})
126
+ ```
127
+
128
+ ## Multi-Zone Script
129
+
130
+ To apply the same block rule across multiple zones:
131
+
132
+ ```bash
133
+ #!/usr/bin/env bash
134
+ API_TOKEN="YOUR_API_TOKEN"
135
+ ZONES=("ZONE_ID_1" "ZONE_ID_2" "ZONE_ID_3")
136
+ EXPRESSION='(ip.src.country in {"RU" "BY" "IR" "KP"})'
137
+
138
+ for ZONE_ID in "${ZONES[@]}"; do
139
+ EXISTING=$(curl -s -X GET \
140
+ "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/rulesets" \
141
+ -H "Authorization: Bearer $API_TOKEN" \
142
+ -H "Content-Type: application/json" | \
143
+ jq -r '.result[] | select(.phase == "http_request_firewall_custom") | .id')
144
+
145
+ if [ -z "$EXISTING" ]; then
146
+ METHOD="POST"
147
+ URL="https://api.cloudflare.com/client/v4/zones/$ZONE_ID/rulesets"
148
+ DATA=$(jq -n --arg expr "$EXPRESSION" '{
149
+ name: "default", description: "WAF Custom Rules",
150
+ kind: "zone", phase: "http_request_firewall_custom",
151
+ rules: [{action: "block", description: "Block sanctioned countries", enabled: true, expression: $expr}]
152
+ }')
153
+ else
154
+ METHOD="PUT"
155
+ URL="https://api.cloudflare.com/client/v4/zones/$ZONE_ID/rulesets/$EXISTING"
156
+ DATA=$(jq -n --arg expr "$EXPRESSION" '{
157
+ description: "WAF Custom Rules",
158
+ rules: [{action: "block", description: "Block sanctioned countries", enabled: true, expression: $expr}]
159
+ }')
160
+ fi
161
+
162
+ RESULT=$(curl -s -X "$METHOD" "$URL" \
163
+ -H "Authorization: Bearer $API_TOKEN" \
164
+ -H "Content-Type: application/json" \
165
+ --data "$DATA" | jq '.success')
166
+
167
+ echo "Zone $ZONE_ID: $RESULT"
168
+ done
169
+ ```
170
+
171
+ ## Notes
172
+
173
+ - `PUT` on an existing ruleset **replaces all rules** in it. Include all rules in the payload if the ruleset already has other rules.
174
+ - WAF Custom Rules require a Cloudflare plan that supports rulesets (Free includes basic WAF; Pro/Business/Enterprise for full custom rules).
175
+ - Changes are applied globally within seconds; no cache purge needed.
@@ -0,0 +1,5 @@
1
+ {
2
+ "name": "olko-cloudflare-block-countries",
3
+ "description": "Block specific countries via Cloudflare WAF Custom Rules using the Cloudflare API. Handles creating new rulesets and updating existing ones across single or multiple zones.",
4
+ "skills": "./skills"
5
+ }
@@ -0,0 +1,177 @@
1
+ <!-- Generated by scripts/build-adapters.sh. Do not edit directly. -->
2
+
3
+ ---
4
+ name: cloudflare-block-countries
5
+ description: 'Block specific countries via Cloudflare WAF Custom Rules using the Cloudflare API. Use when user wants to geo-block traffic, block countries in Cloudflare, set up WAF country rules, or mentions blocking regions. Handles both creating new rulesets and updating existing ones.'
6
+ license: MIT
7
+ allowed-tools: Bash
8
+ compatibility: Codex, Claude Code, Cursor, and other Agent Skills compatible tools. Requires curl and jq.
9
+ metadata:
10
+ author: Oleg Koval
11
+ tags:
12
+ - cloudflare
13
+ - waf
14
+ - security
15
+ - geo-blocking
16
+ - firewall
17
+ ---
18
+
19
+ # Cloudflare Country Block via WAF Custom Rules
20
+
21
+ Block traffic from specific countries using Cloudflare WAF Custom Rules and the Cloudflare API.
22
+
23
+ ## Prerequisites
24
+
25
+ - Cloudflare API Token with `Zone:Rulesets:Edit` permission
26
+ - Zone ID for the target domain
27
+ - `curl` and `jq` installed
28
+
29
+ ## Workflow
30
+
31
+ ### 1. Get Zone IDs
32
+
33
+ ```bash
34
+ curl -s -X GET \
35
+ "https://api.cloudflare.com/client/v4/zones?account.id=YOUR_ACCOUNT_ID&status=active" \
36
+ -H "Authorization: Bearer YOUR_API_TOKEN" \
37
+ -H "Content-Type: application/json" | jq '.result[] | {name: .name, id: .id}'
38
+ ```
39
+
40
+ ### 2. Check Existing Custom Firewall Rulesets
41
+
42
+ ```bash
43
+ curl -s -X GET \
44
+ "https://api.cloudflare.com/client/v4/zones/YOUR_ZONE_ID/rulesets" \
45
+ -H "Authorization: Bearer YOUR_API_TOKEN" \
46
+ -H "Content-Type: application/json" | \
47
+ jq '.result[] | select(.phase == "http_request_firewall_custom")'
48
+ ```
49
+
50
+ If the output is empty — no existing ruleset. Go to **3a**. If a ruleset exists, note its `id` and go to **3b**.
51
+
52
+ ### 3a. Create NEW Ruleset with Block Rule
53
+
54
+ ```bash
55
+ curl -s -X POST \
56
+ "https://api.cloudflare.com/client/v4/zones/YOUR_ZONE_ID/rulesets" \
57
+ -H "Authorization: Bearer YOUR_API_TOKEN" \
58
+ -H "Content-Type: application/json" \
59
+ --data '{
60
+ "name": "default",
61
+ "description": "WAF Custom Rules",
62
+ "kind": "zone",
63
+ "phase": "http_request_firewall_custom",
64
+ "rules": [
65
+ {
66
+ "action": "block",
67
+ "description": "Block traffic from sanctioned countries",
68
+ "enabled": true,
69
+ "expression": "(ip.src.country in {\"RU\" \"BY\" \"IR\" \"KP\"})"
70
+ }
71
+ ]
72
+ }' | jq '.success'
73
+ ```
74
+
75
+ ### 3b. Update EXISTING Ruleset
76
+
77
+ ```bash
78
+ curl -s -X PUT \
79
+ "https://api.cloudflare.com/client/v4/zones/YOUR_ZONE_ID/rulesets/RULESET_ID" \
80
+ -H "Authorization: Bearer YOUR_API_TOKEN" \
81
+ -H "Content-Type: application/json" \
82
+ --data '{
83
+ "description": "WAF Custom Rules",
84
+ "rules": [
85
+ {
86
+ "action": "block",
87
+ "description": "Block traffic from sanctioned countries",
88
+ "enabled": true,
89
+ "expression": "(ip.src.country in {\"RU\" \"BY\" \"IR\" \"KP\"})"
90
+ }
91
+ ]
92
+ }' | jq '.success'
93
+ ```
94
+
95
+ ### 4. Verify the Rule is Active
96
+
97
+ ```bash
98
+ curl -s -X GET \
99
+ "https://api.cloudflare.com/client/v4/zones/YOUR_ZONE_ID/rulesets/RULESET_ID" \
100
+ -H "Authorization: Bearer YOUR_API_TOKEN" \
101
+ -H "Content-Type: application/json" | jq '.result.rules'
102
+ ```
103
+
104
+ ## Country Codes Reference
105
+
106
+ | Code | Country |
107
+ |------|---------|
108
+ | `RU` | Russia |
109
+ | `BY` | Belarus |
110
+ | `IR` | Iran |
111
+ | `KP` | North Korea |
112
+ | `CN` | China |
113
+ | `CU` | Cuba |
114
+ | `SY` | Syria |
115
+ | `VE` | Venezuela |
116
+
117
+ ## WAF Expression Examples
118
+
119
+ ```
120
+ # Block multiple countries
121
+ (ip.src.country in {"RU" "BY" "IR" "KP"})
122
+
123
+ # Block country + specific ASN
124
+ (ip.src.country eq "RU") or (ip.geoip.asnum eq 12345)
125
+
126
+ # Block all except allowlisted IPs
127
+ (ip.src.country in {"RU" "BY"}) and not (ip.src in {1.2.3.4/32})
128
+ ```
129
+
130
+ ## Multi-Zone Script
131
+
132
+ To apply the same block rule across multiple zones:
133
+
134
+ ```bash
135
+ #!/usr/bin/env bash
136
+ API_TOKEN="YOUR_API_TOKEN"
137
+ ZONES=("ZONE_ID_1" "ZONE_ID_2" "ZONE_ID_3")
138
+ EXPRESSION='(ip.src.country in {"RU" "BY" "IR" "KP"})'
139
+
140
+ for ZONE_ID in "${ZONES[@]}"; do
141
+ EXISTING=$(curl -s -X GET \
142
+ "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/rulesets" \
143
+ -H "Authorization: Bearer $API_TOKEN" \
144
+ -H "Content-Type: application/json" | \
145
+ jq -r '.result[] | select(.phase == "http_request_firewall_custom") | .id')
146
+
147
+ if [ -z "$EXISTING" ]; then
148
+ METHOD="POST"
149
+ URL="https://api.cloudflare.com/client/v4/zones/$ZONE_ID/rulesets"
150
+ DATA=$(jq -n --arg expr "$EXPRESSION" '{
151
+ name: "default", description: "WAF Custom Rules",
152
+ kind: "zone", phase: "http_request_firewall_custom",
153
+ rules: [{action: "block", description: "Block sanctioned countries", enabled: true, expression: $expr}]
154
+ }')
155
+ else
156
+ METHOD="PUT"
157
+ URL="https://api.cloudflare.com/client/v4/zones/$ZONE_ID/rulesets/$EXISTING"
158
+ DATA=$(jq -n --arg expr "$EXPRESSION" '{
159
+ description: "WAF Custom Rules",
160
+ rules: [{action: "block", description: "Block sanctioned countries", enabled: true, expression: $expr}]
161
+ }')
162
+ fi
163
+
164
+ RESULT=$(curl -s -X "$METHOD" "$URL" \
165
+ -H "Authorization: Bearer $API_TOKEN" \
166
+ -H "Content-Type: application/json" \
167
+ --data "$DATA" | jq '.success')
168
+
169
+ echo "Zone $ZONE_ID: $RESULT"
170
+ done
171
+ ```
172
+
173
+ ## Notes
174
+
175
+ - `PUT` on an existing ruleset **replaces all rules** in it. Include all rules in the payload if the ruleset already has other rules.
176
+ - WAF Custom Rules require a Cloudflare plan that supports rulesets (Free includes basic WAF; Pro/Business/Enterprise for full custom rules).
177
+ - Changes are applied globally within seconds; no cache purge needed.
@@ -0,0 +1,3 @@
1
+ # Codex adapter
2
+
3
+ Use the canonical skill directly from `packages/software-development/cloudflare-block-countries/SKILL.md`.
@@ -0,0 +1,6 @@
1
+ {
2
+ "name": "olko:cloudflare-block-countries",
3
+ "version": "0.1.0",
4
+ "description": "Block specific countries via Cloudflare WAF Custom Rules using the Cloudflare API. Handles creating new rulesets and updating existing ones across single or multiple zones.",
5
+ "skills": "skills/"
6
+ }
@@ -0,0 +1,177 @@
1
+ <!-- Generated by scripts/build-adapters.sh. Do not edit directly. -->
2
+
3
+ ---
4
+ name: cloudflare-block-countries
5
+ description: 'Block specific countries via Cloudflare WAF Custom Rules using the Cloudflare API. Use when user wants to geo-block traffic, block countries in Cloudflare, set up WAF country rules, or mentions blocking regions. Handles both creating new rulesets and updating existing ones.'
6
+ license: MIT
7
+ allowed-tools: Bash
8
+ compatibility: Codex, Claude Code, Cursor, and other Agent Skills compatible tools. Requires curl and jq.
9
+ metadata:
10
+ author: Oleg Koval
11
+ tags:
12
+ - cloudflare
13
+ - waf
14
+ - security
15
+ - geo-blocking
16
+ - firewall
17
+ ---
18
+
19
+ # Cloudflare Country Block via WAF Custom Rules
20
+
21
+ Block traffic from specific countries using Cloudflare WAF Custom Rules and the Cloudflare API.
22
+
23
+ ## Prerequisites
24
+
25
+ - Cloudflare API Token with `Zone:Rulesets:Edit` permission
26
+ - Zone ID for the target domain
27
+ - `curl` and `jq` installed
28
+
29
+ ## Workflow
30
+
31
+ ### 1. Get Zone IDs
32
+
33
+ ```bash
34
+ curl -s -X GET \
35
+ "https://api.cloudflare.com/client/v4/zones?account.id=YOUR_ACCOUNT_ID&status=active" \
36
+ -H "Authorization: Bearer YOUR_API_TOKEN" \
37
+ -H "Content-Type: application/json" | jq '.result[] | {name: .name, id: .id}'
38
+ ```
39
+
40
+ ### 2. Check Existing Custom Firewall Rulesets
41
+
42
+ ```bash
43
+ curl -s -X GET \
44
+ "https://api.cloudflare.com/client/v4/zones/YOUR_ZONE_ID/rulesets" \
45
+ -H "Authorization: Bearer YOUR_API_TOKEN" \
46
+ -H "Content-Type: application/json" | \
47
+ jq '.result[] | select(.phase == "http_request_firewall_custom")'
48
+ ```
49
+
50
+ If the output is empty — no existing ruleset. Go to **3a**. If a ruleset exists, note its `id` and go to **3b**.
51
+
52
+ ### 3a. Create NEW Ruleset with Block Rule
53
+
54
+ ```bash
55
+ curl -s -X POST \
56
+ "https://api.cloudflare.com/client/v4/zones/YOUR_ZONE_ID/rulesets" \
57
+ -H "Authorization: Bearer YOUR_API_TOKEN" \
58
+ -H "Content-Type: application/json" \
59
+ --data '{
60
+ "name": "default",
61
+ "description": "WAF Custom Rules",
62
+ "kind": "zone",
63
+ "phase": "http_request_firewall_custom",
64
+ "rules": [
65
+ {
66
+ "action": "block",
67
+ "description": "Block traffic from sanctioned countries",
68
+ "enabled": true,
69
+ "expression": "(ip.src.country in {\"RU\" \"BY\" \"IR\" \"KP\"})"
70
+ }
71
+ ]
72
+ }' | jq '.success'
73
+ ```
74
+
75
+ ### 3b. Update EXISTING Ruleset
76
+
77
+ ```bash
78
+ curl -s -X PUT \
79
+ "https://api.cloudflare.com/client/v4/zones/YOUR_ZONE_ID/rulesets/RULESET_ID" \
80
+ -H "Authorization: Bearer YOUR_API_TOKEN" \
81
+ -H "Content-Type: application/json" \
82
+ --data '{
83
+ "description": "WAF Custom Rules",
84
+ "rules": [
85
+ {
86
+ "action": "block",
87
+ "description": "Block traffic from sanctioned countries",
88
+ "enabled": true,
89
+ "expression": "(ip.src.country in {\"RU\" \"BY\" \"IR\" \"KP\"})"
90
+ }
91
+ ]
92
+ }' | jq '.success'
93
+ ```
94
+
95
+ ### 4. Verify the Rule is Active
96
+
97
+ ```bash
98
+ curl -s -X GET \
99
+ "https://api.cloudflare.com/client/v4/zones/YOUR_ZONE_ID/rulesets/RULESET_ID" \
100
+ -H "Authorization: Bearer YOUR_API_TOKEN" \
101
+ -H "Content-Type: application/json" | jq '.result.rules'
102
+ ```
103
+
104
+ ## Country Codes Reference
105
+
106
+ | Code | Country |
107
+ |------|---------|
108
+ | `RU` | Russia |
109
+ | `BY` | Belarus |
110
+ | `IR` | Iran |
111
+ | `KP` | North Korea |
112
+ | `CN` | China |
113
+ | `CU` | Cuba |
114
+ | `SY` | Syria |
115
+ | `VE` | Venezuela |
116
+
117
+ ## WAF Expression Examples
118
+
119
+ ```
120
+ # Block multiple countries
121
+ (ip.src.country in {"RU" "BY" "IR" "KP"})
122
+
123
+ # Block country + specific ASN
124
+ (ip.src.country eq "RU") or (ip.geoip.asnum eq 12345)
125
+
126
+ # Block all except allowlisted IPs
127
+ (ip.src.country in {"RU" "BY"}) and not (ip.src in {1.2.3.4/32})
128
+ ```
129
+
130
+ ## Multi-Zone Script
131
+
132
+ To apply the same block rule across multiple zones:
133
+
134
+ ```bash
135
+ #!/usr/bin/env bash
136
+ API_TOKEN="YOUR_API_TOKEN"
137
+ ZONES=("ZONE_ID_1" "ZONE_ID_2" "ZONE_ID_3")
138
+ EXPRESSION='(ip.src.country in {"RU" "BY" "IR" "KP"})'
139
+
140
+ for ZONE_ID in "${ZONES[@]}"; do
141
+ EXISTING=$(curl -s -X GET \
142
+ "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/rulesets" \
143
+ -H "Authorization: Bearer $API_TOKEN" \
144
+ -H "Content-Type: application/json" | \
145
+ jq -r '.result[] | select(.phase == "http_request_firewall_custom") | .id')
146
+
147
+ if [ -z "$EXISTING" ]; then
148
+ METHOD="POST"
149
+ URL="https://api.cloudflare.com/client/v4/zones/$ZONE_ID/rulesets"
150
+ DATA=$(jq -n --arg expr "$EXPRESSION" '{
151
+ name: "default", description: "WAF Custom Rules",
152
+ kind: "zone", phase: "http_request_firewall_custom",
153
+ rules: [{action: "block", description: "Block sanctioned countries", enabled: true, expression: $expr}]
154
+ }')
155
+ else
156
+ METHOD="PUT"
157
+ URL="https://api.cloudflare.com/client/v4/zones/$ZONE_ID/rulesets/$EXISTING"
158
+ DATA=$(jq -n --arg expr "$EXPRESSION" '{
159
+ description: "WAF Custom Rules",
160
+ rules: [{action: "block", description: "Block sanctioned countries", enabled: true, expression: $expr}]
161
+ }')
162
+ fi
163
+
164
+ RESULT=$(curl -s -X "$METHOD" "$URL" \
165
+ -H "Authorization: Bearer $API_TOKEN" \
166
+ -H "Content-Type: application/json" \
167
+ --data "$DATA" | jq '.success')
168
+
169
+ echo "Zone $ZONE_ID: $RESULT"
170
+ done
171
+ ```
172
+
173
+ ## Notes
174
+
175
+ - `PUT` on an existing ruleset **replaces all rules** in it. Include all rules in the payload if the ruleset already has other rules.
176
+ - WAF Custom Rules require a Cloudflare plan that supports rulesets (Free includes basic WAF; Pro/Business/Enterprise for full custom rules).
177
+ - Changes are applied globally within seconds; no cache purge needed.