destructive_command_guard 0.4.3

A Claude Code hook that blocks destructive commands before they execute
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
//! Kong API Gateway pack - protections for destructive Kong Gateway operations.
//!
//! Covers destructive operations for:
//! - Kong CLI (`kong delete services`, `kong delete routes`, etc.)
//! - deck CLI (`deck reset`, `deck sync` with destructive flags)
//! - Kong Admin API (DELETE requests to :8001)

use crate::packs::{DestructivePattern, Pack, SafePattern};
use crate::{destructive_pattern, safe_pattern};

/// Create the Kong API Gateway pack.
#[must_use]
pub fn create_pack() -> Pack {
    Pack {
        id: "apigateway.kong".to_string(),
        name: "Kong API Gateway",
        description: "Protects against destructive Kong Gateway CLI, deck CLI, and Admin API \
                      operations.",
        keywords: &["kong", "deck", "8001"],
        safe_patterns: create_safe_patterns(),
        destructive_patterns: create_destructive_patterns(),
        keyword_matcher: None,
        safe_regex_set: None,
        safe_regex_set_is_complete: false,
    }
}

fn create_safe_patterns() -> Vec<SafePattern> {
    vec![
        // Kong CLI - read operations
        safe_pattern!("kong-version", r"kong\s+(?:version|--version|-v)\b"),
        safe_pattern!("kong-help", r"kong\s+(?:help|--help|-h)\b"),
        safe_pattern!("kong-health", r"kong\s+health\b"),
        safe_pattern!("kong-check", r"kong\s+check\b"),
        safe_pattern!("kong-config-parse", r"kong\s+config\s+(?:parse|init)\b"),
        // deck CLI - read/safe operations
        safe_pattern!("deck-version", r"deck\s+(?:version|--version)\b"),
        safe_pattern!("deck-help", r"deck\s+(?:help|--help|-h)\b"),
        safe_pattern!("deck-ping", r"deck\s+ping\b"),
        safe_pattern!("deck-dump", r"deck\s+dump\b"),
        safe_pattern!("deck-diff", r"deck\s+diff\b"),
        safe_pattern!("deck-validate", r"deck\s+validate\b"),
        safe_pattern!("deck-convert", r"deck\s+convert\b"),
        safe_pattern!("deck-file", r"deck\s+file\b"),
        // Kong Admin API - explicit GET requests only
        safe_pattern!(
            "kong-admin-explicit-get",
            r"curl\s+.*(?:-X\s+GET|--request\s+GET)\s+.*(?:localhost|127\.0\.0\.1):8001/"
        ),
    ]
}

