Remediation/Intune/INT-POLICIES.ps1
|
# UNTESTED: this script has never been run on a real tenant. Read it, try it on a test tenant first, and use it at your own risk. <# .SYNOPSIS Copies the chosen Intune policies to the target, with no assignment, then assigns them on decision (procedure INT-POLICIES). .DESCRIPTION Comparison: the same name in the target counts as equivalent, which is a heuristic. Equivalents go to the equivalent-policies report; the rest goes in decisions\INT-POLICIES.csv, to compare with the target design. Only Recreate is copied. Copy through Graph: read at the source, read-only fields removed, creation in the target. - Compliance and Configuration: v1.0 (deviceCompliancePolicies, deviceConfigurations). - Settings catalog: beta only (configurationPolicies and its settings). - Administrative template: beta only (groupPolicyConfigurations, then updateDefinitionValues). - Enrollment: not copied, each tenant has its default configurations. Every policy is created with no assignment. decisions\INT-POLICIES-ASSIGNMENTS.csv says which to assign to the group of migrated devices, created by INT-AUTOPILOT. Graph replaces the assignments: the script keeps those that exist. Limits: an encrypted value of a custom profile cannot be carried over; the script stops on this policy, to recreate by hand. The Defender configuration specific to a tenant is proposed as Ignore. An administrative template based on an imported ADMX is not copied. Scripts and remediations are not in the inventory of the sources: check them in the admin center. #> $c = Open-TcRemediation $GroupName = (Get-TcObjectName ' - Migrated devices') $Roots = @{ 'Compliance' = @{ Uri = 'v1.0/deviceManagement/deviceCompliancePolicies'; Name = 'displayName' } 'Configuration' = @{ Uri = 'v1.0/deviceManagement/deviceConfigurations'; Name = 'displayName' } 'Settings catalog' = @{ Uri = 'beta/deviceManagement/configurationPolicies'; Name = 'name' } "Administrative template" = @{ Uri = 'beta/deviceManagement/groupPolicyConfigurations'; Name = 'displayName' } } $ReadOnlyFields = 'id', 'createdDateTime', 'lastModifiedDateTime', 'version', 'supportsScopeTags', 'settingCount', 'isAssigned', 'creationSource', 'priorityMetaData', 'assignments', 'scheduledActionsForRule', 'deviceManagementApplicabilityRuleOsEdition', 'deviceManagementApplicabilityRuleOsVersion', 'deviceManagementApplicabilityRuleDeviceMode', 'policyConfigurationIngestionType' $EmptyGuid = '00000000-0000-0000-0000-000000000000' function Find-PolicyRow { <# Policy of a category, by name; $null if it does not exist on this tenant. #> param([string]$Tenant, [string]$Category, [string]$Name) $r = $Roots[$Category] return @(Invoke-TcGraph -Tenant $Tenant -Uri (Get-TcMember $r 'Uri') -All | Where-Object { $_[(Get-TcMember $r 'Name')] -eq $Name }) | Select-Object -First 1 } function Get-CopyBody { <# Creation body: the policy read, with no read-only field or annotation. #> param($Source) $h = @{} foreach ($k in $Source.Keys) { if ($k -in $ReadOnlyFields -or $k -match '@odata\.(context|id|etag)$' -or $null -eq $Source[$k]) { continue } $h[$k] = $Source[$k] } $h['roleScopeTagIds'] = @('0') return $h } function Copy-CompliancePolicy { param([string]$Source, $Policy) $x = Invoke-TcGraph -Tenant $Source -Uri ("{0}/{1}?`$expand=scheduledActionsForRule(`$expand=scheduledActionConfigurations)" -f (Get-TcMember $Roots['Compliance'] 'Uri'), (Get-TcMember $Policy 'id')) $body = Get-CopyBody $x # Graph requires this property at creation; notification templates are specific to the tenant and do not follow. $actions = @(foreach ($r in @((Get-TcMember $x 'scheduledActionsForRule') | Where-Object { $_ })) { foreach ($s in @((Get-TcMember $r 'scheduledActionConfigurations') | Where-Object { $_ })) { @{ actionType = (Get-TcMember $s 'actionType'); gracePeriodHours = (Get-TcMember $s 'gracePeriodHours'); notificationTemplateId = $EmptyGuid; notificationMessageCCList = @() } } }) if (-not $actions.Count) { $actions = @(@{ actionType = 'block'; gracePeriodHours = 0; notificationTemplateId = $EmptyGuid; notificationMessageCCList = @() }) } $body.scheduledActionsForRule = @(@{ ruleName = 'PasswordRequired'; scheduledActionConfigurations = $actions }) return (Invoke-TcGraph -Tenant 'Target' -Method POST -Uri (Get-TcMember $Roots['Compliance'] 'Uri') -Body $body) } function Copy-ConfigurationPolicy { param([string]$Source, $Policy) $x = Invoke-TcGraph -Tenant $Source -Uri ("{0}/{1}" -f (Get-TcMember $Roots['Configuration'] 'Uri'), (Get-TcMember $Policy 'id')) $secretItems = @(@((Get-TcMember $x 'omaSettings')) | Where-Object { (Get-TcMember $_ 'isEncrypted') -or (Get-TcMember $_ 'secretReferenceValueId') }) if ($secretItems.Count) { throw ("{0} holds {1} encrypted value(s): recreate it by hand, then mark it Ignore." -f (Get-TcMember $Policy 'displayName'), $secretItems.Count) } return (Invoke-TcGraph -Tenant 'Target' -Method POST -Uri (Get-TcMember $Roots['Configuration'] 'Uri') -Body (Get-CopyBody $x)) } function Copy-CatalogPolicy { param([string]$Source, $Policy) $r = (Get-TcMember $Roots['Settings catalog'] 'Uri') $x = Invoke-TcGraph -Tenant $Source -Uri "$r/$((Get-TcMember $Policy 'id'))" $settingRows = @(Invoke-TcGraph -Tenant $Source -Uri "$r/$((Get-TcMember $Policy 'id'))/settings" -All) $body = @{ name = (Get-TcMember $x 'name'); description = (Get-TcMember $x 'description'); platforms = (Get-TcMember $x 'platforms'); technologies = (Get-TcMember $x 'technologies'); roleScopeTagIds = @('0') settings = @($settingRows | ForEach-Object { @{ '@odata.type' = '#microsoft.graph.deviceManagementConfigurationSetting'; settingInstance = (Get-TcMember $_ 'settingInstance') } }) } $namePattern = Get-TcProperty (Get-TcProperty $x 'templateReference') 'templateId' if ($namePattern) { $body.templateReference = @{ templateId = $namePattern } } return (Invoke-TcGraph -Tenant 'Target' -Method POST -Uri $r -Body $body) } function Copy-AdministrativeTemplate { param([string]$Source, $Policy) $r = (Get-TcMember $Roots["Administrative template"] 'Uri') if ((Get-TcMember $Policy 'policyConfigurationIngestionType') -in 'custom', 'mixed') { throw ("{0} rests on an imported ADMX: recreate it by hand, then mark it Ignore." -f (Get-TcMember $Policy 'displayName')) } $values = @(Invoke-TcGraph -Tenant $Source -Uri ("{0}/{1}/definitionValues?`$expand=definition(`$select=id)" -f $r, (Get-TcMember $Policy 'id')) -All) $additions = @(foreach ($v in $values) { $definitionId = (Get-TcMember (Get-TcMember $v 'definition') 'id') $presentations = @(Invoke-TcGraph -Tenant $Source -Uri ("{0}/{1}/definitionValues/{2}/presentationValues?`$expand=presentation(`$select=id)" -f $r, (Get-TcMember $Policy 'id'), (Get-TcMember $v 'id')) -All) @{ enabled = (Get-TcMember $v 'enabled'); 'definition@odata.bind' = "https://graph.microsoft.com/beta/deviceManagement/groupPolicyDefinitions('$definitionId')" presentationValues = @(foreach ($p in $presentations) { $copy = @{ '@odata.type' = $p['@odata.type']; 'presentation@odata.bind' = ("https://graph.microsoft.com/beta/deviceManagement/groupPolicyDefinitions('{0}')/presentations('{1}')" -f $definitionId, (Get-TcMember (Get-TcMember $p 'presentation') 'id')) } foreach ($k in 'value', 'values') { if ($p.ContainsKey($k)) { $copy[$k] = $p[$k] } } $copy }) } }) $n = Invoke-TcGraph -Tenant 'Target' -Method POST -Uri $r -Body @{ displayName = (Get-TcMember $Policy 'displayName'); description = (Get-TcMember $Policy 'description'); roleScopeTagIds = @('0') } Invoke-TcGraph -Tenant 'Target' -Method POST -Uri "$r/$((Get-TcMember $n 'id'))/updateDefinitionValues" -Body @{ added = $additions; updated = @(); deletedIds = @() } | Out-Null return $n } $sources = @(Get-TcRemediationPlanRole -RuleId 'INT-POLICIES') $targetNames = @(Get-TcRemediationTable 'Target' 'intunePolicies' | ForEach-Object { ([string]$_['Name']).Trim().ToLowerInvariant() }) $proposals = [System.Collections.Generic.List[object]]::new() $equivalents = [System.Collections.Generic.List[object]]::new() foreach ($t in $sources) { Write-TcRemediationTenant $t $policies = @(Get-TcRemediationTable $t 'intunePolicies') $missingItems = @($policies | Where-Object { ([string]$_['Name']).Trim().ToLowerInvariant() -notin $targetNames }) Write-TcRemediationNote ("{0} policy(ies), {1} with no equivalent of the same name in the target" -f $policies.Count, $missingItems.Count) foreach ($p in $policies | Where-Object { $_ -notin $missingItems }) { $equivalents.Add([ordered]@{ Tenant = $t; 'Category' = $p['Category']; Name = $p['Name']; Platform = $p['Platform'] }) } foreach ($p in $missingItems) { $ignoreItems = $p['Category'] -eq 'Enrollment' -or $p['Platform'] -eq 'windowsDefenderAdvancedThreatProtection' $proposals.Add([ordered]@{ Tenant = $t; 'Category' = $p['Category']; Name = $p['Name']; Platform = $p['Platform']; Assignments = $p['Assignments']; 'Modified' = $p['Modified']; Proposal = $(if ($ignoreItems) { 'Ignore' } else { 'Recreate' }) }) } } Close-TcRemediationBlock Export-TcRemediationReport -Name 'equivalent-policies' -Rows @($equivalents) | Out-Null $decisions = @(Get-TcDecision -Name 'INT-POLICIES' -Proposal @($proposals) -Key 'Tenant', 'Category', 'Name' -Choice 'Recreate', 'Ignore') $chosenPolicies = @($decisions | Where-Object { (Get-TcMember $_ 'Decision') -eq 'Recreate' }) foreach ($d in $chosenPolicies | Where-Object { (Get-TcMember $_ 'Tenant') -in $c.Tenants }) { if (-not $Roots.ContainsKey((Get-TcMember $d 'Category'))) { Write-TcRemediationAlert ("{0} ({1}): category this script cannot copy, to recreate by hand" -f (Get-TcMember $d 'Name'), (Get-TcMember $d 'Category')); continue } Add-TcBlockAction -Tenant 'Target' -Service Graph -Object (Get-TcMember $d 'Name') -Description ("Copy {0} from {1}, with no assignment" -f (Get-TcMember $d 'Name'), (Get-TcMember $d 'Tenant')) -Data @{ Source = (Get-TcMember $d 'Tenant'); Category = (Get-TcMember $d 'Category'); Name = (Get-TcMember $d 'Name') } ` -Test { param($a) [bool](Find-PolicyRow -Tenant $a.tenant -Category $a.data.Category -Name $a.data.Name) } ` -Block { param($a) $p = Find-PolicyRow -Tenant $a.data.Source -Category $a.data.Category -Name $a.data.Name if (-not $p) { throw "$($a.data.Name) not found in $($a.data.Source)" } $n = switch ($a.data.Category) { 'Compliance' { Copy-CompliancePolicy -Source $a.data.Source -Policy $p } 'Configuration' { Copy-ConfigurationPolicy -Source $a.data.Source -Policy $p } 'Settings catalog' { Copy-CatalogPolicy -Source $a.data.Source -Policy $p } default { Copy-AdministrativeTemplate -Source $a.data.Source -Policy $p } } $a.undo = @{ type = 'Graph'; tenant = $a.tenant; method = 'DELETE'; uri = ('{0}/{1}' -f (Get-TcMember $Roots[$a.data.Category] 'Uri'), (Get-TcMember $n 'id')) } } } $assignmentProposals = @($chosenPolicies | Where-Object { $Roots.ContainsKey((Get-TcMember $_ 'Category')) } | ForEach-Object { [ordered]@{ Tenant = (Get-TcMember $_ 'Tenant'); 'Category' = (Get-TcMember $_ 'Category'); Name = (Get-TcMember $_ 'Name'); Platform = (Get-TcMember $_ 'Platform'); Group = $GroupName; Proposal = '' } }) $assignments = @() if ($assignmentProposals.Count) { $assignments = @(Get-TcDecision -Name 'INT-POLICIES-ASSIGNMENTS' -Proposal $assignmentProposals -Key 'Tenant', 'Category', 'Name' -Choice 'Assign', 'Do not assign' | Where-Object { (Get-TcMember $_ 'Decision') -eq 'Assign' }) } foreach ($d in $assignments | Where-Object { (Get-TcMember $_ 'Tenant') -in $c.Tenants }) { Add-TcBlockAction -Tenant 'Target' -Service Graph -Object (Get-TcMember $d 'Name') -Description ("Assign {0} to the group of migrated devices" -f (Get-TcMember $d 'Name')) -Data @{ Category = (Get-TcMember $d 'Category'); Name = (Get-TcMember $d 'Name'); Group = $GroupName } ` -Test { param($a) $p = Find-PolicyRow -Tenant $a.tenant -Category $a.data.Category -Name $a.data.Name if (-not $p) { throw "$($a.data.Name) missing from the target: copy it first" } $group = @((Get-TcMember (Invoke-TcGraph -Tenant $a.tenant -Uri ("v1.0/groups?`$filter=displayName eq '{0}'&`$select=id" -f $a.data.Group)) 'value')) | Select-Object -First 1 if (-not $group) { throw "Group $($a.data.Group) missing from the target: run DEV-02" } $a.data.RootPath = '{0}/{1}' -f (Get-TcMember $Roots[$a.data.Category] 'Uri'), (Get-TcMember $p 'id') $a.data.GroupId = (Get-TcMember $group 'id') $a.data.ExistingItems = @((Get-TcMember (Invoke-TcGraph -Tenant $a.tenant -Uri "$($a.data.RootPath)/assignments") 'value') | ForEach-Object { @{ target = (Get-TcMember $_ 'target') } }) return [bool]@($a.data.ExistingItems | Where-Object { (Get-TcMember (Get-TcMember $_ 'target') 'groupId') -eq (Get-TcMember $group 'id') }).Count } ` -Block { param($a) $target = @{ '@odata.type' = '#microsoft.graph.groupAssignmentTarget'; groupId = $a.data.GroupId } $a.undo = @{ type = 'Graph'; tenant = $a.tenant; method = 'POST'; uri = "$($a.data.RootPath)/assign"; body = @{ assignments = @($a.data.ExistingItems) } } Invoke-TcGraph -Tenant $a.tenant -Method POST -Uri "$($a.data.RootPath)/assign" -Body @{ assignments = @(@($a.data.ExistingItems) + @(@{ target = $target })) } | Out-Null } } Invoke-TcRemediationPlan -Verify { param($t) $expected = @($chosenPolicies | Where-Object { (Get-TcMember $_ 'Tenant') -eq $t -and $Roots.ContainsKey((Get-TcMember $_ 'Category')) }) if (-not $expected.Count) { return } $missingOnes = @($expected | Where-Object { -not (Find-PolicyRow -Tenant 'Target' -Category (Get-TcMember $_ 'Category') -Name (Get-TcMember $_ 'Name')) }) if ($missingOnes.Count) { New-TcRemediationResult Warning ("{0} policy(ies) out of {1} missing from the target" -f $missingOnes.Count, $expected.Count) } else { New-TcRemediationResult OK ("{0} policy(ies) of {1} present in the target" -f $expected.Count, $t) } } Close-TcRemediation |