Public/Get-AACGovernanceSetting.ps1
|
function Get-AACGovernanceSetting { <# .EXTERNALHELP Azure.Admin.Console-help.xml .SYNOPSIS Checks the tenant-wide and per-subscription settings governance depends on - who can create management groups, where new subscriptions land, Defender security contacts, classic administrators, preview features, resource providers registered unevenly, and each subscription's offer and state. .DESCRIPTION Reads, read-only: Management groups the tenant root group's hierarchy settings: whether creating a management group needs permission, and the default management group for new subscriptions Resource Graph each subscription's offer (quota ID), state and spending limit Management group each management group above the subscriptions: diagnostics whether its activity log (administrative and policy events) is exported anywhere Each subscription Defender for Cloud security contacts and email notifications, classic administrators, the preview features registered, and the resource providers registered Each setting (AAC.GovernanceSetting) has its area, scope, value, status - Warning (fix it), Info (worth knowing) or OK - and what to do. Warnings: anyone can create management groups, new subscriptions land in the root group, a subscription with no security contact or with alert notifications off, a management group whose activity log goes nowhere, classic co-administrators left, a subscription not Enabled. Reader is enough (Reader on the tenant root group for its settings; what can't be read is left out, and said). .PARAMETER SubscriptionId Only these subscriptions. .PARAMETER ManagementGroupId Only the subscriptions under these management groups (at any depth). .PARAMETER Area Only these areas: ManagementGroups, Diagnostics, Subscription, DefenderContacts, ClassicAdministrators, PreviewFeatures, ResourceProviders. .PARAMETER Status Only these: Warning, Info, OK. .PARAMETER CsvPath Write the settings 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 settings. .PARAMETER NoDisplay Return the settings without showing the view. .PARAMETER NoPaging Show the whole view at once. .EXAMPLE Get-AACGovernanceSetting Every governance setting, the warnings first. .EXAMPLE Get-AACGovernanceSetting -Status Warning -NoDisplay | Select-Object Area, Scope, Setting, Recommendation Only what needs fixing. .EXAMPLE Get-AACGovernanceSetting -Area PreviewFeatures, ResourceProviders -HtmlPath .\Settings.html The preview features and unevenly registered providers, as a report. .OUTPUTS AAC.GovernanceSetting #> [CmdletBinding()] [OutputType('AAC.GovernanceSetting')] param( [ValidatePattern('^[0-9a-fA-F]{8}(-[0-9a-fA-F]{4}){3}-[0-9a-fA-F]{12}$')] [string[]] $SubscriptionId, [string[]] $ManagementGroupId, [ValidateSet('ManagementGroups', 'Diagnostics', 'Subscription', 'DefenderContacts', 'ClassicAdministrators', 'PreviewFeatures', 'ResourceProviders')] [string[]] $Area, [ValidateSet('Warning', 'Info', 'OK')] [string[]] $Status, [string] $CsvPath, [string] $HtmlPath, [string] $PdfPath, [string] $Title = 'Governance settings', [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 { $_ }); TenantId = [string]$(if ($script:AACSession) { $script:AACSession.TenantId }) } $null = Get-AACAccessToken if ($interactive) { Write-AACRule -Title 'Azure Admin Console :: Governance settings' -Color 'mediumpurple' } $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)" # Each subscription's offer and spending limit. $details = Invoke-AACGraphBatch -Query ([ordered]@{ details = "resourcecontainers | where type =~ 'microsoft.resources/subscriptions' | project subscriptionId = tolower(subscriptionId), quotaId = tostring(properties.subscriptionPolicies.quotaId), spendingLimit = tostring(properties.subscriptionPolicies.spendingLimit)" }) -SubscriptionId $scope.GraphScope -AllowFailure @('details') $policy = @{} foreach ($row in @($details.Rows['details'] | Where-Object { $_ })) { $policy[[string]$row['subscriptionId']] = $row } $subscriptions = @(foreach ($s in $scope.Subscriptions) { $copy = @{}; foreach ($k in $s.Keys) { $copy[$k] = $s[$k] } $id = ([string]$s['subscriptionId']).ToLowerInvariant() if ($policy.Contains($id)) { $copy['quotaId'] = $policy[$id]['quotaId']; $copy['spendingLimit'] = $policy[$id]['spendingLimit'] } $copy }) $uris = [ordered]@{} if ($request.TenantId) { $uris['hierarchy'] = "/providers/Microsoft.Management/managementGroups/$($request.TenantId)/settings/default?api-version=2021-04-01" } foreach ($id in $scope.Ids) { $uris["Contacts|$id"] = "/subscriptions/$id/providers/Microsoft.Security/securityContacts?api-version=2020-01-01-preview" $uris["Classic|$id"] = "/subscriptions/$id/providers/Microsoft.Authorization/classicAdministrators?api-version=2015-07-01" $uris["Features|$id"] = "/subscriptions/$id/providers/Microsoft.Features/features?api-version=2021-07-01" $uris["Providers|$id"] = "/subscriptions/$id/providers?api-version=2021-04-01" } $groups = @($scope.Subscriptions | ForEach-Object { @($_['chain']) | Where-Object { $_ -is [System.Collections.IDictionary] } | ForEach-Object { [string]$_['name'] } } | Select-Object -Unique) foreach ($g in $groups) { $uris["GroupDiag|$g"] = "/providers/Microsoft.Management/managementGroups/$g/providers/microsoft.insights/diagnosticSettings?api-version=2020-01-01-preview" } Update-AACProgress -Id 'arm' -Total ([Math]::Max(1, $uris.Count)) -Description "Reading the settings of $($scope.Ids.Count) subscription(s)" $answers = Invoke-AACArmParallel -Uri @($uris.Values) -OnProgress { param($ArmDone, $ArmTotal) Update-AACProgress -Id 'arm' -Increment 1 } $hierarchy = $null $hierarchyError = '' $per = @{} $groupDiag = [ordered]@{} foreach ($key in $uris.Keys) { $answer = $answers[$uris[$key]] if ($key -eq 'hierarchy') { # No settings yet answers 404: the defaults (anyone may create; new subscriptions land in the root). if ($answer -and [int]$answer.Status -eq 404) { $hierarchy = @{ properties = @{ requireAuthorizationForGroupCreation = $false; defaultManagementGroup = '' } } } elseif ($answer -and -not $answer.Error) { $hierarchy = $answer.Body } else { $hierarchyError = $(if ($answer) { [string]$answer.Error } else { 'no answer' }) } continue } $kind, $id = $key.Split('|') if ($kind -eq 'GroupDiag') { $groupDiag[$id] = if ($answer -and -not $answer.Error) { @{ Items = @($answer.Items | Where-Object { $null -ne $_ }) } } else { @{ Error = $(if ($answer) { [string]$answer.Error } else { 'no answer' }) } } continue } $id = $id.ToLowerInvariant() if (-not $per.Contains($id)) { $per[$id] = @{ Errors = @{} } } if ($answer -and -not $answer.Error) { $per[$id][$kind] = @($answer.Items | Where-Object { $null -ne $_ }) } else { $per[$id][$kind] = $null; $per[$id].Errors[$kind] = $(if ($answer) { [string]$answer.Error } else { 'no answer' }) } } Update-AACProgress -Id 'arm' -Complete -Description "Read the settings of $($scope.Ids.Count) subscription(s)" @{ Result = (ConvertTo-AACGovernanceSetting -Hierarchy $hierarchy -HierarchyError $hierarchyError -TenantId $request.TenantId -Subscription $subscriptions -PerSubscription $per -GroupDiagnostics $groupDiag); Scope = $scope } } $settings = @($outcome.Result.Settings) if ($Area) { $areas = @{ ManagementGroups = 'Management groups'; Diagnostics = 'Diagnostics'; Subscription = 'Subscription'; DefenderContacts = 'Defender contacts'; ClassicAdministrators = 'Classic administrators'; PreviewFeatures = 'Preview features'; ResourceProviders = 'Resource providers' }; $wanted = @($Area | ForEach-Object { $areas[$_] }); $settings = @($settings | Where-Object { $wanted -contains $_.Area }) } if ($Status) { $settings = @($settings | Where-Object { $Status -contains $_.Status }) } $warnings = @($settings | Where-Object Status -EQ 'Warning').Count $tones = @{ Warning = 'warn'; Info = 'info'; OK = 'good' } $report = @{ Subtitle = 'Governance settings: management group hierarchy, subscriptions, Defender contacts, classic administrators, preview features and resource providers' Facts = [ordered]@{ Scope = $outcome.Scope.Label; Subscriptions = "$($outcome.Result.Stats.Subscriptions)" } Status = $(if ($warnings) { 'Warning' } else { 'Success' }) Headline = $(if ($warnings) { "$warnings setting(s) to fix: $((@($settings | Where-Object Status -EQ 'Warning' | Group-Object Area | ForEach-Object { "$($_.Count) $($_.Name.ToLowerInvariant())" })) -join ', ')" } else { 'No governance setting needs fixing.' }) Tiles = @( @{ Value = '{0:N0}' -f $warnings; Label = 'to fix'; Tone = $(if ($warnings) { 'warn' } else { 'good' }); Table = 'settings'; Filters = @{ Status = 'Warning' } } @{ Value = '{0:N0}' -f @($settings | Where-Object { $_.Area -eq 'Defender contacts' -and $_.Status -eq 'Warning' }).Count; Label = 'contact gaps'; Tone = 'warn'; Table = 'settings'; Filters = @{ Area = 'Defender contacts' } } @{ Value = '{0:N0}' -f @($settings | Where-Object Area -EQ 'Preview features').Count; Label = 'preview features'; Tone = 'info'; Table = 'settings'; Filters = @{ Area = 'Preview features' } } @{ Value = '{0:N0}' -f @($settings | Where-Object Area -EQ 'Resource providers').Count; Label = 'uneven providers'; Tone = 'info'; Table = 'settings'; Filters = @{ Area = 'Resource providers' } } ) Notices = @($outcome.Result.Notices | ForEach-Object { @{ Status = 'Warning'; Text = $_ } }) Charts = @( @{ Title = 'By area'; Items = @($settings | Group-Object Area | Sort-Object Count -Descending | ForEach-Object { @{ Label = $_.Name; Value = $_.Count; Filter = $_.Name } }); Table = 'settings'; Column = 'Area'; Tone = 'info'; Console = $true } @{ Title = 'By status'; Kind = 'donut'; CenterLabel = 'settings'; Items = @(foreach ($s in 'Warning', 'Info', 'OK') { $n = @($settings | Where-Object Status -EQ $s).Count; if ($n) { @{ Label = $s; Value = $n; Tone = $tones[$s]; Filter = $s } } }); Table = 'settings'; Column = 'Status' } ) Tables = @( @{ Id = 'settings'; Title = 'Governance settings'; Section = 'Settings'; Rows = $settings; Noun = 'settings'; GroupBy = @('Area', 'Status'); ConsoleLimit = 30 Empty = 'No setting matches.'; EmptyStatus = 'Info' Columns = @( @{ Key = 'Status'; Label = 'Status'; Type = 'badge'; Tones = $tones; Facet = $true; Console = $true; Pdf = $true } @{ Key = 'Area'; Label = 'Area'; Facet = $true; Console = $true; Pdf = $true } @{ Key = 'Scope'; Label = 'Scope'; Facet = $true; Console = $true; Pdf = $true } @{ Key = 'Setting'; Label = 'Setting'; Console = $true; Pdf = $true } @{ Key = 'Value'; Label = 'Value'; Type = 'wide'; Console = $true; Pdf = $true } @{ Key = 'Recommendation'; Label = 'What to do'; Type = 'wide'; Pdf = $true } @{ Key = 'ScopeId'; Label = 'Scope ID'; Type = 'mono' } ) } ) Hint = '-Area and -Status narrow it; -NoDisplay returns the settings; -HtmlPath, -PdfPath or -CsvPath for a report.' } Invoke-AACReportOutput -Report $report -Title $Title -CsvObject $settings -Noun 'setting' -CsvPath (& $resolve $CsvPath) -HtmlPath (& $resolve $HtmlPath) -PdfPath (& $resolve $PdfPath) ` -ShowView:$interactive -NoPaging:$NoPaging -Object $settings -ReturnObject:($PassThru -or $NoDisplay -or $pipedOnward) } |