mirror of
https://github.com/Micke-K/IntuneManagement.git
synced 2026-09-28 10:55:38 +02:00
1041 lines
42 KiB
PowerShell
1041 lines
42 KiB
PowerShell
#ImportOrder 200
|
|
|
|
[Diagnostics.CodeAnalysis.SuppressMessageAttribute("TypeNotFound","", Justification = "")]
|
|
class DeviceConfigurationGroup : IntunePolicyGroupBase
|
|
{
|
|
DeviceConfigurationGroup() : Base()
|
|
{
|
|
$this.Init()
|
|
}
|
|
|
|
Init()
|
|
{
|
|
$this._ID = "DeviceConfiguration"
|
|
$this._Name = "Configuration"
|
|
$this._Icon = "DeviceConfiguration"
|
|
|
|
}
|
|
}
|
|
|
|
#########################################################################################
|
|
#
|
|
# Device Configuration
|
|
#
|
|
#########################################################################################
|
|
|
|
#region Device Configuration
|
|
|
|
class DeviceConfigurationType : IntunePolicyTypeBase
|
|
{
|
|
DeviceConfigurationType() : Base()
|
|
{
|
|
$this.Init()
|
|
}
|
|
|
|
Init()
|
|
{
|
|
$this._PolicyGroup = (Get-SingletonObject "DeviceConfigurationGroup")
|
|
if($null -ne $this._PolicyGroup) {
|
|
$this._PolicyGroup.AddPolicyType($this)
|
|
}
|
|
|
|
$this._PolicyName = "Device Configuration"
|
|
|
|
$this._ID = "DeviceConfiguration"
|
|
$this._API = "deviceManagement/deviceConfigurations"
|
|
$this._QueryList = "`?`$filter=not%20isof(%27microsoft.graph.windowsUpdateForBusinessConfiguration%27)%20and%20not%20isof(%27microsoft.graph.iosUpdateConfiguration%27)%20and%20not%20isof(%27microsoft.graph.macOSSoftwareUpdateConfiguration%27)"
|
|
$this._PolicyBaseName = "Device Configuration"
|
|
$this._PropertiesToRemove = @('privacyAccessControls')
|
|
$this._Permissions = @("DeviceManagementConfiguration.ReadWrite.All")
|
|
# The object class sets a per-row PolicyName ("iOS Wi-Fi") - the kind the
|
|
# portal calls Profile type. PolicyType._PolicyBaseName was a per-type constant.
|
|
$this._SubTypeColumn = "PolicyName=Type"
|
|
$this._Icon = "DeviceConfiguration"
|
|
$this._ObjectClass = "DeviceConfigurationObject"
|
|
$this._NavigationProperties = $true
|
|
$this._ExpandAssignmentsList = $false
|
|
|
|
}
|
|
|
|
PostImportCommand([PSCustomObject]$PolicyObject, [PSCustomObject]$SourceObject)
|
|
{
|
|
if(($PolicyObject.JsonObject.'@OData.Type' -like "#microsoft.graph.windows10GeneralConfiguration" -or
|
|
$SourceObject.JsonObject.'@OData.Type' -like "#microsoft.graph.windows10GeneralConfiguration") -and
|
|
($SourceObject.privacyAccessControls | Measure-Object).Count -gt 0)
|
|
{
|
|
$privacyObject = [PSCustomObject]@{
|
|
windowsPrivacyAccessControls = $SourceObject.JsonObject.privacyAccessControls
|
|
}
|
|
$json = $privacyObject | ConvertTo-Json -Depth 20
|
|
|
|
$url = (?? $this.PolicyType.APIPOST $this.PolicyType.API) + "/$($this.Id)/windowsPrivacyAccessControls"
|
|
$PolicyObject.Set($url, $json) | Out-Null
|
|
}
|
|
}
|
|
|
|
# OMA decrypt for windows10CustomConfiguration moved onto DeviceConfigurationObject
|
|
# via the sub-resource batch contract; this type no longer overrides GetFullObject.
|
|
}
|
|
|
|
class DeviceConfigurationObject : IntunePolicyBase
|
|
{
|
|
DeviceConfigurationObject([PSCustomObject]$JsonObj) : Base($JsonObj) { $this.Init() }
|
|
|
|
DeviceConfigurationObject() : Base()
|
|
{
|
|
$this.Init()
|
|
}
|
|
|
|
Hidden Init()
|
|
{
|
|
$this._PolicyType = (Get-SingletonObject "DeviceConfigurationType")
|
|
$this._PolicyName = Get-TemplatePolicyTypeName $this.JsonObject.'@odata.type' $this._PolicyType.PolicyBaseType
|
|
|
|
# windows10CustomConfiguration is the only odata.type with secret-referenced
|
|
# OMA settings. Opt into the sub-resource batching contract only for those —
|
|
# other DeviceConfiguration subtypes have nothing extra to fetch.
|
|
if($this.JsonObject.'@odata.type' -eq '#microsoft.graph.windows10CustomConfiguration') {
|
|
$this._HasSubResourceBatch = $true
|
|
}
|
|
}
|
|
|
|
[PSCustomObject[]] GetSubResourceBatchRequests([int]$Phase)
|
|
{
|
|
if($Phase -ne 1) { return @() }
|
|
if($this.JsonObject.'@odata.type' -ne '#microsoft.graph.windows10CustomConfiguration') { return @() }
|
|
|
|
$reqs = [System.Collections.Generic.List[PSCustomObject]]::new()
|
|
foreach($omaSetting in @($this.JsonObject.omaSettings)) {
|
|
if(-not $omaSetting -or $omaSetting.isEncrypted -ne $true) { continue }
|
|
if(-not $omaSetting.secretReferenceValueId) { continue }
|
|
$secretId = $omaSetting.secretReferenceValueId
|
|
[void]$reqs.Add([PSCustomObject]@{
|
|
Key = "oma:$secretId"
|
|
# Function-call endpoint; Graph supports these inside $batch GETs.
|
|
Url = "deviceManagement/deviceConfigurations/$($this.Id)/getOmaSettingPlainTextValue(secretReferenceValueId='$secretId')"
|
|
})
|
|
}
|
|
return $reqs.ToArray()
|
|
}
|
|
|
|
[PSCustomObject[]] ApplySubResourceBatchResult([int]$Phase, [string]$Key, $Body)
|
|
{
|
|
if($Phase -ne 1 -or -not $Key.StartsWith('oma:')) { return @() }
|
|
if(-not $Body -or -not $Body.Value) { return @() }
|
|
|
|
$secretId = $Key.Substring(4)
|
|
# secretReferenceValueId uniquely identifies the OMA setting on this policy;
|
|
# walk omaSettings to find the row whose plaintext we just resolved.
|
|
foreach($omaSetting in @($this.JsonObject.omaSettings)) {
|
|
if($omaSetting -and $omaSetting.secretReferenceValueId -eq $secretId) {
|
|
$omaSetting.isEncrypted = $false
|
|
$omaSetting.secretReferenceValueId = $null
|
|
|
|
if($omaSetting.'@odata.type' -eq '#microsoft.graph.omaSettingStringXml' -or
|
|
$omaSetting.'value@odata.type' -eq '#Binary') {
|
|
$bytes = [System.Text.Encoding]::UTF8.GetBytes($Body.Value)
|
|
$omaSetting.value = [Convert]::ToBase64String($bytes)
|
|
}
|
|
else {
|
|
$omaSetting.value = $Body.Value
|
|
}
|
|
break
|
|
}
|
|
}
|
|
|
|
return @()
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#########################################################################################
|
|
#
|
|
# Device Configuration
|
|
#
|
|
#########################################################################################
|
|
|
|
# region Device Configuration
|
|
|
|
class HardwareConfigurationsType : IntunePolicyTypeBase
|
|
{
|
|
HardwareConfigurationsType() : Base()
|
|
{
|
|
$this.Init()
|
|
}
|
|
|
|
Init()
|
|
{
|
|
$this._PolicyGroup = (Get-SingletonObject "DeviceConfigurationGroup")
|
|
if($null -ne $this._PolicyGroup) {
|
|
$this._PolicyGroup.AddPolicyType($this)
|
|
}
|
|
|
|
$this._PolicyFileAttributes += "configurationFileContent"
|
|
$this._PolicyName = "BIOS configurations and other settings"
|
|
|
|
$this._ID = "hardwareConfigurations"
|
|
$this._API = "deviceManagement/hardwareConfigurations"
|
|
# The /assign action's parameter is type-specific here - the default
|
|
# 'assignments' body key is rejected with 400.
|
|
$this._AssignmentsType = "hardwareConfigurationAssignments"
|
|
$this._PropertiesToRemoveForUpdate = @('version')
|
|
$this._PolicyBaseName = "Device Configuration"
|
|
$this._Permissions = @("DeviceManagementConfiguration.ReadWrite.All")
|
|
# The object class sets a per-row PolicyName ("iOS Wi-Fi") - the kind the
|
|
# portal calls Profile type. PolicyType._PolicyBaseName was a per-type constant.
|
|
$this._SubTypeColumn = "PolicyName=Type"
|
|
$this._Icon = "DeviceConfiguration"
|
|
$this._ObjectClass = "HardwareConfigurationObject"
|
|
$this._NavigationProperties = $true
|
|
|
|
}
|
|
}
|
|
|
|
class HardwareConfigurationObject : IntunePolicyBase
|
|
{
|
|
HardwareConfigurationObject([PSCustomObject]$JsonObj) : Base($JsonObj) { $this.Init() }
|
|
|
|
HardwareConfigurationObject() : Base()
|
|
{
|
|
$this.Init()
|
|
}
|
|
|
|
Hidden Init()
|
|
{
|
|
$this._PolicyType = (Get-SingletonObject "HardwareConfigurationsType")
|
|
$this._PlatformName = Get-LanguageString "Platform.windows" -IgnoreMissing
|
|
$this._PolicyName = Get-TemplatePolicyTypeName $this.JsonObject.'@odata.type' $this._PolicyType.PolicyBaseType
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#########################################################################################
|
|
#
|
|
# Settings Catalog
|
|
#
|
|
#########################################################################################
|
|
|
|
# region Settings Catalog
|
|
|
|
class SettingsCatalogType : SettingsCatalogTypeBase
|
|
{
|
|
SettingsCatalogType() : Base()
|
|
{
|
|
([SettingsCatalogType]$this).Init()
|
|
}
|
|
|
|
Init()
|
|
{
|
|
$this._PolicyGroup = (Get-SingletonObject "DeviceConfigurationGroup")
|
|
$this._ID = "SettingsCatalog"
|
|
$this._QueryList = "?`$filter=templateReference/templateFamily eq 'none'"
|
|
|
|
if($null -ne $this._PolicyGroup) {
|
|
$this._PolicyGroup.AddPolicyType($this)
|
|
}
|
|
|
|
$this._PolicyTypeOrder = 150
|
|
}
|
|
|
|
# ToDo: Verify that it works
|
|
#PostExportCommand([PSCustomObject]$PolicyObject, [String]$PathToFile)
|
|
#{
|
|
# Add-GraphAssignmentsToExportFile $PolicyObject $PathToFile
|
|
#}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#########################################################################################
|
|
#
|
|
# Android OEM Config
|
|
#
|
|
#########################################################################################
|
|
|
|
# region Android OEM Config
|
|
|
|
class AndroidOEMConfigType : IntunePolicyTypeBase
|
|
{
|
|
AndroidOEMConfigType() : Base()
|
|
{
|
|
$this.Init()
|
|
}
|
|
|
|
Init()
|
|
{
|
|
$this._PolicyGroup = (Get-SingletonObject "DeviceConfigurationGroup")
|
|
if($null -ne $this._PolicyGroup) {
|
|
$this._PolicyGroup.AddPolicyType($this)
|
|
}
|
|
|
|
$this._PolicyName = "Android OEM Config"
|
|
|
|
$this._ID = "AndroidOEMConfig"
|
|
# Platform default: the endpoint serves exactly one platform and the
|
|
# objects carry no platforms/platformType field, so the column would
|
|
# otherwise be blank (OEMConfig requires Android Enterprise).
|
|
$this._PlatformName = Get-LanguageString "Platform.androidEnterprise" -IgnoreMissing
|
|
$this._API = "deviceAppManagement/mobileAppConfigurations"
|
|
$this._QueryList = "?`$filter=microsoft.graph.androidManagedStoreAppConfiguration/appSupportsOemConfig eq true"
|
|
$this._Dependencies = @("Applications")
|
|
$this._Permissions = @("DeviceManagementConfiguration.ReadWrite.All")
|
|
$this._Icon = "DeviceConfiguration"
|
|
$this._ObjectClass = "AndroidOEMConfigObject"
|
|
$this._ExpandAssignmentsList = $false
|
|
|
|
}
|
|
|
|
[Hashtable]PreImportAssignmentsCommand([PSCustomObject]$PolicyObject, [PSCustomObject]$SourceObject)
|
|
{
|
|
return (@{ "API" = "$($this.API)/$($PolicyObject.Id)/microsoft.graph.managedDeviceMobileAppConfiguration/assign" })
|
|
}
|
|
|
|
[Hashtable]PreImportCommand([PSCustomObject]$PolicyObject)
|
|
{
|
|
Import-AppConfigurationTargetedApps $PolicyObject
|
|
return $null
|
|
}
|
|
}
|
|
|
|
class AndroidOEMConfigObject : IntunePolicyBase
|
|
{
|
|
AndroidOEMConfigObject([PSCustomObject]$JsonObj) : Base($JsonObj) { $this.Init() }
|
|
|
|
AndroidOEMConfigObject() : Base()
|
|
{
|
|
$this.Init()
|
|
}
|
|
|
|
Hidden Init()
|
|
{
|
|
$this._PolicyType = (Get-SingletonObject "AndroidOEMConfigType")
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#########################################################################################
|
|
#
|
|
# Administrative Templates
|
|
#
|
|
#########################################################################################
|
|
|
|
# region Administrative Templates
|
|
|
|
class AdminTemplateType : IntunePolicyTypeBase
|
|
{
|
|
AdminTemplateType() : Base()
|
|
{
|
|
$this.Init()
|
|
}
|
|
|
|
Init()
|
|
{
|
|
$this._PolicyGroup = (Get-SingletonObject "DeviceConfigurationGroup")
|
|
if($null -ne $this._PolicyGroup) {
|
|
$this._PolicyGroup.AddPolicyType($this)
|
|
}
|
|
|
|
$this._PolicyName = "Administrative template"
|
|
$this._PolicyBaseName = "Administrative templates"
|
|
|
|
$this._ID = "AdministrativeTemplates"
|
|
$this._API = "deviceManagement/groupPolicyConfigurations"
|
|
$this._PropertiesToRemove = @("definitionValues","policyConfigurationIngestionType")
|
|
$this._Permissions = @("DeviceManagementConfiguration.ReadWrite.All")
|
|
$this._Icon = "DeviceConfiguration"
|
|
$this._ObjectClass = "AdminTemplateObject"
|
|
$this._Dependencies = @("ADMXFiles")
|
|
|
|
}
|
|
|
|
PostImportCommand([PSCustomObject]$PolicyObject, [PSCustomObject]$SourceObject)
|
|
{
|
|
if($SourceObject.Object.definitionValues)
|
|
{
|
|
Import-GPOSetting $PolicyObject $SourceObject.Object.definitionValues
|
|
}
|
|
}
|
|
|
|
[Hashtable]GetCompareConfig()
|
|
{
|
|
return @{
|
|
Prop = "definitionValues"
|
|
GetKey = { param($s) Get-DefinitionValueKey $s }
|
|
GetValue = { param($s) Get-DefinitionValueDisplayValue $s }
|
|
GetCategory = { param($s) if($s.definition) { "$($s.definition.categoryPath)" } else { "$($s.'#Definition_categoryPath')" } }
|
|
Hydrate = {
|
|
param($policy)
|
|
if(-not $policy -or -not $policy.Object) { return }
|
|
if($policy.Object.definitionValues) { return }
|
|
if(-not $policy._TokenId) { return }
|
|
$url = "deviceManagement/groupPolicyConfigurations/$($policy.Id)/definitionValues?`$expand=definition,presentationValues"
|
|
$result = Invoke-MSGraphAPI -Url $url -ODataMetadata "skip" -TokenId $policy._TokenId
|
|
if($result -and $result.value)
|
|
{
|
|
$policy.Object | Add-Member NoteProperty -Name "definitionValues" -Value $result.value -Force
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
class AdminTemplateObject : IntunePolicyBase
|
|
{
|
|
Hidden [Hashtable]$_SubResourceState = $null
|
|
|
|
AdminTemplateObject([PSCustomObject]$JsonObj) : Base($JsonObj) { $this.Init() }
|
|
|
|
AdminTemplateObject() : Base()
|
|
{
|
|
$this.Init()
|
|
}
|
|
|
|
Hidden Init()
|
|
{
|
|
$this._PolicyType = (Get-SingletonObject "AdminTemplateType")
|
|
$this._PlatformName = Get-LanguageString "Platform.windows" -IgnoreMissing
|
|
|
|
# Opt into the sub-resource batching contract — Invoke-PolicyHydrate
|
|
# drives the definitionValues + presentationValues cascade via $batch.
|
|
$this._HasSubResourceBatch = $true
|
|
}
|
|
|
|
[PSCustomObject[]] GetSubResourceBatchRequests([int]$Phase)
|
|
{
|
|
if($Phase -ne 1) { return @() }
|
|
$reqs = @([PSCustomObject]@{
|
|
Key = 'definitionValues'
|
|
Url = "deviceManagement/groupPolicyConfigurations/$($this.Id)/definitionValues?`$expand=definition"
|
|
})
|
|
# When the per-id GET returns policyConfigurationIngestionType='unknown',
|
|
# the list endpoint filtered by id returns the resolved value
|
|
# (mixed/custom/builtIn/...). Add the list-filter call to Phase 1 so the
|
|
# apply method can patch the JsonObject before serialisation. Matches
|
|
# what the OLD project's Get-GPOObjectSettings did at fetch time.
|
|
if($this.JsonObject.policyConfigurationIngestionType -eq 'unknown') {
|
|
$reqs += [PSCustomObject]@{
|
|
Key = 'ingestionType'
|
|
Url = "deviceManagement/groupPolicyConfigurations?`$filter=id eq '$($this.Id)'"
|
|
}
|
|
}
|
|
return $reqs
|
|
}
|
|
|
|
[PSCustomObject[]] ApplySubResourceBatchResult([int]$Phase, [string]$Key, $Body)
|
|
{
|
|
if($Phase -eq 1 -and $Key -eq 'ingestionType') {
|
|
$row = if($Body -and $Body.value) { @($Body.value)[0] } else { $null }
|
|
if($row -and $row.policyConfigurationIngestionType) {
|
|
$this.JsonObject.policyConfigurationIngestionType = $row.policyConfigurationIngestionType
|
|
}
|
|
return @()
|
|
}
|
|
|
|
if($Phase -eq 1 -and $Key -eq 'definitionValues') {
|
|
$rawValues = if($Body -and $Body.value) { @($Body.value) } else { @() }
|
|
|
|
# Reshape into the legacy on-disk format: drop the embedded
|
|
# `definition` object, replace it with a `definition@odata.bind`
|
|
# URL plus four `#Definition_*` flat fields used downstream by
|
|
# compare/documentation/import. Mirrors the original
|
|
# Get-GPOObjectSettings transform.
|
|
$domain = Get-GraphDomain $this._TokenId
|
|
$apiVersion = $this._PolicyType.APIVersion
|
|
$this._SubResourceState = @{}
|
|
$flatList = @()
|
|
|
|
foreach($defValue in $rawValues) {
|
|
if(-not $defValue) { continue }
|
|
$defId = $defValue.definition.id
|
|
$flat = [ordered]@{
|
|
"enabled" = $defValue.enabled
|
|
"definition@odata.bind" = "https://$domain/$apiVersion/deviceManagement/groupPolicyDefinitions('$defId')"
|
|
}
|
|
if($defValue.definition.categoryPath) {
|
|
$flat["#Definition_Id"] = $defId
|
|
$flat["#Definition_displayName"] = $defValue.definition.displayName
|
|
$flat["#Definition_classType"] = $defValue.definition.classType
|
|
$flat["#Definition_categoryPath"] = $defValue.definition.categoryPath
|
|
}
|
|
$flatObj = [PSCustomObject]$flat
|
|
$flatList += $flatObj
|
|
|
|
if($defValue.id) {
|
|
$this._SubResourceState[$defValue.id] = [PSCustomObject]@{
|
|
Flat = $flatObj
|
|
DefinitionId = $defId
|
|
HasCategory = [bool]$defValue.definition.categoryPath
|
|
}
|
|
}
|
|
}
|
|
|
|
Add-Member -InputObject $this.JsonObject -MemberType NoteProperty -Name 'definitionValues' -Value $flatList -Force
|
|
|
|
$followups = @()
|
|
foreach($defValue in $rawValues) {
|
|
if(-not $defValue.id) { continue }
|
|
$followups += [PSCustomObject]@{
|
|
Key = "presentationValues:$($defValue.id)"
|
|
Url = "deviceManagement/groupPolicyConfigurations/$($this.Id)/definitionValues/$($defValue.id)/presentationValues?`$expand=presentation"
|
|
}
|
|
}
|
|
return $followups
|
|
}
|
|
|
|
if($Phase -eq 2 -and $Key -like 'presentationValues:*') {
|
|
$defValueId = $Key.Substring('presentationValues:'.Length)
|
|
$state = $null
|
|
if($null -ne $this._SubResourceState) {
|
|
$state = $this._SubResourceState[$defValueId]
|
|
}
|
|
if(-not $state) { return @() }
|
|
|
|
$rawPres = if($Body -and $Body.value) { @($Body.value) } else { @() }
|
|
if($rawPres.Count -eq 0) { return @() }
|
|
|
|
$domain = Get-GraphDomain $this._TokenId
|
|
$apiVersion = $this._PolicyType.APIVersion
|
|
$defId = $state.DefinitionId
|
|
$hasCategory = $state.HasCategory
|
|
$shaped = @()
|
|
|
|
foreach($pv in $rawPres) {
|
|
if(-not $pv) { continue }
|
|
$presId = $pv.presentation.id
|
|
Add-Member -InputObject $pv -MemberType NoteProperty -Name 'presentation@odata.bind' `
|
|
-Value "https://$domain/$apiVersion/deviceManagement/groupPolicyDefinitions('$defId')/presentations('$presId')" -Force
|
|
if($hasCategory) {
|
|
Add-Member -InputObject $pv -MemberType NoteProperty -Name '#Presentation_Id' -Value $presId -Force
|
|
Add-Member -InputObject $pv -MemberType NoteProperty -Name '#Presentation_Label' -Value $pv.presentation.label -Force
|
|
}
|
|
Remove-ObjectProperty $pv 'presentation'
|
|
Remove-ObjectProperty $pv 'id'
|
|
Remove-ObjectProperty $pv 'lastModifiedDateTime'
|
|
Remove-ObjectProperty $pv 'createdDateTime'
|
|
$shaped += $pv
|
|
}
|
|
|
|
Add-Member -InputObject $state.Flat -MemberType NoteProperty -Name 'presentationValues' -Value $shaped -Force
|
|
}
|
|
|
|
return @()
|
|
}
|
|
}
|
|
|
|
#########################################################################################
|
|
#
|
|
# ADMX File
|
|
#
|
|
#########################################################################################
|
|
|
|
# region ADMX File
|
|
|
|
class ADMXFileType : IntunePolicyTypeBase
|
|
{
|
|
ADMXFileType() : Base()
|
|
{
|
|
$this.Init()
|
|
}
|
|
|
|
Init()
|
|
{
|
|
$this._PolicyGroup = (Get-SingletonObject "DeviceConfigurationGroup")
|
|
if($null -ne $this._PolicyGroup) {
|
|
$this._PolicyGroup.AddPolicyType($this)
|
|
}
|
|
|
|
$this._PolicyName = "ADMX File"
|
|
$this._ID = "ADMXFiles"
|
|
$this._APITitle = "ADMX Files"
|
|
$this._API = "deviceManagement/groupPolicyUploadedDefinitionFiles"
|
|
$this._NameProperty = "fileName"
|
|
$this._PropertiesToRemove = @("languageCodes","targetPrefix","targetNamespace","policyType","revision","status","uploadDateTime")
|
|
$this._Permissions = @("DeviceManagementConfiguration.ReadWrite.All")
|
|
$this._Icon = "DeviceConfiguration"
|
|
$this._ObjectClass = "ADMXFileObject"
|
|
$this._ImportOrder = 45
|
|
$this._ExpandAssignmentsList = $false
|
|
$this._SupportsAssignments = $false
|
|
|
|
# Restricted button set: ADMX Files support only View / Import / Export
|
|
# (no Compare/Copy/Document/Delete). Graph returns `content` as null on
|
|
# GET, so an export is the object's metadata only - the ADMX/ADML bytes
|
|
# are not in it. Import therefore reads the source files from disk; see
|
|
# PreImportCommand.
|
|
$this._ShowButtons = @("View","Import","Export")
|
|
}
|
|
|
|
[IntunePolicyBase[]]PreImportPolicies([IntunePolicyBase[]]$PolicyObjects)
|
|
{
|
|
return @($PolicyObjects | sort-object -property @{e={$_.Object.lastModifiedDateTime}} )
|
|
}
|
|
|
|
# Rebuild the upload payload from the ADMX/ADML files on disk, because the
|
|
# exported json cannot carry them (Graph never returns `content`).
|
|
#
|
|
# Both layouts these files ship in are accepted, searched in this order:
|
|
#
|
|
# <folder>/<name>.admx + <folder>/<name>.adml flat (the v3 layout)
|
|
# <folder>/<name>.admx + <folder>/<lang>/<name>.adml per-language subfolders
|
|
#
|
|
# <folder> is the exported json's own folder, falling back to the "App
|
|
# packages folder" setting - the same two-location search v3 did. Every
|
|
# language subfolder holding a matching .adml is uploaded, so a multi-language
|
|
# ADMX keeps all of its languages rather than only the default one.
|
|
[Hashtable]PreImportCommand([PSCustomObject]$PolicyObject)
|
|
{
|
|
$obj = $PolicyObject.Object
|
|
|
|
if(-not $obj.fileName)
|
|
{
|
|
Write-Log "ADMX File object has no fileName. It will not be imported." 2
|
|
return @{ "Import" = $false }
|
|
}
|
|
|
|
$baseName = [IO.Path]::GetFileNameWithoutExtension($obj.fileName)
|
|
|
|
$searchFolders = @()
|
|
if($PolicyObject.FileInfo -and $PolicyObject.FileInfo.Directory.FullName)
|
|
{
|
|
$searchFolders += $PolicyObject.FileInfo.Directory.FullName
|
|
}
|
|
$pkgPath = Get-SettingValue "IntuneAppPackagesFolder"
|
|
if($pkgPath) { $searchFolders += $pkgPath }
|
|
|
|
$admxFile = $null
|
|
$sourceFolder = $null
|
|
foreach($folder in $searchFolders)
|
|
{
|
|
$candidate = [IO.Path]::Combine($folder, $obj.fileName)
|
|
if(Test-Path -LiteralPath $candidate -PathType Leaf)
|
|
{
|
|
$admxFile = $candidate
|
|
$sourceFolder = $folder
|
|
break
|
|
}
|
|
}
|
|
|
|
if(-not $admxFile)
|
|
{
|
|
Write-Log "ADMX file $($obj.fileName) not found in the export folder or the app packages folder. The ADMX File object will not be imported." 2
|
|
return @{ "Import" = $false }
|
|
}
|
|
|
|
# defaultLanguageCode names the language of a flat .adml, which carries no
|
|
# language in its path. Read it before it is stripped below.
|
|
$defaultLanguage = ?? $obj.defaultLanguageCode "en-US"
|
|
|
|
$languageFiles = @()
|
|
|
|
$flatADML = [IO.Path]::Combine($sourceFolder, "$baseName.adml")
|
|
if(Test-Path -LiteralPath $flatADML -PathType Leaf)
|
|
{
|
|
$languageFiles += [PSCustomObject]@{ Path = $flatADML; LanguageCode = $defaultLanguage }
|
|
}
|
|
|
|
foreach($dir in @(Get-ChildItem -LiteralPath $sourceFolder -Directory -ErrorAction SilentlyContinue))
|
|
{
|
|
$langADML = [IO.Path]::Combine($dir.FullName, "$baseName.adml")
|
|
if(Test-Path -LiteralPath $langADML -PathType Leaf)
|
|
{
|
|
# A flat .adml already claimed this language - keep the first.
|
|
if(@($languageFiles | Where-Object { $_.LanguageCode -eq $dir.Name }).Count -gt 0) { continue }
|
|
$languageFiles += [PSCustomObject]@{ Path = $langADML; LanguageCode = $dir.Name }
|
|
}
|
|
}
|
|
|
|
if($languageFiles.Count -eq 0)
|
|
{
|
|
Write-Log "No ADML file found for $($obj.fileName). Looked for $baseName.adml in $sourceFolder and in its language sub-folders. The ADMX File object will not be imported." 2
|
|
return @{ "Import" = $false }
|
|
}
|
|
|
|
# ReadAllText detects the source encoding; re-encode as UTF-8. Vendor ADMX
|
|
# often ships as UTF-16 with a BOM, which Intune rejects with "Data at the
|
|
# root level is invalid". v3 wrote ASCII bytes here, which silently mangles
|
|
# any non-ASCII character in a policy name or description.
|
|
$obj.content = [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes([IO.File]::ReadAllText($admxFile)))
|
|
|
|
$obj.groupPolicyUploadedLanguageFiles = @(
|
|
foreach($languageFile in $languageFiles)
|
|
{
|
|
[PSCustomObject]@{
|
|
"@odata.type" = "#microsoft.graph.groupPolicyUploadedLanguageFile"
|
|
fileName = [IO.Path]::GetFileName($languageFile.Path)
|
|
languageCode = $languageFile.LanguageCode
|
|
content = [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes([IO.File]::ReadAllText($languageFile.Path)))
|
|
}
|
|
})
|
|
|
|
# Graph rejects the property on create ("needs to be null, taken from the
|
|
# ADML file"), so remove it rather than blanking it - a null would still be
|
|
# serialized into the body.
|
|
if($obj.PSObject.Properties['defaultLanguageCode'])
|
|
{
|
|
$obj.PSObject.Properties.Remove('defaultLanguageCode')
|
|
}
|
|
|
|
Write-Log "Import ADMX $($obj.fileName) with $($languageFiles.Count) language file(s): $(($languageFiles | ForEach-Object { $_.LanguageCode }) -join ', ')"
|
|
|
|
return $null
|
|
}
|
|
|
|
# Ingesting an ADMX creates new definition ids, so any cached definition map
|
|
# for this tenant now points at ids that no longer exist. Import-GPOSetting
|
|
# builds that cache to resolve custom ADMX settings on admin-template import;
|
|
# leaving it stale is what made an admin template imported right after its
|
|
# ADMX silently resolve nothing. v3 cleared the same cache here.
|
|
PostImportCommand([PSCustomObject]$PolicyObject, [PSCustomObject]$SourceObject)
|
|
{
|
|
$tokenInfo = Get-OperationTokenInfo $PolicyObject.TokenId
|
|
if($tokenInfo) { Clear-CacheObject "ADMXDefinitions_$($tokenInfo.TenantId)" }
|
|
}
|
|
}
|
|
|
|
class ADMXFileObject : IntunePolicyBase
|
|
{
|
|
ADMXFileObject([PSCustomObject]$JsonObj) : Base($JsonObj) { $this.Init() }
|
|
|
|
ADMXFileObject() : Base()
|
|
{
|
|
$this.Init()
|
|
}
|
|
|
|
Hidden Init()
|
|
{
|
|
$this._PolicyType = (Get-SingletonObject "ADMXFileType")
|
|
$this._PolicyName = Get-TemplatePolicyTypeName $this.JsonObject.'@odata.type' $this._PolicyType.PolicyBaseType
|
|
$this._PlatformName = Get-LanguageString "Platform.windows" -IgnoreMissing
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
# Get-GPOObjectSettings was the pre-refactor GPO definitionValues +
|
|
# presentationValues fetcher. AdminTemplateObject.GetSubResourceBatchRequests /
|
|
# ApplySubResourceBatchResult now drives the same flow through the unified
|
|
# sub-resource batch contract, so the original helper is dead code. Deleted
|
|
# along with its `headers = @{ "Accept" = "application/json"; "odata.metadata" = "minimal" }`
|
|
# line — that form splits the OData metadata directive into a separate header
|
|
# key Graph ignores, leaving the response at the default `full` metadata, the
|
|
# opposite of the intent.
|
|
|
|
function Import-GPOSetting
|
|
{
|
|
param($PolicyObject, $Settings)
|
|
|
|
if($PolicyObject)
|
|
{
|
|
Write-Status "Import settings for $($PolicyObject.Name)"
|
|
|
|
$hasCustomADMX = $null -ne ($Settings | Where-Object { $null -ne $_.'#Definition_categoryPath' })
|
|
|
|
# Get-OperationTokenInfo, not Get-TokenInfo: an imported object that carries no
|
|
# token of its own has TokenId 0 - never $null - and 0 means "no filter, every
|
|
# token" to Get-TokenInfo. With a second tenant signed in the key would name
|
|
# both at once, so both tenants share one partition and one tenant's custom
|
|
# ADMX definitions could be used to resolve the other tenant's import.
|
|
$tokenInfo = Get-OperationTokenInfo $PolicyObject.TokenId
|
|
$cacheId = "ADMXDefinitions_$($tokenInfo.TenantId)"
|
|
|
|
$customADMXDefinitions = $null
|
|
|
|
if($hasCustomADMX)
|
|
{
|
|
$customADMXDefinitions = Get-CacheObject $cacheId
|
|
if(-not $customADMXDefinitions)
|
|
{
|
|
Write-Status "Import custom ADMX settings"
|
|
$tmpCustomCategories = Invoke-MSGraphAPI -Url "deviceManagement/groupPolicyCategories?`$expand=definitions(`$select=id, displayName, categoryPath, classType)&`$select=id, displayName&`$filter=ingestionSource eq 'custom'" -ODataMetadata "Minimal" -TokenId $PolicyObject.TokenId
|
|
if($tmpCustomCategories.Value)
|
|
{
|
|
$customADMXDefinitions = @{}
|
|
foreach($tmpCat in $tmpCustomCategories.Value)
|
|
{
|
|
foreach($tmpDef in $tmpCat.definitions)
|
|
{
|
|
$key = ($tmpDef.displayName + $tmpDef.categoryPath + $tmpDef.classType).ToLower()
|
|
$val = [PSCustomObject]@{
|
|
Definition = $tmpDef
|
|
Category = $tmpCat
|
|
Presentations = $null
|
|
}
|
|
try {
|
|
$customADMXDefinitions.Add($key, $val)
|
|
}
|
|
catch {
|
|
Write-Log "Failed to add '$($tmpDef.displayName)' in category '$($tmpDef.categoryPath)' of class $($tmpDef.classType)" 3
|
|
}
|
|
}
|
|
}
|
|
}
|
|
Set-CacheObject $cacheId $customADMXDefinitions "TenantCache_$($tokenInfo.TenantId)"
|
|
}
|
|
}
|
|
|
|
# Batch the per-setting POSTs into a single Graph $batch (chunked at 20 by the
|
|
# batcher) instead of one HTTP call per setting. The rewrite/strip logic below
|
|
# mutates each setting in place; we collect the prepared sub-requests in the
|
|
# loop and dispatch them once at the end.
|
|
$importBatch = [System.Collections.Generic.List[PSCustomObject]]::new()
|
|
|
|
foreach($setting in $Settings)
|
|
{
|
|
if($setting.'#Definition_categoryPath' -and $customADMXDefinitions -is [HashTable] -and $customADMXDefinitions.Count -gt 0)
|
|
{
|
|
$defVal = $null
|
|
$key = ($setting.'#Definition_displayName' + $setting.'#Definition_categoryPath' + $setting.'#Definition_classType').ToLower()
|
|
if($key -and $customADMXDefinitions.ContainsKey($key))
|
|
{
|
|
$defVal = $customADMXDefinitions[$key]
|
|
}
|
|
elseif($key)
|
|
{
|
|
Write-Log "No custom ADMX definitiona found for setting $($setting.'#Definition_displayName')" 2
|
|
}
|
|
else
|
|
{
|
|
Write-Log "Setting $($setting.'#Definition_displayName') does not have information to be imported in the environment"
|
|
}
|
|
|
|
if($defVal)
|
|
{
|
|
$setting.'definition@odata.bind' = $setting.'definition@odata.bind' -replace $setting.'#Definition_Id', $defVal.Definition.Id
|
|
if(($setting.presentationValues | Measure-Object).Count -gt 0)
|
|
{
|
|
if(-not $defVal.Presentations)
|
|
{
|
|
$tmpPresentation = Invoke-MSGraphAPI -Url "deviceManagement/groupPolicyDefinitions/$($defVal.Definition.Id)/presentations" -ODataMetadata "Minimal" -TokenId $PolicyObject.TokenId
|
|
if($tmpPresentation.value)
|
|
{
|
|
foreach($settingPresentation in $setting.presentationValues)
|
|
{
|
|
$tmpPresentationVal = $tmpPresentation.value | Where-Object label -eq $settingPresentation.'#Presentation_Label'
|
|
if($tmpPresentationVal)
|
|
{
|
|
$settingPresentation.'presentation@odata.bind' = $settingPresentation.'presentation@odata.bind' -replace $setting.'#Definition_Id', $defVal.Definition.Id
|
|
$settingPresentation.'presentation@odata.bind' = $settingPresentation.'presentation@odata.bind' -replace $settingPresentation.'#Presentation_Id', $tmpPresentationVal.Id
|
|
}
|
|
else
|
|
{
|
|
Write-Log "Could not find a presentation value with label $($settingPresentation.'#Presentation_Label'). Setting will not be configured" 2
|
|
continue
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
Write-Log "Could not find presentation for setting $($settingPresentation.'#Presentation_Label'). Setting will not be configured." 2
|
|
continue
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
Write-Log "Settings might not be available if imported in another environment" 3
|
|
}
|
|
}
|
|
elseif($setting.'#Definition_categoryPath')
|
|
{
|
|
Write-Log "Custom AMDX settings cannot be imported without ADMX file imported. Definitions not found" 2
|
|
continue
|
|
}
|
|
|
|
Remove-GraphPropertiesForImport $PolicyObject $setting
|
|
|
|
if($true)
|
|
{
|
|
foreach($tmpProp in (($setting.PSObject.Properties | Where-Object Name -like "#*").Name))
|
|
{
|
|
Remove-Property $setting $tmpProp
|
|
}
|
|
|
|
foreach($settingPresentation in $setting.presentationValues)
|
|
{
|
|
foreach($tmpProp in (($settingPresentation.PSObject.Properties | Where-Object Name -like "#*").Name))
|
|
{
|
|
Remove-Property $settingPresentation $tmpProp
|
|
}
|
|
}
|
|
}
|
|
|
|
# Queue the setting POST; actual dispatch happens after the loop in a single batch.
|
|
[void]$importBatch.Add([PSCustomObject]@{
|
|
id = [Guid]::NewGuid().Guid
|
|
method = "POST"
|
|
url = "deviceManagement/groupPolicyConfigurations/$($PolicyObject.id)/definitionValues"
|
|
headers = @{ "Content-Type" = "application/json" }
|
|
body = ($setting | ConvertTo-Json -Depth 20 | ConvertFrom-Json)
|
|
})
|
|
}
|
|
|
|
if($importBatch.Count -gt 0)
|
|
{
|
|
Invoke-GraphBatchRequest -BatchObjects $importBatch -BatchType "GPOSettingsImport" -TokenId $PolicyObject.TokenId | Out-Null
|
|
}
|
|
}
|
|
}
|
|
|
|
#########################################################################################
|
|
#
|
|
# Inventory Policies
|
|
#
|
|
#########################################################################################
|
|
|
|
class InventoryPoliciesType : IntunePolicyTypeBase
|
|
{
|
|
InventoryPoliciesType() : Base() { $this.Init() }
|
|
Init()
|
|
{
|
|
$this._PolicyGroup = (Get-SingletonObject "DeviceConfigurationGroup")
|
|
$this._PolicyName = "Inventory Policies"
|
|
$this._ID = "InventoryPolicies"
|
|
# Endpoint rejects a name $filter (verified live 2026-08-27) - searches filter client-side.
|
|
$this._SupportsNameFilter = $false
|
|
$this._API = "deviceManagement/inventoryPolicies"
|
|
$this._QueryList = "?`$expand=settings"
|
|
$this._Permissions = @("DeviceManagementConfiguration.ReadWrite.All")
|
|
$this._PropertiesToRemove = @('settingCount')
|
|
$this._NameProperty = "name"
|
|
$this._ObjectClass = "InventoryPoliciesObject"
|
|
$this._Icon = "DeviceConfiguration"
|
|
if($null -ne $this._PolicyGroup) { $this._PolicyGroup.AddPolicyType($this) }
|
|
}
|
|
}
|
|
|
|
class InventoryPoliciesObject : IntunePolicyBase
|
|
{
|
|
InventoryPoliciesObject([PSCustomObject]$JsonObj) : Base($JsonObj) { $this.Init() }
|
|
InventoryPoliciesObject() : Base() { $this.Init() }
|
|
Hidden Init()
|
|
{
|
|
$this._PolicyType = (Get-SingletonObject "InventoryPoliciesType")
|
|
}
|
|
}
|
|
|
|
|
|
#########################################################################################
|
|
#
|
|
# Policy Sets
|
|
#
|
|
#########################################################################################
|
|
#
|
|
# Sits under the Applications group because Intune's portal puts PolicySets
|
|
# under Apps and the Graph endpoint is rooted at deviceAppManagement. Each
|
|
# PolicySet bundles references to other policies (apps, configs, scripts,
|
|
# etc.) and assigns them as one unit.
|
|
#
|
|
# Cross-tenant import: items[].payloadId references are re-pointed by name
|
|
# (or dropped when unresolvable) via Resolve-IntunePolicySetItems - see
|
|
# Internal/IntunePolicySetImport.ps1.
|
|
|
|
#region PolicySetsObject
|
|
|
|
[Diagnostics.CodeAnalysis.SuppressMessageAttribute("TypeNotFound","", Justification = "")]
|
|
class PolicySetObject : IntunePolicyBase
|
|
{
|
|
PolicySetObject([PSCustomObject]$JsonObj) : Base($JsonObj) { $this.Init() }
|
|
|
|
PolicySetObject() : Base()
|
|
{
|
|
$this.Init()
|
|
}
|
|
|
|
Hidden Init()
|
|
{
|
|
$this._PolicyType = (Get-SingletonObject "PolicySetsType")
|
|
# No _PlatformName — PolicySets are cross-platform bundles.
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region PolicySetsType
|
|
[Diagnostics.CodeAnalysis.SuppressMessageAttribute("TypeNotFound","", Justification = "")]
|
|
class PolicySetsType : IntunePolicyTypeBase
|
|
{
|
|
PolicySetsType() : Base()
|
|
{
|
|
$this.Init()
|
|
}
|
|
|
|
Init()
|
|
{
|
|
$this._PolicyGroup = (Get-SingletonObject "DeviceConfigurationGroup")
|
|
$this._PolicyName = "Policy Sets"
|
|
$this._ID = "PolicySets"
|
|
$this._API = "deviceAppManagement/policySets"
|
|
# items + assignments come back via $expand. Without items the round-
|
|
# trip on export loses the bundled references; assignments load with
|
|
# the policy so the assignments tool can see them.
|
|
$this._Expand = "items,assignments"
|
|
$this._Permissions = @("DeviceManagementApps.ReadWrite.All")
|
|
# status / errorCode / lastModifiedDateTime are server-set; strip on
|
|
# both create and update to avoid Graph rejecting writes.
|
|
$this._PropertiesToRemove = @('status','errorCode')
|
|
# items + assignments are navigation properties Graph rejects on
|
|
# PATCH ("Cannot apply PATCH to navigation property ..."); items go
|
|
# through the /update action in PreUpdateCommand instead.
|
|
$this._PropertiesToRemoveForUpdate = @('status','errorCode','items','assignments')
|
|
# PolicySet uses `roleScopeTags` (string array of tag IDs, NOT the
|
|
# more common `roleScopeTagIds`). Same Microsoft Graph quirk as
|
|
# AssignmentFilters — see reference_assignment_filter_scope_tag_property.
|
|
$this._ScopeTagProperty = "roleScopeTags"
|
|
# Policy sets reference other objects, so they must import LAST. The
|
|
# default order is 1000; 200 put them ahead of Compliance, Settings
|
|
# Catalog, App Protection and App Config, which are exactly what a set
|
|
# bundles. It went unnoticed because same-tenant imports resolve items
|
|
# by payloadId before the referenced objects are re-created; a portable
|
|
# import resolves by name and dropped every item. v3 used 2000 for the
|
|
# same reason.
|
|
$this._ImportOrder = 2000
|
|
$this._ObjectClass = "PolicySetObject"
|
|
# Graph returns HTTP 400 on `deviceAppManagement/policySets?$expand=assignments`,
|
|
# so suppress the list-URL expand. Per-policy GETs still use _Expand to
|
|
# carry items + assignments — that variant Graph accepts.
|
|
$this._ExpandAssignmentsList = $false
|
|
# The per-policy /assignments navigation GET 400s too - only the
|
|
# single-object $expand variant works.
|
|
$this._AssignmentsViaExpand = $true
|
|
# No /assign segment on policySets ("Resource not found for the
|
|
# segment 'assign'") - the update action takes the full `assignments`
|
|
# replacement list with the same semantics.
|
|
$this._AssignAction = "update"
|
|
$this._AssignmentObjectType = "#microsoft.graph.policySetAssignment"
|
|
# status / errorCode track the ASYNC processing state of the set
|
|
# ('notAssigned' -> 'success'), and every item embeds the same
|
|
# per-item state - so identically-configured sets compare unequal on
|
|
# raw properties. Item MEMBERSHIP drift is still caught by the
|
|
# documentation compare (PolicySetDocHandler documents each item).
|
|
$this._ComparePropertiesToSkip = @('status','errorCode','items')
|
|
# PolicySets aren't platform-specific; the bundled items each have
|
|
# their own platforms but the set itself spans them.
|
|
$this._HasPlatform = $false
|
|
|
|
if($null -ne $this._PolicyGroup) {
|
|
$this._PolicyGroup.AddPolicyType($this)
|
|
}
|
|
}
|
|
|
|
[Hashtable]PreImportCommand([PSCustomObject]$PolicyObject)
|
|
{
|
|
# Re-point (or drop) cross-tenant item references and strip items to
|
|
# the POST-safe shape - see Internal/IntunePolicySetImport.ps1.
|
|
Resolve-IntunePolicySetItems -PolicyObject $PolicyObject -TokenId ([int]$PolicyObject.TokenId)
|
|
return (@{})
|
|
}
|
|
|
|
[Hashtable]PreUpdateCommand([PSCustomObject]$PolicyObject, [PSCustomObject]$ExistingObject)
|
|
{
|
|
# items cannot be PATCHed - push membership deltas through the
|
|
# /update action first; the PATCH then carries metadata only
|
|
# (_PropertiesToRemoveForUpdate strips items + assignments).
|
|
Update-IntunePolicySetItems -PolicyObject $PolicyObject -ExistingObject $ExistingObject -TokenId ([int]$PolicyObject.TokenId)
|
|
return (@{})
|
|
}
|
|
}
|
|
#endregion |