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:
368
Scripts/Export-AssignmentsToCsv.ps1
Normal file
368
Scripts/Export-AssignmentsToCsv.ps1
Normal file
@@ -0,0 +1,368 @@
|
||||
#requires -Version 5.1
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Export Intune policy/app assignments to CSV or Markdown for documentation.
|
||||
.DESCRIPTION
|
||||
Generates a CSV or Markdown report of assignments for selected object types.
|
||||
Useful for documentation, change tracking, and compliance audits.
|
||||
.EXAMPLE
|
||||
./Scripts/Export-AssignmentsToCsv.ps1 -TenantId "..." -Format Csv -OutputPath ./assignments.csv
|
||||
./Scripts/Export-AssignmentsToCsv.ps1 -TenantId "..." -Format Markdown -OutputPath ./assignments.md
|
||||
#>
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$TenantId,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[ValidateSet("Csv","Markdown")]
|
||||
[string]$Format,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$OutputPath,
|
||||
|
||||
[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-FzfMenu
|
||||
{
|
||||
param(
|
||||
[Parameter(Mandatory)]
|
||||
[string[]]$Items,
|
||||
[string]$Header = "Select one",
|
||||
[switch]$Multi
|
||||
)
|
||||
$argsList = @("--header=$Header")
|
||||
if($Multi) { $argsList += "--multi" }
|
||||
$selected = $Items | fzf @argsList --bind=space:toggle
|
||||
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
|
||||
$assignableTypes = @(
|
||||
[PSCustomObject]@{ Title = "Applications"; API = "/deviceAppManagement/mobileApps"; HasIntent = $true; NameProp = "displayName" },
|
||||
[PSCustomObject]@{ Title = "Device Configuration"; API = "/deviceManagement/deviceConfigurations"; HasIntent = $false; NameProp = "displayName" },
|
||||
[PSCustomObject]@{ Title = "Settings Catalog"; API = "/deviceManagement/configurationPolicies"; HasIntent = $false; NameProp = "name" },
|
||||
[PSCustomObject]@{ Title = "Compliance Policies"; API = "/deviceManagement/deviceCompliancePolicies"; HasIntent = $false; NameProp = "displayName" },
|
||||
[PSCustomObject]@{ Title = "Administrative Templates"; API = "/deviceManagement/groupPolicyConfigurations"; HasIntent = $false; NameProp = "displayName" },
|
||||
[PSCustomObject]@{ Title = "Endpoint Security"; API = "/deviceManagement/intents"; HasIntent = $false; NameProp = "displayName" },
|
||||
[PSCustomObject]@{ Title = "App Protection"; API = "/deviceAppManagement/managedAppPolicies"; HasIntent = $false; NameProp = "displayName" },
|
||||
[PSCustomObject]@{ Title = "App Configuration (Device)"; API = "/deviceAppManagement/mobileAppConfigurations"; HasIntent = $false; NameProp = "displayName" },
|
||||
[PSCustomObject]@{ Title = "Platform Scripts"; API = "/deviceManagement/deviceManagementScripts"; HasIntent = $false; NameProp = "displayName" },
|
||||
[PSCustomObject]@{ Title = "macOS Scripts"; API = "/deviceManagement/deviceShellScripts"; HasIntent = $false; NameProp = "displayName" },
|
||||
[PSCustomObject]@{ Title = "Device Health Scripts"; API = "/deviceManagement/deviceHealthScripts"; HasIntent = $false; NameProp = "displayName" },
|
||||
[PSCustomObject]@{ Title = "macOS Custom Attributes"; API = "/deviceManagement/deviceCustomAttributeShellScripts"; HasIntent = $false; NameProp = "displayName" },
|
||||
[PSCustomObject]@{ Title = "Enrollment Restrictions"; API = "/deviceManagement/deviceEnrollmentConfigurations"; HasIntent = $false; NameProp = "displayName" },
|
||||
[PSCustomObject]@{ Title = "Enrollment Status Page"; API = "/deviceManagement/deviceEnrollmentConfigurations"; HasIntent = $false; NameProp = "displayName" },
|
||||
[PSCustomObject]@{ Title = "Autopilot"; API = "/deviceManagement/windowsAutopilotDeploymentProfiles"; HasIntent = $false; NameProp = "displayName" },
|
||||
[PSCustomObject]@{ Title = "Terms and Conditions"; API = "/deviceManagement/termsAndConditions"; HasIntent = $false; NameProp = "displayName" },
|
||||
[PSCustomObject]@{ Title = "Policy Sets"; API = "/deviceAppManagement/policySets"; HasIntent = $false; NameProp = "displayName" },
|
||||
[PSCustomObject]@{ Title = "Update Policies"; API = "/deviceManagement/windowsUpdateForBusinessConfigurations"; HasIntent = $false; NameProp = "displayName" },
|
||||
[PSCustomObject]@{ Title = "Feature Updates"; API = "/deviceManagement/windowsFeatureUpdateProfiles"; HasIntent = $false; NameProp = "displayName" },
|
||||
[PSCustomObject]@{ Title = "Quality Updates"; API = "/deviceManagement/windowsQualityUpdateProfiles"; HasIntent = $false; NameProp = "displayName" },
|
||||
[PSCustomObject]@{ Title = "Device Management Intents"; API = "/deviceManagement/intents"; HasIntent = $false; NameProp = "displayName" }
|
||||
)
|
||||
#endregion
|
||||
|
||||
#region Select types and gather data
|
||||
$typeTitles = $assignableTypes | ForEach-Object { $_.Title }
|
||||
$selectedTypeTitles = Select-MenuItem -Items $typeTitles -Header "Select object types to export (multi-select)" -Multi
|
||||
if(-not $selectedTypeTitles)
|
||||
{
|
||||
Write-Host "No types selected. Exiting." -ForegroundColor Yellow
|
||||
exit 0
|
||||
}
|
||||
|
||||
Write-Host "`nLoading groups for name resolution..." -ForegroundColor Cyan
|
||||
$groupsResponse = Invoke-GraphRequest "/groups?`$select=id,displayName&`$top=999"
|
||||
$groups = $groupsResponse.value
|
||||
|
||||
$reportRows = @()
|
||||
|
||||
foreach($typeTitle in $selectedTypeTitles)
|
||||
{
|
||||
$objectType = $assignableTypes | Where-Object { $_.Title -eq $typeTitle } | Select-Object -First 1
|
||||
Write-Host "`nExporting $($objectType.Title) assignments..." -ForegroundColor Cyan
|
||||
|
||||
try
|
||||
{
|
||||
$objectsResponse = Invoke-GraphRequest "$($objectType.API)?`$select=id,$($objectType.NameProp)&`$orderby=$($objectType.NameProp)"
|
||||
$objects = $objectsResponse.value | Where-Object { $_ }
|
||||
|
||||
foreach($obj in $objects)
|
||||
{
|
||||
try
|
||||
{
|
||||
$assignmentsResponse = Invoke-GraphRequest "$($objectType.API)/$($obj.id)/assignments"
|
||||
foreach($ass in $assignmentsResponse.value)
|
||||
{
|
||||
$targetType = $ass.target."@odata.type"
|
||||
$targetName = "Unknown"
|
||||
$groupId = $ass.target.groupId
|
||||
if($targetType -eq "#microsoft.graph.groupAssignmentTarget")
|
||||
{
|
||||
$grp = $groups | Where-Object { $_.id -eq $groupId } | Select-Object -First 1
|
||||
$targetName = if($grp) { $grp.displayName } else { $groupId }
|
||||
}
|
||||
elseif($targetType -eq "#microsoft.graph.exclusionGroupAssignmentTarget")
|
||||
{
|
||||
$grp = $groups | Where-Object { $_.id -eq $groupId } | Select-Object -First 1
|
||||
$targetName = if($grp) { "Exclude: $($grp.displayName)" } else { "Exclude: $groupId" }
|
||||
}
|
||||
elseif($targetType -eq "#microsoft.graph.allLicensedUsersAssignmentTarget")
|
||||
{
|
||||
$targetName = "All Users"
|
||||
}
|
||||
elseif($targetType -eq "#microsoft.graph.allDevicesAssignmentTarget")
|
||||
{
|
||||
$targetName = "All Devices"
|
||||
}
|
||||
|
||||
$filterName = ""
|
||||
if($ass.target.deviceAndAppManagementAssignmentFilterId)
|
||||
{
|
||||
$filterName = $ass.target.deviceAndAppManagementAssignmentFilterId
|
||||
}
|
||||
|
||||
$intent = ""
|
||||
if($objectType.HasIntent -and $ass.intent)
|
||||
{
|
||||
$intent = $ass.intent
|
||||
}
|
||||
|
||||
$reportRows += [PSCustomObject]@{
|
||||
ObjectType = $objectType.Title
|
||||
ObjectName = if($objectType.NameProp -eq "name") { $obj.name } else { $obj.displayName }
|
||||
ObjectId = $obj.id
|
||||
Target = $targetName
|
||||
TargetType = $targetType
|
||||
Intent = $intent
|
||||
Filter = $filterName
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
# suppress per-object errors
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
Write-Host " WARNING: Could not load objects for $($objectType.Title)" -ForegroundColor DarkYellow
|
||||
}
|
||||
}
|
||||
|
||||
if($reportRows.Count -eq 0)
|
||||
{
|
||||
Write-Host "No assignments found to export. Exiting." -ForegroundColor Yellow
|
||||
exit 0
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Export
|
||||
$OutputPath = (Resolve-Path (Split-Path -Parent $OutputPath) -ErrorAction SilentlyContinue).Path + "/" + (Split-Path -Leaf $OutputPath)
|
||||
|
||||
if($Format -eq "Csv")
|
||||
{
|
||||
$reportRows | Export-Csv -LiteralPath $OutputPath -NoTypeInformation -Encoding utf8 -Force
|
||||
Write-Host "`nExported $($reportRows.Count) rows to CSV: $OutputPath" -ForegroundColor Green
|
||||
}
|
||||
elseif($Format -eq "Markdown")
|
||||
{
|
||||
$md = @()
|
||||
$md += "# Intune Assignments Report"
|
||||
$md += ""
|
||||
$md += "**Tenant:** $($org.value[0].displayName) "
|
||||
$md += "**Generated:** $(Get-Date -Format "yyyy-MM-dd HH:mm") "
|
||||
$md += "**Total Rows:** $($reportRows.Count)"
|
||||
$md += ""
|
||||
|
||||
$grouped = $reportRows | Group-Object -Property ObjectType
|
||||
foreach($g in $grouped)
|
||||
{
|
||||
$md += "## $($g.Name)"
|
||||
$md += ""
|
||||
$md += "| Object | Target | Intent | Filter |"
|
||||
$md += "|--------|--------|--------|--------|"
|
||||
foreach($row in ($g.Group | Sort-Object ObjectName, Target))
|
||||
{
|
||||
$intentCol = if($row.Intent) { $row.Intent } else { "-" }
|
||||
$filterCol = if($row.Filter) { $row.Filter } else { "-" }
|
||||
$md += "| $($row.ObjectName) | $($row.Target) | $intentCol | $filterCol |"
|
||||
}
|
||||
$md += ""
|
||||
}
|
||||
|
||||
$md | Out-File -LiteralPath $OutputPath -Encoding utf8 -Force
|
||||
Write-Host "`nExported $($reportRows.Count) rows to Markdown: $OutputPath" -ForegroundColor Green
|
||||
}
|
||||
#endregion
|
||||
Reference in New Issue
Block a user