lib/_lib-arm.ps1
|
<# Shared helpers for the ARM-plane checks (Resources / CAF / WAF / Defender). Dot-sourced once by run-checks.ps1 after Connect-AzAccount. Everything runs off the Az context, so no manual token threading. Azure Resource Graph (Search-Graph) is the workhorse — one KQL query spans every subscription the signed-in user can read, including the securityresources (Defender), advisorresources (Advisor) and authorizationresources (RBAC) tables. Invoke-Arm is the REST fallback for the few things Resource Graph doesn't expose (management-group hierarchy, budgets, diagnostic settings, classic admins, policy compliance). #> function global:Search-Graph { <# Run a Resource Graph KQL query across all readable subscriptions, paging. -Skip has a minimum of 1, so the first page must omit it entirely. #> param([Parameter(Mandatory)][string]$Query) $all = @(); $skip = 0 do { $batch = if ($skip -gt 0) { @(Search-AzGraph -Query $Query -First 1000 -Skip $skip -ErrorAction Stop) } else { @(Search-AzGraph -Query $Query -First 1000 -ErrorAction Stop) } if ($batch.Count) { $all += $batch } $skip += 1000 } while ($batch.Count -eq 1000) , $all } function global:Get-Subs { <# Enabled subscriptions the signed-in user can read. #> @(Get-AzSubscription -ErrorAction Stop | Where-Object { $_.State -eq 'Enabled' }) } function global:Invoke-Arm { <# GET an ARM REST path (relative, no host) and follow nextLink. Returns .value[]. #> param([Parameter(Mandatory)][string]$Path, [Parameter(Mandatory)][string]$ApiVersion) $sep = if ($Path -like '*`?*') { '&' } else { '?' } $uri = "$Path${sep}api-version=$ApiVersion" $all = @() while ($uri) { $resp = Invoke-AzRestMethod -Method GET -Path $uri -ErrorAction Stop if ([int]$resp.StatusCode -ge 400) { throw "ARM $($resp.StatusCode): $($resp.Content)" } # 204 No Content is a documented empty result (reservationRecommendations returns it # when there is nothing to recommend). ConvertFrom-Json throws on an empty string, so # without this guard "nothing to report" reaches the caller as a failure. if (-not $resp.Content) { break } $c = $resp.Content | ConvertFrom-Json if ($null -ne $c.value) { $all += $c.value } else { $all += $c } $uri = $c.nextLink if ($uri) { $uri = $uri -replace '^https://management\.azure\.com', '' } } , $all } function global:Sub-Name { <# Friendly "<name> (<id>)" for a subscription id, cached. #> param([string]$Id) if (-not $global:AZTK_subnames) { $global:AZTK_subnames = @{} } if (-not $global:AZTK_subnames.ContainsKey($Id)) { $s = try { (Get-AzSubscription -SubscriptionId $Id -ErrorAction Stop).Name } catch { $null } $global:AZTK_subnames[$Id] = if ($s) { "$s ($Id)" } else { $Id } } $global:AZTK_subnames[$Id] } |