Managing app assignments in Microsoft Intune can be a time-consuming task, especially when dealing with multiple applications and groups. This PowerShell script aims to simplify and automate the process of adding new inclusion or exclusion assignments to Intune apps without removing existing assignments. Whether you need to include or exclude a group from app deployments, this script provides a streamlined solution.
Before running the script, ensure that you have the following:
Here’s a breakdown of the script’s key components and how it works:
The script begins by defining variables for client ID, tenant ID, and an optional prefix for filtering groups. It also sets up logging and initializes counters to track the number of assignments processed, succeeded, and failed.
param (
[string]$Prefix = "XYZ-App",
[string]$ClientId = "your-client-id",
[string]$TenantId = "your-tenant-id"
)
# Define the log file path
$LogFilePath = "C:\Temp\IntuneAssignmentScript.log"
# Initialize counters
$processedCount = 0
$successCount = 0
$failureCount = 0
A Log-Message
function is used to log messages to both the console and a log file. This function helps in tracking the script’s execution and handling errors.
function Log-Message {
param (
[string]$Message,
[string]$Level = "INFO"
)
$timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
$logEntry = "$timestamp [$Level] $Message"
Add-Content -Path $LogFilePath -Value $logEntry
if ($Level -eq "ERROR") {
Write-Error $Message
} else {
Write-Host $Message
}
}
The Get-AuthToken
function authenticates with the Graph API using the provided tenant and user credentials. It handles token expiration and re-authentication if necessary.
function Get-AuthToken {
param (
[Parameter(Mandatory = $true)]
[string]$User
)
# Authentication logic...
# Check if the token has expired
$DateTime = (Get-Date).ToUniversalTime()
$TokenExpires = [datetime]$authToken.ExpiresOn.UtcDateTime - $DateTime
if ($TokenExpires.TotalMinutes -le 0) {
Log-Message "Authentication Token expired $($TokenExpires.TotalMinutes) minutes ago" "ERROR"
$global:MyCreds = Get-Credential -Message 'Enter specify UPN for Azure authentication:'
$global:authToken = Get-AuthToken -User $global:MyCreds.UserName
}
}
The script retrieves groups and apps from Intune, displaying them in interactive GridView dialogs for selection. This makes it easy to select the appropriate entities for assignment.
# Get the group
$selectedGroup = Select-Group -Prefix $Prefix
if ($selectedGroup) {
$GroupId = $selectedGroup.Id
$GroupName = $selectedGroup.DisplayName
Log-Message "Selected Group ID: $GroupId"
Log-Message "Selected Group Name: $GroupName"
} else {
Log-Message "No group selected." "ERROR"
Return
}
# Get the apps
Log-Message "Getting Apps from Intune. Be patient, this can take a while."
$apps = Get-MgBetaDeviceAppMgtMobileApp -ExpandProperty Assignments | select DisplayName, Id | Sort-Object
$selectedApps = $apps | Out-GridView -PassThru -Title "Select App(s) You Want to Assign:"
Depending on the selected deployment type (include or exclude), the script creates the appropriate assignment request body. It then sends the request to the Graph API and logs the result.
# Create the request body depending upon whether it will be an include or exclude assignment.
if ($selectedChoice -eq 'Exclude') {
# Define the request body template for exclude assignment
Log-Message "Defining body template for Graph calls to exclude group."
$bodyTemplate = @{
target = @{
groupId = $GroupId
"@odata.type" = "microsoft.graph.exclusionGroupAssignmentTarget"
}
intent = "Required"
"@odata.type" = "#microsoft.graph.mobileAppAssignment"
}
}
if ($selectedChoice -eq 'Include') {
# Define the request body template for include assignment
Log-Message "Defining body template for Graph calls to include group."
$bodyTemplate = @{
target = @{
groupId = $GroupId
"@odata.type" = "#microsoft.graph.groupAssignmentTarget"
deviceAndAppManagementAssignmentFilterId = $FilterId
deviceAndAppManagementAssignmentFilterType = $FilterType
}
intent = "Required"
settings = @{
"@odata.type" = "#microsoft.graph.win32LobAppAssignmentSettings"
notifications = "showReboot"
installTimeSettings = $null
restartSettings = @{
"@odata.type" = "#microsoft.graph.win32LobAppRestartSettings"
gracePeriodInMinutes = "720"
countdownDisplayBeforeRestartInMinutes = "90"
restartNotificationSnoozeDurationInMinutes = "15"
}
deliveryOptimizationPriority = "notConfigured"
}
}
}
# Send the POST request
foreach ($app in $selectedApps) {
$Id = $app.Id
$AppName = $app.DisplayName
# Define the API URL with the variable for the assignment ID
$apiUrl = "https://graph.microsoft.com/beta/deviceAppManagement/mobileApps/$Id/assignments"
Log-Message "API URL: $apiUrl"
try {
$response = Invoke-WebRequest -Uri $apiUrl -Headers $headers -Method Post -Body $jsonBody -ContentType "application/json"
$responseContent = $response.Content | ConvertFrom-Json
Log-Message "Response Content: $($responseContent | ConvertTo-Json -Depth 3)"
# Success message
if ($selectedChoice -eq 'Include') {
if ($FilterId) {
# Line break added for readability on the blog. Feel free to remove it.
Log-Message "The $GroupName group has been successfully assigned to the app $AppName as an included group using the `
filter $FilterName at $DateTime."
} else {
Log-Message "The $GroupName group has been successfully assigned to the app $AppName as an included group at $DateTime."
}
} else {
Log-Message "The $GroupName group has been successfully assigned to the app $AppName as an excluded group at $DateTime."
}
$processedCount++
$successCount++
}
catch {
Log-Message "Error: $_" "ERROR"
if ($_.Exception.Response) {
$respStream = $_.Exception.Response.GetResponseStream()
$reader = New-Object System.IO.StreamReader($respStream)
$responseBody = $reader.ReadToEnd() | ConvertFrom-Json
Log-Message "Response Body: $($responseBody | ConvertTo-Json -Depth 3)" "ERROR"
if ($responseBody.error.message -match "The MobileApp Assignment already exists") {
Log-Message "The MobileApp Assignment already exists for AppId: $Id and AssignmentId: $GroupId" "ERROR"
} else {
Log-Message "An error occurred for AppId: $($Id): $($responseBody.error.message)" "ERROR"
}
}
$failureCount++
}
}
# Write a summary message after the loop completes
$summaryMessage = "$processedCount assignments have been processed: $successCount succeeded, $failureCount failed."
Write-Host $summaryMessage
Log-Message $summaryMessage
This PowerShell script provides a robust and automated way to manage Intune app assignments. By leveraging the Microsoft Graph API and interactive GridView dialogs, it simplifies the process of including or excluding groups from app deployments. The built-in logging and error handling ensure that administrators can easily track the script’s actions and address any issues that arise.
For your reference, here’s the complete script:
<#
.SYNOPSIS
Adds a new inclusion or exclusion assignment to a list of Intune Apps without removing the existing assignments.
.DESCRIPTION
This script adds a new inclusion or exclusion assignment to a list of Intune Apps in Microsoft Intune without removing existing assignments.
It allows you to authenticate with Azure AD, select apps from Intune, and select a group to assign or exclude from the app deployment.
Some of the include assignment properties are hardcoded because I was tired of working on this. Feel free to improve upon my work.
.PARAMETER GroupPrefix
Optional parameter to filter groups by their display name prefix.
.PARAMETER AppPrefix
Optional parameter to filter Apps by their display name prefix.
.PARAMETER ClientId
The client ID used for Graph authentication.
.PARAMETER TenantId
The Azure AD tenant ID.
.EXAMPLE
.\Add-IntuneAssignment.ps1 -Prefix "642-App" -ClientId "your-client-id" -TenantId "your-tenant-id"
.EXAMPLE
.\Add-IntuneAssignment.ps1
Without specifying parameters (uses default values).
.NOTES
The script requires the Microsoft.Graph and AzureAD modules.
Script Name: Add-IntuneAssignment.ps1
Author: John Marcum (PJM)
Date: 6/19/2024
Version: 2.0
6/26/2024: Removed the reliance on Azure AD module. - PJM
# LEGAL DISCLAIMER
# This script is provided "as is" without any warranty of any kind, either express or implied, including but not limited to the implied warranties of merchantability, fitness for a particular purpose, or non-infringement. The entire risk as to the quality and performance of the script is with you.
# In no event shall the authors or copyright holders be liable for any claim, damages, or other liability, whether in an action of contract, tort, or otherwise, arising from, out of, or in connection with the script or the use or other dealings in the script.
# You should never run any script from the Internet without understanding its contents and effects. It is highly recommended that you thoroughly test the script in a safe environment before running it in production.
#>
param (
[string]$GroupPrefix = "123-",
[string]$AppPrefix = "TEST-",
[string]$ClientId = "",
[string]$TenantId = ""
)
#### Begin Setting Variables ####
# Define the ID of the app used for Graph authentication (If required)
$clientId = $ClientId
# Define the Azure AD tenant ID (If required)
$TenantId = $TenantId
# Optionally define a prefix to filter the group names from the selection dialog box.
$GroupPrefix = $GroupPrefix
# Define the log file path
$LogFile = "c:\temp\IntuneAssignmentScript-$(Get-Date -UFormat "%m-%d-%Y_%H-%m").log"
# Initialize counters to track the number of assignments processed, succeeded, and failed
$processedCount = 0
$successCount = 0
$failureCount = 0
#### End Setting Variables ####
######## Begin Functions ########
####################################################
# Function to log messages to a log file and to the screen
function Log-Message {
param (
[string]$Message,
[string]$Level = "INFO"
)
$timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
$logEntry = "$timestamp [$Level] $Message"
Add-Content -Path $LogFile -Value $logEntry
switch ($Level) {
"ERROR" { Write-Error $Message }
default { Write-Host $Message }
}
}
####################################################
# Function to get the group from Graph.
function Select-Group {
<#
.SYNOPSIS
Retrieves groups from Graph and allows selection via GridView.
.DESCRIPTION
This function retrieves groups from Azure AD based on an optional prefix and displays them in a GridView for selection.
.PARAMETER GroupPrefix
The prefix to filter groups by their display name.
#>
param (
[string]$GroupPrefix
)
Log-Message $GroupPrefix
if ($GroupPrefix) {
Log-Message "Getting groups starting with $($GroupPrefix)"
$Groups = Get-MgGroup -Filter "startswith(DisplayName, '$GroupPrefix')" -Property DisplayName, Id
}
else {
Log-Message "Getting all groups"
$Groups = Get-MgGroup -Property DisplayName, Id
}
if ($Groups.Count -eq 0) {
Log-Message "No groups found with the given prefix." "ERROR"
return $null
}
# Customizing the output to display only DisplayName
$TargetGroup = $Groups.DisplayName | Out-GridView -Title "Select a Single Group:" -OutputMode Single
# Retrieve the Id based on the selected DisplayName
$GroupId = $Groups | Where-Object { $_.DisplayName -eq $TargetGroup } | Select-Object -ExpandProperty Id
if ($TargetGroup -and $GroupId) {
Log-Message "Target Group Name: $($TargetGroup)"
Log-Message "Target Group ID: $($GroupId)"
return [PSCustomObject]@{
DisplayName = $TargetGroup
Id = $GroupId
}
}
else {
Log-Message "No group selected. Please select a group to proceed." "ERROR"
return $null
}
}
####################################################
# Function to ensure modules are installed and updated
function Assert-ModuleExists {
<#
.SYNOPSIS
Ensures the specified module is installed and up to date.
.DESCRIPTION
This function checks if a specified module is installed and up to date. If not, it installs or updates the module.
.PARAMETER ModuleName
The name of the module to check, install, or update.
#>
param (
[string]$ModuleName
)
$installedModule = Get-Module -Name $ModuleName -ListAvailable | Sort-Object Version -Descending | Select-Object -First 1
$latestModule = Find-Module -Name $ModuleName | Sort-Object Version -Descending | Select-Object -First 1
if ($installedModule) {
if ($latestModule) {
if ($installedModule.Version -lt $latestModule.Version) {
Log-Message "Updating module $ModuleName ..."
Update-Module -Name $ModuleName -Force
Log-Message "Module updated to version $($latestModule.Version)"
}
else {
Log-Message "Module $ModuleName is already up to date."
}
}
else {
Log-Message "Module $ModuleName is not found in the repository." "ERROR"
}
}
else {
Log-Message "Installing module $ModuleName ..."
Install-Module -Name $ModuleName -Force
Log-Message "Module installed"
}
}
####################################################
# Function to select an option from a list
function Select-Option {
<#
.SYNOPSIS
Displays a list of choices in a GridView for user selection.
.DESCRIPTION
This function displays a list of choices in a GridView for user selection and returns the selected choice.
.PARAMETER Choices
The list of choices to display.
.PARAMETER Title
The title of the GridView window.
#>
param (
[string[]]$Choices,
[string]$Title
)
$selectedChoice = $Choices | Out-GridView -Title $Title -OutputMode Single -ErrorAction SilentlyContinue
if ($selectedChoice) {
return $selectedChoice
}
else {
Log-Message "No option selected. Please select an option." "ERROR"
return $null
}
}
####################################################
######## End Functions ########
######## Script Entry Point ########
# Install required modules
Log-Message "Checking whether Microsoft.Graph.Beta.Groups module is installed"
Assert-ModuleExists -ModuleName 'Microsoft.Graph.Beta.Groups'
Log-Message "Checking whether Microsoft.Graph.Authentication module is installed"
Assert-ModuleExists -ModuleName 'Microsoft.Graph.Authentication'
Log-Message "Checking whether Microsoft.Graph.Devices.CorporateManagement is installed"
Assert-ModuleExists -ModuleName 'Microsoft.Graph.Devices.CorporateManagement'
# Connect to Graph
$context = Get-MgContext
if (!($context)) {
Log-Message "Connect to Graph."
Connect-MgGraph -ClientId $clientId -TenantId $TenantId -NoWelcome
}
Log-Message "Connected to Graph"
Log-Message "Scopes: $($context.Scopes)"
# Get the app(s) from Intune
Log-Message "Getting Apps from Intune. Be patient, this can take a while."
if ([string]::IsNullOrEmpty($AppPrefix)) {
$Apps = Get-MgBetaDeviceAppMgtMobileApp -ExpandProperty Assignments | select DisplayName, Id | Sort-Object
}
else {
$Apps = Get-MgBetaDeviceAppMgtMobileApp -Filter "startswith(DisplayName, '$($AppPrefix)')" -ExpandProperty Assignments | select DisplayName, Id | Sort-Object
}
if ($Apps.Count -eq 0) {
Log-Message "No apps found with the given prefix." "ERROR"
}
else {
Log-Message "Apps retrieved successfully."
}
$selectedApps = $apps | Out-GridView -PassThru -Title "Select App(s) You Want to Assign:"
$appIds = $selectedApps | select -ExpandProperty Id
if (!($appIds)) {
Log-Message "No apps selected!" "ERROR"
Return
}
# Get groups and display them in GridView for user selection
Log-Message "Getting groups from Graph"
# Get the group
$selectedGroup = Select-Group -GroupPrefix $GroupPrefix
if ($selectedGroup) {
$GroupId = $selectedGroup.Id
$GroupName = $selectedGroup.DisplayName
Log-Message "Selected Group ID: $GroupId"
Log-Message "Selected Group Name: $GroupName"
}
else {
Log-Message "No group selected." "ERROR"
Return
}
# Determine the assignment type
$AssignmentChoices = "Include", "Exclude"
$selectedChoice = Select-Option -Choices $AssignmentChoices -Title "Select Deployment Type:"
if (-not $selectedChoice) {
Log-Message "No option selected. Please select either 'Include' or 'Exclude'." "ERROR"
Return
}
# Set the variable based on the selected choice
if ($selectedChoice -eq "Include") {
$DeployType = "Included"
}
elseif ($selectedChoice -eq "Exclude") {
$DeployType = "Excluded"
}
# Output the selected group and deployment type
Log-Message "The selected group: $GroupId will be $DeployType"
# Only prompt for filter if the deployment type is Include
$FilterId = $null
$FilterType = $null
$FilterName = $null
if ($selectedChoice -eq 'Include') {
# Make the API request to get assignment filters
$filtersResponse = Invoke-MgGraphRequest -Uri "https://graph.microsoft.com/beta/deviceManagement/assignmentFilters" -Method Get
# Check the content of the response
if ($filtersResponse -and $filtersResponse.value) {
$filters = $filtersResponse.value
# Extract DisplayName and Id from the hashtables
$extractedFilters = $filters | ForEach-Object {
[PSCustomObject]@{
DisplayName = $_.DisplayName
Id = $_.Id
}
}
# Debugging: Output the extracted filters data
$extractedFilters | ForEach-Object { Write-Host "DisplayName: $($_.DisplayName), Id: $($_.Id)" }
# Select DisplayName and Id properties and pipe to Out-GridView
$selectedFilter = $extractedFilters | Out-GridView -Title "Select an Assignment Filter (Optional):" -OutputMode Single
# Debugging: Check if a filter was selected
if ($selectedFilter) {
Log-Message "Selected Filter DisplayName: $($selectedFilter.DisplayName)"
Log-Message "Selected Filter Id: $($selectedFilter.Id)"
}
else {
Log-Message "No filter type selected. Please select a filter type." "ERROR"
}
}
else {
Log-Message "No filters found or API request failed." "ERROR"
}
if ($selectedFilter) {
$FilterId = $selectedFilter.Id
$FilterName = $selectedFilter.DisplayName
Log-Message "Selected Filter ID: $FilterId"
Log-Message "Selected Filter Name: $FilterName"
# Determine the filter type
$FilterTypeChoices = "Include", "Exclude"
$FilterType = Select-Option -Choices $FilterTypeChoices -Title "Select Filter Type:"
if (-not $FilterType) {
Return
}
}
}
# Create the request body depending upon whether it will be an include or exclude assignment.
if ($selectedChoice -eq 'Exclude') {
# Define the request body template for exclude assignment
Log-Message "Defining body template for Graph calls to exclude group."
$bodyTemplate = @{
target = @{
groupId = $GroupId
"@odata.type" = "microsoft.graph.exclusionGroupAssignmentTarget"
}
intent = "Required"
"@odata.type" = "#microsoft.graph.mobileAppAssignment"
}
}
if ($selectedChoice -eq 'Include') {
# Define the request body template for include assignment
Log-Message "Defining body template for Graph calls to include group."
$bodyTemplate = @{
target = @{
groupId = $GroupId
"@odata.type" = "#microsoft.graph.groupAssignmentTarget"
deviceAndAppManagementAssignmentFilterId = $FilterId
deviceAndAppManagementAssignmentFilterType = $FilterType
}
intent = "Required"
settings = @{
"@odata.type" = "#microsoft.graph.win32LobAppAssignmentSettings"
notifications = "showReboot"
installTimeSettings = $null
restartSettings = @{
"@odata.type" = "#microsoft.graph.win32LobAppRestartSettings"
gracePeriodInMinutes = "720"
countdownDisplayBeforeRestartInMinutes = "90"
restartNotificationSnoozeDurationInMinutes = "15"
}
deliveryOptimizationPriority = "notConfigured"
}
}
}
# Convert the body template to JSON
$jsonBody = $bodyTemplate | ConvertTo-Json -Depth 3
# Debug: Print Body, and JSON Body
Log-Message "Body: $bodyTemplate"
Log-Message "JSON Body: $jsonBody"
# Loop through each app ID
foreach ($app in $selectedApps) {
$Id = $app.Id
$AppName = $app.DisplayName
# Define the API URL with the variable for the assignment ID
$apiUrl = "https://graph.microsoft.com/beta/deviceAppManagement/mobileApps/$Id/assignments"
# Debug: Print API URL
Log-Message "API URL: $apiUrl"
# Send the POST request
try {
$response = Invoke-MgGraphRequest -Uri $apiUrl -Body $jsonBody -Method POST -ContentType "application/json"
# Output the response
Log-Message "Response Content: $($response | ConvertTo-Json -Depth 3)"
# Success message
if ($selectedChoice -eq 'Include') {
if ($FilterId) {
Log-Message "The $GroupName group has been successfully assigned to the app $AppName as an included group using the filter $FilterName at $DateTime."
}
else {
Log-Message "The $GroupName group has been successfully assigned to the app $AppName as an included group at $DateTime."
}
}
else {
Log-Message "The $GroupName group has been successfully assigned to the app $AppName as an excluded group at $DateTime."
}
# Increment the counter
$processedCount++
$successCount++
}
catch {
# Capture and output the error details
Log-Message "Error: $($_.Exception.Message)" "ERROR"
if ($_.Exception.Response) {
$responseBody = $null
try {
$responseStream = $_.Exception.Response.RawContentStream
$reader = New-Object System.IO.StreamReader($responseStream)
$responseBody = $reader.ReadToEnd() | ConvertFrom-Json
}
catch {
Log-Message "Failed to parse error response content." "ERROR"
}
if ($responseBody) {
$errorMessage = $responseBody.error.message
Log-Message "Response Body: $($responseBody | ConvertTo-Json -Depth 3)" "ERROR"
# Check if the error indicates that the assignment already exists
if ($errorMessage -match "The MobileApp Assignment already exists") {
Log-Message "The MobileApp Assignment already exists for AppName: $($AppName) and AssignmentId: $GroupId" "ERROR"
}
else {
Log-Message "An error occurred for AppName: $($AppName): $errorMessage" "ERROR"
}
}
else {
Log-Message "Error: Unable to parse error response content." "ERROR"
}
}
else {
Log-Message "Error: No response received." "ERROR"
}
# Increment the failure counter
$failureCount++
}
}
# Write a message after the loop completes
$summaryMessage = "$processedCount assignments have been processed: $successCount succeeded, $failureCount failed."
Log-Message $summaryMessage
Cookie | Duration | Description |
---|---|---|
cookielawinfo-checkbox-analytics | 11 months | This cookie is set by GDPR Cookie Consent plugin. The cookie is used to store the user consent for the cookies in the category "Analytics". |
cookielawinfo-checkbox-functional | 11 months | The cookie is set by GDPR cookie consent to record the user consent for the cookies in the category "Functional". |
cookielawinfo-checkbox-necessary | 11 months | This cookie is set by GDPR Cookie Consent plugin. The cookies is used to store the user consent for the cookies in the category "Necessary". |
cookielawinfo-checkbox-others | 11 months | This cookie is set by GDPR Cookie Consent plugin. The cookie is used to store the user consent for the cookies in the category "Other. |
cookielawinfo-checkbox-performance | 11 months | This cookie is set by GDPR Cookie Consent plugin. The cookie is used to store the user consent for the cookies in the category "Performance". |
viewed_cookie_policy | 11 months | The cookie is set by the GDPR Cookie Consent plugin and is used to store whether or not user has consented to the use of cookies. It does not store any personal data. |