~/2025/04/17/powershell-intune-bulk-assign-app-protection-policies-by-group.md
PowerShell: Intune – Bulk Assign App Protection Policies by Group
--- author: Tom Lasswell date: read: 3 min in: [ps, scripts] tags: [powershell, intune, entra-id] ---
$ grep -n '^#' post.md
App Protection Policy assignment in the Intune console is a one-at-a-time affair: open the policy, click Assignment, pick groups, save, repeat for the next policy. When a reorg lands and I need to add a dozen new department groups to three or four existing policies, I reach for Microsoft Graph instead. The catch is that the assign action on these policies takes the complete assignment list in its request body, so the script has to read the existing assignments before it writes a new list, or everyone who was already targeted silently drops off.
This version uses only Graph v1.0 endpoints: list the iOS or Android policies, read /assignments on the one you picked, merge in the new group targets (as includes or as exclusions), and post the combined list to managedAppPolicies/{id}/assign.
Requirements
- PowerShell 7.x (Windows PowerShell 5.1 also works).
Microsoft.Graph.AuthenticationandMicrosoft.Graph.Groupsmodules (Install-Module Microsoft.Graph.Authentication, Microsoft.Graph.Groups -Scope CurrentUser).- Graph permissions, delegated or application:
DeviceManagementApps.ReadWrite.All(the only permission the v1.0assignaction accepts) andGroup.Read.Allto resolve group names.GroupMember.Read.Allis also enough forGet-MgGroupif you want the narrower scope. - An active Intune license on the tenant. Microsoft notes that the Intune Graph API requires one.
- An Intune role that can edit app protection policies (Intune Administrator, or a custom role with the app protection permissions) for the signed-in account.
- The Entra ID groups must already exist; the script doesn't create them.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
PolicyName | String | Yes | Display name of the App Protection Policy, exactly as it appears in Intune |
Platform | String | Yes | iOS or Android; picks the iosManagedAppProtections or androidManagedAppProtections collection |
GroupNames | String | Yes | Display names of the Entra ID groups to add to the assignment |
AsExclusion | Switch | No | Adds the groups as excluded groups (exclusionGroupAssignmentTarget) instead of included groups |
RemoveExisting | Switch | No | Replaces the assignment list instead of merging with what's already there |
The script supports -WhatIf, which resolves everything and prints the final assignment count without calling assign.
Usage
Add three new groups to an existing iOS policy without disturbing who's already assigned:
.\Add-IntuneAppProtectionAssignment.ps1 -PolicyName "Corporate Data - iOS" -Platform iOS -GroupNames "SG-Sales-EMEA", "SG-Sales-APAC", "SG-Finance-Contractors"
Exclude a pilot group from an Android policy, previewing first:
.\Add-IntuneAppProtectionAssignment.ps1 -PolicyName "BYOD Baseline - Android" -Platform Android -GroupNames "SG-MAM-Pilot" -AsExclusion -WhatIf
Replace the assignment list entirely for an Android policy:
.\Add-IntuneAppProtectionAssignment.ps1 -PolicyName "BYOD Baseline - Android" -Platform Android -GroupNames "SG-BYOD-All" -RemoveExisting
Sample output:
Resolved policy 'Corporate Data - iOS' (id: 3f2c9b4a-...)
Resolved group 'SG-Sales-EMEA' (id: 8a1d...)
Resolved group 'SG-Sales-APAC' (id: 5e77...)
Resolved group 'SG-Finance-Contractors' (id: c410...)
Existing assignments preserved: 2
New group targets added: 3 (include)
Assignment updated: 5 target(s) on 'Corporate Data - iOS'.
To check the result afterwards, read the assignments back:
$uri = "https://graph.microsoft.com/v1.0/deviceAppManagement/iosManagedAppProtections/<policy-id>/assignments"
(Invoke-MgGraphRequest -Method GET -Uri $uri).value | ForEach-Object { $_.target }
Script
<#
.SYNOPSIS
Adds Entra ID groups to an Intune App Protection Policy assignment.
.DESCRIPTION
Connects to Microsoft Graph, resolves an iOS or Android App Protection
Policy by display name, resolves the target groups by display name, and
calls the v1.0 assign action. The assign action takes the complete list of
assignments, so the script reads the policy's current assignments first
and merges the new group targets into them (unless -RemoveExisting is set).
Groups can be added as included or excluded targets. Supports -WhatIf.
.PARAMETER PolicyName
Display name of the App Protection Policy, exactly as it appears in Intune.
.PARAMETER Platform
iOS or Android. Picks the iosManagedAppProtections or
androidManagedAppProtections collection.
.PARAMETER GroupNames
Display names of the Entra ID groups to add to the assignment.
.PARAMETER AsExclusion
Adds the groups as excluded groups instead of included groups.
.PARAMETER RemoveExisting
Replaces the assignment list instead of merging with what is already
assigned.
.EXAMPLE
.\Add-IntuneAppProtectionAssignment.ps1 -PolicyName "Corporate Data - iOS" -Platform iOS -GroupNames "SG-Sales-EMEA", "SG-Sales-APAC"
.EXAMPLE
.\Add-IntuneAppProtectionAssignment.ps1 -PolicyName "BYOD Baseline - Android" -Platform Android -GroupNames "SG-MAM-Pilot" -AsExclusion -WhatIf
.NOTES
Author : Thomas Lasswell (https://www.techcolumnist.com)
Version : 1.0 (2025-04-17)
Requires: Microsoft.Graph.Authentication, Microsoft.Graph.Groups;
DeviceManagementApps.ReadWrite.All and Group.Read.All.
.LINK
https://learn.microsoft.com/en-us/graph/api/intune-mam-targetedmanagedappprotection-assign?view=graph-rest-1.0
#>
[CmdletBinding(SupportsShouldProcess = $true)]
param (
[Parameter(Mandatory = $true)]
[string]$PolicyName,
[Parameter(Mandatory = $true)]
[ValidateSet('iOS', 'Android')]
[string]$Platform,
[Parameter(Mandatory = $true)]
[string[]]$GroupNames,
[Parameter(Mandatory = $false)]
[switch]$AsExclusion,
[Parameter(Mandatory = $false)]
[switch]$RemoveExisting
)
$ErrorActionPreference = 'Stop'
$graph = 'https://graph.microsoft.com/v1.0'
# Follow @odata.nextLink so large collections are read completely.
function Get-GraphCollection {
param ([string]$Uri)
$items = @()
while ($Uri) {
$page = Invoke-MgGraphRequest -Method GET -Uri $Uri
$items += $page.value
$Uri = $page.'@odata.nextLink'
}
return $items
}
if (-not (Get-MgContext)) {
Connect-MgGraph -Scopes "DeviceManagementApps.ReadWrite.All", "Group.Read.All" -NoWelcome
}
$resource = if ($Platform -eq 'iOS') { 'iosManagedAppProtections' } else { 'androidManagedAppProtections' }
# Resolve the policy by display name (client-side match, exact and case-insensitive).
$policies = @(Get-GraphCollection -Uri "$graph/deviceAppManagement/$resource" |
Where-Object { $_.displayName -eq $PolicyName })
if ($policies.Count -eq 0) {
throw "No $Platform App Protection Policy found with display name '$PolicyName'."
}
if ($policies.Count -gt 1) {
throw "Multiple $Platform App Protection Policies matched '$PolicyName'. Rename them so the display name is unique."
}
$policyId = $policies[0].id
Write-Host "Resolved policy '$PolicyName' (id: $policyId)"
# Resolve each target group to its object id. Single quotes are doubled for OData.
$targetType = if ($AsExclusion) { '#microsoft.graph.exclusionGroupAssignmentTarget' } else { '#microsoft.graph.groupAssignmentTarget' }
$groupTargets = foreach ($groupName in $GroupNames) {
$escapedName = $groupName.Replace("'", "''")
$group = @(Get-MgGroup -Filter "displayName eq '$escapedName'" -Property "id,displayName")
if ($group.Count -eq 0) {
throw "No Entra ID group found with display name '$groupName'."
}
if ($group.Count -gt 1) {
throw "Multiple Entra ID groups matched '$groupName'. Use a unique display name."
}
Write-Host "Resolved group '$groupName' (id: $($group[0].Id))"
@{
target = @{
'@odata.type' = $targetType
groupId = $group[0].Id
}
}
}
# Read the current assignment list so the assign call does not drop anyone.
$existingAssignments = @()
if (-not $RemoveExisting) {
$current = Get-GraphCollection -Uri "$graph/deviceAppManagement/$resource/$policyId/assignments"
$existingAssignments = @($current | ForEach-Object { @{ target = $_.target } })
Write-Host "Existing assignments preserved: $($existingAssignments.Count)"
}
# De-duplicate on group id so re-running the script is safe.
$existingGroupIds = @($existingAssignments |
Where-Object { $_.target.ContainsKey('groupId') } |
ForEach-Object { $_.target.groupId })
$newTargets = @($groupTargets | Where-Object { $_.target.groupId -notin $existingGroupIds })
$finalAssignments = @($existingAssignments) + $newTargets
$mode = if ($AsExclusion) { 'exclude' } else { 'include' }
Write-Host "New group targets added: $($newTargets.Count) ($mode)"
if ($newTargets.Count -eq 0 -and -not $RemoveExisting) {
Write-Host "Nothing to change."
return
}
$body = @{ assignments = $finalAssignments } | ConvertTo-Json -Depth 10
if ($PSCmdlet.ShouldProcess($PolicyName, "Assign $($finalAssignments.Count) target(s)")) {
Invoke-MgGraphRequest -Method POST -Uri "$graph/deviceAppManagement/managedAppPolicies/$policyId/assign" -Body $body -ContentType "application/json"
Write-Host "Assignment updated: $($finalAssignments.Count) target(s) on '$PolicyName'."
}
Notes
- The request body of
assignis the fullassignmentscollection, and Microsoft's reference gives no merge option. Post only the new group and the other assignments are gone. Always read.../assignmentsfirst unless you deliberately want-RemoveExisting. - Endpoint choice: v1.0 documents the list and assignments calls under
iosManagedAppProtectionsandandroidManagedAppProtections, and theassignaction under/deviceAppManagement/managedAppPolicies/{id}/assign, which is what the script posts to. Microsoft recommends v1.0 over/betafor Intune where both exist, because beta changes more often. - The v1.0 list call doesn't document
$filtersupport, so the script pulls the collection and matches the display name locally. With a few dozen policies that's one or two requests. - Existing assignment targets are copied as-is, including any
exclusionGroupAssignmentTargetentries, so current exclusions survive the merge. De-duplication only compares group ids: a group that is already included won't also be added as an exclusion. Remove it in the console first if that's what you want. - The script doesn't touch Device Configuration profiles or Compliance Policies. Those use different resources (
deviceConfigurations,deviceCompliancePolicies) with their own assign actions. - Write limits: Microsoft's documented Intune Graph limit is 100 POST/PUT/PATCH/DELETE requests per 20 seconds per app per tenant. One policy is one
assigncall, so you only get near that if you loop the script over hundreds of policies.