Files
macOS_IntuneManagement/Scripts/Start-IntuneToolkit.ps1

459 lines
14 KiB
PowerShell

#requires -Version 5.1
<#
.SYNOPSIS
Unified launcher for the macOS Intune Toolkit.
.DESCRIPTION
Presents a single terminal UI to choose from all available
headless Intune management tools. Passes through common auth parameters.
Press Esc to go back to the menu from any selection.
.EXAMPLE
./Scripts/Start-IntuneToolkit.ps1 -TenantId "contoso.onmicrosoft.com"
#>
[CmdletBinding()]
param(
[string]$TenantId,
[string]$AppId,
[string]$Secret,
[string]$Certificate,
[ValidateSet("AppOnly","Browser","DeviceCode")]
[string]$AuthMode = "AppOnly",
[string]$RedirectUri,
[string]$SettingsFile
)
$ErrorActionPreference = "Stop"
#region Helper functions
function Test-FzfAvailable
{
return [bool](Get-Command fzf -ErrorAction SilentlyContinue)
}
function Show-FzfHint
{
if(Test-FzfAvailable) { return }
Write-Host "`n[fzf not found]" -ForegroundColor Yellow -NoNewline
Write-Host " Install fzf for the best interactive menu experience.`n" -ForegroundColor DarkGray
if($IsMacOS)
{
Write-Host " macOS: brew install fzf" -ForegroundColor DarkGray
}
elseif($IsLinux)
{
Write-Host " Debian/Ubuntu: sudo apt install fzf" -ForegroundColor DarkGray
Write-Host " Fedora: sudo dnf install fzf" -ForegroundColor DarkGray
Write-Host " Arch: sudo pacman -S fzf" -ForegroundColor DarkGray
}
else
{
Write-Host " Windows: winget install junegunn.fzf" -ForegroundColor DarkGray
Write-Host " choco install fzf" -ForegroundColor DarkGray
}
Write-Host " (Falling back to numbered menus for now.)`n" -ForegroundColor DarkGray
}
function Show-FzfMenu
{
param(
[Parameter(Mandatory)]
[string[]]$Items,
[string]$Header = "Select one"
)
$selected = $Items | fzf --header=$Header
if(-not $selected) { return $null }
return $selected
}
function Show-NumberedMenu
{
param(
[Parameter(Mandatory)]
[string[]]$Items,
[string]$Header = "Select one"
)
Write-Host "`n$Header" -ForegroundColor Cyan
for($i=0; $i -lt $Items.Count; $i++)
{
Write-Host " $($i+1). $($Items[$i])"
}
$choice = Read-Host "Enter a number (0 to exit)"
if($choice -eq "0") { return "EXIT" }
$index = [int]$choice - 1
if($index -ge 0 -and $index -lt $Items.Count)
{
return $Items[$index]
}
return $null
}
function Select-MenuItem
{
param(
[Parameter(Mandatory)]
[string[]]$Items,
[string]$Header = "Select one"
)
if(Test-FzfAvailable)
{
return Show-FzfMenu -Items $Items -Header $Header
}
return Show-NumberedMenu -Items $Items -Header $Header
}
#endregion
$projectRoot = Split-Path -Parent $PSScriptRoot
#region Tenant selection
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")
}
function Get-SavedTenants
{
param([string]$SettingsPath)
if(-not (Test-Path $SettingsPath)) { return @() }
try
{
$raw = Get-Content $SettingsPath -Raw -ErrorAction Stop | ConvertFrom-Json -AsHashtable -ErrorAction Stop
$tenants = @()
foreach($key in $raw.Keys)
{
if($key -match '^[0-9a-fA-F]{8}-([0-9a-fA-F]{4}-){3}[0-9a-fA-F]{12}$')
{
$name = $null
if($raw[$key] -is [hashtable] -and $raw[$key].ContainsKey('TenantName'))
{
$name = $raw[$key]['TenantName']
}
elseif($raw[$key] -is [psobject] -and $raw[$key].PSObject.Properties['TenantName'])
{
$name = $raw[$key].TenantName
}
$display = if($name) { "$name ($key)" } else { $key }
$tenants += [PSCustomObject]@{ TenantId = $key; TenantName = $name; Display = $display }
}
}
return $tenants | Sort-Object Display
}
catch
{
return @()
}
}
function Update-TenantNameCache
{
param([string]$SettingsPath, [string]$TenantId, [string]$TenantName)
if(-not (Test-Path $SettingsPath)) { return }
try
{
$raw = Get-Content $SettingsPath -Raw -ErrorAction Stop | ConvertFrom-Json -AsHashtable -ErrorAction Stop
if($raw[$TenantId] -is [hashtable])
{
$raw[$TenantId]['TenantName'] = $TenantName
}
else
{
$raw[$TenantId] = @{ TenantName = $TenantName }
}
$raw | ConvertTo-Json -Depth 10 | Set-Content -Path $SettingsPath -Force
}
catch { }
}
function Resolve-TenantName
{
param([string]$TenantId, [string]$SettingsPath)
$settingsObj = $null
try
{
$settingsObj = Get-Content $SettingsPath -Raw -ErrorAction Stop | ConvertFrom-Json -AsHashtable -ErrorAction Stop
}
catch { return $null }
$tenantNode = $settingsObj[$TenantId]
if(-not $tenantNode) { return $null }
$appId = $tenantNode['GraphAzureAppId']
if(-not $appId) { return $null }
$secret = $tenantNode['GraphAzureAppSecret']
$cert = $tenantNode['GraphAzureAppCert']
if(-not $secret -and $IsMacOS)
{
try
{
$keychainSecret = security find-generic-password -a "IntuneManagement" -s "IntuneMgmt-$AppId" -w 2>$null
if($keychainSecret) { $secret = $keychainSecret }
}
catch { }
}
$runtimeModule = Join-Path $projectRoot "Runtime/IntuneManagement.Runtime.psd1"
if(-not (Test-Path $runtimeModule)) { return $null }
$invokeParams = @{
Silent = $true
JSonSettings = $true
JSonFile = $SettingsPath
TenantId = $TenantId
AppId = $appId
AuthMode = "AppOnly"
}
if($secret) { $invokeParams.Secret = $secret }
elseif($cert) { $invokeParams.Certificate = $cert }
try
{
Import-Module $runtimeModule -Force | Out-Null
Initialize-IntuneManagementRuntime -View "IntuneGraphAPI" @invokeParams | Out-Null
if(Get-Command Invoke-GraphRequest -ErrorAction SilentlyContinue)
{
$org = Invoke-GraphRequest "/organization" -ErrorAction Stop
if($org.value -and $org.value[0].displayName)
{
return $org.value[0].displayName
}
}
}
catch { }
return $null
}
$settingsPath = $SettingsFile
if(-not $settingsPath) { $settingsPath = Get-DefaultSettingsPath }
if(-not $TenantId)
{
$tenants = Get-SavedTenants -SettingsPath $settingsPath
$tenantOptions = @()
foreach($t in $tenants)
{
$tenantOptions += $t.Display
}
$tenantOptions += "[+ Onboard new tenant]"
$tenantOptions += "[Exit]"
$selectedTenantDisplay = Select-MenuItem -Items $tenantOptions -Header "Select a tenant"
if(-not $selectedTenantDisplay -or $selectedTenantDisplay -eq "[Exit]")
{
exit 0
}
elseif($selectedTenantDisplay -eq "[+ Onboard new tenant]")
{
$TenantId = Read-Host "Enter the new Tenant ID (GUID)"
if(-not $TenantId)
{
Write-Host "No tenant ID provided. Exiting." -ForegroundColor Yellow
exit 0
}
$initPath = Join-Path $projectRoot "Scripts/Initialize-IntuneAuth.ps1"
& $initPath -TenantId $TenantId
Write-Host "`nOnboarding complete. Restarting launcher..." -ForegroundColor Green
Start-Sleep -Seconds 1
& $PSCommandPath
exit 0
}
else
{
$TenantId = $selectedTenantDisplay -replace '.*\(([0-9a-fA-F]{8}-([0-9a-fA-F]{4}-){3}[0-9a-fA-F]{12})\)$', '$1'
if(-not $TenantId)
{
$TenantId = $selectedTenantDisplay
}
}
}
$currentTenant = (Get-SavedTenants -SettingsPath $settingsPath) | Where-Object { $_.TenantId -eq $TenantId } | Select-Object -First 1
if(-not $currentTenant -or -not $currentTenant.TenantName)
{
Write-Host "`nResolving tenant name..." -ForegroundColor Cyan
$resolvedName = Resolve-TenantName -TenantId $TenantId -SettingsPath $settingsPath
if($resolvedName)
{
Update-TenantNameCache -SettingsPath $settingsPath -TenantId $TenantId -TenantName $resolvedName
Write-Host "Cached tenant name: $resolvedName" -ForegroundColor Green
$currentTenant = [PSCustomObject]@{ TenantId = $TenantId; TenantName = $resolvedName; Display = "$resolvedName ($TenantId)" }
}
}
#endregion
Show-FzfHint
# Build common parameter hashtable
$commonParams = @{
TenantId = $TenantId
AppId = $AppId
Secret = $Secret
Certificate = $Certificate
AuthMode = $AuthMode
RedirectUri = $RedirectUri
SettingsFile = $SettingsFile
}
$menuItems = @(
"15. Delete tenant auth and app registration"
"14. Delete local tenant auth only"
"13. Refresh tenant names"
"12. Initialize auth (one-time setup)"
"11. Deploy baseline (dry-run / WhatIf)"
"10. Deploy baseline"
"9. Bulk device operations"
"8. Bulk rename policies"
"7. Export assignments to CSV/Markdown"
"6. Restore assignments"
"5. Backup assignments"
"4. Bulk assignment manager (policies)"
"3. Bulk app assignment"
"2. Import policies"
"1. Export policies"
"0. Exit"
)
while($true)
{
Clear-Host
Write-Host "========================================" -ForegroundColor Cyan
Write-Host " macOS Intune Toolkit" -ForegroundColor Cyan
Write-Host "========================================" -ForegroundColor Cyan
if($currentTenant -and $currentTenant.TenantName)
{
Write-Host " Tenant: $($currentTenant.TenantName) ($TenantId)" -ForegroundColor Green
}
else
{
Write-Host " Tenant: $TenantId" -ForegroundColor Green
}
Write-Host " Press Esc to go back, Space to select" -ForegroundColor DarkGray
$selection = Select-MenuItem -Items $menuItems -Header "Select a tool to launch"
if(-not $selection)
{
continue
}
if($selection -eq "EXIT" -or $selection -like "*0. Exit*")
{
Write-Host "`nExiting. Goodbye!" -ForegroundColor Yellow
exit 0
}
$choiceNumber = [int]($selection -replace "^(\d+)\..*$", '$1')
switch($choiceNumber)
{
1 { $script = "Start-HeadlessIntune.ps1"; $commonParams.Interactive = $true }
2 { $script = "Start-HeadlessIntune.ps1"; $commonParams.Interactive = $true }
3 { $script = "Scripts/Bulk-AppAssignment.ps1" }
4 { $script = "Scripts/Bulk-AssignmentManager.ps1" }
5 { $script = "Scripts/Backup-Restore-Assignments.ps1"; $commonParams.Mode = "Backup" }
6 { $script = "Scripts/Backup-Restore-Assignments.ps1"; $commonParams.Mode = "Restore" }
7 { $script = "Scripts/Export-AssignmentsToCsv.ps1" }
8 { $script = "Scripts/Bulk-RenamePolicies.ps1" }
9 { $script = "Scripts/Bulk-DeviceOperations.ps1" }
10 { $script = "Scripts/Deploy-IntuneBaseline.ps1" }
11 { $script = "Scripts/Deploy-IntuneBaseline.ps1"; $commonParams.WhatIf = $true }
12 { $script = "Scripts/Initialize-IntuneAuth.ps1" }
13 { $script = $null }
14 { $script = "Scripts/Initialize-IntuneAuth.ps1" }
15 { $script = "Scripts/Initialize-IntuneAuth.ps1" }
default { continue }
}
# Clear any mode-specific params from previous loop iteration
$commonParams.Remove("Interactive")
$commonParams.Remove("Mode")
$commonParams.Remove("WhatIf")
switch($choiceNumber)
{
1 { $commonParams.Interactive = $true }
2 { $commonParams.Interactive = $true }
5 { $commonParams.Mode = "Backup" }
6 { $commonParams.Mode = "Restore" }
11 { $commonParams.WhatIf = $true }
}
if($choiceNumber -eq 13)
{
Write-Host "`nRefreshing tenant names..." -ForegroundColor Cyan
$tenantsToRefresh = Get-SavedTenants -SettingsPath $settingsPath
$refreshed = 0
$failed = 0
foreach($t in $tenantsToRefresh)
{
Write-Host " Resolving $($t.TenantId) ..." -ForegroundColor DarkGray -NoNewline
$name = Resolve-TenantName -TenantId $t.TenantId -SettingsPath $settingsPath
if($name)
{
Update-TenantNameCache -SettingsPath $settingsPath -TenantId $t.TenantId -TenantName $name
Write-Host " -> $name" -ForegroundColor Green
$refreshed++
if($t.TenantId -eq $TenantId)
{
$currentTenant = [PSCustomObject]@{ TenantId = $TenantId; TenantName = $name; Display = "$name ($TenantId)" }
}
}
else
{
Write-Host " -> FAILED" -ForegroundColor Red
$failed++
}
}
Write-Host "`nRefresh complete. Success: $refreshed, Failed: $failed" -ForegroundColor Cyan
Write-Host "`nPress any key to return to the menu..." -ForegroundColor DarkGray
$null = $Host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown")
continue
}
$scriptPath = Join-Path $projectRoot $script
if(-not (Test-Path $scriptPath))
{
throw "Script not found: $scriptPath"
}
Write-Host "`nLaunching $script ...`n" -ForegroundColor Green
# Clone params and sanitize for scripts that don't accept the full auth set
$launchParams = $commonParams.Clone()
if($script -eq "Scripts/Initialize-IntuneAuth.ps1")
{
@("AppId","Secret","Certificate","AuthMode","RedirectUri","Interactive","Mode","WhatIf") | ForEach-Object { $launchParams.Remove($_) }
}
if($choiceNumber -eq 14)
{
$launchParams.Delete = $true
}
if($choiceNumber -eq 15)
{
$launchParams.DeleteApp = $true
}
# Execute in same process so TUI flows naturally
& $scriptPath @launchParams
if($choiceNumber -eq 14 -or $choiceNumber -eq 15)
{
Write-Host "`nTenant auth deleted. Exiting." -ForegroundColor Yellow
exit 0
}
Write-Host "`nPress any key to return to the menu..." -ForegroundColor DarkGray
$null = $Host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown")
}