IntuneAppAssigner.ps1

<#PSScriptInfo
 
.VERSION 0.5.4
.GUID 71c3b7d1-f435-4f11-b7c0-4acf00b7daca
.AUTHOR Nick Benton
.COMPANYNAME
.COPYRIGHT GPL
.TAGS Graph Intune Windows Android iOS macOS Apps
.LICENSEURI https://github.com/ennnbeee/IntuneAppAssigner/blob/main/LICENSE
.PROJECTURI https://github.com/ennnbeee/IntuneAppAssigner
.ICONURI https://raw.githubusercontent.com/ennnbeee/IntuneAppAssigner/refs/heads/main/img/iaa-icon.png
.EXTERNALMODULEDEPENDENCIES Microsoft.Graph.Authentication Microsoft.PowerShell.ConsoleGuiTools
.REQUIREDSCRIPTS
.EXTERNALSCRIPTDEPENDENCIES
.RELEASENOTES
v0.5.4 - Updated authentication logic to allow for existing graph sessions
v0.5.3 - Bug fixes
v0.5.2 - Code review and optimizations.
v0.5.1 - Updated error handling for Graph API connection.
v0.5.0 - Support for Apple VPP apps.
v0.4.4 - Logic improvements for App Config, assignment intents, and bug fixes
v0.4.3 - Option to export app assignments
v0.4.2 - Logic improvements
v0.4.1 - Bug fixes
v0.4.0 - Updated to include assignment review mode and uninstall intent
v0.3.0 - Support for Windows apps
v0.2.1 - Bug Fixes
v0.2.0 - Supports macOS apps
v0.1.3 - Improvements to App Config creation logic.
v0.1.2 - Bug Fixes
v0.1.1 - Allow for creation of App Config policies.
v0.1.0 - Initial release.
 
.PRIVATEDATA
#>


<#
.SYNOPSIS
Allows for bulk assignment changes to Intune apps.
 
.DESCRIPTION
The IntuneAppAssigner script is a PowerShell tool designed to facilitate the bulk assignment of mobile applications within Microsoft Intune.
It provides an interactive interface for administrators to select applications, define assignment parameters, and apply these settings across user and device groups efficiently.
 
.PARAMETER appConfigPrefix
Used to specify the prefix of the Android or iOS/iPadOS App Config policies, if not configured no prefix is used.
 
.PARAMETER tenantId
Provide the Id of the Entra ID tenant to connect to.
 
.PARAMETER appId
Provide the Id of the Entra App registration to be used for authentication.
 
.PARAMETER appSecret
Provide the App secret to allow for authentication to graph
 
.EXAMPLE
Interactive Authentication
.\IntuneAppAssigner.ps1
 
.EXAMPLE
Pass through Authentication
.\IntuneAppAssigner.ps1 -tenantId '437e8ffb-3030-469a-99da-e5b527908099'
 
.EXAMPLE
App Authentication
.\IntuneAppAssigner.ps1 -tenantId '437e8ffb-3030-469a-99da-e5b527908099' -appId '799ebcfa-ca81-4e72-baaf-a35126464d67' -appSecret 'g708Q~uof4xo9dU_1EjGQIuUr0UyBHNZmY2m3dy6'
 
#>


[CmdletBinding(DefaultParameterSetName = 'Default')]

param(

    [Parameter(Mandatory = $false, HelpMessage = 'Provide the Id of the Entra ID tenant to connect to')]
    [ValidateLength(36, 36)]
    [String]$tenantId,

    [Parameter(Mandatory = $false, ParameterSetName = 'appAuth', HelpMessage = 'Provide the Id of the Entra App registration to be used for authentication')]
    [ValidateLength(36, 36)]
    [String]$appId,

    [Parameter(Mandatory = $true, ParameterSetName = 'appAuth', HelpMessage = 'Provide the App secret to allow for authentication to graph')]
    [ValidateNotNullOrEmpty()]
    [String]$appSecret,

    [Parameter(Mandatory = $false, HelpMessage = 'Specify an optional profile name prefix for Android and iOS App Config policies')]
    [String]$appConfigPrefix

)

