Skip to content

Commit 433baea

Browse files
authored
Add files via upload
1 parent 888752b commit 433baea

2 files changed

Lines changed: 479 additions & 0 deletions

File tree

Lines changed: 275 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,275 @@
1+
#requires -Version 7.0
2+
<#
3+
.SYNOPSIS
4+
Audits Azure Storage accounts and blob container ACLs for anonymous public access.
5+
6+
.DESCRIPTION
7+
Performs a read-only audit across accessible Azure subscriptions. The script reads
8+
storage account settings through Azure Resource Manager, enumerates container ACLs
9+
with Get-AzRmStorageContainer, classifies effective anonymous access, and exports
10+
account-level and container-level CSV reports. It does not request storage keys,
11+
generate SAS tokens, or modify Azure resources.
12+
13+
.PARAMETER StorageAccountName
14+
Optional storage account names to audit. When omitted, all storage accounts in the
15+
selected subscription scope are audited.
16+
17+
.PARAMETER CurrentSubscriptionOnly
18+
Audits only the currently selected Azure subscription.
19+
20+
.PARAMETER OutputDirectory
21+
Directory in which CSV reports are written.
22+
23+
.EXAMPLE
24+
./Get-AzureStorageAnonymousAccess.ps1
25+
26+
.EXAMPLE
27+
./Get-AzureStorageAnonymousAccess.ps1 -StorageAccountName acmepublicassets,acmeappstorage
28+
29+
.EXAMPLE
30+
./Get-AzureStorageAnonymousAccess.ps1 -CurrentSubscriptionOnly -OutputDirectory ./audit-output
31+
#>
32+
33+
[CmdletBinding()]
34+
param(
35+
[Parameter()]
36+
[ValidatePattern('^[a-z0-9]{3,24}$')]
37+
[string[]]$StorageAccountName,
38+
39+
[Parameter()]
40+
[switch]$CurrentSubscriptionOnly,
41+
42+
[Parameter()]
43+
[string]$OutputDirectory = (Join-Path $PWD 'AzureStorageAnonymousAccessAudit')
44+
)
45+
46+
Set-StrictMode -Version Latest
47+
$ErrorActionPreference = 'Stop'
48+
49+
foreach ($commandName in @(
50+
'Connect-AzAccount',
51+
'Get-AzContext',
52+
'Get-AzSubscription',
53+
'Set-AzContext',
54+
'Get-AzStorageAccount',
55+
'Get-AzRmStorageContainer'
56+
)) {
57+
if (-not (Get-Command $commandName -ErrorAction SilentlyContinue)) {
58+
throw "Required command '$commandName' was not found. Install or update Az.Accounts and Az.Storage."
59+
}
60+
}
61+
62+
if (-not (Get-AzContext -ErrorAction SilentlyContinue)) {
63+
Connect-AzAccount | Out-Null
64+
}
65+
66+
New-Item -Path $OutputDirectory -ItemType Directory -Force | Out-Null
67+
68+
$rawSubscriptions = if ($CurrentSubscriptionOnly) {
69+
@((Get-AzContext).Subscription)
70+
}
71+
else {
72+
@(Get-AzSubscription | Where-Object State -eq 'Enabled')
73+
}
74+
75+
$subscriptions = @(
76+
foreach ($subscription in $rawSubscriptions) {
77+
$subscriptionId = if (-not [string]::IsNullOrWhiteSpace([string]$subscription.Id)) {
78+
[string]$subscription.Id
79+
}
80+
elseif (-not [string]::IsNullOrWhiteSpace([string]$subscription.SubscriptionId)) {
81+
[string]$subscription.SubscriptionId
82+
}
83+
else {
84+
$null
85+
}
86+
87+
if ([string]::IsNullOrWhiteSpace($subscriptionId)) {
88+
Write-Warning "Skipping subscription '$($subscription.Name)' because no subscription ID was returned."
89+
continue
90+
}
91+
92+
[pscustomobject]@{
93+
Name = [string]$subscription.Name
94+
Id = $subscriptionId
95+
}
96+
}
97+
)
98+
99+
if ($subscriptions.Count -eq 0) {
100+
throw 'No accessible Azure subscriptions with valid subscription IDs were returned.'
101+
}
102+
103+
$requestedNames = @{}
104+
foreach ($name in @($StorageAccountName)) {
105+
if (-not [string]::IsNullOrWhiteSpace($name)) {
106+
$requestedNames[$name.ToLowerInvariant()] = $true
107+
}
108+
}
109+
110+
$accountResults = [System.Collections.Generic.List[object]]::new()
111+
$containerResults = [System.Collections.Generic.List[object]]::new()
112+
$foundNames = @{}
113+
114+
Write-Host "Auditing Azure Storage across $($subscriptions.Count) subscription(s)..." -ForegroundColor Cyan
115+
116+
foreach ($subscription in $subscriptions) {
117+
try {
118+
Set-AzContext -SubscriptionId $subscription.Id -ErrorAction Stop | Out-Null
119+
$accounts = @(Get-AzStorageAccount -ErrorAction Stop)
120+
}
121+
catch {
122+
Write-Warning "Could not enumerate subscription '$($subscription.Name)': $($_.Exception.Message)"
123+
continue
124+
}
125+
126+
foreach ($account in $accounts) {
127+
$accountName = [string]$account.StorageAccountName
128+
$accountKey = $accountName.ToLowerInvariant()
129+
130+
if ($requestedNames.Count -gt 0 -and -not $requestedNames.ContainsKey($accountKey)) {
131+
continue
132+
}
133+
134+
$foundNames[$accountKey] = $true
135+
$allowBlobPublicAccess = $account.AllowBlobPublicAccess
136+
$publicNetworkAccess = if ($null -eq $account.PublicNetworkAccess) { 'NotSet' } else { [string]$account.PublicNetworkAccess }
137+
$networkDefaultAction = if ($null -eq $account.NetworkRuleSet.DefaultAction) { 'NotSet' } else { [string]$account.NetworkRuleSet.DefaultAction }
138+
$containers = @()
139+
$enumerationStatus = 'Succeeded'
140+
$enumerationError = $null
141+
142+
try {
143+
$containers = @(
144+
Get-AzRmStorageContainer `
145+
-ResourceGroupName $account.ResourceGroupName `
146+
-StorageAccountName $accountName `
147+
-ErrorAction Stop
148+
)
149+
}
150+
catch {
151+
$enumerationStatus = 'Failed'
152+
$enumerationError = $_.Exception.Message
153+
}
154+
155+
$publicContainerCount = 0
156+
157+
foreach ($container in $containers) {
158+
$publicAccess = if ([string]::IsNullOrWhiteSpace([string]$container.PublicAccess)) {
159+
'None'
160+
}
161+
else {
162+
[string]$container.PublicAccess
163+
}
164+
165+
$isPublicAcl = $publicAccess -in @('Blob', 'Container')
166+
$effectiveAnonymousRead = ($allowBlobPublicAccess -eq $true) -and $isPublicAcl
167+
$anonymousEnumeration = ($allowBlobPublicAccess -eq $true) -and ($publicAccess -eq 'Container')
168+
169+
if ($effectiveAnonymousRead) {
170+
$publicContainerCount++
171+
}
172+
173+
$containerResults.Add([pscustomobject]@{
174+
SubscriptionName = $subscription.Name
175+
SubscriptionId = $subscription.Id
176+
ResourceGroup = $account.ResourceGroupName
177+
StorageAccountName = $accountName
178+
ContainerName = [string]$container.Name
179+
ContainerPublicAccess = $publicAccess
180+
AllowBlobPublicAccess = $allowBlobPublicAccess
181+
PublicNetworkAccess = $publicNetworkAccess
182+
EffectiveAnonymousRead = $effectiveAnonymousRead
183+
AnonymousEnumeration = $anonymousEnumeration
184+
BlobEndpoint = "https://${accountName}.blob.core.windows.net/$($container.Name)/"
185+
})
186+
}
187+
188+
$risk = if ($enumerationStatus -eq 'Failed') {
189+
'Unknown-ContainerAclUnavailable'
190+
}
191+
elseif ($allowBlobPublicAccess -eq $false) {
192+
'BlockedAtAccountLevel'
193+
}
194+
elseif ($allowBlobPublicAccess -eq $true -and $publicContainerCount -gt 0) {
195+
'ConfirmedAnonymousAccessByConfiguration'
196+
}
197+
elseif ($allowBlobPublicAccess -eq $true) {
198+
'PermittedAtAccountNoPublicContainerFound'
199+
}
200+
else {
201+
'Review-AccountSettingNotExplicit'
202+
}
203+
204+
$accountResults.Add([pscustomobject]@{
205+
SubscriptionName = $subscription.Name
206+
SubscriptionId = $subscription.Id
207+
ResourceGroup = $account.ResourceGroupName
208+
StorageAccountName = $accountName
209+
Location = [string]$account.Location
210+
Kind = [string]$account.Kind
211+
AllowBlobPublicAccess = $allowBlobPublicAccess
212+
PublicNetworkAccess = $publicNetworkAccess
213+
NetworkDefaultAction = $networkDefaultAction
214+
ContainerEnumeration = $enumerationStatus
215+
TotalContainers = if ($enumerationStatus -eq 'Succeeded') { $containers.Count } else { $null }
216+
PublicContainers = if ($enumerationStatus -eq 'Succeeded') { $publicContainerCount } else { $null }
217+
EffectiveAnonymousBlobRisk = $risk
218+
Error = $enumerationError
219+
})
220+
}
221+
}
222+
223+
foreach ($name in $requestedNames.Keys) {
224+
if (-not $foundNames.ContainsKey($name)) {
225+
$accountResults.Add([pscustomobject]@{
226+
SubscriptionName = $null
227+
SubscriptionId = $null
228+
ResourceGroup = $null
229+
StorageAccountName = $name
230+
Location = $null
231+
Kind = $null
232+
AllowBlobPublicAccess = $null
233+
PublicNetworkAccess = $null
234+
NetworkDefaultAction = $null
235+
ContainerEnumeration = 'NotRun'
236+
TotalContainers = $null
237+
PublicContainers = $null
238+
EffectiveAnonymousBlobRisk = 'Unknown-NotFoundOrNoRBAC'
239+
Error = 'The account was not found in accessible subscriptions.'
240+
})
241+
}
242+
}
243+
244+
$timestamp = Get-Date -Format 'yyyyMMdd-HHmmss'
245+
$accountCsv = Join-Path $OutputDirectory "StorageAccountAnonymousAccessSummary-$timestamp.csv"
246+
$containerCsv = Join-Path $OutputDirectory "StorageContainerPublicAccessDetails-$timestamp.csv"
247+
248+
$accountResults |
249+
Sort-Object EffectiveAnonymousBlobRisk, SubscriptionName, StorageAccountName |
250+
Export-Csv -Path $accountCsv -NoTypeInformation -Encoding utf8
251+
252+
$containerResults |
253+
Sort-Object @{ Expression = 'EffectiveAnonymousRead'; Descending = $true }, SubscriptionName, StorageAccountName, ContainerName |
254+
Export-Csv -Path $containerCsv -NoTypeInformation -Encoding utf8
255+
256+
$confirmedAccounts = @($accountResults | Where-Object EffectiveAnonymousBlobRisk -eq 'ConfirmedAnonymousAccessByConfiguration')
257+
$publicContainers = @($containerResults | Where-Object EffectiveAnonymousRead -eq $true)
258+
$enumerableContainers = @($containerResults | Where-Object AnonymousEnumeration -eq $true)
259+
$unknownAccounts = @($accountResults | Where-Object EffectiveAnonymousBlobRisk -like 'Unknown-*')
260+
261+
Write-Host ''
262+
Write-Host '=== Azure Storage Anonymous Access Audit ===' -ForegroundColor Cyan
263+
Write-Host "Accounts audited : $($accountResults.Count)"
264+
Write-Host "Accounts with anonymous access : $($confirmedAccounts.Count)" -ForegroundColor $(if ($confirmedAccounts.Count -gt 0) { 'Red' } else { 'Green' })
265+
Write-Host "Containers with anonymous reads : $($publicContainers.Count)" -ForegroundColor $(if ($publicContainers.Count -gt 0) { 'Red' } else { 'Green' })
266+
Write-Host "Anonymously enumerable containers: $($enumerableContainers.Count)" -ForegroundColor $(if ($enumerableContainers.Count -gt 0) { 'Red' } else { 'Green' })
267+
Write-Host "Unknown or inaccessible accounts : $($unknownAccounts.Count)" -ForegroundColor Yellow
268+
Write-Host ''
269+
270+
$accountResults |
271+
Format-Table StorageAccountName, AllowBlobPublicAccess, PublicNetworkAccess, TotalContainers, PublicContainers, EffectiveAnonymousBlobRisk -AutoSize
272+
273+
Write-Host "Account report : $accountCsv" -ForegroundColor Cyan
274+
Write-Host "Container report: $containerCsv" -ForegroundColor Cyan
275+
Write-Host 'Read-only audit complete. No Azure settings were changed.' -ForegroundColor Green

0 commit comments

Comments
 (0)