Remove-macOS-OldIntuneApps.ps1

<#PSScriptInfo
.VERSION 1.0.0
.GUID d2ace103-adeb-47be-80cd-2180db770ece
.AUTHOR Giovanni Solone
.TAGS powershell intune macos apps microsoft graph cleanup duplicates
.LICENSEURI https://opensource.org/licenses/MIT
.PROJECTURI https://github.com/gioxx/Nebula.Scripts/Intune/Remove-macOS-OldIntuneApps.ps1
#>


# Requires -Version 7.0

<#
.SYNOPSIS
Script to manage macOS apps in Microsoft Graph, focusing on duplicates and old versions.
.DESCRIPTION
This script retrieves all macOS apps from Microsoft Graph, identifies duplicates based on display name,
and allows for moving assignments from old versions to the latest version. It also provides an option to remove
old versions if they are unassigned.
.PARAMETER RemoveIfUnassigned
When specified, the script will remove old versions of apps that are unassigned.
.PARAMETER Force
When specified, the script will not prompt for confirmation before removing apps.
.EXAMPLE
.\Remove-macOS-OldIntuneApps.ps1
Runs the script interactively, allowing you to review duplicates and move assignments.
.EXAMPLE
.\Remove-macOS-OldIntuneApps.ps1 -RemoveIfUnassigned -Force
Removes old versions of macOS apps that are unassigned without prompting for confirmation.
#>


param (
    [switch]$RemoveIfUnassigned,
    [switch]$Force
)

Import-Module Microsoft.Graph.Beta.Devices.CorporateManagement
Connect-MgGraph -NoWelcome

# Retrieve all macOS apps matching specific types and availability
$macOSApps = Get-MgBetaDeviceAppManagementMobileApp -Filter `
    "(isof('microsoft.graph.macOSDmgApp') or isof('microsoft.graph.macOSPkgApp') or isof('microsoft.graph.macOSLobApp') or isof('microsoft.graph.macOSMicrosoftEdgeApp') or isof('microsoft.graph.macOSMicrosoftDefenderApp') or isof('microsoft.graph.macOSOfficeSuiteApp') or isof('microsoft.graph.macOsVppApp') or isof('microsoft.graph.webApp') or isof('microsoft.graph.macOSWebClip')) `
    and (microsoft.graph.managedApp/appAvailability eq null or microsoft.graph.managedApp/appAvailability eq 'lineOfBusiness' or isAssigned eq true)"
 `
    -Sort "displayName asc"

# Extract key properties including values from AdditionalProperties
$appsInfo = foreach ($app in $macOSApps) {
    $fileName = $null
    $bundleVersion = $null

    if ($app.AdditionalProperties.ContainsKey("fileName")) {
        $fileName = $app.AdditionalProperties["fileName"]
    }

    if ($app.AdditionalProperties.ContainsKey("primaryBundleVersion")) {
        $bundleVersion = $app.AdditionalProperties["primaryBundleVersion"]
    }

    [PSCustomObject]@{
        id          = $app.Id
        displayName = $app.DisplayName
        isAssigned  = $app.IsAssigned
        fileName    = $fileName
        Version     = $bundleVersion
    }
}

# Show a full table of all macOS apps
$appsInfo | Format-Table -AutoSize

# Group apps by name to identify duplicates
$duplicateApps = $appsInfo | Group-Object -Property displayName | Where-Object { $_.Count -gt 1 }

# Exit if no duplicates were found
if (-not $duplicateApps) {
    Write-Host "No duplicate apps found." -ForegroundColor Green
    return
}

# Display groups with more than one version
$duplicateApps | Sort-Object -Property Count -Descending | Format-Table -AutoSize

# Identify older versions (excluding latest per group)
$oldVersions = foreach ($group in $duplicateApps) {
    $ordered = $group.Group | Sort-Object -Property Version -Descending
    $ordered | Select-Object -Skip 1
}

# Show older versions
if ($oldVersions) {
    Write-Host "`n--- Old versions found ---" -ForegroundColor Yellow
    $oldVersions | Sort-Object displayName, Version | Format-Table displayName, Version, isAssigned, fileName, id -AutoSize
} else {
    Write-Host "No old versions found." -ForegroundColor Green
}

