IntuneManagement 4.0.0-beta1

This commit is contained in:
Mikael Karlsson
2026-09-23 19:13:09 +10:00
commit 7869619510
892 changed files with 577109 additions and 0 deletions
@@ -0,0 +1,294 @@
# Android Managed Store App Configuration documentation handler.
#
# Ported from old Extensions/DocumentationCustom.psm1:1928. Claims
# @odata.type='#microsoft.graph.androidManagedStoreAppConfiguration' and
# (per the plan's batching) also the legacy androidForWorkMobileAppConfig
# variant since both have the same shape.
#
# Profile applicability translates to one of three workProfile/deviceOwner
# variants which becomes the "Profile type" basic-info value.
# Outlook ObjectInfo translation deferred until the walker is ported.
class AppConfigAndroidStoreDocHandler : DocumentationHandlerBase {
AppConfigAndroidStoreDocHandler() {
$this.ODataTypes = @(
'#microsoft.graph.androidManagedStoreAppConfiguration',
'#microsoft.graph.androidForWorkMobileAppConfiguration'
)
}
[void] Document([object]$PolicyObject, [DocumentationContext]$Context) {
$obj = $PolicyObject.JsonObject
# ProfileType maps to a localized "App configuration" suffix
$profileString = switch ($obj.profileApplicability) {
'default' { Get-LanguageString 'ProfileType.workProfileAndDeviceOwner' }
'androidWorkProfile' { Get-LanguageString 'ProfileType.workProfileOnly' }
'androidDeviceOwner' { Get-LanguageString 'ProfileType.deviceOwnerOnly' }
default { $null }
}
# Pass profileString as the Profile-type override so Add-BasicDefaultValues
# emits one (and only one) Profile type row matching old engine's pattern
# at DocumentationCustom.psm1:1955.
Add-BasicDefaultValues $PolicyObject $profileString
Add-BasicAdditionalValues $PolicyObject
# Targeted apps — resolved to displayNames when catalog available
$allApps = Get-CDAllTenantApps
$appsList = @()
foreach ($id in $obj.targetedMobileApps) {
$app = $allApps | Where-Object Id -EQ $id | Select-Object -First 1
$appsList += if ($app -and $app.displayName) { $app.displayName } else { $id }
}
Add-BasicPropertyValue (Get-LanguageString 'SettingDetails.targetedAppLabel') ($appsList -join $Context.ObjectSeparator) 'targetedMobileApps'
if ($obj.packageId) {
Add-BasicPropertyValue (Get-LanguageString 'SettingDetails.packageId') $obj.packageId 'packageId'
}
# appSupportsOemConfig is the discriminator the portal uses to split OEMConfig
# policies into their own blade - surface it so an OEMConfig policy isn't
# documented as an ordinary app configuration. NB: TableHeaders.configurationType
# renders as "Profile type" and would collide with the row above.
if ($obj.appSupportsOemConfig -eq $true) {
Add-BasicPropertyValue (Get-LanguageString 'TableHeaders.policyType') (Get-LanguageString 'ConfigurationTypes.androidForWorkOemConfig') 'appSupportsOemConfig'
}
# connectedAppsEnabled - "Connected apps" toggle. The portal offers
# Enabled / Not configured (not Enabled/Disabled).
$connKey = if ($obj.connectedAppsEnabled -eq $true) { 'Inputs.enabled' } else { 'Inputs.notConfigured' }
Add-BasicPropertyValue (Get-LanguageString 'SettingDetails.connectedApps') (Get-LanguageString $connKey) 'connectedAppsEnabled'
# credentialProviderRoleState - androidAppCredentialProviderRoleState enum
# (notConfigured / allowed only). The portal renders the same two options as
# the connected-apps toggle: Enabled / Not configured.
$credKeys = @{
'notConfigured' = 'Inputs.notConfigured'
'allowed' = 'Inputs.enabled'
}
$credRaw = "$($obj.credentialProviderRoleState)"
if ($credRaw) {
$credKey = $credKeys[$credRaw]
$credValue = if ($credKey) { Get-LanguageString $credKey -IgnoreMissing } else { $null }
if ([string]::IsNullOrEmpty($credValue)) { $credValue = $credRaw }
Add-BasicPropertyValue (Get-LanguageString 'SettingDetails.credentialProvider') $credValue 'credentialProviderRoleState'
}
if (-not $obj.payloadJson) { return }
$payloadData = $null
try {
$payloadData = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($obj.payloadJson)) | ConvertFrom-Json
}
catch {
Write-LogError 'Failed to parse Android managed-store payloadJson' $_.Exception
return
}
# Outlook gets schema-driven translation (port of old DocumentationCustom.psm1:1987-2005).
if ($obj.packageId -eq 'com.microsoft.office.outlook') {
$hasAccountType = @($payloadData.managedProperty | Where-Object { $_.key -eq 'com.microsoft.outlook.EmailProfile.AccountType' })
$outlookSettings = [PSCustomObject]@{ configureEmail = [bool]$hasAccountType }
foreach ($mp in @($payloadData.managedProperty)) {
$valueProp = $mp.PSObject.Properties | Where-Object Name -Like 'value*' | Select-Object -First 1
$outlookSettings | Add-Member -MemberType NoteProperty -Name $mp.key -Value $valueProp.Value -Force
}
Invoke-DocAppConfigManifest $outlookSettings (Join-Path (Join-Path $script:AppRootFolder 'Config\ObjectInfo') '#AppConfigOutlookDevice.json') $Context
}
# Outlook translation applied above; remaining managedProperty entries
# fall through to the additional-settings table.
$addedSettings = Get-DocumentedSettings
# Friendly names / descriptions / enum labels come from the app's own
# managed-configuration schema when a tenant is reachable.
$schema = Get-CDAndroidAppConfigSchema $obj.packageId
$additionalSettings = @()
$hasDescription = $false
foreach ($row in (Expand-AndroidManagedProperties $payloadData.managedProperty '' 0 $schema)) {
if ($addedSettings | Where-Object EntityKey -EQ $row.Key) { continue }
if ($row.Description) { $hasDescription = $true }
$additionalSettings += [PSCustomObject]@{
Name = $row.Name
Key = $row.Key
ValueType = $row.ValueType
Value = $row.Value
Description = $row.Description
EntityKey = $row.Key
Category = Get-LanguageString 'TACSettings.generalSettings'
SubCategory = Get-LanguageString 'SettingDetails.additionalConfiguration'
}
}
if ($additionalSettings.Count -gt 0) {
# Keep the raw key visible next to the friendly name, and only add the
# description column when the schema actually supplied any.
$columns = if ($hasDescription) { @('Name','Key','ValueType','Value','Description') } else { @('Name','Key','ValueType','Value') }
Add-CustomTable 'AdditionalSettings' $columns $additionalSettings -Order 110
}
# Permissions table. Portal grid is 4 columns: friendly name, permission
# state, raw permission name (prefix stripped) and permission group.
$permissions = @()
foreach ($p in $obj.permissionActions) {
$tail = $p.permission.Split('.')[-1]
$permissionStr = if ($tail) {
# Language ids drop the underscores (READ_CALENDAR -> readCalendar);
# PowerShell member lookup is case-insensitive so the raw upper-case
# form resolves too.
$lngId = $tail -replace '_',''
$resolved = Get-LanguageString "AndroidForWorkAppPermissions.Permissions.$lngId" -IgnoreMissing
if ($resolved) { $resolved } else { $tail }
} else { $p.permission }
$actionStr = $p.action
$resolvedAction = Get-LanguageString "AndroidForWorkAppPermissions.Action.$($p.action)" -IgnoreMissing
if ($resolvedAction) { $actionStr = $resolvedAction }
$permissions += [PSCustomObject]@{
Permission = $permissionStr
PermissionState = $actionStr
PermissionName = $tail
PermissionGroup = Get-AndroidPermissionGroup $tail
EntityKey = $p.permission
}
}
if ($permissions.Count -gt 0) {
Add-CustomTable 'Permissions' @('Permission','PermissionState','PermissionName','PermissionGroup') $permissions -Order 115 -LanguageId 'AndroidForWorkAppPermissions.permissionsTitle'
}
}
}
# Managed-configuration schema for one Managed Google Play app. The portal uses
# this to show friendly names instead of raw keys, a description column, the real
# data type (choice/multiselect/bundle...) and enum labels via `selections`.
#
# GET /deviceManagement/androidManagedStoreAppConfigurationSchemas('app:<packageId>')
#
# Returns a hashtable keyed by schemaItemKey. `nestedSchemaItems` carries the
# members of bundles/bundle arrays (linked to their parent by index/parentIndex),
# so nested leaves get friendly names too. Cached per run and per package; offline
# / source-unavailable returns an empty map and every caller degrades to raw keys.
function Get-CDAndroidAppConfigSchema {
param([string]$PackageId)
if (-not $PackageId) { return @{} }
$ctx = Get-CurrentDocumentationContext
if (-not $ctx.PSObject.Properties['_AndroidAppConfigSchemas']) {
$ctx | Add-Member -MemberType NoteProperty -Name '_AndroidAppConfigSchemas' -Value (@{}) -Force
}
if ($ctx._AndroidAppConfigSchemas.ContainsKey($PackageId)) { return $ctx._AndroidAppConfigSchemas[$PackageId] }
$map = @{}
# The schema is generic app metadata (same on every tenant), so this is gated on
# connectivity only - not on SourceTenantUnavailable.
if (Test-DocumentationGraphAvailable) {
try {
$url = "/deviceManagement/androidManagedStoreAppConfigurationSchemas('app:$PackageId')"
$resp = Invoke-MSGraphAPI -Url $url -ODataMetadata 'minimal' -NoError
foreach ($item in @($resp.schemaItems) + @($resp.nestedSchemaItems)) {
if ($item.schemaItemKey -and -not $map.ContainsKey($item.schemaItemKey)) {
$map[$item.schemaItemKey] = $item
}
}
}
catch {
Write-LogError "Failed to load Android app configuration schema for $PackageId" $_.Exception
}
}
$ctx._AndroidAppConfigSchemas[$PackageId] = $map
return $map
}
# Android permission -> permission group. Groups are Android platform constants
# (not localized - the portal renders them verbatim in its 4th grid column).
$script:_androidPermissionGroups = @{
'READ_CALENDAR' = 'CALENDAR'; 'WRITE_CALENDAR' = 'CALENDAR'
'CAMERA' = 'CAMERA'
'READ_CONTACTS' = 'CONTACTS'; 'WRITE_CONTACTS' = 'CONTACTS'; 'GET_ACCOUNTS' = 'CONTACTS'
'ACCESS_FINE_LOCATION' = 'LOCATION'; 'ACCESS_COARSE_LOCATION' = 'LOCATION'; 'ACCESS_BACKGROUND_LOCATION' = 'LOCATION'
'RECORD_AUDIO' = 'MICROPHONE'
'READ_PHONE_STATE' = 'PHONE'; 'CALL_PHONE' = 'PHONE'; 'READ_CALL_LOG' = 'PHONE'; 'WRITE_CALL_LOG' = 'PHONE'
'ADD_VOICEMAIL' = 'PHONE'; 'USE_SIP' = 'PHONE'; 'PROCESS_OUTGOING_CALLS' = 'PHONE'
'BODY_SENSORS' = 'SENSORS'; 'BODY_SENSORS_BACKGROUND' = 'SENSORS'
'SEND_SMS' = 'SMS'; 'RECEIVE_SMS' = 'SMS'; 'READ_SMS' = 'SMS'; 'RECEIVE_WAP_PUSH' = 'SMS'; 'RECEIVE_MMS' = 'SMS'
'READ_EXTERNAL_STORAGE' = 'STORAGE'; 'WRITE_EXTERNAL_STORAGE' = 'STORAGE'
'POST_NOTIFICATIONS' = 'NOTIFICATIONS'
'READ_MEDIA_VIDEO' = 'MEDIA'; 'READ_MEDIA_IMAGES' = 'MEDIA'; 'READ_MEDIA_AUDIO' = 'MEDIA'
'BLUETOOTH_CONNECT' = 'DEVICES'; 'NEARBY_WIFI_DEVICES' = 'DEVICES'; 'NEARBY_DEVICES' = 'DEVICES'
}
function Get-AndroidPermissionGroup {
param([string]$PermissionName)
if (-not $PermissionName) { return $null }
$script:_androidPermissionGroups[$PermissionName]
}
# Flatten a Google managed-configuration `managedProperty` array into one row per
# LEAF value. The payload supports six value shapes, two of which nest without
# bound (portal JSON-editor schema: valueBool / valueInteger / valueString /
# valueStringArray / valueBundle / valueBundleArray):
#
# valueBundle -> { managedProperty: [ ... ] } rendered as "parent.child"
# valueBundleArray -> [ { managedProperty: [...] }, ... ] rendered as "parent[0].child"
#
# Without this, a bundle rendered as the PowerShell object stringification and a
# bundle array rendered as a bare "," (the -join of an array of objects).
function Expand-AndroidManagedProperties {
param($ManagedProperties, [string]$Prefix = '', [int]$Depth = 0, $Schema = @{})
if ($Depth -gt 10) {
Write-Log 'Android app config: managedProperty nesting deeper than 10 levels; remaining levels not documented' 2
return
}
$ctx = Get-CurrentDocumentationContext
foreach ($mp in @($ManagedProperties)) {
if (-not $mp) { continue }
$key = if ($Prefix) { "$Prefix$($mp.key)" } else { [string]$mp.key }
$valueProp = $mp.PSObject.Properties | Where-Object Name -Like 'value*' | Select-Object -First 1
if (-not $valueProp) { continue }
# Schema is keyed by the app's own schemaItemKey, not by our dotted path
$schemaItem = $Schema[[string]$mp.key]
switch ($valueProp.Name) {
'valueBundle' {
Expand-AndroidManagedProperties $valueProp.Value.managedProperty "$key." ($Depth + 1) $Schema
}
'valueBundleArray' {
$idx = 0
foreach ($bundle in @($valueProp.Value)) {
Expand-AndroidManagedProperties $bundle.managedProperty "$key[$idx]." ($Depth + 1) $Schema
$idx++
}
}
default {
$val = $valueProp.Value
# choice / multiselect store the selection VALUE; the schema carries
# the friendly name for each in `selections`
if ($schemaItem.selections) {
$val = @($val | ForEach-Object {
$raw = $_
$sel = $schemaItem.selections | Where-Object { "$($_.value)" -eq "$raw" } | Select-Object -First 1
if ($sel.name) { $sel.name } else { $raw }
})
}
if ($val -is [array]) { $val = $val -join $ctx.ObjectSeparator }
[PSCustomObject]@{
Key = $key
Name = ?? $schemaItem.displayName $key
ValueType = if ($schemaItem.dataType) { $schemaItem.dataType } else { $valueProp.Name.Substring(5) }
Value = $val
Description = $schemaItem.description
}
}
}
}
}
[DocumentationRegistry]::RegisterHandler([AppConfigAndroidStoreDocHandler]::new())
@@ -0,0 +1,192 @@
# iOS Mobile App Configuration documentation handler.
#
# Ported from old Extensions/DocumentationCustom.psm1:2065. Claims
# @odata.type='#microsoft.graph.iosMobileAppConfiguration'.
#
# Two main paths:
# 1. iOS plist (base64'd encodedSettingXml) — parsed offline, key/value/type
# rows emitted directly
# 2. settings collection (Outlook-specific or generic appConfig key/value)
# The Outlook ObjectInfo translation needs the ObjectInfo JSON walker
# (deferred); offline we fall through to raw key=value rows under the
# generic "Additional configuration" subcategory.
class AppConfigMobileAppDocHandler : DocumentationHandlerBase {
AppConfigMobileAppDocHandler() {
$this.ODataTypes = @('#microsoft.graph.iosMobileAppConfiguration')
}
[void] Document([object]$PolicyObject, [DocumentationContext]$Context) {
$obj = $PolicyObject.JsonObject
Add-BasicDefaultValues $PolicyObject
Add-BasicAdditionalValues $PolicyObject
Add-BasicPropertyValue (Get-LanguageString 'TableHeaders.configurationType') (Get-LanguageString 'SettingDetails.appConfiguration') '@odata.type'
Add-BasicPropertyValue (Get-LanguageString 'Inputs.enrollmentTypeLabel') (Get-LanguageString 'EnrollmentType.devicesWithEnrollment') 'enrollmentType'
$platformId = Get-ObjectPlatformFromType $obj
if ($platformId) {
Add-BasicPropertyValue (Get-LanguageString 'Inputs.platformLabel') (Get-LanguageString "Platform.$platformId") 'platform'
}
# Targeted apps — resolve IDs to displayNames when the tenant catalog is
# available, otherwise emit raw IDs.
$allApps = Get-CDAllTenantApps
$appsList = @()
foreach ($id in $obj.targetedMobileApps) {
$app = $allApps | Where-Object Id -EQ $id | Select-Object -First 1
$appsList += if ($app -and $app.displayName) { $app.displayName } else { $id }
}
Add-BasicPropertyValue (Get-LanguageString 'SettingDetails.targetedAppLabel') ($appsList -join $Context.ObjectSeparator) 'targetedMobileApps'
$category = Get-LanguageString 'TableHeaders.settings'
if ($obj.encodedSettingXml) {
# iOS plist. The portal emits a bare <dict> root but Graph also accepts a
# <plist> wrapper, and the portal's validator explicitly allows nested
# <dict>/<array> values - so the walk has to recurse.
$xml = $null
try {
$xml = [xml]([System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($obj.encodedSettingXml)))
}
catch {
Write-LogError 'Failed to convert iOS encodedSettingXml to XML' $_.Exception
return
}
$rootDict = if ($xml.dict) { $xml.dict } elseif ($xml.plist.dict) { $xml.plist.dict } else { $null }
if (-not $rootDict) {
Write-Log 'iOS app config: encodedSettingXml has no <dict> root; no settings documented' 2
return
}
$plistRows = @(Expand-IosPlistDictionary $rootDict)
# ValueType is not part of the default documentation properties, so also
# emit the portal's 3-column grid (key / value type / value).
if ($plistRows.Count -gt 0) {
$typeRows = foreach ($row in $plistRows) {
[PSCustomObject]@{
ConfigurationKey = $row.Key
ValueType = Get-AppConfigValueTypeName $row.ValueType
ConfigurationValue = $row.Value
EntityKey = $row.Key
}
}
Add-CustomTable 'AppConfigSettings' @('ConfigurationKey','ValueType','ConfigurationValue') $typeRows -Order 110 -LanguageId 'TableHeaders.settings'
}
return
}
# Outlook gets schema-driven translation (port of old DocumentationCustom.psm1:2141-2176).
$isOutlook = $false
foreach ($id in $obj.targetedMobileApps) {
$app = $allApps | Where-Object Id -EQ $id | Select-Object -First 1
if ($app.displayName -eq 'Microsoft Outlook') { $isOutlook = $true; break }
}
if (-not $isOutlook -and @($obj.settings | Where-Object { $_.appConfigKey -like 'com.microsoft.outlook*' })) { $isOutlook = $true }
if ($isOutlook) {
$hasAccountType = @($obj.settings | Where-Object { $_.appConfigKey -eq 'com.microsoft.outlook.EmailProfile.AccountType' })
$outlookSettings = [PSCustomObject]@{ configureEmail = [bool]$hasAccountType }
foreach ($setting in @($obj.settings)) {
$val = if ($setting.appConfigKeyType -eq 'booleanType') { $setting.appConfigKeyValue -eq 'true' } else { $setting.appConfigKeyValue }
$outlookSettings | Add-Member -MemberType NoteProperty -Name $setting.appConfigKey -Value $val -Force
}
Invoke-DocAppConfigManifest $outlookSettings (Join-Path (Join-Path $script:AppRootFolder 'Config\ObjectInfo') '#AppConfigOutlookDevice.json') $Context
}
# Remaining settings fall through to raw key=value rows under the
# "Additional configuration" subcategory.
$addedSettings = Get-DocumentedSettings
$languageTitleId = 'TableHeaders.settings'
$typeRows = @()
foreach ($setting in $obj.settings) {
if ($addedSettings | Where-Object EntityKey -EQ $setting.appConfigKey) {
$languageTitleId = 'SettingDetails.additionalConfiguration'
continue
}
# The portal grid shows the value TYPE as its own column; keep that
# (a tokenType value like {{userprincipalname}} is not a literal string).
$typeRows += [PSCustomObject]@{
ConfigurationKey = $setting.appConfigKey
ValueType = Get-AppConfigValueTypeName $setting.appConfigKeyType
ConfigurationValue = $setting.appConfigKeyValue
EntityKey = $setting.appConfigKey
}
}
if ($typeRows.Count -gt 0) {
Add-CustomTable 'AppConfigSettings' @('ConfigurationKey','ValueType','ConfigurationValue') $typeRows -Order 110 -LanguageId $languageTitleId
}
}
}
# Localized name for an app-config value type. Accepts both the Graph
# mdmAppConfigKeyType members (stringType/integerType/realType/booleanType/
# tokenType) and the bare plist element names (string/integer/real/boolean/...).
# tokenType has no language string - the portal shows it blank, so the raw value
# is a strict improvement.
function Get-AppConfigValueTypeName {
param([string]$ValueType)
if (-not $ValueType) { return $null }
$key = switch -Regex ($ValueType) {
'^string' { 'SettingDetails.string' }
'^integer' { 'SettingDetails.integer' }
'^real' { 'SettingDetails.real' }
'^boolean' { 'SettingDetails.boolean' }
default { $null }
}
if (-not $key) { return $ValueType }
$value = Get-LanguageString $key -IgnoreMissing
if ([string]::IsNullOrEmpty($value)) { $ValueType } else { $value }
}
# Flatten an iOS plist <dict> into one row per LEAF value. Nested containers are
# addressed the way the plist itself addresses them:
# <dict> -> "parent.child"
# <array> -> "parent[0]"
# Without this, a nested container produced an EMPTY value (.'#text' on an element
# with element children returns nothing) and the payload was silently lost.
function Expand-IosPlistDictionary {
param($DictNode, [string]$Prefix = '', [int]$Depth = 0)
if ($Depth -gt 10) {
Write-Log 'iOS app config: plist nesting deeper than 10 levels; remaining levels not documented' 2
return
}
$children = @($DictNode.ChildNodes)
for ($i = 0; $i -lt $children.Count; $i++) {
if ($children[$i].Name -ne 'key') { continue }
$name = $children[$i].'#text'
$i++
if ($i -ge $children.Count) { break }
$valueNode = $children[$i]
$key = if ($Prefix) { "$Prefix$name" } else { [string]$name }
switch ($valueNode.Name) {
'true' { [PSCustomObject]@{ Key = $key; ValueType = 'boolean'; Value = 'true' } }
'false' { [PSCustomObject]@{ Key = $key; ValueType = 'boolean'; Value = 'false' } }
'dict' { Expand-IosPlistDictionary $valueNode "$key." ($Depth + 1) }
'array' {
$idx = 0
foreach ($item in @($valueNode.ChildNodes)) {
$itemKey = "$key[$idx]"
if ($item.Name -eq 'dict') { Expand-IosPlistDictionary $item "$itemKey." ($Depth + 1) }
elseif ($item.Name -eq 'true' -or $item.Name -eq 'false') {
[PSCustomObject]@{ Key = $itemKey; ValueType = 'boolean'; Value = $item.Name }
}
else {
[PSCustomObject]@{ Key = $itemKey; ValueType = $item.Name; Value = $item.'#text' }
}
$idx++
}
}
default { [PSCustomObject]@{ Key = $key; ValueType = $valueNode.Name; Value = $valueNode.'#text' } }
}
}
}
[DocumentationRegistry]::RegisterHandler([AppConfigMobileAppDocHandler]::new())
@@ -0,0 +1,48 @@
# Assignment Filter documentation handler.
#
# Ported from old Extensions/DocumentationCustom.psm1:3823 (Invoke-CDDocument-
# AssignmentFilter, ~35 LOC). Claims @odata.type='#microsoft.graph.deviceAnd
# AppManagementAssignmentFilter' and produces a 7-row BasicInfo + 1-row
# Settings table (the rule syntax).
#
# Platform value: app-management platforms (androidMobileApplicationManagement
# etc.) resolve to empty strings in Strings-en.json which Get-LanguageString
# returns as $null — so BasicInfo emits the row with Value=null, matching the
# golden's `"Platform": null` for app filters.
class AssignmentFilterDocHandler : DocumentationHandlerBase {
AssignmentFilterDocHandler() {
$this.ODataTypes = @('#microsoft.graph.deviceAndAppManagementAssignmentFilter')
}
[void] Document([object]$PolicyObject, [DocumentationContext]$Context) {
$obj = $PolicyObject.JsonObject
# BasicInfo order: Name, Description, Created, Last modified, Profile type, Platform
# (Scope tags appended automatically by the engine's post-step.)
Add-BasicDefaultValues $PolicyObject
Add-BasicAdditionalValues $PolicyObject
Add-BasicPropertyValue (Get-LanguageString 'TableHeaders.configurationType') (Get-LanguageString 'Filters.filters') '@odata.type'
Add-BasicPropertyValue (Get-LanguageString 'Inputs.platformLabel') (Get-LanguageString "Platform.$($obj.platform)") 'platform'
# Filter scope: devices vs apps. Disambiguates app-management filters, whose
# platform row resolves to null (see header note).
$mgmtType = switch ($obj.assignmentFilterManagementType) {
'devices' { Get-LanguageString 'Titles.devices' }
'apps' { Get-LanguageString 'Titles.apps' }
default { $obj.assignmentFilterManagementType }
}
Add-BasicPropertyValue (Get-LanguageString 'SettingDetails.managementType') $mgmtType 'assignmentFilterManagementType'
# Settings: a single Rule syntax row
Add-CustomSettingObject ([PSCustomObject]@{
Name = Get-LanguageString 'Filters.ruleSyntax'
Value = $obj.rule
EntityKey = 'rule'
Category = Get-LanguageString 'SettingDetails.rules'
SubCategory = $null
})
}
}
[DocumentationRegistry]::RegisterHandler([AssignmentFilterDocHandler]::new())
@@ -0,0 +1,44 @@
# Authentication Context documentation handler.
#
# Claims @odata.type='#microsoft.graph.authenticationContextClassReference'. Auth
# contexts (c1..c99) were previously only referenced by Conditional Access policies
# (ID -> displayName, see ConditionalAccessDocHandler); this handler documents the
# standalone object: display name, description and whether it is published to apps
# (isAvailable).
#
# Note: AuthenticationContextType strips @odata.type on export (_PropertiesToRemove),
# so this handler matches live documentation runs. File-based runs of an exported
# auth context lose the discriminator and fall through to NoProvider - a pre-existing
# export-cleanup limitation, not addressed here.
class AuthenticationContextDocHandler : DocumentationHandlerBase {
AuthenticationContextDocHandler() {
$this.ODataTypes = @('#microsoft.graph.authenticationContextClassReference')
}
[void] Document([object]$PolicyObject, [DocumentationContext]$Context) {
$obj = $PolicyObject.JsonObject
# No plain-noun language string exists for the auth-context type, so use the
# PolicyType title for the Profile type row.
$nameValue = if ($obj.displayName) { $obj.displayName } else { $PolicyObject.Name }
Add-BasicPropertyValue (Get-LanguageString 'SettingDetails.nameName') $nameValue 'displayName'
Add-BasicPropertyValue (Get-LanguageString 'TableHeaders.configurationType') $PolicyObject.PolicyType.Title '@odata.type'
if ($obj.description) {
Add-BasicPropertyValue (Get-LanguageString 'SettingDetails.descriptionName') $obj.description 'description'
}
if ($null -ne $obj.isAvailable) {
$availKey = if ($obj.isAvailable -eq $true) { 'Inputs.enabled' } else { 'Inputs.disabled' }
Add-CustomSettingObject ([PSCustomObject]@{
Name = (Get-LanguageString 'AzureCA.AuthContext.InfoBlade.publishLabel')
Value = (Get-LanguageString $availKey)
Category = $null
SubCategory = $null
EntityKey = 'isAvailable'
})
}
}
}
[DocumentationRegistry]::RegisterHandler([AuthenticationContextDocHandler]::new())
@@ -0,0 +1,76 @@
# Authentication Strength documentation handler.
#
# Claims @odata.type='#microsoft.graph.authenticationStrengthPolicy'. Authentication
# strengths were previously only referenced as a Conditional Access grant control
# (ID -> displayName, see ConditionalAccessDocHandler); this handler documents the
# standalone policy object.
#
# The policy's substance is allowedCombinations: an OR-list of method combinations,
# where each combination is an AND-set of authentication methods (comma-joined in
# the raw value, e.g. "password,microsoftAuthenticatorPush"). Each method maps to
# AzureCA.AuthenticationStrength.Mode.<method>. A few methods (federatedMultiFactor,
# federatedSingleFactor) have no Mode string yet, so fall back to a humanised token
# rather than emit a raw enum value.
class AuthenticationStrengthDocHandler : DocumentationHandlerBase {
AuthenticationStrengthDocHandler() {
$this.ODataTypes = @('#microsoft.graph.authenticationStrengthPolicy')
}
[void] Document([object]$PolicyObject, [DocumentationContext]$Context) {
$obj = $PolicyObject.JsonObject
# BasicInfo: Name + Profile type + Description
$nameValue = if ($obj.displayName) { $obj.displayName } else { $PolicyObject.Name }
Add-BasicPropertyValue (Get-LanguageString 'SettingDetails.nameName') $nameValue 'displayName'
Add-BasicPropertyValue (Get-LanguageString 'TableHeaders.configurationType') (Get-LanguageString 'AzureCA.WhatIfBlade.authenticationStrength') '@odata.type'
if ($obj.description) {
Add-BasicPropertyValue (Get-LanguageString 'SettingDetails.descriptionName') $obj.description 'description'
}
# allowedCombinations: OR-list of AND-combinations. Render each combination
# as "Method A + Method B" on its own line.
$comboLines = @()
foreach ($combo in @($obj.allowedCombinations)) {
if ([string]::IsNullOrWhiteSpace($combo)) { continue }
$methodNames = @()
foreach ($method in ($combo -split ',')) {
$m = $method.Trim()
if (-not $m) { continue }
$methodNames += (Get-AuthenticationMethodLabel $m)
}
if ($methodNames.Count -gt 0) {
$comboLines += ($methodNames -join ' + ')
}
}
if ($comboLines.Count -gt 0) {
Add-CustomSettingObject ([PSCustomObject]@{
Name = (Get-LanguageString 'AzureCA.policyControlAuthenticationStrengthDisplayedName')
Value = ($comboLines -join $Context.ObjectSeparator)
Category = $null
SubCategory = $null
EntityKey = 'allowedCombinations'
})
}
}
}
# Map an authentication-method enum token to its localised label, falling back to
# a humanised form (federatedMultiFactor -> "Federated Multi Factor") for tokens
# that have no Mode string. -IgnoreMissing keeps the log clean for known gaps.
function Get-AuthenticationMethodLabel {
param([string]$Method)
if ([string]::IsNullOrEmpty($Method)) { return $Method }
$label = Get-LanguageString "AzureCA.AuthenticationStrength.Mode.$Method" -IgnoreMissing
if (-not [string]::IsNullOrEmpty($label)) { return $label }
# No Mode string: split camelCase into Title-cased words.
$spaced = [regex]::Replace($Method, '(?<=[a-z0-9])(?=[A-Z])', ' ')
$ci = [System.Globalization.CultureInfo]::InvariantCulture
return (($spaced -split ' ' | Where-Object { $_ } | ForEach-Object { $ci.TextInfo.ToTitleCase($_.ToLower()) }) -join ' ')
}
[DocumentationRegistry]::RegisterHandler([AuthenticationStrengthDocHandler]::new())
@@ -0,0 +1,53 @@
# Co-Management Settings documentation handler.
#
# Ported from old Extensions/DocumentationCustom.psm1:3862. Hardcoded
# Platform = Windows 10 (Co-Management is Windows-only).
class CoManagementDocHandler : DocumentationHandlerBase {
CoManagementDocHandler() {
$this.ODataTypes = @('#microsoft.graph.deviceComanagementAuthorityConfiguration')
}
[void] Document([object]$PolicyObject, [DocumentationContext]$Context) {
$obj = $PolicyObject.JsonObject
Add-BasicDefaultValues $PolicyObject
Add-BasicAdditionalValues $PolicyObject
Add-BasicPropertyValue (Get-LanguageString 'TableHeaders.configurationType') ((Get-LanguageString 'WindowsEnrollment.coManagementAuthorityTitle').Trim()) '@odata.type'
Add-BasicPropertyValue (Get-LanguageString 'Inputs.platformLabel') (Get-LanguageString 'Platform.Windows10') 'platform'
$category = Get-LanguageString 'TableHeaders.settings'
$yes = Get-LanguageString 'BooleanActions.yes'
$no = Get-LanguageString 'SettingDetails.no'
$installValue = if ($obj.installConfigurationManagerAgent -eq $true) { $yes } else { $no }
Add-CustomSettingObject ([PSCustomObject]@{
Name = (Get-LanguageString 'CoManagementAuthority.installAgent')
Value = $installValue
EntityKey = 'installConfigurationManagerAgent'
Category = $category
SubCategory = $null
})
if ($obj.installConfigurationManagerAgent -eq $true) {
Add-CustomSettingObject ([PSCustomObject]@{
Name = (Get-LanguageString 'CoManagementAuthority.commandLineArgs')
Value = $obj.configurationManagerAgentCommandLineArgument
EntityKey = 'configurationManagerAgentCommandLineArgument'
Category = $category
SubCategory = $null
})
}
$ownershipValue = if ($obj.managedDeviceAuthority -eq 1) { $yes } else { $no }
Add-CustomSettingObject ([PSCustomObject]@{
Name = (Get-LanguageString 'CoManagementAuthority.managedDeviceOwnership')
Value = $ownershipValue
EntityKey = 'managedDeviceAuthority'
Category = $category
SubCategory = (Get-LanguageString 'CoManagementAuthority.advancedProperty')
})
}
}
[DocumentationRegistry]::RegisterHandler([CoManagementDocHandler]::new())
@@ -0,0 +1,73 @@
# Custom compliance script documentation handler.
#
# Ported from old Extensions/DocumentationCustom.psm1:4788
# (Invoke-CDDocumentDeviceComplianceScript). Claims
# @odata.type='#microsoft.graph.deviceComplianceScript'.
#
# deviceComplianceScript has no ObjectCategories entry, so - like the Scope
# Tag handler - basic info rows are emitted manually instead of via
# Add-BasicDefaultValues (which would add blank Platform/Profile rows).
class ComplianceScriptDocHandler : DocumentationHandlerBase {
ComplianceScriptDocHandler() {
$this.ODataTypes = @('#microsoft.graph.deviceComplianceScript')
}
[void] Document([object]$PolicyObject, [DocumentationContext]$Context) {
$obj = $PolicyObject.JsonObject
if ($PolicyObject.Name) {
Add-BasicPropertyValue (Get-LanguageString 'SettingDetails.nameName') $PolicyObject.Name 'displayName'
}
$descValue = if ($obj.description) { $obj.description } else { '' }
Add-BasicPropertyValue (Get-LanguageString 'SettingDetails.descriptionName') $descValue 'description'
if ($obj.publisher) {
Add-BasicPropertyValue (Get-LanguageString 'SettingDetails.publisher') $obj.publisher 'publisher'
}
Add-BasicPropertyValue (Get-LanguageString 'TableHeaders.configurationType') (Get-LanguageString 'Titles.complianceScriptManagementPreview') 'configurationType'
$category = Get-LanguageString 'TableHeaders.settings'
$valueYes = Get-LanguageString 'BooleanActions.yes'
$valueNo = Get-LanguageString 'SettingDetails.no'
if ($obj.detectionScriptContent -and -not ($Context.Options -and $Context.Options['IncludeScripts'] -eq $false)) {
$scriptBody = ''
try { $scriptBody = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($obj.detectionScriptContent)) } catch { }
if ($scriptBody) {
Add-CustomSettingObject ([PSCustomObject]@{
Name = Get-LanguageString 'ProactiveRemediations.Create.Settings.DetectionScriptMultiLineTextBox.label'
Value = $scriptBody
EntityKey = 'detectionScriptContent'
Category = $category
SubCategory = $null
})
}
}
Add-CustomSettingObject ([PSCustomObject]@{
Name = Get-LanguageString 'WindowsManagement.scriptContextLabel'
Value = $(if ($obj.runAsAccount -eq 'system') { $valueNo } else { $valueYes })
EntityKey = 'runAsAccount'
Category = $category
SubCategory = $null
})
Add-CustomSettingObject ([PSCustomObject]@{
Name = Get-LanguageString 'WindowsManagement.enforceSignatureCheckLabel'
Value = $(if ($obj.enforceSignatureCheck -eq $false) { $valueNo } else { $valueYes })
EntityKey = 'enforceSignatureCheck'
Category = $category
SubCategory = $null
})
Add-CustomSettingObject ([PSCustomObject]@{
Name = Get-LanguageString 'WindowsManagement.runAs64BitLabel'
Value = $(if ($obj.runAs32Bit -eq $true) { $valueNo } else { $valueYes })
EntityKey = 'runAs32Bit'
Category = $category
SubCategory = $null
})
}
}
[DocumentationRegistry]::RegisterHandler([ComplianceScriptDocHandler]::new())
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,113 @@
# Custom OMA-URI documentation handler.
#
# Ported from old Extensions/DocumentationCustom.psm1:3519. Emits Name +
# Platform basic info, then 4-5 rows per OMA-URI setting (Name, Description,
# OMA-URI path, Data type, Value). Encrypted values are skipped offline;
# live runs fetch via /deviceConfigurations/.../getOmaSettingPlainTextValue.
#
# Claims all 4 CustomConfiguration variants in one handler.
class CustomOMAUriDocHandler : DocumentationHandlerBase {
CustomOMAUriDocHandler() {
$this.ODataTypes = @(
'#microsoft.graph.windows10CustomConfiguration',
'#microsoft.graph.androidForWorkCustomConfiguration',
'#microsoft.graph.androidWorkProfileCustomConfiguration',
'#microsoft.graph.androidCustomConfiguration'
)
}
[void] Document([object]$PolicyObject, [DocumentationContext]$Context) {
$obj = $PolicyObject.JsonObject
Add-BasicDefaultValues $PolicyObject
# Note: old code at L3534 has the configurationType BasicPropertyValue
# commented out. Faithful port — skipping.
$platformId = Get-ObjectPlatformFromType $obj
if ($platformId) {
Add-BasicPropertyValue (Get-LanguageString 'Inputs.platformLabel') (Get-LanguageString "Platform.$platformId") 'platform'
}
$category = Get-LanguageString 'SettingDetails.customPolicyOMAURISettingsName'
$typeLabelMap = @{
'#microsoft.graph.omaSettingString' = 'SettingDetails.stringName'
'#microsoft.graph.omaSettingBase64' = 'SettingDetails.base64Name'
'#microsoft.graph.omaSettingBoolean' = 'SettingDetails.booleanName'
'#microsoft.graph.omaSettingDateTime' = 'SettingDetails.dateTimeName'
'#microsoft.graph.omaSettingFloatingPoint' = 'SettingDetails.floatingPointName'
'#microsoft.graph.omaSettingInteger' = 'SettingDetails.integerName'
'#microsoft.graph.omaSettingStringXml' = 'SettingDetails.stringXMLName'
}
foreach ($setting in $obj.omaSettings) {
$sub = $setting.displayName
$oma = $setting.omaUri
$type = if ($setting.PSObject.Properties['@OData.Type']) { $setting.'@OData.Type' } else { $setting.'@odata.type' }
Add-CustomSettingObject ([PSCustomObject]@{
Name = (Get-LanguageString 'SettingDetails.nameName')
Value = $setting.displayName
EntityKey = "displayName_$oma"
Category = $category; SubCategory = $sub
})
Add-CustomSettingObject ([PSCustomObject]@{
Name = (Get-LanguageString 'TableHeaders.description')
Value = $setting.description
EntityKey = "description_$oma"
Category = $category; SubCategory = $sub
})
Add-CustomSettingObject ([PSCustomObject]@{
Name = (Get-LanguageString 'SettingDetails.oMAURIName')
Value = $oma
EntityKey = "omaUri_$oma"
Category = $category; SubCategory = $sub
})
$typeKey = $typeLabelMap[$type]
if ($typeKey) {
$typeValue = Get-LanguageString $typeKey
if ($typeValue) {
Add-CustomSettingObject ([PSCustomObject]@{
Name = (Get-LanguageString 'SettingDetails.dataTypeName')
Value = $typeValue
EntityKey = "type_$oma"
Category = $category; SubCategory = $sub
})
}
}
# Value row — skip when encrypted unless we can resolve via Graph
if ($setting.isEncrypted -ne $true) {
$value = $setting.value
if ($type -eq '#microsoft.graph.omaSettingStringXml' -and $value) {
try { $value = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($value)) } catch { }
}
Add-CustomSettingObject ([PSCustomObject]@{
Name = (Get-LanguageString 'SettingDetails.valueName')
Value = $value
EntityKey = "value_$oma"
Category = $category; SubCategory = $sub
})
}
elseif (-not $Context.SourceTenantUnavailable -and (Test-DocumentationGraphAvailable) -and $setting.secretReferenceValueId) {
try {
$url = "/deviceManagement/deviceConfigurations/$($obj.id)/getOmaSettingPlainTextValue(secretReferenceValueId='$($setting.secretReferenceValueId)')"
$resp = Invoke-MSGraphAPI -Url $url
if ($resp.Value) {
Add-CustomSettingObject ([PSCustomObject]@{
Name = (Get-LanguageString 'SettingDetails.valueName')
Value = $resp.Value
EntityKey = "value_$oma"
Category = $category; SubCategory = $sub
})
}
}
catch { Write-LogError "Failed to resolve encrypted OMA-URI value for $oma" $_.Exception }
}
}
}
}
[DocumentationRegistry]::RegisterHandler([CustomOMAUriDocHandler]::new())
@@ -0,0 +1,24 @@
# Device Category documentation handler.
#
# Claims @odata.type='#microsoft.graph.deviceCategory'. Device categories are
# name + description only; there is no ObjectCategories entry (so no
# Add-BasicDefaultValues - it would emit blank Platform/Profile rows) and no
# old-project documenter to port.
class DeviceCategoryDocHandler : DocumentationHandlerBase {
DeviceCategoryDocHandler() {
$this.ODataTypes = @('#microsoft.graph.deviceCategory')
}
[void] Document([object]$PolicyObject, [DocumentationContext]$Context) {
$obj = $PolicyObject.JsonObject
if ($PolicyObject.Name) {
Add-BasicPropertyValue (Get-LanguageString 'SettingDetails.nameName') $PolicyObject.Name 'displayName'
}
$descValue = if ($obj.description) { $obj.description } else { '' }
Add-BasicPropertyValue (Get-LanguageString 'SettingDetails.descriptionName') $descValue 'description'
}
}
[DocumentationRegistry]::RegisterHandler([DeviceCategoryDocHandler]::new())
@@ -0,0 +1,143 @@
# Device Enrollment Platform Restriction documentation handler.
#
# Ported from old Extensions/DocumentationCustom.psm1:4345. Claims the two
# platform-restriction @odata.types:
# ...deviceEnrollmentPlatformRestrictionConfiguration (single platform)
# ...deviceEnrollmentPlatformRestrictionsConfiguration (all platforms — the
# aggregate that emits
# one row block per
# platform sub-restriction)
#
# Doesn't handle deviceEnrollmentLimitConfiguration — that has a different
# shape (single "Device limit" setting) and lives behind the generic Profile
# input provider in the old engine.
class EnrollmentPlatformRestrictionDocHandler : DocumentationHandlerBase {
EnrollmentPlatformRestrictionDocHandler() {
$this.ODataTypes = @(
'#microsoft.graph.deviceEnrollmentPlatformRestrictionConfiguration',
'#microsoft.graph.deviceEnrollmentPlatformRestrictionsConfiguration'
)
}
[void] Document([object]$PolicyObject, [DocumentationContext]$Context) {
$obj = $PolicyObject.JsonObject
Add-BasicDefaultValues $PolicyObject
Add-BasicAdditionalValues $PolicyObject
Add-BasicPropertyValue (Get-LanguageString 'TableHeaders.configurationType') (Get-LanguageString 'Titles.deviceTypeEnrollmentRestrictions') '@odata.type'
# platformType (single variant) -> Platform.* language id
$singlePlatformLngId = switch ($obj.platformType) {
'androidForWork' { 'androidWorkProfile' }
'mac' { 'macOS' }
'ios' { 'iOS' }
'android' { 'android' }
'windows' { 'windows' }
'tvos' { 'tvOS' }
'visionOS' { 'visionOS' }
default { $obj.platformType }
}
$isAggregate = $obj.'@odata.type' -eq '#microsoft.graph.deviceEnrollmentPlatformRestrictionsConfiguration'
if ($isAggregate) {
# The default "All users and all devices" config carries one sub-restriction
# per platform. Graph exposes 10, including the legacy macRestriction (a dupe
# of the version-capable macOSRestriction) and the deprecated
# windowsMobileRestriction. Render the current platforms; prefer
# macOSRestriction over macRestriction. $platformMap: property -> name key.
$platform = Get-LanguageString 'AzureCA.classicPolicyAllPlatforms'
$platformMap = [ordered]@{
'androidForWorkRestriction' = 'Platform.androidWorkProfile'
'androidRestriction' = 'Platform.android'
'iosRestriction' = 'Platform.iOS'
'macOSRestriction' = 'Platform.macOS'
'tvosRestriction' = 'Platform.tvOS'
'visionOSRestriction' = 'Platform.visionOS'
'windowsRestriction' = 'Platform.windows'
'windowsHomeSkuRestriction' = 'Devices.windowsHomeSku'
}
}
else {
$platform = Get-LanguageString "Platform.$singlePlatformLngId"
$platformMap = [ordered]@{ 'platformRestriction' = "Platform.$singlePlatformLngId" }
}
Add-BasicPropertyValue (Get-LanguageString 'Inputs.platformLabel') $platform 'platformType'
$allowStr = Get-LanguageString 'BooleanActions.allow'
$blockStr = Get-LanguageString 'BooleanActions.block'
$category = Get-LanguageString 'EnrollmentRestrictions.DeviceType.platformSettings'
$cantRestrictStr = Get-LanguageString 'EnrollmentRestrictions.DeviceType.cannotRestrict'
foreach ($prop in $platformMap.Keys) {
$restrict = $obj.$prop
if (-not $restrict) { continue }
$nameKey = $platformMap[$prop]
$typeStr = Get-LanguageString $nameKey
# OS version range, blank when unset. macOSRestriction is version-capable,
# so (unlike the legacy macRestriction the old handler forced to "cannot
# restrict") every platform now reports its actual osMin/osMax range.
$version = if ($restrict.osMinimumVersion -or $restrict.osMaximumVersion) {
"$($restrict.osMinimumVersion)-$($restrict.osMaximumVersion)"
} else { '' }
# Manufacturer blocking: old code has a typo (`'andriod'` instead of
# `'android'`) which means only 'androidWorkProfile' actually emits
# the blockedManufacturers list. Everything else — including the
# correctly-spelled 'android' (device administrator) — falls
# through to "Restriction not supported". Preserving the behavior
# because golden fixtures match it; the typo is a known wart in
# old code that fixing would silently change output.
$blockedManufacturers = if ($nameKey -eq 'Platform.androidWorkProfile') {
@($restrict.blockedManufacturers) -join $Context.PropertySeparator
} else {
$cantRestrictStr
}
# Aggregate variant uses platform name as SubCategory; single variant uses $null
$subCategory = if ($isAggregate) { $typeStr } else { $null }
Add-CustomSettingObject ([PSCustomObject]@{
Name = (Get-LanguageString 'EnrollmentRestrictions.DeviceType.type')
Value = $typeStr
EntityKey = 'platformType'
Category = $category; SubCategory = $subCategory
})
$platformAccess = if ($restrict.platformBlocked) { $blockStr } else { $allowStr }
Add-CustomSettingObject ([PSCustomObject]@{
Name = (Get-LanguageString 'EnrollmentRestrictions.DeviceType.platform')
Value = $platformAccess
EntityKey = 'platformBlocked'
Category = $category; SubCategory = $subCategory
})
Add-CustomSettingObject ([PSCustomObject]@{
Name = (Get-LanguageString 'EnrollmentRestrictions.DeviceType.versions')
Value = $version
EntityKey = 'versions'
Category = $category; SubCategory = $subCategory
})
$personalAccess = if ($restrict.personalDeviceEnrollmentBlocked) { $blockStr } else { $allowStr }
Add-CustomSettingObject ([PSCustomObject]@{
Name = (Get-LanguageString 'EnrollmentRestrictions.DeviceType.personal')
Value = $personalAccess
EntityKey = 'personalDeviceEnrollmentBlocked'
Category = $category; SubCategory = $subCategory
})
Add-CustomSettingObject ([PSCustomObject]@{
Name = (Get-LanguageString 'EnrollmentRestrictions.DeviceType.deviceManufacturer')
Value = $blockedManufacturers
EntityKey = 'blockedManufacturers'
Category = $category; SubCategory = $subCategory
})
}
}
}
[DocumentationRegistry]::RegisterHandler([EnrollmentPlatformRestrictionDocHandler]::new())
@@ -0,0 +1,206 @@
# Managed App Configuration documentation handler.
#
# Ported from old Extensions/DocumentationCustom.psm1:2196. Claims
# @odata.type='#microsoft.graph.targetedManagedAppConfiguration'.
#
# Outlook + Edge ObjectInfo translations (and the Edge bookmark/AllowList/
# BlockList delimiter rewrites) deferred until the walker is ported. Offline
# falls through to raw customSettings under TACSettings.generalSettings.
class ManagedAppConfigDocHandler : DocumentationHandlerBase {
ManagedAppConfigDocHandler() {
$this.ODataTypes = @('#microsoft.graph.targetedManagedAppConfiguration')
}
[void] Document([object]$PolicyObject, [DocumentationContext]$Context) {
$obj = $PolicyObject.JsonObject
Add-BasicDefaultValues $PolicyObject
Add-BasicPropertyValue (Get-LanguageString 'TableHeaders.configurationType') (Get-LanguageString 'SettingDetails.appConfiguration') '@odata.type'
$customApps, $publishedApps = Get-CDMobileApps $obj.Apps
Add-BasicPropertyValue (Get-LanguageString 'Inputs.enrollmentTypeLabel') (Get-LanguageString 'EnrollmentType.devicesWithoutEnrollment') 'enrollmentType'
Add-BasicPropertyValue (Get-LanguageString 'SettingDetails.publicApps') ($publishedApps -join $Context.ObjectSeparator) 'publishedApps'
Add-BasicPropertyValue (Get-LanguageString 'SettingDetails.customApps') ($customApps -join $Context.ObjectSeparator) 'customApps'
# appGroupType - "Target policy to". The all* variants don't list individual
# apps, so surfacing the mode is what tells the reader the scope. Graph's
# enum member is allCoreMicrosoftApps; the portal's string is coreMicrosoftApps.
$appGroupTypeKeys = @{
'selectedPublicApps' = 'AppGroupType.selectedPublicApps'
'allApps' = 'AppGroupType.allApps'
'allMicrosoftApps' = 'AppGroupType.allMicrosoftApps'
'allCoreMicrosoftApps' = 'AppGroupType.coreMicrosoftApps'
}
$agtRaw = "$($obj.appGroupType)"
if ($agtRaw) {
$agtKey = $appGroupTypeKeys[$agtRaw]
$agtValue = if ($agtKey) { Get-LanguageString $agtKey -IgnoreMissing } else { $null }
if ([string]::IsNullOrEmpty($agtValue)) { $agtValue = $agtRaw }
Add-BasicPropertyValue (Get-LanguageString 'SettingDetails.targetPolicyTo') $agtValue 'appGroupType'
}
# targetedAppManagementLevels - flags enum, returned as a comma-separated
# string (e.g. "mdm, androidEnterprise"). Map each flag to its label,
# falling back to the raw flag value when no string exists.
$mgmtLevelKeys = @{
'unspecified' = 'AppProtection.allAppTypes'
'unmanaged' = 'AppProtection.appsOnUnmanagedDevices'
'mdm' = 'AppProtection.appsOnIntuneManagedDevices'
'androidEnterprise' = 'AppProtection.appsInAndroidWorkProfile'
'androidEnterpriseDedicatedDevicesWithAzureAdSharedMode' = 'AppProtection.appsOnAndroidEnterpriseDedicatedDevicesWithAzureAdSharedMode'
'androidOpenSourceProjectUserAssociated' = 'AppProtection.appsOnAndroidOpenSourceProjectUserAssociated'
'androidOpenSourceProjectUserless' = 'AppProtection.appsOnAndroidOpenSourceProjectUserless'
}
$mgmtRaw = "$($obj.targetedAppManagementLevels)"
if ($mgmtRaw) {
$mgmtParts = @()
foreach ($lvl in ($mgmtRaw -split ',')) {
$lvlTrim = $lvl.Trim()
if (-not $lvlTrim) { continue }
$lvlKey = $mgmtLevelKeys[$lvlTrim]
$lvlValue = if ($lvlKey) { Get-LanguageString $lvlKey -IgnoreMissing } else { $null }
if ([string]::IsNullOrEmpty($lvlValue)) { $lvlValue = $lvlTrim }
$mgmtParts += $lvlValue
}
if ($mgmtParts.Count -gt 0) {
Add-BasicPropertyValue (Get-LanguageString 'SettingDetails.managementType') ($mgmtParts -join $Context.ObjectSeparator) 'targetedAppManagementLevels'
}
}
Add-BasicAdditionalValues $PolicyObject
# Outlook / Edge get schema-driven translation via their ObjectInfo files
# (port of old DocumentationCustom.psm1:2229-2260). Build a flat settings
# object keyed by customSetting name, then walk the matching manifest.
$appSettings = [PSCustomObject]@{}
foreach ($setting in @($obj.customSettings)) {
$appSettings | Add-Member -MemberType NoteProperty -Name $setting.name -Value $setting.value -Force
}
$objInfoDir = Join-Path $script:AppRootFolder 'Config\ObjectInfo'
# Unpack every packed/delimited value BEFORE the manifests read them, so the
# rewrite also benefits the raw fall-through rows below.
# NB Where-Object: on a property-less object .PSObject.Properties.Name is
# $null, and @($null) is a one-element array containing $null
foreach ($name in @($appSettings.PSObject.Properties.Name | Where-Object { $_ })) {
$sep = $script:_mamPackedSettingSeparators[$name]
if (-not $sep -or -not $appSettings.$name -or $appSettings.$name -isnot [string]) { continue }
$unpacked = $appSettings.$name
# Record separator first - doing it the other way round destroys it
if ($sep.RecordSep) { $unpacked = $unpacked.Replace($sep.RecordSep, $Context.ObjectSeparator) }
if ($sep.FieldSep) { $unpacked = $unpacked.Replace($sep.FieldSep, $Context.PropertySeparator) }
$appSettings.$name = $unpacked
}
# App identities differ per platform: Outlook/Edge are packageId on Android,
# bundleId on iOS and windowsAppId on Windows. Matching only one of them
# silently skipped the whole manifest for the other platforms.
if (Test-MamAppTargeted $obj.Apps @('com.microsoft.office.outlook')) {
Invoke-DocAppConfigManifest $appSettings (Join-Path $objInfoDir '#AppConfigOutlookApp.json') $Context
}
if (Test-MamAppTargeted $obj.Apps @('com.microsoft.msedge', 'com.microsoft.emmx', 'com.microsoft.edge')) {
Invoke-DocAppConfigManifest $appSettings (Join-Path $objInfoDir '#AppConfigEdgeApp.json') $Context
}
# Settings-catalog settings (the "Settings catalog" wizard step, used by the
# Windows MAM flavour). Without this a policy whose entire payload lives in
# `settings` documented as a header and nothing else.
Invoke-DocMamSettingsCatalog $obj $Context
# Remaining customSettings fall through to raw key=value rows.
$addedSettings = Get-DocumentedSettings
$category = Get-LanguageString 'TACSettings.generalSettings'
foreach ($setting in $obj.customSettings) {
if ($addedSettings | Where-Object EntityKey -EQ $setting.name) { continue }
# Use the unpacked value when one was produced above
$value = if ($null -ne $appSettings.PSObject.Properties[$setting.name]) { $appSettings."$($setting.name)" } else { $setting.value }
Add-CustomSettingObject ([PSCustomObject]@{
Name = $setting.name
Value = $value
EntityKey = $setting.name
Category = $category
})
}
}
}
# Document the settings-catalog part of a MAM app configuration.
#
# The Managed apps wizard has a "Settings catalog" step whose values land in the
# `settings` navigation property (Collection(deviceManagementConfigurationSetting))
# rather than in customSettings - Windows MAM policies are entirely settings-catalog.
# The portal renders it as its own blade ABOVE the classic Settings blade, so these
# rows go into a table of their own (negative Order = before the settings table)
# instead of being merged into it.
#
# Resolution is the SettingsCatalog provider's own code - see
# Get-SettingsCatalogDocumentationRows. This used to be a copy of it that had
# drifted, losing the category grouping.
function Invoke-DocMamSettingsCatalog {
param($Obj, [DocumentationContext]$Context)
$cfgSettings = @($Obj.settings)
$hasDefs = $false
foreach ($s in $cfgSettings) {
if ($s.settingDefinitions -and ($s.settingDefinitions | Measure-Object).Count -gt 0) { $hasDefs = $true; break }
}
# Source-tenant-specific fetch (by policy id) - same gating as the Settings
# Catalog provider. Exports carrying settings inline still work offline via the
# walker's generic per-setting definition fallback.
if (-not $hasDefs -and $Obj.Id -and -not $Context.SourceTenantUnavailable -and (Test-DocumentationGraphAvailable)) {
try {
$resp = Invoke-MSGraphAPI -Url "/deviceAppManagement/targetedManagedAppConfigurations('$($Obj.Id)')/settings?`$expand=settingDefinitions&`$top=1000" -AdditionalHeaders (Get-DocAcceptLanguageHeaders $Context) -ODataMetadata 'minimal' -NoError
if ($resp -and $resp.Value) { $cfgSettings = @($resp.Value) }
}
catch {
Write-LogError "Failed to fetch settings catalog settings for app configuration $($Obj.Id)" $_.Exception
}
}
if (@($cfgSettings).Count -eq 0) { return }
$rows = @(Get-SettingsCatalogDocumentationRows $cfgSettings $Context)
if ($rows.Count -eq 0) { return }
Add-CustomTable 'SettingsCatalog' @('Name','Value') $rows -Order -100 -LanguageId 'SettingDetails.settingsCatalog'
}
# MAM settings whose value packs multiple records/fields into one string.
# RecordSep splits repeated records, FieldSep splits fields inside a record.
$script:_mamPackedSettingSeparators = @{
# title|url pairs, records separated by ||
'com.microsoft.intune.mam.managedbrowser.bookmarks' = @{ RecordSep = '||'; FieldSep = '|' }
'com.microsoft.intune.mam.managedbrowser.managedTopSites' = @{ RecordSep = '||'; FieldSep = '|' }
# plain pipe-separated lists
'com.microsoft.intune.mam.managedbrowser.AllowListURLs' = @{ RecordSep = '|' }
'com.microsoft.intune.mam.managedbrowser.BlockListURLs' = @{ RecordSep = '|' }
'com.microsoft.intune.mam.managedbrowser.disabledFeatures' = @{ RecordSep = '|' }
'com.microsoft.intune.mam.managedbrowser.InternalPagesBlockList' = @{ RecordSep = '|' }
'com.microsoft.intune.mam.managedbrowser.PopupsAllowedForUrls' = @{ RecordSep = '|' }
'com.microsoft.intune.mam.managedbrowser.PopupsBlockedForUrls' = @{ RecordSep = '|' }
'com.microsoft.intune.mam.managedbrowser.FileUploadAllowedForUrls' = @{ RecordSep = '|' }
'com.microsoft.intune.mam.managedbrowser.FileUploadBlockedForUrls' = @{ RecordSep = '|' }
'com.microsoft.intune.mam.managedbrowser.NewTabPageLayout.Custom' = @{ RecordSep = '|' }
}
# True when any targeted app matches one of the given app identifiers on ANY
# platform identity (Android packageId / iOS bundleId / Windows windowsAppId).
function Test-MamAppTargeted {
param($Apps, [string[]]$Identifiers)
foreach ($app in @($Apps)) {
$id = $app.mobileAppIdentifier
if (-not $id) { continue }
foreach ($candidate in @($id.packageId, $id.bundleId, $id.windowsAppId)) {
if ($candidate -and $candidate -in $Identifiers) { return $true }
}
}
return $false
}
[DocumentationRegistry]::RegisterHandler([ManagedAppConfigDocHandler]::new())
@@ -0,0 +1,89 @@
# Named Location documentation handler (Country + IP variants).
#
# Ported from old Extensions/DocumentationCustom.psm1:2280 (country) + 2323 (IP).
# Single class claims both @odata.types since they share the same BasicInfo
# header shape and one varies only the settings.
class NamedLocationDocHandler : DocumentationHandlerBase {
NamedLocationDocHandler() {
$this.ODataTypes = @(
'#microsoft.graph.countryNamedLocation',
'#microsoft.graph.ipNamedLocation',
'#microsoft.graph.compliantNetworkNamedLocation'
)
}
[void] Document([object]$PolicyObject, [DocumentationContext]$Context) {
$obj = $PolicyObject.JsonObject
Add-BasicDefaultValues $PolicyObject
Add-BasicPropertyValue (Get-LanguageString 'TableHeaders.configurationType') (Get-LanguageString 'AzureCA.menuItemNamedNetworks') '@odata.type'
Add-BasicAdditionalValues $PolicyObject
switch ($obj.'@odata.type') {
'#microsoft.graph.countryNamedLocation' { Invoke-NamedLocationCountrySettings $obj $Context }
'#microsoft.graph.ipNamedLocation' { Invoke-NamedLocationIPSettings $obj $Context }
'#microsoft.graph.compliantNetworkNamedLocation' { Invoke-NamedLocationCompliantNetworkSettings $obj $Context }
}
}
}
function Invoke-NamedLocationCountrySettings {
param($obj, [DocumentationContext]$Context)
$lookupSuffix = if ($obj.countryLookupMethod -eq 'clientIpAddress') { 'ip' } else { 'gps' }
Add-CustomSettingObject ([PSCustomObject]@{
Name = (Get-LanguageString 'AzureCA.NamedLocation.Form.CountryLookup.ariaLabel')
Value = (Get-LanguageString "AzureCA.NamedLocation.Form.CountryLookup.$lookupSuffix")
EntityKey = 'countryLookupMethod'
})
$includeKey = if ($obj.includeUnknownCountriesAndRegions -eq $true) { 'Inputs.enabled' } else { 'Inputs.disabled' }
Add-CustomSettingObject ([PSCustomObject]@{
Name = (Get-LanguageString 'AzureCA.NamedLocation.Form.Include.label')
Value = (Get-LanguageString $includeKey)
EntityKey = 'includeUnknownCountriesAndRegions'
})
$countryNames = @()
foreach ($code in $obj.countriesAndRegions) {
$countryNames += Get-LanguageString "AzureIAMCommon.CountryNames.countryName$($code.ToUpper())"
}
Add-CustomSettingObject ([PSCustomObject]@{
Name = (Get-LanguageString 'AzureCA.NamedLocation.Type.countries')
Value = ($countryNames -join $Context.ObjectSeparator)
EntityKey = 'countriesAndRegions'
})
}
function Invoke-NamedLocationIPSettings {
param($obj, [DocumentationContext]$Context)
$trustedKey = if ($obj.isTrusted -eq $true) { 'Inputs.enabled' } else { 'Inputs.disabled' }
Add-CustomSettingObject ([PSCustomObject]@{
Name = (Get-LanguageString 'AzureCA.NamedLocation.Form.Trusted.label')
Value = (Get-LanguageString $trustedKey)
EntityKey = 'isTrusted'
})
$ipList = @()
foreach ($range in $obj.ipRanges) { $ipList += $range.cidrAddress }
Add-CustomSettingObject ([PSCustomObject]@{
Name = (Get-LanguageString 'AzureCA.namedNetworkIpRangesTab')
Value = ($ipList -join $Context.ObjectSeparator)
EntityKey = 'ipRanges'
})
}
function Invoke-NamedLocationCompliantNetworkSettings {
param($obj, [DocumentationContext]$Context)
# Built-in read-only location; its only meaningful setting is the trusted flag.
$trustedKey = if ($obj.isTrusted -eq $true) { 'Inputs.enabled' } else { 'Inputs.disabled' }
Add-CustomSettingObject ([PSCustomObject]@{
Name = (Get-LanguageString 'AzureCA.NamedLocation.Form.Trusted.label')
Value = (Get-LanguageString $trustedKey)
EntityKey = 'isTrusted'
})
}
[DocumentationRegistry]::RegisterHandler([NamedLocationDocHandler]::new())
@@ -0,0 +1,95 @@
# Notification message template documentation handler.
#
# Ported from old Extensions/DocumentationCustom.psm1:3660. Emits 4 branding-
# option rows ("Show company logo/name/contact/portal link" enable/disable)
# plus one row per localized message template with the locale name as label
# and the subject+body as value.
class NotificationDocHandler : DocumentationHandlerBase {
NotificationDocHandler() {
$this.ODataTypes = @('#microsoft.graph.notificationMessageTemplate')
}
[void] Document([object]$PolicyObject, [DocumentationContext]$Context) {
$obj = $PolicyObject.JsonObject
Add-BasicDefaultValues $PolicyObject
Add-BasicPropertyValue (Get-LanguageString 'TableHeaders.configurationType') (Get-LanguageString 'Titles.notifications') '@odata.type'
$category = Get-LanguageString 'TableHeaders.settings'
# brandingOptions is a comma-separated string like "includeCompanyLogo,includeCompanyName"
# or "none". Split into a hash for membership tests.
$brandingFlags = @{}
if ($obj.brandingOptions) {
foreach ($flag in $obj.brandingOptions.Split(',')) {
$brandingFlags[$flag.Trim()] = $true
}
}
$brandingLabelMap = [ordered]@{
'includeCompanyLogo' = 'NotificationMessage.companyLogo'
'includeCompanyName' = 'NotificationMessage.companyName'
'includeContactInformation' = 'NotificationMessage.companyContact'
'includeCompanyPortalLink' = 'NotificationMessage.iwLink'
'includeDeviceDetails' = 'NotificationMessage.deviceDetails'
}
foreach ($flag in $brandingLabelMap.Keys) {
$valueKey = if ($brandingFlags.ContainsKey($flag)) { 'BooleanActions.enable' } else { 'BooleanActions.disable' }
Add-CustomSettingObject ([PSCustomObject]@{
Name = (Get-LanguageString $brandingLabelMap[$flag])
Value = (Get-LanguageString $valueKey)
EntityKey = $flag
Category = $category
SubCategory = $null
})
}
# Localized message templates
$subCategory = Get-LanguageString 'NotificationMessage.listTitle'
foreach ($template in $obj.localizedNotificationMessages) {
$label = Get-NotificationLocaleLabel $template.locale
if (-not $label) { continue }
$value = $template.subject
if ($template.isDefault) {
$value = $value + $Context.ObjectSeparator + (Get-LanguageString 'NotificationMessage.isDefaultLocale') + ': ' + (Get-LanguageString 'SettingDetails.trueOption')
}
$fullValue = $value + $Context.ObjectSeparator + $template.messageTemplate
Add-CustomSettingObject ([PSCustomObject]@{
Name = $label
Value = $fullValue
EntityKey = $template.locale
Category = $category
SubCategory = $subCategory
})
}
}
}
# Localized message templates are labeled by language name. Most languages
# pass through [cultureinfo].EnglishName.ToLower(); a handful with regional
# splits (en-US/UK, es-ES/MX, fr-CA/FR, pt-PT/BR, zh-TW/CN, nb-* -> norwegian)
# get a suffix. Old code at DocumentationCustom.psm1:3735-3796.
function Get-NotificationLocaleLabel {
param([string]$Locale)
if (-not $Locale) { return $null }
$first, $second = $Locale.Split('-')
try { $lng = ([cultureinfo]$first).EnglishName.ToLower() } catch { return $null }
switch ($first) {
'en' { switch ($second) { 'US' { $lng += 'US' }; 'GB' { $lng += 'UK' } } }
'es' { switch ($second) { 'es' { $lng += 'Spain' }; 'mx' { $lng += 'Mexico' } } }
'fr' { switch ($second) { 'ca' { $lng += 'Canada' }; 'fr' { $lng += 'France' } } }
'pt' { switch ($second) { 'pt' { $lng += 'Portugal' }; 'br' { $lng += 'Brazil' } } }
'zh' { switch ($second) { 'tw' { $lng += 'Traditional' }; 'cn' { $lng += 'Simplified' } } }
'nb' { $lng = 'norwegian' }
}
return (Get-LanguageString "NotificationMessage.NotificationMessageTemplatesTab.$lng")
}
[DocumentationRegistry]::RegisterHandler([NotificationDocHandler]::new())
@@ -0,0 +1,100 @@
# Policy Set documentation handler.
#
# Ported from old Extensions/DocumentationCustom.psm1:3422. Categorizes the
# policy-set items (apps / device configs / enrollment) into 3 sections, then
# emits one row per item with the item's displayName and a type-specific
# value (priority number for ordered items, AAD/AD for autopilot, etc.).
class PolicySetDocHandler : DocumentationHandlerBase {
PolicySetDocHandler() {
$this.ODataTypes = @('#microsoft.graph.policySet')
}
[void] Document([object]$PolicyObject, [DocumentationContext]$Context) {
$obj = $PolicyObject.JsonObject
Add-BasicDefaultValues $PolicyObject
Add-BasicPropertyValue (Get-LanguageString 'TableHeaders.configurationType') (Get-LanguageString 'SettingDetails.appConfiguration') '@odata.type'
$sections = @(
[PSCustomObject]@{
Category = (Get-LanguageString 'PolicySet.appManagement')
Types = @(
@{ ODataType = '#microsoft.graph.mobileAppPolicySetItem'; SubKey = 'appTitle' }
@{ ODataType = '#microsoft.graph.targetedManagedAppConfigurationPolicySetItem'; SubKey = 'appConfigurationTitle' }
@{ ODataType = '#microsoft.graph.managedDeviceMobileAppConfigurationPolicySetItem'; SubKey = 'appConfigurationTitle' }
@{ ODataType = '#microsoft.graph.managedAppProtectionPolicySetItem'; SubKey = 'appProtectionTitle' }
@{ ODataType = '#microsoft.graph.iosLobAppProvisioningConfigurationPolicySetItem'; SubKey = 'iOSAppProvisioningTitle' }
)
}
[PSCustomObject]@{
Category = (Get-LanguageString 'PolicySet.deviceManagement')
Types = @(
@{ ODataType = '#microsoft.graph.deviceConfigurationPolicySetItem'; SubKey = 'deviceConfigurationTitle' }
@{ ODataType = '#microsoft.graph.deviceManagementConfigurationPolicyPolicySetItem'; SubKey = 'SettingDetails.settingsCatalog' }
@{ ODataType = '#microsoft.graph.deviceCompliancePolicyPolicySetItem'; SubKey = 'deviceComplianceTitle' }
@{ ODataType = '#microsoft.graph.deviceManagementScriptPolicySetItem'; SubKey = 'powershellScriptTitle' }
)
}
[PSCustomObject]@{
Category = (Get-LanguageString 'PolicySet.deviceEnrollment')
Types = @(
@{ ODataType = '#microsoft.graph.enrollmentRestrictionsConfigurationPolicySetItem'; SubKey = 'deviceTypeRestrictionTitle' }
@{ ODataType = '#microsoft.graph.windowsAutopilotDeploymentProfilePolicySetItem'; SubKey = 'windowsAutopilotDeploymentProfileTitle' }
@{ ODataType = '#microsoft.graph.windows10EnrollmentCompletionPageConfigurationPolicySetItem'; SubKey = 'enrollmentStatusSettingTitle' }
)
}
)
foreach ($section in $sections) {
foreach ($subType in $section.Types) {
foreach ($item in ($obj.items | Where-Object { $_.'@OData.Type' -eq $subType.ODataType -or $_.'@odata.type' -eq $subType.ODataType })) {
if ($item.status -eq 'error') {
Write-Log "Skipping missing $($subType.ODataType) type with id $($item.id). Error code: $($item.errorCode)" 2
continue
}
# SubKey is a bare key under PolicySet.* unless it already
# carries a namespace (dotted), letting new item types reuse
# strings that live outside the PolicySet section.
$subKeyFull = if ($subType.SubKey -like '*.*') { $subType.SubKey } else { "PolicySet.$($subType.SubKey)" }
Add-CustomSettingObject ([PSCustomObject]@{
Name = $item.displayName
Value = (Get-PolicySetItemValue $item)
EntityKey = $item.id
Category = $section.Category
SubCategory = (Get-LanguageString $subKeyFull)
})
}
}
}
}
}
function Get-PolicySetItemValue {
param($item)
$odata = if ($item.PSObject.Properties['@OData.Type']) { $item.'@OData.Type' } else { $item.'@odata.type' }
if ($odata -in @(
'#microsoft.graph.enrollmentRestrictionsConfigurationPolicySetItem',
'#microsoft.graph.windows10EnrollmentCompletionPageConfigurationPolicySetItem'
)) {
return $item.Priority
}
if ($odata -eq '#microsoft.graph.windowsAutopilotDeploymentProfilePolicySetItem') {
if ($item.itemType -eq '#microsoft.graph.azureADWindowsAutopilotDeploymentProfile') {
return (Get-LanguageString 'Autopilot.DirectoryService.azureAD')
}
if ($item.itemType -eq '#microsoft.graph.activeDirectoryWindowsAutopilotDeploymentProfile') {
return (Get-LanguageString 'Autopilot.DirectoryService.activeDirectoryAD')
}
}
# TODO phase-4-followup: other PolicySet item types as fixtures arrive
return $null
}
[DocumentationRegistry]::RegisterHandler([PolicySetDocHandler]::new())
@@ -0,0 +1,68 @@
# Reusable setting (deviceManagementReusablePolicySetting) documentation
# handler.
#
# Claims @odata.type='#microsoft.graph.deviceManagementReusablePolicySetting'.
# Covers the reusable settings surfaced as policy types (currently the Linux
# custom-compliance discovery script). The object is a name + description +
# settingDefinitionId wrapper around a single settings-catalog setting
# instance whose simpleSettingValue carries the payload (base64 script for
# the discovery-script definition). No old-project documenter existed.
class ReusableSettingDocHandler : DocumentationHandlerBase {
ReusableSettingDocHandler() {
$this.ODataTypes = @('#microsoft.graph.deviceManagementReusablePolicySetting')
}
[void] Document([object]$PolicyObject, [DocumentationContext]$Context) {
$obj = $PolicyObject.JsonObject
if ($PolicyObject.Name) {
Add-BasicPropertyValue (Get-LanguageString 'SettingDetails.nameName') $PolicyObject.Name 'displayName'
}
$descValue = if ($obj.description) { $obj.description } else { '' }
Add-BasicPropertyValue (Get-LanguageString 'SettingDetails.descriptionName') $descValue 'description'
$category = Get-LanguageString 'TableHeaders.settings'
if ($obj.settingDefinitionId) {
$definitionLabel = Get-LanguageString 'SettingDetails.settingIdName' -IgnoreMissing
if (-not $definitionLabel) { $definitionLabel = 'Setting definition' }
Add-CustomSettingObject ([PSCustomObject]@{
Name = $definitionLabel
Value = [string]$obj.settingDefinitionId
EntityKey = 'settingDefinitionId'
Category = $category
SubCategory = $null
})
}
$rawValue = [string]$obj.settingInstance.simpleSettingValue.value
if ($rawValue -and -not ($Context.Options -and $Context.Options['IncludeScripts'] -eq $false)) {
# The discovery-script definition stores the script base64-encoded;
# fall back to the raw value for definitions that don't.
$value = $rawValue
try {
$decoded = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($rawValue))
if ($decoded) { $value = $decoded }
}
catch { }
$valueLabel = if ([string]$obj.settingDefinitionId -like '*discoveryscript*') {
Get-LanguageString 'ProactiveRemediations.Create.Settings.DetectionScriptMultiLineTextBox.label'
}
else {
$lbl = Get-LanguageString 'SettingDetails.valueName' -IgnoreMissing
if ($lbl) { $lbl } else { 'Value' }
}
Add-CustomSettingObject ([PSCustomObject]@{
Name = $valueLabel
Value = $value
EntityKey = 'settingInstanceValue'
Category = $category
SubCategory = $null
})
}
}
}
[DocumentationRegistry]::RegisterHandler([ReusableSettingDocHandler]::new())
@@ -0,0 +1,186 @@
# Role Definition documentation handler.
#
# Ported from old Extensions/DocumentationCustom.psm1:4513. Resolves
# permissions to resource/action names via /deviceManagement/resourceOperations
# (generic schema - resolved from any connected tenant) and enriches assignments
# with directory display names (source-tenant-specific - skipped when the source
# tenant is unavailable, emitting raw IDs).
class RoleDefinitionDocHandler : DocumentationHandlerBase {
RoleDefinitionDocHandler() {
$this.ODataTypes = @('#microsoft.graph.deviceAndAppManagementRoleDefinition')
}
[void] Document([object]$PolicyObject, [DocumentationContext]$Context) {
$obj = $PolicyObject.JsonObject
Add-BasicDefaultValues $PolicyObject
Add-BasicAdditionalValues $PolicyObject
Add-BasicPropertyValue (Get-LanguageString 'TableHeaders.configurationType') (Get-LanguageString 'RoleAssignment.rolesMenuTitle') '@odata.type'
# Built-in vs custom role. isBuiltIn is true for Microsoft-supplied roles.
if ($null -ne $obj.isBuiltIn) {
$builtInValue = if ($obj.isBuiltIn) { Get-LanguageString 'SettingDetails.yes' } else { Get-LanguageString 'SettingDetails.no' }
Add-BasicPropertyValue (Get-LanguageString 'DisplayRoleTypes.builtInRole') $builtInValue 'isBuiltIn'
}
# --- Permissions section: resolve action IDs to resource/action names ---
$roleResources = @()
# resourceOperations is a GENERIC catalog (resource/action names) - same on
# every tenant - so resolved from any connected tenant.
if (Test-DocumentationGraphAvailable) {
try {
$resp = Invoke-MSGraphAPI -Url '/deviceManagement/resourceOperations'
$roleResources = @($resp.Value)
}
catch {
Write-LogError 'Failed to fetch /deviceManagement/resourceOperations for role permissions' $_.Exception
}
}
$permissionsCategory = Get-LanguageString 'Titles.permissions'
# Prefer the modern rolePermissions structure: union allowedResourceActions
# across ALL rolePermissions/resourceActions. Fall back to the legacy flat
# permissions[0].actions list when rolePermissions is absent (older payloads
# and some built-in roles only populate the legacy list).
$actionIds = @()
if ($obj.rolePermissions) {
foreach ($rolePermission in @($obj.rolePermissions)) {
foreach ($resourceAction in @($rolePermission.resourceActions)) {
foreach ($allowed in @($resourceAction.allowedResourceActions)) {
if ($allowed -and $actionIds -notcontains $allowed) { $actionIds += $allowed }
}
}
}
}
if ($actionIds.Count -eq 0 -and $obj.permissions -and $obj.permissions[0]) {
$actionIds = @($obj.permissions[0].actions)
}
if ($roleResources.Count -gt 0 -and $actionIds.Count -gt 0) {
# Resolved live: group resolved actions by resourceName
$assignedActions = @()
foreach ($id in $actionIds) {
$r = $roleResources | Where-Object Id -EQ $id | Select-Object -First 1
if ($r) { $assignedActions += $r }
}
$byResource = $assignedActions | Select-Object resourceName -Unique | Sort-Object -Property resourceName
foreach ($rn in $byResource.resourceName) {
$actions = @($assignedActions | Where-Object resourceName -EQ $rn)
$resourceId = $actions[0].resource
$actionNames = ($actions | ForEach-Object { $_.actionName })
Add-CustomSettingObject ([PSCustomObject]@{
Name = $rn
Value = ($actionNames -join $Context.ObjectSeparator)
EntityKey = $resourceId
Category = $permissionsCategory
SubCategory = $null
})
}
}
elseif ($actionIds.Count -gt 0) {
# Offline: emit a single row with raw action IDs so the row count
# is non-zero and compare-style downstream tools have something
Add-CustomSettingObject ([PSCustomObject]@{
Name = (Get-LanguageString 'Titles.permissions')
Value = ($actionIds -join $Context.ObjectSeparator)
EntityKey = 'actions'
Category = $permissionsCategory
SubCategory = $null
})
}
# --- Assignments section ---
# roleAssignments are enriched in place (full assignment + roleScopeTags)
# by RoleDefinitionObject's sub-resource contract during hydration, so
# each entry already carries displayName/description/members/scopeMembers/
# roleScopeTags — no per-assignment Graph fetch here. The
# deviceManagement/roleAssignments/<id> API now lives only on the class.
# Offline runs still skip this section (matching prior behavior); the
# member display-name resolution (getByIds) is shared reference data and
# stays online-only.
if ($Context.SourceTenantUnavailable -or -not (Test-DocumentationGraphAvailable)) { return }
$assignmentsCategory = Get-LanguageString 'TableHeaders.assignments'
foreach ($info in @($obj.roleAssignments)) {
if (-not $info -or [string]::IsNullOrWhiteSpace([string]$info.id)) {
Write-Log 'RoleDefinition: skipping role assignment without an id' 2
continue
}
# Resolve member + scope IDs to displayNames in one batch
$ids = @()
foreach ($id in @($info.members + $info.scopeMembers)) {
if ($id -and $ids -notcontains $id) { $ids += $id }
}
$idInfo = @()
if ($ids.Count -gt 0) {
try {
$body = @{ ids = $ids } | ConvertTo-Json
$resp = Invoke-MSGraphAPI -Url "/directoryObjects/getByIds?`$select=displayName,id" -Content $body -Method POST
$idInfo = @($resp.Value)
}
catch { Write-LogError 'Failed to resolve role-assignment member display names' $_.Exception }
}
$sub = $info.displayName
Add-CustomSettingObject ([PSCustomObject]@{
Name = (Get-LanguageString 'SettingDetails.nameName')
Value = $info.displayName
EntityKey = 'displayName'
Category = $assignmentsCategory; SubCategory = $sub
})
if ($info.description) {
Add-CustomSettingObject ([PSCustomObject]@{
Name = (Get-LanguageString 'SettingDetails.descriptionName')
Value = $info.description
EntityKey = 'description'
Category = $assignmentsCategory; SubCategory = $sub
})
}
$admins = @()
foreach ($id in @($info.members)) {
$resolved = $idInfo | Where-Object Id -EQ $id | Select-Object -First 1
$admins += if ($resolved.displayName) { $resolved.displayName } else { $id }
}
if ($admins.Count -gt 0) {
Add-CustomSettingObject ([PSCustomObject]@{
Name = (Get-LanguageString 'RoleAssignment.RoleAssignmentAdmin')
Value = ($admins -join $Context.ObjectSeparator)
EntityKey = 'members'
Category = $assignmentsCategory; SubCategory = $sub
})
}
$scopeMembers = @()
foreach ($id in @($info.scopeMembers)) {
$resolved = $idInfo | Where-Object Id -EQ $id | Select-Object -First 1
$scopeMembers += if ($resolved.displayName) { $resolved.displayName } else { $id }
}
if ($scopeMembers.Count -gt 0) {
Add-CustomSettingObject ([PSCustomObject]@{
Name = (Get-LanguageString 'RoleAssignment.RoleAssignmentScope')
Value = ($scopeMembers -join $Context.ObjectSeparator)
EntityKey = 'scopeMembers'
Category = $assignmentsCategory; SubCategory = $sub
})
}
$scopeTags = @($info.roleScopeTags | ForEach-Object { $_.displayName })
if ($scopeTags.Count -gt 0) {
Add-CustomSettingObject ([PSCustomObject]@{
Name = (Get-LanguageString 'TableHeaders.scopeTags')
Value = ($scopeTags -join $Context.ObjectSeparator)
EntityKey = 'scopeTags'
Category = $assignmentsCategory; SubCategory = $sub
})
}
}
}
}
[DocumentationRegistry]::RegisterHandler([RoleDefinitionDocHandler]::new())
@@ -0,0 +1,46 @@
# Role Scope Tag documentation handler.
#
# Ported from old Extensions/DocumentationCustom.psm1:4882 (Invoke-
# CDDocumentScopeTag). Claims @odata.type='#microsoft.graph.roleScopeTag'.
#
# Old handler accumulated per-tag rows into a cross-batch
# $script:ObjectTypeFullTable hashtable that the output providers flushed as a
# single consolidated "Scope Tags" table at PostProcess time. The new engine
# already has $ctx.ObjectTypeFullTable for the same purpose, but no output
# provider consumes it yet — until that's wired up, we just emit per-object
# BasicInfo so each tag at least documents independently rather than being
# dropped on the floor as NoProvider.
#
# Assignments are intentionally NOT translated here: Invoke-TranslateAssignments
# is a separate ~370-LOC port (DocumentationMigration.md risk #4) that no
# handler in the new project calls yet. When that lands, this handler can
# trivially call it after the BasicInfo block.
class ScopeTagDocHandler : DocumentationHandlerBase {
ScopeTagDocHandler() {
$this.ODataTypes = @('#microsoft.graph.roleScopeTag')
}
[void] Document([object]$PolicyObject, [DocumentationContext]$Context) {
$obj = $PolicyObject.JsonObject
# Plain Name + Description rows. Scope tags don't have Platform supported
# or Profile type rows in any old-engine path, so skip the
# Add-BasicDefaultValues helper (which would emit blank Platform /
# Profile rows from a missing ObjectCategories entry).
if ($PolicyObject.Name) {
$nameProp = if ($PolicyObject.PolicyType._NameProperty) { $PolicyObject.PolicyType._NameProperty } else { 'displayName' }
Add-BasicPropertyValue (Get-LanguageString 'SettingDetails.nameName') $PolicyObject.Name $nameProp
}
$descValue = if ($obj.description) { $obj.description } else { '' }
Add-BasicPropertyValue (Get-LanguageString 'SettingDetails.descriptionName') $descValue 'description'
if ($null -ne $obj.isBuiltIn) {
$val = if ($obj.isBuiltIn) { Get-LanguageString 'SettingDetails.yes' } else { Get-LanguageString 'SettingDetails.no' }
Add-BasicPropertyValue (Get-LanguageString 'RoleScopeTag.isBuiltIn') $val 'isBuiltIn'
}
}
}
[DocumentationRegistry]::RegisterHandler([ScopeTagDocHandler]::new())
@@ -0,0 +1,90 @@
# Terms of Use (agreement) documentation handler.
#
# Ported from old Extensions/DocumentationCustom.psm1:2361. Note this handler
# does NOT use Add-BasicDefaultValues — the old engine only emits Name +
# Profile type for agreements (no Description, no Created/Modified).
class TermsOfUseDocHandler : DocumentationHandlerBase {
TermsOfUseDocHandler() {
$this.ODataTypes = @('#microsoft.graph.agreement')
}
[void] Document([object]$PolicyObject, [DocumentationContext]$Context) {
$obj = $PolicyObject.JsonObject
$offLabel = Get-LanguageString 'SettingDetails.offOption'
$onLabel = Get-LanguageString 'SettingDetails.onOption'
# BasicInfo: just Name + Profile type
Add-BasicPropertyValue (Get-LanguageString 'SettingDetails.nameName') $obj.displayName 'displayName'
Add-BasicPropertyValue (Get-LanguageString 'TableHeaders.configurationType') (Get-LanguageString 'AzureCA.menuItemTermsOfUse') '@odata.type'
$viewingValue = if ($obj.isViewingBeforeAcceptanceRequired) { $onLabel } else { $offLabel }
Add-CustomSettingObject ([PSCustomObject]@{
Name = (Get-LanguageString 'TermsOfUse.Wizard.agreementIsViewingBeforeAcceptanceRequiredLabel')
Value = $viewingValue; Category = $null; SubCategory = $null
EntityKey = 'isViewingBeforeAcceptanceRequired'
})
$perDeviceValue = if ($obj.isPerDeviceAcceptanceRequired) { $onLabel } else { $offLabel }
Add-CustomSettingObject ([PSCustomObject]@{
Name = (Get-LanguageString 'TermsOfUse.Wizard.agreementIsPerDeviceAcceptanceRequiredLabel')
Value = $perDeviceValue; Category = $null; SubCategory = $null
EntityKey = 'isPerDeviceAcceptanceRequired'
})
$expirationValue = if ($obj.termsExpiration) { $onLabel } else { $offLabel }
Add-CustomSettingObject ([PSCustomObject]@{
Name = (Get-LanguageString 'TermsOfUse.Wizard.isAcceptanceExpirationEnabledLabel')
Value = $expirationValue; Category = $null; SubCategory = $null
EntityKey = 'isAcceptanceExpirationEnabledLabel'
})
# Expiration details (only when termsExpiration is set)
if ($obj.termsExpiration.startDateTime) {
try {
if ($obj.termsExpiration.startDateTime -is [datetime]) {
$tmp = if ($obj.termsExpiration.startDateTime.Kind -eq 'Utc') { $obj.termsExpiration.startDateTime.ToLocalTime() } else { $obj.termsExpiration.startDateTime }
}
else {
$tmp = ([datetime]::Parse($obj.termsExpiration.startDateTime, [System.Globalization.CultureInfo]::InvariantCulture, [System.Globalization.DateTimeStyles]::AssumeUniversal -bor [System.Globalization.DateTimeStyles]::AdjustToUniversal)).ToLocalTime()
}
$startStr = $tmp.ToShortDateString()
}
catch {
Write-Log "Failed to parse date from string $($obj.termsExpiration.startDateTime)" 2
$startStr = $obj.termsExpiration.startDateTime
}
Add-CustomSettingObject ([PSCustomObject]@{
Name = (Get-LanguageString 'TermsOfUse.Wizard.acceptanceExpirationStartDateTimeLabel')
Value = $startStr; Category = $null; SubCategory = $null
EntityKey = 'startDateTime'
})
$freqValue = switch ($obj.termsExpiration.frequency) {
'P365D' { Get-LanguageString 'TermsOfUse.AcceptanceExpirationFrequency.annually' }
'P180D' { Get-LanguageString 'TermsOfUse.AcceptanceExpirationFrequency.biannually' }
'P30D' { Get-LanguageString 'TermsOfUse.AcceptanceExpirationFrequency.monthly' }
'P90D' { Get-LanguageString 'TermsOfUse.AcceptanceExpirationFrequency.quarterly' }
default { $null }
}
Add-CustomSettingObject ([PSCustomObject]@{
Name = (Get-LanguageString 'TermsOfUse.Wizard.acceptanceExpirationFrequencyLabel')
Value = $freqValue; Category = $null; SubCategory = $null
EntityKey = 'frequency'
})
}
if ($null -ne $obj.userReacceptRequiredFrequency) {
Add-CustomSettingObject ([PSCustomObject]@{
Name = (Get-LanguageString 'TermsOfUse.Wizard.acceptanceDurationLabel')
Value = (Get-DurationValue $obj.userReacceptRequiredFrequency)
Category = $null; SubCategory = $null
EntityKey = 'userReacceptRequiredFrequency'
})
}
}
}
[DocumentationRegistry]::RegisterHandler([TermsOfUseDocHandler]::new())
@@ -0,0 +1,400 @@
# Windows Kiosk Configuration documentation handler.
#
# Ported from old Extensions/DocumentationCustom.psm1:3923 (Invoke-
# CDDocumentWindowsKioskConfiguration). Claims @odata.type=
# '#microsoft.graph.windowsKioskConfiguration'.
#
# Generic Profile/walker can't handle this type because the actual settings
# live under nested $obj.kioskProfiles[0].appConfiguration /
# .userAccountsConfiguration with discriminated @odata.type subtypes
# (windowsKioskSingleWin32App, windowsKioskSingleUWPApp, windowsKioskMultipleApps,
# windowsKioskAutologon, windowsKioskAzureADGroup/User, etc.). A handler with
# explicit subtype dispatch is required.
#
# Preserves an old-engine quirk: when userAccountsConfiguration is an array of
# mixed AAD User + AAD Group entries, PS evaluates the switch on the implicit
# array of @odata.types and `-eq 'kioskAADUserAndGroup'` on the resulting array
# returns the matching items (truthy in if-test). The "User logon type" row
# then shows empty because `"SettingDetails.$($logonTypeLngId)"` interpolates
# the array as space-joined.
class WindowsKioskDocHandler : DocumentationHandlerBase {
WindowsKioskDocHandler() {
$this.ODataTypes = @('#microsoft.graph.windowsKioskConfiguration')
}
[void] Document([object]$PolicyObject, [DocumentationContext]$Context) {
$obj = $PolicyObject.JsonObject
# ---- Basic info ----
Add-BasicDefaultValues $PolicyObject
Add-BasicAdditionalValues $PolicyObject
Add-BasicPropertyValue (Get-LanguageString 'TableHeaders.configurationType') (Get-LanguageString 'Category.kioskConfigurationV2') '@odata.type'
# Old engine emits a Platform row from $obj.platform. The raw payload
# doesn't carry one for this type, so the lookup resolves to empty —
# golden fixtures still contain the empty row, so emit it for parity.
Add-BasicPropertyValue (Get-LanguageString 'Inputs.platformLabel') (Get-LanguageString "Platform.$($obj.platform)") 'platform'
# ---- Settings ----
$category = Get-LanguageString 'Category.kiosk'
$appConfig = $obj.kioskProfiles[0].appConfiguration
$userConfig = $obj.kioskProfiles[0].userAccountsConfiguration
# kioskMode dispatch
if ($appConfig.'@odata.type' -eq '#microsoft.graph.windowsKioskSingleWin32App' -or
$appConfig.'@odata.type' -eq '#microsoft.graph.windowsKioskSingleUWPApp') {
$kioskModeType = 'single'
$kioskMode = Get-LanguageString 'SettingDetails.kioskSelectionSingleMode'
}
else {
$kioskModeType = 'multi'
$kioskMode = Get-LanguageString 'SettingDetails.kioskSelectionMultiMode'
}
Add-CustomSettingObject ([PSCustomObject]@{
Name = Get-LanguageString 'SettingDetails.kioskSelectionName'
Value = $kioskMode
EntityKey = 'kioskMode'
Category = $category
SubCategory = $null
})
# User logon type
$logonTypeLngId = switch ($userConfig.'@odata.type') {
'#microsoft.graph.windowsKioskAutologon' { 'kioskUserLogonTypeAutologon' }
'#microsoft.graph.windowsKioskAzureADUser' { 'kioskAADUserAndGroup' }
'#microsoft.graph.windowsKioskAzureADGroup' { 'kioskAADUserAndGroup' }
'#microsoft.graph.windowsKioskLocalUser' { 'kioskAppTypeStore' }
'#microsoft.graph.windowsKioskVisitor' { 'kioskVisitor' }
}
$logonType = if ($logonTypeLngId) {
Get-LanguageString "SettingDetails.$logonTypeLngId"
} else {
Write-Log "Unknown kiosk user logon type. $($userConfig.'@odata.type')" 2
$null
}
Add-CustomSettingObject ([PSCustomObject]@{
Name = Get-LanguageString 'SettingDetails.kioskSelectionUsers'
Value = $logonType
EntityKey = 'userAccountsConfigurationType'
Category = $category
SubCategory = $null
})
# User logon name(s)
if ($logonTypeLngId -eq 'kioskAADUserAndGroup') {
$aadUser = Get-LanguageString 'SettingDetails.kioskAADUser'
$aadGroup = Get-LanguageString 'SettingDetails.kioskAADGroup'
$users = @()
foreach ($u in $userConfig) {
$sep = $Context.PropertySeparator
if ($u.'@odata.type' -eq '#microsoft.graph.windowsKioskAzureADUser') {
$users += "$($u.userPrincipalName)$sep$aadUser"
}
else {
$users += "$($u.displayName)$sep$aadGroup"
}
}
Add-CustomSettingObject ([PSCustomObject]@{
Name = Get-LanguageString 'SettingDetails.kioskUserAccountName'
Value = $users -join $Context.ObjectSeparator
EntityKey = 'userAccounts'
Category = $category
SubCategory = $null
})
}
elseif ($userConfig.'@odata.type' -eq '#microsoft.graph.windowsKioskLocalUser') {
Add-CustomSettingObject ([PSCustomObject]@{
Name = Get-LanguageString 'SettingDetails.kioskUserAccountName'
Value = $userConfig.userName
EntityKey = 'userName'
Category = $category
SubCategory = $null
})
}
# Single-app: detect underlying app type and emit type-specific rows
if ($kioskModeType -eq 'single') {
$uwpAppType = $null
$appType = $null
if ($appConfig.'@odata.type' -eq '#microsoft.graph.windowsKioskSingleWin32App') {
$uwpAppType = 'win32App'
$appType = Get-LanguageString 'SettingDetails.selectWin32AppForEdge86'
}
elseif ($appConfig.'@odata.type' -eq '#microsoft.graph.windowsKioskSingleUWPApp') {
if ($appConfig.uwpApp.appUserModelId -like 'Microsoft.MicrosoftEdge*') {
$uwpAppType = 'edge'
$appType = Get-LanguageString 'SettingDetails.selectMicrosoftEdgeApp'
}
elseif ($appConfig.uwpApp.appUserModelId -like 'Microsoft.KioskBrowser*') {
$uwpAppType = 'kioskBrowser'
$appType = Get-LanguageString 'SettingDetails.selectKioskBrowserApp'
}
else {
$uwpAppType = 'storeApp'
$appType = Get-LanguageString 'SettingDetails.selectStoreApp'
}
}
Add-CustomSettingObject ([PSCustomObject]@{
Name = Get-LanguageString 'SettingDetails.kioskApplicationType'
Value = $appType
EntityKey = 'kioskApplicationType'
Category = $category
SubCategory = $null
})
$edgeKioskModeType = if ($appConfig.win32App.edgeKioskType -eq 'publicBrowsing') {
Get-LanguageString 'SettingDetails.edgeKioskModeTypePublicBrowsingInPrivate'
} else {
Get-LanguageString 'SettingDetails.edgeKioskModeTypeDigitalSignage'
}
if ($uwpAppType -eq 'win32App') {
Add-CustomSettingObject ([PSCustomObject]@{
Name = Get-LanguageString 'SettingDetails.win32EdgeKioskUrl'
Value = $appConfig.win32App.edgeKiosk
EntityKey = 'edgeKiosk'
Category = $category
SubCategory = $null
})
Add-CustomSettingObject ([PSCustomObject]@{
Name = Get-LanguageString 'SettingDetails.edgeKioskModeType'
Value = $edgeKioskModeType
EntityKey = 'edgeKioskType'
Category = $category
SubCategory = $null
})
Add-CustomSettingObject ([PSCustomObject]@{
Name = Get-LanguageString 'SettingDetails.edgeKioskResetAfterIdleTimeInMinutesName'
Value = $appConfig.win32App.edgeKioskIdleTimeoutMinutes
EntityKey = 'edgeKioskIdleTimeoutMinutes'
Category = $category
SubCategory = $null
})
}
elseif ($uwpAppType -eq 'edge') {
Add-CustomSettingObject ([PSCustomObject]@{
Name = Get-LanguageString 'SettingDetails.edgeKioskModeType'
Value = $edgeKioskModeType
EntityKey = 'edgeKioskType'
Category = $category
SubCategory = $null
})
}
elseif ($uwpAppType -eq 'kioskBrowser') {
$show = Get-LanguageString 'BooleanActions.show'
$hide = Get-LanguageString 'BooleanActions.hide'
Add-CustomSettingObject ([PSCustomObject]@{
Name = Get-LanguageString 'SettingDetails.win10KioskBrowserDefaultWebsiteName'
Value = $obj.kioskBrowserDefaultUrl
EntityKey = 'kioskBrowserDefaultUrl'
Category = $category
SubCategory = $null
})
Add-CustomSettingObject ([PSCustomObject]@{
Name = Get-LanguageString 'SettingDetails.win10KioskBrowserHomeButtonName'
Value = if ($obj.kioskBrowserEnableHomeButton) { $show } else { $hide }
EntityKey = 'kioskBrowserEnableHomeButton'
Category = $category
SubCategory = $null
})
Add-CustomSettingObject ([PSCustomObject]@{
Name = Get-LanguageString 'SettingDetails.win10KioskBrowserNavigationButtonName'
Value = if ($obj.kioskBrowserEnableNavigationButtons) { $show } else { $hide }
EntityKey = 'kioskBrowserEnableNavigationButtons'
Category = $category
SubCategory = $null
})
Add-CustomSettingObject ([PSCustomObject]@{
Name = Get-LanguageString 'SettingDetails.win10KioskBrowserEnableEndSessionButtonName'
Value = if ($obj.kioskBrowserEnableEndSessionButton) { $show } else { $hide }
EntityKey = 'kioskBrowserEnableEndSessionButton'
Category = $category
SubCategory = $null
})
Add-CustomSettingObject ([PSCustomObject]@{
Name = Get-LanguageString 'SettingDetails.edgeKioskResetAfterIdleTimeInMinutesName'
Value = $obj.kioskBrowserRestartOnIdleTimeInMinutes
EntityKey = 'kioskBrowserRestartOnIdleTimeInMinutes'
Category = $category
SubCategory = $null
})
Add-CustomSettingObject ([PSCustomObject]@{
Name = Get-LanguageString 'SettingDetails.win10BlockedWebsitesName'
Value = $obj.kioskBrowserBlockedURLs -join $Context.ObjectSeparator
EntityKey = 'kioskBrowserBlockedURLs'
Category = $category
SubCategory = $null
})
Add-CustomSettingObject ([PSCustomObject]@{
Name = Get-LanguageString 'SettingDetails.win10AllowedWebsitesName'
Value = $obj.kioskBrowserBlockedUrlExceptions -join $Context.ObjectSeparator
EntityKey = 'kioskBrowserBlockedUrlExceptions'
Category = $category
SubCategory = $null
})
}
elseif ($uwpAppType -eq 'storeApp') {
Add-CustomSettingObject ([PSCustomObject]@{
Name = Get-LanguageString 'SettingDetails.kioskModeAppStoreUrlOrManagedAppIdName'
Value = $appConfig.uwpApp.name
EntityKey = 'edgeKioskType'
Category = $category
SubCategory = $null
})
}
}
# Multi-app: app table + start-layout / taskbar / downloads rows
if ($kioskModeType -eq 'multi') {
$apps = @()
foreach ($app in $appConfig.apps) {
$kioskTypeLngId = switch ($app.appType) {
'aumId' { 'kioskAppTypeAUMID' }
'desktop' { 'kioskAppTypeDesktop' }
'store' { 'kioskAppTypeStore' }
default { 'kioskAppTypeUnknown' }
}
$kioskTileLngId = switch ($app.startLayoutTileSize) {
'medium' { 'kioskTileMedium' }
'small' { 'kioskTileSmall' }
'wide' { 'kioskTileWide' }
'large' { 'kioskTileLarge' }
}
$sep = $Context.PropertySeparator
$autoLaunchStr = if ($app.autoLaunch -eq $true) { Get-LanguageString 'SettingDetails.yes' } else { Get-LanguageString 'SettingDetails.no' }
$apps += '{0}{1}{2}{3}{4}{5}{6}' -f $app.Name, $sep, (Get-LanguageString "SettingDetails.$kioskTypeLngId"), $sep, $autoLaunchStr, $sep, (Get-LanguageString "SettingDetails.$kioskTileLngId")
}
if ($apps.Count -gt 0) {
Add-CustomSettingObject ([PSCustomObject]@{
Name = Get-LanguageString 'SettingDetails.kioskAppTableName'
Value = $apps -join $Context.ObjectSeparator
EntityKey = 'kioskApps'
Category = $category
SubCategory = $null
})
}
$altLayout = if ($null -ne $appConfig.startMenuLayoutXml) { Get-LanguageString 'SettingDetails.yes' } else { Get-LanguageString 'SettingDetails.no' }
Add-CustomSettingObject ([PSCustomObject]@{
Name = Get-LanguageString 'SettingDetails.alternativeStartLayoutName'
Value = $altLayout
EntityKey = 'alternativeStartLayout'
Category = $category
SubCategory = $null
})
if ($null -ne $appConfig.startMenuLayoutXml) {
$xmlStr = try {
[System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($appConfig.startMenuLayoutXml))
} catch { $appConfig.startMenuLayoutXml }
Add-CustomSettingObject ([PSCustomObject]@{
Name = Get-LanguageString 'SettingDetails.kioskStartMenuLayoutXmlName'
Value = $xmlStr
EntityKey = 'startMenuLayoutXml'
Category = $category
SubCategory = $null
})
}
$taskBar = if ($appConfig.showTaskBar) { Get-LanguageString 'BooleanActions.show' } else { Get-LanguageString 'BooleanActions.hide' }
Add-CustomSettingObject ([PSCustomObject]@{
Name = Get-LanguageString 'SettingDetails.kioskShowTaskbarName'
Value = $taskBar
EntityKey = 'showTaskBar'
Category = $category
SubCategory = $null
})
$downloads = if ($appConfig.allowAccessToDownloadsFolder) { Get-LanguageString 'SettingDetails.yes' } else { Get-LanguageString 'SettingDetails.no' }
Add-CustomSettingObject ([PSCustomObject]@{
Name = Get-LanguageString 'SettingDetails.win10KioskAccessDownloadsFolderName'
Value = $downloads
EntityKey = 'allowAccessToDownloadsFolder'
Category = $category
SubCategory = $null
})
# disallowDesktopApps blocks classic Win32/desktop apps on the multi-app
# kiosk. No scraped label exists for this toggle, so use an ASCII literal
# (the Yes/No value is still localized).
$disallowDesktopApps = if ($appConfig.disallowDesktopApps) { Get-LanguageString 'SettingDetails.yes' } else { Get-LanguageString 'SettingDetails.no' }
Add-CustomSettingObject ([PSCustomObject]@{
Name = 'Block desktop (Win32) apps'
Value = $disallowDesktopApps
EntityKey = 'disallowDesktopApps'
Category = $category
SubCategory = $null
})
}
# Force-restart maintenance window
$forceUpdateLng = if ($obj.windowsKioskForceUpdateSchedule) { 'BooleanActions.require' } else { 'BooleanActions.notConfigured' }
Add-CustomSettingObject ([PSCustomObject]@{
Name = Get-LanguageString 'SettingDetails.kioskForceRestart'
Value = Get-LanguageString $forceUpdateLng
EntityKey = 'windowsKioskForceUpdateSchedule'
Category = $category
SubCategory = $null
})
if ($obj.windowsKioskForceUpdateSchedule) {
try {
$startDateObj = if ($obj.windowsKioskForceUpdateSchedule.startDateTime -is [DateTime]) {
$tmp = $obj.windowsKioskForceUpdateSchedule.startDateTime
if ($tmp.Kind -eq [DateTimeKind]::Utc) { $tmp.ToLocalTime() } else { $tmp }
} else {
Get-Date $obj.windowsKioskForceUpdateSchedule.startDateTime -ErrorAction Stop
}
Add-CustomSettingObject ([PSCustomObject]@{
Name = Get-LanguageString 'SettingDetails.kioskStartDateTime'
Value = ($startDateObj.ToShortDateString() + $Context.ObjectSeparator + $startDateObj.ToShortTimeString())
EntityKey = 'startDateTime'
Category = $category
SubCategory = $null
})
$recurrenceType = switch ($obj.windowsKioskForceUpdateSchedule.recurrence) {
'weekly' { 'kioskWeekly' }
'monthly' { 'kioskMonthly' }
default { 'kioskDaily' }
}
Add-CustomSettingObject ([PSCustomObject]@{
Name = Get-LanguageString 'SettingDetails.kioskRecurrence'
Value = Get-LanguageString "SettingDetails.$recurrenceType"
EntityKey = 'recurrence'
Category = $category
SubCategory = $null
})
if ($obj.windowsKioskForceUpdateSchedule.recurrence -eq 'weekly') {
Add-CustomSettingObject ([PSCustomObject]@{
Name = Get-LanguageString 'SettingDetails.dayOfWeek'
Value = Get-LanguageString "SettingDetails.$($obj.windowsKioskForceUpdateSchedule.dayofWeek)"
EntityKey = 'dayofWeek'
Category = $category
SubCategory = $null
})
}
elseif ($obj.windowsKioskForceUpdateSchedule.recurrence -eq 'monthly') {
Add-CustomSettingObject ([PSCustomObject]@{
Name = Get-LanguageString 'SettingDetails.dayOfMonth'
Value = $obj.windowsKioskForceUpdateSchedule.dayofMonth
EntityKey = 'dayofMonth'
Category = $category
SubCategory = $null
})
}
}
catch { Write-Log "Failed to format kiosk force-update schedule: $($_.Exception.Message)" 2 }
}
}
}
[DocumentationRegistry]::RegisterHandler([WindowsKioskDocHandler]::new())