use crate::packs::{DestructivePattern, Pack, SafePattern};
use crate::{destructive_pattern, safe_pattern};
#[must_use]
pub fn create_pack() -> Pack {
Pack {
id: "cloud.azure".to_string(),
name: "Azure CLI",
description: "Protects against destructive Azure CLI operations like vm delete, \
storage account delete, and resource group delete",
keywords: &[
"az", "delete", "vm", "storage", "acr", "registry",
"keyvault", "role", "ad", "dns", "cosmosdb", "monitor", "purge",
],
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-show",
r"az\b(?:\s+--?\S+(?:\s+\S+)?)*\s+\S+\s+show(?=\s|$)"
),
safe_pattern!(
"az-list",
r"az\b(?:\s+--?\S+(?:\s+\S+)?)*\s+\S+\s+list(?=\s|$)"
),
safe_pattern!(
"az-account",
r"az\b(?:\s+--?\S+(?:\s+\S+)?)*\s+account(?=\s|$)"
),
safe_pattern!(
"az-configure",
r"az\b(?:\s+--?\S+(?:\s+\S+)?)*\s+configure(?=\s|$)"
),
safe_pattern!("az-login", r"az\b(?:\s+--?\S+(?:\s+\S+)?)*\s+login(?=\s|$)"),
safe_pattern!(
"az-version",
r"az\b(?:\s+--?\S+(?:\s+\S+)?)*\s+version(?=\s|$)"
),
safe_pattern!("az-help", r"az\b.*--help"),
safe_pattern!("az-what-if", r"az\b.*--what-if"),
]
}
#[allow(clippy::too_many_lines)]
fn create_destructive_patterns() -> Vec<DestructivePattern> {
vec![
destructive_pattern!(
"vm-delete",
r"az\b.*?\bvm\s+delete",
"az vm delete permanently destroys virtual machines.",
Critical,
"vm delete permanently destroys Azure VMs:\n\n\
- VM is deallocated and deleted\n\
- OS disk deleted (unless --os-disk=detach)\n\
- Data disks detached but not deleted\n\
- Public IP released\n\n\
Preserve disks: az vm delete --os-disk detach --data-disks detach"
),
destructive_pattern!(
"storage-delete",
r"az\b.*?\bstorage\s+account\s+delete",
"az storage account delete permanently destroys the storage account and all data.",
Critical,
"storage account delete destroys entire storage account:\n\n\
- ALL blobs, files, queues, tables deleted\n\
- All containers and their contents gone\n\
- Cannot be recovered without backups\n\n\
List contents first: az storage container list --account-name NAME"
),
destructive_pattern!(
"blob-delete",
r"az\b.*?\bstorage\s+(?:blob|container)\s+delete",
"az storage blob/container delete permanently removes data.",
High,
"storage blob/container delete removes data:\n\n\
- Blob delete removes individual blobs\n\
- Container delete removes container and ALL blobs\n\
- Soft delete may allow recovery if enabled\n\n\
Check soft delete: az storage account show --name NAME --query blobServiceProperties"
),
destructive_pattern!(
"sql-delete",
r"az\b.*?\bsql\s+(?:server|db)\s+delete",
"az sql server/db delete permanently destroys the database.",
Critical,
"sql server/db delete destroys databases:\n\n\
- Server delete removes ALL databases on server\n\
- Database delete removes specific database\n\
- Point-in-time restore possible within retention period\n\n\
Create backup: az sql db export --name DB --server SRV --storage-uri URI"
),
destructive_pattern!(
"group-delete",
r"az\b.*?\bgroup\s+delete",
"az group delete removes the entire resource group and ALL resources within it!",
Critical,
"group delete removes ENTIRE resource group:\n\n\
- ALL resources in the group deleted\n\
- VMs, storage, databases, networks - everything\n\
- Cannot be undone\n\
- --no-wait returns immediately (deletion continues)\n\n\
This is one of the most destructive Azure commands!"
),
destructive_pattern!(
"aks-delete",
r"az\b.*?\baks\s+delete",
"az aks delete removes the entire AKS cluster.",
Critical,
"aks delete removes the entire Kubernetes cluster:\n\n\
- All nodes and workloads terminated\n\
- Persistent volumes may be deleted\n\
- Load balancers and IPs released\n\
- Node resource group also deleted\n\n\
Backup workloads: kubectl get all -A -o yaml > backup.yaml"
),
destructive_pattern!(
"webapp-delete",
r"az\b.*?\bwebapp\s+delete",
"az webapp delete removes the App Service.",
High,
"webapp delete removes App Service:\n\n\
- Application code and configuration deleted\n\
- Custom domain mappings removed\n\
- SSL certificates may be deleted\n\
- Deployment slots also deleted\n\n\
Backup config: az webapp config show --name NAME -g RG"
),
destructive_pattern!(
"functionapp-delete",
r"az\b.*?\bfunctionapp\s+delete",
"az functionapp delete removes the Azure Function App.",
High,
"functionapp delete removes Azure Functions:\n\n\
- All functions and configuration deleted\n\
- Triggers and bindings removed\n\
- Function keys lost\n\
- Associated storage may be affected\n\n\
Export functions if not in version control."
),
destructive_pattern!(
"cosmosdb-delete",
r"az\b.*?\bcosmosdb\s+(?:delete|database\s+delete|collection\s+delete)",
"az cosmosdb delete permanently destroys the Cosmos DB resource.",
Critical,
"cosmosdb delete destroys Cosmos DB resources:\n\n\
- Account delete removes entire Cosmos DB account\n\
- Database delete removes database and all containers\n\
- Collection delete removes container and data\n\n\
Enable point-in-time restore for recovery options."
),
destructive_pattern!(
"keyvault-delete",
r"az\b.*?\bkeyvault\s+delete",
"az keyvault delete removes the Key Vault. Secrets may be unrecoverable.",
Critical,
"keyvault delete removes Key Vault:\n\n\
- All secrets, keys, certificates deleted\n\
- Soft delete allows recovery (if enabled)\n\
- Purge protection prevents permanent deletion\n\
- Vault name reserved for recovery period\n\n\
Check protection: az keyvault show --name NAME --query properties.enablePurgeProtection"
),
destructive_pattern!(
"vnet-delete",
r"az\b.*?\bnetwork\s+vnet\s+delete",
"az network vnet delete removes the virtual network.",
High,
"network vnet delete removes virtual network:\n\n\
- Network must be empty (no subnets in use)\n\
- Connected resources lose connectivity\n\
- Peerings to other VNets broken\n\
- Network security groups may remain\n\n\
Check usage: az network vnet subnet list --vnet-name VNET -g RG"
),
destructive_pattern!(
"acr-delete",
r"az\b.*?\bacr\s+delete",
"az acr delete removes the container registry and all images.",
Critical,
"acr delete removes entire container registry:\n\n\
- ALL repositories and images deleted\n\
- All tags and manifests gone\n\
- Webhooks and replications removed\n\
- Registry name becomes available to others\n\n\
List repos: az acr repository list --name REGISTRY"
),
destructive_pattern!(
"acr-repository-delete",
r"az\b.*?\bacr\s+repository\s+delete",
"az acr repository delete permanently deletes the repository and its images.",
High,
"acr repository delete removes repository:\n\n\
- All tags and images in repository deleted\n\
- Running containers unaffected (cached)\n\
- New pulls will fail\n\n\
List tags: az acr repository show-tags --name REG --repository REPO"
),
destructive_pattern!(
"acr-repository-untag",
r"az\b.*?\bacr\s+repository\s+untag",
"az acr repository untag removes tags from images.",
Medium,
"acr repository untag removes image tags:\n\n\
- Tag removed from manifest\n\
- Image still exists if other tags reference it\n\
- Untagged images may be garbage collected\n\n\
Lower risk: manifests can be re-tagged if digest known."
),
destructive_pattern!(
"keyvault-item-delete-or-purge",
r"az\b.*?\bkeyvault\s+(?:key|secret|certificate|storage)\s+(?:delete|purge)",
"Key Vault item delete/purge (az keyvault <key|secret|certificate|storage> …) — purge bypasses soft-delete and is irreversible.",
Critical,
"keyvault delete/purge on a Key Vault sub-resource:\n\n\
- delete: soft-delete (recoverable within retention window)\n\
- purge: PERMANENT; cannot be recovered; bypasses retention\n\
- Purging a KEK (Key-Encryption-Key) makes all data encrypted under it unrecoverable\n\
- Applications/services bound to the item fail immediately\n\n\
Restore soft-deleted items within retention:\n \
az keyvault <sub> recover --name NAME --vault-name VAULT\n\n\
Check purge protection:\n \
az keyvault show --name VAULT --query properties.enablePurgeProtection"
),
destructive_pattern!(
"role-assignment-delete",
r"az\b.*?\brole\s+assignment\s+delete",
"az role assignment delete removes an RBAC binding — users/SPs lose permissions.",
High,
"role assignment delete revokes an RBAC assignment:\n\n\
- The target user / service principal / group loses the role's permissions\n\
- Can cascade — CI/CD pipelines, workloads, operators lose access\n\
- Re-adding via `az role assignment create` is possible if IDs are known\n\n\
List bindings first:\n \
az role assignment list --assignee PRINCIPAL-ID"
),
destructive_pattern!(
"ad-sp-delete",
r"az\b.*?\bad\s+sp\s+delete",
"az ad sp delete removes a service principal — workloads using it lose auth.",
Critical,
"ad sp delete removes an Azure AD service principal:\n\n\
- All workloads authenticating via this SP lose access\n\
- SP credentials (client secrets, certs) are invalidated\n\
- Can soft-delete and restore within 30 days via Graph API\n\
- Associated app registration is NOT deleted (requires `az ad app delete`)\n\n\
Preview usages:\n \
az role assignment list --assignee SP-APP-ID"
),
destructive_pattern!(
"ad-app-delete",
r"az\b.*?\bad\s+app\s+delete",
"az ad app delete removes an Azure AD app registration — every service principal for it stops working.",
Critical,
"ad app delete removes the Azure AD application registration:\n\n\
- All service principals derived from this app break\n\
- OAuth grants to this app are invalidated\n\
- Associated federated credentials are deleted\n\
- Can restore within 30 days via Graph API soft-delete\n\n\
Consider disabling instead:\n \
az ad app update --id APP-ID --set disabledByMicrosoftStatus=None"
),
destructive_pattern!(
"network-dns-zone-delete",
r"az\b.*?\bnetwork\s+dns\s+zone\s+delete",
"az network dns zone delete removes an Azure DNS zone — domains stop resolving.",
Critical,
"network dns zone delete removes an Azure DNS zone:\n\n\
- All record sets in the zone are deleted\n\
- Domains delegated to this zone's nameservers stop resolving\n\
- Production traffic goes dark\n\
- No undelete\n\n\
Export records first:\n \
az network dns record-set list -g RG -z ZONE -o json > zone-backup.json"
),
destructive_pattern!(
"monitor-log-profiles-delete",
r"az\b.*?\bmonitor\s+log-profiles\s+delete",
"az monitor log-profiles delete removes a subscription activity-log export — compliance/forensics.",
High,
"monitor log-profiles delete stops exporting the Azure activity log:\n\n\
- Historical entries at the destination (Storage/Event Hub) are preserved\n\
- Future activity events stop flowing to the configured export\n\
- Compliance audit (SOC2, ISO 27001, FedRAMP) may require this profile\n\n\
Each subscription only supports one log profile at a time."
),
destructive_pattern!(
"cosmosdb-sql-container-delete",
r"az\b.*?\bcosmosdb\s+(?:sql|mongodb|cassandra|gremlin|table)\s+(?:database|container|keyspace|graph)\s+delete",
"az cosmosdb <api> <db|container|keyspace> delete permanently destroys Cosmos DB data.",
Critical,
"cosmosdb <api> delete destroys Cosmos DB SQL/Mongo/Cassandra/Gremlin/Table API resources:\n\n\
- Container/keyspace deletion removes ALL documents/rows inside\n\
- Point-in-time restore is only available if continuous backup is enabled\n\
- Periodic backups can restore within retention, with a new account name\n\n\
Enable continuous backup for safer recovery:\n \
az cosmosdb update --name ACCOUNT --resource-group RG \\\n \
--backup-policy-type Continuous"
),
]
}
#[cfg(test)]
mod tests {
use super::*;
use crate::packs::test_helpers::*;
#[test]
fn azure_safe_pattern_does_not_bypass_via_flag_value() {
let pack = create_pack();
assert_blocks(
&pack,
"az cosmosdb sql container delete --account-name prod --database-name orders --name ledger -g prod",
"Cosmos DB",
);
assert_blocks(
&pack,
"az vm delete --name prod-vm -g prod --no-wait",
"vm delete",
);
assert_allows(&pack, "az account list");
assert_allows(&pack, "az account show");
assert_allows(&pack, "az --subscription prod account list");
}
#[test]
fn azure_security_and_data_critical_services_blocked() {
let pack = create_pack();
assert_blocks(
&pack,
"az keyvault key delete --name prod-kek --vault-name prod-vault",
"Key Vault",
);
assert_blocks(
&pack,
"az keyvault secret delete --name prod-db-pw --vault-name prod-vault",
"Key Vault",
);
assert_blocks(
&pack,
"az keyvault key purge --name prod-kek --vault-name prod-vault",
"Key Vault",
);
assert_blocks(
&pack,
"az keyvault certificate purge --name prod-tls --vault-name prod-vault",
"Key Vault",
);
assert_blocks(
&pack,
"az role assignment delete --assignee user@corp.com --role Contributor --scope /subscriptions/X",
"RBAC",
);
assert_blocks(
&pack,
"az ad sp delete --id 11111111-2222-3333-4444-555555555555",
"service principal",
);
assert_blocks(
&pack,
"az ad app delete --id 11111111-2222-3333-4444-555555555555",
"app registration",
);
assert_blocks(
&pack,
"az network dns zone delete --name prod.example.com --resource-group prod",
"DNS zone",
);
assert_blocks(
&pack,
"az monitor log-profiles delete --name prod-audit",
"activity-log",
);
assert_blocks(
&pack,
"az cosmosdb sql container delete --account-name prod --database-name orders --name ledger -g prod",
"Cosmos DB",
);
assert_blocks(
&pack,
"az --subscription prod keyvault key purge --name k --vault-name v",
"Key Vault",
);
}
#[test]
fn azure_patterns_match_with_global_flags() {
let pack = create_pack();
assert_blocks(
&pack,
"az --subscription prod vm delete --name prod-vm --resource-group prod",
"vm delete",
);
assert_blocks(
&pack,
"az --debug --subscription prod group delete --name prod-rg --yes",
"resource group",
);
assert_blocks(
&pack,
"az --output json --subscription prod aks delete --name prod-aks --resource-group prod",
"AKS cluster",
);
assert_blocks(
&pack,
"az --only-show-errors --subscription prod keyvault delete --name prod-vault",
"Key Vault",
);
}
#[test]
fn acr_patterns_block() {
let pack = create_pack();
assert_blocks(&pack, "az acr delete --name myregistry", "acr delete");
assert_blocks(
&pack,
"az acr repository delete --name myregistry --image repo:tag",
"repository delete",
);
assert_blocks(
&pack,
"az acr repository untag --name myregistry --image repo:tag",
"repository untag",
);
}
}