feat(toolkit): complete macOS Intune Toolkit v1

Core enhancements:
- Expanded default export/import scope to ~45 object types including DeviceManagementIntents
- Added -AllPages pagination support across Graph queries for large tenants
- Invoke-GraphRequest now throws on 4xx/5xx instead of silently returning null
- Added macOS Keychain fallback for secret retrieval in headless auth flow
- Added NameSearchPattern/NameReplacePattern mutation support through export/import forms

New toolkit scripts:
- Bulk-AppAssignment.ps1: bulk-assign apps to groups/All Users/All Devices
- Bulk-AssignmentManager.ps1: add/remove assignments for any policy type with correct @odata.type
- Backup-Restore-Assignments.ps1: JSON backup with cross-tenant group resolution
- Export-AssignmentsToCsv.ps1: CSV/Markdown documentation output
- Bulk-RenamePolicies.ps1: regex search/replace and prefix mutations
- Bulk-DeviceOperations.ps1: delete/retire/wipe/lock/sync with -WhatIf safeguards
- Start-IntuneManagementTui.ps1: interactive terminal UI for headless operations
- Create-IntuneManagementApp.ps1: helper for app registration setup

Updated existing scripts:
- Export-Policies.ps1 / Import-Policies.ps1: wired mutation params through
- Start-HeadlessIntune.ps1: integrated TUI and new parameter forwarding
This commit is contained in:
2026-04-14 15:11:09 +02:00
parent 0ddd21ab14
commit e13d14edcb
18 changed files with 3649 additions and 69 deletions

View File

