6 Commits

Author SHA1 Message Date
tomas.kracmar 60a655e6d4 refactor(tui): reorder main menu into logical groups
Menu had grown by prepending each new feature to the top, so items
had no relation to their neighbors. Grouped into Export/Import, Bulk
operations, Baselines & compliance, Reporting, Tenant & auth admin,
with Exit last. Item numbers (used for dispatch) unchanged, only
display order moved.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-07 18:51:18 +02:00
tomas.kracmar 81c5e96fc7 feat(tui): add fzf-based folder browser for export/import path prompts
Read-Host has no tab-completion (that's a top-level PSReadLine feature,
not available to script prompts), so reuse the fzf dependency already
required for menus to browse folders interactively instead: navigate
into subfolders, go up with .., confirm current folder, or type a path
manually. Falls back to plain Read-Host when fzf isn't installed.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-07 11:53:05 +02:00
tomas.kracmar 30170a1909 fix(tui): stop re-asking action when launcher already picked Export/Import
Same missing-passthrough bug as the earlier tenant re-prompt fix, this time
for -Action: Start-IntuneManagementTui.ps1 always showed the
Export/Import/DeployCISBaseline/GenerateReports picker even when the
launcher's menu items 1/2 had already decided it. Launcher now passes
-Action through, TUI skips the prompt when supplied.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-07 11:37:35 +02:00
tomas.kracmar ae71f8458d feat(toolkit): add bulk delete tool, fix TUI exit hang and tenant re-prompt
- New Scripts/Bulk-DeletePolicies.ps1: type-DELETE confirmation, -WhatIf dry-run
- Fix infinite loop in Start-IntuneToolkit.ps1 when fzf missing (numbered-menu
  Exit returned "EXIT" but check only matched fzf's "[Exit]" string)
- Start-IntuneManagementTui.ps1 no longer re-prompts for TenantId when the
  launcher already resolved one; also fixes misleading "regex" wording on
  the Name filter prompt (it's a literal substring match)

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-07 11:31:22 +02:00
tomas.kracmar 2f0d937ca1 fix(ca-exporter): resolve AuthenticationFlows TransferMethods and surface in Excel
Raw object dump replaced with TransferMethods extraction (falls back to
AdditionalProperties for older module versions). Field added to the
Conditions section of Excel detail sheets.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-27 09:53:42 +02:00
tomas.kracmar b10258c5b4 fix(ca-exporter): pre-fetch auth context class references to fix ID resolution
Replaced per-policy Graph API calls with a single pre-fetch of all
authentication context class references into a hashtable, consistent
with every other lookup in the script. Also fixes silent ID drops when
DisplayName was missing — GetOrDefault now falls back to the raw ID.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-27 09:49:48 +02:00
8 changed files with 537 additions and 62 deletions
+38
View File
@@ -1,5 +1,43 @@
# macOS Intune Toolkit Changelog
## 2026-08-07 — v4.4.1 — Reorder TUI menu into logical groups
### Changed
- **`Start-IntuneToolkit.ps1`**
- Menu had grown by prepending each new feature to the top, so items ended up in reverse-add order with no relation between neighbors. Reordered into groups — Export/Import, Bulk operations, Baselines & compliance, Reporting, Tenant & auth admin — with Exit last. Item numbers (used for dispatch) are unchanged, only display order moved, so no other script or doc references needed updating.
## 2026-08-07 — v4.4.0 — Folder browser for Export/Import path prompts
### Added
- **`Scripts/Private/Start-IntuneManagementTui.ps1`**
- New `Select-FolderPath` helper: when `fzf` is available, the Export/Import root folder prompt (and the report flow's export/backup-root/output-dir prompts) becomes an interactive folder browser — navigate into subfolders, go up with `..`, confirm the current folder, or type a path manually. `Read-Host` alone has no tab-completion (that's a top-level PSReadLine feature, not available to script prompts), so this reuses the `fzf` dependency already required for menus instead of building custom completion.
- Falls back to a plain `Read-Host` prompt when `fzf` isn't installed — no regression from prior behavior.
## 2026-08-07 — v4.3.1 — Fix redundant action prompt
### Fixed
- **`Scripts/Private/Start-IntuneManagementTui.ps1`**
- Choosing **1. Export policies** or **2. Import policies** in the launcher still re-asked "Select action" (Export/Import/DeployCISBaseline/GenerateReports) — the same missing-passthrough issue as the tenant re-prompt fix below, just for `-Action`. Now accepts `-Action` and skips the prompt when it's already supplied.
- **`Start-IntuneToolkit.ps1`** / **`Scripts/Start-HeadlessIntune.ps1`**
- Menu items 1/2 now pass `-Action Export` / `-Action Import` through to the headless script and on to the TUI.
## 2026-08-07 — v4.3.0 — Bulk delete tool and TUI fixes
### Added
- **`Scripts/Bulk-DeletePolicies.ps1`**
- Select object type, filter by name, multi-select objects, and delete them from the tenant.
- Requires typing `DELETE` to confirm since the operation is irreversible. Supports `-WhatIf` dry-run.
- Wired into `Start-IntuneToolkit.ps1` as menu item **21. Bulk delete policies**.
### Fixed
- **`Start-IntuneToolkit.ps1`**
- Fixed an infinite loop when `fzf` is not installed: the numbered-menu fallback returned `"EXIT"` for the tenant-selection Exit option, but the check only matched the fzf path's `"[Exit]"` string, so choosing Exit was silently ignored and the tenant became the literal string `"EXIT"`.
- **`Scripts/Private/Start-IntuneManagementTui.ps1`**
- The interactive Export/Import/GenerateReports/DeployCISBaseline flow always re-prompted for a Tenant ID even when the tenant was already selected in the outer launcher, because it was invoked with no parameters and had an empty `param()` block. It now accepts `-TenantId` and skips its three tenant prompts when the value is already supplied.
- Clarified the "Name filter" prompt text — it's a plain case-insensitive substring match, not a regex (the underlying match escapes the value via `[RegEx]::Escape()`), so the old `'^Win-OIB-'` example was misleading.
- **`Scripts/Start-HeadlessIntune.ps1`**
- Passes `-TenantId` through to `Start-IntuneManagementTui.ps1` so the fix above takes effect.
## 2026-06-23 — v4.2.0 — Entra directory role membership export
### Added
+2 -1
View File
@@ -2,7 +2,7 @@
Cross-platform, headless Intune policy export/import with PowerShell.
**Current version:** `4.2.0` — see [`CHANGELOG_macOS_IntuneToolkit.md`](CHANGELOG_macOS_IntuneToolkit.md) for recent changes.
**Current version:** `4.4.1` — see [`CHANGELOG_macOS_IntuneToolkit.md`](CHANGELOG_macOS_IntuneToolkit.md) for recent changes.
This repository is now CLI-first. The old WPF application surface has been removed from the repo. The supported workflow is:
@@ -146,6 +146,7 @@ pwsh ./Scripts/Start-HeadlessIntune.ps1 `
* **Bulk assignments** — [`Bulk-AssignmentManager.ps1`](Scripts/Bulk-AssignmentManager.ps1) adds or removes assignments for any policy type using the bulk `/assign` endpoint. [`Bulk-AppAssignment.ps1`](Scripts/Bulk-AppAssignment.ps1) does the same for applications.
* **Backup / restore assignments** — [`Backup-Restore-Assignments.ps1`](Scripts/Backup-Restore-Assignments.ps1) saves assignments to JSON and can restore them with cross-tenant group name resolution.
* **Bulk rename** — [`Bulk-RenamePolicies.ps1`](Scripts/Bulk-RenamePolicies.ps1) performs search/replace or prefix mutations across policy names and descriptions.
* **Bulk delete** — [`Bulk-DeletePolicies.ps1`](Scripts/Bulk-DeletePolicies.ps1) deletes selected policies/apps by object type and name filter, requires typing `DELETE` to confirm, supports `-WhatIf`.
* **Device operations** — [`Bulk-DeviceOperations.ps1`](Scripts/Bulk-DeviceOperations.ps1) supports delete, retire, wipe, lock, and sync with `-WhatIf` safeguards.
* **Assignment documentation** — [`Export-AssignmentsToCsv.ps1`](Scripts/Export-AssignmentsToCsv.ps1) exports assignments to CSV and Markdown.
* **Reporting utilities** — [`Export-SettingsReport.py`](Scripts/Export-SettingsReport.py), [`Export-AssignmentReport.py`](Scripts/Export-AssignmentReport.py), and [`Export-ObjectInventoryReport.py`](Scripts/Export-ObjectInventoryReport.py) generate CSV/Markdown reports from local exports.
+354
View File
@@ -0,0 +1,354 @@
#requires -Version 5.1
<#
.SYNOPSIS
Bulk delete Intune policies/apps by object type and name filter.
.DESCRIPTION
Select an object type, optionally filter by name, multi-select objects,
and delete them from the tenant. Requires typing DELETE to confirm since
this is irreversible. Supports -WhatIf dry-run.
.EXAMPLE
./Scripts/Bulk-DeletePolicies.ps1 -TenantId "contoso.onmicrosoft.com" -WhatIf
#>
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]$TenantId,
[string]$AppId,
[string]$Secret,
[string]$Certificate,
[ValidateSet("AppOnly","Browser","DeviceCode")]
[string]$AuthMode = "AppOnly",
[string]$RedirectUri,
[string]$SettingsFile,
[switch]$WhatIf
)
$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
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 (deletable types)
$deletableTypes = @(
[PSCustomObject]@{ Title = "Applications"; API = "/deviceAppManagement/mobileApps"; NameProp = "displayName" },
[PSCustomObject]@{ Title = "Device Configuration"; API = "/deviceManagement/deviceConfigurations"; NameProp = "displayName" },
[PSCustomObject]@{ Title = "Settings Catalog"; API = "/deviceManagement/configurationPolicies"; NameProp = "name" },
[PSCustomObject]@{ Title = "Compliance Policies"; API = "/deviceManagement/deviceCompliancePolicies"; NameProp = "displayName" },
[PSCustomObject]@{ Title = "Administrative Templates"; API = "/deviceManagement/groupPolicyConfigurations"; NameProp = "displayName" },
[PSCustomObject]@{ Title = "Endpoint Security"; API = "/deviceManagement/intents"; NameProp = "displayName" },
[PSCustomObject]@{ Title = "App Protection"; API = "/deviceAppManagement/managedAppPolicies"; NameProp = "displayName" },
[PSCustomObject]@{ Title = "App Configuration (Device)"; API = "/deviceAppManagement/mobileAppConfigurations"; NameProp = "displayName" },
[PSCustomObject]@{ Title = "Platform Scripts"; API = "/deviceManagement/deviceManagementScripts"; NameProp = "displayName" },
[PSCustomObject]@{ Title = "macOS Scripts"; API = "/deviceManagement/deviceShellScripts"; NameProp = "displayName" },
[PSCustomObject]@{ Title = "Device Health Scripts"; API = "/deviceManagement/deviceHealthScripts"; NameProp = "displayName" },
[PSCustomObject]@{ Title = "macOS Custom Attributes"; API = "/deviceManagement/deviceCustomAttributeShellScripts"; NameProp = "displayName" },
[PSCustomObject]@{ Title = "Enrollment Restrictions"; API = "/deviceManagement/deviceEnrollmentConfigurations"; NameProp = "displayName" },
[PSCustomObject]@{ Title = "Autopilot"; API = "/deviceManagement/windowsAutopilotDeploymentProfiles"; NameProp = "displayName" },
[PSCustomObject]@{ Title = "Terms and Conditions"; API = "/deviceManagement/termsAndConditions"; NameProp = "displayName" },
[PSCustomObject]@{ Title = "Policy Sets"; API = "/deviceAppManagement/policySets"; NameProp = "displayName" },
[PSCustomObject]@{ Title = "Update Policies"; API = "/deviceManagement/windowsUpdateForBusinessConfigurations"; NameProp = "displayName" },
[PSCustomObject]@{ Title = "Feature Updates"; API = "/deviceManagement/windowsFeatureUpdateProfiles"; NameProp = "displayName" },
[PSCustomObject]@{ Title = "Quality Updates"; API = "/deviceManagement/windowsQualityUpdateProfiles"; NameProp = "displayName" },
[PSCustomObject]@{ Title = "Device Management Intents"; API = "/deviceManagement/intents"; NameProp = "displayName" }
)
#endregion
Clear-Host
Write-Host "========================================" -ForegroundColor Cyan
Write-Host " Intune Bulk Delete Tool" -ForegroundColor Cyan
Write-Host "========================================" -ForegroundColor Cyan
#region Select object type
$typeTitles = $deletableTypes | ForEach-Object { $_.Title }
$selectedTypeTitle = Select-MenuItem -Items $typeTitles -Header "Select object type"
if(-not $selectedTypeTitle) { Write-Host "Cancelled." -ForegroundColor Yellow; exit 0 }
$objectType = $deletableTypes | Where-Object { $_.Title -eq $selectedTypeTitle } | Select-Object -First 1
#endregion
#region Load objects
Write-Host "`nLoading $($objectType.Title) objects..." -ForegroundColor Cyan
$api = "$($objectType.API)?`$select=id,$($objectType.NameProp)&`$orderby=$($objectType.NameProp)"
$objectsResponse = Invoke-GraphRequest $api -AllPages
$objects = $objectsResponse.value | Where-Object { $_ } | Sort-Object $objectType.NameProp
Write-Host "Found $($objects.Count) objects." -ForegroundColor Green
$filter = Read-Host "`nFilter by current name (optional, press Enter to skip)"
if(-not [string]::IsNullOrWhiteSpace($filter))
{
$objects = $objects | Where-Object { $_."$($objectType.NameProp)" -like "*$filter*" }
Write-Host "Filtered to $($objects.Count) objects." -ForegroundColor Green
}
if($objects.Count -eq 0)
{
Write-Host "No objects found. Exiting." -ForegroundColor Yellow
exit 0
}
$objectDisplays = $objects | ForEach-Object { "$($_."$($objectType.NameProp)") [$($_.id)]" }
$selectedDisplays = Select-MenuItem -Items $objectDisplays -Header "Select objects to DELETE (multi-select)" -Multi
if(-not $selectedDisplays)
{
Write-Host "No objects selected. Exiting." -ForegroundColor Yellow
exit 0
}
$selectedObjects = @()
foreach($disp in $selectedDisplays)
{
$id = $disp -replace '.*\[(.*?)\]$', '$1'
$obj = $objects | Where-Object { $_.id -eq $id } | Select-Object -First 1
if($obj) { $selectedObjects += $obj }
}
#endregion
#region Confirm
Write-Host "`nThe following $($selectedObjects.Count) $($objectType.Title) object(s) will be PERMANENTLY DELETED:" -ForegroundColor Red
foreach($obj in $selectedObjects)
{
Write-Host " - $($obj."$($objectType.NameProp)") [$($obj.id)]" -ForegroundColor DarkGray
}
if(-not $WhatIf)
{
$typed = Read-Host "`nThis cannot be undone. Type DELETE to confirm"
if($typed -cne "DELETE")
{
Write-Host "Confirmation text did not match. Cancelled." -ForegroundColor Yellow
exit 0
}
}
#endregion
#region Execute
$success = 0
$failed = 0
foreach($obj in $selectedObjects)
{
$name = $obj."$($objectType.NameProp)"
try
{
if($WhatIf)
{
Write-Host " WHATIF: Would delete $name [$($obj.id)]" -ForegroundColor Magenta
$success++
}
else
{
$maxRetries = 3
$retryDelay = 2
$deleted = $false
for($r = 1; $r -le $maxRetries; $r++)
{
try
{
$null = Invoke-GraphRequest "$($objectType.API)/$($obj.id)" -HttpMethod DELETE
Write-Host " OK: Deleted '$name'" -ForegroundColor Green
$success++
$deleted = $true
break
}
catch
{
$statusCode = $_.Exception.Response.StatusCode
if($r -lt $maxRetries -and ($statusCode -ge 500 -or $statusCode -eq 429))
{
Write-Host " Retry $r/$maxRetries after $retryDelay`s (HTTP $statusCode)..." -ForegroundColor DarkYellow
Start-Sleep -Seconds $retryDelay
}
else
{
throw
}
}
}
if(-not $deleted) { throw "Delete failed after $maxRetries attempts." }
}
}
catch
{
Write-Host " ERROR: Failed to delete '$name'. $($_.Exception.Message)" -ForegroundColor Red
$failed++
}
}
Write-Host "`n========================================" -ForegroundColor Cyan
Write-Host " Bulk Delete Complete" -ForegroundColor Cyan
Write-Host "========================================" -ForegroundColor Cyan
Write-Host " Success : $success"
Write-Host " Failed : $failed"
#endregion
@@ -393,6 +393,14 @@ try {
$termsOfUseAgreements = @{}
}
# Authentication context class references
try {
$authContextClassReferences = Get-MgIdentityConditionalAccessAuthenticationContextClassReference -All -ErrorAction Stop | Group-Object -Property Id -AsHashTable
} catch {
Write-Warning "Authentication context class references could not be retrieved. Context names will not be resolved."
$authContextClassReferences = @{}
}
# GSA network filtering profiles
try {
$networkFilteringProfiles = Invoke-MgGraphRequest -Uri 'https://graph.microsoft.com/beta/networkAccess/filteringProfiles' -Method GET -OutputType PSObject -ErrorAction Stop |
@@ -441,15 +449,8 @@ foreach ($policy in $conditionalAccessPolicies) {
@($policy.Conditions?.ClientApplications?.IncludeServicePrincipals) | ForEach-Object { $includeServicePrincipals.Add($servicePrincipals.GetOrDefault($_, $_)) }
@($policy.Conditions?.ClientApplications?.ExcludeServicePrincipals) | ForEach-Object { $excludeServicePrincipals.Add($servicePrincipals.GetOrDefault($_, $_)) }
$includeAuthenticationContext = [System.Collections.Generic.List[Object]]::new()
@($policy.Conditions?.Applications?.IncludeAuthenticationContextClassReferences) | ForEach-Object {
try {
$context = Get-MgIdentityConditionalAccessAuthenticationContextClassReference -Filter "Id eq '$PSItem'" -ErrorAction Stop
if ($context.DisplayName) { $includeAuthenticationContext.Add($context.DisplayName) }
} catch {
$includeAuthenticationContext.Add($PSItem)
}
}
$includeAuthenticationContext = @($policy.Conditions?.Applications?.IncludeAuthenticationContextClassReferences) |
ForEach-Object { $authContextClassReferences.GetOrDefault($_, $_) }
$includeLocations = @($policy.Conditions?.Locations?.IncludeLocations) | ForEach-Object { $namedLocations.GetOrDefault($_, $_) }
$excludeLocations = @($policy.Conditions?.Locations?.ExcludeLocations) | ForEach-Object { $namedLocations.GetOrDefault($_, $_) }
@@ -493,7 +494,10 @@ foreach ($policy in $conditionalAccessPolicies) {
$includeGuestsOrExternalUserTypes = $policy.Conditions?.Users?.IncludeGuestsOrExternalUsers?.guestOrExternalUserTypes
$includeGuestsOrExternalUserTenants = @($policy.Conditions?.Users?.IncludeGuestsOrExternalUsers?.externalTenants?.AdditionalProperties?['members'])
$authenticationFlows = @($policy.Conditions?.AuthenticationFlows)
$authFlowsObj = $policy.Conditions?.AuthenticationFlows
$authenticationFlows = if ($authFlowsObj) {
$authFlowsObj.TransferMethods ?? $authFlowsObj.AdditionalProperties?['transferMethods']
}
$applicationsAdditional = $null
if ($policy.Conditions?.Applications?.AdditionalProperties) {
@@ -552,7 +556,7 @@ foreach ($policy in $conditionalAccessPolicies) {
ExcludeDeviceStates = (@($excludeDeviceStates) -join $separator)
IncludeGuestsOrExternalUserTypes = $includeGuestsOrExternalUserTypes
IncludeGuestOrExternalUserTenants = (@($includeGuestsOrExternalUserTenants) -join $separator)
AuthenticationFlows = (@($authenticationFlows) -join $separator)
AuthenticationFlows = $authenticationFlows
ApplicationsAdditional = $applicationsAdditional
ConditionsAdditional = $conditionsAdditional
IncludeServicePrincipals = ($includeServicePrincipals -join $separator)
@@ -647,7 +651,8 @@ if ($ExportExcel) {
@{ Label = 'Device filter mode'; Col = 'DeviceFilterMode' },
@{ Label = 'Device filter rule'; Col = 'DeviceFilterRule' },
@{ Label = 'Sign-in risk'; Col = 'SignInRiskLevels' },
@{ Label = 'User risk'; Col = 'UserRiskLevels' }
@{ Label = 'User risk'; Col = 'UserRiskLevels' },
@{ Label = 'Authentication flows'; Col = 'AuthenticationFlows' }
)},
@{ Title = 'Grant'; Fields = @(
@{ Label = 'Operator'; Col = 'GrantControlsOperator' },
+76 -7
View File
@@ -8,7 +8,10 @@
Uses fzf on macOS/Linux when available; falls back to numbered menus.
#>
[CmdletBinding()]
param()
param(
[string]$TenantId,
[string]$Action
)
$ErrorActionPreference = "Stop"
@@ -104,6 +107,41 @@ function Select-MenuItem
return Show-NumberedMenu -Items $Items -Header $Header -Multi:$Multi
}
function Select-FolderPath
{
param(
[Parameter(Mandatory)]
[string]$Prompt,
[string]$StartPath = (Get-Location).Path
)
if(-not (Test-FzfAvailable))
{
return Read-Host $Prompt
}
$current = if(Test-Path -LiteralPath $StartPath) { (Resolve-Path -LiteralPath $StartPath).Path } else { (Get-Location).Path }
while($true)
{
$entries = @("[Use this folder]", "[Type path manually]")
$parent = Split-Path -Path $current -Parent
if($parent) { $entries += ".." }
$subdirs = Get-ChildItem -LiteralPath $current -Directory -ErrorAction SilentlyContinue | Sort-Object Name | ForEach-Object { "$($_.Name)/" }
$entries += $subdirs
$choice = $entries | fzf --header="$Prompt [current: $current]"
if(-not $choice) { return $null }
switch ($choice)
{
"[Use this folder]" { return $current }
"[Type path manually]" { $typed = Read-Host "Enter path"; if(-not [string]::IsNullOrWhiteSpace($typed)) { return $typed }; continue }
".." { $current = $parent; continue }
default { $current = Join-Path $current ($choice.TrimEnd('/')); continue }
}
}
}
function Read-YesNo
{
param(
@@ -157,18 +195,33 @@ while($true)
Write-Host " Press Esc to go back, Space to select" -ForegroundColor DarkGray
# 1. Action
if($Action)
{
$action = $Action
}
else
{
$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
if($TenantId)
{
$tenantId = $TenantId
Write-Host "Tenant: $tenantId" -ForegroundColor DarkGray
}
else
{
$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"
@@ -242,25 +295,33 @@ while($true)
$exportPath = $null
if($dataSource -like "*fresh*")
{
if($TenantId)
{
$tenantIdForReport = $TenantId
Write-Host "Tenant: $tenantIdForReport" -ForegroundColor DarkGray
}
else
{
$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)"
$exportPath = Select-FolderPath -Prompt "Select 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.)"
$backupRoot = Select-FolderPath -Prompt "Select backup root (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"
$outputDir = Select-FolderPath -Prompt "Select output directory for reports"
if([string]::IsNullOrWhiteSpace($outputDir)) { Write-Host "Output directory is required." -ForegroundColor Red; continue }
$includeAssignmentsInSettings = $false
@@ -312,11 +373,19 @@ while($true)
}
# 2. TenantId
if($TenantId)
{
$tenantId = $TenantId
Write-Host "`nTenant: $tenantId" -ForegroundColor DarkGray
}
else
{
$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
@@ -324,12 +393,12 @@ while($true)
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
$pathPrompt = if($action -eq "Export") { "Select export root folder" } else { "Select import root folder" }
$path = Select-FolderPath -Prompt $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-')"
$nameFilter = Read-Host "Name filter, text to match anywhere in name, case-insensitive (optional, e.g. 'Win-OIB-')"
# 6. Name Mutation
$nameSearchPattern = Read-Host "Name search regex for mutation (optional, e.g. '^Win-OIB-')"
+1 -1
View File
@@ -84,7 +84,7 @@ if($Interactive)
$tuiScript = Join-Path (Split-Path -Parent $PSScriptRoot) "Scripts/Private/Start-IntuneManagementTui.ps1"
if(Test-Path $tuiScript)
{
$tuiResult = & $tuiScript
$tuiResult = & $tuiScript -TenantId $TenantId -Action $Action
if(-not $tuiResult) { Write-Host "No selection made. Exiting." -ForegroundColor Yellow; exit 0 }
foreach($prop in $tuiResult.PSObject.Properties)
{
+30 -22
View File
@@ -249,7 +249,7 @@ if(-not $TenantId)
$tenantOptions += "[Exit]"
$selectedTenantDisplay = Select-MenuItem -Items $tenantOptions -Header "Select a tenant"
if(-not $selectedTenantDisplay -or $selectedTenantDisplay -eq "[Exit]")
if(-not $selectedTenantDisplay -or $selectedTenantDisplay -eq "[Exit]" -or $selectedTenantDisplay -eq "EXIT")
{
exit 0
}
@@ -308,26 +308,32 @@ $commonParams = @{
}
$menuItems = @(
"20. Export Entra role membership"
"19. Document Conditional Access policies"
"18. Rotate app secret"
"17. Deploy CIS M365 baseline"
"16. Generate reports"
"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"
# Export / Import
"1. Export policies"
"2. Import policies"
"7. Export assignments to CSV/Markdown"
"5. Backup assignments"
"6. Restore assignments"
# Bulk operations
"3. Bulk app assignment"
"4. Bulk assignment manager (policies)"
"8. Bulk rename policies"
"21. Bulk delete policies"
"9. Bulk device operations"
# Baselines & compliance
"10. Deploy baseline"
"11. Deploy baseline (dry-run / WhatIf)"
"17. Deploy CIS M365 baseline"
"19. Document Conditional Access policies"
# Reporting
"16. Generate reports"
"20. Export Entra role membership"
# Tenant & auth admin
"12. Initialize auth (one-time setup)"
"13. Refresh tenant names"
"18. Rotate app secret"
"14. Delete local tenant auth only"
"15. Delete tenant auth and app registration"
"0. Exit"
)
@@ -371,6 +377,7 @@ while($true)
6 { $script = "Scripts/Backup-Restore-Assignments.ps1" }
7 { $script = "Scripts/Export-AssignmentsToCsv.ps1" }
8 { $script = "Scripts/Bulk-RenamePolicies.ps1" }
21 { $script = "Scripts/Bulk-DeletePolicies.ps1" }
9 { $script = "Scripts/Bulk-DeviceOperations.ps1" }
10 { $script = "Scripts/Deploy-IntuneBaseline.ps1" }
11 { $script = "Scripts/Deploy-IntuneBaseline.ps1" }
@@ -383,13 +390,14 @@ while($true)
# Clear any mode-specific params from previous loop iteration
$commonParams.Remove("Interactive")
$commonParams.Remove("Action")
$commonParams.Remove("Mode")
$commonParams.Remove("WhatIf")
switch($choiceNumber)
{
1 { $commonParams.Interactive = $true }
2 { $commonParams.Interactive = $true }
1 { $commonParams.Interactive = $true; $commonParams.Action = "Export" }
2 { $commonParams.Interactive = $true; $commonParams.Action = "Import" }
5 { $commonParams.Mode = "Backup" }
6 { $commonParams.Mode = "Restore" }
11 { $commonParams.WhatIf = $true }
+1 -1
View File
@@ -1 +1 @@
4.2.0
4.4.1