d3e0769799
- Restructure launchers: Start-IntuneToolkit.ps1 moves to repo root; Start-HeadlessIntune.ps1 moves to Scripts/; TUI helper moves to Scripts/Private/ - Add AGENTS.md with project architecture, entry points, and security notes - Add CIS M365 baseline assets (CISM365-v7, M365-CIS-Rapid) and reporting scripts - Add Python reporting utilities (Export-SettingsReport, Export-AssignmentReport, Export-ObjectInventoryReport) and CA wizard helpers - Update Deploy-IntuneBaseline.ps1 with Merge conflict resolution, ReportPath, and optimized group loading - Update Initialize-IntuneAuth.ps1 with -RotateSecret and configurable secret expiry - Update Extensions for Settings Catalog definition auto-export - Update README with v4.1.0, new entry points and script catalog - Bump VERSION to 4.1.0 - Harden .gitignore against .DS_Store, __pycache__, .venv-pdf/, local exports, Settings.json and IntuneManagement.log
419 lines
15 KiB
PowerShell
419 lines
15 KiB
PowerShell
#requires -Version 5.1
|
|
<#
|
|
.SYNOPSIS
|
|
Interactive terminal UI for IntuneManagement headless export/import.
|
|
.DESCRIPTION
|
|
Prompts for action, tenant, paths, filters, object types, and toggles.
|
|
Returns a PSCustomObject that Start-HeadlessIntune.ps1 consumes.
|
|
Uses fzf on macOS/Linux when available; falls back to numbered menus.
|
|
#>
|
|
[CmdletBinding()]
|
|
param()
|
|
|
|
$ErrorActionPreference = "Stop"
|
|
|
|
#region Helper functions
|
|
function Test-FzfAvailable
|
|
{
|
|
return [bool](Get-Command fzf -ErrorAction SilentlyContinue)
|
|
}
|
|
|
|
function Show-FzfHint
|
|
{
|
|
if(Test-FzfAvailable) { return }
|
|
Write-Host "[fzf not found]" -ForegroundColor Yellow -NoNewline
|
|
Write-Host " Install fzf for the best interactive menu experience. Falling back to numbered menus." -ForegroundColor DarkGray
|
|
if($IsMacOS)
|
|
{
|
|
Write-Host " Install: brew install fzf" -ForegroundColor DarkGray
|
|
}
|
|
elseif($IsLinux)
|
|
{
|
|
Write-Host " Install: sudo apt install fzf (or dnf/pacman)" -ForegroundColor DarkGray
|
|
}
|
|
else
|
|
{
|
|
Write-Host " Install: winget install junegunn.fzf (or choco install fzf)" -ForegroundColor DarkGray
|
|
}
|
|
Write-Host ""
|
|
}
|
|
|
|
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 Read-YesNo
|
|
{
|
|
param(
|
|
[string]$Prompt,
|
|
[bool]$Default = $false
|
|
)
|
|
$defaultChar = if($Default) { "Y" } else { "N" }
|
|
$response = Read-Host "$Prompt [Y/n] (default: $defaultChar)"
|
|
if([string]::IsNullOrWhiteSpace($response)) { return $Default }
|
|
return $response -like 'y*'
|
|
}
|
|
|
|
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 Load defaults
|
|
$modulePath = Join-Path (Split-Path -Parent (Split-Path -Parent $PSScriptRoot)) "Headless/IntuneManagement.Headless.psd1"
|
|
Import-Module $modulePath -Force
|
|
|
|
$defaultTypes = Get-DefaultIntunePolicyObjectTypes
|
|
$settingsPath = Get-DefaultSettingsPath
|
|
$preloadedTenantId = $null
|
|
if(Test-Path $settingsPath)
|
|
{
|
|
try
|
|
{
|
|
$settings = Get-Content $settingsPath -Raw | ConvertFrom-Json
|
|
if($settings.TenantId) { $preloadedTenantId = $settings.TenantId }
|
|
}
|
|
catch {}
|
|
}
|
|
#endregion
|
|
|
|
Show-FzfHint
|
|
|
|
while($true)
|
|
{
|
|
Clear-Host
|
|
Write-Host "========================================" -ForegroundColor Cyan
|
|
Write-Host " IntuneManagement Terminal UI" -ForegroundColor Cyan
|
|
Write-Host "========================================" -ForegroundColor Cyan
|
|
Write-Host " Press Esc to go back, Space to select" -ForegroundColor DarkGray
|
|
|
|
# 1. Action
|
|
$action = Select-MenuItem -Items @("Export","Import","DeployCISBaseline","GenerateReports") -Header "Select action"
|
|
if(-not $action) { continue }
|
|
|
|
# CIS M365 Baseline deployment flow
|
|
if($action -eq "DeployCISBaseline")
|
|
{
|
|
# 2a. TenantId
|
|
$tenantPrompt = "Enter Tenant ID"
|
|
if($preloadedTenantId) { $tenantPrompt += " (default: $preloadedTenantId)" }
|
|
$tenantId = Read-Host $tenantPrompt
|
|
if([string]::IsNullOrWhiteSpace($tenantId)) { $tenantId = $preloadedTenantId }
|
|
if([string]::IsNullOrWhiteSpace($tenantId)) { Write-Host "Tenant ID is required." -ForegroundColor Red; continue }
|
|
|
|
# 2b. Baseline path
|
|
$defaultBaseline = Join-Path (Split-Path -Parent (Split-Path -Parent $PSScriptRoot)) "Baselines/CISM365-v7-Generated.yaml"
|
|
$baselinePath = Read-Host "Baseline YAML path (default: $defaultBaseline)"
|
|
if([string]::IsNullOrWhiteSpace($baselinePath)) { $baselinePath = $defaultBaseline }
|
|
if(-not (Test-Path $baselinePath)) { Write-Host "Baseline file not found: $baselinePath" -ForegroundColor Red; continue }
|
|
|
|
# 2c. Mode
|
|
$mode = Select-MenuItem -Items @("Assess","Deploy") -Header "Select mode"
|
|
if(-not $mode) { continue }
|
|
|
|
# 2d. Apply (only for Deploy)
|
|
$apply = $false
|
|
if($mode -eq "Deploy")
|
|
{
|
|
$apply = Read-YesNo -Prompt "Apply changes? (No = dry-run report)" -Default $false
|
|
}
|
|
|
|
# 2e. Workloads
|
|
$allWorkloads = @("EntraID","ConditionalAccess","Exchange","SharePoint","Teams","PowerBI","Defender","Purview")
|
|
Write-Host "`nWorkload selection..." -ForegroundColor Cyan
|
|
$workloadSelection = Select-MenuItem -Items $allWorkloads -Header "Select workloads (Space to multi-select, or choose 'all')" -Multi
|
|
if(-not $workloadSelection) { $workloadSelection = $allWorkloads }
|
|
|
|
# 2f. Auth mode
|
|
$authMode = Select-MenuItem -Items @("AppOnly","Browser","DeviceCode") -Header "Select authentication mode"
|
|
if(-not $authMode) { $authMode = "Browser" }
|
|
|
|
# 2g. Review
|
|
Clear-Host
|
|
Write-Host "Review your CIS M365 Baseline deployment:" -ForegroundColor Green
|
|
Write-Host " TenantId : $tenantId"
|
|
Write-Host " Baseline : $baselinePath"
|
|
Write-Host " Mode : $mode"
|
|
if($mode -eq "Deploy") { Write-Host " Apply : $apply" }
|
|
Write-Host " Workloads : $($workloadSelection -join ', ')"
|
|
Write-Host " Auth Mode : $authMode"
|
|
|
|
$confirm = Read-Host "`nProceed? [Y/n] (or type 'back' to restart)"
|
|
if($confirm -eq "back") { continue }
|
|
if(-not ([string]::IsNullOrWhiteSpace($confirm) -or $confirm -match "^\s*y"))
|
|
{
|
|
Write-Host "Cancelled." -ForegroundColor Yellow
|
|
continue
|
|
}
|
|
|
|
$result = [PSCustomObject]@{
|
|
Action = $action
|
|
TenantId = $tenantId
|
|
BaselinePath = $baselinePath
|
|
Mode = $mode
|
|
Apply = $apply
|
|
Workloads = $workloadSelection
|
|
AuthMode = $authMode
|
|
}
|
|
return $result
|
|
}
|
|
|
|
# Generate Reports flow
|
|
if($action -eq "GenerateReports")
|
|
{
|
|
$reportTypes = @("Settings","Assignments","ObjectInventory","All")
|
|
$reportType = Select-MenuItem -Items $reportTypes -Header "Select report type"
|
|
if(-not $reportType) { continue }
|
|
|
|
$dataSource = Select-MenuItem -Items @("Use existing backup","Pull fresh data from tenant") -Header "Data source"
|
|
if(-not $dataSource) { continue }
|
|
|
|
$backupRoot = $null
|
|
$tenantIdForReport = $null
|
|
$exportPath = $null
|
|
|
|
if($dataSource -like "*fresh*")
|
|
{
|
|
$tenantPrompt = "Enter Tenant ID"
|
|
if($preloadedTenantId) { $tenantPrompt += " (default: $preloadedTenantId)" }
|
|
$tenantIdForReport = Read-Host $tenantPrompt
|
|
if([string]::IsNullOrWhiteSpace($tenantIdForReport)) { $tenantIdForReport = $preloadedTenantId }
|
|
if([string]::IsNullOrWhiteSpace($tenantIdForReport)) { Write-Host "Tenant ID is required." -ForegroundColor Red; continue }
|
|
|
|
$exportPath = Read-Host "Export path (where to save fresh data)"
|
|
if([string]::IsNullOrWhiteSpace($exportPath)) { Write-Host "Export path is required." -ForegroundColor Red; continue }
|
|
$backupRoot = $exportPath
|
|
}
|
|
else
|
|
{
|
|
$backupRoot = Read-Host "Backup root path (folder containing 'Settings Catalog', etc.)"
|
|
if([string]::IsNullOrWhiteSpace($backupRoot)) { Write-Host "Backup root is required." -ForegroundColor Red; continue }
|
|
if(-not (Test-Path $backupRoot)) { Write-Host "Path not found: $backupRoot" -ForegroundColor Red; continue }
|
|
}
|
|
|
|
$outputDir = Read-Host "Enter output directory for reports"
|
|
if([string]::IsNullOrWhiteSpace($outputDir)) { Write-Host "Output directory is required." -ForegroundColor Red; continue }
|
|
|
|
$includeAssignmentsInSettings = $false
|
|
if($reportType -in @("Settings","All"))
|
|
{
|
|
$includeAssignmentsInSettings = Read-YesNo -Prompt "Include assignment columns in settings report?" -Default $false
|
|
}
|
|
|
|
Clear-Host
|
|
Write-Host "Review report generation:" -ForegroundColor Green
|
|
Write-Host " Report Type : $reportType"
|
|
Write-Host " Data Source : $dataSource"
|
|
if($dataSource -like "*fresh*")
|
|
{
|
|
Write-Host " Tenant ID : $tenantIdForReport"
|
|
Write-Host " Export Path : $exportPath"
|
|
}
|
|
else
|
|
{
|
|
Write-Host " Backup Root : $backupRoot"
|
|
}
|
|
Write-Host " Output Dir : $outputDir"
|
|
if($reportType -in @("Settings","All"))
|
|
{
|
|
Write-Host " Include Assignments : $includeAssignmentsInSettings"
|
|
}
|
|
|
|
$confirm = Read-Host "`nProceed? [Y/n] (or type 'back' to restart)"
|
|
if($confirm -eq "back") { continue }
|
|
if(-not ([string]::IsNullOrWhiteSpace($confirm) -or $confirm -like 'y*'))
|
|
{
|
|
Write-Host "Cancelled." -ForegroundColor Yellow; continue
|
|
}
|
|
|
|
$result = [PSCustomObject]@{
|
|
Action = "GenerateReports"
|
|
DataSource = $dataSource
|
|
ReportType = $reportType
|
|
BackupRoot = $backupRoot
|
|
OutputDir = $outputDir
|
|
IncludeAssignmentsInSettings = $includeAssignmentsInSettings
|
|
}
|
|
if($dataSource -like "*fresh*")
|
|
{
|
|
$result | Add-Member -NotePropertyName TenantId -NotePropertyValue $tenantIdForReport
|
|
$result | Add-Member -NotePropertyName ExportPath -NotePropertyValue $exportPath
|
|
}
|
|
return $result
|
|
}
|
|
|
|
# 2. TenantId
|
|
$tenantPrompt = "Enter Tenant ID"
|
|
if($preloadedTenantId) { $tenantPrompt += " (default: $preloadedTenantId)" }
|
|
$tenantId = Read-Host $tenantPrompt
|
|
if([string]::IsNullOrWhiteSpace($tenantId)) { $tenantId = $preloadedTenantId }
|
|
if([string]::IsNullOrWhiteSpace($tenantId)) { Write-Host "Tenant ID is required." -ForegroundColor Red; continue }
|
|
|
|
# 3. Object Types
|
|
Write-Host "`nObject type selection..." -ForegroundColor Cyan
|
|
$typeSelection = Select-MenuItem -Items $defaultTypes -Header "Select object types to include (Space to multi-select)" -Multi
|
|
if(-not $typeSelection) { continue }
|
|
|
|
# 4. Path
|
|
$pathPrompt = if($action -eq "Export") { "Enter export root folder path" } else { "Enter import root folder path" }
|
|
$path = Read-Host $pathPrompt
|
|
if([string]::IsNullOrWhiteSpace($path)) { Write-Host "Path is required." -ForegroundColor Red; return $null }
|
|
|
|
# 5. Name Filter
|
|
$nameFilter = Read-Host "Name filter regex (optional, e.g. '^Win-OIB-')"
|
|
|
|
# 6. Name Mutation
|
|
$nameSearchPattern = Read-Host "Name search regex for mutation (optional, e.g. '^Win-OIB-')"
|
|
$nameReplacePattern = $null
|
|
if(-not [string]::IsNullOrWhiteSpace($nameSearchPattern))
|
|
{
|
|
$nameReplacePattern = Read-Host "Replacement string (e.g. 'Win-TEST-')"
|
|
}
|
|
|
|
# 7. Import-specific options
|
|
$importType = $null
|
|
$includeScopeTags = $false
|
|
$replaceDependencyIds = $false
|
|
if($action -eq "Import")
|
|
{
|
|
$importType = Select-MenuItem -Items @("alwaysImport","skipIfExist","replace","replace_with_assignments","update") -Header "Select import behavior"
|
|
if(-not $importType) { $importType = "alwaysImport" }
|
|
$includeScopeTags = Read-YesNo -Prompt "Import scope tags?" -Default $false
|
|
$replaceDependencyIds = Read-YesNo -Prompt "Replace dependency IDs?" -Default $false
|
|
}
|
|
|
|
# 8. Common toggles
|
|
$includeAssignments = Read-YesNo -Prompt "Include assignments?" -Default $false
|
|
$addCompanyName = $false
|
|
if($action -eq "Export")
|
|
{
|
|
$addCompanyName = Read-YesNo -Prompt "Add company name to folders?" -Default $false
|
|
}
|
|
|
|
# 9. Review
|
|
Clear-Host
|
|
Write-Host "Review your selection:" -ForegroundColor Green
|
|
Write-Host " Action : $action"
|
|
Write-Host " TenantId : $tenantId"
|
|
Write-Host " Object Types : $($typeSelection -join ', ')"
|
|
if($action -eq "Export")
|
|
{
|
|
Write-Host " Export Path : $path"
|
|
Write-Host " Add Company Name : $addCompanyName"
|
|
}
|
|
else
|
|
{
|
|
Write-Host " Import Path : $path"
|
|
Write-Host " Import Type : $importType"
|
|
Write-Host " Include Scope Tags : $includeScopeTags"
|
|
Write-Host " Replace Dep IDs : $replaceDependencyIds"
|
|
}
|
|
Write-Host " Name Filter : $(if($nameFilter){$nameFilter}else{'(none)'})"
|
|
Write-Host " Name Search Pattern : $(if($nameSearchPattern){$nameSearchPattern}else{'(none)'})"
|
|
Write-Host " Name Replace Pattern: $(if($nameReplacePattern){$nameReplacePattern}else{'(none)'})"
|
|
Write-Host " Include Assignments : $includeAssignments"
|
|
|
|
$confirm = Read-Host "`nProceed? [Y/n] (or type 'back' to restart)"
|
|
if($confirm -eq "back") { continue }
|
|
if(-not ([string]::IsNullOrWhiteSpace($confirm) -or $confirm -match "^\s*y"))
|
|
{
|
|
Write-Host "Cancelled." -ForegroundColor Yellow
|
|
continue
|
|
}
|
|
|
|
# 10. Build result
|
|
$result = [PSCustomObject]@{
|
|
Action = $action
|
|
TenantId = $tenantId
|
|
ObjectTypes = $typeSelection
|
|
NameFilter = $nameFilter
|
|
NameSearchPattern = $nameSearchPattern
|
|
NameReplacePattern = $nameReplacePattern
|
|
IncludeAssignments = $includeAssignments
|
|
}
|
|
|
|
if($action -eq "Export")
|
|
{
|
|
$result | Add-Member -NotePropertyName ExportPath -NotePropertyValue $path
|
|
$result | Add-Member -NotePropertyName AddCompanyName -NotePropertyValue $addCompanyName
|
|
}
|
|
else
|
|
{
|
|
$result | Add-Member -NotePropertyName ImportPath -NotePropertyValue $path
|
|
$result | Add-Member -NotePropertyName ImportType -NotePropertyValue $importType
|
|
$result | Add-Member -NotePropertyName IncludeScopeTags -NotePropertyValue $includeScopeTags
|
|
$result | Add-Member -NotePropertyName ReplaceDependencyIds -NotePropertyValue $replaceDependencyIds
|
|
}
|
|
|
|
return $result
|
|
}
|