#region Functions
function Connect-ToGraph {

    <#
.SYNOPSIS
Authenticates to the Graph API via the Microsoft.Graph.Authentication module.
 
.DESCRIPTION
The Connect-ToGraph cmdlet is a wrapper cmdlet that helps authenticate to the Intune Graph API using the Microsoft.Graph.Authentication module. It leverages an Azure AD app ID and app secret for authentication or user-based auth.
 
.PARAMETER TenantId
Specifies the tenantId from Entra ID to which to authenticate.
 
.PARAMETER AppId
Specifies the Azure AD app ID (GUID) for the application that will be used to authenticate.
 
.PARAMETER AppSecret
Specifies the Azure AD app secret corresponding to the app ID that will be used to authenticate.
 
.PARAMETER Scopes
Specifies the user scopes for interactive authentication.
 
.EXAMPLE
Connect-ToGraph -tenantId $tenantId -appId $app -appSecret $secret
 
#>


    [cmdletbinding()]
    param
    (
        [Parameter(Mandatory = $false)] [string]$tenantId,
        [Parameter(Mandatory = $false)] [string]$appId,
        [Parameter(Mandatory = $false)] [string]$appSecret,
        [Parameter(Mandatory = $false)] [string[]]$scopes
    )

    process {
        Import-Module Microsoft.Graph.Authentication
        $version = (Get-Module microsoft.graph.authentication | Select-Object -ExpandProperty Version).major

        if ($null -ne $appId -or $appId -ne '') {
            $body = @{
                grant_type    = 'client_credentials';
                client_id     = $appId;
                client_secret = $appSecret;
                scope         = 'https://graph.microsoft.com/.default';
            }

            $response = Invoke-RestMethod -Method Post -Uri "https://login.microsoftonline.com/$tenantId/oauth2/v2.0/token" -Body $body
            $accessToken = $response.access_token

            if ($version -eq 2) {
                Write-Host 'Version 2 module detected'
                $accessTokenFinal = ConvertTo-SecureString -String $accessToken -AsPlainText -Force
            }
            else {
                Write-Host 'Version 1 Module Detected'
                Select-MgProfile -Name Beta
                $accessTokenFinal = $accessToken
            }
            $graph = Connect-MgGraph -AccessToken $accessTokenFinal
            Write-Host "Connected to Intune tenant $TenantId using app-based authentication (Azure AD authentication not supported)"
        }
        else {
            if ($version -eq 2) {
                Write-Host 'Version 2 module detected'
            }
            else {
                Write-Host 'Version 1 Module Detected'
                Select-MgProfile -Name Beta
            }
            $graph = Connect-MgGraph -TenantId $tenantId -Scopes $scopes
            Write-Host "Connected to Intune tenant $($graph.TenantId)"
        }
    }
}
function Test-JSONData {

    <#
    .SYNOPSIS
    Validates JSON data format.
 
    .DESCRIPTION
    The Test-JSONData function checks if the provided JSON string is in a valid format.
 
    .PARAMETER JSON
    Specifies the JSON string to validate.
 
    .EXAMPLE
    Test-JSONData -JSON '{"key": "value"}'
    #>


    param (
        $JSON
    )

    try {
        $TestJSON = ConvertFrom-Json $JSON -ErrorAction Stop
        $TestJSON | Out-Null
        $validJson = $true
    }
    catch {
        $validJson = $false
        $_.ErrorDetails.Message
    }
    if (!$validJson) {
        Write-Host "Provided JSON isn't in valid JSON format" -ForegroundColor Red
        throw
    }

}
function Get-MobileApp() {

    <#
    .SYNOPSIS
    Allows for searching for mobile apps or getting mobile app information from Intune.
 
    .DESCRIPTION
    This function allows for searching for mobile apps or getting mobile app information from Intune.
 
    .PARAMETER Id
    Specifies the Id of the mobile app to retrieve. If not provided, all mobile apps will be returned.
 
    #>


    [cmdletbinding()]

    param (
        $Id
    )

    $graphApiVersion = 'Beta'
    if ($null -ne $Id) {
        $resource = "deviceAppManagement/mobileApps('$Id')"
    }
    else {
        $resource = 'deviceAppManagement/mobileApps'
    }

    try {
        $uri = "https://graph.microsoft.com/$graphApiVersion/$($resource)"

        if ($null -ne $Id) {
            Invoke-MgGraphRequest -Uri $uri -Method Get -OutputType PSObject
        }
        else {
            (Invoke-MgGraphRequest -Uri $uri -Method Get -OutputType PSObject).Value
        }
    }
    catch {
        Write-Host "❌ Graph request to $uri failed" -ForegroundColor Red
        if ($_.ErrorDetails -and $_.ErrorDetails.Message) {
            Write-Host $_.ErrorDetails.Message -ForegroundColor Red
        }
        else {
            Write-Host $_.Exception.Message -ForegroundColor Red
        }
        throw
    }
}
function Get-AssignmentFilter() {

    <#
    .SYNOPSIS
    Allows for getting assignment filters from Intune.
 
    .DESCRIPTION
    This function allows for getting assignment filters from Intune.
 
    .PARAMETER Id
    Specifies the Id of the assignment filter to retrieve. If not provided, all assignment filters will be returned.
 
    #>


    param
    (

        [parameter(Mandatory = $false)]
        [string]$Id
    )

    $graphApiVersion = 'beta'

    try {
        if ($Id) {
            $resource = "deviceManagement/assignmentFilters/$Id"
            $uri = "https://graph.microsoft.com/$graphApiVersion/$($resource)"
            Invoke-MgGraphRequest -Uri $uri -Method Get -OutputType PSObject
        }
        else {
            $resource = 'deviceManagement/assignmentFilters'
            $uri = "https://graph.microsoft.com/$graphApiVersion/$($resource)"
            (Invoke-MgGraphRequest -Uri $uri -Method Get -OutputType PSObject).Value
        }

    }
    catch {
        Write-Host "❌ Graph request to $uri failed" -ForegroundColor Red
        if ($_.ErrorDetails -and $_.ErrorDetails.Message) {
            Write-Host $_.ErrorDetails.Message -ForegroundColor Red
        }
        else {
            Write-Host $_.Exception.Message -ForegroundColor Red
        }
        throw
    }
}
function Get-MDMGroup() {

    <#
    .SYNOPSIS
    Allows for searching for groups or getting group information from Entra ID.
 
    .DESCRIPTION
    This function allows for searching for groups or getting group information from Entra ID.
 
    .PARAMETER groupName
    Specifies a search term for the group name. If not provided, all groups will be returned.
 
    .PARAMETER Id
    Specifies the Id of the group to retrieve. If not provided, all groups apps will be returned.
 
 
    #>


    [cmdletbinding()]

    param
    (
        [parameter(Mandatory = $false)]
        [string]$groupName,

        [parameter(Mandatory = $false)]
        [string]$Id
    )

    $graphApiVersion = 'beta'
    $resource = 'groups'

    try {
        if ($groupName) {
            $searchTerm = 'search="displayName:' + $groupName + '"'
            $uri = "https://graph.microsoft.com/$graphApiVersion/$resource`?$searchTerm"
        }
        elseif ($Id) {
            $uri = "https://graph.microsoft.com/$graphApiVersion/$resource/$Id"
        }
        else {
            $uri = "https://graph.microsoft.com/$graphApiVersion/$resource"
        }

        $graphResults = Invoke-MgGraphRequest -Uri $uri -Method Get -Headers @{ConsistencyLevel = 'eventual' } -OutputType PSObject

        if ($null -ne $graphResults.value) {
            $results = @()
            $results += $graphResults.value

            $pages = $graphResults.'@odata.nextLink'
            while ($null -ne $pages) {

                $additional = Invoke-MgGraphRequest -Uri $pages -Method Get -Headers @{ConsistencyLevel = 'eventual' } -OutputType PSObject

                if ($pages) {
                    $pages = $additional.'@odata.nextLink'
                }
                $results += $additional.value
            }
            $results
        }
        else {
            $graphResults
        }

    }
    catch {
        Write-Host "❌ Graph request to $uri failed" -ForegroundColor Red
        if ($_.ErrorDetails -and $_.ErrorDetails.Message) {
            Write-Host $_.ErrorDetails.Message -ForegroundColor Red
        }
        else {
            Write-Host $_.Exception.Message -ForegroundColor Red
        }
        throw
    }
}
function Get-AppAssignment() {

    <#
    .SYNOPSIS
    Allows for getting app assignments from Intune.
 
    .DESCRIPTION
    This function allows for getting app assignments from Intune.
 
    .PARAMETER Id
    Specifies the Id of the mobile app to retrieve assignments for.
 
    #>


    [cmdletbinding()]

    param
    (
        [parameter(Mandatory = $true)]
        $Id
    )

    $graphApiVersion = 'Beta'
    $resource = "deviceAppManagement/mobileApps/$Id/?`$expand=categories,assignments"

    try {
        $uri = "https://graph.microsoft.com/$graphApiVersion/$($resource)"
        (Invoke-MgGraphRequest -Uri $uri -Method Get -OutputType PSObject)
    }
    catch {
        Write-Host "❌ Graph request to $uri failed" -ForegroundColor Red
        if ($_.ErrorDetails -and $_.ErrorDetails.Message) {
            Write-Host $_.ErrorDetails.Message -ForegroundColor Red
        }
        else {
            Write-Host $_.Exception.Message -ForegroundColor Red
        }
        throw
    }
}
function Remove-AppAssignment() {

    <#
    .SYNOPSIS
    Allows for removing app assignments from Intune.
 
    .DESCRIPTION
    This function allows for removing app assignments from Intune.
 
    .PARAMETER Id
    Specifies the Id of the mobile app to remove the assignment from.
 
    .PARAMETER AssignmentId
    Specifies the Id of the assignment to remove.
 
    #>


    [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'low')]

    param
    (
        [parameter(Mandatory = $true)]
        $Id,
        [parameter(Mandatory = $true)]
        $AssignmentId
    )

    $graphApiVersion = 'Beta'
    $resource = "deviceAppManagement/mobileApps/$Id/assignments/$AssignmentId"

    if ($PSCmdlet.ShouldProcess('Removing App Assignment')) {
        try {
            $uri = "https://graph.microsoft.com/$graphApiVersion/$($resource)"
            (Invoke-MgGraphRequest -Uri $uri -Method Delete)
            Write-Host '✅ Successfully removed App assignment' -ForegroundColor Green
        }
        catch {
            Write-Host "❌ Graph request to $uri failed" -ForegroundColor Red
            if ($_.ErrorDetails -and $_.ErrorDetails.Message) {
                Write-Host $_.ErrorDetails.Message -ForegroundColor Red
            }
            else {
                Write-Host $_.Exception.Message -ForegroundColor Red
            }
            throw
        }
    }
    elseif ($WhatIfPreference.IsPresent) {
        Write-Output 'App Assignment would have been removed'
    }
    else {
        Write-Output 'App assignment not removed'
    }
}
function Add-AppAssignment() {

    <#
    .SYNOPSIS
    Allows for adding app assignments to Intune.
 
    .DESCRIPTION
    This function allows for adding app assignments to Intune.
 
    .PARAMETER applicationType
    Specifies the type of App to be assigned, accepts the '#microsoft.graph' mobile app types, e.g. '#microsoft.graph.iosLobApp', '#microsoft.graph.androidStoreApp', etc.
 
    .PARAMETER Id
    Specifies the Id of the mobile app to add the assignment to.
 
    .PARAMETER targetGroupId
    Specifies the Id of the group to assign the app to.
 
    .PARAMETER installIntent
    Specifies the install intent for the app assignment. Valid values are 'Available', 'Required', or 'Uninstall'.
 
    .PARAMETER filterID
    Specifies the Id of the assignment filter to apply to the assignment.
 
    .PARAMETER filterMode
    Specifies the filter mode for the assignment. Valid values are 'Include' or 'Exclude'.
 
    .PARAMETER all
    Specifies if the app should be assigned to all users or all devices. Valid values are 'Users' or 'Devices'.
 
    .PARAMETER action
    Specifies the action to take when adding the assignment. Valid values are 'Replace' or 'Add'.
 
 
    #>


    [cmdletbinding()]

    param
    (

        [parameter(Mandatory = $false)]
        [ValidateNotNullOrEmpty()]
        $applicationType,

        [parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        $Id,

        [parameter(Mandatory = $false)]
        [ValidateNotNullOrEmpty()]
        $targetGroupId,

        [parameter(Mandatory = $true)]
        [ValidateSet('Available', 'Required', 'Uninstall')]
        [ValidateNotNullOrEmpty()]
        $installIntent,

        [parameter(Mandatory = $false)]
        $filterID,

        [ValidateSet('Include', 'Exclude')]
        $filterMode,

        [parameter(Mandatory = $false)]
        [ValidateSet('Users', 'Devices')]
        [ValidateNotNullOrEmpty()]
        $all,

        [parameter(Mandatory = $true)]
        [ValidateSet('Replace', 'Add')]
        $action
    )

    $graphApiVersion = 'beta'
    $resource = "deviceAppManagement/mobileApps/$Id/assign"
    $additionalAssignmentSettings = @('#microsoft.graph.iosVppApp', '#microsoft.graph.iosStoreApp', '#microsoft.graph.iosLobApp', '#microsoft.graph.macOsVppApp')
    $removeExistingAssignments = @('#microsoft.graph.iosVppApp', '#microsoft.graph.macOsVppApp')
    try {
        $targetGroups = @()
        $assignmentContinue = $true
        if ($action -eq 'Add') {
            $appDetails = Get-AppAssignment -Id $Id
            $assignments = $appDetails.assignments
            if (@($assignments).count -ge 1) {
                foreach ($assignment in $assignments) {
                    if (($null -ne $targetGroupId) -and ($targetGroupId -eq $assignment.target.groupId)) {
                        Write-Host "`n❗ The App $($appDetails.displayName) is already assigned to the selected Group" -ForegroundColor Yellow
                        $assignmentContinue = $false
                    }
                    elseif (($all -eq 'Devices') -and ($assignment.target.'@odata.type' -eq '#microsoft.graph.allDevicesAssignmentTarget')) {
                        Write-Host "`n❗ The App $($appDetails.displayName) is already assigned to the All devices Group" -ForegroundColor Yellow
                        $assignmentContinue = $false
                    }
                    elseif (($all -eq 'Users') -and ($assignment.target.'@odata.type' -eq '#microsoft.graph.allLicensedUsersAssignmentTarget')) {
                        Write-Host "`n❗ The App $($appDetails.displayName) is already assigned to the All users Group" -ForegroundColor Yellow
                        $assignmentContinue = $false
                    }
                    else {
                        $targetGroup = New-Object -TypeName psobject
                        switch (($assignment.target).'@odata.type') {
                            '#microsoft.graph.groupAssignmentTarget' {
                                $targetGroup | Add-Member -MemberType NoteProperty -Name '@odata.type' -Value '#microsoft.graph.groupAssignmentTarget'
                                $targetGroup | Add-Member -MemberType NoteProperty -Name 'groupId' -Value $assignment.target.groupId
                            }
                            '#microsoft.graph.allLicensedUsersAssignmentTarget' {
                                $targetGroup | Add-Member -MemberType NoteProperty -Name '@odata.type' -Value '#microsoft.graph.allLicensedUsersAssignmentTarget'
                            }
                            '#microsoft.graph.allDevicesAssignmentTarget' {
                                $targetGroup | Add-Member -MemberType NoteProperty -Name '@odata.type' -Value '#microsoft.graph.allDevicesAssignmentTarget'
                            }
                        }
                        if ($assignment.target.deviceAndAppManagementAssignmentFilterType -ne 'none') {
                            $targetGroup | Add-Member -MemberType NoteProperty -Name 'deviceAndAppManagementAssignmentFilterId' -Value $assignment.target.deviceAndAppManagementAssignmentFilterId
                            $targetGroup | Add-Member -MemberType NoteProperty -Name 'deviceAndAppManagementAssignmentFilterType' -Value $assignment.target.deviceAndAppManagementAssignmentFilterType
                        }

                        $target = New-Object -TypeName psobject
                        $target | Add-Member -MemberType NoteProperty -Name '@odata.type' -Value '#microsoft.graph.mobileAppAssignment'
                        $target | Add-Member -MemberType NoteProperty -Name 'intent' -Value $assignment.intent
                        $target | Add-Member -MemberType NoteProperty -Name 'target' -Value $targetGroup
                        if (![string]::IsNullOrEmpty($assignment.settings)) {
                            $target | Add-Member -MemberType NoteProperty -Name 'settings' -Value $assignment.settings
                        }
                        $targetGroups += $target
                    }
                }
            }
        }

        if ($assignmentContinue -eq $true) {
            if ($applicationType -in $removeExistingAssignments) {
                $appDetails = Get-AppAssignment -Id $Id
                $assignments = $appDetails.assignments
                if (@($assignments).count -ge 1) {
                    foreach ($assignment in $assignments) {
                        Remove-AppAssignment -Id $Id -AssignmentId $assignment.id
                    }
                }
            }

            if ($applicationType -in $additionalAssignmentSettings) {
                $assignmentSettings = New-Object -TypeName psobject
                switch ($applicationType) {
                    '#microsoft.graph.iosVppApp' {
                        $assignmentSettings | Add-Member -MemberType NoteProperty -Name '@odata.type' -Value '#microsoft.graph.iosVppAppAssignmentSettings'
                        $assignmentSettings | Add-Member -MemberType NoteProperty -Name 'preventAutoAppUpdate' -Value $false
                        $assignmentSettings | Add-Member -MemberType NoteProperty -Name 'vpnConfigurationId' -Value $null
                        $assignmentSettings | Add-Member -MemberType NoteProperty -Name 'useDeviceLicensing' -Value $true
                        $assignmentSettings | Add-Member -MemberType NoteProperty -Name 'isRemovable' -Value $null

                    }
                    '#microsoft.graph.iosStoreApp' {
                        $assignmentSettings | Add-Member -MemberType NoteProperty -Name '@odata.type' -Value '#microsoft.graph.iosStoreAppAssignmentSettings'
                        $assignmentSettings | Add-Member -MemberType NoteProperty -Name 'vpnConfigurationId' -Value $null
                        $assignmentSettings | Add-Member -MemberType NoteProperty -Name 'isRemovable' -Value $null
                    }
                    '#microsoft.graph.iosLobApp' {
                        $assignmentSettings | Add-Member -MemberType NoteProperty -Name '@odata.type' -Value '#microsoft.graph.iosLobAppAssignmentSettings'
                        $assignmentSettings | Add-Member -MemberType NoteProperty -Name 'vpnConfigurationId' -Value $null
                        $assignmentSettings | Add-Member -MemberType NoteProperty -Name 'isRemovable' -Value $null
                    }
                    '#microsoft.graph.macOsVppApp' {
                        $assignmentSettings | Add-Member -MemberType NoteProperty -Name '@odata.type' -Value '#microsoft.graph.macOsVppAppAssignmentSettings'
                        $assignmentSettings | Add-Member -MemberType NoteProperty -Name 'useDeviceLicensing' -Value $true
                        $assignmentSettings | Add-Member -MemberType NoteProperty -Name 'preventAutoAppUpdate' -Value $false
                    }
                    default {
                        $assignmentSettings = $null
                    }
                }
                $assignmentSettings | Add-Member -MemberType NoteProperty -Name 'uninstallOnDeviceRemoval' -Value $true
                $assignmentSettings | Add-Member -MemberType NoteProperty -Name 'preventManagedAppBackup' -Value $true
            }

            $target = New-Object -TypeName psobject
            $target | Add-Member -MemberType NoteProperty -Name '@odata.type' -Value '#microsoft.graph.mobileAppAssignment'
            $target | Add-Member -MemberType NoteProperty -Name 'intent' -Value $installIntent

            $targetGroup = New-Object -TypeName psobject
            if ($targetGroupId) {
                $targetGroup | Add-Member -MemberType NoteProperty -Name '@odata.type' -Value '#microsoft.graph.groupAssignmentTarget'
                $targetGroup | Add-Member -MemberType NoteProperty -Name 'groupId' -Value $targetGroupId
            }
            else {
                switch ($all) {
                    'Users' {
                        $targetGroup | Add-Member -MemberType NoteProperty -Name '@odata.type' -Value '#microsoft.graph.allLicensedUsersAssignmentTarget'
                    }
                    'Devices' {
                        $targetGroup | Add-Member -MemberType NoteProperty -Name '@odata.type' -Value '#microsoft.graph.allDevicesAssignmentTarget'
                    }
                }
            }

            if ($filterMode) {
                $targetGroup | Add-Member -MemberType NoteProperty -Name 'deviceAndAppManagementAssignmentFilterId' -Value $filterID
                $targetGroup | Add-Member -MemberType NoteProperty -Name 'deviceAndAppManagementAssignmentFilterType' -Value $filterMode
            }

            $target | Add-Member -MemberType NoteProperty -Name 'target' -Value $targetGroup
            if ($null -ne $assignmentSettings) {
                $target | Add-Member -MemberType NoteProperty -Name 'settings' -Value $assignmentSettings
            }

            $targetGroups += $target
            $Output = New-Object -TypeName psobject
            $Output | Add-Member -MemberType NoteProperty -Name 'mobileAppAssignments' -Value @($targetGroups)

            $JSON = $Output | ConvertTo-Json -Depth 10

            $uri = "https://graph.microsoft.com/$graphApiVersion/$($resource)"
            Invoke-MgGraphRequest -Uri $uri -Method Post -Body $JSON -ContentType 'application/json'
            Write-Host '✅ Successfully created App assignment' -ForegroundColor Green
        }
        else {
            Write-Host '⚠ No assignment changes made to App' -ForegroundColor Yellow
        }
    }
    catch {
        Write-Host "❌ Graph request to $uri failed" -ForegroundColor Red
        if ($_.ErrorDetails -and $_.ErrorDetails.Message) {
            Write-Host $_.ErrorDetails.Message -ForegroundColor Red
        }
        else {
            Write-Host $_.Exception.Message -ForegroundColor Red
        }
        throw
    }
}
function New-ManagedDeviceAppConfig() {

    <#
    .SYNOPSIS
    Allows for creating Managed Device App Config policies in Intune.
 
    .DESCRIPTION
    This function allows for creating Managed Device App Config policies in Intune.
 
    .PARAMETER JSON
    Specifies the JSON string for the Managed Device App Config profile to create.
 
    #>


    [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'low')]

    param
    (
        [parameter(Mandatory = $true)]
        $JSON
    )

    $graphApiVersion = 'Beta'
    $resource = 'deviceAppManagement/mobileAppConfigurations'

    if ($PSCmdlet.ShouldProcess('Creating new Managed Device App Config Profile')) {
        try {
            Test-JSONData -Json $JSON
            $uri = "https://graph.microsoft.com/$graphApiVersion/$($resource)"
            Invoke-MgGraphRequest -Uri $uri -Method POST -Body $JSON -ContentType 'application/json' | Out-Null
            Write-Host '✅ Successfully created App Config policy.' -ForegroundColor Green
        }
        catch {
            Write-Host "❌ Graph request to $uri failed" -ForegroundColor Red
            if ($_.ErrorDetails -and $_.ErrorDetails.Message) {
                Write-Host $_.ErrorDetails.Message -ForegroundColor Red
            }
            else {
                Write-Host $_.Exception.Message -ForegroundColor Red
            }
            throw
        }
    }
    elseif ($WhatIfPreference.IsPresent) {
        Write-Output 'Managed Device App Config Profile would have been created'
    }
    else {
        Write-Output 'Managed Device App Config Profile was not created'
    }
}
function Get-ManagedDeviceAppConfig() {

    <#
    .SYNOPSIS
    Allows for getting Managed Device App Config policies from Intune.
 
    .DESCRIPTION
    This function allows for getting Managed Device App Config policies from Intune.
 
    #>


    [cmdletbinding()]

    $graphApiVersion = 'Beta'
    $resource = 'deviceAppManagement/mobileAppConfigurations'

    try {
        $uri = "https://graph.microsoft.com/$graphApiVersion/$($resource)"
        (Invoke-MgGraphRequest -Uri $uri -Method GET -OutputType PSObject).value
    }
    catch {
        Write-Host "❌ Graph request to $uri failed" -ForegroundColor Red
        if ($_.ErrorDetails -and $_.ErrorDetails.Message) {
            Write-Host $_.ErrorDetails.Message -ForegroundColor Red
        }
        else {
            Write-Host $_.Exception.Message -ForegroundColor Red
        }
        throw
    }
}
function Read-YesNoChoice {
    <#
        .SYNOPSIS
        Prompt the user for a Yes No choice.
 
        .DESCRIPTION
        Prompt the user for a Yes No choice and returns 0 for no and 1 for yes.
 
        .PARAMETER Title
        Title for the prompt
 
        .PARAMETER Message
        Message for the prompt
 
        .PARAMETER DefaultOption
        Specifies the default option if nothing is selected
 
        .INPUTS
        None. You cannot pipe objects to Read-YesNoChoice.
 
        .OUTPUTS
        Int. Read-YesNoChoice returns an Int, 0 for no and 1 for yes.
 
        .EXAMPLE
        PS> $choice = Read-YesNoChoice -Title "Please Choose" -Message "Yes or No?"
 
        Please Choose
        Yes or No?
        [N] No [Y] Yes [?] Help (default is "N"): y
        PS> $choice
        1
 
        .EXAMPLE
        PS> $choice = Read-YesNoChoice -Title "Please Choose" -Message "Yes or No?" -DefaultOption 1
 
        Please Choose
        Yes or No?
        [N] No [Y] Yes [?] Help (default is "Y"):
        PS> $choice
        1
 
        .LINK
        Online version: https://www.chriscolden.net/2024/03/01/yes-no-choice-function-in-powershell/
    #>


    param (
        [Parameter(Mandatory = $true)][String]$Title,
        [Parameter(Mandatory = $true)][String]$Message,
        [Parameter(Mandatory = $false)][Int]$DefaultOption = 0
    )

    $No = New-Object System.Management.Automation.Host.ChoiceDescription '&No', 'No'
    $Yes = New-Object System.Management.Automation.Host.ChoiceDescription '&Yes', 'Yes'
    $Options = [System.Management.Automation.Host.ChoiceDescription[]]($No, $Yes)

    return $host.ui.PromptForChoice($Title, $Message, $Options, $DefaultOption)
}
#endregion

