tests/GraphExtraction.StrictMode.Tests.ps1
|
#Requires -Version 7.0 #Requires -Modules Pester <# AB#5547 — regression test for the unset $MGContainerExtension crash. Start-AZSCGraphExtraction consumed $MGContainerExtension unconditionally in the resourcecontainers query but only assigned it inside the -ManagementGroup branch, so an inventory run WITHOUT -ManagementGroup threw "The variable '$MGContainerExtension' cannot be retrieved because it has not been set" as soon as the caller had StrictMode on. This was found by a live tenant run, NOT by the suite: all 1688 tests passed while the bug was present, because nothing invoked the extractor from a StrictMode caller. That is the gap this file closes — it drives the real function under Set-StrictMode -Version Latest with the Azure calls stubbed out, so the variable-initialisation contract is enforced offline from now on. AB#5648 update: Start-AZSCGraphExtraction no longer builds queries or issues Resource Graph calls — it is a shim over src/collect/Get-ScoutRawInventory.ps1, and the Invoke-AZSCInventoryLoop stub this file used to install no longer exists. The stub therefore moved DOWN one layer to Search-AzGraph itself, which makes these assertions stronger rather than weaker: they now pin the query text that actually reaches Resource Graph through the whole (shim -> single-pass collector) call path, not the text handed to an intermediate function. No Azure connection. #> BeforeAll { $root = Split-Path $PSScriptRoot -Parent # Neutralise Import-Module so Get-ScoutRawInventory's `Import-Module Az.ResourceGraph` # cannot pull the real module (and therefore the real Search-AzGraph) into this session. function Import-Module { param([Parameter(ValueFromRemainingArguments)] $Rest) } . "$root/src/collect/Get-ScoutRawInventory.ps1" . "$root/Modules/Private/Extraction/Start-AZTIGraphExtraction.ps1" function Get-AZSCManagementGroups { param($ManagementGroup, $Subscriptions) return $Subscriptions } $script:subs = @([pscustomobject]@{ id = 'sub1'; name = 'Sub One' }) # The function's switch-ish parameters are declared untyped (legacy v1 signature) and are # tested with `.IsPresent`, so they only work when the caller passes a real SwitchParameter. # Invoke-AzureScout always does, via Start-AZSCExtractionOrchestration — mirror that contract # here rather than relying on the $null default, which is a separate latent fragility in this # file and out of scope for AB#5547. $script:switchArgs = @{ IncludeTags = [switch]$false SkipAdvisory = [switch]$true SecurityCenter = [switch]$false } } Describe 'Start-AZSCGraphExtraction under StrictMode (AB#5547)' { BeforeEach { $script:capturedQueries = @() function Search-AzGraph { param([string] $Query, [int] $First, [string] $SkipToken, [string] $ManagementGroup, [string[]] $Subscription, [string] $ErrorAction) $script:capturedQueries += $Query return @() } } It 'does not throw on an unset variable when no -ManagementGroup is supplied' { Set-StrictMode -Version Latest { Start-AZSCGraphExtraction -Subscriptions $script:subs -AzureEnvironment 'AzureCloud' ` @script:switchArgs 3>$null 4>$null 6>$null | Out-Null } | Should -Not -Throw } It 'builds the resourcecontainers query with an empty management-group extension' { Set-StrictMode -Version Latest Start-AZSCGraphExtraction -Subscriptions $script:subs -AzureEnvironment 'AzureCloud' ` @script:switchArgs 3>$null 4>$null 6>$null | Out-Null $containerQuery = @($script:capturedQueries | Where-Object { $_ -match '^resourcecontainers\b' }) $containerQuery.Count | Should -BeGreaterThan 0 # The management-group join must be absent entirely, not rendered as a literal null. ($containerQuery -join "`n") | Should -Not -Match 'managementGroupAncestorsChain' } It 'still injects the management-group extension when -ManagementGroup IS supplied' { Set-StrictMode -Version Latest Start-AZSCGraphExtraction -Subscriptions $script:subs -AzureEnvironment 'AzureCloud' ` -ManagementGroup 'mg-root' @script:switchArgs 3>$null 4>$null 6>$null | Out-Null $containerQuery = @($script:capturedQueries | Where-Object { $_ -match '^resourcecontainers\b' }) -join "`n" $containerQuery | Should -Match 'managementGroupAncestorsChain' $containerQuery | Should -Match 'mg-root' } It 'returns the documented shape rather than a bare collection' { Set-StrictMode -Version Latest $result = Start-AZSCGraphExtraction -Subscriptions $script:subs -AzureEnvironment 'AzureCloud' ` @script:switchArgs 3>$null 4>$null 6>$null foreach ($field in 'Resources', 'ResourceContainers', 'Advisories', 'Security', 'Retirements') { $result.PSObject.Properties.Name | Should -Contain $field } } } Describe 'Start-AZSCGraphExtraction issues no Resource Graph query of its own (AB#5648)' { BeforeEach { $script:capturedQueries = @() function Search-AzGraph { param([string] $Query, [int] $First, [string] $SkipToken, [string] $ManagementGroup, [string[]] $Subscription, [string] $ErrorAction) $script:capturedQueries += $Query return @() } } It 'the shim invokes no Resource Graph cmdlet and no retired extraction function' { # Asserted against the AST's COMMAND names, not the raw file text: this file's own # comments name both retired functions (explaining what replaced them), and a text # match cannot tell an explanation from a call. $root = Split-Path $PSScriptRoot -Parent $path = "$root/Modules/Private/Extraction/Start-AZTIGraphExtraction.ps1" $ast = [System.Management.Automation.Language.Parser]::ParseFile($path, [ref]$null, [ref]$null) $commands = @( $ast.FindAll({ $args[0] -is [System.Management.Automation.Language.CommandAst] }, $true) | ForEach-Object { $_.GetCommandName() } | Where-Object { $_ } ) $commands | Should -Not -Contain 'Search-AzGraph' $commands | Should -Not -Contain 'Invoke-AZSCInventoryLoop' $commands | Should -Contain 'Get-ScoutRawInventory' # And no KQL table name survives in any string literal the shim builds. $literals = @( $ast.FindAll({ $args[0] -is [System.Management.Automation.Language.StringConstantExpressionAst] }, $true) | ForEach-Object { $_.Value } ) -join "`n" foreach ($table in 'networkresources', 'recoveryservicesresources', 'desktopvirtualizationresources', 'advisorresources', 'securityresources') { $literals | Should -Not -Match ([regex]::Escape($table)) } } It 'the deleted legacy paging engine is really gone' { $root = Split-Path $PSScriptRoot -Parent Test-Path "$root/Modules/Private/Extraction/Invoke-AZTIInventoryLoop.ps1" | Should -BeFalse } It 'preserves the legacy per-table query set, filters and ordering through the shim' { Set-StrictMode -Version Latest Start-AZSCGraphExtraction -Subscriptions $script:subs -AzureEnvironment 'AzureCloud' ` -SkipAdvisory ([switch]$false) -SecurityCenter ([switch]$true) -IncludeTags ([switch]$false) ` 3>$null 4>$null 6>$null | Out-Null $all = $script:capturedQueries -join "`n" foreach ($table in 'resources', 'networkresources', 'SupportResources', 'recoveryservicesresources', 'desktopvirtualizationresources', 'resourcecontainers', 'advisorresources', 'securityresources') { $all | Should -Match ('(?m)^' + [regex]::Escape($table) + '\b') } # The four UI/authoring artifact types stayed excluded, on the `resources` table only. $resourcesQuery = @($script:capturedQueries | Where-Object { $_ -match '^resources\b' }) -join "`n" $resourcesQuery | Should -Match 'microsoft\.portal/dashboards' # And the advisor/security filters are unchanged. $all | Should -Match "properties\.impact in~ \('Medium','High'\)" $all | Should -Match "type =~ 'microsoft\.security/assessments'" } It 'omits SupportResources on AzureUSGovernment, exactly as the legacy extractor did' { Set-StrictMode -Version Latest Start-AZSCGraphExtraction -Subscriptions $script:subs -AzureEnvironment 'AzureUSGovernment' ` @script:switchArgs 3>$null 4>$null 6>$null | Out-Null ($script:capturedQueries -join "`n") | Should -Not -Match '(?m)^SupportResources\b' } It 'renders the resource-group filter clause and still requires a subscription id' { Set-StrictMode -Version Latest Start-AZSCGraphExtraction -Subscriptions $script:subs -AzureEnvironment 'AzureCloud' ` -SubscriptionID 'sub1' -ResourceGroup 'rg-alpha' @script:switchArgs 3>$null 4>$null 6>$null | Out-Null ($script:capturedQueries -join "`n") | Should -Match "where resourceGroup in~ \('rg-alpha'\)" { Start-AZSCGraphExtraction -Subscriptions $script:subs -AzureEnvironment 'AzureCloud' ` -ResourceGroup 'rg-alpha' @script:switchArgs 3>$null 4>$null 6>$null | Out-Null } | Should -Throw '*SubscriptionID must also be provided*' } It 'renders the tag filter clause when -TagKey/-TagValue are supplied' { Set-StrictMode -Version Latest Start-AZSCGraphExtraction -Subscriptions $script:subs -AzureEnvironment 'AzureCloud' ` -TagKey 'Environment' -TagValue 'Production' @script:switchArgs 3>$null 4>$null 6>$null | Out-Null $all = $script:capturedQueries -join "`n" $all | Should -Match "where tagKey =~ 'Environment'" $all | Should -Match "tagValue =~ 'Production'" } It 'projects the tags column only when -IncludeTags is supplied' { Set-StrictMode -Version Latest Start-AZSCGraphExtraction -Subscriptions $script:subs -AzureEnvironment 'AzureCloud' ` -IncludeTags ([switch]$false) -SkipAdvisory ([switch]$true) -SecurityCenter ([switch]$false) ` 3>$null 4>$null 6>$null | Out-Null ($script:capturedQueries -join "`n") | Should -Not -Match 'extendedLocation,tags' $script:capturedQueries = @() Start-AZSCGraphExtraction -Subscriptions $script:subs -AzureEnvironment 'AzureCloud' ` -IncludeTags ([switch]$true) -SkipAdvisory ([switch]$true) -SecurityCenter ([switch]$false) ` 3>$null 4>$null 6>$null | Out-Null ($script:capturedQueries -join "`n") | Should -Match 'extendedLocation,tags' } } |