mirror of
https://github.com/Micke-K/IntuneManagement.git
synced 2026-09-28 10:55:38 +02:00
179 lines
6.2 KiB
PowerShell
179 lines
6.2 KiB
PowerShell
<#
|
|
.SYNOPSIS
|
|
Startup-time sanity check for IntunePolicyType classes against Graph metadata.
|
|
|
|
.DESCRIPTION
|
|
Cross-references each registered IntunePolicyTypeBase subclass against the
|
|
cached Graph Beta CSDL (%LOCALAPPDATA%\IntuneManagement\GraphMetaData.xml).
|
|
Catches three classes of registration bug that the framework cannot detect
|
|
at runtime:
|
|
|
|
1. _API points to an endpoint that does not exist in the public schema
|
|
(typo, portal-only API, removed endpoint).
|
|
2. _QueryList references an isof('microsoft.graph.X') type that does not
|
|
exist as an EntityType in the schema (misspelled type name).
|
|
3. Two PolicyTypes share the same _API + _QueryList — a sibling-type
|
|
filter swap (e.g. an iOS type accidentally filtering on a macOS type).
|
|
|
|
Issues are written to the log at warning level. The validation is best-effort
|
|
and gracefully skipped if metadata is unavailable.
|
|
|
|
.NOTES
|
|
Sibling of Tools\Audit-PolicyTypeFlags.ps1, but runs in-process at startup
|
|
against the live $script:IntuneTypes registry rather than scraping source.
|
|
#>
|
|
|
|
# PolicyType IDs whose _API is known to be absent from the public Graph metadata
|
|
# but is still in active use (portal-only first-party endpoints, etc.).
|
|
# Keep this list small and document the reason next to each entry.
|
|
$script:PolicyTypeMetadataApiAllowlist = @(
|
|
# Portal-only endpoint per project_known_bugs_from_review.md (item 5).
|
|
# User decision 2026-05-18: leave registration as-is, don't re-flag.
|
|
"InventoryPolicies"
|
|
)
|
|
|
|
function Test-PolicyTypeMetadata
|
|
{
|
|
[CmdletBinding()]
|
|
param(
|
|
[object[]]$PolicyTypes,
|
|
[xml]$MetadataXml
|
|
)
|
|
|
|
if(-not $PolicyTypes) { $PolicyTypes = $script:IntuneTypes }
|
|
if(-not $PolicyTypes -or $PolicyTypes.Count -eq 0) { return @() }
|
|
|
|
if(-not $MetadataXml)
|
|
{
|
|
# -NoDownload: this is a diagnostic sanity check that runs on AppInitialized,
|
|
# i.e. inside Import-Module. It must never be the reason a 7-8 MB metadata
|
|
# download happens at load. Cached metadata validates; no cache just skips
|
|
# (handled immediately below).
|
|
Get-GraphMetaData -NoDownload
|
|
$MetadataXml = $script:GraphMetaDataXML
|
|
}
|
|
|
|
if(-not $MetadataXml)
|
|
{
|
|
Write-Log "Test-PolicyTypeMetadata: Graph metadata not available; skipping validation" 2
|
|
return @()
|
|
}
|
|
|
|
$nsm = New-Object System.Xml.XmlNamespaceManager $MetadataXml.NameTable
|
|
$nsm.AddNamespace("e", "http://docs.oasis-open.org/odata/ns/edm")
|
|
|
|
$entityNames = @{}
|
|
foreach($et in $MetadataXml.SelectNodes("//e:EntityType", $nsm))
|
|
{
|
|
$entityNames[$et.Name] = $true
|
|
}
|
|
|
|
$navPropNames = @{}
|
|
foreach($np in $MetadataXml.SelectNodes("//e:NavigationProperty", $nsm))
|
|
{
|
|
$navPropNames[$np.Name] = $true
|
|
}
|
|
|
|
$issues = @()
|
|
$apiQueryGroups = @{}
|
|
|
|
foreach($pt in $PolicyTypes)
|
|
{
|
|
$api = $pt._API
|
|
$id = $pt._ID
|
|
$qList = $pt._QueryList
|
|
|
|
if(-not $api) { continue }
|
|
|
|
# --- Rule 1: API last segment must exist as a NavigationProperty somewhere ---
|
|
$apiPath = $api -replace '%[^%]+%', 'x' # strip placeholders like %OrganizationId%
|
|
$segments = @($apiPath -split '/' | Where-Object { $_ })
|
|
$lastSegment = if($segments.Count) { $segments[-1] } else { $null }
|
|
|
|
if($lastSegment -and -not $navPropNames.ContainsKey($lastSegment) -and $id -notin $script:PolicyTypeMetadataApiAllowlist)
|
|
{
|
|
$issues += [PSCustomObject]@{
|
|
Severity = "Warning"
|
|
PolicyTypeId = $id
|
|
Issue = "API endpoint not in Graph metadata"
|
|
Detail = "_API='$api' - last segment '$lastSegment' is not a navigation property in GraphMetaData.xml. Likely portal-only, typo, or removed."
|
|
}
|
|
}
|
|
|
|
# --- Rule 2: every isof('microsoft.graph.X') must reference a known EntityType ---
|
|
if($qList)
|
|
{
|
|
$decoded = [uri]::UnescapeDataString($qList)
|
|
foreach($m in [regex]::Matches($decoded, "isof\(\s*'(?:microsoft\.graph\.|graph\.)?([A-Za-z0-9_]+)'\s*\)"))
|
|
{
|
|
$typeName = $m.Groups[1].Value
|
|
if(-not $entityNames.ContainsKey($typeName))
|
|
{
|
|
$issues += [PSCustomObject]@{
|
|
Severity = "Warning"
|
|
PolicyTypeId = $id
|
|
Issue = "isof() type not in Graph metadata"
|
|
Detail = "_QueryList references microsoft.graph.$typeName which is not an EntityType in GraphMetaData.xml."
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
# --- Rule 3: collect (API, QueryList) pairs for duplicate detection ---
|
|
if($qList)
|
|
{
|
|
$key = "$api||$qList"
|
|
if(-not $apiQueryGroups.ContainsKey($key))
|
|
{
|
|
$apiQueryGroups[$key] = @()
|
|
}
|
|
$apiQueryGroups[$key] += $id
|
|
}
|
|
}
|
|
|
|
foreach($key in $apiQueryGroups.Keys)
|
|
{
|
|
$ids = $apiQueryGroups[$key]
|
|
if($ids.Count -lt 2) { continue }
|
|
|
|
$sepIdx = $key.IndexOf("||")
|
|
$apiPart = $key.Substring(0, $sepIdx)
|
|
$queryPart = $key.Substring($sepIdx + 2)
|
|
|
|
$issues += [PSCustomObject]@{
|
|
Severity = "Warning"
|
|
PolicyTypeId = ($ids -join ", ")
|
|
Issue = "Duplicate API+QueryList across PolicyTypes"
|
|
Detail = "PolicyTypes [$($ids -join ', ')] all use _API='$apiPart' with identical _QueryList='$queryPart'. One is likely mis-filtered (sibling type swap)."
|
|
}
|
|
}
|
|
|
|
return $issues
|
|
}
|
|
|
|
function Invoke-PolicyTypeMetadataValidation
|
|
{
|
|
[CmdletBinding()]
|
|
param()
|
|
|
|
try
|
|
{
|
|
$issues = Test-PolicyTypeMetadata
|
|
if(-not $issues -or $issues.Count -eq 0)
|
|
{
|
|
Write-LogDebug "PolicyType metadata validation: no issues found"
|
|
return
|
|
}
|
|
|
|
Write-Log "PolicyType metadata validation: $($issues.Count) issue(s) found" 2
|
|
foreach($i in $issues)
|
|
{
|
|
Write-Log " [$($i.PolicyTypeId)] $($i.Issue) - $($i.Detail)" 2
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
Write-LogError "PolicyType metadata validation failed" $_.Exception
|
|
}
|
|
}
|