Private/Get-AACAssessmentQuery.ps1
|
function Get-AACAssessmentQuery { <# .SYNOPSIS Builds the Resource Graph query of one Invoke-AACAssessment sheet (Get-AACAssessmentCatalog) - or the scope filter every resource query shares. .DESCRIPTION -Sheet: returns @{ Query; Columns } - Columns maps each label to its spec and the query column it comes back in (c0, c1, ... for KQL values; r0, r1, ... for the raw arrays a PowerShell transform turns into text; nothing for 'x:' columns filled in later). The query: <table> | where type in~ (...) <Where> <scope filter> <Pre> | project id, name, resourceGroup, subscriptionId, location, c0 = ..., [tags] -Filter alone: returns the scope filter - resource groups (in~, any case) and a tag (its key and/or value, any case, the way ARI matches them) - for the other resource queries. #> [CmdletBinding(DefaultParameterSetName = 'Sheet')] [OutputType([hashtable], [string])] param( [Parameter(Mandatory, ParameterSetName = 'Sheet')] [hashtable] $Sheet, [string[]] $ResourceGroupName, [string] $TagKey, [string] $TagValue, [Parameter(ParameterSetName = 'Sheet')] [switch] $IncludeTag, [Parameter(Mandatory, ParameterSetName = 'Filter')] [switch] $Filter ) $quote = { param([string] $Text) "'" + ($Text -replace '\\', '\\' -replace "'", "\'") + "'" } $scope = [System.Text.StringBuilder]::new() $groups = @($ResourceGroupName | Where-Object { $_ }) if ($groups.Count) { [void]$scope.Append(" | where resourceGroup in~ ($((@($groups | ForEach-Object { & $quote $_ })) -join ', '))") } if ($TagKey -or $TagValue) { # A copy of the tags, one per row, so the tags themselves stay whole. [void]$scope.Append(' | where isnotempty(tags) | extend aacTag = tags | mv-expand aacTag | extend aacTagKey = tostring(bag_keys(aacTag)[0])') $conditions = @( if ($TagKey) { "aacTagKey =~ $(& $quote $TagKey)" } if ($TagValue) { "tostring(aacTag[aacTagKey]) =~ $(& $quote $TagValue)" } ) [void]$scope.Append(" | where $($conditions -join ' and ') | project-away aacTag, aacTagKey") } if ($Filter) { return $scope.ToString() } # A property named like a KQL keyword (pool.count) is a ParserFailure in # Resource Graph: it's written pool['count'] instead - outside quotes. $keywords = 'count|type|kind|title|default|filter|set|range|top|take|limit|order|sort|by|on|in|as|to|of|and|or|not|has|let|with|from|step|print|search|find|parse|sample|distinct|union|join|extend|project|summarize|where|render|contains|between' $safe = { param([string] $Expression) $parts = [regex]::Split($Expression, '(@?''(?:[^''\\]|\\.)*''|@?"(?:[^"\\]|\\.)*")') (@(for ($i = 0; $i -lt $parts.Count; $i++) { if ($i % 2) { $parts[$i] } else { [regex]::Replace($parts[$i], "(?<=[A-Za-z0-9_\]])\.($keywords)\b", { param($m) "['$($m.Groups[1].Value)']" }) } })) -join '' } $types = @($Sheet.Type | ForEach-Object { ([string]$_).ToLowerInvariant() }) $table = if ($Sheet.Contains('Table') -and $Sheet.Table) { $Sheet.Table } else { 'resources' } $columns = [ordered]@{} $projection = [System.Collections.Generic.List[string]]::new() $index = 0 foreach ($label in $Sheet.Columns.Keys) { $spec = [string]$Sheet.Columns[$label] $entry = @{ Spec = $spec; Column = '' } if ($spec -like 'x:*') { $columns[$label] = $entry; continue } if ($spec -like '@*') { # An array or object, as is: a PowerShell transform makes it text. $path = ($spec -replace '^@[a-z]+:', '') -replace '\|.*$', '' $entry.Column = "r$index" $projection.Add("r$index = $(& $safe $path)") } else { $expression = switch -Regex ($spec) { '^kql:(.+)$' { $Matches[1]; break } '^int:(.+)$' { "toint($($Matches[1]))"; break } '^num:(.+)$' { "round(todouble($($Matches[1])), 2)"; break } '^gb:(.+)$' { "round(todouble($($Matches[1])) / 1073741824, 2)"; break } '^len:(.+)$' { "array_length($($Matches[1]))"; break } '^join:(.+)$' { "strcat_array($($Matches[1]), ', ')"; break } '^leaf:(.+)$' { "extract(@'[^/]+`$', 0, tostring($($Matches[1])))"; break } '^vnet:(.+)$' { "extract(@'(?i)/virtualnetworks/([^/]+)', 1, tostring($($Matches[1])))"; break } '^subnet:(.+)$' { "extract(@'(?i)/subnets/([^/]+)', 1, tostring($($Matches[1])))"; break } '^date:(.+)$' { "tostring($($Matches[1]))"; break } default { "tostring($spec)" } } $entry.Column = "c$index" $projection.Add("c$index = $(& $safe $expression)") } $columns[$label] = $entry $index++ } $query = [System.Text.StringBuilder]::new($table) if ($types.Count -eq 1) { [void]$query.Append(" | where type =~ $(& $quote $types[0])") } else { [void]$query.Append(" | where type in~ ($((@($types | ForEach-Object { & $quote $_ })) -join ', '))") } if ($Sheet.Contains('Where') -and $Sheet.Where) { [void]$query.Append(" $($Sheet.Where)") } [void]$query.Append($scope.ToString()) if ($Sheet.Contains('Pre') -and $Sheet.Pre) { [void]$query.Append(" $(& $safe $Sheet.Pre)") } $tagColumn = if ($IncludeTag) { ', tags' } else { '' } [void]$query.Append(" | project id, name, resourceGroup, subscriptionId, location$(if ($projection.Count) { ', ' + ($projection -join ', ') })$tagColumn") @{ Query = $query.ToString(); Columns = $columns } } |