mirror of
https://github.com/Micke-K/IntuneManagement.git
synced 2026-09-28 19:05:38 +02:00
IntuneManagement 4.0.0-beta1
This commit is contained in:
@@ -0,0 +1,382 @@
|
||||
# Administrative Templates (ADMX / Group Policy) input provider.
|
||||
#
|
||||
# Ported from old Extensions/Documentation.psm1:916 (Invoke-TranslateADMXObject,
|
||||
# ~190 LOC). Claims @odata.type='#microsoft.graph.groupPolicyConfiguration'
|
||||
# and translates each definitionValue into a documented setting row.
|
||||
#
|
||||
# Definition resolution falls back through three sources, in order:
|
||||
# 1. inline $definitionValue.definition (live $expand=definition export)
|
||||
# 2. #Definition_* flat fields the new project's exporter promotes for
|
||||
# offline use (#Definition_displayName / categoryPath / classType / Id)
|
||||
# 3. live Graph fetch via Invoke-MSGraphAPI
|
||||
# Rows whose displayName can't be resolved (no inline, no embedded, no Graph)
|
||||
# are skipped — matches the golden fixture's offline behavior.
|
||||
#
|
||||
# Presentation values (the configured values for each ADMX setting) translate
|
||||
# differently per presentation type:
|
||||
# DropdownList -> map raw value to item.displayName
|
||||
# ValueList -> name=value pairs joined
|
||||
# MultiText -> values joined
|
||||
# Boolean/Decimal/LongDecimal/Text -> raw value
|
||||
#
|
||||
# Those joined strings stay in Value / ValueWithLabel / RawValue, which Compare,
|
||||
# CSV, Word and JSON all read. The RENDERED table (FullValueTable, used by the
|
||||
# HTML / Markdown / Atlassian outputs) is built separately by
|
||||
# ConvertTo-ADMXValueTable so a list or multi-text setting gets one row per item
|
||||
# instead of one cell holding everything joined, and an explicit-value list gets
|
||||
# its own Key column.
|
||||
#
|
||||
# Settings sorted by CategoryPath at end (matches old code's tail sort).
|
||||
|
||||
function Invoke-InitializeADMXInput {
|
||||
Add-DocumentationInputProvider ([PSCustomObject]@{
|
||||
Name = 'ADMX'
|
||||
Order = 50
|
||||
Match = { param($PolicyObject) $PolicyObject.JsonObject.'@odata.type' -eq '#microsoft.graph.groupPolicyConfiguration' }
|
||||
Translate = { param($PolicyObject, $Context) Invoke-TranslateADMXPolicyObject $PolicyObject $Context }
|
||||
})
|
||||
}
|
||||
|
||||
function Invoke-TranslateADMXPolicyObject {
|
||||
param($PolicyObject, [DocumentationContext]$Context)
|
||||
|
||||
$valueProperty = if ($Context.Options.ValueOutputProperty -eq 'valueWithLabel') { 'ValueWithLabel' } else { 'Value' }
|
||||
$Context.DisplayProperties = @('Name','Status','Value','Category','CategoryPath','RawValue','ValueWithLabel','Created','Modified','Class','DefinitionId')
|
||||
$Context.DefaultDocumentationProperties = @('Name','Status',$valueProperty)
|
||||
|
||||
$obj = $PolicyObject.JsonObject
|
||||
|
||||
# --- BasicInfo header ---
|
||||
Add-BasicDefaultValues $PolicyObject -SkipProperties @('')
|
||||
Add-BasicPropertyValue (Get-LanguageString 'TableHeaders.configurationType') (Get-LanguageString 'Titles.groupPolicy') '@odata.type'
|
||||
# (Platform supported deliberately omitted — old code at L922 has it commented out;
|
||||
# groupPolicyConfiguration is Windows-only by definition.)
|
||||
Add-BasicAdditionalValues $PolicyObject
|
||||
# --- Categories cache (batch-scoped, lazy) ---
|
||||
# Generic ADMX category/definition catalog - same on every tenant. Seed from the
|
||||
# session-persistent cache (like CfgCategories); on a miss, fetch from any
|
||||
# connected tenant and warm the cache so later runs in the session skip the GET.
|
||||
if (-not $Context.ADMXCategories -or $Context.ADMXCategories.Count -eq 0) {
|
||||
$Context.ADMXCategories = Get-CacheObject "DocADMXCategories" (@())
|
||||
}
|
||||
if ((-not $Context.ADMXCategories -or $Context.ADMXCategories.Count -eq 0) -and
|
||||
(Test-DocumentationGraphAvailable)) {
|
||||
try {
|
||||
$url = "deviceManagement/groupPolicyCategories?`$expand=parent(`$select=id,displayName,isRoot),definitions(`$select=id,displayName,categoryPath,classType,policyType)&`$select=id,displayName,isRoot"
|
||||
$resp = Invoke-MSGraphAPI -Url $url -ODataMetadata 'skip' -AdditionalHeaders (Get-DocAcceptLanguageHeaders $Context)
|
||||
if ($resp.Value) {
|
||||
$Context.ADMXCategories = @($resp.Value)
|
||||
Set-CacheObject "DocADMXCategories" $Context.ADMXCategories -Persistent
|
||||
}
|
||||
}
|
||||
catch {
|
||||
Write-LogError 'Failed to load ADMX group policy categories' $_.Exception
|
||||
}
|
||||
}
|
||||
|
||||
# --- definitionValues ---
|
||||
$definitionValues = @()
|
||||
if ($obj.definitionValues) {
|
||||
$definitionValues = @($obj.definitionValues)
|
||||
}
|
||||
elseif (-not $Context.SourceTenantUnavailable -and (Test-DocumentationGraphAvailable)) {
|
||||
# Source-tenant-specific: THIS policy's definitionValues by id (404s elsewhere).
|
||||
try {
|
||||
$url = "deviceManagement/groupPolicyConfigurations('$($obj.Id)')/definitionValues?`$expand=definition(`$select=id,classType,displayName,policyType,groupPolicyCategoryId)"
|
||||
$resp = Invoke-MSGraphAPI -Url $url -AdditionalHeaders (Get-DocAcceptLanguageHeaders $Context)
|
||||
$definitionValues = @($resp.Value)
|
||||
}
|
||||
catch {
|
||||
Write-LogError "Failed to load definitionValues for ADMX policy $($obj.Id)" $_.Exception
|
||||
}
|
||||
}
|
||||
if ($definitionValues.Count -eq 0) { return }
|
||||
|
||||
$enabledStr = Get-LanguageString 'Inputs.enabled'
|
||||
$disabledStr = Get-LanguageString 'Inputs.disabled'
|
||||
$propertyStr = Get-LanguageString 'ApplicabilityRules.GridLabel.property'
|
||||
$valueStr = Get-LanguageString 'ApplicabilityRules.GridLabel.value'
|
||||
$keyStr = Get-LanguageString 'SettingDetails.keyColumn'
|
||||
|
||||
## ToDo: Preload the presentation Definitions for all definitionValues with presentationValues defined in one batch
|
||||
# e.g. $definitionValues | Where presentationValues -ne $null -> Add to batch and fetch all in one call.
|
||||
|
||||
$rows = @()
|
||||
foreach ($defValue in $definitionValues) {
|
||||
$definition = Resolve-ADMXDefinition $defValue $Context
|
||||
if (-not $definition -or -not $definition.displayName) {
|
||||
# Unresolvable in current mode — skip (matches golden's offline behavior)
|
||||
continue
|
||||
}
|
||||
|
||||
# Category path: prefer the definition's own field; fall back to the cached
|
||||
# categories lookup when only an id is available.
|
||||
$categoryPath = $definition.categoryPath
|
||||
if (-not $categoryPath -and $Context.ADMXCategories.Count -gt 0) {
|
||||
$matched = $Context.ADMXCategories.definitions | Where-Object { $_.id -eq $definition.id } | Select-Object -First 1
|
||||
if ($matched) { $categoryPath = $matched.categoryPath }
|
||||
}
|
||||
|
||||
# Presentation values — only present when the policy carries configured values
|
||||
$presentationValues = Resolve-ADMXPresentationValues $defValue $obj $Context
|
||||
|
||||
$values = @()
|
||||
$valuesWithLabel = @()
|
||||
$rawValues = @()
|
||||
# One entry per presentation - its label plus the rows it contributes to
|
||||
# the rendered table. Multi-valued presentations contribute one row per
|
||||
# item. The flat $values / $valuesWithLabel / $rawValues below are
|
||||
# deliberately built exactly as before: Compare joins on them.
|
||||
$presEntries = @()
|
||||
# Presentation values present: map each to its label + value (per type)
|
||||
foreach ($pv in $presentationValues) {
|
||||
# Generic presentation metadata (label/dropdown items) resolved from any connected tenant.
|
||||
if (-not $pv.presentation -and $pv.'presentation@odata.bind' -and (Test-DocumentationGraphAvailable)) {
|
||||
try {
|
||||
$pres = Invoke-MSGraphAPI -Url $pv.'presentation@odata.bind' -AdditionalHeaders (Get-DocAcceptLanguageHeaders $Context)
|
||||
if ($pres) { $pv | Add-Member -MemberType NoteProperty -Name 'presentation' -Value $pres -Force }
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
$rawValue = $pv.value
|
||||
$label = $pv.presentation.label
|
||||
$value = $null
|
||||
$valueRows = @()
|
||||
|
||||
switch ($pv.presentation.'@odata.type') {
|
||||
'#microsoft.graph.groupPolicyPresentationDropdownList' {
|
||||
$value = ($pv.presentation.items | Where-Object value -EQ $rawValue).displayName
|
||||
$valueRows = @([PSCustomObject]@{ Key = $null; Value = $value })
|
||||
}
|
||||
default {
|
||||
switch ($pv.'@odata.type') {
|
||||
'#microsoft.graph.groupPolicyPresentationValueList' {
|
||||
$arr = @()
|
||||
foreach ($v in $pv.values) {
|
||||
$arr += "$($v.name)$($Context.PropertySeparator)$($v.value)"
|
||||
$valueRows += [PSCustomObject]@{ Key = $v.name; Value = $v.value }
|
||||
}
|
||||
$value = $arr -join $Context.ObjectSeparator
|
||||
# A plain <list> (no explicitValue) stores each item in
|
||||
# 'name' and leaves 'value' empty. Those are single-column
|
||||
# items, not key/value pairs - fold name into the value.
|
||||
if (@($valueRows | Where-Object { "$($_.Value)" -ne '' }).Count -eq 0) {
|
||||
$valueRows = @($valueRows | ForEach-Object { [PSCustomObject]@{ Key = $null; Value = $_.Key } })
|
||||
}
|
||||
}
|
||||
'#microsoft.graph.groupPolicyPresentationValueMultiText' {
|
||||
$value = $pv.values -join $Context.ObjectSeparator
|
||||
$valueRows = @(foreach ($v in $pv.values) { [PSCustomObject]@{ Key = $null; Value = $v } })
|
||||
}
|
||||
default {
|
||||
# Boolean / Decimal / LongDecimal / Text — value is the raw scalar
|
||||
$value = $rawValue
|
||||
$valueRows = @([PSCustomObject]@{ Key = $null; Value = $rawValue })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$presEntries += [PSCustomObject]@{ Label = $label; Rows = @($valueRows) }
|
||||
$valuesWithLabel += "$label $value"
|
||||
$values += $value
|
||||
$rawValues += $rawValue
|
||||
}
|
||||
|
||||
$tableValue = ConvertTo-ADMXValueTable -Entries $presEntries `
|
||||
-PropertyHeader $propertyStr -KeyHeader $keyStr -ValueHeader $valueStr
|
||||
|
||||
$status = if ($defValue.enabled -eq $true) { $enabledStr } else { $disabledStr }
|
||||
|
||||
$combinedValue = $status
|
||||
if ($values) {
|
||||
$combinedValue += $Context.ObjectSeparator + ($values -join $Context.ObjectSeparator)
|
||||
}
|
||||
|
||||
$combinedValueWithLabel = $status
|
||||
if ($valuesWithLabel) {
|
||||
$combinedValueWithLabel += $Context.ObjectSeparator + ($valuesWithLabel -join $Context.ObjectSeparator)
|
||||
}
|
||||
|
||||
$rows += [PSCustomObject]@{
|
||||
Name = $definition.displayName
|
||||
Description = $definition.explainText
|
||||
Status = $status
|
||||
Value = $values -join $Context.ObjectSeparator
|
||||
CombinedValue = $combinedValue
|
||||
ValueWithLabel = $valuesWithLabel -join $Context.ObjectSeparator
|
||||
FullValueTable = $tableValue
|
||||
CombinedValueWithLabel = $combinedValueWithLabel
|
||||
RawValue = $rawValues -join $Context.PropertySeparator
|
||||
Class = $definition.classType
|
||||
DefinitionId = $definition.id
|
||||
Created = $defValue.createdDateTime
|
||||
Modified = $defValue.lastModifiedDateTime
|
||||
Category = $categoryPath
|
||||
CategoryPath = $categoryPath
|
||||
EntityKey = $definition.id # Required for Compare
|
||||
AlwaysAddValue = if($null -ne $defValue.enabled) { $true } else { $false } # Always include Value if defined
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($row in ($rows | Sort-Object -Property CategoryPath, Name)) {
|
||||
$Context.AddSetting($row)
|
||||
}
|
||||
}
|
||||
|
||||
# Builds the rendered value table for one ADMX setting.
|
||||
#
|
||||
# The column shape is decided once for the whole setting, because the HTML,
|
||||
# Markdown and Atlassian renderers read their headers from the FIRST row and then
|
||||
# fetch every later row by those same property names - mixing shapes inside one
|
||||
# setting would silently blank cells. So: two columns (Property | Value) unless
|
||||
# some presentation contributed real key/value pairs, in which case three
|
||||
# (Property | Key | Value) for every row of that setting.
|
||||
#
|
||||
# The property label is written on the first row of each presentation only, so a
|
||||
# multi-item list reads as one labelled block instead of repeating the label.
|
||||
#
|
||||
# $Entries is one object per presentation: { Label; Rows = @({ Key; Value }) }.
|
||||
function ConvertTo-ADMXValueTable {
|
||||
param(
|
||||
$Entries,
|
||||
[string]$PropertyHeader,
|
||||
[string]$KeyHeader,
|
||||
[string]$ValueHeader
|
||||
)
|
||||
|
||||
$entryArr = @($Entries)
|
||||
if ($entryArr.Count -eq 0) { return @() }
|
||||
|
||||
$hasKeys = $false
|
||||
foreach ($entry in $entryArr) {
|
||||
foreach ($row in @($entry.Rows)) {
|
||||
if ("$($row.Key)" -ne '') { $hasKeys = $true; break }
|
||||
}
|
||||
if ($hasKeys) { break }
|
||||
}
|
||||
|
||||
# A translation that collides with another header would throw on the ordered
|
||||
# hashtable below, so fall back to the untranslated column name.
|
||||
if ($hasKeys -and ($KeyHeader -eq $PropertyHeader -or $KeyHeader -eq $ValueHeader -or -not $KeyHeader)) {
|
||||
$KeyHeader = 'Key'
|
||||
}
|
||||
|
||||
$table = @()
|
||||
foreach ($entry in $entryArr) {
|
||||
$rows = @($entry.Rows)
|
||||
# A presentation with nothing configured still shows its label.
|
||||
if ($rows.Count -eq 0) { $rows = @([PSCustomObject]@{ Key = $null; Value = $null }) }
|
||||
|
||||
$first = $true
|
||||
foreach ($row in $rows) {
|
||||
$out = [ordered]@{}
|
||||
$out[$PropertyHeader] = if ($first) { $entry.Label } else { '' }
|
||||
if ($hasKeys) { $out[$KeyHeader] = $row.Key }
|
||||
$out[$ValueHeader] = $row.Value
|
||||
$table += [PSCustomObject]$out
|
||||
$first = $false
|
||||
}
|
||||
}
|
||||
|
||||
# Comma so a single-row table doesn't unroll to a bare object on return.
|
||||
return ,$table
|
||||
}
|
||||
|
||||
# Three-tier definition resolution: inline -> embedded #Definition_* flat
|
||||
# fields -> live Graph fetch. Returns the synthesized/fetched definition or
|
||||
# $null if nothing resolved.
|
||||
function Resolve-ADMXDefinition {
|
||||
param($DefinitionValue, [DocumentationContext]$Context)
|
||||
|
||||
# 1. Inline (live $expand=definition export already populated it)
|
||||
if ($DefinitionValue.definition -and $DefinitionValue.definition.displayName) {
|
||||
return $DefinitionValue.definition
|
||||
}
|
||||
|
||||
# 2. Embedded #Definition_* flat fields (new project's exporter prefix)
|
||||
$embeddedDisplayName = $DefinitionValue.'#Definition_displayName'
|
||||
if ($embeddedDisplayName) {
|
||||
$syn = [PSCustomObject]@{
|
||||
id = $DefinitionValue.'#Definition_Id'
|
||||
displayName = $embeddedDisplayName
|
||||
classType = $DefinitionValue.'#Definition_classType'
|
||||
categoryPath = $DefinitionValue.'#Definition_categoryPath'
|
||||
explainText = $null
|
||||
policyType = $null
|
||||
}
|
||||
# Attach for future calls
|
||||
$DefinitionValue | Add-Member -MemberType NoteProperty -Name 'definition' -Value $syn -Force
|
||||
return $syn
|
||||
}
|
||||
|
||||
# 3. Live Graph fetch via definition@odata.bind URL. The definition is GENERIC
|
||||
# schema (groupPolicyDefinitions) - same on every tenant - so gated only on
|
||||
# connectivity, not on SourceTenantUnavailable.
|
||||
if ($DefinitionValue.'definition@odata.bind' -and (Test-DocumentationGraphAvailable)) {
|
||||
try {
|
||||
$url = $DefinitionValue.'definition@odata.bind'
|
||||
$def = Invoke-MSGraphAPI -Url $url -AdditionalHeaders (Get-DocAcceptLanguageHeaders $Context)
|
||||
if ($def) {
|
||||
$DefinitionValue | Add-Member -MemberType NoteProperty -Name 'definition' -Value $def -Force
|
||||
return $def
|
||||
}
|
||||
}
|
||||
catch {
|
||||
Write-LogError "Failed to fetch ADMX definition from $($DefinitionValue.'definition@odata.bind')" $_.Exception
|
||||
}
|
||||
}
|
||||
|
||||
return $null
|
||||
}
|
||||
|
||||
# Resolves presentation values + their presentation metadata. Returns array,
|
||||
# possibly empty for definitionValues with no configured presentationValues
|
||||
# (i.e. ADMX settings that are simply Enabled/Disabled with no inputs).
|
||||
function Resolve-ADMXPresentationValues {
|
||||
param($DefinitionValue, $PolicyObject, [DocumentationContext]$Context)
|
||||
|
||||
# Already inline? Order them by the canonical presentation order if we can.
|
||||
if ($DefinitionValue.presentationValues -and $DefinitionValue.presentationValues.Count -gt 0) {
|
||||
# The canonical presentation list is generic schema; reorder only needs a
|
||||
# connected tenant. Without one, keep the inline order.
|
||||
if (-not (Test-DocumentationGraphAvailable)) {
|
||||
return @($DefinitionValue.presentationValues)
|
||||
}
|
||||
# Live: pull the canonical presentation list so we can reorder
|
||||
try {
|
||||
$url = "$($DefinitionValue.'definition@odata.bind')/presentations"
|
||||
$resp = Invoke-MSGraphAPI -Url $url -AdditionalHeaders (Get-DocAcceptLanguageHeaders $Context)
|
||||
$canon = @($resp.Value)
|
||||
if ($canon.Count -gt 0) {
|
||||
$ordered = @()
|
||||
foreach ($p in $canon) {
|
||||
$match = $DefinitionValue.presentationValues | Where-Object 'presentation@odata.bind' -Like "*$($p.Id)*" | Select-Object -First 1
|
||||
if ($match) { $ordered += $match } else { $ordered = @(); break }
|
||||
}
|
||||
if ($ordered.Count -gt 0) { return $ordered }
|
||||
}
|
||||
} catch { }
|
||||
return @($DefinitionValue.presentationValues)
|
||||
}
|
||||
|
||||
# Live fetch (when fixture exported without presentationValues inline). These are
|
||||
# the policy's CONFIGURED values, fetched by policy id - source-tenant-specific
|
||||
# (404s elsewhere) - so gated on -not SourceTenantUnavailable.
|
||||
if ($DefinitionValue.id -and -not $Context.SourceTenantUnavailable -and (Test-DocumentationGraphAvailable)) {
|
||||
try {
|
||||
# Should never get here - $DefinitionValue.id will be empty.
|
||||
$url = "/deviceManagement/groupPolicyConfigurations/$($PolicyObject.id)/definitionValues/$($DefinitionValue.id)/presentationValues?`$expand=presentation"
|
||||
$resp = Invoke-MSGraphAPI -Url $url -AdditionalHeaders (Get-DocAcceptLanguageHeaders $Context)
|
||||
return @($resp.Value)
|
||||
}
|
||||
catch {
|
||||
Write-LogError "Failed to fetch ADMX presentationValues for $($DefinitionValue.id)" $_.Exception
|
||||
}
|
||||
}
|
||||
|
||||
return @()
|
||||
}
|
||||
|
||||
Invoke-InitializeADMXInput
|
||||
@@ -0,0 +1,154 @@
|
||||
# Compliance V2 input provider — schema-driven compliance policies on the
|
||||
# /deviceManagement/compliancePolicies endpoint.
|
||||
#
|
||||
# Ported from old Extensions/Documentation.psm1:1444 (Invoke-TranslateComplianceV2-
|
||||
# Object). Claims @odata.type='#microsoft.graph.deviceManagementCompliancePolicy'.
|
||||
#
|
||||
# Mirrors DocumentationInputSettingsCatalog.ps1 — same recursive setting walker
|
||||
# (Add-SettingsSetting), same batch caches on the [DocumentationContext]
|
||||
# ($ctx.CfgCategories, $ctx.CachedCfgSettings), same Category/SubCategory
|
||||
# grouping at the end. Differences:
|
||||
# - Settings endpoint: /deviceManagement/compliancePolicies/<id>/settings
|
||||
# - Categories endpoint: /deviceManagement/complianceCategories with the
|
||||
# linux/linuxMdm template filter (matches old code at L1471)
|
||||
# - platformSupported row uses $obj.platforms directly (compliance policies
|
||||
# are single-platform, no templateReference indirection)
|
||||
|
||||
function Invoke-InitializeComplianceV2Input {
|
||||
Add-DocumentationInputProvider ([PSCustomObject]@{
|
||||
Name = 'ComplianceV2'
|
||||
Order = 30
|
||||
Match = { param($PolicyObject) $PolicyObject.JsonObject.'@odata.type' -eq '#microsoft.graph.deviceManagementCompliancePolicy' }
|
||||
Translate = { param($PolicyObject, $Context) Invoke-TranslateComplianceV2Object $PolicyObject $Context }
|
||||
})
|
||||
}
|
||||
|
||||
function Invoke-TranslateComplianceV2Object {
|
||||
param($PolicyObject, [DocumentationContext]$Context)
|
||||
|
||||
$obj = $PolicyObject.JsonObject
|
||||
|
||||
# --- BasicInfo header rows ---
|
||||
Add-BasicDefaultValues $PolicyObject
|
||||
Add-BasicPropertyValue (Get-LanguageString 'TableHeaders.configurationType') (Get-LanguageString 'ConfigurationTypes.settingsCatalog') '@odata.type'
|
||||
|
||||
if ($obj.platforms) {
|
||||
$platformType = Get-LanguageString "Platform.$($obj.platforms)"
|
||||
if ($platformType) {
|
||||
Add-BasicPropertyValue (Get-LanguageString 'SettingDetails.platformSupported') $platformType 'platforms'
|
||||
}
|
||||
}
|
||||
|
||||
Add-BasicAdditionalValues $PolicyObject
|
||||
# --- Settings ---
|
||||
# Prefer in-policy settings WITH inline settingDefinitions. Inline settings
|
||||
# WITHOUT definitions (the hydrate body only does ?$expand=Settings) would
|
||||
# send the walker into a per-setting /configurationSettings/{id} N+1 — the
|
||||
# same trap the Settings Catalog provider fixed; enrich instead.
|
||||
$cfgSettings = @()
|
||||
if ($obj.Settings -and ($obj.Settings | Measure-Object).Count -gt 0) {
|
||||
$cfgSettings = @($obj.Settings)
|
||||
}
|
||||
|
||||
$hasDefs = $false
|
||||
foreach ($s in $cfgSettings) {
|
||||
if ($s.settingDefinitions -and ($s.settingDefinitions | Measure-Object).Count -gt 0) {
|
||||
$hasDefs = $true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
# Bulk runs: Initialize-DocumentationRunPrefetch already fetched these in
|
||||
# one Graph $batch — consume from the per-run cache (authoritative for this
|
||||
# run, even when empty, so an empty-settings policy doesn't trigger a
|
||||
# redundant live GET). The live GET below is the lazy fallback for the
|
||||
# single-policy Get-GraphDocumentation path.
|
||||
if (-not $hasDefs -and $Context.PrefetchedPolicySettings.ContainsKey([string]$obj.Id)) {
|
||||
$cfgSettings = @($Context.PrefetchedPolicySettings[[string]$obj.Id])
|
||||
$hasDefs = $true
|
||||
}
|
||||
|
||||
# Source-tenant-specific: fetches THIS policy's settings by id (404s elsewhere).
|
||||
# Stays gated on -not SourceTenantUnavailable; the walker's generic per-setting
|
||||
# configurationSettings/{id} fallback resolves schema when source is gone.
|
||||
if (-not $hasDefs -and -not $Context.SourceTenantUnavailable -and (Test-DocumentationGraphAvailable)) {
|
||||
try {
|
||||
$headers = @{}
|
||||
if ($Context.Language -and $Context.Language -ne 'en') {
|
||||
$headers['Accept-Language'] = $Context.Language
|
||||
}
|
||||
$resp = Invoke-MSGraphAPI -Url "/deviceManagement/compliancePolicies('$($obj.Id)')/settings?`$expand=settingDefinitions&`$top=1000" -AdditionalHeaders $headers
|
||||
if ($resp -and $resp.Value) {
|
||||
$cfgSettings = @($resp.Value)
|
||||
}
|
||||
}
|
||||
catch {
|
||||
Write-LogError "Failed to fetch settings for compliance policy $($obj.Id)" $_.Exception
|
||||
}
|
||||
}
|
||||
|
||||
if ($cfgSettings.Count -eq 0) {
|
||||
Write-Log "ComplianceV2: no settings to document for $($obj.name)" 2
|
||||
return
|
||||
}
|
||||
|
||||
# --- Generic schema caches (session-persistent, shared by reference with the
|
||||
# Settings Catalog provider so the walker's per-setting definition fetches
|
||||
# warm one shared cache). ---
|
||||
$Context.CachedCfgSettings = Get-CacheObject "DocCfgSettingDefinitions" $Context.CachedCfgSettings
|
||||
Set-CacheObject "DocCfgSettingDefinitions" $Context.CachedCfgSettings -Persistent
|
||||
|
||||
# --- Categories (batch-cached). Old code unions linux/linuxMdm template
|
||||
# categories into the same $global:cfgCategories the Settings Catalog uses;
|
||||
# we mirror that by appending to $ctx.CfgCategories rather than replacing.
|
||||
|
||||
|
||||
$Context.CfgCategories = Get-CacheObject "CfgCategories" (@())
|
||||
|
||||
# Generic schema (complianceCategories) - same on every tenant - gated only on connectivity.
|
||||
if (-not ($Context.CfgCategories | Where-Object { $_.settingUsage -eq 'compliance' }) -and (Test-DocumentationGraphAvailable)) {
|
||||
try {
|
||||
$resp = Invoke-MSGraphAPI -Url "/deviceManagement/complianceCategories" -ODataMetadata 'minimal' -AdditionalHeaders (Get-DocAcceptLanguageHeaders $Context)
|
||||
#$resp = Invoke-MSGraphAPI -Url "/deviceManagement/complianceCategories?`$templateCategory=True&`$filter=platforms has 'linux' and technologies has 'linuxMdm'"
|
||||
$Context.CfgCategories += @($resp.Value)
|
||||
Set-CacheObject "CfgCategories" $Context.CfgCategories -Persistent
|
||||
}
|
||||
catch {
|
||||
Write-LogError 'Failed to fetch compliance categories' $_.Exception
|
||||
}
|
||||
}
|
||||
|
||||
# --- Seed definition cache from inline settingDefinitions ---
|
||||
foreach ($cfgSetting in $cfgSettings) {
|
||||
if (-not $cfgSetting.settingDefinitions) { continue }
|
||||
$defObj = $cfgSetting.settingDefinitions | Where-Object id -EQ $cfgSetting.settingInstance.settingDefinitionId | Select-Object -First 1
|
||||
if ($defObj -and -not $Context.CachedCfgSettings.ContainsKey($defObj.Id)) {
|
||||
$Context.CachedCfgSettings[$defObj.Id] = $defObj
|
||||
}
|
||||
}
|
||||
|
||||
# --- Walk each top-level setting via the shared SettingsCatalog walker ---
|
||||
Reset-SettingsCatalogPolicyBuffer
|
||||
foreach ($cfgSetting in $cfgSettings) {
|
||||
Add-SettingsSetting $cfgSetting.settingInstance $cfgSetting.settingDefinitions | Out-Null
|
||||
}
|
||||
|
||||
# --- Drain buffer into SettingsData grouped by (Category, SubCategory) ---
|
||||
$buffer = Get-SettingsCatalogPolicyBuffer
|
||||
$unique = $buffer |
|
||||
Select-Object @{ l='CategoryID'; e={ $_.CategoryDefinition.Id } },
|
||||
@{ l='SubCategoryID'; e={ $_.SubCategoryDefinition.Id } } -Unique
|
||||
|
||||
foreach ($pair in $unique) {
|
||||
$rows = $buffer | Where-Object {
|
||||
$_.CategoryDefinition.Id -eq $pair.CategoryID -and
|
||||
$_.SubCategoryDefinition.Id -eq $pair.SubCategoryID
|
||||
}
|
||||
foreach ($row in $rows) {
|
||||
if ($row.Show -eq $false) { continue }
|
||||
$Context.AddSetting($row)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Invoke-InitializeComplianceV2Input
|
||||
@@ -0,0 +1,515 @@
|
||||
# Intent input provider — deviceManagementIntent (Endpoint Security baselines
|
||||
# and templates).
|
||||
#
|
||||
# Ported from old Extensions/Documentation.psm1:1580 (Invoke-TranslateIntent-
|
||||
# Object + helpers). Claims @odata.type='#microsoft.graph.deviceManagementIntent'.
|
||||
#
|
||||
# Intent settings live under /deviceManagement/templates/{templateId}/categories
|
||||
# (with $expand=settingDefinitions) and the per-intent values come from
|
||||
# /deviceManagement/intents/{intentId}/categories/{catId}/settings. Each setting
|
||||
# may be Simple / Collection / Complex / AbstractComplex with recursive children
|
||||
# and dependency constraints that hide settings whose parents aren't configured.
|
||||
#
|
||||
# Live Graph dependencies (resolved via Invoke-MSGraphAPI):
|
||||
# /deviceManagement/templates/{tid}/categories?$expand=settingDefinitions
|
||||
# /deviceManagement/intents/{iid}/categories/{cid}/settings?$expand=...
|
||||
# /deviceManagement/templates/{tid}/categories/{cid}/RecommendedSettings
|
||||
#
|
||||
# Batch-cached on the [DocumentationContext] ($ctx.IntentCategories,
|
||||
# $ctx.IntentCatRecommendedSettings) so a bulk run of N intents against the
|
||||
# same template only pays the round-trips once.
|
||||
|
||||
function Invoke-InitializeIntentInput {
|
||||
Add-DocumentationInputProvider ([PSCustomObject]@{
|
||||
Name = 'Intent'
|
||||
Order = 40
|
||||
Match = { param($PolicyObject) $PolicyObject.JsonObject.'@odata.type' -eq '#microsoft.graph.deviceManagementIntent' }
|
||||
Translate = { param($PolicyObject, $Context) Invoke-TranslateIntentObject $PolicyObject $Context }
|
||||
})
|
||||
}
|
||||
|
||||
function Invoke-TranslateIntentObject {
|
||||
param($PolicyObject, [DocumentationContext]$Context)
|
||||
|
||||
$Context.DefaultDocumentationProperties = @('Name','Value','RecommendedValue')
|
||||
|
||||
$obj = $PolicyObject.JsonObject
|
||||
|
||||
Add-BasicDefaultValues $PolicyObject
|
||||
|
||||
$baseLineTemplates = Get-CacheObject "BaseLineTemplates"
|
||||
if(-not $baseLineTemplates)
|
||||
{
|
||||
$baseLineTemplates = (Invoke-MSGraphAPI -Url "/deviceManagement/templates").Value
|
||||
Set-CacheObject "BaseLineTemplates" $baseLineTemplates -Persistent
|
||||
}
|
||||
|
||||
$baseLineTemplate = $baseLineTemplates | Where-Object Id -eq $obj.templateId
|
||||
if(-not $baseLineTemplate)
|
||||
{
|
||||
Write-Log "Could not find Baseline Template with Id $($obj.templateId)" 3
|
||||
}
|
||||
else {
|
||||
$platformType = Get-LanguageString "Platform.$($baseLineTemplate.platformType)"
|
||||
|
||||
if($platformType) { Add-BasicPropertyValue (Get-LanguageString "SettingDetails.platformSupported") $platformType 'platformSupported'}
|
||||
|
||||
if ($baseLineTemplate.templateSubtype -eq "none")
|
||||
{
|
||||
$templateCategoory = $baseLineTemplate.templateType
|
||||
} else {
|
||||
$templateCategoory = $baseLineTemplate.templateSubtype
|
||||
}
|
||||
Add-BasicPropertyValue (Get-LanguageString "TableHeaders.Category") (Get-IntentCategoryFromTemplateType $templateCategoory) "basicCategory"
|
||||
Add-BasicPropertyValue (Get-LanguageString "TableHeaders.policyType") $baseLineTemplate.displayName "basicPolicyType"
|
||||
}
|
||||
|
||||
Add-BasicAdditionalValues $PolicyObject
|
||||
if (-not $obj.templateId) {
|
||||
Write-Log "Intent: no templateId on '$($obj.displayName)' - cannot translate settings" 2
|
||||
return
|
||||
}
|
||||
|
||||
# Built-in ES template schema is generic. Seed/share the session-persistent
|
||||
# caches by reference so per-templateId/per-category writes below warm the
|
||||
# cache automatically and survive across runs (and tenant switches).
|
||||
$Context.IntentCategories = Get-CacheObject "DocIntentCategories" $Context.IntentCategories
|
||||
Set-CacheObject "DocIntentCategories" $Context.IntentCategories -Persistent
|
||||
$Context.IntentCatRecommendedSettings = Get-CacheObject "DocIntentRecommendedSettings" $Context.IntentCatRecommendedSettings
|
||||
Set-CacheObject "DocIntentRecommendedSettings" $Context.IntentCatRecommendedSettings -Persistent
|
||||
|
||||
# --- Template categories (batch-cached per templateId) ---
|
||||
$categories = $Context.IntentCategories[$obj.templateId]
|
||||
if (-not $categories) {
|
||||
# Built-in Endpoint Security template schema (by templateId) is generic -
|
||||
# same on every tenant - so resolved from any connected tenant, even when
|
||||
# the source tenant of the export is gone.
|
||||
if (-not (Test-DocumentationGraphAvailable)) {
|
||||
Write-Log "Intent: no tenant connected and no cached template categories for $($obj.templateId) - settings will not render" 2
|
||||
return
|
||||
}
|
||||
try {
|
||||
$headers = @{}
|
||||
if ($Context.Language -and $Context.Language -ne 'en') { $headers['Accept-Language'] = $Context.Language }
|
||||
$resp = Invoke-MSGraphAPI -Url "/deviceManagement/templates/$($obj.templateId)/categories?`$expand=settingDefinitions" -AdditionalHeaders $headers
|
||||
$categories = @($resp.Value)
|
||||
$Context.IntentCategories[$obj.templateId] = $categories
|
||||
}
|
||||
catch {
|
||||
Write-LogError "Intent: failed to fetch template categories for $($obj.templateId)" $_.Exception
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
# Per-object setting buffer (drained at the end into Context.SettingsData
|
||||
# in dependency-respecting order via Add-IntentSettingObjectToList).
|
||||
$script:_intentObjectSettings = [System.Collections.Generic.List[object]]::new()
|
||||
$script:_intentEmittedIds = @{}
|
||||
|
||||
foreach ($category in ($categories | Sort-Object -Property displayName)) {
|
||||
# Per-intent settings for this category (skipped when the input is an
|
||||
# offline file with .settings inlined).
|
||||
$settings = $null
|
||||
if ($obj.'@ObjectFromFile' -eq $true) {
|
||||
$settings = $obj.settings
|
||||
}
|
||||
elseif (-not $Context.SourceTenantUnavailable -and (Test-DocumentationGraphAvailable)) {
|
||||
# Source-tenant-specific: this intent's configured values by id (404s elsewhere).
|
||||
# Export path is the @ObjectFromFile branch above.
|
||||
try {
|
||||
$headers = @{}
|
||||
if ($Context.Language -and $Context.Language -ne 'en') { $headers['Accept-Language'] = $Context.Language }
|
||||
$resp = Invoke-MSGraphAPI -Url "/deviceManagement/intents/$($obj.Id)/categories/$($category.Id)/settings?`$expand=Microsoft.Graph.DeviceManagementComplexSettingInstance/Value" -AdditionalHeaders $headers
|
||||
$settings = $resp.Value
|
||||
}
|
||||
catch {
|
||||
Write-LogError "Intent: failed to fetch settings for intent=$($obj.Id) category=$($category.Id)" $_.Exception
|
||||
continue
|
||||
}
|
||||
}
|
||||
if (-not $settings) { continue }
|
||||
|
||||
# Recommended settings (template-level, also batch-cached per categoryId)
|
||||
if (-not $Context.IntentCatRecommendedSettings.ContainsKey($category.Id)) {
|
||||
# Template-level recommended settings (by templateId) are generic schema.
|
||||
if (Test-DocumentationGraphAvailable) {
|
||||
try {
|
||||
$headers = @{}
|
||||
if ($Context.Language -and $Context.Language -ne 'en') { $headers['Accept-Language'] = $Context.Language }
|
||||
$resp = Invoke-MSGraphAPI -Url "/deviceManagement/templates/$($obj.templateId)/categories/$($category.Id)/RecommendedSettings" -AdditionalHeaders $headers
|
||||
$Context.IntentCatRecommendedSettings[$category.Id] = @($resp.Value)
|
||||
}
|
||||
catch {
|
||||
Write-LogError "Intent: failed to fetch recommended settings for template=$($obj.templateId) category=$($category.Id)" $_.Exception
|
||||
$Context.IntentCatRecommendedSettings[$category.Id] = @()
|
||||
}
|
||||
}
|
||||
else {
|
||||
$Context.IntentCatRecommendedSettings[$category.Id] = @()
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($settingObj in $settings) {
|
||||
Get-IntentSettingInfo $settingObj $category $settingObj.definitionId $settings $Context | Out-Null
|
||||
}
|
||||
}
|
||||
|
||||
# Drain top-level settings (those with no parent and no dependencies).
|
||||
# Children/dependents get visited recursively by Add-IntentSettingObjectToList.
|
||||
$tops = $script:_intentObjectSettings | Where-Object {
|
||||
$null -eq $_.ParentId -and (($_.Dependencies | Measure-Object).Count -eq 0)
|
||||
}
|
||||
foreach ($s in $tops) {
|
||||
Add-IntentSettingObjectToList $s $Context
|
||||
}
|
||||
}
|
||||
|
||||
# Ordered emit: respects dependency constraints (parents resolve to permitted
|
||||
# values before dependent children are added) and recurses to children of any
|
||||
# emitted setting.
|
||||
function Add-IntentSettingObjectToList {
|
||||
param($objSetting, [DocumentationContext]$Context)
|
||||
|
||||
if ($script:_intentEmittedIds.ContainsKey([string]$objSetting.Id)) { return }
|
||||
|
||||
$passConstraint = $true
|
||||
$hasConstraint = $false
|
||||
foreach ($dependencyObj in $objSetting.SettingDefinition.dependencies) {
|
||||
$dependencyItemObj = $script:_intentObjectSettings | Where-Object { $_.SettingDefinition.Id -eq $dependencyObj.definitionId } | Select-Object -First 1
|
||||
if ($dependencyObj.constraints.Count -gt 0) {
|
||||
$hasConstraint = $true
|
||||
foreach ($constraint in $dependencyObj.constraints) {
|
||||
switch ($constraint.'@odata.type') {
|
||||
'#microsoft.graph.deviceManagementSettingBooleanConstraint' {
|
||||
if (($null -eq $dependencyItemObj.RawValue -and $constraint.value -eq $false) -or
|
||||
($dependencyItemObj.RawValue -and "$($dependencyItemObj.RawValue)" -ne "$($constraint.value)")) {
|
||||
$passConstraint = $false
|
||||
}
|
||||
}
|
||||
'#microsoft.graph.deviceManagementEnumConstraint' {
|
||||
if (-not ($constraint.values | Where-Object Value -EQ $dependencyItemObj.RawValue)) {
|
||||
$passConstraint = $false
|
||||
}
|
||||
}
|
||||
'#microsoft.graph.deviceManagementSettingIntegerConstraint' {
|
||||
# Old code inverts the comparison — passes when value is OUT of range.
|
||||
# Preserving the (buggy?) behavior for golden parity.
|
||||
if ($dependencyItemObj.RawValue -ge $constraint.minimumValue -and
|
||||
$dependencyItemObj.RawValue -le $constraint.maximumValue) {
|
||||
$passConstraint = $false
|
||||
}
|
||||
}
|
||||
}
|
||||
if (-not $passConstraint) { break }
|
||||
}
|
||||
}
|
||||
else {
|
||||
# No explicit constraint — dependency just has to be "set"
|
||||
$passConstraint = ($null -ne $dependencyItemObj.RawValue -and
|
||||
"$($dependencyItemObj.RawValue)" -ne 'NotConfigured' -and
|
||||
"$($dependencyItemObj.RawValue)" -ne 'False')
|
||||
}
|
||||
if (-not $passConstraint) { break }
|
||||
}
|
||||
|
||||
if (-not $passConstraint) { return }
|
||||
|
||||
if ($hasConstraint) { $objSetting.Level = $objSetting.Level + 1 }
|
||||
|
||||
# Attach recommended-value comparison (purely informational on the emitted row)
|
||||
$recommendedSetting = $Context.IntentCatRecommendedSettings[$objSetting.CategoryObject.Id] |
|
||||
Where-Object definitionId -EQ $objSetting.SettingId | Select-Object -First 1
|
||||
if ($recommendedSetting.valueJson -and ($objSetting.ValueSet -eq $false -or
|
||||
$recommendedSetting.valueJson -ne ($objSetting.RawValue | ConvertTo-Json -Depth 50 -Compress))) {
|
||||
$objSetting | Add-Member -MemberType NoteProperty -Name 'RecommendedValue' `
|
||||
-Value ($recommendedSetting.valueJson | ConvertFrom-Json) -Force
|
||||
}
|
||||
|
||||
$Context.AddSetting($objSetting)
|
||||
$script:_intentEmittedIds[[string]$objSetting.Id] = $true
|
||||
|
||||
if ($objSetting.ValueSet -eq $false) { return }
|
||||
|
||||
# Recurse: dependents (settings whose dependencies include this one)
|
||||
foreach ($depObj in ($script:_intentObjectSettings | Where-Object {
|
||||
$_.Dependencies.definitionId -eq $objSetting.SettingDefinition.Id
|
||||
})) {
|
||||
Add-IntentSettingObjectToList $depObj $Context
|
||||
}
|
||||
|
||||
# Recurse: children (settings with ParentId pointing at this one and no deps)
|
||||
foreach ($depObj in ($script:_intentObjectSettings | Where-Object {
|
||||
$_.ParentId -eq $objSetting.Id -and (($_.Dependencies | Measure-Object).Count -eq 0)
|
||||
})) {
|
||||
Add-IntentSettingObjectToList $depObj $Context
|
||||
}
|
||||
}
|
||||
|
||||
# Recursive setting parser. Builds a per-setting PSCustomObject with all the
|
||||
# metadata the emit step needs, pushes it onto $script:_intentObjectSettings,
|
||||
# and recurses into Complex / AbstractComplex / Collection children.
|
||||
function Get-IntentSettingInfo {
|
||||
param(
|
||||
$valueObj, $category, $defId, $allSettings, [DocumentationContext]$Context,
|
||||
[switch]$SkipConvertValue, [switch]$PassThru, $parentDef = $null
|
||||
)
|
||||
|
||||
$defObj = $category.settingDefinitions | Where-Object id -EQ $defId | Select-Object -First 1
|
||||
if (-not $defObj) { return }
|
||||
|
||||
$itemValue = $null
|
||||
$itemFullValue = $null
|
||||
|
||||
$rawValue = if ($SkipConvertValue) { $valueObj } else { $valueObj.valueJson | ConvertFrom-Json }
|
||||
|
||||
$valueSet = Get-IsIntentObjectConfigured $rawValue
|
||||
|
||||
if ($valueSet -eq $false) {
|
||||
# Skip child settings
|
||||
}
|
||||
elseif ($valueObj.'@odata.type' -eq '#microsoft.graph.deviceManagementCollectionSettingInstance' -or
|
||||
$defObj.'@odata.type' -eq '#microsoft.graph.deviceManagementComplexSettingDefinition' -or
|
||||
$defObj.valueType -eq 'collection') {
|
||||
$valueArr = @()
|
||||
$elementDefObj = if ($defObj.elementDefinitionId) {
|
||||
$category.settingDefinitions | Where-Object id -EQ $defObj.elementDefinitionId | Select-Object -First 1
|
||||
} else { $defObj }
|
||||
|
||||
if ($elementDefObj.propertyDefinitionIds) {
|
||||
# Each element is itself a record of N properties — emit the
|
||||
# FullValueTable so output providers can render it as a table.
|
||||
$itemFullValue = @()
|
||||
foreach ($tmpValue in $rawValue) {
|
||||
$htFullPropInfo = [ordered]@{}
|
||||
$arrValue = ''
|
||||
foreach ($propertyDefinitionId in $elementDefObj.propertyDefinitionIds) {
|
||||
$propDefObj = $category.settingDefinitions | Where-Object id -EQ $propertyDefinitionId | Select-Object -First 1
|
||||
if ($propDefObj.elementDefinitionId) {
|
||||
$propDefObj = $category.settingDefinitions | Where-Object id -EQ $propDefObj.elementDefinitionId | Select-Object -First 1
|
||||
}
|
||||
if ($arrValue) { $arrValue = $arrValue + $Context.PropertySeparator }
|
||||
$propName = $propertyDefinitionId.Split('_')[-1]
|
||||
$propValue = @()
|
||||
foreach ($childTmpValue in $tmpValue.$propName) {
|
||||
$propValue += Get-IntentObjectValue $propDefObj $childTmpValue
|
||||
}
|
||||
$colName = if ($propDefObj.displayName) { $propDefObj.displayName } else { $propName }
|
||||
$htFullPropInfo.Add($colName, $tmpValue.$propName)
|
||||
$arrValue = $arrValue + ($propValue -join $Context.PropertySeparator)
|
||||
}
|
||||
$itemFullValue += [PSCustomObject]$htFullPropInfo
|
||||
$valueArr += $arrValue
|
||||
}
|
||||
}
|
||||
elseif ($rawValue) {
|
||||
foreach ($tmpValue in $rawValue) {
|
||||
$valueArr += (Get-IntentObjectValue $elementDefObj $tmpValue)
|
||||
}
|
||||
}
|
||||
|
||||
if ($valueArr.Count -gt 0) {
|
||||
$itemValue = $valueArr -join $Context.ObjectSeparator
|
||||
}
|
||||
$valueSet = $valueArr.Count -gt 0
|
||||
}
|
||||
elseif ($valueObj.'@odata.type' -eq '#microsoft.graph.deviceManagementAbstractComplexSettingInstance' -or
|
||||
$defObj.'@odata.type' -eq '#microsoft.graph.deviceManagementAbstractComplexSettingDefinition') {
|
||||
$tmpDef = $category.settingDefinitions | Where-Object {
|
||||
$_.id -eq $rawValue.implementationId -or $_.id -eq $rawValue.'$implementationId'
|
||||
} | Select-Object -First 1
|
||||
if ($tmpDef) {
|
||||
$itemValue = $tmpDef.displayName
|
||||
}
|
||||
else {
|
||||
$valueSet = $false
|
||||
}
|
||||
}
|
||||
else {
|
||||
$itemValue = Get-IntentObjectValue $defObj $rawValue
|
||||
if (-not $itemValue) { $valueSet = $false }
|
||||
}
|
||||
|
||||
if ($valueSet -eq $false) {
|
||||
$itemValue = Get-LanguageString 'SettingDetails.notConfigured'
|
||||
$rawValue = $null
|
||||
}
|
||||
elseif (-not $itemValue) {
|
||||
$itemValue = $rawValue
|
||||
}
|
||||
|
||||
$curObjectInfo = [PSCustomObject]@{
|
||||
Name = $defObj.displayName
|
||||
Description = $defObj.description
|
||||
Category = $category.displayName
|
||||
CategoryDescription = $category.description
|
||||
CategoryObject = $category
|
||||
Value = $itemValue
|
||||
FullValueTable = $itemFullValue
|
||||
RawValue = $rawValue
|
||||
SettingDefinition = $defObj
|
||||
Dependencies = $defObj.dependencies
|
||||
ValueSet = $valueSet
|
||||
Id = [Guid]::NewGuid()
|
||||
ParentId = $null
|
||||
SettingId = $defObj.Id
|
||||
ParentSettingId = $parentDef.Id
|
||||
Level = 0
|
||||
}
|
||||
$script:_intentObjectSettings.Add($curObjectInfo)
|
||||
|
||||
if ($valueSet -eq $false) {
|
||||
# Skip children if value not set
|
||||
}
|
||||
elseif ($valueObj.'@odata.type' -eq '#microsoft.graph.deviceManagementComplexSettingInstance' -or
|
||||
$defObj.'@odata.type' -eq '#microsoft.graph.deviceManagementComplexSettingDefinition') {
|
||||
if ($valueObj.Value) {
|
||||
$isValueSet = $false
|
||||
if ($defObj.propertyDefinitionIds) {
|
||||
foreach ($childDefId in $defObj.propertyDefinitionIds) {
|
||||
$childSetting = $valueObj.Value | Where-Object DefinitionId -EQ $childDefId | Select-Object -First 1
|
||||
if ($childSetting) {
|
||||
$objValueInfo = Get-IntentSettingInfo $childSetting $category $childSetting.definitionId $allSettings $Context -PassThru -parentDef $defObj
|
||||
$objValueInfo.ParentId = $curObjectInfo.Id
|
||||
if (($objValueInfo.RawValue -is [bool] -and $objValueInfo.RawValue -eq $true) -or
|
||||
($objValueInfo.RawValue -is [string] -and -not [string]::IsNullOrEmpty($objValueInfo.RawValue) -and
|
||||
$objValueInfo.RawValue -ne 'notConfigured' -and -not [string]::IsNullOrEmpty($objValueInfo.Value)) -or
|
||||
($objValueInfo.RawValue -isnot [bool] -and $objValueInfo.RawValue -isnot [string])) {
|
||||
$isValueSet = $true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
foreach ($childSetting in $valueObj.Value) {
|
||||
$objValueInfo = Get-IntentSettingInfo $childSetting $category $childSetting.definitionId $allSettings $Context -PassThru -parentDef $defObj
|
||||
$objValueInfo.ParentId = $curObjectInfo.Id
|
||||
}
|
||||
$isValueSet = $true
|
||||
}
|
||||
}
|
||||
elseif ($rawValue -and $defObj.propertyDefinitionIds) {
|
||||
$isValueSet = $false
|
||||
$isDefault = $true
|
||||
foreach ($childDefId in $defObj.propertyDefinitionIds) {
|
||||
$propName = $childDefId.Split('_')[-1]
|
||||
$objValueInfo = Get-IntentSettingInfo $rawValue.$propName $category $childDefId $allSettings $Context -SkipConvertValue -PassThru -parentDef $defObj
|
||||
if ($objValueInfo.ValueSet -eq $true) { $isValueSet = $true }
|
||||
if ($objValueInfo.SettingDefinition.constraints -and
|
||||
$objValueInfo.SettingDefinition.constraints[0].'@odata.type' -eq '#microsoft.graph.deviceManagementEnumConstraint' -and
|
||||
($objValueInfo.SettingDefinition.constraints[0].values | Measure-Object).Count -gt 0) {
|
||||
if ($objValueInfo.SettingDefinition.constraints[0].values[0].value -ne $rawValue.$propName) {
|
||||
$isDefault = $false
|
||||
}
|
||||
}
|
||||
elseif ($objValueInfo.SettingDefinition.valueType -eq 'string') {
|
||||
if ($null -ne $rawValue.$propName) { $isDefault = $false }
|
||||
}
|
||||
elseif ($objValueInfo.SettingDefinition.valueType -eq 'boolean') {
|
||||
if ($false -ne $rawValue.$propName) { $isDefault = $false }
|
||||
}
|
||||
$objValueInfo.ParentId = $curObjectInfo.Id
|
||||
}
|
||||
if ($isDefault) { $isValueSet = $false }
|
||||
}
|
||||
else {
|
||||
$isValueSet = $false
|
||||
}
|
||||
|
||||
$curObjectInfo.Value = if ($isValueSet) { 'Configure' } else { Get-LanguageString 'SettingDetails.notConfigured' }
|
||||
$curObjectInfo.ValueSet = $isValueSet
|
||||
$curObjectInfo.FullValueTable = $null
|
||||
}
|
||||
elseif (($valueObj.'@odata.type' -eq '#microsoft.graph.deviceManagementAbstractComplexSettingInstance' -or
|
||||
$defObj.'@odata.type' -eq '#microsoft.graph.deviceManagementAbstractComplexSettingDefinition') -and
|
||||
$rawValue -and $tmpDef) {
|
||||
foreach ($childDefId in $tmpDef.propertyDefinitionIds) {
|
||||
$propName = $childDefId.Split('_')[-1]
|
||||
$objValueInfo = Get-IntentSettingInfo $rawValue.$propName $category $childDefId $allSettings $Context -SkipConvertValue -PassThru -parentDef $defObj
|
||||
$objValueInfo.ParentId = $curObjectInfo.Id
|
||||
}
|
||||
}
|
||||
|
||||
if ($PassThru) { $curObjectInfo }
|
||||
}
|
||||
|
||||
# Translates a raw setting value via its definition (enum / boolean / raw passthrough).
|
||||
function Get-IntentObjectValue {
|
||||
param($defObj, $rawValue)
|
||||
|
||||
if ($defObj.constraints.'@odata.type' -eq '#microsoft.graph.deviceManagementEnumConstraint') {
|
||||
$tmpOption = $defObj.constraints.Values | Where-Object value -EQ $rawValue | Select-Object -First 1
|
||||
if (-not $tmpOption -and $null -eq $rawValue) {
|
||||
# No defaultValue on the setting definition — fall back to first option.
|
||||
# Old-code wart preserved for golden parity.
|
||||
$tmpOption = $defObj.constraints.Values[0]
|
||||
}
|
||||
return $tmpOption.displayName
|
||||
}
|
||||
elseif ($defObj.valueType -eq 'boolean') {
|
||||
if ($rawValue -eq 'True') { return (Get-LanguageString 'SettingDetails.yes') }
|
||||
return $null
|
||||
}
|
||||
return $rawValue
|
||||
}
|
||||
|
||||
# Hook for custom "is configured?" rules. Old code always returns true; kept as
|
||||
# a function so type-specific overrides can be wired in later.
|
||||
function Get-IsIntentObjectConfigured {
|
||||
param($obj)
|
||||
return $true
|
||||
}
|
||||
|
||||
# Template-type to friendly category-name lookup. Used by BasicInfo "Type"
|
||||
# row when the input provider lands templateType resolution in v2; for now
|
||||
# only exported so handlers can reuse the mapping.
|
||||
function Get-IntentCategoryFromTemplateType {
|
||||
param([string]$TemplateType)
|
||||
|
||||
if (-not $TemplateType) {
|
||||
Write-Log 'Get-IntentCategoryFromTemplateType called with empty TemplateType' 2
|
||||
return $null
|
||||
}
|
||||
|
||||
# Captured before the prefix is stripped: whether the family was security-shaped
|
||||
# is what decides if failing to map it is worth reporting (see the default arm).
|
||||
$isSecurityFamily = $TemplateType.StartsWith('endpointSecurity') -or $TemplateType -match 'baseline'
|
||||
|
||||
if ($TemplateType.StartsWith('endpointSecurity')) {
|
||||
$TemplateType = $TemplateType.Substring(16)
|
||||
}
|
||||
|
||||
switch ($TemplateType) {
|
||||
'accountProtection' { return (Get-LanguageString 'SecurityTemplate.accountProtection') }
|
||||
'antivirus' { return (Get-LanguageString 'SecurityTemplate.antivirus') }
|
||||
'diskEncryption' { return (Get-LanguageString 'SecurityTemplate.diskEncryption') }
|
||||
'endpointDetectionReponse' { return (Get-LanguageString 'SecurityTemplate.eDR') }
|
||||
'attackSurfaceReduction' { return (Get-LanguageString 'SecurityTemplate.aSR') }
|
||||
'firewall' { return (Get-LanguageString 'SecurityTemplate.firewall') }
|
||||
{ $_ -in @('securityBaseline','baseline','advancedThreatProtectionSecurityBaseline','microsoftEdgeSecurityBaseline') } {
|
||||
return (Get-LanguageString 'Titles.securityBaselines')
|
||||
}
|
||||
# Not a security template, but it reaches this mapper the same way: the
|
||||
# Settings Catalog provider asks for a category name for every family it
|
||||
# documents, and the Apple ADE enrollment policies are this one. Without an
|
||||
# arm here the row read 'enrollmentConfiguration'. PolicySet.deviceEnrollment
|
||||
# is an existing key, so the label localizes with everything else.
|
||||
'enrollmentConfiguration' { return (Get-LanguageString 'PolicySet.deviceEnrollment') }
|
||||
default {
|
||||
# Only a security-shaped family is expected to resolve here. The Settings
|
||||
# Catalog provider (Get-IntentCategoryName) calls this for EVERY
|
||||
# templateFamily and documents the raw value when it does not map, so a
|
||||
# family like 'enrollmentConfiguration' is a normal outcome rather than a
|
||||
# problem - warning about it once per policy put a wall of yellow in the
|
||||
# log of any tenant with Apple ADE policies and buried the real signal.
|
||||
if ($isSecurityFamily) {
|
||||
Write-Log "Could not translate Intent Template type $TemplateType" 2
|
||||
}
|
||||
else {
|
||||
Write-LogDebug "No Intent category mapping for template family '$TemplateType'; documented as-is"
|
||||
}
|
||||
return $TemplateType
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Invoke-InitializeIntentInput
|
||||
@@ -0,0 +1,125 @@
|
||||
# Manifest input provider.
|
||||
#
|
||||
# Bridges the old Documentation\ObjectInfo\ "manifest" JSON files (a flat
|
||||
# array of property descriptors describing how to translate a policy
|
||||
# object). Different from the Profile provider's category files, which:
|
||||
# - Live alongside as <category>_<policyType>.json
|
||||
# - Wrap their section array under a key matching the file basename
|
||||
# - Are looked up via ObjectCategories.json (Get-PolicyObjectCategoryInfo)
|
||||
#
|
||||
# Manifest files instead are:
|
||||
# - Flat top-level arrays
|
||||
# - Named directly by @odata.type: <odata.type>.json
|
||||
# e.g. #microsoft.graph.hardwareConfiguration.json
|
||||
# - Or named by PolicyType Id: #<typeId>.json
|
||||
# e.g. #Applications.json, #Autopilot.json
|
||||
#
|
||||
# These files exist for ~21 PolicyTypes that aren't catalogued in
|
||||
# ObjectCategories.json (Applications, AppProtection, BIOS hardware
|
||||
# configs, EnrollmentLimit/Notification/StatusPage, WindowsUpdate
|
||||
# profiles, MacScripts, PowerShell/HealthScripts, etc.) so without this
|
||||
# provider every one of those types renders an empty HTML stub.
|
||||
#
|
||||
# Match order: this file's basename ("Manifest") sorts before "Profile"
|
||||
# so it gets first shot at types that aren't already claimed by a
|
||||
# DocHandler or one of the specific schema providers (ADMX/Compliance V2
|
||||
# /Intent/SettingsCatalog).
|
||||
#
|
||||
# Old code reference: Extensions/Documentation.psm1:268-284 (the dispatcher
|
||||
# branches that test File.Exists on the two filename forms) +
|
||||
# Extensions/Documentation.psm1:4016 (Invoke-TranslateCustomProfileObject —
|
||||
# the helper that loaded a flat array and called Invoke-TranslateSection).
|
||||
|
||||
function Invoke-InitializeManifestInput {
|
||||
Add-DocumentationInputProvider ([PSCustomObject]@{
|
||||
Name = 'Manifest'
|
||||
Order = 10
|
||||
Match = {
|
||||
param($PolicyObject)
|
||||
$path = Get-DocumentationManifestPath $PolicyObject
|
||||
return [bool]$path
|
||||
}
|
||||
Translate = { param($PolicyObject, $Context) Invoke-TranslateManifestPolicyObject $PolicyObject $Context }
|
||||
})
|
||||
}
|
||||
|
||||
# Looks up a manifest file for $PolicyObject. Returns the resolved path or
|
||||
# $null. Tries @odata.type first, then PolicyType.Id with '#' prefix.
|
||||
function Get-DocumentationManifestPath {
|
||||
param($PolicyObject)
|
||||
|
||||
$obj = $PolicyObject.JsonObject
|
||||
|
||||
$dir = Join-Path $script:AppRootFolder 'Config\ObjectInfo'
|
||||
|
||||
$odata = [string]$obj.'@odata.type'
|
||||
if ($odata) {
|
||||
$path = Join-Path $dir "$odata.json"
|
||||
if (Test-Path -LiteralPath $path -PathType Leaf) {
|
||||
Write-Log "Manifest input provider: Found file based on OData type: $path"
|
||||
return $path
|
||||
}
|
||||
}
|
||||
|
||||
$typeId = $null
|
||||
if ($PolicyObject.PSObject.Properties['PolicyType'] -and $PolicyObject.PolicyType) {
|
||||
$typeId = [string]$PolicyObject.PolicyType.Id
|
||||
}
|
||||
if ($typeId) {
|
||||
$path = Join-Path $dir "#$typeId.json"
|
||||
if (Test-Path -LiteralPath $path -PathType Leaf) {
|
||||
Write-Log "Manifest input provider: Found file based on PolicyType.Id: $path"
|
||||
return $path
|
||||
}
|
||||
}
|
||||
|
||||
return $null
|
||||
}
|
||||
|
||||
function Invoke-TranslateManifestPolicyObject {
|
||||
param($PolicyObject, [DocumentationContext]$Context)
|
||||
|
||||
$obj = $PolicyObject.JsonObject
|
||||
|
||||
$path = Get-DocumentationManifestPath $PolicyObject
|
||||
if (-not $path) { return }
|
||||
|
||||
# Header rows (matches Profile provider so output looks identical for
|
||||
# both code paths — manifest vs category-driven).
|
||||
Add-BasicDefaultValues $PolicyObject
|
||||
|
||||
# Add app name for apps
|
||||
$appType = Get-GraphApplicationType $PolicyObject
|
||||
if($appType)
|
||||
{
|
||||
$appTypeName = Get-LanguageString "AppType.$($appType.LanguageId)"
|
||||
if($appTypeName) { Add-BasicPropertyValue (Get-LanguageString "Inputs.installationSourceLabel") $appTypeName }
|
||||
}
|
||||
|
||||
$Context.CurrentObject = $obj
|
||||
Initialize-DocumentationObjectInfoObject $obj
|
||||
|
||||
try {
|
||||
$manifest = [IO.File]::ReadAllText($path) | ConvertFrom-Json
|
||||
} catch {
|
||||
Write-LogError "Failed to read manifest $path" $_.Exception
|
||||
return
|
||||
}
|
||||
|
||||
if (-not $manifest) { return }
|
||||
|
||||
# Manifest is a flat array (no per-file wrapper key), so pass it directly
|
||||
# to the walker. No $ObjInfo - the manifest doesn't come from
|
||||
# ObjectCategories.json.
|
||||
try {
|
||||
$Context.CurrentSubCategory = ''
|
||||
Invoke-TranslateSection $obj $manifest $null
|
||||
} catch {
|
||||
Write-LogError "Failed to translate manifest $(Split-Path -Leaf $path)" $_.Exception
|
||||
}
|
||||
|
||||
Add-BasicAdditionalValues $PolicyObject
|
||||
|
||||
}
|
||||
|
||||
Invoke-InitializeManifestInput
|
||||
@@ -0,0 +1,195 @@
|
||||
# Generic Profile input provider.
|
||||
#
|
||||
# Ported from old Extensions/Documentation.psm1:2057 (Invoke-TranslateProfile-
|
||||
# Object). Claims any @odata.type catalogued in Config/ObjectCategories.json
|
||||
# that isn't already handled by a more-specific input provider or handler
|
||||
# (first-match-wins dispatch in [DocumentationRegistry]).
|
||||
#
|
||||
# For each catalogued type:
|
||||
# 1. Emit BasicInfo via Add-BasicDefaultValues (which itself reads
|
||||
# ObjectCategories.json for Platform-supported + Profile-type rows)
|
||||
# 2. Emit Created/Modified/Version via Add-BasicAdditionalValues
|
||||
# 3. Find category files: either the explicit Categories list, or every
|
||||
# file matching *_<policyType>.json under Config/ObjectInfo/
|
||||
# 4. Load each as JSON, dispatch to Invoke-TranslateSection walker
|
||||
#
|
||||
# The walker handles all the per-prop dataType dispatching to translate
|
||||
# primitives (Boolean/Option/MultiOption/Table/Duration etc.).
|
||||
|
||||
function Invoke-InitializeProfileInput {
|
||||
Add-DocumentationInputProvider ([PSCustomObject]@{
|
||||
Name = 'Profile'
|
||||
Order = 60
|
||||
Match = {
|
||||
param($PolicyObject)
|
||||
$odata = $PolicyObject.JsonObject.'@odata.type'
|
||||
if (-not $odata) { return $false }
|
||||
if (-not (Get-Command Get-PolicyObjectCategoryInfo -ErrorAction SilentlyContinue)) { return $false }
|
||||
$info = Get-PolicyObjectCategoryInfo $odata
|
||||
return ($null -ne $info -and $null -ne $info.PolicyType)
|
||||
}
|
||||
Translate = { param($PolicyObject, $Context) Invoke-TranslateProfilePolicyObject $PolicyObject $Context }
|
||||
})
|
||||
}
|
||||
|
||||
function Invoke-TranslateProfilePolicyObject {
|
||||
param($PolicyObject, [DocumentationContext]$Context)
|
||||
|
||||
$obj = $PolicyObject.JsonObject
|
||||
|
||||
$objInfo = Get-PolicyObjectCategoryInfo $obj.'@odata.type'
|
||||
if (-not $objInfo) { return }
|
||||
|
||||
# Header rows
|
||||
Add-BasicDefaultValues $PolicyObject
|
||||
Add-BasicAdditionalValues $PolicyObject
|
||||
# Pin '@ObjectFromFile' so the walker's source-unavailable branches (linked
|
||||
# certificates etc.) treat the input as a file-based object even when
|
||||
# SourceTenantUnavailable isn't set.
|
||||
if (-not $obj.PSObject.Properties['@ObjectFromFile']) {
|
||||
$obj | Add-Member -MemberType NoteProperty -Name '@ObjectFromFile' -Value $true -Force
|
||||
}
|
||||
$Context.CurrentObject = $obj
|
||||
Initialize-DocumentationObjectInfoObject $obj
|
||||
|
||||
# Resolve the list of ObjectInfo JSON files to walk for this PolicyType
|
||||
$objectInfoDir = Join-Path $script:AppRootFolder 'Config\ObjectInfo'
|
||||
$allFiles = @()
|
||||
|
||||
if ($objInfo.Categories -and $objInfo.Categories.Count -gt 0) {
|
||||
foreach ($cat in $objInfo.Categories) {
|
||||
$path = Join-Path $objectInfoDir "$($cat.ToLower())_$($objInfo.PolicyType.ToLower()).json"
|
||||
if (Test-Path -LiteralPath $path) {
|
||||
$allFiles += [IO.FileInfo]$path
|
||||
}
|
||||
else {
|
||||
Write-Log "ObjectInfo file '$path' not found for $($objInfo.PolicyType)" 2
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
# Single-file path — find any *_<policyType>.json
|
||||
$pattern = "*_$($objInfo.PolicyType.ToLower()).json"
|
||||
if (Test-Path $objectInfoDir) {
|
||||
$files = Get-ChildItem -Path $objectInfoDir -Filter $pattern -ErrorAction SilentlyContinue
|
||||
if (-not $files) {
|
||||
Write-Log "No ObjectInfo files matching '$pattern' for $($objInfo.PolicyType)" 2
|
||||
}
|
||||
foreach ($f in $files) { $allFiles += $f }
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($fi in $allFiles) {
|
||||
try {
|
||||
$categoryObj = [IO.File]::ReadAllText($fi.FullName) | ConvertFrom-Json
|
||||
$Context.CurrentSubCategory = ''
|
||||
# Per-file custom handlers override the generic walker (old code:
|
||||
# Invoke-CDDocumentTranslateSectionFile, called via docProvider.
|
||||
# TranslateSectionFile hook from Invoke-TranslateProfileObject).
|
||||
# Returns $true if the custom handler emitted rows; $false to fall
|
||||
# through to Invoke-TranslateSection.
|
||||
if (Invoke-DocCustomSectionFileTranslator -Obj $obj -FileInfo $fi -CategoryObj $categoryObj -ObjInfo $objInfo) {
|
||||
continue
|
||||
}
|
||||
# Each ObjectInfo file wraps its section array under a key matching the file's basename
|
||||
$sections = $categoryObj."$($fi.BaseName)"
|
||||
if ($sections) {
|
||||
Invoke-TranslateSection $obj $sections $objInfo
|
||||
}
|
||||
}
|
||||
catch {
|
||||
Write-LogError "Failed to translate ObjectInfo file $($fi.Name)" $_.Exception
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Custom per-(odata.type, fileBaseName) section-file translators. Mirrors old
|
||||
# Extensions/DocumentationCustom.psm1's Invoke-CDDocumentTranslateSectionFile.
|
||||
# Each block returns $true if it emitted rows and the generic walker should be
|
||||
# skipped for this file, $false to fall through.
|
||||
function Invoke-DocCustomSectionFileTranslator {
|
||||
param($Obj, [IO.FileInfo]$FileInfo, $CategoryObj, $ObjInfo)
|
||||
|
||||
# --- Compliance: Custom Compliance category (Windows 10) ----------------
|
||||
# Generic walker can't emit useful rows for `customcompliance_compliancewindows10`
|
||||
# because the manifest's dataType=25 ("home screen", unused) is the child
|
||||
# carrying the actual content, and the rules live on a separate
|
||||
# $obj.deviceCompliancePolicyScript navigation property rather than on the
|
||||
# boolean entityKey the parent points to. Three rows are emitted by hand:
|
||||
# - Custom compliance (Require / Not configured)
|
||||
# - Select your discovery script (resolved displayName)
|
||||
# - Upload and validate the JSON file (base64-decoded rulesContent)
|
||||
if ($Obj.'@odata.type' -eq '#microsoft.graph.windows10CompliancePolicy' -and
|
||||
$FileInfo.BaseName -eq 'customcompliance_compliancewindows10') {
|
||||
|
||||
$category = Get-PolicyObjectCategoryString ($CategoryObj."$($FileInfo.BaseName)".category)
|
||||
|
||||
if ($null -eq $Obj.deviceCompliancePolicyScript) {
|
||||
$propValue = Get-LanguageString 'BooleanActions.notConfigured'
|
||||
$rawValue = 'notConfigured'
|
||||
} else {
|
||||
$propValue = Get-LanguageString 'BooleanActions.require'
|
||||
$rawValue = 'require'
|
||||
}
|
||||
Add-CustomSettingObject ([PSCustomObject]@{
|
||||
Name = Get-LanguageString 'SettingDetails.adminConfiguredComplianceSettingName'
|
||||
Value = $propValue
|
||||
EntityKey = 'deviceCompliancePolicyScript'
|
||||
RawValue = $rawValue
|
||||
Category = $category
|
||||
SubCategory = $null
|
||||
})
|
||||
|
||||
if ($Obj.deviceCompliancePolicyScript) {
|
||||
# Resolve script displayName via shared cache. Offline runs / cache
|
||||
# miss fall back to the script id so the row isn't blank.
|
||||
$scriptId = [string]$Obj.deviceCompliancePolicyScript.deviceComplianceScriptId
|
||||
$scriptName = $scriptId
|
||||
if (-not [string]::IsNullOrEmpty($scriptId)) {
|
||||
$cache = Get-CacheObject 'DocAllCustomCompliancePolicies'
|
||||
# Custom compliance scripts are authored in the source tenant (not
|
||||
# generic schema), so this is gated on source-tenant availability.
|
||||
if (-not $cache -and -not (Get-CurrentDocumentationContext).SourceTenantUnavailable -and (Test-DocumentationGraphAvailable)) {
|
||||
try {
|
||||
$cache = @((Invoke-MSGraphAPI -Url "/deviceManagement/deviceComplianceScripts?`$select=displayName,id" -ODataMetadata 'minimal').value)
|
||||
Set-CacheObject 'DocAllCustomCompliancePolicies' $cache
|
||||
} catch {
|
||||
Write-Log "Failed to fetch deviceComplianceScripts for resolution: $($_.Exception.Message)" 2
|
||||
}
|
||||
}
|
||||
if ($cache) {
|
||||
$match = $cache | Where-Object Id -EQ $scriptId | Select-Object -First 1
|
||||
if ($match.displayName) { $scriptName = $match.displayName }
|
||||
}
|
||||
}
|
||||
Add-CustomSettingObject ([PSCustomObject]@{
|
||||
Name = Get-LanguageString 'CustomCompliance.FilePicker.scriptFileLabel'
|
||||
Value = $scriptName
|
||||
EntityKey = 'deviceComplianceScriptName'
|
||||
Category = $category
|
||||
SubCategory = $null
|
||||
})
|
||||
|
||||
if ($Obj.deviceCompliancePolicyScript.rulesContent) {
|
||||
$rules = try {
|
||||
[System.Text.Encoding]::UTF8.GetString(
|
||||
[System.Convert]::FromBase64String($Obj.deviceCompliancePolicyScript.rulesContent))
|
||||
} catch {
|
||||
[string]$Obj.deviceCompliancePolicyScript.rulesContent
|
||||
}
|
||||
Add-CustomSettingObject ([PSCustomObject]@{
|
||||
Name = Get-LanguageString 'CustomCompliance.UploadFile.jsonFileLabel'
|
||||
Value = $rules
|
||||
EntityKey = 'jsonFileContent'
|
||||
Category = $category
|
||||
SubCategory = $null
|
||||
})
|
||||
}
|
||||
}
|
||||
return $true
|
||||
}
|
||||
|
||||
return $false
|
||||
}
|
||||
|
||||
Invoke-InitializeProfileInput
|
||||
@@ -0,0 +1,142 @@
|
||||
# Settings Catalog input provider.
|
||||
#
|
||||
# Ported from old Extensions/Documentation.psm1:1107 (Invoke-TranslateSettings-
|
||||
# Object, ~100 LOC). Claims @odata.type='#microsoft.graph.deviceManagement
|
||||
# ConfigurationPolicy' and translates the policy's settings via the recursive
|
||||
# walker (Add-SettingsSetting in SettingsCatalogWalker.ps1).
|
||||
#
|
||||
# Live Graph dependencies (resolved through Invoke-MSGraphAPI):
|
||||
# /deviceManagement/configurationPolicies/{id}/settings?$expand=settingDefinitions
|
||||
# /deviceManagement/configurationCategories?$filter=platforms has 'windows10' and technologies has 'mdm'
|
||||
# /deviceManagement/configurationSettings/{id} (per-setting fallback when defs aren't expanded)
|
||||
#
|
||||
# These are batch-cached on the [DocumentationContext] ($ctx.CfgCategories,
|
||||
# $ctx.CachedCfgSettings) so a bulk run pays the cost once. The per-policy
|
||||
# settings fetch (by id) is source-tenant-specific and skipped when
|
||||
# $ctx.SourceTenantUnavailable; the GENERIC schema (setting definitions via
|
||||
# the walker's configurationSettings/{id} fallback, and configurationCategories)
|
||||
# is still resolved from any connected tenant (Test-DocumentationGraphAvailable).
|
||||
# With no tenant at all the provider still runs, producing raw IDs.
|
||||
#
|
||||
# OFFLINE SMOKE TEST DEFERRED: golden-file validation against the provided
|
||||
# fixture (C:/Intune/OldDocumentation/SettingsCatalog/[Testing] Windows 11
|
||||
# Settings.json) needs the policy re-exported with $expand=settings($expand=
|
||||
# settingDefinitions) + a sidecar fixture for scope tags. Until then this
|
||||
# provider is exercised live against a tenant; its structure mirrors the old
|
||||
# code's so trust-the-port applies.
|
||||
|
||||
function Invoke-InitializeSettingsCatalogInput {
|
||||
Add-DocumentationInputProvider ([PSCustomObject]@{
|
||||
Name = 'SettingsCatalog'
|
||||
Order = 20
|
||||
Match = { param($PolicyObject) $PolicyObject.JsonObject.'@odata.type' -eq '#microsoft.graph.deviceManagementConfigurationPolicy' }
|
||||
Translate = { param($PolicyObject, $Context) Invoke-TranslateSettingsCatalogObject $PolicyObject $Context }
|
||||
})
|
||||
}
|
||||
|
||||
function Invoke-TranslateSettingsCatalogObject {
|
||||
param($PolicyObject, [DocumentationContext]$Context)
|
||||
|
||||
$obj = $PolicyObject.JsonObject
|
||||
|
||||
# --- BasicInfo header rows ---
|
||||
Add-BasicDefaultValues $PolicyObject
|
||||
Add-BasicPropertyValue (Get-LanguageString 'TableHeaders.configurationType') (Get-LanguageString 'ConfigurationTypes.settingsCatalog') '@odata.type'
|
||||
|
||||
if ($obj.templateReference.templateId) {
|
||||
Add-BasicPropertyValue (Get-LanguageString 'TableHeaders.Category') (Get-IntentCategoryName $obj.templateReference.templateFamily) 'templateFamily'
|
||||
Add-BasicPropertyValue (Get-LanguageString 'TableHeaders.policyType') $obj.templateReference.templateDisplayName 'templateDisplayName'
|
||||
}
|
||||
|
||||
if ($obj.platforms) {
|
||||
$platformType = Get-LanguageString "Platform.$($obj.platforms)"
|
||||
if ($platformType) {
|
||||
Add-BasicPropertyValue (Get-LanguageString 'SettingDetails.platformSupported') $platformType 'platforms'
|
||||
}
|
||||
}
|
||||
|
||||
Add-BasicAdditionalValues $PolicyObject
|
||||
# --- Settings ---
|
||||
# Prefer in-policy settings (export / hydrate with $expand=settings has them
|
||||
# inline). When settingDefinitions are not also inline — the hydrate body URL
|
||||
# only does `?$expand=Settings`, NOT `?$expand=Settings($expand=settingDefinitions)`
|
||||
# — the SettingsCatalog walker falls back to a sequential per-setting
|
||||
# /configurationSettings/{id} GET (one round-trip per settingInstance),
|
||||
# which scales linearly with setting count and crushes bulk-doc runs.
|
||||
# One enrich call per policy collapses that N+1 to a single per-policy call.
|
||||
$cfgSettings = @()
|
||||
if ($obj.Settings -and ($obj.Settings | Measure-Object).Count -gt 0) {
|
||||
$cfgSettings = @($obj.Settings)
|
||||
}
|
||||
|
||||
$hasDefs = $false
|
||||
foreach ($s in $cfgSettings) {
|
||||
if ($s.settingDefinitions -and ($s.settingDefinitions | Measure-Object).Count -gt 0) {
|
||||
$hasDefs = $true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
# Bulk runs: Initialize-DocumentationRunPrefetch already fetched these in
|
||||
# one Graph $batch — consume from the per-run cache (authoritative for this
|
||||
# run, even when empty, so an empty-settings policy doesn't trigger a
|
||||
# redundant live GET). The live GET below is the lazy fallback for the
|
||||
# single-policy Get-GraphDocumentation path.
|
||||
if (-not $hasDefs -and $Context.PrefetchedPolicySettings.ContainsKey([string]$obj.Id)) {
|
||||
$cfgSettings = @($Context.PrefetchedPolicySettings[[string]$obj.Id])
|
||||
$hasDefs = $true
|
||||
}
|
||||
|
||||
# Source-tenant-specific: fetches THIS policy's settings by id, which 404s on
|
||||
# any other tenant. Stays gated on -not SourceTenantUnavailable. When the
|
||||
# source is gone but the export carries settings inline (no defs), the walker's
|
||||
# generic per-setting configurationSettings/{id} fallback resolves the schema.
|
||||
if (-not $hasDefs -and -not $Context.SourceTenantUnavailable -and (Test-DocumentationGraphAvailable)) {
|
||||
try {
|
||||
$headers = @{}
|
||||
if ($Context.Language -and $Context.Language -ne 'en') {
|
||||
$headers['Accept-Language'] = $Context.Language
|
||||
}
|
||||
$resp = Invoke-MSGraphAPI -Url "/deviceManagement/configurationPolicies('$($obj.Id)')/settings?`$expand=settingDefinitions&`$top=1000" -AdditionalHeaders $headers -ODataMetadata 'minimal'
|
||||
if ($resp -and $resp.Value) {
|
||||
$cfgSettings = @($resp.Value)
|
||||
}
|
||||
}
|
||||
catch {
|
||||
Write-LogError "Failed to fetch settings for policy $($obj.Id)" $_.Exception
|
||||
}
|
||||
}
|
||||
|
||||
if ($cfgSettings.Count -eq 0) {
|
||||
Write-Log "SettingsCatalog: no settings to document for $($obj.name)" 2
|
||||
return
|
||||
}
|
||||
|
||||
# Schema caching, the walk and the (Category, SubCategory) grouping are shared
|
||||
# with the MAM app-configuration handler - see
|
||||
# Get-SettingsCatalogDocumentationRows in Core/SettingsCatalogWalker.ps1.
|
||||
foreach ($row in (Get-SettingsCatalogDocumentationRows $cfgSettings $Context)) {
|
||||
$Context.AddSetting($row)
|
||||
}
|
||||
|
||||
Invoke-DocumentationSettingsCatalogPostProcess $obj $Context
|
||||
}
|
||||
|
||||
# Settings Catalog uses an intent-style category mapping that's distinct from
|
||||
# Get-DocObjectTypeString (which is for group/category headers in the OUTPUT,
|
||||
# not for BasicInfo rows). Delegates to the Intent provider's
|
||||
# Get-IntentCategoryFromTemplateType (the port of old Documentation.psm1:1523
|
||||
# Get-IntentCategory), so endpoint-security-family catalogs show the localized
|
||||
# category name instead of the raw templateFamily (e.g. endpointSecurityAntivirus).
|
||||
function Get-IntentCategoryName {
|
||||
param($TemplateType)
|
||||
if (-not $TemplateType) { return '' }
|
||||
if (Get-Command Get-IntentCategoryFromTemplateType -ErrorAction SilentlyContinue) {
|
||||
$mapped = Get-IntentCategoryFromTemplateType $TemplateType
|
||||
if ($mapped) { return $mapped }
|
||||
}
|
||||
if ($TemplateType -is [string]) { return $TemplateType }
|
||||
return "$TemplateType"
|
||||
}
|
||||
|
||||
Invoke-InitializeSettingsCatalogInput
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user