~/2025/04/17/powershell-intune-bulk-assign-app-protection-policies-by-group.md

PowerShell: Intune – Bulk Assign App Protection Policies by Group

---
author: 
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.Authentication and Microsoft.Graph.Groups modules (Install-Module Microsoft.Graph.Authentication, Microsoft.Graph.Groups -Scope CurrentUser).
  • Graph permissions, delegated or application: DeviceManagementApps.ReadWrite.All (the only permission the v1.0 assign action accepts) and Group.Read.All to resolve group names. GroupMember.Read.All is also enough for Get-MgGroup if 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

NameTypeRequiredDescription
PolicyNameStringYesDisplay name of the App Protection Policy, exactly as it appears in Intune
PlatformStringYesiOS or Android; picks the iosManagedAppProtections or androidManagedAppProtections collection
GroupNamesStringYesDisplay names of the Entra ID groups to add to the assignment
AsExclusionSwitchNoAdds the groups as excluded groups (exclusionGroupAssignmentTarget) instead of included groups
RemoveExistingSwitchNoReplaces 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:

powershell
.\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:

powershell
.\Add-IntuneAppProtectionAssignment.ps1 -PolicyName "BYOD Baseline - Android" -Platform Android -GroupNames "SG-MAM-Pilot" -AsExclusion -WhatIf

Replace the assignment list entirely for an Android policy:

powershell
.\Add-IntuneAppProtectionAssignment.ps1 -PolicyName "BYOD Baseline - Android" -Platform Android -GroupNames "SG-BYOD-All" -RemoveExisting

Sample output:

text
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:

powershell
$uri = "https://graph.microsoft.com/v1.0/deviceAppManagement/iosManagedAppProtections/<policy-id>/assignments"
(Invoke-MgGraphRequest -Method GET -Uri $uri).value | ForEach-Object { $_.target }

Script

powershell
<#
.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 assign is the full assignments collection, and Microsoft's reference gives no merge option. Post only the new group and the other assignments are gone. Always read .../assignments first unless you deliberately want -RemoveExisting.
  • Endpoint choice: v1.0 documents the list and assignments calls under iosManagedAppProtections and androidManagedAppProtections, and the assign action under /deviceAppManagement/managedAppPolicies/{id}/assign, which is what the script posts to. Microsoft recommends v1.0 over /beta for Intune where both exist, because beta changes more often.
  • The v1.0 list call doesn't document $filter support, 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 exclusionGroupAssignmentTarget entries, 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 assign call, so you only get near that if you loop the script over hundreds of policies.

Source