Files
IntuneManagement/Internal/Documentation/InputProviders/DocumentationObjectInfoCustomization.ps1
T

1320 lines
90 KiB
PowerShell

# Documentation customizations - the port of the old Extensions/DocumentationCustom.psm1
# provider. Registered once as a MatchAll ObjectInfo customizer, so its hooks run for
# EVERY object flowing through the Manifest/Profile (ObjectInfo) walk, exactly like the
# old single provider; each hook branches internally on @odata.type and no-ops for
# types it doesn't handle. These augment schema-driven translation and deliberately do
# not claim whole-object handler dispatch (that is what DocumentationHandlerBase is for).
function Set-DocumentationSyntheticProperty {
param($Obj, [string]$Name, $Value)
if ($null -eq $Obj) { return }
$Obj | Add-Member -MemberType NoteProperty -Name $Name -Value $Value -Force
}
function ConvertFrom-DocumentationBase64 {
param($Value)
if (-not $Value) { return $Value }
try { return [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String([string]$Value)) }
catch { return $Value }
}
function Add-DocumentationEncodedScript {
param([DocumentationContext]$Context, [string]$Header, [string]$Caption, $Content)
if ($Content) { $Context.AddScript($Header, $Caption, (ConvertFrom-DocumentationBase64 $Content)) }
}
function Initialize-DocumentationCustomObject {
param($Obj, [DocumentationContext]$Context)
$type = [string]$Obj.'@odata.type'
switch ($type) {
'#microsoft.graph.deviceEnrollmentNotificationConfiguration' {
# Not a v3 port - there was no v3 support for this type.
#
# Almost nothing the portal shows for this policy is on the policy.
# The two properties that look like they hold the notification channel
# and its template do not: measured against three policies in a live
# tenant, templateType comes back as the string "0" (a value the
# published enum does not even define) and notificationMessageTemplateId
# is an all-zero GUID. What the policy really stores is
# notificationTemplates, whose entries are shaped "<Channel>_<templateId>"
# (e.g. "Push_49d3fb35-..."), one per enabled channel. The subject and
# body live on that template as its localized messages, and every
# template carries the same internal display name
# ("EnrollmentNotificationInternalMEO"), so the name is worth nothing
# to a reader and the message text is worth everything.
$templates = Get-CDNotificationMessageTemplates
foreach ($channel in 'push', 'email') {
$prefix = "$($channel)_"
$entry = @($Obj.notificationTemplates) |
Where-Object { "$_".StartsWith($prefix, [StringComparison]::OrdinalIgnoreCase) } |
Select-Object -First 1
# A channel is on exactly when the policy carries a template for it.
Set-DocumentationSyntheticProperty $Obj "$($channel)NotificationEnabled" ([bool]$entry)
$subject = $null
$body = $null
if ($entry) {
$templateId = ("$entry" -split '_', 2)[1]
$template = $templates | Where-Object { $_.id -eq $templateId } | Select-Object -First 1
$messages = @($template.localizedNotificationMessages)
# The locale the policy asks for, else the template's own
# default, else whatever single message the template has.
$message = $null
if ($Obj.defaultLocale) { $message = $messages | Where-Object { $_.locale -eq $Obj.defaultLocale } | Select-Object -First 1 }
if (-not $message) { $message = $messages | Where-Object { $_.isDefault } | Select-Object -First 1 }
if (-not $message) { $message = $messages | Select-Object -First 1 }
$subject = $message.subject
$body = $message.messageTemplate
}
Set-DocumentationSyntheticProperty $Obj "$($channel)NotificationSubject" $subject
Set-DocumentationSyntheticProperty $Obj "$($channel)NotificationMessage" $body
}
# The portal renders brandingOptions as one On/Off toggle per flag under
# Email Header / Email Footer rather than as a single combined value, so
# the document does too. These come from the POLICY: the templates in a
# live tenant carry brandingOptions of their own and the portal ignores
# them (a policy reading "none" shows every toggle off even where its
# email template sets all five).
$branding = @([string]$Obj.brandingOptions -split ',' | ForEach-Object { $_.Trim() })
$brandingFlags = [ordered]@{
brandingIncludeCompanyLogo = 'includeCompanyLogo'
brandingIncludeCompanyName = 'includeCompanyName'
brandingIncludeContactInformation = 'includeContactInformation'
brandingIncludeCompanyPortalLink = 'includeCompanyPortalLink'
brandingIncludeDeviceDetails = 'includeDeviceDetails'
}
foreach ($propertyName in @($brandingFlags.Keys)) {
Set-DocumentationSyntheticProperty $Obj $propertyName ($branding -contains $brandingFlags[$propertyName])
}
}
'#microsoft.graph.androidWorkProfileGeneralDeviceConfiguration' {
$package = $Obj.vpnAlwaysOnPackageIdentifier
$known = @('com.cisco.anyconnect.vpn.android.avf','com.f5.edge.client_ics','com.paloaltonetworks.globalprotect','net.pulsesecure.pulsesecure')
Set-DocumentationSyntheticProperty $Obj 'vpnAlwaysOnPackageIdentifierSelector' $(if (-not $package) { $null } elseif ($package -in $known) { $package } else { 'custom' })
Set-DocumentationSyntheticProperty $Obj 'vpnAlwaysOnEnabled' (-not [string]::IsNullOrEmpty($package))
}
'#microsoft.graph.androidDeviceOwnerGeneralDeviceConfiguration' {
$package = $Obj.vpnAlwaysOnPackageIdentifier
$known = @('com.cisco.anyconnect.vpn.android.avf','com.f5.edge.client_ics','com.paloaltonetworks.globalprotect','net.pulsesecure.pulsesecure')
Set-DocumentationSyntheticProperty $Obj 'vpnAlwaysOnPackageIdentifierSelector' $(if (-not $package) { $null } elseif ($package -in $known) { $package } else { 'custom' })
Set-DocumentationSyntheticProperty $Obj 'vpnAlwaysOnEnabled' (-not [string]::IsNullOrEmpty($package))
Set-DocumentationSyntheticProperty $Obj 'globalProxyEnabled' ($null -ne $Obj.globalProxy)
if ($Obj.globalProxy.proxyAutoConfigURL) {
Set-DocumentationSyntheticProperty $Obj 'globalProxyTypeSelector' 'proxyAutoConfig'
Set-DocumentationSyntheticProperty $Obj 'globalProxyProxyAutoConfigURL' $Obj.globalProxy.proxyAutoConfigURL
}
elseif ($Obj.globalProxy.host) {
Set-DocumentationSyntheticProperty $Obj 'globalProxyTypeSelector' 'direct'
Set-DocumentationSyntheticProperty $Obj 'globalProxyHost' $Obj.globalProxy.host
Set-DocumentationSyntheticProperty $Obj 'globalProxyPort' $Obj.globalProxy.port
Set-DocumentationSyntheticProperty $Obj 'globalProxyExcludedHosts' $Obj.globalProxy.excludedHosts
}
if ($Obj.PSObject.Properties['factoryResetDeviceAdministratorEmails']) {
Set-DocumentationSyntheticProperty $Obj 'factoryResetProtections' $(if (@($Obj.factoryResetDeviceAdministratorEmails).Count) { 'factoryResetProtectionEnabled' } else { 'factoryResetProtectionDisabled' })
Set-DocumentationSyntheticProperty $Obj 'googleAccountEmailAddressesList' (@($Obj.factoryResetDeviceAdministratorEmails) -join $Context.ObjectSeparator)
}
if ($Obj.PSObject.Properties['passwordBlockKeyguardFeatures']) { Set-DocumentationSyntheticProperty $Obj 'passwordBlockKeyguardFeaturesList' $Obj.passwordBlockKeyguardFeatures }
if ($Obj.PSObject.Properties['stayOnModes']) { Set-DocumentationSyntheticProperty $Obj 'stayOnModesList' $Obj.stayOnModes }
if ($Obj.PSObject.Properties['playStoreMode']) { Set-DocumentationSyntheticProperty $Obj 'publicPlayStoreEnabled' ($Obj.playStoreMode -eq 'blockList') }
}
'#microsoft.graph.androidEasEmailProfileConfiguration' {
Set-DocumentationSyntheticProperty $Obj 'domainNameSourceType' $(if ($null -ne $Obj.customDomainName) { 'CustomDomainName' } else { 'AAD' })
}
'#microsoft.graph.windowsDeliveryOptimizationConfiguration' {
Set-DocumentationSyntheticProperty $Obj 'groupIdSourceSelector' $(if ($Obj.groupIdSource.groupIdSourceOption) { $Obj.groupIdSource.groupIdSourceOption } else { 'notConfigured' })
}
'#microsoft.graph.androidManagedAppProtection' { Initialize-ManagedAppProtectionSyntheticProperties $Obj }
'#microsoft.graph.iosManagedAppProtection' { Initialize-ManagedAppProtectionSyntheticProperties $Obj }
'#microsoft.graph.defaultManagedAppProtection' { Initialize-ManagedAppProtectionSyntheticProperties $Obj }
'#microsoft.graph.windowsUpdateForBusinessConfiguration' {
Set-DocumentationSyntheticProperty $Obj 'useDeadLineSettings' ($null -ne $Obj.deadlineForFeatureUpdatesInDays -or $null -ne $Obj.deadlineForQualityUpdatesInDays -or $null -ne $Obj.deadlineGracePeriodInDays -or $null -ne $Obj.postponeRebootUntilAfterDeadline)
}
'#microsoft.graph.azureADWindowsAutopilotDeploymentProfile' { Initialize-AutopilotSyntheticProperties $Obj 'azureAD' }
'#microsoft.graph.activeDirectoryWindowsAutopilotDeploymentProfile' { Initialize-AutopilotSyntheticProperties $Obj 'hybrid' }
'#microsoft.graph.officeSuiteApp' {
Set-DocumentationSyntheticProperty $Obj 'VersionToInstall' $(if ([string]::IsNullOrEmpty($Obj.targetVersion)) { Get-LanguageString 'SettingDetails.latest' } else { $Obj.targetVersion })
Set-DocumentationSyntheticProperty $Obj 'useMicrosoftSearchAsDefault' ($Obj.excludedApps.bing -eq $false)
if ($Obj.officeConfigurationXml) { Set-DocumentationSyntheticProperty $Obj 'MSAppsConfigXml' (ConvertFrom-DocumentationBase64 $Obj.officeConfigurationXml) }
}
'#microsoft.graph.win32LobApp' { Initialize-Win32LobAppSyntheticProperties $Obj $Context }
'#microsoft.graph.iosGeneralDeviceConfiguration' { Initialize-IosGeneralSyntheticProperties $Obj $Context }
'#microsoft.graph.iosUpdateConfiguration' { Initialize-IosUpdateSyntheticProperties $Obj $Context }
'#microsoft.graph.windowsWifiEnterpriseEAPConfiguration' { Initialize-WifiEnterpriseEapSyntheticProperties $Obj }
'#microsoft.graph.windowsKioskConfiguration' { Initialize-WindowsKioskSyntheticProperties $Obj }
'#microsoft.graph.windows10VpnConfiguration' {
Set-DocumentationSyntheticProperty $Obj 'syntheticWipOrApps' $(if ($Obj.windowsInformationProtectionDomain) { 1 } elseif ($Obj.onlyAssociatedAppsCanUseConnection) { 2 } else { 0 })
if ($null -eq $Obj.profileTarget) { $Obj.profileTarget = 'user' }
}
'#microsoft.graph.androidDeviceOwnerVpnConfiguration' { Initialize-AndroidVpnSyntheticProperties $Obj $Context }
'#microsoft.graph.androidForWorkVpnConfiguration' { Initialize-AndroidVpnSyntheticProperties $Obj $Context }
'#microsoft.graph.androidWorkProfileVpnConfiguration' { Initialize-AndroidVpnSyntheticProperties $Obj $Context }
'#microsoft.graph.iosDeviceFeaturesConfiguration' {
foreach ($configuration in @($Obj.iosSingleSignOnExtension.configurations)) {
Set-DocumentationSyntheticProperty $configuration 'configurationKey' $configuration.key
Set-DocumentationSyntheticProperty $configuration 'configurationValue' $configuration.value
$configurationType = switch ($configuration.'@odata.type') {
'#microsoft.graph.keyStringValuePair' { Get-LanguageString 'SettingDetails.singleSignOnExtensionConfigurationsTypeColumnOptionString' }
'#microsoft.graph.keyBooleanValuePair' { Get-LanguageString 'SettingDetails.singleSignOnExtensionConfigurationsTypeColumnOptionBoolean' }
'#microsoft.graph.keyIntegerValuePair' { Get-LanguageString 'SettingDetails.singleSignOnExtensionConfigurationsTypeColumnOptionInteger' }
default { 'notConfigured' }
}
Set-DocumentationSyntheticProperty $configuration 'configurationType' $configurationType
}
Set-DocumentationSyntheticProperty $Obj 'kerberosPrincipalName' $(if ($Obj.singleSignOnSettings.kerberosPrincipalName) { $Obj.singleSignOnSettings.kerberosPrincipalName } else { 'notConfigured' })
Set-DocumentationSyntheticProperty $Obj 'singleSignOnExtensionType' $(if ($Obj.iosSingleSignOnExtension.'@odata.type') { $Obj.iosSingleSignOnExtension.'@odata.type' } else { 'notConfigured' })
}
'#microsoft.graph.macOSDeviceFeaturesConfiguration' {
Set-DocumentationSyntheticProperty $Obj 'singleSignOnExtensionType' $(if ($Obj.macOSSingleSignOnExtension.'@odata.type') { $Obj.macOSSingleSignOnExtension.'@odata.type' } else { 'notConfigured' })
}
'#microsoft.graph.deviceHealthScript' {
Set-DocumentationSyntheticProperty $Obj 'detectionScriptAdded' (-not [string]::IsNullOrEmpty($Obj.detectionScriptContent))
Set-DocumentationSyntheticProperty $Obj 'remediationScriptAdded' (-not [string]::IsNullOrEmpty($Obj.remediationScriptContent))
Set-DocumentationSyntheticProperty $Obj 'useLoggedOnCredentials' ($Obj.runAsAccount -ne 'system')
if ($Obj.detectionScriptContent) {
$decoded = ConvertFrom-DocumentationBase64 $Obj.detectionScriptContent
Set-DocumentationSyntheticProperty $Obj 'detectionScriptContentString' $decoded
$header = Get-LanguageString 'ProactiveRemediations.Create.Settings.DetectionScriptMultiLineTextBox.label'
$Context.AddScript($header, "$header - $($Obj.displayName)", $decoded)
}
if ($Obj.remediationScriptContent) {
$decoded = ConvertFrom-DocumentationBase64 $Obj.remediationScriptContent
Set-DocumentationSyntheticProperty $Obj 'remediationScriptContentString' $decoded
$header = Get-LanguageString 'ProactiveRemediations.Create.Settings.RemediationScriptMultiLineTextBox.label'
$Context.AddScript($header, "$header - $($Obj.displayName)", $decoded)
}
}
'#microsoft.graph.hardwareConfiguration' {
$vendor = Get-LanguageString "HardwareConfig.Settings.Tab.HardwareDropDown.$($Obj.hardwareConfigurationFormat)" $Obj.hardwareConfigurationFormat -IgnoreMissing
Set-DocumentationSyntheticProperty $Obj 'vendor' $vendor
if ($Obj.configurationFileContent) {
$decoded = ConvertFrom-DocumentationBase64 $Obj.configurationFileContent
Set-DocumentationSyntheticProperty $Obj 'configurationFileContentString' $decoded
$header = Get-LanguageString 'HardwareConfig.Settings.Tab.Configurations.perDevicePassword'
$Context.AddScript($header, "$header - $($Obj.displayName)", $decoded)
}
}
'#microsoft.graph.deviceManagementScript' { Add-DocumentationEncodedScript $Context $Obj.fileName "$($Obj.displayName) - $(Get-LanguageString 'WindowsManagement.powerShellScriptObjectName')" $Obj.scriptContent }
'#microsoft.graph.deviceShellScript' { Add-DocumentationEncodedScript $Context $Obj.fileName "$($Obj.displayName) - $(Get-LanguageString 'WindowsManagement.shellScriptObjectName')" $Obj.scriptContent }
'#microsoft.graph.deviceCustomAttributeShellScript' { Add-DocumentationEncodedScript $Context $Obj.fileName "$($Obj.displayName) - $(Get-LanguageString 'WindowsManagement.customAttributeObjectName')" $Obj.scriptContent }
'#microsoft.graph.windows10EndpointProtectionConfiguration' { Initialize-EndpointProtectionSyntheticProperties $Obj }
'#microsoft.graph.windows10GeneralConfiguration' { Initialize-Windows10GeneralSyntheticProperties $Obj $Context }
'#microsoft.graph.windowsFeatureUpdateProfile' { Initialize-FeatureUpdateSyntheticProperties $Obj $Context }
'#microsoft.graph.windows10EnrollmentCompletionPageConfiguration' { Initialize-EnrollmentStatusPageSyntheticProperties $Obj $Context }
'#microsoft.graph.macOSEndpointProtectionConfiguration' {
Set-DocumentationSyntheticProperty $Obj 'firewallAllowedApps' @($Obj.firewallApplications | Where-Object allowsIncomingConnections -EQ $true)
Set-DocumentationSyntheticProperty $Obj 'firewallBlockedApps' @($Obj.firewallApplications | Where-Object allowsIncomingConnections -EQ $false)
}
'#microsoft.graph.windows10TeamGeneralConfiguration' {
Set-DocumentationSyntheticProperty $Obj 'syntheticAzureOperationalInsightsEnabled' ($Obj.azureOperationalInsightsBlockTelemetry -eq $false)
Set-DocumentationSyntheticProperty $Obj 'syntheticMaintenanceWindowEnabled' ($Obj.maintenanceWindowBlocked -eq $false)
}
'#microsoft.graph.windowsWifiConfiguration' {
if ($Obj.wifiSecurityType -eq 'wpa2Personal') { $Obj.preSharedKey = '********' }
}
}
if ($Obj.PSObject.Properties['securityRequireSafetyNetAttestationBasicIntegrity'] -and $Obj.PSObject.Properties['securityRequireSafetyNetAttestationCertifiedDevice']) {
$value = if ($Obj.securityRequireSafetyNetAttestationBasicIntegrity -and $Obj.securityRequireSafetyNetAttestationCertifiedDevice) { 'basicIntegrityAndCertified' }
elseif ($Obj.securityRequireSafetyNetAttestationBasicIntegrity) { 'basicIntegrity' } else { 'notConfigured' }
Set-DocumentationSyntheticProperty $Obj 'androidSafetyNetAttestationOptions' $value
}
Initialize-ConditionalLaunchSyntheticProperties $Obj
# Default enrollment policies: title the document by which policy it is.
#
# Five enrollment types ship a tenant default named "All users and all
# devices" - device limit, platform restrictions, enrollment status page,
# Windows Hello for Business and Windows Restore - so a document headed by the
# display name does not say which policy it is. The name is still documented, as
# the Name row inside the table.
#
# All five opt in, not just the two documented from a hand-written manifest:
# they now share one second-level heading
# (Core/DocumentationEnrollmentGrouping.ps1), which turns the identical titles
# from a cosmetic problem into three indistinguishable siblings under the same
# heading. The title, the sort order and the "is this the tenant default" test
# come from that one file so the heading and its children cannot drift apart.
if ($Context) {
$enrollmentName = Get-DocumentationEnrollmentDefaultName $Context.PolicyObject
if ($enrollmentName) { $Context.DocumentName = $enrollmentName }
}
# MergeBasicInfo stays limited to the two types whose settings come from a
# hand-written manifest. It changes the table layout, which the retitling above
# does not, and the other three render their own tables correctly already.
$mergeBasicInfoTypes = @(
'#microsoft.graph.deviceEnrollmentWindowsHelloForBusinessConfiguration'
'#microsoft.graph.windowsRestoreDeviceEnrollmentConfiguration'
)
if ($type -in $mergeBasicInfoTypes -and $Context) {
$Context.MergeBasicInfo = $true
}
}
function Initialize-AndroidVpnSyntheticProperties {
param($Obj, $Context)
Initialize-AndroidVpnCustomSettings $Obj
Initialize-AndroidVpnTunnelSiteName $Obj $Context
}
function Initialize-AndroidVpnCustomSettings {
param($Obj)
# The portal's Microsoft Defender "Custom settings" table (Configuration key /
# Value type / Configuration value) has no matching Graph property. Graph stores it
# inside customData: each customData entry's value is itself a JSON array of
# {key,type,value} rows, where type is "int"/"string"/"bool". Parse those rows into a
# synthetic sharedVpnCustomData collection matching the dataType-21 table declared in
# the androiddeviceownervpn / androidforworkvpn ObjectInfo files. The type column is
# translated here (int->Integer, ...) because the table walker does not translate
# option columns - same reason the iOS SSO configurationType is pre-translated above.
# (customData is also consumed raw by the netMotionMobility key/value table, but that
# renders under a different connection-type option, so there is no conflict.)
if (-not $Obj.customData) { return }
$typeStrings = @{
'int' = Get-LanguageString 'SettingDetails.integer'
'string' = Get-LanguageString 'SettingDetails.string'
'bool' = Get-LanguageString 'SettingDetails.boolean'
}
$rows = @()
foreach ($entry in @($Obj.customData)) {
if ([string]::IsNullOrEmpty($entry.value)) { continue }
$parsed = $null
try { $parsed = $entry.value | ConvertFrom-Json -ErrorAction Stop } catch { continue }
foreach ($item in @($parsed)) {
if ($null -eq $item -or -not $item.PSObject.Properties['key']) { continue }
$typeValue = if ($item.type -and $typeStrings.ContainsKey([string]$item.type)) { $typeStrings[[string]$item.type] } else { $item.type }
$rows += [PSCustomObject]@{
key = $item.key
type = $typeValue
value = $item.value
}
}
}
if ($rows.Count -gt 0) {
Set-DocumentationSyntheticProperty $Obj 'sharedVpnCustomData' $rows
}
}
function Initialize-AndroidVpnTunnelSiteName {
param($Obj, $Context)
# "Change the site" (microsoftTunnelSiteId) is stored as a bare GUID. The portal shows
# the tunnel site's friendly name instead, so resolve the id against the tenant's
# microsoftTunnelSites and overwrite the property with the display name. The site list
# is fetched once and cached on the context for the run (same lazy pattern as the
# Feature Update catalog / iOS update versions). Offline (no Graph) the raw id is kept.
if (-not $Obj.microsoftTunnelSiteId -or -not $Context) { return }
if (-not (Test-DocumentationGraphAvailable)) { return }
if (-not $Context.PSObject.Properties['_MicrosoftTunnelSites']) {
$Context | Add-Member -MemberType NoteProperty -Name '_MicrosoftTunnelSites' -Value $null -Force
}
if ($null -eq $Context._MicrosoftTunnelSites) {
try { $Context._MicrosoftTunnelSites = @((Invoke-MSGraphAPI -Url '/deviceManagement/microsoftTunnelSites').value) }
catch { $Context._MicrosoftTunnelSites = @(); Write-LogError 'Failed to load microsoftTunnelSites' $_.Exception }
}
$site = $Context._MicrosoftTunnelSites | Where-Object id -EQ $Obj.microsoftTunnelSiteId | Select-Object -First 1
if ($site.displayName) {
Set-DocumentationSyntheticProperty $Obj 'microsoftTunnelSiteId' $site.displayName
}
}
function Initialize-ManagedAppProtectionSyntheticProperties {
param($Obj)
Set-DocumentationSyntheticProperty $Obj 'overrideFingerprint' ($null -ne $Obj.pinRequiredInsteadOfBiometricTimeout -and $Obj.pinRequiredInsteadOfBiometricTimeout -ne 'PT0S')
Set-DocumentationSyntheticProperty $Obj 'pinReset' ($null -ne $Obj.periodBeforePinReset -and $Obj.periodBeforePinReset -notin @('PT0S', 'P0D'))
# Portal infers "unmanaged browser" from a configured custom browser: package id on
# Android, protocol on iOS (managedBrowser itself stays notConfigured in that case).
Set-DocumentationSyntheticProperty $Obj 'managedBrowserSelection' $(if ($Obj.customBrowserPackageId -or $Obj.customBrowserProtocol) { 'unmanagedBrowser' } else { $Obj.managedBrowser })
Set-DocumentationSyntheticProperty $Obj 'encryptOrgData' ($Obj.appDataEncryptionType -ne 'useDeviceSettings')
# Portal Purview toggle: On when purviewContentEvaluationRequired is set and not notRequired
# (null and notRequired both render as Off in the portal).
Set-DocumentationSyntheticProperty $Obj 'purviewEvaluationEnabled' ($null -ne $Obj.purviewContentEvaluationRequired -and $Obj.purviewContentEvaluationRequired -ne 'notRequired')
# Portal "Send org data to other apps": managedApps refines into the two Open-In variants
# via the iOS-only booleans (absent on Android, so the raw value passes through there).
$sendData = $Obj.allowedOutboundDataTransferDestinations
if ($sendData -eq 'managedApps') {
if ($Obj.disableProtectionOfManagedOutboundOpenInData -eq $false -and $Obj.filterOpenInToOnlyManagedApps -eq $true) { $sendData = 'managedAppsWithOpenInSharing' }
elseif ($Obj.disableProtectionOfManagedOutboundOpenInData -eq $true -and $Obj.filterOpenInToOnlyManagedApps -eq $false) { $sendData = 'managedAppsWithOSSharing' }
}
Set-DocumentationSyntheticProperty $Obj 'sendDataSelector' $sendData
# Portal "Receive data from other apps": the 4th option "All apps with incoming org data"
# is encoded as allowedInboundDataTransferSources=allApps + protectInboundDataFromUnknownSources=true.
$receiveData = $Obj.allowedInboundDataTransferSources
if ($receiveData -eq 'allApps' -and $Obj.protectInboundDataFromUnknownSources -eq $true) { $receiveData = 'allAppsWithIncomingOrgData' }
Set-DocumentationSyntheticProperty $Obj 'receiveDataSelector' $receiveData
# Portal "Biometrics instead of PIN for access" (Android + Default): fingerprintAndBiometricEnabled
# is authoritative when non-null; otherwise derived from the two legacy blocked flags
# (enabled unless BOTH block). The portal only ever writes fingerprintAndBiometricEnabled.
$biometric = $Obj.fingerprintAndBiometricEnabled
if ($null -eq $biometric) { $biometric = (-not $Obj.biometricAuthenticationBlocked) -or (-not $Obj.fingerprintBlocked) }
Set-DocumentationSyntheticProperty $Obj 'fingerprintAndBiometricEnabledResolved' $biometric
}
function Initialize-AutopilotSyntheticProperties {
param($Obj, [string]$JoinType)
Set-DocumentationSyntheticProperty $Obj 'applyDeviceNameTemplate' (-not [string]::IsNullOrEmpty($Obj.deviceNameTemplate))
if (-not $Obj.outOfBoxExperienceSettings) { return }
Set-DocumentationSyntheticProperty $Obj.outOfBoxExperienceSettings 'azureADJoinType' $JoinType
Set-DocumentationSyntheticProperty $Obj.outOfBoxExperienceSettings 'isLanguageSet' (-not [string]::IsNullOrEmpty($Obj.language))
if ([string]::IsNullOrEmpty($Obj.language)) { $Obj.language = 'user-select' }
}
function Initialize-Windows10GeneralSyntheticProperties {
param($Obj, [DocumentationContext]$Context)
Set-DocumentationSyntheticProperty $Obj 'syntheticDefenderDetectedMalwareActionsEnabled' ($null -ne $Obj.defenderDetectedMalwareActions)
Set-DocumentationSyntheticProperty $Obj 'networkProxyUseScriptUrlName' (-not [string]::IsNullOrEmpty($Obj.networkProxyAutomaticConfigurationUrl))
Set-DocumentationSyntheticProperty $Obj 'networkProxyUseManualServerName' ($null -ne $Obj.networkProxyServer.address)
if ($Obj.networkProxyServer.address) {
$parts = $Obj.networkProxyServer.address -split ':', 2
Set-DocumentationSyntheticProperty $Obj 'networkProxyServerName' $parts[0]
Set-DocumentationSyntheticProperty $Obj 'networkProxyServerPort' $(if ($parts.Count -gt 1) { $parts[1] } else { '' })
}
Set-DocumentationSyntheticProperty $Obj 'networkProxyExceptionsTextString' (@($Obj.networkProxyServer.exceptions) -join $Context.PropertySeparator)
Set-DocumentationSyntheticProperty $Obj 'useForLocalAddresses' ($Obj.networkProxyServer.useForLocalAddresses -eq $true)
# NOTE: reproduced exactly from the old code (edgeDisplayHomeButton is sourced
# from networkProxyServer.useForLocalAddresses there - preserved for output parity).
Set-DocumentationSyntheticProperty $Obj 'edgeDisplayHomeButton' ($Obj.networkProxyServer.useForLocalAddresses -eq $true)
$searchEngineValue = 0
switch ($Obj.edgeSearchEngine.edgeSearchEngineOpenSearchXmlUrl) {
'default' { $searchEngineValue = 1 }
'bing' { $searchEngineValue = 2 }
'https://go.microsoft.com/fwlink/?linkid=842596' { $searchEngineValue = 3 }
'https://go.microsoft.com/fwlink/?linkid=842600' { $searchEngineValue = 4 }
default { if ($Obj.edgeSearchEngine.edgeSearchEngineOpenSearchXmlUrl) { $searchEngineValue = 5 } }
}
Set-DocumentationSyntheticProperty $Obj 'edgeSearchEngineDropDown' $searchEngineValue
$curApp = $null
$perAppPrivacy = @()
foreach ($appItem in @($Obj.privacyAccessControls | Where-Object { $null -ne $_.appDisplayName })) {
if ($curApp -ne $appItem.appDisplayName) {
$perAppPrivacy += [PSCustomObject]@{ appPackageName = $appItem.appPackageFamilyName; appName = $appItem.appDisplayName }
$curApp = $appItem.appDisplayName
}
}
Set-DocumentationSyntheticProperty $Obj 'perAppPrivacy' $perAppPrivacy
}
function Initialize-EndpointProtectionSyntheticProperties {
param($Obj)
$printProps = @($Obj.PSObject.Properties | Where-Object Name -Like 'applicationGuardAllowPrint*')
Set-DocumentationSyntheticProperty $Obj 'applicationGuardAllowPrinting' (@($printProps | Where-Object Value -EQ $true).Count -gt 0)
Set-DocumentationSyntheticProperty $Obj 'applicationGuardPrintSettings' @($printProps | Where-Object Value -EQ $true | ForEach-Object Name)
$fwProps = @($Obj.PSObject.Properties | Where-Object Name -Like 'firewallIPSecExemptionsAllow*')
Set-DocumentationSyntheticProperty $Obj 'firewallSyntheticPresharedKeyEncodingMethod' (@($fwProps | Where-Object Value -EQ $true).Count -gt 0)
Set-DocumentationSyntheticProperty $Obj 'firewallSyntheticIPsecExemptions' @($fwProps | Where-Object Value -EQ $true | ForEach-Object Name)
Set-DocumentationSyntheticProperty $Obj 'firewallSyntheticProfileDomainfirewallEnabled' ($null -ne $Obj.firewallProfileDomain)
Set-DocumentationSyntheticProperty $Obj 'firewallSyntheticProfilePrivatefirewallEnabled' ($null -ne $Obj.firewallProfilePrivate)
Set-DocumentationSyntheticProperty $Obj 'firewallSyntheticProfilePublicfirewallEnabled' ($null -ne $Obj.firewallProfilePublic)
Add-DocumentationDefenderFirewallSettings $Obj $Obj.firewallProfileDomain 'Domain'
Add-DocumentationDefenderFirewallSettings $Obj $Obj.firewallProfilePrivate 'Private'
Add-DocumentationDefenderFirewallSettings $Obj $Obj.firewallProfilePublic 'Public'
Set-DocumentationSyntheticProperty $Obj 'bitLockerBaseConfigureEncryptionMethods' $(if ($null -ne $Obj.bitLockerSystemDrivePolicy.encryptionMethod) { $true } else { $null })
Set-DocumentationSyntheticProperty $Obj 'bitLockerSystemDriveEncryptionMethod' $Obj.bitLockerSystemDrivePolicy.encryptionMethod
Set-DocumentationSyntheticProperty $Obj 'bitLockerFixedDriveEncryptionMethod' $Obj.bitLockerFixedDrivePolicy.encryptionMethod
Set-DocumentationSyntheticProperty $Obj 'bitLockerRemovableDriveEncryptionMethod' $Obj.bitLockerRemovableDrivePolicy.encryptionMethod
$sysPol = $Obj.bitLockerSystemDrivePolicy
Set-DocumentationSyntheticProperty $sysPol 'bitLockerMinimumPinLength' $(if ($null -ne $sysPol.minimumPinLength) { $true } else { $null })
Set-DocumentationSyntheticProperty $sysPol 'bitLockerSyntheticSystemDrivePolicybitLockerDriveRecovery' $(if ($null -ne $sysPol.recoveryOptions) { $true } else { $null })
$prebootOption = $null
if ($null -eq $sysPol.prebootRecoveryUrl -and $null -eq $sysPol.prebootRecoveryEnableMessageAndUrl) { $prebootOption = 'default' }
elseif ($sysPol.prebootRecoveryUrl -eq '' -and $sysPol.prebootRecoveryEnableMessageAndUrl -eq '') { $prebootOption = 'empty' }
elseif ($sysPol.prebootRecoveryUrl) { $prebootOption = 'customURL' }
elseif ($sysPol.prebootRecoveryEnableMessageAndUrl) { $prebootOption = 'customMessage' }
Set-DocumentationSyntheticProperty $sysPol 'bitLockerPrebootRecoveryMsgURLOption' $prebootOption
if ($sysPol.recoveryOptions) {
foreach ($tmpProp in @($sysPol.recoveryOptions.PSObject.Properties.Name)) {
Set-DocumentationSyntheticProperty $sysPol "bitLockerSyntheticSystemDrivePolicy$tmpProp" $sysPol.recoveryOptions.$tmpProp
}
}
$fixedPol = $Obj.bitLockerFixedDrivePolicy
Set-DocumentationSyntheticProperty $fixedPol 'bitLockerSyntheticFixedDrivePolicybitLockerDriveRecovery' $(if ($null -ne $fixedPol.recoveryOptions) { $true } else { $null })
if ($fixedPol.recoveryOptions) {
foreach ($tmpProp in @($fixedPol.recoveryOptions.PSObject.Properties.Name)) {
Set-DocumentationSyntheticProperty $fixedPol "bitLockerSyntheticFixedDrivePolicy$tmpProp" $fixedPol.recoveryOptions.$tmpProp
}
}
Set-DocumentationSyntheticProperty $fixedPol 'bitLockerSyntheticFixedDrivePolicyrequireEncryptionForWriteAccess' $fixedPol.requireEncryptionForWriteAccess
Set-DocumentationSyntheticProperty $Obj.bitLockerRemovableDrivePolicy 'bitLockerSyntheticRemovableDrivePolicyrequireEncryptionForWriteAccess' $Obj.bitLockerRemovableDrivePolicy.requireEncryptionForWriteAccess
$appLockerType = 'notConfigured'
if ($Obj.appLockerApplicationControl -eq 'enforceComponentsStoreAppsAndSmartlocker') { $appLockerType = 'allow' }
if ($Obj.appLockerApplicationControl -eq 'auditComponentsAndStoreApps') { $appLockerType = 'audit' }
Set-DocumentationSyntheticProperty $Obj 'appLockerApplicationControlType' $appLockerType
}
# Port of the old Add-DefenderFirewallSettings - projects a firewall profile's
# *Blocked/*NotMerged property pairs into firewallSyntheticProfile<Type>* values
# (blocked/allowed/notConfigured) on the top object.
function Add-DocumentationDefenderFirewallSettings {
param($Obj, $FwSettings, [string]$FwType)
if (-not $FwSettings) { return }
foreach ($fwProp in @($FwSettings.PSObject.Properties | Where-Object { $_.Name -like '*Blocked' -or $_.Name -like '*NotMerged' }).Name) {
if ($fwProp -like '*Blocked') {
$blockedValue = $FwSettings.$fwProp
$propPre = $fwProp.Substring(0, $fwProp.Length - 7)
if ($FwSettings.PSObject.Properties | Where-Object Name -EQ "$($propPre)Required") { $nonBlockedValue = $FwSettings."$($propPre)Required" }
elseif ($FwSettings.PSObject.Properties | Where-Object Name -EQ "$($propPre)Allowed") { $nonBlockedValue = $FwSettings."$($propPre)Allowed" }
else { continue }
$fwPropName = "firewallSyntheticProfile$FwType$propPre"
}
else {
$blockedValue = $FwSettings.$fwProp
$propPre = $fwProp.Substring(0, $fwProp.Length - 9)
if ($FwSettings.PSObject.Properties | Where-Object Name -EQ "$($propPre)Merged") { $nonBlockedValue = $FwSettings."$($propPre)Merged" }
else { continue }
$fwPropName = "firewallSyntheticProfile$FwType${propPre}Merge"
}
$fwValue = 'notConfigured'
if ($blockedValue -eq $true -and $nonBlockedValue -eq $false) { $fwValue = 'blocked' }
elseif ($blockedValue -eq $false -and $nonBlockedValue -eq $true) { $fwValue = 'allowed' }
Set-DocumentationSyntheticProperty $Obj $fwPropName $fwValue
}
}
function Initialize-FeatureUpdateSyntheticProperties {
param($Obj, [DocumentationContext]$Context)
# Resolve the friendly feature-update name from the (generic, tenant-independent)
# update catalog, cached on the context for the run. Falls back to the version +
# "not supported" label when offline or the version isn't in the catalog.
$verInfoTxt = $null
if (Test-DocumentationGraphAvailable) {
if (-not $Context.PSObject.Properties['_Win10FeatureUpdates']) {
$Context | Add-Member -MemberType NoteProperty -Name '_Win10FeatureUpdates' -Value $null -Force
}
if ($null -eq $Context._Win10FeatureUpdates) {
try { $Context._Win10FeatureUpdates = @((Invoke-MSGraphAPI -Url '/deviceManagement/windowsUpdateCatalogItems/microsoft.graph.windowsFeatureUpdateCatalogItem').value) }
catch { $Context._Win10FeatureUpdates = @(); Write-LogError 'Failed to load windowsFeatureUpdateCatalogItems' $_.Exception }
}
$verInfo = $Context._Win10FeatureUpdates | Where-Object version -EQ $Obj.featureUpdateVersion | Select-Object -First 1
if ($verInfo) { $verInfoTxt = $verInfo.displayName }
}
if (-not $verInfoTxt) {
$verInfoTxt = '{0} ({1})' -f $Obj.featureUpdateVersion, (Get-LanguageString 'WindowsFeatureUpdate.EndOFSupportStatus.notSupported')
}
Set-DocumentationSyntheticProperty $Obj 'featureUpdateDisplayName' $verInfoTxt
if ($Obj.rolloutSettings.offerStartDateTimeInUTC -and $Obj.rolloutSettings.offerEndDateTimeInUTC) {
Set-DocumentationSyntheticProperty $Obj 'featureUpdateRolloutOption' 'gradualRollout'
Set-DocumentationSyntheticProperty $Obj 'featureUpdateRolloutStartDate' ([datetime]$Obj.rolloutSettings.offerStartDateTimeInUTC).ToLongDateString()
Set-DocumentationSyntheticProperty $Obj 'featureUpdateRolloutEndDate' ([datetime]$Obj.rolloutSettings.offerEndDateTimeInUTC).ToLongDateString()
if ($null -ne $Obj.rolloutSettings.offerIntervalInDays) { Set-DocumentationSyntheticProperty $Obj 'featureUpdateRolloutInterval' $Obj.rolloutSettings.offerIntervalInDays }
}
elseif ($Obj.rolloutSettings.offerStartDateTimeInUTC) {
Set-DocumentationSyntheticProperty $Obj 'featureUpdateRolloutOption' 'startDateOnly'
Set-DocumentationSyntheticProperty $Obj 'featureUpdateRolloutStartDate' ([datetime]$Obj.rolloutSettings.offerStartDateTimeInUTC).ToLongDateString()
}
else {
Set-DocumentationSyntheticProperty $Obj 'featureUpdateRolloutOption' 'immediateStart'
}
}
function Initialize-EnrollmentStatusPageSyntheticProperties {
param($Obj, [DocumentationContext]$Context)
$timeout = if ([int]$Obj.installProgressTimeoutInMinutes -eq 0) { 60 } else { $Obj.installProgressTimeoutInMinutes }
Set-DocumentationSyntheticProperty $Obj 'InstallProgressTimeout' $timeout
Set-DocumentationSyntheticProperty $Obj 'showCustomErrorMessage' (-not [string]::IsNullOrEmpty($Obj.customErrorMessage))
if (@($Obj.selectedMobileAppIds).Count -eq 0) {
Set-DocumentationSyntheticProperty $Obj 'waitForApps' (Get-LanguageString 'EnrollmentStatusScreen.Apps.useSelectedAppsAll')
return
}
$apps = Get-CDAllTenantApps
$names = foreach ($appId in @($Obj.selectedMobileAppIds)) {
$app = $apps | Where-Object Id -EQ $appId | Select-Object -First 1
if ($app) { $app.displayName } else { Write-Log "No app found with id $appId" 3 }
}
Set-DocumentationSyntheticProperty $Obj 'waitForApps' (@($names) -join $Context.ObjectSeparator)
}
# Map a managedAppRemediationAction enum value (block/wipe/warn/blockWhenSettingIsSupported)
# to its SettingDetails action language key. Returns $null for an unknown value.
function Get-ConditionalLaunchActionId {
param([string]$Action)
switch ($Action) {
'block' { 'blockAccess' }
'wipe' { 'wipeData' }
'warn' { 'warn' }
'blockWhenSettingIsSupported' { 'knoxConditionalLaunchSettingBlockWhenSettingIsSupported' } # "Block access on supported devices"
default { $null }
}
}
# Device manufacturer / model conditions use the "Allow specified (...)" action labels.
function Get-ConditionalLaunchAllowSpecifiedActionId {
param([string]$Action)
switch ($Action) {
'wipe' { 'allowSpecifiedWipe' }
default { 'allowSpecifiedBlock' }
}
}
function Get-DocumentationConditionalLaunchSetting {
param($Obj, [string]$SettingId, [string]$Property, [string]$ActionId, [switch]$SkipValue)
if (-not $ActionId) { return $null }
$value = $Obj."$Property"
# Treat every "unconfigured" sentinel as no value: null, empty, notConfigured,
# none (SafetyNet types), and the empty patch-version placeholder.
if ($null -eq $value -or "$value" -in @('', 'notConfigured', 'none', '0000-00-00')) { return $null }
if ($value -is [string] -and $value -match '^P(T|\d)') {
try {
$duration = [Xml.XmlConvert]::ToTimeSpan($value)
$value = if ($Property -eq 'periodOfflineBeforeAccessCheck') { $duration.TotalMinutes }
elseif ($Property -eq 'gracePeriodToBlockAppsDuringOffClockHours') { $duration.TotalSeconds }
else { $duration.TotalDays }
}
catch { }
}
elseif ($SkipValue) { $value = $null }
return [PSCustomObject]@{
Setting = Get-LanguageString "SettingDetails.$SettingId"
Value = $value
Action = Get-LanguageString "SettingDetails.$ActionId"
}
}
function Initialize-ConditionalLaunchSyntheticProperties {
param($Obj)
if (-not $Obj.PSObject.Properties['periodOfflineBeforeWipeIsEnforced']) { return }
$rows = @()
# ---- Shared device / app conditions (iOS + Android + WIP) ----
$rows += Get-DocumentationConditionalLaunchSetting $Obj 'maxPinAttempts' 'maximumPinRetries' $(if ($Obj.appActionIfMaximumPinRetriesExceeded -eq 'block') { 'resetPin' } else { 'wipeData' })
$rows += Get-DocumentationConditionalLaunchSetting $Obj 'offlineGracePeriod' 'periodOfflineBeforeAccessCheck' 'blockMinutes'
$rows += Get-DocumentationConditionalLaunchSetting $Obj 'offlineGracePeriod' 'periodOfflineBeforeWipeIsEnforced' 'wipeDays'
$rows += Get-DocumentationConditionalLaunchSetting $Obj 'minAppVersion' 'minimumWipeAppVersion' 'wipeData'
$rows += Get-DocumentationConditionalLaunchSetting $Obj 'minAppVersion' 'minimumRequiredAppVersion' 'blockAccess'
$rows += Get-DocumentationConditionalLaunchSetting $Obj 'minAppVersion' 'minimumWarningAppVersion' 'warn'
$rows += Get-DocumentationConditionalLaunchSetting $Obj 'minOSVersion' 'minimumWipeOsVersion' 'wipeData'
$rows += Get-DocumentationConditionalLaunchSetting $Obj 'minOSVersion' 'minimumRequiredOsVersion' 'blockAccess'
$rows += Get-DocumentationConditionalLaunchSetting $Obj 'minOSVersion' 'minimumWarningOsVersion' 'warn'
$rows += Get-DocumentationConditionalLaunchSetting $Obj 'maxOSVersion' 'maximumWipeOsVersion' 'wipeData'
$rows += Get-DocumentationConditionalLaunchSetting $Obj 'maxOSVersion' 'maximumRequiredOsVersion' 'blockAccess'
$rows += Get-DocumentationConditionalLaunchSetting $Obj 'maxOSVersion' 'maximumWarningOsVersion' 'warn'
$rows += Get-DocumentationConditionalLaunchSetting $Obj 'onlineButUnableToCheckin' 'appActionIfUnableToAuthenticateUser' $(if ($Obj.appActionIfUnableToAuthenticateUser -eq 'block') { 'blockAccess' } else { 'wipeData' }) -SkipValue
$rows += Get-DocumentationConditionalLaunchSetting $Obj 'jailbrokenRootedDevices' 'appActionIfDeviceComplianceRequired' $(if ($Obj.appActionIfDeviceComplianceRequired -eq 'block') { 'blockAccess' } else { 'wipeData' }) -SkipValue
$rows += Get-DocumentationConditionalLaunchSetting $Obj 'maximumAllowedDeviceThreatLevel' 'maximumAllowedDeviceThreatLevel' $(if ($Obj.mobileThreatDefenseRemediationAction -eq 'wipe') { 'wipeData' } else { 'blockAccess' })
# Primary MTD service - value-only row (no action column in the portal grid)
$mtdPriority = [string]$Obj.mobileThreatDefensePartnerPriority
if ($mtdPriority -in @('defenderOverThirdPartyPartner', 'thirdPartyPartnerOverDefender')) {
$rows += [PSCustomObject]@{
Setting = Get-LanguageString 'SettingDetails.primaryMtdService' 'Primary MTD service' -IgnoreMissing
Value = if ($mtdPriority -eq 'defenderOverThirdPartyPartner') { Get-LanguageString 'SettingDetails.microsoftDefenderForEndpoint' 'Microsoft Defender for Endpoint' -IgnoreMissing }
else { Get-LanguageString 'SettingDetails.mobileThreatDefenseNonMicrosoft' }
Action = $null
}
}
$rows += Get-DocumentationConditionalLaunchSetting $Obj 'clockedOutAccount' 'appActionIfAccountIsClockedOut' (Get-ConditionalLaunchActionId ([string]$Obj.appActionIfAccountIsClockedOut)) -SkipValue
# "User Clock Status grace period" - duration rendered in seconds, fixed Block action
$rows += Get-DocumentationConditionalLaunchSetting $Obj 'gracePeriodToBlockAppsDuringOffClockHours' 'gracePeriodToBlockAppsDuringOffClockHours' 'blockAccess'
# ---- iOS specific ----
$rows += Get-DocumentationConditionalLaunchSetting $Obj 'minSdkVersion' 'minimumWipeSdkVersion' 'wipeData'
$rows += Get-DocumentationConditionalLaunchSetting $Obj 'minSdkVersion' 'minimumRequiredSdkVersion' 'blockAccess'
$rows += Get-DocumentationConditionalLaunchSetting $Obj 'minSdkVersion' 'minimumWarningSdkVersion' 'warn'
$rows += Get-DocumentationConditionalLaunchSetting $Obj 'deviceModels' 'allowedIosDeviceModels' (Get-ConditionalLaunchAllowSpecifiedActionId ([string]$Obj.appActionIfIosDeviceModelNotAllowed))
# ---- Android specific ----
# Security patch level (Android)
$rows += Get-DocumentationConditionalLaunchSetting $Obj 'minPatchVersion' 'minimumWipePatchVersion' 'wipeData'
$rows += Get-DocumentationConditionalLaunchSetting $Obj 'minPatchVersion' 'minimumRequiredPatchVersion' 'blockAccess'
$rows += Get-DocumentationConditionalLaunchSetting $Obj 'minPatchVersion' 'minimumWarningPatchVersion' 'warn'
# Company Portal version (Android)
$rows += Get-DocumentationConditionalLaunchSetting $Obj 'minimumCompanyPortalVersion' 'minimumWipeCompanyPortalVersion' 'wipeData'
$rows += Get-DocumentationConditionalLaunchSetting $Obj 'minimumCompanyPortalVersion' 'minimumRequiredCompanyPortalVersion' 'blockAccess'
$rows += Get-DocumentationConditionalLaunchSetting $Obj 'minimumCompanyPortalVersion' 'minimumWarningCompanyPortalVersion' 'warn'
# Company Portal update deferral -> "Max Company Portal version age (days)" (Android).
# One portal setting, up to three rows; each action has its own day-count property. 0 = unset.
foreach ($cp in @(
@{ Prop = 'warnAfterCompanyPortalUpdateDeferralInDays'; Action = 'warn' },
@{ Prop = 'blockAfterCompanyPortalUpdateDeferralInDays'; Action = 'blockAccess' },
@{ Prop = 'wipeAfterCompanyPortalUpdateDeferralInDays'; Action = 'wipeData' })) {
if ($Obj."$($cp.Prop)" -gt 0) {
$rows += Get-DocumentationConditionalLaunchSetting $Obj 'maximumCompanyPortalVersionAge' $cp.Prop $cp.Action
}
}
# Device manufacturers / models (Android)
$rows += Get-DocumentationConditionalLaunchSetting $Obj 'deviceManufacturers' 'allowedAndroidDeviceManufacturers' (Get-ConditionalLaunchAllowSpecifiedActionId ([string]$Obj.appActionIfAndroidDeviceManufacturerNotAllowed))
$rows += Get-DocumentationConditionalLaunchSetting $Obj 'deviceModels' 'allowedAndroidDeviceModels' (Get-ConditionalLaunchAllowSpecifiedActionId ([string]$Obj.appActionIfAndroidDeviceModelNotAllowed))
# Samsung Knox device attestation (Android)
$rows += Get-DocumentationConditionalLaunchSetting $Obj 'samsungKnoxAttestationRequired' 'appActionIfSamsungKnoxAttestationRequired' (Get-ConditionalLaunchActionId ([string]$Obj.appActionIfSamsungKnoxAttestationRequired)) -SkipValue
# SafetyNet / Play Integrity - guard on the required*Type, not the always-set action.
# Portal rows: "Require threat scan on apps" (apps verification, no value),
# "Play integrity verdict" (device attestation, value = integrity level) and
# "Play Integrity verdict evaluation type" (hardware-backed, value-only row).
if (("$($Obj.requiredAndroidSafetyNetAppsVerificationType)") -notin @('', 'none')) {
$actId = Get-ConditionalLaunchActionId ([string]$Obj.appActionIfAndroidSafetyNetAppsVerificationFailed)
if ($actId) {
$rows += [PSCustomObject]@{
Setting = Get-LanguageString 'SettingDetails.requireThreatScanOnApps'
Value = $null
Action = Get-LanguageString "SettingDetails.$actId"
}
}
}
$attType = [string]$Obj.requiredAndroidSafetyNetDeviceAttestationType
if ($attType -notin @('', 'none')) {
$attKey = switch ($attType) {
'basicIntegrity' { 'androidPlayIntegrityVerdictBasicIntegrity' }
'basicIntegrityAndDeviceCertification' { 'androidPlayIntegrityVerdictBasicAndDeviceIntegrity' }
default { $null }
}
$actId = Get-ConditionalLaunchActionId ([string]$Obj.appActionIfAndroidSafetyNetDeviceAttestationFailed)
if ($actId) {
$rows += [PSCustomObject]@{
Setting = Get-LanguageString 'SettingDetails.playIntegrityVerdict'
Value = if ($attKey) { Get-LanguageString "SettingDetails.$attKey" } else { $attType }
Action = Get-LanguageString "SettingDetails.$actId"
}
}
# Evaluation type only surfaces in the portal while an attestation row exists
if ([string]$Obj.requiredAndroidSafetyNetEvaluationType -eq 'hardwareBacked') {
$rows += [PSCustomObject]@{
Setting = Get-LanguageString 'SettingDetails.playIntegrityVerdictEvaluationType'
Value = Get-LanguageString 'SettingDetails.requiredAndroidPlayIntegrityVerdictEvaluationTypeHardwareBacked'
Action = $null
}
}
}
# Require device lock / minimum passcode complexity (Android). Only the
# configured complexity threshold carries an action; appActionIfDeviceLockNotSet
# defaults to 'block' even when unconfigured, so it is not emitted on its own.
foreach ($lvl in @(
@{ Prop = 'appActionIfDevicePasscodeComplexityLessThanLow'; Level = 'low' },
@{ Prop = 'appActionIfDevicePasscodeComplexityLessThanMedium'; Level = 'medium' },
@{ Prop = 'appActionIfDevicePasscodeComplexityLessThanHigh'; Level = 'high' })) {
$act = [string]$Obj."$($lvl.Prop)"
if ([string]::IsNullOrEmpty($act)) { continue }
$actId = Get-ConditionalLaunchActionId $act
if (-not $actId) { continue }
$rows += [PSCustomObject]@{
Setting = Get-LanguageString 'SettingDetails.requireDeviceLockComplexityOnApps'
Value = Get-LanguageString "SettingDetails.$($lvl.Level)"
Action = Get-LanguageString "SettingDetails.$actId"
}
}
$rows = @($rows | Where-Object { $null -ne $_ })
if ($rows.Count) { Set-DocumentationSyntheticProperty $Obj 'ConditionalLaunchSettings' $rows }
}
function Initialize-IosGeneralSyntheticProperties {
param($Obj, [DocumentationContext]$Context)
if ([string]::IsNullOrEmpty($Obj.KioskModeAppTypeDropDown)) {
$kioskMode = $null
if ($Obj.kioskModeAppStoreUrl) { $kioskMode = 0 }
elseif ($Obj.kioskModeManagedAppId) { $kioskMode = 1 }
elseif ($Obj.kioskModeBuiltInAppId) { $kioskMode = 2 }
if ($null -ne $kioskMode) { Set-DocumentationSyntheticProperty $Obj 'KioskModeAppTypeDropDown' $kioskMode }
}
$mediaRegion = 'notConfigured'
foreach ($mediaRatingProp in @($Obj.PSObject.Properties | Where-Object { $_.Name -like 'mediaContentRating*' -and $_.Name -notlike '*@odata.type' -and $_.Name -ne 'mediaContentRatingApps' }).Name) {
if ($null -ne $Obj.$mediaRatingProp) { $mediaRegion = $mediaRatingProp; break }
}
Set-DocumentationSyntheticProperty $Obj 'MediaContentRatingRegionSelectorDropDown' $mediaRegion
$cellularBlock = 'none'
$roamingBlock = 'none'
$tmpRule = $Obj.networkUsageRules | Where-Object cellularDataBlocked -EQ $true
if ($tmpRule) {
$cellularBlock = if ($tmpRule.managedApps) { 'choose' } else { 'all' }
Set-DocumentationSyntheticProperty $Obj 'networkUsageRulesCellularDataList' ($tmpRule.managedApps -join $Context.ObjectSeparator)
}
$tmpRule = $Obj.networkUsageRules | Where-Object cellularDataBlockWhenRoaming -EQ $true
if ($tmpRule) {
$roamingBlock = if ($tmpRule.managedApps) { 'choose' } else { 'all' }
Set-DocumentationSyntheticProperty $Obj 'networkUsageRulesCellularRoamingDataList' $tmpRule.managedApps
}
Set-DocumentationSyntheticProperty $Obj 'networkUsageRulesCellularDataBlockType' $cellularBlock
Set-DocumentationSyntheticProperty $Obj 'networkUsageRulesCellularRoamingDataBlockType' $roamingBlock
}
function Get-DocumentationIosUpdateTimeLabel {
param([string]$Time, [string[]]$HourWords)
if ([string]::IsNullOrEmpty($Time)) { return '' }
$hour = [int]($Time.Split(':')[0])
$when = 'AM'
if ($hour -gt 12) { $when = 'PM'; $hour = $hour - 12 }
$hourStr = if ($hour -ge 0 -and $hour -le 11) { $HourWords[$hour] } else { '' }
return (Get-LanguageString "SettingDetails.$($hourStr)$($when)Option")
}
function Initialize-IosUpdateSyntheticProperties {
param($Obj, [DocumentationContext]$Context)
# iOS available-update versions are generic schema (same on every tenant), cached
# on the context for the run and gated on connectivity only.
$verInfo = $null
if (Test-DocumentationGraphAvailable) {
if (-not $Context.PSObject.Properties['_IosAvailableUpdateVersions']) {
$Context | Add-Member -MemberType NoteProperty -Name '_IosAvailableUpdateVersions' -Value $null -Force
}
if ($null -eq $Context._IosAvailableUpdateVersions) {
try {
$vers = @((Invoke-MSGraphAPI -Url '/deviceManagement/deviceConfigurations/getIosAvailableUpdateVersions').value)
$Context._IosAvailableUpdateVersions = @($vers | Sort-Object -Property productVersion -Descending)
} catch { $Context._IosAvailableUpdateVersions = @(); Write-LogError 'Failed to load getIosAvailableUpdateVersions' $_.Exception }
}
$verInfo = @($Context._IosAvailableUpdateVersions | Where-Object productVersion -EQ $Obj.desiredOsVersion)
}
$versionText = '{0} {1}' -f (Get-LanguageString 'SoftwareUpdates.IosUpdatePolicy.Settings.IOSVersion.prefix'), $Obj.desiredOsVersion
if (-not $verInfo) {
$versionText = "$versionText ($(Get-LanguageString 'SoftwareUpdates.IosUpdatePolicy.Settings.IOSVersion.noLongerSupported'))"
}
elseif ($verInfo[0].productVersion -eq $Obj.desiredOsVersion) {
$versionText = "$versionText ($(Get-LanguageString 'SoftwareUpdates.IosUpdatePolicy.Settings.IOSVersion.latestUpdate'))"
}
Set-DocumentationSyntheticProperty $Obj 'versionInfo' $versionText
$hourWords = @('twelve','one','two','three','four','five','six','seven','eight','nine','ten','eleven')
$timeWindows = @()
foreach ($tw in @($Obj.customUpdateTimeWindows)) {
$startDay = Get-LanguageString "SettingDetails.$($tw.startDay)"
$endDay = Get-LanguageString "SettingDetails.$($tw.endDay)"
$startTime = Get-DocumentationIosUpdateTimeLabel $tw.startTime $hourWords
$endTime = Get-DocumentationIosUpdateTimeLabel $tw.endTime $hourWords
$timeWindows += ($startDay + $Context.PropertySeparator + $startTime + $Context.PropertySeparator + $endDay + $Context.PropertySeparator + $endTime)
}
# Property name intentionally 'timeWidows' - the ObjectInfo JSON reads that
# (mis)spelling; preserved for parity.
Set-DocumentationSyntheticProperty $Obj 'timeWidows' ($timeWindows -join $Context.ObjectSeparator)
}
function Initialize-WifiEnterpriseEapSyntheticProperties {
param($Obj)
if ($Obj.authenticationMethod -eq 'derivedCredential') { return }
$idCertType = $null
if ($Obj.'#CustomRef_identityCertificateForClientAuthentication' -and $Obj.'@ObjectFromFile' -eq $true) {
$idCert = $Obj.'#CustomRef_identityCertificateForClientAuthentication'
$idx = $idCert.IndexOf('|:|')
if ($idx -gt -1) { $idCertType = $idCert.Substring($idx + 3) }
}
elseif (Test-DocumentationGraphAvailable) {
try {
$idCert = Invoke-MSGraphAPI -Url $Obj.'identityCertificateForClientAuthentication@odata.navigationLink' -ODataMetadata 'minimal' -NoError
$idCertType = $idCert.'@odata.type'
} catch {}
}
$clientCertType = $null
if ($idCertType -like '*Pkcs*') { $clientCertType = 'PKCS certificate' }
elseif ($idCertType -like '*SCEP*') { $clientCertType = 'SCEP certificate' }
if ($clientCertType) { $Obj.authenticationMethod = $clientCertType }
}
function Initialize-WindowsKioskSyntheticProperties {
param($Obj)
$appCfg = $Obj.kioskProfiles[0].appConfiguration
if (-not $appCfg) { return }
$uwpAppType = $null
if ($appCfg.'@odata.type' -eq '#microsoft.graph.windowsKioskSingleWin32App') {
$uwpAppType = 'win32App'
$appCfg.'@odata.type' = '#microsoft.graph.windowsKioskSingleUWPApp'
}
elseif ($appCfg.uwpApp.appUserModelId -like 'Microsoft.MicrosoftEdge*') { $uwpAppType = 'edge' }
elseif ($appCfg.uwpApp.appUserModelId -like 'Microsoft.KioskBrowser*') { $uwpAppType = 'kioskBrowser' }
elseif ($appCfg.uwpApp.appUserModelId) { $uwpAppType = 'managed' }
Set-DocumentationSyntheticProperty $appCfg 'uwpAppType' $uwpAppType
if ($Obj.windowsKioskForceUpdateSchedule) { Set-DocumentationSyntheticProperty $Obj 'hasForceRestart' $true }
}
# Win32 LOB app documenter - port of the old DocumentationCustom.psm1 win32LobApp
# branch. Synthesizes the fields #Applications.json reads (return codes, requirement
# + detection rule tables, dependency/supersedence lists, install/uninstall types,
# win10Release) and emits install/uninstall/requirement/detection scripts.
function Initialize-Win32LobAppSyntheticProperties {
param($Obj, [DocumentationContext]$Context)
$sep = $Context.ObjectSeparator
$returnCodes = @()
foreach ($rc in @($Obj.returnCodes)) {
$returnCodes += [PSCustomObject]@{
returnCode = $rc.returnCode
type = (Get-LanguageString "Win32ReturnCodes.CodeTypes.$($rc.type)")
}
}
# Dependencies / supersedence resolved from the relationships API. This is
# source-tenant-specific (the related app ids only exist on the origin tenant),
# so gate it like the other source-tenant lookups.
$dependencyApps = @()
$supersededApps = @()
if (($Obj.dependentAppCount -gt 0 -or $Obj.supersededAppCount -gt 0) -and
-not $Context.SourceTenantUnavailable -and (Test-DocumentationGraphAvailable)) {
try {
$url = "/deviceAppManagement/mobileApps/$($Obj.Id)/relationships?`$filter=targetType%20eq%20microsoft.graph.mobileAppRelationshipType%27child%27"
$relationships = (Invoke-MSGraphAPI -Url $url).value
foreach ($rel in @($relationships)) {
if ($rel.'@odata.type' -eq '#microsoft.graph.mobileAppDependency') {
$key = if ($rel.dependencyType -eq 'autoInstall') { 'win32DependenciesAutoInstall' } else { 'win32DependenciesDetect' }
$dependencyApps += ('{0} {1}' -f $rel.targetDisplayName, (Get-LanguageString "SettingDetails.$key"))
}
elseif ($rel.'@odata.type' -eq '#microsoft.graph.mobileAppSupersedence') {
$key = if ($rel.supersedenceType -eq 'update') { 'win32SupersedenceUpdate' } else { 'win32SupersedenceReplace' }
$supersededApps += ('{0} {1}' -f $rel.targetDisplayName, (Get-LanguageString "SettingDetails.$key"))
}
}
}
catch { Write-LogError "Failed to load Win32 app relationships for $($Obj.Id)" $_.Exception }
}
# Install script (present only when the app content was decrypted on export -
# the '#ScriptInfo' synthetic). Falls back to the command-line installer type.
if ($Obj.activeInstallScript.'#ScriptInfo') {
$Obj.installCommandLine = $null
$installType = 'Win32Program.installScript'
Set-DocumentationSyntheticProperty $Obj 'activeInstallScript' ([PSCustomObject]@{
scriptType = 'install'
scriptName = $Obj.activeInstallScript.'#ScriptInfo'.displayName
scriptContent = $Obj.activeInstallScript.'#ScriptInfo'.ScriptContent
enforceSignatureCheck = $Obj.activeInstallScript.'#ScriptInfo'.enforceSignatureCheck
runAs32Bit = $Obj.activeInstallScript.'#ScriptInfo'.runAs32Bit
})
Add-DocumentationEncodedScript $Context $Obj.activeInstallScript.'#ScriptInfo'.displayName "$($Obj.displayName) - $($Obj.activeInstallScript.'#ScriptInfo'.displayName)" $Obj.activeInstallScript.'#ScriptInfo'.content
}
else {
$installType = 'Win32Program.installCommand'
Set-DocumentationSyntheticProperty $Obj 'activeInstallScript' $null
}
# Uninstall script
if ($Obj.activeUninstallScript.'#ScriptInfo') {
$Obj.uninstallCommandLine = $null
$uninstallType = 'Win32Program.uninstallScript'
Set-DocumentationSyntheticProperty $Obj 'activeUninstallScript' ([PSCustomObject]@{
scriptType = 'uninstall'
scriptName = $Obj.activeUninstallScript.'#ScriptInfo'.displayName
scriptContent = $Obj.activeUninstallScript.'#ScriptInfo'.ScriptContent
enforceSignatureCheck = $Obj.activeUninstallScript.'#ScriptInfo'.enforceSignatureCheck
runAs32Bit = $Obj.activeUninstallScript.'#ScriptInfo'.runAs32Bit
})
Add-DocumentationEncodedScript $Context $Obj.activeUninstallScript.'#ScriptInfo'.displayName "$($Obj.displayName) - $($Obj.activeUninstallScript.'#ScriptInfo'.displayName)" $Obj.activeUninstallScript.'#ScriptInfo'.content
}
else {
$uninstallType = 'Win32Program.uninstallCommand'
Set-DocumentationSyntheticProperty $Obj 'activeUninstallScript' $null
}
# Requirement rules -> summary strings + a flat property/value row set (matches
# the old behavior: rows from all rules accumulate into one table).
$requirementRulesSummary = @()
$requirementRules = @()
foreach ($rule in @($Obj.requirementRules)) {
if ($rule.'@odata.type' -eq '#microsoft.graph.win32LobAppFileSystemRequirement') {
$lngId = 'fileType'; $textValue = $rule.path
}
elseif ($rule.'@odata.type' -eq '#microsoft.graph.win32LobAppRegistryRequirement') {
$lngId = 'registry'; $textValue = $rule.keyPath
}
else {
$lngId = 'script'; $textValue = $rule.displayName
Add-DocumentationEncodedScript $Context $rule.displayName "$($Obj.displayName) - Requirement script" $rule.scriptContent
}
$requirementRulesSummary += ('{0} {1}' -f (Get-LanguageString "Win32Requirements.AdditionalRequirements.RequirementTypeOptions.$lngId"), $textValue)
$requirementRules += Add-CDDocumentRequirementRule $rule
}
# Detection rules
$detectionRulesSummary = @()
$detectionRules = @()
if (@($Obj.detectionRules) | Where-Object '@odata.type' -EQ '#microsoft.graph.win32LobAppPowerShellScriptDetection') {
$detectionRulesType = Get-LanguageString 'DetectionRules.RuleConfigurationOptions.customScript'
foreach ($rule in @($Obj.detectionRules)) {
$header = Get-LanguageString 'ProactiveRemediations.Create.Settings.DetectionScriptMultiLineTextBox.label'
Add-DocumentationEncodedScript $Context $header "$($Obj.displayName) - $header" $rule.scriptContent
}
}
else {
$detectionRulesType = Get-LanguageString 'DetectionRules.RuleConfigurationOptions.manual'
foreach ($rule in @($Obj.detectionRules)) {
if ($rule.'@odata.type' -eq '#microsoft.graph.win32LobAppFileSystemDetection') {
$lngId = 'file'; $textValue = $rule.path
}
elseif ($rule.'@odata.type' -eq '#microsoft.graph.win32LobAppRegistryDetection') {
$lngId = 'registry'; $textValue = $rule.keyPath
}
else {
$lngId = 'mSI'; $textValue = $rule.productCode
}
$detectionRulesSummary += ('{0} {1}' -f (Get-LanguageString "DetectionRules.Manual.RuleTypeOptions.$lngId"), $textValue)
$detectionRules += Add-CDDocumentDetectionRule $rule
}
}
Set-DocumentationSyntheticProperty $Obj 'requirementRulesSummary' ($requirementRulesSummary -join $sep)
Set-DocumentationSyntheticProperty $Obj 'detectionRulesSummary' ($detectionRulesSummary -join $sep)
Set-DocumentationSyntheticProperty $Obj 'dependencyApps' ($dependencyApps -join $sep)
Set-DocumentationSyntheticProperty $Obj 'supersededApps' ($supersededApps -join $sep)
Set-DocumentationSyntheticProperty $Obj 'detectionRulesType' $detectionRulesType
Set-DocumentationSyntheticProperty $Obj 'requirementRulesTranslated' $requirementRules
Set-DocumentationSyntheticProperty $Obj 'detectionRulesTranslated' $detectionRules
Set-DocumentationSyntheticProperty $Obj 'returnCodes' $returnCodes
if($Obj.minimumSupportedWindowsRelease -is [string] -and $Obj.minimumSupportedWindowsRelease.length -eq 4) {
Set-DocumentationSyntheticProperty $Obj 'win10Release' (Get-LanguageString "MinimumOperatingSystem.Windows.V10Release.release$($obj.minimumSupportedWindowsRelease)")
}
elseif($Obj.minimumSupportedWindowsRelease -like "Windows10_*") {
Set-DocumentationSyntheticProperty $Obj 'win10Release' (Get-LanguageString "MinimumOperatingSystem.Windows.V10Release.release$(($Obj.minimumSupportedWindowsRelease.Split('_')[-1]))")
}
elseif($Obj.minimumSupportedWindowsRelease -like "Windows11_*") {
Set-DocumentationSyntheticProperty $Obj 'win10Release' (Get-LanguageString "MinimumOperatingSystem.Windows.V11Release.release$(($Obj.minimumSupportedWindowsRelease.Split('_')[-1]))")
}
Set-DocumentationSyntheticProperty $Obj 'installerType' (Get-LanguageString $installType)
Set-DocumentationSyntheticProperty $Obj 'uninstallerType' (Get-LanguageString $uninstallType)
if($Obj.allowedArchitectures) {
$archs = @()
if ($Obj.allowedArchitectures -like "*x86*") { $archs += (Get-LanguageString 'ArchitectureOptions.thirtyTwoBitInstructionSet') }
if ($Obj.allowedArchitectures -like "*x64*") { $archs += (Get-LanguageString 'ArchitectureOptions.sixtyFourBitInstructionSet') }
if ($Obj.allowedArchitectures -like "*arm64*") { $archs += (Get-LanguageString 'ArchitectureOptions.arm64InstructionSet') }
Set-DocumentationSyntheticProperty $Obj 'allowedArchitecturesTranslated' ($archs -join $sep)
}
else {
Set-DocumentationSyntheticProperty $Obj 'allowedArchitecturesTranslated' (Get-LanguageString 'Win32Requirements.allowedArchitecturesNoRadioButton')
}
}
# Win32 requirement rule -> array of {property, value} rows. Port of the old
# Add-CDDocumentRequirementRule.
function Add-CDDocumentRequirementRule {
param($Rule)
$strYes = Get-LanguageString 'SettingDetails.yes'
$strNo = Get-LanguageString 'SettingDetails.no'
$ruleInfo = @()
if ($Rule.'@odata.type' -eq '#microsoft.graph.win32LobAppFileSystemRequirement') {
$ruleInfo += [PSCustomObject]@{ property = (Get-LanguageString 'Win32Requirements.AdditionalRequirements.requirementType'); value = (Get-LanguageString 'Win32Requirements.AdditionalRequirements.RequirementTypeOptions.fileType') }
$ruleInfo += [PSCustomObject]@{ property = (Get-LanguageString 'DetectionRules.Manual.FileRule.path'); value = $Rule.path }
$ruleInfo += [PSCustomObject]@{ property = (Get-LanguageString 'DetectionRules.Manual.FileRule.fileOrFolder'); value = $Rule.fileOrFolderName }
$ruleInfo += [PSCustomObject]@{
property = (Get-LanguageString 'Win32Requirements.AdditionalRequirements.File.property')
value = switch ($Rule.detectionType) {
'createdDate' { Get-LanguageString 'DetectionRules.Manual.FileRule.DetectionMethodOptions.dateCreated' }
'modifiedDate' { Get-LanguageString 'DetectionRules.Manual.FileRule.DetectionMethodOptions.dateModified' }
'doesNotExist' { Get-LanguageString 'DetectionRules.Manual.FileRule.DetectionMethodOptions.doesNotExist' }
'exists' { Get-LanguageString 'DetectionRules.Manual.FileRule.DetectionMethodOptions.fileOrFolderExists' }
'sizeInMB' { Get-LanguageString 'DetectionRules.Manual.FileRule.DetectionMethodOptions.sizeInMB' }
'version' { Get-LanguageString 'DetectionRules.Manual.FileRule.DetectionMethodOptions.version' }
default { Get-LanguageString 'BooleanActions.notConfigured' }
}
}
if ($Rule.detectionValue -and $Rule.operator) {
$ruleInfo += [PSCustomObject]@{ property = (Get-LanguageString 'DetectionRules.Manual.FileRule.operator'); value = (Get-CDDocumentOperatorString $Rule.operator) }
$detectionValue = $Rule.detectionValue
if ($Rule.detectionType -eq 'createdDate' -or $Rule.detectionType -eq 'modifiedDate') {
try { $detectionValue = (Get-Date $Rule.detectionValue).ToString() } catch {}
}
$ruleInfo += [PSCustomObject]@{ property = (Get-LanguageString 'DetectionRules.Manual.FileRule.value'); value = $detectionValue }
}
$ruleInfo += [PSCustomObject]@{ property = (Get-LanguageString 'DetectionRules.Manual.RegistryRule.associatedWith32Bit'); value = $(if ($Rule.check32BitOn64System -eq $true) { $strYes } else { $strNo }) }
}
elseif ($Rule.'@odata.type' -eq '#microsoft.graph.win32LobAppRegistryRequirement') {
$ruleInfo += [PSCustomObject]@{ property = (Get-LanguageString 'Win32Requirements.AdditionalRequirements.requirementType'); value = (Get-LanguageString 'Win32Requirements.AdditionalRequirements.RequirementTypeOptions.registry') }
$ruleInfo += [PSCustomObject]@{ property = (Get-LanguageString 'Win32Requirements.AdditionalRequirements.Registry.keyPath'); value = $Rule.keyPath }
$ruleInfo += [PSCustomObject]@{ property = (Get-LanguageString 'Win32Requirements.AdditionalRequirements.Registry.valueName'); value = $Rule.valueName }
$ruleInfo += [PSCustomObject]@{
property = (Get-LanguageString 'Win32Requirements.AdditionalRequirements.Registry.registryRequirement')
value = switch ($Rule.detectionType) {
'doesNotExist' { if ($Rule.valueName) { Get-LanguageString 'DetectionRules.Manual.RegistryRule.DetectionMethodOptions.valueDoesNotExist' } else { Get-LanguageString 'DetectionRules.Manual.RegistryRule.DetectionMethodOptions.keyDoesNotExist' } }
'exists' { if ($Rule.valueName) { Get-LanguageString 'DetectionRules.Manual.RegistryRule.DetectionMethodOptions.valueExists' } else { Get-LanguageString 'DetectionRules.Manual.RegistryRule.DetectionMethodOptions.keyExists' } }
'integer' { Get-LanguageString 'DetectionRules.Manual.RegistryRule.DetectionMethodOptions.integerComparison' }
'string' { Get-LanguageString 'DetectionRules.Manual.RegistryRule.DetectionMethodOptions.stringComparison' }
'version' { Get-LanguageString 'DetectionRules.Manual.RegistryRule.DetectionMethodOptions.versionComparison' }
default { Get-LanguageString 'BooleanActions.notConfigured' }
}
}
if ($Rule.detectionValue -and $Rule.operator) {
$ruleInfo += [PSCustomObject]@{ property = (Get-LanguageString 'Win32Requirements.AdditionalRequirements.Registry.operator'); value = (Get-CDDocumentOperatorString $Rule.operator) }
$ruleInfo += [PSCustomObject]@{ property = (Get-LanguageString 'DetectionRules.Manual.RegistryRule.value'); value = $Rule.detectionValue }
}
$ruleInfo += [PSCustomObject]@{ property = (Get-LanguageString 'DetectionRules.Manual.RegistryRule.associatedWith32Bit'); value = $(if ($Rule.check32BitOn64System -eq $true) { $strYes } else { $strNo }) }
}
elseif ($Rule.'@odata.type' -eq '#microsoft.graph.win32LobAppPowerShellScriptRequirement') {
$ruleInfo += [PSCustomObject]@{ property = (Get-LanguageString 'Win32Requirements.AdditionalRequirements.requirementType'); value = (Get-LanguageString 'Win32Requirements.AdditionalRequirements.RequirementTypeOptions.script') }
$ruleInfo += [PSCustomObject]@{ property = (Get-LanguageString 'Win32Requirements.AdditionalRequirements.Script.scriptName'); value = $Rule.displayName }
$ruleInfo += [PSCustomObject]@{ property = (Get-LanguageString 'DetectionRules.CustomScript.runAs32Bit'); value = $(if ($Rule.runAs32Bit -eq $true) { $strYes } else { $strNo }) }
$ruleInfo += [PSCustomObject]@{ property = (Get-LanguageString 'Win32Requirements.AdditionalRequirements.Script.loggedOnCredentials'); value = $(if ($Rule.runAsAccount -ne 'system') { $strYes } else { $strNo }) }
$ruleInfo += [PSCustomObject]@{ property = (Get-LanguageString 'Win32Requirements.AdditionalRequirements.Script.enforceSignatureCheck'); value = $(if ($Rule.enforceSignatureCheck -eq $true) { $strYes } else { $strNo }) }
$ruleInfo += [PSCustomObject]@{
property = (Get-LanguageString 'Win32Requirements.AdditionalRequirements.Script.requirementMethod')
value = switch ($Rule.detectionType) {
'string' { Get-LanguageString 'Win32Requirements.AdditionalRequirements.Script.RequirementMethodOptions.string' }
'dateTime' { Get-LanguageString 'Win32Requirements.AdditionalRequirements.Script.RequirementMethodOptions.dateTime' }
'integer' { Get-LanguageString 'Win32Requirements.AdditionalRequirements.Script.RequirementMethodOptions.integer' }
'float' { Get-LanguageString 'Win32Requirements.AdditionalRequirements.Script.RequirementMethodOptions.float' }
'version' { Get-LanguageString 'Win32Requirements.AdditionalRequirements.Script.RequirementMethodOptions.version' }
'boolean' { Get-LanguageString 'Win32Requirements.AdditionalRequirements.Script.RequirementMethodOptions.boolean' }
default { Get-LanguageString 'BooleanActions.notConfigured' }
}
}
$ruleInfo += [PSCustomObject]@{ property = (Get-LanguageString 'Win32Requirements.AdditionalRequirements.Registry.operator'); value = (Get-CDDocumentOperatorString $Rule.operator) }
$ruleInfo += [PSCustomObject]@{ property = (Get-LanguageString 'Win32Requirements.AdditionalRequirements.Script.value'); value = $Rule.detectionValue }
}
return $ruleInfo
}
# Win32 detection rule -> array of {property, value} rows. Port of the old
# Add-CDDocumentDetectionRule.
function Add-CDDocumentDetectionRule {
param($Rule)
$strYes = Get-LanguageString 'SettingDetails.yes'
$strNo = Get-LanguageString 'SettingDetails.no'
$ruleInfo = @()
if ($Rule.'@odata.type' -eq '#microsoft.graph.win32LobAppFileSystemDetection') {
$ruleInfo += [PSCustomObject]@{ property = (Get-LanguageString 'DetectionRules.Manual.ruleType'); value = (Get-LanguageString 'DetectionRules.Manual.RuleTypeOptions.file') }
$ruleInfo += [PSCustomObject]@{ property = (Get-LanguageString 'DetectionRules.Manual.FileRule.path'); value = $Rule.path }
$ruleInfo += [PSCustomObject]@{ property = (Get-LanguageString 'DetectionRules.Manual.FileRule.fileOrFolder'); value = $Rule.fileOrFolderName }
$ruleInfo += [PSCustomObject]@{
property = (Get-LanguageString 'DetectionRules.Manual.FileRule.detectionMethod')
value = switch ($Rule.detectionType) {
'createdDate' { Get-LanguageString 'DetectionRules.Manual.FileRule.DetectionMethodOptions.dateCreated' }
'modifiedDate' { Get-LanguageString 'DetectionRules.Manual.FileRule.DetectionMethodOptions.dateModified' }
'doesNotExist' { Get-LanguageString 'DetectionRules.Manual.FileRule.DetectionMethodOptions.doesNotExist' }
'exists' { Get-LanguageString 'DetectionRules.Manual.FileRule.DetectionMethodOptions.fileOrFolderExists' }
'sizeInMB' { Get-LanguageString 'DetectionRules.Manual.FileRule.DetectionMethodOptions.sizeInMB' }
'version' { Get-LanguageString 'DetectionRules.Manual.FileRule.DetectionMethodOptions.version' }
default { Get-LanguageString 'BooleanActions.notConfigured' }
}
}
if ($Rule.detectionValue -and $Rule.operator) {
$ruleInfo += [PSCustomObject]@{ property = (Get-LanguageString 'DetectionRules.Manual.FileRule.operator'); value = (Get-CDDocumentOperatorString $Rule.operator) }
$detectionValue = $Rule.detectionValue
if ($Rule.detectionType -eq 'createdDate' -or $Rule.detectionType -eq 'modifiedDate') {
try { $detectionValue = (Get-Date $Rule.detectionValue).ToString() } catch {}
}
$ruleInfo += [PSCustomObject]@{ property = (Get-LanguageString 'DetectionRules.Manual.FileRule.value'); value = $detectionValue }
}
$ruleInfo += [PSCustomObject]@{ property = (Get-LanguageString 'DetectionRules.Manual.RegistryRule.associatedWith32Bit'); value = $(if ($Rule.check32BitOn64System -eq $true) { $strYes } else { $strNo }) }
}
elseif ($Rule.'@odata.type' -eq '#microsoft.graph.win32LobAppRegistryDetection') {
$ruleInfo += [PSCustomObject]@{ property = (Get-LanguageString 'DetectionRules.Manual.ruleType'); value = (Get-LanguageString 'DetectionRules.Manual.RuleTypeOptions.registry') }
$ruleInfo += [PSCustomObject]@{ property = (Get-LanguageString 'DetectionRules.Manual.RegistryRule.keyPath'); value = $Rule.keyPath }
$ruleInfo += [PSCustomObject]@{ property = (Get-LanguageString 'DetectionRules.Manual.RegistryRule.valueName'); value = $Rule.valueName }
$ruleInfo += [PSCustomObject]@{
property = (Get-LanguageString 'DetectionRules.Manual.RegistryRule.detectionMethod')
value = switch ($Rule.detectionType) {
'doesNotExist' { if ($Rule.valueName) { Get-LanguageString 'DetectionRules.Manual.RegistryRule.DetectionMethodOptions.valueDoesNotExist' } else { Get-LanguageString 'DetectionRules.Manual.RegistryRule.DetectionMethodOptions.keyDoesNotExist' } }
'exists' { if ($Rule.valueName) { Get-LanguageString 'DetectionRules.Manual.RegistryRule.DetectionMethodOptions.valueExists' } else { Get-LanguageString 'DetectionRules.Manual.RegistryRule.DetectionMethodOptions.keyExists' } }
'integer' { Get-LanguageString 'DetectionRules.Manual.RegistryRule.DetectionMethodOptions.integerComparison' }
'string' { Get-LanguageString 'DetectionRules.Manual.RegistryRule.DetectionMethodOptions.stringComparison' }
'version' { Get-LanguageString 'DetectionRules.Manual.RegistryRule.DetectionMethodOptions.versionComparison' }
default { Get-LanguageString 'BooleanActions.notConfigured' }
}
}
if ($Rule.detectionValue -and $Rule.operator) {
$ruleInfo += [PSCustomObject]@{ property = (Get-LanguageString 'DetectionRules.Manual.RegistryRule.operator'); value = (Get-CDDocumentOperatorString $Rule.operator) }
$ruleInfo += [PSCustomObject]@{ property = (Get-LanguageString 'DetectionRules.Manual.RegistryRule.value'); value = $Rule.detectionValue }
}
$ruleInfo += [PSCustomObject]@{ property = (Get-LanguageString 'DetectionRules.Manual.RegistryRule.associatedWith32Bit'); value = $(if ($Rule.check32BitOn64System -eq $true) { $strYes } else { $strNo }) }
}
else {
$ruleInfo += [PSCustomObject]@{ property = (Get-LanguageString 'DetectionRules.Manual.ruleType'); value = (Get-LanguageString 'DetectionRules.Manual.RuleTypeOptions.mSI') }
$ruleInfo += [PSCustomObject]@{ property = (Get-LanguageString 'DetectionRules.Manual.MsiRule.productCode'); value = $Rule.productCode }
$ruleInfo += [PSCustomObject]@{ property = (Get-LanguageString 'DetectionRules.Manual.MsiRule.productVersionCheck'); value = $(if ($null -ne $Rule.productVersion) { $strYes } else { $strNo }) }
if ($null -ne $Rule.productVersion) {
$ruleInfo += [PSCustomObject]@{ property = (Get-LanguageString 'DetectionRules.Manual.MsiRule.operator'); value = (Get-CDDocumentOperatorString $Rule.productVersionOperator) }
$ruleInfo += [PSCustomObject]@{ property = (Get-LanguageString 'DetectionRules.Manual.MsiRule.productVersion'); value = (Get-CDDocumentOperatorString $Rule.productVersion) }
}
}
return $ruleInfo
}
function Get-CDDocumentOperatorString {
param($Operator)
switch ($Operator) {
'notConfigured' { Get-LanguageString 'BooleanActions.notConfigured' }
'equal' { Get-LanguageString 'DetectionRules.ComparisonOperators.equals' }
'notEqual' { Get-LanguageString 'DetectionRules.ComparisonOperators.notEqualTo' }
'greaterThan' { Get-LanguageString 'DetectionRules.ComparisonOperators.greaterThan' }
'greaterThanOrEqual' { Get-LanguageString 'DetectionRules.ComparisonOperators.greaterThanOrEqualTo' }
'lessThan' { Get-LanguageString 'DetectionRules.ComparisonOperators.lessThan' }
'lessThanOrEqual' { Get-LanguageString 'DetectionRules.ComparisonOperators.lessThanOrEqualTo' }
'exists' { Get-LanguageString 'DetectionRules.Manual.FileRule.DetectionMethodOptions.fileOrFolderExists' }
default { $Operator }
}
}
function Get-DocumentationCustomPropertyObject {
param($TopObj, $Obj, $Prop)
if ($TopObj.'@odata.type' -ne '#microsoft.graph.windows10EndpointProtectionConfiguration') { return $null }
switch ($Prop.entityKey) {
'startupAuthenticationRequired' { return $TopObj.bitLockerSystemDrivePolicy }
'bitLockerSyntheticFixedDrivePolicyrequireEncryptionForWriteAccess' { return $TopObj.bitLockerFixedDrivePolicy }
'bitLockerSyntheticRemovableDrivePolicyrequireEncryptionForWriteAccess' { return $TopObj.bitLockerRemovableDrivePolicy }
}
return $null
}
function Get-DocumentationCustomChildObject {
param($TopObj, $Obj, $Prop)
switch ($TopObj.'@odata.type') {
'#microsoft.graph.windows10GeneralConfiguration' { if ($Prop.entityKey -eq 'syntheticDefenderDetectedMalwareActionsEnabled') { return $TopObj.defenderDetectedMalwareActions } }
'#microsoft.graph.iosDeviceFeaturesConfiguration' {
if ($Prop.entityKey -eq 'kerberosPrincipalName') { return $TopObj.singleSignOnSettings }
if ($Prop.entityKey -eq 'singleSignOnExtensionType') { return $TopObj.iosSingleSignOnExtension }
}
'#microsoft.graph.macOSDeviceFeaturesConfiguration' { if ($Prop.entityKey -eq 'singleSignOnExtensionType') { return $TopObj.macOSSingleSignOnExtension } }
'#microsoft.graph.windows10EndpointProtectionConfiguration' {
if ($Prop.entityKey -eq 'applicationGuardPrintSettings') { return $TopObj.applicationGuardPrintSettings }
if ($Prop.entityKey -eq 'firewallSyntheticIPsecExemptions') { return $TopObj.firewallSyntheticIPsecExemptions }
}
}
return $null
}
function Get-DocumentationCustomProfileValue {
param($TopObj, $Obj, $Prop, [DocumentationContext]$Context)
$type = [string]$TopObj.'@odata.type'
if ($type -eq '#microsoft.graph.windowsDeliveryOptimizationConfiguration' -and $Prop.entityKey -eq 'groupIdSourceSelector') {
Invoke-TranslateOption $Obj $Prop -SkipOptionChildren | Out-Null
return $false
}
if ($type -in @('#microsoft.graph.androidManagedAppProtection','#microsoft.graph.iosManagedAppProtection') -and $Prop.entityKey -eq 'apps') {
$customApps, $publishedApps = Get-CDMobileApps $Obj.apps
Add-PropertyInfo $Prop ($publishedApps -join $Context.ObjectSeparator) ($publishedApps -join $Context.PropertySeparator)
$info = Get-PropertyInfo $Prop ($customApps -join $Context.ObjectSeparator) ($customApps -join $Context.PropertySeparator)
$info.Name = Get-LanguageString 'SettingDetails.customApps'
$info.Description = ''
Add-PropertyInfoObject $info
return $false
}
if ($type -in @('#microsoft.graph.windowsInformationProtectionPolicy','#microsoft.graph.mdmWindowsInformationProtectionPolicy')) {
if ($Prop.entityKey -eq 'enterpriseIPRanges') {
$ranges = foreach ($rangeGroup in @($Obj.enterpriseIPRanges)) {
$values = foreach ($range in @($rangeGroup.ranges)) { "$($range.lowerAddress)-$($range.upperAddress)" }
if ($values) { "$($rangeGroup.displayName)$($Context.PropertySeparator)$($values -join $Context.PropertySeparator)" }
}
$ipv4 = @($ranges | Where-Object { $_ -match '\.' })
if ($ipv4.Count) { foreach ($range in $ipv4) { Add-PropertyInfo $Prop $range $range } }
else { Add-PropertyInfo $Prop $null $null }
$ipv6 = @($ranges | Where-Object { $_ -match ':' })
if (-not $ipv6.Count) {
$info = Get-PropertyInfo $Prop $null $null
$info.Name = Get-LanguageString 'WipPolicySettings.iPv6Ranges'
Add-PropertyInfoObject $info
}
foreach ($range in $ipv6) {
$info = Get-PropertyInfo $Prop $range $range
$info.Name = Get-LanguageString 'WipPolicySettings.iPv6Ranges'
Add-PropertyInfoObject $info
}
return $false
}
if ($Prop.entityKey -eq 'enterpriseProxiedDomains') {
foreach ($domain in @($Obj.enterpriseProxiedDomains)) {
$value = "$($domain.displayName)$($Context.PropertySeparator)$(@($domain.proxiedDomains.ipAddressOrFQDN) -join $Context.PropertySeparator)"
Add-PropertyInfo $Prop $value $value
}
return $false
}
}
if ($type -like '#microsoft.graph.windows*SCEPCertificateProfile' -and $Prop.entityKey -in @('subjectNameFormat','subjectAlternativeNameType')) { return $false }
if ($type -eq '#microsoft.graph.windows10GeneralConfiguration') {
if ($Prop.entityKey -eq 'startMenuAppListVisibility') {
$value = ([string]$Obj.startMenuAppListVisibility) -replace ',(?! )', ', '
Invoke-TranslateOption $Obj $Prop -PropValue $value | Out-Null
return $false
}
$privacy = $Obj.privacyAccessControls | Where-Object { $_.dataCategory -eq $Prop.entityKey -and $null -eq $_.appDisplayName } | Select-Object -First 1
if ($privacy) {
Invoke-TranslateOption $privacy $Prop -PropValue $privacy.accessLevel | Out-Null
return $false
}
}
if ($type -eq '#microsoft.graph.windows10EndpointProtectionConfiguration') {
if ($Prop.entityKey -eq 'applicationGuardEnabled') { return $false }
if ($Prop.entityKey -eq 'bitLockerRecoveryPasswordRotation') {
Invoke-TranslateOption $TopObj $Prop | Out-Null
return $false
}
}
if ($type -eq '#microsoft.graph.windowsHealthMonitoringConfiguration' -and $Prop.entityKey -eq 'configDeviceHealthMonitoringScope' -and ($Prop.options | Where-Object value -EQ 'healthMonitoring')) { return $false }
if ($type -eq '#microsoft.graph.windows10VpnConfiguration') {
if ($Prop.entityKey -eq 'enableSplitTunneling' -and $Prop.enabled -eq $false) { return $false }
if ($Prop.entityKey -eq 'eapXml' -and $Obj.eapXml) {
$value = ConvertFrom-DocumentationBase64 $Obj.eapXml
Add-PropertyInfo $Prop $value $value
return $false
}
}
if ($type -eq '#microsoft.graph.windowsUpdateForBusinessConfiguration' -and $Prop.entityKey -in @('businessReadyUpdatesOnly','autoRestartNotificationDismissal','scheduleRestartWarningInHours','scheduleImminentRestartWarningInMinutes','deliveryOptimizationMode')) { return $false }
return $null
}
function Finalize-DocumentationCustomObject {
param($Obj, [DocumentationContext]$Context)
if ($Obj.'@odata.type' -ne '#microsoft.graph.windows10EndpointProtectionConfiguration') { return }
foreach ($categoryId in @('bitLocker','xboxServices')) {
$category = Get-LanguageString "Category.$categoryId"
if (-not $category) { continue }
$rows = @($Context.SettingsData | Where-Object Category -EQ $category)
$configured = $rows | Where-Object {
if ($categoryId -eq 'bitLocker' -and $_.EntityKey -like 'startupAuthenticationTpm*') { return $false }
return $null -ne $_.RawValue -and $_.RawValue -ne $_.DefaultValue
} | Select-Object -First 1
if ($configured) { continue }
foreach ($row in $rows) { [void]$Context.SettingsData.Remove($row) }
}
}
function Invoke-DocumentationCustomPostAddValue {
param($TopObj, $Prop)
if ($TopObj.'@odata.type' -ne '#microsoft.graph.windowsUpdateForBusinessConfiguration') { return }
if ($Prop.entityKey -eq 'featureUpdatesDeferralPeriodInDays') {
$tmp = [PSCustomObject]@{ nameResourceKey='allowWindows11UpgradeName'; descriptionResourceKey='allowWindows11UpgradeDescription'; entityKey='allowWindows11Upgrade'; dataType=0; booleanActions=109; category=$Prop.category }
Add-PropertyInfo $tmp (Invoke-TranslateBoolean $TopObj $tmp) $TopObj.allowWindows11Upgrade
}
if ($Prop.entityKey -eq 'featureUpdatesRollbackWindowInDays') {
$configured = $TopObj.businessReadyUpdatesOnly -notin @('businessReadyOnly','all','userDefined')
$tmp = [PSCustomObject]@{ nameResourceKey='preReleaseBuilds'; descriptionResourceKey='preReleaseBuildsDescription'; entityKey='preReleaseEnabled'; dataType=0; booleanActions=2; category=$Prop.category }
Add-PropertyInfo $tmp $(if ($configured) { Get-LanguageString 'BooleanActions.enable' } else { Get-LanguageString 'BooleanActions.notConfigured' }) $TopObj.businessReadyUpdatesOnly
if ($configured) {
$tmp = [PSCustomObject]@{ nameResourceKey='preReleaseChannel'; descriptionResourceKey='preReleaseBuildsDescription'; entityKey='businessReadyUpdatesOnly'; dataType=0; booleanActions=2; category=$Prop.category }
Add-PropertyInfo $tmp (Get-LanguageString "SettingDetails.$($TopObj.businessReadyUpdatesOnly)Option") $TopObj.businessReadyUpdatesOnly
}
}
}
Add-DocumentationObjectInfoCustomizer ([PSCustomObject]@{
Name = 'DocumentationCustomizations'
# Runs for every object; each hook branches on @odata.type internally (and no-ops
# for types it doesn't handle), matching the old DocumentationCustom.psm1 provider.
MatchAll = $true
InitializeObject = { param($Obj, $Context) Initialize-DocumentationCustomObject $Obj $Context }
GetPropertyObject = { param($TopObj, $Obj, $Prop, $Context) Get-DocumentationCustomPropertyObject $TopObj $Obj $Prop }
GetChildObject = { param($TopObj, $Obj, $Prop, $Context) Get-DocumentationCustomChildObject $TopObj $Obj $Prop }
GetProfileValue = { param($TopObj, $Obj, $Prop, $Context) Get-DocumentationCustomProfileValue $TopObj $Obj $Prop $Context }
PostAddValue = { param($TopObj, $Prop, $Context) Invoke-DocumentationCustomPostAddValue $TopObj $Prop }
FinalizeObject = { param($Obj, $Context) Finalize-DocumentationCustomObject $Obj $Context }
})