src/assess/Test-ScoutPermission.ps1
|
#Requires -Version 7.0 Set-StrictMode -Version Latest $ErrorActionPreference = 'Stop' <# .SYNOPSIS Verify the read-only access each selected assessment needs and emit remediation. .NOTES Read-only throughout. Tracks ADO Story AB#5051. #> function Test-ScoutPermission { param([string[]] $Assessment, $Manifest) $results = @() # ARM: Reader at MG root $ctx = Get-AzContext $mgReader = Get-AzRoleAssignment -Scope "/providers/Microsoft.Management/managementGroups/$($ctx.Tenant.Id)" ` -SignInName $ctx.Account.Id -ErrorAction SilentlyContinue | Where-Object RoleDefinitionName -eq 'Reader' $results += [pscustomobject]@{ Check = 'ARM Reader @ MG root'; Ok = [bool]$mgReader Fix = 'Assign Reader at the tenant root management group scope.' } # Graph app permissions needed when ingesting the governance visualizer. # Two independent guards here: (1) $Manifest itself can be $null (a caller # that never loaded manifests/assessments.psd1) -- indexing $null[$_] throws # "Cannot index into a null array", not $null, so it must be checked before # the bracket lookup runs at all; (2) $Manifest[$_] is $null whenever $_ is # an assessment name that isn't actually a key in the manifest -- e.g. a # caller-supplied name never validated against the manifest -- and # `$null.Ingest` throws PropertyNotFoundException, not $null, under # Set-StrictMode -Version Latest, so guard the lookup before dotting into it. if ($Assessment | Where-Object { $def = if ($null -ne $Manifest) { $Manifest[$_] } else { $null }; $def -and (@($def.Ingest) -contains 'AzGovViz') }) { foreach ($p in 'User.Read.All', 'Group.Read.All', 'Application.Read.All', 'PrivilegedAccess.Read.AzureResources') { $results += [pscustomobject]@{ Check = "Graph: $p"; Ok = $null Fix = "Grant application permission $p with admin consent (or use Directory Readers)." } } } # Out-Host (not a bare, unassigned Format-Table call) -- an unassigned # Format-Table's format objects (FormatStartData/FormatEntryData/...) go # straight to the SUCCESS stream like any other pipeline output, which # silently prepended them onto this function's real return value below. # A caller that reads a named property off each returned item (e.g. # Invoke-ScoutPipeline.ps1's `$_.Check`) then throws # PropertyNotFoundException under Set-StrictMode -Version Latest the # moment it reaches one of those format objects -- this is the exact # crash a real caller hit in the permission-audit path. Out-Host renders # the same table for an interactive caller without adding anything to # the output stream. $results | Format-Table -AutoSize | Out-Host return $results } |