Beta release
This commit is contained in:
Mikael Karlsson
2021-10-17 14:02:08 +11:00
parent 5976b0bffd
commit 4add87884a
18 changed files with 959 additions and 107 deletions

Binary file not shown.

View File

@@ -69,7 +69,11 @@ function Initialize-CloudAPIManagement
[string]
$View = "",
[switch]
$ShowConsoleWindow
$ShowConsoleWindow,
[switch]
$JSonSettings,
[string]
$JSonFile
)
$global:wpfNS = "xmlns='http://schemas.microsoft.com/winfx/2006/xaml/presentation' xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'"
@@ -99,6 +103,16 @@ function Initialize-CloudAPIManagement
Hide-Console
}
if($JSonSettings -eq $true)
{
$global:UseJSonSettings = $true
$global:JSonSettingFile = $JSonFile
}
else
{
$global:UseJSonSettings = $false
}
$global:txtSplashText.Text = "Unblock files"
[System.Windows.Forms.Application]::DoEvents()
Unblock-AllFiles $PSScriptRoot

328
Core.psm1
View File

@@ -6,13 +6,12 @@ Core UI and Settings fatures for the CloudAPIPowerShellManager solution
This module handles the WPF UI
.NOTES
Version: 3.1.0
Author: Mikael Karlsson
#>
function Get-ModuleVersion
{
'3.1.7'
'3.3.0'
}
function Start-CoreApp
@@ -35,10 +34,18 @@ function Start-CoreApp
# Load all modules in the Modules folder
$global:modulesPath = [IO.Path]::GetDirectoryName($PSCommandPath) + "\Extensions"
#Import-Module ($PSScriptRoot + "\Core.psm1") -Force -Global
Add-DefaultSettings
if($global:UseJSonSettings -eq $true)
{
Initialize-JsonSettings
}
if($global:UseJSonSettings -eq $false)
{
Write-Log "Use settings in registry"
}
Write-Log "#####################################################################################"
Write-Log "Application started"
Write-Log "#####################################################################################"
@@ -53,7 +60,7 @@ function Start-CoreApp
exit 1
}
$global:Debug = Get-SettingValue "Debug"
Initialize-Settings
$global:currentViewObject = $null
$global:FirstTimeRunning = ((Get-Setting "" "FirstTimeRunning" "true") -eq "true")
$global:MainAppStarted = $false
@@ -795,34 +802,230 @@ function Expand-FileName
#endregion
#region Reg functions
#region Save/Read Settings functions
########################################################################
#
# Reg functions
# Save/Read Settings
#
########################################################################
function Initialize-Settings
{
param([switch]$Updated)
$global:Debug = Get-SettingValue "Debug"
$global:logFile = $null
$global:logFileMaxSize = $null
if($Updated -eq $true)
{
Invoke-ModuleFunction "Invoke-SettingsUpdated"
}
}
function Initialize-JsonSettings
{
if(-not $global:JSonSettingFile)
{
$global:JSonSettingFile = "$($env:LOCALAPPDATA)\CloudAPIPowerShellManagement\Settings.json"
$fi = [IO.FileInfo]$global:JSonSettingFile
if($fi.Exists -eq $false)
{
Export-Settings $fi.FullName
}
}
else
{
$fi = [IO.FileInfo]$global:JSonSettingFile
if($fi.Exists -eq $false)
{
try
{
Write-Host "Settings file $($fi.FullName) does not exist. Create empty settings"
@{} | ConvertTo-Json | Out-File -FilePath $global:JSonSettingFile -Force -Encoding utf8
}
catch
{
Clear-JsonSettingsValues
Write-LogError "Failed to create json setting file $($fi.FullName). Veirfy write access. Registry settings will be used." $_.Exception
}
}
}
$fi = [IO.FileInfo]$global:JSonSettingFile
if($fi.Exists -eq $true)
{
try
{
$global:JsonSettingsObj = (ConvertFrom-Json (Get-Content -Path $fi.FullName -Raw))
Write-Log "Use json settings file: $($fi.FullName)"
return
}
catch
{
Clear-JsonSettingsValues
Write-LogError "Failed to read json setting file $($fi.FullName). Registry settings will be used." $_.Exception
}
}
else
{
Clear-JsonSettingsValues
Write-LogError "Could not find json setting file $($fi.FullName). Registry settings will be used"
}
}
function Clear-JsonSettingsValues
{
# Failed - Revert back to reg settings
$global:JsonSettingsObj = $null
$global:JSonSettingFile = $null
$global:UseJSonSettings = $false
}
function Save-Setting
{
param($SubPath, $Key, $Value, $Type = "String")
param($SubPath = "", $Key = "", $Value, $Type = "String")
$regPath = Get-RegPath $SubPath
if((Test-Path $regPath) -eq $false)
if($global:JsonSettingsObj -and $global:JSonSettingFile)
{
New-Item (Get-RegPath $SubPath) -Force -ErrorAction SilentlyContinue | Out-Null
if($SubPath)
{
$arrParts = $SubPath.Split(@('/','\'))
}
else
{
$arrParts = @()
}
$parentSetting = $global:JsonSettingsObj
foreach($part in $arrParts)
{
if(($parentSetting.PSObject.Properties | Where Name -eq $part))
{
$parentSetting = $parentSetting.$part
}
else
{
$parentSetting.$part = @()
$parentSetting = $parentSetting.$part
}
}
try
{
if($null -eq $Value)
{
if(($parentSetting.PSObject.Properties | Where Name -eq $Key))
{
$parentSetting.PSObject.Properties.Remove($Key) | Out-Null
}
}
else
{
if($Type -eq "String" -and $null -ne $value)
{
$Value = $value.ToString()
}
elseif($Type -eq "DWord" -and $null -ne $Value)
{
$Value = [Int]::Parse($Value)
}
if(-not ($parentSetting.PSObject.Properties | Where Name -eq $Key))
{
$parentSetting | Add-Member -MemberType NoteProperty -Name $Key -Value $Value
}
else
{
$parentSetting.$Key = $Value
}
}
$global:JsonSettingsObj | ConvertTo-Json -Depth 20 | Out-File -LiteralPath $global:JSonSettingFile -Force -Encoding utf8
}
catch
{
Write-LogError "Failed to save json setting value $Key" $_.Exception
}
}
else
{
$regPath = Get-RegPath $SubPath
if((Test-Path $regPath) -eq $false)
{
New-Item (Get-RegPath $SubPath) -Force -ErrorAction SilentlyContinue | Out-Null
}
New-ItemProperty -Path $regPath -Name $Key -Value $Value -Type $Type -Force | Out-Null
}
New-ItemProperty -Path $regPath -Name $Key -Value $Value -Type $Type -Force | Out-Null
}
function Get-Setting
{
param($SubPath, $Key, $defautValue)
param($SubPath = "", $Key = "", $defautValue)
try
if(-not $key)
{
$val = Get-ItemPropertyValue -Path (Get-RegPath $SubPath) -Name $Key -ErrorAction SilentlyContinue
return
}
catch { }
$val = $null
if($global:JsonSettingsObj)
{
try
{
if($SubPath)
{
$arrParts = $SubPath.Split(@('/','\'))
}
else
{
$arrParts = @()
}
$parentSetting = $global:JsonSettingsObj
$found = $true
foreach($part in $arrParts)
{
if(($parentSetting.PSObject.Properties | Where Name -eq $part))
{
$parentSetting = $parentSetting.$part
}
else
{
$found = $false
break
}
}
if($null -ne $parentSetting.$Key -and $found)
{
$val = $parentSetting.$Key
}
}
catch
{
Write-LogError "Failed to read json setting value $Key" $_.Exception
}
}
else
{
try
{
$val = Get-ItemPropertyValue -Path (Get-RegPath $SubPath) -Name $Key -ErrorAction SilentlyContinue
}
catch
{
if($_.Exception.HResult -ne -2147024809) # Skip reporting missing values
{
Write-LogError "Failed to read registry setting value $Key" $_.Exception
}
}
}
if(-not $val)
{
$defautValue
@@ -845,6 +1048,77 @@ function Get-RegPath
$path
}
function Export-Settings
{
param($fileName)
try
{
$fi = [IO.FileInfo]$fileName
if($fi.Directory.Exists -eq $false)
{
$fi.Directory.Create()
}
}
catch
{
Write-LogError "Failed to create folder for settings file" $_.Exception
return
}
$settingObj = [ordered]@{}
Add-RegKeyToSettings $settingObj "HKCU:\Software\CloudAPIPowerShellManagement"
$json = $settingObj | ConvertTo-Json -Depth 20
try
{
$json | Out-File -filePath $fileName -encoding utf8 -Force -ErrorAction Stop
}
catch
{
Write-LogError "Failed to save json setting file" $_.Exception
}
}
function Add-RegKeyToSettings
{
param($settingObj, $regKey)
try
{
$keyObj = Get-Item -Path $regKey
foreach($keyValue in ($keyObj.GetValueNames() | Sort))
{
try
{
$settingObj.Add($keyValue, $keyObj.GetValue($keyValue))
}
catch
{
Write-LogError "Failed to add setting from reg key $keyValue in $regKey" $_.Exception
}
}
foreach($subKey in ($keyObj.GetSubKeyNames() | Sort))
{
$settingObjSub = [ordered]@{}
$settingObj.Add($subKey, $settingObjSub)
try
{
Add-RegKeyToSettings $settingObjSub ($regKey + '\' + $subKey)
}
catch
{
Write-LogError "Failed to add setting for reg subkey $subKey in $regKey" $_.Exception
}
}
}
catch
{
Write-LogError "Failed to add reg keys to json settings" $_.Exception
}
}
#endregion
#region Setting functions
@@ -1028,13 +1302,30 @@ function Show-SettingsForm
Add-XamlEvent $settingsForm "btnSave" "Add_Click" ({
Save-AllSettings
$global:Debug = Get-SettingValue "Debug"
})
Add-XamlEvent $settingsForm "btnClose" "Add_Click" ({
Show-ModalObject
})
if($JsonSettingsObj)
{
Set-XamlProperty $settingsForm "btnExport" "Visibility" "Collapsed"
}
else
{
Add-XamlEvent $settingsForm "btnExport" "Add_Click" ({
$sf = [System.Windows.Forms.SaveFileDialog]::new()
$sf.FileName = $script:currentObjName
$sf.DefaultExt = "*.json"
$sf.Filter = "Json (*.json)|*.json|All files (*.*)|*.*"
if($sf.ShowDialog() -eq "OK")
{
Export-Settings $sf.FileName
}
})
}
$tmp = $global:appSettingSections | Where-Object Id -eq "General"
if($tmp.Values.Count -gt 0)
{
@@ -1174,6 +1465,9 @@ function Save-AllSettings
{
& $global:currentViewObject.ViewInfo.SaveSettings
}
Initialize-Settings -Updated
Start-Sleep -Seconds 1 # It goes to quick...ToDo: Do this in a better way
Write-Status ""
}

View File

@@ -11,7 +11,7 @@ Objects can be compared based on Properties or Documentatation info.
function Get-ModuleVersion
{
'1.0.7'
'1.0.8'
}
function Invoke-InitializeModule
@@ -50,16 +50,16 @@ function Add-CompareProvider
if($global:compareProviders.Count -eq 0)
{
$global:compareProviders += [PSCustomObject]@{
Name = "Exported File"
Name = "Intune Objects with Exported Files"
Value = "export"
ObjectCompare = { Compare-ObjectsBasedonProperty @args }
BulkCompare = { Start-BulkCompareExportObjects @args }
ProviderOptions = "CompareExportOptions"
Activate = { Invoke-ActivateCompareExportObjects @args }
Activate = { Invoke-ActivateCompareWithExportObjects @args }
}
$global:compareProviders += [PSCustomObject]@{
Name = "Named Objects"
Name = "Named Objects in Intune"
Value = "name"
BulkCompare = { Start-BulkCompareNamedObjects @args }
ProviderOptions = "CompareNamedOptions"
@@ -67,6 +67,15 @@ function Add-CompareProvider
RemoveProperties = @("Id")
}
$global:compareProviders += [PSCustomObject]@{
Name = "Files in Exported Folders"
Value = "exportedFolders"
ObjectCompare = { Compare-ObjectsBasedonProperty @args }
BulkCompare = { Start-BulkCompareExportFolders @args }
ProviderOptions = "CompareExportedFilesOptions"
Activate = { Invoke-ActivateCompareExportedObjects @args }
}
$global:compareProviders += [PSCustomObject]@{
Name = "Existing objects"
Value = "existing"
@@ -269,7 +278,9 @@ function Set-CompareProviderOptions
}
}
function Invoke-ActivateCompareExportObjects
# Compare Intune object with exported folder
function Invoke-ActivateCompareWithExportObjects
{
param($providerOptions, $firstTime)
@@ -292,6 +303,39 @@ function Invoke-ActivateCompareExportObjects
}
}
# Compare two exported folders
function Invoke-ActivateCompareExportedObjects
{
param($providerOptions, $firstTime)
if($firstTime)
{
$path = Get-Setting "" "LastUsedFullPath"
if($path)
{
$path = [IO.Directory]::GetParent($path).FullName
}
Set-XamlProperty $providerOptions "txtExportPathSource" "Text" (?? $path (Get-SettingValue "RootFolder"))
Set-XamlProperty $providerOptions "txtExportPathCompare" "Text" (Get-SettingValue "ExportPathCompare")
Add-XamlEvent $providerOptions "browseExportPathSource" "add_click" ({
$folder = Get-Folder (Get-XamlProperty $this.Parent "txtExportPathSource" "Text") "Select root folder for source"
if($folder)
{
Set-XamlProperty $this.Parent "txtExportPathSource" "Text" $folder
}
})
Add-XamlEvent $providerOptions "browseExportPathCompare" "add_click" ({
$folder = Get-Folder (Get-XamlProperty $this.Parent "txtExportPathCompare" "Text") "Select folder to compare the source with"
if($folder)
{
Set-XamlProperty $this.Parent "txtExportPathCompare" "Text" $folder
}
})
}
}
function Invoke-ActivateCompareNamesObjects
{
param($providerOptions, $firstTime)
@@ -468,6 +512,8 @@ function Start-BulkCompareExportObjects
Write-Log "Start bulk Exported Objects compare"
Write-Log "****************************************************************"
$compareObjectsResult = @()
$txtNameFilter = (Get-XamlProperty $global:ccContentProviderOptions.Content "txtCompareNameFilter" "Text").Trim()
$rootFolder = (Get-XamlProperty $global:ccContentProviderOptions.Content "txtExportPath" "Text")
$compareProps = $script:defaultCompareProps
@@ -521,12 +567,20 @@ function Start-BulkCompareExportObjects
Write-Log "Object from file '$($fileObj.FullName)' has no Id property. Compare not supported" 2
continue
}
$objName = Get-GraphObjectName $fileObj.Object $fileObj.ObjectType
if($txtNameFilter -and $objName -notmatch [RegEx]::Escape($txtNameFilter))
{
continue
}
$curObject = $graphObjects | Where { $_.Object.Id -eq $fileObj.Object.Id }
if(-not $curObject)
{
# Add objects that are exported but deleted
Write-Log "Object '$((Get-GraphObjectName $fileObj.Object $fileObj.ObjectType))' with id $($fileObj.Object.Id) not found in Intune. Deleted?" 2
Write-Log "Object '$($objName)' with id $($fileObj.Object.Id) not found in Intune. Deleted?" 2
$compareProperties = @([PSCustomObject]@{
Object1Value = $null
Object2Value = (Get-GraphObjectName $fileObj.Object $item.ObjectType)
@@ -554,13 +608,19 @@ function Start-BulkCompareExportObjects
# Add objects that are not exported
if(($compareObjectsResult | Where { $_.Id -eq $graphObj.Id})) { continue }
$objName = Get-GraphObjectName $graphObj.Object $item.ObjectType
if($txtNameFilter -and $objName -notmatch [RegEx]::Escape($txtNameFilter))
{
continue
}
$compareObjectsResult += [PSCustomObject]@{
Object1 = $curObject.Object
Object2 = $null
ObjectType = $item.ObjectType
Id = $graphObj.Id
Result = @([PSCustomObject]@{
Object1Value = (Get-GraphObjectName $graphObj.Object $item.ObjectType)
Object1Value = $objName
Object2Value = $null
Match = $false
})
@@ -594,18 +654,18 @@ function Start-BulkCompareExportObjects
if($outputType -eq "objectType")
{
Save-BulkCompareResults $compResultValues (Join-Path $rootFolder "Compare_$(((Get-Date).ToString("yyyyMMdd-HHmm"))).csv") $compareProps
Save-BulkCompareResults $compResultValues (Join-Path $folder "Compare_$(((Get-Date).ToString("yyyyMMdd-HHmm"))).csv") $compareProps
}
}
else
{
Write-Log "Folder $folder not found. Skipping import" 2
Write-Log "Folder $folder not found. Skipping compare" 2
}
}
if($outputType -eq "all" -and $compResultValues.Count -gt 0)
{
Save-BulkCompareResults $compResultValues (Join-Path $folder "Compare_$(((Get-Date).ToString("yyyyMMDD-HHmm"))).csv") $compareProps
Save-BulkCompareResults $compResultValues (Join-Path $rootFolder "Compare_$(((Get-Date).ToString("yyyyMMDD-HHmm"))).csv") $compareProps
}
Write-Log "****************************************************************"
@@ -618,6 +678,185 @@ function Start-BulkCompareExportObjects
}
}
function Start-BulkCompareExportFolders
{
Write-Log "****************************************************************"
Write-Log "Start bulk Exported Folders compare"
Write-Log "****************************************************************"
$compareObjectsResult = @()
$txtNameFilter = (Get-XamlProperty $global:ccContentProviderOptions.Content "txtCompareNameFilter" "Text").Trim()
$rootFolderSource = (Get-XamlProperty $global:ccContentProviderOptions.Content "txtExportPathSource" "Text")
$rootFolderCompare = (Get-XamlProperty $global:ccContentProviderOptions.Content "txtExportPathCompare" "Text")
$compareProps = $script:defaultCompareProps
foreach($removeProp in $global:cbCompareProvider.SelectedItem.RemoveProperties)
{
$compareProps.Remove($removeProp) | Out-Null
}
foreach($removeProp in $global:cbCompareType.SelectedItem.RemoveProperties)
{
$compareProps.Remove($removeProp) | Out-Null
}
if(-not $rootFolderSource -or -not $rootFolderCompare)
{
[System.Windows.MessageBox]::Show("Both folders must be specified", "Error", "OK", "Error")
return
}
if([IO.Directory]::Exists($rootFolderSource) -eq $false)
{
[System.Windows.MessageBox]::Show("Root folder $rootFolderSource does not exist", "Error", "OK", "Error")
return
}
if([IO.Directory]::Exists($rootFolderCompare) -eq $false)
{
[System.Windows.MessageBox]::Show("Root folder $rootFolderCompare does not exist", "Error", "OK", "Error")
return
}
$outputType = $global:cbCompareSave.SelectedValue
Save-Setting "Compare" "SaveType" $outputType
$compResultValues = @()
foreach($item in ($global:dgObjectsToCompare.ItemsSource | where Selected -eq $true))
{
Write-Status "Compare $($item.ObjectType.Title) objects" -Force -SkipLog
Write-Log "----------------------------------------------------------------"
Write-Log "Compare $($item.ObjectType.Title) objects"
Write-Log "----------------------------------------------------------------"
$folderSource = Join-Path $rootFolderSource $item.ObjectType.Id
$folderCompare = Join-Path $rootFolderCompare $item.ObjectType.Id
if([IO.Directory]::Exists($folderSource))
{
Save-Setting "" "LastUsedFullPath" $folderSource
$fileCompareObjs = @(Get-GraphFileObjects $folderCompare -ObjectType $item.ObjectType)
foreach ($fileSourceObj in @(Get-GraphFileObjects $folderSource -ObjectType $item.ObjectType))
{
$objName = Get-GraphObjectName $fileSourceObj.Object $item.ObjectType
if($txtNameFilter -and $objName -notmatch [RegEx]::Escape($txtNameFilter))
{
continue
}
if(-not $fileSourceObj.Object.Id)
{
Write-Log "Object from file '$($fileSourceObj.FullName)' has no Id property. Compare not supported" 2
continue
}
$compareObject = $fileCompareObjs | Where { $_.Object.Id -eq $fileSourceObj.Object.Id }
if(-not $compareObject)
{
# Add objects that are exported but deleted
Write-Log "Object '$($objName)' with id $($fileSourceObj.Object.Id) not found in Intune. Deleted?" 2
$compareProperties = @([PSCustomObject]@{
Object1Value = $null
Object2Value = (Get-GraphObjectName $fileSourceObj.Object $fileSourceObj.ObjectType)
Match = $false
})
}
else
{
$fileSourceObj.Object | Add-Member Noteproperty -Name "@ObjectFromFile" -Value $true -Force
$compareObject.Object | Add-Member Noteproperty -Name "@ObjectFromFile" -Value $true -Force
$compareProperties = Compare-Objects $compareObject.Object $fileSourceObj.Object $item.ObjectType
}
$compareObjectsResult += [PSCustomObject]@{
Object1 = $compareObject.Object
Object2 = $fileSourceObj.Object
ObjectType = $item.ObjectType
Id = $fileSourceObj.Object.Id
Result = $compareProperties
}
}
foreach($fileCompareObj in $fileCompareObjs)
{
# Add objects that were not exported in source folder
if(($compareObjectsResult | Where { $_.Id -eq $fileCompareObj.Object.Id})) { continue }
$objName = Get-GraphObjectName $fileCompareObj.Object $item.ObjectType
if($txtNameFilter -and $objName -notmatch [RegEx]::Escape($txtNameFilter))
{
continue
}
$compareObjectsResult += [PSCustomObject]@{
Object1 = $fileCompareObj.Object
Object2 = $null
ObjectType = $item.ObjectType
Id = $fileCompareObj.Object.Id
Result = @([PSCustomObject]@{
Object1Value = (Get-GraphObjectName $fileCompareObj.Object $item.ObjectType)
Object2Value = $null
Match = $false
})
}
}
if($outputType -eq "objectType")
{
$compResultValues = @()
}
foreach($compObj in @($compareObjectsResult | Where { $_.ObjectType.Id -eq $item.ObjectType.Id }))
{
$objName = Get-GraphObjectName (?? $compObj.Object1 $compObj.Object2) $item.ObjectType
foreach($compValue in $compObj.Result)
{
$compResultValues += [PSCustomObject]@{
ObjectName = $objName
Id = $compObj.Id
Type = $compObj.ObjectType.Title
ODataType = $compObj.Object1.'@OData.Type'
Property = $compValue.PropertyName
Value1 = $compValue.Object1Value
Value2 = $compValue.Object2Value
Category = $compValue.Category
SubCategory = $compValue.SubCategory
Match = $compValue.Match
}
}
}
if($outputType -eq "objectType")
{
Save-BulkCompareResults $compResultValues (Join-Path $folderSource "Compare_$(((Get-Date).ToString("yyyyMMdd-HHmm"))).csv") $compareProps
}
}
else
{
Write-Log "Folder $folderSource not found. Skipping compare" 2
}
}
if($outputType -eq "all" -and $compResultValues.Count -gt 0)
{
Save-BulkCompareResults $compResultValues (Join-Path $rootFolderSource "Compare_$(((Get-Date).ToString("yyyyMMDD-HHmm"))).csv") $compareProps
}
Write-Log "****************************************************************"
Write-Log "Bulk compare Exported Folders finished"
Write-Log "****************************************************************"
Write-Status ""
if($compareObjectsResult.Count -eq 0)
{
[System.Windows.MessageBox]::Show("No objects were comparced. Verify folder and exported files", "Error", "OK", "Error")
}
}
function Save-BulkCompareResults
{
param($compResultValues, $file, $props)
@@ -1064,6 +1303,11 @@ function Compare-ObjectsBasedonDocumentation
$val1 = $prop.$settingsValue
$prop2 = $docObj2.Settings | Where { $_.EntityKey -eq $prop.EntityKey -and $_.Category -eq $prop.Category -and $_.SubCategory -eq $prop.SubCategory -and $_.Enabled -eq $prop.Enabled }
$val2 = $prop2.$settingsValue
if($val1 -isnot [array] -and $val2 -is [array] -and $val2.Count -gt 1)
{
Write-Log "Multiple compare results returend for $($prop.Name). Using first result" 2
$val2 = $val2[0]
}
Add-CompareProperty $prop.Name $val1 $val2 $prop.Category $prop.SubCategory
}
@@ -1076,6 +1320,12 @@ function Compare-ObjectsBasedonDocumentation
$val2 = $prop.$settingsValue
$prop2 = $docObj1.Settings | Where { $_.EntityKey -eq $prop.EntityKey -and $_.Category -eq $prop.Category -and $_.SubCategory -eq $prop.SubCategory -and $_.Enabled -eq $prop.Enabled }
$val1 = $prop2.$settingsValue
if($val2 -isnot [array] -and $val1 -is [array] -and $val1.Count -gt 1)
{
Write-Log "Multiple compare results returend for $($prop.Name). Using first result" 2
$val1 = $val1[0]
}
Add-CompareProperty $prop.Name $val1 $val2 $prop.Category $prop.SubCategory
}
}

View File

@@ -3,7 +3,7 @@
#https://docs.microsoft.com/en-us/office/vba/api/overview/word
function Get-ModuleVersion
{
'1.0.4'
'1.0.5'
}
function Invoke-InitializeModule
@@ -117,6 +117,7 @@ function Invoke-WordPreProcessItems
Save-Setting "Documentation" "WordExportProperties" $global:cbWordDocumentationProperties.SelectedValue
Save-Setting "Documentation" "WordCustomDisplayProperties" $global:txtWordCustomProperties.Text
Save-Setting "Documentation" "WordDocumentTemplate" $global:txtWordDocumentTemplate.Text
Save-Setting "Documentation" "WordDocumentName" $global:txtWordDocumentName.Text
Save-Setting "Documentation" "WordAddCategories" $global:chkWordAddCategories.IsChecked
Save-Setting "Documentation" "WordAddSubCategories" $global:chkWordAddSubCategories.IsChecked

View File

@@ -9,7 +9,7 @@ Module for listing Intune assignments
#>
function Get-ModuleVersion
{
'1.0.1'
'1.0.2'
}
function Invoke-InitializeModule
@@ -170,6 +170,7 @@ function Get-EMIntuneAssignments
}
else
{
$assignmentObj = $assignment.target.groupId
Write-Warning "Could not find a group with ID $($assignment.target.groupId)"
}
$included = $assignment.target.'@odata.type' -eq "#microsoft.graph.groupAssignmentTarget"

View File

@@ -10,7 +10,7 @@ This module manages Authentication for the application with MSAL. It is also res
#>
function Get-ModuleVersion
{
'3.0.5'
'3.3.0'
}
$global:msalAuthenticator = $null
@@ -18,10 +18,28 @@ function Invoke-InitializeModule
{
$script:MSALAllApps = @()
$global:MSALToken = $null
$global:MSALAuthority = $null
$global:MSALTenantId = $null
$script:AccessableTenants = $null
$global:SkipTokenCacheHelperEx = $null
$script:lstAADEnvironments = @(
[PSCustomObject]@{
Name = "Azure AD Public"
Value = "public"
URL = "login.microsoftonline.com"
},
[PSCustomObject]@{
Name = "Azure AD US Government"
Value = "usGov"
URL = "login.microsoftonline.us"
},
[PSCustomObject]@{
Name = "Azure AD China"
Value = "china"
URL = "login.partner.microsoftonline.cn"
}
)
$global:appSettingSections += (New-Object PSObject -Property @{
Title = "MSAL"
Id = "MSAL"
@@ -68,6 +86,14 @@ function Invoke-InitializeModule
Description = "Request Azure AD Role read permission when getting the token. This can be use to resolve the SIDs to Azure Roles for the wids property on the Access Token. Note: This might trigger a consent prompt"
}) "MSAL"
Add-SettingsObject (New-Object PSObject -Property @{
Title = "Azure Login"
Key = "AzureLogin"
Type = "List"
ItemsSource = $script:lstAADEnvironments
DefaultValue = "public"
}) "MSAL"
Add-MSALPrereq
#$script:MSALDLLMissing = $true #!!!!
@@ -91,9 +117,19 @@ function Get-MSALAuthenticationObject
$global:msalAuthenticator
}
function Invoke-SettingsUpdated
{
Initialize-MSALSettings
}
function Initialize-MSALSettings
{
}
function Clear-MSALCurentUserVaiables
{
$global:MSALAuthority = $null
$global:MSALTenantId = $null
}
function Get-MSALCurrentApp
@@ -485,19 +521,42 @@ function Get-MsalAuthenticationToken
$authResult
}
function Get-MSALLoginEnvironment
{
$loginValue = Get-SettingValue "AzureLogin" "public"
$loginEnv = $script:lstAADEnvironments | Where value -eq $loginValue
return (?? $loginEnv.Environment "login.microsoftonline.com")
}
function Get-MSALApp
{
param($appInfo)
param($appInfo, $loginHint)
$msalApp = $script:MSALAllApps | Where { $_.ClientId -eq $appInfo.ClientID -and (-not $appInfo.RedirectUri -or $_.AppConfig.RedirectUri -eq $appInfo.RedirectUri)}
if(-not $msalApp)
$tenant = ?? $appInfo.TenantId "organizations"
if($loginHint.Environment)
{
Write-Log "Add MSAL App $($appInfo.ClientID) $((?? $appInfo.TenantId $appInfo.Authority))"
$authority = "https://$($loginHint.Environment)/$tenant/"
}
elseif($appInfo.Authority)
{
$authority = $appInfo.Authority
}
else
{
$authority = "https://$((Get-MSALLoginEnvironment))/$tenant/"
}
if(-not $msalApp -or $msalApp.Authority -ne $authority)
{
Write-Log "Add MSAL App $($appInfo.ClientID) $authority"
$appBuilder = [Microsoft.Identity.Client.PublicClientApplicationBuilder]::Create($appInfo.ClientID)
if($appInfo.TenantId) { [void]$appBuilder.WithAuthority("https://login.microsoftonline.com/$($appInfo.TenantId)/") }
elseif ($appInfo.Authority) { [void]$appBuilder.WithAuthority($appInfo.Authority) }
[void]$appBuilder.WithAuthority($authority)
#if($appInfo.TenantId) { [void]$appBuilder.WithAuthority("https://$((?? $loginHint.Environment (Get-MSALLoginEnvironment)))/$($appInfo.TenantId)/") }
#elseif ($appInfo.Authority) { [void]$appBuilder.WithAuthority($appInfo.Authority) }
if($appInfo.RedirectUri) { [void]$appBuilder.WithRedirectUri($appInfo.RedirectUri) }
[void] $appBuilder.WithClientName("CloudAPIPowerShellManagement")
@@ -514,6 +573,18 @@ function Get-MSALApp
return $msalApp
}
function Get-MSALAppAuthority
{
try
{
([uri]$global:MSALApp.Authority).Authority
}
catch
{
Get-MSALLoginEnvironment
}
}
function Connect-MSALUser
{
param(
@@ -529,7 +600,9 @@ function Connect-MSALUser
[switch]
$Interactive,
$Account
$Account,
$Tenant
)
# No login during first time the app is started
@@ -543,11 +616,10 @@ function Connect-MSALUser
return
}
if(-not $global:appObj.TenantId -and -not $global:appObj.Authority)
{
Write-Log "Tenant id/Authority is missing. Cannot authenticate" 3
return
}
#if(-not $global:appObj.TenantId -and -not $global:appObj.Authority)
#{
# Write-Log "Tenant id/Authority is missing. Cannot authenticate" 3
#}
if ($global:SkipTokenCacheHelperEx -ne $true -and -not ("TokenCacheHelperEx" -as [type]))
{
@@ -598,14 +670,15 @@ function Connect-MSALUser
$Scopes = [String[]]$reqScopes
}
$global:MSALApp = Get-MSALApp $global:appObj
$global:MSALApp = Get-MSALApp $global:appObj $Account
$loginHint = ""
$global:MSALAccounts = $global:MSALApp.GetAccountsAsync().GetAwaiter().GetResult()
if($Account)
{
$loginHint = $global:MSALAccounts | Where UserName -eq $Account
if($global:MSALToken -and $global:MSALToken.Account.UserName -ne $Account)
$userName = ?? $Account.UserName $Account
$loginHint = $global:MSALAccounts | Where UserName -eq $userName
if($global:MSALToken -and $global:MSALToken.Account.UserName -ne $userName)
{
# We're logging in with someone else...
Clear-MSALCurentUserVaiables
@@ -640,8 +713,8 @@ function Connect-MSALUser
$prompConsent = $false
$authResult = $null
$tenantId = $global:appObj.TenantId
$authority = ?? $global:MSALAuthority $global:appObj.Authority
$tenantId = ?? $global:MSALTenantId $global:appObj.TenantId
#$authority = ?? $global:MSALApp.Authority $global:appObj.Authority
try
{
@@ -652,8 +725,8 @@ function Connect-MSALUser
{
$aquireTokenObj = $global:MSALApp.AcquireTokenSilent($Scopes, $loginHint)
if($ForceRefresh) { [void]$aquireTokenObj.WithForceRefresh($ForceRefresh) }
if ($tenantId) { [void] $aquireTokenObj.WithAuthority("https://login.microsoftonline.com/$($TenantId)/") }
if ($authority) { [void]$aquireTokenObj.WithAuthority($authority) }
if ($tenantId) { [void]$aquireTokenObj.WithAuthority("https://$((Get-MSALAppAuthority))/$($tenantId)/") }
else { [void]$aquireTokenObj.WithAuthority($global:MSALApp.Authority) }
$authResult = Get-MsalAuthenticationToken $aquireTokenObj
@@ -709,7 +782,10 @@ function Connect-MSALUser
}
}
}
catch {}
catch
{
Write-LogError "Failed to perform silent login" $_.Exception
}
# Interactive login is only allowed once the app has started. Skip if silent login failed during startup
if($global:MainAppStarted -and ((-not $authResult -and $Silent -ne $true) -or $prompConsent))
@@ -726,12 +802,12 @@ function Connect-MSALUser
if ($tenantId)
{
Write-Log "Tenant id: $tenantId"
[void] $aquireTokenObj.WithAuthority("https://login.microsoftonline.com/$tenantId)/")
[void]$aquireTokenObj.WithAuthority("https://$((Get-MSALAppAuthority))/$tenantId/")
}
elseif ($authority)
else
{
Write-Log "Authority: $authority"
[void]$aquireTokenObj.WithAuthority($authority)
Write-Log "Authority: $($global:MSALApp.Authority)"
[void]$aquireTokenObj.WithAuthority($global:MSALApp.Authority)
}
if($loginHintName)
@@ -786,8 +862,8 @@ function Connect-MSALUser
# Can we reuse the app used for login?
$appBuilder = [Microsoft.Identity.Client.PublicClientApplicationBuilder]::Create($global:appObj.ClientID)
if($tenantId) { [void]$appBuilder.WithAuthority("https://login.microsoftonline.com/$($tenantId)") }
elseif ($authority) { [void]$appBuilder.WithAuthority($authority) }
if($tenantId) { [void]$appBuilder.WithAuthority("https://$((Get-MSALAppAuthority))/$($tenantId)") }
else { [void]$appBuilder.WithAuthority($global:MSALApp.Authority) }
if($global:appObj.RedirectUri) { [void]$appBuilder.WithRedirectUri($global:appObj.RedirectUri) }
$app = $appBuilder.Build()
@@ -977,7 +1053,7 @@ function Get-MSALProfileEllipse
Write-Status "Logging in with $($this.Tag.UserName)"
Hide-Popup
Clear-MSALCurentUserVaiables
Connect-MSALUser -Account $this.Tag.UserName
Connect-MSALUser -Account $this.Tag #!!!.UserName
if($global:curObjectType)
{
@@ -1200,7 +1276,7 @@ function Get-MSALProfileEllipse
Write-Status "Logging in with $($this.Tag.UserName)"
Hide-Popup
Clear-MSALCurentUserVaiables
Connect-MSALUser -Account $this.Tag.UserName
Connect-MSALUser -Account $this.Tag #!!!.UserName
if($global:curObjectType)
{
@@ -1311,9 +1387,9 @@ function Get-MSALProfileEllipse
$lnkButton.add_Click({
Write-Status "Logging in to $($this.Tag.DisplayName)"
# Set authority to selected tenant
$global:MSALAuthority = "https://login.microsoftonline.com/$($this.Tag.tenantId)/"
$global:MSALTenantId = $this.Tag.tenantId
Hide-Popup
Connect-MSALUser -Account $global:MSALToken.Account.Username
Connect-MSALUser -Account ($global:MSALAccounts | Where UserName -eq $global:MSALToken.Account.Username)
if($global:curObjectType)
{

View File

@@ -10,13 +10,14 @@ This module manages Microsoft Grap fuctions like calling APIs, managing graph ob
#>
function Get-ModuleVersion
{
'3.1.7'
'3.1.8'
}
$global:MSGraphGlobalApps = @(
#Authority="https://login.microsoftonline.com/organizations/"
(New-Object PSObject -Property @{Name="";ClientId="";RedirectUri="";Authority=""}),
(New-Object PSObject -Property @{Name="Microsoft Intune PowerShell";ClientId="d1ddf0e4-d672-4dae-b554-9d5bdfd93547";RedirectUri="urn:ietf:wg:oauth:2.0:oob";Authority="https://login.microsoftonline.com/organizations/"}),
(New-Object PSObject -Property @{Name="Microsoft Graph PowerShell";ClientId="14d82eec-204b-4c2f-b7e8-296a70dab67e";RedirectUri="https://login.microsoftonline.com/common/oauth2/nativeclient";Authority="https://login.microsoftonline.com/organizations/"})
(New-Object PSObject -Property @{Name="Microsoft Intune PowerShell";ClientId="d1ddf0e4-d672-4dae-b554-9d5bdfd93547";RedirectUri="urn:ietf:wg:oauth:2.0:oob"; }),
(New-Object PSObject -Property @{Name="Microsoft Graph PowerShell";ClientId="14d82eec-204b-4c2f-b7e8-296a70dab67e";RedirectUri="https://login.microsoftonline.com/common/oauth2/nativeclient";})
)
function Invoke-InitializeModule
@@ -150,6 +151,15 @@ function Invoke-InitializeModule
DefaultValue = $false
Description = "This will enable the option to update/replace an existing object during import"
}) "ImportExport"
Add-SettingsObject (New-Object PSObject -Property @{
Title = "Add ID to export file"
Key = "AddIDToExportFile"
Type = "Boolean"
DefaultValue = $false
Description = "This will add object ID to the export file to support objects with the same name e.g. ObjectName_ObjectId.json"
}) "ImportExport"
}
function Get-GraphAppInfo
@@ -192,6 +202,16 @@ function Invoke-GraphAuthenticationUpdated
$global:migFileObj = $null
}
function Invoke-SettingsUpdated
{
Initialize-GraphSettings
}
function Initialize-GraphSettings
{
}
function Invoke-GraphRequest
{
param (
@@ -845,6 +865,7 @@ function Show-GraphBulkExportForm
Set-XamlProperty $script:exportForm "txtExportPath" "Text" (?? (Get-Setting "" "LastUsedRoot") (Get-SettingValue "RootFolder"))
Set-XamlProperty $script:exportForm "chkAddCompanyName" "IsChecked" (Get-SettingValue "AddCompanyName")
Set-XamlProperty $script:exportForm "chkExportAssignments" "IsChecked" (Get-SettingValue "ExportAssignments")
#Set-XamlProperty $script:exportForm "txtExportNameFilter" "Text" (Get-Setting "" "ExportNameFilter")
Add-XamlEvent $script:exportForm "browseExportPath" "add_click" ({
$folder = Get-Folder (Get-XamlProperty $script:exportForm "txtExportPath" "Text") "Select root folder for export"
@@ -903,6 +924,9 @@ function Show-GraphBulkExportForm
Write-Log "****************************************************************"
Write-Log "Start bulk export"
Write-Log "****************************************************************"
$global:AADObjectCache = $null
foreach($item in $script:exportObjects)
{
if($item.Selected -ne $true) { continue }
@@ -911,6 +935,10 @@ function Show-GraphBulkExportForm
Write-Log "Export $($item.ObjectType.Title) objects"
Write-Log "----------------------------------------------------------------"
$txtNameFilter = $global:txtExportNameFilter.Text.Trim()
Save-Setting "" "ExportNameFilter" $txtNameFilter
if($txtNameFilter) { Write-Log "Name filter: $txtNameFilter" }
try
{
$folder = Get-GraphObjectFolder $item.ObjectType (Get-XamlProperty $script:exportForm "txtExportPath" "Text") (Get-XamlProperty $script:exportForm "chkAddObjectType" "IsChecked") (Get-XamlProperty $script:exportForm "chkAddCompanyName" "IsChecked")
@@ -918,7 +946,14 @@ function Show-GraphBulkExportForm
$objects = @(Get-GraphObjects -property $item.ObjectType.ViewProperties -objectType $item.ObjectType)
foreach($obj in $objects)
{
Write-Status "Export $($item.Title): $((Get-GraphObjectName $obj.Object $obj.ObjectType))" -Force
$objName = Get-GraphObjectName $obj.Object $obj.ObjectType
if($txtNameFilter -and $objName -notmatch [RegEx]::Escape($txtNameFilter))
{
continue
}
Write-Status "Export $($item.Title): $objName" -Force
Export-GraphObject $obj.Object $item.ObjectType $folder
}
Save-Setting "" "LastUsedFullPath" $folder
@@ -1064,6 +1099,7 @@ function Show-GraphBulkImportForm
Set-XamlProperty $script:importForm "chkImportScopes" "IsChecked" (Get-SettingValue "ImportScopeTags")
Set-XamlProperty $script:importForm "cbImportType" "ItemsSource" $script:lstImportTypes
Set-XamlProperty $script:importForm "cbImportType" "SelectedValue" (Get-SettingValue "ImportType" "alwaysImport")
#Set-XamlProperty $script:importForm "txtImportNameFilter" "Text" (Get-Setting "" "ImportNameFilter")
if((Get-SettingValue "AllowUpdate") -eq $true)
{
@@ -1140,6 +1176,10 @@ function Show-GraphBulkImportForm
Get-GraphDependencyDefaultObjects
$importedObjects = 0
$txtNameFilter = $global:txtImportNameFilter.Text.Trim()
Save-Setting "" "ImportNameFilter" $txtNameFilter
if($txtNameFilter) { Write-Log "Name filter: $txtNameFilter" }
$allowUpdate = ((Get-SettingValue "AllowUpdate") -eq $true)
foreach($item in ($script:importObjects | where Selected -eq $true | sort-object -property @{e={$_.ObjectType.ImportOrder}}))
@@ -1172,6 +1212,13 @@ function Show-GraphBulkImportForm
foreach ($fileObj in @($filesToImport))
{
$objName = Get-GraphObjectName $fileObj.Object $item.ObjectType
if($txtNameFilter -and $objName -notmatch [RegEx]::Escape($txtNameFilter))
{
continue
}
if($allowUpdate -and $global:cbImportType.SelectedValue -ne "alwaysImport" -and $graphObjects -and (Reset-GraphObjet $fileObj $graphObjects))
{
$importedObjects++
@@ -1274,6 +1321,8 @@ function Show-GraphBulkDeleteForm
$script:deleteForm = Get-XamlObject ($global:AppRootFolder + "\Xaml\BulkDeleteForm.xaml") -AddVariables
if(-not $script:deleteForm) { return }
Set-XamlProperty $script:deleteForm "txtDeleteNameFilter" "Text" (Get-Setting "" "txtDeleteNameFilter")
$script:deleteObjects = @()
foreach($objType in $global:lstMenuItems.ItemsSource)
{
@@ -1343,13 +1392,24 @@ function Show-GraphBulkDeleteForm
Write-Log "Delete $($item.ObjectType.Title) objects"
Write-Log "----------------------------------------------------------------"
$txtNameFilter = $global:txtDeleteNameFilter.Text.Trim()
Save-Setting "" "DeleteNameFilter" $txtNameFilter
if($txtNameFilter) { Write-Log "Name filter: $txtNameFilter" }
try
{
Write-Status "Get $($item.Title) objects" -Force
$objects = @(Get-GraphObjects -property $item.ObjectType.ViewProperties -objectType $item.ObjectType)
foreach($obj in $objects)
{
Write-Status "Delete $($item.Title): $((Get-GraphObjectName $obj.Object $obj.ObjectType))" -Force
$objName = Get-GraphObjectName $obj.Object $obj.ObjectType
if($txtNameFilter -and $objName -notmatch [RegEx]::Escape($txtNameFilter))
{
continue
}
Write-Status "Delete $($item.Title): $objName" -Force -SkipLog
Remove-GraphObject $obj.Object $obj.ObjectType $folder
}
}
@@ -1875,7 +1935,7 @@ function Add-GraphMigrationObject
$global:migFileObj = $null
}
if(-not $global:migFileObj)
if(-not $global:migFileObj -or ([IO.File]::Exists($migFileName) -eq $false))
{
if(-not ([IO.File]::Exists($migFileName)))
{
@@ -2238,7 +2298,13 @@ function Export-GraphObject
Remove-Property $obj "Assignments"
}
$obj | ConvertTo-Json -Depth 20 | Out-File -LiteralPath ([IO.Path]::Combine($exportFolder, (Remove-InvalidFileNameChars "$((Get-GraphObjectName $obj $objectType)).json"))) -Force
$fileName = Get-GraphObjectName $obj $objectType
if((Get-SettingValue "AddIDToExportFile") -eq $true -and $obj.Id)
{
$fileName = ($fileName + "_" + $obj.Id)
}
$obj | ConvertTo-Json -Depth 20 | Out-File -LiteralPath ([IO.Path]::Combine($exportFolder, (Remove-InvalidFileNameChars "$($fileName).json"))) -Force
if($objectType.PostExportCommand)
{

View File

@@ -23,9 +23,9 @@ Before logging on:
* The app will use the Intune PowerShell Azure Enterprise Application by default but request all permissions required by the script. The will most likely cause a consent prompt since it uses more permission than the Intune module. Enable **Use Default Permissions** in Settings to only request the current permissions granted to the Enterprise App.
**Note:** Using default permission might reduce functionality e.g. permissions for one or more object types might be missing
* Enable **Get Tenant List** in Settings if accessing multiple environments with the same account. This might cause a Consent prompt
* Enable **Get Tenant List** in Settings if accessing multiple environments with the same account e.g. a guest account in other tenants. This might cause a Consent prompt
Start the script by running **Start.cmd**, **Start-WithConsole.cmd** or **Start-IntuneManagement.ps1**. **Start-WithConsole.cmd** will leave the command prompt window open so you can see the log while running the app.
Start the script by running **Start.cmd**, **Start-WithJson.cmd**, **Start-WithConsole.cmd** or **Start-IntuneManagement.ps1**. **Start-WithConsole.cmd** will leave the command prompt window open so you can see the log while running the app.
## Documentation
@@ -171,6 +171,28 @@ See [Change Log](ReleaseNotes.md) for more information
## Authentication
See [MSAL Info](MSALInfo.md) for more information about authentication
## Settings
Settings for the script is default stored in the registry. However, the script supports settings to be stored in a json file so it can be copied between computers. Registry settings can be exported in the Settings dialog.
To use settings based on a json file:
```
Start-IntuneManagement.ps1 -JSonSettings [-JSonFile <PathToFile>]
```
If only -JSonSettings is used the script will use the default json setting file:
```
%LOCALAPPDATA%\CloudAPIPowerShellManagement\Settings.json
```
Use -JSonFile for custom location of the file
Start-WithJson.cmd is included as an example on how to start the script with json settings.
**Note:** If the file can't be created, the script will revert back registry. Make sure that the script can write to the file. It is not recommended to store the file in a folder that requires UAC to get write permissions.
## Supported Intune objects
* App Configurations (App and Device)
* App Protection
@@ -235,7 +257,7 @@ Android Store Apps are **not** imported. The Create API is documented in Microso
Using multiple tenants support causes multiple logins/consent prompts the first time if 'Microsoft Graph PowerShell' is used. Querying the API for tenant list uses a different scope that is not included by default in the 'Microsoft Graph PowerShell' app.
~~Using multiple tenants support *might* cause and endless loop in the login screen and cause duplicate accounts in token cache. Actual cause is not found yet but it happens on rare occasions and it looks like it happens when a guest account is used. Workaround: Cancel the login, restart the script, logout and restart the script again.~~ - Not seen this in a long time. Please create issue if this happens
~~Using multiple tenants support *might* cause and endless loop in the login screen and cause duplicate accounts in token cache. Actual cause is not found yet but it happens on rare occasions and it looks like it happens when a guest account is used. Workaround: Cancel the login, restart the script, logout and restart the script again.~~ - Not seen this in a long time. Please create an issue if this happens
When multi tenant settings is Enabled/Disabled, the Profile Info is not updated until the account is changed or app is restarted. Profile Info popup is built after logon.

View File

@@ -1,5 +1,45 @@
# Release Notes
## 3.3.0 (Beta) - 2021-10-17
This is a **BETA** release. It contains core changes for Authentication and Settings management. Please report any issues [here](https://github.com/Micke-K/IntuneManagement/issues).
**New features**
- Support for Settings in Json files
Settings can now be stored in json files and copied between devices.
See [Readme](README.md#Settings) on how to use this feature
This is based on [Issue 33](https://github.com/Micke-K/IntuneManagement/issues/33)
- Bulk Compare for exported folders
The tool can now compare two exported folders
This is based on [Issue 32](https://github.com/Micke-K/IntuneManagement/issues/32)
- Support for Azure AD US Government cloud and Azure AD China cloud. Default is Azure AD Public cloud.
Change cloud in Settings
**Note:** This is a major change to the authentication. This may have an impact if a custom configured Azure app is used.
This is based on [Issue 26](https://github.com/Micke-K/IntuneManagement/issues/26). Please report any problem, progress or testing with US Government/China cloud or if there are any issues when a custom configured Azure app is used.
- Export can now add Id to the name of the backup file
This can be used if there are multiple objects with the same name.
This can be enabled in Settings. Backup file name will be <Name>_<Id>.json.
- Export/Import/Compare/Delete now supports name filter
Objects are filtered based on escaped RegEx -nomatch expression so wildcards are not supported.
- IntuneAssignments report will now include the id of deleted groups
**Fixes**
* Fixed an issue in Export. Groups were not exported if exporting multiple times and multiple folders during the same session.
* Fixed an issue in Compare where the csv file was not stored in the correct folder
* Fixed an issue in Compare where the comparing object may return System[]. This can happen if the generated files has multiple documentation items for a property. First result will be used.
## 3.2.3 - 2021-10-07
**New features**

View File

@@ -1,7 +1,11 @@
[CmdletBinding(SupportsShouldProcess=$True)]
param(
[switch]
$ShowConsoleWindow
$ShowConsoleWindow,
[switch]
$JSonSettings,
[string]
$JSonFile
)
Import-Module ($PSScriptRoot + "\CloudAPIPowerShellManagement.psd1") -Force
Initialize-CloudAPIManagement -View "IntuneGraphAPI" -ShowConsoleWindow:($ShowConsoleWindow)
Initialize-CloudAPIManagement -View "IntuneGraphAPI" -ShowConsoleWindow:($ShowConsoleWindow) -JSonSettings:($JSonSettings) -JSonFile $JSonFile

1
Start-WithJson.cmd Normal file
View File

@@ -0,0 +1 @@
cmd /c powershell -ex bypass -File "%~DP0Start-IntuneManagement.ps1" -JSonSettings

View File

@@ -1,19 +1,31 @@
<Grid xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Margin="5,5,5,5" Grid.IsSharedSizeScope='True'>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="TitleColumn" />
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<StackPanel Orientation="Horizontal" Grid.Row='0' Margin="0,0,5,0" VerticalAlignment="Top">
<StackPanel Orientation="Horizontal" Grid.Row='0' Margin="0,0,5,0" >
<Label Content="Name filter" />
<Rectangle Style="{DynamicResource InfoIcon}" ToolTip="Specify name filter for the objects to delete" />
</StackPanel>
<TextBox Text="" Name="txtDeleteNameFilter" Grid.Column='1' Grid.Row='0' Margin="0,5,0,0" />
<StackPanel Orientation="Horizontal" Grid.Row='1' Margin="5,0,5,0" VerticalAlignment="Top" Grid.ColumnSpan="2">
<Label Content="Objects to delete" />
<Rectangle Style="{DynamicResource InfoIcon}" ToolTip="All objects of the seleted types will be deleted" Margin="0,2,0,0" />
</StackPanel>
<DataGrid Name="dgBulkDeleteObjects" Grid.Row='1' CanUserAddRows="False" AutoGenerateColumns="False" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Background="White" Margin="0,0,0,5">
<DataGrid Name="dgBulkDeleteObjects" Grid.Row='2' Grid.ColumnSpan="2" CanUserAddRows="False" AutoGenerateColumns="False" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Background="White" Margin="0,0,0,5">
</DataGrid>
<StackPanel Name="spDeleteSubMenu" Orientation="Horizontal" HorizontalAlignment="Right" Grid.Row='2' Grid.ColumnSpan='2' >
<StackPanel Name="spDeleteSubMenu" Orientation="Horizontal" HorizontalAlignment="Right" Grid.Row='3' Grid.ColumnSpan='2' >
<Button Name="btnDelete" Content="Delete" Width='100' Margin="5,0,0,0" />
<Button Name="btnClose" Content="Close" Width='100' Margin="5,0,0,0" />
</StackPanel>

View File

@@ -37,24 +37,30 @@
<Button Grid.Column="2" Name="browseExportPath" Padding="5,0,5,0" Width="50" ToolTip="Browse for folder">...</Button>
</Grid>
<StackPanel Orientation="Horizontal" Grid.Row='1' Margin="0,0,5,0" >
<Label Content="Name filter" />
<Rectangle Style="{DynamicResource InfoIcon}" ToolTip="Specify name filter for the objects to export" />
</StackPanel>
<TextBox Text="" Name="txtExportNameFilter" Grid.Column='1' Grid.Row='1' Margin="0,5,0,0" />
<!-- Force object type in name by setting it to true and disable the checkbox. Leave it on for information -->
<StackPanel Orientation="Horizontal" Grid.Row='1' Margin="0,0,5,0">
<StackPanel Orientation="Horizontal" Grid.Row='2' Margin="0,0,5,0">
<Label Content="Add object name to path" />
<Rectangle Style="{DynamicResource InfoIcon}" ToolTip="This will export all objects to a sub-directory of the export path with name based on object type" />
</StackPanel>
<CheckBox Grid.Column='1' Grid.Row='1' Name='chkAddObjectType' VerticalAlignment="Center" IsEnabled="false" IsChecked="true" />
<CheckBox Grid.Column='1' Grid.Row='2' Name='chkAddObjectType' VerticalAlignment="Center" IsEnabled="false" IsChecked="true" />
<StackPanel Orientation="Horizontal" Grid.Row='2' Margin="0,0,5,0">
<StackPanel Orientation="Horizontal" Grid.Row='3' Margin="0,0,5,0">
<Label Content="Export Assignments" />
<Rectangle Style="{DynamicResource InfoIcon}" ToolTip="Export object assignments" />
</StackPanel>
<CheckBox Grid.Column='1' Grid.Row='2' Name='chkExportAssignments' VerticalAlignment="Center" IsChecked="true" />
<CheckBox Grid.Column='1' Grid.Row='3' Name='chkExportAssignments' VerticalAlignment="Center" IsChecked="true" />
<StackPanel Orientation="Horizontal" Grid.Row='3' Margin="0,0,5,0">
<StackPanel Orientation="Horizontal" Grid.Row='4' Margin="0,0,5,0">
<Label Content="Add company name to path" />
<Rectangle Style="{DynamicResource InfoIcon}" ToolTip="This will add the company name in Azure to the export path" />
</StackPanel>
<CheckBox Grid.Column='1' Grid.Row='3' Name='chkAddCompanyName' VerticalAlignment="Center" IsChecked="true" />
<CheckBox Grid.Column='1' Grid.Row='4' Name='chkAddCompanyName' VerticalAlignment="Center" IsChecked="true" />
</Grid>
<Grid Grid.Row='1' VerticalAlignment="Stretch">

View File

@@ -15,6 +15,7 @@
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="TitleColumn" />
@@ -38,38 +39,44 @@
<Button Grid.Column="2" Name="browseImportPath" Padding="5,0,5,0" Width="50" ToolTip="Browse for folder">...</Button>
</Grid>
<StackPanel Orientation="Horizontal" Grid.Row='1' Margin="0,0,5,0" Name="spMigrationTableInfo">
<StackPanel Orientation="Horizontal" Grid.Row='1' Margin="0,0,5,0" >
<Label Content="Name filter" />
<Rectangle Style="{DynamicResource InfoIcon}" ToolTip="Specify name filter for the objects to import" />
</StackPanel>
<TextBox Text="" Name="txtImportNameFilter" Grid.Column='1' Grid.Row='1' Margin="0,5,0,0" />
<StackPanel Orientation="Horizontal" Grid.Row='2' Margin="0,0,5,0" Name="spMigrationTableInfo">
<Label Content="Migration Table" />
<Rectangle Style="{DynamicResource InfoIcon}" ToolTip="This contains information about the exported environment e.g. Groups, ScopeTags etc. Note: This is only used when import object from a different tenant" />
</StackPanel>
<Label Grid.Column='1' Grid.Row='1' Name="lblMigrationTableInfo" />
<Label Grid.Column='1' Grid.Row='2' Name="lblMigrationTableInfo" />
<!-- Force object type in name by setting it to true and disable the checkbox. Leave it on for information -->
<StackPanel Orientation="Horizontal" Grid.Row='2' Margin="0,0,5,0">
<StackPanel Orientation="Horizontal" Grid.Row='3' Margin="0,0,5,0">
<Label Content="Add object name to path" />
<Rectangle Style="{DynamicResource InfoIcon}" ToolTip="This will import objects from a sub-directory of the import path with name based on object type" />
</StackPanel>
<CheckBox Grid.Column='1' Grid.Row='2' Name='chkAddObjectType' VerticalAlignment="Center" IsEnabled="false" IsChecked="true" />
<CheckBox Grid.Column='1' Grid.Row='3' Name='chkAddObjectType' VerticalAlignment="Center" IsEnabled="false" IsChecked="true" />
<StackPanel Orientation="Horizontal" Grid.Row='3' Margin="0,0,5,0">
<StackPanel Orientation="Horizontal" Grid.Row='4' Margin="0,0,5,0">
<Label Content="Import Scope (Tags)" />
<Rectangle Style="{DynamicResource InfoIcon}" ToolTip="This will import ScopeTags. The ScopeTags must exist in the target environment before thay can be assigned during import of an object" />
</StackPanel>
<CheckBox Grid.Column='1' Grid.Row='3' Name='chkImportScopes' VerticalAlignment="Center" IsChecked="true" />
<CheckBox Grid.Column='1' Grid.Row='4' Name='chkImportScopes' VerticalAlignment="Center" IsChecked="true" />
<StackPanel Orientation="Horizontal" Grid.Row='4' Margin="0,0,5,0">
<StackPanel Orientation="Horizontal" Grid.Row='5' Margin="0,0,5,0">
<Label Content="Import Assignments" />
<Rectangle Style="{DynamicResource InfoIcon}" ToolTip="Import object assignments. Note: This will create groups that don't exist in the target environment" />
</StackPanel>
<CheckBox Grid.Column='1' Grid.Row='4' Name='chkImportAssignments' VerticalAlignment="Center" IsChecked="true" />
<CheckBox Grid.Column='1' Grid.Row='5' Name='chkImportAssignments' VerticalAlignment="Center" IsChecked="true" />
<StackPanel Orientation="Horizontal" Grid.Row='5' Margin="0,0,5,0">
<StackPanel Orientation="Horizontal" Grid.Row='6' Margin="0,0,5,0">
<Label Content="Replace Dependecy IDs" />
<Rectangle Style="{DynamicResource InfoIcon}" ToolTip="Replaces IDs of dependency objects e.g. App Config references Applications. Increases import time but makes sure objects are imported correctly. Note: References objects must exist!" />
</StackPanel>
<CheckBox Grid.Column='1' Grid.Row='5' Name='chkReplaceDependencyIDs' VerticalAlignment="Center" IsChecked="true" />
<CheckBox Grid.Column='1' Grid.Row='6' Name='chkReplaceDependencyIDs' VerticalAlignment="Center" IsChecked="true" />
<StackPanel Orientation="Horizontal" Grid.Row='6' Margin="0,0,5,0" Name="lblImportType" Visibility="Collapsed">
<StackPanel Orientation="Horizontal" Grid.Row='7' Margin="0,0,5,0" Name="lblImportType" Visibility="Collapsed">
<Label Content="Import Type" />
<Rectangle Style="{DynamicResource InfoIcon}">
<Rectangle.ToolTip>
@@ -83,7 +90,7 @@
</Rectangle.ToolTip>
</Rectangle>
</StackPanel>
<ComboBox Name="cbImportType" Margin="0,5,0,0" MinWidth="250" Grid.Row='6' Grid.Column="1" HorizontalAlignment="Left"
<ComboBox Name="cbImportType" Margin="0,5,0,0" MinWidth="250" Grid.Row='7' Grid.Column="1" HorizontalAlignment="Left"
DisplayMemberPath="Name" SelectedValuePath="Value" Visibility="Collapsed" />
</Grid>

View File

@@ -8,11 +8,17 @@
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<StackPanel Orientation="Horizontal" Margin="0,0,5,0" >
<StackPanel Orientation="Horizontal" Grid.Row='0' Margin="0,0,5,0" >
<Label Content="Name filter" />
<Rectangle Style="{DynamicResource InfoIcon}" ToolTip="Specify name filter for the objects to compare" />
</StackPanel>
<TextBox Text="" Name="txtCompareNameFilter" Grid.Column='1' Grid.Row='0' Margin="0,5,5,0" />
<StackPanel Orientation="Horizontal" Grid.Row='1' Margin="0,0,5,0" >
<Label Content="Export root" />
<Rectangle Style="{DynamicResource InfoIcon}" ToolTip="The root folder where exported files are stored. This sould be the Company Name folder if it was included in the export." />
</StackPanel>
<Grid Grid.Column='1' Grid.Row='0' Margin="0,5,5,0">
<Grid Grid.Column='1' Grid.Row='1' Margin="0,5,5,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="5" />

View File

@@ -0,0 +1,51 @@
<Grid Name="grdImportProperties" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="TitleColumn" />
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<StackPanel Orientation="Horizontal" Grid.Row='0' Margin="0,0,5,0" >
<Label Content="Name filter" />
<Rectangle Style="{DynamicResource InfoIcon}" ToolTip="Specify name filter for the objects to compare" />
</StackPanel>
<TextBox Text="" Name="txtCompareNameFilter" Grid.Column='1' Grid.Row='0' Margin="0,5,5,0" />
<StackPanel Orientation="Horizontal" Grid.Row='1' Margin="0,0,5,0" >
<Label Content="Source root" />
<Rectangle Style="{DynamicResource InfoIcon}" ToolTip="The root folder where exported files are stored. This sould be the Company Name folder if it was included in the export." />
</StackPanel>
<Grid Grid.Column='1' Grid.Row='1' Margin="0,5,5,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="5" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<TextBox Text="" Name="txtExportPathSource" />
<Button Grid.Column="2" Name="browseExportPathSource" Padding="5,2,5,2" Width="50" ToolTip="Browse for folder">...</Button>
</Grid>
<StackPanel Orientation="Horizontal" Grid.Row='2' Margin="0,0,5,0" >
<Label Content="Compare root" />
<Rectangle Style="{DynamicResource InfoIcon}" ToolTip="The folder with exported object that should be compared with the source" />
</StackPanel>
<Grid Grid.Column='1' Grid.Row='2' Margin="0,5,5,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="5" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<TextBox Text="" Name="txtExportPathCompare" />
<Button Grid.Column="2" Name="browseExportPathCompare" Padding="5,2,5,2" Width="50" ToolTip="Browse for folder">...</Button>
</Grid>
</Grid>

View File

@@ -25,7 +25,8 @@
</ScrollViewer>
<StackPanel Grid.Row="1" HorizontalAlignment="Right" Orientation="Horizontal" Margin="0,5,0,5">
<Button Name="btnSave" Width="100">Save</Button>
<Button Name="btnExport" Width="100">Export</Button>
<Button Name="btnSave" Width="100" Margin="15,0,0,0">Save</Button>
<Button Name="btnClose" Width="100" Margin="15,0,0,0">Close</Button>
</StackPanel>
</Grid>