Private/ConvertTo-AACResourceLock.ps1
|
function ConvertTo-AACResourceLock { <# .SYNOPSIS Builds Get-AACResourceLock's list (AAC.ResourceLock) from each subscription's locks and the Resource Graph inventory of resource groups and critical resources: every lock, and the critical resources no lock protects. .DESCRIPTION No Azure calls. -Lock the locks (Microsoft.Authorization/locks items: id, name, properties.level, notes, owners) -ResourceGroup Resource Graph rows: id, name, subscriptionId -Resource Resource Graph rows of the critical types: id, name, type, resourceGroup, subscriptionId -SubscriptionName ID (lower case) -> name A lock protects its scope and everything below it: a lock on a subscription covers its resource groups and resources, one on a resource group its resources. Rows (AAC.ResourceLock): Locked each lock - its level (CanNotDelete or ReadOnly), scope, notes. A ReadOnly lock is a finding: Medium on a subscription or resource group (it blocks more than writes - listing storage keys, scaling, many portal blades), Low on a resource Unlocked each critical resource with no lock on it, its resource group or its subscription - Low: one mistaken delete away from losing it Returns @{ Locks; Notices; Stats } - Stats with how many critical resources are locked directly, through a parent, or not at all, and how many resource groups are covered. #> [CmdletBinding()] [OutputType([hashtable])] param( [AllowEmptyCollection()] [object[]] $Lock = @(), [AllowEmptyCollection()] [object[]] $ResourceGroup = @(), [AllowEmptyCollection()] [object[]] $Resource = @(), [hashtable] $SubscriptionName = @{}, [string[]] $Unread = @() ) $get = ${function:Get-AACMember} $text = { param($Object, [string] $Path) $v = & $get $Object $Path; if ($null -eq $v) { '' } else { [string]$v } } $notices = [System.Collections.Generic.List[string]]::new() foreach ($u in @($Unread | Where-Object { $_ })) { $notices.Add("The locks couldn't be read for $u - its resources show as unlocked even if they aren't.") } $subName = { param([string] $Id) if ($SubscriptionName.Contains($Id)) { $SubscriptionName[$Id] } else { $Id } } $describe = { param([string] $ScopeId) $s = $ScopeId.ToLowerInvariant() if ($s -match '^/subscriptions/([^/]+)/resourcegroups/([^/]+)/providers/(.+)/([^/]+)$') { return @{ Type = 'Resource'; Name = "$($Matches[4]) ($($Matches[2]))"; Sub = $Matches[1]; ResourceType = (($Matches[3] -split '/') | Select-Object -First 2) -join '/' } } if ($s -match '^/subscriptions/([^/]+)/resourcegroups/([^/]+)$') { return @{ Type = 'Resource group'; Name = "$($Matches[2]) ($(& $subName $Matches[1]))"; Sub = $Matches[1]; ResourceType = '' } } if ($s -match '^/subscriptions/([^/]+)$') { return @{ Type = 'Subscription'; Name = (& $subName $Matches[1]); Sub = $Matches[1]; ResourceType = '' } } @{ Type = 'Other'; Name = $ScopeId; Sub = ''; ResourceType = '' } } $rows = [System.Collections.Generic.List[object]]::new() $add = { param([string] $Severity, [string] $Status, [string] $Level, [string] $Name, $Where, [string] $Notes, [string] $Findings, [string] $ScopeId, [string] $Id, [string] $ResourceType = '') $item = [pscustomobject][ordered]@{ Severity = $Severity Status = $Status Level = $Level Lock = $Name ScopeType = $Where.Type Scope = $Where.Name ResourceType = $(if ($ResourceType) { $ResourceType } else { $Where.ResourceType }) Notes = $Notes Findings = $Findings Subscription = $(if ($Where.Sub) { & $subName $Where.Sub } else { '' }) ScopeId = $ScopeId Id = $Id } $item.PSObject.TypeNames.Insert(0, 'AAC.ResourceLock') $rows.Add($item) } # --- The locks, and the scopes they cover --------------------------------------------------------------------------------- $lockedScopes = @{} $seen = @{} foreach ($l in @($Lock)) { $id = & $text $l 'id' if (-not $id -or $seen.Contains($id.ToLowerInvariant())) { continue } $seen[$id.ToLowerInvariant()] = $true $scopeId = ($id -replace '(?i)/providers/Microsoft\.Authorization/locks/[^/]+$', '').ToLowerInvariant() $where = & $describe $scopeId $p = & $get $l 'properties' $level = & $text $p 'level' $lockedScopes[$scopeId] = $level $severity = 'Info'; $finding = '' if ($level -eq 'ReadOnly') { if ($where.Type -in 'Subscription', 'Resource group') { $severity = 'Medium'; $finding = "A ReadOnly lock on a $($where.Type.ToLowerInvariant()) blocks more than changes: listing storage keys, scaling, restarting and many portal pages fail too. Use CanNotDelete unless everything in it must be frozen." } else { $severity = 'Low'; $finding = 'ReadOnly blocks some reads too (listing keys, connection strings): check the resource still works as needed.' } } & $add $severity 'Locked' $level (& $text $l 'name') $where (& $text $p 'notes') $finding $scopeId $id } # --- Critical resources: locked directly, through a parent, or not at all ---------------------------------------------------- $direct = 0; $inherited = 0; $open = 0 foreach ($r in @($Resource)) { $id = (& $text $r 'id').ToLowerInvariant() $sub = (& $text $r 'subscriptionId').ToLowerInvariant() $group = "/subscriptions/$sub/resourcegroups/$((& $text $r 'resourceGroup').ToLowerInvariant())" if ($lockedScopes.Contains($id)) { $direct++; continue } if ($lockedScopes.Contains($group) -or $lockedScopes.Contains("/subscriptions/$sub")) { $inherited++; continue } $open++ & $add 'Low' 'Unlocked' '' '' (& $describe $id) '' 'No lock on it, its resource group or its subscription: one mistaken delete away from losing it. Add a CanNotDelete lock.' $id '' (& $text $r 'type').ToLowerInvariant() } $groupsCovered = @($ResourceGroup | Where-Object { $gid = (& $text $_ 'id').ToLowerInvariant(); $sub = (& $text $_ 'subscriptionId').ToLowerInvariant(); $lockedScopes.Contains($gid) -or $lockedScopes.Contains("/subscriptions/$sub") }).Count $rank = (Get-AACSeverityRank).Rank $order = @{ Subscription = 0; 'Resource group' = 1; Resource = 2; Other = 3 } $sorted = @($rows | Sort-Object -Property @{ Expression = { $rank[$_.Severity] } }, Status, @{ Expression = { $order[$_.ScopeType] } }, Scope) @{ Locks = $sorted Notices = @($notices) Stats = @{ Locks = @($sorted | Where-Object Status -EQ 'Locked').Count ReadOnly = @($sorted | Where-Object Level -EQ 'ReadOnly').Count Critical = @($Resource).Count Direct = $direct Inherited = $inherited Unlocked = $open Groups = @($ResourceGroup).Count GroupsCovered = $groupsCovered } } } |