lib/_lib.ps1
|
<# Shared helpers for the custom Graph checks. Dot-sourced once by run-checks.ps1 (so the cache below persists across all checks in a run). Everything is raw Microsoft Graph REST via Invoke-MgGraphRequest — no Graph SDK submodules needed. #> # Well-known Microsoft first-party tenant ids — used to exclude Microsoft-owned # service principals/apps from "your tenant's risk" findings. $global:AZTK_MS_TENANTS = @( 'f8cdef31-a31e-4b4a-93e4-5f571e91255a', # Microsoft Services '72f988bf-86f1-41af-91ab-2d7cd011db47' # Microsoft corp ) function global:Get-GraphAll { <# Follow @odata.nextLink and return all .value entries as PSObjects. Emits the entries individually (no ", $all" wrapper). The comma idiom returns the array as ONE pipeline object, which silently breaks the two most natural call sites: foreach ($x in Get-GraphAll ...) -> ONE iteration, $x bound to the whole array Get-GraphAll ... | Where-Object -> the filter sees one array, not N objects Guard expressions then evaluate against the array ("$x.type -eq 'ManagedIdentity'" returns a non-empty match set), so a check would `continue` past everything and record a silent PASS. Callers that want a guaranteed array wrap in @(...). #> param([Parameter(Mandatory)][string]$Uri) $all = @() while ($Uri) { $resp = Invoke-MgGraphRequest -Method GET -Uri $Uri -OutputType PSObject if ($null -ne $resp.value) { $all += $resp.value } else { $all += $resp } $Uri = $resp.'@odata.nextLink' } $all } function global:Get-AppsCached { <# One applications enumeration shared by every app-registration check. #> if (-not $global:AZTK_apps) { $sel = 'id,appId,displayName,signInAudience,passwordCredentials,keyCredentials,spa,notes,publicClient,web,verifiedPublisher' $global:AZTK_apps = @(Get-GraphAll "https://graph.microsoft.com/v1.0/applications?`$select=$sel&`$top=999") } $global:AZTK_apps } function global:Test-MicrosoftFirstParty { <# True if a servicePrincipal/app object is Microsoft-owned (exclude from risk). #> param($Obj) $owner = $Obj.appOwnerOrganizationId return ($owner -and ($global:AZTK_MS_TENANTS -contains $owner)) } function global:To-Date { <# Parse a Graph ISO timestamp to [datetime], or $null. #> param($Value) if (-not $Value) { return $null } try { return [datetime]$Value } catch { return $null } } function global:Get-EntraPlans { <# Service-plan names present on the tenant (cached), for licence gating. #> if (-not $global:AZTK_plans) { try { $skus = @(Get-GraphAll 'https://graph.microsoft.com/v1.0/subscribedSkus?$select=servicePlans') $global:AZTK_plans = @($skus.servicePlans.servicePlanName) | Sort-Object -Unique } catch { $global:AZTK_plans = @() } } $global:AZTK_plans } function global:Test-EntraLicence { <# True if any of the given service-plan names is present. AAD_PREMIUM = Entra ID P1, AAD_PREMIUM_P2 = Entra ID P2. Used to loud-skip P1/P2-only checks. #> param([Parameter(Mandatory)][string[]]$Plan) $have = Get-EntraPlans foreach ($p in $Plan) { if ($have -contains $p) { return $true } } return $false } function global:Skip-Check { <# Return a loud-skip result object (never a silent pass). #> param([Parameter(Mandatory)][string]$Name, [Parameter(Mandatory)][string]$Reason) return [pscustomobject]@{ Name = $Name; Status = 'skip'; Detail = $Reason } } |