Private/Get-AACRuleData.ps1
|
function Get-AACRuleData { <# .SYNOPSIS Reads the Azure estate in the shape PSRule for Azure expects - the same data Export-AzRuleData produces, without the Az modules. .DESCRIPTION PSRule for Azure (https://azure.github.io/PSRule.Rules.Azure/) checks live resources from Export-AzRuleData's output: each resource with its full properties, plus a 'resources' array of the child settings its rules look at (a storage account's blob services and containers, a SQL server's auditing and firewall rules, a Key Vault's diagnostic settings, ...). Export-AzRuleData needs the Az modules; this reads the same things with the Connect-AAC sign-in: 1. Resources, resource groups and subscriptions from Azure Resource Graph (a few calls however many resources there are). 2. The child settings, from Azure Resource Manager, for the types Export-AzRuleData expands - the same children and API versions as its ResourceExpandVisitor (PSRule.Rules.Azure v1.47). That is one to a dozen calls per resource of those types; resources of other types need none. Security alerts aren't read, as Export-AzRuleData doesn't by default. Shared keys on network connections are masked, as Export-AzRuleData does. Reader on the subscriptions is enough. A child that can't be read (usually 403 - Reader can't list some settings) is left out and reported in Warnings, so the caller can say which results may be affected; the resource itself is still returned. Returns @{ Resources = <hashtables>; Warnings = <strings> }. Progress goes to the Invoke-AACProgress display when one is running (Ids 'psrule-read' and 'psrule-expand'). #> [CmdletBinding()] [OutputType([hashtable])] param( # Defaults to every subscription the signed-in account can see. [string[]] $SubscriptionId = @(), # Only these resource types (wildcards work). Resource groups and # subscriptions are included only when no type is given, or when # their own type matches. [string[]] $ResourceType = @() ) $warnings = [System.Collections.Generic.List[string]]::new() # --- Resource Graph --------------------------------------------------------- $graph = { param([string] $Query) $body = @{ query = $Query; options = @{ resultFormat = 'objectArray' } } if ($SubscriptionId.Count -gt 0) { $body.subscriptions = @($SubscriptionId) } do { $response = Invoke-AACArmRequest -Method Post -Uri '/providers/Microsoft.ResourceGraph/resources?api-version=2022-10-01' -Body ($body | ConvertTo-Json -Depth 10) $response['data'] $skipToken = $response['$skipToken'] $body.options['$skipToken'] = $skipToken } while ($skipToken) } $wanted = { param([string] $Type) $ResourceType.Count -eq 0 -or @($ResourceType | Where-Object { $Type -like $_ }).Count -gt 0 } Update-AACProgress -Id 'psrule-read' -Total 3 -Description 'Reading resources from Azure Resource Graph' $resources = [System.Collections.Generic.List[object]]::new() foreach ($row in @(& $graph @' Resources | project id, name, type, kind, location, resourceGroup, subscriptionId, tenantId, tags, sku, plan, zones, identity, managedBy, extendedLocation, properties | order by type asc, name asc '@)) { if (-not (& $wanted $row['type'])) { continue } # Export-AzRuleData's field names: resourceGroupName, not resourceGroup. $row['resourceGroupName'] = $row['resourceGroup'] $row.Remove('resourceGroup') foreach ($key in @($row.Keys)) { if ($null -eq $row[$key]) { $row.Remove($key) } } $resources.Add($row) } Update-AACProgress -Id 'psrule-read' -Increment 1 -Description 'Reading resource groups and subscriptions' if (& $wanted 'Microsoft.Resources/resourceGroups') { foreach ($row in @(& $graph @' ResourceContainers | where type =~ 'microsoft.resources/subscriptions/resourcegroups' | project id, name, location, subscriptionId, tenantId, tags, managedBy, properties | order by name asc '@)) { $row['type'] = 'Microsoft.Resources/resourceGroups' foreach ($key in @($row.Keys)) { if ($null -eq $row[$key]) { $row.Remove($key) } } $resources.Add($row) } } Update-AACProgress -Id 'psrule-read' -Increment 1 $subscriptionNames = @{} foreach ($row in @(& $graph @' ResourceContainers | where type =~ 'microsoft.resources/subscriptions' | project subscriptionId, name, tenantId, properties | order by name asc '@)) { $subscriptionNames[[string]$row['subscriptionId']] = [string]$row['name'] if (& $wanted 'Microsoft.Subscription') { $resources.Add(@{ type = 'Microsoft.Subscription' id = "/subscriptions/$($row['subscriptionId'])" subscriptionId = $row['subscriptionId'] name = $row['name'] displayName = $row['name'] tenantId = $row['tenantId'] properties = $row['properties'] }) } } Update-AACProgress -Id 'psrule-read' -Complete -Description ('Read {0:N0} resources, resource groups and subscriptions' -f $resources.Count) # --- Child settings ----------------------------------------------------------- # Every item of an ARM list (following nextLink); a singleton setting # returned as one object counts as a list of one. 404 means "none", as # does 400 for settings a SKU doesn't have (Export-AzRuleData leaves # those out too). Anything else is noted in Warnings. $describe = { param([string] $Id) $parts = $Id.Trim('/').Split('/') if ($parts.Count -ge 2) { "$($parts[-2])/$($parts[-1])" } else { $Id } } $list = { param([string] $Id, [string] $Child, [string] $ApiVersion) $uri = if ($Child.StartsWith('/')) { "$Id$($Child)?api-version=$ApiVersion" } else { "$Id/$($Child)?api-version=$ApiVersion" } try { while ($uri) { $response = Invoke-AACArmRequest -Uri $uri $uri = $null if ($response -is [System.Collections.IDictionary]) { if ($response.Contains('value')) { foreach ($item in @($response['value'])) { if ($item -is [System.Collections.IDictionary]) { $item } } $uri = $response['nextLink'] } elseif ($response.Contains('id')) { $response } } } } catch { $status = [int]$_.Exception.Data['StatusCode'] if ($status -notin 400, 404) { $warnings.Add("$(& $describe $Id): could not read $($Child.TrimStart('/')) ($(if ($status) { "HTTP $status" } else { 'no response' })): $($_.Exception.Message)") } } } $getOne = { param([string] $Id, [string] $ApiVersion) try { Invoke-AACArmRequest -Uri "$($Id)?api-version=$ApiVersion" } catch { $status = [int]$_.Exception.Data['StatusCode'] if ($status -ne 404) { $warnings.Add("$(& $describe $Id): could not read it ($(if ($status) { "HTTP $status" } else { 'no response' })): $($_.Exception.Message)") } } } $add = { param([System.Collections.IDictionary] $Parent, [object[]] $Children) $items = @($Children | Where-Object { $_ -is [System.Collections.IDictionary] }) if ($items.Count -eq 0) { return } if (-not $Parent.Contains('resources')) { $Parent['resources'] = [System.Collections.Generic.List[object]]::new() } foreach ($item in $items) { $Parent['resources'].Add($item) } } $prop = { param($Object, [string] $Path) $value = $Object foreach ($part in $Path.Split('.')) { if ($value -isnot [System.Collections.IDictionary]) { return $null } $key = $value.Keys | Where-Object { $_ -eq $part } | Select-Object -First 1 $value = if ($null -ne $key) { $value[$key] } else { $null } } $value } $diagnostics = { param($Resource) & $add $Resource @(& $list $Resource['id'] '/providers/microsoft.insights/diagnosticSettings' '2021-05-01-preview') } # Type -> how to expand it. Mirrors ResourceExpandVisitor. $expand = @{ 'microsoft.resources/resourcegroups' = { param($r) & $add $r @(& $list $r['id'] '/providers/Microsoft.Authorization/roleAssignments' '2022-04-01') & $add $r @(& $list $r['id'] '/providers/Microsoft.Authorization/locks' '2016-09-01') } 'microsoft.subscription' = { param($r) & $add $r @(& $list $r['id'] '/providers/Microsoft.Authorization/roleAssignments' '2022-04-01') & $add $r @(& $list $r['id'] '/providers/Microsoft.Authorization/classicAdministrators' '2015-07-01') & $add $r @(& $list $r['id'] '/providers/Microsoft.Security/autoProvisioningSettings' '2017-08-01-preview') & $add $r @(& $list $r['id'] '/providers/Microsoft.Security/securityContacts' '2017-08-01-preview') & $add $r @(& $list $r['id'] '/providers/Microsoft.Security/pricings' '2018-06-01') & $add $r @(& $list $r['id'] '/providers/Microsoft.Authorization/policyAssignments' '2019-06-01') } 'microsoft.apimanagement/service' = { param($r) $id = $r['id'] $apis = @(& $list $id 'apis' '2024-05-01') & $add $r $apis foreach ($api in $apis) { & $add $r @(& $list $api['id'] 'policies' '2024-05-01') if ([string](& $prop $api 'properties.type') -eq 'graphql') { foreach ($resolver in @(& $list $api['id'] 'resolvers' '2022-08-01')) { & $add $r @(& $list $resolver['id'] 'policies' '2022-08-01') } } else { foreach ($operation in @(& $list $api['id'] 'operations' '2022-08-01')) { & $add $r @(& $list $operation['id'] 'policies' '2022-08-01') } } } & $add $r @(& $list $id 'backends' '2024-05-01') $products = @(& $list $id 'products' '2024-05-01') & $add $r $products foreach ($product in $products) { & $add $r @(& $list $product['id'] 'policies' '2024-05-01') } & $add $r @(& $list $id 'policies' '2024-05-01') foreach ($child in 'identityProviders', 'diagnostics', 'loggers', 'certificates', 'namedValues', 'authorizationServers', 'portalsettings') { & $add $r @(& $list $id $child '2022-08-01') } & $add $r @(& $list $id '/providers/Microsoft.Security/apiCollections' '2022-11-20-preview') } 'microsoft.automation/automationaccounts' = { param($r) & $add $r @(& $list $r['id'] 'variables' '2022-08-08') & $add $r @(& $list $r['id'] 'webhooks' '2015-10-31') } 'microsoft.cdn/profiles/endpoints' = { param($r) & $add $r @(& $list $r['id'] 'customDomains' '2023-05-01') & $add $r @(& $list $r['id'] 'originGroups' '2023-05-01') & $diagnostics $r } 'microsoft.cdn/profiles' = { param($r) foreach ($child in 'customDomains', 'originGroups', 'ruleSets', 'secrets', 'securityPolicies') { & $add $r @(& $list $r['id'] $child '2023-05-01') } } 'microsoft.cdn/profiles/afdendpoints' = { param($r) & $add $r @(& $list $r['id'] 'routes' '2023-05-01') } 'microsoft.containerregistry/registries' = { param($r) & $add $r @(& $list $r['id'] 'replications' '2023-01-01-preview') & $add $r @(& $list $r['id'] 'webhooks' '2023-01-01-preview') & $add $r @(& $list $r['id'] 'tasks' '2019-04-01') foreach ($usage in @(& $list $r['id'] 'listUsages' '2023-01-01-preview')) { $usage['type'] = 'Microsoft.ContainerRegistry/registries/listUsages' & $add $r @($usage) } } 'microsoft.containerservice/managedclusters' = { param($r) if ([string](& $prop $r 'properties.networkProfile.networkPlugin') -eq 'azure') { foreach ($pool in @(& $prop $r 'properties.agentPoolProfiles')) { $subnetId = & $prop $pool 'vnetSubnetID' if ($subnetId) { & $add $r @(& $getOne $subnetId '2022-07-01') } } } & $add $r @(& $list $r['id'] 'maintenanceConfigurations' '2024-03-02-preview') & $diagnostics $r } 'microsoft.sql/servers' = { param($r) foreach ($child in 'firewallRules', 'administrators', 'securityAlertPolicies', 'vulnerabilityAssessments', 'auditingSettings') { & $add $r @(& $list $r['id'] $child '2021-11-01') } & $add $r @(& $list $r['id'] 'sqlVulnerabilityAssessments' '2024-05-01-preview') } 'microsoft.sql/servers/databases' = { param($r) $lower = ([string]$r['id']).ToLowerInvariant() & $add $r @(& $list $lower 'dataMaskingPolicies' '2014-04-01') & $add $r @(& $list $r['id'] 'transparentDataEncryption' '2021-11-01') & $add $r @(& $list $lower 'connectionPolicies' '2014-04-01') & $add $r @(& $list $lower 'geoBackupPolicies' '2014-04-01') } 'microsoft.dbforpostgresql/servers' = { param($r) foreach ($child in 'administrators', 'firewallRules', 'securityAlertPolicies', 'configurations') { & $add $r @(& $list $r['id'] $child '2017-12-01') } } 'microsoft.dbforpostgresql/flexibleservers' = { param($r) foreach ($child in 'administrators', 'firewallRules', 'configurations') { & $add $r @(& $list $r['id'] $child '2023-03-01-preview') } } 'microsoft.dbformysql/servers' = { param($r) foreach ($child in 'administrators', 'firewallRules', 'securityAlertPolicies', 'configurations') { & $add $r @(& $list $r['id'] $child '2017-12-01') } } 'microsoft.dbformysql/flexibleservers' = { param($r) foreach ($child in 'administrators', 'firewallRules', 'configurations') { & $add $r @(& $list $r['id'] $child '2023-06-30') } } 'microsoft.storage/storageaccounts' = { param($r) $kind = [string]$r['kind'] if ($kind -and $kind -ne 'FileStorage') { $blobServices = @(& $list $r['id'] 'blobServices' '2023-01-01') & $add $r $blobServices foreach ($service in $blobServices) { & $add $r @(& $list $service['id'] 'containers' '2023-01-01') } } elseif ($kind -and $kind -notin 'BlobStorage', 'BlockBlobStorage') { $fileServices = @(& $list $r['id'] 'fileServices' '2023-01-01') & $add $r $fileServices foreach ($service in $fileServices) { & $add $r @(& $list $service['id'] 'shares' '2023-01-01') } } & $add $r @(& $list $r['id'] '/providers/Microsoft.Security/DefenderForStorageSettings' '2022-12-01-preview') } 'microsoft.web/sites' = { param($r) & $add $r @(& $list $r['id'] 'config' '2022-09-01') } 'microsoft.web/sites/slots' = { param($r) & $add $r @(& $list $r['id'] 'config' '2022-09-01') } 'microsoft.recoveryservices/vaults' = { param($r) & $add $r @(& $list $r['id'] 'replicationRecoveryPlans' '2022-09-10') & $add $r @(& $list $r['id'] 'replicationAlertSettings' '2022-09-10') & $add $r @(& $list $r['id'] 'backupstorageconfig/vaultstorageconfig' '2022-09-01-preview') } 'microsoft.compute/virtualmachines' = { param($r) foreach ($nic in @(& $prop $r 'properties.networkProfile.networkInterfaces')) { $nicId = & $prop $nic 'id' if ($nicId) { & $add $r @(& $getOne $nicId '2022-07-01') } } $view = & $getOne "$($r['id'])/instanceView" '2021-11-01' $power = @(& $prop $view 'statuses') | Where-Object { [string](& $prop $_ 'code') -like 'PowerState/*' } | Select-Object -First 1 if ($power) { $r['PowerState'] = [string](& $prop $power 'code') } } 'microsoft.keyvault/vaults' = { param($r) & $diagnostics $r } 'microsoft.network/frontdoors' = { param($r) & $diagnostics $r } 'microsoft.kusto/clusters' = { param($r) & $add $r @(& $list $r['id'] 'databases' '2021-08-27') } 'microsoft.eventhub/namespaces' = { param($r) & $add $r @(& $list $r['id'] 'eventhubs' '2021-11-01') } 'microsoft.servicebus/namespaces' = { param($r) & $add $r @(& $list $r['id'] 'queues' '2021-06-01-preview') & $add $r @(& $list $r['id'] 'topics' '2021-06-01-preview') } 'microsoft.eventgrid/topics' = { param($r) & $add $r @(& $list $r['id'] 'eventSubscriptions' '2023-12-15-preview') } 'microsoft.eventgrid/domains' = { param($r) $topics = @(& $list $r['id'] 'topics' '2023-12-15-preview') foreach ($topic in $topics) { & $add $topic @(& $list $topic['id'] 'eventSubscriptions' '2023-12-15-preview') } & $add $r $topics & $add $r @(& $list $r['id'] 'eventSubscriptions' '2023-12-15-preview') } 'microsoft.eventgrid/namespaces' = { param($r) $topics = @(& $list $r['id'] 'topics' '2023-12-15-preview') foreach ($topic in $topics) { & $add $topic @(& $list $topic['id'] 'eventSubscriptions' '2023-12-15-preview') } & $add $r $topics } 'microsoft.devcenter/projects' = { param($r) $pools = @(& $list $r['id'] 'pools' '2023-04-01') foreach ($pool in $pools) { & $add $pool @(& $list $pool['id'] 'schedules' '2023-04-01') } & $add $r $pools } 'microsoft.network/firewallpolicies' = { param($r) & $add $r @(& $list $r['id'] 'ruleCollectionGroups' '2023-09-01') if ([string](& $prop $r 'properties.sku.tier') -eq 'Premium') { & $add $r @(& $list $r['id'] 'signatureOverrides' '2023-09-01') } } 'microsoft.network/virtualhubs' = { param($r) & $add $r @(& $list $r['id'] 'routingIntent' '2023-04-01') } 'microsoft.network/dnszones' = { param($r) & $add $r @(& $list $r['id'] 'dnssecConfigs' '2023-07-01-preview') } } # Never pass a VPN connection's shared key on to anything. foreach ($resource in $resources) { if ([string]$resource['type'] -eq 'microsoft.network/connections' -and (& $prop $resource 'properties.sharedKey')) { $resource['properties']['sharedKey'] = '*** MASKED ***' } } $toExpand = @($resources | Where-Object { $expand.ContainsKey(([string]$_['type']).ToLowerInvariant()) }) if ($toExpand.Count -gt 0) { Update-AACProgress -Id 'psrule-expand' -Total $toExpand.Count -Description ('Reading the settings of {0:N0} resources' -f $toExpand.Count) foreach ($resource in $toExpand) { Update-AACProgress -Id 'psrule-expand' -Description "Reading settings: $($resource['name'])" & $expand[([string]$resource['type']).ToLowerInvariant()] $resource Update-AACProgress -Id 'psrule-expand' -Increment 1 } Update-AACProgress -Id 'psrule-expand' -Complete -Description ('Read the settings of {0:N0} resources{1}' -f $toExpand.Count, $(if ($warnings.Count) { " ($($warnings.Count) could not be read)" })) } @{ Resources = $resources.ToArray() SubscriptionNames = $subscriptionNames Warnings = $warnings.ToArray() } } |