Public/Get-AACResourceLock.ps1
|
function Get-AACResourceLock { <# .EXTERNALHELP Azure.Admin.Console-help.xml .SYNOPSIS Lists every resource lock - on subscriptions, resource groups and resources - and the critical resources no lock protects, with ReadOnly locks that break more than they protect flagged. .DESCRIPTION Reads, read-only: each subscription's locks (Resource Manager - a subscription's list includes those on its resource groups and resources), and from Resource Graph its resource groups and the resources of the critical types. Rows (AAC.ResourceLock): Locked each lock: level (CanNotDelete or ReadOnly), scope, notes. ReadOnly is a finding - Medium on a subscription or resource group, where it also blocks listing keys, scaling and many portal pages; Low on a resource Unlocked each critical resource with no lock on it, its resource group or its subscription (Low) A lock covers its scope and everything below it. The headline says how many critical resources are locked directly, through a parent, or not at all, and how many resource groups a lock covers. The critical types (-CriticalType to choose your own): key vaults, storage accounts, SQL servers, Cosmos DB, PostgreSQL and MySQL flexible servers, Recovery Services and backup vaults, virtual networks, ExpressRoute circuits, Azure Firewalls, public and private DNS zones, Log Analytics workspaces. Reader is enough. .PARAMETER SubscriptionId Only these subscriptions. .PARAMETER ManagementGroupId Only the subscriptions under these management groups (at any depth). .PARAMETER CriticalType The resource types that should be locked (Resource Graph type names, such as 'microsoft.keyvault/vaults'), instead of the default list. .PARAMETER Status Only these: Locked, Unlocked. .PARAMETER CsvPath Write the locks to this CSV file. .PARAMETER HtmlPath Write an interactive HTML report. .PARAMETER PdfPath Write a PDF report. .PARAMETER Title The reports' title. .PARAMETER PassThru Show the view and also return the rows. .PARAMETER NoDisplay Return the rows without showing the view. .PARAMETER NoPaging Show the whole view at once. .EXAMPLE Get-AACResourceLock Every lock, and the critical resources left unlocked. .EXAMPLE Get-AACResourceLock -Status Unlocked -NoDisplay | Select-Object Scope, ResourceType The critical resources nothing protects from deletion. .EXAMPLE Get-AACResourceLock -CriticalType 'microsoft.keyvault/vaults', 'microsoft.recoveryservices/vaults' -HtmlPath .\Locks.html Locks, judged against your own list of critical types. .OUTPUTS AAC.ResourceLock #> [CmdletBinding()] [OutputType('AAC.ResourceLock')] param( [ValidatePattern('^[0-9a-fA-F]{8}(-[0-9a-fA-F]{4}){3}-[0-9a-fA-F]{12}$')] [string[]] $SubscriptionId, [string[]] $ManagementGroupId, [ValidatePattern('^[A-Za-z0-9.]+/[A-Za-z0-9/]+$')] [string[]] $CriticalType = @('microsoft.keyvault/vaults', 'microsoft.storage/storageaccounts', 'microsoft.sql/servers', 'microsoft.documentdb/databaseaccounts', 'microsoft.dbforpostgresql/flexibleservers', 'microsoft.dbformysql/flexibleservers', 'microsoft.recoveryservices/vaults', 'microsoft.dataprotection/backupvaults', 'microsoft.network/virtualnetworks', 'microsoft.network/expressroutecircuits', 'microsoft.network/azurefirewalls', 'microsoft.network/dnszones', 'microsoft.network/privatednszones', 'microsoft.operationalinsights/workspaces'), [ValidateSet('Locked', 'Unlocked')] [string[]] $Status, [string] $CsvPath, [string] $HtmlPath, [string] $PdfPath, [string] $Title = 'Resource locks', [switch] $PassThru, [switch] $NoDisplay, [switch] $NoPaging ) trap { if ($_.Exception -is [System.Management.Automation.PipelineStoppedException]) { return }; $PSCmdlet.ThrowTerminatingError((Show-AACError -ErrorRecord $_ -Cmdlet $PSCmdlet)) } $pipedOnward = $MyInvocation.PipelinePosition -lt $MyInvocation.PipelineLength $interactive = -not $NoDisplay -and -not $pipedOnward $resolve = { param([string] $Path) if ($Path) { $PSCmdlet.SessionState.Path.GetUnresolvedProviderPathFromPSPath($Path) } } $request = @{ SubscriptionId = @($SubscriptionId | Where-Object { $_ }); ManagementGroupId = @($ManagementGroupId | Where-Object { $_ }); CriticalType = @($CriticalType | ForEach-Object { $_.ToLowerInvariant() } | Select-Object -Unique) } $null = Get-AACAccessToken if ($interactive) { Write-AACRule -Title 'Azure Admin Console :: Resource locks' -Color 'orange1' } $outcome = Invoke-AACProgress -ScriptBlock { Update-AACProgress -Id 'scope' -Indeterminate -Description 'Finding the subscriptions' $scope = Resolve-AACScope -SubscriptionId $request.SubscriptionId -ManagementGroupId $request.ManagementGroupId Update-AACProgress -Id 'scope' -Complete -Description "Scope: $($scope.Label)" $types = ($request.CriticalType | ForEach-Object { "'$_'" }) -join ', ' Update-AACProgress -Id 'inventory' -Indeterminate -Description 'Reading the resource groups and critical resources' $inventory = Invoke-AACGraphBatch -Query ([ordered]@{ groups = "resourcecontainers | where type =~ 'microsoft.resources/subscriptions/resourcegroups' | project id = tolower(id), name, subscriptionId = tolower(subscriptionId)" resources = "resources | where type in~ ($types) | project id = tolower(id), name, type = tolower(type), resourceGroup, subscriptionId = tolower(subscriptionId)" }) -SubscriptionId $scope.GraphScope Update-AACProgress -Id 'inventory' -Complete -Description ('Read {0:N0} resource group(s) and {1:N0} critical resource(s)' -f @($inventory.Rows['groups']).Count, @($inventory.Rows['resources']).Count) $uris = [ordered]@{} foreach ($id in $scope.Ids) { $uris[$id] = "/subscriptions/$id/providers/Microsoft.Authorization/locks?api-version=2016-09-01" } Update-AACProgress -Id 'locks' -Total ([Math]::Max(1, $uris.Count)) -Description "Reading the locks of $($scope.Ids.Count) subscription(s)" $answers = Invoke-AACArmParallel -Uri @($uris.Values) -OnProgress { param($LockDone, $LockTotal) Update-AACProgress -Id 'locks' -Increment 1 } $locks = [System.Collections.Generic.List[object]]::new() $unread = [System.Collections.Generic.List[string]]::new() foreach ($id in $uris.Keys) { $answer = $answers[$uris[$id]] if (-not $answer -or $answer.Error) { $unread.Add("$($scope.Names[$id.ToLowerInvariant()]) ($(if ($answer) { $answer.Error } else { 'no answer' }))"); continue } foreach ($l in @($answer.Items | Where-Object { $null -ne $_ })) { $locks.Add($l) } } Update-AACProgress -Id 'locks' -Complete -Description ('Read {0:N0} lock(s)' -f $locks.Count) $result = ConvertTo-AACResourceLock -Lock $locks.ToArray() -ResourceGroup @($inventory.Rows['groups'] | Where-Object { $_ }) -Resource @($inventory.Rows['resources'] | Where-Object { $_ }) -SubscriptionName $scope.Names -Unread $unread.ToArray() @{ Result = $result; Scope = $scope } } $rows = @($outcome.Result.Locks) if ($Status) { $rows = @($rows | Where-Object { $Status -contains $_.Status }) } $s = $outcome.Result.Stats $rank = Get-AACSeverityRank $statusTones = @{ Locked = 'good'; Unlocked = 'warn' } $report = @{ Subtitle = 'Resource locks: every lock, and the critical resources no lock protects' Facts = [ordered]@{ Scope = $outcome.Scope.Label; 'Critical types' = "$($request.CriticalType.Count)" } Status = $(if ($s.Unlocked -or @($rows | Where-Object Severity -EQ 'Medium').Count) { 'Warning' } else { 'Success' }) Headline = "$($s.Locks) lock(s) ($($s.ReadOnly) ReadOnly); of $($s.Critical) critical resource(s), $($s.Direct) locked directly, $($s.Inherited) through a parent, $($s.Unlocked) unlocked; $($s.GroupsCovered) of $($s.Groups) resource group(s) covered" Tiles = @( @{ Value = '{0:N0}' -f $s.Locks; Label = 'locks'; Tone = 'info'; Table = 'locks'; Filters = @{ Status = 'Locked' } } @{ Value = '{0:N0}' -f $s.Unlocked; Label = 'critical, unlocked'; Tone = $(if ($s.Unlocked) { 'warn' } else { 'good' }); Table = 'locks'; Filters = @{ Status = 'Unlocked' } } @{ Value = '{0:N0}' -f $s.ReadOnly; Label = 'ReadOnly locks'; Tone = $(if ($s.ReadOnly) { 'warn' } else { 'good' }) } @{ Value = $(if ($s.Groups) { '{0:N0}%' -f (100 * $s.GroupsCovered / $s.Groups) } else { '-' }); Label = 'resource groups covered'; Tone = 'info' } ) Notices = @($outcome.Result.Notices | ForEach-Object { @{ Status = 'Warning'; Text = $_ } }) Charts = @( @{ Title = 'Critical resources'; Kind = 'donut'; CenterLabel = 'resources'; Items = @(@{ Label = 'Locked directly'; Value = $s.Direct; Tone = 'good' }, @{ Label = 'Through a parent'; Value = $s.Inherited; Tone = 'info' }, @{ Label = 'Unlocked'; Value = $s.Unlocked; Tone = 'warn' } | Where-Object { $_.Value }); Console = $true } @{ Title = 'Unlocked, by type'; Items = @($rows | Where-Object Status -EQ 'Unlocked' | Group-Object ResourceType | Sort-Object Count -Descending | ForEach-Object { @{ Label = $_.Name; Value = $_.Count; Filter = $_.Name } }); Table = 'locks'; Column = 'ResourceType'; Tone = 'warn' } ) Tables = @( @{ Id = 'locks'; Title = 'Locks and unlocked critical resources'; Section = 'Locks'; Rows = $rows; Noun = 'rows'; GroupBy = @('Status', 'ScopeType'); ConsoleLimit = 30 Empty = 'No locks, and no critical resources.'; EmptyStatus = 'Info' Columns = @( @{ Key = 'Severity'; Label = 'Severity'; Type = 'badge'; Tones = $rank.Tone; Facet = $true; Console = $true; Pdf = $true } @{ Key = 'Status'; Label = 'Status'; Type = 'badge'; Tones = $statusTones; Facet = $true; Console = $true; Pdf = $true } @{ Key = 'Level'; Label = 'Level'; Facet = $true; Console = $true; Pdf = $true } @{ Key = 'ScopeType'; Label = 'Scope type'; Facet = $true; Console = $true } @{ Key = 'Scope'; Label = 'Scope'; Console = $true; Pdf = $true } @{ Key = 'ResourceType'; Label = 'Type'; Facet = $true; Console = $true } @{ Key = 'Lock'; Label = 'Lock'; Console = $true } @{ Key = 'Findings'; Label = 'Findings'; Type = 'wide'; Pdf = $true } @{ Key = 'Notes'; Label = 'Notes'; Type = 'wide' } @{ Key = 'Subscription'; Label = 'Subscription'; Facet = $true } @{ Key = 'ScopeId'; Label = 'Scope ID'; Type = 'mono' } ) } ) Hint = '-Status Unlocked shows what nothing protects; -CriticalType sets the types to check; -NoDisplay returns the rows; -HtmlPath, -PdfPath or -CsvPath for a report.' } Invoke-AACReportOutput -Report $report -Title $Title -CsvObject $rows -Noun 'row' -CsvPath (& $resolve $CsvPath) -HtmlPath (& $resolve $HtmlPath) -PdfPath (& $resolve $PdfPath) ` -ShowView:$interactive -NoPaging:$NoPaging -Object $rows -ReturnObject:($PassThru -or $NoDisplay -or $pipedOnward) } |