Add headless macOS CLI workflow

This commit is contained in:
2026-04-08 15:18:32 +02:00
parent faffa95d8a
commit 8fe71c0078
12 changed files with 917 additions and 66 deletions

View File

@@ -1,6 +1,21 @@
#region Console functions
function Test-IsWindowsPlatform
{
return ([Environment]::OSVersion.Platform -eq [System.PlatformID]::Win32NT)
}
function Invoke-AppDoEvents
{
if("System.Windows.Forms.Application" -as [type])
{
[System.Windows.Forms.Application]::DoEvents()
}
}
# https://stackoverflow.com/questions/40617800/opening-powershell-script-and-hide-command-prompt-but-not-the-gui
if(Test-IsWindowsPlatform)
{
Add-Type -Name Window -Namespace Console -MemberDefinition '
[DllImport("Kernel32.dll")]
public static extern IntPtr GetConsoleWindow();
@@ -17,9 +32,11 @@ public static extern bool SetConsoleIcon(IntPtr hIcon);
[DllImport("user32.dll")]
public static extern int SendMessage(int hWnd, uint wMsg, uint wParam, IntPtr lParam);
'
}
function Show-Console
{
if(-not (Test-IsWindowsPlatform)) { return }
$consolePtr = [Console.Window]::GetConsoleWindow()
# Hide = 0,
@@ -41,6 +58,7 @@ function Show-Console
function Hide-Console
{
if(-not (Test-IsWindowsPlatform)) { return }
$consolePtr = [Console.Window]::GetConsoleWindow()
#0 hide
[Console.Window]::ShowWindow($consolePtr, 0) | Out-Null
@@ -94,17 +112,19 @@ function Initialize-CloudAPIManagement
)
$PSModuleAutoloadingPreference = "none"
$global:hideUI = ($Silent -eq $true)
$global:SilentBatchFile = $SilentBatchFile
$global:wpfNS = "xmlns='http://schemas.microsoft.com/winfx/2006/xaml/presentation' xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'"
[void] [System.Reflection.Assembly]::LoadWithPartialName("System.Windows.Forms")
Add-Type -AssemblyName PresentationFramework
if($global:hideUI -ne $true)
{
[void] [System.Reflection.Assembly]::LoadWithPartialName("System.Windows.Forms")
Add-Type -AssemblyName PresentationFramework
}
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
$global:hideUI = ($Silent -eq $true)
$global:SilentBatchFile = $SilentBatchFile
if($tenantId)
{
Write-Host "Using Tenant Id: $tenantId"
@@ -163,7 +183,7 @@ function Initialize-CloudAPIManagement
$global:txtSplashTitle.Text = ("Initializing Cloud API PowerShell Management")
$global:SplashScreen.Show() | Out-Null
[System.Windows.Forms.Application]::DoEvents()
Invoke-AppDoEvents
}
catch
{
@@ -204,15 +224,15 @@ function Initialize-CloudAPIManagement
{
$global:txtSplashText.Text = "Unblock files"
}
[System.Windows.Forms.Application]::DoEvents()
Invoke-AppDoEvents
Unblock-AllFiles $PSScriptRoot
if($global:hideUI -ne $true)
{
$global:txtSplashText.Text = "Load core module"
}
[System.Windows.Forms.Application]::DoEvents()
Import-Module ($PSScriptRoot + "\Core.psm1") -Force -Global
Invoke-AppDoEvents
Import-Module (Join-Path $PSScriptRoot "Core.psm1") -Force -Global
Start-CoreApp $View
}

132
Core.psm1
View File

@@ -14,6 +14,14 @@ function Get-ModuleVersion
'3.9.6'
}
function Invoke-AppDoEvents
{
if("System.Windows.Forms.Application" -as [type])
{
[System.Windows.Forms.Application]::DoEvents()
}
}
function Initialize-Window
{
param($xamlFile)
@@ -67,7 +75,27 @@ function Start-CoreApp
$global:AppRootFolder = $PSScriptRoot
# Load all modules in the Modules folder
$global:modulesPath = [IO.Path]::GetDirectoryName($PSCommandPath) + "\Extensions"
$global:modulesPath = Join-Path ([IO.Path]::GetDirectoryName($PSCommandPath)) "Extensions"
if($global:hideUI -eq $true)
{
$global:skipModules = @(
"Compare.psm1",
"Copy.psm1",
"Documentation.psm1",
"DocumentationCustom.psm1",
"DocumentationHTML.psm1",
"DocumentationMD.psm1",
"DocumentationWord.psm1",
"IntuneAssignments.psm1",
"IntuneFilterUsage.psm1",
"IntuneTools.psm1"
)
}
else
{
$global:skipModules = @()
}
Add-DefaultSettings
@@ -122,7 +150,7 @@ function Start-CoreApp
$global:MainAppStarted = $false
Set-SplashWindowText "Initialize views"
[System.Windows.Forms.Application]::DoEvents()
Invoke-AppDoEvents
Invoke-ModuleFunction "Invoke-InitializeModule"
@@ -176,8 +204,6 @@ function Start-CoreApp
Write-Log "SilentBatchFile $($global:SilentBatchFile) not found" 3
return
}
Invoke-ModuleFunction "Invoke-ShowMainWindow"
Invoke-ModuleFunction "Invoke-InitSilentBatchJob"
Start-RunSilentBatchJob
@@ -199,13 +225,13 @@ function Start-RunSilentBatchJob
function Import-AllModules
{
foreach($file in (Get-Item -path "$($global:modulesPath)\*.psm1"))
foreach($file in (Get-Item -path (Join-Path $global:modulesPath "*.psm1")))
{
$fileName = [IO.Path]::GetFileName($file)
if($skipModules -contains $fileName) { Write-Warning "Module $fileName excluded"; continue; }
Set-SplashWindowText "Import module $fileName"
[System.Windows.Forms.Application]::DoEvents()
Invoke-AppDoEvents
$module = Import-Module $file -PassThru -Force -Global -ErrorAction SilentlyContinue
if($module)
@@ -422,7 +448,14 @@ function Set-XamlProperty
{
if($obj)
{
$obj."$propertyName" = $value
if(-not ($obj.PSObject.Properties.Name -contains $propertyName))
{
$obj | Add-Member -MemberType NoteProperty -Name $propertyName -Value $value -Force
}
else
{
$obj."$propertyName" = $value
}
}
else
{
@@ -571,15 +604,15 @@ function Set-BatchProperties
try
{
if($obj -is [System.Windows.Controls.CheckBox])
if($obj.PSObject.Properties.Name -contains "IsChecked")
{
$obj.IsChecked = $prop.Value -eq $true
}
elseif($obj -is [System.Windows.Controls.TextBox])
elseif($obj.PSObject.Properties.Name -contains "Text")
{
$obj.Text = $prop.Value
}
elseif($obj -is [System.Windows.Controls.ComboBox])
elseif($obj.PSObject.Properties.Name -contains "SelectedValue")
{
$obj.SelectedValue = $prop.Value
}
@@ -601,6 +634,60 @@ function Set-BatchProperties
}
#endregion
function New-HeadlessControl
{
param(
[string]$Name,
[ValidateSet("TextBox","CheckBox","ComboBox","Label","DataGrid")]
[string]$Type = "TextBox"
)
$control = [PSCustomObject]@{
Name = $Name
Focusable = $true
Visibility = "Visible"
IsEnabled = $true
Text = ""
IsChecked = $false
SelectedValue = $null
ItemsSource = @()
Content = ""
}
if($Type -eq "DataGrid")
{
$control.Focusable = $false
}
$control
}
function New-HeadlessForm
{
param($Controls)
$form = [PSCustomObject]@{
Controls = @{}
}
foreach($control in $Controls)
{
$form.Controls[$control.Name] = $control
}
$form | Add-Member -MemberType ScriptMethod -Name FindName -Value {
param($name)
$this.Controls[$name]
}
$form | Add-Member -MemberType ScriptMethod -Name RegisterName -Value {
param($name, $control)
$this.Controls[$name] = $control
}
$form
}
#region Dialogs
function Show-AboutDialog
@@ -1204,6 +1291,21 @@ function Expand-FileName
#endregion
function Get-CloudApiDataFolder
{
if($env:LOCALAPPDATA)
{
return (Join-Path $env:LOCALAPPDATA "CloudAPIPowerShellManagement")
}
if(Test-IsWindowsPlatform)
{
return (Join-Path $env:USERPROFILE "AppData/Local/CloudAPIPowerShellManagement")
}
return (Join-Path $HOME "Library/Application Support/CloudAPIPowerShellManagement")
}
#region Save/Read Settings functions
########################################################################
#
@@ -1231,7 +1333,7 @@ function Initialize-JsonSettings
{
if(-not $global:JSonSettingFile)
{
$global:JSonSettingFile = "$($env:LOCALAPPDATA)\CloudAPIPowerShellManagement\Settings.json"
$global:JSonSettingFile = Join-Path (Get-CloudApiDataFolder) "Settings.json"
$fi = [IO.FileInfo]$global:JSonSettingFile
if($fi.Exists -eq $false)
{
@@ -1548,7 +1650,7 @@ function Add-RegKeyToSettings
try
{
$keyObj = Get-Item -Path $regKey -ErrorAction SilentlyContinue
foreach($keyValue in ($keyObj.GetValueNames() | Sort))
foreach($keyValue in ($keyObj.GetValueNames() | Sort-Object))
{
try
{
@@ -1560,7 +1662,7 @@ function Add-RegKeyToSettings
}
}
foreach($subKey in ($keyObj.GetSubKeyNames() | Sort))
foreach($subKey in ($keyObj.GetSubKeyNames() | Sort-Object))
{
$settingObjSub = [ordered]@{}
@@ -1967,7 +2069,7 @@ function Add-DefaultSettings
Value = ""
}
foreach($color in ([System.Drawing.Color].GetProperties() | Where { $_.PropertyType -eq [System.Drawing.Color] } | Sort -Property Name | Select Name).Name)
foreach($color in ([System.Drawing.Color].GetProperties() | Where { $_.PropertyType -eq [System.Drawing.Color] } | Sort-Object -Property Name | Select Name).Name)
{
$script:lstColors += [PSCustomObject]@{
Name = $color
@@ -2286,7 +2388,7 @@ function Add-ViewItem
if($viewObject.ViewInfo.Permissions -is [Object[]] -and $viewObject.ViewInfo.Permissions -notcontains $scope) { $viewObject.ViewInfo.Permissions += $scope }
}
if($viewItem.Icon -or [IO.File]::Exists(($global:AppRootFolder + "\Xaml\Icons\$($viewItem.Id).xaml")))
if($global:hideUI -ne $true -and ($viewItem.Icon -or [IO.File]::Exists(($global:AppRootFolder + "\Xaml\Icons\$($viewItem.Id).xaml"))))
{
$ctrl = Get-XamlObject ($global:AppRootFolder + "\Xaml\Icons\$((?? $viewItem.Icon $viewItem.Id)).xaml")
$viewItem | Add-Member -NotePropertyName "IconImage" -NotePropertyValue $ctrl

View File

@@ -105,9 +105,12 @@ function Invoke-InitializeModule
Write-Log "Microsoft Intune PowerShell is being decomissioned. Please change to a supported app eg Microsoft Graph or a custom app!" 2
}
$viewPanel = Get-XamlObject ($global:AppRootFolder + "\Xaml\EndpointManagerPanel.xaml") -AddVariables
Set-EMViewPanel $viewPanel
$viewPanel = $null
if($global:hideUI -ne $true)
{
$viewPanel = Get-XamlObject (Join-Path $global:AppRootFolder "Xaml/EndpointManagerPanel.xaml") -AddVariables
Set-EMViewPanel $viewPanel
}
#Add menu group and items
$global:EMViewObject = (New-Object PSObject -Property @{

View File

@@ -138,8 +138,8 @@ function Invoke-InitializeModule
}) "MSAL"
$script:MSALUseWAM = Get-SettingValue "UseWAM"
if($script:MSALUseWAM -and $PSVersionTable.PSVersion.Major -lt 7) {
Write-Log "WAM is only supported in PowerShell 7 and later. Disabling WAM" 2
if($script:MSALUseWAM -and ($PSVersionTable.PSVersion.Major -lt 7 -or -not (Test-IsWindowsPlatform))) {
Write-Log "WAM is only supported on Windows with PowerShell 7 and later. Disabling WAM" 2
$script:MSALUseWAM = $false
}
@@ -268,7 +268,7 @@ function Get-MSALUserInfo
### Only get user info from home tenant
$global:Me = $tmpMe
Write-Log "Get profile picture"
$global:profilePhoto = "$($env:LOCALAPPDATA)\CloudAPIPowerShellManagement\$($global:Me.Id).jpeg"
$global:profilePhoto = Join-Path (Get-CloudApiDataFolder) "$($global:Me.Id).jpeg"
MSGraph\Invoke-GraphRequest "me/photos/48x48/`$value" -OutFile $global:profilePhoto -SkipAuthentication -NoError | Out-Null
}
}
@@ -517,17 +517,17 @@ function Add-MSALPrereq
$DLLFiles = @()
if($PSVersionTable.PSVersion.Major -ge 7) {
$DLLFiles += [IO.FileInfo]($ScriptRoot + "\Bin\Microsoft.IdentityModel.Abstractions.dll")
$DLLFiles += [IO.FileInfo](Join-Path $ScriptRoot "Bin/Microsoft.IdentityModel.Abstractions.dll")
}
else {
$DLLFiles += [IO.FileInfo]($ScriptRoot + "\Bin\6.35.0\Microsoft.IdentityModel.Abstractions.dll")
$DLLFiles += [IO.FileInfo](Join-Path $ScriptRoot "Bin/6.35.0/Microsoft.IdentityModel.Abstractions.dll")
}
$DLLFiles += [IO.FileInfo]($ScriptRoot + "\Bin\Microsoft.Identity.Client.dll")
$DLLFiles += [IO.FileInfo](Join-Path $ScriptRoot "Bin/Microsoft.Identity.Client.dll")
if($script:MSALUseWAM) {
$DLLFiles += [IO.FileInfo]($ScriptRoot + "\BIN\$MSALDLLPath\Microsoft.Identity.Client.Extensions.Msal.dll")
$DLLFiles += [IO.FileInfo]($ScriptRoot + "\BIN\$MSALDLLPath\Microsoft.Identity.Client.Broker.dll")
$DLLFiles += [IO.FileInfo]($ScriptRoot + "\BIN\$MSALDLLPath\Microsoft.Identity.Client.Desktop.dll")
$DLLFiles += [IO.FileInfo]($ScriptRoot + "\BIN\$MSALDLLPath\Microsoft.Identity.Client.NativeInterop.dll")
$DLLFiles += [IO.FileInfo](Join-Path $ScriptRoot "Bin/$MSALDLLPath/Microsoft.Identity.Client.Extensions.Msal.dll")
$DLLFiles += [IO.FileInfo](Join-Path $ScriptRoot "Bin/$MSALDLLPath/Microsoft.Identity.Client.Broker.dll")
$DLLFiles += [IO.FileInfo](Join-Path $ScriptRoot "Bin/$MSALDLLPath/Microsoft.Identity.Client.Desktop.dll")
$DLLFiles += [IO.FileInfo](Join-Path $ScriptRoot "Bin/$MSALDLLPath/Microsoft.Identity.Client.NativeInterop.dll")
}
$DLLFiles | ForEach-Object {
@@ -570,7 +570,7 @@ function Add-MSALPrereq
try
{
Add-Type -Path ($ScriptRoot + "\CS\TokenCacheHelperEx.cs") -ReferencedAssemblies $RequiredAssemblies -IgnoreWarnings
Add-Type -Path (Join-Path $ScriptRoot "CS/TokenCacheHelperEx.cs") -ReferencedAssemblies $RequiredAssemblies -IgnoreWarnings
}
catch
{
@@ -809,7 +809,7 @@ function Get-MsalAuthenticationToken
{
# Login hung on rare occations
# Workaround: Added DoEvents
[System.Windows.Forms.Application]::DoEvents()
Invoke-AppDoEvents
Start-Sleep -Seconds 1
}
}
@@ -954,7 +954,7 @@ function Get-MSALApp
if($global:SkipTokenCacheHelperEx -ne $true -and (Get-SettingValue "CacheMSALToken"))
{
[TokenCacheHelperEx]::EnableSerialization($msalApp.UserTokenCache, "%LOCALAPPDATA%\CloudAPIPowerShellManagement\msalcahce.bin3")
[TokenCacheHelperEx]::EnableSerialization($msalApp.UserTokenCache, (Join-Path (Get-CloudApiDataFolder) "msalcahce.bin3"))
}
$script:MSALAllApps += $msalApp
}
@@ -1350,7 +1350,7 @@ function Connect-MSALUser
if((Get-SettingValue "CacheMSALToken"))
{
[TokenCacheHelperEx]::EnableSerialization($app.UserTokenCache, "%LOCALAPPDATA%\CloudAPIPowerShellManagement\msalcahce.bin3")
[TokenCacheHelperEx]::EnableSerialization($app.UserTokenCache, (Join-Path (Get-CloudApiDataFolder) "msalcahce.bin3"))
}
### Silent login

View File

@@ -782,7 +782,7 @@ function Add-GraphObjectProperties
if($objects.Count -gt 0 -and $SortProperty -and ($objects[0] | GM -MemberType NoteProperty -Name $SortProperty))
{
$objects = $objects | sort -Property $SortProperty
$objects = $objects | Sort-Object -Property $SortProperty
}
$objects
@@ -1400,12 +1400,77 @@ function Invoke-SilentBatchJob
}
}
function New-GraphSilentBatchForm
{
param($Controls)
$form = New-HeadlessForm $Controls
foreach($control in $Controls)
{
New-Variable -Name $control.Name -Value $control -Force -Scope Global
}
$form
}
function New-GraphSilentExportForm
{
$controls = @(
(New-HeadlessControl -Name "txtExportPath" -Type "TextBox"),
(New-HeadlessControl -Name "txtExportNameFilter" -Type "TextBox"),
(New-HeadlessControl -Name "chkAddObjectType" -Type "CheckBox"),
(New-HeadlessControl -Name "chkExportAssignments" -Type "CheckBox"),
(New-HeadlessControl -Name "chkAddCompanyName" -Type "CheckBox"),
(New-HeadlessControl -Name "dgObjectsToExport" -Type "DataGrid")
)
$form = New-GraphSilentBatchForm $controls
Set-XamlProperty $form "txtExportPath" "Text" (?? (Get-Setting "" "LastUsedRoot") (Get-SettingValue "RootFolder"))
Set-XamlProperty $form "chkAddObjectType" "IsChecked" $true
Set-XamlProperty $form "chkExportAssignments" "IsChecked" (Get-SettingValue "ExportAssignments")
Set-XamlProperty $form "chkAddCompanyName" "IsChecked" (Get-SettingValue "AddCompanyName")
$form
}
function New-GraphSilentImportForm
{
$path = Get-Setting "" "LastUsedFullPath"
if($path)
{
$path = [IO.Directory]::GetParent($path).FullName
}
$controls = @(
(New-HeadlessControl -Name "txtImportPath" -Type "TextBox"),
(New-HeadlessControl -Name "txtImportNameFilter" -Type "TextBox"),
(New-HeadlessControl -Name "lblMigrationTableInfo" -Type "Label"),
(New-HeadlessControl -Name "chkAddObjectType" -Type "CheckBox"),
(New-HeadlessControl -Name "chkImportScopes" -Type "CheckBox"),
(New-HeadlessControl -Name "chkImportAssignments" -Type "CheckBox"),
(New-HeadlessControl -Name "chkReplaceDependencyIDs" -Type "CheckBox"),
(New-HeadlessControl -Name "cbImportType" -Type "ComboBox"),
(New-HeadlessControl -Name "dgObjectsToImport" -Type "DataGrid")
)
$form = New-GraphSilentBatchForm $controls
Set-XamlProperty $form "txtImportPath" "Text" (?? $path (Get-SettingValue "RootFolder"))
Set-XamlProperty $form "chkAddObjectType" "IsChecked" $true
Set-XamlProperty $form "chkImportAssignments" "IsChecked" (Get-SettingValue "ImportAssignments")
Set-XamlProperty $form "chkImportScopes" "IsChecked" (Get-SettingValue "ImportScopeTags")
Set-XamlProperty $form "chkReplaceDependencyIDs" "IsChecked" $true
Set-XamlProperty $form "cbImportType" "ItemsSource" $script:lstImportTypes
Set-XamlProperty $form "cbImportType" "SelectedValue" (Get-SettingValue "ImportType" "alwaysImport")
$form
}
function Start-GraphSilentBulkExport
{
param($settingsObj)
$script:exportForm = Get-XamlObject ($global:AppRootFolder + "\Xaml\BulkExportForm.xaml") -AddVariables
if(-not $script:exportForm) { return }
$script:exportForm = New-GraphSilentExportForm
$script:exportObjects = Get-GraphBatchObjectTypes $settingsObj.BulkExport
@@ -1414,8 +1479,6 @@ function Start-GraphSilentBulkExport
if(-not $viewObj.Title) { continue }
if($viewObj.ObjectType.ShowButtons -is [Object[]] -and $viewObj.ObjectType.ShowButtons -notcontains "Export") { continue }
Add-GraphExportExtensions $script:exportForm 0 $viewObj.ObjectType
}
Set-BatchProperties $settingsObj.BulkExport $script:exportForm
@@ -1449,8 +1512,7 @@ function Start-GraphSilentBulkImport
{
param($settingsObj)
$script:importForm = Get-XamlObject ($global:AppRootFolder + "\Xaml\BulkImportForm.xaml") -AddVariables
if(-not $script:importForm) { return }
$script:importForm = New-GraphSilentImportForm
# Get all objects but not selected
# This will allow dependencies
@@ -1479,11 +1541,10 @@ function Start-GraphSilentBulkImport
if(-not $viewObj.Title) { continue }
if($viewObj.ObjectType.ShowButtons -is [Object[]] -and $viewObj.ObjectType.ShowButtons -notcontains "Import") { continue }
Add-GraphImportExtensions $script:importForm 0 $viewObj.ObjectType
}
Set-BatchProperties $settingsObj.BulkImport $script:importForm
Set-XamlProperty $script:importForm "lblMigrationTableInfo" "Content" (Get-MigrationTableInfo)
$global:dgObjectsToImport.ItemsSource = @($script:importObjects)
@@ -2358,7 +2419,7 @@ function Get-GraphFileObjects
}
$fileArr = @()
foreach($file in (Get-Item -path "$path\*.json" @params))
foreach($file in (Get-Item -path (Join-Path $path "*.json") @params))
{
if($ObjectType.LoadObject)
{
@@ -2864,7 +2925,7 @@ function Add-GroupMigrationObject
# Export group info to json file for possible import
$grouspPath = Join-Path $path "Groups"
if(-not (Test-Path $grouspPath)) { mkdir -Path $grouspPath -Force -ErrorAction SilentlyContinue | Out-Null }
$fileName = "$grouspPath\$((Remove-InvalidFileNameChars $groupObj.displayName)).json"
$fileName = Join-Path $grouspPath "$((Remove-InvalidFileNameChars $groupObj.displayName)).json"
Save-GraphObjectToFile $groupObj $fileName
}
}
@@ -2907,7 +2968,7 @@ function Add-GraphMigrationObject
# Export group info to json file for possible import
$grouspPath = Join-Path $path "Groups"
if(-not (Test-Path $grouspPath)) { mkdir -Path $grouspPath -Force -ErrorAction SilentlyContinue | Out-Null }
$fileName = "$grouspPath\$((Remove-InvalidFileNameChars $graphObj.displayName)).json"
$fileName = Join-Path $grouspPath "$((Remove-InvalidFileNameChars $graphObj.displayName)).json"
Save-GraphObjectToFile $graphObj $fileName
}
}
@@ -3050,7 +3111,7 @@ function Get-GraphMigrationObjectsFromFile
if($global:GraphMigrationTable)
{
$fi = [IO.FileInfo]$global:GraphMigrationTable
$groupFi = [IO.FileInfo]($fi.DirectoryName + "\Groups\$((Remove-InvalidFileNameChars $migTableGroupName)).json")
$groupFi = [IO.FileInfo](Join-Path (Join-Path $fi.DirectoryName "Groups") "$((Remove-InvalidFileNameChars $migTableGroupName)).json")
}
if($groupFi.Exists -eq $true)
@@ -3197,13 +3258,13 @@ function Add-GraphDependencyObjects
continue
}
if([IO.Directory]::Exists(($importPath + "\" + $dep)))
if([IO.Directory]::Exists((Join-Path $importPath $dep)))
{
$path = ($importPath + "\" + $dep)
$path = (Join-Path $importPath $dep)
}
elseif([IO.Directory]::Exists(($parentPath + "\" + $dep)))
elseif([IO.Directory]::Exists((Join-Path $parentPath $dep)))
{
$path = ($parentPath + "\" + $dep)
$path = (Join-Path $parentPath $dep)
}
else
{
@@ -3674,7 +3735,7 @@ function Invoke-GraphBatchRequest
$retryAfter = 0
$tmpResults = Invoke-GraphRequest -Url "`$batch" -Body $json -Method "POST"
foreach($batchResult in ($tmpResults.responses | Sort -Property Id))
foreach($batchResult in ($tmpResults.responses | Sort-Object -Property Id))
{
if($batchResult.Status -ge 300 -or -not $batchResult.body)
{
@@ -4151,7 +4212,7 @@ function Show-GraphObjectInfo
if($prop.Name.Contains('@') -or $prop.Name.Contains('#')) { continue }
$objProps += ([PSCustomObject]@{ Name=$prop.Name;Value=$prop })
}
$objProps = $objProps | sort -Property Name
$objProps = $objProps | Sort-Object -Property Name
Set-XamlProperty $script:detailsForm "lstObjectProperties" "ItemsSource" $objProps
Add-XamlEvent $script:detailsForm "btnObjectColumnsReset" "Add_Click" -scriptBlock ([scriptblock]{

View File

@@ -0,0 +1,17 @@
@{
RootModule = 'IntuneManagement.Headless.psm1'
ModuleVersion = '0.1.0'
GUID = 'b5b4183d-8d6b-4b31-bbde-f2f0f0a0739d'
Author = 'OpenAI Codex'
Copyright = '(c) OpenAI. Adapter module for headless Intune policy migration.'
Description = 'Headless export/import wrapper for IntuneManagement.'
FunctionsToExport = @(
'Get-DefaultIntunePolicyObjectTypes',
'Export-IntunePolicies',
'Import-IntunePolicies',
'Invoke-IntunePolicyAction'
)
AliasesToExport = @()
VariablesToExport = @()
CmdletsToExport = @()
}

View File

@@ -0,0 +1,311 @@
function Get-DefaultIntunePolicyObjectTypes
{
@(
"DeviceConfiguration",
"SettingsCatalog",
"AdministrativeTemplates",
"CompliancePolicies",
"EndpointSecurity",
"PolicySets"
)
}
function Get-IntuneManagementProjectRoot
{
Split-Path -Parent $PSScriptRoot
}
function Resolve-HeadlessSettingsPath
{
param([string]$SettingsFile)
if($SettingsFile)
{
return $SettingsFile
}
Join-Path ([IO.Path]::GetTempPath()) "IntuneManagement.Settings.json"
}
function New-TemporaryBatchFile
{
param([string]$Prefix)
Join-Path ([IO.Path]::GetTempPath()) ("IntuneManagement.{0}.{1}.json" -f $Prefix, [guid]::NewGuid().ToString())
}
function Test-AuthParameters
{
param(
[string]$Secret,
[string]$Certificate
)
if((-not $Secret) -and (-not $Certificate))
{
throw "Specify -Secret or -Certificate."
}
}
function Invoke-IntuneHeadlessBatch
{
param(
[Parameter(Mandatory = $true)]
[string]$TenantId,
[Parameter(Mandatory = $true)]
[string]$AppId,
[string]$Secret,
[string]$Certificate,
[Parameter(Mandatory = $true)]
[psobject]$BatchConfig,
[string]$SettingsFile,
[string]$BatchFile
)
Test-AuthParameters -Secret $Secret -Certificate $Certificate
$projectRoot = Get-IntuneManagementProjectRoot
$startScript = Join-Path $projectRoot "Start-IntuneManagement.ps1"
if(-not (Test-Path $startScript))
{
throw "Could not find Start-IntuneManagement.ps1 in $projectRoot"
}
$settingsPath = Resolve-HeadlessSettingsPath $SettingsFile
$deleteBatchFile = $false
if(-not $BatchFile)
{
$BatchFile = New-TemporaryBatchFile "Batch"
$deleteBatchFile = $true
}
try
{
$BatchConfig | ConvertTo-Json -Depth 20 | Out-File -LiteralPath $BatchFile -Encoding utf8 -Force
$invokeParams = @{
Silent = $true
JSonSettings = $true
JSonFile = $settingsPath
TenantId = $TenantId
AppId = $AppId
SilentBatchFile = $BatchFile
}
if($Secret)
{
$invokeParams.Secret = $Secret
}
else
{
$invokeParams.Certificate = $Certificate
}
& $startScript @invokeParams
}
finally
{
if($deleteBatchFile -and (Test-Path $BatchFile))
{
Remove-Item -LiteralPath $BatchFile -Force -ErrorAction SilentlyContinue
}
}
}
function Export-IntunePolicies
{
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]$TenantId,
[Parameter(Mandatory = $true)]
[string]$AppId,
[string]$Secret,
[string]$Certificate,
[Parameter(Mandatory = $true)]
[string]$ExportPath,
[string]$SettingsFile,
[string]$BatchFile,
[string]$NameFilter = "",
[string[]]$ObjectTypes = (Get-DefaultIntunePolicyObjectTypes),
[switch]$IncludeAssignments,
[switch]$AddCompanyName
)
$batchConfig = [PSCustomObject]@{
BulkExport = @(
[PSCustomObject]@{ Name = "txtExportPath"; Value = $ExportPath },
[PSCustomObject]@{ Name = "txtExportNameFilter"; Value = $NameFilter },
[PSCustomObject]@{ Name = "chkAddObjectType"; Value = $true },
[PSCustomObject]@{ Name = "chkExportAssignments"; Value = $IncludeAssignments.IsPresent },
[PSCustomObject]@{ Name = "chkAddCompanyName"; Value = $AddCompanyName.IsPresent },
[PSCustomObject]@{ Name = "ObjectTypes"; Type = "Custom"; ObjectTypes = @($ObjectTypes) }
)
}
Invoke-IntuneHeadlessBatch `
-TenantId $TenantId `
-AppId $AppId `
-Secret $Secret `
-Certificate $Certificate `
-BatchConfig $batchConfig `
-SettingsFile $SettingsFile `
-BatchFile $BatchFile
}
function Import-IntunePolicies
{
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]$TenantId,
[Parameter(Mandatory = $true)]
[string]$AppId,
[string]$Secret,
[string]$Certificate,
[Parameter(Mandatory = $true)]
[string]$ImportPath,
[string]$SettingsFile,
[string]$BatchFile,
[string]$NameFilter = "",
[ValidateSet("alwaysImport","skipIfExist","replace","replace_with_assignments","update")]
[string]$ImportType = "alwaysImport",
[string[]]$ObjectTypes = (Get-DefaultIntunePolicyObjectTypes),
[switch]$IncludeAssignments,
[switch]$IncludeScopeTags,
[switch]$ReplaceDependencyIds
)
$batchConfig = [PSCustomObject]@{
BulkImport = @(
[PSCustomObject]@{ Name = "txtImportPath"; Value = $ImportPath },
[PSCustomObject]@{ Name = "txtImportNameFilter"; Value = $NameFilter },
[PSCustomObject]@{ Name = "chkAddObjectType"; Value = $true },
[PSCustomObject]@{ Name = "chkImportScopes"; Value = $IncludeScopeTags.IsPresent },
[PSCustomObject]@{ Name = "chkImportAssignments"; Value = $IncludeAssignments.IsPresent },
[PSCustomObject]@{ Name = "chkReplaceDependencyIDs"; Value = $ReplaceDependencyIds.IsPresent },
[PSCustomObject]@{ Name = "cbImportType"; Value = $ImportType },
[PSCustomObject]@{ Name = "ObjectTypes"; Type = "Custom"; ObjectTypes = @($ObjectTypes) }
)
}
Invoke-IntuneHeadlessBatch `
-TenantId $TenantId `
-AppId $AppId `
-Secret $Secret `
-Certificate $Certificate `
-BatchConfig $batchConfig `
-SettingsFile $SettingsFile `
-BatchFile $BatchFile
}
function Invoke-IntunePolicyAction
{
[CmdletBinding(DefaultParameterSetName = 'Export')]
param(
[Parameter(Mandatory = $true)]
[ValidateSet("Export","Import")]
[string]$Action,
[Parameter(Mandatory = $true)]
[string]$TenantId,
[Parameter(Mandatory = $true)]
[string]$AppId,
[string]$Secret,
[string]$Certificate,
[string]$SettingsFile,
[string]$BatchFile,
[string]$NameFilter = "",
[string[]]$ObjectTypes = (Get-DefaultIntunePolicyObjectTypes),
[string]$ExportPath,
[string]$ImportPath,
[ValidateSet("alwaysImport","skipIfExist","replace","replace_with_assignments","update")]
[string]$ImportType = "alwaysImport",
[switch]$IncludeAssignments,
[switch]$AddCompanyName,
[switch]$IncludeScopeTags,
[switch]$ReplaceDependencyIds
)
switch($Action)
{
"Export"
{
if(-not $ExportPath) { throw "Export requires -ExportPath." }
Export-IntunePolicies `
-TenantId $TenantId `
-AppId $AppId `
-Secret $Secret `
-Certificate $Certificate `
-ExportPath $ExportPath `
-SettingsFile $SettingsFile `
-BatchFile $BatchFile `
-NameFilter $NameFilter `
-ObjectTypes $ObjectTypes `
-IncludeAssignments:$IncludeAssignments `
-AddCompanyName:$AddCompanyName
}
"Import"
{
if(-not $ImportPath) { throw "Import requires -ImportPath." }
Import-IntunePolicies `
-TenantId $TenantId `
-AppId $AppId `
-Secret $Secret `
-Certificate $Certificate `
-ImportPath $ImportPath `
-SettingsFile $SettingsFile `
-BatchFile $BatchFile `
-NameFilter $NameFilter `
-ImportType $ImportType `
-ObjectTypes $ObjectTypes `
-IncludeAssignments:$IncludeAssignments `
-IncludeScopeTags:$IncludeScopeTags `
-ReplaceDependencyIds:$ReplaceDependencyIds
}
}
}

40
Headless/README.md Normal file
View File

@@ -0,0 +1,40 @@
# IntuneManagement Headless
This is the CLI-first surface for a cross-platform fork of the original IntuneManagement project.
The original project is still a Windows WPF application. This layer treats that codebase as an implementation backend and exposes a smaller product surface focused on:
* app-only authentication
* headless export/import
* macOS/Linux/Windows execution with `pwsh`
* automation and CI usage
## Entry points
* [Start-HeadlessIntune.ps1](/Users/avedelphina/Local/IntuneManagement/Start-HeadlessIntune.ps1)
* [Scripts/Export-Policies.ps1](/Users/avedelphina/Local/IntuneManagement/Scripts/Export-Policies.ps1)
* [Scripts/Import-Policies.ps1](/Users/avedelphina/Local/IntuneManagement/Scripts/Import-Policies.ps1)
* [Headless/IntuneManagement.Headless.psd1](/Users/avedelphina/Local/IntuneManagement/Headless/IntuneManagement.Headless.psd1)
## Default policy scope
The default object types are:
* `DeviceConfiguration`
* `SettingsCatalog`
* `AdministrativeTemplates`
* `CompliancePolicies`
* `EndpointSecurity`
* `PolicySets`
## Example
```powershell
pwsh ./Start-HeadlessIntune.ps1 `
-Action Export `
-TenantId "<source-tenant-id>" `
-AppId "<app-id>" `
-Secret "<client-secret>" `
-ExportPath "/tmp/intune-export" `
-IncludeAssignments
```

134
README.md
View File

@@ -1,4 +1,14 @@
# IntuneManagement with PowerShell and WPF UI
# IntuneManagement
This repository now contains two usable surfaces:
* the original Windows WPF application
* a newer headless CLI-first surface for cross-platform export/import
The CLI-first fork shape starts here:
* [Start-HeadlessIntune.ps1](/Users/avedelphina/Local/IntuneManagement/Start-HeadlessIntune.ps1)
* [Headless/README.md](/Users/avedelphina/Local/IntuneManagement/Headless/README.md)
<p align="center">
<a href="https://twitter.com/Micke_K_72">
@@ -64,6 +74,128 @@ The tool will by default generate the files; `BulkExport.json` and `BulkImport.j
The app authentication can either be passed on the command line or stored in the settings. Tennant Settings is required for multiple environments.
### Headless use on macOS/Linux
The WPF UI is still Windows-only, but the **silent batch** mode can be used headlessly with `pwsh` on macOS/Linux.
For non-Windows use:
* Run `Start-IntuneManagement.ps1` with `-Silent`
* Use app-only authentication with `-TenantId`, `-AppId` and `-Secret` or `-Certificate`
* Use `-JSonSettings` and preferably specify `-JSonFile`
* Pass a batch configuration JSON with `-SilentBatchFile`
Common policy object type IDs:
* `DeviceConfiguration`
* `SettingsCatalog`
* `AdministrativeTemplates`
* `CompliancePolicies`
* `EndpointSecurity`
* `PolicySets`
* `ConditionalAccess`
**Example export batch file**
```json
{
"BulkExport": [
{ "Name": "txtExportPath", "Value": "/tmp/intune-export" },
{ "Name": "txtExportNameFilter", "Value": "" },
{ "Name": "chkAddObjectType", "Value": true },
{ "Name": "chkExportAssignments", "Value": true },
{ "Name": "chkAddCompanyName", "Value": false },
{
"Name": "ObjectTypes",
"Type": "Custom",
"ObjectTypes": [
"DeviceConfiguration",
"SettingsCatalog",
"AdministrativeTemplates",
"CompliancePolicies",
"EndpointSecurity",
"PolicySets"
]
}
]
}
```
**Example export command**
```powershell
pwsh ./Start-IntuneManagement.ps1 `
-Silent `
-JSonSettings -JSonFile "/tmp/intune-settings.json" `
-TenantId "<source-tenant-id>" `
-AppId "<app-id>" `
-Secret "<client-secret>" `
-SilentBatchFile "/tmp/BulkExport.json"
```
**Example import batch file**
```json
{
"BulkImport": [
{ "Name": "txtImportPath", "Value": "/tmp/intune-export/SourceTenantName" },
{ "Name": "txtImportNameFilter", "Value": "" },
{ "Name": "chkAddObjectType", "Value": true },
{ "Name": "chkImportScopes", "Value": true },
{ "Name": "chkImportAssignments", "Value": true },
{ "Name": "chkReplaceDependencyIDs", "Value": true },
{ "Name": "cbImportType", "Value": "alwaysImport" },
{
"Name": "ObjectTypes",
"Type": "Custom",
"ObjectTypes": [
"DeviceConfiguration",
"SettingsCatalog",
"AdministrativeTemplates",
"CompliancePolicies",
"EndpointSecurity",
"PolicySets"
]
}
]
}
```
**Example import command**
```powershell
pwsh ./Start-IntuneManagement.ps1 `
-Silent `
-JSonSettings -JSonFile "/tmp/intune-settings.json" `
-TenantId "<target-tenant-id>" `
-AppId "<app-id>" `
-Secret "<client-secret>" `
-SilentBatchFile "/tmp/BulkImport.json"
```
Wrapper scripts are also included:
```powershell
pwsh ./Scripts/Export-Policies.ps1 `
-TenantId "<source-tenant-id>" `
-AppId "<app-id>" `
-Secret "<client-secret>" `
-ExportPath "/tmp/intune-export" `
-IncludeAssignments
```
```powershell
pwsh ./Scripts/Import-Policies.ps1 `
-TenantId "<target-tenant-id>" `
-AppId "<app-id>" `
-Secret "<client-secret>" `
-ImportPath "/tmp/intune-export/SourceTenantName" `
-ImportType alwaysImport `
-IncludeAssignments `
-IncludeScopeTags `
-ReplaceDependencyIds
```
**Command line example:**
Start-IntuneManagement.ps1 -Silent -TenantId "<*TenantID*>" -SilentBatchFile <*PathToFile*> [-AppId <*AppId*>] [-Secret <*Secret*> | -Certificate <*CertThumb*>]

View File

@@ -0,0 +1,43 @@
<#
.SYNOPSIS
Headless Intune policy export wrapper for macOS/Linux/Windows.
#>
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]$TenantId,
[Parameter(Mandatory = $true)]
[string]$AppId,
[string]$Secret,
[string]$Certificate,
[Parameter(Mandatory = $true)]
[string]$ExportPath,
[string]$SettingsFile,
[string]$BatchFile,
[string]$NameFilter = "",
[string[]]$ObjectTypes = @(
"DeviceConfiguration",
"SettingsCatalog",
"AdministrativeTemplates",
"CompliancePolicies",
"EndpointSecurity",
"PolicySets"
),
[switch]$IncludeAssignments,
[switch]$AddCompanyName
)
$modulePath = Join-Path (Split-Path -Parent $PSScriptRoot) "Headless/IntuneManagement.Headless.psd1"
Import-Module $modulePath -Force
Export-IntunePolicies @PSBoundParameters

View File

@@ -0,0 +1,48 @@
<#
.SYNOPSIS
Headless Intune policy import wrapper for macOS/Linux/Windows.
#>
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]$TenantId,
[Parameter(Mandatory = $true)]
[string]$AppId,
[string]$Secret,
[string]$Certificate,
[Parameter(Mandatory = $true)]
[string]$ImportPath,
[string]$SettingsFile,
[string]$BatchFile,
[string]$NameFilter = "",
[ValidateSet("alwaysImport","skipIfExist","replace","replace_with_assignments","update")]
[string]$ImportType = "alwaysImport",
[string[]]$ObjectTypes = @(
"DeviceConfiguration",
"SettingsCatalog",
"AdministrativeTemplates",
"CompliancePolicies",
"EndpointSecurity",
"PolicySets"
),
[switch]$IncludeAssignments,
[switch]$IncludeScopeTags,
[switch]$ReplaceDependencyIds
)
$modulePath = Join-Path (Split-Path -Parent $PSScriptRoot) "Headless/IntuneManagement.Headless.psd1"
Import-Module $modulePath -Force
Import-IntunePolicies @PSBoundParameters

74
Start-HeadlessIntune.ps1 Normal file
View File

@@ -0,0 +1,74 @@
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[ValidateSet("Export","Import")]
[string]$Action,
[Parameter(Mandatory = $true)]
[string]$TenantId,
[Parameter(Mandatory = $true)]
[string]$AppId,
[string]$Secret,
[string]$Certificate,
[string]$SettingsFile,
[string]$BatchFile,
[string]$NameFilter = "",
[string[]]$ObjectTypes,
[string]$ExportPath,
[string]$ImportPath,
[ValidateSet("alwaysImport","skipIfExist","replace","replace_with_assignments","update")]
[string]$ImportType = "alwaysImport",
[switch]$IncludeAssignments,
[switch]$AddCompanyName,
[switch]$IncludeScopeTags,
[switch]$ReplaceDependencyIds
)
$modulePath = Join-Path $PSScriptRoot "Headless/IntuneManagement.Headless.psd1"
Import-Module $modulePath -Force
$invokeParams = @{
Action = $Action
TenantId = $TenantId
AppId = $AppId
SettingsFile = $SettingsFile
BatchFile = $BatchFile
NameFilter = $NameFilter
ExportPath = $ExportPath
ImportPath = $ImportPath
ImportType = $ImportType
IncludeAssignments = $IncludeAssignments
AddCompanyName = $AddCompanyName
IncludeScopeTags = $IncludeScopeTags
ReplaceDependencyIds = $ReplaceDependencyIds
}
if($PSBoundParameters.ContainsKey("ObjectTypes"))
{
$invokeParams.ObjectTypes = $ObjectTypes
}
if($Secret)
{
$invokeParams.Secret = $Secret
}
elseif($Certificate)
{
$invokeParams.Certificate = $Certificate
}
Invoke-IntunePolicyAction @invokeParams