destructive_command_guard 0.5.6

An AI coding agent 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
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
//! Azure Blob Storage pack - protections for destructive Azure storage operations.
//!
//! Covers destructive operations:
//! - Container deletion (az storage container delete)
//! - Blob deletion (az storage blob delete, delete-batch)
//! - Storage account deletion
//! - azcopy remove and sync --delete-destination

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

/// Create the Azure Blob Storage pack.
#[must_use]
pub fn create_pack() -> Pack {
    Pack {
        id: "storage.azure_blob".to_string(),
        name: "Azure Blob Storage",
        description: "Protects against destructive Azure Blob Storage operations like container \
                      deletion, blob deletion, and azcopy remove.",
        keywords: &["az storage", "azcopy"],
        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![
        // az storage container operations (read-only)
        safe_pattern!(
            "az-storage-container-list",
            r"\baz\b(?:\s+--?\S+(?:\s+\S+)?)*\s+storage\s+container\s+list(?=\s|$)"
        ),
        safe_pattern!(
            "az-storage-container-show",
            r"\baz\b(?:\s+--?\S+(?:\s+\S+)?)*\s+storage\s+container\s+show(?=\s|$)"
        ),
        safe_pattern!(
            "az-storage-container-exists",
            r"\baz\b(?:\s+--?\S+(?:\s+\S+)?)*\s+storage\s+container\s+exists(?=\s|$)"
        ),
        // az storage blob operations (read-only)
        safe_pattern!(
            "az-storage-blob-list",
            r"\baz\b(?:\s+--?\S+(?:\s+\S+)?)*\s+storage\s+blob\s+list(?=\s|$)"
        ),
        safe_pattern!(
            "az-storage-blob-show",
            r"\baz\b(?:\s+--?\S+(?:\s+\S+)?)*\s+storage\s+blob\s+show(?=\s|$)"
        ),
        safe_pattern!(
            "az-storage-blob-exists",
            r"\baz\b(?:\s+--?\S+(?:\s+\S+)?)*\s+storage\s+blob\s+exists(?=\s|$)"
        ),
        safe_pattern!(
            "az-storage-blob-download",
            r"\baz\b(?:\s+--?\S+(?:\s+\S+)?)*\s+storage\s+blob\s+download(?=\s|$)"
        ),
        safe_pattern!(
            "az-storage-blob-download-batch",
            r"\baz\b(?:\s+--?\S+(?:\s+\S+)?)*\s+storage\s+blob\s+download-batch(?=\s|$)"
        ),
        safe_pattern!(
            "az-storage-blob-url",
            r"\baz\b(?:\s+--?\S+(?:\s+\S+)?)*\s+storage\s+blob\s+url(?=\s|$)"
        ),
        safe_pattern!(
            "az-storage-blob-metadata-show",
            r"\baz\b(?:\s+--?\S+(?:\s+\S+)?)*\s+storage\s+blob\s+metadata\s+show(?=\s|$)"
        ),
        // az storage account operations (read-only)
        safe_pattern!(
            "az-storage-account-list",
            r"\baz\b(?:\s+--?\S+(?:\s+\S+)?)*\s+storage\s+account\s+list(?=\s|$)"
        ),
        safe_pattern!(
            "az-storage-account-show",
            r"\baz\b(?:\s+--?\S+(?:\s+\S+)?)*\s+storage\s+account\s+show(?=\s|$)"
        ),
        safe_pattern!(
            "az-storage-account-keys-list",
            r"\baz\b(?:\s+--?\S+(?:\s+\S+)?)*\s+storage\s+account\s+keys\s+list(?=\s|$)"
        ),
        // azcopy operations (read-only)
        safe_pattern!("azcopy-list", r"\bazcopy\s+(?:--\S+\s+)*list(?=\s|$)"),
        safe_pattern!("azcopy-copy", r"\bazcopy\s+(?:--\S+\s+)*copy(?=\s|$)"),
        safe_pattern!(
            "azcopy-jobs-list",
            r"\bazcopy\s+(?:--\S+\s+)*jobs\s+list(?=\s|$)"
        ),
        safe_pattern!(
            "azcopy-jobs-show",
            r"\bazcopy\s+(?:--\S+\s+)*jobs\s+show(?=\s|$)"
        ),
        safe_pattern!("azcopy-login", r"\bazcopy\s+(?:--\S+\s+)*login(?=\s|$)"),
        safe_pattern!("azcopy-env", r"\bazcopy\s+(?:--\S+\s+)*env(?=\s|$)"),
    ]
}

fn create_destructive_patterns() -> Vec<DestructivePattern> {
    vec![
        // Container deletion
        destructive_pattern!(
            "az-storage-container-delete",
            r"\baz\b(?:\s+--?\S+(?:\s+\S+)?)*\s+storage\s+container\s+delete\b",
            "az storage container delete removes an Azure storage container.",
            Critical,
            "Deleting an Azure storage container removes all blobs within it permanently. \
             Unless soft delete is enabled, data cannot be recovered. Applications and \
             services referencing this container will fail.\n\n\
             Safer alternatives:\n\
             - az storage container list: Review containers first\n\
             - az storage blob list -c X: Inventory contents\n\
             - Enable soft delete before testing deletions"
        ),
        // Blob deletion (order matters: delete-batch before delete)
        destructive_pattern!(
            "az-storage-blob-delete-batch",
            r"\baz\b(?:\s+--?\S+(?:\s+\S+)?)*\s+storage\s+blob\s+delete-batch\b",
            "az storage blob delete-batch removes multiple blobs from Azure storage.",
            High,
            "Batch deletion removes many blobs at once based on patterns. Without soft \
             delete enabled, removed blobs cannot be recovered. Patterns may match more \
             files than expected.\n\n\
             Safer alternatives:\n\
             - az storage blob list --pattern: Preview matching blobs\n\
             - Enable soft delete for recovery window\n\
             - azcopy copy to backup before deletion"
        ),
        destructive_pattern!(
            "az-storage-blob-delete",
            r"\baz\b(?:\s+--?\S+(?:\s+\S+)?)*\s+storage\s+blob\s+delete(?:\s|$)",
            "az storage blob delete removes a blob from Azure storage.",
            Medium,
            "Deleting a single blob removes it from storage. Without soft delete, the data \
             is permanently lost. Applications expecting this blob will receive 404 errors.\n\n\
             Safer alternatives:\n\
             - az storage blob show: Verify blob before deletion\n\
             - az storage blob download: Backup content first\n\
             - Enable soft delete for recovery"
        ),
        // Storage account deletion
        destructive_pattern!(
            "az-storage-account-delete",
            r"\baz\b(?:\s+--?\S+(?:\s+\S+)?)*\s+storage\s+account\s+delete\b",
            "az storage account delete removes an entire Azure storage account.",
            Critical,
            "Deleting a storage account removes all containers, blobs, tables, queues, \
             and file shares within it. The account name may not be immediately reclaimable. \
             All dependent applications will fail completely.\n\n\
             Safer alternatives:\n\
             - az storage account show: Review account configuration\n\
             - azcopy copy: Backup all data before deletion\n\
             - Verify no active resources depend on this account"
        ),
        // azcopy remove
        destructive_pattern!(
            "azcopy-remove",
            r"\bazcopy\s+(?:--\S+\s+)*remove\b",
            "azcopy remove deletes files from Azure storage.",
            High,
            "The azcopy remove command deletes blobs from Azure storage. With --recursive, \
             entire directory trees are removed. Without soft delete, data is permanently \
             lost and cannot be recovered.\n\n\
             Safer alternatives:\n\
             - azcopy list: Preview files to be deleted\n\
             - Enable soft delete before removing\n\
             - azcopy copy to backup location first"
        ),
        // azcopy sync with delete
        destructive_pattern!(
            "azcopy-sync-delete",
            r"\bazcopy\s+(?:--\S+\s+)*sync\b.*--delete-destination\b",
            "azcopy sync --delete-destination removes destination files not in source.",
            High,
            "The --delete-destination flag removes files from the destination that don't \
             exist in the source. If source and destination are swapped, or if source is \
             unexpectedly empty, this can cause complete data loss.\n\n\
             Safer alternatives:\n\
             - azcopy sync --dry-run: Preview changes first\n\
             - azcopy sync without --delete-destination: Only adds/updates\n\
             - Backup destination before syncing"
        ),
    ]
}

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

    #[test]
    fn test_pack_creation() {
        let pack = create_pack();
        assert_eq!(pack.id, "storage.azure_blob");
        assert_eq!(pack.name, "Azure Blob Storage");
        assert!(!pack.description.is_empty());
        assert!(pack.keywords.contains(&"az storage"));
        assert!(pack.keywords.contains(&"azcopy"));

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

    #[test]
    fn allows_safe_commands() {
        let pack = create_pack();
        // Container operations
        assert_safe_pattern_matches(&pack, "az storage container list --account-name myaccount");
        assert_safe_pattern_matches(&pack, "az storage container show -n mycontainer");
        assert_safe_pattern_matches(&pack, "az storage container exists -n mycontainer");
        // Blob operations
        assert_safe_pattern_matches(&pack, "az storage blob list -c mycontainer");
        assert_safe_pattern_matches(&pack, "az storage blob show -c mycontainer -n myblob");
        assert_safe_pattern_matches(&pack, "az storage blob exists -c mycontainer -n myblob");
        assert_safe_pattern_matches(
            &pack,
            "az storage blob download -c mycontainer -n myblob -f local.txt",
        );
        assert_safe_pattern_matches(
            &pack,
            "az storage blob download-batch -d ./local -s mycontainer",
        );
        assert_safe_pattern_matches(&pack, "az storage blob url -c mycontainer -n myblob");
        assert_safe_pattern_matches(
            &pack,
            "az storage blob metadata show -c mycontainer -n myblob",
        );
        // Account operations
        assert_safe_pattern_matches(&pack, "az storage account list");
        assert_safe_pattern_matches(&pack, "az storage account show -n myaccount");
        assert_safe_pattern_matches(&pack, "az storage account keys list -n myaccount");
        // azcopy operations
        assert_safe_pattern_matches(&pack, "azcopy list https://account.blob.core.windows.net/");
        assert_safe_pattern_matches(
            &pack,
            "azcopy copy ./local https://account.blob.core.windows.net/container",
        );
        assert_safe_pattern_matches(&pack, "azcopy jobs list");
        assert_safe_pattern_matches(&pack, "azcopy jobs show jobid");
        assert_safe_pattern_matches(&pack, "azcopy login");
        assert_safe_pattern_matches(&pack, "azcopy env");
    }

    #[test]
    fn blocks_destructive_commands() {
        let pack = create_pack();
        // Container deletion
        assert_blocks_with_pattern(
            &pack,
            "az storage container delete -n mycontainer",
            "az-storage-container-delete",
        );
        assert_blocks_with_pattern(
            &pack,
            "az storage container delete --name mycontainer --account-name myaccount",
            "az-storage-container-delete",
        );
        // Blob deletion
        assert_blocks_with_pattern(
            &pack,
            "az storage blob delete -c mycontainer -n myblob",
            "az-storage-blob-delete",
        );
        assert_blocks_with_pattern(
            &pack,
            "az storage blob delete-batch -s mycontainer",
            "az-storage-blob-delete-batch",
        );
        assert_blocks_with_pattern(
            &pack,
            "az storage blob delete-batch --source mycontainer --pattern '*.log'",
            "az-storage-blob-delete-batch",
        );
        // Account deletion
        assert_blocks_with_pattern(
            &pack,
            "az storage account delete -n myaccount",
            "az-storage-account-delete",
        );
        assert_blocks_with_pattern(
            &pack,
            "az storage account delete --name myaccount --yes",
            "az-storage-account-delete",
        );
        // azcopy remove
        assert_blocks_with_pattern(
            &pack,
            "azcopy remove https://account.blob.core.windows.net/container/blob",
            "azcopy-remove",
        );
        assert_blocks_with_pattern(
            &pack,
            "azcopy remove --recursive https://account.blob.core.windows.net/container",
            "azcopy-remove",
        );
        // azcopy sync with delete
        assert_blocks_with_pattern(
            &pack,
            "azcopy sync ./local https://account.blob.core.windows.net/container --delete-destination true",
            "azcopy-sync-delete",
        );
        assert_blocks_with_pattern(
            &pack,
            "azcopy sync --delete-destination=true ./src https://dest.blob.core.windows.net/",
            "azcopy-sync-delete",
        );
    }

    #[test]
    fn azure_blob_blocks_each_destructive_pattern() {
        let pack = create_pack();
        assert_blocks(
            &pack,
            "az storage container delete -n mycontainer",
            "az storage container delete removes an Azure storage container",
        );
        assert_blocks(
            &pack,
            "az storage blob delete-batch -s mycontainer",
            "az storage blob delete-batch removes multiple blobs",
        );
        assert_blocks(
            &pack,
            "az storage blob delete -c mycontainer -n myblob",
            "az storage blob delete removes a blob from Azure storage",
        );
        assert_blocks(
            &pack,
            "az storage account delete -n myaccount",
            "az storage account delete removes an entire Azure storage account",
        );
        assert_blocks(
            &pack,
            "azcopy remove https://account.blob.core.windows.net/container/blob",
            "azcopy remove deletes files from Azure storage",
        );
        assert_blocks(
            &pack,
            "azcopy sync ./local https://account.blob.core.windows.net/container --delete-destination true",
            "azcopy sync --delete-destination removes destination files not in source",
        );
    }

    #[test]
    fn azure_blob_blocks_with_correct_severity() {
        let pack = create_pack();
        assert_blocks_with_severity(
            &pack,
            "az storage container delete -n mycontainer",
            Severity::Critical,
        );
        assert_blocks_with_severity(
            &pack,
            "az storage blob delete-batch -s mycontainer",
            Severity::High,
        );
        assert_blocks_with_severity(
            &pack,
            "az storage blob delete -c mycontainer -n myblob",
            Severity::Medium,
        );
        assert_blocks_with_severity(
            &pack,
            "az storage account delete -n myaccount",
            Severity::Critical,
        );
        assert_blocks_with_severity(
            &pack,
            "azcopy remove https://account.blob.core.windows.net/container/blob",
            Severity::High,
        );
        assert_blocks_with_severity(
            &pack,
            "azcopy sync ./local https://account.blob.core.windows.net/container --delete-destination true",
            Severity::High,
        );
    }

    #[test]
    fn azure_blob_all_safe_patterns_match() {
        let pack = create_pack();
        // az storage container safe
        assert_safe_pattern_matches(&pack, "az storage container list --account-name myaccount");
        assert_safe_pattern_matches(&pack, "az storage container show -n mycontainer");
        assert_safe_pattern_matches(&pack, "az storage container exists -n mycontainer");
        // az storage blob safe
        assert_safe_pattern_matches(&pack, "az storage blob list -c mycontainer");
        assert_safe_pattern_matches(&pack, "az storage blob show -c mycontainer -n myblob");
        assert_safe_pattern_matches(&pack, "az storage blob exists -c mycontainer -n myblob");
        assert_safe_pattern_matches(
            &pack,
            "az storage blob download -c mycontainer -n myblob -f local.txt",
        );
        assert_safe_pattern_matches(
            &pack,
            "az storage blob download-batch -d ./local -s mycontainer",
        );
        assert_safe_pattern_matches(&pack, "az storage blob url -c mycontainer -n myblob");
        assert_safe_pattern_matches(
            &pack,
            "az storage blob metadata show -c mycontainer -n myblob",
        );
        // az storage account safe
        assert_safe_pattern_matches(&pack, "az storage account list");
        assert_safe_pattern_matches(&pack, "az storage account show -n myaccount");
        assert_safe_pattern_matches(&pack, "az storage account keys list -n myaccount");
        // azcopy safe
        assert_safe_pattern_matches(&pack, "azcopy list https://account.blob.core.windows.net/");
        assert_safe_pattern_matches(
            &pack,
            "azcopy copy ./local https://account.blob.core.windows.net/container",
        );
        assert_safe_pattern_matches(&pack, "azcopy jobs list");
        assert_safe_pattern_matches(&pack, "azcopy jobs show jobid");
        assert_safe_pattern_matches(&pack, "azcopy login");
        assert_safe_pattern_matches(&pack, "azcopy env");
    }

    #[test]
    fn azure_blob_unrelated_commands_no_match() {
        let pack = create_pack();
        assert_no_match(&pack, "git status");
        assert_no_match(&pack, "echo hello");
        assert_no_match(&pack, "ls -la");
        assert_no_match(&pack, "docker ps");
    }
}