#region variables
$requiredScopes = @('DeviceManagementApps.ReadWrite.All', 'Group.Read.All', 'DeviceManagementConfiguration.Read.All')
[String[]]$scopes = $requiredScopes -join ', '
$rndWait = Get-Random -Minimum 1 -Maximum 2
$noFiltering = @('#microsoft.graph.macOSPkgApp', '#microsoft.graph.macOSDmgApp')
$noUninstall = @('#microsoft.graph.macOSPkgApp', '#microsoft.graph.macOSOfficeSuiteApp', '#microsoft.graph.macOSMicrosoftDefenderApp', '#microsoft.graph.macOSMicrosoftEdgeApp')
$appsIntuneMAM = @(
    'com.microsoft.officemobile'
    'com.microsoft.Office.Word'
    'com.microsoft.Office.Excel'
    'com.microsoft.Office.Powerpoint'
    'com.microsoft.office.onenote'
    'com.microsoft.msedge'
    'com.microsoft.skydrive'
    'com.microsoft.Office.Outlook'
    'com.microsoft.skype.teams'
    'com.microsoft.copilot'
    'com.microsoft.onenote'
    'com.microsoft.office.officehubrow'
    'com.microsoft.office.word'
    'com.microsoft.office.excel'
    'com.microsoft.office.powerpoint'
    'com.microsoft.office.onenote'
    'com.microsoft.emmx'
    'com.microsoft.skydrive'
    'com.microsoft.office.outlook'
    'com.microsoft.teams'
    'com.microsoft.copilot'
)
$pathToScript = if ( $PSScriptRoot ) {
    # Console or vscode debug/run button/F5 temp console
    $PSScriptRoot
}
else {
    if ( $psISE ) { Split-Path -Path $psISE.CurrentFile.FullPath }
    else {
        if ($profile -match 'VScode') {
            # vscode "Run Code Selection" button/F8 in integrated console
            Split-Path $psEditor.GetEditorContext().CurrentFile.Path
        }
        else {
            Write-Output 'unknown directory to set path variable. exiting script.'
            exit 0
        }
    }
}
#endregion