# Highlight old versions that are still assigned
$stillAssigned = $oldVersions | Where-Object { $_.isAssigned -eq $true }

if ($stillAssigned) {
    Write-Host "`n--- WARNING: Old versions still assigned ---" -ForegroundColor Red
    $stillAssigned | Sort-Object displayName, Version | Format-Table displayName, Version, fileName, id -AutoSize
} else {
    Write-Host "All old versions are unassigned. Safe to remove (please use the -RemoveIfUnassigned switch)." -ForegroundColor Green
}

# Move assignments from old versions to the newest version
foreach ($group in $duplicateApps) {
    # Sort the group by version descending
    $ordered = $group.Group | Sort-Object -Property Version -Descending
    $newestApp = $ordered[0]
    $oldApps = $ordered | Select-Object -Skip 1

    foreach ($oldApp in $oldApps) {
        # Skip apps that are not still assigned
        if (-not ($stillAssigned | Where-Object { $_.id -eq $oldApp.id })) {
            continue
        }

        # Get assignments from the old version
        $assignments = Get-MgBetaDeviceAppManagementMobileAppAssignment -MobileAppId $oldApp.id

        if (-not $assignments) {
            Write-Host "`nNo assignments found for $($oldApp.displayName) [$($oldApp.Version)]"
            continue
        }

        Write-Host "`n==== SIMULATION ====" -ForegroundColor Yellow
        Write-Host "App name : $($oldApp.displayName)"
        Write-Host "Old version : $($oldApp.Version)"
        Write-Host "New version : $($newestApp.Version)"
        Write-Host "Assignments to move:" -ForegroundColor Gray
        $assignments | Format-Table id, intent, @{Name = "Target"; Expression = { $_.target.groupId } }, -AutoSize

        # Ask user confirmation
        $choice = Read-Host "Do you want to move these assignments to the newer version? [y/n] (default: y)"

        if ([string]::IsNullOrWhiteSpace($choice) -or $choice.ToLower() -eq "y") {
            foreach ($assignment in $assignments) {
                # Build new assignment body
                $newAssignment = @{
                    target = $assignment.target
                    intent = $assignment.intent
                }

                # Create new assignment
                New-MgBetaDeviceAppManagementMobileAppAssignment -MobileAppId $newestApp.id -BodyParameter $newAssignment

                # Remove old assignment
                Remove-MgBetaDeviceAppManagementMobileAppAssignment -MobileAppId $oldApp.id -MobileAppAssignmentId $assignment.id

                Write-Host "Moved assignment $($assignment.id) to version $($newestApp.Version)" -ForegroundColor Green
            }
        } else {
            Write-Host "Skipped reassignment for $($oldApp.displayName) [$($oldApp.Version)]" -ForegroundColor DarkGray
        }
    }
}

# Remove old versions if unassigned and switch is enabled
if ($RemoveIfUnassigned -and (-not $stillAssigned)) {
    foreach ($app in $oldVersions) {
        Write-Host "`n==== SIMULATION: REMOVE $($app.displayName) [$($app.Version)] ====" -ForegroundColor Yellow
        Write-Host "App ID : $($app.id)"
        Write-Host "File : $($app.fileName)"

        if (-not $Force) {
            $confirm = Read-Host "Confirm removal of this app? [y/n] (default: n)"
            if ($confirm.ToLower() -ne "y") {
                Write-Host "Skipped removal for $($app.displayName) [$($app.Version)]" -ForegroundColor DarkGray
                continue
            }
        }

        # Perform actual removal
        Remove-MgBetaDeviceAppManagementMobileApp -MobileAppId $app.id
        Write-Host "Removed app $($app.displayName) [$($app.Version)]" -ForegroundColor Green
    }
} elseif ($RemoveIfUnassigned -and $stillAssigned) {
    Write-Host "`nRemoval blocked: some old versions are still assigned. Nothing will be removed." -ForegroundColor Red
}