#requires -Version 5.1 <# .SYNOPSIS Bulk delete Intune policies/apps by object type and name filter. .DESCRIPTION Select an object type, optionally filter by name, multi-select objects, and delete them from the tenant. Requires typing DELETE to confirm since this is irreversible. Supports -WhatIf dry-run. .EXAMPLE ./Scripts/Bulk-DeletePolicies.ps1 -TenantId "contoso.onmicrosoft.com" -WhatIf #> [CmdletBinding()] param( [Parameter(Mandatory = $true)] [string]$TenantId, [string]$AppId, [string]$Secret, [string]$Certificate, [ValidateSet("AppOnly","Browser","DeviceCode")] [string]$AuthMode = "AppOnly", [string]$RedirectUri, [string]$SettingsFile, [switch]$WhatIf ) $ErrorActionPreference = "Stop" #region Helper functions function Test-FzfAvailable { return [bool](Get-Command fzf -ErrorAction SilentlyContinue) } function Show-FzfMenu { param( [Parameter(Mandatory)] [string[]]$Items, [string]$Header = "Select one", [switch]$Multi ) $argsList = @("--header=$Header") if($Multi) { $argsList += "--multi" } $selected = $Items | fzf @argsList if(-not $selected) { return $null } if($Multi) { return @($selected -split "`r?`n" | Where-Object { $_ }) } return $selected } function Show-NumberedMenu { param( [Parameter(Mandatory)] [string[]]$Items, [string]$Header = "Select one or more", [switch]$Multi ) Write-Host "`n$Header" -ForegroundColor Cyan for($i=0; $i -lt $Items.Count; $i++) { Write-Host " $($i+1). $($Items[$i])" } if($Multi) { $prompt = "Enter numbers separated by commas (e.g. 1,3,5) or 'all'" } else { $prompt = "Enter a number" } $choice = Read-Host $prompt if($choice -eq "all" -and $Multi) { return $Items } $indices = $choice -split "," | ForEach-Object { $_.Trim() } | Where-Object { $_ -match "^\d+$" } | ForEach-Object { [int]$_ - 1 } | Where-Object { $_ -ge 0 -and $_ -lt $Items.Count } if($Multi) { return $Items[$indices] | Select-Object -Unique } else { if($indices.Count -eq 0) { return $null } return $Items[$indices[0]] } } function Select-MenuItem { param( [Parameter(Mandatory)] [string[]]$Items, [string]$Header = "Select one", [switch]$Multi ) if(Test-FzfAvailable) { return Show-FzfMenu -Items $Items -Header $Header -Multi:$Multi } return Show-NumberedMenu -Items $Items -Header $Header -Multi:$Multi } function Get-DefaultSettingsPath { if($IsWindows -or $env:OS -eq "Windows_NT") { if($env:LOCALAPPDATA) { return (Join-Path $env:LOCALAPPDATA "macOS_IntuneManagement\Settings.json") } return (Join-Path $env:USERPROFILE "AppData\Local\macOS_IntuneManagement\Settings.json") } if($IsMacOS) { return (Join-Path $HOME "Library/Application Support/macOS_IntuneManagement/Settings.json") } return (Join-Path $HOME ".local/share/macOS_IntuneManagement/Settings.json") } #endregion #region Initialize Runtime $projectRoot = Split-Path -Parent $PSScriptRoot $runtimeModule = Join-Path $projectRoot "Runtime/IntuneManagement.Runtime.psd1" if(-not (Test-Path $runtimeModule)) { throw "Could not find IntuneManagement.Runtime.psd1 in $projectRoot" } $settingsPath = $SettingsFile if(-not $settingsPath) { $settingsPath = Get-DefaultSettingsPath } # Pre-load auth from settings if($AuthMode -eq "AppOnly" -and (Test-Path $settingsPath) -and (-not $AppId -or (-not $Secret -and -not $Certificate))) { try { $raw = Get-Content -Path $settingsPath -Raw -ErrorAction Stop $settingsObj = ConvertFrom-Json $raw -AsHashtable -ErrorAction Stop if($settingsObj -and $settingsObj.ContainsKey($TenantId)) { $tenantNode = $settingsObj[$TenantId] if(-not $AppId -and $tenantNode.ContainsKey("GraphAzureAppId")) { $AppId = $tenantNode["GraphAzureAppId"] } if(-not $Secret -and $tenantNode.ContainsKey("GraphAzureAppSecret")) { $Secret = $tenantNode["GraphAzureAppSecret"] } if(-not $Certificate -and $tenantNode.ContainsKey("GraphAzureAppCert")) { $Certificate = $tenantNode["GraphAzureAppCert"] } } if(-not $Secret -and $IsMacOS -and $AppId) { try { $keychainSecret = security find-generic-password -a "IntuneManagement" -s "IntuneMgmt-$AppId" -w 2>$null if($keychainSecret) { $Secret = $keychainSecret } } catch { } } } catch { } } $invokeParams = @{ Silent = $true JSonSettings = $true JSonFile = $settingsPath TenantId = $TenantId AppId = $AppId AuthMode = $AuthMode } if($RedirectUri) { $invokeParams.RedirectUri = $RedirectUri } if($AuthMode -eq "AppOnly" -and $Secret) { $invokeParams.Secret = $Secret } elseif($AuthMode -eq "AppOnly") { $invokeParams.Certificate = $Certificate } Import-Module $runtimeModule -Force Initialize-IntuneManagementRuntime -View "IntuneGraphAPI" @invokeParams #endregion #region Ensure Graph connectivity if(-not (Get-Command Invoke-GraphRequest -ErrorAction SilentlyContinue)) { throw "Graph runtime did not load Invoke-GraphRequest. Aborting." } Write-Host "`nConnecting to Microsoft Graph..." -ForegroundColor Cyan try { $org = Invoke-GraphRequest "/organization" Write-Host "Connected to tenant: $($org.value[0].displayName) ($($org.value[0].id))" -ForegroundColor Green } catch { throw "Failed to connect to Graph. Ensure auth parameters are correct. Error: $_" } #endregion #region Object type registry (deletable types) $deletableTypes = @( [PSCustomObject]@{ Title = "Applications"; API = "/deviceAppManagement/mobileApps"; NameProp = "displayName" }, [PSCustomObject]@{ Title = "Device Configuration"; API = "/deviceManagement/deviceConfigurations"; NameProp = "displayName" }, [PSCustomObject]@{ Title = "Settings Catalog"; API = "/deviceManagement/configurationPolicies"; NameProp = "name" }, [PSCustomObject]@{ Title = "Compliance Policies"; API = "/deviceManagement/deviceCompliancePolicies"; NameProp = "displayName" }, [PSCustomObject]@{ Title = "Administrative Templates"; API = "/deviceManagement/groupPolicyConfigurations"; NameProp = "displayName" }, [PSCustomObject]@{ Title = "Endpoint Security"; API = "/deviceManagement/intents"; NameProp = "displayName" }, [PSCustomObject]@{ Title = "App Protection"; API = "/deviceAppManagement/managedAppPolicies"; NameProp = "displayName" }, [PSCustomObject]@{ Title = "App Configuration (Device)"; API = "/deviceAppManagement/mobileAppConfigurations"; NameProp = "displayName" }, [PSCustomObject]@{ Title = "Platform Scripts"; API = "/deviceManagement/deviceManagementScripts"; NameProp = "displayName" }, [PSCustomObject]@{ Title = "macOS Scripts"; API = "/deviceManagement/deviceShellScripts"; NameProp = "displayName" }, [PSCustomObject]@{ Title = "Device Health Scripts"; API = "/deviceManagement/deviceHealthScripts"; NameProp = "displayName" }, [PSCustomObject]@{ Title = "macOS Custom Attributes"; API = "/deviceManagement/deviceCustomAttributeShellScripts"; NameProp = "displayName" }, [PSCustomObject]@{ Title = "Enrollment Restrictions"; API = "/deviceManagement/deviceEnrollmentConfigurations"; NameProp = "displayName" }, [PSCustomObject]@{ Title = "Autopilot"; API = "/deviceManagement/windowsAutopilotDeploymentProfiles"; NameProp = "displayName" }, [PSCustomObject]@{ Title = "Terms and Conditions"; API = "/deviceManagement/termsAndConditions"; NameProp = "displayName" }, [PSCustomObject]@{ Title = "Policy Sets"; API = "/deviceAppManagement/policySets"; NameProp = "displayName" }, [PSCustomObject]@{ Title = "Update Policies"; API = "/deviceManagement/windowsUpdateForBusinessConfigurations"; NameProp = "displayName" }, [PSCustomObject]@{ Title = "Feature Updates"; API = "/deviceManagement/windowsFeatureUpdateProfiles"; NameProp = "displayName" }, [PSCustomObject]@{ Title = "Quality Updates"; API = "/deviceManagement/windowsQualityUpdateProfiles"; NameProp = "displayName" }, [PSCustomObject]@{ Title = "Device Management Intents"; API = "/deviceManagement/intents"; NameProp = "displayName" } ) #endregion Clear-Host Write-Host "========================================" -ForegroundColor Cyan Write-Host " Intune Bulk Delete Tool" -ForegroundColor Cyan Write-Host "========================================" -ForegroundColor Cyan #region Select object type $typeTitles = $deletableTypes | ForEach-Object { $_.Title } $selectedTypeTitle = Select-MenuItem -Items $typeTitles -Header "Select object type" if(-not $selectedTypeTitle) { Write-Host "Cancelled." -ForegroundColor Yellow; exit 0 } $objectType = $deletableTypes | Where-Object { $_.Title -eq $selectedTypeTitle } | Select-Object -First 1 #endregion #region Load objects Write-Host "`nLoading $($objectType.Title) objects..." -ForegroundColor Cyan $api = "$($objectType.API)?`$select=id,$($objectType.NameProp)&`$orderby=$($objectType.NameProp)" $objectsResponse = Invoke-GraphRequest $api -AllPages $objects = $objectsResponse.value | Where-Object { $_ } | Sort-Object $objectType.NameProp Write-Host "Found $($objects.Count) objects." -ForegroundColor Green $filter = Read-Host "`nFilter by current name (optional, press Enter to skip)" if(-not [string]::IsNullOrWhiteSpace($filter)) { $objects = $objects | Where-Object { $_."$($objectType.NameProp)" -like "*$filter*" } Write-Host "Filtered to $($objects.Count) objects." -ForegroundColor Green } if($objects.Count -eq 0) { Write-Host "No objects found. Exiting." -ForegroundColor Yellow exit 0 } $objectDisplays = $objects | ForEach-Object { "$($_."$($objectType.NameProp)") [$($_.id)]" } $selectedDisplays = Select-MenuItem -Items $objectDisplays -Header "Select objects to DELETE (multi-select)" -Multi if(-not $selectedDisplays) { Write-Host "No objects selected. Exiting." -ForegroundColor Yellow exit 0 } $selectedObjects = @() foreach($disp in $selectedDisplays) { $id = $disp -replace '.*\[(.*?)\]$', '$1' $obj = $objects | Where-Object { $_.id -eq $id } | Select-Object -First 1 if($obj) { $selectedObjects += $obj } } #endregion #region Confirm Write-Host "`nThe following $($selectedObjects.Count) $($objectType.Title) object(s) will be PERMANENTLY DELETED:" -ForegroundColor Red foreach($obj in $selectedObjects) { Write-Host " - $($obj."$($objectType.NameProp)") [$($obj.id)]" -ForegroundColor DarkGray } if(-not $WhatIf) { $typed = Read-Host "`nThis cannot be undone. Type DELETE to confirm" if($typed -cne "DELETE") { Write-Host "Confirmation text did not match. Cancelled." -ForegroundColor Yellow exit 0 } } #endregion #region Execute $success = 0 $failed = 0 foreach($obj in $selectedObjects) { $name = $obj."$($objectType.NameProp)" try { if($WhatIf) { Write-Host " WHATIF: Would delete $name [$($obj.id)]" -ForegroundColor Magenta $success++ } else { $maxRetries = 3 $retryDelay = 2 $deleted = $false for($r = 1; $r -le $maxRetries; $r++) { try { $null = Invoke-GraphRequest "$($objectType.API)/$($obj.id)" -HttpMethod DELETE Write-Host " OK: Deleted '$name'" -ForegroundColor Green $success++ $deleted = $true break } catch { $statusCode = $_.Exception.Response.StatusCode if($r -lt $maxRetries -and ($statusCode -ge 500 -or $statusCode -eq 429)) { Write-Host " Retry $r/$maxRetries after $retryDelay`s (HTTP $statusCode)..." -ForegroundColor DarkYellow Start-Sleep -Seconds $retryDelay } else { throw } } } if(-not $deleted) { throw "Delete failed after $maxRetries attempts." } } } catch { Write-Host " ERROR: Failed to delete '$name'. $($_.Exception.Message)" -ForegroundColor Red $failed++ } } Write-Host "`n========================================" -ForegroundColor Cyan Write-Host " Bulk Delete Complete" -ForegroundColor Cyan Write-Host "========================================" -ForegroundColor Cyan Write-Host " Success : $success" Write-Host " Failed : $failed" #endregion