#region intro
Clear-Host
Write-Host '
░▀█▀░█▀█░▀█▀░█░█░█▀█░█▀▀
░░█░░█░█░░█░░█░█░█░█░█▀▀
░▀▀▀░▀░▀░░▀░░▀▀▀░▀░▀░▀▀▀'
 -ForegroundColor Cyan
Write-Host '
░█▀█░█▀█░█▀█
░█▀█░█▀▀░█▀▀
░▀░▀░▀░░░▀░░'
 -ForegroundColor Red
Write-Host '
░█▀█░█▀▀░█▀▀░▀█▀░█▀▀░█▀█░█▀▀░█▀▄
░█▀█░▀▀█░▀▀█░░█░░█░█░█░█░█▀▀░█▀▄
░▀░▀░▀▀▀░▀▀▀░▀▀▀░▀▀▀░▀░▀░▀▀▀░▀░▀'
 -ForegroundColor DarkRed

Write-Host "`nIntuneAppAssigner - Update and review App Assignments in bulk." -ForegroundColor Green
Write-Host "`nNick Benton - oddsandendpoints.co.uk" -NoNewline;
Write-Host ' | Version' -NoNewline; Write-Host ' 0.5.4 Public Preview' -ForegroundColor Yellow -NoNewline
Write-Host ' | Last updated: ' -NoNewline; Write-Host '2026-05-28' -ForegroundColor Magenta
Write-Host "`nIf you have any feedback, open an issue at https://github.com/ennnbeee/IntuneAppAssigner/issues" -ForegroundColor Cyan
Start-Sleep -Seconds $rndWait
#endregion

