npx skills add mukul975/Anthropic-Cybersecurity-SkillsMITRE ATT&CK
NIST CSF 2.0
MITRE ATLAS
When to Use
- When performing a security audit of Azure Storage accounts across subscriptions
- When responding to Microsoft Defender for Storage alerts about anonymous access or data exfiltration
- When compliance requires verification of encryption, network restrictions, and access logging
- When investigating potential data exposure through publicly accessible blob containers
- When onboarding Azure subscriptions and establishing storage security baselines
Do not use for Azure SQL or Cosmos DB security auditing (use dedicated database security tools), for real-time threat detection on storage operations (use Defender for Storage), or for Azure Files or Data Lake Gen2 specific auditing without adapting the checks.
Prerequisites
- Azure CLI installed and authenticated (
az login) with Reader and Storage Account Contributor roles - Az PowerShell module installed for advanced queries (
Install-Module Az.Storage) - Microsoft Defender for Storage enabled for threat detection
- Access to Azure Resource Graph for cross-subscription queries
- ScoutSuite or Prowler Azure provider for automated assessment
Workflow
Step 1: Enumerate All Storage Accounts and Basic Configuration
List all storage accounts across subscriptions and assess their baseline security settings.
# List all storage accounts across all subscriptions
az storage account list \
--query "[].{Name:name, ResourceGroup:resourceGroup, Location:location, Kind:kind, Sku:sku.name, HttpsOnly:enableHttpsTrafficOnly, MinTLS:minimumTlsVersion, PublicAccess:allowBlobPublicAccess}" \
-o table
# Use Resource Graph for cross-subscription enumeration
az graph query -q "
Resources
| where type == 'microsoft.storage/storageaccounts'
| project name, resourceGroup, subscriptionId, location,
properties.allowBlobPublicAccess,
properties.enableHttpsTrafficOnly,
properties.minimumTlsVersion,
properties.networkAcls.defaultAction
" -o tableStep 2: Detect Publicly Accessible Blob Containers
Identify storage accounts and containers allowing anonymous public access to blob data.
# Check each storage account for public blob access setting
for account in $(az storage account list --query "[].name" -o tsv); do
public=$(az storage account show --name "$account" --query "allowBlobPublicAccess" -o tsv)
echo "$account: allowBlobPublicAccess=$public"
done
# List containers with public access level set
for account in $(az storage account list --query "[?allowBlobPublicAccess==true].name" -o tsv); do
key=$(az storage account keys list --account-name "$account" --query "[0].value" -o tsv)
echo "=== $account ==="
az storage container list \
--account-name "$account" \
--account-key "$key" \
--query "[?properties.publicAccess!='off' && properties.publicAccess!=null].{Container:name, PublicAccess:properties.publicAccess}" \
-o table 2>/dev/null
done
# Test anonymous access to discovered public containers
curl -s "https://ACCOUNT.blob.core.windows.net/CONTAINER?restype=container&comp=list" | head -50Step 3: Audit Network Access and Firewall Rules
Check for storage accounts accessible from all networks versus those restricted to specific VNets or IP ranges.
# Find storage accounts with default network action set to Allow (open to all networks)
az storage account list \
--query "[?networkRuleSet.defaultAction=='Allow'].{Name:name, DefaultAction:networkRuleSet.defaultAction, VNetRules:networkRuleSet.virtualNetworkRules}" \
-o table
# Detailed network rule audit
for account in $(az storage account list --query "[].name" -o tsv); do
echo "=== $account ==="
az storage account show --name "$account" \
--query "{DefaultAction:networkRuleSet.defaultAction, IPRules:networkRuleSet.ipRules[*].ipAddressOrRange, VNetRules:networkRuleSet.virtualNetworkRules[*].virtualNetworkResourceId, Bypass:networkRuleSet.bypass}" \
-o json
done
# Find storage accounts with private endpoints
az network private-endpoint list \
--query "[?privateLinkServiceConnections[0].groupIds[0]=='blob'].{Name:name, Storage:privateLinkServiceConnections[0].privateLinkServiceId}" \
-o tableStep 4: Verify Encryption Settings and Key Management
Ensure all storage accounts use encryption at rest with appropriate key management (Microsoft-managed or customer-managed keys).
# Check encryption configuration for all storage accounts
for account in $(az storage account list --query "[].name" -o tsv); do
echo "=== $account ==="
az storage account show --name "$account" \
--query "{Encryption:encryption.services, KeySource:encryption.keySource, KeyVaultUri:encryption.keyVaultProperties.keyVaultUri, InfraEncryption:encryption.requireInfrastructureEncryption}" \
-o json
done
# Find accounts without infrastructure encryption (double encryption)
az storage account list \
--query "[?encryption.requireInfrastructureEncryption!=true].{Name:name, KeySource:encryption.keySource}" \
-o table
# Check for accounts using TLS version below 1.2
az storage account list \
--query "[?minimumTlsVersion!='TLS1_2'].{Name:name, TLS:minimumTlsVersion}" \
-o tableStep 5: Audit Shared Access Signatures and Access Keys
Identify overly permissive SAS tokens and check for access key usage patterns.
# Check when storage account keys were last rotated
for account in $(az storage account list --query "[].name" -o tsv); do
echo "=== $account ==="
az storage account keys list \
--account-name "$account" \
--query "[].{KeyName:keyName, CreationTime:creationTime}" \
-o table
done
# Check if storage account allows shared key access (should be disabled for AAD-only)
az storage account list \
--query "[].{Name:name, AllowSharedKeyAccess:allowSharedKeyAccess}" \
-o table
# Review stored access policies on containers (SAS governance)
for account in $(az storage account list --query "[].name" -o tsv); do
key=$(az storage account keys list --account-name "$account" --query "[0].value" -o tsv 2>/dev/null)
for container in $(az storage container list --account-name "$account" --account-key "$key" --query "[].name" -o tsv 2>/dev/null); do
policies=$(az storage container policy list --container-name "$container" --account-name "$account" --account-key "$key" 2>/dev/null)
[ -n "$policies" ] && echo "$account/$container: $policies"
done
doneStep 6: Check Diagnostic Logging and Monitoring
Verify that storage analytics logging and Azure Monitor diagnostic settings are enabled.
# Check diagnostic settings for storage accounts
for account in $(az storage account list --query "[].name" -o tsv); do
rg=$(az storage account show --name "$account" --query "resourceGroup" -o tsv)
echo "=== $account ==="
az monitor diagnostic-settings list \
--resource "/subscriptions/$(az account show --query id -o tsv)/resourceGroups/$rg/providers/Microsoft.Storage/storageAccounts/$account" \
--query "[].{Name:name, Logs:logs[*].category, Metrics:metrics[*].category}" \
-o json 2>/dev/null || echo " No diagnostic settings configured"
done
# Check blob service logging properties
for account in $(az storage account list --query "[].name" -o tsv); do
key=$(az storage account keys list --account-name "$account" --query "[0].value" -o tsv 2>/dev/null)
az storage logging show \
--account-name "$account" \
--account-key "$key" \
--services b 2>/dev/null
doneKey Concepts
| Term | Definition |
|---|---|
| Blob Public Access | Storage account setting that allows anonymous read access to blob containers and their contents without authentication |
| Shared Access Signature | Time-limited URI with embedded authentication tokens granting delegated access to Azure Storage resources with specific permissions |
| Network ACL Default Action | Storage firewall setting that determines whether traffic is allowed or denied by default, with exceptions for specified IPs and VNets |
| Customer-Managed Key | Encryption key stored in Azure Key Vault that the customer controls for storage encryption instead of Microsoft-managed keys |
| Stored Access Policy | Named policy on a container that defines SAS permissions, start/expiry times, and can be revoked independently of individual SAS tokens |
| Defender for Storage | Microsoft Defender plan providing threat detection for anomalous storage access patterns, malware uploads, and data exfiltration |
Tools & Systems
- Azure CLI: Primary tool for querying storage account configuration, containers, and access policies
- Azure Resource Graph: Cross-subscription query engine for efficient enumeration of storage security settings at scale
- Microsoft Defender for Storage: Threat detection service identifying anomalous access patterns and potential data exfiltration
- Prowler Azure: Open-source tool with automated storage security checks aligned to CIS Azure Foundations
- ScoutSuite: Multi-cloud auditing tool with Azure storage-specific checks for public access, encryption, and networking
Common Scenarios
Scenario: Detecting a Storage Account Exposed by a Developer Misconfiguration
Context: A developer creates a storage account for a web application and enables blob public access to serve static files. They accidentally store API keys and database connection strings in a publicly accessible container.
Approach:
- Run
az storage account listfiltering forallowBlobPublicAccess=true - Enumerate containers with public access level set to
bloborcontainer - List contents of public containers to identify sensitive files
- Check Defender for Storage alerts for anomalous access from unexpected IPs
- Immediately set
allowBlobPublicAccesstofalseon the storage account - Rotate any exposed credentials found in public containers
- Enable network ACLs restricting access to the application VNet
- Configure Azure CDN or Front Door for legitimate public content delivery
Pitfalls: Disabling blob public access immediately breaks applications serving content publicly. Coordinate with the development team and implement Azure CDN before disabling public access. SAS tokens generated before a key rotation remain valid until expiry unless the underlying storage key is regenerated.
Output Format
Azure Storage Security Audit Report
======================================
Subscription: Production (SUB-ID)
Assessment Date: 2026-02-23
Storage Accounts Audited: 24
CRITICAL FINDINGS:
[STOR-001] Public Blob Access Enabled
Account: webapp-static-prod
Container: uploads (PublicAccess: blob)
Risk: Anonymous users can read all blobs in the container
Contents: 1,247 files including .env and config.json
Remediation: Disable allowBlobPublicAccess, use Azure CDN with SAS
[STOR-002] Storage Account Open to All Networks
Account: data-lake-analytics
Default Action: Allow (no network restrictions)
Risk: Accessible from any network including the internet
Remediation: Set default action to Deny, add VNet rules
SUMMARY:
Public blob access enabled: 3 / 24
Open to all networks: 8 / 24
Missing infrastructure encryption: 12 / 24
TLS version below 1.2: 2 / 24
No diagnostic logging: 10 / 24
Shared key access enabled: 18 / 24
Keys not rotated in 90+ days: 14 / 24References and resources
Everything below is rendered for inspection. Script files are read-only and never run.
References 1
api-reference.md2.6 KB
Azure Storage Misconfiguration Detection API Reference
Azure CLI - Storage Account Enumeration
# List all storage accounts
az storage account list --query "[].{name:name, rg:resourceGroup, https:enableHttpsTrafficOnly, tls:minimumTlsVersion, publicAccess:allowBlobPublicAccess}" -o table
# Show account details
az storage account show --name mystorageacct --resource-group myrg
# Resource Graph cross-subscription query
az graph query -q "Resources | where type == 'microsoft.storage/storageaccounts' | project name, properties.allowBlobPublicAccess, properties.minimumTlsVersion"Container Access Level Checks
# List containers with access levels
az storage container list --account-name mystorageacct \
--query "[].{name:name, access:properties.publicAccess}" -o table
# Set container to private
az storage container set-permission --name mycontainer \
--account-name mystorageacct --public-access offNetwork Rules
# Show network rules
az storage account network-rule list --account-name mystorageacct --resource-group myrg
# Set default action to Deny
az storage account update --name mystorageacct --resource-group myrg \
--default-action Deny
# Add IP rule
az storage account network-rule add --account-name mystorageacct \
--resource-group myrg --ip-address 203.0.113.0/24Security Settings
# Enforce HTTPS only
az storage account update --name mystorageacct -g myrg --https-only true
# Set minimum TLS 1.2
az storage account update --name mystorageacct -g myrg --min-tls-version TLS1_2
# Disable public blob access
az storage account update --name mystorageacct -g myrg --allow-blob-public-access false
# Enable soft delete
az storage blob service-properties delete-policy update \
--account-name mystorageacct --enable true --days-retained 14Azure Storage Security Checklist
| Check | CLI Command | Expected |
|---|---|---|
| HTTPS only | show --query enableHttpsTrafficOnly |
true |
| TLS 1.2+ | show --query minimumTlsVersion |
TLS1_2 |
| No public access | show --query allowBlobPublicAccess |
false |
| Network deny default | network-rule list |
defaultAction: Deny |
| Logging enabled | storage logging show |
All services enabled |
| Soft delete on | blob service-properties |
Enabled 7-14 days |
Defender for Storage Alerts
| Alert | Description |
|---|---|
| Anonymous access to storage | Unauthenticated blob access |
| Unusual data extraction | Anomalous download volume |
| Access from Tor exit node | Storage access from Tor |
| Unusual access pattern | Access from unexpected location |
Scripts 1
agent.py6.7 KB
#!/usr/bin/env python3
"""Azure Storage misconfiguration detection agent using Azure CLI."""
import json
import subprocess
import sys
from datetime import datetime
def az_cli(args):
"""Execute Azure CLI command and return parsed JSON."""
cmd = ["az"] + args + ["--output", "json"]
try:
result = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
if result.returncode == 0 and result.stdout.strip():
return json.loads(result.stdout)
return {"error": result.stderr.strip()} if result.returncode != 0 else {}
except Exception as e:
return {"error": str(e)}
def list_storage_accounts():
"""List all storage accounts with security-relevant properties."""
return az_cli([
"storage", "account", "list",
"--query", "[].{name:name, resourceGroup:resourceGroup, location:location, "
"httpsOnly:enableHttpsTrafficOnly, minTls:minimumTlsVersion, "
"publicAccess:allowBlobPublicAccess, kind:kind, sku:sku.name}",
])
def check_public_containers(account_name, account_key=None):
"""Check for publicly accessible blob containers in a storage account."""
args = ["storage", "container", "list", "--account-name", account_name,
"--query", "[].{name:name, publicAccess:properties.publicAccess}"]
if account_key:
args.extend(["--account-key", account_key])
result = az_cli(args)
if isinstance(result, list):
public = [c for c in result if c.get("publicAccess") and c["publicAccess"] != "None"]
return {
"account": account_name,
"total_containers": len(result),
"public_containers": public,
"public_count": len(public),
}
return result
def check_encryption_settings(account_name, resource_group):
"""Verify encryption configuration for a storage account."""
result = az_cli([
"storage", "account", "show",
"--name", account_name, "--resource-group", resource_group,
"--query", "{encryption:encryption, httpsOnly:enableHttpsTrafficOnly, "
"minTls:minimumTlsVersion, keySource:encryption.keySource}",
])
return result
def check_network_rules(account_name, resource_group):
"""Check network access rules for a storage account."""
result = az_cli([
"storage", "account", "show",
"--name", account_name, "--resource-group", resource_group,
"--query", "{defaultAction:networkRuleSet.defaultAction, "
"bypass:networkRuleSet.bypass, "
"ipRules:networkRuleSet.ipRules, "
"virtualNetworkRules:networkRuleSet.virtualNetworkRules}",
])
issues = []
if isinstance(result, dict):
if result.get("defaultAction") == "Allow":
issues.append("Default network action is Allow (should be Deny)")
if not result.get("ipRules") and not result.get("virtualNetworkRules"):
if result.get("defaultAction") == "Allow":
issues.append("No IP or VNet restrictions configured")
return {"account": account_name, "network_rules": result, "issues": issues}
def check_logging_enabled(account_name, account_key=None):
"""Check if storage analytics logging is enabled."""
args = ["storage", "logging", "show", "--account-name", account_name, "--services", "bqt"]
if account_key:
args.extend(["--account-key", account_key])
return az_cli(args)
def check_soft_delete(account_name, resource_group):
"""Check if soft delete is enabled for blobs and containers."""
result = az_cli([
"storage", "account", "blob-service-properties", "show",
"--account-name", account_name, "--resource-group", resource_group,
"--query", "{deleteRetention:deleteRetentionPolicy, "
"containerDeleteRetention:containerDeleteRetentionPolicy, "
"versioning:isVersioningEnabled}",
])
return result
def audit_storage_account(account_name, resource_group):
"""Run full security audit on a single storage account."""
findings = []
account = az_cli([
"storage", "account", "show",
"--name", account_name, "--resource-group", resource_group,
])
if isinstance(account, dict) and "error" not in account:
if not account.get("enableHttpsTrafficOnly"):
findings.append({"severity": "HIGH", "finding": "HTTPS-only not enforced"})
tls = account.get("minimumTlsVersion", "")
if tls and tls < "TLS1_2":
findings.append({"severity": "HIGH", "finding": f"Minimum TLS version is {tls} (should be TLS1_2)"})
if account.get("allowBlobPublicAccess"):
findings.append({"severity": "CRITICAL", "finding": "Public blob access is enabled at account level"})
network = check_network_rules(account_name, resource_group)
if network.get("issues"):
for issue in network["issues"]:
findings.append({"severity": "HIGH", "finding": issue})
return {
"account": account_name,
"resource_group": resource_group,
"findings": findings,
"finding_count": len(findings),
"timestamp": datetime.utcnow().isoformat() + "Z",
}
def audit_all_accounts():
"""Audit all storage accounts across the subscription."""
accounts = list_storage_accounts()
if not isinstance(accounts, list):
return accounts
results = []
for acct in accounts:
name = acct.get("name")
rg = acct.get("resourceGroup")
if name and rg:
audit = audit_storage_account(name, rg)
results.append(audit)
total_findings = sum(r.get("finding_count", 0) for r in results)
critical = sum(
1 for r in results
for f in r.get("findings", [])
if f.get("severity") == "CRITICAL"
)
return {
"timestamp": datetime.utcnow().isoformat() + "Z",
"accounts_audited": len(results),
"total_findings": total_findings,
"critical_findings": critical,
"results": results,
}
if __name__ == "__main__":
action = sys.argv[1] if len(sys.argv) > 1 else "audit-all"
if action == "audit-all":
print(json.dumps(audit_all_accounts(), indent=2, default=str))
elif action == "audit" and len(sys.argv) > 3:
print(json.dumps(audit_storage_account(sys.argv[2], sys.argv[3]), indent=2))
elif action == "list":
print(json.dumps(list_storage_accounts(), indent=2))
elif action == "public" and len(sys.argv) > 2:
print(json.dumps(check_public_containers(sys.argv[2]), indent=2))
elif action == "network" and len(sys.argv) > 3:
print(json.dumps(check_network_rules(sys.argv[2], sys.argv[3]), indent=2))
else:
print("Usage: agent.py [audit-all|audit <name> <rg>|list|public <name>|network <name> <rg>]")