@gfargo/doorman 3.1.0 → 3.2.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.
- package/package.json +1 -1
- package/skills/doorman/.source-version +1 -0
- package/skills/doorman/SKILL.md +74 -77
- package/skills/doorman/references/cicd.md +240 -0
- package/skills/doorman/references/cloudflare.md +188 -0
- package/skills/doorman/references/rules.md +352 -0
- package/skills/doorman/references/templates.md +191 -0
- package/skills/doorman/sync/README.md +43 -0
- package/skills/doorman/sync/plugin.json +23 -0
- package/skills/doorman/sync/sync-doorman.yml +80 -0
- package/skills/doorman/rules-reference.md +0 -478
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
# Rule Authoring Reference
|
|
2
|
+
|
|
3
|
+
Complete reference for creating and configuring Doorman firewall rules.
|
|
4
|
+
|
|
5
|
+
## Rule Structure
|
|
6
|
+
|
|
7
|
+
Every rule in the `rules` array has this shape:
|
|
8
|
+
|
|
9
|
+
```json
|
|
10
|
+
{
|
|
11
|
+
"id": "rule_descriptive_name",
|
|
12
|
+
"name": "Human-Readable Name",
|
|
13
|
+
"description": "What this rule does and why",
|
|
14
|
+
"active": true,
|
|
15
|
+
"conditionGroup": [
|
|
16
|
+
{ "conditions": [/* AND — all must match */] },
|
|
17
|
+
{ "conditions": [/* OR — this group is an alternative */] }
|
|
18
|
+
],
|
|
19
|
+
"action": { "mitigate": { "action": "deny" } }
|
|
20
|
+
}
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
### Fields
|
|
24
|
+
|
|
25
|
+
| Field | Type | Required | Description |
|
|
26
|
+
|-------|------|----------|-------------|
|
|
27
|
+
| `id` | string | No | Unique identifier. Convention: `rule_` prefix, snake_case. Auto-generated if omitted. |
|
|
28
|
+
| `name` | string | Yes | Display name shown in tables and logs. |
|
|
29
|
+
| `description` | string | No | Explains the rule's purpose. Improves health score. |
|
|
30
|
+
| `active` | boolean | Yes | `true` to enforce, `false` to disable without deleting. |
|
|
31
|
+
| `conditionGroup` | array | Yes | Array of condition groups (see below). |
|
|
32
|
+
| `action` | object | Yes | What to do when conditions match (see below). |
|
|
33
|
+
|
|
34
|
+
## Condition Groups
|
|
35
|
+
|
|
36
|
+
```json
|
|
37
|
+
"conditionGroup": [
|
|
38
|
+
{ "conditions": [/* AND — all must match */] },
|
|
39
|
+
{ "conditions": [/* OR — this group is an alternative */] }
|
|
40
|
+
]
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
**Logic**:
|
|
44
|
+
- Conditions **within** a group: AND (all must match)
|
|
45
|
+
- **Between** groups: OR (any group matching triggers the rule)
|
|
46
|
+
|
|
47
|
+
## Conditions
|
|
48
|
+
|
|
49
|
+
Each condition has:
|
|
50
|
+
|
|
51
|
+
```json
|
|
52
|
+
{ "type": "field_type", "op": "operator", "value": "match_value", "key": "header_name", "neg": false }
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
| Field | Type | Required | Description |
|
|
56
|
+
|-------|------|----------|-------------|
|
|
57
|
+
| `type` | string | Yes | What to match against (see types below). |
|
|
58
|
+
| `op` | string | Yes | How to compare (see operators below). |
|
|
59
|
+
| `value` | string/number/array | Yes | Value to match. Arrays for `inc` operator. |
|
|
60
|
+
| `key` | string | No | Required for `header`, `query`, `cookie` types. |
|
|
61
|
+
| `neg` | boolean | No | `true` to negate the condition (NOT logic). Default: `false`. |
|
|
62
|
+
|
|
63
|
+
### Condition Types
|
|
64
|
+
|
|
65
|
+
| Type | Description | Example Value |
|
|
66
|
+
|------|-------------|---------------|
|
|
67
|
+
| `path` | URL path | `"/api/users"` |
|
|
68
|
+
| `method` | HTTP method | `"POST"` |
|
|
69
|
+
| `host` | Hostname | `"example.com"` |
|
|
70
|
+
| `user_agent` | User-Agent header | `"Googlebot"` |
|
|
71
|
+
| `ip_address` | Client IP | `"192.168.1.1"` |
|
|
72
|
+
| `header` | HTTP header (requires `key`) | `"application/json"` |
|
|
73
|
+
| `query` | Query parameter (requires `key`) | `"true"` |
|
|
74
|
+
| `cookie` | Cookie value (requires `key`) | `"session_abc"` |
|
|
75
|
+
| `geo_country` | Country code (ISO 3166-1) | `"US"` or `["US","CA"]` |
|
|
76
|
+
| `geo_city` | City name | `"New York"` |
|
|
77
|
+
| `geo_continent` | Continent code | `"NA"` |
|
|
78
|
+
| `geo_country_region` | Region/state code | `"CA"` |
|
|
79
|
+
| `geo_as_number` | ASN number | `13335` |
|
|
80
|
+
| `scheme` | URL scheme | `"https"` |
|
|
81
|
+
| `protocol` | HTTP protocol version | `"HTTP/2"` |
|
|
82
|
+
|
|
83
|
+
**Vercel-only types** (not available on Cloudflare):
|
|
84
|
+
- `environment` — deployment environment (`"production"`, `"preview"`)
|
|
85
|
+
- `ja3_digest` — TLS fingerprint
|
|
86
|
+
- `ja4_digest` — TLS fingerprint v4
|
|
87
|
+
- `region` — Vercel edge region
|
|
88
|
+
- `rate_limit_api_id` — rate limit API identifier
|
|
89
|
+
|
|
90
|
+
### Operators
|
|
91
|
+
|
|
92
|
+
| Operator | Name | Description | Value Type |
|
|
93
|
+
|----------|------|-------------|------------|
|
|
94
|
+
| `eq` | Equals | Exact match | string/number |
|
|
95
|
+
| `pre` | Prefix | Starts with | string |
|
|
96
|
+
| `suf` | Suffix | Ends with | string |
|
|
97
|
+
| `sub` | Substring | Contains | string |
|
|
98
|
+
| `inc` | Includes | Is any of (array match) | string[] |
|
|
99
|
+
| `re` | Regex | Regular expression match | string (regex pattern) |
|
|
100
|
+
| `ex` | Exists | Field/header exists | `true` |
|
|
101
|
+
| `nex` | Not Exists | Field/header does not exist | `true` |
|
|
102
|
+
|
|
103
|
+
### Using `key` for Header/Query/Cookie
|
|
104
|
+
|
|
105
|
+
```json
|
|
106
|
+
{ "type": "header", "op": "eq", "value": "application/json", "key": "Content-Type" }
|
|
107
|
+
{ "type": "query", "op": "eq", "value": "true", "key": "debug" }
|
|
108
|
+
{ "type": "cookie", "op": "ex", "value": true, "key": "session_id" }
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### Using `neg` for Negation
|
|
112
|
+
|
|
113
|
+
```json
|
|
114
|
+
{ "type": "geo_country", "op": "inc", "value": ["US", "CA", "GB"], "neg": true }
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
This matches requests NOT from US, CA, or GB.
|
|
118
|
+
|
|
119
|
+
## Actions
|
|
120
|
+
|
|
121
|
+
### Deny (Block)
|
|
122
|
+
|
|
123
|
+
```json
|
|
124
|
+
{ "action": { "mitigate": { "action": "deny" } } }
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### Deny with Duration
|
|
128
|
+
|
|
129
|
+
```json
|
|
130
|
+
{ "action": { "mitigate": { "action": "deny", "actionDuration": "1h" } } }
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
Duration formats: `"30s"`, `"5m"`, `"1h"`, `"1d"`, `"permanent"`
|
|
134
|
+
|
|
135
|
+
### Challenge (CAPTCHA)
|
|
136
|
+
|
|
137
|
+
```json
|
|
138
|
+
{ "action": { "mitigate": { "action": "challenge" } } }
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
### Rate Limit
|
|
142
|
+
|
|
143
|
+
```json
|
|
144
|
+
{
|
|
145
|
+
"action": {
|
|
146
|
+
"mitigate": {
|
|
147
|
+
"action": "rate_limit",
|
|
148
|
+
"rateLimit": {
|
|
149
|
+
"requests": 100,
|
|
150
|
+
"window": "60s"
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
| Field | Type | Required | Description |
|
|
158
|
+
|-------|------|----------|-------------|
|
|
159
|
+
| `requests` | number | Yes | Max requests allowed in window. |
|
|
160
|
+
| `window` | string | Yes | Time window: `"10s"`, `"1m"`, `"5m"`, `"1h"` |
|
|
161
|
+
| `characteristics` | string[] | No | What to rate limit by. Default: `["ip.src"]`. Options: `"ip.src"`, `"http.request.uri.path"`, `"http.request.headers[\"user-agent\"]"` |
|
|
162
|
+
| `mitigationTimeout` | number | No | How long (seconds) to block after limit exceeded. Default: 3600. |
|
|
163
|
+
| `countingExpression` | string | No | Cloudflare-specific: expression for what counts toward the limit. |
|
|
164
|
+
|
|
165
|
+
### Redirect
|
|
166
|
+
|
|
167
|
+
```json
|
|
168
|
+
{
|
|
169
|
+
"action": {
|
|
170
|
+
"mitigate": {
|
|
171
|
+
"action": "redirect",
|
|
172
|
+
"redirect": { "location": "https://example.com/blocked", "permanent": false }
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
| Field | Type | Description |
|
|
179
|
+
|-------|------|-------------|
|
|
180
|
+
| `location` | string | Redirect URL (absolute or relative path). |
|
|
181
|
+
| `permanent` | boolean | `true` for 301, `false` for 302. |
|
|
182
|
+
|
|
183
|
+
### Log Only
|
|
184
|
+
|
|
185
|
+
```json
|
|
186
|
+
{ "action": { "mitigate": { "action": "log" } } }
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
### Bypass
|
|
190
|
+
|
|
191
|
+
```json
|
|
192
|
+
{ "action": { "mitigate": { "action": "bypass" } } }
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
## IP Blocking Rules
|
|
196
|
+
|
|
197
|
+
IP rules go in the `ips` array, separate from `rules`:
|
|
198
|
+
|
|
199
|
+
```json
|
|
200
|
+
{
|
|
201
|
+
"ips": [
|
|
202
|
+
{
|
|
203
|
+
"id": "ip_malicious_actor",
|
|
204
|
+
"ip": "192.168.1.100/32",
|
|
205
|
+
"hostname": "attacker.example.com",
|
|
206
|
+
"action": "deny",
|
|
207
|
+
"notes": "Blocked 2024-01-15: repeated brute force attempts"
|
|
208
|
+
}
|
|
209
|
+
]
|
|
210
|
+
}
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
| Field | Type | Required | Description |
|
|
214
|
+
|-------|------|----------|-------------|
|
|
215
|
+
| `id` | string | No | Unique identifier. |
|
|
216
|
+
| `ip` | string | Yes | IP address or CIDR range. Use `/32` for single IPs. |
|
|
217
|
+
| `hostname` | string | No | Associated hostname (documentation only). |
|
|
218
|
+
| `action` | string | Yes | `"deny"` (only supported value currently). |
|
|
219
|
+
| `notes` | string | No | Why this IP was blocked. |
|
|
220
|
+
|
|
221
|
+
### CIDR Notation
|
|
222
|
+
|
|
223
|
+
```
|
|
224
|
+
192.168.1.1/32 -> single IP
|
|
225
|
+
192.168.1.0/24 -> 256 IPs (192.168.1.0 - 192.168.1.255)
|
|
226
|
+
10.0.0.0/8 -> class A range
|
|
227
|
+
0.0.0.0/0 -> all IPv4 (use with caution!)
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
## Common Patterns
|
|
231
|
+
|
|
232
|
+
### Block multiple countries
|
|
233
|
+
|
|
234
|
+
```json
|
|
235
|
+
{
|
|
236
|
+
"name": "OFAC Sanctions Compliance",
|
|
237
|
+
"active": true,
|
|
238
|
+
"conditionGroup": [
|
|
239
|
+
{ "conditions": [{ "type": "geo_country", "op": "inc", "value": ["CU", "IR", "KP", "SY", "RU"] }] }
|
|
240
|
+
],
|
|
241
|
+
"action": { "mitigate": { "action": "deny" } }
|
|
242
|
+
}
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
### Allow only specific countries (block everyone else)
|
|
246
|
+
|
|
247
|
+
```json
|
|
248
|
+
{
|
|
249
|
+
"name": "Allow Only US/CA/GB",
|
|
250
|
+
"active": true,
|
|
251
|
+
"conditionGroup": [
|
|
252
|
+
{ "conditions": [{ "type": "geo_country", "op": "inc", "value": ["US", "CA", "GB"], "neg": true }] }
|
|
253
|
+
],
|
|
254
|
+
"action": { "mitigate": { "action": "deny" } }
|
|
255
|
+
}
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
### Rate limit API with path + method
|
|
259
|
+
|
|
260
|
+
```json
|
|
261
|
+
{
|
|
262
|
+
"name": "Rate Limit POST to API",
|
|
263
|
+
"active": true,
|
|
264
|
+
"conditionGroup": [
|
|
265
|
+
{ "conditions": [
|
|
266
|
+
{ "type": "path", "op": "pre", "value": "/api/" },
|
|
267
|
+
{ "type": "method", "op": "eq", "value": "POST" }
|
|
268
|
+
]}
|
|
269
|
+
],
|
|
270
|
+
"action": {
|
|
271
|
+
"mitigate": {
|
|
272
|
+
"action": "rate_limit",
|
|
273
|
+
"rateLimit": { "requests": 50, "window": "1m" },
|
|
274
|
+
"actionDuration": "5m"
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
### Block by header (missing auth)
|
|
281
|
+
|
|
282
|
+
```json
|
|
283
|
+
{
|
|
284
|
+
"name": "Block Missing API Key",
|
|
285
|
+
"active": true,
|
|
286
|
+
"conditionGroup": [
|
|
287
|
+
{ "conditions": [
|
|
288
|
+
{ "type": "path", "op": "pre", "value": "/api/" },
|
|
289
|
+
{ "type": "header", "op": "nex", "value": true, "key": "X-API-Key" }
|
|
290
|
+
]}
|
|
291
|
+
],
|
|
292
|
+
"action": { "mitigate": { "action": "deny" } }
|
|
293
|
+
}
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
### Challenge suspicious user agents
|
|
297
|
+
|
|
298
|
+
```json
|
|
299
|
+
{
|
|
300
|
+
"name": "Challenge Suspicious Bots",
|
|
301
|
+
"active": true,
|
|
302
|
+
"conditionGroup": [
|
|
303
|
+
{ "conditions": [{ "type": "user_agent", "op": "sub", "value": "curl" }] },
|
|
304
|
+
{ "conditions": [{ "type": "user_agent", "op": "sub", "value": "wget" }] },
|
|
305
|
+
{ "conditions": [{ "type": "user_agent", "op": "sub", "value": "python-requests" }] }
|
|
306
|
+
],
|
|
307
|
+
"action": { "mitigate": { "action": "challenge" } }
|
|
308
|
+
}
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
### Block WordPress attack paths (regex)
|
|
312
|
+
|
|
313
|
+
```json
|
|
314
|
+
{
|
|
315
|
+
"name": "Block WordPress Paths",
|
|
316
|
+
"active": true,
|
|
317
|
+
"conditionGroup": [
|
|
318
|
+
{ "conditions": [{ "type": "path", "op": "re", "value": "/(wp-admin|wp-login\\.php|xmlrpc\\.php|wp-content)" }] }
|
|
319
|
+
],
|
|
320
|
+
"action": { "mitigate": { "action": "deny" } }
|
|
321
|
+
}
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
### Redirect old paths
|
|
325
|
+
|
|
326
|
+
```json
|
|
327
|
+
{
|
|
328
|
+
"name": "Redirect Legacy API",
|
|
329
|
+
"active": true,
|
|
330
|
+
"conditionGroup": [
|
|
331
|
+
{ "conditions": [{ "type": "path", "op": "pre", "value": "/v1/api/" }] }
|
|
332
|
+
],
|
|
333
|
+
"action": {
|
|
334
|
+
"mitigate": {
|
|
335
|
+
"action": "redirect",
|
|
336
|
+
"redirect": { "location": "/v2/api/", "permanent": true }
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
## Health Score Tips
|
|
343
|
+
|
|
344
|
+
To maximize your configuration health score:
|
|
345
|
+
|
|
346
|
+
- Add `description` to every rule
|
|
347
|
+
- Use `id` fields with `rule_` prefix and snake_case
|
|
348
|
+
- Avoid regex when simpler operators work
|
|
349
|
+
- Remove disabled rules you no longer need
|
|
350
|
+
- Include rate limiting for API endpoints
|
|
351
|
+
- Include bot protection rules
|
|
352
|
+
- Use IP blocking for known threats
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
# Templates Reference
|
|
2
|
+
|
|
3
|
+
Pre-built rule templates for common security patterns. Add them to your config with a single command.
|
|
4
|
+
|
|
5
|
+
## Usage
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
doorman template # Browse available templates interactively
|
|
9
|
+
doorman template <name> # Add a specific template
|
|
10
|
+
doorman template <name> --config ./path/to/config.json # Add to specific config
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Templates append rules to your existing config. Run `doorman validate` after adding to confirm compatibility.
|
|
14
|
+
|
|
15
|
+
## Available Templates
|
|
16
|
+
|
|
17
|
+
### ai-bots
|
|
18
|
+
|
|
19
|
+
**Block AI Bots Firewall Rule**
|
|
20
|
+
|
|
21
|
+
Detects and logs known AI crawlers and training data scrapers. Default action is `log` — change to `deny` or `challenge` after reviewing traffic.
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
doorman template ai-bots
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Detected bots include: GPTBot, ChatGPT-User, ClaudeBot, Claude-Web, Bytespider, CCBot, Google-Extended, GoogleOther, Amazonbot, Applebot-Extended, Meta-ExternalAgent, PerplexityBot, OAI-SearchBot, Diffbot, anthropic-ai, cohere-ai, and many more.
|
|
28
|
+
|
|
29
|
+
**Rule added:**
|
|
30
|
+
```json
|
|
31
|
+
{
|
|
32
|
+
"id": "rule_detect_ai_bots",
|
|
33
|
+
"name": "Detect AI Bots",
|
|
34
|
+
"active": true,
|
|
35
|
+
"conditionGroup": [
|
|
36
|
+
{ "conditions": [{ "type": "user_agent", "op": "re", "value": "GPTBot|ChatGPT-User|ClaudeBot|..." }] }
|
|
37
|
+
],
|
|
38
|
+
"action": { "mitigate": { "action": "log" } }
|
|
39
|
+
}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
**Customization**: Change `"action": "log"` to `"action": "deny"` to block, or `"action": "challenge"` to CAPTCHA.
|
|
43
|
+
|
|
44
|
+
**Note**: Uses `re` (regex) operator — works on all Vercel plans, but Cloudflare Enterprise only. For Cloudflare non-Enterprise, split into multiple `sub` (contains) conditions instead.
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
### bad-bots
|
|
49
|
+
|
|
50
|
+
**Block Bad Bots Firewall Rule**
|
|
51
|
+
|
|
52
|
+
Detects known malicious bots, scrapers, vulnerability scanners, and attack tools. Comprehensive list of 500+ known bad user agents.
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
doorman template bad-bots
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Includes: scrapers (HTTrack, WebCopier, SiteRipper), vulnerability scanners (Nikto, Nmap, Acunetix, SQLmap, WPScan, Nuclei), SEO crawlers (AhrefsBot, SemrushBot, MJ12bot, DotBot), and spam/attack tools (Zeus, Havij, Dirbuster).
|
|
59
|
+
|
|
60
|
+
**Rule added:**
|
|
61
|
+
```json
|
|
62
|
+
{
|
|
63
|
+
"id": "rule_detect_bad_bots",
|
|
64
|
+
"name": "Detect Bad Bots",
|
|
65
|
+
"active": true,
|
|
66
|
+
"conditionGroup": [
|
|
67
|
+
{ "conditions": [{ "type": "user_agent", "op": "re", "value": "AhrefsBot|SemrushBot|MJ12bot|..." }] }
|
|
68
|
+
],
|
|
69
|
+
"action": { "mitigate": { "action": "log" } }
|
|
70
|
+
}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
**Customization**: Same as ai-bots — change action to `deny` for production blocking. Consider `challenge` for borderline bots.
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
### block-ofac-sanctioned-countries
|
|
78
|
+
|
|
79
|
+
**Block OFAC-Sanctioned Countries**
|
|
80
|
+
|
|
81
|
+
Blocks traffic from countries under US OFAC (Office of Foreign Assets Control) sanctions. Enforces a one-hour persistent block after first violation.
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
doorman template block-ofac-sanctioned-countries
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Countries blocked: Syria (SY), Iran (IR), Russia (RU), Cuba (CU), North Korea (KP).
|
|
88
|
+
|
|
89
|
+
**Rule added:**
|
|
90
|
+
```json
|
|
91
|
+
{
|
|
92
|
+
"id": "rule_block_traffic_from_ofac_sanctioned_countries",
|
|
93
|
+
"name": "Block traffic from OFAC-sanctioned countries",
|
|
94
|
+
"description": "Blocks traffic from OFAC-sanctioned countries and enforces a one-hour persistent block after the first violation.",
|
|
95
|
+
"active": true,
|
|
96
|
+
"conditionGroup": [
|
|
97
|
+
{ "conditions": [{ "type": "geo_country", "op": "inc", "value": ["SY", "IR", "RU", "CU", "KP"] }] }
|
|
98
|
+
],
|
|
99
|
+
"action": { "mitigate": { "action": "deny", "actionDuration": "1h" } }
|
|
100
|
+
}
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
**Customization**: Add or remove country codes as sanctions change. Consult https://sanctionssearch.ofac.treas.gov/ for the current list.
|
|
104
|
+
|
|
105
|
+
---
|
|
106
|
+
|
|
107
|
+
### wordpress
|
|
108
|
+
|
|
109
|
+
**Deny Common WordPress URLs Firewall Rule**
|
|
110
|
+
|
|
111
|
+
Blocks requests to WordPress-specific paths that are common attack vectors on non-WordPress sites. If your site does not run WordPress, these paths should never receive legitimate traffic.
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
doorman template wordpress
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Paths blocked: `/wp-admin`, `/wp-login.php`, `/xmlrpc.php`, `/wp-content`, `/wp-includes`, `/wp-signup.php`, `/wp-activate.php`, `/register.php`, `/wp-register.php`
|
|
118
|
+
|
|
119
|
+
**Rule added:**
|
|
120
|
+
```json
|
|
121
|
+
{
|
|
122
|
+
"name": "Deny WordPress URLs",
|
|
123
|
+
"active": true,
|
|
124
|
+
"conditionGroup": [
|
|
125
|
+
{ "conditions": [{ "type": "path", "op": "re", "value": "/(wp-admin|wp-login\\.php|xmlrpc\\.php|wp-content|wp-includes|wp-signup\\.php|wp-activate\\.php|register\\.php|wp-register\\.php)" }] }
|
|
126
|
+
],
|
|
127
|
+
"action": { "mitigate": { "action": "deny" } }
|
|
128
|
+
}
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
**Note**: Uses regex — for Cloudflare non-Enterprise, split into multiple `pre` (prefix) conditions:
|
|
132
|
+
```json
|
|
133
|
+
"conditionGroup": [
|
|
134
|
+
{ "conditions": [{ "type": "path", "op": "pre", "value": "/wp-admin" }] },
|
|
135
|
+
{ "conditions": [{ "type": "path", "op": "pre", "value": "/wp-login.php" }] },
|
|
136
|
+
{ "conditions": [{ "type": "path", "op": "pre", "value": "/xmlrpc.php" }] },
|
|
137
|
+
{ "conditions": [{ "type": "path", "op": "pre", "value": "/wp-content" }] }
|
|
138
|
+
]
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
## Creating Custom Templates
|
|
142
|
+
|
|
143
|
+
Templates are TypeScript modules in `src/lib/templates/rules/`. Each exports a `Template` object:
|
|
144
|
+
|
|
145
|
+
```typescript
|
|
146
|
+
import { Template } from '../types'
|
|
147
|
+
|
|
148
|
+
export const myTemplate: Template = {
|
|
149
|
+
metadata: {
|
|
150
|
+
title: 'My Custom Template',
|
|
151
|
+
reference: 'https://example.com/docs',
|
|
152
|
+
},
|
|
153
|
+
config: {
|
|
154
|
+
rules: [
|
|
155
|
+
{
|
|
156
|
+
id: 'rule_my_template',
|
|
157
|
+
name: 'My Rule',
|
|
158
|
+
description: 'What it does',
|
|
159
|
+
active: true,
|
|
160
|
+
conditionGroup: [
|
|
161
|
+
{ conditions: [{ type: 'path', op: 'pre', value: '/protected' }] }
|
|
162
|
+
],
|
|
163
|
+
action: { mitigate: { action: 'deny' } },
|
|
164
|
+
},
|
|
165
|
+
],
|
|
166
|
+
},
|
|
167
|
+
}
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
Register it in `src/lib/templates/index.ts` to make it available via `doorman template <name>`.
|
|
171
|
+
|
|
172
|
+
## Template Best Practices
|
|
173
|
+
|
|
174
|
+
1. **Start with `log` action** — observe traffic before blocking. Switch to `deny` once confident.
|
|
175
|
+
2. **Add descriptions** — templates without descriptions lower the health score.
|
|
176
|
+
3. **Combine templates** — use multiple templates together for layered security.
|
|
177
|
+
4. **Validate after adding** — always run `doorman validate` after adding a template.
|
|
178
|
+
5. **Review regex patterns** — template regex patterns are comprehensive but may need tuning for your specific use case.
|
|
179
|
+
|
|
180
|
+
## Recommended Starter Stack
|
|
181
|
+
|
|
182
|
+
```bash
|
|
183
|
+
doorman template ai-bots
|
|
184
|
+
doorman template bad-bots
|
|
185
|
+
doorman template wordpress
|
|
186
|
+
doorman template block-ofac-sanctioned-countries
|
|
187
|
+
doorman validate
|
|
188
|
+
doorman sync
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
This gives you bot protection, attack path blocking, and geo-compliance in under a minute.
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# Auto-Sync to gfargo/skills
|
|
2
|
+
|
|
3
|
+
These files are templates for setting up auto-sync to the [gfargo/skills](https://github.com/gfargo/skills) central skills repository.
|
|
4
|
+
|
|
5
|
+
## Setup Steps
|
|
6
|
+
|
|
7
|
+
1. **In `gfargo/skills`**, create the plugin directory structure:
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
plugins/security/
|
|
11
|
+
plugins/security/.claude-plugin/plugin.json <- copy plugin.json here
|
|
12
|
+
plugins/security/skills/doorman/ <- synced from this repo
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
2. **Copy `sync-doorman.yml`** to `gfargo/skills/.github/workflows/sync-doorman.yml`
|
|
16
|
+
|
|
17
|
+
3. **Update `gfargo/skills/.claude-plugin/marketplace.json`** to add the security plugin:
|
|
18
|
+
|
|
19
|
+
```json
|
|
20
|
+
{
|
|
21
|
+
"name": "security",
|
|
22
|
+
"source": "./plugins/security",
|
|
23
|
+
"description": "Web application firewall management as code with Doorman: create rules, block IPs, rate limit, protect against bots, and deploy WAF configs to Vercel and Cloudflare.",
|
|
24
|
+
"version": "1.0.0",
|
|
25
|
+
"author": { "name": "Griffen Fargo" }
|
|
26
|
+
}
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
4. The workflow runs daily and on `workflow_dispatch`. It:
|
|
30
|
+
- Checks the latest release tag on `gfargo/doorman`
|
|
31
|
+
- Compares against `.source-version` in the destination
|
|
32
|
+
- If newer, clones at that tag, mirrors `skills/doorman/` content
|
|
33
|
+
- Bumps the security plugin version, commits, tags, and creates a release
|
|
34
|
+
|
|
35
|
+
## Installation (end users)
|
|
36
|
+
|
|
37
|
+
Once published:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
npx skills add gfargo/skills --skill doorman
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Or via the `gfargo/skills` README install instructions.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "security",
|
|
3
|
+
"description": "Web application firewall management as code with Doorman: create rules, block IPs, rate limit, protect against bots, and deploy WAF configs to Vercel and Cloudflare.",
|
|
4
|
+
"version": "1.0.0",
|
|
5
|
+
"author": {
|
|
6
|
+
"name": "Griffen Fargo",
|
|
7
|
+
"email": "ghfargo@gmail.com"
|
|
8
|
+
},
|
|
9
|
+
"homepage": "https://github.com/gfargo/doorman",
|
|
10
|
+
"keywords": [
|
|
11
|
+
"security",
|
|
12
|
+
"firewall",
|
|
13
|
+
"waf",
|
|
14
|
+
"vercel",
|
|
15
|
+
"cloudflare",
|
|
16
|
+
"doorman",
|
|
17
|
+
"iac",
|
|
18
|
+
"rules",
|
|
19
|
+
"ip-blocking",
|
|
20
|
+
"rate-limiting",
|
|
21
|
+
"bot-protection"
|
|
22
|
+
]
|
|
23
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
name: Sync doorman skill from source
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
schedule:
|
|
5
|
+
- cron: '17 14 * * *'
|
|
6
|
+
workflow_dispatch: {}
|
|
7
|
+
|
|
8
|
+
permissions:
|
|
9
|
+
contents: write
|
|
10
|
+
|
|
11
|
+
env:
|
|
12
|
+
SOURCE_REPO: gfargo/doorman
|
|
13
|
+
SOURCE_SKILL_PATH: skills/doorman
|
|
14
|
+
DEST_SKILL_PATH: plugins/security/skills/doorman
|
|
15
|
+
PLUGIN_JSON: plugins/security/.claude-plugin/plugin.json
|
|
16
|
+
MARKETPLACE_JSON: .claude-plugin/marketplace.json
|
|
17
|
+
|
|
18
|
+
jobs:
|
|
19
|
+
sync:
|
|
20
|
+
runs-on: ubuntu-latest
|
|
21
|
+
steps:
|
|
22
|
+
- uses: actions/checkout@v4
|
|
23
|
+
|
|
24
|
+
- name: Check latest doorman release
|
|
25
|
+
id: check
|
|
26
|
+
env:
|
|
27
|
+
GH_TOKEN: ${{ github.token }}
|
|
28
|
+
run: |
|
|
29
|
+
latest=$(gh api "repos/${SOURCE_REPO}/releases/latest" --jq '.tag_name')
|
|
30
|
+
current=$(cat "${DEST_SKILL_PATH}/.source-version" 2>/dev/null || echo "none")
|
|
31
|
+
echo "latest=$latest" >> "$GITHUB_OUTPUT"
|
|
32
|
+
echo "current=$current" >> "$GITHUB_OUTPUT"
|
|
33
|
+
if [ "$latest" = "$current" ]; then
|
|
34
|
+
echo "up_to_date=true" >> "$GITHUB_OUTPUT"
|
|
35
|
+
else
|
|
36
|
+
echo "up_to_date=false" >> "$GITHUB_OUTPUT"
|
|
37
|
+
fi
|
|
38
|
+
|
|
39
|
+
- name: Fetch source at latest tag
|
|
40
|
+
if: steps.check.outputs.up_to_date == 'false'
|
|
41
|
+
run: |
|
|
42
|
+
git clone --depth 1 --branch "${{ steps.check.outputs.latest }}" \
|
|
43
|
+
"https://github.com/${SOURCE_REPO}.git" /tmp/source
|
|
44
|
+
|
|
45
|
+
- name: Mirror skill content
|
|
46
|
+
if: steps.check.outputs.up_to_date == 'false'
|
|
47
|
+
run: |
|
|
48
|
+
rsync -a --delete --exclude='sync/' "/tmp/source/${SOURCE_SKILL_PATH}/" "${DEST_SKILL_PATH}/"
|
|
49
|
+
echo "${{ steps.check.outputs.latest }}" > "${DEST_SKILL_PATH}/.source-version"
|
|
50
|
+
|
|
51
|
+
- name: Bump security plugin version
|
|
52
|
+
if: steps.check.outputs.up_to_date == 'false'
|
|
53
|
+
id: bump
|
|
54
|
+
run: |
|
|
55
|
+
old=$(jq -r '.version' "${PLUGIN_JSON}")
|
|
56
|
+
IFS='.' read -r major minor patch <<< "$old"
|
|
57
|
+
new="${major}.${minor}.$((patch + 1))"
|
|
58
|
+
jq --arg v "$new" '.version = $v' "${PLUGIN_JSON}" > "${PLUGIN_JSON}.tmp" && mv "${PLUGIN_JSON}.tmp" "${PLUGIN_JSON}"
|
|
59
|
+
jq --arg v "$new" '(.plugins[] | select(.name == "security") | .version) = $v' "${MARKETPLACE_JSON}" > "${MARKETPLACE_JSON}.tmp" && mv "${MARKETPLACE_JSON}.tmp" "${MARKETPLACE_JSON}"
|
|
60
|
+
echo "new_version=$new" >> "$GITHUB_OUTPUT"
|
|
61
|
+
|
|
62
|
+
- name: Commit and tag
|
|
63
|
+
if: steps.check.outputs.up_to_date == 'false'
|
|
64
|
+
run: |
|
|
65
|
+
git config user.name "github-actions[bot]"
|
|
66
|
+
git config user.email "github-actions[bot]@users.noreply.github.com"
|
|
67
|
+
git add "${DEST_SKILL_PATH}" "${PLUGIN_JSON}" "${MARKETPLACE_JSON}"
|
|
68
|
+
git commit -m "sync: doorman ${{ steps.check.outputs.latest }} (security v${{ steps.bump.outputs.new_version }})"
|
|
69
|
+
git tag "v${{ steps.bump.outputs.new_version }}"
|
|
70
|
+
git push origin HEAD:main
|
|
71
|
+
git push origin "v${{ steps.bump.outputs.new_version }}"
|
|
72
|
+
|
|
73
|
+
- name: Create release
|
|
74
|
+
if: steps.check.outputs.up_to_date == 'false'
|
|
75
|
+
env:
|
|
76
|
+
GH_TOKEN: ${{ github.token }}
|
|
77
|
+
run: |
|
|
78
|
+
gh release create "v${{ steps.bump.outputs.new_version }}" \
|
|
79
|
+
--title "security v${{ steps.bump.outputs.new_version }} — doorman ${{ steps.check.outputs.latest }}" \
|
|
80
|
+
--notes "Synced security:doorman to [gfargo/doorman@${{ steps.check.outputs.latest }}](https://github.com/${SOURCE_REPO}/releases/tag/${{ steps.check.outputs.latest }})"
|