#region preflight
if ($PSVersionTable.PSVersion.Major -lt 7) {
    Write-Host 'WARNING: Earlier versions of PowerShell are not supported, use PowerShell 7 or later.' -ForegroundColor Yellow
    exit
}
#endregion

#region module check
$modules = @('Microsoft.Graph.Authentication', 'Microsoft.PowerShell.ConsoleGuiTools')
foreach ($module in $modules) {
    Write-Host "`nChecking for $module PowerShell module..." -ForegroundColor Cyan
    if (!(Get-Module -Name $module -ListAvailable)) {
        Install-Module -Name $module -Scope CurrentUser -AllowClobber
    }
    Write-Host "PowerShell Module $module found." -ForegroundColor Green
    if (!([System.AppDomain]::CurrentDomain.GetAssemblies() | Where-Object FullName -Like "*$module*")) {
        Import-Module -Name $module -Force
    }
}
#endregion

#region app auth
try {
    if (Get-MgContext) {
        Write-Host 'Existing Graph session detected, using current authentication context.' -ForegroundColor Yellow

    }
    else {
        if (!$tenantId) {
            Write-Host 'Connecting using interactive authentication' -ForegroundColor Yellow
            Connect-MgGraph -Scopes $scopes -NoWelcome -ErrorAction Stop
        }
        else {
            if ((!$appId -and !$appSecret) -or ($appId -and !$appSecret) -or (!$appId -and $appSecret)) {
                Write-Host 'Missing App Details, connecting using user authentication' -ForegroundColor Yellow
                Connect-ToGraph -tenantId $tenantId -Scopes $scopes -ErrorAction Stop
            }
            else {
                Write-Host 'Connecting using App authentication' -ForegroundColor Yellow
                Connect-ToGraph -tenantId $tenantId -appId $appId -appSecret $appSecret -ErrorAction Stop
            }
        }
    }
    $context = Get-MgContext
    Write-Host "`nSuccessfully connected to Microsoft Graph tenant $($context.TenantId)." -ForegroundColor Green
}
catch {
    Write-Error $_.Exception.Message
    exit
}
#endregion

#region scopes
$currentScopes = $context.Scopes
$missingScopes = $requiredScopes | Where-Object { $_ -notin $currentScopes }
if ($missingScopes.Count -gt 0) {
    Write-Host 'WARNING: The following scope permissions are missing:' -ForegroundColor Yellow
    $missingScopes | ForEach-Object { Write-Host " - $_" -ForegroundColor Yellow }
    Write-Host "`nEnsure these permissions are granted to the app registration for full functionality." -ForegroundColor Yellow
    exit
}
else {
    Write-Host "`nAll required scope permissions are present." -ForegroundColor Green
}
Start-Sleep -Seconds $rndWait
#endregion

