release: v4.1.0 — restructure entry points, add CIS baselines, reporting tools and fzf hints

- 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
This commit is contained in:
2026-06-14 15:24:42 +02:00
parent e333af978c
commit d3e0769799
30 changed files with 8711 additions and 175 deletions
@@ -0,0 +1,418 @@
#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
}