@@ -592,6 +592,7 @@ function Invoke-GraphRequest
catch{}
Write-LogError "Failed to invoke MS Graph with URL $Url (Request ID: $requestId). Status code: $($_.Exception.Response.StatusCode)$extMessage" $_.Exception
throw $_.Exception
}
}
} while($retryRequest -eq $true)
@@ -1329,6 +1330,21 @@ function Invoke-InitSilentBatchJob
{
$global:ClientSecret = Get-SettingValue "GraphAzureAppSecret" -TenantID $global:TenantId
$global:ClientCert = Get-SettingValue "GraphAzureAppCert" -TenantID $global:TenantId
# macOS Keychain fallback for client secret
if(-not $global:ClientSecret -and $IsMacOS -and $global:AzureAppId)
{
try
{
$keychainSecret = security find-generic-password -a "IntuneManagement" -s "IntuneMgmt-$($global:AzureAppId)" -w 2>$null
if($keychainSecret)
{
$global:ClientSecret = $keychainSecret
Write-Log "Retrieved client secret from macOS Keychain"
}
}
catch { }
}
}
}
@@ -1423,6 +1439,8 @@ function New-GraphSilentExportForm
$controls = @(
(New-HeadlessControl -Name "txtExportPath" -Type "TextBox"),
(New-HeadlessControl -Name "txtExportNameFilter" -Type "TextBox"),
(New-HeadlessControl -Name "txtExportNameSearchPattern" -Type "TextBox"),
(New-HeadlessControl -Name "txtExportNameReplacePattern" -Type "TextBox"),
(New-HeadlessControl -Name "chkAddObjectType" -Type "CheckBox"),
(New-HeadlessControl -Name "chkExportAssignments" -Type "CheckBox"),
(New-HeadlessControl -Name "chkAddCompanyName" -Type "CheckBox"),
@@ -1449,6 +1467,8 @@ function New-GraphSilentImportForm
$controls = @(
(New-HeadlessControl -Name "txtImportPath" -Type "TextBox"),
(New-HeadlessControl -Name "txtImportNameFilter" -Type "TextBox"),
(New-HeadlessControl -Name "txtImportNameSearchPattern" -Type "TextBox"),
(New-HeadlessControl -Name "txtImportNameReplacePattern" -Type "TextBox"),
(New-HeadlessControl -Name "lblMigrationTableInfo" -Type "Label"),
(New-HeadlessControl -Name "chkAddObjectType" -Type "CheckBox"),
(New-HeadlessControl -Name "chkImportScopes" -Type "CheckBox"),
@@ -1631,6 +1651,13 @@ function Start-GraphObjectExport
Save-Setting "" "ExportNameFilter" $txtNameFilter
if($txtNameFilter) { Write-Log "Name filter: $txtNameFilter" }
$txtSearchPattern = ""
if($global:txtExportNameSearchPattern -ne $null) { $txtSearchPattern = $global:txtExportNameSearchPattern.Text.Trim() }
$txtReplacePattern = ""
if($global:txtExportNameReplacePattern -ne $null) { $txtReplacePattern = $global:txtExportNameReplacePattern.Text.Trim() }
if($txtSearchPattern) { Write-Log "Name mutation: replace '$txtSearchPattern' with '$txtReplacePattern'" }
try
{
$folder = Get-GraphObjectFolder $item.ObjectType $script:exportRoot (Get-XamlProperty $script:exportForm "chkAddObjectType" "IsChecked") (Get-XamlProperty $script:exportForm "chkAddCompanyName" "IsChecked")
@@ -1650,6 +1677,11 @@ function Start-GraphObjectExport
{
if(-not $batchResult.Object) { continue }
$objName = Get-GraphObjectName $batchResult.Object $batchResult.ObjectType
if($txtSearchPattern -and $objName -match $txtSearchPattern)
{
$objName = $objName -replace $txtSearchPattern, $txtReplacePattern
Set-GraphObjectName $batchResult.Object $batchResult.ObjectType $objName
}
Write-Status "Export $($item.Title): $objName ($($i)/$($total))" -Force
Export-GraphObject $batchResult.Object $batchResult.ObjectType $folder -IsFullObject
$i++
@@ -1667,6 +1699,12 @@ function Start-GraphObjectExport
continue
}
if($txtSearchPattern -and $objName -match $txtSearchPattern)
{
$objName = $objName -replace $txtSearchPattern, $txtReplacePattern
Set-GraphObjectName $obj.Object $obj.ObjectType $objName
}
Write-Status "Export $($item.Title): $objName" -Force
Export-GraphObject $obj.Object $item.ObjectType $folder
}
@@ -2130,6 +2168,12 @@ function Start-GraphObjectImport
Save-Setting "" "ImportNameFilter" $txtNameFilter
if($txtNameFilter) { Write-Log "Name filter: $txtNameFilter" }
$txtSearchPattern = ""
if($global:txtImportNameSearchPattern -ne $null) { $txtSearchPattern = $global:txtImportNameSearchPattern.Text.Trim() }
$txtReplacePattern = ""
if($global:txtImportNameReplacePattern -ne $null) { $txtReplacePattern = $global:txtImportNameReplacePattern.Text.Trim() }
if($txtSearchPattern) { Write-Log "Name mutation: replace '$txtSearchPattern' with '$txtReplacePattern'" }
$allowUpdate = $true
foreach($item in ($script:importObjects | where Selected -eq $true | sort-object -property @{e={$_.ObjectType.ImportOrder}}))
@@ -2176,6 +2220,12 @@ function Start-GraphObjectImport
continue
}
if($txtSearchPattern -and $objName -match $txtSearchPattern)
{
$objName = $objName -replace $txtSearchPattern, $txtReplacePattern
Set-GraphObjectName $fileObj.Object $item.ObjectType $objName
}
if($allowUpdate -and $global:cbImportType.SelectedValue -ne "alwaysImport" -and $graphObjects -and (Reset-GraphObject $fileObj $graphObjects))
{
$importedObjects++
@@ -2928,7 +2978,7 @@ function Add-GroupMigrationObject
{
# Export group info to json file for possible import
$grouspPath = Join-Path $path "Groups"
if(-not (Test-Path $grouspPath)) { mkdir -Path $grouspPath -Force -ErrorAction SilentlyContinue | Out-Null }
if(-not (Test-Path $grouspPath)) { New-Item -ItemType Directory -Path $grouspPath -Force -ErrorAction SilentlyContinue | Out-Null }
$fileName = Join-Path $grouspPath "$((Remove-InvalidFileNameChars $groupObj.displayName)).json"
Save-GraphObjectToFile $groupObj $fileName
}
@@ -2971,7 +3021,7 @@ function Add-GraphMigrationObject
{
# Export group info to json file for possible import
$grouspPath = Join-Path $path "Groups"
if(-not (Test-Path $grouspPath)) { mkdir -Path $grouspPath -Force -ErrorAction SilentlyContinue | Out-Null }
if(-not (Test-Path $grouspPath)) { New-Item -ItemType Directory -Path $grouspPath -Force -ErrorAction SilentlyContinue | Out-Null }
$fileName = Join-Path $grouspPath "$((Remove-InvalidFileNameChars $graphObj.displayName)).json"
Save-GraphObjectToFile $graphObj $fileName
}
@@ -3045,7 +3095,7 @@ function Add-GraphMigrationObjectToFile
Type = $objType
})
if(-not (Test-Path $path)) { mkdir -Path $path -Force -ErrorAction SilentlyContinue | Out-Null }
if(-not (Test-Path $path)) { New-Item -ItemType Directory -Path $path -Force -ErrorAction SilentlyContinue | Out-Null }
ConvertTo-Json $global:migFileObj -Depth 50 | Out-File $migFileName -Force
$true # New object was added
@@ -3099,6 +3149,11 @@ function Get-GraphMigrationObjectsFromFile
Write-Status "Loading migration objects"
$txtSearchPattern = ""
if($global:txtImportNameSearchPattern -ne $null) { $txtSearchPattern = $global:txtImportNameSearchPattern.Text.Trim() }
$txtReplacePattern = ""
if($global:txtImportNameReplacePattern -ne $null) { $txtReplacePattern = $global:txtImportNameReplacePattern.Text.Trim() }
if($global:chkImportAssignments.IsChecked -eq $true)
{
# Only check groups if Assignments are imported
@@ -3108,6 +3163,11 @@ function Get-GraphMigrationObjectsFromFile
if($migObj.Type -like "*group*")
{
$migTableGroupName = $migObj.DisplayName.Trim()
$originalGroupName = $migTableGroupName
if($txtSearchPattern -and $migTableGroupName -match $txtSearchPattern)
{
$migTableGroupName = $migTableGroupName -replace $txtSearchPattern, $txtReplacePattern
}
$obj = (Invoke-GraphRequest "/groups?`$filter=displayName eq '$($migTableGroupName)'").Value
if(-not $obj)
{
@@ -3115,7 +3175,7 @@ function Get-GraphMigrationObjectsFromFile
if($global:GraphMigrationTable)
{
$fi = [IO.FileInfo]$global:GraphMigrationTable
$groupFi = [IO.FileInfo](Join-Path (Join-Path $fi.DirectoryName "Groups") "$((Remove-InvalidFileNameChars $migTableGroupName)).json")
$groupFi = [IO.FileInfo](Join-Path (Join-Path $fi.DirectoryName "Groups") "$((Remove-InvalidFileNameChars $originalGroupName)).json")
}
if($groupFi.Exists -eq $true)
@@ -3133,6 +3193,10 @@ function Get-GraphMigrationObjectsFromFile
Remove-Property $groupObj $prop.Name
}
$groupObj.displayName = $groupObj.displayName.Trim()
if($txtSearchPattern -and $groupObj.displayName -match $txtSearchPattern)
{
$groupObj.displayName = $groupObj.displayName -replace $txtSearchPattern, $txtReplacePattern
}
$groupJson = ConvertTo-Json $groupObj -Depth 50
}
else
@@ -4632,7 +4696,7 @@ function Get-GraphObjectFile
$fileName = "$((Remove-InvalidFileNameChars $fileName)).json"
if($path)
{
$fileName = "$path\$fileName"
$fileName = Join-Path $path $fileName
}
$fileName