fn create_destructive_patterns() -> Vec<DestructivePattern> {
    vec![
        // deck reset - CRITICAL! Removes all Kong config
        destructive_pattern!(
            "deck-reset",
            r"deck\s+(?:gateway\s+)?reset\b",
            "deck reset removes ALL Kong configuration. This is extremely dangerous and irreversible.",
            Critical,
            "Resetting Kong removes ALL services, routes, plugins, consumers, certificates, and \
             upstreams. Your entire API gateway configuration is wiped. All traffic through Kong \
             will fail immediately. There is no undo.\n\n\
             Safer alternatives:\n\
             - deck dump: Export current configuration first\n\
             - deck diff: Review what will change before any sync\n\
             - Delete specific entities instead of full reset"
        ),
        // deck sync with --select-tag can be destructive
        destructive_pattern!(
            "deck-sync-destructive",
            r"deck\s+(?:gateway\s+)?sync\b.*--select-tag\b",
            "deck sync with --select-tag can remove entities not matching the tag.",
            High,
            "Using --select-tag with deck sync removes entities not matching the tag in your \
             state file. This can accidentally delete services and routes managed by other teams \
             or systems. Entities without tags are at risk of deletion.\n\n\
             Safer alternatives:\n\
             - deck diff --select-tag: Preview changes first\n\
             - Use consistent tagging across all entities\n\
             - Consider deck sync without --select-tag to avoid surprises"
        ),
        // Kong Admin API - DELETE requests (supports both DELETE-first and URL-first ordering)
        destructive_pattern!(
            "kong-admin-delete-services",
            r"curl\s+.*(?:(?:-X\s+DELETE|--request\s+DELETE).*(?:localhost|127\.0\.0\.1):8001/services|(?:localhost|127\.0\.0\.1):8001/services.*(?:-X\s+DELETE|--request\s+DELETE))",
            "DELETE request to Kong Admin API removes services.",
            High,
            "Deleting a Kong service removes the upstream service definition. All routes \
             associated with the service become orphaned and stop working. Traffic to those \
             endpoints will return 404 errors.\n\n\
             Safer alternatives:\n\
             - GET /services first: List services to verify the target\n\
             - Delete routes pointing to the service first\n\
             - Use deck dump to export configuration before changes"
        ),
        destructive_pattern!(
            "kong-admin-delete-routes",
            r"curl\s+.*(?:(?:-X\s+DELETE|--request\s+DELETE).*(?:localhost|127\.0\.0\.1):8001/routes|(?:localhost|127\.0\.0\.1):8001/routes.*(?:-X\s+DELETE|--request\s+DELETE))",
            "DELETE request to Kong Admin API removes routes.",
            High,
            "Deleting a Kong route removes the path-to-service mapping. Requests to that path \
             will return 404 errors. Plugins attached to the route are also removed. The \
             associated service is not affected.\n\n\
             Safer alternatives:\n\
             - GET /routes first: Verify the route details\n\
             - Test in a non-production environment\n\
             - Consider disabling plugins instead of deleting the route"
        ),
        destructive_pattern!(
            "kong-admin-delete-plugins",
            r"curl\s+.*(?:(?:-X\s+DELETE|--request\s+DELETE).*(?:localhost|127\.0\.0\.1):8001/plugins|(?:localhost|127\.0\.0\.1):8001/plugins.*(?:-X\s+DELETE|--request\s+DELETE))",
            "DELETE request to Kong Admin API removes plugins.",
            Medium,
            "Deleting a Kong plugin removes its functionality from the associated service, route, \
             or consumer. Authentication, rate limiting, logging, or transformation features \
             provided by the plugin stop immediately.\n\n\
             Safer alternatives:\n\
             - GET /plugins first: Review plugin configuration\n\
             - PATCH to disable the plugin instead of deleting\n\
             - Export plugin config with deck dump before deletion"
        ),
        destructive_pattern!(
            "kong-admin-delete-consumers",
            r"curl\s+.*(?:(?:-X\s+DELETE|--request\s+DELETE).*(?:localhost|127\.0\.0\.1):8001/consumers|(?:localhost|127\.0\.0\.1):8001/consumers.*(?:-X\s+DELETE|--request\s+DELETE))",
            "DELETE request to Kong Admin API removes consumers.",
            High,
            "Deleting a Kong consumer removes the API client identity. All credentials (API keys, \
             JWT, OAuth) for that consumer are revoked. Plugins configured per-consumer stop \
             applying. Affected clients lose API access.\n\n\
             Safer alternatives:\n\
             - GET /consumers first: Review consumer details\n\
             - Delete individual credentials instead of the consumer\n\
             - Notify affected clients before deletion"
        ),
        destructive_pattern!(
            "kong-admin-delete-upstreams",
            r"curl\s+.*(?:(?:-X\s+DELETE|--request\s+DELETE).*(?:localhost|127\.0\.0\.1):8001/upstreams|(?:localhost|127\.0\.0\.1):8001/upstreams.*(?:-X\s+DELETE|--request\s+DELETE))",
            "DELETE request to Kong Admin API removes upstreams.",
            High,
            "Deleting a Kong upstream removes the load balancer and all its targets. Services \
             using this upstream will fail to route traffic. Health checks and circuit breaker \
             settings are lost.\n\n\
             Safer alternatives:\n\
             - GET /upstreams first: Review upstream configuration\n\
             - Update services to use a different upstream first\n\
             - Remove targets individually to verify impact"
        ),
        destructive_pattern!(
            "kong-admin-delete-targets",
            r"curl\s+.*(?:(?:-X\s+DELETE|--request\s+DELETE).*(?:localhost|127\.0\.0\.1):8001/.*targets|(?:localhost|127\.0\.0\.1):8001/.*targets.*(?:-X\s+DELETE|--request\s+DELETE))",
            "DELETE request to Kong Admin API removes targets.",
            Medium,
            "Deleting a Kong target removes a backend server from the upstream pool. Traffic \
             is redistributed to remaining targets. If this was the last target, the upstream \
             has no backends and requests fail.\n\n\
             Safer alternatives:\n\
             - GET /upstreams/{id}/targets first: List current targets\n\
             - Set target weight to 0 to drain traffic first\n\
             - Verify other targets can handle the load"
        ),
        destructive_pattern!(
            "kong-admin-delete-certificates",
            r"curl\s+.*(?:(?:-X\s+DELETE|--request\s+DELETE).*(?:localhost|127\.0\.0\.1):8001/certificates|(?:localhost|127\.0\.0\.1):8001/certificates.*(?:-X\s+DELETE|--request\s+DELETE))",
            "DELETE request to Kong Admin API removes certificates.",
            High,
            "Deleting a Kong certificate removes the TLS/SSL certificate. SNIs using this \
             certificate will fail TLS handshakes. HTTPS traffic to affected domains will \
             receive certificate errors.\n\n\
             Safer alternatives:\n\
             - GET /certificates first: Review certificate details\n\
             - Upload a replacement certificate before deletion\n\
             - Remove associated SNIs first"
        ),
        destructive_pattern!(
            "kong-admin-delete-snis",
            r"curl\s+.*(?:(?:-X\s+DELETE|--request\s+DELETE).*(?:localhost|127\.0\.0\.1):8001/snis|(?:localhost|127\.0\.0\.1):8001/snis.*(?:-X\s+DELETE|--request\s+DELETE))",
            "DELETE request to Kong Admin API removes SNIs.",
            High,
            "Deleting a Kong SNI removes the domain-to-certificate mapping. HTTPS requests \
             for that hostname will fail TLS negotiation or use a default certificate if \
             configured. Clients will see certificate warnings.\n\n\
             Safer alternatives:\n\
             - GET /snis first: Review SNI configuration\n\
             - Update the SNI to point to a different certificate\n\
             - Verify the certificate is no longer needed for this domain"
        ),
        // Generic DELETE to any Kong Admin API endpoint
        destructive_pattern!(
            "kong-admin-delete-generic",
            r"curl\s+.*(?:(?:-X\s+DELETE|--request\s+DELETE).*(?:localhost|127\.0\.0\.1):8001/|(?:localhost|127\.0\.0\.1):8001/.*(?:-X\s+DELETE|--request\s+DELETE))",
            "DELETE request to Kong Admin API can remove configuration.",
            Medium,
            "DELETE requests to the Kong Admin API remove configuration objects. The specific \
             impact depends on the endpoint: services, routes, plugins, consumers, certificates, \
             or other Kong entities. Deletions take effect immediately.\n\n\
             Safer alternatives:\n\
             - Use GET requests first to review the resource\n\
             - Export configuration with deck dump\n\
             - Test changes in a staging environment"
        ),
    ]
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::packs::test_helpers::*;

    #[test]
    fn test_pack_creation() {
        let pack = create_pack();
        assert_eq!(pack.id, "apigateway.kong");
        assert_eq!(pack.name, "Kong API Gateway");
        assert!(!pack.description.is_empty());
        assert!(pack.keywords.contains(&"kong"));
        assert!(pack.keywords.contains(&"deck"));

        assert_patterns_compile(&pack);
        assert_all_patterns_have_reasons(&pack);
        assert_unique_pattern_names(&pack);
    }

    #[test]
    fn allows_safe_commands() {
        let pack = create_pack();
        // Kong CLI - read operations
        assert_safe_pattern_matches(&pack, "kong version");
        assert_safe_pattern_matches(&pack, "kong --version");
        assert_safe_pattern_matches(&pack, "kong -v");
        assert_safe_pattern_matches(&pack, "kong help");
        assert_safe_pattern_matches(&pack, "kong --help");
        assert_safe_pattern_matches(&pack, "kong health");
        assert_safe_pattern_matches(&pack, "kong check /etc/kong/kong.conf");
        assert_safe_pattern_matches(&pack, "kong config parse /etc/kong/kong.conf");
        assert_safe_pattern_matches(&pack, "kong config init");
        // deck CLI - read operations
        assert_safe_pattern_matches(&pack, "deck version");
        assert_safe_pattern_matches(&pack, "deck --version");
        assert_safe_pattern_matches(&pack, "deck help");
        assert_safe_pattern_matches(&pack, "deck --help");
        assert_safe_pattern_matches(&pack, "deck ping");
        assert_safe_pattern_matches(&pack, "deck dump");
        assert_safe_pattern_matches(&pack, "deck dump --output-file kong.yaml");
        assert_safe_pattern_matches(&pack, "deck diff");
        assert_safe_pattern_matches(&pack, "deck diff --state kong.yaml");
        assert_safe_pattern_matches(&pack, "deck validate");
        assert_safe_pattern_matches(&pack, "deck convert");
        assert_safe_pattern_matches(&pack, "deck file");
        // Kong Admin API - explicit GET requests
        assert_safe_pattern_matches(&pack, "curl -X GET localhost:8001/routes");
        assert_safe_pattern_matches(&pack, "curl --request GET localhost:8001/plugins");
        // Implicit GET requests are allowed by default (no destructive match)
        assert_allows(&pack, "curl localhost:8001/");
        assert_allows(&pack, "curl localhost:8001/services");
        assert_allows(&pack, "curl 127.0.0.1:8001/status");
    }

    #[test]
    fn blocks_deck_reset() {
        let pack = create_pack();
        assert_blocks_with_pattern(&pack, "deck reset", "deck-reset");
        assert_blocks_with_pattern(&pack, "deck reset --force", "deck-reset");
        assert_blocks_with_pattern(&pack, "deck gateway reset", "deck-reset");
    }

    #[test]
    fn blocks_deck_sync_select_tag() {
        let pack = create_pack();
        assert_blocks_with_pattern(
            &pack,
            "deck sync --select-tag production",
            "deck-sync-destructive",
        );
        assert_blocks_with_pattern(
            &pack,
            "deck gateway sync --select-tag team-a",
            "deck-sync-destructive",
        );
    }

    #[test]
    fn blocks_admin_api_delete_services() {
        let pack = create_pack();
        assert_blocks_with_pattern(
            &pack,
            "curl -X DELETE localhost:8001/services/my-service",
            "kong-admin-delete-services",
        );
        assert_blocks_with_pattern(
            &pack,
            "curl --request DELETE localhost:8001/services/abc123",
            "kong-admin-delete-services",
        );
    }

    #[test]
    fn blocks_admin_api_delete_routes() {
        let pack = create_pack();
        assert_blocks_with_pattern(
            &pack,
            "curl -X DELETE localhost:8001/routes/my-route",
            "kong-admin-delete-routes",
        );
    }

    #[test]
    fn blocks_admin_api_delete_plugins() {
        let pack = create_pack();
        assert_blocks_with_pattern(
            &pack,
            "curl -X DELETE localhost:8001/plugins/rate-limiting",
            "kong-admin-delete-plugins",
        );
    }

    #[test]
    fn blocks_admin_api_delete_consumers() {
        let pack = create_pack();
        assert_blocks_with_pattern(
            &pack,
            "curl -X DELETE localhost:8001/consumers/user123",
            "kong-admin-delete-consumers",
        );
    }

    #[test]
    fn blocks_admin_api_delete_upstreams() {
        let pack = create_pack();
        assert_blocks_with_pattern(
            &pack,
            "curl -X DELETE localhost:8001/upstreams/backend",
            "kong-admin-delete-upstreams",
        );
    }

    #[test]
    fn blocks_admin_api_delete_targets() {
        let pack = create_pack();
        // Note: This URL matches upstreams pattern first (contains /upstreams/)
        // but the command is still blocked which is the desired behavior
        assert_blocks_with_pattern(
            &pack,
            "curl -X DELETE localhost:8001/upstreams/backend/targets/host1",
            "kong-admin-delete-upstreams",
        );
        // Direct targets endpoint
        assert_blocks_with_pattern(
            &pack,
            "curl -X DELETE localhost:8001/targets/abc123",
            "kong-admin-delete-targets",
        );
    }

    #[test]
    fn blocks_admin_api_delete_certificates() {
        let pack = create_pack();
        assert_blocks_with_pattern(
            &pack,
            "curl -X DELETE localhost:8001/certificates/abc123",
            "kong-admin-delete-certificates",
        );
    }

    #[test]
    fn blocks_admin_api_delete_snis() {
        let pack = create_pack();
        assert_blocks_with_pattern(
            &pack,
            "curl -X DELETE localhost:8001/snis/example.com",
            "kong-admin-delete-snis",
        );
    }

    #[test]
    fn blocks_admin_api_with_ip_address() {
        let pack = create_pack();
        assert_blocks_with_pattern(
            &pack,
            "curl -X DELETE 127.0.0.1:8001/services/test",
            "kong-admin-delete-services",
        );
    }

    #[test]
    fn blocks_url_first_ordering() {
        let pack = create_pack();
        // URL before -X DELETE flag (common curl pattern)
        assert_blocks_with_pattern(
            &pack,
            "curl localhost:8001/services/my-service -X DELETE",
            "kong-admin-delete-services",
        );
        assert_blocks_with_pattern(
            &pack,
            "curl localhost:8001/routes/my-route -X DELETE",
            "kong-admin-delete-routes",
        );
        assert_blocks_with_pattern(
            &pack,
            "curl 127.0.0.1:8001/plugins/rate-limit -X DELETE",
            "kong-admin-delete-plugins",
        );
    }

    #[test]
    fn allows_non_curl_strings_with_kong_admin_tokens() {
        let pack = create_pack();
        assert_allows(&pack, "echo localhost:8001/services/my-service -X DELETE");
    }
}