Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 60a655e6d4 | |||
| 81c5e96fc7 | |||
| 30170a1909 | |||
| ae71f8458d | |||
| 2f0d937ca1 | |||
| b10258c5b4 |
@@ -1,5 +1,43 @@
|
|||||||
# macOS Intune Toolkit Changelog
|
# 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
|
## 2026-06-23 — v4.2.0 — Entra directory role membership export
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
Cross-platform, headless Intune policy export/import with PowerShell.
|
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:
|
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.
|
* **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.
|
* **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 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.
|
* **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.
|
* **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.
|
* **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.
|
||||||
|
|||||||
@@ -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 = @{}
|
$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
|
# GSA network filtering profiles
|
||||||
try {
|
try {
|
||||||
$networkFilteringProfiles = Invoke-MgGraphRequest -Uri 'https://graph.microsoft.com/beta/networkAccess/filteringProfiles' -Method GET -OutputType PSObject -ErrorAction Stop |
|
$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?.IncludeServicePrincipals) | ForEach-Object { $includeServicePrincipals.Add($servicePrincipals.GetOrDefault($_, $_)) }
|
||||||
@($policy.Conditions?.ClientApplications?.ExcludeServicePrincipals) | ForEach-Object { $excludeServicePrincipals.Add($servicePrincipals.GetOrDefault($_, $_)) }
|
@($policy.Conditions?.ClientApplications?.ExcludeServicePrincipals) | ForEach-Object { $excludeServicePrincipals.Add($servicePrincipals.GetOrDefault($_, $_)) }
|
||||||
|
|
||||||
$includeAuthenticationContext = [System.Collections.Generic.List[Object]]::new()
|
$includeAuthenticationContext = @($policy.Conditions?.Applications?.IncludeAuthenticationContextClassReferences) |
|
||||||
@($policy.Conditions?.Applications?.IncludeAuthenticationContextClassReferences) | ForEach-Object {
|
ForEach-Object { $authContextClassReferences.GetOrDefault($_, $_) }
|
||||||
try {
|
|
||||||
$context = Get-MgIdentityConditionalAccessAuthenticationContextClassReference -Filter "Id eq '$PSItem'" -ErrorAction Stop
|
|
||||||
if ($context.DisplayName) { $includeAuthenticationContext.Add($context.DisplayName) }
|
|
||||||
} catch {
|
|
||||||
$includeAuthenticationContext.Add($PSItem)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$includeLocations = @($policy.Conditions?.Locations?.IncludeLocations) | ForEach-Object { $namedLocations.GetOrDefault($_, $_) }
|
$includeLocations = @($policy.Conditions?.Locations?.IncludeLocations) | ForEach-Object { $namedLocations.GetOrDefault($_, $_) }
|
||||||
$excludeLocations = @($policy.Conditions?.Locations?.ExcludeLocations) | 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
|
$includeGuestsOrExternalUserTypes = $policy.Conditions?.Users?.IncludeGuestsOrExternalUsers?.guestOrExternalUserTypes
|
||||||
$includeGuestsOrExternalUserTenants = @($policy.Conditions?.Users?.IncludeGuestsOrExternalUsers?.externalTenants?.AdditionalProperties?['members'])
|
$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
|
$applicationsAdditional = $null
|
||||||
if ($policy.Conditions?.Applications?.AdditionalProperties) {
|
if ($policy.Conditions?.Applications?.AdditionalProperties) {
|
||||||
@@ -552,7 +556,7 @@ foreach ($policy in $conditionalAccessPolicies) {
|
|||||||
ExcludeDeviceStates = (@($excludeDeviceStates) -join $separator)
|
ExcludeDeviceStates = (@($excludeDeviceStates) -join $separator)
|
||||||
IncludeGuestsOrExternalUserTypes = $includeGuestsOrExternalUserTypes
|
IncludeGuestsOrExternalUserTypes = $includeGuestsOrExternalUserTypes
|
||||||
IncludeGuestOrExternalUserTenants = (@($includeGuestsOrExternalUserTenants) -join $separator)
|
IncludeGuestOrExternalUserTenants = (@($includeGuestsOrExternalUserTenants) -join $separator)
|
||||||
AuthenticationFlows = (@($authenticationFlows) -join $separator)
|
AuthenticationFlows = $authenticationFlows
|
||||||
ApplicationsAdditional = $applicationsAdditional
|
ApplicationsAdditional = $applicationsAdditional
|
||||||
ConditionsAdditional = $conditionsAdditional
|
ConditionsAdditional = $conditionsAdditional
|
||||||
IncludeServicePrincipals = ($includeServicePrincipals -join $separator)
|
IncludeServicePrincipals = ($includeServicePrincipals -join $separator)
|
||||||
@@ -646,8 +650,9 @@ if ($ExportExcel) {
|
|||||||
@{ Label = 'Locations exclude'; Col = 'ExcludeLocations' },
|
@{ Label = 'Locations exclude'; Col = 'ExcludeLocations' },
|
||||||
@{ Label = 'Device filter mode'; Col = 'DeviceFilterMode' },
|
@{ Label = 'Device filter mode'; Col = 'DeviceFilterMode' },
|
||||||
@{ Label = 'Device filter rule'; Col = 'DeviceFilterRule' },
|
@{ Label = 'Device filter rule'; Col = 'DeviceFilterRule' },
|
||||||
@{ Label = 'Sign-in risk'; Col = 'SignInRiskLevels' },
|
@{ Label = 'Sign-in risk'; Col = 'SignInRiskLevels' },
|
||||||
@{ Label = 'User risk'; Col = 'UserRiskLevels' }
|
@{ Label = 'User risk'; Col = 'UserRiskLevels' },
|
||||||
|
@{ Label = 'Authentication flows'; Col = 'AuthenticationFlows' }
|
||||||
)},
|
)},
|
||||||
@{ Title = 'Grant'; Fields = @(
|
@{ Title = 'Grant'; Fields = @(
|
||||||
@{ Label = 'Operator'; Col = 'GrantControlsOperator' },
|
@{ Label = 'Operator'; Col = 'GrantControlsOperator' },
|
||||||
|
|||||||
@@ -8,7 +8,10 @@
|
|||||||
Uses fzf on macOS/Linux when available; falls back to numbered menus.
|
Uses fzf on macOS/Linux when available; falls back to numbered menus.
|
||||||
#>
|
#>
|
||||||
[CmdletBinding()]
|
[CmdletBinding()]
|
||||||
param()
|
param(
|
||||||
|
[string]$TenantId,
|
||||||
|
[string]$Action
|
||||||
|
)
|
||||||
|
|
||||||
$ErrorActionPreference = "Stop"
|
$ErrorActionPreference = "Stop"
|
||||||
|
|
||||||
@@ -104,6 +107,41 @@ function Select-MenuItem
|
|||||||
return Show-NumberedMenu -Items $Items -Header $Header -Multi:$Multi
|
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
|
function Read-YesNo
|
||||||
{
|
{
|
||||||
param(
|
param(
|
||||||
@@ -157,18 +195,33 @@ while($true)
|
|||||||
Write-Host " Press Esc to go back, Space to select" -ForegroundColor DarkGray
|
Write-Host " Press Esc to go back, Space to select" -ForegroundColor DarkGray
|
||||||
|
|
||||||
# 1. Action
|
# 1. Action
|
||||||
$action = Select-MenuItem -Items @("Export","Import","DeployCISBaseline","GenerateReports") -Header "Select action"
|
if($Action)
|
||||||
if(-not $action) { continue }
|
{
|
||||||
|
$action = $Action
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
$action = Select-MenuItem -Items @("Export","Import","DeployCISBaseline","GenerateReports") -Header "Select action"
|
||||||
|
if(-not $action) { continue }
|
||||||
|
}
|
||||||
|
|
||||||
# CIS M365 Baseline deployment flow
|
# CIS M365 Baseline deployment flow
|
||||||
if($action -eq "DeployCISBaseline")
|
if($action -eq "DeployCISBaseline")
|
||||||
{
|
{
|
||||||
# 2a. TenantId
|
# 2a. TenantId
|
||||||
$tenantPrompt = "Enter Tenant ID"
|
if($TenantId)
|
||||||
if($preloadedTenantId) { $tenantPrompt += " (default: $preloadedTenantId)" }
|
{
|
||||||
$tenantId = Read-Host $tenantPrompt
|
$tenantId = $TenantId
|
||||||
if([string]::IsNullOrWhiteSpace($tenantId)) { $tenantId = $preloadedTenantId }
|
Write-Host "Tenant: $tenantId" -ForegroundColor DarkGray
|
||||||
if([string]::IsNullOrWhiteSpace($tenantId)) { Write-Host "Tenant ID is required." -ForegroundColor Red; continue }
|
}
|
||||||
|
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
|
# 2b. Baseline path
|
||||||
$defaultBaseline = Join-Path (Split-Path -Parent (Split-Path -Parent $PSScriptRoot)) "Baselines/CISM365-v7-Generated.yaml"
|
$defaultBaseline = Join-Path (Split-Path -Parent (Split-Path -Parent $PSScriptRoot)) "Baselines/CISM365-v7-Generated.yaml"
|
||||||
@@ -243,24 +296,32 @@ while($true)
|
|||||||
|
|
||||||
if($dataSource -like "*fresh*")
|
if($dataSource -like "*fresh*")
|
||||||
{
|
{
|
||||||
$tenantPrompt = "Enter Tenant ID"
|
if($TenantId)
|
||||||
if($preloadedTenantId) { $tenantPrompt += " (default: $preloadedTenantId)" }
|
{
|
||||||
$tenantIdForReport = Read-Host $tenantPrompt
|
$tenantIdForReport = $TenantId
|
||||||
if([string]::IsNullOrWhiteSpace($tenantIdForReport)) { $tenantIdForReport = $preloadedTenantId }
|
Write-Host "Tenant: $tenantIdForReport" -ForegroundColor DarkGray
|
||||||
if([string]::IsNullOrWhiteSpace($tenantIdForReport)) { Write-Host "Tenant ID is required." -ForegroundColor Red; continue }
|
}
|
||||||
|
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 }
|
if([string]::IsNullOrWhiteSpace($exportPath)) { Write-Host "Export path is required." -ForegroundColor Red; continue }
|
||||||
$backupRoot = $exportPath
|
$backupRoot = $exportPath
|
||||||
}
|
}
|
||||||
else
|
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([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 }
|
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 }
|
if([string]::IsNullOrWhiteSpace($outputDir)) { Write-Host "Output directory is required." -ForegroundColor Red; continue }
|
||||||
|
|
||||||
$includeAssignmentsInSettings = $false
|
$includeAssignmentsInSettings = $false
|
||||||
@@ -312,11 +373,19 @@ while($true)
|
|||||||
}
|
}
|
||||||
|
|
||||||
# 2. TenantId
|
# 2. TenantId
|
||||||
$tenantPrompt = "Enter Tenant ID"
|
if($TenantId)
|
||||||
if($preloadedTenantId) { $tenantPrompt += " (default: $preloadedTenantId)" }
|
{
|
||||||
$tenantId = Read-Host $tenantPrompt
|
$tenantId = $TenantId
|
||||||
if([string]::IsNullOrWhiteSpace($tenantId)) { $tenantId = $preloadedTenantId }
|
Write-Host "`nTenant: $tenantId" -ForegroundColor DarkGray
|
||||||
if([string]::IsNullOrWhiteSpace($tenantId)) { Write-Host "Tenant ID is required." -ForegroundColor Red; continue }
|
}
|
||||||
|
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
|
# 3. Object Types
|
||||||
Write-Host "`nObject type selection..." -ForegroundColor Cyan
|
Write-Host "`nObject type selection..." -ForegroundColor Cyan
|
||||||
@@ -324,12 +393,12 @@ while($true)
|
|||||||
if(-not $typeSelection) { continue }
|
if(-not $typeSelection) { continue }
|
||||||
|
|
||||||
# 4. Path
|
# 4. Path
|
||||||
$pathPrompt = if($action -eq "Export") { "Enter export root folder path" } else { "Enter import root folder path" }
|
$pathPrompt = if($action -eq "Export") { "Select export root folder" } else { "Select import root folder" }
|
||||||
$path = Read-Host $pathPrompt
|
$path = Select-FolderPath -Prompt $pathPrompt
|
||||||
if([string]::IsNullOrWhiteSpace($path)) { Write-Host "Path is required." -ForegroundColor Red; return $null }
|
if([string]::IsNullOrWhiteSpace($path)) { Write-Host "Path is required." -ForegroundColor Red; return $null }
|
||||||
|
|
||||||
# 5. Name Filter
|
# 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
|
# 6. Name Mutation
|
||||||
$nameSearchPattern = Read-Host "Name search regex for mutation (optional, e.g. '^Win-OIB-')"
|
$nameSearchPattern = Read-Host "Name search regex for mutation (optional, e.g. '^Win-OIB-')"
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ if($Interactive)
|
|||||||
$tuiScript = Join-Path (Split-Path -Parent $PSScriptRoot) "Scripts/Private/Start-IntuneManagementTui.ps1"
|
$tuiScript = Join-Path (Split-Path -Parent $PSScriptRoot) "Scripts/Private/Start-IntuneManagementTui.ps1"
|
||||||
if(Test-Path $tuiScript)
|
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 }
|
if(-not $tuiResult) { Write-Host "No selection made. Exiting." -ForegroundColor Yellow; exit 0 }
|
||||||
foreach($prop in $tuiResult.PSObject.Properties)
|
foreach($prop in $tuiResult.PSObject.Properties)
|
||||||
{
|
{
|
||||||
|
|||||||
+30
-22
@@ -249,7 +249,7 @@ if(-not $TenantId)
|
|||||||
$tenantOptions += "[Exit]"
|
$tenantOptions += "[Exit]"
|
||||||
|
|
||||||
$selectedTenantDisplay = Select-MenuItem -Items $tenantOptions -Header "Select a tenant"
|
$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
|
exit 0
|
||||||
}
|
}
|
||||||
@@ -308,26 +308,32 @@ $commonParams = @{
|
|||||||
}
|
}
|
||||||
|
|
||||||
$menuItems = @(
|
$menuItems = @(
|
||||||
"20. Export Entra role membership"
|
# Export / Import
|
||||||
"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"
|
|
||||||
"1. Export policies"
|
"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"
|
"0. Exit"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -371,6 +377,7 @@ while($true)
|
|||||||
6 { $script = "Scripts/Backup-Restore-Assignments.ps1" }
|
6 { $script = "Scripts/Backup-Restore-Assignments.ps1" }
|
||||||
7 { $script = "Scripts/Export-AssignmentsToCsv.ps1" }
|
7 { $script = "Scripts/Export-AssignmentsToCsv.ps1" }
|
||||||
8 { $script = "Scripts/Bulk-RenamePolicies.ps1" }
|
8 { $script = "Scripts/Bulk-RenamePolicies.ps1" }
|
||||||
|
21 { $script = "Scripts/Bulk-DeletePolicies.ps1" }
|
||||||
9 { $script = "Scripts/Bulk-DeviceOperations.ps1" }
|
9 { $script = "Scripts/Bulk-DeviceOperations.ps1" }
|
||||||
10 { $script = "Scripts/Deploy-IntuneBaseline.ps1" }
|
10 { $script = "Scripts/Deploy-IntuneBaseline.ps1" }
|
||||||
11 { $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
|
# Clear any mode-specific params from previous loop iteration
|
||||||
$commonParams.Remove("Interactive")
|
$commonParams.Remove("Interactive")
|
||||||
|
$commonParams.Remove("Action")
|
||||||
$commonParams.Remove("Mode")
|
$commonParams.Remove("Mode")
|
||||||
$commonParams.Remove("WhatIf")
|
$commonParams.Remove("WhatIf")
|
||||||
|
|
||||||
switch($choiceNumber)
|
switch($choiceNumber)
|
||||||
{
|
{
|
||||||
1 { $commonParams.Interactive = $true }
|
1 { $commonParams.Interactive = $true; $commonParams.Action = "Export" }
|
||||||
2 { $commonParams.Interactive = $true }
|
2 { $commonParams.Interactive = $true; $commonParams.Action = "Import" }
|
||||||
5 { $commonParams.Mode = "Backup" }
|
5 { $commonParams.Mode = "Backup" }
|
||||||
6 { $commonParams.Mode = "Restore" }
|
6 { $commonParams.Mode = "Restore" }
|
||||||
11 { $commonParams.WhatIf = $true }
|
11 { $commonParams.WhatIf = $true }
|
||||||
|
|||||||
Reference in New Issue
Block a user