# 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 (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