#region Script
do {
    Clear-Variable -Name choice*
    #region App Type
    #region app discovery
    $allMobileApps = Get-MobileApp
    $appsAND = $allMobileApps | Where-Object { (!($_.'@odata.type').Contains('managed')) -and ($_.'@odata.type').contains('android') }
    $appsIOS = $allMobileApps | Where-Object { (!($_.'@odata.type').Contains('managed')) -and ($_.'@odata.type').contains('ios') }
    $appsMAC = $allMobileApps | Where-Object { (!($_.'@odata.type').Contains('managed')) -and ($_.'@odata.type').contains('macO') }
    $appsWIN = $allMobileApps | Where-Object { (!($_.'@odata.type').Contains('managed')) -and (($_.'@odata.type').contains('win') -or ($_.'@odata.type').contains('office')) }
    #endregion app discovery
    do {
        Start-Sleep -Seconds $rndWait
        Clear-Host
        $apps = $null
        $choiceAppTypeOptions = @()
        Write-Host "`n📱 Select which app type:" -ForegroundColor White
        if ($null -ne $appsAND) {
            Write-Host "`n (1) Android App Assignment" -ForegroundColor Green
            $choiceAppTypeOptions += '1'
        }
        if ($null -ne $appsIOS) {
            Write-Host "`n (2) iOS/iPadOS App Assignment" -ForegroundColor Blue
            $choiceAppTypeOptions += '2'
        }
        if ($null -ne $appsMAC) {
            Write-Host "`n (3) macOS App Assignment" -ForegroundColor Magenta
            $choiceAppTypeOptions += '3'
        }
        if ($null -ne $appsWIN) {
            Write-Host "`n (4) Windows App Assignment" -ForegroundColor Cyan
            $choiceAppTypeOptions += '4'
        }
        Write-Host "`n (E) Exit`n" -ForegroundColor White
        $choiceAppTypeOptions += 'E'

        $choiceAppType = Read-Host -Prompt "Select option $($choiceAppTypeOptions -join ', '), then press enter"
        while ( $choiceAppType -notin $choiceAppTypeOptions) {
            $choiceAppType = Read-Host -Prompt "Select option $($choiceAppTypeOptions -join ', '), then press enter"
        }

        switch ($choiceAppType) {
            '1' { $appType = 'android'; $appTypeDisplay = 'Android'; $appPackage = 'packageId' }
            '2' { $appType = 'ios'; $appTypeDisplay = 'iOS/iPadOS'; $appPackage = 'bundleId' }
            '3' { $appType = 'macOS'; $appTypeDisplay = 'macOS' }
            '4' { $appType = 'win'; $appTypeDisplay = 'Windows' }
            'E' { exit }
        }

        switch ($appType) {
            'android' { $availableApps = $appsAND | Select-Object -Property @{Label = 'AppName'; Expression = 'displayName' }, @{Label = 'AppPublisher'; Expression = 'publisher' }, @{Label = 'AppType'; Expression = '@odata.type' }, @{Label = 'AppID'; Expression = 'id' }, @{Label = 'AppPackage'; Expression = $appPackage } | Sort-Object -Property 'AppName' }
            'ios' { $availableApps = $appsIOS | Select-Object -Property @{Label = 'AppName'; Expression = 'displayName' }, @{Label = 'AppPublisher'; Expression = 'publisher' }, @{Label = 'AppType'; Expression = '@odata.type' }, @{Label = 'AppID'; Expression = 'id' }, @{Label = 'AppPackage'; Expression = $appPackage } | Sort-Object -Property 'AppName' }
            'macOS' { $availableApps = $appsMAC | Select-Object -Property @{Label = 'AppName'; Expression = 'displayName' }, @{Label = 'AppPublisher'; Expression = 'publisher' }, @{Label = 'AppType'; Expression = '@odata.type' }, @{Label = 'AppID'; Expression = 'id' } | Sort-Object -Property 'AppName' }
            'win' { $availableApps = $appsWIN | Select-Object -Property @{Label = 'AppName'; Expression = 'displayName' }, @{Label = 'AppPublisher'; Expression = 'publisher' }, @{Label = 'AppType'; Expression = '@odata.type' }, @{Label = 'AppID'; Expression = 'id' } | Sort-Object -Property 'AppName' }
        }

        while ($apps.count -eq 0) {
            Write-Host "`nSelect the $appTypeDisplay apps you wish to modify or review assignments." -ForegroundColor Cyan
            Start-Sleep -Seconds $rndWait
            $apps = @($availableApps | Out-ConsoleGridView -Title 'Select apps to assign or review' -OutputMode Multiple)
        }
    }
    until ($apps.count -ne 0)
    #endregion App Type

    #region Assignment Type
    do {
        #region assignment actions
        Clear-Host
        Start-Sleep -Seconds $rndWait
        Write-Host "`n🪄 Select the assignment action:" -ForegroundColor White
        Write-Host "`n (1) Replace all existing assignments" -ForegroundColor Yellow
        Write-Host "`n (2) Add to the existing assignments" -ForegroundColor Green
        Write-Host "`n (3) Review existing assignments" -ForegroundColor Cyan
        Write-Host "`n (E) Exit`n" -ForegroundColor White

        $choiceAssignmentTypeOptions = @('1', '2', '3', 'E')
        $choiceAssignmentType = Read-Host -Prompt "Select option $($choiceAssignmentTypeOptions -join ', '), then press enter"
        while ( $choiceAssignmentType -notin $choiceAssignmentTypeOptions) {
            $choiceAssignmentType = Read-Host -Prompt "Select option $($choiceAssignmentTypeOptions -join ', '), then press enter"
        }
        switch ($choiceAssignmentType) {
            '1' { $action = 'Replace'; $decisionReview = 0 }
            '2' { $action = 'Add'; $decisionReview = 0 }
            '3' { $action = 'Review' }
            'E' { exit }
        }
        #endregion assignment actions

        #region Review
        if ($action -eq 'Review') {
            Clear-Host
            Start-Sleep -Seconds $rndWait
            Write-Host "`n🔄 Getting existing assignments for the following $appTypeDisplay apps:`n" -ForegroundColor Cyan
            $appAssignmentReport = @()
            foreach ($app in $apps) {
                Write-Host "$($app.AppName)" -ForegroundColor White
                $appAssignments = (Get-AppAssignment -Id $app.AppID).assignments
                if ($appAssignments.count -gt 0) {
                    foreach ($appAssignment in $appAssignments) {

                        $assignmentGroupType = switch ($appAssignment.target.'@odata.type') {
                            '#microsoft.graph.allLicensedUsersAssignmentTarget' { 'All users' }
                            '#microsoft.graph.allDevicesAssignmentTarget' { 'All devices' }
                            '#microsoft.graph.groupAssignmentTarget' { Get-MDMGroup -id $($appAssignment.target.groupId) | Select-Object -ExpandProperty displayName }
                        }
                        if ($($appAssignment.target.deviceAndAppManagementAssignmentFilterType) -ne 'none') {
                            $assignmentFilterMode = (Get-Culture).TextInfo.ToTitleCase($($appAssignment.target.deviceAndAppManagementAssignmentFilterType).ToLower())
                            $assignmentFilter = (Get-AssignmentFilter -Id $($appAssignment.target.deviceAndAppManagementAssignmentFilterId)).displayName
                        }
                        else {
                            $assignmentFilterMode = 'n/a'
                            $assignmentFilter = 'n/a'
                        }

                        $appAssignmentReport += [PSCustomObject]@{
                            'App'         = $app.AppName
                            'Publisher'   = $app.AppPublisher
                            'Intent'      = $(Get-Culture).TextInfo.ToTitleCase($($appAssignment.intent).ToLower())
                            'Assignment'  = $assignmentGroupType
                            'Filter Mode' = $assignmentFilterMode
                            'Filter'      = $assignmentFilter
                        }
                    }
                }
                else {
                    $appAssignmentReport += [PSCustomObject]@{
                        'App'         = $app.AppName
                        'Publisher'   = $app.AppPublisher
                        'Intent'      = 'n/a'
                        'Assignment'  = 'n/a'
                        'Filter Mode' = 'n/a'
                        'Filter'      = 'n/a'
                    }
                }
            }
            Clear-Host
            Start-Sleep -Seconds $rndWait
            Write-Host "`nThe below are the existing $appTypeDisplay app assignments:" -ForegroundColor Cyan
            $appAssignmentReport | Format-Table -AutoSize
            Write-Host "`n✨ All existing assignments for the selected $appTypeDisplay apps captured." -ForegroundColor Green

            $decisionExport = Read-YesNoChoice -Title '📝 Export Review to CSV' -Message 'Do you want to export the above assignment report to a CSV?' -DefaultOption 0
            if ($decisionExport -eq 1) {
                $timeStamp = Get-Date -Format 'yyyyMMdd-HHmmss'
                $exportPath = "$pathToScript\AppAssignmentsReview-$appType-$timeStamp.csv"
                $appAssignmentReport | Export-Csv -Path $exportPath -NoTypeInformation -Encoding UTF8
                Write-Host "`n✅ Exported app assignments to $exportPath" -ForegroundColor Green
            }
            $decisionReview = Read-YesNoChoice -Title '➡ Continue the Script' -Message 'Do you want to amend these app assignments?' -DefaultOption 1
            if ($decisionReview -eq 0) {
                exit 0
                #region Script Relaunch
                #$decisionRelaunch = Read-YesNoChoice -Title '♻ Relaunch the Script' -Message 'Do you want to relaunch the Script?' -DefaultOption 1
                #endregion Script Relaunch
            }
        }
        #endregion Review
    }
    until ($decisionReview -eq 0)
    #endregion Assignment Type

    #region Install Intent
    Clear-Host
    Start-Sleep -Seconds $rndWait
    Write-Host "`n💽 Select the installation intent:" -ForegroundColor White
    Write-Host "`n (1) Assign Apps as 'Required' to enrolled devices" -ForegroundColor Green
    Write-Host "`n (2) Assign Apps as 'Available' to enrolled devices" -ForegroundColor Cyan
    Write-Host "`n (3) Assign Apps as 'Uninstall' to enrolled devices" -ForegroundColor Yellow
    Write-Host "`n (4) Remove All Assignments types" -ForegroundColor Red
    Write-Host "`n (E) Exit`n" -ForegroundColor White

    $choiceInstallIntentOptions = @('1', '2', '3', '4', 'E')
    $choiceInstallIntent = Read-Host -Prompt "Select option $($choiceInstallIntentOptions -join ', '), then press enter"
    while ( $choiceInstallIntent -notin $choiceInstallIntentOptions) {
        $choiceInstallIntent = Read-Host -Prompt "Select option $($choiceInstallIntentOptions -join ', '), then press enter"
    }

    switch ($choiceInstallIntent) {
        '1' { $installIntent = 'Required' }
        '2' { $installIntent = 'Available' }
        '3' { $installIntent = 'Uninstall' }
        '4' { $installIntent = 'Remove' }
        'E' { exit }
    }
    #endregion Install Intent

    #region Group Assignment
    Clear-Host
    Start-Sleep -Seconds $rndWait
    if ($installIntent -ne 'Remove') {
        Clear-Host
        Start-Sleep -Seconds $rndWait
        $choiceAssignmentTargetOptions = @()
        Write-Host "`n👥 Select which group to assign the apps: " -ForegroundColor White
        if ($choiceInstallIntent -ne 2) {
            Write-Host "`n (1) Assign Apps to the 'All devices' group" -ForegroundColor Green
            $choiceAssignmentTargetOptions += '1'
        }
        Write-Host "`n (2) Assign Apps to the 'All users' group" -ForegroundColor Green
        Write-Host "`n (3) Assign Apps to a selected Group of users or devices" -ForegroundColor Cyan
        Write-Host "`n (E) Exit`n" -ForegroundColor White
        $choiceAssignmentTargetOptions += '2', '3', 'E'
        $choiceAssignmentTarget = Read-Host -Prompt "Select option $($choiceAssignmentTargetOptions -join ', '), then press enter"
        while ( $choiceAssignmentTarget -notin $choiceAssignmentTargetOptions) {
            $choiceAssignmentTarget = Read-Host -Prompt "Select option $($choiceAssignmentTargetOptions -join ', '), then press enter"
        }

        switch ($choiceAssignmentTarget) {
            '1' { $assignmentType = 'Devices' }
            '2' { $assignmentType = 'Users' }
            '3' {
                $assignmentType = 'Group'
                $groupName = $null
                if ($choiceInstallIntent -eq 2) {
                    Write-Host "Assigning Apps as 'Available' to groups containing Devices will not work, ensure you select a group containing Users." -ForegroundColor yellow
                }
                $groupName = Read-Host 'Enter a search term for the Assignment Group of at least three characters'
                while ($groupName.Length -lt 3) {
                    $groupName = Read-Host 'Enter a search term for the Assignment Group of at least three characters'
                }
                Start-Sleep -Seconds $rndWait
                Write-Host "`nSelect the Group for the assignment." -ForegroundColor Cyan
                Start-Sleep -Seconds $rndWait
                $assignmentGroup = $null
                while ($null -eq $assignmentGroup) {
                    $assignmentGroup = Get-MDMGroup -GroupName $groupName | Select-Object -Property @{Label = 'GroupName'; Expression = 'displayName' }, @{Label = 'GroupID'; Expression = 'id' } | Sort-Object -Property 'GroupName' | Out-ConsoleGridView -Title 'Select Assignment Group' -OutputMode Single
                }
            }
            'E' { exit }
        }

        Clear-Host
        Start-Sleep -Seconds $rndWait
        Write-Host "`n🎯 Select the Filter mode: " -ForegroundColor White
        Write-Host "`n (1) Include Filter" -ForegroundColor Green
        Write-Host "`n (2) Exclude Filter" -ForegroundColor Yellow
        Write-Host "`n (3) No Filters" -ForegroundColor Cyan
        Write-Host "`n (E) Exit`n" -ForegroundColor White

        $choiceAssignmentFilterOptions = @('1', '2', '3', 'E')
        $choiceAssignmentFilter = Read-Host -Prompt "Select option $($choiceAssignmentFilterOptions -join ', '), then press enter"
        while ( $choiceAssignmentFilter -notin $choiceAssignmentFilterOptions) {
            $choiceAssignmentFilter = Read-Host -Prompt "Select option $($choiceAssignmentFilterOptions -join ', '), then press enter"
        }

        switch ($choiceAssignmentFilter) {
            '1' { $filtering = 'Yes'; $filterMode = 'Include' }
            '2' { $filtering = 'Yes'; $filterMode = 'Exclude' }
            '3' { $filtering = 'No' }
            'E' { exit }
        }
        Start-Sleep -Seconds $rndWait
        if ($filtering -eq 'Yes') {
            $assignmentFilter = $null
            Write-Host "`nSelect the Assignment Filter for the assignment." -ForegroundColor Cyan
            Start-Sleep -Seconds $rndWait
            while ($null -eq $assignmentFilter) {
                $assignmentFilter = Get-AssignmentFilter | Where-Object { ($_.platform) -like ("*$appType*") -and ($_.assignmentFilterManagementType -eq 'devices') } | Select-Object -Property @{Label = 'FilterName'; Expression = 'displayName' }, @{Label = 'FilterRule'; Expression = 'rule' }, @{Label = 'FilterID'; Expression = 'id' } | Sort-Object -Property 'FilterName' | Out-ConsoleGridView -Title 'Select Assignment Filter' -OutputMode Single
            }
        }
    }
    #endregion Group Assignment

    #region App Config
    if (($appType -eq 'ios' -or $appType -eq 'android') -and ($installIntent -ne 'Remove')) {
        $intuneMAMApps = $null
        $apps | ForEach-Object {
            if ($_.AppPackage -in $appsIntuneMAM) {
                $intuneMAMApps = 'Yes'
            }
        }
        if ($intuneMAMApps -eq 'Yes') {
            Clear-Host
            Start-Sleep -Seconds $rndWait
            Write-Host "`n🪧 Select if Work Account App Config policies should be created:" -ForegroundColor White
            Write-Host "`n (1) Create App Config policies" -ForegroundColor Green
            Write-Host "`n (2) Do not create App Config policies" -ForegroundColor Cyan
            Write-Host "`n (E) Exit`n" -ForegroundColor White

            $choiceAppConfig = Read-Host -Prompt 'Based on whether App Config policies should be created, type 1, 2, or E to exit the script, then press enter'
            while ( ($choiceAppConfig -notin ('1', '2', 'E'))) {
                $choiceAppConfig = Read-Host -Prompt 'Based on whether App Config policies should be created, type 1, 2, or E to exit the script, then press enter'
            }

            switch ($choiceAppConfig) {
                '1' {
                    $appConfig = 'Yes'
                    Clear-Host
                    Start-Sleep -Seconds $rndWait
                    Write-Host "`n🏢 Select which App Config policies should be created:" -ForegroundColor White
                    Write-Host "`n (1) Both COPE and BYOD policies" -ForegroundColor Green
                    Write-Host "`n (2) Only COPE policies" -ForegroundColor Cyan
                    Write-Host "`n (3) Only BYOD policies" -ForegroundColor Yellow
                    Write-Host "`n (E) Exit`n" -ForegroundColor White

                    $choiceAppConfigOwnership = Read-Host -Prompt 'Based on which App Config policies should be created, type 1, 2, 3, or E to exit the script, then press enter'
                    while ( ($choiceAppConfigOwnership -notin ('1', '2', '3', 'E'))) {
                        $choiceAppConfigOwnership = Read-Host -Prompt 'Based on which App Config policies should be created, type 1, 2, 3, or E to exit the script, then press enter'
                    }

                    switch ($choiceAppConfigOwnership) {
                        '1' { $appConfigOwnership = 'Both' }
                        '2' { $appConfigOwnership = 'COPE' }
                        '3' { $appConfigOwnership = 'BYOD' }
                        'E' { exit }
                    }
                }
                '2' { $appConfig = 'No' }
                'E' { exit }
            }
        }
    }
    #endregion App Config

    #region App Assignment Check
    Clear-Host
    Start-Sleep -Seconds $rndWait
    Write-Host 'App Assignment Summary' -ForegroundColor Green
    Write-Host "`nThe following $appTypeDisplay Apps have been selected:" -ForegroundColor Cyan
    $($apps.'AppName') | Format-List
    if ($installIntent -ne 'Remove') {
        Write-Host "`nThe following Assignment Action has been selected:" -ForegroundColor Cyan
        Write-Host "$action"
        Write-Host "`nThe following Install Intent has been selected:" -ForegroundColor Cyan
        Write-Host "$installIntent"
        if ($installIntent -eq 'Uninstall') {
            Write-Host
            foreach ($app in $apps) {
                if ($app.'AppType' -in $noUninstall) {
                    Write-Host "Note: App $($app.AppName) does not support Uninstall assignments, this app will be skipped." -ForegroundColor Yellow
                }
            }
        }
        Write-Host "`nThe following Assignment Group has been selected:" -ForegroundColor Cyan
        if ($assignmentType -eq 'Group') {
            Write-Host "$($assignmentGroup.GroupName)"
        }
        else {
            Write-Host "All $assignmentType"
        }
        if ($filtering -eq 'Yes') {
            Write-Host "`nThe following Assignment Filter has been selected with Filter mode $filterMode`:" -ForegroundColor Cyan
            Write-Host "$($assignmentFilter.FilterName)"
            Write-Host
            foreach ($app in $apps) {
                if ($app.'AppType' -in $noFiltering) {
                    Write-Host "Note: App $($app.AppName) does not support Assignment Filters, this app will be assigned without a Filter." -ForegroundColor Yellow
                }
            }
        }
    }
    else {
        Write-Host 'All App assignments will be removed.' -ForegroundColor Red
    }
    if ($appConfig -eq 'Yes') {
        Write-Host "`nApp Configuration policies will be created for apps that support the 'Work/School Account only' setting." -ForegroundColor Cyan
    }

    $decisionConfirm = Read-YesNoChoice -Title '⏯ Review the above settings before proceeding' -Message 'Do you want to assign the selected Apps with above settings?' -DefaultOption 1
    if ($decisionConfirm -eq 0) {
        #region Script Relaunch
        $decisionRelaunch = Read-YesNoChoice -Title '♻ Relaunch the Script' -Message 'Do you want to relaunch the Script?' -DefaultOption 1
        #endregion Script Relaunch
    }
    else {
        Write-Host '▶ Proceeding with the assignment changes...' -ForegroundColor Green
        Start-Sleep -Seconds $rndWait
        #region App Assignment
        Clear-Host
        Start-Sleep -Seconds $rndWait
        if ($installIntent -ne 'Remove') {
            if ($installIntent -ne 'Uninstall') {
                if ($assignmentType -eq 'Group') {
                    if ($filtering -eq 'Yes') {
                        foreach ($app in $apps) {
                            if ($app.'AppType' -in $noFiltering) {
                                Write-Host "⏭ App $($app.AppName) does not support Assignment Filters, skipping Filter assignment." -ForegroundColor Yellow
                                Add-AppAssignment -applicationType $app.AppType -Id $app.AppID -InstallIntent $installIntent -TargetGroupId $assignmentGroup.GroupID -Action $action

                            }
                            else {
                                Add-AppAssignment -applicationType $app.AppType -Id $app.AppID -InstallIntent $installIntent -TargetGroupId $assignmentGroup.GroupID -FilterMode $filterMode -FilterID $assignmentFilter.FilterID -Action $action

                            }
                        }
                    }
                    else {
                        foreach ($app in $apps) {
                            Add-AppAssignment -applicationType $app.AppType -Id $app.AppID -InstallIntent $installIntent -TargetGroupId $assignmentGroup.GroupID -Action $action

                        }
                    }
                }
                else {
                    if ($filtering -eq 'Yes') {
                        foreach ($app in $apps) {
                            if ($app.'AppType' -in $noFiltering) {
                                Write-Host "⏭ App $($app.AppName) does not support Assignment Filters, skipping Filter assignment." -ForegroundColor Yellow
                                Add-AppAssignment -applicationType $app.AppType -Id $app.AppID -InstallIntent $installIntent -All $assignmentType -Action $action

                            }
                            else {
                                Add-AppAssignment -applicationType $app.AppType -Id $app.AppID -InstallIntent $installIntent -All $assignmentType -FilterMode $filterMode -FilterID $assignmentFilter.FilterID -Action $action

                            }
                        }
                    }
                    else {
                        foreach ($app in $apps) {
                            Add-AppAssignment -applicationType $app.AppType -Id $app.AppID -InstallIntent $installIntent -All $assignmentType -Action $action
                        }
                    }
                }
            }
            else {
                if ($assignmentType -eq 'Group') {
                    if ($filtering -eq 'Yes') {
                        foreach ($app in $apps) {
                            if ($app.'AppType' -in $noFiltering) {
                                if ($app.'AppType' -in $noUninstall) {
                                    Write-Host "⏭ App $($app.AppName) does not support Uninstall intent, skipping assignment." -ForegroundColor Yellow
                                }
                                else {
                                    Write-Host "⏭ App $($app.AppName) does not support Assignment Filters, skipping Filter assignment." -ForegroundColor Yellow
                                    Add-AppAssignment -applicationType $app.AppType -Id $app.AppID -InstallIntent $installIntent -TargetGroupId $assignmentGroup.GroupID -Action $action
                                }
                            }
                            else {
                                if ($app.'AppType' -in $noUninstall) {
                                    Write-Host "⏭ App $($app.AppName) does not support Uninstall intent, skipping assignment." -ForegroundColor Yellow
                                }
                                else {
                                    Add-AppAssignment -applicationType $app.AppType -Id $app.AppID -InstallIntent $installIntent -TargetGroupId $assignmentGroup.GroupID -FilterMode $filterMode -FilterID $assignmentFilter.FilterID -Action $action

                                }
                            }
                        }
                    }
                    else {
                        foreach ($app in $apps) {
                            if ($app.'AppType' -in $noUninstall) {
                                Write-Host "⏭ App $($app.AppName) does not support Uninstall intent, skipping assignment." -ForegroundColor Yellow
                            }
                            else {
                                Add-AppAssignment -applicationType $app.AppType -Id $app.AppID -InstallIntent $installIntent -TargetGroupId $assignmentGroup.GroupID -Action $action
                            }
                        }
                    }
                }
                else {
                    if ($filtering -eq 'Yes') {
                        foreach ($app in $apps) {
                            if ($app.'AppType' -in $noFiltering) {
                                if ($app.'AppType' -in $noUninstall) {
                                    Write-Host "⏭ App $($app.AppName) does not support Uninstall intent, skipping assignment." -ForegroundColor Yellow
                                }
                                else {
                                    Write-Host "⏭ App $($app.AppName) does not support Assignment Filters, skipping Filter assignment." -ForegroundColor Yellow
                                    Add-AppAssignment -applicationType $app.AppType -Id $app.AppID -InstallIntent $installIntent -All $assignmentType -Action $action
                                }
                            }
                            else {
                                if ($app.'AppType' -in $noUninstall) {
                                    Write-Host "⏭ App $($app.AppName) does not support Uninstall intent, skipping assignment." -ForegroundColor Yellow
                                }
                                else {
                                    Add-AppAssignment -applicationType $app.AppType -Id $app.AppID -InstallIntent $installIntent -All $assignmentType -FilterMode $filterMode -FilterID $assignmentFilter.FilterID -Action $action
                                }
                            }
                        }
                    }
                    else {
                        foreach ($app in $apps) {
                            if ($app.'AppType' -in $noUninstall) {
                                Write-Host "⏭ App $($app.AppName) does not support Uninstall intent, skipping assignment." -ForegroundColor Yellow
                            }
                            else {
                                Add-AppAssignment -applicationType $app.AppType -Id $app.AppID -InstallIntent $installIntent -All $assignmentType -Action $action
                            }
                        }
                    }
                }
            }
        }
        else {
            foreach ($app in $apps) {
                $assignments = (Get-AppAssignment -Id $app.AppID).assignments
                foreach ($assignment in $assignments) {
                    try {
                        Remove-AppAssignment -Id $app.AppID -AssignmentId $assignment.id
                    }
                    catch {
                        Write-Host "❌ Unable to remove App Assignment from $($app.AppName)" -ForegroundColor Red
                    }
                }
            }
        }
        #endregion App Assignment

        #region App Config
        if ($appConfig -eq 'Yes') {
            foreach ($app in $apps) {
                switch ($appType) {
                    'ios' {
                        $appConfigCOPEDisplayName = "$appConfigPrefix`IOS-COPE-$($($app.AppName).Replace(' ',''))"
                        $appConfigBYODDisplayName = "$appConfigPrefix`IOS-BYOD-$($($app.AppName).Replace(' ',''))"
                        $appConfigCOPEJson = @"
{
    "@odata.type": "#microsoft.graph.iosMobileAppConfiguration",
    "displayName": "$appConfigCOPEDisplayName",
    "description": "",
    "targetedMobileApps": [
        "$($app.AppID)"
    ],
    "settings": [
        {
            "appConfigKey": "IntuneMAMUPN ",
            "appConfigKeyType": "StringType",
            "appConfigKeyValue": "{{UserPrincipalName}}"
        }
    ]
}
"@

                        $appConfigBYODJson = @"
{
    "@odata.type": "#microsoft.graph.iosMobileAppConfiguration",
    "displayName": "$appConfigBYODDisplayName",
    "description": "",
    "targetedMobileApps": [
        "$($app.AppID)"
    ],
    "settings": [
        {
            "appConfigKey": "IntuneMAMUPN ",
            "appConfigKeyType": "StringType",
            "appConfigKeyValue": "{{UserPrincipalName}}"
        }
    ]
}
"@

                    }
                    'android' {
                        $appConfigCOPEDisplayName = "$appConfigPrefix`AND-COPE-$($($app.AppName).Replace(' ',''))"
                        $appConfigBYODDisplayName = "$appConfigPrefix`AND-BYOD-$($($app.AppName).Replace(' ',''))"
                        $appConfigSettingsJSON = @"
{
    "kind": "androidenterprise#managedConfiguration",
    "productId": "app:$($app.'AppPackage')",
    "managedProperty": [
        {
            "key": "com.microsoft.intune.mam.AllowedAccountUPNs",
            "valueString": "{{UserPrincipalName}}"
        }
    ]
}
"@

                        [string]$appConfigSettingsString = $appConfigSettingsJSON | ConvertFrom-Json | ConvertTo-Json -Compress
                        $appConfigSettingsEncoded = [Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes("$appConfigSettingsString"))
                        $appConfigCOPEJson = @"
{
    "@odata.type": "#microsoft.graph.androidManagedStoreAppConfiguration",
    "displayName": "$appConfigCOPEDisplayName",
    "description": "",
    "profileApplicability": "androidDeviceOwner",
    "targetedMobileApps": [
        "$($app.AppID)"
    ],
    "packageId": "app:$($app.'AppPackage')",
    "payloadJson": "$appConfigSettingsEncoded",
    "permissionActions": [],
    "connectedAppsEnabled": false,
}
"@

                        $appConfigBYODJson = @"
{
    "@odata.type": "#microsoft.graph.androidManagedStoreAppConfiguration",
    "displayName": "$appConfigBYODDisplayName",
    "description": "",
    "profileApplicability": "androidWorkProfile",
    "targetedMobileApps": [
        "$($app.AppID)"
    ],
    "packageId": "app:$($app.'AppPackage')",
    "payloadJson": "$appConfigSettingsEncoded",
    "permissionActions": [],
    "connectedAppsEnabled": false,
}
"@

                    }
                }

                if ($($app.'AppPackage') -in $appsIntuneMAM) {

                    if ($appConfigOwnership -eq 'Both' -or $appConfigOwnership -eq 'COPE') {
                        $appConfigCOExists = Get-ManagedDeviceAppConfig | Where-Object { $_.displayName -eq $appConfigCOPEDisplayName }
                        if ($null -ne $appConfigCOExists) {
                            Write-Host "⏭ A COPE App Config policy already exists for $($app.AppName), skipping creation" -ForegroundColor Cyan
                        }
                        else {
                            New-ManagedDeviceAppConfig -JSON $appConfigCOPEJson
                        }
                    }
                    if ($appConfigOwnership -eq 'Both' -or $appConfigOwnership -eq 'BYOD') {
                        $appConfigBYODExists = Get-ManagedDeviceAppConfig | Where-Object { $_.displayName -eq $appConfigBYODDisplayName }
                        if ($null -ne $appConfigBYODExists) {
                            Write-Host "⏭ A BYOD App Config policy already exists for $($app.AppName), skipping creation" -ForegroundColor Cyan
                        }
                        else {
                            New-ManagedDeviceAppConfig -JSON $appConfigBYODJson
                        }
                    }
                }
                else {
                    Write-Host "⏭ Skipping creation of App Config policy, $($app.AppName) does not support the 'Work/School account only' setting." -ForegroundColor Cyan
                }
            }
        }

        #region Script Relaunch
        Write-Host "`n✨ All actions completed." -ForegroundColor Green
        $decisionRelaunch = Read-YesNoChoice -Title '♻ Relaunch the Script' -Message 'Do you want to relaunch the Script?' -DefaultOption 1
        #endregion Script Relaunch
        #endregion App Config
    }
    #endregion App Assignment Check
}
until ($decisionRelaunch -eq 0)
#endregion