use crate::packs::{DestructivePattern, Pack, SafePattern};
use crate::{destructive_pattern, safe_pattern};
#[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![
safe_pattern!(
"az-storage-container-list",
r"\baz\s+storage\s+container\s+list\b"
),
safe_pattern!(
"az-storage-container-show",
r"\baz\s+storage\s+container\s+show\b"
),
safe_pattern!(
"az-storage-container-exists",
r"\baz\s+storage\s+container\s+exists\b"
),
safe_pattern!("az-storage-blob-list", r"\baz\s+storage\s+blob\s+list\b"),
safe_pattern!("az-storage-blob-show", r"\baz\s+storage\s+blob\s+show\b"),
safe_pattern!(
"az-storage-blob-exists",
r"\baz\s+storage\s+blob\s+exists\b"
),
safe_pattern!(
"az-storage-blob-download",
r"\baz\s+storage\s+blob\s+download\b"
),
safe_pattern!(
"az-storage-blob-download-batch",
r"\baz\s+storage\s+blob\s+download-batch\b"
),
safe_pattern!("az-storage-blob-url", r"\baz\s+storage\s+blob\s+url\b"),
safe_pattern!(
"az-storage-blob-metadata-show",
r"\baz\s+storage\s+blob\s+metadata\s+show\b"
),
safe_pattern!(
"az-storage-account-list",
r"\baz\s+storage\s+account\s+list\b"
),
safe_pattern!(
"az-storage-account-show",
r"\baz\s+storage\s+account\s+show\b"
),
safe_pattern!(
"az-storage-account-keys-list",
r"\baz\s+storage\s+account\s+keys\s+list\b"
),
safe_pattern!("azcopy-list", r"\bazcopy\s+(?:--\S+\s+)*list\b"),
safe_pattern!("azcopy-copy", r"\bazcopy\s+(?:--\S+\s+)*copy\b"),
safe_pattern!("azcopy-jobs-list", r"\bazcopy\s+(?:--\S+\s+)*jobs\s+list\b"),
safe_pattern!("azcopy-jobs-show", r"\bazcopy\s+(?:--\S+\s+)*jobs\s+show\b"),
safe_pattern!("azcopy-login", r"\bazcopy\s+(?:--\S+\s+)*login\b"),
safe_pattern!("azcopy-env", r"\bazcopy\s+(?:--\S+\s+)*env\b"),
]
}
fn create_destructive_patterns() -> Vec<DestructivePattern> {
vec![
destructive_pattern!(
"az-storage-container-delete",
r"\baz\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"
),
destructive_pattern!(
"az-storage-blob-delete-batch",
r"\baz\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\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"
),
destructive_pattern!(
"az-storage-account-delete",
r"\baz\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"
),
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"
),
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::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();
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");
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",
);
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");
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();
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",
);
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",
);
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",
);
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",
);
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",
);
}
}