Private/ConvertTo-InforcerSettingRows.ps1
|
function ConvertTo-InforcerSettingRows { <# .SYNOPSIS Recursively traverses a Settings Catalog settingInstance tree into flat Name/Value rows. .DESCRIPTION Handles all 5 settingInstance @odata.type variants: - choiceSettingInstance - simpleSettingInstance - simpleSettingCollectionInstance - groupSettingCollectionInstance - choiceSettingCollectionInstance Each output row is a [PSCustomObject] with exactly 4 properties: Name - friendly displayName resolved via Resolve-InforcerSettingName Value - resolved choice label, literal value, comma-joined collection, or '' for headers Indent - nesting depth (0 = top-level, increments for each recursive level) IsConfigured - $true for value-bearing rows, $false for group headers / unhandled types Unknown @odata.type values produce a warning and an "(unhandled type: ...)" value row. .PARAMETER SettingInstance A settingInstance object from a Settings Catalog policy (policyTypeId 10). .PARAMETER Depth Current nesting depth (recursion accumulator). Default 0. .EXAMPLE ConvertTo-InforcerSettingRows -SettingInstance $settingInstance #> [CmdletBinding()] param( [Parameter(Mandatory)] $SettingInstance, [Parameter()] [int]$Depth = 0 ) $rows = [System.Collections.Generic.List[object]]::new() $odataType = $SettingInstance.'@odata.type' $defId = $SettingInstance.settingDefinitionId $resolved = Resolve-InforcerSettingName -SettingDefinitionId $defId switch -Wildcard ($odataType) { '*choiceSettingInstance' { $csv = $SettingInstance.choiceSettingValue $choiceLabel = '' if ($csv -and $csv.value) { $choiceLabel = (Resolve-InforcerSettingName -SettingDefinitionId $defId -ChoiceValue $csv.value).ValueLabel } [void]$rows.Add([PSCustomObject]@{ Name = $resolved.DisplayName Value = $choiceLabel Indent = $Depth IsConfigured = $true DefinitionId = $defId }) if ($csv -and $csv.children) { foreach ($child in @($csv.children)) { if ($null -ne $child) { foreach ($r in (ConvertTo-InforcerSettingRows -SettingInstance $child -Depth ($Depth + 1))) { [void]$rows.Add($r) } } } } } '*simpleSettingInstance' { $value = $SettingInstance.simpleSettingValue.value [void]$rows.Add([PSCustomObject]@{ Name = $resolved.DisplayName Value = $value Indent = $Depth IsConfigured = $true DefinitionId = $defId }) } '*simpleSettingCollectionInstance' { $values = @($SettingInstance.simpleSettingCollectionValue | ForEach-Object { $_.value }) -join ', ' [void]$rows.Add([PSCustomObject]@{ Name = $resolved.DisplayName Value = $values Indent = $Depth IsConfigured = $true DefinitionId = $defId }) } '*groupSettingCollectionInstance' { [void]$rows.Add([PSCustomObject]@{ Name = $resolved.DisplayName Value = '' Indent = $Depth IsConfigured = $false DefinitionId = $defId }) foreach ($group in @($SettingInstance.groupSettingCollectionValue)) { if ($null -ne $group -and $group.children) { foreach ($child in @($group.children)) { if ($null -ne $child) { foreach ($r in (ConvertTo-InforcerSettingRows -SettingInstance $child -Depth ($Depth + 1))) { [void]$rows.Add($r) } } } } } } '*choiceSettingCollectionInstance' { foreach ($item in @($SettingInstance.choiceSettingCollectionValue)) { if ($null -ne $item) { $label = (Resolve-InforcerSettingName -SettingDefinitionId $defId -ChoiceValue $item.value).ValueLabel [void]$rows.Add([PSCustomObject]@{ Name = $resolved.DisplayName Value = $label Indent = $Depth IsConfigured = $true DefinitionId = $defId }) } } } default { Write-Warning "Unhandled settingInstance type: $odataType for '$defId'" [void]$rows.Add([PSCustomObject]@{ Name = $defId Value = "(unhandled type: $odataType)" Indent = $Depth IsConfigured = $false DefinitionId = $defId }) } } $rows } function ConvertTo-FriendlySettingName { <# .SYNOPSIS Converts camelCase property names to human-readable titles. .DESCRIPTION Splits camelCase/PascalCase identifiers into space-separated words with title casing. Preserves known acronyms (VPN, DNS, PIN, etc.) as uppercase. Names that already contain spaces are returned unchanged. .PARAMETER Name The camelCase property name to convert. .EXAMPLE ConvertTo-FriendlySettingName -Name 'activeHoursEnd' # Returns: Active Hours End #> [CmdletBinding()] param( [Parameter(Mandatory)] [string]$Name ) # Skip if already friendly (contains spaces) or is empty if ([string]::IsNullOrWhiteSpace($Name) -or $Name.Contains(' ')) { return $Name } # Known acronyms to preserve as uppercase $acronyms = @('MAM','MDM','VPN','DNS','URL','OMA','URI','SSID','WIFI','UUID','PIN','USB','DMA','TPM','VBA','MFA','AAD','DLP','OS','IP','ID','IT','UI','HTTP','HTTPS','API','SSO','SMS','OTP','TCP','UDP','SSL','TLS','LDAP','SCEP','PKCS','EAP','PEAP','WPA','WEP','NAT','DHCP','FQDN','IOS','MACOS','P2P') # Insert space before each uppercase letter that follows a lowercase letter # Also insert space before a run of uppercase followed by lowercase (e.g. "PINReset" -> "PIN Reset") $result = [System.Text.RegularExpressions.Regex]::Replace($Name, '(?<=[a-z])(?=[A-Z])', ' ') $result = [System.Text.RegularExpressions.Regex]::Replace($result, '(?<=[A-Z])(?=[A-Z][a-z])', ' ') # Title-case each word, then restore known acronyms $words = $result -split '\s+' $output = [System.Collections.Generic.List[string]]::new() foreach ($word in $words) { if ([string]::IsNullOrWhiteSpace($word)) { continue } $upper = $word.ToUpperInvariant() # Check if this word (case-insensitive) matches a known acronym $matched = $false foreach ($acr in $acronyms) { if ($upper -eq $acr) { [void]$output.Add($acr) $matched = $true break } } if (-not $matched) { # Title case: first letter upper, rest as-is (preserve casing in mixed words) [void]$output.Add($word.Substring(0,1).ToUpperInvariant() + $word.Substring(1)) } } return ($output -join ' ') } function ConvertTo-FlatSettingRows { <# .SYNOPSIS Enumerates a policyData object's properties as flat Name/Value rows. .DESCRIPTION Used for non-Settings Catalog policy types (policyTypeId != 10). Iterates over all properties of the policyData object, skipping reserved/metadata properties, and produces Name/Value rows. Nested PSObject values are recursed with incrementing Indent (up to depth 2 to avoid unbounded recursion on complex graph objects). Each output row is a [PSCustomObject] with 4 properties: Name, Value, Indent, IsConfigured Skipped property names: @odata.type, id, createdDateTime, lastModifiedDateTime, roleScopeTagIds, version, templateId, displayName, description, assignments, settings .PARAMETER PolicyData The policyData object from a non-catalog policy. May be $null (returns empty list). .PARAMETER Depth Current nesting depth (recursion accumulator). Default 0. .EXAMPLE ConvertTo-FlatSettingRows -PolicyData $policy.policyData #> [CmdletBinding()] param( [Parameter()] $PolicyData, [Parameter()] [int]$Depth = 0 ) $rows = [System.Collections.Generic.List[object]]::new() if ($null -eq $PolicyData) { return $rows } $skip = @( '@odata.type', '@odata.context', 'id', 'createdDateTime', 'lastModifiedDateTime', 'roleScopeTagIds', 'version', 'templateId', 'displayName', 'description', 'assignments', 'settings', 'name', 'deletedDateTime', 'policyGuid' ) foreach ($prop in $PolicyData.PSObject.Properties) { if ($prop.Name -in $skip) { continue } if ($prop.Name -match '@odata') { continue } # Skip internal flags (but keep linkedComplianceScript — it's rendered by MR card) if ($prop.Name -eq '_claimedByCompliancePolicy') { continue } if ($prop.Name -eq 'Length' -and $prop.Value -is [int]) { continue } $val = $prop.Value if ($val -is [PSObject] -and $val.PSObject.Properties.Count -gt 0 -and $Depth -lt 2) { [void]$rows.Add([PSCustomObject]@{ Name = (ConvertTo-FriendlySettingName -Name $prop.Name) Value = '' Indent = $Depth IsConfigured = $false }) foreach ($r in (ConvertTo-FlatSettingRows -PolicyData $val -Depth ($Depth + 1))) { [void]$rows.Add($r) } } elseif ($val -is [array] -and $val.Count -gt 0 -and $val[0] -is [PSObject] -and $Depth -lt 2) { # Array of objects — show count + extract display names (original behavior) # Special handling: recurse into scheduledActionConfigurations for compliance rules $propLower = $prop.Name.ToLowerInvariant() if ($propLower -eq 'scheduledactionsforrule' -or $propLower -eq 'scheduledactionconfigurations') { # Compliance: recurse to extract actionType, gracePeriodHours, rulesContent etc. [void]$rows.Add([PSCustomObject]@{ Name = (ConvertTo-FriendlySettingName -Name $prop.Name) Value = '' Indent = $Depth IsConfigured = $false }) foreach ($item in $val) { if ($item -is [PSObject]) { foreach ($r in (ConvertTo-FlatSettingRows -PolicyData $item -Depth ($Depth + 1))) { [void]$rows.Add($r) } } } } else { [void]$rows.Add([PSCustomObject]@{ Name = (ConvertTo-FriendlySettingName -Name $prop.Name) Value = "$($val.Count) items" Indent = $Depth IsConfigured = $true }) foreach ($item in $val) { if ($item -is [PSObject]) { $itemName = $null foreach ($nameField in @('displayName', 'name', 'id', 'bundleId', 'packageId')) { $nv = $item.PSObject.Properties[$nameField] if ($nv -and $nv.Value) { $itemName = $nv.Value.ToString(); break } } # Fallback: check nested mobileAppIdentifier (MAM app protection policies) if (-not $itemName) { $mai = $item.PSObject.Properties['mobileAppIdentifier'] if ($mai -and $mai.Value -is [PSObject]) { foreach ($nameField in @('bundleId', 'packageId')) { $nv2 = $mai.Value.PSObject.Properties[$nameField] if ($nv2 -and $nv2.Value) { $itemName = $nv2.Value.ToString(); break } } } } if ($itemName) { [void]$rows.Add([PSCustomObject]@{ Name = $itemName Value = '' Indent = $Depth + 1 IsConfigured = $true }) } } } } } else { $strVal = if ($null -eq $val) { '' } elseif ($val -is [array]) { $joined = @($val | ForEach-Object { if ($_ -is [string] -or $_ -is [ValueType]) { $_.ToString() } }) -join ', ' if ([string]::IsNullOrWhiteSpace($joined) -and $val.Count -gt 0) { "$($val.Count) items" } else { $joined } } else { $val.ToString() } # Decode base64-encoded content (scripts and rulesContent JSON) if ($prop.Name -match '(?i)scriptContent|detectionScriptContent|remediationScriptContent|rulesContent' -and $strVal -is [string] -and $strVal.Length -gt 20 -and $strVal -notmatch '\s') { try { $decoded = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($strVal)) $strVal = "__SCRIPT_CODE__$decoded" } catch { <# not valid base64, keep original #> } } [void]$rows.Add([PSCustomObject]@{ Name = (ConvertTo-FriendlySettingName -Name $prop.Name) Value = $strVal Indent = $Depth IsConfigured = $true }) } } $rows } |