From 65ef411e2048977831f3cae5ae8dfbe0cd2e32a1 Mon Sep 17 00:00:00 2001 From: Mikael Karlsson <43226266+Micke-K@users.noreply.github.com> Date: Fri, 14 Aug 2026 21:40:48 +1000 Subject: [PATCH 1/4] Fixed Issue #428 --- Core.psm1 | 5908 ++++++++++---------- Extensions/EndpointManager.psm1 | 9178 +++++++++++++++---------------- 2 files changed, 7544 insertions(+), 7542 deletions(-) diff --git a/Core.psm1 b/Core.psm1 index 5b1f98c..71b7b75 100644 --- a/Core.psm1 +++ b/Core.psm1 @@ -1,2954 +1,2956 @@ -<# -.SYNOPSIS -Core UI and Settings fatures for the CloudAPIPowerShellManager solution - -.DESCRIPTION -This module handles the WPF UI - -.NOTES - Author: Mikael Karlsson -#> - -function Get-ModuleVersion -{ - '3.9.6' -} - -function Initialize-Window -{ - param($xamlFile) - - try - { - [xml]$xaml = Get-Content $xamlFile - [xml]$styles = Get-Content ($global:AppRootFolder + "\Themes\Styles.xaml") - - ### Update relative path to full path for ResourceDictionary - [System.Xml.XmlNamespaceManager] $nsm = $xaml.NameTable; - $nsm.AddNamespace("s", 'http://schemas.microsoft.com/winfx/2006/xaml/presentation'); - foreach($rsdNode in ($xaml.SelectNodes("//s:ResourceDictionary[@Source]", $nsm))) - { - $rsdNode.Source = (Join-Path ($global:AppRootFolder) ($rsdNode.Source)).ToString() - } - - # Add Styles - foreach($node in $styles.DocumentElement.ChildNodes) - { - $tmpNode = $xaml.CreateElement("Temp") - $tmpNode.InnerXml = $node.OuterXml - $xaml.Window.'Window.Resources'.ResourceDictionary.AppendChild($tmpNode.Style) | Out-Null - } - return ([Windows.Markup.XamlReader]::Load((New-Object System.Xml.XmlNodeReader $xaml))) - } - catch - { - Write-LogError "Failed to initialize window" $_.Exception - return - } -} - -function Start-CoreApp -{ - param($View) - - if(-not $global:defaultGlobalVariables) - { - $global:defaultGlobalVariables = Get-Variable -Scope Global - } - - $global:useDefaultFolderDialog = $false - $global:WindowsAPICodePackLoaded = $false - $script:proxyURI = $null - - $global:loadedModules = @() - $global:viewObjects = @() - $script:LogItems = [System.Collections.ObjectModel.ObservableCollection[object]]::new() - - $global:AppRootFolder = $PSScriptRoot - - # Load all modules in the Modules folder - $global:modulesPath = [IO.Path]::GetDirectoryName($PSCommandPath) + "\Extensions" - - 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 "#####################################################################################" - - Write-Log "PowerShell version: $($PSVersionTable.PSVersion.ToString())" - if($PSVersionTable.BuildVersion) { - Write-Log "PowerShell build: $($PSVersionTable.BuildVersion.ToString())" - } - if($PSVersionTable.CLRVersion) { - Write-Log "PowerShell CLR: $($PSVersionTable.CLRVersion.ToString())" - } - Write-Log "PowerShell edition: $($PSVersionTable.PSEdition)" - - try - { - $osName = Get-ItemPropertyValue "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion" -Name "ProductName" -ErrorAction Stop - $patchLevel = Get-ItemPropertyValue "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion" -Name "UBR" -ErrorAction Stop - $ver = [Version]::new([Environment]::OSVersion.Version.Major,[Environment]::OSVersion.Version.Minor, [Environment]::OSVersion.Version.Build, $patchLevel) - Write-Log "OS: $osName $ver" - } - catch - { - Write-Log "OS version: $([environment]::OSVersion.VersionString)" - } - - if(Test-Path $global:modulesPath) - { - Import-AllModules - } - else - { - Write-Warning "Extensions folder $($global:modulesPath) not found. Aborting..." 3 - exit 1 - } - - Initialize-Settings - $global:currentViewObject = $null - $global:FirstTimeRunning = ((Get-Setting "" "FirstTimeRunning" "true") -eq "true") - $global:MainAppStarted = $false - - Set-SplashWindowText "Initialize views" - [System.Windows.Forms.Application]::DoEvents() - - Invoke-ModuleFunction "Invoke-InitializeModule" - - if($global:hideUI -ne $true) - { - #Add menu group and items - $script:LogViewObject = (New-Object PSObject -Property @{ - Title = "Log" - Description = "View log items" - ID = "CoreLog" - HideMenu = $true - Activating = { Show-LogView } - Permissions = @() - ViewPanel = $null - }) - - Add-ViewObject $script:LogViewObject - - #This will load the main window - $global:txtSplashText.Text = "Load main window" - [System.Windows.Forms.Application]::DoEvents() - Get-MainWindow - - if($global:window) - { - $global:txtSplashText.Text = "Open default view" - [System.Windows.Forms.Application]::DoEvents() - - Show-View $View - - if((Get-SettingValue "CheckForUpdates") -eq $true) { Get-IsLatestVersion } - - Invoke-ModuleFunction "Invoke-ShowMainWindow" - - $global:txtSplashText.Text = "Open main window" - [System.Windows.Forms.Application]::DoEvents() - $global:window.ShowDialog() | Out-Null - } - } - else - { - if(-not $global:SilentBatchFile) - { - Write-Log "SilentBatchFile must be specified" 3 - return - } - $silentFI = [IO.FileInfo]$global:SilentBatchFile - - if($silentFI.Exists -eq $false) - { - Write-Log "SilentBatchFile $($global:SilentBatchFile) not found" 3 - return - } - Invoke-ModuleFunction "Invoke-ShowMainWindow" - - Invoke-ModuleFunction "Invoke-InitSilentBatchJob" - - Start-RunSilentBatchJob - } -} - -function Start-RunSilentBatchJob -{ - try - { - $settingObj = (ConvertFrom-Json (Get-Content -Path $global:SilentBatchFile -Raw -ErrorAction Stop)) - Invoke-ModuleFunction "Invoke-SilentBatchJob" $settingObj - } - catch - { - Write-LogError "Failed to trigger silent batch job." $_.Exception - } -} - -function Import-AllModules -{ - foreach($file in (Get-Item -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() - - $module = Import-Module $file -PassThru -Force -Global -ErrorAction SilentlyContinue - if($module) - { - $global:loadedModules += $module - Write-Host "Module $($module.Name) loaded successfully" - } - else - { - Write-Warning "Failed to load module $file" - } - } -} - -function Set-SplashWindowText -{ - param($text) - - if($global:hideUI -eq $true) { return } - - $global:txtSplashText.Text = $text -} - -#region Log functions -function Write-Log -{ - param($Text, $type = 1) - - if($script:logFailed -eq $true) { return } - - if(-not $global:logFile) { $global:logFile = Get-SettingValue "LogFile" ([IO.Path]::Combine($global:AppRootFolder,"CloudAPIPowerShellManagement.log")) } - - if(-not $global:logFileMaxSize) { [Int64]$global:logFileMaxSize = Get-SettingValue "LogFileSize" 1024; $global:logFileMaxSize = $global:logFileMaxSize * 1kb } - - if($null -eq $global:logOutputError) { $global:logOutputError = Get-SettingValue "LogOutputError" } - - $fi = [IO.FileInfo]$global:logFile - - if($fi.Length -gt $global:logFileMaxSize) - { - # Larger than max size. Rename current to .bak - # Delete current .bak if it exists - $bakFile = ($fi.DirectoryName + "\" + $fi.BaseName + ".lo_") - if([IO.File]::Exists($bakFile)) - { - try - { - [IO.File]::Delete($bakFile) - } - catch { } - } - try - { - $fi.MoveTo($bakFile) - } - catch { } - } - - try - { - $logPath = [IO.Path]::GetDirectoryName($global:logFile) - if(-not (Test-Path $logPath)) { mkdir -Path $logPath -Force -ErrorAction SilentlyContinue | Out-Null } - } - catch - { - $script:logFailed = $true - return - } - - $date = Get-Date - - if($global:PSCommandPath) - { - $fileObj = [System.IO.FileInfo]$global:PSCommandPath - } - else - { - $fileObj = [System.IO.FileInfo]$PSCommandPath - } - - $timeStr = "$($date.ToString(""HH"")):$($date.ToString(""mm"")):$($date.ToString(""ss"")).000+000" - $dateStr = "$($date.ToString(""MM""))-$($date.ToString(""dd""))-$($date.ToString(""yyyy""))" - $logOut = "" - - if($type -eq 2) - { - Write-Warning $Text - $typeStr = "Warning" - } - elseif($type -eq 3) - { - if($global:logOutputError -ne $false) - { - $host.ui.WriteErrorLine($Text) - } - else - { - Write-Warning $Text - } - $typeStr = "Error" - } - else - { - write-host $Text - $typeStr = "Info" - } - - $script:LogItems.Add([PSCustomObject]@{ - ID = ($script:LogItems.Count + 1) - DateTime = $date - Type = $type - TypeText = $typeStr - Text = $Text - }) - - try - { - out-file -filePath $global:logFile -append -encoding "ASCII" -inputObject $logOut - } - catch { } -} - -function Write-LogDebug -{ - param($Text, $type = 1) - - if($global:Debug) - { - Write-Log ("Debug: " + $text) $type - } -} - -function Write-LogError -{ - param($Text, $Exception) - - if($Text -and $Exception.message) - { - $Text += " Exception: $($Exception.Message)" - } - - Write-Log $Text 3 - - if((Get-SettingValue "ShowStackTrace") -eq $true) - { - Write-Log "Stack trace:`n $($Exception.StackTrace)" - - Write-Log "Script stack trace:`n $($Exception.ScriptStackTrace)" - - } -} - -function Write-Status -{ - param($Text, [switch]$SkipLog, [switch]$Block, [switch]$Force) - - if($global:hideUI -eq $true) - { - if($SkipLog -ne $true) { Write-Log $text } - return - } - - if(-not $text) { $global:BlockStatusUpdates = $false } - elseif($global:BlockStatusUpdates -eq $true -and $Force -ne $true) { return } - elseif($Block -eq $true) { $global:BlockStatusUpdates = $true } - - $global:txtInfo.Content = $Text - if($text) - { - $global:grdStatus.Visibility = "Visible" - if($SkipLog -ne $true) { Write-Log $text } - } - else - { - $global:grdStatus.Visibility = "Collapsed" - } - - [System.Windows.Forms.Application]::DoEvents() -} - -#endregion - -#region Popup -function Show-Popup -{ - param($popup) - - if(-not $global:grdPopup -or -not $global:cvsPopup) { return } - - $global:cvsPopup.AddChild($popup) | Out-Null - $global:grdPopup.Visibility = "Visible" - - [System.Windows.Forms.Application]::DoEvents() -} - -function Hide-Popup -{ - if(-not $global:grdPopup -or -not $global:cvsPopup) { return } - $global:cvsPopup.Children.Clear() - $global:grdPopup.Visibility = "Collapsed" - [System.Windows.Forms.Application]::DoEvents() -} -#endregion - -#region Xaml functions - -function Set-XamlProperty -{ - param($xamlObj, $controlName, $propertyName, $value) - - $obj = $xamlObj.FindName($controlName) - - try - { - if($obj) - { - $obj."$propertyName" = $value - } - else - { - Write-Log "Could not find object with name $controlName" 3 - } - } - catch - { - Write-LogError "Failed to set Xaml property value. Control: $controlName. Property: $propertyName. Error:" $_.Exception - } -} - -function Get-XamlProperty -{ - param($xamlObj, $controlName, $propertyName, $defaultValue = $null) - - $obj = $xamlObj.FindName($controlName) - - try - { - if($obj) - { - return (?? $obj."$propertyName" $defaultValue) - } - else - { - Write-Log "Could not find object with name $controlName" 3 - } - } - catch - { - Write-LogError "Failed to set Xaml property value. Control: $controlName. Property: $propertyName. Error:" $_.Exception - } -} - -function Add-XamlEvent -{ - param($xamlObj, $controlName, $eventName, $scriptBlock) - - try { - $obj = $xamlObj.FindName($controlName) - if($obj) - { - $obj."$eventName"($scriptBlock) - } - else - { - Write-Log "Failed to add Xaml event $eventName to $controlName. Control not found" 3 - } - } - catch - { - Write-LogError "Failed to add Xaml event $eventName to $controlName. Error:" $_.Exception - } -} - -function Add-XamlVariables -{ - param($xaml, $obj) - - # Generate a global variable for each object with Name property set - # Ref: https://learn-powershell.net/2014/08/10/powershell-and-wpf-radio-button/ - $xaml.SelectNodes("//*[@*[contains(translate(name(.),'n','N'),'Name')]]") | ForEach-Object { - Write-LogDebug "Add global variable $($_.Name)" - New-Variable -Name $_.Name -Value $obj.FindName($_.Name) -Force -Scope Global - } -} - -function Get-XamlObject -{ - param($fileName, [switch]$AddVariables) - - if(([IO.File]::Exists($fileName))) - { - try - { - [xml]$xaml = Get-Content $fileName - - $xamlObj = ([Windows.Markup.XamlReader]::Load((New-Object System.Xml.XmlNodeReader $xaml))) - - if($xamlObj -and $AddVariables -eq $true) - { - Add-XamlVariables $xaml $xamlObj - } - return $xamlObj - } - catch - { - Write-LogError "Failed to load Xaml file $fileName. Error:" $_.Exception - } - } - else - { - Write-Log "Failed to open Xaml file. File not found: $fileName" - } -} - -function Invoke-RegisterName -{ - param($parent, $name, $registerTo) - - try - { - $control = $parent.FindName($name) - if($control) - { - $registerTo.RegisterName($name, $control) - } - } - catch - { - Write-LogError "Failed to register $name" $_.Exception - } -} - -#endregion - -#region Silent Functions -function Set-BatchProperties -{ - param($settingsObj, $form, [switch]$SkipMissingControlWarning) - - if(-not $settingsObj -or -not $form) - { - return - } - - foreach($prop in $settingsObj) #($settingsObj | GM | Where MemberType -eq NoteProperty)) - { - if($prop.Type -eq "Custom") { continue } - - $obj = $form.FindName($prop.Name) - if(-not $obj) - { - if($SkipMissingControlWarning -ne $true) - { - Write-Log "No setting for $($prop.Name) found" 2 - } - continue - } - - if($prop.Value -is [String] -and [string]::IsNullOrEmpty($prop.Value)) - { - continue - } - - try - { - if($obj -is [System.Windows.Controls.CheckBox]) - { - $obj.IsChecked = $prop.Value -eq $true - } - elseif($obj -is [System.Windows.Controls.TextBox]) - { - $obj.Text = $prop.Value - } - elseif($obj -is [System.Windows.Controls.ComboBox]) - { - $obj.SelectedValue = $prop.Value - } - else - { - try - { - Write-Log "Unsupported object type for silent batch job: $($obj.GetType().FullName)" 3 - } - catch - {} - } - } - catch - { - Write-LogError "Failed to set batch job property for $($prop.Name)" $_.Exception - } - } -} -#endregion - -#region Dialogs - -function Show-AboutDialog -{ - $script:dlgAbout = Get-XamlObject ($global:AppRootFolder + "\Xaml\AboutDialog.xaml") - if(-not $script:dlgAbout) { return } - - $loadedItems = @() - $externalModules = @("MSAL.PS","Az.Account") - $externalAssemblies = @("Microsoft.Identity.Client.dll") - - foreach($module in (((Get-Module | Where-Object { $_.ModuleBase -like "$($global:AppRootFolder)*" -or $_.Name -in $externalModules })))) - { - $ver = $module.Version - if($module.Version.Major -eq 0 -and $module.Version.Minor -eq 0) - { - $cmd = $module.ExportedFunctions["Get-ModuleVersion"] - if($cmd) - { - $tmpVer = Invoke-Command -ScriptBlock $cmd.ScriptBlock - $ver = ?? $tmpVer $ver - } - } - - $loadedItems += (New-Object PSObject -Property @{ - Name = $module.Name - Version = $ver - Type = "PSModule" - }) - } - - $assms = [System.AppDomain]::CurrentDomain.GetAssemblies() | Where { $_.GlobalAssemblyCache -eq $false -and [String]::IsNullOrEmpty($_.Location) -eq $false } - foreach($assmName in $externalAssemblies) - { - $assmObjs = $assms | Where { $_.Location -like "*\$($assmName)" } - foreach($assmObj in $assmObjs) - { - try - { - $fi = [IO.FileInfo]"$($assmObj.Location)" - $loadedItems += (New-Object PSObject -Property @{ - Name = $fi.Name - Version = $fi.VersionInfo.FileVersion - Type = "Assembly" - }) - } - catch {} - } - } - - Set-XamlProperty $script:dlgAbout "txtTitle" "Text" "CloudAPIPowerShellManagement" - Set-XamlProperty $script:dlgAbout "txtViewTitle" "Text" ("Current view: " + $global:currentViewObject.ViewInfo.Title) - if($global:currentViewObject.ViewInfo.Description) - { - Set-XamlProperty $script:dlgAbout "txtViewDescription" "Text" $global:currentViewObject.ViewInfo.Description - } - - Set-XamlProperty $script:dlgAbout "lstModules" "ItemsSource" $loadedItems - - Add-XamlEvent $script:dlgAbout "linkSource" "Add_RequestNavigate" ({ [System.Diagnostics.Process]::Start($_.Uri.AbsoluteUri); $_.Handled = $true }) - - Show-ModalForm "About" $script:dlgAbout -} - -function Show-UpdatesDialog -{ - $script:dlgUpdates = Get-XamlObject ($global:AppRootFolder + "\Xaml\UpdatesDialog.xaml") - if(-not $script:dlgUpdates) { return } - - Write-Status "Getting Release Notes Information" - - Add-XamlEvent $script:dlgUpdates "btnClose" "add_click" { - $script:dlgUpdates = $null - Show-ModalObject - } - - $fileContent = Get-Content -Raw -Path ($global:AppRootFolder + "\ReleaseNotes.md") - try - { - $tmp = $fileContent.Replace("`r`n","`n") - $mystring = ("blob $($tmp.Length)`0" + $tmp) - $mystream = [IO.MemoryStream]::new([byte[]][char[]]$mystring) - $curHash = Get-FileHash -InputStream $mystream -Algorithm SHA1 - } - finally - { - if($mystream) { $mystream.Dispose() } - } - $params = @{} - $proxyURI = Get-ProxyURI - if($proxyURI) - { - $params.Add("proxy", $proxyURI) - $params.Add("UseBasicParsing", $true) - } - - $content = Invoke-RestMethod "https://api.github.com/repos/Micke-K/IntuneManagement/contents/ReleaseNotes.md" @params - if($content) - { - $txt = [System.Text.Encoding]::UTF8.GetString(([System.Convert]::FromBase64String($content.content))) - Set-XamlProperty $script:dlgUpdates "txtReleaseNotes" "Text" $txt - - if($content.sha -ne $curHash.Hash) - { - # ReleaseNotes.md not matching - Set-XamlProperty $script:dlgUpdates "tabLocalReleaseNotes" "Visibility" "Visible" - Set-XamlProperty $script:dlgUpdates "txtReleaseNotes" "Text" $fileContent - Set-XamlProperty $script:dlgUpdates "txtReleaseNotesMatch" "Visibility" "Collapsed" - } - else - { - Set-XamlProperty $script:dlgUpdates "txtReleaseNotesNoMatch" "Visibility" "Collapsed" - Set-XamlProperty $script:dlgUpdates "tabLocalReleaseNotes" "Visibility" "Collapsed" - } - } - - Write-Status "" - - Show-ModalForm "Release Notes" $script:dlgUpdates -HideButtons -} - -function Get-IsLatestVersion -{ - if($global:MainAppStarted -ne $true) - { - $global:txtSplashText.Text = "Check for updates" - [System.Windows.Forms.Application]::DoEvents() - } - - $gitHubVer = $null - - $params = @{} - $proxyURI = Get-ProxyURI - if($proxyURI) - { - $params.Add("proxy", $proxyURI) - $params.Add("UseBasicParsing", $true) - } - - $content = Invoke-RestMethod "https://api.github.com/repos/Micke-K/IntuneManagement/releases/latest" @params - if($content.Name) - { - try - { - $gitHubVer = [version]$content.Name - } - catch {} - } - - if($null -eq $gitHubVer) - { - $params = @{} - $proxyURI = Get-ProxyURI - if($proxyURI) - { - $params.Add("proxy", $proxyURI) - $params.Add("UseBasicParsing", $true) - } - - $content = Invoke-RestMethod "https://api.github.com/repos/Micke-K/IntuneManagement/contents/CloudAPIPowerShellManagement.psd1" @params - $gitHubText = [System.Text.Encoding]::UTF8.GetString(([System.Convert]::FromBase64String($content.content))) - $gitHubInfo = Get-ModuleDataTable $gitHubText - try - { - $gitHubVer = [version]$gitHubInfo.ModuleVersion - } - catch {} - } - - if(-not $gitHubVer) - { - Write-log "Failed to get version info in GitHub" 2 - return - } - - $LocalInfo = $null - $localVer = $null - try - { - Import-LocalizedData -BindingVariable LocalInfo -BaseDirectory $global:AppRootFolder -FileName "CloudAPIPowerShellManagement.psd1" -ErrorAction Stop - $localVer = [version]$LocalInfo.ModuleVersion - } - catch { } - - if(-not $localVer) - { - Write-log "Failed to get version info from local file" 2 - return - } - - if($localVer -lt $gitHubVer) - { - Write-Log "Local version and GitHub version does not match" 2 - Write-Log "Local version: $($localVer.ToString())" - Write-Log "GitHub version: $($gitHubVer.ToString())" - [System.Windows.MessageBox]::Show("There is a new version available on GitHub $($gitHubVer.ToString())`n`nCurrent version is $($localVer.ToString())", "Old version!", "OK", "Warning") - } - else - { - Write-Log "Running latest version: $($localVer.ToString())" - } -} - -function Get-ModuleDataTable -{ - param($moduleText) - - $result = $null - - if(-not $moduleText) { return } - - try - { - $Path = [IO.path]::ChangeExtension([IO.Path]::GetTempFileName(), "psd1") - $FI = [io.FileInfo]$path - $Utf8NoBomEncoding = New-Object System.Text.UTF8Encoding $False - [System.IO.File]::WriteAllLines($FI.FullName, $moduleText, $Utf8NoBomEncoding) - $Result = $null - Import-LocalizedData -BindingVariable Result -BaseDirectory $FI.DirectoryName -FileName $fi.Name - } - catch - { - - } - finally - { - try { [IO.File]::Delete(([IO.path]::ChangeExtension($FI.FullName, "tmp"))) } catch {} - try { $FI.Delete() } catch{} - } - - $Result -} - -function Show-InputDialog -{ - param( - $FormTitle = "Input", - $FormText, - $DefaultValue) - - $script:inputBox = Initialize-Window ($global:AppRootFolder + "\Xaml\InputDialog.xaml") - if(-not $script:inputBox) { return } - - $script:inputBox.Title = $FormTitle - - Set-XamlProperty $script:inputBox "txtLabel" "Content" $FormText - Set-XamlProperty $script:inputBox "txtValue" "Text" $DefaultValue - - $script:txtValue = $script:inputBox.FindName("txtValue") - - Add-XamlEvent $script:inputBox "btnOk" "Add_Click" ({ $script:inputBox.Close() }) - Add-XamlEvent $script:inputBox "btnCancel" "Add_Click" ({ $script:txtValue.Text ="";$script:inputBox.Close() }) - - $inputBox.Add_ContentRendered({ - $script:txtValue.SelectAll(); - $script:txtValue.Focus(); - }) - - $inputBox.Owner = $global:window - $inputBox.Icon = $global:Window.Icon - - $inputBox.ShowDialog() | Out-null - - return $script:txtValue.Text -} - -function Show-ModalForm -{ - param( - $FormTitle = "", - $formObject, - [switch]$HideButtons) - - $xamlStr = Get-Content ($global:AppRootFolder + "\Xaml\ModalForm.xaml") - - $modalForm = [Windows.Markup.XamlReader]::Parse($xamlStr) - - if($HideButtons -eq $true) - { - Set-XamlProperty $modalForm "spButtons" "Visibility" "Collapsed" - } - else - { - Add-XamlEvent $modalForm "btnClose" "Add_Click" ({ - Show-ModalObject - }) - } - - Set-XamlProperty $modalForm "txtTitle" "Text" $FormTitle - - $grdModalContainer = $modalForm.FindName("grdModalContainer") - if($grdModalContainer -and $formObject) - { - $formObject.SetValue([System.Windows.Controls.Grid]::RowProperty,1) - $grdModalContainer.Children.Add($formObject) | Out-Null - } - Show-ModalObject $modalForm -} -function Show-ModalObject -{ - param( $obj ) - - if($obj) - { - $obj.SetValue([System.Windows.Controls.Grid]::RowProperty,1) - $obj.SetValue([System.Windows.Controls.Grid]::ColumnProperty,1) - $global:grdModal.Children.Add($obj) | Out-Null - $global:grdModal.Visibility = "Visible" - } - else - { - $global:grdModal.Children.Clear() - $global:grdModal.Visibility = "Collapsed" - } - - [System.Windows.Forms.Application]::DoEvents() -} -#endregion - -#region Controls -function Show-AuthenticationInfo -{ - if($global:grdMenu) - { - $global:txtSplashText.Text = "Get profile picture" - [System.Windows.Forms.Application]::DoEvents() - - $authenticationProvider = $global:currentViewObject.ViewInfo.Authentication - if($global:grdMenu.Children[-1].Tag -eq "ProfilePicture") - { - $global:grdMenu.Children.Remove($global:grdMenu.Children[-1]) - } - - if($authenticationProvider.ProfilePicture) - { - $profileObj = & $authenticationProvider.ProfilePicture -Size 24 -Fontsize 12 -Popup -AuthenticationProvider $authenticationProvider - if($profileObj) - { - $profileObj.Tag = "ProfilePicture" - $profileObj.SetValue([System.Windows.Controls.Grid]::ColumnProperty,2) | Out-Null - $global:grdMenu.Children.Add($profileObj) | Out-Null - } - } - [System.Windows.Forms.Application]::DoEvents() - } -} - -function Set-EnvironmentInfo -{ - param($environmentName) - - if(-not $global:grdEnvironment) - { - return - } - - if(-not $script:mnuDefaultBGColor) - { - $script:mnuDefaultBGColor = $global:mnuMain.Background - } - if(-not $script:mnuDefaultFGColor) - { - $script:mnuDefaultFGColor = $global:mnuMain.Foreground - } - - if($global:grdEnvironment -and $environmentName) - { - $global:grdEnvironment.Visibility = "Visible" - if((Get-SettingValue "MenuShowOrganizationName") -eq $true) - { - $global:lblEnvironment.Content = $environmentName - } - else - { - $global:lblEnvironment.Content = "" - } - $bgColor = (Get-SettingValue "MenuBGColor") - $fgColor = (Get-SettingValue "MenuFGColor") - - if(-not $bgColor) - { - $bgColor = $script:mnuDefaultBGColor - } - - if($bgColor) - { - $global:grdMenu.Background = $bgColor - $global:mnuMain.Background = $bgColor - } - - if(-not $fgColor) - { - $fgColor = $script:mnuDefaultFGColor - } - - if($fgColor) - { - $global:lblEnvironment.Foreground = $fgColor - $global:mnuMain.Foreground = $fgColor - } - } - else - { - $global:grdEnvironment.Visibility = "Collapsed" - $global:lblEnvironment.Content = "" - $global:mnuMain.Background = $script:mnuDefaultBGColor - $global:mnuMain.Foreground = $script:mnuDefaultFGColor - $global:lblEnvironment.Foreground = $script:mnuDefaultFGColor - } -} - -#endregion - -#region Generic functions -function Invoke-Coalesce ($value, $default) -{ - # Use IsNullOrEmpty instead of -not - if ([String]::IsNullOrEmpty($value)) { $value = $default } - - return $value -} - -function Invoke-IfTrue ($expression, $valueIfTrue, $valueIfFalse) -{ - if ($expression) { return $valueIfTrue } - else { return $valueIfFalse } -} - -function Set-ObjectGrid -{ - param( $obj ) - - if($obj) - { - $global:grdObject.Children.Add($obj) | Out-Null - $global:grdObject.Visibility = "Visible" - } - else - { - $global:grdObject.Children.Clear() - $global:grdObject.Visibility = "Collapsed" - } - - [System.Windows.Forms.Application]::DoEvents() -} - -function Remove-InvalidFileNameChars -{ - param($Name) - - $re = "[{0}]" -f [RegEx]::Escape(([IO.Path]::GetInvalidFileNameChars() -join '')) - - $Name = $Name -replace $re - - - return $Name -} - -function Remove-ObjectProperty -{ - param($obj, $property) - - if(-not $obj -or -not $property) { return } - - if(($obj | Get-Member -MemberType NoteProperty -Name $property)) - { - $obj.PSObject.Properties.Remove($property) - } -} - -function Get-Folder -{ - param($path = $env:temp, $title = "Select a directory") - - if($global:useDefaultFolderDialog -ne $true) - { - try - { - if($global:WindowsAPICodePackLoaded -eq $false) - { - $apiCodec = Join-Path $global:AppRootFolder "Bin\Microsoft.WindowsAPICodePack.Shell.dll" - if([IO.File]::Exists($apiCodec)) - { - Add-Type -Path $apiCodec | Out-Null - $global:WindowsAPICodePackLoaded = $true - } - else - { - Write-Log "Could not find Microsoft.WindowsAPICodePack.Shell.dll" 2 - } - } - $dlgCOFD = New-Object Microsoft.WindowsAPICodePack.Dialogs.CommonOpenFileDialog - } - catch - { - Write-LogError "Failed to load Microsoft.WindowsAPICodePack.Shell.dll. Verify that the .Net 3.5 feature is enabled" $_.Exception - } - } - - if($dlgCOFD -and $global:useDefaultFolderDialog -ne $true) - { - $dlgCOFD.EnsureReadOnly = $true - $dlgCOFD.IsFolderPicker = $true - $dlgCOFD.AllowNonFileSystemItems = $false - $dlgCOFD.Multiselect = $false - $dlgCOFD.Title = $title - - if($path -and (Test-Path $path)) - { - $dlgCOFD.InitialDirectory = $path - } - if($dlgCOFD.ShowDialog($window) -eq [Microsoft.WindowsAPICodePack.Dialogs.CommonFileDialogResult]::Ok) - { - $dlgCofd.FileName - } - } - else - { - $global:useDefaultFolderDialog = $true - [Reflection.Assembly]::LoadWithPartialName("System.Windows.Forms") | Out-Null - [System.Windows.Forms.Application]::EnableVisualStyles() - $dlgFBD = New-Object System.Windows.Forms.FolderBrowserDialog - $dlgFBD.SelectedPath = "C:\" - $dlgFBD.ShowNewFolderButton = $false - $dlgFBD.Description = $title - if($dlgFBD.ShowDialog() -eq "OK") - { - $dlgFBD.SelectedPath - } - $dlgFBD.Dispose() - } -} -function Remove-Property -{ - param($obj, $prop) - - if(-not $prop) { return } - - if(($obj | GM -MemberType NoteProperty -Name $prop)) - { - Write-LogDebug "Remove property $prop" - $obj.PSObject.Properties.Remove($prop) | Out-Null - } -} - -function Get-GridCheckboxColumn -{ - param($bindingProperty = "IsSelected", [scriptblock]$scriptBlock) - - $binding = [System.Windows.Data.Binding]::new($bindingProperty) - $binding.UpdateSourceTrigger = [System.Windows.Data.UpdateSourceTrigger]::PropertyChanged - $column = [System.Windows.Controls.DataGridTemplateColumn]::new() - $fef = [System.Windows.FrameworkElementFactory]::new([System.Windows.Controls.CheckBox]) - $binding.Mode = [System.Windows.Data.BindingMode]::TwoWay - $fef.SetValue([System.Windows.Controls.CheckBox]::IsCheckedProperty,$binding) - if($null -ne $scriptBlock) - { - [System.Windows.RoutedEventHandler]$checkedEventHandler = $scriptBlock - $fef.AddHandler([System.Windows.Controls.CheckBox]::CheckedEvent, $checkedEventHandler) - } - $dt = [System.Windows.DataTemplate]::new() - $dt.VisualTree = $fef - $column.CellTemplate = $dt - $header = [System.Windows.Controls.CheckBox]::new() - $header.Margin = [System.Windows.Thickness]::new(-4,0,0,0) # Align header checkbox with the row checkboxes - $header.ToolTip = "Select/deselect all items" - $column.Header = $header - if($null -ne $scriptBlock) - { - #$header.add_click($scriptBlock) - } - - $column -} - -function Expand-FileName -{ - param($fileName) - - [Environment]::SetEnvironmentVariable("Date",(Get-Date).ToString("yyyy-MM-dd"),[System.EnvironmentVariableTarget]::Process) - [Environment]::SetEnvironmentVariable("DateTime",(Get-Date).ToString("yyyyMMdd-HHmm"),[System.EnvironmentVariableTarget]::Process) - [Environment]::SetEnvironmentVariable("Organization",$global:Organization.displayName,[System.EnvironmentVariableTarget]::Process) - - $fileName = [Environment]::ExpandEnvironmentVariables($fileName) - - foreach($tmpFolder in ([System.Enum]::GetNames([System.Environment+SpecialFolder]))) - { - $fileName = $fileName -replace "%$($tmpFolder)%",([Environment]::GetFolderPath($tmpFolder)) - } - - [Environment]::SetEnvironmentVariable("Date",$null,[System.EnvironmentVariableTarget]::Process) - [Environment]::SetEnvironmentVariable("DateTime",$null,[System.EnvironmentVariableTarget]::Process) - [Environment]::SetEnvironmentVariable("Organization",$null,[System.EnvironmentVariableTarget]::Process) - - # Remove invalid path characters - $re = "[{0}]" -f [RegEx]::Escape(([IO.Path]::GetInvalidPathChars() -join '')) - $fileName = $fileName -replace $re - - $fileName -} - -#endregion - -#region Save/Read Settings functions -######################################################################## -# -# Save/Read Settings -# -######################################################################## -function Initialize-Settings -{ - param([switch]$Updated) - - $global:Debug = Get-SettingValue "Debug" - $global:logFile = $null - $global:logFileMaxSize = $null - $global:logOutputError = $null - $script:proxyURI = $null - - if($Updated -eq $true) - { - Set-EnvironmentInfo $global:Organization.displayName - 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") - - if($global:hideUI -eq $true) { return } - - if($global:JsonSettingsObj -and $global:JSonSettingFile) - { - if($SubPath) - { - $arrParts = $SubPath.TrimEnd(@('/','\')).Split(@('/','\')) - } - else - { - $arrParts = @() - } - - $parentSetting = $global:JsonSettingsObj - - foreach($part in $arrParts) - { - if(-not $part.Trim()) { continue } - - if(($parentSetting.PSObject.Properties | Where Name -eq $part)) - { - $parentSetting = $parentSetting.$part - } - else - { - $parentSetting | Add-Member -MemberType NoteProperty -Name $part -Value ([PSCustomObject]@{}) - $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 - } -} - -function Remove-Setting -{ - param($SubPath = "", $Key = "") - - if($global:JsonSettingsObj) - { - if($SubPath) - { - $arrParts = $SubPath.TrimEnd(@('/','\')).Split(@('/','\')) - } - else - { - $arrParts = @() - } - - $parentSetting = $global:JsonSettingsObj - - foreach($part in $arrParts) - { - if(($parentSetting.PSObject.Properties | Where Name -eq $part)) - { - $parentSetting = $parentSetting.$part - } - else - { - return - } - } - - if(($parentSetting.PSObject.Properties | Where Name -eq $Key)) - { - $parentSetting.PSObject.Properties.Remove($Key) - } - } - else - { - $regPath = Get-RegPath $subPath - try - { - $temp = Get-Item -LiteralPath $regPath -ErrorAction SilentlyContinue - if(($temp -and $temp.Property -contains $Key)) - { - Remove-ItemProperty -Path $regPath -Name $Key -Force -ErrorAction Stop - } - } - catch - { - Write-LogError "Failed to remove reg value: $($Key) in key $($regPath)" $_.Exception - } - } -} - -function Get-Setting -{ - param($SubPath = "", $Key = "", $defaultValue) - - if(-not $key) - { - return - } - - $val = $null - - if($global:JsonSettingsObj) - { - try - { - if($SubPath) - { - $arrParts = $SubPath.TrimEnd(@('/','\')).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) - { - $defaultValue - } - else - { - $val - } -} - -function Get-RegPath -{ - param($SubPath) - - $path = "HKCU:\Software\CloudAPIPowerShellManagement" - if($SubPath) - { - $path = $path + "\" + $SubPath - } - - $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 -ErrorAction SilentlyContinue - 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 - } -} - -function Remove-TenantSetting -{ - param($settingValue) - - $subPath = ($global:Organization.Id + "\" + $settingValue.SubPath) - - if($global:JsonSettingsObj) - { - if($SubPath) - { - $arrParts = $SubPath.TrimEnd(@('/','\')).Split(@('/','\')) - } - else - { - $arrParts = @() - } - - $parentSetting = $global:JsonSettingsObj - - foreach($part in $arrParts) - { - if(($parentSetting.PSObject.Properties | Where Name -eq $part)) - { - $parentSetting = $parentSetting.$part - } - else - { - return - } - } - - if(($parentSetting.PSObject.Properties | Where Name -eq $settingValue.Key)) - { - $parentSetting.PSObject.Properties.Remove($settingValue.Key) - } - - } - else - { - $regPath = Get-RegPath $subPath - try - { - $temp = Get-Item -LiteralPath $regPath -ErrorAction SilentlyContinue - if(($temp.Property -contains $settingValue.Key)) - { - Remove-ItemProperty -Path $regPath -Name $settingValue.Key -Force -ErrorAction Stop - } - } - catch - { - Write-LogError "Failed to remove reg value: $($settingValue.Key) in key $($regPath)" $_.Exception - } - } -} - -function Get-IsTenantSettingConfigured -{ - param($settingValue) - - $subPath = ($global:Organization.Id + "\" + $settingValue.SubPath) - - if($global:JsonSettingsObj) - { - if($SubPath) - { - $arrParts = $SubPath.TrimEnd(@('/','\')).Split(@('/','\')) - } - else - { - $arrParts = @() - } - - $parentSetting = $global:JsonSettingsObj - - foreach($part in $arrParts) - { - if(($parentSetting.PSObject.Properties | Where Name -eq $part)) - { - $parentSetting = $parentSetting.$part - } - else - { - return $false - } - } - - return ($null -ne ($parentSetting.PSObject.Properties | Where Name -eq $settingValue.Key)) - - } - else - { - $regPath = Get-RegPath $subPath - try - { - $temp = Get-Item -LiteralPath $regPath -ErrorAction Stop - return ($temp.GetValueNames() -contains $settingValue.Key) - } - catch - { - - } - } - return $false -} -#endregion - -#region Setting functions - -######################################################################## -# -# Settings functions -# -######################################################################## - -function Add-SettingsItem -{ - param($settingItem, $settingValue) - - $rd = [System.Windows.Controls.RowDefinition]::new() - $rd.Height = [double]::NaN - $spSettings.RowDefinitions.Add($rd) - $settingItem.SetValue([System.Windows.Controls.Grid]::RowProperty,$spSettings.RowDefinitions.Count-1) - - if(-not $settingValue) - { - $settingItem.SetValue([System.Windows.Controls.Grid]::ColumnSpanProperty, 99) - } - else - { - if($settingValue.Description) - { - $descriptionInfo = "" + - "" + - $settingValue.Description + - "" + - "" - } - - $xaml = @" - - - $descriptionInfo - -"@ - - if($script:tenantSettings -and $settingValue) - { - #_IsChecked - $tenantConfig = [System.Windows.Controls.CheckBox]::new() - $tenantConfig.ToolTip = "Enable tenant specific setting" - $tenantConfig.SetValue([System.Windows.Controls.Grid]::RowProperty,$spSettings.RowDefinitions.Count-1) - $tenantConfig.SetValue([System.Windows.Controls.Grid]::ColumnProperty, 0) - $tenantConfig.Margin = "0,5,0,0" - $tenantConfig.Tag = $settingValue - $tenantConfig.IsChecked = (Get-IsTenantSettingConfigured $settingValue) - $settingItem.IsEnabled = $tenantConfig.IsChecked - $tenantConfig.add_Click({ - if($this.Tag.Control) { $this.Tag.Control.IsEnabled = $this.IsChecked } - } - ) - $spSettings.AddChild($tenantConfig) - } - - $settingsTitle = [Windows.Markup.XamlReader]::Parse($xaml) - $settingsTitle.SetValue([System.Windows.Controls.Grid]::RowProperty,$spSettings.RowDefinitions.Count-1) - $settingsTitle.SetValue([System.Windows.Controls.Grid]::ColumnProperty, 1) - - $settingItem.SetValue([System.Windows.Controls.Grid]::ColumnProperty, 2) - $spSettings.AddChild($settingsTitle) - $settingItem.Margin = "0,5,0,0" - } - $spSettings.AddChild($settingItem) -} - -function Add-SettingTextBox -{ - param($id, $value) - - $xaml = @" -$value -"@ - return [Windows.Markup.XamlReader]::Parse($xaml) -} - -function Add-SettingCheckBox -{ - param($id, $value) - - $tmpValue = ($value -eq $true -or $value -eq "true").ToString().ToLower() - - $xaml = @" - -"@ - return [Windows.Markup.XamlReader]::Parse($xaml) -} - -function Add-SettingComboBox -{ - param($id, $value, $settingObj) - - $nameProp = ?? $settingObj.DisplayMemberPath "Name" - $valueProp = ?? $settingObj.SelectedValuePath "Value" - - $xaml = @" - -"@ - $xamlObj = [Windows.Markup.XamlReader]::Parse($xaml) - - $xamlObj.ItemsSource = $settingObj.ItemsSource - if($value) - { - $xamlObj.SelectedValue = $value - } - - $xamlObj -} - -function Add-SettingFolder -{ - param($id, $value) - $xaml = @" - - - - - - - $value - - - -"@ - - $obj = [Windows.Markup.XamlReader]::Parse($xaml) - - $btnBrowse = $obj.FindName("browse_$($id)") - $txtObj = $obj.FindName($id) - if($btnBrowse) - { - $btnBrowse.Tag = $txtObj - $btnBrowse.Add_Click({ - $folder = Get-Folder $this.Tag.Text - if($folder) { $this.Tag.Text = $folder } - }) - } - return $obj -} - -function Add-SettingValue -{ - param($settingValue) - - $id = "id_" + [Guid]::NewGuid().ToString('n') - - if($settingValue.TenantSettings -eq $false -and $script:tenantSettings) - { - return # Value nut supported in Tenant Settings - } - elseif($settingValue.GlobalSettings -eq $false -and $script:tenantSettings -ne $true) - { - return # Value nut supported in Global Settings - } - - $value = Get-SettingValue $settingValue.Key -GlobalOnly:($script:tenantSettings -ne $true) - - if($settingValue.Type -eq "folder") - { - $settingObj = Add-SettingFolder $id $value - } - elseif($settingValue.Type -eq "Boolean") - { - $settingObj = Add-SettingCheckBox $id $value - } - elseif($settingValue.Type -eq "List") - { - $settingObj = Add-SettingComboBox $id $value $settingValue - } - else - { - $settingObj = Add-SettingTextBox $id $value - } - - if($settingObj) - { - Add-SettingsItem $settingObj $settingValue - # Find the control in the setting object that contains the actual value - # $settingObj might be a grid that contains the TextBox with the settings value - $ctrl = $settingObj.FindName($id) - if(($settingValue | Get-Member -MemberType NoteProperty -Name "Control")) - { - $settingValue.Control = $ctrl - } - else - { - $settingValue | Add-Member -MemberType NoteProperty -Name "Control" -Value $ctrl - } - } -} - -function Add-SettingTitle -{ - param($title, $marginTop = "0") - - $xaml = @" - -"@ - - #$global:spSettings.Children.Add([Windows.Markup.XamlReader]::Parse($xaml)) - Add-SettingsItem ([Windows.Markup.XamlReader]::Parse($xaml)) | Out-Null -} - -function Show-SettingsForm -{ - param([switch]$Tenant) - - $settingsStr = Get-Content ($global:AppRootFolder+ "\Xaml\SettingsForm.xaml") - - $settingsForm = [Windows.Markup.XamlReader]::Parse($settingsStr) - $global:settingControls = @() - $global:spSettings = $settingsForm.FindName("spSettings") - - $script:tenantSettings = ($Tenant -eq $true) - Add-XamlEvent $settingsForm "btnSave" "Add_Click" ({ - Save-AllSettings - }) - - Add-XamlEvent $settingsForm "btnClose" "Add_Click" ({ - $script:tenantSettings = $null - Show-ModalObject - }) - - if($JsonSettingsObj -or $script:tenantSettings -eq $true) - { - 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) - { - Add-SettingTitle $tmp.Title - foreach($settingObj in $tmp.Values) - { - Add-SettingValue $settingObj - } - } - - foreach($settingObj in $global:appSettingSections) - { - if(-not ($settingObj | Get-Member -MemberType NoteProperty -Name "Priority")) - { - $settingObj | Add-Member -MemberType NoteProperty -Name "Priority" -Value 100 - } - if($settingObj.Priority -lt 1) { $settingObj.Priority = 1} - } - - foreach($section in ($global:appSettingSections | Where-Object Id -ne "General" | Sort-Object -Property Priority,Title)) - { - if($section.Values.Count -eq 0) { continue } - Add-SettingTitle $section.Title 5 - foreach($settingObj in $section.Values) - { - Add-SettingValue $settingObj - } - } - Show-ModalObject $settingsForm -} - -function Add-DefaultSettings -{ - $global:appSettingSections = @() - - $script:lstColors = @() - $script:lstColors += [PSCustomObject]@{ - Name = "" - Value = "" - } - - foreach($color in ([System.Drawing.Color].GetProperties() | Where { $_.PropertyType -eq [System.Drawing.Color] } | Sort -Property Name | Select Name).Name) - { - $script:lstColors += [PSCustomObject]@{ - Name = $color - Value = $color - } - } - - $global:appSettingSections += (New-Object PSObject -Property @{ - Title = "General" - Id = "General" - Values = @() - }) - - Add-SettingsObject (New-Object PSObject -Property @{ - Title = "Log file" - Key = "LogFile" - Type = "File" - }) "General" - - Add-SettingsObject (New-Object PSObject -Property @{ - Title = "Max log file size" - Key = "LogFileSize" - Type = "Int" - DefaultValue = 1024 - }) "General" - - Add-SettingsObject (New-Object PSObject -Property @{ - Title = "Add errors to PowerShell output" - Key = "LogOutputError" - Type = "Boolean" - Description = "Write errors to the Error Output of the PS Host. If disabled, errors will be written as a Warning. Eg. disable this if automation should skip logging PowerShell errors." - DefaultValue = $true - }) "General" - - Add-SettingsObject (New-Object PSObject -Property @{ - Title = "Show stack error" - Key = "ShowStackTrace" - Type = "Boolean" - Description = "Write exception stack trace info to the log." - DefaultValue = $false - }) "General" - - Add-SettingsObject (New-Object PSObject -Property @{ - Title = "Debug" - Key = "Debug" - Type = "Boolean" - DefaultValue = $false - }) "General" - - Add-SettingsObject (New-Object PSObject -Property @{ - Title = "Hide No-access items" - Key = "HideNoAccess" - Type = "Boolean" - Description="Remove items from the menu if object permissions is missing. Default is to mark them with red" - DefaultValue = $false - }) "General" - - Add-SettingsObject (New-Object PSObject -Property @{ - Title = "Preview" - Key = "PreviewFeatures" - Type = "Boolean" - DefaultValue = $false - Description = "Enable features that are marked as Preview. This might require a restart and prompt for consent" - }) "General" - - Add-SettingsObject (New-Object PSObject -Property @{ - Title = "Check for updates" - Key = "CheckForUpdates" - Type = "Boolean" - DefaultValue = $true - Description = "Check GitHub if there is a later version available" - }) "General" - - Add-SettingsObject (New-Object PSObject -Property @{ - Title = "Menu Background color" - Key = "MenuBGColor" - Type = "List" - ItemsSource = $script:lstColors - DefaultValue = "" - }) "General" - - Add-SettingsObject (New-Object PSObject -Property @{ - Title = "Menu Foreground color" - Key = "MenuFGColor" - Type = "List" - ItemsSource = $script:lstColors - DefaultValue = "" - }) "General" - - Add-SettingsObject (New-Object PSObject -Property @{ - Title = "Show tenant name" - Key = "MenuShowOrganizationName" - Type = "Boolean" - DefaultValue = $true - Description = "Adds the organization name next to the login info on the menu bar" - }) "General" - - Add-SettingsObject (New-Object PSObject -Property @{ - Title = "Proxy URI" - Key = "ProxyURI" - Description = "Specify the URI for the proxy eg http://<server>:<port>" - }) "General" - -} - -function Add-SettingsObject -{ - param($obj, $section) - - $section = $global:appSettingSections | Where-Object Id -eq $section - if(-not $section) - { - Write-Log "Could not find section $section" 3 - return - } - - try - { - $section.Values += $obj - } - catch { } -} - -function Save-AllSettings -{ - Write-Status "Save settings" - $dt1 = Get-Date - $curHideNoAccess = Get-SettingValue "HideNoAccess" - - foreach($section in $global:appSettingSections) - { - foreach($settingObj in $section.Values) - { - if(-not $settingObj.Control) { continue } - if($settingObj.Control.IsEnabled -eq $false -and $script:tenantSettings) - { - Remove-TenantSetting $settingObj - continue - } - - $valueFound = $false - if($settingObj.Control.GetType().Name -eq "TextBox") - { - $value = $settingObj.Control.Text - if($settingObj.Type -eq "Int") - { - try - { - $value = [int]$value - } - catch - { - # Log or set invalid - $value = $settingObj.Value - } - } - $valueFound = $true - } - elseif($settingObj.Control.GetType().Name -eq "CheckBox") - { - $value = $settingObj.Control.IsChecked - $valueFound = $true - } - elseif($settingObj.Control.GetType().Name -eq "ComboBox") - { - Write-LogDebug "$($settingObj.Control.Text) | $($settingObj.Control.SelectedIndex)" - if($settingObj.Control.SelectedIndex -eq -1) - { - $value = $settingObj.Control.Text - } - else - { - $value = $settingObj.Control.SelectedValue - } - $valueFound = $true - } - - if($valueFound) - { - if($script:tenantSettings) - { - $subPath = ($global:Organization.Id + "\" + $settingObj.SubPath) - } - else - { - $subPath = $settingObj.SubPath - } - Save-Setting $subPath $settingObj.Key $value - } - } - } - - if($global:currentViewObject.ViewInfo.SaveSettings) - { - & $global:currentViewObject.ViewInfo.SaveSettings - } - - Initialize-Settings -Updated - - $newHideNoAccess = Get-SettingValue "HideNoAccess" - if($curHideNoAccess -ne $newHideNoAccess ) - { - Show-ViewMenu - } - - if($dt1.AddSeconds(1) -lt (Get-Date)) - { - Start-Sleep -Seconds 1 # It goes to quick...ToDo: Do this in a better way - } - Write-Status "" -} - -function Get-SettingValue -{ - param($Key, $defaultValue, [switch]$GlobalOnly, [switch]$TenantOnly, $TenantID) - - foreach($section in $global:appSettingSections) - { - $settingObj = $section.Values | Where Key -eq $Key - if($settingObj) { break } - } - - if(-not $defaultValue) { $defaultValue = $settingObj.DefaultValue } - - $value = $null - if(-not $TenantID) { $TenantID = $global:Organization.Id} - - if($GlobalOnly -ne $true -and $TenantID) - { - # Try get Tenant specific value first - $value = Get-Setting ($TenantID + "\" + $settingObj.SubPath) $settingObj.Key - } - - if($null -eq $value -and $TenantOnly -ne $true) - { - # Get global setting value if tenant value was not found - $value = Get-Setting $settingObj.SubPath $settingObj.Key $defaultValue - } - - if($value) - { - if($settingObj.Type -eq "Boolean") - { - $value = $value -eq $true -or $value -eq "true" - } - elseif($settingObj.Type -eq "Boolean") - { - try - { - $value = [int]$value - } - catch - { - if($settingObj.DefaultValue) - { - try - { - $value = [int]$settingObj.DefaultValue - } - catch { } - } - } - } - - # Keep last read value - if($settingObj -and ($settingObj | Get-Member -MemberType NoteProperty -Name "Value")) - { - $settingObj.Value = $value # Keep last read value - } - else - { - $settingObj | Add-Member -MemberType NoteProperty -Name "Value" -Value $value - } - } - $value -} - -#endregion - -#region Menu functions - -##################################################################################################### -# -# Menu functions -# -##################################################################################################### - -function Add-ViewObject -{ - param($viewObject) - - $global:viewObjects += New-Object PSObject -Property @{ ViewInfo = $viewObject; ViewItems = @() } -} - -function Add-ViewItem -{ - param($viewItem) - - $viewObject = $global:viewObjects | Where { $_.ViewInfo.Id -eq $viewItem.ViewID } - if(-not $viewObject) - { - if(($arrMenuInlcude -and $arrMenuInlcude -notcontains $viewItem.ViewID) -or ($arrMenuExlcude -and $arrMenuExlcude -contains $viewItem.ViewID)) { return } - - Write-Log "Could not find menu with id $($viewItem.ViewID). Item $($viewItem.Title) not added" 2 - return - } - - ### !!! ToDo: Should not be here... - if(-not ($viewItem.PSObject.Properties | Where Name -eq "ImportOrder")) - { - $viewItem | Add-Member -NotePropertyName "ImportOrder" -NotePropertyValue 1000 - } - - foreach($scope in $viewItem.Permissons) - { - 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"))) - { - $ctrl = Get-XamlObject ($global:AppRootFolder + "\Xaml\Icons\$((?? $viewItem.Icon $viewItem.Id)).xaml") - $viewItem | Add-Member -NotePropertyName "IconImage" -NotePropertyValue $ctrl - } - - $viewObject.ViewItems += $viewItem -} - -function Show-View -{ - param($viewId) - - if(($global:viewObjects | measure).Count -eq 0) - { - Write-Log "No View Objects loaded!" 3 - return - } - - if(-not $viewId) - { - # Use first View if not specified - # ToDo: Use last or default view - $viewId = $global:viewObjects[0].ViewInfo.Id - } - - if($global:currentViewObject.ViewInfo.ID -eq $viewId) { return } # Current view already selected - - # Get the View object - $viewObject = $global:viewObjects | Where { $_.ViewInfo.Id -eq $viewId } - if(-not $viewObject) - { - Write-Log "Could not find View with id $($viewId)" 3 - return - } - Write-Log "Change view to $($viewObject.ViewInfo.Title)" - - if($global:currentViewObject -ne $viewObject -and $global:currentViewObject.ViewInfo.Deactivating) - { - Write-Log "Deactivating View $($global:currentViewObject.ViewInfo.Title)" - & $global:currentViewObject.ViewInfo.Deactivating - } - - $global:currentViewObject = $viewObject - - Show-ViewMenu - - $lblMenuTitle.Content = $viewObject.ViewInfo.Title - - $grdViewPanel.Children.Clear() - - if($viewObject.ViewInfo.Authenticate) - { - $global:txtSplashText.Text = "Authenticate" - [System.Windows.Forms.Application]::DoEvents() - & $viewObject.ViewInfo.Authenticate - } - - if($viewObject.ViewInfo.Activating) - { - Write-Log "Activating View $($viewObject.ViewInfo.Title)" - & $viewObject.ViewInfo.Activating - } - - if($viewObject.ViewInfo.ViewPanel) - { - $grdViewPanel.Children.Add($viewObject.ViewInfo.ViewPanel) | Out-Null - } - - Set-MainTitle - - Show-AuthenticationInfo - - if($viewObject.ViewInfo.HideMenu -eq $true) - { - $global:grdViewItemMenu.Visibility = "Collapsed" - } - else - { - $global:grdViewItemMenu.Visibility = "Visible" - } - - if($viewObject.ViewInfo.Activated) - { - Write-Log "Activated View $($viewObject.ViewInfo.Title)" - & $viewObject.ViewInfo.Activated - } - - Invoke-ModuleFunction "Invoke-ViewActivated" -} - -function Show-ViewMenu -{ - $viewObject = $global:currentViewObject - - $viewItems = ?: ($viewObject.ViewInfo.Sort -ne $false) ($viewObject.ViewItems | Sort-Object -Property Title) ($viewObject.ViewItems) - - if((Get-SettingValue "HideNoAccess")) - { - $viewItems = $viewItems | Where { $_."@HasPermissions" -ne $false } - } - - $lstMenuItems.ItemsSource = @($viewItems) -} - -#endregion - -#region Main Window -function Set-MainTitle -{ - if(-not $global:window -or -not $global:currentViewObject.ViewInfo.Title) { return } - - Write-LogDebug "Set main title to $($global:currentViewObject.ViewInfo.Title)" - - $global:window.Title = ?? $global:currentViewObject.ViewInfo.Title "Cloud API PowerShell Management" -} - -function Get-MainWindow -{ - try - { - [xml]$xaml = Get-Content ($global:AppRootFolder + "\Xaml\MainWindow.xaml") - [xml]$styles = Get-Content ($global:AppRootFolder + "\Themes\Styles.xaml") - - ### Update relative path to full path for ResourceDictionary - [System.Xml.XmlNamespaceManager] $nsm = $xaml.NameTable; - $nsm.AddNamespace("s", 'http://schemas.microsoft.com/winfx/2006/xaml/presentation'); - foreach($rsdNode in ($xaml.SelectNodes("//s:ResourceDictionary[@Source]", $nsm))) - { - $rsdNode.Source = (Join-Path ($PSScriptRoot) ($rsdNode.Source)).ToString() - } - - # Add Styles - foreach($node in $styles.DocumentElement.ChildNodes) - { - $tmpNode = $xaml.CreateElement("Temp") - $tmpNode.InnerXml = $node.OuterXml - $xaml.Window.'Window.Resources'.ResourceDictionary.AppendChild($tmpNode.Style) | Out-Null - } - $global:window = [Windows.Markup.XamlReader]::Load((New-Object System.Xml.XmlNodeReader $xaml)) - } - catch - { - Write-LogError "Failed to initialize main window" $_.Exception - return - } - - # ToDo: Convert to a list for data binding - Add-XamlEvent $window "mnuSettings" "Add_Click" -scriptBlock ([scriptblock]{ Show-SettingsForm }) - Add-XamlEvent $window "mnuTenantSettings" "Add_Click" -scriptBlock ([scriptblock]{ Show-SettingsForm -Tenant }) - Add-XamlEvent $window "mnuUpdates" "Add_Click" -scriptBlock ([scriptblock]{ Show-UpdatesDialog }) - Add-XamlEvent $window "mnuAbout" "Add_Click" -scriptBlock ([scriptblock]{ Show-AboutDialog }) - Add-XamlEvent $window "mnuExit" "Add_Click" -scriptBlock ([scriptblock]{ - if([System.Windows.MessageBox]::Show("Are you sure you want to exit?", "Exit?", "YesNo", "Question") -eq "Yes") - { - $window.Close() - } - } - ) - - Add-XamlVariables $xaml $window - - $lstMenuItems.Add_SelectionChanged({ - if($global:currentViewObject.ViewInfo.ItemChanged) - { - & $global:currentViewObject.ViewInfo.ItemChanged - } - }) - - $global:grdPopup.add_MouseLeftButtonDown( { Hide-Popup } ) - - # ToDo: !!! Intune should not be default icon... - $iconFile = "$($global:AppRootFolder)\Intune.ico" - if([io.File]::Exists($iconFile)) - { - $Window.Icon = $iconFile - } - - $window.Add_Closed({ - }) - - $window.add_Loaded({ - $global:SplashScreen.Hide() - $global:window.Activate() - [System.Windows.Forms.Application]::DoEvents() - #$global:window.Topmost = $true - #$global:window.Topmost = $false - #$global:window.Focus() - - $global:MainAppStarted = $true - - if($global:FirstTimeRunning) - { - $script:welcomeForm = Get-XamlObject ($global:AppRootFolder + "\Xaml\Welcome.xaml") -AddVariables - - Add-XamlEvent $script:welcomeForm "gitHubLink" "Add_RequestNavigate" ({ [System.Diagnostics.Process]::Start($_.Uri.AbsoluteUri); $_.Handled = $true }) - Add-XamlEvent $script:welcomeForm "licenseLink" "Add_RequestNavigate" ({ [System.Diagnostics.Process]::Start($_.Uri.AbsoluteUri); $_.Handled = $true }) - Add-XamlEvent $script:welcomeForm "addCustomApp" "Add_RequestNavigate" ({ [System.Diagnostics.Process]::Start($_.Uri.AbsoluteUri); $_.Handled = $true }) - - Add-XamlEvent $script:welcomeForm "chkAcceptConditions" "add_click" { - $global:btnAcceptConditions.IsEnabled = ($this.IsChecked -eq $true) - } - - Add-XamlEvent $script:welcomeForm "btnAcceptConditions" "add_click" { - Save-Setting "" "LicenseAccepted" "True" - Save-Setting "" "FirstTimeRunning" "False" - Save-Setting "" "AppChangeInformed" "true" - Show-ModalObject - - if($global:currentViewObject.ViewInfo.Authentication.ShowErrors) - { - & $global:currentViewObject.ViewInfo.Authentication.ShowErrors - } - } - - Add-XamlEvent $script:welcomeForm "btnCancel" "add_click" { - if([System.Windows.MessageBox]::Show("Conditions not accepted`n`nDo you want to close the application?", "Close App?", "YesNo", "Warning") -eq "Yes") - { - $window.Close() - } - } - - Show-ModalForm $window.Title $script:welcomeForm -HideButtons - } - else - { - if($global:informOldAzureApp -eq $true) - { - $appIdChangeInformed = Get-Setting "" "AppChangeInformed" "false" - if($appIdChangeInformed -ne "true") { - $script:oldAzureAppForm = Get-XamlObject ($global:AppRootFolder + "\Xaml\OldAzureApp.xaml") - - Add-XamlEvent $script:oldAzureAppForm "addCustomApp" "Add_RequestNavigate" ({ [System.Diagnostics.Process]::Start($_.Uri.AbsoluteUri); $_.Handled = $true }) - - Add-XamlEvent $script:oldAzureAppForm "btnOK" "add_click" { - if((Get-XamlProperty $script:oldAzureAppForm "chkChangeApp" "IsChecked") -eq $true) { - Write-Log "Set default app ID to $($global:DefaultAzureApp)" - Save-Setting "EndpointManager" "EMAzureApp" $global:DefaultAzureApp - $script:azureAppChanged = $true - } - - if((Get-XamlProperty $script:oldAzureAppForm "chkSkippMessage" "IsChecked") -eq $true) { - Save-Setting "" "AppChangeInformed" "true" - } - Show-ModalObject - if($script:azureAppChanged -eq $true -and $global:currentViewObject) { - [System.Windows.Forms.Application]::DoEvents() - & $global:currentViewObject.ViewInfo.Authenticate - } - } - - Show-ModalForm $window.Title $script:oldAzureAppForm -HideButtons - } - } - - ###!!! Force login here - if($global:currentViewObject.ViewInfo.Authenticate) - { - # Skip for now...need additional code to skip previous login and force this based on setting. - #!!!& $global:currentViewObject.ViewInfo.Authenticate -Params (@{"Interactve"=$true}) - } - } - }) - - foreach($view in $global:viewObjects) - { - $subItem = [System.Windows.Controls.MenuItem]::new() - $subItem.Header = $view.ViewInfo.Title - $subItem.Tag = $view.ViewInfo.Id - $subItem.Add_Click({ - if($this.Tag) - { - Show-View $this.Tag - } - }) - $global:mnuViews.AddChild($subItem) | Out-Null - } - -} - -#endregion - -#region Module functions -function Invoke-ModuleFunction -{ - param($function, $arguments = $null) - - Write-Log "Trigger function $function" - - $params = @{} - if($arguments) - { - $params.Add("ArgumentList",$arguments) - } - foreach($module in $global:loadedModules) - { - # Get command with ExportedFunctions instead of Get-Command - $cmd = $module.ExportedFunctions[$function] - if($cmd) - { - Write-Log "Trigger $function in $($module.Name)" - Invoke-Command -ScriptBlock $cmd.ScriptBlock @params - } - else - { - #Write-Log "$function not found in $($module.Name)" 2 - } - } -} - -#endregion - -#region JWTToken - -### See JWT token documentation for more info: https://tools.ietf.org/html/rfc7519 -### AccessToken documentation https://docs.microsoft.com/en-us/azure/active-directory/develop/access-tokens -function Get-JWTtoken -{ - param($token) - - if(-not $token) { return } - - if(-not $token.StartsWith("eyJ")) - { - Write-Log "Invalid JWT token" 3; return - } - - # First part is the header. Second part is the payload. Third part is the signature - $arr = $token.Split(".") - - if($arr.Count -lt 2) { Write-Log "Invalid token" 3; return } - - $header = $arr[0].Replace('-', '+').Replace('_', '/') # change base64url to base64 - while ($header.Length % 4) { $header += "=" } # Add padding to match required length - - $payload = $arr[1].Replace('-', '+').Replace('_', '/') # change base64url to base64 - while ($payload.Length % 4) { $payload += "=" } # Add padding to match required length - - return (New-Object PSObject -Property @{ - Header=(([System.Text.Encoding]::UTF8.GetString(([System.Convert]::FromBase64String($header)))) | ConvertFrom-Json) - Payload=(([System.Text.Encoding]::UTF8.GetString(([System.Convert]::FromBase64String($payload)))) | ConvertFrom-Json) - }) -} -#endregion - -function Add-GridObject -{ - param($grid, $obj) - - $rd = [System.Windows.Controls.RowDefinition]::new() - $rd.Height = [double]::NaN - $obj.SetValue([System.Windows.Controls.Grid]::RowProperty,$grid.RowDefinitions.Count) | Out-Null - $grid.RowDefinitions.Add($rd) | Out-Null - $grid.Children.Add($obj) | Out-Null -} - -function Get-IsAdmin -{ - (New-Object Security.Principal.WindowsPrincipal ([Security.Principal.WindowsIdentity]::GetCurrent())).IsInRole([Security.Principal.WindowsBuiltinRole]::Administrator) -} - -function Get-NumericUpDownControl -{ - param($id, [decimal]$minValue = 0, [decimal]$maxValue = 9999, [int]$step = 1) - - try - { - [xml]$xaml = Get-Content ($global:AppRootFolder + "\Xaml\NumericUpDown.xaml") - $xamlObj = ([Windows.Markup.XamlReader]::Load((New-Object System.Xml.XmlNodeReader $xaml))) - - $xamlObj.Name = $id - $xamlObj.Children[0].Name = $id + "_TextBox" - $xamlObj.Children[1].Name = $id + "_UpButton" - $xamlObj.Children[1].Name = $id + "_DownButton" - - $settings = [PSCustomObject]@{ - MinValue = $minValue - MaxValue = $maxValue - Step = $step - _lastKnownValue = $null - } - - $xamlObj | Add-Member -MemberType NoteProperty -Name "Settings" -Value $settings - - $xamlObj.Children[0].Add_TextChanged({ - $val = $null - if([decimal]::TryParse($this.Parent.Children[0].Text, [ref]$val)) - { - $this.Parent.Settings._lastKnownValue = $val; - } - }) - - $xamlObj.Children[0].Add_LostFocus({ - $val = $null - if([decimal]::TryParse($this.Parent.Children[0].Text, [ref]$val)) - { - ; - } - elseif($this.Parent.Settings._lastKnownValue) - { - $val = $this.Parent.Settings._lastKnownValue - } - - if($val -ne $null) - { - if($val -gt $this.Parent.Settings.MaxValue) - { - $val = $this.Parent.Settings.MaxValue - } - elseif($val -lt $this.Parent.Settings.MinValue) - { - $val = $this.Parent.Settings.MinValue - } - $this.Parent.Children[0].Text = $val.ToString() - } - }) - - $xamlObj.Children[1].Add_Click({ - $val = $null - if([decimal]::TryParse($this.Parent.Children[0].Text, [ref]$val)) - { - $val = $val + $this.Parent.Settings.Step - if($val -gt $this.Parent.Settings.MaxValue) - { - $val = $this.Parent.Settings.MaxValue - } - $this.Parent.Children[0].Text = $val.ToString() - } - }) - - $xamlObj.Children[2].Add_Click({ - $val = $null - if([decimal]::TryParse($this.Parent.Children[0].Text, [ref]$val)) - { - $val = $val - $this.Parent.Settings.Step - if($val -lt $this.Parent.Settings.MinValue) - { - $val = $this.Parent.Settings.MinValue - } - $this.Parent.Children[0].Text = $val.ToString() - } - }) - - return $xamlObj - - } - catch - { - Write-LogError "Failed to create NumericUpDown control" $_.Exception - return $null - } - -} - -function Format-XML -{ - param([xml]$xml, $indent = 2) - - if(-not $xml) { return } - - #From: https://devblogs.microsoft.com/powershell/format-xml/ - $StringWriter = New-Object System.IO.StringWriter - $XmlWriter = New-Object System.XMl.XmlTextWriter $StringWriter - $xmlWriter.Formatting = "indented" - $xmlWriter.Indentation = $Indent - $xml.WriteContentTo($XmlWriter) - $XmlWriter.Flush() - $StringWriter.Flush() - $StringWriter.ToString() -} - -function Update-XmlFormatting { - [CmdletBinding()] - param( - [Parameter(Mandatory, ValueFromPipeline)] - [string]$Xml - ) - process { - - $Xml = $Xml -replace '<([^\s/>]+)([^>]*)\s/>' , '<$1$2/>' - - $Xml = ($Xml -split "`r?`n") | - Where-Object { $_.Trim().Length -gt 0 } | - ForEach-Object { $_ } | - Out-String - - $Xml = $Xml -replace "`r`n", "`n" - - return $Xml.Trim() - } -} - -function Show-LogView -{ - if($script:LogViewObject -and -not $script:LogViewObject.ViewPanel) - { - $viewPanel = Get-XamlObject ($global:AppRootFolder + "\Xaml\LogInfo.xaml") - - if(-not $viewPanel) { return } - - $script:LogViewObject.ViewPanel = $viewPanel - - Set-XamlProperty $viewPanel "dgLogInfo" "ItemsSource" $script:LogItems - - Add-XamlEvent $viewPanel "dgLogInfo" "add_selectionChanged" ({ - $obj = $this.Parent.FindName("txtLogInfo") - if($obj) - { - $obj.Parent.DataContext = $this.SelectedValue - } - }) - } -} - -function Get-Base64ScriptContent -{ - param($encodeContent, [switch]$RemoveSignature) - - if(-not $encodeContent) { return } - - try - { - $scriptContent = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($encodeContent)) - - if($RemoveSignature -eq $true) - { - $x = $scriptContent.IndexOf("# SIG # Begin signature block") - if($x -gt 0) - { - $scriptContent = $scriptContent.SubString(0,$x) - $scriptContent = $scriptContent + "# SIG # Begin signature block`nSignature data excluded..." - } - } - - $scriptContent - } - catch - { - - } -} - -function Get-ProxyURI -{ - if($null -eq $script:proxyURI) - { - $script:proxyUri = Get-SettingValue "ProxyURI" - } - - if($null -eq $script:proxyURI) - { - $script:proxyUri = "" - } - return $script:proxyURI -} - -function Start-DownloadFile -{ - param($sourceURL, $targetFile) - - Write-Log "Download file from $sourceURL" - if(-not $sourceURL) - { - return - } - - if(-not $targetFile) - { - Write-Log "Target file is missing" - return - } - - [void][System.Reflection.Assembly]::LoadWithPartialName("System.Web.Extensions") - $wc = New-Object System.Net.WebClient - $wc.Encoding = [System.Text.Encoding]::UTF8 - $proxyURI = Get-ProxyURI - if($proxyURI) - { - $wc.Proxy = [System.Net.WebProxy]::new($proxyURI) - } - - try - { - $title = $sourceURL.Split("/")[-1] - $title = $title.Split("/")[0] - } - catch - { - $title = $sourceURL - } - - try - { - Write-Status "Download file: `n$title" - $wc.DownloadFile($sourceURL, $targetFile) - Write-Log "File downloaded to $targetFile" - } - catch - { - Write-LogError "Failed to download file" $_.Exception - } - finally - { - $wc.Dispose() - } -} - -function Get-ASCIIBytes -{ - param($String) - - $bytes = [System.Text.Encoding]::ASCII.GetBytes($String) - - if ($bytes[0] -eq 0x2b -and $bytes[1] -eq 0x2f -and $bytes[2] -eq 0x76) - { [Text.Encoding]::UTF7.GetBytes($String) } - elseif ($bytes[0] -eq 0xff -and $bytes[1] -eq 0xfe) - { [Text.Encoding]::Unicode.GetBytes($String) } - elseif ($bytes[0] -eq 0xfe -and $bytes[1] -eq 0xff) - { [Text.Encoding]::BigEndianUnicode.GetBytes($String) } - elseif ($bytes[0] -eq 0x00 -and $bytes[1] -eq 0x00 -and $bytes[2] -eq 0xfe -and $bytes[3] -eq 0xff) - { [Text.Encoding]::UTF32.GetBytes($String) } - elseif ($bytes[0] -eq 0xef -and $bytes[1] -eq 0xbb -and $bytes[2] -eq 0xbf) - { [Text.Encoding]::UTF8.GetBytes($String) } - - $bytes -} - -function Get-DataGridValues -{ - param($dataGrid) - - $dgColumns = $dataGrid.Columns - - $properties = @() - - foreach($tmpCol in $dgColumns) - { - if(-not $tmpCol.Binding.Path.Path) { continue } - $propName = $tmpCol.Binding.Path.Path - $properties += @{n=$tmpCol.Header;e=([Scriptblock]::Create("`$_.$propName"))} - } - - ($dataGrid.ItemsSource | Select -Property $properties) -} - -function Get-GUIDs -{ - param($text) - - $regExpGuid = "[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}" - - $uniqueGuids = New-Object System.Collections.Generic.HashSet[String] - - # Use regular expressions to extract the GUIDs - [regex]::Matches($text, $regExpGuid) | ForEach-Object { $uniqueGuids.Add($_.Value) | Out-Null } - - $uniqueGuids -} - -New-Alias -Name ?? -value Invoke-Coalesce -New-Alias -Name ?: -value Invoke-IfTrue +<# +.SYNOPSIS +Core UI and Settings fatures for the CloudAPIPowerShellManager solution + +.DESCRIPTION +This module handles the WPF UI + +.NOTES + Author: Mikael Karlsson +#> + +function Get-ModuleVersion +{ + '3.9.6' +} + +function Initialize-Window +{ + param($xamlFile) + + try + { + [xml]$xaml = Get-Content $xamlFile + [xml]$styles = Get-Content ($global:AppRootFolder + "\Themes\Styles.xaml") + + ### Update relative path to full path for ResourceDictionary + [System.Xml.XmlNamespaceManager] $nsm = $xaml.NameTable; + $nsm.AddNamespace("s", 'http://schemas.microsoft.com/winfx/2006/xaml/presentation'); + foreach($rsdNode in ($xaml.SelectNodes("//s:ResourceDictionary[@Source]", $nsm))) + { + $rsdNode.Source = (Join-Path ($global:AppRootFolder) ($rsdNode.Source)).ToString() + } + + # Add Styles + foreach($node in $styles.DocumentElement.ChildNodes) + { + $tmpNode = $xaml.CreateElement("Temp") + $tmpNode.InnerXml = $node.OuterXml + $xaml.Window.'Window.Resources'.ResourceDictionary.AppendChild($tmpNode.Style) | Out-Null + } + return ([Windows.Markup.XamlReader]::Load((New-Object System.Xml.XmlNodeReader $xaml))) + } + catch + { + Write-LogError "Failed to initialize window" $_.Exception + return + } +} + +function Start-CoreApp +{ + param($View) + + if(-not $global:defaultGlobalVariables) + { + $global:defaultGlobalVariables = Get-Variable -Scope Global + } + + $global:useDefaultFolderDialog = $false + $global:WindowsAPICodePackLoaded = $false + $script:proxyURI = $null + + $global:loadedModules = @() + $global:viewObjects = @() + $script:LogItems = [System.Collections.ObjectModel.ObservableCollection[object]]::new() + + $global:AppRootFolder = $PSScriptRoot + + # Load all modules in the Modules folder + $global:modulesPath = [IO.Path]::GetDirectoryName($PSCommandPath) + "\Extensions" + + 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 "#####################################################################################" + + Write-Log "PowerShell version: $($PSVersionTable.PSVersion.ToString())" + if($PSVersionTable.BuildVersion) { + Write-Log "PowerShell build: $($PSVersionTable.BuildVersion.ToString())" + } + if($PSVersionTable.CLRVersion) { + Write-Log "PowerShell CLR: $($PSVersionTable.CLRVersion.ToString())" + } + Write-Log "PowerShell edition: $($PSVersionTable.PSEdition)" + + try + { + $osName = Get-ItemPropertyValue "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion" -Name "ProductName" -ErrorAction Stop + $patchLevel = Get-ItemPropertyValue "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion" -Name "UBR" -ErrorAction Stop + $ver = [Version]::new([Environment]::OSVersion.Version.Major,[Environment]::OSVersion.Version.Minor, [Environment]::OSVersion.Version.Build, $patchLevel) + Write-Log "OS: $osName $ver" + } + catch + { + Write-Log "OS version: $([environment]::OSVersion.VersionString)" + } + + if(Test-Path $global:modulesPath) + { + Import-AllModules + } + else + { + Write-Warning "Extensions folder $($global:modulesPath) not found. Aborting..." 3 + exit 1 + } + + Initialize-Settings + $global:currentViewObject = $null + $global:FirstTimeRunning = ((Get-Setting "" "FirstTimeRunning" "true") -eq "true") + $global:MainAppStarted = $false + + Set-SplashWindowText "Initialize views" + [System.Windows.Forms.Application]::DoEvents() + + Invoke-ModuleFunction "Invoke-InitializeModule" + + if($global:hideUI -ne $true) + { + #Add menu group and items + $script:LogViewObject = (New-Object PSObject -Property @{ + Title = "Log" + Description = "View log items" + ID = "CoreLog" + HideMenu = $true + Activating = { Show-LogView } + Permissions = @() + ViewPanel = $null + }) + + Add-ViewObject $script:LogViewObject + + #This will load the main window + $global:txtSplashText.Text = "Load main window" + [System.Windows.Forms.Application]::DoEvents() + Get-MainWindow + + if($global:window) + { + $global:txtSplashText.Text = "Open default view" + [System.Windows.Forms.Application]::DoEvents() + + Show-View $View + + if((Get-SettingValue "CheckForUpdates") -eq $true) { Get-IsLatestVersion } + + Invoke-ModuleFunction "Invoke-ShowMainWindow" + + $global:txtSplashText.Text = "Open main window" + [System.Windows.Forms.Application]::DoEvents() + $global:window.ShowDialog() | Out-Null + } + } + else + { + if(-not $global:SilentBatchFile) + { + Write-Log "SilentBatchFile must be specified" 3 + return + } + $silentFI = [IO.FileInfo]$global:SilentBatchFile + + if($silentFI.Exists -eq $false) + { + Write-Log "SilentBatchFile $($global:SilentBatchFile) not found" 3 + return + } + Invoke-ModuleFunction "Invoke-ShowMainWindow" + + Invoke-ModuleFunction "Invoke-InitSilentBatchJob" + + Start-RunSilentBatchJob + } +} + +function Start-RunSilentBatchJob +{ + try + { + $settingObj = (ConvertFrom-Json (Get-Content -Path $global:SilentBatchFile -Raw -ErrorAction Stop)) + Invoke-ModuleFunction "Invoke-SilentBatchJob" $settingObj + } + catch + { + Write-LogError "Failed to trigger silent batch job." $_.Exception + } +} + +function Import-AllModules +{ + foreach($file in (Get-Item -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() + + $module = Import-Module $file -PassThru -Force -Global -ErrorAction SilentlyContinue + if($module) + { + $global:loadedModules += $module + Write-Host "Module $($module.Name) loaded successfully" + } + else + { + Write-Warning "Failed to load module $file" + } + } +} + +function Set-SplashWindowText +{ + param($text) + + if($global:hideUI -eq $true) { return } + + $global:txtSplashText.Text = $text +} + +#region Log functions +function Write-Log +{ + param($Text, $type = 1) + + if($script:logFailed -eq $true) { return } + + if(-not $global:logFile) { $global:logFile = Get-SettingValue "LogFile" ([IO.Path]::Combine($global:AppRootFolder,"CloudAPIPowerShellManagement.log")) } + + if(-not $global:logFileMaxSize) { [Int64]$global:logFileMaxSize = Get-SettingValue "LogFileSize" 1024; $global:logFileMaxSize = $global:logFileMaxSize * 1kb } + + if($null -eq $global:logOutputError) { $global:logOutputError = Get-SettingValue "LogOutputError" } + + $fi = [IO.FileInfo]$global:logFile + + if($fi.Length -gt $global:logFileMaxSize) + { + # Larger than max size. Rename current to .bak + # Delete current .bak if it exists + $bakFile = ($fi.DirectoryName + "\" + $fi.BaseName + ".lo_") + if([IO.File]::Exists($bakFile)) + { + try + { + [IO.File]::Delete($bakFile) + } + catch { } + } + try + { + $fi.MoveTo($bakFile) + } + catch { } + } + + try + { + $logPath = [IO.Path]::GetDirectoryName($global:logFile) + if(-not (Test-Path $logPath)) { mkdir -Path $logPath -Force -ErrorAction SilentlyContinue | Out-Null } + } + catch + { + $script:logFailed = $true + return + } + + $date = Get-Date + + if($global:PSCommandPath) + { + $fileObj = [System.IO.FileInfo]$global:PSCommandPath + } + else + { + $fileObj = [System.IO.FileInfo]$PSCommandPath + } + + $timeStr = "$($date.ToString(""HH"")):$($date.ToString(""mm"")):$($date.ToString(""ss"")).000+000" + $dateStr = "$($date.ToString(""MM""))-$($date.ToString(""dd""))-$($date.ToString(""yyyy""))" + $logOut = "" + + if($type -eq 2) + { + Write-Warning $Text + $typeStr = "Warning" + } + elseif($type -eq 3) + { + if($global:logOutputError -ne $false) + { + $host.ui.WriteErrorLine($Text) + } + else + { + Write-Warning $Text + } + $typeStr = "Error" + } + else + { + write-host $Text + $typeStr = "Info" + } + + $script:LogItems.Add([PSCustomObject]@{ + ID = ($script:LogItems.Count + 1) + DateTime = $date + Type = $type + TypeText = $typeStr + Text = $Text + }) + + try + { + out-file -filePath $global:logFile -append -encoding "ASCII" -inputObject $logOut + } + catch { } +} + +function Write-LogDebug +{ + param($Text, $type = 1) + + if($global:Debug) + { + Write-Log ("Debug: " + $text) $type + } +} + +function Write-LogError +{ + param($Text, $Exception) + + if($Text -and $Exception.message) + { + $Text += " Exception: $($Exception.Message)" + } + + Write-Log $Text 3 + + if((Get-SettingValue "ShowStackTrace") -eq $true) + { + Write-Log "Stack trace:`n $($Exception.StackTrace)" + + Write-Log "Script stack trace:`n $($Exception.ScriptStackTrace)" + + } +} + +function Write-Status +{ + param($Text, [switch]$SkipLog, [switch]$Block, [switch]$Force) + + if($global:hideUI -eq $true) + { + if($SkipLog -ne $true) { Write-Log $text } + return + } + + if(-not $text) { $global:BlockStatusUpdates = $false } + elseif($global:BlockStatusUpdates -eq $true -and $Force -ne $true) { return } + elseif($Block -eq $true) { $global:BlockStatusUpdates = $true } + + $global:txtInfo.Content = $Text + if($text) + { + $global:grdStatus.Visibility = "Visible" + if($SkipLog -ne $true) { Write-Log $text } + } + else + { + $global:grdStatus.Visibility = "Collapsed" + } + + [System.Windows.Forms.Application]::DoEvents() +} + +#endregion + +#region Popup +function Show-Popup +{ + param($popup) + + if(-not $global:grdPopup -or -not $global:cvsPopup) { return } + + $global:cvsPopup.AddChild($popup) | Out-Null + $global:grdPopup.Visibility = "Visible" + + [System.Windows.Forms.Application]::DoEvents() +} + +function Hide-Popup +{ + if(-not $global:grdPopup -or -not $global:cvsPopup) { return } + $global:cvsPopup.Children.Clear() + $global:grdPopup.Visibility = "Collapsed" + [System.Windows.Forms.Application]::DoEvents() +} +#endregion + +#region Xaml functions + +function Set-XamlProperty +{ + param($xamlObj, $controlName, $propertyName, $value) + + $obj = $xamlObj.FindName($controlName) + + try + { + if($obj) + { + $obj."$propertyName" = $value + } + else + { + Write-Log "Could not find object with name $controlName" 3 + } + } + catch + { + Write-LogError "Failed to set Xaml property value. Control: $controlName. Property: $propertyName. Error:" $_.Exception + } +} + +function Get-XamlProperty +{ + param($xamlObj, $controlName, $propertyName, $defaultValue = $null) + + $obj = $xamlObj.FindName($controlName) + + try + { + if($obj) + { + return (?? $obj."$propertyName" $defaultValue) + } + else + { + Write-Log "Could not find object with name $controlName" 3 + } + } + catch + { + Write-LogError "Failed to set Xaml property value. Control: $controlName. Property: $propertyName. Error:" $_.Exception + } +} + +function Add-XamlEvent +{ + param($xamlObj, $controlName, $eventName, $scriptBlock) + + try { + $obj = $xamlObj.FindName($controlName) + if($obj) + { + $obj."$eventName"($scriptBlock) + } + else + { + Write-Log "Failed to add Xaml event $eventName to $controlName. Control not found" 3 + } + } + catch + { + Write-LogError "Failed to add Xaml event $eventName to $controlName. Error:" $_.Exception + } +} + +function Add-XamlVariables +{ + param($xaml, $obj) + + # Generate a global variable for each object with Name property set + # Ref: https://learn-powershell.net/2014/08/10/powershell-and-wpf-radio-button/ + $xaml.SelectNodes("//*[@*[contains(translate(name(.),'n','N'),'Name')]]") | ForEach-Object { + Write-LogDebug "Add global variable $($_.Name)" + New-Variable -Name $_.Name -Value $obj.FindName($_.Name) -Force -Scope Global + } +} + +function Get-XamlObject +{ + param($fileName, [switch]$AddVariables) + + if(([IO.File]::Exists($fileName))) + { + try + { + [xml]$xaml = Get-Content $fileName + + $xamlObj = ([Windows.Markup.XamlReader]::Load((New-Object System.Xml.XmlNodeReader $xaml))) + + if($xamlObj -and $AddVariables -eq $true) + { + Add-XamlVariables $xaml $xamlObj + } + return $xamlObj + } + catch + { + Write-LogError "Failed to load Xaml file $fileName. Error:" $_.Exception + } + } + else + { + Write-Log "Failed to open Xaml file. File not found: $fileName" + } +} + +function Invoke-RegisterName +{ + param($parent, $name, $registerTo) + + try + { + $control = $parent.FindName($name) + if($control) + { + $registerTo.RegisterName($name, $control) + } + } + catch + { + Write-LogError "Failed to register $name" $_.Exception + } +} + +#endregion + +#region Silent Functions +function Set-BatchProperties +{ + param($settingsObj, $form, [switch]$SkipMissingControlWarning) + + if(-not $settingsObj -or -not $form) + { + return + } + + foreach($prop in $settingsObj) #($settingsObj | GM | Where MemberType -eq NoteProperty)) + { + if($prop.Type -eq "Custom") { continue } + + $obj = $form.FindName($prop.Name) + if(-not $obj) + { + if($SkipMissingControlWarning -ne $true) + { + Write-Log "No setting for $($prop.Name) found" 2 + } + continue + } + + if($prop.Value -is [String] -and [string]::IsNullOrEmpty($prop.Value)) + { + continue + } + + try + { + if($obj -is [System.Windows.Controls.CheckBox]) + { + $obj.IsChecked = $prop.Value -eq $true + } + elseif($obj -is [System.Windows.Controls.TextBox]) + { + $obj.Text = $prop.Value + } + elseif($obj -is [System.Windows.Controls.ComboBox]) + { + $obj.SelectedValue = $prop.Value + } + else + { + try + { + Write-Log "Unsupported object type for silent batch job: $($obj.GetType().FullName)" 3 + } + catch + {} + } + } + catch + { + Write-LogError "Failed to set batch job property for $($prop.Name)" $_.Exception + } + } +} +#endregion + +#region Dialogs + +function Show-AboutDialog +{ + $script:dlgAbout = Get-XamlObject ($global:AppRootFolder + "\Xaml\AboutDialog.xaml") + if(-not $script:dlgAbout) { return } + + $loadedItems = @() + $externalModules = @("MSAL.PS","Az.Account") + $externalAssemblies = @("Microsoft.Identity.Client.dll") + + foreach($module in (((Get-Module | Where-Object { $_.ModuleBase -like "$($global:AppRootFolder)*" -or $_.Name -in $externalModules })))) + { + $ver = $module.Version + if($module.Version.Major -eq 0 -and $module.Version.Minor -eq 0) + { + $cmd = $module.ExportedFunctions["Get-ModuleVersion"] + if($cmd) + { + $tmpVer = Invoke-Command -ScriptBlock $cmd.ScriptBlock + $ver = ?? $tmpVer $ver + } + } + + $loadedItems += (New-Object PSObject -Property @{ + Name = $module.Name + Version = $ver + Type = "PSModule" + }) + } + + $assms = [System.AppDomain]::CurrentDomain.GetAssemblies() | Where { $_.GlobalAssemblyCache -eq $false -and [String]::IsNullOrEmpty($_.Location) -eq $false } + foreach($assmName in $externalAssemblies) + { + $assmObjs = $assms | Where { $_.Location -like "*\$($assmName)" } + foreach($assmObj in $assmObjs) + { + try + { + $fi = [IO.FileInfo]"$($assmObj.Location)" + $loadedItems += (New-Object PSObject -Property @{ + Name = $fi.Name + Version = $fi.VersionInfo.FileVersion + Type = "Assembly" + }) + } + catch {} + } + } + + Set-XamlProperty $script:dlgAbout "txtTitle" "Text" "CloudAPIPowerShellManagement" + Set-XamlProperty $script:dlgAbout "txtViewTitle" "Text" ("Current view: " + $global:currentViewObject.ViewInfo.Title) + if($global:currentViewObject.ViewInfo.Description) + { + Set-XamlProperty $script:dlgAbout "txtViewDescription" "Text" $global:currentViewObject.ViewInfo.Description + } + + Set-XamlProperty $script:dlgAbout "lstModules" "ItemsSource" $loadedItems + + Add-XamlEvent $script:dlgAbout "linkSource" "Add_RequestNavigate" ({ Start-Process $_.Uri.AbsoluteUri; $_.Handled = $true }) + + Show-ModalForm "About" $script:dlgAbout +} + +function Show-UpdatesDialog +{ + $script:dlgUpdates = Get-XamlObject ($global:AppRootFolder + "\Xaml\UpdatesDialog.xaml") + if(-not $script:dlgUpdates) { return } + + Write-Status "Getting Release Notes Information" + + Add-XamlEvent $script:dlgUpdates "btnClose" "add_click" { + $script:dlgUpdates = $null + Show-ModalObject + } + + Add-XamlEvent $script:dlgUpdates "linkSource" "Add_RequestNavigate" ({ Start-Process $_.Uri.AbsoluteUri; $_.Handled = $true }) + + $fileContent = Get-Content -Raw -Path ($global:AppRootFolder + "\ReleaseNotes.md") + try + { + $tmp = $fileContent.Replace("`r`n","`n") + $mystring = ("blob $($tmp.Length)`0" + $tmp) + $mystream = [IO.MemoryStream]::new([byte[]][char[]]$mystring) + $curHash = Get-FileHash -InputStream $mystream -Algorithm SHA1 + } + finally + { + if($mystream) { $mystream.Dispose() } + } + $params = @{} + $proxyURI = Get-ProxyURI + if($proxyURI) + { + $params.Add("proxy", $proxyURI) + $params.Add("UseBasicParsing", $true) + } + + $content = Invoke-RestMethod "https://api.github.com/repos/Micke-K/IntuneManagement/contents/ReleaseNotes.md" @params + if($content) + { + $txt = [System.Text.Encoding]::UTF8.GetString(([System.Convert]::FromBase64String($content.content))) + Set-XamlProperty $script:dlgUpdates "txtReleaseNotes" "Text" $txt + + if($content.sha -ne $curHash.Hash) + { + # ReleaseNotes.md not matching + Set-XamlProperty $script:dlgUpdates "tabLocalReleaseNotes" "Visibility" "Visible" + Set-XamlProperty $script:dlgUpdates "txtReleaseNotes" "Text" $fileContent + Set-XamlProperty $script:dlgUpdates "txtReleaseNotesMatch" "Visibility" "Collapsed" + } + else + { + Set-XamlProperty $script:dlgUpdates "txtReleaseNotesNoMatch" "Visibility" "Collapsed" + Set-XamlProperty $script:dlgUpdates "tabLocalReleaseNotes" "Visibility" "Collapsed" + } + } + + Write-Status "" + + Show-ModalForm "Release Notes" $script:dlgUpdates -HideButtons +} + +function Get-IsLatestVersion +{ + if($global:MainAppStarted -ne $true) + { + $global:txtSplashText.Text = "Check for updates" + [System.Windows.Forms.Application]::DoEvents() + } + + $gitHubVer = $null + + $params = @{} + $proxyURI = Get-ProxyURI + if($proxyURI) + { + $params.Add("proxy", $proxyURI) + $params.Add("UseBasicParsing", $true) + } + + $content = Invoke-RestMethod "https://api.github.com/repos/Micke-K/IntuneManagement/releases/latest" @params + if($content.Name) + { + try + { + $gitHubVer = [version]$content.Name + } + catch {} + } + + if($null -eq $gitHubVer) + { + $params = @{} + $proxyURI = Get-ProxyURI + if($proxyURI) + { + $params.Add("proxy", $proxyURI) + $params.Add("UseBasicParsing", $true) + } + + $content = Invoke-RestMethod "https://api.github.com/repos/Micke-K/IntuneManagement/contents/CloudAPIPowerShellManagement.psd1" @params + $gitHubText = [System.Text.Encoding]::UTF8.GetString(([System.Convert]::FromBase64String($content.content))) + $gitHubInfo = Get-ModuleDataTable $gitHubText + try + { + $gitHubVer = [version]$gitHubInfo.ModuleVersion + } + catch {} + } + + if(-not $gitHubVer) + { + Write-log "Failed to get version info in GitHub" 2 + return + } + + $LocalInfo = $null + $localVer = $null + try + { + Import-LocalizedData -BindingVariable LocalInfo -BaseDirectory $global:AppRootFolder -FileName "CloudAPIPowerShellManagement.psd1" -ErrorAction Stop + $localVer = [version]$LocalInfo.ModuleVersion + } + catch { } + + if(-not $localVer) + { + Write-log "Failed to get version info from local file" 2 + return + } + + if($localVer -lt $gitHubVer) + { + Write-Log "Local version and GitHub version does not match" 2 + Write-Log "Local version: $($localVer.ToString())" + Write-Log "GitHub version: $($gitHubVer.ToString())" + [System.Windows.MessageBox]::Show("There is a new version available on GitHub $($gitHubVer.ToString())`n`nCurrent version is $($localVer.ToString())", "Old version!", "OK", "Warning") + } + else + { + Write-Log "Running latest version: $($localVer.ToString())" + } +} + +function Get-ModuleDataTable +{ + param($moduleText) + + $result = $null + + if(-not $moduleText) { return } + + try + { + $Path = [IO.path]::ChangeExtension([IO.Path]::GetTempFileName(), "psd1") + $FI = [io.FileInfo]$path + $Utf8NoBomEncoding = New-Object System.Text.UTF8Encoding $False + [System.IO.File]::WriteAllLines($FI.FullName, $moduleText, $Utf8NoBomEncoding) + $Result = $null + Import-LocalizedData -BindingVariable Result -BaseDirectory $FI.DirectoryName -FileName $fi.Name + } + catch + { + + } + finally + { + try { [IO.File]::Delete(([IO.path]::ChangeExtension($FI.FullName, "tmp"))) } catch {} + try { $FI.Delete() } catch{} + } + + $Result +} + +function Show-InputDialog +{ + param( + $FormTitle = "Input", + $FormText, + $DefaultValue) + + $script:inputBox = Initialize-Window ($global:AppRootFolder + "\Xaml\InputDialog.xaml") + if(-not $script:inputBox) { return } + + $script:inputBox.Title = $FormTitle + + Set-XamlProperty $script:inputBox "txtLabel" "Content" $FormText + Set-XamlProperty $script:inputBox "txtValue" "Text" $DefaultValue + + $script:txtValue = $script:inputBox.FindName("txtValue") + + Add-XamlEvent $script:inputBox "btnOk" "Add_Click" ({ $script:inputBox.Close() }) + Add-XamlEvent $script:inputBox "btnCancel" "Add_Click" ({ $script:txtValue.Text ="";$script:inputBox.Close() }) + + $inputBox.Add_ContentRendered({ + $script:txtValue.SelectAll(); + $script:txtValue.Focus(); + }) + + $inputBox.Owner = $global:window + $inputBox.Icon = $global:Window.Icon + + $inputBox.ShowDialog() | Out-null + + return $script:txtValue.Text +} + +function Show-ModalForm +{ + param( + $FormTitle = "", + $formObject, + [switch]$HideButtons) + + $xamlStr = Get-Content ($global:AppRootFolder + "\Xaml\ModalForm.xaml") + + $modalForm = [Windows.Markup.XamlReader]::Parse($xamlStr) + + if($HideButtons -eq $true) + { + Set-XamlProperty $modalForm "spButtons" "Visibility" "Collapsed" + } + else + { + Add-XamlEvent $modalForm "btnClose" "Add_Click" ({ + Show-ModalObject + }) + } + + Set-XamlProperty $modalForm "txtTitle" "Text" $FormTitle + + $grdModalContainer = $modalForm.FindName("grdModalContainer") + if($grdModalContainer -and $formObject) + { + $formObject.SetValue([System.Windows.Controls.Grid]::RowProperty,1) + $grdModalContainer.Children.Add($formObject) | Out-Null + } + Show-ModalObject $modalForm +} +function Show-ModalObject +{ + param( $obj ) + + if($obj) + { + $obj.SetValue([System.Windows.Controls.Grid]::RowProperty,1) + $obj.SetValue([System.Windows.Controls.Grid]::ColumnProperty,1) + $global:grdModal.Children.Add($obj) | Out-Null + $global:grdModal.Visibility = "Visible" + } + else + { + $global:grdModal.Children.Clear() + $global:grdModal.Visibility = "Collapsed" + } + + [System.Windows.Forms.Application]::DoEvents() +} +#endregion + +#region Controls +function Show-AuthenticationInfo +{ + if($global:grdMenu) + { + $global:txtSplashText.Text = "Get profile picture" + [System.Windows.Forms.Application]::DoEvents() + + $authenticationProvider = $global:currentViewObject.ViewInfo.Authentication + if($global:grdMenu.Children[-1].Tag -eq "ProfilePicture") + { + $global:grdMenu.Children.Remove($global:grdMenu.Children[-1]) + } + + if($authenticationProvider.ProfilePicture) + { + $profileObj = & $authenticationProvider.ProfilePicture -Size 24 -Fontsize 12 -Popup -AuthenticationProvider $authenticationProvider + if($profileObj) + { + $profileObj.Tag = "ProfilePicture" + $profileObj.SetValue([System.Windows.Controls.Grid]::ColumnProperty,2) | Out-Null + $global:grdMenu.Children.Add($profileObj) | Out-Null + } + } + [System.Windows.Forms.Application]::DoEvents() + } +} + +function Set-EnvironmentInfo +{ + param($environmentName) + + if(-not $global:grdEnvironment) + { + return + } + + if(-not $script:mnuDefaultBGColor) + { + $script:mnuDefaultBGColor = $global:mnuMain.Background + } + if(-not $script:mnuDefaultFGColor) + { + $script:mnuDefaultFGColor = $global:mnuMain.Foreground + } + + if($global:grdEnvironment -and $environmentName) + { + $global:grdEnvironment.Visibility = "Visible" + if((Get-SettingValue "MenuShowOrganizationName") -eq $true) + { + $global:lblEnvironment.Content = $environmentName + } + else + { + $global:lblEnvironment.Content = "" + } + $bgColor = (Get-SettingValue "MenuBGColor") + $fgColor = (Get-SettingValue "MenuFGColor") + + if(-not $bgColor) + { + $bgColor = $script:mnuDefaultBGColor + } + + if($bgColor) + { + $global:grdMenu.Background = $bgColor + $global:mnuMain.Background = $bgColor + } + + if(-not $fgColor) + { + $fgColor = $script:mnuDefaultFGColor + } + + if($fgColor) + { + $global:lblEnvironment.Foreground = $fgColor + $global:mnuMain.Foreground = $fgColor + } + } + else + { + $global:grdEnvironment.Visibility = "Collapsed" + $global:lblEnvironment.Content = "" + $global:mnuMain.Background = $script:mnuDefaultBGColor + $global:mnuMain.Foreground = $script:mnuDefaultFGColor + $global:lblEnvironment.Foreground = $script:mnuDefaultFGColor + } +} + +#endregion + +#region Generic functions +function Invoke-Coalesce ($value, $default) +{ + # Use IsNullOrEmpty instead of -not + if ([String]::IsNullOrEmpty($value)) { $value = $default } + + return $value +} + +function Invoke-IfTrue ($expression, $valueIfTrue, $valueIfFalse) +{ + if ($expression) { return $valueIfTrue } + else { return $valueIfFalse } +} + +function Set-ObjectGrid +{ + param( $obj ) + + if($obj) + { + $global:grdObject.Children.Add($obj) | Out-Null + $global:grdObject.Visibility = "Visible" + } + else + { + $global:grdObject.Children.Clear() + $global:grdObject.Visibility = "Collapsed" + } + + [System.Windows.Forms.Application]::DoEvents() +} + +function Remove-InvalidFileNameChars +{ + param($Name) + + $re = "[{0}]" -f [RegEx]::Escape(([IO.Path]::GetInvalidFileNameChars() -join '')) + + $Name = $Name -replace $re + + + return $Name +} + +function Remove-ObjectProperty +{ + param($obj, $property) + + if(-not $obj -or -not $property) { return } + + if(($obj | Get-Member -MemberType NoteProperty -Name $property)) + { + $obj.PSObject.Properties.Remove($property) + } +} + +function Get-Folder +{ + param($path = $env:temp, $title = "Select a directory") + + if($global:useDefaultFolderDialog -ne $true) + { + try + { + if($global:WindowsAPICodePackLoaded -eq $false) + { + $apiCodec = Join-Path $global:AppRootFolder "Bin\Microsoft.WindowsAPICodePack.Shell.dll" + if([IO.File]::Exists($apiCodec)) + { + Add-Type -Path $apiCodec | Out-Null + $global:WindowsAPICodePackLoaded = $true + } + else + { + Write-Log "Could not find Microsoft.WindowsAPICodePack.Shell.dll" 2 + } + } + $dlgCOFD = New-Object Microsoft.WindowsAPICodePack.Dialogs.CommonOpenFileDialog + } + catch + { + Write-LogError "Failed to load Microsoft.WindowsAPICodePack.Shell.dll. Verify that the .Net 3.5 feature is enabled" $_.Exception + } + } + + if($dlgCOFD -and $global:useDefaultFolderDialog -ne $true) + { + $dlgCOFD.EnsureReadOnly = $true + $dlgCOFD.IsFolderPicker = $true + $dlgCOFD.AllowNonFileSystemItems = $false + $dlgCOFD.Multiselect = $false + $dlgCOFD.Title = $title + + if($path -and (Test-Path $path)) + { + $dlgCOFD.InitialDirectory = $path + } + if($dlgCOFD.ShowDialog($window) -eq [Microsoft.WindowsAPICodePack.Dialogs.CommonFileDialogResult]::Ok) + { + $dlgCofd.FileName + } + } + else + { + $global:useDefaultFolderDialog = $true + [Reflection.Assembly]::LoadWithPartialName("System.Windows.Forms") | Out-Null + [System.Windows.Forms.Application]::EnableVisualStyles() + $dlgFBD = New-Object System.Windows.Forms.FolderBrowserDialog + $dlgFBD.SelectedPath = "C:\" + $dlgFBD.ShowNewFolderButton = $false + $dlgFBD.Description = $title + if($dlgFBD.ShowDialog() -eq "OK") + { + $dlgFBD.SelectedPath + } + $dlgFBD.Dispose() + } +} +function Remove-Property +{ + param($obj, $prop) + + if(-not $prop) { return } + + if(($obj | GM -MemberType NoteProperty -Name $prop)) + { + Write-LogDebug "Remove property $prop" + $obj.PSObject.Properties.Remove($prop) | Out-Null + } +} + +function Get-GridCheckboxColumn +{ + param($bindingProperty = "IsSelected", [scriptblock]$scriptBlock) + + $binding = [System.Windows.Data.Binding]::new($bindingProperty) + $binding.UpdateSourceTrigger = [System.Windows.Data.UpdateSourceTrigger]::PropertyChanged + $column = [System.Windows.Controls.DataGridTemplateColumn]::new() + $fef = [System.Windows.FrameworkElementFactory]::new([System.Windows.Controls.CheckBox]) + $binding.Mode = [System.Windows.Data.BindingMode]::TwoWay + $fef.SetValue([System.Windows.Controls.CheckBox]::IsCheckedProperty,$binding) + if($null -ne $scriptBlock) + { + [System.Windows.RoutedEventHandler]$checkedEventHandler = $scriptBlock + $fef.AddHandler([System.Windows.Controls.CheckBox]::CheckedEvent, $checkedEventHandler) + } + $dt = [System.Windows.DataTemplate]::new() + $dt.VisualTree = $fef + $column.CellTemplate = $dt + $header = [System.Windows.Controls.CheckBox]::new() + $header.Margin = [System.Windows.Thickness]::new(-4,0,0,0) # Align header checkbox with the row checkboxes + $header.ToolTip = "Select/deselect all items" + $column.Header = $header + if($null -ne $scriptBlock) + { + #$header.add_click($scriptBlock) + } + + $column +} + +function Expand-FileName +{ + param($fileName) + + [Environment]::SetEnvironmentVariable("Date",(Get-Date).ToString("yyyy-MM-dd"),[System.EnvironmentVariableTarget]::Process) + [Environment]::SetEnvironmentVariable("DateTime",(Get-Date).ToString("yyyyMMdd-HHmm"),[System.EnvironmentVariableTarget]::Process) + [Environment]::SetEnvironmentVariable("Organization",$global:Organization.displayName,[System.EnvironmentVariableTarget]::Process) + + $fileName = [Environment]::ExpandEnvironmentVariables($fileName) + + foreach($tmpFolder in ([System.Enum]::GetNames([System.Environment+SpecialFolder]))) + { + $fileName = $fileName -replace "%$($tmpFolder)%",([Environment]::GetFolderPath($tmpFolder)) + } + + [Environment]::SetEnvironmentVariable("Date",$null,[System.EnvironmentVariableTarget]::Process) + [Environment]::SetEnvironmentVariable("DateTime",$null,[System.EnvironmentVariableTarget]::Process) + [Environment]::SetEnvironmentVariable("Organization",$null,[System.EnvironmentVariableTarget]::Process) + + # Remove invalid path characters + $re = "[{0}]" -f [RegEx]::Escape(([IO.Path]::GetInvalidPathChars() -join '')) + $fileName = $fileName -replace $re + + $fileName +} + +#endregion + +#region Save/Read Settings functions +######################################################################## +# +# Save/Read Settings +# +######################################################################## +function Initialize-Settings +{ + param([switch]$Updated) + + $global:Debug = Get-SettingValue "Debug" + $global:logFile = $null + $global:logFileMaxSize = $null + $global:logOutputError = $null + $script:proxyURI = $null + + if($Updated -eq $true) + { + Set-EnvironmentInfo $global:Organization.displayName + 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") + + if($global:hideUI -eq $true) { return } + + if($global:JsonSettingsObj -and $global:JSonSettingFile) + { + if($SubPath) + { + $arrParts = $SubPath.TrimEnd(@('/','\')).Split(@('/','\')) + } + else + { + $arrParts = @() + } + + $parentSetting = $global:JsonSettingsObj + + foreach($part in $arrParts) + { + if(-not $part.Trim()) { continue } + + if(($parentSetting.PSObject.Properties | Where Name -eq $part)) + { + $parentSetting = $parentSetting.$part + } + else + { + $parentSetting | Add-Member -MemberType NoteProperty -Name $part -Value ([PSCustomObject]@{}) + $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 + } +} + +function Remove-Setting +{ + param($SubPath = "", $Key = "") + + if($global:JsonSettingsObj) + { + if($SubPath) + { + $arrParts = $SubPath.TrimEnd(@('/','\')).Split(@('/','\')) + } + else + { + $arrParts = @() + } + + $parentSetting = $global:JsonSettingsObj + + foreach($part in $arrParts) + { + if(($parentSetting.PSObject.Properties | Where Name -eq $part)) + { + $parentSetting = $parentSetting.$part + } + else + { + return + } + } + + if(($parentSetting.PSObject.Properties | Where Name -eq $Key)) + { + $parentSetting.PSObject.Properties.Remove($Key) + } + } + else + { + $regPath = Get-RegPath $subPath + try + { + $temp = Get-Item -LiteralPath $regPath -ErrorAction SilentlyContinue + if(($temp -and $temp.Property -contains $Key)) + { + Remove-ItemProperty -Path $regPath -Name $Key -Force -ErrorAction Stop + } + } + catch + { + Write-LogError "Failed to remove reg value: $($Key) in key $($regPath)" $_.Exception + } + } +} + +function Get-Setting +{ + param($SubPath = "", $Key = "", $defaultValue) + + if(-not $key) + { + return + } + + $val = $null + + if($global:JsonSettingsObj) + { + try + { + if($SubPath) + { + $arrParts = $SubPath.TrimEnd(@('/','\')).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) + { + $defaultValue + } + else + { + $val + } +} + +function Get-RegPath +{ + param($SubPath) + + $path = "HKCU:\Software\CloudAPIPowerShellManagement" + if($SubPath) + { + $path = $path + "\" + $SubPath + } + + $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 -ErrorAction SilentlyContinue + 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 + } +} + +function Remove-TenantSetting +{ + param($settingValue) + + $subPath = ($global:Organization.Id + "\" + $settingValue.SubPath) + + if($global:JsonSettingsObj) + { + if($SubPath) + { + $arrParts = $SubPath.TrimEnd(@('/','\')).Split(@('/','\')) + } + else + { + $arrParts = @() + } + + $parentSetting = $global:JsonSettingsObj + + foreach($part in $arrParts) + { + if(($parentSetting.PSObject.Properties | Where Name -eq $part)) + { + $parentSetting = $parentSetting.$part + } + else + { + return + } + } + + if(($parentSetting.PSObject.Properties | Where Name -eq $settingValue.Key)) + { + $parentSetting.PSObject.Properties.Remove($settingValue.Key) + } + + } + else + { + $regPath = Get-RegPath $subPath + try + { + $temp = Get-Item -LiteralPath $regPath -ErrorAction SilentlyContinue + if(($temp.Property -contains $settingValue.Key)) + { + Remove-ItemProperty -Path $regPath -Name $settingValue.Key -Force -ErrorAction Stop + } + } + catch + { + Write-LogError "Failed to remove reg value: $($settingValue.Key) in key $($regPath)" $_.Exception + } + } +} + +function Get-IsTenantSettingConfigured +{ + param($settingValue) + + $subPath = ($global:Organization.Id + "\" + $settingValue.SubPath) + + if($global:JsonSettingsObj) + { + if($SubPath) + { + $arrParts = $SubPath.TrimEnd(@('/','\')).Split(@('/','\')) + } + else + { + $arrParts = @() + } + + $parentSetting = $global:JsonSettingsObj + + foreach($part in $arrParts) + { + if(($parentSetting.PSObject.Properties | Where Name -eq $part)) + { + $parentSetting = $parentSetting.$part + } + else + { + return $false + } + } + + return ($null -ne ($parentSetting.PSObject.Properties | Where Name -eq $settingValue.Key)) + + } + else + { + $regPath = Get-RegPath $subPath + try + { + $temp = Get-Item -LiteralPath $regPath -ErrorAction Stop + return ($temp.GetValueNames() -contains $settingValue.Key) + } + catch + { + + } + } + return $false +} +#endregion + +#region Setting functions + +######################################################################## +# +# Settings functions +# +######################################################################## + +function Add-SettingsItem +{ + param($settingItem, $settingValue) + + $rd = [System.Windows.Controls.RowDefinition]::new() + $rd.Height = [double]::NaN + $spSettings.RowDefinitions.Add($rd) + $settingItem.SetValue([System.Windows.Controls.Grid]::RowProperty,$spSettings.RowDefinitions.Count-1) + + if(-not $settingValue) + { + $settingItem.SetValue([System.Windows.Controls.Grid]::ColumnSpanProperty, 99) + } + else + { + if($settingValue.Description) + { + $descriptionInfo = "" + + "" + + $settingValue.Description + + "" + + "" + } + + $xaml = @" + + + $descriptionInfo + +"@ + + if($script:tenantSettings -and $settingValue) + { + #_IsChecked + $tenantConfig = [System.Windows.Controls.CheckBox]::new() + $tenantConfig.ToolTip = "Enable tenant specific setting" + $tenantConfig.SetValue([System.Windows.Controls.Grid]::RowProperty,$spSettings.RowDefinitions.Count-1) + $tenantConfig.SetValue([System.Windows.Controls.Grid]::ColumnProperty, 0) + $tenantConfig.Margin = "0,5,0,0" + $tenantConfig.Tag = $settingValue + $tenantConfig.IsChecked = (Get-IsTenantSettingConfigured $settingValue) + $settingItem.IsEnabled = $tenantConfig.IsChecked + $tenantConfig.add_Click({ + if($this.Tag.Control) { $this.Tag.Control.IsEnabled = $this.IsChecked } + } + ) + $spSettings.AddChild($tenantConfig) + } + + $settingsTitle = [Windows.Markup.XamlReader]::Parse($xaml) + $settingsTitle.SetValue([System.Windows.Controls.Grid]::RowProperty,$spSettings.RowDefinitions.Count-1) + $settingsTitle.SetValue([System.Windows.Controls.Grid]::ColumnProperty, 1) + + $settingItem.SetValue([System.Windows.Controls.Grid]::ColumnProperty, 2) + $spSettings.AddChild($settingsTitle) + $settingItem.Margin = "0,5,0,0" + } + $spSettings.AddChild($settingItem) +} + +function Add-SettingTextBox +{ + param($id, $value) + + $xaml = @" +$value +"@ + return [Windows.Markup.XamlReader]::Parse($xaml) +} + +function Add-SettingCheckBox +{ + param($id, $value) + + $tmpValue = ($value -eq $true -or $value -eq "true").ToString().ToLower() + + $xaml = @" + +"@ + return [Windows.Markup.XamlReader]::Parse($xaml) +} + +function Add-SettingComboBox +{ + param($id, $value, $settingObj) + + $nameProp = ?? $settingObj.DisplayMemberPath "Name" + $valueProp = ?? $settingObj.SelectedValuePath "Value" + + $xaml = @" + +"@ + $xamlObj = [Windows.Markup.XamlReader]::Parse($xaml) + + $xamlObj.ItemsSource = $settingObj.ItemsSource + if($value) + { + $xamlObj.SelectedValue = $value + } + + $xamlObj +} + +function Add-SettingFolder +{ + param($id, $value) + $xaml = @" + + + + + + + $value + + + +"@ + + $obj = [Windows.Markup.XamlReader]::Parse($xaml) + + $btnBrowse = $obj.FindName("browse_$($id)") + $txtObj = $obj.FindName($id) + if($btnBrowse) + { + $btnBrowse.Tag = $txtObj + $btnBrowse.Add_Click({ + $folder = Get-Folder $this.Tag.Text + if($folder) { $this.Tag.Text = $folder } + }) + } + return $obj +} + +function Add-SettingValue +{ + param($settingValue) + + $id = "id_" + [Guid]::NewGuid().ToString('n') + + if($settingValue.TenantSettings -eq $false -and $script:tenantSettings) + { + return # Value nut supported in Tenant Settings + } + elseif($settingValue.GlobalSettings -eq $false -and $script:tenantSettings -ne $true) + { + return # Value nut supported in Global Settings + } + + $value = Get-SettingValue $settingValue.Key -GlobalOnly:($script:tenantSettings -ne $true) + + if($settingValue.Type -eq "folder") + { + $settingObj = Add-SettingFolder $id $value + } + elseif($settingValue.Type -eq "Boolean") + { + $settingObj = Add-SettingCheckBox $id $value + } + elseif($settingValue.Type -eq "List") + { + $settingObj = Add-SettingComboBox $id $value $settingValue + } + else + { + $settingObj = Add-SettingTextBox $id $value + } + + if($settingObj) + { + Add-SettingsItem $settingObj $settingValue + # Find the control in the setting object that contains the actual value + # $settingObj might be a grid that contains the TextBox with the settings value + $ctrl = $settingObj.FindName($id) + if(($settingValue | Get-Member -MemberType NoteProperty -Name "Control")) + { + $settingValue.Control = $ctrl + } + else + { + $settingValue | Add-Member -MemberType NoteProperty -Name "Control" -Value $ctrl + } + } +} + +function Add-SettingTitle +{ + param($title, $marginTop = "0") + + $xaml = @" + +"@ + + #$global:spSettings.Children.Add([Windows.Markup.XamlReader]::Parse($xaml)) + Add-SettingsItem ([Windows.Markup.XamlReader]::Parse($xaml)) | Out-Null +} + +function Show-SettingsForm +{ + param([switch]$Tenant) + + $settingsStr = Get-Content ($global:AppRootFolder+ "\Xaml\SettingsForm.xaml") + + $settingsForm = [Windows.Markup.XamlReader]::Parse($settingsStr) + $global:settingControls = @() + $global:spSettings = $settingsForm.FindName("spSettings") + + $script:tenantSettings = ($Tenant -eq $true) + Add-XamlEvent $settingsForm "btnSave" "Add_Click" ({ + Save-AllSettings + }) + + Add-XamlEvent $settingsForm "btnClose" "Add_Click" ({ + $script:tenantSettings = $null + Show-ModalObject + }) + + if($JsonSettingsObj -or $script:tenantSettings -eq $true) + { + 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) + { + Add-SettingTitle $tmp.Title + foreach($settingObj in $tmp.Values) + { + Add-SettingValue $settingObj + } + } + + foreach($settingObj in $global:appSettingSections) + { + if(-not ($settingObj | Get-Member -MemberType NoteProperty -Name "Priority")) + { + $settingObj | Add-Member -MemberType NoteProperty -Name "Priority" -Value 100 + } + if($settingObj.Priority -lt 1) { $settingObj.Priority = 1} + } + + foreach($section in ($global:appSettingSections | Where-Object Id -ne "General" | Sort-Object -Property Priority,Title)) + { + if($section.Values.Count -eq 0) { continue } + Add-SettingTitle $section.Title 5 + foreach($settingObj in $section.Values) + { + Add-SettingValue $settingObj + } + } + Show-ModalObject $settingsForm +} + +function Add-DefaultSettings +{ + $global:appSettingSections = @() + + $script:lstColors = @() + $script:lstColors += [PSCustomObject]@{ + Name = "" + Value = "" + } + + foreach($color in ([System.Drawing.Color].GetProperties() | Where { $_.PropertyType -eq [System.Drawing.Color] } | Sort -Property Name | Select Name).Name) + { + $script:lstColors += [PSCustomObject]@{ + Name = $color + Value = $color + } + } + + $global:appSettingSections += (New-Object PSObject -Property @{ + Title = "General" + Id = "General" + Values = @() + }) + + Add-SettingsObject (New-Object PSObject -Property @{ + Title = "Log file" + Key = "LogFile" + Type = "File" + }) "General" + + Add-SettingsObject (New-Object PSObject -Property @{ + Title = "Max log file size" + Key = "LogFileSize" + Type = "Int" + DefaultValue = 1024 + }) "General" + + Add-SettingsObject (New-Object PSObject -Property @{ + Title = "Add errors to PowerShell output" + Key = "LogOutputError" + Type = "Boolean" + Description = "Write errors to the Error Output of the PS Host. If disabled, errors will be written as a Warning. Eg. disable this if automation should skip logging PowerShell errors." + DefaultValue = $true + }) "General" + + Add-SettingsObject (New-Object PSObject -Property @{ + Title = "Show stack error" + Key = "ShowStackTrace" + Type = "Boolean" + Description = "Write exception stack trace info to the log." + DefaultValue = $false + }) "General" + + Add-SettingsObject (New-Object PSObject -Property @{ + Title = "Debug" + Key = "Debug" + Type = "Boolean" + DefaultValue = $false + }) "General" + + Add-SettingsObject (New-Object PSObject -Property @{ + Title = "Hide No-access items" + Key = "HideNoAccess" + Type = "Boolean" + Description="Remove items from the menu if object permissions is missing. Default is to mark them with red" + DefaultValue = $false + }) "General" + + Add-SettingsObject (New-Object PSObject -Property @{ + Title = "Preview" + Key = "PreviewFeatures" + Type = "Boolean" + DefaultValue = $false + Description = "Enable features that are marked as Preview. This might require a restart and prompt for consent" + }) "General" + + Add-SettingsObject (New-Object PSObject -Property @{ + Title = "Check for updates" + Key = "CheckForUpdates" + Type = "Boolean" + DefaultValue = $true + Description = "Check GitHub if there is a later version available" + }) "General" + + Add-SettingsObject (New-Object PSObject -Property @{ + Title = "Menu Background color" + Key = "MenuBGColor" + Type = "List" + ItemsSource = $script:lstColors + DefaultValue = "" + }) "General" + + Add-SettingsObject (New-Object PSObject -Property @{ + Title = "Menu Foreground color" + Key = "MenuFGColor" + Type = "List" + ItemsSource = $script:lstColors + DefaultValue = "" + }) "General" + + Add-SettingsObject (New-Object PSObject -Property @{ + Title = "Show tenant name" + Key = "MenuShowOrganizationName" + Type = "Boolean" + DefaultValue = $true + Description = "Adds the organization name next to the login info on the menu bar" + }) "General" + + Add-SettingsObject (New-Object PSObject -Property @{ + Title = "Proxy URI" + Key = "ProxyURI" + Description = "Specify the URI for the proxy eg http://<server>:<port>" + }) "General" + +} + +function Add-SettingsObject +{ + param($obj, $section) + + $section = $global:appSettingSections | Where-Object Id -eq $section + if(-not $section) + { + Write-Log "Could not find section $section" 3 + return + } + + try + { + $section.Values += $obj + } + catch { } +} + +function Save-AllSettings +{ + Write-Status "Save settings" + $dt1 = Get-Date + $curHideNoAccess = Get-SettingValue "HideNoAccess" + + foreach($section in $global:appSettingSections) + { + foreach($settingObj in $section.Values) + { + if(-not $settingObj.Control) { continue } + if($settingObj.Control.IsEnabled -eq $false -and $script:tenantSettings) + { + Remove-TenantSetting $settingObj + continue + } + + $valueFound = $false + if($settingObj.Control.GetType().Name -eq "TextBox") + { + $value = $settingObj.Control.Text + if($settingObj.Type -eq "Int") + { + try + { + $value = [int]$value + } + catch + { + # Log or set invalid + $value = $settingObj.Value + } + } + $valueFound = $true + } + elseif($settingObj.Control.GetType().Name -eq "CheckBox") + { + $value = $settingObj.Control.IsChecked + $valueFound = $true + } + elseif($settingObj.Control.GetType().Name -eq "ComboBox") + { + Write-LogDebug "$($settingObj.Control.Text) | $($settingObj.Control.SelectedIndex)" + if($settingObj.Control.SelectedIndex -eq -1) + { + $value = $settingObj.Control.Text + } + else + { + $value = $settingObj.Control.SelectedValue + } + $valueFound = $true + } + + if($valueFound) + { + if($script:tenantSettings) + { + $subPath = ($global:Organization.Id + "\" + $settingObj.SubPath) + } + else + { + $subPath = $settingObj.SubPath + } + Save-Setting $subPath $settingObj.Key $value + } + } + } + + if($global:currentViewObject.ViewInfo.SaveSettings) + { + & $global:currentViewObject.ViewInfo.SaveSettings + } + + Initialize-Settings -Updated + + $newHideNoAccess = Get-SettingValue "HideNoAccess" + if($curHideNoAccess -ne $newHideNoAccess ) + { + Show-ViewMenu + } + + if($dt1.AddSeconds(1) -lt (Get-Date)) + { + Start-Sleep -Seconds 1 # It goes to quick...ToDo: Do this in a better way + } + Write-Status "" +} + +function Get-SettingValue +{ + param($Key, $defaultValue, [switch]$GlobalOnly, [switch]$TenantOnly, $TenantID) + + foreach($section in $global:appSettingSections) + { + $settingObj = $section.Values | Where Key -eq $Key + if($settingObj) { break } + } + + if(-not $defaultValue) { $defaultValue = $settingObj.DefaultValue } + + $value = $null + if(-not $TenantID) { $TenantID = $global:Organization.Id} + + if($GlobalOnly -ne $true -and $TenantID) + { + # Try get Tenant specific value first + $value = Get-Setting ($TenantID + "\" + $settingObj.SubPath) $settingObj.Key + } + + if($null -eq $value -and $TenantOnly -ne $true) + { + # Get global setting value if tenant value was not found + $value = Get-Setting $settingObj.SubPath $settingObj.Key $defaultValue + } + + if($value) + { + if($settingObj.Type -eq "Boolean") + { + $value = $value -eq $true -or $value -eq "true" + } + elseif($settingObj.Type -eq "Boolean") + { + try + { + $value = [int]$value + } + catch + { + if($settingObj.DefaultValue) + { + try + { + $value = [int]$settingObj.DefaultValue + } + catch { } + } + } + } + + # Keep last read value + if($settingObj -and ($settingObj | Get-Member -MemberType NoteProperty -Name "Value")) + { + $settingObj.Value = $value # Keep last read value + } + else + { + $settingObj | Add-Member -MemberType NoteProperty -Name "Value" -Value $value + } + } + $value +} + +#endregion + +#region Menu functions + +##################################################################################################### +# +# Menu functions +# +##################################################################################################### + +function Add-ViewObject +{ + param($viewObject) + + $global:viewObjects += New-Object PSObject -Property @{ ViewInfo = $viewObject; ViewItems = @() } +} + +function Add-ViewItem +{ + param($viewItem) + + $viewObject = $global:viewObjects | Where { $_.ViewInfo.Id -eq $viewItem.ViewID } + if(-not $viewObject) + { + if(($arrMenuInlcude -and $arrMenuInlcude -notcontains $viewItem.ViewID) -or ($arrMenuExlcude -and $arrMenuExlcude -contains $viewItem.ViewID)) { return } + + Write-Log "Could not find menu with id $($viewItem.ViewID). Item $($viewItem.Title) not added" 2 + return + } + + ### !!! ToDo: Should not be here... + if(-not ($viewItem.PSObject.Properties | Where Name -eq "ImportOrder")) + { + $viewItem | Add-Member -NotePropertyName "ImportOrder" -NotePropertyValue 1000 + } + + foreach($scope in $viewItem.Permissons) + { + 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"))) + { + $ctrl = Get-XamlObject ($global:AppRootFolder + "\Xaml\Icons\$((?? $viewItem.Icon $viewItem.Id)).xaml") + $viewItem | Add-Member -NotePropertyName "IconImage" -NotePropertyValue $ctrl + } + + $viewObject.ViewItems += $viewItem +} + +function Show-View +{ + param($viewId) + + if(($global:viewObjects | measure).Count -eq 0) + { + Write-Log "No View Objects loaded!" 3 + return + } + + if(-not $viewId) + { + # Use first View if not specified + # ToDo: Use last or default view + $viewId = $global:viewObjects[0].ViewInfo.Id + } + + if($global:currentViewObject.ViewInfo.ID -eq $viewId) { return } # Current view already selected + + # Get the View object + $viewObject = $global:viewObjects | Where { $_.ViewInfo.Id -eq $viewId } + if(-not $viewObject) + { + Write-Log "Could not find View with id $($viewId)" 3 + return + } + Write-Log "Change view to $($viewObject.ViewInfo.Title)" + + if($global:currentViewObject -ne $viewObject -and $global:currentViewObject.ViewInfo.Deactivating) + { + Write-Log "Deactivating View $($global:currentViewObject.ViewInfo.Title)" + & $global:currentViewObject.ViewInfo.Deactivating + } + + $global:currentViewObject = $viewObject + + Show-ViewMenu + + $lblMenuTitle.Content = $viewObject.ViewInfo.Title + + $grdViewPanel.Children.Clear() + + if($viewObject.ViewInfo.Authenticate) + { + $global:txtSplashText.Text = "Authenticate" + [System.Windows.Forms.Application]::DoEvents() + & $viewObject.ViewInfo.Authenticate + } + + if($viewObject.ViewInfo.Activating) + { + Write-Log "Activating View $($viewObject.ViewInfo.Title)" + & $viewObject.ViewInfo.Activating + } + + if($viewObject.ViewInfo.ViewPanel) + { + $grdViewPanel.Children.Add($viewObject.ViewInfo.ViewPanel) | Out-Null + } + + Set-MainTitle + + Show-AuthenticationInfo + + if($viewObject.ViewInfo.HideMenu -eq $true) + { + $global:grdViewItemMenu.Visibility = "Collapsed" + } + else + { + $global:grdViewItemMenu.Visibility = "Visible" + } + + if($viewObject.ViewInfo.Activated) + { + Write-Log "Activated View $($viewObject.ViewInfo.Title)" + & $viewObject.ViewInfo.Activated + } + + Invoke-ModuleFunction "Invoke-ViewActivated" +} + +function Show-ViewMenu +{ + $viewObject = $global:currentViewObject + + $viewItems = ?: ($viewObject.ViewInfo.Sort -ne $false) ($viewObject.ViewItems | Sort-Object -Property Title) ($viewObject.ViewItems) + + if((Get-SettingValue "HideNoAccess")) + { + $viewItems = $viewItems | Where { $_."@HasPermissions" -ne $false } + } + + $lstMenuItems.ItemsSource = @($viewItems) +} + +#endregion + +#region Main Window +function Set-MainTitle +{ + if(-not $global:window -or -not $global:currentViewObject.ViewInfo.Title) { return } + + Write-LogDebug "Set main title to $($global:currentViewObject.ViewInfo.Title)" + + $global:window.Title = ?? $global:currentViewObject.ViewInfo.Title "Cloud API PowerShell Management" +} + +function Get-MainWindow +{ + try + { + [xml]$xaml = Get-Content ($global:AppRootFolder + "\Xaml\MainWindow.xaml") + [xml]$styles = Get-Content ($global:AppRootFolder + "\Themes\Styles.xaml") + + ### Update relative path to full path for ResourceDictionary + [System.Xml.XmlNamespaceManager] $nsm = $xaml.NameTable; + $nsm.AddNamespace("s", 'http://schemas.microsoft.com/winfx/2006/xaml/presentation'); + foreach($rsdNode in ($xaml.SelectNodes("//s:ResourceDictionary[@Source]", $nsm))) + { + $rsdNode.Source = (Join-Path ($PSScriptRoot) ($rsdNode.Source)).ToString() + } + + # Add Styles + foreach($node in $styles.DocumentElement.ChildNodes) + { + $tmpNode = $xaml.CreateElement("Temp") + $tmpNode.InnerXml = $node.OuterXml + $xaml.Window.'Window.Resources'.ResourceDictionary.AppendChild($tmpNode.Style) | Out-Null + } + $global:window = [Windows.Markup.XamlReader]::Load((New-Object System.Xml.XmlNodeReader $xaml)) + } + catch + { + Write-LogError "Failed to initialize main window" $_.Exception + return + } + + # ToDo: Convert to a list for data binding + Add-XamlEvent $window "mnuSettings" "Add_Click" -scriptBlock ([scriptblock]{ Show-SettingsForm }) + Add-XamlEvent $window "mnuTenantSettings" "Add_Click" -scriptBlock ([scriptblock]{ Show-SettingsForm -Tenant }) + Add-XamlEvent $window "mnuUpdates" "Add_Click" -scriptBlock ([scriptblock]{ Show-UpdatesDialog }) + Add-XamlEvent $window "mnuAbout" "Add_Click" -scriptBlock ([scriptblock]{ Show-AboutDialog }) + Add-XamlEvent $window "mnuExit" "Add_Click" -scriptBlock ([scriptblock]{ + if([System.Windows.MessageBox]::Show("Are you sure you want to exit?", "Exit?", "YesNo", "Question") -eq "Yes") + { + $window.Close() + } + } + ) + + Add-XamlVariables $xaml $window + + $lstMenuItems.Add_SelectionChanged({ + if($global:currentViewObject.ViewInfo.ItemChanged) + { + & $global:currentViewObject.ViewInfo.ItemChanged + } + }) + + $global:grdPopup.add_MouseLeftButtonDown( { Hide-Popup } ) + + # ToDo: !!! Intune should not be default icon... + $iconFile = "$($global:AppRootFolder)\Intune.ico" + if([io.File]::Exists($iconFile)) + { + $Window.Icon = $iconFile + } + + $window.Add_Closed({ + }) + + $window.add_Loaded({ + $global:SplashScreen.Hide() + $global:window.Activate() + [System.Windows.Forms.Application]::DoEvents() + #$global:window.Topmost = $true + #$global:window.Topmost = $false + #$global:window.Focus() + + $global:MainAppStarted = $true + + if($global:FirstTimeRunning) + { + $script:welcomeForm = Get-XamlObject ($global:AppRootFolder + "\Xaml\Welcome.xaml") -AddVariables + + Add-XamlEvent $script:welcomeForm "gitHubLink" "Add_RequestNavigate" ({ Start-Process $_.Uri.AbsoluteUri; $_.Handled = $true }) + Add-XamlEvent $script:welcomeForm "licenseLink" "Add_RequestNavigate" ({ Start-Process $_.Uri.AbsoluteUri; $_.Handled = $true }) + Add-XamlEvent $script:welcomeForm "addCustomApp" "Add_RequestNavigate" ({ Start-Process $_.Uri.AbsoluteUri; $_.Handled = $true }) + + Add-XamlEvent $script:welcomeForm "chkAcceptConditions" "add_click" { + $global:btnAcceptConditions.IsEnabled = ($this.IsChecked -eq $true) + } + + Add-XamlEvent $script:welcomeForm "btnAcceptConditions" "add_click" { + Save-Setting "" "LicenseAccepted" "True" + Save-Setting "" "FirstTimeRunning" "False" + Save-Setting "" "AppChangeInformed" "true" + Show-ModalObject + + if($global:currentViewObject.ViewInfo.Authentication.ShowErrors) + { + & $global:currentViewObject.ViewInfo.Authentication.ShowErrors + } + } + + Add-XamlEvent $script:welcomeForm "btnCancel" "add_click" { + if([System.Windows.MessageBox]::Show("Conditions not accepted`n`nDo you want to close the application?", "Close App?", "YesNo", "Warning") -eq "Yes") + { + $window.Close() + } + } + + Show-ModalForm $window.Title $script:welcomeForm -HideButtons + } + else + { + if($global:informOldAzureApp -eq $true) + { + $appIdChangeInformed = Get-Setting "" "AppChangeInformed" "false" + if($appIdChangeInformed -ne "true") { + $script:oldAzureAppForm = Get-XamlObject ($global:AppRootFolder + "\Xaml\OldAzureApp.xaml") + + Add-XamlEvent $script:oldAzureAppForm "addCustomApp" "Add_RequestNavigate" ({ Start-Process $_.Uri.AbsoluteUri; $_.Handled = $true }) + + Add-XamlEvent $script:oldAzureAppForm "btnOK" "add_click" { + if((Get-XamlProperty $script:oldAzureAppForm "chkChangeApp" "IsChecked") -eq $true) { + Write-Log "Set default app ID to $($global:DefaultAzureApp)" + Save-Setting "EndpointManager" "EMAzureApp" $global:DefaultAzureApp + $script:azureAppChanged = $true + } + + if((Get-XamlProperty $script:oldAzureAppForm "chkSkippMessage" "IsChecked") -eq $true) { + Save-Setting "" "AppChangeInformed" "true" + } + Show-ModalObject + if($script:azureAppChanged -eq $true -and $global:currentViewObject) { + [System.Windows.Forms.Application]::DoEvents() + & $global:currentViewObject.ViewInfo.Authenticate + } + } + + Show-ModalForm $window.Title $script:oldAzureAppForm -HideButtons + } + } + + ###!!! Force login here + if($global:currentViewObject.ViewInfo.Authenticate) + { + # Skip for now...need additional code to skip previous login and force this based on setting. + #!!!& $global:currentViewObject.ViewInfo.Authenticate -Params (@{"Interactve"=$true}) + } + } + }) + + foreach($view in $global:viewObjects) + { + $subItem = [System.Windows.Controls.MenuItem]::new() + $subItem.Header = $view.ViewInfo.Title + $subItem.Tag = $view.ViewInfo.Id + $subItem.Add_Click({ + if($this.Tag) + { + Show-View $this.Tag + } + }) + $global:mnuViews.AddChild($subItem) | Out-Null + } + +} + +#endregion + +#region Module functions +function Invoke-ModuleFunction +{ + param($function, $arguments = $null) + + Write-Log "Trigger function $function" + + $params = @{} + if($arguments) + { + $params.Add("ArgumentList",$arguments) + } + foreach($module in $global:loadedModules) + { + # Get command with ExportedFunctions instead of Get-Command + $cmd = $module.ExportedFunctions[$function] + if($cmd) + { + Write-Log "Trigger $function in $($module.Name)" + Invoke-Command -ScriptBlock $cmd.ScriptBlock @params + } + else + { + #Write-Log "$function not found in $($module.Name)" 2 + } + } +} + +#endregion + +#region JWTToken + +### See JWT token documentation for more info: https://tools.ietf.org/html/rfc7519 +### AccessToken documentation https://docs.microsoft.com/en-us/azure/active-directory/develop/access-tokens +function Get-JWTtoken +{ + param($token) + + if(-not $token) { return } + + if(-not $token.StartsWith("eyJ")) + { + Write-Log "Invalid JWT token" 3; return + } + + # First part is the header. Second part is the payload. Third part is the signature + $arr = $token.Split(".") + + if($arr.Count -lt 2) { Write-Log "Invalid token" 3; return } + + $header = $arr[0].Replace('-', '+').Replace('_', '/') # change base64url to base64 + while ($header.Length % 4) { $header += "=" } # Add padding to match required length + + $payload = $arr[1].Replace('-', '+').Replace('_', '/') # change base64url to base64 + while ($payload.Length % 4) { $payload += "=" } # Add padding to match required length + + return (New-Object PSObject -Property @{ + Header=(([System.Text.Encoding]::UTF8.GetString(([System.Convert]::FromBase64String($header)))) | ConvertFrom-Json) + Payload=(([System.Text.Encoding]::UTF8.GetString(([System.Convert]::FromBase64String($payload)))) | ConvertFrom-Json) + }) +} +#endregion + +function Add-GridObject +{ + param($grid, $obj) + + $rd = [System.Windows.Controls.RowDefinition]::new() + $rd.Height = [double]::NaN + $obj.SetValue([System.Windows.Controls.Grid]::RowProperty,$grid.RowDefinitions.Count) | Out-Null + $grid.RowDefinitions.Add($rd) | Out-Null + $grid.Children.Add($obj) | Out-Null +} + +function Get-IsAdmin +{ + (New-Object Security.Principal.WindowsPrincipal ([Security.Principal.WindowsIdentity]::GetCurrent())).IsInRole([Security.Principal.WindowsBuiltinRole]::Administrator) +} + +function Get-NumericUpDownControl +{ + param($id, [decimal]$minValue = 0, [decimal]$maxValue = 9999, [int]$step = 1) + + try + { + [xml]$xaml = Get-Content ($global:AppRootFolder + "\Xaml\NumericUpDown.xaml") + $xamlObj = ([Windows.Markup.XamlReader]::Load((New-Object System.Xml.XmlNodeReader $xaml))) + + $xamlObj.Name = $id + $xamlObj.Children[0].Name = $id + "_TextBox" + $xamlObj.Children[1].Name = $id + "_UpButton" + $xamlObj.Children[1].Name = $id + "_DownButton" + + $settings = [PSCustomObject]@{ + MinValue = $minValue + MaxValue = $maxValue + Step = $step + _lastKnownValue = $null + } + + $xamlObj | Add-Member -MemberType NoteProperty -Name "Settings" -Value $settings + + $xamlObj.Children[0].Add_TextChanged({ + $val = $null + if([decimal]::TryParse($this.Parent.Children[0].Text, [ref]$val)) + { + $this.Parent.Settings._lastKnownValue = $val; + } + }) + + $xamlObj.Children[0].Add_LostFocus({ + $val = $null + if([decimal]::TryParse($this.Parent.Children[0].Text, [ref]$val)) + { + ; + } + elseif($this.Parent.Settings._lastKnownValue) + { + $val = $this.Parent.Settings._lastKnownValue + } + + if($val -ne $null) + { + if($val -gt $this.Parent.Settings.MaxValue) + { + $val = $this.Parent.Settings.MaxValue + } + elseif($val -lt $this.Parent.Settings.MinValue) + { + $val = $this.Parent.Settings.MinValue + } + $this.Parent.Children[0].Text = $val.ToString() + } + }) + + $xamlObj.Children[1].Add_Click({ + $val = $null + if([decimal]::TryParse($this.Parent.Children[0].Text, [ref]$val)) + { + $val = $val + $this.Parent.Settings.Step + if($val -gt $this.Parent.Settings.MaxValue) + { + $val = $this.Parent.Settings.MaxValue + } + $this.Parent.Children[0].Text = $val.ToString() + } + }) + + $xamlObj.Children[2].Add_Click({ + $val = $null + if([decimal]::TryParse($this.Parent.Children[0].Text, [ref]$val)) + { + $val = $val - $this.Parent.Settings.Step + if($val -lt $this.Parent.Settings.MinValue) + { + $val = $this.Parent.Settings.MinValue + } + $this.Parent.Children[0].Text = $val.ToString() + } + }) + + return $xamlObj + + } + catch + { + Write-LogError "Failed to create NumericUpDown control" $_.Exception + return $null + } + +} + +function Format-XML +{ + param([xml]$xml, $indent = 2) + + if(-not $xml) { return } + + #From: https://devblogs.microsoft.com/powershell/format-xml/ + $StringWriter = New-Object System.IO.StringWriter + $XmlWriter = New-Object System.XMl.XmlTextWriter $StringWriter + $xmlWriter.Formatting = "indented" + $xmlWriter.Indentation = $Indent + $xml.WriteContentTo($XmlWriter) + $XmlWriter.Flush() + $StringWriter.Flush() + $StringWriter.ToString() +} + +function Update-XmlFormatting { + [CmdletBinding()] + param( + [Parameter(Mandatory, ValueFromPipeline)] + [string]$Xml + ) + process { + + $Xml = $Xml -replace '<([^\s/>]+)([^>]*)\s/>' , '<$1$2/>' + + $Xml = ($Xml -split "`r?`n") | + Where-Object { $_.Trim().Length -gt 0 } | + ForEach-Object { $_ } | + Out-String + + $Xml = $Xml -replace "`r`n", "`n" + + return $Xml.Trim() + } +} + +function Show-LogView +{ + if($script:LogViewObject -and -not $script:LogViewObject.ViewPanel) + { + $viewPanel = Get-XamlObject ($global:AppRootFolder + "\Xaml\LogInfo.xaml") + + if(-not $viewPanel) { return } + + $script:LogViewObject.ViewPanel = $viewPanel + + Set-XamlProperty $viewPanel "dgLogInfo" "ItemsSource" $script:LogItems + + Add-XamlEvent $viewPanel "dgLogInfo" "add_selectionChanged" ({ + $obj = $this.Parent.FindName("txtLogInfo") + if($obj) + { + $obj.Parent.DataContext = $this.SelectedValue + } + }) + } +} + +function Get-Base64ScriptContent +{ + param($encodeContent, [switch]$RemoveSignature) + + if(-not $encodeContent) { return } + + try + { + $scriptContent = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($encodeContent)) + + if($RemoveSignature -eq $true) + { + $x = $scriptContent.IndexOf("# SIG # Begin signature block") + if($x -gt 0) + { + $scriptContent = $scriptContent.SubString(0,$x) + $scriptContent = $scriptContent + "# SIG # Begin signature block`nSignature data excluded..." + } + } + + $scriptContent + } + catch + { + + } +} + +function Get-ProxyURI +{ + if($null -eq $script:proxyURI) + { + $script:proxyUri = Get-SettingValue "ProxyURI" + } + + if($null -eq $script:proxyURI) + { + $script:proxyUri = "" + } + return $script:proxyURI +} + +function Start-DownloadFile +{ + param($sourceURL, $targetFile) + + Write-Log "Download file from $sourceURL" + if(-not $sourceURL) + { + return + } + + if(-not $targetFile) + { + Write-Log "Target file is missing" + return + } + + [void][System.Reflection.Assembly]::LoadWithPartialName("System.Web.Extensions") + $wc = New-Object System.Net.WebClient + $wc.Encoding = [System.Text.Encoding]::UTF8 + $proxyURI = Get-ProxyURI + if($proxyURI) + { + $wc.Proxy = [System.Net.WebProxy]::new($proxyURI) + } + + try + { + $title = $sourceURL.Split("/")[-1] + $title = $title.Split("/")[0] + } + catch + { + $title = $sourceURL + } + + try + { + Write-Status "Download file: `n$title" + $wc.DownloadFile($sourceURL, $targetFile) + Write-Log "File downloaded to $targetFile" + } + catch + { + Write-LogError "Failed to download file" $_.Exception + } + finally + { + $wc.Dispose() + } +} + +function Get-ASCIIBytes +{ + param($String) + + $bytes = [System.Text.Encoding]::ASCII.GetBytes($String) + + if ($bytes[0] -eq 0x2b -and $bytes[1] -eq 0x2f -and $bytes[2] -eq 0x76) + { [Text.Encoding]::UTF7.GetBytes($String) } + elseif ($bytes[0] -eq 0xff -and $bytes[1] -eq 0xfe) + { [Text.Encoding]::Unicode.GetBytes($String) } + elseif ($bytes[0] -eq 0xfe -and $bytes[1] -eq 0xff) + { [Text.Encoding]::BigEndianUnicode.GetBytes($String) } + elseif ($bytes[0] -eq 0x00 -and $bytes[1] -eq 0x00 -and $bytes[2] -eq 0xfe -and $bytes[3] -eq 0xff) + { [Text.Encoding]::UTF32.GetBytes($String) } + elseif ($bytes[0] -eq 0xef -and $bytes[1] -eq 0xbb -and $bytes[2] -eq 0xbf) + { [Text.Encoding]::UTF8.GetBytes($String) } + + $bytes +} + +function Get-DataGridValues +{ + param($dataGrid) + + $dgColumns = $dataGrid.Columns + + $properties = @() + + foreach($tmpCol in $dgColumns) + { + if(-not $tmpCol.Binding.Path.Path) { continue } + $propName = $tmpCol.Binding.Path.Path + $properties += @{n=$tmpCol.Header;e=([Scriptblock]::Create("`$_.$propName"))} + } + + ($dataGrid.ItemsSource | Select -Property $properties) +} + +function Get-GUIDs +{ + param($text) + + $regExpGuid = "[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}" + + $uniqueGuids = New-Object System.Collections.Generic.HashSet[String] + + # Use regular expressions to extract the GUIDs + [regex]::Matches($text, $regExpGuid) | ForEach-Object { $uniqueGuids.Add($_.Value) | Out-Null } + + $uniqueGuids +} + +New-Alias -Name ?? -value Invoke-Coalesce +New-Alias -Name ?: -value Invoke-IfTrue Export-ModuleMember -alias * -function * \ No newline at end of file diff --git a/Extensions/EndpointManager.psm1 b/Extensions/EndpointManager.psm1 index 75341c2..46b2536 100644 --- a/Extensions/EndpointManager.psm1 +++ b/Extensions/EndpointManager.psm1 @@ -1,4590 +1,4590 @@ -<# -.SYNOPSIS -Module for managing Intune objects - -.DESCRIPTION -This module is for the Endpoint Manager/Intune View. It manages Export/Import/Copy of Intune objects - -.NOTES - Author: Mikael Karlsson -#> -function Get-ModuleVersion -{ - '3.10.0.6' -} - -function Invoke-InitializeModule -{ - #Add settings - $global:appSettingSections += (New-Object PSObject -Property @{ - Title = "Endpoint Manager/Intune" - Id = "EndpointManager" - Values = @() - Priority = 10 - }) - - Add-SettingsObject (New-Object PSObject -Property @{ - Title = "Application" - Key = "EMAzureApp" - Type = "List" - SelectedValuePath = "ClientId" - ItemsSource = $global:MSGraphGlobalApps - DefaultValue = "" - SubPath = "EndpointManager" - }) "EndpointManager" - - Add-SettingsObject (New-Object PSObject -Property @{ - Title = "Application Id" - Key = "EMCustomAppId" - Type = "String" - DefaultValue = "" - SubPath = "EndpointManager" - }) "EndpointManager" - - Add-SettingsObject (New-Object PSObject -Property @{ - Title = "Redirect URL" - Key = "EMCustomAppRedirect" - Type = "String" - DefaultValue = "" - SubPath = "EndpointManager" - }) "EndpointManager" - - Add-SettingsObject (New-Object PSObject -Property @{ - Title = "Tenant Id" - Key = "EMCustomTenantId" - Type = "String" - DefaultValue = "" - SubPath = "EndpointManager" - }) "EndpointManager" - - Add-SettingsObject (New-Object PSObject -Property @{ - Title = "Authority" - Key = "EMCustomAuthority" - Type = "String" - DefaultValue = "" - SubPath = "EndpointManager" - }) "EndpointManager" - - Add-SettingsObject (New-Object PSObject -Property @{ - Title = "App packages folder" - Key = "EMIntuneAppPackages" - Type = "Folder" - Description = "Root folder where intune app packages are located" - SubPath = "EndpointManager" - }) "EndpointManager" - - Add-SettingsObject (New-Object PSObject -Property @{ - Title = "Save Encryption File" - Key = "EMSaveEncryptionFile" - Type = "Boolean" - Description = "Save encryption file when uploading an app. This can then be used to when downloading the app file." - SubPath = "EndpointManager" - }) "EndpointManager" - - Add-SettingsObject (New-Object PSObject -Property @{ - Title = "App download folder" - Key = "EMIntuneAppDownloadFolder" - Type = "Folder" - Description = "Folder where app packages will be downloaded and where encryption files will be saved" - SubPath = "EndpointManager" - }) "EndpointManager" - - Get-SettingValue "ProxyURI" - - if($global:FirstTimeRunning) { - Save-Setting "EndpointManager" "EMAzureApp" $global:DefaultAzureApp - } - - $currentAppID = Get-SettingValue "EMAzureApp" - $customAppID = Get-SettingValue "EMCustomAppId" - $global:informOldAzureApp = $false - - if(($global:OldAzureApps -is [Array] -and $currentAppID -in $global:OldAzureApps) -or (-not $currentAppID -and -not $customAppID)) - { - $global:informOldAzureApp = $true - 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 - - #Add menu group and items - $global:EMViewObject = (New-Object PSObject -Property @{ - Title = "Intune Manager" - Description = "Manages Intune environments. This view can be used for copying objects in an Intune environment. It can also be used for backing up an entire Intune environment and cloning the Intune environment into another tenant." - ID="IntuneGraphAPI" - ViewPanel = $viewPanel - AuthenticationID = "MSAL" - ItemChanged = { Show-GraphObjects -ObjectTypeChanged; Invoke-ModuleFunction "Invoke-GraphObjectsChanged"; Write-Status ""} - Deactivating = { Invoke-EMDeactivateView } - Activating = { Invoke-EMActivatingView } - Authentication = (Get-MSALAuthenticationObject) - Authenticate = { Invoke-EMAuthenticateToMSAL @args } - AppInfo = (Get-GraphAppInfo "EMAzureApp" $global:DefaultAzureApp "EM") - SaveSettings = { Invoke-EMSaveSettings } - - Permissions = @() - }) - - Add-ViewObject $global:EMViewObject - - Add-ViewItem (New-Object PSObject -Property @{ - Title = "Device Configuration" - Id = "DeviceConfiguration" - ViewID = "IntuneGraphAPI" - API = "/deviceManagement/deviceConfigurations" - QUERYLIST = "`$filter=not%20isof(%27microsoft.graph.windowsUpdateForBusinessConfiguration%27)%20and%20not%20isof(%27microsoft.graph.iosUpdateConfiguration%27)" - #ExportFullObject = $false - Permissons=@("DeviceManagementConfiguration.ReadWrite.All") - PropertiesToRemove = @("privacyAccessControls") - PostFileImportCommand = { Start-PostFileImportDeviceConfiguration @args } - PostCopyCommand = { Start-PostCopyDeviceConfiguration @args } - PostGetCommand = { Start-PostGetDeviceConfiguration @args } - GroupId = "DeviceConfiguration" - NavigationProperties=$true - }) - - Add-ViewItem (New-Object PSObject -Property @{ - Title = "Conditional Access" - Id = "ConditionalAccess" - ViewID = "IntuneGraphAPI" - API = "/identity/conditionalAccess/policies" - Permissons=@("Policy.Read.All","Policy.ReadWrite.ConditionalAccess","Application.Read.All") - Dependencies = @("NamedLocations","Applications","TermsOfUse","AuthenticationStrengths","AssignmentFilters") - GroupId = "ConditionalAccess" - ImportExtension = { Add-ConditionalAccessImportExtensions @args } - PreImportCommand = { Start-PreImportConditionalAccess @args } - PostExportCommand = { Start-PostExportConditionalAccess @args } - ExpandAssignmentsList = $false - SupportsPageSize = $false - }) - - Add-ViewItem (New-Object PSObject -Property @{ - Title = "Terms of use" - Id = "TermsOfUse" - ViewID = "IntuneGraphAPI" - ViewProperties = @("id", "displayName") - Expand = "files" - QUERYLIST = "`$expand=files" - API = "/identityGovernance/termsOfUse/agreements" - Permissons=@("Agreement.ReadWrite.All") - PreImportCommand = { Start-PreImportTermsOfUse @args } - PostExportCommand = { Start-PostExportTermsOfUse @args } - GroupId = "ConditionalAccess" - SupportsPageSize = $false - }) - - Add-ViewItem (New-Object PSObject -Property @{ - Title = "Named Locations" - Id = "NamedLocations" - ViewID = "IntuneGraphAPI" - API = "/identity/conditionalAccess/namedLocations" - Permissons=@("Policy.ReadWrite.ConditionalAccess") - ImportOrder = 50 - GroupId = "ConditionalAccess" - ExpandAssignmentsList = $false - SupportsPageSize = $false - }) - - Add-ViewItem (New-Object PSObject -Property @{ - Title = "Endpoint Security" - Id = "EndpointSecurity" - ViewID = "IntuneGraphAPI" - API = "/deviceManagement/intents" - PropertiesToRemove = @('Settings','@OData.Type') - PreImportCommand = { Start-PreImportEndpointSecurity @args } - PostListCommand = { Start-PostListEndpointSecurity @args } - PostExportCommand = { Start-PostExportEndpointSecurity @args } - PostFileImportCommand = { Start-PostFileImportEndpointSecurity @args } - PostGetCommand = { Start-PostGetEndpointSecurity @args } - #PreCopyCommand = { Start-PreCopyEndpointSecurity @args } - PostCopyCommand = { Start-PostCopyEndpointSecurity @args } - PreUpdateCommand = { Start-PreUpdateEndpointSecurity @args } - Permissons=@("DeviceManagementConfiguration.ReadWrite.All") - Dependencies = @("ReusableSettings") - GroupId = "EndpointSecurity" - SupportsPageSize = $false - }) - - Add-ViewItem (New-Object PSObject -Property @{ - Title = "Compliance Policies" - Id = "CompliancePolicies" - ViewID = "IntuneGraphAPI" - Expand = "scheduledActionsForRule(`$expand=scheduledActionConfigurations)" - API = "/deviceManagement/deviceCompliancePolicies" - Permissons=@("DeviceManagementConfiguration.ReadWrite.All") - Dependencies = @("Locations","Notifications","ComplianceScripts") - PostExportCommand = { Start-PostExportCompliancePolicies @args } - PreUpdateCommand = { Start-PreUpdateCompliancePolicies @args } - GroupId = "CompliancePolicies" - SupportsPageSize = $false - }) - - Add-ViewItem (New-Object PSObject -Property @{ - Title = "Compliance Policies - V2" - Id = "CompliancePoliciesV2" - ViewID = "IntuneGraphAPI" - API = "/deviceManagement/compliancePolicies" - NameProperty = "name" - PropertiesToRemove = @('settingCount') - ViewProperties = @("name","description","Id") - Expand="settings" - Permissons=@("DeviceManagementConfiguration.ReadWrite.All") - GroupId = "CompliancePolicies" - Icon = "CompliancePolicies" - }) - - - Add-ViewItem (New-Object PSObject -Property @{ - Title = "Compliance Scripts" - Id = "ComplianceScripts" - ViewID = "IntuneGraphAPI" - API = "/deviceManagement/deviceComplianceScripts" - PostImportCommand = { Start-PostImportComplianceScripts @args } - Permissons=@("DeviceManagementScripts.ReadWrite.All") - GroupId = "CompliancePolicies" - Icon = "Scripts" - ImportOrder = 80 - }) - - Add-ViewItem (New-Object PSObject -Property @{ - Title = "Intune Branding" - Id = "IntuneBranding" - API = "/deviceManagement/intuneBrandingProfiles" - ViewID = "IntuneGraphAPI" - NameProperty = "profileName" - ViewProperties = @("profileName", "displayName", "description", "id","isDefaultProfile") - PreImportCommand = { Start-PreImportIntuneBranding @args } - PostImportCommand = { Start-PostImportIntuneBranding @args } - PostGetCommand = { Start-PostGetIntuneBranding @args } - PostExportCommand = { Start-PostExportIntuneBranding @args } - PreDeleteCommand = { Start-PreDeleteIntuneBranding @args } - PreUpdateCommand = { Start-PreUpdateIntuneBranding @args } - Permissons=@("DeviceManagementApps.ReadWrite.All") - Icon = "Branding" - SkipRemoveProperties = @('Id') # Id is removed by PreImport. Required for default profile - PropertiesToRemoveForUpdate = @('isDefaultProfile','disableClientTelemetry') - GroupId = "TenantAdmin" - SupportsPageSize = $false - }) - - <# - # BUG in Graph? Cannot create default branding. Can only create it when importing another object - # Header required Accept-Language: sv-SE - # Documentation says to use Content-Language but that doesn't work - - # Could work with https://main.iam.ad.ext.azure.com/api/LoginTenantBrandings - - #> - - Add-ViewItem (New-Object PSObject -Property @{ - Title = "Azure Branding" - Id = "AzureBranding" - API = "/organization/%OrganizationId%/branding/localizations" - ViewID = "IntuneGraphAPI" - ViewProperties = @("Id") - PreImportCommand = { Start-PreImportAzureBranding @args } - PostListCommand = { Start-PostListAzureBranding @args } - ShowButtons = @("Export","View") - NameProperty = "Id" - Permissons=@("Organization.ReadWrite.All") - Icon = "Branding" - SkipRemoveProperties = @('Id') - GroupId = "Azure" - SkipAddIDOnExport = $true - ExpandAssignmentsList = $false - SupportsPageSize = $false - }) - - Add-ViewItem (New-Object PSObject -Property @{ - Title = "Enrollment Status Page" - Id = "EnrollmentStatusPage" - API = "/deviceManagement/deviceEnrollmentConfigurations" - ViewID = "IntuneGraphAPI" - PreImportCommand = { Start-PreImportESP @args } - PostExportCommand = { Start-PostExportESP @args } - PreDeleteCommand = { Start-PreDeleteEnrollmentRestrictions @args } # Note: Uses same PreDelete as restrictions - PreReplaceCommand = { Start-PreReplaceEnrollmentRestrictions @args } # Note: Uses same PreReplaceCommand as restrictions - PostReplaceCommand = { Start-PostReplaceEnrollmentRestrictions @args } # Note: Uses same PostReplaceCommand as restrictions - PreFilesImportCommand = { Start-PreFilesImportEnrollmentRestrictions @args } # Note: Uses same PreFilesImportCommand as restrictions - PreImportAssignmentsCommand = { Start-PreImportAssignmentsEnrollmentRestrictions @args } # Note: Uses same PreFilesImportCommand as restrictions - PostListCommand = { Start-PostListESP @args } - #PreUpdateCommand = { Start-PreUpdateEnrollmentRestrictions @args } # Note: Uses same PreUpdateCommand as restrictions - #QUERYLIST = "`$filter=endsWith(id,'Windows10EnrollmentCompletionPageConfiguration')" - Permissons=@("DeviceManagementServiceConfig.ReadWrite.All") - SkipRemoveProperties = @('Id') - Dependencies = @("Applications") - AssignmentsType = "enrollmentConfigurationAssignments" - PropertiesToRemoveForUpdate = @('priority') - GroupId = "WinEnrollment" - SupportsPageSize = $false - }) - - Add-ViewItem (New-Object PSObject -Property @{ - Title = "Enrollment Restrictions" - Id = "EnrollmentRestrictions" - API = "/deviceManagement/deviceEnrollmentConfigurations" - ViewID = "IntuneGraphAPI" - #QUERYLIST = "`$filter=not endsWith(id,'Windows10EnrollmentCompletionPageConfiguration')" - PostExportCommand = { Start-PostExportEnrollmentRestrictions @args } - PreImportCommand = { Start-PreImportEnrollmentRestrictions @args } - PreDeleteCommand = { Start-PreDeleteEnrollmentRestrictions @args } - PreReplaceCommand = { Start-PreReplaceEnrollmentRestrictions @args } - PostReplaceCommand = { Start-PostReplaceEnrollmentRestrictions @args } - PreFilesImportCommand = { Start-PreFilesImportEnrollmentRestrictions @args } - PostListCommand = { Start-PostListEnrollmentRestrictions @args } - PreImportAssignmentsCommand = { Start-PreImportAssignmentsEnrollmentRestrictions @args } - #PreUpdateCommand = { Start-PreUpdateEnrollmentRestrictions @args } - PropertiesToRemoveForUpdate = @('priority') - Permissons=@("DeviceManagementServiceConfig.ReadWrite.All") - SkipRemoveProperties = @('Id') - AssignmentsType = "enrollmentConfigurationAssignments" - GroupId = "EnrollmentRestrictions" - ViewProperties = @("displayName","platformType","description","Id") - SupportsPageSize = $false - }) - - Add-ViewItem (New-Object PSObject -Property @{ - Title = "Co-Management Settings" - Id = "CoManagementSettings" - ViewID = "IntuneGraphAPI" - API = "/deviceManagement/deviceEnrollmentConfigurations" - PostReplaceCommand = { Start-PostReplaceEnrollmentRestrictions @args } # Note: Uses same PostReplaceCommand as restrictions - PreFilesImportCommand = { Start-PreFilesImportEnrollmentRestrictions @args } # Note: Uses same PreFilesImportCommand as restrictions - PostListCommand = { Start-PostListCoManagementSettings @args } - PropertiesToRemoveForUpdate = @('priority') - Permissons=@("DeviceManagementServiceConfig.ReadWrite.All") - SkipRemoveProperties = @('Id') - GroupId = "WinEnrollment" - Icon = "EnrollmentStatusPage" - SupportsPageSize = $false - }) - - Add-ViewItem (New-Object PSObject -Property @{ - Title = "Administrative Templates" - Id = "AdministrativeTemplates" - API = "/deviceManagement/groupPolicyConfigurations" - ViewID = "IntuneGraphAPI" - PostGetCommand = { Start-PostGetAdministrativeTemplate @args } - PostExportCommand = { Start-PostExportAdministrativeTemplate @args } - PostCopyCommand = { Start-PostCopyAdministrativeTemplate @args } - PostFileImportCommand = { Start-PostFileImportAdministrativeTemplate @args } - PreImportCommand = { Start-PreImportAdministrativeTemplate @args } - LoadObject = { Start-LoadAdministrativeTemplate @args } - PropertiesToRemove = @("definitionValues","policyConfigurationIngestionType") - Permissons=@("DeviceManagementConfiguration.ReadWrite.All") - Icon="DeviceConfiguration" - GroupId = "DeviceConfiguration" - CompareValue = "CombinedValueWithLabel" - Dependencies = @("ADMXFiles") - }) - - Add-ViewItem (New-Object PSObject -Property @{ - Title = "Scripts (PowerShell)" - Id = "PowerShellScripts" - API = "/deviceManagement/deviceManagementScripts" - ViewID = "IntuneGraphAPI" - DetailExtension = { Add-ScriptExtensions @args } - ExportExtension = { Add-ScriptExportExtensions @args } - PostExportCommand = { Start-PostExportScripts @args } - Permissons=@("DeviceManagementScripts.ReadWrite.All") - AssignmentsType = "deviceManagementScriptAssignments" - Icon="Scripts" - GroupId = "Scripts" - }) - - Add-ViewItem (New-Object PSObject -Property @{ - Title = "Scripts (Shell)" - Id = "MacScripts" - API = "/deviceManagement/deviceShellScripts" - ViewID = "IntuneGraphAPI" - DetailExtension = { Add-ScriptExtensions @args } - ExportExtension = { Add-ScriptExportExtensions @args } - PostExportCommand = { Start-PostExportScripts @args } - Permissons=@("DeviceManagementScripts.ReadWrite.All") - AssignmentsType = "deviceManagementScriptAssignments" - Icon="Scripts" - GroupId = "Scripts" - }) - - Add-ViewItem (New-Object PSObject -Property @{ - Title = "Custom Attributes" - Id = "MacCustomAttributes" - API = "/deviceManagement/deviceCustomAttributeShellScripts" - ViewID = "IntuneGraphAPI" - Permissons=@("DeviceManagementScripts.ReadWrite.All") - AssignmentsType = "deviceManagementScriptAssignments" - Icon="CustomAttributes" - GroupId = "CustomAttributes" # MacOS Settings - DetailExtension = { Add-ScriptExtensions @args } - ExportExtension = { Add-ScriptExportExtensions @args } - PostExportCommand = { Start-PostExportScripts @args } - PropertiesToRemoveForUpdate = @('customAttributeName','customAttributeType','displayName') - #PreUpdateCommand = { Start-PreUpdateMacCustomAttributes @args } - }) - - Add-ViewItem (New-Object PSObject -Property @{ - Title = "Terms and Conditions" - Id = "TermsAndConditions" - API = "/deviceManagement/termsAndConditions" - ViewID = "IntuneGraphAPI" - Permissons=@("DeviceManagementServiceConfig.ReadWrite.All") - ExpandAssignments = $false # Not supported for this object type - PostExportCommand = { Start-PostExportTermsAndConditions @args } - PreImportAssignmentsCommand = { Start-PreImportAssignmentsTermsAndConditions @args } - GroupId = "TenantAdmin" - ExpandAssignmentsList = $false - SupportsPageSize = $false - }) - - Add-ViewItem (New-Object PSObject -Property @{ - Title = "App Protection" - Id = "AppProtection" - API = "/deviceAppManagement/managedAppPolicies" - ViewID = "IntuneGraphAPI" - PreGetCommand = { Start-GetAppProtection @args } - PostListCommand = { Start-PostListAppProtection @args } - PreImportCommand = { Start-PreImportAppProtection @args } - PostImportCommand = { Start-PostImportAppProtection @args } - PreImportAssignmentsCommand = { Start-PreImportAssignmentsAppProtection @args } - PreUpdateCommand = { Start-PreUpdateAppProtection @args } - ExportFullObject = $true - PropertiesToRemove = @('exemptAppLockerFiles') - PropertiesToRemoveForUpdate = @("protectedAppLockerFiles","version") # ToDo: !!! Add support for protectedAppLockerFiles? - Permissons=@("DeviceManagementApps.ReadWrite.All") - Dependencies = @("Applications") - GroupId = "AppProtection" - ExpandAssignmentsList = $false - SupportsPageSize = $false - }) - - # These are also included in the managedAppPolicies API - # So all custom commands will be handled by the same functions as App Protection - Add-ViewItem (New-Object PSObject -Property @{ - Title = "App Configuration (App)" - Id = "AppConfigurationManagedApp" - API = "/deviceAppManagement/targetedManagedAppConfigurations" - ViewID = "IntuneGraphAPI" - PreGetCommand = { Start-GetAppProtection @args } - PreImportCommand = { Start-PreImportAppProtection @args } - PostImportCommand = { Start-PostImportAppProtection @args } - PreImportAssignmentsCommand = { Start-PreImportAssignmentsAppProtection @args } - PreUpdateCommand = { Start-PreUpdateAppConfigurationApp @args } - Permissons=@("DeviceManagementApps.ReadWrite.All") - Dependencies = @("Applications") - Icon = "AppConfiguration" - GroupId = "AppConfiguration" - ExpandAssignmentsList = $false - SupportsPageSize = $false - }) - - Add-ViewItem (New-Object PSObject -Property @{ - Title = "App Configuration (Device)" - Id = "AppConfigurationManagedDevice" - API = "/deviceAppManagement/mobileAppConfigurations" - QUERYLIST = "`$filter=microsoft.graph.androidManagedStoreAppConfiguration/appSupportsOemConfig%20eq%20false%20or%20isof(%27microsoft.graph.androidManagedStoreAppConfiguration%27)%20eq%20false" - ViewID = "IntuneGraphAPI" - Permissons=@("DeviceManagementApps.ReadWrite.All") - Dependencies = @("Applications") - PreFilesImportCommand = { Start-PreFilesImportAppConfiguration @args } - PreImportAssignmentsCommand = { Start-PreImportAssignmentsAppConfiguration @args } - PostExportCommand = { Start-PostExportAppConfiguration @args } - Icon = "AppConfiguration" - GroupId = "AppConfiguration" - }) - - Add-ViewItem (New-Object PSObject -Property @{ - Title = "Applications" - Id = "Applications" - API = "/deviceAppManagement/mobileApps" - ViewID = "IntuneGraphAPI" - PropertiesToRemove = @('uploadState','publishingState','isAssigned','dependentAppCount','supersedingAppCount','supersededAppCount','committedContentVersion','isFeatured','size','categories') #,'minimumSupportedWindowsRelease' - QUERYLIST = "`$filter=(microsoft.graph.managedApp/appAvailability%20eq%20null%20or%20microsoft.graph.managedApp/appAvailability%20eq%20%27lineOfBusiness%27%20or%20isAssigned%20eq%20true)&`$orderby=displayName" - QuerySearch=$true - Permissons=@("DeviceManagementApps.ReadWrite.All") - AssignmentsType="mobileAppAssignments" - AssignmentProperties = @("@odata.type","target","settings","intent") - AssignmentTargetProperties = @("@odata.type","groupId","deviceAndAppManagementAssignmentFilterId","deviceAndAppManagementAssignmentFilterType") - ImportOrder = 60 - Expand="categories,assignments" # ODataMetadata is set to minimal so assignments can't be autodetected - ODataMetadata="minimal" # categories property not supported with ODataMetadata full - PostFileImportCommand = { Start-PostFileImportApplication @args } - PostCopyCommand = { Start-PostCopyApplication @args } - PreUpdateCommand = { Start-PreUpdateApplication @args } - PreImportCommand = { Start-PreImportCommandApplication @args } - DetailExtension = { Add-DetailExtensionApplications @args } - PreImportAssignmentsCommand = { Start-PreImportAssignmentsApplications @args } - PreDeleteCommand = { Start-PreDeleteApplications @args } - PostExportCommand = { Start-PostExportApplications @args } - PostListCommand = { Start-PostListApplications @args } - ExportExtension = { Add-ScriptExportApplications @args } - PostGetCommand = { Start-PostGetApplications @args } - PostImportCommand = { Start-PostImportApplications @args } - PostFilesImportCommand = { Start-PostFilesImportApplications @args } - GroupId = "Apps" - ScopeTagsReturnedInList = $false - }) - - Add-ViewItem (New-Object PSObject -Property @{ - Title = "Autopilot" - Id = "AutoPilot" - API = "/deviceManagement/windowsAutopilotDeploymentProfiles" - ViewID = "IntuneGraphAPI" - CopyDefaultName = "%displayName% Copy" # '-' is not allowed in the name - Permissons=@("DeviceManagementServiceConfig.ReadWrite.All") - PreImportAssignmentsCommand = { Start-PreImportAssignmentsAutoPilot @args } - PreDeleteCommand = { Start-PreDeleteAutoPilot @args } - PropertiesToRemoveForUpdate = @('managementServiceAppId') - GroupId = "WinEnrollment" - }) - - Add-ViewItem (New-Object PSObject -Property @{ - Title = "Policy Sets" - Id = "PolicySets" - API = "/deviceAppManagement/policySets" - ViewID = "IntuneGraphAPI" - Expand = "Items" - PreImportAssignmentsCommand = { Start-PreImportAssignmentsPolicySets @args } - PreImportCommand = { Start-PreImportPolicySets @args } - PreUpdateCommand = { Start-PreUpdatePolicySets @args } - PostListCommand = { Start-PostListPolicySets @args } - Permissons=@("DeviceManagementConfiguration.ReadWrite.All") - ImportOrder = 2000 # Policy Sets reference other objects so make sure it is imported last - Dependencies = @("Applications","AppConfiguration","AppProtection","AutoPilot","EnrollmentRestrictions","EnrollmentStatusPage","DeviceConfiguration","AdministrativeTemplates","SettingsCatalog","CompliancePolicies") - GroupId = "PolicySets" - ExpandAssignmentsList = $false # expand is not allowed, IsAssigned is set in PostListCommand - }) - - Add-ViewItem (New-Object PSObject -Property @{ - Title = "Update Policies" - Id = "UpdatePolicies" - ViewID = "IntuneGraphAPI" - API = "/deviceManagement/deviceConfigurations" - QUERYLIST = "`$filter=isof(%27microsoft.graph.windowsUpdateForBusinessConfiguration%27)%20or%20isof(%27microsoft.graph.iosUpdateConfiguration%27)" - #ExportFullObject = $false - Permissons=@("DeviceManagementConfiguration.ReadWrite.All") - GroupId = "WinUpdatePolicies" - PropertiesToRemoveForUpdate = @('version','qualityUpdatesPauseStartDate','featureUpdatesPauseStartDate','qualityUpdatesWillBeRolledBack','featureUpdatesWillBeRolledBack') - }) - - Add-ViewItem (New-Object PSObject -Property @{ - Title = "Feature Updates" - Id = "FeatureUpdates" - ViewID = "IntuneGraphAPI" - API = "/deviceManagement/windowsFeatureUpdateProfiles" - Permissons=@("DeviceManagementConfiguration.ReadWrite.All") - GroupId = "WinFeatureUpdates" - PropertiesToRemoveForUpdate = @('deployableContentDisplayName','endOfSupportDate') - #PreUpdateCommand = { Start-PreUpdateFeatureUpdates @args } - }) - - Add-ViewItem (New-Object PSObject -Property @{ - Title = "Quality Updates (Profiles)" - Id = "QualityUpdates" - ViewID = "IntuneGraphAPI" - API = "/deviceManagement/windowsQualityUpdateProfiles" - Permissons=@("DeviceManagementConfiguration.ReadWrite.All") - Icon = "UpdatePolicies" - GroupId = "WinQualityUpdates" - PropertiesToRemoveForUpdate = @('releaseDateDisplayName','deployableContentDisplayName') - SupportsPageSize = $false - }) - - Add-ViewItem (New-Object PSObject -Property @{ - Title = "Quality Updates (Policies)" - Id = "QualityUpdatePolicies" - ViewID = "IntuneGraphAPI" - API = "/deviceManagement/windowsQualityUpdatePolicies" - Permissons=@("DeviceManagementConfiguration.ReadWrite.All") - Icon = "UpdatePolicies" - GroupId = "WinQualityUpdates" - SupportsPageSize = $false - }) - - # Locations are not FULLY supported - # They will be imported but Compliance Policies will not be updated with new Location object after import - # ToDo: Add support Export/Import Location Settings - # Location object - Only used by Android Device Admins Compliance Policies - # - These should probably be migrated to Android Enterprise anyway. That is the recommendation by Google - # Property that needs to be updated on the Compliance Policy - # deviceManagement/managementConditionStatements/$obj.conditionStatementId - - # Location objects support removed from Intune - <# - Add-ViewItem (New-Object PSObject -Property @{ - Title = "Locations" - Id = "Locations" - ViewID = "IntuneGraphAPI" - API = "/deviceManagement/managementConditions" - Permissons=@("DeviceManagementConfiguration.ReadWrite.All") - PreImportCommand = { Start-PreImportLocations @args } - ImportOrder = 30 - GroupId = "CompliancePolicies" - }) - #> - - Add-ViewItem (New-Object PSObject -Property @{ - Title = "Settings Catalog" - Id = "SettingsCatalog" - ViewID = "IntuneGraphAPI" - API = "/deviceManagement/configurationPolicies" - PropertiesToRemove = @('settingCount') - Permissons=@("DeviceManagementConfiguration.ReadWrite.All") - NameProperty = "name" - ViewProperties = @("name","description","Id") - Expand="Settings" - Icon="DeviceConfiguration" - PreImportCommand = { Start-PreImportSettingsCatalog @args } - PostExportCommand = { Start-PostExportSettingsCatalog @args } - PreUpdateCommand = { Start-PreUpdateSettingsCatalog @args } - PostGetCommand = { Start-PostGetSettingsCatalog @args } - Dependencies = @("ReusableSettings") - GroupId = "DeviceConfiguration" - }) - - Add-ViewItem (New-Object PSObject -Property @{ - Title = "Inventory Policies" - Id = "InventoryPolicies" - ViewID = "IntuneGraphAPI" - API = "/deviceManagement/inventoryPolicies" - PropertiesToRemove = @('settingCount') - Permissons=@("DeviceManagementConfiguration.ReadWrite.All") - NameProperty = "name" - ViewProperties = @("name","description","Id") - Expand="Settings" - Icon="DeviceConfiguration" - GroupId = "DeviceConfiguration" - }) - - Add-ViewItem (New-Object PSObject -Property @{ - Title = "BIOS Configurations" - Id = "HardwareConfigurations" - ViewID = "IntuneGraphAPI" - DetailExtension = { Add-PolicyFileExtensions @args } - ExportExtension = { Add-PolicyFileExportExtensions @args } - PostExportCommand = { Start-PostExportPolicyFile @args } - PropertiesToRemoveForUpdate = @('version') - PolicyFileAttribute = "configurationFileContent" - API = "/deviceManagement/hardwareConfigurations" - Permissons=@("DeviceManagementConfiguration.ReadWrite.All") - Icon="DeviceConfiguration" - GroupId = "DeviceConfiguration" - SupportsPageSize = $false - }) - - Add-ViewItem (New-Object PSObject -Property @{ - Title = "Role Definitions" - Id = "RoleDefinitions" - ViewID = "IntuneGraphAPI" - API = "/deviceManagement/roleDefinitions" - QUERYLIST = "`$filter=isBuiltIn%20eq%20false" - PostExportCommand = { Start-PostExportRoleDefinitions @args } - PreImportCommand = { Start-PreImportRoleDefinitions @args } - PostFileImportCommand = { Start-PostFileImportRoleDefinitions @args } - Permissons=@("DeviceManagementRBAC.ReadWrite.All") - ImportOrder = 20 - #expand=roleassignments - PropertiesToRemoveForUpdate = @('isBuiltInRoleDefinition','isBuiltIn','roleAssignments') ### !!! ToDo: Add support for roleAssignments - GroupId = "TenantAdmin" - ExpandAssignments = $false - ExpandAssignmentsList = $false - }) - - Add-ViewItem (New-Object PSObject -Property @{ - Title = "Scope (Tags)" - Id = "ScopeTags" - ViewID = "IntuneGraphAPI" - API = "/deviceManagement/roleScopeTags" - QUERYLIST = "`$filter=isBuiltIn%20eq%20false" - Permissons=@("DeviceManagementRBAC.ReadWrite.All") - PostExportCommand = { Start-PostExportScopeTags @args } - PostGetCommand = { Start-PostGetScopeTags @args } - ImportOrder = 10 - DocumentAll = $true - GroupId = "TenantAdmin" - ExpandAssignmentsList = $false # Adds the assignmnets property but always empty - }) - - Add-ViewItem (New-Object PSObject -Property @{ - Title = "Notifications" - Id = "Notifications" - ViewID = "IntuneGraphAPI" - API = "/deviceManagement/notificationMessageTemplates" - Permissons=@("DeviceManagementServiceConfig.ReadWrite.All") - ImportOrder = 40 - Expand = "localizedNotificationMessages" - PreImportCommand = { Start-PreImportNotifications @args } - PostFileImportCommand = { Start-PostFileImportNotifications @args } - PostCopyCommand = { Start-PostCopyNotifications @args } - PropertiesToRemoveForUpdate = @('defaultLocale','localizedNotificationMessages') ### !!! ToDo: Add support for localizedNotificationMessages - GroupId = "CompliancePolicies" - ExpandAssignmentsList = $false - }) - - # This has some pre-reqs for working! - # Import is tested and verified in a tenant with Googple Play connection configured - # And the OEM app was dpwnloaded e.g. Knox Service Plugin - # Import failed in a tenant where Google Play was NOT configured - Add-ViewItem (New-Object PSObject -Property @{ - Title = "Android OEM Config" - Id = "AndroidOEMConfig" - ViewID = "IntuneGraphAPI" - QUERYLIST = "`$filter=microsoft.graph.androidManagedStoreAppConfiguration/appSupportsOemConfig%20eq%20true" - API = "/deviceAppManagement/mobileAppConfigurations" - PreImportAssignmentsCommand = { Start-PreImportAssignmentsAppConfiguration @args } - PreFilesImportCommand = { Start-PreFilesImportAppConfiguration @args } - PostExportCommand = { Start-PostExportAppConfiguration @args } - Permissons=@("DeviceManagementConfiguration.ReadWrite.All") - Icon="DeviceConfiguration" - Dependencies = @("Applications") - GroupId = "DeviceConfiguration" - }) - - # Copy/Export/Import not verified! - Add-ViewItem (New-Object PSObject -Property @{ - Title = "Apple Enrollment Types" - Id = "AppleEnrollmentTypes" - ViewID = "IntuneGraphAPI" - API = "/deviceManagement/appleUserInitiatedEnrollmentProfiles" - Permissons=@("DeviceManagementServiceConfig.ReadWrite.All") - PropertiesToRemoveForUpdate = @('platform') - GroupId = "AppleEnrollment" - }) - - Add-ViewItem (New-Object PSObject -Property @{ - Title = "Filters" - Id = "AssignmentFilters" - ViewID = "IntuneGraphAPI" - API = "/deviceManagement/assignmentFilters" - Permissons=@("DeviceManagementConfiguration.ReadWrite.All") - ImportOrder = 15 - GroupId = "TenantAdmin" - PropertiesToRemoveForUpdate = @('platform') - ExpandAssignmentsList = $false - PropertiesToRemove = @("payloads") - }) - - Add-ViewItem (New-Object PSObject -Property @{ - Title = "Health Scripts" - Id = "DeviceHealthScripts" - ViewID = "IntuneGraphAPI" - QUERYLIST = "`$filter=isGlobalScript%20eq%20false" # Looks like filters are not working for deviceHealthScripts - API = "/deviceManagement/deviceHealthScripts" - PreDeleteCommand = { Start-PreDeleteDeviceHealthScripts @args } - PreImportCommand = { Start-PreImportDeviceHealthScripts @args } - PreUpdateCommand = { Start-PreUpdateDeviceHealthScripts @args } - PostExportCommand = { Start-PostExportDeviceHealthScripts @args } - ExportExtension = { Add-ScriptExportExtensions @args } - Permissons=@("DeviceManagementScripts.ReadWrite.All") - GroupId = "EndpointAnalytics" - Icon = "Report" - AssignmentsType = "deviceHealthScriptAssignments" - AssignmentProperties = @("target","runSchedule","runRemediationScript") - PropertiesToRemoveForUpdate = @('version','isGlobalScript','highestAvailableVersion') - }) - - Add-ViewItem (New-Object PSObject -Property @{ - Title = "ADMX Files" - Id = "ADMXFiles" - ViewID = "IntuneGraphAPI" - NameProperty = "fileName" - API = "/deviceManagement/groupPolicyUploadedDefinitionFiles" - Permissons=@("DeviceManagementConfiguration.ReadWrite.All") - ImportOrder = 45 - GroupId = "DeviceConfiguration" - Icon = "DeviceConfiguration" - ExpandAssignmentsList = $false - PreFilesImportCommand = { Start-PreFilesImportADMXFiles @args } - PreImportCommand = { Start-PreImportADMXFiles @args } - PostImportCommand = { Start-PostImportADMXFiles @args } - PreDeleteCommand = { Start-PreDeleteADMXFiles @args } - ViewProperties = @("fileName","status","Id") - PropertiesToRemove = @("languageCodes","targetPrefix","targetNamespace","policyType","revision","status","uploadDateTime") - SupportsPageSize = $false - }) - - <# - Add-ViewItem (New-Object PSObject -Property @{ - Title = "iOS Enrollment Profile" - Id = "iOSDepProfile" - ViewID = "IntuneGraphAPI" - API = "/deviceManagement/depIOSEnrollmentProfile" - Permissons=@("DeviceManagementServiceConfig.ReadWrite.All") - GroupId = "DeviceConfiguration" - Icon = "DeviceConfiguration" - ExpandAssignmentsList = $false - ViewProperties = @("fileName","status","Id") - }) - #> - - Add-ViewItem (New-Object PSObject -Property @{ - Title = "Reusable Settings" - Id = "ReusableSettings" - ViewID = "IntuneGraphAPI" - API = "/deviceManagement/reusablePolicySettings" - PropertiesToRemove = @('Settings','@OData.Type') - PostGetCommand = { Start-PostGetReusableSettings @args } - ImportOrder = 70 - Permissons=@("DeviceManagementConfiguration.ReadWrite.All") - ExpandAssignmentsList = $false - SkipRemoveProperties = @("@OData.Type") - Icon = "EndpointSecurity" - GroupId = "EndpointSecurity" - }) - - Add-ViewItem (New-Object PSObject -Property @{ - Title = "Authentication Strengths" - Id = "AuthenticationStrengths" - ViewID = "IntuneGraphAPI" - API = "/identity/conditionalAccess/authenticationStrengths/policies" - PreImportCommand = { Start-PreImportCommandAuthenticationStrengths @args } - PropertiesToRemove = @() - ImportOrder = 45 - Permissons=@("Policy.ReadWrite.ConditionalAccess") - ExpandAssignmentsList = $false - Icon = "ConditionalAccess" - GroupId = "EndpointSecurity" - SupportsPageSize = $false - }) - - Add-ViewItem (New-Object PSObject -Property @{ - Title = "Authentication Context" - Id = "AuthenticationContext" - ViewID = "IntuneGraphAPI" - API = "/identity/conditionalAccess/authenticationContextClassReferences" - PropertiesToRemove = @("@odata.type") - SkipRemoveProperties = @('Id') - ImportOrder = 46 - PreImportCommand = { Start-PreImportCommandAuthenticationContext @args } - Permissons=@("Policy.ReadWrite.ConditionalAccess") - ExpandAssignmentsList = $false - Icon = "ConditionalAccess" - GroupId = "EndpointSecurity" - SupportsPageSize = $false - }) - - Add-ViewItem (New-Object PSObject -Property @{ - Title = "W365 Provisioning Policies" - Id = "W365ProvisioningPolicies" - ViewID = "IntuneGraphAPI" - API = "/deviceManagement/virtualEndpoint/provisioningPolicies" - Permissons=@("CloudPC.ReadWrite.All") - Icon = "Devices" - GroupId = "DeviceConfiguration" - }) - - Add-ViewItem (New-Object PSObject -Property @{ - Title = "W365 User Settings" - Id = "W365UserSettings" - ViewID = "IntuneGraphAPI" - API = "/deviceManagement/virtualEndpoint/userSettings" - Permissons = @("CloudPC.ReadWrite.All") - Icon = "Devices" - GroupId = "DeviceConfiguration" - }) - - Add-ViewItem (New-Object PSObject -Property @{ - Title = "Driver Update Profiles" - Id = "DriverUpdateProfiles" - ViewID = "IntuneGraphAPI" - API = "/deviceManagement/windowsDriverUpdateProfiles" - Permissons = @("DeviceManagementConfiguration.ReadWrite.All") - Icon = "UpdatePolicies" - GroupId = "WinDriverUpdatePolicies" - }) - - Add-ViewItem (New-Object PSObject -Property @{ - Title = "Device Categories" - Id = "DeviceCategories" - ViewID = "IntuneGraphAPI" - API = "/deviceManagement/deviceCategories" - QUERYLIST = "`$top=500" - Permissons = @("DeviceManagementConfiguration.ReadWrite.All") - GroupId = "DeviceConfiguration" - ExpandAssignmentsList = $false - }) - -} - -function Invoke-EMAuthenticateToMSAL -{ - param($params = @{}) - - $global:EMViewObject.AppInfo = Get-GraphAppInfo "EMAzureApp" $global:DefaultAzureApp "EM" - Set-MSALCurrentApp $global:EMViewObject.AppInfo - & $global:msalAuthenticator.Login -Account (?? $global:MSALToken.Account.UserName (Get-Setting "" "LastLoggedOnUser")) @params -} - -function Invoke-EMDeactivateView -{ - $tmp = $mnuMain.Items | Where Name -eq "EMBulk" - if($tmp) { $mnuMain.Items.Remove($tmp) } -} - -function Invoke-EMActivatingView -{ - Show-MSALError - - # Refresh values in case they have changed - $global:EMViewObject.AppInfo = (Get-GraphAppInfo "EMAzureApp" $global:DefaultAzureApp "EM") - if(-not $global:EMViewObject.Authentication) - { - $global:EMViewObject.Authentication = Get-MSALAuthenticationObject - } - - # Add View specific menus - Add-GraphBulkMenu -} - -function Invoke-EMSaveSettings -{ - $tmpApp = Get-GraphAppInfo "EMAzureApp" $global:DefaultAzureApp - - if($global:appObj.ClientID -ne $tmpApp.ClientId -and $global:MSALToken) - { - # The app has changed. Need to authenticate to the new app - Write-Status "Logging in to $((?? $global:appObj.Name "selected application"))" - $global:EMViewObject.AppInfo = $tmpApp - Set-MSALCurrentApp $global:EMViewObject.AppInfo - Clear-MSALCurentUserVaiables - Connect-MSALUser -Account $global:MSALToken.Account.Username - Write-Status "" - } - - Set-EMUIStatus -} - -function Invoke-GraphAuthenticationUpdated -{ - Set-EMUIStatus - - $script:CustomADMXDefinitions = $null -} - -function Set-EMUIStatus -{ - # Hide/Show Delete button - $allowDelete = Get-SettingValue "EMAllowDelete" - $global:btnDelete.Visibility = (?: ($allowDelete -eq $true) "Visible" "Collapsed") - - # Hide/Show Delete on Bulk menu - $allowBulkDelete = Get-SettingValue "EMAllowBulkDelete" - $mnuBulk = $mnuMain.Items | Where Name -eq "EMBulk" - - if($mnuBulk) - { - $mnuBulkDelete = $mnuBulk.Items | Where Name -eq "mnuBulkDelete" - if($mnuBulkDelete) - { - $mnuBulkDelete.Visibility = (?: ($allowBulkDelete -eq $true) "Visible" "Collapsed") - } - } -} - -function Set-EMViewPanel -{ - param($panel) - - # ToDo: Create View specific pannel and move this to graph - Add-XamlEvent $panel "btnView" "Add_Click" -scriptBlock ([scriptblock]{ - Show-GraphObjectInfo - }) - - Add-XamlEvent $panel "btnDelete" "Add_Click" -scriptBlock ([scriptblock]{ - Remove-GraphObjects - }) - - Add-XamlEvent $panel "btnCopy" "Add_Click" -scriptBlock ([scriptblock]{ - Copy-GraphObject - }) - - Add-XamlEvent $panel "btnExport" "Add_Click" -scriptBlock ([scriptblock]{ - Show-GraphExportForm - }) - - Add-XamlEvent $panel "btnImport" "Add_Click" -scriptBlock ([scriptblock]{ - Show-GraphImportForm - }) - - Add-XamlEvent $panel "txtFilter" "Add_LostFocus" ({ #param($obj, $e) - Invoke-FilterBoxChanged $this - #$e.Handled = $true - }) - - Add-XamlEvent $panel "txtFilter" "Add_GotFocus" ({ - if($this.Tag -eq "1" -and $this.Text -eq "Filter") { $this.Text = "" } - Invoke-FilterBoxChanged $this - }) - - Add-XamlEvent $panel "txtFilter" "Add_TextChanged" ({ - Invoke-FilterBoxChanged $this - }) - - Invoke-FilterBoxChanged ($panel.FindName("txtFilter")) - - $allowDelete = Get-SettingValue "EMAllowDelete" - Set-XamlProperty $panel "btnDelete" "Visibility" (?: ($allowDelete -eq $true) "Visible" "Collapsed") - - $global:dgObjects.add_selectionChanged({ - Invoke-ModuleFunction "Invoke-EMSelectedItemsChanged" - }) - - # ToDo: Move this to the view object - $dpd = [System.ComponentModel.DependencyPropertyDescriptor]::FromProperty([System.Windows.Controls.ItemsControl]::ItemsSourceProperty, [System.Windows.Controls.DataGrid]) - if($dpd) - { - $dpd.AddValueChanged($global:dgObjects, { - Set-XamlProperty $global:dgObjects.Parent "txtFilter" "Text" "" - $enabled = (?: ($null -eq $this.ItemsSource -or ($this.ItemsSource | measure).Count -eq 0) $false $true) - Set-XamlProperty $global:dgObjects.Parent "btnImport" "IsEnabled" $true # Always all Import if ObjectType allows it - Set-XamlProperty $global:dgObjects.Parent "btnExport" "IsEnabled" $enabled - }) - } - - $btnRefresh = Get-XamlObject ($global:AppRootFolder + "\Xaml\RefreshButton.xaml") - if($btnRefresh) - { - $btnRefresh.SetValue([System.Windows.Controls.Grid]::ColumnProperty,$grdTitle.ColumnDefinitions.Count - 1) - $btnRefresh.Margin = "0,0,5,3" - $btnRefresh.Cursor = "Hand" - $btnRefresh.Name = "btnRefresh" - $btnRefresh.Focusable = $false - $grdTitle.Children.Add($btnRefresh) | Out-Null - - $tooltip = [System.Windows.Controls.ToolTip]::new() - $tooltip.Content = "Refresh all objects" - [System.Windows.Controls.ToolTipService]::SetToolTip($btnRefresh, $tooltip) - - $panel.RegisterName($btnRefresh.Name, $btnRefresh) - - $tooltip = [System.Windows.Controls.ToolTip]::new() - $tooltip.Content = "Refresh objects" - - [System.Windows.Controls.ToolTipService]::SetToolTip($btnRefresh, $tooltip) - - $btnRefresh.Add_Click({ - $txtFilterText = $null - $txtFilter = $this.Parent.FindName("txtFilter") - if($txtFilter) { $txtFilterText = $txtFilter.Text } #= "" } - - Show-GraphObjects $txtFilterText - - if($txtFilterText -and $txtFilter) - { - $txtFilter.Text = $txtFilterText - Invoke-FilterBoxChanged $txtFilter - } - - Write-Status "" - }) - } - - $global:btnLoadAllPages.add_click({ - Write-Status "Loading $($global:curObjectType.Title) objects" - [array]$graphObjects = Get-GraphObjects -property $global:curObjectType.ViewProperties -objectType $global:curObjectType -AllPages - if(-not $global:dgObjects.Columns) - { - Show-GraphObjects -FromGraphObjects $graphObjects - } - else - { - $graphObjects | Where-Object { $_ -ne $null } | ForEach-Object { $global:dgObjects.ItemsSource.AddNewItem($_) | Out-Null } - } - $global:dgObjects.ItemsSource.CommitNew() - Set-GraphPagesButtonStatus - Invoke-FilterBoxChanged $global:txtFilter -ForceUpdate - Write-Status "" - }) - - $global:btnLoadNextPage.add_click({ - Write-Status "Loading $($global:curObjectType.Title) objects" - [array]$graphObjects = Get-GraphObjects -property $global:curObjectType.ViewProperties -objectType $global:curObjectType -SinglePage - if(-not $global:dgObjects.Columns) - { - Show-GraphObjects -FromGraphObjects $graphObjects - } - else - { - $graphObjects | Where-Object { $_ -ne $null } | ForEach-Object { $global:dgObjects.ItemsSource.AddNewItem($_) | Out-Null } - } - $global:dgObjects.ItemsSource.CommitNew() - Set-GraphPagesButtonStatus - Invoke-FilterBoxChanged $global:txtFilter - Write-Status "" - }) -} - -function Invoke-GraphObjectsChanged -{ - $btnRefresh = $global:EMViewObject.ViewPanel.FindName("btnRefresh") - - if($btnRefresh) - { - $tooltip = [System.Windows.Controls.ToolTipService]::GetToolTip($btnRefresh) - if($global:lstMenuItems.SelectedItem.QuerySearch -eq $true) - { - $tooltip.Content = "Refresh objects based on filter. Note: Only filtered objects will be returned. Clear filter and press refresh to reload other objects" - } - else - { - $tooltip.Content = "Refresh all objects" - } - } -} - -function Invoke-EMSelectedItemsChanged -{ - $hasSelectedItems = ($global:dgObjects.ItemsSource | Where IsSelected -eq $true) -or ($null -ne $global:dgObjects.SelectedItem) - Set-XamlProperty $global:dgObjects.Parent "btnView" "IsEnabled" $hasSelectedItems #(?: ($null -eq ($global:dgObjects.SelectedItem)) $false $true) - Set-XamlProperty $global:dgObjects.Parent "btnCopy" "IsEnabled" $hasSelectedItems #(?: ($null -eq $global:dgObjects.SelectedItem) $false $true) - Set-XamlProperty $global:dgObjects.Parent "btnDelete" "IsEnabled" $hasSelectedItems #(?: ($null -eq $global:dgObjects.SelectedItem -and $global:curObjectType.AllowDelete -ne $false) $false $true) -} - -function Invoke-FilterBoxChanged -{ - param($txtBox,[switch]$ForceUpdate) - - $filter = $null - - if($txtBox.Text.Trim() -eq "" -and $txtBox.IsFocused -eq $false) - { - $txtBox.FontStyle = "Italic" - $txtBox.Tag = 1 - $txtBox.Text = "Filter" - $txtBox.Foreground="Lightgray" - } - elseif($ForceUpdate -eq $true) - { - $dgObjects.ItemsSource.Filter = $dgObjects.ItemsSource.Filter - } - elseif($txtBox.Tag -eq "1" -and $txtBox.Text -eq "Filter" -and $txtBox.IsFocused -eq $false) - { - - } - else - { - $txtBox.FontStyle = "Normal" - $txtBox.Tag = $null - $txtBox.Foreground="Black" - $txtBox.Background="White" - - if($txtBox.Text) - { - $filter = { - param ($item) - - return ($null -ne ($item.PSObject.Properties | Where { $_.Name -notin @("IsSelected","Object", "ObjectType") -and $_.Value -match [regex]::Escape($txtBox.Text) })) - - foreach($prop in ($item.PSObject.Properties | Where { $_.Name -notin @("IsSelected","Object", "ObjectType")})) - { - if($prop.Value -match [regex]::Escape($txtBox.Text)) { return $true } - } - $false - } - } - } - - if($dgObjects.ItemsSource -is [System.Windows.Data.ListCollectionView] -and $txtBox.IsFocused -eq $true) - { - $dgObjects.ItemsSource.Filter = $filter - } - - $allObjectsCount = 0 - if($dgObjects.ItemsSource.SourceCollection) - { - $allObjectsCount = $dgObjects.ItemsSource.SourceCollection.Count - } - - $objCount = ($dgObjects.ItemsSource | measure).Count - if($objCount -gt 0) - { - $strAllObjectsInfo = "" - if($allObjectsCount -gt $objCount) - { - $strAllObjectsInfo = " ($($allObjectsCount))" - } - $global:txtEMObjects.Text = "Objects: $objCount$strAllObjectsInfo" - } - else - { - $global:txtEMObjects.Text = "" - } -} -#region Endpoint Security (Intents) functions - -function Start-PreImportEndpointSecurity -{ - param($obj, $objectType) - - @{ - "API"="deviceManagement/templates/$($obj.templateId)/createInstance" - } -} - -function Start-PostListEndpointSecurity -{ - param($objList, $objectType) - - if(-not $script:baseLineTemplates) - { - $script:baseLineTemplates = (Invoke-GraphRequest -Url "/deviceManagement/templates").Value - } - if(-not $script:baseLineTemplates) { return } - - foreach($obj in $objList) - { - if(-not $obj.Object.templateId) { continue } - if($obj.Object.templateId -ne $baseLineTemplate.Id) - { - $baseLineTemplate = $script:baseLineTemplates | Where Id -eq $obj.Object.templateId - } - - if($baseLineTemplate) - { - $obj | Add-Member -MemberType NoteProperty -Name "Type" -Value $baseLineTemplate.displayName - $obj | Add-Member -MemberType NoteProperty -Name "Category" -Value (?: ($baseLineTemplate.templateSubtype -eq "none") $baseLineTemplate.templateType $baseLineTemplate.templateSubtype) - } - - } - $objList -} - -function Start-PostExportEndpointSecurity -{ - param($obj, $objectType, $path) - - $fileName = (Get-GraphObjectName $obj $objectType).Trim('.') - if((Get-SettingValue "AddIDToExportFile") -eq $true -and $obj.Id) - { - $fileName = ($fileName + "_" + $obj.Id) - } - - $settings = Invoke-GraphRequest -Url "$($objectType.API)/$($obj.id)/settings" - $settingsJson = "{ `"settings`": $((ConvertTo-Json $settings.value -Depth 20 ))`n}" - $fileName = "$path\$((Remove-InvalidFileNameChars $fileName))_Settings.json" - Save-GraphObjectToFile $settingsJson $fileName -} - -function Start-PostFileImportEndpointSecurity -{ - param($obj, $objectType, $file) - - $settings = Get-EMSettingsObject $obj $objectType $file - if($settings) - { - Start-GraphPreImport $settings - Invoke-GraphRequest -Url "$($objectType.API)/$($obj.id)/updateSettings" -Body ($settings | ConvertTo-Json -Depth 50) -Method "POST" - } -} - -function Start-PreCopyEndpointSecurity -{ - param($obj, $objectType, $newName) - - $false - - # Intents has a createCopy method. Use "manual" copy to have one standard and making sure Copy works the same as Export/Import - # These objects supports duplicate in the portal - # Keep for reference - # - # $objData = "{`"displayName`":`"$($newName)`"}" - # - #Invoke-GraphRequest -Url "/deviceManagement/intents/$($obj.Id)/createCopy" -Content $objData -HttpMethod "POST" | Out-Null - #$true -} - -function Start-PostCopyEndpointSecurity -{ - param($objCopyFrom, $objNew, $objectType) - - $settings = Invoke-GraphRequest -Url "$($objectType.API)/$($objCopyFrom.id)/settings" -ODataMetadata "Skip" - if($settings) - { - $settingsObj = New-object PSObject @{ "Settings" = $settings.Value } - Invoke-GraphRequest -Url "$($objectType.API)/$($objNew.id)/updateSettings" -Body ($settingsObj | ConvertTo-Json -Depth 20) -Method "POST" - } -} - -function Start-PreUpdateEndpointSecurity -{ - param($obj, $objectType, $curObject, $fromObj) - - if(-not $fromObj.settings) { return } - - $strAPI = "/deviceManagement/intents/$($curObject.Object.id)/updateSettings" - - $curObject = Get-GraphObject $curObject.Object $objectType - - $curValues = @() - foreach($val in $curObject.Object.settings) - { - if($fromObj.settings | Where { $_.definitionId -eq $val.definitionId}) { continue } - - # Set all existing values to null - # Note: This will not remove them from the configured list just set them Not Configured - $curValues += [PSCustomObject]@{ - '@odata.type' = $val.'@odata.type' - definitionId = $val.definitionId - id = $val.id - valueJson = "null" - } - } - - $curValues += $fromObj.settings - - <# - if($curValues.Count -gt 0) - { - $tmpObj = [PSCustomObject]@{ - settings = $curValues - } - $json = ConvertTo-Json $tmpObj -Depth 20 - - # Set all existing values to null - # Note: This will not remove them from the configured list just set them Not Configured - Invoke-GraphRequest -Url $strAPI -Content $json -HttpMethod "POST" | Out-Null - } - #> - - $tmpObj = [PSCustomObject]@{ - settings = $curValues - } - Start-GraphPreImport $tmpObj.settings - - $json = ConvertTo-Json $tmpObj -Depth 20 - Invoke-GraphRequest -Url $strAPI -Content $json -HttpMethod "POST" | Out-Null - - Remove-Property $obj "templateId" -} - -function Start-PostGetEndpointSecurity -{ - param($obj, $objectType) - - Add-EndpointSecurityInfo $obj -} - -function local:Add-EndpointSecurityInfo -{ - param($obj, $baseLineTemplate = $null) - -} -#endregion - -#region - -function Start-PostFileImportDeviceConfiguration -{ - param($obj, $objectType, $importFile) - - if($obj.'@OData.Type' -like "#microsoft.graph.windows10GeneralConfiguration") - { - $tmpObj = Get-GraphObjectFromFile $importFile - - if(($tmpObj.privacyAccessControls | measure).Count -gt 0) - { - $privacyObj = [PSCustomObject]@{ - windowsPrivacyAccessControls = $tmpObj.privacyAccessControls - } - $json = $privacyObj | ConvertTo-Json -Depth 20 - $ret = Invoke-GraphRequest -Url "deviceManagement/deviceConfigurations('$($obj.Id)')/windowsPrivacyAccessControls" -Body $json -Method "POST" - } - } -} - -function Start-PostCopyDeviceConfiguration -{ - param($objCopyFrom, $objNew, $objectType) - - if($objCopyFrom.'@OData.Type' -like "#microsoft.graph.windows10GeneralConfiguration") - { - if(($objCopyFrom.privacyAccessControls | measure).Count -gt 0) - { - $privacyObj = [PSCustomObject]@{ - windowsPrivacyAccessControls = $objCopyFrom.privacyAccessControls - } - $json = $privacyObj | ConvertTo-Json -Depth 20 - Invoke-GraphRequest -Url "deviceManagement/deviceConfigurations('$($objNew.Id)')/windowsPrivacyAccessControls" -Body $json -Method "POST" | Out-null - } - } -} - -function Start-PostGetDeviceConfiguration -{ - param($obj, $objectType) - - if(($obj.Object.omaSettings | measure).Count -gt 0) - { - foreach($omaSetting in ($obj.Object.omaSettings | Where isEncrypted -eq $true)) - { - if($omaSetting.isEncrypted -eq $false) { continue } - - $xmlValue = Invoke-GraphRequest -Url "/deviceManagement/deviceConfigurations/$($obj.Object.Id)/getOmaSettingPlainTextValue(secretReferenceValueId='$($omaSetting.secretReferenceValueId)')" - if($xmlValue.Value) - { - $omaSetting.isEncrypted = $false - $omaSetting.secretReferenceValueId = $null - - if($omaSetting.'@odata.type' -eq "#microsoft.graph.omaSettingStringXml" -or - $omaSetting.'value@odata.type' -eq "#Binary") - { - $Bytes = [System.Text.Encoding]::UTF8.GetBytes($xmlValue.Value) - $omaSetting.value = [Convert]::ToBase64String($bytes) - } - else - { - $omaSetting.value = $xmlValue.Value - } - } - } - } -} - -#endregion - -#region Compliance Policy -function Start-PostExportCompliancePolicies -{ - param($obj, $objectType, $exportPath) - - foreach($scheduledActionsForRule in $obj.scheduledActionsForRule) - { - foreach($scheduledActionConfiguration in $scheduledActionsForRule.scheduledActionConfigurations) - { - foreach($notificationMessageCCGroup in $scheduledActionConfiguration.notificationMessageCCList) - { - Add-GroupMigrationObject $notificationMessageCCGroup - } - } - } -} - -function Start-PreUpdateCompliancePolicies -{ - param($obj, $objectType, $curObject, $fromObj) - - $strAPI = "/deviceManagement/deviceCompliancePolicies/$($curObject.Object.id)/scheduleActionsForRules" - - $tmpObj = [PSCustomObject]@{ - deviceComplianceScheduledActionForRules = $obj.scheduledActionsForRule - } - - $json = ConvertTo-Json $tmpObj -Depth 20 - Invoke-GraphRequest -Url $strAPI -Content $json -HttpMethod "POST" | Out-Null - - Remove-Property $obj "scheduledActionsForRule" -} - -#endregion - -function Start-PostImportComplianceScripts -{ - param($obj, $objectType, $file) - - $endTime = (Get-Date).AddMinutes(2) - - $found = $false - while($endTime -gt (Get-Date)) - { - $tmpObj = Invoke-GraphRequest -Url "$($objectType.API)/$($obj.Id)" -ErrorAction SilentlyContinue - if($tmpObj) { - $found = $true - break - } - Start-Sleep -Seconds 10 - } - - if(-not $found) - { - Write-LogError "Compliance script $($obj.Id) not found after import. Please check the import file." - return - } -} - -#region Intune Branding functions -function Start-PreImportIntuneBranding -{ - param($obj, $objectType) - - $ret = @{} - $global:brandingClone = $null - - if($obj.isDefaultProfile) - { - - # Looks like the ID is the same for all tenants so skip this for now - <# - $defObj = (Invoke-GraphRequest -Url "/deviceManagement/intuneBrandingProfiles?`$filter=isDefaultProfile eq true&`$select=id,displayName").Value[0] - if($defObj) - { - $obj.Id = $defObj.Id - } - #> - - $ret.Add("API",($objectType.API + "/" + $obj.Id)) - $ret.Add("Method","PATCH") # Default profile always exists so update it - - foreach($prop in @("profileName","isDefaultProfile","disableClientTelemetry","profileDescription")) - { - Remove-Property $obj $prop - } - - $ret - } - else - { - # Create new Branding profile does not support images data in the json - # Workaround: (as done by the portal) - # Create a new profile with basic info - # Patch the profile with all the info - - $global:brandingClone = $obj | ConvertTo-Json -Depth 20 | ConvertFrom-Json - - foreach($prop in ($obj.PSObject.Properties | Where {$_.Name -notin @("profileName","profileDescription","roleScopeTagIds")})) #"customPrivacyMessage" - { - Remove-Property $obj $prop.Name - } - } - Remove-Property $obj "Id" -} - -function Start-PostImportIntuneBranding -{ - param($obj, $objectType, $file) - - if($obj.isDefaultProfile -or -not $global:brandingClone) { return } - - foreach($prop in @("Id","isDefaultProfile","customPrivacyMessage","disableClientTelemetry")) #"isDefaultProfile","disableClientTelemetry" - { - Remove-Property $global:brandingClone $prop - } - $json = ($global:brandingClone | ConvertTo-Json -Depth 20) - Invoke-GraphRequest -Url "$($objectType.API)/$($obj.Id)" -Body $json -Method "PATCH" | Out-Null -} - -function Start-PostGetIntuneBranding -{ - param($obj, $objectType) - - foreach($imgType in @("themeColorLogo","lightBackgroundLogo","landingPageCustomizedImage")) - { - Write-LogDebug "Get $imgType for $($obj.Object.profileName)" - $imgJson = Invoke-GraphRequest -Url "$($objectType.API)/$($obj.Object.Id)/$imgType" - if($imgJson.Value) - { - $obj.Object.$imgType = $imgJson - } - } -} - -function Start-PostExportIntuneBranding -{ - param($obj, $objectType, $path) - - foreach($imgType in @("themeColorLogo","lightBackgroundLogo","landingPageCustomizedImage")) - { - if($obj.$imgType.Value) - { - $fileName = "$path\$((Get-GraphObjectName $obj $objectType))_$imgType.jpg" - [IO.File]::WriteAllBytes($fileName, [System.Convert]::FromBase64String($obj.$imgType.Value)) - } - } -} - -function Start-PreDeleteIntuneBranding -{ - param($obj, $objectType) - - if($obj.isDefaultProfile -eq $true) - { - @{ "Delete" = $false } - } -} - -function Start-PreUpdateIntuneBranding -{ - param($obj, $objectType, $curObject, $fromObj) - - if($curObject.Object.isDefaultProfile) - { - foreach($prop in @("profileName","isDefaultProfile","disableClientTelemetry","profileDescription")) - { - Remove-Property $obj $prop - } - } -} - -#endregion - -#region Azure Branding functions -function Start-PreImportAzureBranding -{ - param($obj, $objectType) - - Remove-Property $obj "@odata.Type" - - $ret = @{} - if($obj.Id -eq "0") - { - #$ret.Add("Method","PATCH") # Default profile always exists so update it - #$ret.Add("API",($objectType.API + "/0")) - } - - $ret.Add("API",($objectType.API + "/$($global:Organization.Id)/branding/localizations")) - - # This is NOT wat the documentation says - # Documentation says to use Content-Language - # Any place the documentation states to use Accept-Language is for Get operation - # https://docs.microsoft.com/en-us/graph/api/organizationalbrandingproperties-get?view=graph-rest-beta&tabs=http#request-headers - $ret.Add("AdditionalHeaders", @{ "Accept-Language" = $obj.Id }) - - $ret -} - -function Start-PostListAzureBranding -{ - param($objList, $objectType) - - foreach($obj in $objList) - { - if(-not $obj.Object.id) { continue } - try - { - if($obj.Object.id -eq "0") - { - $language = "Default" - } - else - { - $language = ([cultureinfo]::GetCultureInfo($obj.Object.id)).DisplayName - } - - $obj | Add-Member -MemberType NoteProperty -Name "Language" -Value $language - } - catch{} - } - $objList -} - -#endregion - -#region Script functions -function Add-ScriptExtensions -{ - param($form, $buttonPanel, $index = 0) - - $btnDownload = New-Object System.Windows.Controls.Button - $btnDownload.Content = 'Download' - $btnDownload.Name = 'btnDownload' - $btnDownload.Margin = "0,0,5,0" - $btnDownload.Width = "100" - - $btnDownload.Add_Click({ - Invoke-DownloadScript - }) - - $tmp = $form.FindName($buttonPanel) - if($tmp) - { - $tmp.Children.Insert($index, $btnDownload) - } - - $btnDownload = New-Object System.Windows.Controls.Button - $btnDownload.Content = 'Edit' - $btnDownload.Name = 'btnEdit' - $btnDownload.Margin = "0,0,5,0" - $btnDownload.Width = "100" - - $btnDownload.Add_Click({ - Invoke-EditScript - }) - - $tmp = $form.FindName($buttonPanel) - if($tmp) - { - $tmp.Children.Insert($index, $btnDownload) - } -} - -function Add-ScriptExportExtensions -{ - param($form, $buttonPanel, $index = 0) - - $ctrl = $form.FindName("chkExportScript") - if(-not $ctrl) - { - $xaml = @" - - -"@ - $label = [Windows.Markup.XamlReader]::Parse($xaml) - - $global:chkExportScript = [System.Windows.Controls.CheckBox]::new() - $global:chkExportScript.IsChecked = $true - $global:chkExportScript.VerticalAlignment = "Center" - $global:chkExportScript.Name = "chkExportScript" - - @($label, $global:chkExportScript) - } -} - -function Start-PostExportScripts -{ - param($obj, $objectType, $exportPath) - - if($obj.scriptContent -and $global:chkExportScript.IsChecked) - { - Write-Log "Export script $($obj.FileName)" - $fileName = [IO.Path]::Combine($exportPath, $obj.FileName) - [IO.File]::WriteAllBytes($fileName, ([System.Convert]::FromBase64String($obj.scriptContent))) - } -} - -function Invoke-DownloadScript -{ - if(-not $global:dgObjects.SelectedItem.Object.id) { return } - - $obj = (Get-GraphObject $global:dgObjects.SelectedItem $global:curObjectType).Object - Write-Status "" - - if($obj.scriptContent) - { - Write-Log "Download PowerShell script '$($obj.FileName)' from $($obj.displayName)" - - $dlgSave = New-Object -Typename System.Windows.Forms.SaveFileDialog - $dlgSave.InitialDirectory = Get-SettingValue "IntuneRootFolder" $env:Temp - $dlgSave.FileName = $obj.FileName - if($dlgSave.ShowDialog() -eq [System.Windows.Forms.DialogResult]::OK -and $dlgSave.Filename) - { - # Changed to WriteAllBytes to get rid of BOM characters from Custom Attribute file - [IO.File]::WriteAllBytes($dlgSave.FileName, ([System.Convert]::FromBase64String($obj.scriptContent))) - } - } -} - -function Invoke-EditScript -{ - if(-not $global:dgObjects.SelectedItem.Object.id) { return } - - $obj = (Get-GraphObject $global:dgObjects.SelectedItem $global:curObjectType) - Write-Status "" - if(-not $obj.Object.scriptContent) { return } - $script:currentScriptObject = $obj - - $script:editForm = Get-XamlObject ($global:AppRootFolder + "\Xaml\EditScriptDialog.xaml") - - if(-not $script:editForm) { return } - - Set-XamlProperty $script:editForm "txtEditScriptTitle" "Text" "Edit: $($obj.Object.displayName)" - - $scriptText = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($obj.Object.scriptContent)) - Set-XamlProperty $script:editForm "txtScriptText" "Text" $scriptText - - $script:currentModal = $null - if($global:grdModal.Children.Count -gt 0) - { - $script:currentModal = $global:grdModal.Children[0] - } - - Add-XamlEvent $script:editForm "btnSaveScriptEdit" "add_click" ({ - $scriptText = Get-XamlProperty $script:editForm "txtScriptText" "Text" - $pre = [System.Text.Encoding]::UTF8.GetPreamble() - $utfBOM = [System.Text.Encoding]::UTF8.GetString($pre) - if($scriptText.startsWith($utfBOM)) - { - # Remove UTF8 BOM bytes - $scriptText = $scriptText.Remove(0, $utfBOM.Length) - } - $bytes = [System.Text.Encoding]::UTF8.GetBytes($scriptText) - $encodedText = [Convert]::ToBase64String($bytes) - - if($script:currentScriptObject.Object.scriptContent -ne $encodedText) - { - # Save script - if(([System.Windows.MessageBox]::Show("Are you sure you want to update the script?`n`nObject:`n$($script:currentScriptObject.displayName)", "Update script?", "YesNo", "Warning")) -eq "Yes") - { - Write-Status "Update $($script:currentScriptObject.displayName)" - $obj = $script:currentScriptObject.Object | ConvertTo-Json -Depth 20 | ConvertFrom-Json - $obj.scriptContent = $encodedText - Start-GraphPreImport $obj $script:currentScriptObject.ObjectType - foreach($prop in $script:currentScriptObject.ObjectType.PropertiesToRemoveForUpdate) - { - Remove-Property $obj $prop - } - Remove-Property $obj "Assignments" - Remove-Property $obj "isAssigned" - - $json = ConvertTo-Json $obj -Depth 15 - - $objectUpdated = (Invoke-GraphRequest -Url "$($script:currentScriptObject.ObjectType.API)/$($script:currentScriptObject.Object.Id)" -Content $json -HttpMethod "PATCH") - if(-not $objectUpdated) - { - Write-Log "Failed to update script" 3 - [System.Windows.MessageBox]::Show("Failed to save the script object. See log for more information","Update failed!", "OK", "Error") - } - Write-Status "" - } - } - - $global:grdModal.Children.Clear() - if($script:currentModal) - { - $global:grdModal.Children.Add($script:currentModal) - } - [System.Windows.Forms.Application]::DoEvents() - }) - - Add-XamlEvent $script:editForm "btnCancelScriptEdit" "add_click" ({ - $global:grdModal.Children.Clear() - if($script:currentModal) - { - $global:grdModal.Children.Add($script:currentModal) - } - [System.Windows.Forms.Application]::DoEvents() - }) - - $global:grdModal.Children.Clear() - $script:editForm.SetValue([System.Windows.Controls.Grid]::RowProperty,1) - $script:editForm.SetValue([System.Windows.Controls.Grid]::ColumnProperty,1) - $global:grdModal.Children.Add($script:editForm) | Out-Null - [System.Windows.Forms.Application]::DoEvents() -} - -#endregion - -#region Policy File functions -function Add-PolicyFileExtensions -{ - param($form, $buttonPanel, $index = 0) - - $btnDownload = New-Object System.Windows.Controls.Button - $btnDownload.Content = 'Download' - $btnDownload.Name = 'btnDownload' - $btnDownload.Margin = "0,0,5,0" - $btnDownload.Width = "100" - - $btnDownload.Add_Click({ - Invoke-DownloadPolicyFile - }) - - $tmp = $form.FindName($buttonPanel) - if($tmp) - { - $tmp.Children.Insert($index, $btnDownload) - } - - $btnDownload = New-Object System.Windows.Controls.Button - $btnDownload.Content = 'Edit' - $btnDownload.Name = 'btnEdit' - $btnDownload.Margin = "0,0,5,0" - $btnDownload.Width = "100" - - $btnDownload.Add_Click({ - Invoke-EditPolicyFile - }) - - $tmp = $form.FindName($buttonPanel) - if($tmp) - { - $tmp.Children.Insert($index, $btnDownload) - } -} - -function Add-PolicyFileExportExtensions -{ - param($form, $buttonPanel, $index = 0) - - $ctrl = $form.FindName("chkExportPolicyFile") - if(-not $ctrl) - { - $xaml = @" - - -"@ - $label = [Windows.Markup.XamlReader]::Parse($xaml) - - $global:chkExportPolicyFile = [System.Windows.Controls.CheckBox]::new() - $global:chkExportPolicyFile.IsChecked = $true - $global:chkExportPolicyFile.VerticalAlignment = "Center" - $global:chkExportPolicyFile.Name = "chkExportPolicyFile" - - @($label, $global:chkExportPolicyFile) - } -} - -function Start-PostExportPolicyFile -{ - param($obj, $objectType, $exportPath) - - if($objectType.PolicyFileAttribute -and $obj.$($objectType.PolicyFileAttribute) -and $global:chkExportPolicyFile.IsChecked) - { - Write-Log "Export policy file from attribute $($obj.PolicyFileAttribute)" - $fileNameOut = ?? $obj.FileName "$($obj.PolicyFileAttribute).json" - $fileName = [IO.Path]::Combine($exportPath, $fileNameOut) - [IO.File]::WriteAllBytes($fileName, ([System.Convert]::FromBase64String($obj.$($objectType.PolicyFileAttribute)))) - } -} - -function Invoke-DownloadPolicyFile -{ - if(-not $global:dgObjects.SelectedItem.Object.id) { return } - - $obj = (Get-GraphObject $global:dgObjects.SelectedItem $global:curObjectType).Object - Write-Status "" - - if($global:curObjectType.PolicyFileAttribute -and $obj.$($global:curObjectType.PolicyFileAttribute)) - { - $fileNameOut = ?? $obj.FileName "$($obj.PolicyFileAttribute).json" - Write-Log "Download policy file '$($fileNameOut)' from $($obj.displayName)" - - $dlgSave = New-Object -Typename System.Windows.Forms.SaveFileDialog - $dlgSave.InitialDirectory = Get-SettingValue "IntuneRootFolder" $env:Temp - $dlgSave.FileName = $fileNameOut - if($dlgSave.ShowDialog() -eq [System.Windows.Forms.DialogResult]::OK -and $dlgSave.Filename) - { - # Changed to WriteAllBytes to get rid of BOM characters from Custom Attribute file - [IO.File]::WriteAllBytes($dlgSave.FileName, ([System.Convert]::FromBase64String($obj.$($global:curObjectType.PolicyFileAttribute)))) - } - } -} - -function Invoke-EditPolicyFile -{ - if(-not $global:dgObjects.SelectedItem.Object.id) { return } - - $obj = (Get-GraphObject $global:dgObjects.SelectedItem $global:curObjectType) - Write-Status "" - if(-not $global:curObjectType.PolicyFileAttribute -or -not $obj.Object.$($global:curObjectType.PolicyFileAttribute)) { return } - $script:currentScriptObject = $obj - - $script:editForm = Get-XamlObject ($global:AppRootFolder + "\Xaml\EditScriptDialog.xaml") - - if(-not $script:editForm) { return } - - Set-XamlProperty $script:editForm "txtEditScriptTitle" "Text" "Edit: $($obj.Object.displayName)" - - $scriptText = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($obj.Object.$($global:curObjectType.PolicyFileAttribute))) - Set-XamlProperty $script:editForm "txtScriptText" "Text" $scriptText - - $script:currentModal = $null - if($global:grdModal.Children.Count -gt 0) - { - $script:currentModal = $global:grdModal.Children[0] - } - - Add-XamlEvent $script:editForm "btnSaveScriptEdit" "add_click" ({ - $scriptText = Get-XamlProperty $script:editForm "txtScriptText" "Text" - $pre = [System.Text.Encoding]::UTF8.GetPreamble() - $utfBOM = [System.Text.Encoding]::UTF8.GetString($pre) - if($scriptText.startsWith($utfBOM)) - { - # Remove UTF8 BOM bytes - $scriptText = $scriptText.Remove(0, $utfBOM.Length) - } - $bytes = [System.Text.Encoding]::UTF8.GetBytes($scriptText) - $encodedText = [Convert]::ToBase64String($bytes) - - if($script:currentScriptObject.Object.scriptContent -ne $encodedText) - { - # Save script - if(([System.Windows.MessageBox]::Show("Are you sure you want to update the $($global:curObjectType.PolicyFileAttribute) attribute?`n`nObject:`n$($script:currentScriptObject.displayName)", "Update script?", "YesNo", "Warning")) -eq "Yes") - { - Write-Status "Update $($script:currentScriptObject.displayName)" - $obj = $script:currentScriptObject.Object | ConvertTo-Json -Depth 20 | ConvertFrom-Json - $obj.$($global:curObjectType.PolicyFileAttribute) = $encodedText - Start-GraphPreImport $obj $script:currentScriptObject.ObjectType - foreach($prop in $script:currentScriptObject.ObjectType.PropertiesToRemoveForUpdate) - { - Remove-Property $obj $prop - } - Remove-Property $obj "Assignments" - Remove-Property $obj "isAssigned" - - $json = ConvertTo-Json $obj -Depth 15 - - $objectUpdated = (Invoke-GraphRequest -Url "$($script:currentScriptObject.ObjectType.API)/$($script:currentScriptObject.Object.Id)" -Content $json -HttpMethod "PATCH") - if(-not $objectUpdated) - { - Write-Log "Failed to update script" 3 - [System.Windows.MessageBox]::Show("Failed to save the policy. See log for more information","Update failed!", "OK", "Error") - } - Write-Status "" - } - } - - $global:grdModal.Children.Clear() - if($script:currentModal) - { - $global:grdModal.Children.Add($script:currentModal) - } - [System.Windows.Forms.Application]::DoEvents() - }) - - Add-XamlEvent $script:editForm "btnCancelScriptEdit" "add_click" ({ - $global:grdModal.Children.Clear() - if($script:currentModal) - { - $global:grdModal.Children.Add($script:currentModal) - } - [System.Windows.Forms.Application]::DoEvents() - }) - - $global:grdModal.Children.Clear() - $script:editForm.SetValue([System.Windows.Controls.Grid]::RowProperty,1) - $script:editForm.SetValue([System.Windows.Controls.Grid]::ColumnProperty,1) - $global:grdModal.Children.Add($script:editForm) | Out-Null - [System.Windows.Forms.Application]::DoEvents() -} - -#endregion - -#region Terms and Conditions -function Start-PostExportTermsAndConditions -{ - param($obj, $objectType, $path) - - Add-EMAssignmentsToExportFile $obj $objectType $path -} - -function Start-PreImportAssignmentsTermsAndConditions -{ - param($obj, $objectType, $file, $assignments) - - Add-EMAssignmentsToObject $obj $objectType $file $assignments -} -#endregion - -#region App Protection functions - -function Start-GetAppProtection -{ - param($obj, $objectType) - - if(-not $obj."@odata.type") { return } - - Get-GraphMetaData - - $objectClass = $null - if($global:metaDataXML) - { - try - { - $tmp = $obj."@odata.type".Split('.')[-1] - $objectClass = Get-GraphObjectClassName $tmp - } - catch - { - - } - $expand = $null - if($objectClass -eq "windowsInformationProtectionPolicies") - { - $expand = "?`$expand=protectedAppLockerFiles,exemptAppLockerFiles" - } - - if($objectClass) - { - @{"API"="/deviceAppManagement/$objectClass/$($obj.Id)$expand"} - } - } -} - -function Start-PostListAppProtection -{ - param($objList, $objectType) - - # App Configurations for Managed Apps are included in App Protections e.g. the /deviceAppManagement/managedAppPolicies API - # For some reason, the $filter option is not supported to filter out these objects - # e.g. not isof(...) to excluded the type, not startsWith(id, 'A_') to exlude based on Id - # These filters generates a request error so filter them out manually in this function instead - # The portal is probably doing the same thing since these are included in the return but not in the UI - $objList | Where { $_.Object.'@OData.Type' -ne '#microsoft.graph.targetedManagedAppConfiguration' } -} - -function Start-PreImportAppProtection -{ - param($obj, $objectType) - - if(($obj.Apps | measure).Count -gt 0) - { - $global:ImportObjectInfo = @{ Apps=$obj.Apps } - } - else - { - $global:ImportObjectInfo = $null - } - - $global:ImportObjectClass = $null - if($obj."@odata.type") - { - try - { - $global:ImportObjectClass = Get-GraphObjectClassName ($obj."@odata.type".Split('.')[-1]) - } - catch {} - } - - Remove-Property $obj "apps" - Remove-Property $obj "apps@odata.context" - - try - { - $tmp = $obj."@odata.type".Split('.')[-1] - $objectClass = Get-GraphObjectClassName $tmp - if($objectClass) - { - @{"API"="/deviceAppManagement/$objectClass"} - } - } - catch {} -} - -function Start-PostImportAppProtection -{ - param($obj, $objectType, $file) - - if($global:ImportObjectInfo.Apps) - { - # No "@odata.type" on the created object so reload new object - #$newObject = (Invoke-GraphRequest "$($objectType.API)?`$filter=id eq '$($obj.Id)'").Value - $newObject = Invoke-GraphRequest "$($objectType.API)/$($obj.Id)" - if($newObject) - { - try - { - $tmp = $newObject."@odata.type".Split('.')[-1] - $objectClass = Get-GraphObjectClassName $tmp - - $apps = [PSCustomObject]@{ - appGroupType = $obj.appGroupType - apps = @($global:ImportObjectInfo.Apps) - } - $json = $apps | ConvertTo-Json -Depth 20 - - Invoke-GraphRequest -Url "/deviceAppManagement/$objectClass/$($obj.Id)/targetApps" -Content $json -HttpMethod POST | Out-Null - } - catch {} - } - } - $global:ImportObjectInfo = $null -} - -function Start-PreImportAssignmentsAppProtection -{ - param($obj, $objectType, $file, $assignments) - - if($global:ImportObjectClass) - { - @{"API"="/deviceAppManagement/$($global:ImportObjectClass)/$($obj.Id)/assign"} - } -} - -function Start-PreUpdateAppConfigurationApp -{ - param($obj, $objectType, $curObject, $fromObj) - - if($obj.Apps) - { - try - { - Write-Log "Update App Configuruation Apps" - - $apps = [PSCustomObject]@{ - appGroupType = $obj.appGroupType - apps = @($obj.Apps) - } - $json = $apps | ConvertTo-Json -Depth 20 - $objectClass = 'targetedManagedAppConfigurations' - - Invoke-GraphRequest -Url "/deviceAppManagement/$objectClass/$($curObject.Object.Id)/targetApps" -Content $json -HttpMethod POST | Out-Null - } - catch {} - } - - Remove-Property $obj "apps" -} - -function Start-PreUpdateAppProtection -{ - param($obj, $objectType, $curObject, $fromObj) - - if($curObject.Object.'@OData.Type' -eq "#microsoft.graph.windowsInformationProtectionPolicy") - { - $api = "/deviceAppManagement/windowsInformationProtectionPolicies/$($curObject.Object.Id)" - } - elseif($curObject.Object.'@OData.Type' -eq "#microsoft.graph.mdmWindowsInformationProtectionPolicy") - { - $api = "/deviceAppManagement/mdmWindowsInformationProtectionPolicies/$($curObject.Object.Id)" - } - elseif($curObject.Object.'@OData.Type' -eq "#microsoft.graph.iosManagedAppProtection") - { - $api = "/deviceAppManagement/iosManagedAppProtections/$($curObject.Object.Id)" - } - elseif($curObject.Object.'@OData.Type' -eq "#microsoft.graph.androidManagedAppProtection") - { - $api = "/deviceAppManagement/androidManagedAppProtections/$($curObject.Object.Id)" - } - else - { - return (Start-PreUpdateAppConfigurationApp $obj $objectType $curObject $fromObj) - } - - if($obj.Apps) - { - try - { - Write-Log "Update App Protection Apps" - - $apps = [PSCustomObject]@{ - appGroupType = $obj.appGroupType - apps = @($obj.Apps) - } - $json = $apps | ConvertTo-Json -Depth 20 - - Invoke-GraphRequest -Url "$api/targetApps" -Content $json -HttpMethod POST | Out-Null - } - catch {} - - Remove-Property $obj "apps" - } - - @{ "API" = $api } - -} -#endregion - -#region App Configuration -function Start-PostExportAppConfiguration -{ - param($obj, $objectType, $path) - - #Add-EMAssignmentsToExportFile $obj $objectType $path - - Write-Log "Export app config for $($objectType.Id) with OData.Type: $($obj.'@OData.Type')" - - if($obj.'@OData.Type' -eq "#microsoft.graph.androidManagedAppProtection" -or - $obj.'@OData.Type' -eq "#microsoft.graph.androidForWorkMobileAppConfiguration" -or - $obj.'@OData.Type' -eq "#microsoft.graph.androidManagedStoreAppConfiguration" -or - $obj.'@OData.Type' -eq "#microsoft.graph.iosMobileAppConfiguration") - { - $fileName = (Get-GraphObjectName $obj $objectType).Trim('.') - if((Get-SettingValue "AddIDToExportFile") -eq $true -and $obj.Id) - { - $fileName = ($fileName + "_" + $obj.Id) - } - $tmpObj = $null - $fileName = "$path\$((Remove-InvalidFileNameChars $fileName)).json" - if([IO.File]::Exists($fileName)) - { - $tmpObj = Get-GraphObjectFromFile $fileName - } - else - { - Write-Log "File not found: $fileName. Could not add App names." 3 - } - - if(($tmpObj.targetedMobileApps | measure).Count -gt 0) - { - Write-Log "Add target apps info" - $targetedApps = @() - foreach($appId in $tmpObj.targetedMobileApps) - { - $appObj = Invoke-GraphRequest -Url "/deviceAppManagement/mobileApps/$($appId)" #?`select=id,displayName" -ODataMetadata "Minimal" - if($appObj) - { - Write-Log "Add target app info $($appObj.displayName) ($($appObj.Id)) of type $($appObj.'@OData.Type')" - $targetedApps += $appObj.displayName + '|!|' + $appObj.Id + '|!|' + $appObj.'@OData.Type' - } - } - - if($targetedApps.Count -gt 0) - { - Write-Log "Add CustomRefTargetedApps property" - $tmpObj | Add-Member -MemberType NoteProperty -Name "#CustomRefTargetedApps" -Value ($targetedApps -join "|*|") - Write-Log "Save file $fileName" - Save-GraphObjectToFile $tmpObj $fileName - } - } - else - { - Write-Log "No target apps found" 2 - } - } -} - -function Start-PreFilesImportAppConfiguration -{ - param($objectType, $filesToImport) - - $targetedAppsObjects = $filesToImport | Where { $null -ne $_.Object."#CustomRefTargetedApps" } - - if(($targetedAppsObjects | measure).Count -gt 0) - { - Write-Log "Policies with Targeted Apps detected" - foreach($fileObject in $targetedAppsObjects) - { - Add-AppConfigurationTargets $objectType $fileObject - } - } - $filesToImport -} - -function local:Add-AppConfigurationTargets -{ - param($obj, $fileObj) - - if($fileObj.Object."#CustomRefTargetedApps" -and $fileObj.Object.targetedMobileApps) - { - Write-Log "Adding app target for $($fileObj.Object.displayName)" - - $targetedAppsInfo = $fileObj.Object."#CustomRefTargetedApps" - - $translatedTargetedApps = @() - - if($targetedAppsInfo) - { - foreach($targetedApp in ($targetedAppsInfo -split "[|][*][|]")) - { - $appName, $appId, $appType = $targetedApp -split "[|][!][|]" - if(-not $appName -or -not $appId) - { - Write-Log "App Name and Id is missing in string: $targetedApp" 2 - continue - } - $tmpApps = (Invoke-GraphRequest -Url "/deviceAppManagement/mobileApps?`$filter=displayName eq '$appName'").value - if(-not $tmpApps) - { - Write-Log "No application found with name $appName. $appId will not be translated and added to target list" 2 - continue - } - - Write-Log "Found $(($tmpApps | measure).Count) applications" 2 - foreach ($tmpApp in $tmpApps) { - Write-Log "Found '$($tmpApp.displayName)' ($($tmpApp.id)) of type $($($tmpApp.'@OData.Type'))" - } - - $tmpApp = $tmpApps | Where-Object '@OData.Type' -eq $appType - if(-not $tmpApp) - { - Write-Log "No $appName application found of type $appType. $appId will not be translated and added to target list" 2 - } - elseif(($tmpApp | measure).Count -gt 1) { - Write-Log "$(($tmpApp | measure).Count) applications found with name '$appName' of type $appType. $appId will not be translated and added to target list" 2 - } - else { - Write-Log "Found '$appName' with id $($tmpApp.Id) ($appType)" - $translatedTargetedApps += $tmpApp.Id - } - } - - if($translatedTargetedApps.Count -gt 0) { - Write-Log "Updating translated targeted apps" - $fileObj.Object.targetedMobileApps = $translatedTargetedApps - } - else { - Write-Log "Could not find targeted apps in the evnironment. Verify that they are added. Policy import might fail" 3 - } - } - } -} - -function Start-PreImportAssignmentsAppConfiguration -{ - param($obj, $objectType, $file, $assignments) - - @{"API"="/deviceAppManagement/mobileAppConfigurations/$($obj.Id)/microsoft.graph.managedDeviceMobileAppConfiguration/assign"} -} -#endregon - -#region Applications - -function Start-PostCopyApplication -{ - param($objCopyFrom, $objNew, $objectType) - - Start-ImportApp $objNew - Start-AddInstallScripts $objNew $objCopyFrom - Write-Status "" -} - -function Start-PostFileImportApplication -{ - param($obj, $objectType, $file) - - $tmpObj = Get-GraphObjectFromFile $file - - if(-not ($obj.PSObject.Properties | Where Name -eq '@odata.type')) - { - # Add @odata.type property if it is missing. Required by app package import - $obj | Add-Member -MemberType NoteProperty -Name '@odata.type' -Value $objectType.'@odata.type' - } - - $fi = [IO.FileInfo]$file - $tmpFilName = $fi.DirectoryName + "\" + $obj.FileName - - if([IO.File]::Exists($tmpFilName) -eq $false) - { - $tmpFilName = $null - } - - Start-ImportApp $obj $tmpFilName - Start-AddInstallScripts $obj $tmpObj -} - -function local:Start-ImportApp -{ - param($obj, $packageFile = $null) - - if(-not $obj.'@odata.type') { return } - - if($null -eq $packageFile) - { - $pkgPath = Get-SettingValue "EMIntuneAppPackages" - - if(-not $pkgPath -or [IO.Directory]::Exists($pkgPath) -eq $false) - { - Write-LogDebug "Package source directory is either missing or does not exist" 2 - return - } - - $packageFile = "$($pkgPath)\$($obj.fileName)" - } - $fi = [IO.FileInfo]$packageFile - - if($fi.Exists -eq $false) - { - Write-LogDebug "Package source file $($fi.FullName) not found" 2 - return - } - - Write-Status "Import appliction package file $($fi.FullName)" - Write-Log "Import application file '$($($fi.FullName))' for $($obj.displayName)" - - $appType = $obj.'@odata.type'.Trim('#') - - if($appType -eq "microsoft.graph.win32LobApp") - { - $fileEncryptionInfo = Copy-Win32LOBPackage $packageFile $obj - } - elseif($appType -eq "microsoft.graph.windowsMobileMSI") - { - $fileEncryptionInfo = Copy-MSILOB $packageFile $obj - } - elseif($appType -eq "microsoft.graph.windowsUniversalAppX") - { - $fileEncryptionInfo = Copy-MSIXLOB $packageFile $obj - } - elseif($appType -eq "microsoft.graph.iosLOBApp") - { - $fileEncryptionInfo = Copy-iOSLOB $packageFile $obj - } - elseif($appType -eq "microsoft.graph.androidLOBApp") - { - $fileEncryptionInfo = Copy-AndroidLOB $packageFile $obj - } - else - { - Write-Log "Unsupported application type $appType. File will not be uploaded" 2 - } - - if((Get-SettingValue "EMSaveEncryptionFile") -eq $true) - { - if($fileEncryptionInfo) - { - $jsonEncryptionInfo = $fileEncryptionInfo | ConvertTo-Json -Depth 10 - - $pkgPath = Get-SettingValue "EMIntuneAppDownloadFolder" (Get-SettingValue "EMIntuneAppPackages") - if($pkgPath -and [IO.Directory]::Exists($pkgPath)) - { - $obj = Invoke-GraphRequest -Url "/deviceAppManagement/mobileApps/$($obj.id)" -ODataMetadata "Minimal" - $fullPath = $pkgPath + "\$($obj.displayName)_$($obj.id)_$($obj.committedContentVersion).json" - $jsonEncryptionInfo | Out-File -FilePath $fullPath -Force -Encoding utf8 - } - } - } -} - -function local:Start-AddInstallScripts -{ - param($obj, $fromAppObj) - - if($fromAppObj -and ($fromAppObj.activeInstallScript."#ScriptInfo" -or $fromAppObj.activeUninstallScript."#ScriptInfo")) - { - Write-Log "Importing scripts for $($obj.displayName)" - - $scriptsAdded = $false - $jsonData = @{} - $jsonData."@odata.type" = "#microsoft.graph.win32LobApp" - $jsonData."committedContentVersion" = "1" - - foreach ($scriptType in @('activeInstallScript','activeUninstallScript')) { - $scriptInfo = $fromAppObj.$scriptType.'#ScriptInfo' - if (-not $scriptInfo) { continue } - - Write-Log "Add $($scriptType -replace '^active','') script: $($scriptInfo.displayName)" - - $json = [ordered]@{ - '@odata.type' = $scriptInfo.'@odata.type' - displayName = $scriptInfo.displayName - enforceSignatureCheck = $scriptInfo.enforceSignatureCheck - runAs32Bit = $scriptInfo.runAs32Bit - content = $scriptInfo.content - } | ConvertTo-Json -Depth 10 -Compress - - $scriptObject = Invoke-GraphRequest -Url "deviceAppManagement/mobileApps/$($obj.id)/microsoft.graph.win32LobApp/contentVersions/1/scripts" -Method POST -Content $json - - if ($scriptObject) { - $jsonData.$scriptType = @{ targetId = $scriptObject.Id } - $scriptsAdded = $true - } - } - - $i = 0 - while($true) - { - $scripts = Invoke-GraphRequest -Url "deviceAppManagement/mobileApps/$($obj.id)/microsoft.graph.win32LobApp/contentVersions/1/scripts" - if(-not $scripts) - { - Write-Log "Failed to retrieve scripts for app after adding. Skipping Install/Uninstall script config." 2 - return - } - - if(($scripts.value.state | Select -Unique) -eq "commitSuccess") - { - Write-Log "Scripts added successfully" - break - } - if($i -ge 12) - { - Write-Log "Install/Uninstall scripts are still not in pending state after waiting for 1 minute." 3 - return - } - - Write-Log "Waiting for scripts to be added..." - Start-Sleep -Seconds 5 - $i++ - } - - if($scriptsAdded) - { - Write-Log "Add script info to app" - $json = ConvertTo-Json $jsonData -Depth 10 - $status = Invoke-GraphRequest -Url "deviceAppManagement/mobileApps/$($obj.id)" -Method PATCH -Body $json - if($status -eq $true) - { - Write-Log "Install/Uninstall script info updated successfully" - } - else - { - Write-Log "Failed to update Install/Uninstall script info" 2 - } - } - } -} - -function Start-PreUpdateApplication -{ - param($obj, $objectType, $curObject, $fromObj) - - if($curObject.Object.'@OData.type' -eq "#microsoft.graph.windowsMobileMSI") - { - Remove-Property $obj "useDeviceContext" - } - elseif($curObject.Object.'@OData.type' -eq "#microsoft.graph.officeSuiteApp") - { - Remove-Property $obj "officeConfigurationXml" - Remove-Property $obj "officePlatformArchitecture" - Remove-Property $obj "developer" - Remove-Property $obj "owner" - Remove-Property $obj "publisher" - } - - Remove-Property $obj "appStoreUrl" -} - -function Start-PreImportCommandApplication -{ - param($obj, $objectType, $file, $assignments) - - if($obj.'@OData.Type' -in @('#microsoft.graph.microsoftStoreForBusinessApp','#microsoft.graph.androidStoreApp')) - { - Write-Log "App type '$($obj.'@OData.Type')' not supported for import" 2 - @{ "Import" = $false } - } - - if($obj.'@OData.Type' -eq '#microsoft.graph.officeSuiteApp') - { - if($obj.officeSuiteAppDefaultFileFormat -eq "notConfigured") - { - $obj.officeSuiteAppDefaultFileFormat = "officeOpenXMLFormat" - } - } - - if($obj.activeInstallScript) { $obj.activeInstallScript = $null } - if($obj.activeUninstallScript) { $obj.activeUninstallScript = $null } -} - -function Add-DetailExtensionApplications -{ - param($form, $buttonPanel, $index = 0) - - $btnUpload = New-Object System.Windows.Controls.Button - $btnUpload.Content = 'Upload' - $btnUpload.Name = 'btnUploadAppfile' - $btnUpload.Margin = "0,0,5,0" - $btnUpload.Width = "100" - - $btnUpload.Add_Click({ - if($global:dgObjects.SelectedItem.Object.publishingState -ne "notPublished") - { - # Only allow upload of not published apps - # Use portal to replace app file... - if(([System.Windows.MessageBox]::Show("Are you sure you want to upload a new file for the app?`n`nApplication:`n$($global:dgObjects.SelectedItem.Object.displayName)", "Update app file?", "YesNo", "Warning")) -ne "Yes") - { - return - } - } - - $pkgPath = Get-SettingValue "EMIntuneAppPackages" - - $of = [System.Windows.Forms.OpenFileDialog]::new() - $of.FileName = $global:dgObjects.SelectedItem.Object.fileName - $of.DefaultExt = "*.intunewin" - $of.Filter = "Intune Win32 (*.intunewin)|*.*" - $of.Multiselect = $false - - if($pkgPath -and [IO.Directory]::Exists($pkgPath)) - { - $of.InitialDirectory = $pkgPath - } - - if($of.ShowDialog() -eq "OK") - { - Write-Status "Import $($global:dgObjects.SelectedItem.Object.displayName) file" - Start-ImportApp $global:dgObjects.SelectedItem.Object $of.FileName - Write-Status "" - } - }) - - $tmp = $form.FindName($buttonPanel) - if($tmp) - { - $tmp.Children.Insert($index, $btnUpload) - } - - $btnDownload = New-Object System.Windows.Controls.Button - $btnDownload.Content = 'Download' - $btnDownload.Name = 'btnDownloadAppfile' - $btnDownload.Margin = "0,0,5,0" - $btnDownload.Width = "100" - - $btnDownload.Add_Click({ - Write-Status "Download file" - $obj = $global:dgObjects.SelectedItem.Object - #$obj = Invoke-GraphRequest -Url "/deviceAppManagement/mobileApps/$($obj.id)" - - $pkgPath = Get-SettingValue "EMIntuneAppDownloadFolder" (Get-SettingValue "EMIntuneAppPackages") - - $dlgSave = [System.Windows.Forms.SaveFileDialog]::new() - $dlgSave.InitialDirectory = $pkgPath - $dlgSave.FileName = ($obj.FileName + ".encrypted") - $dlgSave.DefaultExt = "*.encrypted" - $dlgSave.Filter = "Encrypted intunewin (*.encrypted)|*.encrypted|All files (*.*)|*.*" - - if($dlgSave.ShowDialog() -eq [System.Windows.Forms.DialogResult]::OK -and $dlgSave.Filename) - { - $contentFileObj = Start-DownloadAppContent $obj $dlgSave.FileName - - if([IO.File]::Exists($dlgSave.FileName)) - { - $fullPath = Find-AppEncryptionFile $obj $contentFileObj $pkgPath - if([IO.File]::Exists($fullPath) -eq $false) - { - if(([System.Windows.MessageBox]::Show("Could not find decryption file for $($obj.displayName)`nApp Id: $($obj.id)`nContent version $($obj.committedContentVersion)`n`nDo you want to browse for the file?", "Encryption file not found", "YesNo", "Warning")) -eq "Yes") - { - $of = [System.Windows.Forms.OpenFileDialog]::new() - $of.InitialDirectory = $pkgPath - $of.DefaultExt = "*.json" - $of.Filter = "Json (*.json)|*.json" - $of.Multiselect = $false - - if($of.ShowDialog() -eq "OK") - { - $fullPath = $of.FileName - } - } - } - - if([IO.File]::Exists($fullPath)) - { - Write-Status "Decrypting file" - $encryptionInfo = ConvertFrom-Json (Get-Content -Path $fullPath -Raw) - if($encryptionInfo.fileEncryptionInfo) - { - $encryptionInfo = $encryptionInfo.fileEncryptionInfo - } - $destination = $pkgPath + "\$($obj.FileName)" - Start-DecryptFile $dlgSave.Filename $destination $encryptionInfo.encryptionKey $encryptionInfo.initializationVector - try { [IO.File]::Delete($dlgSave.Filename) } - catch { - Write-LogError "Failed to delete exported encrypted file" $_.Exception - } - } - else - { - Write-Log "Decryption file for $($obj.displayName) not found. Skipping decryption" 2 - } - } - } - - Write-Status "" - }) - - $tmp = $form.FindName($buttonPanel) - if($tmp) - { - $tmp.Children.Insert($index, $btnDownload) - } -} - -function Find-AppEncryptionFile -{ - param($obj, $contentFileObj, $rootFolders) - - $search = @() - $search += "$($obj.displayName)_$($obj.id)_$($obj.committedContentVersion)" - $search += "$([IO.Path]::GetFileNameWithoutExtension($obj.fileName))_$($contentFileObj.size)" - $search += "$($obj.displayName)_$($contentFileObj.size)" - - foreach($rootFolder in $rootFolders) - { - foreach($searchName in $search) - { - $fullName = ($rootFolder + "\$($searchName).json") - if([IO.File]::Exists($fullName)) - { - return $fullName - } - } - } -} - -function Start-PreImportAssignmentsApplications -{ - param($obj, $objectType, $file, $assignments) - - if($obj.'@odata.type' -eq "#microsoft.graph.windowsMicrosoftEdgeApp") - { - foreach($assignment in $assignments) - { - Remove-Property $assignment.target "deviceAndAppManagementAssignmentFilterId" - Remove-Property $assignment.target "deviceAndAppManagementAssignmentFilterType" - } - @{"Assignments"=$assignments} - } - elseif($obj.'@odata.type' -eq "#microsoft.graph.winGetApp") - { - Write-LogDebug "Wait for app to be published" - $i = 2 - Start-Sleep -s ($i) - $x = 0 - while($x -lt 10) - { - ###!!! - $appInfo = Invoke-GraphRequest -Url "$($objectType.API)/$($obj.id)" -ODataMetadata "skip" - if($appInfo.publishingState -eq "Published") - { - Write-LogDebug "Application $($obj.displayName) is published" - return - } - Start-Sleep -s ($i) - $x++ - if($x -ge 5) { $i++ } - } - - Write-Log "Application '$($obj.displayName)' is not published. Skipping assignment" 2 - @{"Import"=$false} - } -} - -function Start-PreDeleteApplications -{ - param($obj, $objectType) - - if($obj.'@odata.type' -eq "#microsoft.graph.microsoftStoreForBusinessApp") - { - # Don't delete Microsoft Store for Business Apps - @{ "Delete" = $false } - } -} - -function Start-PostExportApplications -{ - param($obj, $objectType, $path) - - if($global:chkExportScript.IsChecked) - { - $fileName = Get-GraphObjectFile $obj $objectType - $fi = [IO.FileInfo]"$path\$fileName" - - try - { - foreach($rule in ($obj.detectionRules | Where '@OData.Type' -eq "#microsoft.graph.win32LobAppPowerShellScriptDetection")) - { - if($rule.ScriptContent) - { - [IO.File]::WriteAllBytes(("$path\$($fi.BaseName)_DetectionScript.ps1"), ([System.Convert]::FromBase64String($rule.ScriptContent))) - - } - } - - foreach($rule in $obj.requirementRules) - { - if($rule.'@OData.Type' -eq "#microsoft.graph.win32LobAppPowerShellScriptRequirement") - { - if($rule.ScriptContent) - { - [IO.File]::WriteAllBytes(("$path\$($fi.BaseName)_RequirementScript.ps1"), ([System.Convert]::FromBase64String($rule.ScriptContent))) - } - } - } - - if($obj.activeInstallScript.'#ScriptInfo'.displayName) - { - [IO.File]::WriteAllBytes(("$path\$($fi.BaseName)_$($obj.activeInstallScript.'#ScriptInfo'.displayName)"), ([System.Convert]::FromBase64String($obj.activeInstallScript.'#ScriptInfo'.content))) - } - - if($obj.activeUninstallScript.'#ScriptInfo'.displayName) - { - [IO.File]::WriteAllBytes(("$path\$($fi.BaseName)_$($obj.activeUninstallScript.'#ScriptInfo'.displayName)"), ([System.Convert]::FromBase64String($obj.activeUninstallScript.'#ScriptInfo'.content))) - } - } - catch - { - Write-LogError "Failed to export scripts" $_.Exception - } - } - - Save-Setting "Intune" "ExportAppFile" $global:chkExportApplicationFile.IsChecked - if($global:chkExportApplicationFile.IsChecked) - { - $encryptionSource = Get-SettingValue "EMIntuneAppDownloadFolder" (Get-SettingValue "EMIntuneAppPackages") - $pkgPath = $path - - if($pkgPath) - { - Write-Status "Download file" - - $exportFile = $pkgPath + "\$($obj.FileName).encrypted" - $contentFileObj = Start-DownloadAppContent $obj $exportFile -GetContentFileInfoOnly - $encryptionFile = Find-AppEncryptionFile $obj $contentFileObj $encryptionSource - if($encryptionFile -and [IO.File]::Exists($encryptionFile)) - { - Start-DownloadFile $contentFileObj.azureStorageUri $exportFile - - if([IO.File]::Exists($exportFile)) - { - Write-Status "Decrypting file" - $encryptionInfo = ConvertFrom-Json (Get-Content -Path $encryptionFile -Raw) - if($encryptionInfo.fileEncryptionInfo) - { - $encryptionInfo = $encryptionInfo.fileEncryptionInfo - } - $destination = $pkgPath + "\$($obj.FileName)" - Start-DecryptFile $exportFile $destination $encryptionInfo.encryptionKey $encryptionInfo.initializationVector - } - - try { [IO.File]::Delete($exportFile) } - catch { - Write-LogError "Failed to delete exported encrypted file" $_.Exception - } - } - else - { - Write-Log "Cound not find encryption file" - } - } - } -} - -function Start-PostListApplications -{ - param($objList, $objectType) - - foreach($obj in ($objList | Where { $_.Object."@OData.Type" -eq "#microsoft.graph.winGetApp"})) - { - if($obj.Object.packageIdentifier -like "9*") - { - $installerType = "UWP" - } - elseif($obj.Object.packageIdentifier -like "X*") - { - $installerType = "Win32" - } - else - { - $objName = Get-GraphObjectName $obj.Object $objectType - Write-Log "Unknown package identifier for app $($objName): $($obj.Object.packageIdentifier)" 2 - $installerType = "Unknown" - } - $obj.Object | Add-Member -MemberType NoteProperty -Name "InstallerType" -Value $installerType - } - $objList -} - -function Add-ScriptExportApplications -{ - param($form, $buttonPanel, $index = 0) - - Add-ScriptExportExtensions $form $buttonPanel $index - - $ctrl = $form.FindName("chkExportApplicationFile") - if(-not $ctrl) - { - $xaml = @" - - -"@ - $label = [Windows.Markup.XamlReader]::Parse($xaml) - - $global:chkExportApplicationFile = [System.Windows.Controls.CheckBox]::new() - $global:chkExportApplicationFile.IsChecked = ((Get-Setting "Intune" "ExportAppFile" "false") -eq "true") - $global:chkExportApplicationFile.VerticalAlignment = "Center" - $global:chkExportApplicationFile.Name = "chkExportApplicationFile" - - @($label, $global:chkExportApplicationFile) - } -} - -function Start-PostGetApplications { - param($obj, $objectType) - - if($obj.Object.dependentAppCount -is [Int] -and ($obj.Object.dependentAppCount -gt 0 -or $obj.Object.supersededAppCount -gt 0)) { - $relationships = (Invoke-GraphRequest -Url "/deviceAppManagement/mobileApps/$($obj.Id)/relationships?`$filter=targetType%20eq%20microsoft.graph.mobileAppRelationshipType%27child%27").value - $dependencyApps = @() - $supersededApps = @() - foreach ($rel in $relationships) { - if ($rel."@odata.type" -eq "#microsoft.graph.mobileAppDependency") { - $dependencyApps += "$($rel.targetDisplayName)|!|$($rel.targetDisplayVersion)|!|$($rel.targetId)|!|$($rel.dependencyType)" - } - elseif ($rel."@odata.type" -eq "#microsoft.graph.mobileAppSupersedence") { - $supersededApps += "$($rel.targetDisplayName)|!|$($rel.targetDisplayVersion)|!|$($rel.targetId)|!|$($rel.supersedenceType)" - } - } - if ($dependencyApps.Count -gt 0) { - $obj.Object | Add-Member -MemberType NoteProperty -Name "#CustomRefDependency" -Value ($dependencyApps -join "|*|") - } - - if ($supersededApps.Count -gt 0) { - $obj.Object | Add-Member -MemberType NoteProperty -Name "#CustomRefSupersedence" -Value ($supersededApps -join "|*|") - } - } - - if($obj.Object.'@odata.type' -eq "#microsoft.graph.win32LobApp") - { - if($obj.Object.activeInstallScript.targetId -or $obj.Object.activeUninstallScript.targetId) - { - $scriptInfo = (Invoke-GraphRequest -Url "/deviceAppManagement/mobileApps/$($obj.Id)/microsoft.graph.win32LobApp/contentVersions/$($obj.Object.committedContentVersion)/scripts/").value - - foreach($script in $scriptInfo) { - $scriptFullInfo = (Invoke-GraphRequest -Url "/deviceAppManagement/mobileApps/$($obj.Id)/microsoft.graph.win32LobApp/contentVersions/$($obj.Object.committedContentVersion)/scripts/$($script.id)?`$select=Id,Content") - if($scriptFullInfo.Content) - { - $script.content = $scriptFullInfo.Content - } - - if($obj.Object.activeInstallScript.targetId -eq $script.id) - { - $tpObject = $obj.Object.activeInstallScript - } - elseif($obj.Object.activeUninstallScript.targetId -eq $script.id) - { - $tpObject = $obj.Object.activeUninstallScript - } - else - { - Write-Log "Script with id $($script.id) is not referenced by active install or uninstall script. Skipping." 2 - continue - } - $tpObject | Add-Member -MemberType NoteProperty -Name "#ScriptInfo" -Value $script -Force - } - } - } -} - -function Start-PostImportApplications -{ - param($obj, $objectType, $file) - - #$tmpObj = Get-GraphObjectFromFile $file -} - -function Start-PostFilesImportApplications -{ - param($objType, $importedObjects, $importedFiles) - - $refObjects = $importedFiles | Where { $null -ne $_.Object."#CustomRefDependency" -or $null -ne $_.Object."#CustomRefSupersedence" } - - if(($refObjects | measure).Count -gt 0) - { - Write-Log "Applicetions with Dependency or Supersedence detected" - foreach($file in $refObjects) - { - Add-ApplicationReferences $file.ImportedObject $file.Object - } - } -} - -function local:Add-ApplicationReferences -{ - param($obj, $fileObj) - - if($fileObj."#CustomRefDependency" -or $fileObj."#CustomRefSupersedence") - { - Write-Log "Adding app references for $($obj.displayName)" - - $depAppsInfo = $fileObj."#CustomRefDependency" - $supAppsInfo = $fileObj."#CustomRefSupersedence" - - $releationShips = [PSCustomObject]@{ - relationships = @() - } - - if($depAppsInfo) - { - foreach($depApp in ($depAppsInfo -split "[|][*][|]")) - { - $appName, $appVer, $appId, $appType = $depApp -split "[|][!][|]" - if(-not $appName -or -not $appVer) - { - Write-Log "Could not get Name and Version from string: $appApp" 2 - continue - } - $tmpApps = (Invoke-GraphRequest -Url "/deviceAppManagement/mobileApps?`$filter=displayName eq '$appName'").value - if(-not $tmpApps) - { - Write-Log "No application found with name $appName" 2 - continue - } - $tmpApp = $tmpApps | Where displayVersion -eq $appVer - if(-not $tmpApp) - { - Write-Log "No $appName application found with version $appVer" 2 - continue - } - elseif(($tmpApp | measure).Count -gt 1) - { - Write-Log "Multiple $appName applications found with version $appVer" 2 - continue - } - Write-Log "Add $appName ($appVer) to Dependency list" - $releationShips.relationships += [PSCustomObject]@{ - "@odata.type" = "#microsoft.graph.mobileAppDependency" - targetId = $tmpApp.Id - dependencyType = $appType - } - } - } - - if($supAppsInfo) - { - foreach($suppApp in ($supAppsInfo -split "[|][*][|]")) - { - $appName, $appVer, $appId, $appType = $suppApp -split "[|][!][|]" - if(-not $appName -or -not $appVer) - { - Write-Log "Could not get Name and Version from string: $suppApp" 2 - continue - } - $tmpApps = (Invoke-GraphRequest -Url "/deviceAppManagement/mobileApps?`$filter=displayName eq '$appName'").value - if(-not $tmpApps) - { - Write-Log "No application found with name $appName" 2 - continue - } - $tmpApp = $tmpApps | Where displayVersion -eq $appVer - if(-not $tmpApp) - { - Write-Log "No $appName application found with version $appVer" 2 - continue - } - elseif(-not ($tmpApp | measure).Count -gt 1) - { - Write-Log "Multiple $appName application found with version $appVer" 2 - continue - } - Write-Log "Add $appName ($appVer) to Supersedence list" - $releationShips.relationships += [PSCustomObject]@{ - "@odata.type" = "#microsoft.graph.mobileAppSupersedence" - targetId = $tmpApp.Id - supersedenceType = $appType - } - } - } - - if($releationShips.relationships.Count -gt 0) - { - $json = Update-JsonForEnvironment (ConvertTo-Json $releationShips -Depth 20) - - Write-Log "Update app references" - Invoke-GraphRequest -Url "/deviceAppManagement/mobileApps/$($obj.Id)/updateRelationships" -Method "POST" -Body $json - } - } -} - -#endregion - -#region Group Policy/Administrative Templates functions -function Get-GPOObjectSettings -{ - param($GPOObj) - - $gpoSettings = @() - - if ($GPOObj.policyConfigurationIngestionType -eq "unknown") { - $tmpObj = (Invoke-GraphRequest -Url "/deviceManagement/groupPolicyConfigurations?`$filter=id eq '$($GPOObj.id)'").value[0] - if ($tmpObj.policyConfigurationIngestionType) { - $GPOObj.policyConfigurationIngestionType = $tmpObj.policyConfigurationIngestionType - } - } - - # Get all configured policies in the Administrative Templates profile - $GPODefinitionValues = Invoke-GraphRequest -Url "/deviceManagement/groupPolicyConfigurations/$($GPOObj.id)/definitionValues?`$expand=definition" -ODataMetadata "skip" - foreach($definitionValue in $GPODefinitionValues.value) - { - # Get presentation values for the current settings (with presentation object included) - $presentationValues = Invoke-GraphRequest -Url "/deviceManagement/groupPolicyConfigurations/$($GPOObj.id)/definitionValues/$($definitionValue.id)/presentationValues?`$expand=presentation" -ODataMetadata "skip" - - # Set base policy settings - $obj = @{ - "enabled" = $definitionValue.enabled - "definition@odata.bind" = "$($global:graphURL)/deviceManagement/groupPolicyDefinitions('$($definitionValue.definition.id)')" - } - - if($definitionValue.definition.categoryPath) - { - $obj.Add("#Definition_Id", $definitionValue.definition.id) - $obj.Add("#Definition_displayName", $definitionValue.definition.displayName) - $obj.Add("#Definition_classType", $definitionValue.definition.classType) - $obj.Add("#Definition_categoryPath", $definitionValue.definition.categoryPath) - } - - if($presentationValues.value) - { - # Policy presentation values set e.g. a drop down list, check box, text box etc. - $obj.presentationValues = @() - - foreach ($presentationValue in $presentationValues.value) - { - # Add presentation@odata.bind property that links the value to the presentation object - $presentationValue | Add-Member -MemberType NoteProperty -Name "presentation@odata.bind" -Value "$($global:graphURL)/deviceManagement/groupPolicyDefinitions('$($definitionValue.definition.id)')/presentations('$($presentationValue.presentation.id)')" - - if($definitionValue.definition.categoryPath) - { - $presentationValue | Add-Member -MemberType NoteProperty -Name "#Presentation_Id" -Value $presentationValue.presentation.id - $presentationValue | Add-Member -MemberType NoteProperty -Name "#Presentation_Label" -Value $presentationValue.presentation.label - } - #Remove presentation object so it is not included in the export - Remove-ObjectProperty $presentationValue "presentation" - - #Optional removes. Import will igonre them - Remove-ObjectProperty $presentationValue "id" - Remove-ObjectProperty $presentationValue "lastModifiedDateTime" - Remove-ObjectProperty $presentationValue "createdDateTime" - - # Add presentation value to the list - $obj.presentationValues += $presentationValue - } - } - $gpoSettings += $obj - } - $gpoSettings -} - -function Import-GPOSetting -{ - param($obj, $settings) - - if($obj) - { - Write-Status "Import settings for $($obj.displayName)" - - $hasCustomADMX = $null -ne ($settings | Where { $null -ne $_.'#Definition_categoryPath' }) - - if($hasCustomADMX) - { - Write-Status "Import custom ADMX settings" - if(-not $script:CustomADMXDefinitions) - { - $tmpCustomCategories = Invoke-GraphRequest -Url "deviceManagement/groupPolicyCategories?`$expand=definitions(`$select=id, displayName, categoryPath, classType)&`$select=id, displayName&`$filter=ingestionSource eq 'custom'" -ODataMetadata "Minimal" - if($tmpCustomCategories.Value) - { - $script:CustomADMXDefinitions = @{} - foreach($tmpCat in $tmpCustomCategories.Value) - { - foreach($tmpDef in $tmpCat.definitions) - { - $key = ($tmpDef.displayName + $tmpDef.categoryPath + $tmpDef.classType).ToLower() - $val = [PSCustomObject]@{ - Definition = $tmpDef - Category = $tmpCat - Presentations = $null - } - try { - $script:CustomADMXDefinitions.Add($key, $val) - } - catch { - Write-Log "Failed to add '$($tmpDef.displayName)' in category '$($tmpDef.categoryPath)' of class $($tmpDef.classType)" 3 - } - } - } - } - } - } - - foreach($setting in $settings) - { - if($setting.'#Definition_categoryPath' -and $script:CustomADMXDefinitions -is [HashTable] -and $script:CustomADMXDefinitions.Count -gt 0) - { - $defVal = $null - $key = ($setting.'#Definition_displayName' + $setting.'#Definition_categoryPath' + $setting.'#Definition_classType').ToLower() - if($key -and $script:CustomADMXDefinitions.ContainsKey($key)) - { - $defVal = $script:CustomADMXDefinitions[$key] - } - elseif($key) - { - Write-Log "No custom ADMX definitiona found for setting $($setting.'#Definition_displayName')" 2 - } - else - { - Write-Log "Setting $($setting.'#Definition_displayName') does not have information to be imported in the environment" - } - - if($defVal) - { - $setting.'definition@odata.bind' = $setting.'definition@odata.bind' -replace $setting.'#Definition_Id', $defVal.Definition.Id - if(($setting.presentationValues | measure).Count -gt 0) - { - if(-not $defVal.Presentations) - { - $tmpPresentation = Invoke-GraphRequest -Url "deviceManagement/groupPolicyDefinitions/$($defVal.Definition.Id)/presentations" -ODataMetadata "Minimal" - if($tmpPresentation.value) - { - foreach($settingPresentation in $setting.presentationValues) - { - $tmpPresentationVal = $tmpPresentation.value | Where label -eq $settingPresentation.'#Presentation_Label' - if($tmpPresentationVal) - { - $settingPresentation.'presentation@odata.bind' = $settingPresentation.'presentation@odata.bind' -replace $setting.'#Definition_Id', $defVal.Definition.Id - $settingPresentation.'presentation@odata.bind' = $settingPresentation.'presentation@odata.bind' -replace $settingPresentation.'#Presentation_Id', $tmpPresentationVal.Id - } - else - { - Write-Log "Could not find a presentation value with label $($settingPresentation.'#Presentation_Label'). Setting will not be configured" 2 - continue - } - } - } - else - { - Write-Log "Could not find presentation for setting $($settingPresentation.'#Presentation_Label'). Setting will not be configured." 2 - continue - } - } - } - } - else - { - Write-Log "Settings might not be available if imported in another environment" 3 - } - } - elseif($setting.'#Definition_categoryPath') - { - Write-Log "Custom AMDX settings cannot be imported without ADMX file imported. Definitions not found" 2 - continue - } - - Start-GraphPreImport $setting - - if($true) - { - foreach($tmpProp in (($setting.PSObject.Properties | Where Name -like "#*").Name)) - { - Remove-Property $setting $tmpProp - } - - foreach($settingPresentation in $setting.presentationValues) - { - foreach($tmpProp in (($settingPresentation.PSObject.Properties | Where Name -like "#*").Name)) - { - Remove-Property $settingPresentation $tmpProp - } - } - } - - # Import each setting for the Administrative Template profile - Invoke-GraphRequest -Url "/deviceManagement/groupPolicyConfigurations/$($obj.id)/definitionValues" -Content (ConvertTo-Json $setting -Depth 20) -HttpMethod POST | Out-Null - } - } -} - -function Start-PostExportAdministrativeTemplate -{ - param($obj, $objectType, $path) - - $fileName = (Get-GraphObjectName $obj $objectType).Trim('.') - if((Get-SettingValue "AddIDToExportFile") -eq $true -and $obj.Id) - { - $fileName = ($fileName + "_" + $obj.Id) - } - - if($obj.definitionValues) - { - $settings = $obj.definitionValues - } - else - { - $settings = Get-GPOObjectSettings $obj - } - - $fileName = "$path\$((Remove-InvalidFileNameChars $fileName))_Settings.json" - Save-GraphObjectToFile $settings $fileName -} - -function Start-PostCopyAdministrativeTemplate -{ - param($objCopyFrom, $objNew, $objectType) - - $settings = Get-GPOObjectSettings $objCopyFrom - if($settings) - { - Import-GPOSetting $objNew $settings - } -} - -function Start-PostFileImportAdministrativeTemplate -{ - param($obj, $objectType, $file) - - $settings = Get-EMSettingsObject $obj $objectType $file -settingsProperty "definitionValues" -SettingsArray - if($settings) - { - $tmpObj = Get-GraphObjectFromFile $file - - Import-GPOSetting $obj $settings - } -} - -function Start-LoadAdministrativeTemplate -{ - param($fileName) - - if(-not $fileName) { return $null } - - $fi = [IO.FileInfo]$fileName - if($fi.Exists -eq $false) { return } - - $obj = Get-GraphObjectFromFile $fi.FullName - - if($obj.definitionValues) - { - return $obj - } - - $settingsFile = $fi.DirectoryName + "\" + $fi.BaseName + "_Settings.json" - - if([IO.File]::Exists($settingsFile)) - { - $definitionValues = Get-GraphObjectFromFile $settingsFile - - $obj | Add-Member Noteproperty -Name "definitionValues" -Value $definitionValues -Force - } - $obj -} - -function Start-PostGetAdministrativeTemplate -{ - param($obj, $objectType) - - $definitionValues = Get-GPOObjectSettings $obj.Object - if($definitionValues) - { - $obj.Object | Add-Member Noteproperty -Name "definitionValues" -Value $definitionValues -Force - } - <# - # Leave for now. This only loads the configured definition values and not the values specified. - # That would require enumerating each definition value which takes time. - $definitionValues = (Invoke-GraphRequest "deviceManagement/groupPolicyConfigurations('$($obj.Id)')/definitionValues?`$expand=definition(`$select=id,classType,displayName,policyType,groupPolicyCategoryId)" -ODataMetadata "minimal").value - - if($definitionValues) - { - $obj.Object | Add-Member Noteproperty -Name "definitionValues" -Value $definitionValues -Force - } - #> -} - -function Start-PreImportAdministrativeTemplate -{ - param($obj, $objectType, $file, $assignments) - - -} - -#endregion - -#region Policy Sets function - -function Start-PreImportAssignmentsPolicySets -{ - param($obj, $objectType, $file, $assignments) - - @{"API"="$($objectType.API)/$($obj.Id)/Update"} -} - -function Start-PreImportPolicySets -{ - param($obj, $objectType) - - @("items@odata.context","status","errorCode") | foreach { Remove-Property $obj $_ } - - # Properties to keep for items - $keepProperties = @("@odata.type","payloadId","intent","settings") - foreach($item in $obj.Items) - { - foreach($prop in ($item.PSObject.Properties | Where {$_.Name -notin $keepProperties})) - { - Remove-Property $item $prop.Name - } - #@("itemType","displayName","status","errorCode") | foreach { Remove-Property $item $_ } - } -} - -function Start-PreUpdatePolicySets -{ - param($obj, $objectType, $curObject, $fromObj) - - Start-PreImportPolicySets $obj $objectType - - $curObject = Get-GraphObject $curObject.Object $objectType - - # Update ref object in the json - # Used when importing in a different environment - $jsonObj = ConvertTo-Json $obj -Depth 15 - $updateObj = Update-JsonForEnvironment $jsonObj | ConvertFrom-Json - - $addedItems = @() - $updatedItems = @() - $deletedItems = @() - - foreach($item in $updateObj.items) - { - if(($curObject.Object.items | Where payloadId -eq $item.payloadId)) - { - $updatedItems += $item - } - else - { - $addedItems += $item - } - } - - foreach($item in $curObject.Object.items) - { - if(-not ($updateObj.Items | Where payloadId -eq $item.payloadId)) - { - $deletedItems += $item.id - } - } - - $updateItemObj = [PSCustomObject]@{ - addedPolicySetItems = $addedItems - deletedPolicySetItems = $deletedItems - updatedPolicySetItems = $updatedItems - } - - Write-Log "Update Policy Set items. Add: $($addedItems.Count), Update: $($updatedItems.Count), Delete: $($deletedItems.Count)" - - $updateApi = "/deviceAppManagement/policySets/$($curObject.Object.Id)/update" - $json = $updateItemObj | ConvertTo-Json -Depth 15 - - Invoke-GraphRequest -Url $updateApi -HttpMethod "POST" -Content $json - Remove-Property $obj "items" -} - -function Update-EMPolicySetAssignment -{ - param($assignment, $sourceObject, $newObject, $objectType) - - $api = "/deviceAppManagement/policySets/$($assignment.SourceId)?`$expand=assignments,items" - - $psObj = Invoke-GraphRequest -Url $api -ODataMetadata "Minimal" - - if(-not $psObj) - { - return - } - - $curItem = $psObj.Items | Where payloadId -eq $sourceObject.Id - - if(-not $curItem) - { - return - } - - $api = "/deviceAppManagement/policySets/$($assignment.SourceId)/update" - - $curItemClone = $curItem | ConvertTo-Json -Depth 20 | ConvertFrom-Json - $newItem = $curItem | ConvertTo-Json -Depth 20 | ConvertFrom-Json - $newItem.payloadId = $newObject.Id - if($newItem.guidedDeploymentTags -is [String] -and [String]::IsNullOrEmpty($newItem.guidedDeploymentTags)) - { - $newItem.guidedDeploymentTags = @() - } - - $keepProperties = @('@odata.type','payloadId','Settings','guidedDeploymentTags') - #itemType? e.g. #microsoft.graph.iosManagedAppProtection - #priority? - - foreach($prop in ($newItem.PSObject.Properties | Where {$_.Name -notin $keepProperties})) - { - Remove-Property $newItem $prop.Name - } - - $update = @{} - $update.Add('addedPolicySetItems',@($newItem)) - $update.Add('updatedPolicySetItems', @()) - $update.Add('deletedPolicySetItems',@($curItemClone.Id)) - - $json = $update | ConvertTo-Json -Depth 20 - - Write-Log "Update PolicySet $($psObj.displayName) - Replace: $((Get-GraphObjectName $newObject $objectType))" - - Invoke-GraphRequest -Url $api -HttpMethod "POST" -Content $json -} - -function Start-PostListPolicySets -{ - param($objList, $objectType) - - foreach($obj in $objList) - { - $obj | Add-Member -MemberType NoteProperty -Name "IsAssigned" -Value ($obj.Object.status -ne "notAssigned") - } - $objList -} -#endregion - -#endregion Locations -function Start-PreImportLocations -{ - param($obj, $objectType) - - if($obj.uniqueName) - { - $arr = $obj.uniqueName.Split('_') - if($arr.Length -ge 3) - { - # Locations requires a unique name so generate a new guid and change the uniqueName property - $obj.uniqueName = ($obj.uniqueName.Substring(0,$obj.uniqueName.Length-$arr[-1].Length) + [Guid]::NewGuid().Tostring("n")) - } - } -} -#endregion - -#region RoleDefinitions -function Start-PostExportRoleDefinitions -{ - param($obj, $objectType, $path) - - $fileName = (Get-GraphObjectName $obj $objectType).Trim('.') - if((Get-SettingValue "AddIDToExportFile") -eq $true -and $obj.Id) - { - $fileName = ($fileName + "_" + $obj.Id) - } - $tmpObj = $null - $fileName = "$path\$((Remove-InvalidFileNameChars $fileName)).json" - if([IO.File]::Exists($fileName)) - { - $tmpObj = Get-GraphObjectFromFile $fileName - } - else - { - Write-Log "File not found: $fileName. Could not get role assignments" 3 - } - - if(($tmpObj.RoleAssignments | measure).Count -gt 0) - { - $roleAssignmentsArr = @() - foreach($roleAssignment in $tmpObj.RoleAssignments) - { - $raObj = Invoke-GraphRequest -Url "/deviceManagement/roleAssignments/$($roleAssignment.Id)?`$expand=microsoft.graph.deviceAndAppManagementRoleAssignment/roleScopeTags" -ODataMetadata "Minimal" - if($raObj) - { - foreach($groupId in $raObj.resourceScopes) { Add-GroupMigrationObject $groupId } - foreach($groupId in $raObj.members) { Add-GroupMigrationObject $groupId } - $roleAssignmentsArr += $raObj - } - } - - if($roleAssignmentsArr.Count -gt 0) - { - $tmpObj.RoleAssignments = $roleAssignmentsArr - Save-GraphObjectToFile $tmpObj $fileName - } - } -} - -function Start-PreImportRoleDefinitions -{ - param($obj, $objectType) - - Remove-Property $obj "RoleAssignments" - Remove-Property $obj "RoleAssignments@odata.context" -} - -function Start-PostFileImportRoleDefinitions -{ - param($obj, $objectType, $file) - - $tmpObj = Get-GraphObjectFromFile $file - - $loadedScopeTags = $global:LoadedDependencyObjects["ScopeTags"] - if(($tmpObj.RoleAssignments | measure).Count -gt 0 -and ($loadedScopeTags | measure).Count -gt 0) - { - # Documentation way did not work so use the same way as the portal - # Should be created with /deviceManagement/roleDefinitions/{roleDefinitionId}/roleAssignments - foreach($roleAssignment in $tmpObj.RoleAssignments) - { - $roleAssignmentObj = New-object PSObject @{ - "description" = $roleAssignment.Description - "displayName"= $roleAssignment.DisplayName - "members" = $roleAssignment.members - "resourceScopes" = $roleAssignment.resourceScopes - "roleDefinition@odata.bind" = "https://graph.microsoft.com/beta/deviceManagement/roleDefinitions('$($obj.Id)')" - "roleScopeTags@odata.bind" = @() - } - - foreach($scopeTag in $roleAssignment.roleScopeTags) - { - $scopeMigObj = $loadedScopeTags | Where OriginalId -eq $scopeTag.Id - if(-not $scopeMigObj.Id) { continue } - $roleAssignmentObj."roleScopeTags@odata.bind" += "https://graph.microsoft.com/beta/deviceManagement/roleScopeTags('$($scopeMigObj.Id)')" - } - - # This will update GroupIds - $json = Update-JsonForEnvironment (ConvertTo-Json $roleAssignmentObj -Depth 20) - - Write-Log "Import Role Assignments" - Invoke-GraphRequest -Url "/deviceManagement/roleAssignments" -Body $json -Method "POST" - } - } -} -#endregion - -#region SettingsCatalog - -function Start-PreImportSettingsCatalog -{ - param($obj, $objectType) - - $returnHT = @{} - $updated = $false - - if($obj.templateReference.templateId) { - # I do not like this at all and it is a lazy but simple implementation... - # It turns out that settingInstanceTemplateId and settingValueTemplateId are case sensitive - # and there is ONE setting with a different casing in the Windows Baseline template. - # The export saves it with lowercase which causes the import to fail. - - Write-Log "Get template $($obj.templateReference.templateId)" - $templateObj = Invoke-GraphRequest -Url "/deviceManagement/configurationPolicyTemplates('$($obj.templateReference.templateId)')" - if($templateObj.lifecycleState -and $templateObj.lifecycleState -ne "active") { - Write-Log "Template '$($templateObj.displayName)' '$($templateObj.displayVersion)' is in '$($templateObj.lifecycleState)' state. Current state: $($templateObj.lifecycleState). Import might fail." 2 - } - #Todo: Should probably check for the latest active version and use that instead of the one in the templateReference - - if(-not $script:baseLineTemplate) { - $script:baseLineTemplate = @{} - } - if($script:baseLineTemplate.ContainsKey($obj.templateReference.templateId)) { - $templateReference = $script:baseLineTemplate[$obj.templateReference.templateId] - } - else { - Write-Log "Get template settings for '$($templateObj.displayName)' '$($templateObj.displayVersion)' ($($obj.templateReference.templateId))" - $templateReference = Invoke-GraphRequest -Url "/deviceManagement/configurationPolicyTemplates('$($obj.templateReference.templateId)')/settingTemplates?`$expand=settingDefinitions&top=1000" - $script:baseLineTemplate.Add($obj.templateReference.templateId, $templateReference) - } - - if($templateReference) { - $newObjJson = $obj | ConvertTo-Json -Depth 50 - $templateIDs = Get-GUIDs ($templateReference | ConvertTo-Json -Depth 50) - $objectIDs = Get-GUIDs ($obj.Settings | ConvertTo-Json -Depth 50) - $diff = Compare-Object $templateIDs $objectIDs -CaseSensitive - foreach($diffItem in ($diff | where SideIndicator -eq "=>")) { - $templateID = $templateIDs | Where { $_ -eq $diffItem.InputObject } - if($templateID) { - # Found but with different casing - $newObjJson = $newObjJson -replace $diffItem.InputObject, $templateID - $updated = $true - } - } - if($updated) { - $returnHT.Add("JSON", $newObjJson) - } - } - } - return $returnHT -} - -function Invoke-CheckSettingsCatalogIds -{ - param($obj, $templateReference) - - foreach($settingTemplate in $obj.value) { - if($settingTemplate.settingDefinitions) { - foreach($settingDefinition in $settingTemplate.settingDefinitions) { - if($settingDefinition.id -and $settingDefinition.id -ne $obj.Id) { - Write-Log "Setting definition ID $($settingDefinition.id) does not match the settings catalog ID $($obj.Id)" 2 - } - } - } - } -} - -function Start-PostExportSettingsCatalog -{ - param($obj, $objectType, $path) - - Add-EMAssignmentsToExportFile $obj $objectType $path -} - -function Start-PreUpdateSettingsCatalog -{ - param($obj, $objectType, $curObject, $fromObj) - - @{"Method"="PUT"} -} - -function Start-PostGetSettingsCatalog -{ - param($obj, $objectType) - - if(-not $obj.Object.Assignments) - { - $url = "$($objectType.API)/$($obj.id)/assignments" - $assignments = (Invoke-GraphRequest -Url $url).Value - if($assignments) - { - $obj.Object.Assignments = $assignments - } - } -} - -#endregion - -#region Notification functions -function Start-PreImportNotifications -{ - param($obj, $objectType) - - Remove-Property $obj "defaultLocale" - Remove-Property $obj "localizedNotificationMessages" - Remove-Property $obj "localizedNotificationMessages@odata.context" -} - -function Start-PostFileImportNotifications -{ - param($obj, $objectType, $file) - - $tmpObj = Get-GraphObjectFromFile $file - - foreach($localizedNotificationMessage in $tmpObj.localizedNotificationMessages) - { - Start-GraphPreImport $localizedNotificationMessage $objectType - Invoke-GraphRequest -Url "$($objectType.API)/$($obj.id)/localizedNotificationMessages" -Body ($localizedNotificationMessage | ConvertTo-Json -Depth 20) -Method "POST" - } -} - -function Start-PostCopyNotifications -{ - param($objCopyFrom, $objNew, $objectType) - - foreach($localizedNotificationMessage in $objCopyFrom.localizedNotificationMessages) - { - Start-GraphPreImport $localizedNotificationMessage $objectType - Invoke-GraphRequest -Url "$($objectType.API)/$($objNew.id)/localizedNotificationMessages" -Body ($localizedNotificationMessage | ConvertTo-Json -Depth 20) -Method "POST" - } -} -#endregion - -#region Enrollment Status Page functions -function Start-PreImportESP -{ - param($obj, $objectType) - - if($obj.Priority -eq 0) - { - $ret = @{} - $ret.Add("API","$($objectType.API)/$($obj.Id)") - $ret.Add("Method","PATCH") # Default profile always exists so update them - $ret - } - else - { - Remove-Property $obj "Id" - } -} - -function Start-PostExportESP -{ - param($obj, $objectType, $path) - - if($obj.Priority -eq 0) - { - Save-EMDefaultPolicy $obj $objectType $path - } -} - -function Start-PostListESP -{ - param($objList, $objectType) - - # endswith not working so filter them out - $objList | Where { $_.Object.'@OData.Type' -eq '#microsoft.graph.windows10EnrollmentCompletionPageConfiguration' } -} -#endregion - -#region Enrollment Restriction functions - -function Start-PostExportEnrollmentRestrictions -{ - param($obj, $objectType, $path) - - if($obj.Priority -eq 0) - { - Save-EMDefaultPolicy $obj $objectType $path - } -} - -function Start-PreImportEnrollmentRestrictions -{ - param($obj, $objectType) - - if($obj.Priority -eq 0) - { - $ret = @{} - $ret.Add("API","$($objectType.API)/$($obj.Id)") - $ret.Add("Method","PATCH") # Default profile always exists so update them - $ret - } - else - { - Remove-Property $obj "Id" - } - - if($obj.windowsMobileRestriction) - { - # Windows Phone operations are no longer supported - Remove-Property $obj "windowsMobileRestriction" - } -} - -function Start-PreDeleteEnrollmentRestrictions -{ - param($obj, $objectType) - - if($obj.Priority -eq 0) - { - @{ "Delete" = $false } - } -} - -function Start-PreReplaceEnrollmentRestrictions -{ - param($obj, $objectType, $sourceObj, $fromFile) - - if($sourceObj.Priority -eq 0) { @{ "Replace" = $false } } -} - -function Start-PostReplaceEnrollmentRestrictions -{ - param($obj, $objectType, $sourceObj, $fromFile) - - if($sourceObj.Priority -eq 0) { return } - - $api = "/deviceManagement/deviceEnrollmentConfigurations/$($obj.id)/setpriority" - - $priority = [PSCustomObject]@{ - priority = $sourceObj.Priority - } - $json = $priority | ConvertTo-Json -Depth 20 - - Write-Log "Update priority for $($obj.displayName) to $($sourceObj.Priority)" - Invoke-GraphRequest $api -HttpMethod "POST" -Content $json -} - -function Start-PreFilesImportEnrollmentRestrictions -{ - param($objectType, $filesToImport) - - $filesToImport | sort-object -property @{e={$_.Object.priority}} -} - -function Start-PreUpdateEnrollmentRestrictions -{ - param($obj, $objectType, $curObject, $fromObj) - - Remove-Property $obj "priority" -} - -function Start-PostListEnrollmentRestrictions -{ - param($objList, $objectType) - - # endswith not working so filter them out - $objList | Where { - ($_.Object.'@OData.Type' -eq '#microsoft.graph.deviceEnrollmentPlatformRestrictionConfiguration' -or - $_.Object.'@OData.Type' -eq '#microsoft.graph.deviceEnrollmentLimitConfiguration' -or - $_.Object.'@OData.Type' -eq '#microsoft.graph.deviceEnrollmentPlatformRestrictionsConfiguration') -and - $_.Object.id -notlike "*_PlatformRestrictions" -and $_.Object.platformType -ne "WindowsPhone" -and $_.Object.platformType -ne "AndroidAosp" - } -} - -function Start-PreImportAssignmentsEnrollmentRestrictions -{ - param($obj, $objectType, $file, $assignments) - - if($obj.Priority -eq 0) - { - # Skip Assignment for Default Policy - @{ "Import" = $false } - } -} - -#endregion - -#region -function Start-PostListCoManagementSettings -{ - param($objList, $objectType) - - # endswith not working so filter them out - $objList | Where { $_.Object.'@OData.Type' -eq '#microsoft.graph.deviceComanagementAuthorityConfiguration' } -} -#endregion - -#region ScopeTags -function Start-PostExportScopeTags -{ - param($obj, $objectType, $path) - - Add-EMAssignmentsToExportFile $obj $objectType $path -} - -function Start-PostGetScopeTags -{ - param($obj, $objectType) - - $strAPI = "$($objectType.API)/$($obj.Object.Id)/assignments" - $tmpObj = Invoke-GraphRequest -Url $strAPI - - if(($tmpObj.value | measure).count -gt 0) - { - $obj.Object.assignments = $tmpObj.value - } -} -#endregion - -#region AutoPilot -function Start-PreImportAssignmentsAutoPilot -{ - param($obj, $objectType, $file, $assignments) - - Add-EMAssignmentsToObject $obj $objectType $file $assignments -} - -function Start-PreDeleteAutoPilot -{ - param($obj, $objectType) - - Write-Log "Delete AutoPilot profile assignments" - - if(-not $obj.Assignments) - { - $tmpObj = (Get-GraphObject $obj $objectType).Object - } - else - { - $tmpObj = $obj - } - - foreach($assignment in $tmpObj.Assignments) - { - if($assignment.Source -ne "direct") { continue } - - $api = "/deviceManagement/windowsAutopilotDeploymentProfiles/$($obj.Id)/assignments/$($assignment.Id)" - - Invoke-GraphRequest $api -HttpMethod "DELETE" - } -} - -#endregion - -#region Health Scripts - -function Start-PreDeleteDeviceHealthScripts -{ - param($obj, $objectType) - - if($obj.isGlobalScript -eq $true) - { - @{ "Delete" = $false } - } -} - -function Start-PreImportDeviceHealthScripts -{ - param($obj, $objectType, $file, $assignments) - - if($obj.isGlobalScript -eq $true) - { - @{ "Import" = $false } - } -} - -function Start-PreUpdateDeviceHealthScripts -{ - param($obj, $objectType, $curObject, $fromObj) - - if($curObject.Object.isGlobalScript -eq $true) - { - @{ "Import" = $false } - } -} - -function Start-PostExportDeviceHealthScripts -{ - param($obj, $objectType, $path) - - if($global:chkExportScript.IsChecked) - { - $fileName = Get-GraphObjectFile $obj $objectType - $fi = [IO.FileInfo]"$path\$fileName" - - try - { - if($obj.detectionScriptContent) - { - [IO.File]::WriteAllBytes(("$path\$($fi.BaseName)_DetectionScript.ps1"), ([System.Convert]::FromBase64String($obj.detectionScriptContent))) - } - - if($obj.remediationScriptContent) - { - [IO.File]::WriteAllBytes(("$path\$($fi.BaseName)_RemediationScript.ps1"), ([System.Convert]::FromBase64String($obj.remediationScriptContent))) - } - } - catch - { - Write-LogError "Failed to export scripts" $_.Exception - } - } -} - -#endregion - -#region Generic functions - -function Save-EMDefaultPolicy -{ - param($obj, $objectType, $path) - - if($obj.Priority -eq 0) - { - try - { - $fileName = $obj.Id.Split('_')[1] - - if($fileName) - { - $oldFile = "$path\$((Get-GraphObjectName $obj $objectType)).json" - if([IO.File]::Exists($oldFile)) - { - # Clean up from old version of the script that used the wrong name for Default policies - try { [IO.File]::Delete($oldFile) | Out-Null } Catch {} - } - Save-GraphObjectToFile $obj "$path\$((Remove-InvalidFileNameChars $fileName)).json" - } - } - catch {} - } -} -function Get-EMSettingsObject -{ - param($obj, $objectType, $file, $settingsProperty = "settings", [switch]$SettingsArray) - - if($obj.$settingsProperty) { return $obj.$settingsProperty } - - $fi = [IO.FileInfo]$file - if($fi.Exists) - { - # Settings property removed during import so lets try exported file first - $tmpObj = Get-GraphObjectFromFile $fi.FullName - if($SettingsArray -eq $true) - { - # Only the an array of settings is expected - return $tmpObj.$settingsProperty - } - else - { - if($tmpObj.$settingsProperty) - { - # A property with the an array of settings is expected - return ([PSCustomObject]@{ - $settingsProperty = $tmpObj.$settingsProperty - }) - } - } - - Write-Log "Settings not included in export file. Try import from _Settings.json file" 2 - $settingsFile = $fi.DirectoryName + "\" + $fi.BaseName + "_Settings.json" - $fiSettings = [IO.FileInfo]$settingsFile - if($fiSettings.Exists -eq $false) - { - Write-Log "Settings file '$($fiSettings.FullName)' was not found" 2 - return - } - Get-GraphObjectFromFile $fiSettings.FullName - } - else - { - Write-Log "Settings not included in export file and _Settings.json file is missing." 3 - } -} - -function Add-EMAssignmentsToExportFile -{ - param($obj, $objectType, $path, $Url = "") - - if($global:chkExportAssignments.IsChecked -ne $true) { return } - - $fileName = (Get-GraphObjectName $obj $objectType).Trim('.') - if((Get-SettingValue "AddIDToExportFile") -eq $true -and $obj.Id) - { - $fileName = ($fileName + "_" + $obj.Id) - } - $fileName = "$path\$((Remove-InvalidFileNameChars $fileName)).json" - if([IO.File]::Exists($fileName) -eq $false) - { - Write-Log "File not found: $fileName. Could not add assignments to file" 3 - return - } - - $tmpObj = Get-GraphObjectFromFile $fileName - - if(-not $url) - { - $url = "$($objectType.API)/$($obj.id)/assignments" - } - $assignments = (Invoke-GraphRequest -Url $url -ODataMetadata "Minimal").Value - if($assignments) - { - if(-not ($tmpObj.PSObject.Properties | Where Name -eq "assignments")) - { - $tmpObj | Add-Member -MemberType NoteProperty -Name "assignments" -Value $assignments - } - else - { - $tmpObj.Assignments = $assignments - } - Save-GraphObjectToFile $tmpObj $fileName - } -} - -function Add-EMAssignmentsToObject -{ - param($obj, $objectType, $file, $assignments) - - # AutoPilot and TaC are using assignments and not assign like other object types - $api = "$($objectType.API)/$($obj.Id)/assignments" - - # These profiles don't support importing of multiple assignments with { "assignment" [...]} - # Each assignment must be imported separately - - foreach($assignment in $assignments) - { - if($assignment.Source -and $assignment.Source -ne "direct") { continue } - - foreach($prop in $assignment.PSObject.Properties) - { - if($prop.Name -in @("Target")) { continue } - Remove-Property $assignment $prop.Name - } - - foreach($prop in $assignment.target.PSObject.Properties) - { - if($prop.Name -in @("@odata.type","groupId")) { continue } - Remove-Property $assignment.target $prop.Name - } - - $json = Update-JsonForEnvironment ($assignment | ConvertTo-Json -Depth 20) - Invoke-GraphRequest -Url $api -Body $json -Method "POST" | Out-Null - } - @{"Import"=$false} -} - -#endregion - -#region Mac Custom Scripts - -function Start-PreUpdateMacCustomAttributes -{ - param($obj, $objectType, $curObject, $fromObj) - - foreach($prop in @('customAttributeName','customAttributeType','displayName')) - { - Remove-Property $obj $prop - } -} - -#endregion - -#region Mac Feature Updates -function Start-PreUpdateFeatureUpdates -{ - param($obj, $objectType, $curObject, $fromObj) - - foreach($prop in @('deployableContentDisplayName','endOfSupportDate')) - { - Remove-Property $obj $prop - } -} -#endregion - -#region Conditional Access -function Add-ConditionalAccessImportExtensions -{ - param($form, $buttonPanel, $index = 0) - - $xaml = @" - - -"@ - $label = [Windows.Markup.XamlReader]::Parse($xaml) - - $CAStates = @() - $CAStates += [PSCustomObject]@{ - Name = "As Exported - Change On to Report-only" - Value = "AsExportedReportOnly" - } - - $CAStates += [PSCustomObject]@{ - Name = "As Exported" - Value = "AsExported" - } - - $CAStates += [PSCustomObject]@{ - Name = "Report-only" - Value = "enabledForReportingButNotEnforced" - } - - $CAStates += [PSCustomObject]@{ - Name = "On" - Value = "enabled" - } - - $CAStates += [PSCustomObject]@{ - Name = "Off" - Value = "disabled" - } - - $defaultCAState = Get-SettingValue "ConditionalAccessState" - - $global:cbImportCAState = [System.Windows.Controls.ComboBox]::new() - $global:cbImportCAState.DisplayMemberPath = "Name" - $global:cbImportCAState.SelectedValuePath = "Value" - $global:cbImportCAState.ItemsSource = $CAStates - $global:cbImportCAState.SelectedValue = $defaultCAState - $global:cbImportCAState.Margin="0,5,0,0" - $global:cbImportCAState.HorizontalAlignment="Left" - $global:cbImportCAState.Width=250 - $global:cbImportCAState.Name = "cbImportCAState" - - @($label, $global:cbImportCAState) -} - -function Start-PreImportConditionalAccess -{ - param($obj, $objectType, $file, $assignments) - - if ($global:cbImportCAState.SelectedValue -and $global:cbImportCAState.SelectedValue -ne "AsExported") { - if ($global:cbImportCAState.SelectedValue -eq "AsExportedReportOnly" -and $obj.state -eq "enabled") { - Write-Log "Change Enabled policy to Report-only" - $obj.state = "enabledForReportingButNotEnforced" - } - else { - $obj.state = $global:cbImportCAState.SelectedValue - } - } - - if($obj.grantControls.authenticationStrength) - { - $obj.grantControls.operator = "AND" - $tmpObj = Get-GraphObjectFromFile $file - - $authSetting = [PSCustomObject]@{ - id = $tmpObj.grantControls.authenticationStrength.id - } - $obj.grantControls.authenticationStrength = $authSetting - } - - if($obj.sessionControls.disableResilienceDefaults -eq $false) - { - $obj.sessionControls.disableResilienceDefaults = $null - } - - # DeviceStates property is depricated - if(($obj.conditions.PSObject.Properties | Where Name -eq "DeviceStates")) - { - $obj.conditions.PSObject.Properties.Remove('DeviceStates') - } -} - -function Start-PostExportConditionalAccess -{ - param($obj, $objectType, $path) - - $ids = @() - foreach($id in ($obj.conditions.users.includeGroups + $obj.conditions.users.excludeGroups)) - { - if($id -in $ids) { continue } - elseif($id -eq "GuestsOrExternalUsers") { continue } - elseif($id -eq "All") { continue } - elseif($id -eq "None") { continue } - - $ids += $id - Add-GraphMigrationObject $id "/groups" "Group" - } - - foreach($id in ($obj.conditions.users.includeUsers +$obj.conditions.users.excludeUsers)) - { - if($id -in $ids) { continue } - elseif($id -eq "GuestsOrExternalUsers") { continue } - elseif($id -eq "All") { continue } - elseif($id -eq "None") { continue } - - $ids += $id - Add-GraphMigrationObject $id "/users" "User" - } - - <# - $roleIds = @() - foreach($id in ($obj.conditions.users.includeRoles + $obj.conditions.users.excludeRoles)) - { - if($id -in $ids) { continue } - $roleIds += $id - } - #> -} -#endregion - -#region Terms of use -function Start-PreImportTermsOfUse -{ - param($obj, $objectType, $file, $assignments) - - $pkgPath = Get-SettingValue "EMIntuneAppPackages" - - if(-not $pkgPath -or [IO.Directory]::Exists($pkgPath) -eq $false) - { - Write-Log "Intune app directory is either missing or does not exist" 2 - } - - try - { - $fi = [IO.FileInfo]$file - } catch {} - - foreach($file in $obj.Files) - { - $pdfFile = $null - - if($fi.Directory.FullName) - { - $pdfFile = "$($fi.Directory.FullName)\$($file.fileName)" - } - - if($null -eq $pdfFile -or [IO.File]::Exists($pdfFile) -eq $false) - { - $pdfFile = "$($pkgPath)\$($file.fileName)" - } - - if([IO.File]::Exists($pdfFile) -eq $false) - { - Write-Log "Terms of use file $($file.fileName) not found. The Terms of Use object will not be imported." 2 - @{"Import" = $false} - return - } - - Write-Log "Add file data: $pdfFile" - - $bytes = [IO.File]::ReadAllBytes($pdfFile) - $file.fileData = [PSCustomObject]@{ - data = [Convert]::ToBase64String($bytes) - } - } -} - -function Start-PostExportTermsOfUse -{ - param($obj, $objectType, $path) - - foreach($file in $obj.Files) - { - $url = "agreements/$($obj.id)/file/localizations('$($file.id)')/fileData/data" - $data = (Invoke-GraphRequest -Url $url -ODataMetadata "Minimal").Value - if($data) - { - Write-Log "Save file $($file.FileName)" - $fileName = "$path\$($file.FileName)" - [IO.File]::WriteAllBytes($fileName, [System.Convert]::FromBase64String($data)) - } - } -} - -#endregion - -#region ADMXFiles - -function Start-PreFilesImportADMXFiles -{ - param($objectType, $filesToImport) - - $filesToImport | sort-object -property @{e={$_.Object.lastModifiedDateTime}} -} - -function Start-PreImportADMXFiles -{ - param($obj, $objectType, $file, $assignments) - - $pkgPath = Get-SettingValue "EMIntuneAppPackages" - - if(-not $pkgPath -or [IO.Directory]::Exists($pkgPath) -eq $false) - { - Write-Log "Intune app directory is either missing or does not exist" 2 - $pkgPath = $null - } - - try - { - $fi = [IO.FileInfo]$file - } catch {} - - $admxFile = $null - - if($fi.Directory.FullName) - { - $admxFile = "$($fi.Directory.FullName)\$($obj.fileName)" - $admlFile = "$($fi.Directory.FullName)\$([io.path]::GetFileNameWithoutExtension($obj.fileName)).adml" - } - - if($null -ne $pkgPath -and ($null -eq $admxFile -or [IO.File]::Exists($admxFile) -eq $false -or [IO.File]::Exists($admxFile) -eq $false)) - { - Write-Log "$($obj.fileName) not foud in Export folder. Look in package path: $pkgPath" - $admxFile = "$($pkgPath)\$($obj.fileName)" - $admlFile = "$($pkgPath)\$([io.path]::GetFileNameWithoutExtension($obj.fileName)).adml" - } - - if([IO.File]::Exists($admxFile) -eq $false) - { - Write-Log "ADMX (or ADML) file $($obj.fileName) not found. The ADMXFile object will not be imported." 2 - @{"Import" = $false} - return - } - - #$bytes = [IO.File]::ReadAllBytes($admxFile) - $bytes = Get-ASCIIBytes ([IO.File]::ReadAllText($admxFile)) - $obj.content = [Convert]::ToBase64String($bytes) - - #$bytes = [IO.File]::ReadAllBytes($admlFile) - $bytes = Get-ASCIIBytes ([IO.File]::ReadAllText($admlFile)) - - $obj.groupPolicyUploadedLanguageFiles += [PSCustomObject]@{ - fileName = [io.path]::GetFileName($admlFile) - content = [Convert]::ToBase64String($bytes) - languageCode = (?? $obj.defaultLanguageCode "en-US") - } - $obj.defaultLanguageCode = "" -} - -function Start-PostImportADMXFiles -{ - param($obj, $objectType, $file) - - $script:CustomADMXDefinitions = $null -} - -function Start-PreDeleteADMXFiles -{ - param($obj, $objectType) - - Write-Status "Delete $($obj.fileName)" - $strAPI = ($objectType.API + "/$($obj.Id)/remove") - Write-Log "Delete $($objectType.Title) object $($obj.fileName)" - Invoke-GraphRequest -Url $strAPI -HttpMethod "POST" -ODataMetadata "none" | Out-Null - - @{ "Delete" = $false } -} - -#endregion - -#region Reusable Groups -function Start-PostGetReusableSettings -{ - param($obj, $objectType) - - $strAPI = "$($objectType.API)/$($obj.Object.Id)?`$select=settinginstance,displayname,description" - $tmpObj = Invoke-GraphRequest -Url $strAPI - - if($tmpObj.settingInstance) - { - $obj.Object | Add-Member Noteproperty -Name "settingInstance" -Value $tmpObj.settingInstance -Force - } -} - -#endregon - -#region Authentication Strength -function Start-PreImportCommandAuthenticationStrengths -{ - param($obj, $objectType, $file, $assignments) - - if($obj.policyType -ne "custom") - { - Write-Log "Built-in Authentication Strength objects cannot be imported" 2 - @{ "Import" = $false } - } -} -#endregion - -#region Authentication Strength -function Start-PreImportCommandAuthenticationContext -{ - param($obj, $objectType, $file, $assignments) - - #@{ "Method" = "PATCH" } - -} -#endregion - - +<# +.SYNOPSIS +Module for managing Intune objects + +.DESCRIPTION +This module is for the Endpoint Manager/Intune View. It manages Export/Import/Copy of Intune objects + +.NOTES + Author: Mikael Karlsson +#> +function Get-ModuleVersion +{ + '3.10.0.6' +} + +function Invoke-InitializeModule +{ + #Add settings + $global:appSettingSections += (New-Object PSObject -Property @{ + Title = "Endpoint Manager/Intune" + Id = "EndpointManager" + Values = @() + Priority = 10 + }) + + Add-SettingsObject (New-Object PSObject -Property @{ + Title = "Application" + Key = "EMAzureApp" + Type = "List" + SelectedValuePath = "ClientId" + ItemsSource = $global:MSGraphGlobalApps + DefaultValue = "" + SubPath = "EndpointManager" + }) "EndpointManager" + + Add-SettingsObject (New-Object PSObject -Property @{ + Title = "Application Id" + Key = "EMCustomAppId" + Type = "String" + DefaultValue = "" + SubPath = "EndpointManager" + }) "EndpointManager" + + Add-SettingsObject (New-Object PSObject -Property @{ + Title = "Redirect URL" + Key = "EMCustomAppRedirect" + Type = "String" + DefaultValue = "" + SubPath = "EndpointManager" + }) "EndpointManager" + + Add-SettingsObject (New-Object PSObject -Property @{ + Title = "Tenant Id" + Key = "EMCustomTenantId" + Type = "String" + DefaultValue = "" + SubPath = "EndpointManager" + }) "EndpointManager" + + Add-SettingsObject (New-Object PSObject -Property @{ + Title = "Authority" + Key = "EMCustomAuthority" + Type = "String" + DefaultValue = "" + SubPath = "EndpointManager" + }) "EndpointManager" + + Add-SettingsObject (New-Object PSObject -Property @{ + Title = "App packages folder" + Key = "EMIntuneAppPackages" + Type = "Folder" + Description = "Root folder where intune app packages are located" + SubPath = "EndpointManager" + }) "EndpointManager" + + Add-SettingsObject (New-Object PSObject -Property @{ + Title = "Save Encryption File" + Key = "EMSaveEncryptionFile" + Type = "Boolean" + Description = "Save encryption file when uploading an app. This can then be used to when downloading the app file." + SubPath = "EndpointManager" + }) "EndpointManager" + + Add-SettingsObject (New-Object PSObject -Property @{ + Title = "App download folder" + Key = "EMIntuneAppDownloadFolder" + Type = "Folder" + Description = "Folder where app packages will be downloaded and where encryption files will be saved" + SubPath = "EndpointManager" + }) "EndpointManager" + + Get-SettingValue "ProxyURI" + + if($global:FirstTimeRunning) { + Save-Setting "EndpointManager" "EMAzureApp" $global:DefaultAzureApp + } + + $currentAppID = Get-SettingValue "EMAzureApp" + $customAppID = Get-SettingValue "EMCustomAppId" + $global:informOldAzureApp = $false + + if(($global:OldAzureApps -is [Array] -and $currentAppID -in $global:OldAzureApps) -or (-not $currentAppID -and -not $customAppID)) + { + $global:informOldAzureApp = $true + 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 + + #Add menu group and items + $global:EMViewObject = (New-Object PSObject -Property @{ + Title = "Intune Manager" + Description = "Manages Intune environments. This view can be used for copying objects in an Intune environment. It can also be used for backing up an entire Intune environment and cloning the Intune environment into another tenant." + ID="IntuneGraphAPI" + ViewPanel = $viewPanel + AuthenticationID = "MSAL" + ItemChanged = { Show-GraphObjects -ObjectTypeChanged; Invoke-ModuleFunction "Invoke-GraphObjectsChanged"; Write-Status ""} + Deactivating = { Invoke-EMDeactivateView } + Activating = { Invoke-EMActivatingView } + Authentication = (Get-MSALAuthenticationObject) + Authenticate = { Invoke-EMAuthenticateToMSAL @args } + AppInfo = (Get-GraphAppInfo "EMAzureApp" $global:DefaultAzureApp "EM") + SaveSettings = { Invoke-EMSaveSettings } + + Permissions = @() + }) + + Add-ViewObject $global:EMViewObject + + Add-ViewItem (New-Object PSObject -Property @{ + Title = "Device Configuration" + Id = "DeviceConfiguration" + ViewID = "IntuneGraphAPI" + API = "/deviceManagement/deviceConfigurations" + QUERYLIST = "`$filter=not%20isof(%27microsoft.graph.windowsUpdateForBusinessConfiguration%27)%20and%20not%20isof(%27microsoft.graph.iosUpdateConfiguration%27)" + #ExportFullObject = $false + Permissons=@("DeviceManagementConfiguration.ReadWrite.All") + PropertiesToRemove = @("privacyAccessControls") + PostFileImportCommand = { Start-PostFileImportDeviceConfiguration @args } + PostCopyCommand = { Start-PostCopyDeviceConfiguration @args } + PostGetCommand = { Start-PostGetDeviceConfiguration @args } + GroupId = "DeviceConfiguration" + NavigationProperties=$true + }) + + Add-ViewItem (New-Object PSObject -Property @{ + Title = "Conditional Access" + Id = "ConditionalAccess" + ViewID = "IntuneGraphAPI" + API = "/identity/conditionalAccess/policies" + Permissons=@("Policy.Read.All","Policy.ReadWrite.ConditionalAccess","Application.Read.All") + Dependencies = @("NamedLocations","Applications","TermsOfUse","AuthenticationStrengths","AssignmentFilters") + GroupId = "ConditionalAccess" + ImportExtension = { Add-ConditionalAccessImportExtensions @args } + PreImportCommand = { Start-PreImportConditionalAccess @args } + PostExportCommand = { Start-PostExportConditionalAccess @args } + ExpandAssignmentsList = $false + SupportsPageSize = $false + }) + + Add-ViewItem (New-Object PSObject -Property @{ + Title = "Terms of use" + Id = "TermsOfUse" + ViewID = "IntuneGraphAPI" + ViewProperties = @("id", "displayName") + Expand = "files" + QUERYLIST = "`$expand=files" + API = "/identityGovernance/termsOfUse/agreements" + Permissons=@("Agreement.ReadWrite.All") + PreImportCommand = { Start-PreImportTermsOfUse @args } + PostExportCommand = { Start-PostExportTermsOfUse @args } + GroupId = "ConditionalAccess" + SupportsPageSize = $false + }) + + Add-ViewItem (New-Object PSObject -Property @{ + Title = "Named Locations" + Id = "NamedLocations" + ViewID = "IntuneGraphAPI" + API = "/identity/conditionalAccess/namedLocations" + Permissons=@("Policy.ReadWrite.ConditionalAccess") + ImportOrder = 50 + GroupId = "ConditionalAccess" + ExpandAssignmentsList = $false + SupportsPageSize = $false + }) + + Add-ViewItem (New-Object PSObject -Property @{ + Title = "Endpoint Security" + Id = "EndpointSecurity" + ViewID = "IntuneGraphAPI" + API = "/deviceManagement/intents" + PropertiesToRemove = @('Settings','@OData.Type') + PreImportCommand = { Start-PreImportEndpointSecurity @args } + PostListCommand = { Start-PostListEndpointSecurity @args } + PostExportCommand = { Start-PostExportEndpointSecurity @args } + PostFileImportCommand = { Start-PostFileImportEndpointSecurity @args } + PostGetCommand = { Start-PostGetEndpointSecurity @args } + #PreCopyCommand = { Start-PreCopyEndpointSecurity @args } + PostCopyCommand = { Start-PostCopyEndpointSecurity @args } + PreUpdateCommand = { Start-PreUpdateEndpointSecurity @args } + Permissons=@("DeviceManagementConfiguration.ReadWrite.All") + Dependencies = @("ReusableSettings") + GroupId = "EndpointSecurity" + SupportsPageSize = $false + }) + + Add-ViewItem (New-Object PSObject -Property @{ + Title = "Compliance Policies" + Id = "CompliancePolicies" + ViewID = "IntuneGraphAPI" + Expand = "scheduledActionsForRule(`$expand=scheduledActionConfigurations)" + API = "/deviceManagement/deviceCompliancePolicies" + Permissons=@("DeviceManagementConfiguration.ReadWrite.All") + Dependencies = @("Locations","Notifications","ComplianceScripts") + PostExportCommand = { Start-PostExportCompliancePolicies @args } + PreUpdateCommand = { Start-PreUpdateCompliancePolicies @args } + GroupId = "CompliancePolicies" + SupportsPageSize = $false + }) + + Add-ViewItem (New-Object PSObject -Property @{ + Title = "Compliance Policies - V2" + Id = "CompliancePoliciesV2" + ViewID = "IntuneGraphAPI" + API = "/deviceManagement/compliancePolicies" + NameProperty = "name" + PropertiesToRemove = @('settingCount') + ViewProperties = @("name","description","Id") + Expand="settings" + Permissons=@("DeviceManagementConfiguration.ReadWrite.All") + GroupId = "CompliancePolicies" + Icon = "CompliancePolicies" + }) + + + Add-ViewItem (New-Object PSObject -Property @{ + Title = "Compliance Scripts" + Id = "ComplianceScripts" + ViewID = "IntuneGraphAPI" + API = "/deviceManagement/deviceComplianceScripts" + PostImportCommand = { Start-PostImportComplianceScripts @args } + Permissons=@("DeviceManagementScripts.ReadWrite.All") + GroupId = "CompliancePolicies" + Icon = "Scripts" + ImportOrder = 80 + }) + + Add-ViewItem (New-Object PSObject -Property @{ + Title = "Intune Branding" + Id = "IntuneBranding" + API = "/deviceManagement/intuneBrandingProfiles" + ViewID = "IntuneGraphAPI" + NameProperty = "profileName" + ViewProperties = @("profileName", "displayName", "description", "id","isDefaultProfile") + PreImportCommand = { Start-PreImportIntuneBranding @args } + PostImportCommand = { Start-PostImportIntuneBranding @args } + PostGetCommand = { Start-PostGetIntuneBranding @args } + PostExportCommand = { Start-PostExportIntuneBranding @args } + PreDeleteCommand = { Start-PreDeleteIntuneBranding @args } + PreUpdateCommand = { Start-PreUpdateIntuneBranding @args } + Permissons=@("DeviceManagementApps.ReadWrite.All") + Icon = "Branding" + SkipRemoveProperties = @('Id') # Id is removed by PreImport. Required for default profile + PropertiesToRemoveForUpdate = @('isDefaultProfile','disableClientTelemetry') + GroupId = "TenantAdmin" + SupportsPageSize = $false + }) + + <# + # BUG in Graph? Cannot create default branding. Can only create it when importing another object + # Header required Accept-Language: sv-SE + # Documentation says to use Content-Language but that doesn't work + + # Could work with https://main.iam.ad.ext.azure.com/api/LoginTenantBrandings + + #> + + Add-ViewItem (New-Object PSObject -Property @{ + Title = "Azure Branding" + Id = "AzureBranding" + API = "/organization/%OrganizationId%/branding/localizations" + ViewID = "IntuneGraphAPI" + ViewProperties = @("Id") + PreImportCommand = { Start-PreImportAzureBranding @args } + PostListCommand = { Start-PostListAzureBranding @args } + ShowButtons = @("Export","View") + NameProperty = "Id" + Permissons=@("Organization.ReadWrite.All") + Icon = "Branding" + SkipRemoveProperties = @('Id') + GroupId = "Azure" + SkipAddIDOnExport = $true + ExpandAssignmentsList = $false + SupportsPageSize = $false + }) + + Add-ViewItem (New-Object PSObject -Property @{ + Title = "Enrollment Status Page" + Id = "EnrollmentStatusPage" + API = "/deviceManagement/deviceEnrollmentConfigurations" + ViewID = "IntuneGraphAPI" + PreImportCommand = { Start-PreImportESP @args } + PostExportCommand = { Start-PostExportESP @args } + PreDeleteCommand = { Start-PreDeleteEnrollmentRestrictions @args } # Note: Uses same PreDelete as restrictions + PreReplaceCommand = { Start-PreReplaceEnrollmentRestrictions @args } # Note: Uses same PreReplaceCommand as restrictions + PostReplaceCommand = { Start-PostReplaceEnrollmentRestrictions @args } # Note: Uses same PostReplaceCommand as restrictions + PreFilesImportCommand = { Start-PreFilesImportEnrollmentRestrictions @args } # Note: Uses same PreFilesImportCommand as restrictions + PreImportAssignmentsCommand = { Start-PreImportAssignmentsEnrollmentRestrictions @args } # Note: Uses same PreFilesImportCommand as restrictions + PostListCommand = { Start-PostListESP @args } + #PreUpdateCommand = { Start-PreUpdateEnrollmentRestrictions @args } # Note: Uses same PreUpdateCommand as restrictions + #QUERYLIST = "`$filter=endsWith(id,'Windows10EnrollmentCompletionPageConfiguration')" + Permissons=@("DeviceManagementServiceConfig.ReadWrite.All") + SkipRemoveProperties = @('Id') + Dependencies = @("Applications") + AssignmentsType = "enrollmentConfigurationAssignments" + PropertiesToRemoveForUpdate = @('priority') + GroupId = "WinEnrollment" + SupportsPageSize = $false + }) + + Add-ViewItem (New-Object PSObject -Property @{ + Title = "Enrollment Restrictions" + Id = "EnrollmentRestrictions" + API = "/deviceManagement/deviceEnrollmentConfigurations" + ViewID = "IntuneGraphAPI" + #QUERYLIST = "`$filter=not endsWith(id,'Windows10EnrollmentCompletionPageConfiguration')" + PostExportCommand = { Start-PostExportEnrollmentRestrictions @args } + PreImportCommand = { Start-PreImportEnrollmentRestrictions @args } + PreDeleteCommand = { Start-PreDeleteEnrollmentRestrictions @args } + PreReplaceCommand = { Start-PreReplaceEnrollmentRestrictions @args } + PostReplaceCommand = { Start-PostReplaceEnrollmentRestrictions @args } + PreFilesImportCommand = { Start-PreFilesImportEnrollmentRestrictions @args } + PostListCommand = { Start-PostListEnrollmentRestrictions @args } + PreImportAssignmentsCommand = { Start-PreImportAssignmentsEnrollmentRestrictions @args } + #PreUpdateCommand = { Start-PreUpdateEnrollmentRestrictions @args } + PropertiesToRemoveForUpdate = @('priority') + Permissons=@("DeviceManagementServiceConfig.ReadWrite.All") + SkipRemoveProperties = @('Id') + AssignmentsType = "enrollmentConfigurationAssignments" + GroupId = "EnrollmentRestrictions" + ViewProperties = @("displayName","platformType","description","Id") + SupportsPageSize = $false + }) + + Add-ViewItem (New-Object PSObject -Property @{ + Title = "Co-Management Settings" + Id = "CoManagementSettings" + ViewID = "IntuneGraphAPI" + API = "/deviceManagement/deviceEnrollmentConfigurations" + PostReplaceCommand = { Start-PostReplaceEnrollmentRestrictions @args } # Note: Uses same PostReplaceCommand as restrictions + PreFilesImportCommand = { Start-PreFilesImportEnrollmentRestrictions @args } # Note: Uses same PreFilesImportCommand as restrictions + PostListCommand = { Start-PostListCoManagementSettings @args } + PropertiesToRemoveForUpdate = @('priority') + Permissons=@("DeviceManagementServiceConfig.ReadWrite.All") + SkipRemoveProperties = @('Id') + GroupId = "WinEnrollment" + Icon = "EnrollmentStatusPage" + SupportsPageSize = $false + }) + + Add-ViewItem (New-Object PSObject -Property @{ + Title = "Administrative Templates" + Id = "AdministrativeTemplates" + API = "/deviceManagement/groupPolicyConfigurations" + ViewID = "IntuneGraphAPI" + PostGetCommand = { Start-PostGetAdministrativeTemplate @args } + PostExportCommand = { Start-PostExportAdministrativeTemplate @args } + PostCopyCommand = { Start-PostCopyAdministrativeTemplate @args } + PostFileImportCommand = { Start-PostFileImportAdministrativeTemplate @args } + PreImportCommand = { Start-PreImportAdministrativeTemplate @args } + LoadObject = { Start-LoadAdministrativeTemplate @args } + PropertiesToRemove = @("definitionValues","policyConfigurationIngestionType") + Permissons=@("DeviceManagementConfiguration.ReadWrite.All") + Icon="DeviceConfiguration" + GroupId = "DeviceConfiguration" + CompareValue = "CombinedValueWithLabel" + Dependencies = @("ADMXFiles") + }) + + Add-ViewItem (New-Object PSObject -Property @{ + Title = "Scripts (PowerShell)" + Id = "PowerShellScripts" + API = "/deviceManagement/deviceManagementScripts" + ViewID = "IntuneGraphAPI" + DetailExtension = { Add-ScriptExtensions @args } + ExportExtension = { Add-ScriptExportExtensions @args } + PostExportCommand = { Start-PostExportScripts @args } + Permissons=@("DeviceManagementScripts.ReadWrite.All") + AssignmentsType = "deviceManagementScriptAssignments" + Icon="Scripts" + GroupId = "Scripts" + }) + + Add-ViewItem (New-Object PSObject -Property @{ + Title = "Scripts (Shell)" + Id = "MacScripts" + API = "/deviceManagement/deviceShellScripts" + ViewID = "IntuneGraphAPI" + DetailExtension = { Add-ScriptExtensions @args } + ExportExtension = { Add-ScriptExportExtensions @args } + PostExportCommand = { Start-PostExportScripts @args } + Permissons=@("DeviceManagementScripts.ReadWrite.All") + AssignmentsType = "deviceManagementScriptAssignments" + Icon="Scripts" + GroupId = "Scripts" + }) + + Add-ViewItem (New-Object PSObject -Property @{ + Title = "Custom Attributes" + Id = "MacCustomAttributes" + API = "/deviceManagement/deviceCustomAttributeShellScripts" + ViewID = "IntuneGraphAPI" + Permissons=@("DeviceManagementScripts.ReadWrite.All") + AssignmentsType = "deviceManagementScriptAssignments" + Icon="CustomAttributes" + GroupId = "CustomAttributes" # MacOS Settings + DetailExtension = { Add-ScriptExtensions @args } + ExportExtension = { Add-ScriptExportExtensions @args } + PostExportCommand = { Start-PostExportScripts @args } + PropertiesToRemoveForUpdate = @('customAttributeName','customAttributeType','displayName') + #PreUpdateCommand = { Start-PreUpdateMacCustomAttributes @args } + }) + + Add-ViewItem (New-Object PSObject -Property @{ + Title = "Terms and Conditions" + Id = "TermsAndConditions" + API = "/deviceManagement/termsAndConditions" + ViewID = "IntuneGraphAPI" + Permissons=@("DeviceManagementServiceConfig.ReadWrite.All") + ExpandAssignments = $false # Not supported for this object type + PostExportCommand = { Start-PostExportTermsAndConditions @args } + PreImportAssignmentsCommand = { Start-PreImportAssignmentsTermsAndConditions @args } + GroupId = "TenantAdmin" + ExpandAssignmentsList = $false + SupportsPageSize = $false + }) + + Add-ViewItem (New-Object PSObject -Property @{ + Title = "App Protection" + Id = "AppProtection" + API = "/deviceAppManagement/managedAppPolicies" + ViewID = "IntuneGraphAPI" + PreGetCommand = { Start-GetAppProtection @args } + PostListCommand = { Start-PostListAppProtection @args } + PreImportCommand = { Start-PreImportAppProtection @args } + PostImportCommand = { Start-PostImportAppProtection @args } + PreImportAssignmentsCommand = { Start-PreImportAssignmentsAppProtection @args } + PreUpdateCommand = { Start-PreUpdateAppProtection @args } + ExportFullObject = $true + PropertiesToRemove = @('exemptAppLockerFiles') + PropertiesToRemoveForUpdate = @("protectedAppLockerFiles","version") # ToDo: !!! Add support for protectedAppLockerFiles? + Permissons=@("DeviceManagementApps.ReadWrite.All") + Dependencies = @("Applications") + GroupId = "AppProtection" + ExpandAssignmentsList = $false + SupportsPageSize = $false + }) + + # These are also included in the managedAppPolicies API + # So all custom commands will be handled by the same functions as App Protection + Add-ViewItem (New-Object PSObject -Property @{ + Title = "App Configuration (App)" + Id = "AppConfigurationManagedApp" + API = "/deviceAppManagement/targetedManagedAppConfigurations" + ViewID = "IntuneGraphAPI" + PreGetCommand = { Start-GetAppProtection @args } + PreImportCommand = { Start-PreImportAppProtection @args } + PostImportCommand = { Start-PostImportAppProtection @args } + PreImportAssignmentsCommand = { Start-PreImportAssignmentsAppProtection @args } + PreUpdateCommand = { Start-PreUpdateAppConfigurationApp @args } + Permissons=@("DeviceManagementApps.ReadWrite.All") + Dependencies = @("Applications") + Icon = "AppConfiguration" + GroupId = "AppConfiguration" + ExpandAssignmentsList = $false + SupportsPageSize = $false + }) + + Add-ViewItem (New-Object PSObject -Property @{ + Title = "App Configuration (Device)" + Id = "AppConfigurationManagedDevice" + API = "/deviceAppManagement/mobileAppConfigurations" + QUERYLIST = "`$filter=microsoft.graph.androidManagedStoreAppConfiguration/appSupportsOemConfig%20eq%20false%20or%20isof(%27microsoft.graph.androidManagedStoreAppConfiguration%27)%20eq%20false" + ViewID = "IntuneGraphAPI" + Permissons=@("DeviceManagementApps.ReadWrite.All") + Dependencies = @("Applications") + PreFilesImportCommand = { Start-PreFilesImportAppConfiguration @args } + PreImportAssignmentsCommand = { Start-PreImportAssignmentsAppConfiguration @args } + PostExportCommand = { Start-PostExportAppConfiguration @args } + Icon = "AppConfiguration" + GroupId = "AppConfiguration" + }) + + Add-ViewItem (New-Object PSObject -Property @{ + Title = "Applications" + Id = "Applications" + API = "/deviceAppManagement/mobileApps" + ViewID = "IntuneGraphAPI" + PropertiesToRemove = @('uploadState','publishingState','isAssigned','dependentAppCount','supersedingAppCount','supersededAppCount','committedContentVersion','isFeatured','size','categories') #,'minimumSupportedWindowsRelease' + QUERYLIST = "`$filter=(microsoft.graph.managedApp/appAvailability%20eq%20null%20or%20microsoft.graph.managedApp/appAvailability%20eq%20%27lineOfBusiness%27%20or%20isAssigned%20eq%20true)&`$orderby=displayName" + QuerySearch=$true + Permissons=@("DeviceManagementApps.ReadWrite.All") + AssignmentsType="mobileAppAssignments" + AssignmentProperties = @("@odata.type","target","settings","intent") + AssignmentTargetProperties = @("@odata.type","groupId","deviceAndAppManagementAssignmentFilterId","deviceAndAppManagementAssignmentFilterType") + ImportOrder = 60 + Expand="categories,assignments" # ODataMetadata is set to minimal so assignments can't be autodetected + ODataMetadata="minimal" # categories property not supported with ODataMetadata full + PostFileImportCommand = { Start-PostFileImportApplication @args } + PostCopyCommand = { Start-PostCopyApplication @args } + PreUpdateCommand = { Start-PreUpdateApplication @args } + PreImportCommand = { Start-PreImportCommandApplication @args } + DetailExtension = { Add-DetailExtensionApplications @args } + PreImportAssignmentsCommand = { Start-PreImportAssignmentsApplications @args } + PreDeleteCommand = { Start-PreDeleteApplications @args } + PostExportCommand = { Start-PostExportApplications @args } + PostListCommand = { Start-PostListApplications @args } + ExportExtension = { Add-ScriptExportApplications @args } + PostGetCommand = { Start-PostGetApplications @args } + PostImportCommand = { Start-PostImportApplications @args } + PostFilesImportCommand = { Start-PostFilesImportApplications @args } + GroupId = "Apps" + ScopeTagsReturnedInList = $false + }) + + Add-ViewItem (New-Object PSObject -Property @{ + Title = "Autopilot" + Id = "AutoPilot" + API = "/deviceManagement/windowsAutopilotDeploymentProfiles" + ViewID = "IntuneGraphAPI" + CopyDefaultName = "%displayName% Copy" # '-' is not allowed in the name + Permissons=@("DeviceManagementServiceConfig.ReadWrite.All") + PreImportAssignmentsCommand = { Start-PreImportAssignmentsAutoPilot @args } + PreDeleteCommand = { Start-PreDeleteAutoPilot @args } + PropertiesToRemoveForUpdate = @('managementServiceAppId') + GroupId = "WinEnrollment" + }) + + Add-ViewItem (New-Object PSObject -Property @{ + Title = "Policy Sets" + Id = "PolicySets" + API = "/deviceAppManagement/policySets" + ViewID = "IntuneGraphAPI" + Expand = "Items" + PreImportAssignmentsCommand = { Start-PreImportAssignmentsPolicySets @args } + PreImportCommand = { Start-PreImportPolicySets @args } + PreUpdateCommand = { Start-PreUpdatePolicySets @args } + PostListCommand = { Start-PostListPolicySets @args } + Permissons=@("DeviceManagementConfiguration.ReadWrite.All") + ImportOrder = 2000 # Policy Sets reference other objects so make sure it is imported last + Dependencies = @("Applications","AppConfiguration","AppProtection","AutoPilot","EnrollmentRestrictions","EnrollmentStatusPage","DeviceConfiguration","AdministrativeTemplates","SettingsCatalog","CompliancePolicies") + GroupId = "PolicySets" + ExpandAssignmentsList = $false # expand is not allowed, IsAssigned is set in PostListCommand + }) + + Add-ViewItem (New-Object PSObject -Property @{ + Title = "Update Policies" + Id = "UpdatePolicies" + ViewID = "IntuneGraphAPI" + API = "/deviceManagement/deviceConfigurations" + QUERYLIST = "`$filter=isof(%27microsoft.graph.windowsUpdateForBusinessConfiguration%27)%20or%20isof(%27microsoft.graph.iosUpdateConfiguration%27)" + #ExportFullObject = $false + Permissons=@("DeviceManagementConfiguration.ReadWrite.All") + GroupId = "WinUpdatePolicies" + PropertiesToRemoveForUpdate = @('version','qualityUpdatesPauseStartDate','featureUpdatesPauseStartDate','qualityUpdatesWillBeRolledBack','featureUpdatesWillBeRolledBack') + }) + + Add-ViewItem (New-Object PSObject -Property @{ + Title = "Feature Updates" + Id = "FeatureUpdates" + ViewID = "IntuneGraphAPI" + API = "/deviceManagement/windowsFeatureUpdateProfiles" + Permissons=@("DeviceManagementConfiguration.ReadWrite.All") + GroupId = "WinFeatureUpdates" + PropertiesToRemoveForUpdate = @('deployableContentDisplayName','endOfSupportDate') + #PreUpdateCommand = { Start-PreUpdateFeatureUpdates @args } + }) + + Add-ViewItem (New-Object PSObject -Property @{ + Title = "Quality Updates (Profiles)" + Id = "QualityUpdates" + ViewID = "IntuneGraphAPI" + API = "/deviceManagement/windowsQualityUpdateProfiles" + Permissons=@("DeviceManagementConfiguration.ReadWrite.All") + Icon = "UpdatePolicies" + GroupId = "WinQualityUpdates" + PropertiesToRemoveForUpdate = @('releaseDateDisplayName','deployableContentDisplayName') + SupportsPageSize = $false + }) + + Add-ViewItem (New-Object PSObject -Property @{ + Title = "Quality Updates (Policies)" + Id = "QualityUpdatePolicies" + ViewID = "IntuneGraphAPI" + API = "/deviceManagement/windowsQualityUpdatePolicies" + Permissons=@("DeviceManagementConfiguration.ReadWrite.All") + Icon = "UpdatePolicies" + GroupId = "WinQualityUpdates" + SupportsPageSize = $false + }) + + # Locations are not FULLY supported + # They will be imported but Compliance Policies will not be updated with new Location object after import + # ToDo: Add support Export/Import Location Settings + # Location object - Only used by Android Device Admins Compliance Policies + # - These should probably be migrated to Android Enterprise anyway. That is the recommendation by Google + # Property that needs to be updated on the Compliance Policy + # deviceManagement/managementConditionStatements/$obj.conditionStatementId + + # Location objects support removed from Intune + <# + Add-ViewItem (New-Object PSObject -Property @{ + Title = "Locations" + Id = "Locations" + ViewID = "IntuneGraphAPI" + API = "/deviceManagement/managementConditions" + Permissons=@("DeviceManagementConfiguration.ReadWrite.All") + PreImportCommand = { Start-PreImportLocations @args } + ImportOrder = 30 + GroupId = "CompliancePolicies" + }) + #> + + Add-ViewItem (New-Object PSObject -Property @{ + Title = "Settings Catalog" + Id = "SettingsCatalog" + ViewID = "IntuneGraphAPI" + API = "/deviceManagement/configurationPolicies" + PropertiesToRemove = @('settingCount') + Permissons=@("DeviceManagementConfiguration.ReadWrite.All") + NameProperty = "name" + ViewProperties = @("name","description","Id") + Expand="Settings" + Icon="DeviceConfiguration" + PreImportCommand = { Start-PreImportSettingsCatalog @args } + PostExportCommand = { Start-PostExportSettingsCatalog @args } + PreUpdateCommand = { Start-PreUpdateSettingsCatalog @args } + PostGetCommand = { Start-PostGetSettingsCatalog @args } + Dependencies = @("ReusableSettings") + GroupId = "DeviceConfiguration" + }) + + Add-ViewItem (New-Object PSObject -Property @{ + Title = "Inventory Policies" + Id = "InventoryPolicies" + ViewID = "IntuneGraphAPI" + API = "/deviceManagement/inventoryPolicies" + PropertiesToRemove = @('settingCount') + Permissons=@("DeviceManagementConfiguration.ReadWrite.All") + NameProperty = "name" + ViewProperties = @("name","description","Id") + Expand="Settings" + Icon="DeviceConfiguration" + GroupId = "DeviceConfiguration" + }) + + Add-ViewItem (New-Object PSObject -Property @{ + Title = "BIOS Configurations" + Id = "HardwareConfigurations" + ViewID = "IntuneGraphAPI" + DetailExtension = { Add-PolicyFileExtensions @args } + ExportExtension = { Add-PolicyFileExportExtensions @args } + PostExportCommand = { Start-PostExportPolicyFile @args } + PropertiesToRemoveForUpdate = @('version') + PolicyFileAttribute = "configurationFileContent" + API = "/deviceManagement/hardwareConfigurations" + Permissons=@("DeviceManagementConfiguration.ReadWrite.All") + Icon="DeviceConfiguration" + GroupId = "DeviceConfiguration" + SupportsPageSize = $false + }) + + Add-ViewItem (New-Object PSObject -Property @{ + Title = "Role Definitions" + Id = "RoleDefinitions" + ViewID = "IntuneGraphAPI" + API = "/deviceManagement/roleDefinitions" + QUERYLIST = "`$filter=isBuiltIn%20eq%20false" + PostExportCommand = { Start-PostExportRoleDefinitions @args } + PreImportCommand = { Start-PreImportRoleDefinitions @args } + PostFileImportCommand = { Start-PostFileImportRoleDefinitions @args } + Permissons=@("DeviceManagementRBAC.ReadWrite.All") + ImportOrder = 20 + #expand=roleassignments + PropertiesToRemoveForUpdate = @('isBuiltInRoleDefinition','isBuiltIn','roleAssignments') ### !!! ToDo: Add support for roleAssignments + GroupId = "TenantAdmin" + ExpandAssignments = $false + ExpandAssignmentsList = $false + }) + + Add-ViewItem (New-Object PSObject -Property @{ + Title = "Scope (Tags)" + Id = "ScopeTags" + ViewID = "IntuneGraphAPI" + API = "/deviceManagement/roleScopeTags" + QUERYLIST = "`$filter=isBuiltIn%20eq%20false" + Permissons=@("DeviceManagementRBAC.ReadWrite.All") + PostExportCommand = { Start-PostExportScopeTags @args } + PostGetCommand = { Start-PostGetScopeTags @args } + ImportOrder = 10 + DocumentAll = $true + GroupId = "TenantAdmin" + ExpandAssignmentsList = $false # Adds the assignmnets property but always empty + }) + + Add-ViewItem (New-Object PSObject -Property @{ + Title = "Notifications" + Id = "Notifications" + ViewID = "IntuneGraphAPI" + API = "/deviceManagement/notificationMessageTemplates" + Permissons=@("DeviceManagementServiceConfig.ReadWrite.All") + ImportOrder = 40 + Expand = "localizedNotificationMessages" + PreImportCommand = { Start-PreImportNotifications @args } + PostFileImportCommand = { Start-PostFileImportNotifications @args } + PostCopyCommand = { Start-PostCopyNotifications @args } + PropertiesToRemoveForUpdate = @('defaultLocale','localizedNotificationMessages') ### !!! ToDo: Add support for localizedNotificationMessages + GroupId = "CompliancePolicies" + ExpandAssignmentsList = $false + }) + + # This has some pre-reqs for working! + # Import is tested and verified in a tenant with Googple Play connection configured + # And the OEM app was dpwnloaded e.g. Knox Service Plugin + # Import failed in a tenant where Google Play was NOT configured + Add-ViewItem (New-Object PSObject -Property @{ + Title = "Android OEM Config" + Id = "AndroidOEMConfig" + ViewID = "IntuneGraphAPI" + QUERYLIST = "`$filter=microsoft.graph.androidManagedStoreAppConfiguration/appSupportsOemConfig%20eq%20true" + API = "/deviceAppManagement/mobileAppConfigurations" + PreImportAssignmentsCommand = { Start-PreImportAssignmentsAppConfiguration @args } + PreFilesImportCommand = { Start-PreFilesImportAppConfiguration @args } + PostExportCommand = { Start-PostExportAppConfiguration @args } + Permissons=@("DeviceManagementConfiguration.ReadWrite.All") + Icon="DeviceConfiguration" + Dependencies = @("Applications") + GroupId = "DeviceConfiguration" + }) + + # Copy/Export/Import not verified! + Add-ViewItem (New-Object PSObject -Property @{ + Title = "Apple Enrollment Types" + Id = "AppleEnrollmentTypes" + ViewID = "IntuneGraphAPI" + API = "/deviceManagement/appleUserInitiatedEnrollmentProfiles" + Permissons=@("DeviceManagementServiceConfig.ReadWrite.All") + PropertiesToRemoveForUpdate = @('platform') + GroupId = "AppleEnrollment" + }) + + Add-ViewItem (New-Object PSObject -Property @{ + Title = "Filters" + Id = "AssignmentFilters" + ViewID = "IntuneGraphAPI" + API = "/deviceManagement/assignmentFilters" + Permissons=@("DeviceManagementConfiguration.ReadWrite.All") + ImportOrder = 15 + GroupId = "TenantAdmin" + PropertiesToRemoveForUpdate = @('platform') + ExpandAssignmentsList = $false + PropertiesToRemove = @("payloads") + }) + + Add-ViewItem (New-Object PSObject -Property @{ + Title = "Health Scripts" + Id = "DeviceHealthScripts" + ViewID = "IntuneGraphAPI" + QUERYLIST = "`$filter=isGlobalScript%20eq%20false" # Looks like filters are not working for deviceHealthScripts + API = "/deviceManagement/deviceHealthScripts" + PreDeleteCommand = { Start-PreDeleteDeviceHealthScripts @args } + PreImportCommand = { Start-PreImportDeviceHealthScripts @args } + PreUpdateCommand = { Start-PreUpdateDeviceHealthScripts @args } + PostExportCommand = { Start-PostExportDeviceHealthScripts @args } + ExportExtension = { Add-ScriptExportExtensions @args } + Permissons=@("DeviceManagementScripts.ReadWrite.All") + GroupId = "EndpointAnalytics" + Icon = "Report" + AssignmentsType = "deviceHealthScriptAssignments" + AssignmentProperties = @("target","runSchedule","runRemediationScript") + PropertiesToRemoveForUpdate = @('version','isGlobalScript','highestAvailableVersion') + }) + + Add-ViewItem (New-Object PSObject -Property @{ + Title = "ADMX Files" + Id = "ADMXFiles" + ViewID = "IntuneGraphAPI" + NameProperty = "fileName" + API = "/deviceManagement/groupPolicyUploadedDefinitionFiles" + Permissons=@("DeviceManagementConfiguration.ReadWrite.All") + ImportOrder = 45 + GroupId = "DeviceConfiguration" + Icon = "DeviceConfiguration" + ExpandAssignmentsList = $false + PreFilesImportCommand = { Start-PreFilesImportADMXFiles @args } + PreImportCommand = { Start-PreImportADMXFiles @args } + PostImportCommand = { Start-PostImportADMXFiles @args } + PreDeleteCommand = { Start-PreDeleteADMXFiles @args } + ViewProperties = @("fileName","status","Id") + PropertiesToRemove = @("languageCodes","targetPrefix","targetNamespace","policyType","revision","status","uploadDateTime") + SupportsPageSize = $false + }) + + <# + Add-ViewItem (New-Object PSObject -Property @{ + Title = "iOS Enrollment Profile" + Id = "iOSDepProfile" + ViewID = "IntuneGraphAPI" + API = "/deviceManagement/depIOSEnrollmentProfile" + Permissons=@("DeviceManagementServiceConfig.ReadWrite.All") + GroupId = "DeviceConfiguration" + Icon = "DeviceConfiguration" + ExpandAssignmentsList = $false + ViewProperties = @("fileName","status","Id") + }) + #> + + Add-ViewItem (New-Object PSObject -Property @{ + Title = "Reusable Settings" + Id = "ReusableSettings" + ViewID = "IntuneGraphAPI" + API = "/deviceManagement/reusablePolicySettings" + PropertiesToRemove = @('Settings','@OData.Type') + PostGetCommand = { Start-PostGetReusableSettings @args } + ImportOrder = 70 + Permissons=@("DeviceManagementConfiguration.ReadWrite.All") + ExpandAssignmentsList = $false + SkipRemoveProperties = @("@OData.Type") + Icon = "EndpointSecurity" + GroupId = "EndpointSecurity" + }) + + Add-ViewItem (New-Object PSObject -Property @{ + Title = "Authentication Strengths" + Id = "AuthenticationStrengths" + ViewID = "IntuneGraphAPI" + API = "/identity/conditionalAccess/authenticationStrengths/policies" + PreImportCommand = { Start-PreImportCommandAuthenticationStrengths @args } + PropertiesToRemove = @() + ImportOrder = 45 + Permissons=@("Policy.ReadWrite.ConditionalAccess") + ExpandAssignmentsList = $false + Icon = "ConditionalAccess" + GroupId = "EndpointSecurity" + SupportsPageSize = $false + }) + + Add-ViewItem (New-Object PSObject -Property @{ + Title = "Authentication Context" + Id = "AuthenticationContext" + ViewID = "IntuneGraphAPI" + API = "/identity/conditionalAccess/authenticationContextClassReferences" + PropertiesToRemove = @("@odata.type") + SkipRemoveProperties = @('Id') + ImportOrder = 46 + PreImportCommand = { Start-PreImportCommandAuthenticationContext @args } + Permissons=@("Policy.ReadWrite.ConditionalAccess") + ExpandAssignmentsList = $false + Icon = "ConditionalAccess" + GroupId = "EndpointSecurity" + SupportsPageSize = $false + }) + + Add-ViewItem (New-Object PSObject -Property @{ + Title = "W365 Provisioning Policies" + Id = "W365ProvisioningPolicies" + ViewID = "IntuneGraphAPI" + API = "/deviceManagement/virtualEndpoint/provisioningPolicies" + Permissons=@("CloudPC.ReadWrite.All") + Icon = "Devices" + GroupId = "DeviceConfiguration" + }) + + Add-ViewItem (New-Object PSObject -Property @{ + Title = "W365 User Settings" + Id = "W365UserSettings" + ViewID = "IntuneGraphAPI" + API = "/deviceManagement/virtualEndpoint/userSettings" + Permissons = @("CloudPC.ReadWrite.All") + Icon = "Devices" + GroupId = "DeviceConfiguration" + }) + + Add-ViewItem (New-Object PSObject -Property @{ + Title = "Driver Update Profiles" + Id = "DriverUpdateProfiles" + ViewID = "IntuneGraphAPI" + API = "/deviceManagement/windowsDriverUpdateProfiles" + Permissons = @("DeviceManagementConfiguration.ReadWrite.All") + Icon = "UpdatePolicies" + GroupId = "WinDriverUpdatePolicies" + }) + + Add-ViewItem (New-Object PSObject -Property @{ + Title = "Device Categories" + Id = "DeviceCategories" + ViewID = "IntuneGraphAPI" + API = "/deviceManagement/deviceCategories" + QUERYLIST = "`$top=500" + Permissons = @("DeviceManagementConfiguration.ReadWrite.All") + GroupId = "DeviceConfiguration" + ExpandAssignmentsList = $false + }) + +} + +function Invoke-EMAuthenticateToMSAL +{ + param($params = @{}) + + $global:EMViewObject.AppInfo = Get-GraphAppInfo "EMAzureApp" $global:DefaultAzureApp "EM" + Set-MSALCurrentApp $global:EMViewObject.AppInfo + & $global:msalAuthenticator.Login -Account (?? $global:MSALToken.Account.UserName (Get-Setting "" "LastLoggedOnUser")) @params +} + +function Invoke-EMDeactivateView +{ + $tmp = $mnuMain.Items | Where Name -eq "EMBulk" + if($tmp) { $mnuMain.Items.Remove($tmp) } +} + +function Invoke-EMActivatingView +{ + Show-MSALError + + # Refresh values in case they have changed + $global:EMViewObject.AppInfo = (Get-GraphAppInfo "EMAzureApp" $global:DefaultAzureApp "EM") + if(-not $global:EMViewObject.Authentication) + { + $global:EMViewObject.Authentication = Get-MSALAuthenticationObject + } + + # Add View specific menus + Add-GraphBulkMenu +} + +function Invoke-EMSaveSettings +{ + $tmpApp = Get-GraphAppInfo "EMAzureApp" $global:DefaultAzureApp + + if($global:appObj.ClientID -ne $tmpApp.ClientId -and $global:MSALToken) + { + # The app has changed. Need to authenticate to the new app + Write-Status "Logging in to $((?? $global:appObj.Name "selected application"))" + $global:EMViewObject.AppInfo = $tmpApp + Set-MSALCurrentApp $global:EMViewObject.AppInfo + Clear-MSALCurentUserVaiables + Connect-MSALUser -Account $global:MSALToken.Account.Username + Write-Status "" + } + + Set-EMUIStatus +} + +function Invoke-GraphAuthenticationUpdated +{ + Set-EMUIStatus + + $script:CustomADMXDefinitions = $null +} + +function Set-EMUIStatus +{ + # Hide/Show Delete button + $allowDelete = Get-SettingValue "EMAllowDelete" + $global:btnDelete.Visibility = (?: ($allowDelete -eq $true) "Visible" "Collapsed") + + # Hide/Show Delete on Bulk menu + $allowBulkDelete = Get-SettingValue "EMAllowBulkDelete" + $mnuBulk = $mnuMain.Items | Where Name -eq "EMBulk" + + if($mnuBulk) + { + $mnuBulkDelete = $mnuBulk.Items | Where Name -eq "mnuBulkDelete" + if($mnuBulkDelete) + { + $mnuBulkDelete.Visibility = (?: ($allowBulkDelete -eq $true) "Visible" "Collapsed") + } + } +} + +function Set-EMViewPanel +{ + param($panel) + + # ToDo: Create View specific pannel and move this to graph + Add-XamlEvent $panel "btnView" "Add_Click" -scriptBlock ([scriptblock]{ + Show-GraphObjectInfo + }) + + Add-XamlEvent $panel "btnDelete" "Add_Click" -scriptBlock ([scriptblock]{ + Remove-GraphObjects + }) + + Add-XamlEvent $panel "btnCopy" "Add_Click" -scriptBlock ([scriptblock]{ + Copy-GraphObject + }) + + Add-XamlEvent $panel "btnExport" "Add_Click" -scriptBlock ([scriptblock]{ + Show-GraphExportForm + }) + + Add-XamlEvent $panel "btnImport" "Add_Click" -scriptBlock ([scriptblock]{ + Show-GraphImportForm + }) + + Add-XamlEvent $panel "txtFilter" "Add_LostFocus" ({ #param($obj, $e) + Invoke-FilterBoxChanged $this + #$e.Handled = $true + }) + + Add-XamlEvent $panel "txtFilter" "Add_GotFocus" ({ + if($this.Tag -eq "1" -and $this.Text -eq "Filter") { $this.Text = "" } + Invoke-FilterBoxChanged $this + }) + + Add-XamlEvent $panel "txtFilter" "Add_TextChanged" ({ + Invoke-FilterBoxChanged $this + }) + + Invoke-FilterBoxChanged ($panel.FindName("txtFilter")) + + $allowDelete = Get-SettingValue "EMAllowDelete" + Set-XamlProperty $panel "btnDelete" "Visibility" (?: ($allowDelete -eq $true) "Visible" "Collapsed") + + $global:dgObjects.add_selectionChanged({ + Invoke-ModuleFunction "Invoke-EMSelectedItemsChanged" + }) + + # ToDo: Move this to the view object + $dpd = [System.ComponentModel.DependencyPropertyDescriptor]::FromProperty([System.Windows.Controls.ItemsControl]::ItemsSourceProperty, [System.Windows.Controls.DataGrid]) + if($dpd) + { + $dpd.AddValueChanged($global:dgObjects, { + Set-XamlProperty $global:dgObjects.Parent "txtFilter" "Text" "" + $enabled = (?: ($null -eq $this.ItemsSource -or ($this.ItemsSource | measure).Count -eq 0) $false $true) + Set-XamlProperty $global:dgObjects.Parent "btnImport" "IsEnabled" $true # Always all Import if ObjectType allows it + Set-XamlProperty $global:dgObjects.Parent "btnExport" "IsEnabled" $enabled + }) + } + + $btnRefresh = Get-XamlObject ($global:AppRootFolder + "\Xaml\RefreshButton.xaml") + if($btnRefresh) + { + $btnRefresh.SetValue([System.Windows.Controls.Grid]::ColumnProperty,$grdTitle.ColumnDefinitions.Count - 1) + $btnRefresh.Margin = "0,0,5,3" + $btnRefresh.Cursor = "Hand" + $btnRefresh.Name = "btnRefresh" + $btnRefresh.Focusable = $false + $grdTitle.Children.Add($btnRefresh) | Out-Null + + $tooltip = [System.Windows.Controls.ToolTip]::new() + $tooltip.Content = "Refresh all objects" + [System.Windows.Controls.ToolTipService]::SetToolTip($btnRefresh, $tooltip) + + $panel.RegisterName($btnRefresh.Name, $btnRefresh) + + $tooltip = [System.Windows.Controls.ToolTip]::new() + $tooltip.Content = "Refresh objects" + + [System.Windows.Controls.ToolTipService]::SetToolTip($btnRefresh, $tooltip) + + $btnRefresh.Add_Click({ + $txtFilterText = $null + $txtFilter = $this.Parent.FindName("txtFilter") + if($txtFilter) { $txtFilterText = $txtFilter.Text } #= "" } + + Show-GraphObjects $txtFilterText + + if($txtFilterText -and $txtFilter) + { + $txtFilter.Text = $txtFilterText + Invoke-FilterBoxChanged $txtFilter + } + + Write-Status "" + }) + } + + $global:btnLoadAllPages.add_click({ + Write-Status "Loading $($global:curObjectType.Title) objects" + [array]$graphObjects = Get-GraphObjects -property $global:curObjectType.ViewProperties -objectType $global:curObjectType -AllPages + if(-not $global:dgObjects.Columns) + { + Show-GraphObjects -FromGraphObjects $graphObjects + } + else + { + $graphObjects | Where-Object { $_ -ne $null } | ForEach-Object { $global:dgObjects.ItemsSource.AddNewItem($_) | Out-Null } + } + $global:dgObjects.ItemsSource.CommitNew() + Set-GraphPagesButtonStatus + Invoke-FilterBoxChanged $global:txtFilter -ForceUpdate + Write-Status "" + }) + + $global:btnLoadNextPage.add_click({ + Write-Status "Loading $($global:curObjectType.Title) objects" + [array]$graphObjects = Get-GraphObjects -property $global:curObjectType.ViewProperties -objectType $global:curObjectType -SinglePage + if(-not $global:dgObjects.Columns) + { + Show-GraphObjects -FromGraphObjects $graphObjects + } + else + { + $graphObjects | Where-Object { $_ -ne $null } | ForEach-Object { $global:dgObjects.ItemsSource.AddNewItem($_) | Out-Null } + } + $global:dgObjects.ItemsSource.CommitNew() + Set-GraphPagesButtonStatus + Invoke-FilterBoxChanged $global:txtFilter + Write-Status "" + }) +} + +function Invoke-GraphObjectsChanged +{ + $btnRefresh = $global:EMViewObject.ViewPanel.FindName("btnRefresh") + + if($btnRefresh) + { + $tooltip = [System.Windows.Controls.ToolTipService]::GetToolTip($btnRefresh) + if($global:lstMenuItems.SelectedItem.QuerySearch -eq $true) + { + $tooltip.Content = "Refresh objects based on filter. Note: Only filtered objects will be returned. Clear filter and press refresh to reload other objects" + } + else + { + $tooltip.Content = "Refresh all objects" + } + } +} + +function Invoke-EMSelectedItemsChanged +{ + $hasSelectedItems = ($global:dgObjects.ItemsSource | Where IsSelected -eq $true) -or ($null -ne $global:dgObjects.SelectedItem) + Set-XamlProperty $global:dgObjects.Parent "btnView" "IsEnabled" $hasSelectedItems #(?: ($null -eq ($global:dgObjects.SelectedItem)) $false $true) + Set-XamlProperty $global:dgObjects.Parent "btnCopy" "IsEnabled" $hasSelectedItems #(?: ($null -eq $global:dgObjects.SelectedItem) $false $true) + Set-XamlProperty $global:dgObjects.Parent "btnDelete" "IsEnabled" $hasSelectedItems #(?: ($null -eq $global:dgObjects.SelectedItem -and $global:curObjectType.AllowDelete -ne $false) $false $true) +} + +function Invoke-FilterBoxChanged +{ + param($txtBox,[switch]$ForceUpdate) + + $filter = $null + + if($txtBox.Text.Trim() -eq "" -and $txtBox.IsFocused -eq $false) + { + $txtBox.FontStyle = "Italic" + $txtBox.Tag = 1 + $txtBox.Text = "Filter" + $txtBox.Foreground="Lightgray" + } + elseif($ForceUpdate -eq $true) + { + $dgObjects.ItemsSource.Filter = $dgObjects.ItemsSource.Filter + } + elseif($txtBox.Tag -eq "1" -and $txtBox.Text -eq "Filter" -and $txtBox.IsFocused -eq $false) + { + + } + else + { + $txtBox.FontStyle = "Normal" + $txtBox.Tag = $null + $txtBox.Foreground="Black" + $txtBox.Background="White" + + if($txtBox.Text) + { + $filter = { + param ($item) + + return ($null -ne ($item.PSObject.Properties | Where { $_.Name -notin @("IsSelected","Object", "ObjectType") -and $_.Value -match [regex]::Escape($txtBox.Text) })) + + foreach($prop in ($item.PSObject.Properties | Where { $_.Name -notin @("IsSelected","Object", "ObjectType")})) + { + if($prop.Value -match [regex]::Escape($txtBox.Text)) { return $true } + } + $false + } + } + } + + if($dgObjects.ItemsSource -is [System.Windows.Data.ListCollectionView] -and $txtBox.IsFocused -eq $true) + { + $dgObjects.ItemsSource.Filter = $filter + } + + $allObjectsCount = 0 + if($dgObjects.ItemsSource.SourceCollection) + { + $allObjectsCount = $dgObjects.ItemsSource.SourceCollection.Count + } + + $objCount = ($dgObjects.ItemsSource | measure).Count + if($objCount -gt 0) + { + $strAllObjectsInfo = "" + if($allObjectsCount -gt $objCount) + { + $strAllObjectsInfo = " ($($allObjectsCount))" + } + $global:txtEMObjects.Text = "Objects: $objCount$strAllObjectsInfo" + } + else + { + $global:txtEMObjects.Text = "" + } +} +#region Endpoint Security (Intents) functions + +function Start-PreImportEndpointSecurity +{ + param($obj, $objectType) + + @{ + "API"="deviceManagement/templates/$($obj.templateId)/createInstance" + } +} + +function Start-PostListEndpointSecurity +{ + param($objList, $objectType) + + if(-not $script:baseLineTemplates) + { + $script:baseLineTemplates = (Invoke-GraphRequest -Url "/deviceManagement/templates").Value + } + if(-not $script:baseLineTemplates) { return } + + foreach($obj in $objList) + { + if(-not $obj.Object.templateId) { continue } + if($obj.Object.templateId -ne $baseLineTemplate.Id) + { + $baseLineTemplate = $script:baseLineTemplates | Where Id -eq $obj.Object.templateId + } + + if($baseLineTemplate) + { + $obj | Add-Member -MemberType NoteProperty -Name "Type" -Value $baseLineTemplate.displayName + $obj | Add-Member -MemberType NoteProperty -Name "Category" -Value (?: ($baseLineTemplate.templateSubtype -eq "none") $baseLineTemplate.templateType $baseLineTemplate.templateSubtype) + } + + } + $objList +} + +function Start-PostExportEndpointSecurity +{ + param($obj, $objectType, $path) + + $fileName = (Get-GraphObjectName $obj $objectType).Trim('.') + if((Get-SettingValue "AddIDToExportFile") -eq $true -and $obj.Id) + { + $fileName = ($fileName + "_" + $obj.Id) + } + + $settings = Invoke-GraphRequest -Url "$($objectType.API)/$($obj.id)/settings" + $settingsJson = "{ `"settings`": $((ConvertTo-Json $settings.value -Depth 20 ))`n}" + $fileName = "$path\$((Remove-InvalidFileNameChars $fileName))_Settings.json" + Save-GraphObjectToFile $settingsJson $fileName +} + +function Start-PostFileImportEndpointSecurity +{ + param($obj, $objectType, $file) + + $settings = Get-EMSettingsObject $obj $objectType $file + if($settings) + { + Start-GraphPreImport $settings + Invoke-GraphRequest -Url "$($objectType.API)/$($obj.id)/updateSettings" -Body ($settings | ConvertTo-Json -Depth 50) -Method "POST" + } +} + +function Start-PreCopyEndpointSecurity +{ + param($obj, $objectType, $newName) + + $false + + # Intents has a createCopy method. Use "manual" copy to have one standard and making sure Copy works the same as Export/Import + # These objects supports duplicate in the portal + # Keep for reference + # + # $objData = "{`"displayName`":`"$($newName)`"}" + # + #Invoke-GraphRequest -Url "/deviceManagement/intents/$($obj.Id)/createCopy" -Content $objData -HttpMethod "POST" | Out-Null + #$true +} + +function Start-PostCopyEndpointSecurity +{ + param($objCopyFrom, $objNew, $objectType) + + $settings = Invoke-GraphRequest -Url "$($objectType.API)/$($objCopyFrom.id)/settings" -ODataMetadata "Skip" + if($settings) + { + $settingsObj = New-object PSObject @{ "Settings" = $settings.Value } + Invoke-GraphRequest -Url "$($objectType.API)/$($objNew.id)/updateSettings" -Body ($settingsObj | ConvertTo-Json -Depth 20) -Method "POST" + } +} + +function Start-PreUpdateEndpointSecurity +{ + param($obj, $objectType, $curObject, $fromObj) + + if(-not $fromObj.settings) { return } + + $strAPI = "/deviceManagement/intents/$($curObject.Object.id)/updateSettings" + + $curObject = Get-GraphObject $curObject.Object $objectType + + $curValues = @() + foreach($val in $curObject.Object.settings) + { + if($fromObj.settings | Where { $_.definitionId -eq $val.definitionId}) { continue } + + # Set all existing values to null + # Note: This will not remove them from the configured list just set them Not Configured + $curValues += [PSCustomObject]@{ + '@odata.type' = $val.'@odata.type' + definitionId = $val.definitionId + id = $val.id + valueJson = "null" + } + } + + $curValues += $fromObj.settings + + <# + if($curValues.Count -gt 0) + { + $tmpObj = [PSCustomObject]@{ + settings = $curValues + } + $json = ConvertTo-Json $tmpObj -Depth 20 + + # Set all existing values to null + # Note: This will not remove them from the configured list just set them Not Configured + Invoke-GraphRequest -Url $strAPI -Content $json -HttpMethod "POST" | Out-Null + } + #> + + $tmpObj = [PSCustomObject]@{ + settings = $curValues + } + Start-GraphPreImport $tmpObj.settings + + $json = ConvertTo-Json $tmpObj -Depth 20 + Invoke-GraphRequest -Url $strAPI -Content $json -HttpMethod "POST" | Out-Null + + Remove-Property $obj "templateId" +} + +function Start-PostGetEndpointSecurity +{ + param($obj, $objectType) + + Add-EndpointSecurityInfo $obj +} + +function local:Add-EndpointSecurityInfo +{ + param($obj, $baseLineTemplate = $null) + +} +#endregion + +#region + +function Start-PostFileImportDeviceConfiguration +{ + param($obj, $objectType, $importFile) + + if($obj.'@OData.Type' -like "#microsoft.graph.windows10GeneralConfiguration") + { + $tmpObj = Get-GraphObjectFromFile $importFile + + if(($tmpObj.privacyAccessControls | measure).Count -gt 0) + { + $privacyObj = [PSCustomObject]@{ + windowsPrivacyAccessControls = $tmpObj.privacyAccessControls + } + $json = $privacyObj | ConvertTo-Json -Depth 20 + $ret = Invoke-GraphRequest -Url "deviceManagement/deviceConfigurations('$($obj.Id)')/windowsPrivacyAccessControls" -Body $json -Method "POST" + } + } +} + +function Start-PostCopyDeviceConfiguration +{ + param($objCopyFrom, $objNew, $objectType) + + if($objCopyFrom.'@OData.Type' -like "#microsoft.graph.windows10GeneralConfiguration") + { + if(($objCopyFrom.privacyAccessControls | measure).Count -gt 0) + { + $privacyObj = [PSCustomObject]@{ + windowsPrivacyAccessControls = $objCopyFrom.privacyAccessControls + } + $json = $privacyObj | ConvertTo-Json -Depth 20 + Invoke-GraphRequest -Url "deviceManagement/deviceConfigurations('$($objNew.Id)')/windowsPrivacyAccessControls" -Body $json -Method "POST" | Out-null + } + } +} + +function Start-PostGetDeviceConfiguration +{ + param($obj, $objectType) + + if(($obj.Object.omaSettings | measure).Count -gt 0) + { + foreach($omaSetting in ($obj.Object.omaSettings | Where isEncrypted -eq $true)) + { + if($omaSetting.isEncrypted -eq $false) { continue } + + $xmlValue = Invoke-GraphRequest -Url "/deviceManagement/deviceConfigurations/$($obj.Object.Id)/getOmaSettingPlainTextValue(secretReferenceValueId='$($omaSetting.secretReferenceValueId)')" + if($xmlValue.Value) + { + $omaSetting.isEncrypted = $false + $omaSetting.secretReferenceValueId = $null + + if($omaSetting.'@odata.type' -eq "#microsoft.graph.omaSettingStringXml" -or + $omaSetting.'value@odata.type' -eq "#Binary") + { + $Bytes = [System.Text.Encoding]::UTF8.GetBytes($xmlValue.Value) + $omaSetting.value = [Convert]::ToBase64String($bytes) + } + else + { + $omaSetting.value = $xmlValue.Value + } + } + } + } +} + +#endregion + +#region Compliance Policy +function Start-PostExportCompliancePolicies +{ + param($obj, $objectType, $exportPath) + + foreach($scheduledActionsForRule in $obj.scheduledActionsForRule) + { + foreach($scheduledActionConfiguration in $scheduledActionsForRule.scheduledActionConfigurations) + { + foreach($notificationMessageCCGroup in $scheduledActionConfiguration.notificationMessageCCList) + { + Add-GroupMigrationObject $notificationMessageCCGroup + } + } + } +} + +function Start-PreUpdateCompliancePolicies +{ + param($obj, $objectType, $curObject, $fromObj) + + $strAPI = "/deviceManagement/deviceCompliancePolicies/$($curObject.Object.id)/scheduleActionsForRules" + + $tmpObj = [PSCustomObject]@{ + deviceComplianceScheduledActionForRules = $obj.scheduledActionsForRule + } + + $json = ConvertTo-Json $tmpObj -Depth 20 + Invoke-GraphRequest -Url $strAPI -Content $json -HttpMethod "POST" | Out-Null + + Remove-Property $obj "scheduledActionsForRule" +} + +#endregion + +function Start-PostImportComplianceScripts +{ + param($obj, $objectType, $file) + + $endTime = (Get-Date).AddMinutes(2) + + $found = $false + while($endTime -gt (Get-Date)) + { + $tmpObj = Invoke-GraphRequest -Url "$($objectType.API)/$($obj.Id)" -ErrorAction SilentlyContinue + if($tmpObj) { + $found = $true + break + } + Start-Sleep -Seconds 10 + } + + if(-not $found) + { + Write-LogError "Compliance script $($obj.Id) not found after import. Please check the import file." + return + } +} + +#region Intune Branding functions +function Start-PreImportIntuneBranding +{ + param($obj, $objectType) + + $ret = @{} + $global:brandingClone = $null + + if($obj.isDefaultProfile) + { + + # Looks like the ID is the same for all tenants so skip this for now + <# + $defObj = (Invoke-GraphRequest -Url "/deviceManagement/intuneBrandingProfiles?`$filter=isDefaultProfile eq true&`$select=id,displayName").Value[0] + if($defObj) + { + $obj.Id = $defObj.Id + } + #> + + $ret.Add("API",($objectType.API + "/" + $obj.Id)) + $ret.Add("Method","PATCH") # Default profile always exists so update it + + foreach($prop in @("profileName","isDefaultProfile","disableClientTelemetry","profileDescription")) + { + Remove-Property $obj $prop + } + + $ret + } + else + { + # Create new Branding profile does not support images data in the json + # Workaround: (as done by the portal) + # Create a new profile with basic info + # Patch the profile with all the info + + $global:brandingClone = $obj | ConvertTo-Json -Depth 20 | ConvertFrom-Json + + foreach($prop in ($obj.PSObject.Properties | Where {$_.Name -notin @("profileName","profileDescription","roleScopeTagIds")})) #"customPrivacyMessage" + { + Remove-Property $obj $prop.Name + } + } + Remove-Property $obj "Id" +} + +function Start-PostImportIntuneBranding +{ + param($obj, $objectType, $file) + + if($obj.isDefaultProfile -or -not $global:brandingClone) { return } + + foreach($prop in @("Id","isDefaultProfile","customPrivacyMessage","disableClientTelemetry")) #"isDefaultProfile","disableClientTelemetry" + { + Remove-Property $global:brandingClone $prop + } + $json = ($global:brandingClone | ConvertTo-Json -Depth 20) + Invoke-GraphRequest -Url "$($objectType.API)/$($obj.Id)" -Body $json -Method "PATCH" | Out-Null +} + +function Start-PostGetIntuneBranding +{ + param($obj, $objectType) + + foreach($imgType in @("themeColorLogo","lightBackgroundLogo","landingPageCustomizedImage")) + { + Write-LogDebug "Get $imgType for $($obj.Object.profileName)" + $imgJson = Invoke-GraphRequest -Url "$($objectType.API)/$($obj.Object.Id)/$imgType" + if($imgJson.Value) + { + $obj.Object.$imgType = $imgJson + } + } +} + +function Start-PostExportIntuneBranding +{ + param($obj, $objectType, $path) + + foreach($imgType in @("themeColorLogo","lightBackgroundLogo","landingPageCustomizedImage")) + { + if($obj.$imgType.Value) + { + $fileName = "$path\$((Get-GraphObjectName $obj $objectType))_$imgType.jpg" + [IO.File]::WriteAllBytes($fileName, [System.Convert]::FromBase64String($obj.$imgType.Value)) + } + } +} + +function Start-PreDeleteIntuneBranding +{ + param($obj, $objectType) + + if($obj.isDefaultProfile -eq $true) + { + @{ "Delete" = $false } + } +} + +function Start-PreUpdateIntuneBranding +{ + param($obj, $objectType, $curObject, $fromObj) + + if($curObject.Object.isDefaultProfile) + { + foreach($prop in @("profileName","isDefaultProfile","disableClientTelemetry","profileDescription")) + { + Remove-Property $obj $prop + } + } +} + +#endregion + +#region Azure Branding functions +function Start-PreImportAzureBranding +{ + param($obj, $objectType) + + Remove-Property $obj "@odata.Type" + + $ret = @{} + if($obj.Id -eq "0") + { + #$ret.Add("Method","PATCH") # Default profile always exists so update it + #$ret.Add("API",($objectType.API + "/0")) + } + + $ret.Add("API",($objectType.API + "/$($global:Organization.Id)/branding/localizations")) + + # This is NOT wat the documentation says + # Documentation says to use Content-Language + # Any place the documentation states to use Accept-Language is for Get operation + # https://docs.microsoft.com/en-us/graph/api/organizationalbrandingproperties-get?view=graph-rest-beta&tabs=http#request-headers + $ret.Add("AdditionalHeaders", @{ "Accept-Language" = $obj.Id }) + + $ret +} + +function Start-PostListAzureBranding +{ + param($objList, $objectType) + + foreach($obj in $objList) + { + if(-not $obj.Object.id) { continue } + try + { + if($obj.Object.id -eq "0") + { + $language = "Default" + } + else + { + $language = ([cultureinfo]::GetCultureInfo($obj.Object.id)).DisplayName + } + + $obj | Add-Member -MemberType NoteProperty -Name "Language" -Value $language + } + catch{} + } + $objList +} + +#endregion + +#region Script functions +function Add-ScriptExtensions +{ + param($form, $buttonPanel, $index = 0) + + $btnDownload = New-Object System.Windows.Controls.Button + $btnDownload.Content = 'Download' + $btnDownload.Name = 'btnDownload' + $btnDownload.Margin = "0,0,5,0" + $btnDownload.Width = "100" + + $btnDownload.Add_Click({ + Invoke-DownloadScript + }) + + $tmp = $form.FindName($buttonPanel) + if($tmp) + { + $tmp.Children.Insert($index, $btnDownload) + } + + $btnDownload = New-Object System.Windows.Controls.Button + $btnDownload.Content = 'Edit' + $btnDownload.Name = 'btnEdit' + $btnDownload.Margin = "0,0,5,0" + $btnDownload.Width = "100" + + $btnDownload.Add_Click({ + Invoke-EditScript + }) + + $tmp = $form.FindName($buttonPanel) + if($tmp) + { + $tmp.Children.Insert($index, $btnDownload) + } +} + +function Add-ScriptExportExtensions +{ + param($form, $buttonPanel, $index = 0) + + $ctrl = $form.FindName("chkExportScript") + if(-not $ctrl) + { + $xaml = @" + + +"@ + $label = [Windows.Markup.XamlReader]::Parse($xaml) + + $global:chkExportScript = [System.Windows.Controls.CheckBox]::new() + $global:chkExportScript.IsChecked = $true + $global:chkExportScript.VerticalAlignment = "Center" + $global:chkExportScript.Name = "chkExportScript" + + @($label, $global:chkExportScript) + } +} + +function Start-PostExportScripts +{ + param($obj, $objectType, $exportPath) + + if($obj.scriptContent -and $global:chkExportScript.IsChecked) + { + Write-Log "Export script $($obj.FileName)" + $fileName = [IO.Path]::Combine($exportPath, $obj.FileName) + [IO.File]::WriteAllBytes($fileName, ([System.Convert]::FromBase64String($obj.scriptContent))) + } +} + +function Invoke-DownloadScript +{ + if(-not $global:dgObjects.SelectedItem.Object.id) { return } + + $obj = (Get-GraphObject $global:dgObjects.SelectedItem $global:curObjectType).Object + Write-Status "" + + if($obj.scriptContent) + { + Write-Log "Download PowerShell script '$($obj.FileName)' from $($obj.displayName)" + + $dlgSave = New-Object -Typename System.Windows.Forms.SaveFileDialog + $dlgSave.InitialDirectory = Get-SettingValue "IntuneRootFolder" $env:Temp + $dlgSave.FileName = $obj.FileName + if($dlgSave.ShowDialog() -eq [System.Windows.Forms.DialogResult]::OK -and $dlgSave.Filename) + { + # Changed to WriteAllBytes to get rid of BOM characters from Custom Attribute file + [IO.File]::WriteAllBytes($dlgSave.FileName, ([System.Convert]::FromBase64String($obj.scriptContent))) + } + } +} + +function Invoke-EditScript +{ + if(-not $global:dgObjects.SelectedItem.Object.id) { return } + + $obj = (Get-GraphObject $global:dgObjects.SelectedItem $global:curObjectType) + Write-Status "" + if(-not $obj.Object.scriptContent) { return } + $script:currentScriptObject = $obj + + $script:editForm = Get-XamlObject ($global:AppRootFolder + "\Xaml\EditScriptDialog.xaml") + + if(-not $script:editForm) { return } + + Set-XamlProperty $script:editForm "txtEditScriptTitle" "Text" "Edit: $($obj.Object.displayName)" + + $scriptText = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($obj.Object.scriptContent)) + Set-XamlProperty $script:editForm "txtScriptText" "Text" $scriptText + + $script:currentModal = $null + if($global:grdModal.Children.Count -gt 0) + { + $script:currentModal = $global:grdModal.Children[0] + } + + Add-XamlEvent $script:editForm "btnSaveScriptEdit" "add_click" ({ + $scriptText = Get-XamlProperty $script:editForm "txtScriptText" "Text" + $pre = [System.Text.Encoding]::UTF8.GetPreamble() + $utfBOM = [System.Text.Encoding]::UTF8.GetString($pre) + if($scriptText.startsWith($utfBOM)) + { + # Remove UTF8 BOM bytes + $scriptText = $scriptText.Remove(0, $utfBOM.Length) + } + $bytes = [System.Text.Encoding]::UTF8.GetBytes($scriptText) + $encodedText = [Convert]::ToBase64String($bytes) + + if($script:currentScriptObject.Object.scriptContent -ne $encodedText) + { + # Save script + if(([System.Windows.MessageBox]::Show("Are you sure you want to update the script?`n`nObject:`n$($script:currentScriptObject.displayName)", "Update script?", "YesNo", "Warning")) -eq "Yes") + { + Write-Status "Update $($script:currentScriptObject.displayName)" + $obj = $script:currentScriptObject.Object | ConvertTo-Json -Depth 20 | ConvertFrom-Json + $obj.scriptContent = $encodedText + Start-GraphPreImport $obj $script:currentScriptObject.ObjectType + foreach($prop in $script:currentScriptObject.ObjectType.PropertiesToRemoveForUpdate) + { + Remove-Property $obj $prop + } + Remove-Property $obj "Assignments" + Remove-Property $obj "isAssigned" + + $json = ConvertTo-Json $obj -Depth 15 + + $objectUpdated = (Invoke-GraphRequest -Url "$($script:currentScriptObject.ObjectType.API)/$($script:currentScriptObject.Object.Id)" -Content $json -HttpMethod "PATCH") + if(-not $objectUpdated) + { + Write-Log "Failed to update script" 3 + [System.Windows.MessageBox]::Show("Failed to save the script object. See log for more information","Update failed!", "OK", "Error") + } + Write-Status "" + } + } + + $global:grdModal.Children.Clear() + if($script:currentModal) + { + $global:grdModal.Children.Add($script:currentModal) + } + [System.Windows.Forms.Application]::DoEvents() + }) + + Add-XamlEvent $script:editForm "btnCancelScriptEdit" "add_click" ({ + $global:grdModal.Children.Clear() + if($script:currentModal) + { + $global:grdModal.Children.Add($script:currentModal) + } + [System.Windows.Forms.Application]::DoEvents() + }) + + $global:grdModal.Children.Clear() + $script:editForm.SetValue([System.Windows.Controls.Grid]::RowProperty,1) + $script:editForm.SetValue([System.Windows.Controls.Grid]::ColumnProperty,1) + $global:grdModal.Children.Add($script:editForm) | Out-Null + [System.Windows.Forms.Application]::DoEvents() +} + +#endregion + +#region Policy File functions +function Add-PolicyFileExtensions +{ + param($form, $buttonPanel, $index = 0) + + $btnDownload = New-Object System.Windows.Controls.Button + $btnDownload.Content = 'Download' + $btnDownload.Name = 'btnDownload' + $btnDownload.Margin = "0,0,5,0" + $btnDownload.Width = "100" + + $btnDownload.Add_Click({ + Invoke-DownloadPolicyFile + }) + + $tmp = $form.FindName($buttonPanel) + if($tmp) + { + $tmp.Children.Insert($index, $btnDownload) + } + + $btnDownload = New-Object System.Windows.Controls.Button + $btnDownload.Content = 'Edit' + $btnDownload.Name = 'btnEdit' + $btnDownload.Margin = "0,0,5,0" + $btnDownload.Width = "100" + + $btnDownload.Add_Click({ + Invoke-EditPolicyFile + }) + + $tmp = $form.FindName($buttonPanel) + if($tmp) + { + $tmp.Children.Insert($index, $btnDownload) + } +} + +function Add-PolicyFileExportExtensions +{ + param($form, $buttonPanel, $index = 0) + + $ctrl = $form.FindName("chkExportPolicyFile") + if(-not $ctrl) + { + $xaml = @" + + +"@ + $label = [Windows.Markup.XamlReader]::Parse($xaml) + + $global:chkExportPolicyFile = [System.Windows.Controls.CheckBox]::new() + $global:chkExportPolicyFile.IsChecked = $true + $global:chkExportPolicyFile.VerticalAlignment = "Center" + $global:chkExportPolicyFile.Name = "chkExportPolicyFile" + + @($label, $global:chkExportPolicyFile) + } +} + +function Start-PostExportPolicyFile +{ + param($obj, $objectType, $exportPath) + + if($objectType.PolicyFileAttribute -and $obj.$($objectType.PolicyFileAttribute) -and $global:chkExportPolicyFile.IsChecked) + { + Write-Log "Export policy file from attribute $($obj.PolicyFileAttribute)" + $fileNameOut = ?? $obj.FileName "$($obj.PolicyFileAttribute).json" + $fileName = [IO.Path]::Combine($exportPath, $fileNameOut) + [IO.File]::WriteAllBytes($fileName, ([System.Convert]::FromBase64String($obj.$($objectType.PolicyFileAttribute)))) + } +} + +function Invoke-DownloadPolicyFile +{ + if(-not $global:dgObjects.SelectedItem.Object.id) { return } + + $obj = (Get-GraphObject $global:dgObjects.SelectedItem $global:curObjectType).Object + Write-Status "" + + if($global:curObjectType.PolicyFileAttribute -and $obj.$($global:curObjectType.PolicyFileAttribute)) + { + $fileNameOut = ?? $obj.FileName "$($obj.PolicyFileAttribute).json" + Write-Log "Download policy file '$($fileNameOut)' from $($obj.displayName)" + + $dlgSave = New-Object -Typename System.Windows.Forms.SaveFileDialog + $dlgSave.InitialDirectory = Get-SettingValue "IntuneRootFolder" $env:Temp + $dlgSave.FileName = $fileNameOut + if($dlgSave.ShowDialog() -eq [System.Windows.Forms.DialogResult]::OK -and $dlgSave.Filename) + { + # Changed to WriteAllBytes to get rid of BOM characters from Custom Attribute file + [IO.File]::WriteAllBytes($dlgSave.FileName, ([System.Convert]::FromBase64String($obj.$($global:curObjectType.PolicyFileAttribute)))) + } + } +} + +function Invoke-EditPolicyFile +{ + if(-not $global:dgObjects.SelectedItem.Object.id) { return } + + $obj = (Get-GraphObject $global:dgObjects.SelectedItem $global:curObjectType) + Write-Status "" + if(-not $global:curObjectType.PolicyFileAttribute -or -not $obj.Object.$($global:curObjectType.PolicyFileAttribute)) { return } + $script:currentScriptObject = $obj + + $script:editForm = Get-XamlObject ($global:AppRootFolder + "\Xaml\EditScriptDialog.xaml") + + if(-not $script:editForm) { return } + + Set-XamlProperty $script:editForm "txtEditScriptTitle" "Text" "Edit: $($obj.Object.displayName)" + + $scriptText = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($obj.Object.$($global:curObjectType.PolicyFileAttribute))) + Set-XamlProperty $script:editForm "txtScriptText" "Text" $scriptText + + $script:currentModal = $null + if($global:grdModal.Children.Count -gt 0) + { + $script:currentModal = $global:grdModal.Children[0] + } + + Add-XamlEvent $script:editForm "btnSaveScriptEdit" "add_click" ({ + $scriptText = Get-XamlProperty $script:editForm "txtScriptText" "Text" + $pre = [System.Text.Encoding]::UTF8.GetPreamble() + $utfBOM = [System.Text.Encoding]::UTF8.GetString($pre) + if($scriptText.startsWith($utfBOM)) + { + # Remove UTF8 BOM bytes + $scriptText = $scriptText.Remove(0, $utfBOM.Length) + } + $bytes = [System.Text.Encoding]::UTF8.GetBytes($scriptText) + $encodedText = [Convert]::ToBase64String($bytes) + + if($script:currentScriptObject.Object.scriptContent -ne $encodedText) + { + # Save script + if(([System.Windows.MessageBox]::Show("Are you sure you want to update the $($global:curObjectType.PolicyFileAttribute) attribute?`n`nObject:`n$($script:currentScriptObject.displayName)", "Update script?", "YesNo", "Warning")) -eq "Yes") + { + Write-Status "Update $($script:currentScriptObject.displayName)" + $obj = $script:currentScriptObject.Object | ConvertTo-Json -Depth 20 | ConvertFrom-Json + $obj.$($global:curObjectType.PolicyFileAttribute) = $encodedText + Start-GraphPreImport $obj $script:currentScriptObject.ObjectType + foreach($prop in $script:currentScriptObject.ObjectType.PropertiesToRemoveForUpdate) + { + Remove-Property $obj $prop + } + Remove-Property $obj "Assignments" + Remove-Property $obj "isAssigned" + + $json = ConvertTo-Json $obj -Depth 15 + + $objectUpdated = (Invoke-GraphRequest -Url "$($script:currentScriptObject.ObjectType.API)/$($script:currentScriptObject.Object.Id)" -Content $json -HttpMethod "PATCH") + if(-not $objectUpdated) + { + Write-Log "Failed to update script" 3 + [System.Windows.MessageBox]::Show("Failed to save the policy. See log for more information","Update failed!", "OK", "Error") + } + Write-Status "" + } + } + + $global:grdModal.Children.Clear() + if($script:currentModal) + { + $global:grdModal.Children.Add($script:currentModal) + } + [System.Windows.Forms.Application]::DoEvents() + }) + + Add-XamlEvent $script:editForm "btnCancelScriptEdit" "add_click" ({ + $global:grdModal.Children.Clear() + if($script:currentModal) + { + $global:grdModal.Children.Add($script:currentModal) + } + [System.Windows.Forms.Application]::DoEvents() + }) + + $global:grdModal.Children.Clear() + $script:editForm.SetValue([System.Windows.Controls.Grid]::RowProperty,1) + $script:editForm.SetValue([System.Windows.Controls.Grid]::ColumnProperty,1) + $global:grdModal.Children.Add($script:editForm) | Out-Null + [System.Windows.Forms.Application]::DoEvents() +} + +#endregion + +#region Terms and Conditions +function Start-PostExportTermsAndConditions +{ + param($obj, $objectType, $path) + + Add-EMAssignmentsToExportFile $obj $objectType $path +} + +function Start-PreImportAssignmentsTermsAndConditions +{ + param($obj, $objectType, $file, $assignments) + + Add-EMAssignmentsToObject $obj $objectType $file $assignments +} +#endregion + +#region App Protection functions + +function Start-GetAppProtection +{ + param($obj, $objectType) + + if(-not $obj."@odata.type") { return } + + Get-GraphMetaData + + $objectClass = $null + if($global:metaDataXML) + { + try + { + $tmp = $obj."@odata.type".Split('.')[-1] + $objectClass = Get-GraphObjectClassName $tmp + } + catch + { + + } + $expand = $null + if($objectClass -eq "windowsInformationProtectionPolicies") + { + $expand = "?`$expand=protectedAppLockerFiles,exemptAppLockerFiles" + } + + if($objectClass) + { + @{"API"="/deviceAppManagement/$objectClass/$($obj.Id)$expand"} + } + } +} + +function Start-PostListAppProtection +{ + param($objList, $objectType) + + # App Configurations for Managed Apps are included in App Protections e.g. the /deviceAppManagement/managedAppPolicies API + # For some reason, the $filter option is not supported to filter out these objects + # e.g. not isof(...) to excluded the type, not startsWith(id, 'A_') to exlude based on Id + # These filters generates a request error so filter them out manually in this function instead + # The portal is probably doing the same thing since these are included in the return but not in the UI + $objList | Where { $_.Object.'@OData.Type' -ne '#microsoft.graph.targetedManagedAppConfiguration' } +} + +function Start-PreImportAppProtection +{ + param($obj, $objectType) + + if(($obj.Apps | measure).Count -gt 0) + { + $global:ImportObjectInfo = @{ Apps=$obj.Apps } + } + else + { + $global:ImportObjectInfo = $null + } + + $global:ImportObjectClass = $null + if($obj."@odata.type") + { + try + { + $global:ImportObjectClass = Get-GraphObjectClassName ($obj."@odata.type".Split('.')[-1]) + } + catch {} + } + + Remove-Property $obj "apps" + Remove-Property $obj "apps@odata.context" + + try + { + $tmp = $obj."@odata.type".Split('.')[-1] + $objectClass = Get-GraphObjectClassName $tmp + if($objectClass) + { + @{"API"="/deviceAppManagement/$objectClass"} + } + } + catch {} +} + +function Start-PostImportAppProtection +{ + param($obj, $objectType, $file) + + if($global:ImportObjectInfo.Apps) + { + # No "@odata.type" on the created object so reload new object + #$newObject = (Invoke-GraphRequest "$($objectType.API)?`$filter=id eq '$($obj.Id)'").Value + $newObject = Invoke-GraphRequest "$($objectType.API)/$($obj.Id)" + if($newObject) + { + try + { + $tmp = $newObject."@odata.type".Split('.')[-1] + $objectClass = Get-GraphObjectClassName $tmp + + $apps = [PSCustomObject]@{ + appGroupType = $obj.appGroupType + apps = @($global:ImportObjectInfo.Apps) + } + $json = $apps | ConvertTo-Json -Depth 20 + + Invoke-GraphRequest -Url "/deviceAppManagement/$objectClass/$($obj.Id)/targetApps" -Content $json -HttpMethod POST | Out-Null + } + catch {} + } + } + $global:ImportObjectInfo = $null +} + +function Start-PreImportAssignmentsAppProtection +{ + param($obj, $objectType, $file, $assignments) + + if($global:ImportObjectClass) + { + @{"API"="/deviceAppManagement/$($global:ImportObjectClass)/$($obj.Id)/assign"} + } +} + +function Start-PreUpdateAppConfigurationApp +{ + param($obj, $objectType, $curObject, $fromObj) + + if($obj.Apps) + { + try + { + Write-Log "Update App Configuruation Apps" + + $apps = [PSCustomObject]@{ + appGroupType = $obj.appGroupType + apps = @($obj.Apps) + } + $json = $apps | ConvertTo-Json -Depth 20 + $objectClass = 'targetedManagedAppConfigurations' + + Invoke-GraphRequest -Url "/deviceAppManagement/$objectClass/$($curObject.Object.Id)/targetApps" -Content $json -HttpMethod POST | Out-Null + } + catch {} + } + + Remove-Property $obj "apps" +} + +function Start-PreUpdateAppProtection +{ + param($obj, $objectType, $curObject, $fromObj) + + if($curObject.Object.'@OData.Type' -eq "#microsoft.graph.windowsInformationProtectionPolicy") + { + $api = "/deviceAppManagement/windowsInformationProtectionPolicies/$($curObject.Object.Id)" + } + elseif($curObject.Object.'@OData.Type' -eq "#microsoft.graph.mdmWindowsInformationProtectionPolicy") + { + $api = "/deviceAppManagement/mdmWindowsInformationProtectionPolicies/$($curObject.Object.Id)" + } + elseif($curObject.Object.'@OData.Type' -eq "#microsoft.graph.iosManagedAppProtection") + { + $api = "/deviceAppManagement/iosManagedAppProtections/$($curObject.Object.Id)" + } + elseif($curObject.Object.'@OData.Type' -eq "#microsoft.graph.androidManagedAppProtection") + { + $api = "/deviceAppManagement/androidManagedAppProtections/$($curObject.Object.Id)" + } + else + { + return (Start-PreUpdateAppConfigurationApp $obj $objectType $curObject $fromObj) + } + + if($obj.Apps) + { + try + { + Write-Log "Update App Protection Apps" + + $apps = [PSCustomObject]@{ + appGroupType = $obj.appGroupType + apps = @($obj.Apps) + } + $json = $apps | ConvertTo-Json -Depth 20 + + Invoke-GraphRequest -Url "$api/targetApps" -Content $json -HttpMethod POST | Out-Null + } + catch {} + + Remove-Property $obj "apps" + } + + @{ "API" = $api } + +} +#endregion + +#region App Configuration +function Start-PostExportAppConfiguration +{ + param($obj, $objectType, $path) + + #Add-EMAssignmentsToExportFile $obj $objectType $path + + Write-Log "Export app config for $($objectType.Id) with OData.Type: $($obj.'@OData.Type')" + + if($obj.'@OData.Type' -eq "#microsoft.graph.androidManagedAppProtection" -or + $obj.'@OData.Type' -eq "#microsoft.graph.androidForWorkMobileAppConfiguration" -or + $obj.'@OData.Type' -eq "#microsoft.graph.androidManagedStoreAppConfiguration" -or + $obj.'@OData.Type' -eq "#microsoft.graph.iosMobileAppConfiguration") + { + $fileName = (Get-GraphObjectName $obj $objectType).Trim('.') + if((Get-SettingValue "AddIDToExportFile") -eq $true -and $obj.Id) + { + $fileName = ($fileName + "_" + $obj.Id) + } + $tmpObj = $null + $fileName = "$path\$((Remove-InvalidFileNameChars $fileName)).json" + if([IO.File]::Exists($fileName)) + { + $tmpObj = Get-GraphObjectFromFile $fileName + } + else + { + Write-Log "File not found: $fileName. Could not add App names." 3 + } + + if(($tmpObj.targetedMobileApps | measure).Count -gt 0) + { + Write-Log "Add target apps info" + $targetedApps = @() + foreach($appId in $tmpObj.targetedMobileApps) + { + $appObj = Invoke-GraphRequest -Url "/deviceAppManagement/mobileApps/$($appId)" #?`select=id,displayName" -ODataMetadata "Minimal" + if($appObj) + { + Write-Log "Add target app info $($appObj.displayName) ($($appObj.Id)) of type $($appObj.'@OData.Type')" + $targetedApps += $appObj.displayName + '|!|' + $appObj.Id + '|!|' + $appObj.'@OData.Type' + } + } + + if($targetedApps.Count -gt 0) + { + Write-Log "Add CustomRefTargetedApps property" + $tmpObj | Add-Member -MemberType NoteProperty -Name "#CustomRefTargetedApps" -Value ($targetedApps -join "|*|") + Write-Log "Save file $fileName" + Save-GraphObjectToFile $tmpObj $fileName + } + } + else + { + Write-Log "No target apps found" 2 + } + } +} + +function Start-PreFilesImportAppConfiguration +{ + param($objectType, $filesToImport) + + $targetedAppsObjects = $filesToImport | Where { $null -ne $_.Object."#CustomRefTargetedApps" } + + if(($targetedAppsObjects | measure).Count -gt 0) + { + Write-Log "Policies with Targeted Apps detected" + foreach($fileObject in $targetedAppsObjects) + { + Add-AppConfigurationTargets $objectType $fileObject + } + } + $filesToImport +} + +function local:Add-AppConfigurationTargets +{ + param($obj, $fileObj) + + if($fileObj.Object."#CustomRefTargetedApps" -and $fileObj.Object.targetedMobileApps) + { + Write-Log "Adding app target for $($fileObj.Object.displayName)" + + $targetedAppsInfo = $fileObj.Object."#CustomRefTargetedApps" + + $translatedTargetedApps = @() + + if($targetedAppsInfo) + { + foreach($targetedApp in ($targetedAppsInfo -split "[|][*][|]")) + { + $appName, $appId, $appType = $targetedApp -split "[|][!][|]" + if(-not $appName -or -not $appId) + { + Write-Log "App Name and Id is missing in string: $targetedApp" 2 + continue + } + $tmpApps = (Invoke-GraphRequest -Url "/deviceAppManagement/mobileApps?`$filter=displayName eq '$appName'").value + if(-not $tmpApps) + { + Write-Log "No application found with name $appName. $appId will not be translated and added to target list" 2 + continue + } + + Write-Log "Found $(($tmpApps | measure).Count) applications" 2 + foreach ($tmpApp in $tmpApps) { + Write-Log "Found '$($tmpApp.displayName)' ($($tmpApp.id)) of type $($($tmpApp.'@OData.Type'))" + } + + $tmpApp = $tmpApps | Where-Object '@OData.Type' -eq $appType + if(-not $tmpApp) + { + Write-Log "No $appName application found of type $appType. $appId will not be translated and added to target list" 2 + } + elseif(($tmpApp | measure).Count -gt 1) { + Write-Log "$(($tmpApp | measure).Count) applications found with name '$appName' of type $appType. $appId will not be translated and added to target list" 2 + } + else { + Write-Log "Found '$appName' with id $($tmpApp.Id) ($appType)" + $translatedTargetedApps += $tmpApp.Id + } + } + + if($translatedTargetedApps.Count -gt 0) { + Write-Log "Updating translated targeted apps" + $fileObj.Object.targetedMobileApps = $translatedTargetedApps + } + else { + Write-Log "Could not find targeted apps in the evnironment. Verify that they are added. Policy import might fail" 3 + } + } + } +} + +function Start-PreImportAssignmentsAppConfiguration +{ + param($obj, $objectType, $file, $assignments) + + @{"API"="/deviceAppManagement/mobileAppConfigurations/$($obj.Id)/microsoft.graph.managedDeviceMobileAppConfiguration/assign"} +} +#endregon + +#region Applications + +function Start-PostCopyApplication +{ + param($objCopyFrom, $objNew, $objectType) + + Start-ImportApp $objNew + Start-AddInstallScripts $objNew $objCopyFrom + Write-Status "" +} + +function Start-PostFileImportApplication +{ + param($obj, $objectType, $file) + + $tmpObj = Get-GraphObjectFromFile $file + + if(-not ($obj.PSObject.Properties | Where Name -eq '@odata.type')) + { + # Add @odata.type property if it is missing. Required by app package import + $obj | Add-Member -MemberType NoteProperty -Name '@odata.type' -Value $objectType.'@odata.type' + } + + $fi = [IO.FileInfo]$file + $tmpFilName = $fi.DirectoryName + "\" + $obj.FileName + + if([IO.File]::Exists($tmpFilName) -eq $false) + { + $tmpFilName = $null + } + + Start-ImportApp $obj $tmpFilName + Start-AddInstallScripts $obj $tmpObj +} + +function local:Start-ImportApp +{ + param($obj, $packageFile = $null) + + if(-not $obj.'@odata.type') { return } + + if($null -eq $packageFile) + { + $pkgPath = Get-SettingValue "EMIntuneAppPackages" + + if(-not $pkgPath -or [IO.Directory]::Exists($pkgPath) -eq $false) + { + Write-LogDebug "Package source directory is either missing or does not exist" 2 + return + } + + $packageFile = "$($pkgPath)\$($obj.fileName)" + } + $fi = [IO.FileInfo]$packageFile + + if($fi.Exists -eq $false) + { + Write-LogDebug "Package source file $($fi.FullName) not found" 2 + return + } + + Write-Status "Import appliction package file $($fi.FullName)" + Write-Log "Import application file '$($($fi.FullName))' for $($obj.displayName)" + + $appType = $obj.'@odata.type'.Trim('#') + + if($appType -eq "microsoft.graph.win32LobApp") + { + $fileEncryptionInfo = Copy-Win32LOBPackage $packageFile $obj + } + elseif($appType -eq "microsoft.graph.windowsMobileMSI") + { + $fileEncryptionInfo = Copy-MSILOB $packageFile $obj + } + elseif($appType -eq "microsoft.graph.windowsUniversalAppX") + { + $fileEncryptionInfo = Copy-MSIXLOB $packageFile $obj + } + elseif($appType -eq "microsoft.graph.iosLOBApp") + { + $fileEncryptionInfo = Copy-iOSLOB $packageFile $obj + } + elseif($appType -eq "microsoft.graph.androidLOBApp") + { + $fileEncryptionInfo = Copy-AndroidLOB $packageFile $obj + } + else + { + Write-Log "Unsupported application type $appType. File will not be uploaded" 2 + } + + if((Get-SettingValue "EMSaveEncryptionFile") -eq $true) + { + if($fileEncryptionInfo) + { + $jsonEncryptionInfo = $fileEncryptionInfo | ConvertTo-Json -Depth 10 + + $pkgPath = Get-SettingValue "EMIntuneAppDownloadFolder" (Get-SettingValue "EMIntuneAppPackages") + if($pkgPath -and [IO.Directory]::Exists($pkgPath)) + { + $obj = Invoke-GraphRequest -Url "/deviceAppManagement/mobileApps/$($obj.id)" -ODataMetadata "Minimal" + $fullPath = $pkgPath + "\$($obj.displayName)_$($obj.id)_$($obj.committedContentVersion).json" + $jsonEncryptionInfo | Out-File -FilePath $fullPath -Force -Encoding utf8 + } + } + } +} + +function local:Start-AddInstallScripts +{ + param($obj, $fromAppObj) + + if($fromAppObj -and ($fromAppObj.activeInstallScript."#ScriptInfo" -or $fromAppObj.activeUninstallScript."#ScriptInfo")) + { + Write-Log "Importing scripts for $($obj.displayName)" + + $scriptsAdded = $false + $jsonData = @{} + $jsonData."@odata.type" = "#microsoft.graph.win32LobApp" + $jsonData."committedContentVersion" = "1" + + foreach ($scriptType in @('activeInstallScript','activeUninstallScript')) { + $scriptInfo = $fromAppObj.$scriptType.'#ScriptInfo' + if (-not $scriptInfo) { continue } + + Write-Log "Add $($scriptType -replace '^active','') script: $($scriptInfo.displayName)" + + $json = [ordered]@{ + '@odata.type' = $scriptInfo.'@odata.type' + displayName = $scriptInfo.displayName + enforceSignatureCheck = $scriptInfo.enforceSignatureCheck + runAs32Bit = $scriptInfo.runAs32Bit + content = $scriptInfo.content + } | ConvertTo-Json -Depth 10 -Compress + + $scriptObject = Invoke-GraphRequest -Url "deviceAppManagement/mobileApps/$($obj.id)/microsoft.graph.win32LobApp/contentVersions/1/scripts" -Method POST -Content $json + + if ($scriptObject) { + $jsonData.$scriptType = @{ targetId = $scriptObject.Id } + $scriptsAdded = $true + } + } + + $i = 0 + while($true) + { + $scripts = Invoke-GraphRequest -Url "deviceAppManagement/mobileApps/$($obj.id)/microsoft.graph.win32LobApp/contentVersions/1/scripts" + if(-not $scripts) + { + Write-Log "Failed to retrieve scripts for app after adding. Skipping Install/Uninstall script config." 2 + return + } + + if(($scripts.value.state | Select -Unique) -eq "commitSuccess") + { + Write-Log "Scripts added successfully" + break + } + if($i -ge 12) + { + Write-Log "Install/Uninstall scripts are still not in pending state after waiting for 1 minute." 3 + return + } + + Write-Log "Waiting for scripts to be added..." + Start-Sleep -Seconds 5 + $i++ + } + + if($scriptsAdded) + { + Write-Log "Add script info to app" + $json = ConvertTo-Json $jsonData -Depth 10 + $status = Invoke-GraphRequest -Url "deviceAppManagement/mobileApps/$($obj.id)" -Method PATCH -Body $json + if($status -eq $true) + { + Write-Log "Install/Uninstall script info updated successfully" + } + else + { + Write-Log "Failed to update Install/Uninstall script info" 2 + } + } + } +} + +function Start-PreUpdateApplication +{ + param($obj, $objectType, $curObject, $fromObj) + + if($curObject.Object.'@OData.type' -eq "#microsoft.graph.windowsMobileMSI") + { + Remove-Property $obj "useDeviceContext" + } + elseif($curObject.Object.'@OData.type' -eq "#microsoft.graph.officeSuiteApp") + { + Remove-Property $obj "officeConfigurationXml" + Remove-Property $obj "officePlatformArchitecture" + Remove-Property $obj "developer" + Remove-Property $obj "owner" + Remove-Property $obj "publisher" + } + + Remove-Property $obj "appStoreUrl" +} + +function Start-PreImportCommandApplication +{ + param($obj, $objectType, $file, $assignments) + + if($obj.'@OData.Type' -in @('#microsoft.graph.microsoftStoreForBusinessApp','#microsoft.graph.androidStoreApp')) + { + Write-Log "App type '$($obj.'@OData.Type')' not supported for import" 2 + @{ "Import" = $false } + } + + if($obj.'@OData.Type' -eq '#microsoft.graph.officeSuiteApp') + { + if($obj.officeSuiteAppDefaultFileFormat -eq "notConfigured") + { + $obj.officeSuiteAppDefaultFileFormat = "officeOpenXMLFormat" + } + } + + if($obj.activeInstallScript) { $obj.activeInstallScript = $null } + if($obj.activeUninstallScript) { $obj.activeUninstallScript = $null } +} + +function Add-DetailExtensionApplications +{ + param($form, $buttonPanel, $index = 0) + + $btnUpload = New-Object System.Windows.Controls.Button + $btnUpload.Content = 'Upload' + $btnUpload.Name = 'btnUploadAppfile' + $btnUpload.Margin = "0,0,5,0" + $btnUpload.Width = "100" + + $btnUpload.Add_Click({ + if($global:dgObjects.SelectedItem.Object.publishingState -ne "notPublished") + { + # Only allow upload of not published apps + # Use portal to replace app file... + if(([System.Windows.MessageBox]::Show("Are you sure you want to upload a new file for the app?`n`nApplication:`n$($global:dgObjects.SelectedItem.Object.displayName)", "Update app file?", "YesNo", "Warning")) -ne "Yes") + { + return + } + } + + $pkgPath = Get-SettingValue "EMIntuneAppPackages" + + $of = [System.Windows.Forms.OpenFileDialog]::new() + $of.FileName = $global:dgObjects.SelectedItem.Object.fileName + $of.DefaultExt = "*.intunewin" + $of.Filter = "Intune Win32 (*.intunewin)|*.*" + $of.Multiselect = $false + + if($pkgPath -and [IO.Directory]::Exists($pkgPath)) + { + $of.InitialDirectory = $pkgPath + } + + if($of.ShowDialog() -eq "OK") + { + Write-Status "Import $($global:dgObjects.SelectedItem.Object.displayName) file" + Start-ImportApp $global:dgObjects.SelectedItem.Object $of.FileName + Write-Status "" + } + }) + + $tmp = $form.FindName($buttonPanel) + if($tmp) + { + $tmp.Children.Insert($index, $btnUpload) + } + + $btnDownload = New-Object System.Windows.Controls.Button + $btnDownload.Content = 'Download' + $btnDownload.Name = 'btnDownloadAppfile' + $btnDownload.Margin = "0,0,5,0" + $btnDownload.Width = "100" + + $btnDownload.Add_Click({ + Write-Status "Download file" + $obj = $global:dgObjects.SelectedItem.Object + #$obj = Invoke-GraphRequest -Url "/deviceAppManagement/mobileApps/$($obj.id)" + + $pkgPath = Get-SettingValue "EMIntuneAppDownloadFolder" (Get-SettingValue "EMIntuneAppPackages") + + $dlgSave = [System.Windows.Forms.SaveFileDialog]::new() + $dlgSave.InitialDirectory = $pkgPath + $dlgSave.FileName = ($obj.FileName + ".encrypted") + $dlgSave.DefaultExt = "*.encrypted" + $dlgSave.Filter = "Encrypted intunewin (*.encrypted)|*.encrypted|All files (*.*)|*.*" + + if($dlgSave.ShowDialog() -eq [System.Windows.Forms.DialogResult]::OK -and $dlgSave.Filename) + { + $contentFileObj = Start-DownloadAppContent $obj $dlgSave.FileName + + if([IO.File]::Exists($dlgSave.FileName)) + { + $fullPath = Find-AppEncryptionFile $obj $contentFileObj $pkgPath + if([IO.File]::Exists($fullPath) -eq $false) + { + if(([System.Windows.MessageBox]::Show("Could not find decryption file for $($obj.displayName)`nApp Id: $($obj.id)`nContent version $($obj.committedContentVersion)`n`nDo you want to browse for the file?", "Encryption file not found", "YesNo", "Warning")) -eq "Yes") + { + $of = [System.Windows.Forms.OpenFileDialog]::new() + $of.InitialDirectory = $pkgPath + $of.DefaultExt = "*.json" + $of.Filter = "Json (*.json)|*.json" + $of.Multiselect = $false + + if($of.ShowDialog() -eq "OK") + { + $fullPath = $of.FileName + } + } + } + + if([IO.File]::Exists($fullPath)) + { + Write-Status "Decrypting file" + $encryptionInfo = ConvertFrom-Json (Get-Content -Path $fullPath -Raw) + if($encryptionInfo.fileEncryptionInfo) + { + $encryptionInfo = $encryptionInfo.fileEncryptionInfo + } + $destination = $pkgPath + "\$($obj.FileName)" + Start-DecryptFile $dlgSave.Filename $destination $encryptionInfo.encryptionKey $encryptionInfo.initializationVector + try { [IO.File]::Delete($dlgSave.Filename) } + catch { + Write-LogError "Failed to delete exported encrypted file" $_.Exception + } + } + else + { + Write-Log "Decryption file for $($obj.displayName) not found. Skipping decryption" 2 + } + } + } + + Write-Status "" + }) + + $tmp = $form.FindName($buttonPanel) + if($tmp) + { + $tmp.Children.Insert($index, $btnDownload) + } +} + +function Find-AppEncryptionFile +{ + param($obj, $contentFileObj, $rootFolders) + + $search = @() + $search += "$($obj.displayName)_$($obj.id)_$($obj.committedContentVersion)" + $search += "$([IO.Path]::GetFileNameWithoutExtension($obj.fileName))_$($contentFileObj.size)" + $search += "$($obj.displayName)_$($contentFileObj.size)" + + foreach($rootFolder in $rootFolders) + { + foreach($searchName in $search) + { + $fullName = ($rootFolder + "\$($searchName).json") + if([IO.File]::Exists($fullName)) + { + return $fullName + } + } + } +} + +function Start-PreImportAssignmentsApplications +{ + param($obj, $objectType, $file, $assignments) + + if($obj.'@odata.type' -eq "#microsoft.graph.windowsMicrosoftEdgeApp") + { + foreach($assignment in $assignments) + { + Remove-Property $assignment.target "deviceAndAppManagementAssignmentFilterId" + Remove-Property $assignment.target "deviceAndAppManagementAssignmentFilterType" + } + @{"Assignments"=$assignments} + } + elseif($obj.'@odata.type' -eq "#microsoft.graph.winGetApp") + { + Write-LogDebug "Wait for app to be published" + $i = 2 + Start-Sleep -s ($i) + $x = 0 + while($x -lt 10) + { + ###!!! + $appInfo = Invoke-GraphRequest -Url "$($objectType.API)/$($obj.id)" -ODataMetadata "skip" + if($appInfo.publishingState -eq "Published") + { + Write-LogDebug "Application $($obj.displayName) is published" + return + } + Start-Sleep -s ($i) + $x++ + if($x -ge 5) { $i++ } + } + + Write-Log "Application '$($obj.displayName)' is not published. Skipping assignment" 2 + @{"Import"=$false} + } +} + +function Start-PreDeleteApplications +{ + param($obj, $objectType) + + if($obj.'@odata.type' -eq "#microsoft.graph.microsoftStoreForBusinessApp") + { + # Don't delete Microsoft Store for Business Apps + @{ "Delete" = $false } + } +} + +function Start-PostExportApplications +{ + param($obj, $objectType, $path) + + if($global:chkExportScript.IsChecked) + { + $fileName = Get-GraphObjectFile $obj $objectType + $fi = [IO.FileInfo]"$path\$fileName" + + try + { + foreach($rule in ($obj.detectionRules | Where '@OData.Type' -eq "#microsoft.graph.win32LobAppPowerShellScriptDetection")) + { + if($rule.ScriptContent) + { + [IO.File]::WriteAllBytes(("$path\$($fi.BaseName)_DetectionScript.ps1"), ([System.Convert]::FromBase64String($rule.ScriptContent))) + + } + } + + foreach($rule in $obj.requirementRules) + { + if($rule.'@OData.Type' -eq "#microsoft.graph.win32LobAppPowerShellScriptRequirement") + { + if($rule.ScriptContent) + { + [IO.File]::WriteAllBytes(("$path\$($fi.BaseName)_RequirementScript.ps1"), ([System.Convert]::FromBase64String($rule.ScriptContent))) + } + } + } + + if($obj.activeInstallScript.'#ScriptInfo'.displayName) + { + [IO.File]::WriteAllBytes(("$path\$($fi.BaseName)_$($obj.activeInstallScript.'#ScriptInfo'.displayName)"), ([System.Convert]::FromBase64String($obj.activeInstallScript.'#ScriptInfo'.content))) + } + + if($obj.activeUninstallScript.'#ScriptInfo'.displayName) + { + [IO.File]::WriteAllBytes(("$path\$($fi.BaseName)_$($obj.activeUninstallScript.'#ScriptInfo'.displayName)"), ([System.Convert]::FromBase64String($obj.activeUninstallScript.'#ScriptInfo'.content))) + } + } + catch + { + Write-LogError "Failed to export scripts" $_.Exception + } + } + + Save-Setting "Intune" "ExportAppFile" $global:chkExportApplicationFile.IsChecked + if($global:chkExportApplicationFile.IsChecked) + { + $encryptionSource = Get-SettingValue "EMIntuneAppDownloadFolder" (Get-SettingValue "EMIntuneAppPackages") + $pkgPath = $path + + if($pkgPath) + { + Write-Status "Download file" + + $exportFile = $pkgPath + "\$($obj.FileName).encrypted" + $contentFileObj = Start-DownloadAppContent $obj $exportFile -GetContentFileInfoOnly + $encryptionFile = Find-AppEncryptionFile $obj $contentFileObj $encryptionSource + if($encryptionFile -and [IO.File]::Exists($encryptionFile)) + { + Start-DownloadFile $contentFileObj.azureStorageUri $exportFile + + if([IO.File]::Exists($exportFile)) + { + Write-Status "Decrypting file" + $encryptionInfo = ConvertFrom-Json (Get-Content -Path $encryptionFile -Raw) + if($encryptionInfo.fileEncryptionInfo) + { + $encryptionInfo = $encryptionInfo.fileEncryptionInfo + } + $destination = $pkgPath + "\$($obj.FileName)" + Start-DecryptFile $exportFile $destination $encryptionInfo.encryptionKey $encryptionInfo.initializationVector + } + + try { [IO.File]::Delete($exportFile) } + catch { + Write-LogError "Failed to delete exported encrypted file" $_.Exception + } + } + else + { + Write-Log "Could not find encryption file" + } + } + } +} + +function Start-PostListApplications +{ + param($objList, $objectType) + + foreach($obj in ($objList | Where { $_.Object."@OData.Type" -eq "#microsoft.graph.winGetApp"})) + { + if($obj.Object.packageIdentifier -like "9*") + { + $installerType = "UWP" + } + elseif($obj.Object.packageIdentifier -like "X*") + { + $installerType = "Win32" + } + else + { + $objName = Get-GraphObjectName $obj.Object $objectType + Write-Log "Unknown package identifier for app $($objName): $($obj.Object.packageIdentifier)" 2 + $installerType = "Unknown" + } + $obj.Object | Add-Member -MemberType NoteProperty -Name "InstallerType" -Value $installerType + } + $objList +} + +function Add-ScriptExportApplications +{ + param($form, $buttonPanel, $index = 0) + + Add-ScriptExportExtensions $form $buttonPanel $index + + $ctrl = $form.FindName("chkExportApplicationFile") + if(-not $ctrl) + { + $xaml = @" + + +"@ + $label = [Windows.Markup.XamlReader]::Parse($xaml) + + $global:chkExportApplicationFile = [System.Windows.Controls.CheckBox]::new() + $global:chkExportApplicationFile.IsChecked = ((Get-Setting "Intune" "ExportAppFile" "false") -eq "true") + $global:chkExportApplicationFile.VerticalAlignment = "Center" + $global:chkExportApplicationFile.Name = "chkExportApplicationFile" + + @($label, $global:chkExportApplicationFile) + } +} + +function Start-PostGetApplications { + param($obj, $objectType) + + if(($obj.Object.dependentAppCount -as [int]) -gt 0 -or ($obj.Object.supersededAppCount -as [int]) -gt 0) { + $relationships = (Invoke-GraphRequest -Url "/deviceAppManagement/mobileApps/$($obj.Id)/relationships?`$filter=targetType%20eq%20microsoft.graph.mobileAppRelationshipType%27child%27").value + $dependencyApps = @() + $supersededApps = @() + foreach ($rel in $relationships) { + if ($rel."@odata.type" -eq "#microsoft.graph.mobileAppDependency") { + $dependencyApps += "$($rel.targetDisplayName)|!|$($rel.targetDisplayVersion)|!|$($rel.targetId)|!|$($rel.dependencyType)" + } + elseif ($rel."@odata.type" -eq "#microsoft.graph.mobileAppSupersedence") { + $supersededApps += "$($rel.targetDisplayName)|!|$($rel.targetDisplayVersion)|!|$($rel.targetId)|!|$($rel.supersedenceType)" + } + } + if ($dependencyApps.Count -gt 0) { + $obj.Object | Add-Member -MemberType NoteProperty -Name "#CustomRefDependency" -Value ($dependencyApps -join "|*|") + } + + if ($supersededApps.Count -gt 0) { + $obj.Object | Add-Member -MemberType NoteProperty -Name "#CustomRefSupersedence" -Value ($supersededApps -join "|*|") + } + } + + if($obj.Object.'@odata.type' -eq "#microsoft.graph.win32LobApp") + { + if($obj.Object.activeInstallScript.targetId -or $obj.Object.activeUninstallScript.targetId) + { + $scriptInfo = (Invoke-GraphRequest -Url "/deviceAppManagement/mobileApps/$($obj.Id)/microsoft.graph.win32LobApp/contentVersions/$($obj.Object.committedContentVersion)/scripts/").value + + foreach($script in $scriptInfo) { + $scriptFullInfo = (Invoke-GraphRequest -Url "/deviceAppManagement/mobileApps/$($obj.Id)/microsoft.graph.win32LobApp/contentVersions/$($obj.Object.committedContentVersion)/scripts/$($script.id)?`$select=Id,Content") + if($scriptFullInfo.Content) + { + $script.content = $scriptFullInfo.Content + } + + if($obj.Object.activeInstallScript.targetId -eq $script.id) + { + $tpObject = $obj.Object.activeInstallScript + } + elseif($obj.Object.activeUninstallScript.targetId -eq $script.id) + { + $tpObject = $obj.Object.activeUninstallScript + } + else + { + Write-Log "Script with id $($script.id) is not referenced by active install or uninstall script. Skipping." 2 + continue + } + $tpObject | Add-Member -MemberType NoteProperty -Name "#ScriptInfo" -Value $script -Force + } + } + } +} + +function Start-PostImportApplications +{ + param($obj, $objectType, $file) + + #$tmpObj = Get-GraphObjectFromFile $file +} + +function Start-PostFilesImportApplications +{ + param($objType, $importedObjects, $importedFiles) + + $refObjects = $importedFiles | Where { $null -ne $_.Object."#CustomRefDependency" -or $null -ne $_.Object."#CustomRefSupersedence" } + + if(($refObjects | measure).Count -gt 0) + { + Write-Log "Applicetions with Dependency or Supersedence detected" + foreach($file in $refObjects) + { + Add-ApplicationReferences $file.ImportedObject $file.Object + } + } +} + +function local:Add-ApplicationReferences +{ + param($obj, $fileObj) + + if($fileObj."#CustomRefDependency" -or $fileObj."#CustomRefSupersedence") + { + Write-Log "Adding app references for $($obj.displayName)" + + $depAppsInfo = $fileObj."#CustomRefDependency" + $supAppsInfo = $fileObj."#CustomRefSupersedence" + + $releationShips = [PSCustomObject]@{ + relationships = @() + } + + if($depAppsInfo) + { + foreach($depApp in ($depAppsInfo -split "[|][*][|]")) + { + $appName, $appVer, $appId, $appType = $depApp -split "[|][!][|]" + if(-not $appName -or -not $appVer) + { + Write-Log "Could not get Name and Version from string: $appApp" 2 + continue + } + $tmpApps = (Invoke-GraphRequest -Url "/deviceAppManagement/mobileApps?`$filter=displayName eq '$appName'").value + if(-not $tmpApps) + { + Write-Log "No application found with name $appName" 2 + continue + } + $tmpApp = $tmpApps | Where displayVersion -eq $appVer + if(-not $tmpApp) + { + Write-Log "No $appName application found with version $appVer" 2 + continue + } + elseif(($tmpApp | measure).Count -gt 1) + { + Write-Log "Multiple $appName applications found with version $appVer" 2 + continue + } + Write-Log "Add $appName ($appVer) to Dependency list" + $releationShips.relationships += [PSCustomObject]@{ + "@odata.type" = "#microsoft.graph.mobileAppDependency" + targetId = $tmpApp.Id + dependencyType = $appType + } + } + } + + if($supAppsInfo) + { + foreach($suppApp in ($supAppsInfo -split "[|][*][|]")) + { + $appName, $appVer, $appId, $appType = $suppApp -split "[|][!][|]" + if(-not $appName -or -not $appVer) + { + Write-Log "Could not get Name and Version from string: $suppApp" 2 + continue + } + $tmpApps = (Invoke-GraphRequest -Url "/deviceAppManagement/mobileApps?`$filter=displayName eq '$appName'").value + if(-not $tmpApps) + { + Write-Log "No application found with name $appName" 2 + continue + } + $tmpApp = $tmpApps | Where displayVersion -eq $appVer + if(-not $tmpApp) + { + Write-Log "No $appName application found with version $appVer" 2 + continue + } + elseif(-not ($tmpApp | measure).Count -gt 1) + { + Write-Log "Multiple $appName application found with version $appVer" 2 + continue + } + Write-Log "Add $appName ($appVer) to Supersedence list" + $releationShips.relationships += [PSCustomObject]@{ + "@odata.type" = "#microsoft.graph.mobileAppSupersedence" + targetId = $tmpApp.Id + supersedenceType = $appType + } + } + } + + if($releationShips.relationships.Count -gt 0) + { + $json = Update-JsonForEnvironment (ConvertTo-Json $releationShips -Depth 20) + + Write-Log "Update app references" + Invoke-GraphRequest -Url "/deviceAppManagement/mobileApps/$($obj.Id)/updateRelationships" -Method "POST" -Body $json + } + } +} + +#endregion + +#region Group Policy/Administrative Templates functions +function Get-GPOObjectSettings +{ + param($GPOObj) + + $gpoSettings = @() + + if ($GPOObj.policyConfigurationIngestionType -eq "unknown") { + $tmpObj = (Invoke-GraphRequest -Url "/deviceManagement/groupPolicyConfigurations?`$filter=id eq '$($GPOObj.id)'").value[0] + if ($tmpObj.policyConfigurationIngestionType) { + $GPOObj.policyConfigurationIngestionType = $tmpObj.policyConfigurationIngestionType + } + } + + # Get all configured policies in the Administrative Templates profile + $GPODefinitionValues = Invoke-GraphRequest -Url "/deviceManagement/groupPolicyConfigurations/$($GPOObj.id)/definitionValues?`$expand=definition" -ODataMetadata "skip" + foreach($definitionValue in $GPODefinitionValues.value) + { + # Get presentation values for the current settings (with presentation object included) + $presentationValues = Invoke-GraphRequest -Url "/deviceManagement/groupPolicyConfigurations/$($GPOObj.id)/definitionValues/$($definitionValue.id)/presentationValues?`$expand=presentation" -ODataMetadata "skip" + + # Set base policy settings + $obj = @{ + "enabled" = $definitionValue.enabled + "definition@odata.bind" = "$($global:graphURL)/deviceManagement/groupPolicyDefinitions('$($definitionValue.definition.id)')" + } + + if($definitionValue.definition.categoryPath) + { + $obj.Add("#Definition_Id", $definitionValue.definition.id) + $obj.Add("#Definition_displayName", $definitionValue.definition.displayName) + $obj.Add("#Definition_classType", $definitionValue.definition.classType) + $obj.Add("#Definition_categoryPath", $definitionValue.definition.categoryPath) + } + + if($presentationValues.value) + { + # Policy presentation values set e.g. a drop down list, check box, text box etc. + $obj.presentationValues = @() + + foreach ($presentationValue in $presentationValues.value) + { + # Add presentation@odata.bind property that links the value to the presentation object + $presentationValue | Add-Member -MemberType NoteProperty -Name "presentation@odata.bind" -Value "$($global:graphURL)/deviceManagement/groupPolicyDefinitions('$($definitionValue.definition.id)')/presentations('$($presentationValue.presentation.id)')" + + if($definitionValue.definition.categoryPath) + { + $presentationValue | Add-Member -MemberType NoteProperty -Name "#Presentation_Id" -Value $presentationValue.presentation.id + $presentationValue | Add-Member -MemberType NoteProperty -Name "#Presentation_Label" -Value $presentationValue.presentation.label + } + #Remove presentation object so it is not included in the export + Remove-ObjectProperty $presentationValue "presentation" + + #Optional removes. Import will igonre them + Remove-ObjectProperty $presentationValue "id" + Remove-ObjectProperty $presentationValue "lastModifiedDateTime" + Remove-ObjectProperty $presentationValue "createdDateTime" + + # Add presentation value to the list + $obj.presentationValues += $presentationValue + } + } + $gpoSettings += $obj + } + $gpoSettings +} + +function Import-GPOSetting +{ + param($obj, $settings) + + if($obj) + { + Write-Status "Import settings for $($obj.displayName)" + + $hasCustomADMX = $null -ne ($settings | Where { $null -ne $_.'#Definition_categoryPath' }) + + if($hasCustomADMX) + { + Write-Status "Import custom ADMX settings" + if(-not $script:CustomADMXDefinitions) + { + $tmpCustomCategories = Invoke-GraphRequest -Url "deviceManagement/groupPolicyCategories?`$expand=definitions(`$select=id, displayName, categoryPath, classType)&`$select=id, displayName&`$filter=ingestionSource eq 'custom'" -ODataMetadata "Minimal" + if($tmpCustomCategories.Value) + { + $script:CustomADMXDefinitions = @{} + foreach($tmpCat in $tmpCustomCategories.Value) + { + foreach($tmpDef in $tmpCat.definitions) + { + $key = ($tmpDef.displayName + $tmpDef.categoryPath + $tmpDef.classType).ToLower() + $val = [PSCustomObject]@{ + Definition = $tmpDef + Category = $tmpCat + Presentations = $null + } + try { + $script:CustomADMXDefinitions.Add($key, $val) + } + catch { + Write-Log "Failed to add '$($tmpDef.displayName)' in category '$($tmpDef.categoryPath)' of class $($tmpDef.classType)" 3 + } + } + } + } + } + } + + foreach($setting in $settings) + { + if($setting.'#Definition_categoryPath' -and $script:CustomADMXDefinitions -is [HashTable] -and $script:CustomADMXDefinitions.Count -gt 0) + { + $defVal = $null + $key = ($setting.'#Definition_displayName' + $setting.'#Definition_categoryPath' + $setting.'#Definition_classType').ToLower() + if($key -and $script:CustomADMXDefinitions.ContainsKey($key)) + { + $defVal = $script:CustomADMXDefinitions[$key] + } + elseif($key) + { + Write-Log "No custom ADMX definitiona found for setting $($setting.'#Definition_displayName')" 2 + } + else + { + Write-Log "Setting $($setting.'#Definition_displayName') does not have information to be imported in the environment" + } + + if($defVal) + { + $setting.'definition@odata.bind' = $setting.'definition@odata.bind' -replace $setting.'#Definition_Id', $defVal.Definition.Id + if(($setting.presentationValues | measure).Count -gt 0) + { + if(-not $defVal.Presentations) + { + $tmpPresentation = Invoke-GraphRequest -Url "deviceManagement/groupPolicyDefinitions/$($defVal.Definition.Id)/presentations" -ODataMetadata "Minimal" + if($tmpPresentation.value) + { + foreach($settingPresentation in $setting.presentationValues) + { + $tmpPresentationVal = $tmpPresentation.value | Where label -eq $settingPresentation.'#Presentation_Label' + if($tmpPresentationVal) + { + $settingPresentation.'presentation@odata.bind' = $settingPresentation.'presentation@odata.bind' -replace $setting.'#Definition_Id', $defVal.Definition.Id + $settingPresentation.'presentation@odata.bind' = $settingPresentation.'presentation@odata.bind' -replace $settingPresentation.'#Presentation_Id', $tmpPresentationVal.Id + } + else + { + Write-Log "Could not find a presentation value with label $($settingPresentation.'#Presentation_Label'). Setting will not be configured" 2 + continue + } + } + } + else + { + Write-Log "Could not find presentation for setting $($settingPresentation.'#Presentation_Label'). Setting will not be configured." 2 + continue + } + } + } + } + else + { + Write-Log "Settings might not be available if imported in another environment" 3 + } + } + elseif($setting.'#Definition_categoryPath') + { + Write-Log "Custom AMDX settings cannot be imported without ADMX file imported. Definitions not found" 2 + continue + } + + Start-GraphPreImport $setting + + if($true) + { + foreach($tmpProp in (($setting.PSObject.Properties | Where Name -like "#*").Name)) + { + Remove-Property $setting $tmpProp + } + + foreach($settingPresentation in $setting.presentationValues) + { + foreach($tmpProp in (($settingPresentation.PSObject.Properties | Where Name -like "#*").Name)) + { + Remove-Property $settingPresentation $tmpProp + } + } + } + + # Import each setting for the Administrative Template profile + Invoke-GraphRequest -Url "/deviceManagement/groupPolicyConfigurations/$($obj.id)/definitionValues" -Content (ConvertTo-Json $setting -Depth 20) -HttpMethod POST | Out-Null + } + } +} + +function Start-PostExportAdministrativeTemplate +{ + param($obj, $objectType, $path) + + $fileName = (Get-GraphObjectName $obj $objectType).Trim('.') + if((Get-SettingValue "AddIDToExportFile") -eq $true -and $obj.Id) + { + $fileName = ($fileName + "_" + $obj.Id) + } + + if($obj.definitionValues) + { + $settings = $obj.definitionValues + } + else + { + $settings = Get-GPOObjectSettings $obj + } + + $fileName = "$path\$((Remove-InvalidFileNameChars $fileName))_Settings.json" + Save-GraphObjectToFile $settings $fileName +} + +function Start-PostCopyAdministrativeTemplate +{ + param($objCopyFrom, $objNew, $objectType) + + $settings = Get-GPOObjectSettings $objCopyFrom + if($settings) + { + Import-GPOSetting $objNew $settings + } +} + +function Start-PostFileImportAdministrativeTemplate +{ + param($obj, $objectType, $file) + + $settings = Get-EMSettingsObject $obj $objectType $file -settingsProperty "definitionValues" -SettingsArray + if($settings) + { + $tmpObj = Get-GraphObjectFromFile $file + + Import-GPOSetting $obj $settings + } +} + +function Start-LoadAdministrativeTemplate +{ + param($fileName) + + if(-not $fileName) { return $null } + + $fi = [IO.FileInfo]$fileName + if($fi.Exists -eq $false) { return } + + $obj = Get-GraphObjectFromFile $fi.FullName + + if($obj.definitionValues) + { + return $obj + } + + $settingsFile = $fi.DirectoryName + "\" + $fi.BaseName + "_Settings.json" + + if([IO.File]::Exists($settingsFile)) + { + $definitionValues = Get-GraphObjectFromFile $settingsFile + + $obj | Add-Member Noteproperty -Name "definitionValues" -Value $definitionValues -Force + } + $obj +} + +function Start-PostGetAdministrativeTemplate +{ + param($obj, $objectType) + + $definitionValues = Get-GPOObjectSettings $obj.Object + if($definitionValues) + { + $obj.Object | Add-Member Noteproperty -Name "definitionValues" -Value $definitionValues -Force + } + <# + # Leave for now. This only loads the configured definition values and not the values specified. + # That would require enumerating each definition value which takes time. + $definitionValues = (Invoke-GraphRequest "deviceManagement/groupPolicyConfigurations('$($obj.Id)')/definitionValues?`$expand=definition(`$select=id,classType,displayName,policyType,groupPolicyCategoryId)" -ODataMetadata "minimal").value + + if($definitionValues) + { + $obj.Object | Add-Member Noteproperty -Name "definitionValues" -Value $definitionValues -Force + } + #> +} + +function Start-PreImportAdministrativeTemplate +{ + param($obj, $objectType, $file, $assignments) + + +} + +#endregion + +#region Policy Sets function + +function Start-PreImportAssignmentsPolicySets +{ + param($obj, $objectType, $file, $assignments) + + @{"API"="$($objectType.API)/$($obj.Id)/Update"} +} + +function Start-PreImportPolicySets +{ + param($obj, $objectType) + + @("items@odata.context","status","errorCode") | foreach { Remove-Property $obj $_ } + + # Properties to keep for items + $keepProperties = @("@odata.type","payloadId","intent","settings") + foreach($item in $obj.Items) + { + foreach($prop in ($item.PSObject.Properties | Where {$_.Name -notin $keepProperties})) + { + Remove-Property $item $prop.Name + } + #@("itemType","displayName","status","errorCode") | foreach { Remove-Property $item $_ } + } +} + +function Start-PreUpdatePolicySets +{ + param($obj, $objectType, $curObject, $fromObj) + + Start-PreImportPolicySets $obj $objectType + + $curObject = Get-GraphObject $curObject.Object $objectType + + # Update ref object in the json + # Used when importing in a different environment + $jsonObj = ConvertTo-Json $obj -Depth 15 + $updateObj = Update-JsonForEnvironment $jsonObj | ConvertFrom-Json + + $addedItems = @() + $updatedItems = @() + $deletedItems = @() + + foreach($item in $updateObj.items) + { + if(($curObject.Object.items | Where payloadId -eq $item.payloadId)) + { + $updatedItems += $item + } + else + { + $addedItems += $item + } + } + + foreach($item in $curObject.Object.items) + { + if(-not ($updateObj.Items | Where payloadId -eq $item.payloadId)) + { + $deletedItems += $item.id + } + } + + $updateItemObj = [PSCustomObject]@{ + addedPolicySetItems = $addedItems + deletedPolicySetItems = $deletedItems + updatedPolicySetItems = $updatedItems + } + + Write-Log "Update Policy Set items. Add: $($addedItems.Count), Update: $($updatedItems.Count), Delete: $($deletedItems.Count)" + + $updateApi = "/deviceAppManagement/policySets/$($curObject.Object.Id)/update" + $json = $updateItemObj | ConvertTo-Json -Depth 15 + + Invoke-GraphRequest -Url $updateApi -HttpMethod "POST" -Content $json + Remove-Property $obj "items" +} + +function Update-EMPolicySetAssignment +{ + param($assignment, $sourceObject, $newObject, $objectType) + + $api = "/deviceAppManagement/policySets/$($assignment.SourceId)?`$expand=assignments,items" + + $psObj = Invoke-GraphRequest -Url $api -ODataMetadata "Minimal" + + if(-not $psObj) + { + return + } + + $curItem = $psObj.Items | Where payloadId -eq $sourceObject.Id + + if(-not $curItem) + { + return + } + + $api = "/deviceAppManagement/policySets/$($assignment.SourceId)/update" + + $curItemClone = $curItem | ConvertTo-Json -Depth 20 | ConvertFrom-Json + $newItem = $curItem | ConvertTo-Json -Depth 20 | ConvertFrom-Json + $newItem.payloadId = $newObject.Id + if($newItem.guidedDeploymentTags -is [String] -and [String]::IsNullOrEmpty($newItem.guidedDeploymentTags)) + { + $newItem.guidedDeploymentTags = @() + } + + $keepProperties = @('@odata.type','payloadId','Settings','guidedDeploymentTags') + #itemType? e.g. #microsoft.graph.iosManagedAppProtection + #priority? + + foreach($prop in ($newItem.PSObject.Properties | Where {$_.Name -notin $keepProperties})) + { + Remove-Property $newItem $prop.Name + } + + $update = @{} + $update.Add('addedPolicySetItems',@($newItem)) + $update.Add('updatedPolicySetItems', @()) + $update.Add('deletedPolicySetItems',@($curItemClone.Id)) + + $json = $update | ConvertTo-Json -Depth 20 + + Write-Log "Update PolicySet $($psObj.displayName) - Replace: $((Get-GraphObjectName $newObject $objectType))" + + Invoke-GraphRequest -Url $api -HttpMethod "POST" -Content $json +} + +function Start-PostListPolicySets +{ + param($objList, $objectType) + + foreach($obj in $objList) + { + $obj | Add-Member -MemberType NoteProperty -Name "IsAssigned" -Value ($obj.Object.status -ne "notAssigned") + } + $objList +} +#endregion + +#endregion Locations +function Start-PreImportLocations +{ + param($obj, $objectType) + + if($obj.uniqueName) + { + $arr = $obj.uniqueName.Split('_') + if($arr.Length -ge 3) + { + # Locations requires a unique name so generate a new guid and change the uniqueName property + $obj.uniqueName = ($obj.uniqueName.Substring(0,$obj.uniqueName.Length-$arr[-1].Length) + [Guid]::NewGuid().Tostring("n")) + } + } +} +#endregion + +#region RoleDefinitions +function Start-PostExportRoleDefinitions +{ + param($obj, $objectType, $path) + + $fileName = (Get-GraphObjectName $obj $objectType).Trim('.') + if((Get-SettingValue "AddIDToExportFile") -eq $true -and $obj.Id) + { + $fileName = ($fileName + "_" + $obj.Id) + } + $tmpObj = $null + $fileName = "$path\$((Remove-InvalidFileNameChars $fileName)).json" + if([IO.File]::Exists($fileName)) + { + $tmpObj = Get-GraphObjectFromFile $fileName + } + else + { + Write-Log "File not found: $fileName. Could not get role assignments" 3 + } + + if(($tmpObj.RoleAssignments | measure).Count -gt 0) + { + $roleAssignmentsArr = @() + foreach($roleAssignment in $tmpObj.RoleAssignments) + { + $raObj = Invoke-GraphRequest -Url "/deviceManagement/roleAssignments/$($roleAssignment.Id)?`$expand=microsoft.graph.deviceAndAppManagementRoleAssignment/roleScopeTags" -ODataMetadata "Minimal" + if($raObj) + { + foreach($groupId in $raObj.resourceScopes) { Add-GroupMigrationObject $groupId } + foreach($groupId in $raObj.members) { Add-GroupMigrationObject $groupId } + $roleAssignmentsArr += $raObj + } + } + + if($roleAssignmentsArr.Count -gt 0) + { + $tmpObj.RoleAssignments = $roleAssignmentsArr + Save-GraphObjectToFile $tmpObj $fileName + } + } +} + +function Start-PreImportRoleDefinitions +{ + param($obj, $objectType) + + Remove-Property $obj "RoleAssignments" + Remove-Property $obj "RoleAssignments@odata.context" +} + +function Start-PostFileImportRoleDefinitions +{ + param($obj, $objectType, $file) + + $tmpObj = Get-GraphObjectFromFile $file + + $loadedScopeTags = $global:LoadedDependencyObjects["ScopeTags"] + if(($tmpObj.RoleAssignments | measure).Count -gt 0 -and ($loadedScopeTags | measure).Count -gt 0) + { + # Documentation way did not work so use the same way as the portal + # Should be created with /deviceManagement/roleDefinitions/{roleDefinitionId}/roleAssignments + foreach($roleAssignment in $tmpObj.RoleAssignments) + { + $roleAssignmentObj = New-object PSObject @{ + "description" = $roleAssignment.Description + "displayName"= $roleAssignment.DisplayName + "members" = $roleAssignment.members + "resourceScopes" = $roleAssignment.resourceScopes + "roleDefinition@odata.bind" = "https://graph.microsoft.com/beta/deviceManagement/roleDefinitions('$($obj.Id)')" + "roleScopeTags@odata.bind" = @() + } + + foreach($scopeTag in $roleAssignment.roleScopeTags) + { + $scopeMigObj = $loadedScopeTags | Where OriginalId -eq $scopeTag.Id + if(-not $scopeMigObj.Id) { continue } + $roleAssignmentObj."roleScopeTags@odata.bind" += "https://graph.microsoft.com/beta/deviceManagement/roleScopeTags('$($scopeMigObj.Id)')" + } + + # This will update GroupIds + $json = Update-JsonForEnvironment (ConvertTo-Json $roleAssignmentObj -Depth 20) + + Write-Log "Import Role Assignments" + Invoke-GraphRequest -Url "/deviceManagement/roleAssignments" -Body $json -Method "POST" + } + } +} +#endregion + +#region SettingsCatalog + +function Start-PreImportSettingsCatalog +{ + param($obj, $objectType) + + $returnHT = @{} + $updated = $false + + if($obj.templateReference.templateId) { + # I do not like this at all and it is a lazy but simple implementation... + # It turns out that settingInstanceTemplateId and settingValueTemplateId are case sensitive + # and there is ONE setting with a different casing in the Windows Baseline template. + # The export saves it with lowercase which causes the import to fail. + + Write-Log "Get template $($obj.templateReference.templateId)" + $templateObj = Invoke-GraphRequest -Url "/deviceManagement/configurationPolicyTemplates('$($obj.templateReference.templateId)')" + if($templateObj.lifecycleState -and $templateObj.lifecycleState -ne "active") { + Write-Log "Template '$($templateObj.displayName)' '$($templateObj.displayVersion)' is in '$($templateObj.lifecycleState)' state. Current state: $($templateObj.lifecycleState). Import might fail." 2 + } + #Todo: Should probably check for the latest active version and use that instead of the one in the templateReference + + if(-not $script:baseLineTemplate) { + $script:baseLineTemplate = @{} + } + if($script:baseLineTemplate.ContainsKey($obj.templateReference.templateId)) { + $templateReference = $script:baseLineTemplate[$obj.templateReference.templateId] + } + else { + Write-Log "Get template settings for '$($templateObj.displayName)' '$($templateObj.displayVersion)' ($($obj.templateReference.templateId))" + $templateReference = Invoke-GraphRequest -Url "/deviceManagement/configurationPolicyTemplates('$($obj.templateReference.templateId)')/settingTemplates?`$expand=settingDefinitions&top=1000" + $script:baseLineTemplate.Add($obj.templateReference.templateId, $templateReference) + } + + if($templateReference) { + $newObjJson = $obj | ConvertTo-Json -Depth 50 + $templateIDs = Get-GUIDs ($templateReference | ConvertTo-Json -Depth 50) + $objectIDs = Get-GUIDs ($obj.Settings | ConvertTo-Json -Depth 50) + $diff = Compare-Object $templateIDs $objectIDs -CaseSensitive + foreach($diffItem in ($diff | where SideIndicator -eq "=>")) { + $templateID = $templateIDs | Where { $_ -eq $diffItem.InputObject } + if($templateID) { + # Found but with different casing + $newObjJson = $newObjJson -replace $diffItem.InputObject, $templateID + $updated = $true + } + } + if($updated) { + $returnHT.Add("JSON", $newObjJson) + } + } + } + return $returnHT +} + +function Invoke-CheckSettingsCatalogIds +{ + param($obj, $templateReference) + + foreach($settingTemplate in $obj.value) { + if($settingTemplate.settingDefinitions) { + foreach($settingDefinition in $settingTemplate.settingDefinitions) { + if($settingDefinition.id -and $settingDefinition.id -ne $obj.Id) { + Write-Log "Setting definition ID $($settingDefinition.id) does not match the settings catalog ID $($obj.Id)" 2 + } + } + } + } +} + +function Start-PostExportSettingsCatalog +{ + param($obj, $objectType, $path) + + Add-EMAssignmentsToExportFile $obj $objectType $path +} + +function Start-PreUpdateSettingsCatalog +{ + param($obj, $objectType, $curObject, $fromObj) + + @{"Method"="PUT"} +} + +function Start-PostGetSettingsCatalog +{ + param($obj, $objectType) + + if(-not $obj.Object.Assignments) + { + $url = "$($objectType.API)/$($obj.id)/assignments" + $assignments = (Invoke-GraphRequest -Url $url).Value + if($assignments) + { + $obj.Object.Assignments = $assignments + } + } +} + +#endregion + +#region Notification functions +function Start-PreImportNotifications +{ + param($obj, $objectType) + + Remove-Property $obj "defaultLocale" + Remove-Property $obj "localizedNotificationMessages" + Remove-Property $obj "localizedNotificationMessages@odata.context" +} + +function Start-PostFileImportNotifications +{ + param($obj, $objectType, $file) + + $tmpObj = Get-GraphObjectFromFile $file + + foreach($localizedNotificationMessage in $tmpObj.localizedNotificationMessages) + { + Start-GraphPreImport $localizedNotificationMessage $objectType + Invoke-GraphRequest -Url "$($objectType.API)/$($obj.id)/localizedNotificationMessages" -Body ($localizedNotificationMessage | ConvertTo-Json -Depth 20) -Method "POST" + } +} + +function Start-PostCopyNotifications +{ + param($objCopyFrom, $objNew, $objectType) + + foreach($localizedNotificationMessage in $objCopyFrom.localizedNotificationMessages) + { + Start-GraphPreImport $localizedNotificationMessage $objectType + Invoke-GraphRequest -Url "$($objectType.API)/$($objNew.id)/localizedNotificationMessages" -Body ($localizedNotificationMessage | ConvertTo-Json -Depth 20) -Method "POST" + } +} +#endregion + +#region Enrollment Status Page functions +function Start-PreImportESP +{ + param($obj, $objectType) + + if($obj.Priority -eq 0) + { + $ret = @{} + $ret.Add("API","$($objectType.API)/$($obj.Id)") + $ret.Add("Method","PATCH") # Default profile always exists so update them + $ret + } + else + { + Remove-Property $obj "Id" + } +} + +function Start-PostExportESP +{ + param($obj, $objectType, $path) + + if($obj.Priority -eq 0) + { + Save-EMDefaultPolicy $obj $objectType $path + } +} + +function Start-PostListESP +{ + param($objList, $objectType) + + # endswith not working so filter them out + $objList | Where { $_.Object.'@OData.Type' -eq '#microsoft.graph.windows10EnrollmentCompletionPageConfiguration' } +} +#endregion + +#region Enrollment Restriction functions + +function Start-PostExportEnrollmentRestrictions +{ + param($obj, $objectType, $path) + + if($obj.Priority -eq 0) + { + Save-EMDefaultPolicy $obj $objectType $path + } +} + +function Start-PreImportEnrollmentRestrictions +{ + param($obj, $objectType) + + if($obj.Priority -eq 0) + { + $ret = @{} + $ret.Add("API","$($objectType.API)/$($obj.Id)") + $ret.Add("Method","PATCH") # Default profile always exists so update them + $ret + } + else + { + Remove-Property $obj "Id" + } + + if($obj.windowsMobileRestriction) + { + # Windows Phone operations are no longer supported + Remove-Property $obj "windowsMobileRestriction" + } +} + +function Start-PreDeleteEnrollmentRestrictions +{ + param($obj, $objectType) + + if($obj.Priority -eq 0) + { + @{ "Delete" = $false } + } +} + +function Start-PreReplaceEnrollmentRestrictions +{ + param($obj, $objectType, $sourceObj, $fromFile) + + if($sourceObj.Priority -eq 0) { @{ "Replace" = $false } } +} + +function Start-PostReplaceEnrollmentRestrictions +{ + param($obj, $objectType, $sourceObj, $fromFile) + + if($sourceObj.Priority -eq 0) { return } + + $api = "/deviceManagement/deviceEnrollmentConfigurations/$($obj.id)/setpriority" + + $priority = [PSCustomObject]@{ + priority = $sourceObj.Priority + } + $json = $priority | ConvertTo-Json -Depth 20 + + Write-Log "Update priority for $($obj.displayName) to $($sourceObj.Priority)" + Invoke-GraphRequest $api -HttpMethod "POST" -Content $json +} + +function Start-PreFilesImportEnrollmentRestrictions +{ + param($objectType, $filesToImport) + + $filesToImport | sort-object -property @{e={$_.Object.priority}} +} + +function Start-PreUpdateEnrollmentRestrictions +{ + param($obj, $objectType, $curObject, $fromObj) + + Remove-Property $obj "priority" +} + +function Start-PostListEnrollmentRestrictions +{ + param($objList, $objectType) + + # endswith not working so filter them out + $objList | Where { + ($_.Object.'@OData.Type' -eq '#microsoft.graph.deviceEnrollmentPlatformRestrictionConfiguration' -or + $_.Object.'@OData.Type' -eq '#microsoft.graph.deviceEnrollmentLimitConfiguration' -or + $_.Object.'@OData.Type' -eq '#microsoft.graph.deviceEnrollmentPlatformRestrictionsConfiguration') -and + $_.Object.id -notlike "*_PlatformRestrictions" -and $_.Object.platformType -ne "WindowsPhone" -and $_.Object.platformType -ne "AndroidAosp" + } +} + +function Start-PreImportAssignmentsEnrollmentRestrictions +{ + param($obj, $objectType, $file, $assignments) + + if($obj.Priority -eq 0) + { + # Skip Assignment for Default Policy + @{ "Import" = $false } + } +} + +#endregion + +#region +function Start-PostListCoManagementSettings +{ + param($objList, $objectType) + + # endswith not working so filter them out + $objList | Where { $_.Object.'@OData.Type' -eq '#microsoft.graph.deviceComanagementAuthorityConfiguration' } +} +#endregion + +#region ScopeTags +function Start-PostExportScopeTags +{ + param($obj, $objectType, $path) + + Add-EMAssignmentsToExportFile $obj $objectType $path +} + +function Start-PostGetScopeTags +{ + param($obj, $objectType) + + $strAPI = "$($objectType.API)/$($obj.Object.Id)/assignments" + $tmpObj = Invoke-GraphRequest -Url $strAPI + + if(($tmpObj.value | measure).count -gt 0) + { + $obj.Object.assignments = $tmpObj.value + } +} +#endregion + +#region AutoPilot +function Start-PreImportAssignmentsAutoPilot +{ + param($obj, $objectType, $file, $assignments) + + Add-EMAssignmentsToObject $obj $objectType $file $assignments +} + +function Start-PreDeleteAutoPilot +{ + param($obj, $objectType) + + Write-Log "Delete AutoPilot profile assignments" + + if(-not $obj.Assignments) + { + $tmpObj = (Get-GraphObject $obj $objectType).Object + } + else + { + $tmpObj = $obj + } + + foreach($assignment in $tmpObj.Assignments) + { + if($assignment.Source -ne "direct") { continue } + + $api = "/deviceManagement/windowsAutopilotDeploymentProfiles/$($obj.Id)/assignments/$($assignment.Id)" + + Invoke-GraphRequest $api -HttpMethod "DELETE" + } +} + +#endregion + +#region Health Scripts + +function Start-PreDeleteDeviceHealthScripts +{ + param($obj, $objectType) + + if($obj.isGlobalScript -eq $true) + { + @{ "Delete" = $false } + } +} + +function Start-PreImportDeviceHealthScripts +{ + param($obj, $objectType, $file, $assignments) + + if($obj.isGlobalScript -eq $true) + { + @{ "Import" = $false } + } +} + +function Start-PreUpdateDeviceHealthScripts +{ + param($obj, $objectType, $curObject, $fromObj) + + if($curObject.Object.isGlobalScript -eq $true) + { + @{ "Import" = $false } + } +} + +function Start-PostExportDeviceHealthScripts +{ + param($obj, $objectType, $path) + + if($global:chkExportScript.IsChecked) + { + $fileName = Get-GraphObjectFile $obj $objectType + $fi = [IO.FileInfo]"$path\$fileName" + + try + { + if($obj.detectionScriptContent) + { + [IO.File]::WriteAllBytes(("$path\$($fi.BaseName)_DetectionScript.ps1"), ([System.Convert]::FromBase64String($obj.detectionScriptContent))) + } + + if($obj.remediationScriptContent) + { + [IO.File]::WriteAllBytes(("$path\$($fi.BaseName)_RemediationScript.ps1"), ([System.Convert]::FromBase64String($obj.remediationScriptContent))) + } + } + catch + { + Write-LogError "Failed to export scripts" $_.Exception + } + } +} + +#endregion + +#region Generic functions + +function Save-EMDefaultPolicy +{ + param($obj, $objectType, $path) + + if($obj.Priority -eq 0) + { + try + { + $fileName = $obj.Id.Split('_')[1] + + if($fileName) + { + $oldFile = "$path\$((Get-GraphObjectName $obj $objectType)).json" + if([IO.File]::Exists($oldFile)) + { + # Clean up from old version of the script that used the wrong name for Default policies + try { [IO.File]::Delete($oldFile) | Out-Null } Catch {} + } + Save-GraphObjectToFile $obj "$path\$((Remove-InvalidFileNameChars $fileName)).json" + } + } + catch {} + } +} +function Get-EMSettingsObject +{ + param($obj, $objectType, $file, $settingsProperty = "settings", [switch]$SettingsArray) + + if($obj.$settingsProperty) { return $obj.$settingsProperty } + + $fi = [IO.FileInfo]$file + if($fi.Exists) + { + # Settings property removed during import so lets try exported file first + $tmpObj = Get-GraphObjectFromFile $fi.FullName + if($SettingsArray -eq $true) + { + # Only the an array of settings is expected + return $tmpObj.$settingsProperty + } + else + { + if($tmpObj.$settingsProperty) + { + # A property with the an array of settings is expected + return ([PSCustomObject]@{ + $settingsProperty = $tmpObj.$settingsProperty + }) + } + } + + Write-Log "Settings not included in export file. Try import from _Settings.json file" 2 + $settingsFile = $fi.DirectoryName + "\" + $fi.BaseName + "_Settings.json" + $fiSettings = [IO.FileInfo]$settingsFile + if($fiSettings.Exists -eq $false) + { + Write-Log "Settings file '$($fiSettings.FullName)' was not found" 2 + return + } + Get-GraphObjectFromFile $fiSettings.FullName + } + else + { + Write-Log "Settings not included in export file and _Settings.json file is missing." 3 + } +} + +function Add-EMAssignmentsToExportFile +{ + param($obj, $objectType, $path, $Url = "") + + if($global:chkExportAssignments.IsChecked -ne $true) { return } + + $fileName = (Get-GraphObjectName $obj $objectType).Trim('.') + if((Get-SettingValue "AddIDToExportFile") -eq $true -and $obj.Id) + { + $fileName = ($fileName + "_" + $obj.Id) + } + $fileName = "$path\$((Remove-InvalidFileNameChars $fileName)).json" + if([IO.File]::Exists($fileName) -eq $false) + { + Write-Log "File not found: $fileName. Could not add assignments to file" 3 + return + } + + $tmpObj = Get-GraphObjectFromFile $fileName + + if(-not $url) + { + $url = "$($objectType.API)/$($obj.id)/assignments" + } + $assignments = (Invoke-GraphRequest -Url $url -ODataMetadata "Minimal").Value + if($assignments) + { + if(-not ($tmpObj.PSObject.Properties | Where Name -eq "assignments")) + { + $tmpObj | Add-Member -MemberType NoteProperty -Name "assignments" -Value $assignments + } + else + { + $tmpObj.Assignments = $assignments + } + Save-GraphObjectToFile $tmpObj $fileName + } +} + +function Add-EMAssignmentsToObject +{ + param($obj, $objectType, $file, $assignments) + + # AutoPilot and TaC are using assignments and not assign like other object types + $api = "$($objectType.API)/$($obj.Id)/assignments" + + # These profiles don't support importing of multiple assignments with { "assignment" [...]} + # Each assignment must be imported separately + + foreach($assignment in $assignments) + { + if($assignment.Source -and $assignment.Source -ne "direct") { continue } + + foreach($prop in $assignment.PSObject.Properties) + { + if($prop.Name -in @("Target")) { continue } + Remove-Property $assignment $prop.Name + } + + foreach($prop in $assignment.target.PSObject.Properties) + { + if($prop.Name -in @("@odata.type","groupId")) { continue } + Remove-Property $assignment.target $prop.Name + } + + $json = Update-JsonForEnvironment ($assignment | ConvertTo-Json -Depth 20) + Invoke-GraphRequest -Url $api -Body $json -Method "POST" | Out-Null + } + @{"Import"=$false} +} + +#endregion + +#region Mac Custom Scripts + +function Start-PreUpdateMacCustomAttributes +{ + param($obj, $objectType, $curObject, $fromObj) + + foreach($prop in @('customAttributeName','customAttributeType','displayName')) + { + Remove-Property $obj $prop + } +} + +#endregion + +#region Mac Feature Updates +function Start-PreUpdateFeatureUpdates +{ + param($obj, $objectType, $curObject, $fromObj) + + foreach($prop in @('deployableContentDisplayName','endOfSupportDate')) + { + Remove-Property $obj $prop + } +} +#endregion + +#region Conditional Access +function Add-ConditionalAccessImportExtensions +{ + param($form, $buttonPanel, $index = 0) + + $xaml = @" + + +"@ + $label = [Windows.Markup.XamlReader]::Parse($xaml) + + $CAStates = @() + $CAStates += [PSCustomObject]@{ + Name = "As Exported - Change On to Report-only" + Value = "AsExportedReportOnly" + } + + $CAStates += [PSCustomObject]@{ + Name = "As Exported" + Value = "AsExported" + } + + $CAStates += [PSCustomObject]@{ + Name = "Report-only" + Value = "enabledForReportingButNotEnforced" + } + + $CAStates += [PSCustomObject]@{ + Name = "On" + Value = "enabled" + } + + $CAStates += [PSCustomObject]@{ + Name = "Off" + Value = "disabled" + } + + $defaultCAState = Get-SettingValue "ConditionalAccessState" + + $global:cbImportCAState = [System.Windows.Controls.ComboBox]::new() + $global:cbImportCAState.DisplayMemberPath = "Name" + $global:cbImportCAState.SelectedValuePath = "Value" + $global:cbImportCAState.ItemsSource = $CAStates + $global:cbImportCAState.SelectedValue = $defaultCAState + $global:cbImportCAState.Margin="0,5,0,0" + $global:cbImportCAState.HorizontalAlignment="Left" + $global:cbImportCAState.Width=250 + $global:cbImportCAState.Name = "cbImportCAState" + + @($label, $global:cbImportCAState) +} + +function Start-PreImportConditionalAccess +{ + param($obj, $objectType, $file, $assignments) + + if ($global:cbImportCAState.SelectedValue -and $global:cbImportCAState.SelectedValue -ne "AsExported") { + if ($global:cbImportCAState.SelectedValue -eq "AsExportedReportOnly" -and $obj.state -eq "enabled") { + Write-Log "Change Enabled policy to Report-only" + $obj.state = "enabledForReportingButNotEnforced" + } + else { + $obj.state = $global:cbImportCAState.SelectedValue + } + } + + if($obj.grantControls.authenticationStrength) + { + $obj.grantControls.operator = "AND" + $tmpObj = Get-GraphObjectFromFile $file + + $authSetting = [PSCustomObject]@{ + id = $tmpObj.grantControls.authenticationStrength.id + } + $obj.grantControls.authenticationStrength = $authSetting + } + + if($obj.sessionControls.disableResilienceDefaults -eq $false) + { + $obj.sessionControls.disableResilienceDefaults = $null + } + + # DeviceStates property is depricated + if(($obj.conditions.PSObject.Properties | Where Name -eq "DeviceStates")) + { + $obj.conditions.PSObject.Properties.Remove('DeviceStates') + } +} + +function Start-PostExportConditionalAccess +{ + param($obj, $objectType, $path) + + $ids = @() + foreach($id in ($obj.conditions.users.includeGroups + $obj.conditions.users.excludeGroups)) + { + if($id -in $ids) { continue } + elseif($id -eq "GuestsOrExternalUsers") { continue } + elseif($id -eq "All") { continue } + elseif($id -eq "None") { continue } + + $ids += $id + Add-GraphMigrationObject $id "/groups" "Group" + } + + foreach($id in ($obj.conditions.users.includeUsers +$obj.conditions.users.excludeUsers)) + { + if($id -in $ids) { continue } + elseif($id -eq "GuestsOrExternalUsers") { continue } + elseif($id -eq "All") { continue } + elseif($id -eq "None") { continue } + + $ids += $id + Add-GraphMigrationObject $id "/users" "User" + } + + <# + $roleIds = @() + foreach($id in ($obj.conditions.users.includeRoles + $obj.conditions.users.excludeRoles)) + { + if($id -in $ids) { continue } + $roleIds += $id + } + #> +} +#endregion + +#region Terms of use +function Start-PreImportTermsOfUse +{ + param($obj, $objectType, $file, $assignments) + + $pkgPath = Get-SettingValue "EMIntuneAppPackages" + + if(-not $pkgPath -or [IO.Directory]::Exists($pkgPath) -eq $false) + { + Write-Log "Intune app directory is either missing or does not exist" 2 + } + + try + { + $fi = [IO.FileInfo]$file + } catch {} + + foreach($file in $obj.Files) + { + $pdfFile = $null + + if($fi.Directory.FullName) + { + $pdfFile = "$($fi.Directory.FullName)\$($file.fileName)" + } + + if($null -eq $pdfFile -or [IO.File]::Exists($pdfFile) -eq $false) + { + $pdfFile = "$($pkgPath)\$($file.fileName)" + } + + if([IO.File]::Exists($pdfFile) -eq $false) + { + Write-Log "Terms of use file $($file.fileName) not found. The Terms of Use object will not be imported." 2 + @{"Import" = $false} + return + } + + Write-Log "Add file data: $pdfFile" + + $bytes = [IO.File]::ReadAllBytes($pdfFile) + $file.fileData = [PSCustomObject]@{ + data = [Convert]::ToBase64String($bytes) + } + } +} + +function Start-PostExportTermsOfUse +{ + param($obj, $objectType, $path) + + foreach($file in $obj.Files) + { + $url = "agreements/$($obj.id)/file/localizations('$($file.id)')/fileData/data" + $data = (Invoke-GraphRequest -Url $url -ODataMetadata "Minimal").Value + if($data) + { + Write-Log "Save file $($file.FileName)" + $fileName = "$path\$($file.FileName)" + [IO.File]::WriteAllBytes($fileName, [System.Convert]::FromBase64String($data)) + } + } +} + +#endregion + +#region ADMXFiles + +function Start-PreFilesImportADMXFiles +{ + param($objectType, $filesToImport) + + $filesToImport | sort-object -property @{e={$_.Object.lastModifiedDateTime}} +} + +function Start-PreImportADMXFiles +{ + param($obj, $objectType, $file, $assignments) + + $pkgPath = Get-SettingValue "EMIntuneAppPackages" + + if(-not $pkgPath -or [IO.Directory]::Exists($pkgPath) -eq $false) + { + Write-Log "Intune app directory is either missing or does not exist" 2 + $pkgPath = $null + } + + try + { + $fi = [IO.FileInfo]$file + } catch {} + + $admxFile = $null + + if($fi.Directory.FullName) + { + $admxFile = "$($fi.Directory.FullName)\$($obj.fileName)" + $admlFile = "$($fi.Directory.FullName)\$([io.path]::GetFileNameWithoutExtension($obj.fileName)).adml" + } + + if($null -ne $pkgPath -and ($null -eq $admxFile -or [IO.File]::Exists($admxFile) -eq $false -or [IO.File]::Exists($admxFile) -eq $false)) + { + Write-Log "$($obj.fileName) not foud in Export folder. Look in package path: $pkgPath" + $admxFile = "$($pkgPath)\$($obj.fileName)" + $admlFile = "$($pkgPath)\$([io.path]::GetFileNameWithoutExtension($obj.fileName)).adml" + } + + if([IO.File]::Exists($admxFile) -eq $false) + { + Write-Log "ADMX (or ADML) file $($obj.fileName) not found. The ADMXFile object will not be imported." 2 + @{"Import" = $false} + return + } + + #$bytes = [IO.File]::ReadAllBytes($admxFile) + $bytes = Get-ASCIIBytes ([IO.File]::ReadAllText($admxFile)) + $obj.content = [Convert]::ToBase64String($bytes) + + #$bytes = [IO.File]::ReadAllBytes($admlFile) + $bytes = Get-ASCIIBytes ([IO.File]::ReadAllText($admlFile)) + + $obj.groupPolicyUploadedLanguageFiles += [PSCustomObject]@{ + fileName = [io.path]::GetFileName($admlFile) + content = [Convert]::ToBase64String($bytes) + languageCode = (?? $obj.defaultLanguageCode "en-US") + } + $obj.defaultLanguageCode = "" +} + +function Start-PostImportADMXFiles +{ + param($obj, $objectType, $file) + + $script:CustomADMXDefinitions = $null +} + +function Start-PreDeleteADMXFiles +{ + param($obj, $objectType) + + Write-Status "Delete $($obj.fileName)" + $strAPI = ($objectType.API + "/$($obj.Id)/remove") + Write-Log "Delete $($objectType.Title) object $($obj.fileName)" + Invoke-GraphRequest -Url $strAPI -HttpMethod "POST" -ODataMetadata "none" | Out-Null + + @{ "Delete" = $false } +} + +#endregion + +#region Reusable Groups +function Start-PostGetReusableSettings +{ + param($obj, $objectType) + + $strAPI = "$($objectType.API)/$($obj.Object.Id)?`$select=settinginstance,displayname,description" + $tmpObj = Invoke-GraphRequest -Url $strAPI + + if($tmpObj.settingInstance) + { + $obj.Object | Add-Member Noteproperty -Name "settingInstance" -Value $tmpObj.settingInstance -Force + } +} + +#endregon + +#region Authentication Strength +function Start-PreImportCommandAuthenticationStrengths +{ + param($obj, $objectType, $file, $assignments) + + if($obj.policyType -ne "custom") + { + Write-Log "Built-in Authentication Strength objects cannot be imported" 2 + @{ "Import" = $false } + } +} +#endregion + +#region Authentication Strength +function Start-PreImportCommandAuthenticationContext +{ + param($obj, $objectType, $file, $assignments) + + #@{ "Method" = "PATCH" } + +} +#endregion + + Export-ModuleMember -alias * -function * \ No newline at end of file From 0492b784a1b33dbca5326730dc53b02a6e0eab80 Mon Sep 17 00:00:00 2001 From: Mikael Karlsson <43226266+Micke-K@users.noreply.github.com> Date: Fri, 14 Aug 2026 21:56:06 +1000 Subject: [PATCH 2/4] Added support for System Broswer login --- Extensions/MSALAuthentication.psm1 | 72 +++++++++++++++++++++++++----- 1 file changed, 62 insertions(+), 10 deletions(-) diff --git a/Extensions/MSALAuthentication.psm1 b/Extensions/MSALAuthentication.psm1 index 32fc7d2..e9d3e32 100644 --- a/Extensions/MSALAuthentication.psm1 +++ b/Extensions/MSALAuthentication.psm1 @@ -10,7 +10,7 @@ This module manages Authentication for the application with MSAL. It is also res #> function Get-ModuleVersion { - '3.9.8a' + '3.9.9' } $global:msalAuthenticator = $null @@ -137,12 +137,28 @@ function Invoke-InitializeModule Description = "Use WAM for enhanced login methods" }) "MSAL" + Add-SettingsObject (New-Object PSObject -Property @{ + Title = "Use System Browser for login" + Key = "UseSystemBrowser" + Type = "Boolean" + DefaultValue = $false + Description = "Use the default system browser (Edge/Chrome/Firefox) for interactive login instead of the embedded WebView. Required for passkey / FIDO2 sign-in. Redirect URI is forced to http://localhost when enabled. Takes precedence over WAM. Note: Requires restart" + }) "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 $script:MSALUseWAM = $false } + # System Browser takes precedence over WAM: passkey/FIDO2 login works in the real browser + # but not in the WAM pane or the embedded WebView. If both are enabled, disable WAM. + $script:MSALUseSystemBrowser = Get-SettingValue "UseSystemBrowser" + if($script:MSALUseSystemBrowser -and $script:MSALUseWAM) { + Write-Log "Both UseWAM and UseSystemBrowser are enabled - the system browser takes precedence for interactive login" 2 + $script:MSALUseWAM = $false + } + Add-MSALPrereq } @@ -931,9 +947,21 @@ function Get-MSALApp [void]$appBuilder.WithAuthority($authority) - if($appInfo.RedirectUri) { [void]$appBuilder.WithRedirectUri($appInfo.RedirectUri) } + # System Browser mode: MSAL's system-browser flow only accepts loopback redirects, + # so any custom redirect URI (nativeclient, ms-appx-web://, etc.) is replaced with + # http://localhost. The app registration in Entra must have this loopback URI added + # under the "Mobile and desktop applications" platform for the login to succeed. + $redirectUri = $appInfo.RedirectUri + if($script:MSALUseSystemBrowser -and $redirectUri -and ($redirectUri -notmatch '^http://localhost')) { + Write-LogDebug "UseSystemBrowser: overriding redirect URI '$redirectUri' with http://localhost" + $redirectUri = "http://localhost" + } + elseif($script:MSALUseSystemBrowser -and -not $redirectUri) { + $redirectUri = "http://localhost" + } + if($redirectUri) { [void]$appBuilder.WithRedirectUri($redirectUri) } - [void] $appBuilder.WithClientName("CloudAPIPowerShellManagement") + [void] $appBuilder.WithClientName("CloudAPIPowerShellManagement") [void] $appBuilder.WithClientVersion($PSVersionTable.PSVersion) if($script:MSALUseWAM) { @@ -1288,14 +1316,23 @@ function Connect-MSALUser [IntPtr]$ParentWindow = [System.Diagnostics.Process]::GetCurrentProcess().MainWindowHandle if ($ParentWindow) { - [void]$aquireTokenObj.WithParentActivityOrWindow($ParentWindow) + [void]$aquireTokenObj.WithParentActivityOrWindow($ParentWindow) + } + + # UseSystemBrowser: force MSAL to launch the default system browser instead of + # any embedded WebView. Required for passkey / FIDO2 sign-in which the embedded + # WebView cannot service. + if($script:MSALUseSystemBrowser) + { + try { [void]$aquireTokenObj.WithUseEmbeddedWebView($false) } + catch { Write-LogDebug "WithUseEmbeddedWebView unavailable: $($_.Exception.Message)" } } # If we need a consent (e.g. App is not approved in the environment) - if ($script:authenticationFailure.Classification -eq "ConsentRequired") + if ($script:authenticationFailure.Classification -eq "ConsentRequired") { - Write-Log "Interactive login with Consent prompt" - [void]$aquireTokenObj.WithPrompt([Microsoft.Identity.Client.Prompt]::Consent) + Write-Log "Interactive login with Consent prompt" + [void]$aquireTokenObj.WithPrompt([Microsoft.Identity.Client.Prompt]::Consent) } $authResult = Get-MsalAuthenticationToken $aquireTokenObj @@ -1346,8 +1383,18 @@ function Connect-MSALUser $appBuilder = [Microsoft.Identity.Client.PublicClientApplicationBuilder]::Create($global:appObj.ClientID) 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) } - + + # Match the redirect URI substitution done in Get-MSALApp - keeps this + # secondary MSAL app consistent with System Browser mode. + $tenantRedirectUri = $global:appObj.RedirectUri + if($script:MSALUseSystemBrowser -and $tenantRedirectUri -and ($tenantRedirectUri -notmatch '^http://localhost')) { + $tenantRedirectUri = "http://localhost" + } + elseif($script:MSALUseSystemBrowser -and -not $tenantRedirectUri) { + $tenantRedirectUri = "http://localhost" + } + if($tenantRedirectUri) { [void]$appBuilder.WithRedirectUri($tenantRedirectUri) } + Add-MSALProxy $appBuilder $app = $appBuilder.Build() @@ -1366,7 +1413,12 @@ function Connect-MSALUser $AquireTokenObj = $app.AcquireTokenInteractive($tmpScope) #[void]$AquireTokenObj.WithAccount($authResult.Account) [void]$AquireTokenObj.WithLoginHint($authResult.Account.Username) - [void]$AquireTokenObj.WithPrompt([Microsoft.Identity.Client.Prompt]::NoPrompt) + [void]$AquireTokenObj.WithPrompt([Microsoft.Identity.Client.Prompt]::NoPrompt) + if($script:MSALUseSystemBrowser) + { + try { [void]$AquireTokenObj.WithUseEmbeddedWebView($false) } + catch { Write-LogDebug "WithUseEmbeddedWebView unavailable: $($_.Exception.Message)" } + } $tmpResults = Get-MsalAuthenticationToken $AquireTokenObj } From f96037a9d00491da546df8fc163d577d21c4d74f Mon Sep 17 00:00:00 2001 From: Mikael Karlsson <43226266+Micke-K@users.noreply.github.com> Date: Wed, 23 Sep 2026 19:48:02 +1000 Subject: [PATCH 3/4] 3.11.0: system browser sign-in, update check limited to 3.x, GitHub templates --- .github/DISCUSSION_TEMPLATE/ideas.yml | 39 +++++++ .github/DISCUSSION_TEMPLATE/q-a.yml | 64 +++++++++++ .github/FUNDING.yml | 4 + .github/ISSUE_TEMPLATE/bug-v3.yml | 116 +++++++++++++++++++ .github/ISSUE_TEMPLATE/bug-v4.yml | 154 ++++++++++++++++++++++++++ .github/ISSUE_TEMPLATE/config.yml | 17 +++ .github/ISSUE_TEMPLATE/feature.yml | 71 ++++++++++++ .github/pull_request_template.md | 47 ++++++++ .gitignore | 2 + CloudAPIPowerShellManagement.psd1 | 2 +- Core.psm1 | 67 ++++++----- README.md | 6 + ReleaseNotes.md | 32 ++++++ SECURITY.md | 67 +++++++++++ 14 files changed, 657 insertions(+), 31 deletions(-) create mode 100644 .github/DISCUSSION_TEMPLATE/ideas.yml create mode 100644 .github/DISCUSSION_TEMPLATE/q-a.yml create mode 100644 .github/FUNDING.yml create mode 100644 .github/ISSUE_TEMPLATE/bug-v3.yml create mode 100644 .github/ISSUE_TEMPLATE/bug-v4.yml create mode 100644 .github/ISSUE_TEMPLATE/config.yml create mode 100644 .github/ISSUE_TEMPLATE/feature.yml create mode 100644 .github/pull_request_template.md create mode 100644 SECURITY.md diff --git a/.github/DISCUSSION_TEMPLATE/ideas.yml b/.github/DISCUSSION_TEMPLATE/ideas.yml new file mode 100644 index 0000000..8681531 --- /dev/null +++ b/.github/DISCUSSION_TEMPLATE/ideas.yml @@ -0,0 +1,39 @@ +title: "" +body: + - type: markdown + attributes: + value: | + Ideas is for shaping something before it becomes a request. Say what you + are trying to achieve rather than the control you picture, and it will be + clear whether the application should grow a feature or already has one. + + - type: dropdown + id: version + attributes: + label: Which version are you using? + options: + - 4.0 beta + - 3.x + - Neither yet, just looking + validations: + required: true + + - type: textarea + id: goal + attributes: + label: What are you trying to achieve? + validations: + required: true + + - type: textarea + id: idea + attributes: + label: How do you picture it working? + + - type: textarea + id: scale + attributes: + label: How often, and at what scale? + description: > + How many tenants, how many policies, how often you do it. Scale changes + the answer more than anything else here. diff --git a/.github/DISCUSSION_TEMPLATE/q-a.yml b/.github/DISCUSSION_TEMPLATE/q-a.yml new file mode 100644 index 0000000..eade962 --- /dev/null +++ b/.github/DISCUSSION_TEMPLATE/q-a.yml @@ -0,0 +1,64 @@ +title: "[Question] " +labels: ["needs-triage"] +body: + - type: markdown + attributes: + value: | + Most questions here turn out to be one of four things: a sign-in method + the embedded window cannot complete, a list that looks short because the + version you are on stops paging, a permission the app registration does + not hold, or an object type that has no public Graph endpoint. The fields + below let that be spotted straight away. + + - type: dropdown + id: version + attributes: + label: Version + options: + - 4.0 beta + - 3.x + validations: + required: true + + - type: dropdown + id: signin + attributes: + label: How do you sign in? + options: + - Interactive, embedded window (the default in v3) + - Interactive, Web Account Manager (WAM) + - Interactive, system browser (the default in v4) + - Device code + - Application and secret + - Application and certificate + - Managed identity or federated credential + - Bring your own token + validations: + required: true + + - type: dropdown + id: cloud + attributes: + label: Cloud + options: + - Public (commercial) + - US Government (GCC High) + - US Government (DoD) + - China (21Vianet) + validations: + required: true + + - type: textarea + id: question + attributes: + label: What are you trying to do? + validations: + required: true + + - type: textarea + id: tried + attributes: + label: What have you tried, and what happened? + description: > + Paste any error text here. **Remove tenant identifiers, user names and + tokens first.** diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..9c07b7c --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,4 @@ +# GitHub renders this as the "Sponsor" button on the repository page +# (public repositories, default branch). Buy Me a Coffee takes the +# username, not the URL: https://buymeacoffee.com/MickeK +buy_me_a_coffee: MickeK diff --git a/.github/ISSUE_TEMPLATE/bug-v3.yml b/.github/ISSUE_TEMPLATE/bug-v3.yml new file mode 100644 index 0000000..b4ec919 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug-v3.yml @@ -0,0 +1,116 @@ +name: Bug report (version 3.x) +description: Something is wrong in the current release. Windows only. +title: "[3.x] " +labels: ["bug", "v3", "needs-triage"] +body: + - type: markdown + attributes: + value: | + Version 3 stays supported until 4.0 leaves beta. + + Before filing, check whether 4.0 already fixes it. Several long-standing + reports here are addressed there: sign-in with passkeys and other + phishing-resistant methods, lists that stopped at 20, 100 or a few + hundred objects, sovereign cloud sign-in, and running without a user + present. The 4.0 beta lives on the `v4` branch. + + - type: checkboxes + id: preflight + attributes: + label: Before reporting + options: + - label: > + If my sign-in involves a passkey, security key, Windows Hello or a + phishing-resistant policy: I know the embedded sign-in window cannot + complete those, and I have said so below rather than reporting it as + a broken login. + required: true + - label: > + I searched existing issues and discussions, including closed ones. + required: true + + - type: input + id: version + attributes: + label: Version + placeholder: 3.10.3 + validations: + required: true + + - type: dropdown + id: signin + attributes: + label: How did you sign in? + options: + - Interactive, embedded window (the default) + - Interactive, other + - Application and secret + - Application and certificate + - Not signed in / sign-in is the problem + validations: + required: true + + - type: dropdown + id: cloud + attributes: + label: Cloud + options: + - Public (commercial) + - US Government (GCC High) + - US Government (DoD) + - China (21Vianet) + validations: + required: true + + - type: input + id: powershell + attributes: + label: PowerShell version + description: Run `$PSVersionTable.PSVersion`. + placeholder: "5.1.22621.4391" + validations: + required: true + + - type: dropdown + id: area + attributes: + label: Which part of the application? + options: + - Sign-in and authentication + - Export + - Import + - Copy + - Compare + - Documentation output + - Assignments, groups or filters + - Bulk or silent operations + - ADMX or tools + - The user interface itself + - Something else + validations: + required: true + + - type: textarea + id: what + attributes: + label: What happened, and what did you expect instead? + validations: + required: true + + - type: textarea + id: steps + attributes: + label: Steps to reproduce + validations: + required: true + + - type: textarea + id: log + attributes: + label: Log + description: > + The relevant lines, or the error text. **Remove tenant identifiers, user + names, access tokens and secrets before pasting.** + render: text + validations: + required: true diff --git a/.github/ISSUE_TEMPLATE/bug-v4.yml b/.github/ISSUE_TEMPLATE/bug-v4.yml new file mode 100644 index 0000000..7327b65 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug-v4.yml @@ -0,0 +1,154 @@ +name: Bug report (version 4.0 beta) +description: Something is wrong in 4.0. Runs on Windows, macOS and Linux. +title: "[4.0] " +labels: ["bug", "v4", "needs-triage"] +body: + - type: markdown + attributes: + value: | + Thanks for testing the beta. Four fields below do most of the work: + **how you signed in**, **which cloud**, **which operating system** and + **the log**. Reports without them usually need a round trip before + anything can happen. + + - type: checkboxes + id: preflight + attributes: + label: Before reporting + description: These three cover the majority of beta reports so far. + options: + - label: > + I read the release notes for this beta, including the breaking changes. + required: true + - label: > + If my sign-in involves a passkey, security key, Windows Hello or any + phishing-resistant policy: I enabled **Use Web Account Manager (WAM) + for login** or **Use system browser for login** in Settings, and tried + again. The embedded window cannot complete those methods. + required: true + - label: > + My problem is not Inventory Policies returning 403. That endpoint is + not published on the public Graph API, so the application cannot read + it with a normal sign-in. It is a Microsoft limitation, not a bug. + A bring-your-own-token sign-in can reach it. + required: true + + - type: input + id: version + attributes: + label: Version + description: The About dialog, or the ModuleVersion in IntuneManagement.psd1. + placeholder: 4.0.0-beta1 + validations: + required: true + + - type: dropdown + id: signin + attributes: + label: How did you sign in? + description: > + The single most useful field in this form. Roughly a third of all reports + on this project have turned out to be sign-in behaviour rather than the + feature being reported. + options: + - Interactive, embedded window + - Interactive, Web Account Manager (WAM) + - Interactive, system browser (the default) + - Device code + - Application and secret + - Application and certificate + - Managed identity or federated credential + - Bring your own token + - Not signed in / sign-in is the problem + validations: + required: true + + - type: dropdown + id: cloud + attributes: + label: Cloud + options: + - Public (commercial) + - US Government (GCC High) + - US Government (DoD) + - China (21Vianet) + validations: + required: true + + - type: dropdown + id: os + attributes: + label: Operating system + description: 4.0 runs the full application outside Windows, so this now matters. + options: + - Windows + - macOS (Apple Silicon) + - macOS (Intel) + - Linux + validations: + required: true + + - type: input + id: powershell + attributes: + label: PowerShell edition and version + description: Run `$PSVersionTable.PSVersion` and `$PSVersionTable.PSEdition`. + placeholder: "7.4.6, Core" + validations: + required: true + + - type: dropdown + id: area + attributes: + label: Which part of the application? + options: + - Sign-in and authentication + - Export + - Import + - Copy + - Compare + - Documentation output + - Assignments, groups or filters + - Bulk operations + - ADMX or tools + - The user interface itself + - Automation through the PowerShell commands + - Something else + validations: + required: true + + - type: input + id: objecttype + attributes: + label: Which object type, if it is specific to one + placeholder: Settings Catalog, Conditional Access, Win32 app, ... + + - type: textarea + id: what + attributes: + label: What happened, and what did you expect instead? + validations: + required: true + + - type: textarea + id: steps + attributes: + label: Steps to reproduce + placeholder: | + 1. Sign in to ... + 2. Open ... + 3. Click ... + validations: + required: true + + - type: textarea + id: log + attributes: + label: Log + description: > + The relevant lines from the log file, or the error text. **Remove tenant + identifiers, user names, access tokens and secrets before pasting.** If + an exported policy file is needed, redact it or use one from a lab tenant. + render: text + validations: + required: true diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..c29d6df --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,17 @@ +# Turns off the "open a blank issue" escape hatch, so every report arrives +# through a form with the fields that make it answerable. Questions go to +# Discussions, which is where most of them already end up. +blank_issues_enabled: false +contact_links: + - name: Question, or not sure it is a bug + url: https://github.com/Micke-K/IntuneManagement/discussions/categories/q-a + about: Ask in Q&A. Most "it does not show my policies" reports are answered there in a day. + - name: Feature idea worth discussing first + url: https://github.com/Micke-K/IntuneManagement/discussions/categories/ideas + about: Ideas that are still taking shape belong here. A concrete request can go straight to an issue. + - name: Version 4.0 beta feedback + url: https://github.com/Micke-K/IntuneManagement/discussions/categories/announcements + about: Testing the 4.0 beta? Start at the pinned announcement, which lists what to try first. + - name: Security vulnerability + url: https://github.com/Micke-K/IntuneManagement/security/advisories/new + about: Never report a security problem in a public issue. Use private reporting. diff --git a/.github/ISSUE_TEMPLATE/feature.yml b/.github/ISSUE_TEMPLATE/feature.yml new file mode 100644 index 0000000..25cc7ae --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature.yml @@ -0,0 +1,71 @@ +name: Feature request +description: Something the application should do and does not. +title: "[Request] " +labels: ["enhancement", "needs-triage"] +body: + - type: markdown + attributes: + value: | + If the idea is still taking shape, [Ideas in + Discussions](https://github.com/Micke-K/IntuneManagement/discussions/categories/ideas) + is the better room. Use this form when you can describe the outcome you + want. + + - type: dropdown + id: version + attributes: + label: Which version is this for? + description: > + This one is read by a human, not by automation, so say what you mean. + A request that 4.0 already covers is worth checking against the release + notes first. + options: + - Version 4.0 + - Version 3.x + - Either + validations: + required: true + + - type: textarea + id: problem + attributes: + label: What are you trying to do? + description: > + The situation, not the solution. "I move policies between two tenants + every month and have to redo the assignments by hand" tells more than + "add a button". + validations: + required: true + + - type: textarea + id: proposal + attributes: + label: What would you like it to do? + validations: + required: true + + - type: textarea + id: workaround + attributes: + label: How do you handle it today? + description: Including "not at all", which is useful to know. + + - type: dropdown + id: area + attributes: + label: Which part of the application? + options: + - Export + - Import + - Copy + - Compare + - Documentation output + - Assignments, groups or filters + - Bulk operations + - ADMX or tools + - The user interface + - Automation through the PowerShell commands + - A policy type that is not supported yet + - Something else + validations: + required: true diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..6f6f3d5 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,47 @@ + + + + +## What does this change? + + + +## Related issue + + + +## How was it tested? + + + +- Version: +- Cloud: +- Operating system and PowerShell version: +- Tests run: + +## Anything a reviewer should know + + diff --git a/.gitignore b/.gitignore index c33ff82..e5d849d 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,5 @@ Extensions_dev/ .gitignore CloudAPIPowerShellManagement.log +AGENTS.md +CLAUDE.md diff --git a/CloudAPIPowerShellManagement.psd1 b/CloudAPIPowerShellManagement.psd1 index 50dc672..e5fa987 100644 --- a/CloudAPIPowerShellManagement.psd1 +++ b/CloudAPIPowerShellManagement.psd1 @@ -12,7 +12,7 @@ RootModule = 'CloudAPIPowerShellManagement.psm1' # Version number of this module. -ModuleVersion = '3.10.3' +ModuleVersion = '3.11.0' # Supported PSEditions # CompatiblePSEditions = @() diff --git a/Core.psm1 b/Core.psm1 index 71b7b75..88ac268 100644 --- a/Core.psm1 +++ b/Core.psm1 @@ -699,7 +699,11 @@ function Show-UpdatesDialog $params.Add("UseBasicParsing", $true) } - $content = Invoke-RestMethod "https://api.github.com/repos/Micke-K/IntuneManagement/contents/ReleaseNotes.md" @params + # The notes at the newest 3.x release tag, never at the default branch: that + # branch will carry version 4 once the branches are renamed. + $latestVer = Get-LatestGitHubVersion $params + $notesRef = if($latestVer) { "?ref=$($latestVer.ToString())" } else { "" } + $content = Invoke-RestMethod "https://api.github.com/repos/Micke-K/IntuneManagement/contents/ReleaseNotes.md$notesRef" @params if($content) { $txt = [System.Text.Encoding]::UTF8.GetString(([System.Convert]::FromBase64String($content.content))) @@ -724,6 +728,37 @@ function Show-UpdatesDialog Show-ModalForm "Release Notes" $script:dlgUpdates -HideButtons } +# Newest published 3.x release on GitHub, or $null. +# +# releases/latest answers the newest release of ANY version. The day 4.0.0 is +# published it would tell every 3.x installation to upgrade to a breaking +# change, and reading the manifest on the default branch stops working once the +# branches are renamed for version 4. The releases list filtered to this major +# is the one source that survives both. Pre-releases and drafts are skipped. +function Get-LatestGitHubVersion +{ + param($Params = @{}) + + $latest = $null + try + { + $releases = Invoke-RestMethod "https://api.github.com/repos/Micke-K/IntuneManagement/releases?per_page=100" @Params + foreach($release in @($releases)) + { + if($release.draft -or $release.prerelease) { continue } + $ver = $null + try { $ver = [version](([string]$release.tag_name) -replace '^v','') } catch { continue } + if($ver.Major -ne 3) { continue } + if($null -eq $latest -or $ver -gt $latest) { $latest = $ver } + } + } + catch + { + Write-Log "Failed to list GitHub releases: $($_.Exception.Message)" 2 + } + return $latest +} + function Get-IsLatestVersion { if($global:MainAppStarted -ne $true) @@ -742,35 +777,7 @@ function Get-IsLatestVersion $params.Add("UseBasicParsing", $true) } - $content = Invoke-RestMethod "https://api.github.com/repos/Micke-K/IntuneManagement/releases/latest" @params - if($content.Name) - { - try - { - $gitHubVer = [version]$content.Name - } - catch {} - } - - if($null -eq $gitHubVer) - { - $params = @{} - $proxyURI = Get-ProxyURI - if($proxyURI) - { - $params.Add("proxy", $proxyURI) - $params.Add("UseBasicParsing", $true) - } - - $content = Invoke-RestMethod "https://api.github.com/repos/Micke-K/IntuneManagement/contents/CloudAPIPowerShellManagement.psd1" @params - $gitHubText = [System.Text.Encoding]::UTF8.GetString(([System.Convert]::FromBase64String($content.content))) - $gitHubInfo = Get-ModuleDataTable $gitHubText - try - { - $gitHubVer = [version]$gitHubInfo.ModuleVersion - } - catch {} - } + $gitHubVer = Get-LatestGitHubVersion $params if(-not $gitHubVer) { diff --git a/README.md b/README.md index 2b43bc9..8865227 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,11 @@ # IntuneManagement with PowerShell and WPF UI +> **Version 4.0 is in beta.** A full rewrite that also runs on macOS and Linux, +> with every operation available as a PowerShell command for automation. +> Try it from the [`v4` branch](../../tree/v4) or the [4.0.0-beta1 pre-release](../../releases/tag/4.0.0-beta1). +> Version 3 stays here and stays supported until 4.0 is final. +> Report version 4 problems with the *Bug report (version 4.0 beta)* form. +

diff --git a/ReleaseNotes.md b/ReleaseNotes.md index 5203a93..8d6dad3 100644 --- a/ReleaseNotes.md +++ b/ReleaseNotes.md @@ -1,4 +1,36 @@ # Release Notes + +## About version 4 + +Version 4.0 is in beta on the `v4` branch ([4.0.0-beta1](https://github.com/Micke-K/IntuneManagement/releases/tag/4.0.0-beta1)). It is a rewrite: a single PowerShell module +with `IM`-prefixed commands, an Avalonia UI that runs on Windows, macOS and Linux, and a +public automation API. Exports made with 3.x import into 4.0. Version 3 stays supported +here until 4.0 is final. From 3.11.0 the update check only offers 3.x releases; older +3.x installations will be offered 4.0.0 once it is published as a final release. + +## 3.11.0 - 2026-09-23 + +**New features** + +- **Sign in with the system browser**
+ Interactive sign-in can run in your default browser instead of the embedded window, + which is where passkeys, security keys and other phishing-resistant methods work. + Turn on **Use system browser for login** in Settings.
+ Based on [Issue 435](https://github.com/Micke-K/IntuneManagement/issues/435) + and [Discussion 425](https://github.com/Micke-K/IntuneManagement/discussions/425)
+ +**Fixes** + +- Links to GitHub in the About, Updates and Welcome dialogs did not open the browser
+ Based on [Issue 428](https://github.com/Micke-K/IntuneManagement/issues/428)
+- Silent bulk compare failed with an `op_Addition` error and wrote no result CSV when + using the **Exported Files with Intune Objects (Id)** provider
+ [PR 429](https://github.com/Micke-K/IntuneManagement/pull/429) by McKenzieCo
+- Typo in the missing-permissions message
+ [PR 424](https://github.com/Micke-K/IntuneManagement/pull/424) by BuggyAl
+- The update check only offers 3.x releases and reads the release notes of the newest + 3.x release, so a version 4 release is never offered to a version 3 installation.
+ ## 3.10.3 - 2026-05-11 **Fixes** diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..cf93d1e --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,67 @@ +# Security Policy + +## Supported versions + +| Version | Branch | Status | +|---|---|---| +| 4.0 beta | `v4` | Pre-release. Fixes go here. | +| 3.x | default branch | Supported until 4.0 leaves beta. Security fixes only after that. | +| 2.x and earlier | - | Not supported. | + +## Reporting a vulnerability + +**Do not open a public issue for a security problem.** + +Use GitHub's private vulnerability reporting: go to the **Security** tab of this +repository and choose **Report a vulnerability**. That opens a private thread +visible only to the maintainer, and it works even though this repository has no +public contact address. + +If that is unavailable to you, contact the maintainer through the link in the +repository profile and ask for a private channel before sending any detail. + +### What to include + +The more of this you can provide, the faster it can be confirmed: + +- The version (`4.0.0-beta1`, `3.10.3`, ...) and how you installed it. +- PowerShell edition and version, and the operating system. +- What an attacker can do, not only what looks wrong. +- Steps to reproduce, ideally against a lab tenant. +- Whether it needs an already signed-in session, and what permissions that + session holds. + +**Never include real tenant identifiers, access tokens, client secrets, +certificates or exported policy files from a production tenant.** Redact them, or +reproduce against a lab tenant. + +### What to expect + +This is a single-maintainer project worked on outside business hours. An +acknowledgement usually takes a few days. A fix ships in the next release for +the affected branch, and the release notes credit the reporter unless you ask +otherwise. + +## Scope + +This is an administrative client that runs on your own machine with credentials +you supply. Reports that are in scope include: + +- Credentials, tokens or secrets written somewhere they should not be, or kept + in memory or on disk longer than needed. +- A path where the application sends tenant data anywhere other than the Microsoft + cloud endpoint it is signed in to. +- Code execution from data the application reads: an exported policy file, an + ADMX file, a documentation template or an imported settings file. +- Anything that causes an operation to run against a different tenant than the + one selected. + +The following are **not** vulnerabilities in this project: + +- An account having more permission in Microsoft Intune than you expected. That + is tenant configuration, not this application. +- Anything requiring an attacker who already controls the machine or the signed-in + session. At that point they can use the Microsoft Graph API directly. +- Missing hardening that Microsoft Entra or Intune is responsible for, such as + token lifetime or conditional access. +- Results from a scanner with no demonstrated impact. From fb819949f5e62a35cf374b571eb48e5a22403b09 Mon Sep 17 00:00:00 2001 From: Mikael Karlsson <43226266+Micke-K@users.noreply.github.com> Date: Wed, 23 Sep 2026 19:53:35 +1000 Subject: [PATCH 4/4] Restore LF line endings in Core.psm1 and EndpointManager.psm1 The commit for issue #428 re-saved both files with CRLF, so every line showed as changed against master, which stores them with LF like the rest of the repository. Content is untouched; only the line endings return to LF. --- Core.psm1 | 5924 ++++++++++---------- Extensions/EndpointManager.psm1 | 9178 +++++++++++++++---------------- 2 files changed, 7551 insertions(+), 7551 deletions(-) diff --git a/Core.psm1 b/Core.psm1 index 88ac268..16690f7 100644 --- a/Core.psm1 +++ b/Core.psm1 @@ -1,2963 +1,2963 @@ -<# -.SYNOPSIS -Core UI and Settings fatures for the CloudAPIPowerShellManager solution - -.DESCRIPTION -This module handles the WPF UI - -.NOTES - Author: Mikael Karlsson -#> - -function Get-ModuleVersion -{ - '3.9.6' -} - -function Initialize-Window -{ - param($xamlFile) - - try - { - [xml]$xaml = Get-Content $xamlFile - [xml]$styles = Get-Content ($global:AppRootFolder + "\Themes\Styles.xaml") - - ### Update relative path to full path for ResourceDictionary - [System.Xml.XmlNamespaceManager] $nsm = $xaml.NameTable; - $nsm.AddNamespace("s", 'http://schemas.microsoft.com/winfx/2006/xaml/presentation'); - foreach($rsdNode in ($xaml.SelectNodes("//s:ResourceDictionary[@Source]", $nsm))) - { - $rsdNode.Source = (Join-Path ($global:AppRootFolder) ($rsdNode.Source)).ToString() - } - - # Add Styles - foreach($node in $styles.DocumentElement.ChildNodes) - { - $tmpNode = $xaml.CreateElement("Temp") - $tmpNode.InnerXml = $node.OuterXml - $xaml.Window.'Window.Resources'.ResourceDictionary.AppendChild($tmpNode.Style) | Out-Null - } - return ([Windows.Markup.XamlReader]::Load((New-Object System.Xml.XmlNodeReader $xaml))) - } - catch - { - Write-LogError "Failed to initialize window" $_.Exception - return - } -} - -function Start-CoreApp -{ - param($View) - - if(-not $global:defaultGlobalVariables) - { - $global:defaultGlobalVariables = Get-Variable -Scope Global - } - - $global:useDefaultFolderDialog = $false - $global:WindowsAPICodePackLoaded = $false - $script:proxyURI = $null - - $global:loadedModules = @() - $global:viewObjects = @() - $script:LogItems = [System.Collections.ObjectModel.ObservableCollection[object]]::new() - - $global:AppRootFolder = $PSScriptRoot - - # Load all modules in the Modules folder - $global:modulesPath = [IO.Path]::GetDirectoryName($PSCommandPath) + "\Extensions" - - 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 "#####################################################################################" - - Write-Log "PowerShell version: $($PSVersionTable.PSVersion.ToString())" - if($PSVersionTable.BuildVersion) { - Write-Log "PowerShell build: $($PSVersionTable.BuildVersion.ToString())" - } - if($PSVersionTable.CLRVersion) { - Write-Log "PowerShell CLR: $($PSVersionTable.CLRVersion.ToString())" - } - Write-Log "PowerShell edition: $($PSVersionTable.PSEdition)" - - try - { - $osName = Get-ItemPropertyValue "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion" -Name "ProductName" -ErrorAction Stop - $patchLevel = Get-ItemPropertyValue "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion" -Name "UBR" -ErrorAction Stop - $ver = [Version]::new([Environment]::OSVersion.Version.Major,[Environment]::OSVersion.Version.Minor, [Environment]::OSVersion.Version.Build, $patchLevel) - Write-Log "OS: $osName $ver" - } - catch - { - Write-Log "OS version: $([environment]::OSVersion.VersionString)" - } - - if(Test-Path $global:modulesPath) - { - Import-AllModules - } - else - { - Write-Warning "Extensions folder $($global:modulesPath) not found. Aborting..." 3 - exit 1 - } - - Initialize-Settings - $global:currentViewObject = $null - $global:FirstTimeRunning = ((Get-Setting "" "FirstTimeRunning" "true") -eq "true") - $global:MainAppStarted = $false - - Set-SplashWindowText "Initialize views" - [System.Windows.Forms.Application]::DoEvents() - - Invoke-ModuleFunction "Invoke-InitializeModule" - - if($global:hideUI -ne $true) - { - #Add menu group and items - $script:LogViewObject = (New-Object PSObject -Property @{ - Title = "Log" - Description = "View log items" - ID = "CoreLog" - HideMenu = $true - Activating = { Show-LogView } - Permissions = @() - ViewPanel = $null - }) - - Add-ViewObject $script:LogViewObject - - #This will load the main window - $global:txtSplashText.Text = "Load main window" - [System.Windows.Forms.Application]::DoEvents() - Get-MainWindow - - if($global:window) - { - $global:txtSplashText.Text = "Open default view" - [System.Windows.Forms.Application]::DoEvents() - - Show-View $View - - if((Get-SettingValue "CheckForUpdates") -eq $true) { Get-IsLatestVersion } - - Invoke-ModuleFunction "Invoke-ShowMainWindow" - - $global:txtSplashText.Text = "Open main window" - [System.Windows.Forms.Application]::DoEvents() - $global:window.ShowDialog() | Out-Null - } - } - else - { - if(-not $global:SilentBatchFile) - { - Write-Log "SilentBatchFile must be specified" 3 - return - } - $silentFI = [IO.FileInfo]$global:SilentBatchFile - - if($silentFI.Exists -eq $false) - { - Write-Log "SilentBatchFile $($global:SilentBatchFile) not found" 3 - return - } - Invoke-ModuleFunction "Invoke-ShowMainWindow" - - Invoke-ModuleFunction "Invoke-InitSilentBatchJob" - - Start-RunSilentBatchJob - } -} - -function Start-RunSilentBatchJob -{ - try - { - $settingObj = (ConvertFrom-Json (Get-Content -Path $global:SilentBatchFile -Raw -ErrorAction Stop)) - Invoke-ModuleFunction "Invoke-SilentBatchJob" $settingObj - } - catch - { - Write-LogError "Failed to trigger silent batch job." $_.Exception - } -} - -function Import-AllModules -{ - foreach($file in (Get-Item -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() - - $module = Import-Module $file -PassThru -Force -Global -ErrorAction SilentlyContinue - if($module) - { - $global:loadedModules += $module - Write-Host "Module $($module.Name) loaded successfully" - } - else - { - Write-Warning "Failed to load module $file" - } - } -} - -function Set-SplashWindowText -{ - param($text) - - if($global:hideUI -eq $true) { return } - - $global:txtSplashText.Text = $text -} - -#region Log functions -function Write-Log -{ - param($Text, $type = 1) - - if($script:logFailed -eq $true) { return } - - if(-not $global:logFile) { $global:logFile = Get-SettingValue "LogFile" ([IO.Path]::Combine($global:AppRootFolder,"CloudAPIPowerShellManagement.log")) } - - if(-not $global:logFileMaxSize) { [Int64]$global:logFileMaxSize = Get-SettingValue "LogFileSize" 1024; $global:logFileMaxSize = $global:logFileMaxSize * 1kb } - - if($null -eq $global:logOutputError) { $global:logOutputError = Get-SettingValue "LogOutputError" } - - $fi = [IO.FileInfo]$global:logFile - - if($fi.Length -gt $global:logFileMaxSize) - { - # Larger than max size. Rename current to .bak - # Delete current .bak if it exists - $bakFile = ($fi.DirectoryName + "\" + $fi.BaseName + ".lo_") - if([IO.File]::Exists($bakFile)) - { - try - { - [IO.File]::Delete($bakFile) - } - catch { } - } - try - { - $fi.MoveTo($bakFile) - } - catch { } - } - - try - { - $logPath = [IO.Path]::GetDirectoryName($global:logFile) - if(-not (Test-Path $logPath)) { mkdir -Path $logPath -Force -ErrorAction SilentlyContinue | Out-Null } - } - catch - { - $script:logFailed = $true - return - } - - $date = Get-Date - - if($global:PSCommandPath) - { - $fileObj = [System.IO.FileInfo]$global:PSCommandPath - } - else - { - $fileObj = [System.IO.FileInfo]$PSCommandPath - } - - $timeStr = "$($date.ToString(""HH"")):$($date.ToString(""mm"")):$($date.ToString(""ss"")).000+000" - $dateStr = "$($date.ToString(""MM""))-$($date.ToString(""dd""))-$($date.ToString(""yyyy""))" - $logOut = "" - - if($type -eq 2) - { - Write-Warning $Text - $typeStr = "Warning" - } - elseif($type -eq 3) - { - if($global:logOutputError -ne $false) - { - $host.ui.WriteErrorLine($Text) - } - else - { - Write-Warning $Text - } - $typeStr = "Error" - } - else - { - write-host $Text - $typeStr = "Info" - } - - $script:LogItems.Add([PSCustomObject]@{ - ID = ($script:LogItems.Count + 1) - DateTime = $date - Type = $type - TypeText = $typeStr - Text = $Text - }) - - try - { - out-file -filePath $global:logFile -append -encoding "ASCII" -inputObject $logOut - } - catch { } -} - -function Write-LogDebug -{ - param($Text, $type = 1) - - if($global:Debug) - { - Write-Log ("Debug: " + $text) $type - } -} - -function Write-LogError -{ - param($Text, $Exception) - - if($Text -and $Exception.message) - { - $Text += " Exception: $($Exception.Message)" - } - - Write-Log $Text 3 - - if((Get-SettingValue "ShowStackTrace") -eq $true) - { - Write-Log "Stack trace:`n $($Exception.StackTrace)" - - Write-Log "Script stack trace:`n $($Exception.ScriptStackTrace)" - - } -} - -function Write-Status -{ - param($Text, [switch]$SkipLog, [switch]$Block, [switch]$Force) - - if($global:hideUI -eq $true) - { - if($SkipLog -ne $true) { Write-Log $text } - return - } - - if(-not $text) { $global:BlockStatusUpdates = $false } - elseif($global:BlockStatusUpdates -eq $true -and $Force -ne $true) { return } - elseif($Block -eq $true) { $global:BlockStatusUpdates = $true } - - $global:txtInfo.Content = $Text - if($text) - { - $global:grdStatus.Visibility = "Visible" - if($SkipLog -ne $true) { Write-Log $text } - } - else - { - $global:grdStatus.Visibility = "Collapsed" - } - - [System.Windows.Forms.Application]::DoEvents() -} - -#endregion - -#region Popup -function Show-Popup -{ - param($popup) - - if(-not $global:grdPopup -or -not $global:cvsPopup) { return } - - $global:cvsPopup.AddChild($popup) | Out-Null - $global:grdPopup.Visibility = "Visible" - - [System.Windows.Forms.Application]::DoEvents() -} - -function Hide-Popup -{ - if(-not $global:grdPopup -or -not $global:cvsPopup) { return } - $global:cvsPopup.Children.Clear() - $global:grdPopup.Visibility = "Collapsed" - [System.Windows.Forms.Application]::DoEvents() -} -#endregion - -#region Xaml functions - -function Set-XamlProperty -{ - param($xamlObj, $controlName, $propertyName, $value) - - $obj = $xamlObj.FindName($controlName) - - try - { - if($obj) - { - $obj."$propertyName" = $value - } - else - { - Write-Log "Could not find object with name $controlName" 3 - } - } - catch - { - Write-LogError "Failed to set Xaml property value. Control: $controlName. Property: $propertyName. Error:" $_.Exception - } -} - -function Get-XamlProperty -{ - param($xamlObj, $controlName, $propertyName, $defaultValue = $null) - - $obj = $xamlObj.FindName($controlName) - - try - { - if($obj) - { - return (?? $obj."$propertyName" $defaultValue) - } - else - { - Write-Log "Could not find object with name $controlName" 3 - } - } - catch - { - Write-LogError "Failed to set Xaml property value. Control: $controlName. Property: $propertyName. Error:" $_.Exception - } -} - -function Add-XamlEvent -{ - param($xamlObj, $controlName, $eventName, $scriptBlock) - - try { - $obj = $xamlObj.FindName($controlName) - if($obj) - { - $obj."$eventName"($scriptBlock) - } - else - { - Write-Log "Failed to add Xaml event $eventName to $controlName. Control not found" 3 - } - } - catch - { - Write-LogError "Failed to add Xaml event $eventName to $controlName. Error:" $_.Exception - } -} - -function Add-XamlVariables -{ - param($xaml, $obj) - - # Generate a global variable for each object with Name property set - # Ref: https://learn-powershell.net/2014/08/10/powershell-and-wpf-radio-button/ - $xaml.SelectNodes("//*[@*[contains(translate(name(.),'n','N'),'Name')]]") | ForEach-Object { - Write-LogDebug "Add global variable $($_.Name)" - New-Variable -Name $_.Name -Value $obj.FindName($_.Name) -Force -Scope Global - } -} - -function Get-XamlObject -{ - param($fileName, [switch]$AddVariables) - - if(([IO.File]::Exists($fileName))) - { - try - { - [xml]$xaml = Get-Content $fileName - - $xamlObj = ([Windows.Markup.XamlReader]::Load((New-Object System.Xml.XmlNodeReader $xaml))) - - if($xamlObj -and $AddVariables -eq $true) - { - Add-XamlVariables $xaml $xamlObj - } - return $xamlObj - } - catch - { - Write-LogError "Failed to load Xaml file $fileName. Error:" $_.Exception - } - } - else - { - Write-Log "Failed to open Xaml file. File not found: $fileName" - } -} - -function Invoke-RegisterName -{ - param($parent, $name, $registerTo) - - try - { - $control = $parent.FindName($name) - if($control) - { - $registerTo.RegisterName($name, $control) - } - } - catch - { - Write-LogError "Failed to register $name" $_.Exception - } -} - -#endregion - -#region Silent Functions -function Set-BatchProperties -{ - param($settingsObj, $form, [switch]$SkipMissingControlWarning) - - if(-not $settingsObj -or -not $form) - { - return - } - - foreach($prop in $settingsObj) #($settingsObj | GM | Where MemberType -eq NoteProperty)) - { - if($prop.Type -eq "Custom") { continue } - - $obj = $form.FindName($prop.Name) - if(-not $obj) - { - if($SkipMissingControlWarning -ne $true) - { - Write-Log "No setting for $($prop.Name) found" 2 - } - continue - } - - if($prop.Value -is [String] -and [string]::IsNullOrEmpty($prop.Value)) - { - continue - } - - try - { - if($obj -is [System.Windows.Controls.CheckBox]) - { - $obj.IsChecked = $prop.Value -eq $true - } - elseif($obj -is [System.Windows.Controls.TextBox]) - { - $obj.Text = $prop.Value - } - elseif($obj -is [System.Windows.Controls.ComboBox]) - { - $obj.SelectedValue = $prop.Value - } - else - { - try - { - Write-Log "Unsupported object type for silent batch job: $($obj.GetType().FullName)" 3 - } - catch - {} - } - } - catch - { - Write-LogError "Failed to set batch job property for $($prop.Name)" $_.Exception - } - } -} -#endregion - -#region Dialogs - -function Show-AboutDialog -{ - $script:dlgAbout = Get-XamlObject ($global:AppRootFolder + "\Xaml\AboutDialog.xaml") - if(-not $script:dlgAbout) { return } - - $loadedItems = @() - $externalModules = @("MSAL.PS","Az.Account") - $externalAssemblies = @("Microsoft.Identity.Client.dll") - - foreach($module in (((Get-Module | Where-Object { $_.ModuleBase -like "$($global:AppRootFolder)*" -or $_.Name -in $externalModules })))) - { - $ver = $module.Version - if($module.Version.Major -eq 0 -and $module.Version.Minor -eq 0) - { - $cmd = $module.ExportedFunctions["Get-ModuleVersion"] - if($cmd) - { - $tmpVer = Invoke-Command -ScriptBlock $cmd.ScriptBlock - $ver = ?? $tmpVer $ver - } - } - - $loadedItems += (New-Object PSObject -Property @{ - Name = $module.Name - Version = $ver - Type = "PSModule" - }) - } - - $assms = [System.AppDomain]::CurrentDomain.GetAssemblies() | Where { $_.GlobalAssemblyCache -eq $false -and [String]::IsNullOrEmpty($_.Location) -eq $false } - foreach($assmName in $externalAssemblies) - { - $assmObjs = $assms | Where { $_.Location -like "*\$($assmName)" } - foreach($assmObj in $assmObjs) - { - try - { - $fi = [IO.FileInfo]"$($assmObj.Location)" - $loadedItems += (New-Object PSObject -Property @{ - Name = $fi.Name - Version = $fi.VersionInfo.FileVersion - Type = "Assembly" - }) - } - catch {} - } - } - - Set-XamlProperty $script:dlgAbout "txtTitle" "Text" "CloudAPIPowerShellManagement" - Set-XamlProperty $script:dlgAbout "txtViewTitle" "Text" ("Current view: " + $global:currentViewObject.ViewInfo.Title) - if($global:currentViewObject.ViewInfo.Description) - { - Set-XamlProperty $script:dlgAbout "txtViewDescription" "Text" $global:currentViewObject.ViewInfo.Description - } - - Set-XamlProperty $script:dlgAbout "lstModules" "ItemsSource" $loadedItems - - Add-XamlEvent $script:dlgAbout "linkSource" "Add_RequestNavigate" ({ Start-Process $_.Uri.AbsoluteUri; $_.Handled = $true }) - - Show-ModalForm "About" $script:dlgAbout -} - -function Show-UpdatesDialog -{ - $script:dlgUpdates = Get-XamlObject ($global:AppRootFolder + "\Xaml\UpdatesDialog.xaml") - if(-not $script:dlgUpdates) { return } - - Write-Status "Getting Release Notes Information" - - Add-XamlEvent $script:dlgUpdates "btnClose" "add_click" { - $script:dlgUpdates = $null - Show-ModalObject - } - - Add-XamlEvent $script:dlgUpdates "linkSource" "Add_RequestNavigate" ({ Start-Process $_.Uri.AbsoluteUri; $_.Handled = $true }) - - $fileContent = Get-Content -Raw -Path ($global:AppRootFolder + "\ReleaseNotes.md") - try - { - $tmp = $fileContent.Replace("`r`n","`n") - $mystring = ("blob $($tmp.Length)`0" + $tmp) - $mystream = [IO.MemoryStream]::new([byte[]][char[]]$mystring) - $curHash = Get-FileHash -InputStream $mystream -Algorithm SHA1 - } - finally - { - if($mystream) { $mystream.Dispose() } - } - $params = @{} - $proxyURI = Get-ProxyURI - if($proxyURI) - { - $params.Add("proxy", $proxyURI) - $params.Add("UseBasicParsing", $true) - } - - # The notes at the newest 3.x release tag, never at the default branch: that - # branch will carry version 4 once the branches are renamed. - $latestVer = Get-LatestGitHubVersion $params - $notesRef = if($latestVer) { "?ref=$($latestVer.ToString())" } else { "" } - $content = Invoke-RestMethod "https://api.github.com/repos/Micke-K/IntuneManagement/contents/ReleaseNotes.md$notesRef" @params - if($content) - { - $txt = [System.Text.Encoding]::UTF8.GetString(([System.Convert]::FromBase64String($content.content))) - Set-XamlProperty $script:dlgUpdates "txtReleaseNotes" "Text" $txt - - if($content.sha -ne $curHash.Hash) - { - # ReleaseNotes.md not matching - Set-XamlProperty $script:dlgUpdates "tabLocalReleaseNotes" "Visibility" "Visible" - Set-XamlProperty $script:dlgUpdates "txtReleaseNotes" "Text" $fileContent - Set-XamlProperty $script:dlgUpdates "txtReleaseNotesMatch" "Visibility" "Collapsed" - } - else - { - Set-XamlProperty $script:dlgUpdates "txtReleaseNotesNoMatch" "Visibility" "Collapsed" - Set-XamlProperty $script:dlgUpdates "tabLocalReleaseNotes" "Visibility" "Collapsed" - } - } - - Write-Status "" - - Show-ModalForm "Release Notes" $script:dlgUpdates -HideButtons -} - -# Newest published 3.x release on GitHub, or $null. -# -# releases/latest answers the newest release of ANY version. The day 4.0.0 is -# published it would tell every 3.x installation to upgrade to a breaking -# change, and reading the manifest on the default branch stops working once the -# branches are renamed for version 4. The releases list filtered to this major -# is the one source that survives both. Pre-releases and drafts are skipped. -function Get-LatestGitHubVersion -{ - param($Params = @{}) - - $latest = $null - try - { - $releases = Invoke-RestMethod "https://api.github.com/repos/Micke-K/IntuneManagement/releases?per_page=100" @Params - foreach($release in @($releases)) - { - if($release.draft -or $release.prerelease) { continue } - $ver = $null - try { $ver = [version](([string]$release.tag_name) -replace '^v','') } catch { continue } - if($ver.Major -ne 3) { continue } - if($null -eq $latest -or $ver -gt $latest) { $latest = $ver } - } - } - catch - { - Write-Log "Failed to list GitHub releases: $($_.Exception.Message)" 2 - } - return $latest -} - -function Get-IsLatestVersion -{ - if($global:MainAppStarted -ne $true) - { - $global:txtSplashText.Text = "Check for updates" - [System.Windows.Forms.Application]::DoEvents() - } - - $gitHubVer = $null - - $params = @{} - $proxyURI = Get-ProxyURI - if($proxyURI) - { - $params.Add("proxy", $proxyURI) - $params.Add("UseBasicParsing", $true) - } - - $gitHubVer = Get-LatestGitHubVersion $params - - if(-not $gitHubVer) - { - Write-log "Failed to get version info in GitHub" 2 - return - } - - $LocalInfo = $null - $localVer = $null - try - { - Import-LocalizedData -BindingVariable LocalInfo -BaseDirectory $global:AppRootFolder -FileName "CloudAPIPowerShellManagement.psd1" -ErrorAction Stop - $localVer = [version]$LocalInfo.ModuleVersion - } - catch { } - - if(-not $localVer) - { - Write-log "Failed to get version info from local file" 2 - return - } - - if($localVer -lt $gitHubVer) - { - Write-Log "Local version and GitHub version does not match" 2 - Write-Log "Local version: $($localVer.ToString())" - Write-Log "GitHub version: $($gitHubVer.ToString())" - [System.Windows.MessageBox]::Show("There is a new version available on GitHub $($gitHubVer.ToString())`n`nCurrent version is $($localVer.ToString())", "Old version!", "OK", "Warning") - } - else - { - Write-Log "Running latest version: $($localVer.ToString())" - } -} - -function Get-ModuleDataTable -{ - param($moduleText) - - $result = $null - - if(-not $moduleText) { return } - - try - { - $Path = [IO.path]::ChangeExtension([IO.Path]::GetTempFileName(), "psd1") - $FI = [io.FileInfo]$path - $Utf8NoBomEncoding = New-Object System.Text.UTF8Encoding $False - [System.IO.File]::WriteAllLines($FI.FullName, $moduleText, $Utf8NoBomEncoding) - $Result = $null - Import-LocalizedData -BindingVariable Result -BaseDirectory $FI.DirectoryName -FileName $fi.Name - } - catch - { - - } - finally - { - try { [IO.File]::Delete(([IO.path]::ChangeExtension($FI.FullName, "tmp"))) } catch {} - try { $FI.Delete() } catch{} - } - - $Result -} - -function Show-InputDialog -{ - param( - $FormTitle = "Input", - $FormText, - $DefaultValue) - - $script:inputBox = Initialize-Window ($global:AppRootFolder + "\Xaml\InputDialog.xaml") - if(-not $script:inputBox) { return } - - $script:inputBox.Title = $FormTitle - - Set-XamlProperty $script:inputBox "txtLabel" "Content" $FormText - Set-XamlProperty $script:inputBox "txtValue" "Text" $DefaultValue - - $script:txtValue = $script:inputBox.FindName("txtValue") - - Add-XamlEvent $script:inputBox "btnOk" "Add_Click" ({ $script:inputBox.Close() }) - Add-XamlEvent $script:inputBox "btnCancel" "Add_Click" ({ $script:txtValue.Text ="";$script:inputBox.Close() }) - - $inputBox.Add_ContentRendered({ - $script:txtValue.SelectAll(); - $script:txtValue.Focus(); - }) - - $inputBox.Owner = $global:window - $inputBox.Icon = $global:Window.Icon - - $inputBox.ShowDialog() | Out-null - - return $script:txtValue.Text -} - -function Show-ModalForm -{ - param( - $FormTitle = "", - $formObject, - [switch]$HideButtons) - - $xamlStr = Get-Content ($global:AppRootFolder + "\Xaml\ModalForm.xaml") - - $modalForm = [Windows.Markup.XamlReader]::Parse($xamlStr) - - if($HideButtons -eq $true) - { - Set-XamlProperty $modalForm "spButtons" "Visibility" "Collapsed" - } - else - { - Add-XamlEvent $modalForm "btnClose" "Add_Click" ({ - Show-ModalObject - }) - } - - Set-XamlProperty $modalForm "txtTitle" "Text" $FormTitle - - $grdModalContainer = $modalForm.FindName("grdModalContainer") - if($grdModalContainer -and $formObject) - { - $formObject.SetValue([System.Windows.Controls.Grid]::RowProperty,1) - $grdModalContainer.Children.Add($formObject) | Out-Null - } - Show-ModalObject $modalForm -} -function Show-ModalObject -{ - param( $obj ) - - if($obj) - { - $obj.SetValue([System.Windows.Controls.Grid]::RowProperty,1) - $obj.SetValue([System.Windows.Controls.Grid]::ColumnProperty,1) - $global:grdModal.Children.Add($obj) | Out-Null - $global:grdModal.Visibility = "Visible" - } - else - { - $global:grdModal.Children.Clear() - $global:grdModal.Visibility = "Collapsed" - } - - [System.Windows.Forms.Application]::DoEvents() -} -#endregion - -#region Controls -function Show-AuthenticationInfo -{ - if($global:grdMenu) - { - $global:txtSplashText.Text = "Get profile picture" - [System.Windows.Forms.Application]::DoEvents() - - $authenticationProvider = $global:currentViewObject.ViewInfo.Authentication - if($global:grdMenu.Children[-1].Tag -eq "ProfilePicture") - { - $global:grdMenu.Children.Remove($global:grdMenu.Children[-1]) - } - - if($authenticationProvider.ProfilePicture) - { - $profileObj = & $authenticationProvider.ProfilePicture -Size 24 -Fontsize 12 -Popup -AuthenticationProvider $authenticationProvider - if($profileObj) - { - $profileObj.Tag = "ProfilePicture" - $profileObj.SetValue([System.Windows.Controls.Grid]::ColumnProperty,2) | Out-Null - $global:grdMenu.Children.Add($profileObj) | Out-Null - } - } - [System.Windows.Forms.Application]::DoEvents() - } -} - -function Set-EnvironmentInfo -{ - param($environmentName) - - if(-not $global:grdEnvironment) - { - return - } - - if(-not $script:mnuDefaultBGColor) - { - $script:mnuDefaultBGColor = $global:mnuMain.Background - } - if(-not $script:mnuDefaultFGColor) - { - $script:mnuDefaultFGColor = $global:mnuMain.Foreground - } - - if($global:grdEnvironment -and $environmentName) - { - $global:grdEnvironment.Visibility = "Visible" - if((Get-SettingValue "MenuShowOrganizationName") -eq $true) - { - $global:lblEnvironment.Content = $environmentName - } - else - { - $global:lblEnvironment.Content = "" - } - $bgColor = (Get-SettingValue "MenuBGColor") - $fgColor = (Get-SettingValue "MenuFGColor") - - if(-not $bgColor) - { - $bgColor = $script:mnuDefaultBGColor - } - - if($bgColor) - { - $global:grdMenu.Background = $bgColor - $global:mnuMain.Background = $bgColor - } - - if(-not $fgColor) - { - $fgColor = $script:mnuDefaultFGColor - } - - if($fgColor) - { - $global:lblEnvironment.Foreground = $fgColor - $global:mnuMain.Foreground = $fgColor - } - } - else - { - $global:grdEnvironment.Visibility = "Collapsed" - $global:lblEnvironment.Content = "" - $global:mnuMain.Background = $script:mnuDefaultBGColor - $global:mnuMain.Foreground = $script:mnuDefaultFGColor - $global:lblEnvironment.Foreground = $script:mnuDefaultFGColor - } -} - -#endregion - -#region Generic functions -function Invoke-Coalesce ($value, $default) -{ - # Use IsNullOrEmpty instead of -not - if ([String]::IsNullOrEmpty($value)) { $value = $default } - - return $value -} - -function Invoke-IfTrue ($expression, $valueIfTrue, $valueIfFalse) -{ - if ($expression) { return $valueIfTrue } - else { return $valueIfFalse } -} - -function Set-ObjectGrid -{ - param( $obj ) - - if($obj) - { - $global:grdObject.Children.Add($obj) | Out-Null - $global:grdObject.Visibility = "Visible" - } - else - { - $global:grdObject.Children.Clear() - $global:grdObject.Visibility = "Collapsed" - } - - [System.Windows.Forms.Application]::DoEvents() -} - -function Remove-InvalidFileNameChars -{ - param($Name) - - $re = "[{0}]" -f [RegEx]::Escape(([IO.Path]::GetInvalidFileNameChars() -join '')) - - $Name = $Name -replace $re - - - return $Name -} - -function Remove-ObjectProperty -{ - param($obj, $property) - - if(-not $obj -or -not $property) { return } - - if(($obj | Get-Member -MemberType NoteProperty -Name $property)) - { - $obj.PSObject.Properties.Remove($property) - } -} - -function Get-Folder -{ - param($path = $env:temp, $title = "Select a directory") - - if($global:useDefaultFolderDialog -ne $true) - { - try - { - if($global:WindowsAPICodePackLoaded -eq $false) - { - $apiCodec = Join-Path $global:AppRootFolder "Bin\Microsoft.WindowsAPICodePack.Shell.dll" - if([IO.File]::Exists($apiCodec)) - { - Add-Type -Path $apiCodec | Out-Null - $global:WindowsAPICodePackLoaded = $true - } - else - { - Write-Log "Could not find Microsoft.WindowsAPICodePack.Shell.dll" 2 - } - } - $dlgCOFD = New-Object Microsoft.WindowsAPICodePack.Dialogs.CommonOpenFileDialog - } - catch - { - Write-LogError "Failed to load Microsoft.WindowsAPICodePack.Shell.dll. Verify that the .Net 3.5 feature is enabled" $_.Exception - } - } - - if($dlgCOFD -and $global:useDefaultFolderDialog -ne $true) - { - $dlgCOFD.EnsureReadOnly = $true - $dlgCOFD.IsFolderPicker = $true - $dlgCOFD.AllowNonFileSystemItems = $false - $dlgCOFD.Multiselect = $false - $dlgCOFD.Title = $title - - if($path -and (Test-Path $path)) - { - $dlgCOFD.InitialDirectory = $path - } - if($dlgCOFD.ShowDialog($window) -eq [Microsoft.WindowsAPICodePack.Dialogs.CommonFileDialogResult]::Ok) - { - $dlgCofd.FileName - } - } - else - { - $global:useDefaultFolderDialog = $true - [Reflection.Assembly]::LoadWithPartialName("System.Windows.Forms") | Out-Null - [System.Windows.Forms.Application]::EnableVisualStyles() - $dlgFBD = New-Object System.Windows.Forms.FolderBrowserDialog - $dlgFBD.SelectedPath = "C:\" - $dlgFBD.ShowNewFolderButton = $false - $dlgFBD.Description = $title - if($dlgFBD.ShowDialog() -eq "OK") - { - $dlgFBD.SelectedPath - } - $dlgFBD.Dispose() - } -} -function Remove-Property -{ - param($obj, $prop) - - if(-not $prop) { return } - - if(($obj | GM -MemberType NoteProperty -Name $prop)) - { - Write-LogDebug "Remove property $prop" - $obj.PSObject.Properties.Remove($prop) | Out-Null - } -} - -function Get-GridCheckboxColumn -{ - param($bindingProperty = "IsSelected", [scriptblock]$scriptBlock) - - $binding = [System.Windows.Data.Binding]::new($bindingProperty) - $binding.UpdateSourceTrigger = [System.Windows.Data.UpdateSourceTrigger]::PropertyChanged - $column = [System.Windows.Controls.DataGridTemplateColumn]::new() - $fef = [System.Windows.FrameworkElementFactory]::new([System.Windows.Controls.CheckBox]) - $binding.Mode = [System.Windows.Data.BindingMode]::TwoWay - $fef.SetValue([System.Windows.Controls.CheckBox]::IsCheckedProperty,$binding) - if($null -ne $scriptBlock) - { - [System.Windows.RoutedEventHandler]$checkedEventHandler = $scriptBlock - $fef.AddHandler([System.Windows.Controls.CheckBox]::CheckedEvent, $checkedEventHandler) - } - $dt = [System.Windows.DataTemplate]::new() - $dt.VisualTree = $fef - $column.CellTemplate = $dt - $header = [System.Windows.Controls.CheckBox]::new() - $header.Margin = [System.Windows.Thickness]::new(-4,0,0,0) # Align header checkbox with the row checkboxes - $header.ToolTip = "Select/deselect all items" - $column.Header = $header - if($null -ne $scriptBlock) - { - #$header.add_click($scriptBlock) - } - - $column -} - -function Expand-FileName -{ - param($fileName) - - [Environment]::SetEnvironmentVariable("Date",(Get-Date).ToString("yyyy-MM-dd"),[System.EnvironmentVariableTarget]::Process) - [Environment]::SetEnvironmentVariable("DateTime",(Get-Date).ToString("yyyyMMdd-HHmm"),[System.EnvironmentVariableTarget]::Process) - [Environment]::SetEnvironmentVariable("Organization",$global:Organization.displayName,[System.EnvironmentVariableTarget]::Process) - - $fileName = [Environment]::ExpandEnvironmentVariables($fileName) - - foreach($tmpFolder in ([System.Enum]::GetNames([System.Environment+SpecialFolder]))) - { - $fileName = $fileName -replace "%$($tmpFolder)%",([Environment]::GetFolderPath($tmpFolder)) - } - - [Environment]::SetEnvironmentVariable("Date",$null,[System.EnvironmentVariableTarget]::Process) - [Environment]::SetEnvironmentVariable("DateTime",$null,[System.EnvironmentVariableTarget]::Process) - [Environment]::SetEnvironmentVariable("Organization",$null,[System.EnvironmentVariableTarget]::Process) - - # Remove invalid path characters - $re = "[{0}]" -f [RegEx]::Escape(([IO.Path]::GetInvalidPathChars() -join '')) - $fileName = $fileName -replace $re - - $fileName -} - -#endregion - -#region Save/Read Settings functions -######################################################################## -# -# Save/Read Settings -# -######################################################################## -function Initialize-Settings -{ - param([switch]$Updated) - - $global:Debug = Get-SettingValue "Debug" - $global:logFile = $null - $global:logFileMaxSize = $null - $global:logOutputError = $null - $script:proxyURI = $null - - if($Updated -eq $true) - { - Set-EnvironmentInfo $global:Organization.displayName - 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") - - if($global:hideUI -eq $true) { return } - - if($global:JsonSettingsObj -and $global:JSonSettingFile) - { - if($SubPath) - { - $arrParts = $SubPath.TrimEnd(@('/','\')).Split(@('/','\')) - } - else - { - $arrParts = @() - } - - $parentSetting = $global:JsonSettingsObj - - foreach($part in $arrParts) - { - if(-not $part.Trim()) { continue } - - if(($parentSetting.PSObject.Properties | Where Name -eq $part)) - { - $parentSetting = $parentSetting.$part - } - else - { - $parentSetting | Add-Member -MemberType NoteProperty -Name $part -Value ([PSCustomObject]@{}) - $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 - } -} - -function Remove-Setting -{ - param($SubPath = "", $Key = "") - - if($global:JsonSettingsObj) - { - if($SubPath) - { - $arrParts = $SubPath.TrimEnd(@('/','\')).Split(@('/','\')) - } - else - { - $arrParts = @() - } - - $parentSetting = $global:JsonSettingsObj - - foreach($part in $arrParts) - { - if(($parentSetting.PSObject.Properties | Where Name -eq $part)) - { - $parentSetting = $parentSetting.$part - } - else - { - return - } - } - - if(($parentSetting.PSObject.Properties | Where Name -eq $Key)) - { - $parentSetting.PSObject.Properties.Remove($Key) - } - } - else - { - $regPath = Get-RegPath $subPath - try - { - $temp = Get-Item -LiteralPath $regPath -ErrorAction SilentlyContinue - if(($temp -and $temp.Property -contains $Key)) - { - Remove-ItemProperty -Path $regPath -Name $Key -Force -ErrorAction Stop - } - } - catch - { - Write-LogError "Failed to remove reg value: $($Key) in key $($regPath)" $_.Exception - } - } -} - -function Get-Setting -{ - param($SubPath = "", $Key = "", $defaultValue) - - if(-not $key) - { - return - } - - $val = $null - - if($global:JsonSettingsObj) - { - try - { - if($SubPath) - { - $arrParts = $SubPath.TrimEnd(@('/','\')).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) - { - $defaultValue - } - else - { - $val - } -} - -function Get-RegPath -{ - param($SubPath) - - $path = "HKCU:\Software\CloudAPIPowerShellManagement" - if($SubPath) - { - $path = $path + "\" + $SubPath - } - - $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 -ErrorAction SilentlyContinue - 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 - } -} - -function Remove-TenantSetting -{ - param($settingValue) - - $subPath = ($global:Organization.Id + "\" + $settingValue.SubPath) - - if($global:JsonSettingsObj) - { - if($SubPath) - { - $arrParts = $SubPath.TrimEnd(@('/','\')).Split(@('/','\')) - } - else - { - $arrParts = @() - } - - $parentSetting = $global:JsonSettingsObj - - foreach($part in $arrParts) - { - if(($parentSetting.PSObject.Properties | Where Name -eq $part)) - { - $parentSetting = $parentSetting.$part - } - else - { - return - } - } - - if(($parentSetting.PSObject.Properties | Where Name -eq $settingValue.Key)) - { - $parentSetting.PSObject.Properties.Remove($settingValue.Key) - } - - } - else - { - $regPath = Get-RegPath $subPath - try - { - $temp = Get-Item -LiteralPath $regPath -ErrorAction SilentlyContinue - if(($temp.Property -contains $settingValue.Key)) - { - Remove-ItemProperty -Path $regPath -Name $settingValue.Key -Force -ErrorAction Stop - } - } - catch - { - Write-LogError "Failed to remove reg value: $($settingValue.Key) in key $($regPath)" $_.Exception - } - } -} - -function Get-IsTenantSettingConfigured -{ - param($settingValue) - - $subPath = ($global:Organization.Id + "\" + $settingValue.SubPath) - - if($global:JsonSettingsObj) - { - if($SubPath) - { - $arrParts = $SubPath.TrimEnd(@('/','\')).Split(@('/','\')) - } - else - { - $arrParts = @() - } - - $parentSetting = $global:JsonSettingsObj - - foreach($part in $arrParts) - { - if(($parentSetting.PSObject.Properties | Where Name -eq $part)) - { - $parentSetting = $parentSetting.$part - } - else - { - return $false - } - } - - return ($null -ne ($parentSetting.PSObject.Properties | Where Name -eq $settingValue.Key)) - - } - else - { - $regPath = Get-RegPath $subPath - try - { - $temp = Get-Item -LiteralPath $regPath -ErrorAction Stop - return ($temp.GetValueNames() -contains $settingValue.Key) - } - catch - { - - } - } - return $false -} -#endregion - -#region Setting functions - -######################################################################## -# -# Settings functions -# -######################################################################## - -function Add-SettingsItem -{ - param($settingItem, $settingValue) - - $rd = [System.Windows.Controls.RowDefinition]::new() - $rd.Height = [double]::NaN - $spSettings.RowDefinitions.Add($rd) - $settingItem.SetValue([System.Windows.Controls.Grid]::RowProperty,$spSettings.RowDefinitions.Count-1) - - if(-not $settingValue) - { - $settingItem.SetValue([System.Windows.Controls.Grid]::ColumnSpanProperty, 99) - } - else - { - if($settingValue.Description) - { - $descriptionInfo = "" + - "" + - $settingValue.Description + - "" + - "" - } - - $xaml = @" - - - $descriptionInfo - -"@ - - if($script:tenantSettings -and $settingValue) - { - #_IsChecked - $tenantConfig = [System.Windows.Controls.CheckBox]::new() - $tenantConfig.ToolTip = "Enable tenant specific setting" - $tenantConfig.SetValue([System.Windows.Controls.Grid]::RowProperty,$spSettings.RowDefinitions.Count-1) - $tenantConfig.SetValue([System.Windows.Controls.Grid]::ColumnProperty, 0) - $tenantConfig.Margin = "0,5,0,0" - $tenantConfig.Tag = $settingValue - $tenantConfig.IsChecked = (Get-IsTenantSettingConfigured $settingValue) - $settingItem.IsEnabled = $tenantConfig.IsChecked - $tenantConfig.add_Click({ - if($this.Tag.Control) { $this.Tag.Control.IsEnabled = $this.IsChecked } - } - ) - $spSettings.AddChild($tenantConfig) - } - - $settingsTitle = [Windows.Markup.XamlReader]::Parse($xaml) - $settingsTitle.SetValue([System.Windows.Controls.Grid]::RowProperty,$spSettings.RowDefinitions.Count-1) - $settingsTitle.SetValue([System.Windows.Controls.Grid]::ColumnProperty, 1) - - $settingItem.SetValue([System.Windows.Controls.Grid]::ColumnProperty, 2) - $spSettings.AddChild($settingsTitle) - $settingItem.Margin = "0,5,0,0" - } - $spSettings.AddChild($settingItem) -} - -function Add-SettingTextBox -{ - param($id, $value) - - $xaml = @" -$value -"@ - return [Windows.Markup.XamlReader]::Parse($xaml) -} - -function Add-SettingCheckBox -{ - param($id, $value) - - $tmpValue = ($value -eq $true -or $value -eq "true").ToString().ToLower() - - $xaml = @" - -"@ - return [Windows.Markup.XamlReader]::Parse($xaml) -} - -function Add-SettingComboBox -{ - param($id, $value, $settingObj) - - $nameProp = ?? $settingObj.DisplayMemberPath "Name" - $valueProp = ?? $settingObj.SelectedValuePath "Value" - - $xaml = @" - -"@ - $xamlObj = [Windows.Markup.XamlReader]::Parse($xaml) - - $xamlObj.ItemsSource = $settingObj.ItemsSource - if($value) - { - $xamlObj.SelectedValue = $value - } - - $xamlObj -} - -function Add-SettingFolder -{ - param($id, $value) - $xaml = @" - - - - - - - $value - - - -"@ - - $obj = [Windows.Markup.XamlReader]::Parse($xaml) - - $btnBrowse = $obj.FindName("browse_$($id)") - $txtObj = $obj.FindName($id) - if($btnBrowse) - { - $btnBrowse.Tag = $txtObj - $btnBrowse.Add_Click({ - $folder = Get-Folder $this.Tag.Text - if($folder) { $this.Tag.Text = $folder } - }) - } - return $obj -} - -function Add-SettingValue -{ - param($settingValue) - - $id = "id_" + [Guid]::NewGuid().ToString('n') - - if($settingValue.TenantSettings -eq $false -and $script:tenantSettings) - { - return # Value nut supported in Tenant Settings - } - elseif($settingValue.GlobalSettings -eq $false -and $script:tenantSettings -ne $true) - { - return # Value nut supported in Global Settings - } - - $value = Get-SettingValue $settingValue.Key -GlobalOnly:($script:tenantSettings -ne $true) - - if($settingValue.Type -eq "folder") - { - $settingObj = Add-SettingFolder $id $value - } - elseif($settingValue.Type -eq "Boolean") - { - $settingObj = Add-SettingCheckBox $id $value - } - elseif($settingValue.Type -eq "List") - { - $settingObj = Add-SettingComboBox $id $value $settingValue - } - else - { - $settingObj = Add-SettingTextBox $id $value - } - - if($settingObj) - { - Add-SettingsItem $settingObj $settingValue - # Find the control in the setting object that contains the actual value - # $settingObj might be a grid that contains the TextBox with the settings value - $ctrl = $settingObj.FindName($id) - if(($settingValue | Get-Member -MemberType NoteProperty -Name "Control")) - { - $settingValue.Control = $ctrl - } - else - { - $settingValue | Add-Member -MemberType NoteProperty -Name "Control" -Value $ctrl - } - } -} - -function Add-SettingTitle -{ - param($title, $marginTop = "0") - - $xaml = @" - -"@ - - #$global:spSettings.Children.Add([Windows.Markup.XamlReader]::Parse($xaml)) - Add-SettingsItem ([Windows.Markup.XamlReader]::Parse($xaml)) | Out-Null -} - -function Show-SettingsForm -{ - param([switch]$Tenant) - - $settingsStr = Get-Content ($global:AppRootFolder+ "\Xaml\SettingsForm.xaml") - - $settingsForm = [Windows.Markup.XamlReader]::Parse($settingsStr) - $global:settingControls = @() - $global:spSettings = $settingsForm.FindName("spSettings") - - $script:tenantSettings = ($Tenant -eq $true) - Add-XamlEvent $settingsForm "btnSave" "Add_Click" ({ - Save-AllSettings - }) - - Add-XamlEvent $settingsForm "btnClose" "Add_Click" ({ - $script:tenantSettings = $null - Show-ModalObject - }) - - if($JsonSettingsObj -or $script:tenantSettings -eq $true) - { - 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) - { - Add-SettingTitle $tmp.Title - foreach($settingObj in $tmp.Values) - { - Add-SettingValue $settingObj - } - } - - foreach($settingObj in $global:appSettingSections) - { - if(-not ($settingObj | Get-Member -MemberType NoteProperty -Name "Priority")) - { - $settingObj | Add-Member -MemberType NoteProperty -Name "Priority" -Value 100 - } - if($settingObj.Priority -lt 1) { $settingObj.Priority = 1} - } - - foreach($section in ($global:appSettingSections | Where-Object Id -ne "General" | Sort-Object -Property Priority,Title)) - { - if($section.Values.Count -eq 0) { continue } - Add-SettingTitle $section.Title 5 - foreach($settingObj in $section.Values) - { - Add-SettingValue $settingObj - } - } - Show-ModalObject $settingsForm -} - -function Add-DefaultSettings -{ - $global:appSettingSections = @() - - $script:lstColors = @() - $script:lstColors += [PSCustomObject]@{ - Name = "" - Value = "" - } - - foreach($color in ([System.Drawing.Color].GetProperties() | Where { $_.PropertyType -eq [System.Drawing.Color] } | Sort -Property Name | Select Name).Name) - { - $script:lstColors += [PSCustomObject]@{ - Name = $color - Value = $color - } - } - - $global:appSettingSections += (New-Object PSObject -Property @{ - Title = "General" - Id = "General" - Values = @() - }) - - Add-SettingsObject (New-Object PSObject -Property @{ - Title = "Log file" - Key = "LogFile" - Type = "File" - }) "General" - - Add-SettingsObject (New-Object PSObject -Property @{ - Title = "Max log file size" - Key = "LogFileSize" - Type = "Int" - DefaultValue = 1024 - }) "General" - - Add-SettingsObject (New-Object PSObject -Property @{ - Title = "Add errors to PowerShell output" - Key = "LogOutputError" - Type = "Boolean" - Description = "Write errors to the Error Output of the PS Host. If disabled, errors will be written as a Warning. Eg. disable this if automation should skip logging PowerShell errors." - DefaultValue = $true - }) "General" - - Add-SettingsObject (New-Object PSObject -Property @{ - Title = "Show stack error" - Key = "ShowStackTrace" - Type = "Boolean" - Description = "Write exception stack trace info to the log." - DefaultValue = $false - }) "General" - - Add-SettingsObject (New-Object PSObject -Property @{ - Title = "Debug" - Key = "Debug" - Type = "Boolean" - DefaultValue = $false - }) "General" - - Add-SettingsObject (New-Object PSObject -Property @{ - Title = "Hide No-access items" - Key = "HideNoAccess" - Type = "Boolean" - Description="Remove items from the menu if object permissions is missing. Default is to mark them with red" - DefaultValue = $false - }) "General" - - Add-SettingsObject (New-Object PSObject -Property @{ - Title = "Preview" - Key = "PreviewFeatures" - Type = "Boolean" - DefaultValue = $false - Description = "Enable features that are marked as Preview. This might require a restart and prompt for consent" - }) "General" - - Add-SettingsObject (New-Object PSObject -Property @{ - Title = "Check for updates" - Key = "CheckForUpdates" - Type = "Boolean" - DefaultValue = $true - Description = "Check GitHub if there is a later version available" - }) "General" - - Add-SettingsObject (New-Object PSObject -Property @{ - Title = "Menu Background color" - Key = "MenuBGColor" - Type = "List" - ItemsSource = $script:lstColors - DefaultValue = "" - }) "General" - - Add-SettingsObject (New-Object PSObject -Property @{ - Title = "Menu Foreground color" - Key = "MenuFGColor" - Type = "List" - ItemsSource = $script:lstColors - DefaultValue = "" - }) "General" - - Add-SettingsObject (New-Object PSObject -Property @{ - Title = "Show tenant name" - Key = "MenuShowOrganizationName" - Type = "Boolean" - DefaultValue = $true - Description = "Adds the organization name next to the login info on the menu bar" - }) "General" - - Add-SettingsObject (New-Object PSObject -Property @{ - Title = "Proxy URI" - Key = "ProxyURI" - Description = "Specify the URI for the proxy eg http://<server>:<port>" - }) "General" - -} - -function Add-SettingsObject -{ - param($obj, $section) - - $section = $global:appSettingSections | Where-Object Id -eq $section - if(-not $section) - { - Write-Log "Could not find section $section" 3 - return - } - - try - { - $section.Values += $obj - } - catch { } -} - -function Save-AllSettings -{ - Write-Status "Save settings" - $dt1 = Get-Date - $curHideNoAccess = Get-SettingValue "HideNoAccess" - - foreach($section in $global:appSettingSections) - { - foreach($settingObj in $section.Values) - { - if(-not $settingObj.Control) { continue } - if($settingObj.Control.IsEnabled -eq $false -and $script:tenantSettings) - { - Remove-TenantSetting $settingObj - continue - } - - $valueFound = $false - if($settingObj.Control.GetType().Name -eq "TextBox") - { - $value = $settingObj.Control.Text - if($settingObj.Type -eq "Int") - { - try - { - $value = [int]$value - } - catch - { - # Log or set invalid - $value = $settingObj.Value - } - } - $valueFound = $true - } - elseif($settingObj.Control.GetType().Name -eq "CheckBox") - { - $value = $settingObj.Control.IsChecked - $valueFound = $true - } - elseif($settingObj.Control.GetType().Name -eq "ComboBox") - { - Write-LogDebug "$($settingObj.Control.Text) | $($settingObj.Control.SelectedIndex)" - if($settingObj.Control.SelectedIndex -eq -1) - { - $value = $settingObj.Control.Text - } - else - { - $value = $settingObj.Control.SelectedValue - } - $valueFound = $true - } - - if($valueFound) - { - if($script:tenantSettings) - { - $subPath = ($global:Organization.Id + "\" + $settingObj.SubPath) - } - else - { - $subPath = $settingObj.SubPath - } - Save-Setting $subPath $settingObj.Key $value - } - } - } - - if($global:currentViewObject.ViewInfo.SaveSettings) - { - & $global:currentViewObject.ViewInfo.SaveSettings - } - - Initialize-Settings -Updated - - $newHideNoAccess = Get-SettingValue "HideNoAccess" - if($curHideNoAccess -ne $newHideNoAccess ) - { - Show-ViewMenu - } - - if($dt1.AddSeconds(1) -lt (Get-Date)) - { - Start-Sleep -Seconds 1 # It goes to quick...ToDo: Do this in a better way - } - Write-Status "" -} - -function Get-SettingValue -{ - param($Key, $defaultValue, [switch]$GlobalOnly, [switch]$TenantOnly, $TenantID) - - foreach($section in $global:appSettingSections) - { - $settingObj = $section.Values | Where Key -eq $Key - if($settingObj) { break } - } - - if(-not $defaultValue) { $defaultValue = $settingObj.DefaultValue } - - $value = $null - if(-not $TenantID) { $TenantID = $global:Organization.Id} - - if($GlobalOnly -ne $true -and $TenantID) - { - # Try get Tenant specific value first - $value = Get-Setting ($TenantID + "\" + $settingObj.SubPath) $settingObj.Key - } - - if($null -eq $value -and $TenantOnly -ne $true) - { - # Get global setting value if tenant value was not found - $value = Get-Setting $settingObj.SubPath $settingObj.Key $defaultValue - } - - if($value) - { - if($settingObj.Type -eq "Boolean") - { - $value = $value -eq $true -or $value -eq "true" - } - elseif($settingObj.Type -eq "Boolean") - { - try - { - $value = [int]$value - } - catch - { - if($settingObj.DefaultValue) - { - try - { - $value = [int]$settingObj.DefaultValue - } - catch { } - } - } - } - - # Keep last read value - if($settingObj -and ($settingObj | Get-Member -MemberType NoteProperty -Name "Value")) - { - $settingObj.Value = $value # Keep last read value - } - else - { - $settingObj | Add-Member -MemberType NoteProperty -Name "Value" -Value $value - } - } - $value -} - -#endregion - -#region Menu functions - -##################################################################################################### -# -# Menu functions -# -##################################################################################################### - -function Add-ViewObject -{ - param($viewObject) - - $global:viewObjects += New-Object PSObject -Property @{ ViewInfo = $viewObject; ViewItems = @() } -} - -function Add-ViewItem -{ - param($viewItem) - - $viewObject = $global:viewObjects | Where { $_.ViewInfo.Id -eq $viewItem.ViewID } - if(-not $viewObject) - { - if(($arrMenuInlcude -and $arrMenuInlcude -notcontains $viewItem.ViewID) -or ($arrMenuExlcude -and $arrMenuExlcude -contains $viewItem.ViewID)) { return } - - Write-Log "Could not find menu with id $($viewItem.ViewID). Item $($viewItem.Title) not added" 2 - return - } - - ### !!! ToDo: Should not be here... - if(-not ($viewItem.PSObject.Properties | Where Name -eq "ImportOrder")) - { - $viewItem | Add-Member -NotePropertyName "ImportOrder" -NotePropertyValue 1000 - } - - foreach($scope in $viewItem.Permissons) - { - 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"))) - { - $ctrl = Get-XamlObject ($global:AppRootFolder + "\Xaml\Icons\$((?? $viewItem.Icon $viewItem.Id)).xaml") - $viewItem | Add-Member -NotePropertyName "IconImage" -NotePropertyValue $ctrl - } - - $viewObject.ViewItems += $viewItem -} - -function Show-View -{ - param($viewId) - - if(($global:viewObjects | measure).Count -eq 0) - { - Write-Log "No View Objects loaded!" 3 - return - } - - if(-not $viewId) - { - # Use first View if not specified - # ToDo: Use last or default view - $viewId = $global:viewObjects[0].ViewInfo.Id - } - - if($global:currentViewObject.ViewInfo.ID -eq $viewId) { return } # Current view already selected - - # Get the View object - $viewObject = $global:viewObjects | Where { $_.ViewInfo.Id -eq $viewId } - if(-not $viewObject) - { - Write-Log "Could not find View with id $($viewId)" 3 - return - } - Write-Log "Change view to $($viewObject.ViewInfo.Title)" - - if($global:currentViewObject -ne $viewObject -and $global:currentViewObject.ViewInfo.Deactivating) - { - Write-Log "Deactivating View $($global:currentViewObject.ViewInfo.Title)" - & $global:currentViewObject.ViewInfo.Deactivating - } - - $global:currentViewObject = $viewObject - - Show-ViewMenu - - $lblMenuTitle.Content = $viewObject.ViewInfo.Title - - $grdViewPanel.Children.Clear() - - if($viewObject.ViewInfo.Authenticate) - { - $global:txtSplashText.Text = "Authenticate" - [System.Windows.Forms.Application]::DoEvents() - & $viewObject.ViewInfo.Authenticate - } - - if($viewObject.ViewInfo.Activating) - { - Write-Log "Activating View $($viewObject.ViewInfo.Title)" - & $viewObject.ViewInfo.Activating - } - - if($viewObject.ViewInfo.ViewPanel) - { - $grdViewPanel.Children.Add($viewObject.ViewInfo.ViewPanel) | Out-Null - } - - Set-MainTitle - - Show-AuthenticationInfo - - if($viewObject.ViewInfo.HideMenu -eq $true) - { - $global:grdViewItemMenu.Visibility = "Collapsed" - } - else - { - $global:grdViewItemMenu.Visibility = "Visible" - } - - if($viewObject.ViewInfo.Activated) - { - Write-Log "Activated View $($viewObject.ViewInfo.Title)" - & $viewObject.ViewInfo.Activated - } - - Invoke-ModuleFunction "Invoke-ViewActivated" -} - -function Show-ViewMenu -{ - $viewObject = $global:currentViewObject - - $viewItems = ?: ($viewObject.ViewInfo.Sort -ne $false) ($viewObject.ViewItems | Sort-Object -Property Title) ($viewObject.ViewItems) - - if((Get-SettingValue "HideNoAccess")) - { - $viewItems = $viewItems | Where { $_."@HasPermissions" -ne $false } - } - - $lstMenuItems.ItemsSource = @($viewItems) -} - -#endregion - -#region Main Window -function Set-MainTitle -{ - if(-not $global:window -or -not $global:currentViewObject.ViewInfo.Title) { return } - - Write-LogDebug "Set main title to $($global:currentViewObject.ViewInfo.Title)" - - $global:window.Title = ?? $global:currentViewObject.ViewInfo.Title "Cloud API PowerShell Management" -} - -function Get-MainWindow -{ - try - { - [xml]$xaml = Get-Content ($global:AppRootFolder + "\Xaml\MainWindow.xaml") - [xml]$styles = Get-Content ($global:AppRootFolder + "\Themes\Styles.xaml") - - ### Update relative path to full path for ResourceDictionary - [System.Xml.XmlNamespaceManager] $nsm = $xaml.NameTable; - $nsm.AddNamespace("s", 'http://schemas.microsoft.com/winfx/2006/xaml/presentation'); - foreach($rsdNode in ($xaml.SelectNodes("//s:ResourceDictionary[@Source]", $nsm))) - { - $rsdNode.Source = (Join-Path ($PSScriptRoot) ($rsdNode.Source)).ToString() - } - - # Add Styles - foreach($node in $styles.DocumentElement.ChildNodes) - { - $tmpNode = $xaml.CreateElement("Temp") - $tmpNode.InnerXml = $node.OuterXml - $xaml.Window.'Window.Resources'.ResourceDictionary.AppendChild($tmpNode.Style) | Out-Null - } - $global:window = [Windows.Markup.XamlReader]::Load((New-Object System.Xml.XmlNodeReader $xaml)) - } - catch - { - Write-LogError "Failed to initialize main window" $_.Exception - return - } - - # ToDo: Convert to a list for data binding - Add-XamlEvent $window "mnuSettings" "Add_Click" -scriptBlock ([scriptblock]{ Show-SettingsForm }) - Add-XamlEvent $window "mnuTenantSettings" "Add_Click" -scriptBlock ([scriptblock]{ Show-SettingsForm -Tenant }) - Add-XamlEvent $window "mnuUpdates" "Add_Click" -scriptBlock ([scriptblock]{ Show-UpdatesDialog }) - Add-XamlEvent $window "mnuAbout" "Add_Click" -scriptBlock ([scriptblock]{ Show-AboutDialog }) - Add-XamlEvent $window "mnuExit" "Add_Click" -scriptBlock ([scriptblock]{ - if([System.Windows.MessageBox]::Show("Are you sure you want to exit?", "Exit?", "YesNo", "Question") -eq "Yes") - { - $window.Close() - } - } - ) - - Add-XamlVariables $xaml $window - - $lstMenuItems.Add_SelectionChanged({ - if($global:currentViewObject.ViewInfo.ItemChanged) - { - & $global:currentViewObject.ViewInfo.ItemChanged - } - }) - - $global:grdPopup.add_MouseLeftButtonDown( { Hide-Popup } ) - - # ToDo: !!! Intune should not be default icon... - $iconFile = "$($global:AppRootFolder)\Intune.ico" - if([io.File]::Exists($iconFile)) - { - $Window.Icon = $iconFile - } - - $window.Add_Closed({ - }) - - $window.add_Loaded({ - $global:SplashScreen.Hide() - $global:window.Activate() - [System.Windows.Forms.Application]::DoEvents() - #$global:window.Topmost = $true - #$global:window.Topmost = $false - #$global:window.Focus() - - $global:MainAppStarted = $true - - if($global:FirstTimeRunning) - { - $script:welcomeForm = Get-XamlObject ($global:AppRootFolder + "\Xaml\Welcome.xaml") -AddVariables - - Add-XamlEvent $script:welcomeForm "gitHubLink" "Add_RequestNavigate" ({ Start-Process $_.Uri.AbsoluteUri; $_.Handled = $true }) - Add-XamlEvent $script:welcomeForm "licenseLink" "Add_RequestNavigate" ({ Start-Process $_.Uri.AbsoluteUri; $_.Handled = $true }) - Add-XamlEvent $script:welcomeForm "addCustomApp" "Add_RequestNavigate" ({ Start-Process $_.Uri.AbsoluteUri; $_.Handled = $true }) - - Add-XamlEvent $script:welcomeForm "chkAcceptConditions" "add_click" { - $global:btnAcceptConditions.IsEnabled = ($this.IsChecked -eq $true) - } - - Add-XamlEvent $script:welcomeForm "btnAcceptConditions" "add_click" { - Save-Setting "" "LicenseAccepted" "True" - Save-Setting "" "FirstTimeRunning" "False" - Save-Setting "" "AppChangeInformed" "true" - Show-ModalObject - - if($global:currentViewObject.ViewInfo.Authentication.ShowErrors) - { - & $global:currentViewObject.ViewInfo.Authentication.ShowErrors - } - } - - Add-XamlEvent $script:welcomeForm "btnCancel" "add_click" { - if([System.Windows.MessageBox]::Show("Conditions not accepted`n`nDo you want to close the application?", "Close App?", "YesNo", "Warning") -eq "Yes") - { - $window.Close() - } - } - - Show-ModalForm $window.Title $script:welcomeForm -HideButtons - } - else - { - if($global:informOldAzureApp -eq $true) - { - $appIdChangeInformed = Get-Setting "" "AppChangeInformed" "false" - if($appIdChangeInformed -ne "true") { - $script:oldAzureAppForm = Get-XamlObject ($global:AppRootFolder + "\Xaml\OldAzureApp.xaml") - - Add-XamlEvent $script:oldAzureAppForm "addCustomApp" "Add_RequestNavigate" ({ Start-Process $_.Uri.AbsoluteUri; $_.Handled = $true }) - - Add-XamlEvent $script:oldAzureAppForm "btnOK" "add_click" { - if((Get-XamlProperty $script:oldAzureAppForm "chkChangeApp" "IsChecked") -eq $true) { - Write-Log "Set default app ID to $($global:DefaultAzureApp)" - Save-Setting "EndpointManager" "EMAzureApp" $global:DefaultAzureApp - $script:azureAppChanged = $true - } - - if((Get-XamlProperty $script:oldAzureAppForm "chkSkippMessage" "IsChecked") -eq $true) { - Save-Setting "" "AppChangeInformed" "true" - } - Show-ModalObject - if($script:azureAppChanged -eq $true -and $global:currentViewObject) { - [System.Windows.Forms.Application]::DoEvents() - & $global:currentViewObject.ViewInfo.Authenticate - } - } - - Show-ModalForm $window.Title $script:oldAzureAppForm -HideButtons - } - } - - ###!!! Force login here - if($global:currentViewObject.ViewInfo.Authenticate) - { - # Skip for now...need additional code to skip previous login and force this based on setting. - #!!!& $global:currentViewObject.ViewInfo.Authenticate -Params (@{"Interactve"=$true}) - } - } - }) - - foreach($view in $global:viewObjects) - { - $subItem = [System.Windows.Controls.MenuItem]::new() - $subItem.Header = $view.ViewInfo.Title - $subItem.Tag = $view.ViewInfo.Id - $subItem.Add_Click({ - if($this.Tag) - { - Show-View $this.Tag - } - }) - $global:mnuViews.AddChild($subItem) | Out-Null - } - -} - -#endregion - -#region Module functions -function Invoke-ModuleFunction -{ - param($function, $arguments = $null) - - Write-Log "Trigger function $function" - - $params = @{} - if($arguments) - { - $params.Add("ArgumentList",$arguments) - } - foreach($module in $global:loadedModules) - { - # Get command with ExportedFunctions instead of Get-Command - $cmd = $module.ExportedFunctions[$function] - if($cmd) - { - Write-Log "Trigger $function in $($module.Name)" - Invoke-Command -ScriptBlock $cmd.ScriptBlock @params - } - else - { - #Write-Log "$function not found in $($module.Name)" 2 - } - } -} - -#endregion - -#region JWTToken - -### See JWT token documentation for more info: https://tools.ietf.org/html/rfc7519 -### AccessToken documentation https://docs.microsoft.com/en-us/azure/active-directory/develop/access-tokens -function Get-JWTtoken -{ - param($token) - - if(-not $token) { return } - - if(-not $token.StartsWith("eyJ")) - { - Write-Log "Invalid JWT token" 3; return - } - - # First part is the header. Second part is the payload. Third part is the signature - $arr = $token.Split(".") - - if($arr.Count -lt 2) { Write-Log "Invalid token" 3; return } - - $header = $arr[0].Replace('-', '+').Replace('_', '/') # change base64url to base64 - while ($header.Length % 4) { $header += "=" } # Add padding to match required length - - $payload = $arr[1].Replace('-', '+').Replace('_', '/') # change base64url to base64 - while ($payload.Length % 4) { $payload += "=" } # Add padding to match required length - - return (New-Object PSObject -Property @{ - Header=(([System.Text.Encoding]::UTF8.GetString(([System.Convert]::FromBase64String($header)))) | ConvertFrom-Json) - Payload=(([System.Text.Encoding]::UTF8.GetString(([System.Convert]::FromBase64String($payload)))) | ConvertFrom-Json) - }) -} -#endregion - -function Add-GridObject -{ - param($grid, $obj) - - $rd = [System.Windows.Controls.RowDefinition]::new() - $rd.Height = [double]::NaN - $obj.SetValue([System.Windows.Controls.Grid]::RowProperty,$grid.RowDefinitions.Count) | Out-Null - $grid.RowDefinitions.Add($rd) | Out-Null - $grid.Children.Add($obj) | Out-Null -} - -function Get-IsAdmin -{ - (New-Object Security.Principal.WindowsPrincipal ([Security.Principal.WindowsIdentity]::GetCurrent())).IsInRole([Security.Principal.WindowsBuiltinRole]::Administrator) -} - -function Get-NumericUpDownControl -{ - param($id, [decimal]$minValue = 0, [decimal]$maxValue = 9999, [int]$step = 1) - - try - { - [xml]$xaml = Get-Content ($global:AppRootFolder + "\Xaml\NumericUpDown.xaml") - $xamlObj = ([Windows.Markup.XamlReader]::Load((New-Object System.Xml.XmlNodeReader $xaml))) - - $xamlObj.Name = $id - $xamlObj.Children[0].Name = $id + "_TextBox" - $xamlObj.Children[1].Name = $id + "_UpButton" - $xamlObj.Children[1].Name = $id + "_DownButton" - - $settings = [PSCustomObject]@{ - MinValue = $minValue - MaxValue = $maxValue - Step = $step - _lastKnownValue = $null - } - - $xamlObj | Add-Member -MemberType NoteProperty -Name "Settings" -Value $settings - - $xamlObj.Children[0].Add_TextChanged({ - $val = $null - if([decimal]::TryParse($this.Parent.Children[0].Text, [ref]$val)) - { - $this.Parent.Settings._lastKnownValue = $val; - } - }) - - $xamlObj.Children[0].Add_LostFocus({ - $val = $null - if([decimal]::TryParse($this.Parent.Children[0].Text, [ref]$val)) - { - ; - } - elseif($this.Parent.Settings._lastKnownValue) - { - $val = $this.Parent.Settings._lastKnownValue - } - - if($val -ne $null) - { - if($val -gt $this.Parent.Settings.MaxValue) - { - $val = $this.Parent.Settings.MaxValue - } - elseif($val -lt $this.Parent.Settings.MinValue) - { - $val = $this.Parent.Settings.MinValue - } - $this.Parent.Children[0].Text = $val.ToString() - } - }) - - $xamlObj.Children[1].Add_Click({ - $val = $null - if([decimal]::TryParse($this.Parent.Children[0].Text, [ref]$val)) - { - $val = $val + $this.Parent.Settings.Step - if($val -gt $this.Parent.Settings.MaxValue) - { - $val = $this.Parent.Settings.MaxValue - } - $this.Parent.Children[0].Text = $val.ToString() - } - }) - - $xamlObj.Children[2].Add_Click({ - $val = $null - if([decimal]::TryParse($this.Parent.Children[0].Text, [ref]$val)) - { - $val = $val - $this.Parent.Settings.Step - if($val -lt $this.Parent.Settings.MinValue) - { - $val = $this.Parent.Settings.MinValue - } - $this.Parent.Children[0].Text = $val.ToString() - } - }) - - return $xamlObj - - } - catch - { - Write-LogError "Failed to create NumericUpDown control" $_.Exception - return $null - } - -} - -function Format-XML -{ - param([xml]$xml, $indent = 2) - - if(-not $xml) { return } - - #From: https://devblogs.microsoft.com/powershell/format-xml/ - $StringWriter = New-Object System.IO.StringWriter - $XmlWriter = New-Object System.XMl.XmlTextWriter $StringWriter - $xmlWriter.Formatting = "indented" - $xmlWriter.Indentation = $Indent - $xml.WriteContentTo($XmlWriter) - $XmlWriter.Flush() - $StringWriter.Flush() - $StringWriter.ToString() -} - -function Update-XmlFormatting { - [CmdletBinding()] - param( - [Parameter(Mandatory, ValueFromPipeline)] - [string]$Xml - ) - process { - - $Xml = $Xml -replace '<([^\s/>]+)([^>]*)\s/>' , '<$1$2/>' - - $Xml = ($Xml -split "`r?`n") | - Where-Object { $_.Trim().Length -gt 0 } | - ForEach-Object { $_ } | - Out-String - - $Xml = $Xml -replace "`r`n", "`n" - - return $Xml.Trim() - } -} - -function Show-LogView -{ - if($script:LogViewObject -and -not $script:LogViewObject.ViewPanel) - { - $viewPanel = Get-XamlObject ($global:AppRootFolder + "\Xaml\LogInfo.xaml") - - if(-not $viewPanel) { return } - - $script:LogViewObject.ViewPanel = $viewPanel - - Set-XamlProperty $viewPanel "dgLogInfo" "ItemsSource" $script:LogItems - - Add-XamlEvent $viewPanel "dgLogInfo" "add_selectionChanged" ({ - $obj = $this.Parent.FindName("txtLogInfo") - if($obj) - { - $obj.Parent.DataContext = $this.SelectedValue - } - }) - } -} - -function Get-Base64ScriptContent -{ - param($encodeContent, [switch]$RemoveSignature) - - if(-not $encodeContent) { return } - - try - { - $scriptContent = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($encodeContent)) - - if($RemoveSignature -eq $true) - { - $x = $scriptContent.IndexOf("# SIG # Begin signature block") - if($x -gt 0) - { - $scriptContent = $scriptContent.SubString(0,$x) - $scriptContent = $scriptContent + "# SIG # Begin signature block`nSignature data excluded..." - } - } - - $scriptContent - } - catch - { - - } -} - -function Get-ProxyURI -{ - if($null -eq $script:proxyURI) - { - $script:proxyUri = Get-SettingValue "ProxyURI" - } - - if($null -eq $script:proxyURI) - { - $script:proxyUri = "" - } - return $script:proxyURI -} - -function Start-DownloadFile -{ - param($sourceURL, $targetFile) - - Write-Log "Download file from $sourceURL" - if(-not $sourceURL) - { - return - } - - if(-not $targetFile) - { - Write-Log "Target file is missing" - return - } - - [void][System.Reflection.Assembly]::LoadWithPartialName("System.Web.Extensions") - $wc = New-Object System.Net.WebClient - $wc.Encoding = [System.Text.Encoding]::UTF8 - $proxyURI = Get-ProxyURI - if($proxyURI) - { - $wc.Proxy = [System.Net.WebProxy]::new($proxyURI) - } - - try - { - $title = $sourceURL.Split("/")[-1] - $title = $title.Split("/")[0] - } - catch - { - $title = $sourceURL - } - - try - { - Write-Status "Download file: `n$title" - $wc.DownloadFile($sourceURL, $targetFile) - Write-Log "File downloaded to $targetFile" - } - catch - { - Write-LogError "Failed to download file" $_.Exception - } - finally - { - $wc.Dispose() - } -} - -function Get-ASCIIBytes -{ - param($String) - - $bytes = [System.Text.Encoding]::ASCII.GetBytes($String) - - if ($bytes[0] -eq 0x2b -and $bytes[1] -eq 0x2f -and $bytes[2] -eq 0x76) - { [Text.Encoding]::UTF7.GetBytes($String) } - elseif ($bytes[0] -eq 0xff -and $bytes[1] -eq 0xfe) - { [Text.Encoding]::Unicode.GetBytes($String) } - elseif ($bytes[0] -eq 0xfe -and $bytes[1] -eq 0xff) - { [Text.Encoding]::BigEndianUnicode.GetBytes($String) } - elseif ($bytes[0] -eq 0x00 -and $bytes[1] -eq 0x00 -and $bytes[2] -eq 0xfe -and $bytes[3] -eq 0xff) - { [Text.Encoding]::UTF32.GetBytes($String) } - elseif ($bytes[0] -eq 0xef -and $bytes[1] -eq 0xbb -and $bytes[2] -eq 0xbf) - { [Text.Encoding]::UTF8.GetBytes($String) } - - $bytes -} - -function Get-DataGridValues -{ - param($dataGrid) - - $dgColumns = $dataGrid.Columns - - $properties = @() - - foreach($tmpCol in $dgColumns) - { - if(-not $tmpCol.Binding.Path.Path) { continue } - $propName = $tmpCol.Binding.Path.Path - $properties += @{n=$tmpCol.Header;e=([Scriptblock]::Create("`$_.$propName"))} - } - - ($dataGrid.ItemsSource | Select -Property $properties) -} - -function Get-GUIDs -{ - param($text) - - $regExpGuid = "[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}" - - $uniqueGuids = New-Object System.Collections.Generic.HashSet[String] - - # Use regular expressions to extract the GUIDs - [regex]::Matches($text, $regExpGuid) | ForEach-Object { $uniqueGuids.Add($_.Value) | Out-Null } - - $uniqueGuids -} - -New-Alias -Name ?? -value Invoke-Coalesce -New-Alias -Name ?: -value Invoke-IfTrue +<# +.SYNOPSIS +Core UI and Settings fatures for the CloudAPIPowerShellManager solution + +.DESCRIPTION +This module handles the WPF UI + +.NOTES + Author: Mikael Karlsson +#> + +function Get-ModuleVersion +{ + '3.9.6' +} + +function Initialize-Window +{ + param($xamlFile) + + try + { + [xml]$xaml = Get-Content $xamlFile + [xml]$styles = Get-Content ($global:AppRootFolder + "\Themes\Styles.xaml") + + ### Update relative path to full path for ResourceDictionary + [System.Xml.XmlNamespaceManager] $nsm = $xaml.NameTable; + $nsm.AddNamespace("s", 'http://schemas.microsoft.com/winfx/2006/xaml/presentation'); + foreach($rsdNode in ($xaml.SelectNodes("//s:ResourceDictionary[@Source]", $nsm))) + { + $rsdNode.Source = (Join-Path ($global:AppRootFolder) ($rsdNode.Source)).ToString() + } + + # Add Styles + foreach($node in $styles.DocumentElement.ChildNodes) + { + $tmpNode = $xaml.CreateElement("Temp") + $tmpNode.InnerXml = $node.OuterXml + $xaml.Window.'Window.Resources'.ResourceDictionary.AppendChild($tmpNode.Style) | Out-Null + } + return ([Windows.Markup.XamlReader]::Load((New-Object System.Xml.XmlNodeReader $xaml))) + } + catch + { + Write-LogError "Failed to initialize window" $_.Exception + return + } +} + +function Start-CoreApp +{ + param($View) + + if(-not $global:defaultGlobalVariables) + { + $global:defaultGlobalVariables = Get-Variable -Scope Global + } + + $global:useDefaultFolderDialog = $false + $global:WindowsAPICodePackLoaded = $false + $script:proxyURI = $null + + $global:loadedModules = @() + $global:viewObjects = @() + $script:LogItems = [System.Collections.ObjectModel.ObservableCollection[object]]::new() + + $global:AppRootFolder = $PSScriptRoot + + # Load all modules in the Modules folder + $global:modulesPath = [IO.Path]::GetDirectoryName($PSCommandPath) + "\Extensions" + + 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 "#####################################################################################" + + Write-Log "PowerShell version: $($PSVersionTable.PSVersion.ToString())" + if($PSVersionTable.BuildVersion) { + Write-Log "PowerShell build: $($PSVersionTable.BuildVersion.ToString())" + } + if($PSVersionTable.CLRVersion) { + Write-Log "PowerShell CLR: $($PSVersionTable.CLRVersion.ToString())" + } + Write-Log "PowerShell edition: $($PSVersionTable.PSEdition)" + + try + { + $osName = Get-ItemPropertyValue "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion" -Name "ProductName" -ErrorAction Stop + $patchLevel = Get-ItemPropertyValue "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion" -Name "UBR" -ErrorAction Stop + $ver = [Version]::new([Environment]::OSVersion.Version.Major,[Environment]::OSVersion.Version.Minor, [Environment]::OSVersion.Version.Build, $patchLevel) + Write-Log "OS: $osName $ver" + } + catch + { + Write-Log "OS version: $([environment]::OSVersion.VersionString)" + } + + if(Test-Path $global:modulesPath) + { + Import-AllModules + } + else + { + Write-Warning "Extensions folder $($global:modulesPath) not found. Aborting..." 3 + exit 1 + } + + Initialize-Settings + $global:currentViewObject = $null + $global:FirstTimeRunning = ((Get-Setting "" "FirstTimeRunning" "true") -eq "true") + $global:MainAppStarted = $false + + Set-SplashWindowText "Initialize views" + [System.Windows.Forms.Application]::DoEvents() + + Invoke-ModuleFunction "Invoke-InitializeModule" + + if($global:hideUI -ne $true) + { + #Add menu group and items + $script:LogViewObject = (New-Object PSObject -Property @{ + Title = "Log" + Description = "View log items" + ID = "CoreLog" + HideMenu = $true + Activating = { Show-LogView } + Permissions = @() + ViewPanel = $null + }) + + Add-ViewObject $script:LogViewObject + + #This will load the main window + $global:txtSplashText.Text = "Load main window" + [System.Windows.Forms.Application]::DoEvents() + Get-MainWindow + + if($global:window) + { + $global:txtSplashText.Text = "Open default view" + [System.Windows.Forms.Application]::DoEvents() + + Show-View $View + + if((Get-SettingValue "CheckForUpdates") -eq $true) { Get-IsLatestVersion } + + Invoke-ModuleFunction "Invoke-ShowMainWindow" + + $global:txtSplashText.Text = "Open main window" + [System.Windows.Forms.Application]::DoEvents() + $global:window.ShowDialog() | Out-Null + } + } + else + { + if(-not $global:SilentBatchFile) + { + Write-Log "SilentBatchFile must be specified" 3 + return + } + $silentFI = [IO.FileInfo]$global:SilentBatchFile + + if($silentFI.Exists -eq $false) + { + Write-Log "SilentBatchFile $($global:SilentBatchFile) not found" 3 + return + } + Invoke-ModuleFunction "Invoke-ShowMainWindow" + + Invoke-ModuleFunction "Invoke-InitSilentBatchJob" + + Start-RunSilentBatchJob + } +} + +function Start-RunSilentBatchJob +{ + try + { + $settingObj = (ConvertFrom-Json (Get-Content -Path $global:SilentBatchFile -Raw -ErrorAction Stop)) + Invoke-ModuleFunction "Invoke-SilentBatchJob" $settingObj + } + catch + { + Write-LogError "Failed to trigger silent batch job." $_.Exception + } +} + +function Import-AllModules +{ + foreach($file in (Get-Item -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() + + $module = Import-Module $file -PassThru -Force -Global -ErrorAction SilentlyContinue + if($module) + { + $global:loadedModules += $module + Write-Host "Module $($module.Name) loaded successfully" + } + else + { + Write-Warning "Failed to load module $file" + } + } +} + +function Set-SplashWindowText +{ + param($text) + + if($global:hideUI -eq $true) { return } + + $global:txtSplashText.Text = $text +} + +#region Log functions +function Write-Log +{ + param($Text, $type = 1) + + if($script:logFailed -eq $true) { return } + + if(-not $global:logFile) { $global:logFile = Get-SettingValue "LogFile" ([IO.Path]::Combine($global:AppRootFolder,"CloudAPIPowerShellManagement.log")) } + + if(-not $global:logFileMaxSize) { [Int64]$global:logFileMaxSize = Get-SettingValue "LogFileSize" 1024; $global:logFileMaxSize = $global:logFileMaxSize * 1kb } + + if($null -eq $global:logOutputError) { $global:logOutputError = Get-SettingValue "LogOutputError" } + + $fi = [IO.FileInfo]$global:logFile + + if($fi.Length -gt $global:logFileMaxSize) + { + # Larger than max size. Rename current to .bak + # Delete current .bak if it exists + $bakFile = ($fi.DirectoryName + "\" + $fi.BaseName + ".lo_") + if([IO.File]::Exists($bakFile)) + { + try + { + [IO.File]::Delete($bakFile) + } + catch { } + } + try + { + $fi.MoveTo($bakFile) + } + catch { } + } + + try + { + $logPath = [IO.Path]::GetDirectoryName($global:logFile) + if(-not (Test-Path $logPath)) { mkdir -Path $logPath -Force -ErrorAction SilentlyContinue | Out-Null } + } + catch + { + $script:logFailed = $true + return + } + + $date = Get-Date + + if($global:PSCommandPath) + { + $fileObj = [System.IO.FileInfo]$global:PSCommandPath + } + else + { + $fileObj = [System.IO.FileInfo]$PSCommandPath + } + + $timeStr = "$($date.ToString(""HH"")):$($date.ToString(""mm"")):$($date.ToString(""ss"")).000+000" + $dateStr = "$($date.ToString(""MM""))-$($date.ToString(""dd""))-$($date.ToString(""yyyy""))" + $logOut = "" + + if($type -eq 2) + { + Write-Warning $Text + $typeStr = "Warning" + } + elseif($type -eq 3) + { + if($global:logOutputError -ne $false) + { + $host.ui.WriteErrorLine($Text) + } + else + { + Write-Warning $Text + } + $typeStr = "Error" + } + else + { + write-host $Text + $typeStr = "Info" + } + + $script:LogItems.Add([PSCustomObject]@{ + ID = ($script:LogItems.Count + 1) + DateTime = $date + Type = $type + TypeText = $typeStr + Text = $Text + }) + + try + { + out-file -filePath $global:logFile -append -encoding "ASCII" -inputObject $logOut + } + catch { } +} + +function Write-LogDebug +{ + param($Text, $type = 1) + + if($global:Debug) + { + Write-Log ("Debug: " + $text) $type + } +} + +function Write-LogError +{ + param($Text, $Exception) + + if($Text -and $Exception.message) + { + $Text += " Exception: $($Exception.Message)" + } + + Write-Log $Text 3 + + if((Get-SettingValue "ShowStackTrace") -eq $true) + { + Write-Log "Stack trace:`n $($Exception.StackTrace)" + + Write-Log "Script stack trace:`n $($Exception.ScriptStackTrace)" + + } +} + +function Write-Status +{ + param($Text, [switch]$SkipLog, [switch]$Block, [switch]$Force) + + if($global:hideUI -eq $true) + { + if($SkipLog -ne $true) { Write-Log $text } + return + } + + if(-not $text) { $global:BlockStatusUpdates = $false } + elseif($global:BlockStatusUpdates -eq $true -and $Force -ne $true) { return } + elseif($Block -eq $true) { $global:BlockStatusUpdates = $true } + + $global:txtInfo.Content = $Text + if($text) + { + $global:grdStatus.Visibility = "Visible" + if($SkipLog -ne $true) { Write-Log $text } + } + else + { + $global:grdStatus.Visibility = "Collapsed" + } + + [System.Windows.Forms.Application]::DoEvents() +} + +#endregion + +#region Popup +function Show-Popup +{ + param($popup) + + if(-not $global:grdPopup -or -not $global:cvsPopup) { return } + + $global:cvsPopup.AddChild($popup) | Out-Null + $global:grdPopup.Visibility = "Visible" + + [System.Windows.Forms.Application]::DoEvents() +} + +function Hide-Popup +{ + if(-not $global:grdPopup -or -not $global:cvsPopup) { return } + $global:cvsPopup.Children.Clear() + $global:grdPopup.Visibility = "Collapsed" + [System.Windows.Forms.Application]::DoEvents() +} +#endregion + +#region Xaml functions + +function Set-XamlProperty +{ + param($xamlObj, $controlName, $propertyName, $value) + + $obj = $xamlObj.FindName($controlName) + + try + { + if($obj) + { + $obj."$propertyName" = $value + } + else + { + Write-Log "Could not find object with name $controlName" 3 + } + } + catch + { + Write-LogError "Failed to set Xaml property value. Control: $controlName. Property: $propertyName. Error:" $_.Exception + } +} + +function Get-XamlProperty +{ + param($xamlObj, $controlName, $propertyName, $defaultValue = $null) + + $obj = $xamlObj.FindName($controlName) + + try + { + if($obj) + { + return (?? $obj."$propertyName" $defaultValue) + } + else + { + Write-Log "Could not find object with name $controlName" 3 + } + } + catch + { + Write-LogError "Failed to set Xaml property value. Control: $controlName. Property: $propertyName. Error:" $_.Exception + } +} + +function Add-XamlEvent +{ + param($xamlObj, $controlName, $eventName, $scriptBlock) + + try { + $obj = $xamlObj.FindName($controlName) + if($obj) + { + $obj."$eventName"($scriptBlock) + } + else + { + Write-Log "Failed to add Xaml event $eventName to $controlName. Control not found" 3 + } + } + catch + { + Write-LogError "Failed to add Xaml event $eventName to $controlName. Error:" $_.Exception + } +} + +function Add-XamlVariables +{ + param($xaml, $obj) + + # Generate a global variable for each object with Name property set + # Ref: https://learn-powershell.net/2014/08/10/powershell-and-wpf-radio-button/ + $xaml.SelectNodes("//*[@*[contains(translate(name(.),'n','N'),'Name')]]") | ForEach-Object { + Write-LogDebug "Add global variable $($_.Name)" + New-Variable -Name $_.Name -Value $obj.FindName($_.Name) -Force -Scope Global + } +} + +function Get-XamlObject +{ + param($fileName, [switch]$AddVariables) + + if(([IO.File]::Exists($fileName))) + { + try + { + [xml]$xaml = Get-Content $fileName + + $xamlObj = ([Windows.Markup.XamlReader]::Load((New-Object System.Xml.XmlNodeReader $xaml))) + + if($xamlObj -and $AddVariables -eq $true) + { + Add-XamlVariables $xaml $xamlObj + } + return $xamlObj + } + catch + { + Write-LogError "Failed to load Xaml file $fileName. Error:" $_.Exception + } + } + else + { + Write-Log "Failed to open Xaml file. File not found: $fileName" + } +} + +function Invoke-RegisterName +{ + param($parent, $name, $registerTo) + + try + { + $control = $parent.FindName($name) + if($control) + { + $registerTo.RegisterName($name, $control) + } + } + catch + { + Write-LogError "Failed to register $name" $_.Exception + } +} + +#endregion + +#region Silent Functions +function Set-BatchProperties +{ + param($settingsObj, $form, [switch]$SkipMissingControlWarning) + + if(-not $settingsObj -or -not $form) + { + return + } + + foreach($prop in $settingsObj) #($settingsObj | GM | Where MemberType -eq NoteProperty)) + { + if($prop.Type -eq "Custom") { continue } + + $obj = $form.FindName($prop.Name) + if(-not $obj) + { + if($SkipMissingControlWarning -ne $true) + { + Write-Log "No setting for $($prop.Name) found" 2 + } + continue + } + + if($prop.Value -is [String] -and [string]::IsNullOrEmpty($prop.Value)) + { + continue + } + + try + { + if($obj -is [System.Windows.Controls.CheckBox]) + { + $obj.IsChecked = $prop.Value -eq $true + } + elseif($obj -is [System.Windows.Controls.TextBox]) + { + $obj.Text = $prop.Value + } + elseif($obj -is [System.Windows.Controls.ComboBox]) + { + $obj.SelectedValue = $prop.Value + } + else + { + try + { + Write-Log "Unsupported object type for silent batch job: $($obj.GetType().FullName)" 3 + } + catch + {} + } + } + catch + { + Write-LogError "Failed to set batch job property for $($prop.Name)" $_.Exception + } + } +} +#endregion + +#region Dialogs + +function Show-AboutDialog +{ + $script:dlgAbout = Get-XamlObject ($global:AppRootFolder + "\Xaml\AboutDialog.xaml") + if(-not $script:dlgAbout) { return } + + $loadedItems = @() + $externalModules = @("MSAL.PS","Az.Account") + $externalAssemblies = @("Microsoft.Identity.Client.dll") + + foreach($module in (((Get-Module | Where-Object { $_.ModuleBase -like "$($global:AppRootFolder)*" -or $_.Name -in $externalModules })))) + { + $ver = $module.Version + if($module.Version.Major -eq 0 -and $module.Version.Minor -eq 0) + { + $cmd = $module.ExportedFunctions["Get-ModuleVersion"] + if($cmd) + { + $tmpVer = Invoke-Command -ScriptBlock $cmd.ScriptBlock + $ver = ?? $tmpVer $ver + } + } + + $loadedItems += (New-Object PSObject -Property @{ + Name = $module.Name + Version = $ver + Type = "PSModule" + }) + } + + $assms = [System.AppDomain]::CurrentDomain.GetAssemblies() | Where { $_.GlobalAssemblyCache -eq $false -and [String]::IsNullOrEmpty($_.Location) -eq $false } + foreach($assmName in $externalAssemblies) + { + $assmObjs = $assms | Where { $_.Location -like "*\$($assmName)" } + foreach($assmObj in $assmObjs) + { + try + { + $fi = [IO.FileInfo]"$($assmObj.Location)" + $loadedItems += (New-Object PSObject -Property @{ + Name = $fi.Name + Version = $fi.VersionInfo.FileVersion + Type = "Assembly" + }) + } + catch {} + } + } + + Set-XamlProperty $script:dlgAbout "txtTitle" "Text" "CloudAPIPowerShellManagement" + Set-XamlProperty $script:dlgAbout "txtViewTitle" "Text" ("Current view: " + $global:currentViewObject.ViewInfo.Title) + if($global:currentViewObject.ViewInfo.Description) + { + Set-XamlProperty $script:dlgAbout "txtViewDescription" "Text" $global:currentViewObject.ViewInfo.Description + } + + Set-XamlProperty $script:dlgAbout "lstModules" "ItemsSource" $loadedItems + + Add-XamlEvent $script:dlgAbout "linkSource" "Add_RequestNavigate" ({ Start-Process $_.Uri.AbsoluteUri; $_.Handled = $true }) + + Show-ModalForm "About" $script:dlgAbout +} + +function Show-UpdatesDialog +{ + $script:dlgUpdates = Get-XamlObject ($global:AppRootFolder + "\Xaml\UpdatesDialog.xaml") + if(-not $script:dlgUpdates) { return } + + Write-Status "Getting Release Notes Information" + + Add-XamlEvent $script:dlgUpdates "btnClose" "add_click" { + $script:dlgUpdates = $null + Show-ModalObject + } + + Add-XamlEvent $script:dlgUpdates "linkSource" "Add_RequestNavigate" ({ Start-Process $_.Uri.AbsoluteUri; $_.Handled = $true }) + + $fileContent = Get-Content -Raw -Path ($global:AppRootFolder + "\ReleaseNotes.md") + try + { + $tmp = $fileContent.Replace("`r`n","`n") + $mystring = ("blob $($tmp.Length)`0" + $tmp) + $mystream = [IO.MemoryStream]::new([byte[]][char[]]$mystring) + $curHash = Get-FileHash -InputStream $mystream -Algorithm SHA1 + } + finally + { + if($mystream) { $mystream.Dispose() } + } + $params = @{} + $proxyURI = Get-ProxyURI + if($proxyURI) + { + $params.Add("proxy", $proxyURI) + $params.Add("UseBasicParsing", $true) + } + + # The notes at the newest 3.x release tag, never at the default branch: that + # branch will carry version 4 once the branches are renamed. + $latestVer = Get-LatestGitHubVersion $params + $notesRef = if($latestVer) { "?ref=$($latestVer.ToString())" } else { "" } + $content = Invoke-RestMethod "https://api.github.com/repos/Micke-K/IntuneManagement/contents/ReleaseNotes.md$notesRef" @params + if($content) + { + $txt = [System.Text.Encoding]::UTF8.GetString(([System.Convert]::FromBase64String($content.content))) + Set-XamlProperty $script:dlgUpdates "txtReleaseNotes" "Text" $txt + + if($content.sha -ne $curHash.Hash) + { + # ReleaseNotes.md not matching + Set-XamlProperty $script:dlgUpdates "tabLocalReleaseNotes" "Visibility" "Visible" + Set-XamlProperty $script:dlgUpdates "txtReleaseNotes" "Text" $fileContent + Set-XamlProperty $script:dlgUpdates "txtReleaseNotesMatch" "Visibility" "Collapsed" + } + else + { + Set-XamlProperty $script:dlgUpdates "txtReleaseNotesNoMatch" "Visibility" "Collapsed" + Set-XamlProperty $script:dlgUpdates "tabLocalReleaseNotes" "Visibility" "Collapsed" + } + } + + Write-Status "" + + Show-ModalForm "Release Notes" $script:dlgUpdates -HideButtons +} + +# Newest published 3.x release on GitHub, or $null. +# +# releases/latest answers the newest release of ANY version. The day 4.0.0 is +# published it would tell every 3.x installation to upgrade to a breaking +# change, and reading the manifest on the default branch stops working once the +# branches are renamed for version 4. The releases list filtered to this major +# is the one source that survives both. Pre-releases and drafts are skipped. +function Get-LatestGitHubVersion +{ + param($Params = @{}) + + $latest = $null + try + { + $releases = Invoke-RestMethod "https://api.github.com/repos/Micke-K/IntuneManagement/releases?per_page=100" @Params + foreach($release in @($releases)) + { + if($release.draft -or $release.prerelease) { continue } + $ver = $null + try { $ver = [version](([string]$release.tag_name) -replace '^v','') } catch { continue } + if($ver.Major -ne 3) { continue } + if($null -eq $latest -or $ver -gt $latest) { $latest = $ver } + } + } + catch + { + Write-Log "Failed to list GitHub releases: $($_.Exception.Message)" 2 + } + return $latest +} + +function Get-IsLatestVersion +{ + if($global:MainAppStarted -ne $true) + { + $global:txtSplashText.Text = "Check for updates" + [System.Windows.Forms.Application]::DoEvents() + } + + $gitHubVer = $null + + $params = @{} + $proxyURI = Get-ProxyURI + if($proxyURI) + { + $params.Add("proxy", $proxyURI) + $params.Add("UseBasicParsing", $true) + } + + $gitHubVer = Get-LatestGitHubVersion $params + + if(-not $gitHubVer) + { + Write-log "Failed to get version info in GitHub" 2 + return + } + + $LocalInfo = $null + $localVer = $null + try + { + Import-LocalizedData -BindingVariable LocalInfo -BaseDirectory $global:AppRootFolder -FileName "CloudAPIPowerShellManagement.psd1" -ErrorAction Stop + $localVer = [version]$LocalInfo.ModuleVersion + } + catch { } + + if(-not $localVer) + { + Write-log "Failed to get version info from local file" 2 + return + } + + if($localVer -lt $gitHubVer) + { + Write-Log "Local version and GitHub version does not match" 2 + Write-Log "Local version: $($localVer.ToString())" + Write-Log "GitHub version: $($gitHubVer.ToString())" + [System.Windows.MessageBox]::Show("There is a new version available on GitHub $($gitHubVer.ToString())`n`nCurrent version is $($localVer.ToString())", "Old version!", "OK", "Warning") + } + else + { + Write-Log "Running latest version: $($localVer.ToString())" + } +} + +function Get-ModuleDataTable +{ + param($moduleText) + + $result = $null + + if(-not $moduleText) { return } + + try + { + $Path = [IO.path]::ChangeExtension([IO.Path]::GetTempFileName(), "psd1") + $FI = [io.FileInfo]$path + $Utf8NoBomEncoding = New-Object System.Text.UTF8Encoding $False + [System.IO.File]::WriteAllLines($FI.FullName, $moduleText, $Utf8NoBomEncoding) + $Result = $null + Import-LocalizedData -BindingVariable Result -BaseDirectory $FI.DirectoryName -FileName $fi.Name + } + catch + { + + } + finally + { + try { [IO.File]::Delete(([IO.path]::ChangeExtension($FI.FullName, "tmp"))) } catch {} + try { $FI.Delete() } catch{} + } + + $Result +} + +function Show-InputDialog +{ + param( + $FormTitle = "Input", + $FormText, + $DefaultValue) + + $script:inputBox = Initialize-Window ($global:AppRootFolder + "\Xaml\InputDialog.xaml") + if(-not $script:inputBox) { return } + + $script:inputBox.Title = $FormTitle + + Set-XamlProperty $script:inputBox "txtLabel" "Content" $FormText + Set-XamlProperty $script:inputBox "txtValue" "Text" $DefaultValue + + $script:txtValue = $script:inputBox.FindName("txtValue") + + Add-XamlEvent $script:inputBox "btnOk" "Add_Click" ({ $script:inputBox.Close() }) + Add-XamlEvent $script:inputBox "btnCancel" "Add_Click" ({ $script:txtValue.Text ="";$script:inputBox.Close() }) + + $inputBox.Add_ContentRendered({ + $script:txtValue.SelectAll(); + $script:txtValue.Focus(); + }) + + $inputBox.Owner = $global:window + $inputBox.Icon = $global:Window.Icon + + $inputBox.ShowDialog() | Out-null + + return $script:txtValue.Text +} + +function Show-ModalForm +{ + param( + $FormTitle = "", + $formObject, + [switch]$HideButtons) + + $xamlStr = Get-Content ($global:AppRootFolder + "\Xaml\ModalForm.xaml") + + $modalForm = [Windows.Markup.XamlReader]::Parse($xamlStr) + + if($HideButtons -eq $true) + { + Set-XamlProperty $modalForm "spButtons" "Visibility" "Collapsed" + } + else + { + Add-XamlEvent $modalForm "btnClose" "Add_Click" ({ + Show-ModalObject + }) + } + + Set-XamlProperty $modalForm "txtTitle" "Text" $FormTitle + + $grdModalContainer = $modalForm.FindName("grdModalContainer") + if($grdModalContainer -and $formObject) + { + $formObject.SetValue([System.Windows.Controls.Grid]::RowProperty,1) + $grdModalContainer.Children.Add($formObject) | Out-Null + } + Show-ModalObject $modalForm +} +function Show-ModalObject +{ + param( $obj ) + + if($obj) + { + $obj.SetValue([System.Windows.Controls.Grid]::RowProperty,1) + $obj.SetValue([System.Windows.Controls.Grid]::ColumnProperty,1) + $global:grdModal.Children.Add($obj) | Out-Null + $global:grdModal.Visibility = "Visible" + } + else + { + $global:grdModal.Children.Clear() + $global:grdModal.Visibility = "Collapsed" + } + + [System.Windows.Forms.Application]::DoEvents() +} +#endregion + +#region Controls +function Show-AuthenticationInfo +{ + if($global:grdMenu) + { + $global:txtSplashText.Text = "Get profile picture" + [System.Windows.Forms.Application]::DoEvents() + + $authenticationProvider = $global:currentViewObject.ViewInfo.Authentication + if($global:grdMenu.Children[-1].Tag -eq "ProfilePicture") + { + $global:grdMenu.Children.Remove($global:grdMenu.Children[-1]) + } + + if($authenticationProvider.ProfilePicture) + { + $profileObj = & $authenticationProvider.ProfilePicture -Size 24 -Fontsize 12 -Popup -AuthenticationProvider $authenticationProvider + if($profileObj) + { + $profileObj.Tag = "ProfilePicture" + $profileObj.SetValue([System.Windows.Controls.Grid]::ColumnProperty,2) | Out-Null + $global:grdMenu.Children.Add($profileObj) | Out-Null + } + } + [System.Windows.Forms.Application]::DoEvents() + } +} + +function Set-EnvironmentInfo +{ + param($environmentName) + + if(-not $global:grdEnvironment) + { + return + } + + if(-not $script:mnuDefaultBGColor) + { + $script:mnuDefaultBGColor = $global:mnuMain.Background + } + if(-not $script:mnuDefaultFGColor) + { + $script:mnuDefaultFGColor = $global:mnuMain.Foreground + } + + if($global:grdEnvironment -and $environmentName) + { + $global:grdEnvironment.Visibility = "Visible" + if((Get-SettingValue "MenuShowOrganizationName") -eq $true) + { + $global:lblEnvironment.Content = $environmentName + } + else + { + $global:lblEnvironment.Content = "" + } + $bgColor = (Get-SettingValue "MenuBGColor") + $fgColor = (Get-SettingValue "MenuFGColor") + + if(-not $bgColor) + { + $bgColor = $script:mnuDefaultBGColor + } + + if($bgColor) + { + $global:grdMenu.Background = $bgColor + $global:mnuMain.Background = $bgColor + } + + if(-not $fgColor) + { + $fgColor = $script:mnuDefaultFGColor + } + + if($fgColor) + { + $global:lblEnvironment.Foreground = $fgColor + $global:mnuMain.Foreground = $fgColor + } + } + else + { + $global:grdEnvironment.Visibility = "Collapsed" + $global:lblEnvironment.Content = "" + $global:mnuMain.Background = $script:mnuDefaultBGColor + $global:mnuMain.Foreground = $script:mnuDefaultFGColor + $global:lblEnvironment.Foreground = $script:mnuDefaultFGColor + } +} + +#endregion + +#region Generic functions +function Invoke-Coalesce ($value, $default) +{ + # Use IsNullOrEmpty instead of -not + if ([String]::IsNullOrEmpty($value)) { $value = $default } + + return $value +} + +function Invoke-IfTrue ($expression, $valueIfTrue, $valueIfFalse) +{ + if ($expression) { return $valueIfTrue } + else { return $valueIfFalse } +} + +function Set-ObjectGrid +{ + param( $obj ) + + if($obj) + { + $global:grdObject.Children.Add($obj) | Out-Null + $global:grdObject.Visibility = "Visible" + } + else + { + $global:grdObject.Children.Clear() + $global:grdObject.Visibility = "Collapsed" + } + + [System.Windows.Forms.Application]::DoEvents() +} + +function Remove-InvalidFileNameChars +{ + param($Name) + + $re = "[{0}]" -f [RegEx]::Escape(([IO.Path]::GetInvalidFileNameChars() -join '')) + + $Name = $Name -replace $re + + + return $Name +} + +function Remove-ObjectProperty +{ + param($obj, $property) + + if(-not $obj -or -not $property) { return } + + if(($obj | Get-Member -MemberType NoteProperty -Name $property)) + { + $obj.PSObject.Properties.Remove($property) + } +} + +function Get-Folder +{ + param($path = $env:temp, $title = "Select a directory") + + if($global:useDefaultFolderDialog -ne $true) + { + try + { + if($global:WindowsAPICodePackLoaded -eq $false) + { + $apiCodec = Join-Path $global:AppRootFolder "Bin\Microsoft.WindowsAPICodePack.Shell.dll" + if([IO.File]::Exists($apiCodec)) + { + Add-Type -Path $apiCodec | Out-Null + $global:WindowsAPICodePackLoaded = $true + } + else + { + Write-Log "Could not find Microsoft.WindowsAPICodePack.Shell.dll" 2 + } + } + $dlgCOFD = New-Object Microsoft.WindowsAPICodePack.Dialogs.CommonOpenFileDialog + } + catch + { + Write-LogError "Failed to load Microsoft.WindowsAPICodePack.Shell.dll. Verify that the .Net 3.5 feature is enabled" $_.Exception + } + } + + if($dlgCOFD -and $global:useDefaultFolderDialog -ne $true) + { + $dlgCOFD.EnsureReadOnly = $true + $dlgCOFD.IsFolderPicker = $true + $dlgCOFD.AllowNonFileSystemItems = $false + $dlgCOFD.Multiselect = $false + $dlgCOFD.Title = $title + + if($path -and (Test-Path $path)) + { + $dlgCOFD.InitialDirectory = $path + } + if($dlgCOFD.ShowDialog($window) -eq [Microsoft.WindowsAPICodePack.Dialogs.CommonFileDialogResult]::Ok) + { + $dlgCofd.FileName + } + } + else + { + $global:useDefaultFolderDialog = $true + [Reflection.Assembly]::LoadWithPartialName("System.Windows.Forms") | Out-Null + [System.Windows.Forms.Application]::EnableVisualStyles() + $dlgFBD = New-Object System.Windows.Forms.FolderBrowserDialog + $dlgFBD.SelectedPath = "C:\" + $dlgFBD.ShowNewFolderButton = $false + $dlgFBD.Description = $title + if($dlgFBD.ShowDialog() -eq "OK") + { + $dlgFBD.SelectedPath + } + $dlgFBD.Dispose() + } +} +function Remove-Property +{ + param($obj, $prop) + + if(-not $prop) { return } + + if(($obj | GM -MemberType NoteProperty -Name $prop)) + { + Write-LogDebug "Remove property $prop" + $obj.PSObject.Properties.Remove($prop) | Out-Null + } +} + +function Get-GridCheckboxColumn +{ + param($bindingProperty = "IsSelected", [scriptblock]$scriptBlock) + + $binding = [System.Windows.Data.Binding]::new($bindingProperty) + $binding.UpdateSourceTrigger = [System.Windows.Data.UpdateSourceTrigger]::PropertyChanged + $column = [System.Windows.Controls.DataGridTemplateColumn]::new() + $fef = [System.Windows.FrameworkElementFactory]::new([System.Windows.Controls.CheckBox]) + $binding.Mode = [System.Windows.Data.BindingMode]::TwoWay + $fef.SetValue([System.Windows.Controls.CheckBox]::IsCheckedProperty,$binding) + if($null -ne $scriptBlock) + { + [System.Windows.RoutedEventHandler]$checkedEventHandler = $scriptBlock + $fef.AddHandler([System.Windows.Controls.CheckBox]::CheckedEvent, $checkedEventHandler) + } + $dt = [System.Windows.DataTemplate]::new() + $dt.VisualTree = $fef + $column.CellTemplate = $dt + $header = [System.Windows.Controls.CheckBox]::new() + $header.Margin = [System.Windows.Thickness]::new(-4,0,0,0) # Align header checkbox with the row checkboxes + $header.ToolTip = "Select/deselect all items" + $column.Header = $header + if($null -ne $scriptBlock) + { + #$header.add_click($scriptBlock) + } + + $column +} + +function Expand-FileName +{ + param($fileName) + + [Environment]::SetEnvironmentVariable("Date",(Get-Date).ToString("yyyy-MM-dd"),[System.EnvironmentVariableTarget]::Process) + [Environment]::SetEnvironmentVariable("DateTime",(Get-Date).ToString("yyyyMMdd-HHmm"),[System.EnvironmentVariableTarget]::Process) + [Environment]::SetEnvironmentVariable("Organization",$global:Organization.displayName,[System.EnvironmentVariableTarget]::Process) + + $fileName = [Environment]::ExpandEnvironmentVariables($fileName) + + foreach($tmpFolder in ([System.Enum]::GetNames([System.Environment+SpecialFolder]))) + { + $fileName = $fileName -replace "%$($tmpFolder)%",([Environment]::GetFolderPath($tmpFolder)) + } + + [Environment]::SetEnvironmentVariable("Date",$null,[System.EnvironmentVariableTarget]::Process) + [Environment]::SetEnvironmentVariable("DateTime",$null,[System.EnvironmentVariableTarget]::Process) + [Environment]::SetEnvironmentVariable("Organization",$null,[System.EnvironmentVariableTarget]::Process) + + # Remove invalid path characters + $re = "[{0}]" -f [RegEx]::Escape(([IO.Path]::GetInvalidPathChars() -join '')) + $fileName = $fileName -replace $re + + $fileName +} + +#endregion + +#region Save/Read Settings functions +######################################################################## +# +# Save/Read Settings +# +######################################################################## +function Initialize-Settings +{ + param([switch]$Updated) + + $global:Debug = Get-SettingValue "Debug" + $global:logFile = $null + $global:logFileMaxSize = $null + $global:logOutputError = $null + $script:proxyURI = $null + + if($Updated -eq $true) + { + Set-EnvironmentInfo $global:Organization.displayName + 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") + + if($global:hideUI -eq $true) { return } + + if($global:JsonSettingsObj -and $global:JSonSettingFile) + { + if($SubPath) + { + $arrParts = $SubPath.TrimEnd(@('/','\')).Split(@('/','\')) + } + else + { + $arrParts = @() + } + + $parentSetting = $global:JsonSettingsObj + + foreach($part in $arrParts) + { + if(-not $part.Trim()) { continue } + + if(($parentSetting.PSObject.Properties | Where Name -eq $part)) + { + $parentSetting = $parentSetting.$part + } + else + { + $parentSetting | Add-Member -MemberType NoteProperty -Name $part -Value ([PSCustomObject]@{}) + $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 + } +} + +function Remove-Setting +{ + param($SubPath = "", $Key = "") + + if($global:JsonSettingsObj) + { + if($SubPath) + { + $arrParts = $SubPath.TrimEnd(@('/','\')).Split(@('/','\')) + } + else + { + $arrParts = @() + } + + $parentSetting = $global:JsonSettingsObj + + foreach($part in $arrParts) + { + if(($parentSetting.PSObject.Properties | Where Name -eq $part)) + { + $parentSetting = $parentSetting.$part + } + else + { + return + } + } + + if(($parentSetting.PSObject.Properties | Where Name -eq $Key)) + { + $parentSetting.PSObject.Properties.Remove($Key) + } + } + else + { + $regPath = Get-RegPath $subPath + try + { + $temp = Get-Item -LiteralPath $regPath -ErrorAction SilentlyContinue + if(($temp -and $temp.Property -contains $Key)) + { + Remove-ItemProperty -Path $regPath -Name $Key -Force -ErrorAction Stop + } + } + catch + { + Write-LogError "Failed to remove reg value: $($Key) in key $($regPath)" $_.Exception + } + } +} + +function Get-Setting +{ + param($SubPath = "", $Key = "", $defaultValue) + + if(-not $key) + { + return + } + + $val = $null + + if($global:JsonSettingsObj) + { + try + { + if($SubPath) + { + $arrParts = $SubPath.TrimEnd(@('/','\')).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) + { + $defaultValue + } + else + { + $val + } +} + +function Get-RegPath +{ + param($SubPath) + + $path = "HKCU:\Software\CloudAPIPowerShellManagement" + if($SubPath) + { + $path = $path + "\" + $SubPath + } + + $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 -ErrorAction SilentlyContinue + 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 + } +} + +function Remove-TenantSetting +{ + param($settingValue) + + $subPath = ($global:Organization.Id + "\" + $settingValue.SubPath) + + if($global:JsonSettingsObj) + { + if($SubPath) + { + $arrParts = $SubPath.TrimEnd(@('/','\')).Split(@('/','\')) + } + else + { + $arrParts = @() + } + + $parentSetting = $global:JsonSettingsObj + + foreach($part in $arrParts) + { + if(($parentSetting.PSObject.Properties | Where Name -eq $part)) + { + $parentSetting = $parentSetting.$part + } + else + { + return + } + } + + if(($parentSetting.PSObject.Properties | Where Name -eq $settingValue.Key)) + { + $parentSetting.PSObject.Properties.Remove($settingValue.Key) + } + + } + else + { + $regPath = Get-RegPath $subPath + try + { + $temp = Get-Item -LiteralPath $regPath -ErrorAction SilentlyContinue + if(($temp.Property -contains $settingValue.Key)) + { + Remove-ItemProperty -Path $regPath -Name $settingValue.Key -Force -ErrorAction Stop + } + } + catch + { + Write-LogError "Failed to remove reg value: $($settingValue.Key) in key $($regPath)" $_.Exception + } + } +} + +function Get-IsTenantSettingConfigured +{ + param($settingValue) + + $subPath = ($global:Organization.Id + "\" + $settingValue.SubPath) + + if($global:JsonSettingsObj) + { + if($SubPath) + { + $arrParts = $SubPath.TrimEnd(@('/','\')).Split(@('/','\')) + } + else + { + $arrParts = @() + } + + $parentSetting = $global:JsonSettingsObj + + foreach($part in $arrParts) + { + if(($parentSetting.PSObject.Properties | Where Name -eq $part)) + { + $parentSetting = $parentSetting.$part + } + else + { + return $false + } + } + + return ($null -ne ($parentSetting.PSObject.Properties | Where Name -eq $settingValue.Key)) + + } + else + { + $regPath = Get-RegPath $subPath + try + { + $temp = Get-Item -LiteralPath $regPath -ErrorAction Stop + return ($temp.GetValueNames() -contains $settingValue.Key) + } + catch + { + + } + } + return $false +} +#endregion + +#region Setting functions + +######################################################################## +# +# Settings functions +# +######################################################################## + +function Add-SettingsItem +{ + param($settingItem, $settingValue) + + $rd = [System.Windows.Controls.RowDefinition]::new() + $rd.Height = [double]::NaN + $spSettings.RowDefinitions.Add($rd) + $settingItem.SetValue([System.Windows.Controls.Grid]::RowProperty,$spSettings.RowDefinitions.Count-1) + + if(-not $settingValue) + { + $settingItem.SetValue([System.Windows.Controls.Grid]::ColumnSpanProperty, 99) + } + else + { + if($settingValue.Description) + { + $descriptionInfo = "" + + "" + + $settingValue.Description + + "" + + "" + } + + $xaml = @" + + + $descriptionInfo + +"@ + + if($script:tenantSettings -and $settingValue) + { + #_IsChecked + $tenantConfig = [System.Windows.Controls.CheckBox]::new() + $tenantConfig.ToolTip = "Enable tenant specific setting" + $tenantConfig.SetValue([System.Windows.Controls.Grid]::RowProperty,$spSettings.RowDefinitions.Count-1) + $tenantConfig.SetValue([System.Windows.Controls.Grid]::ColumnProperty, 0) + $tenantConfig.Margin = "0,5,0,0" + $tenantConfig.Tag = $settingValue + $tenantConfig.IsChecked = (Get-IsTenantSettingConfigured $settingValue) + $settingItem.IsEnabled = $tenantConfig.IsChecked + $tenantConfig.add_Click({ + if($this.Tag.Control) { $this.Tag.Control.IsEnabled = $this.IsChecked } + } + ) + $spSettings.AddChild($tenantConfig) + } + + $settingsTitle = [Windows.Markup.XamlReader]::Parse($xaml) + $settingsTitle.SetValue([System.Windows.Controls.Grid]::RowProperty,$spSettings.RowDefinitions.Count-1) + $settingsTitle.SetValue([System.Windows.Controls.Grid]::ColumnProperty, 1) + + $settingItem.SetValue([System.Windows.Controls.Grid]::ColumnProperty, 2) + $spSettings.AddChild($settingsTitle) + $settingItem.Margin = "0,5,0,0" + } + $spSettings.AddChild($settingItem) +} + +function Add-SettingTextBox +{ + param($id, $value) + + $xaml = @" +$value +"@ + return [Windows.Markup.XamlReader]::Parse($xaml) +} + +function Add-SettingCheckBox +{ + param($id, $value) + + $tmpValue = ($value -eq $true -or $value -eq "true").ToString().ToLower() + + $xaml = @" + +"@ + return [Windows.Markup.XamlReader]::Parse($xaml) +} + +function Add-SettingComboBox +{ + param($id, $value, $settingObj) + + $nameProp = ?? $settingObj.DisplayMemberPath "Name" + $valueProp = ?? $settingObj.SelectedValuePath "Value" + + $xaml = @" + +"@ + $xamlObj = [Windows.Markup.XamlReader]::Parse($xaml) + + $xamlObj.ItemsSource = $settingObj.ItemsSource + if($value) + { + $xamlObj.SelectedValue = $value + } + + $xamlObj +} + +function Add-SettingFolder +{ + param($id, $value) + $xaml = @" + + + + + + + $value + + + +"@ + + $obj = [Windows.Markup.XamlReader]::Parse($xaml) + + $btnBrowse = $obj.FindName("browse_$($id)") + $txtObj = $obj.FindName($id) + if($btnBrowse) + { + $btnBrowse.Tag = $txtObj + $btnBrowse.Add_Click({ + $folder = Get-Folder $this.Tag.Text + if($folder) { $this.Tag.Text = $folder } + }) + } + return $obj +} + +function Add-SettingValue +{ + param($settingValue) + + $id = "id_" + [Guid]::NewGuid().ToString('n') + + if($settingValue.TenantSettings -eq $false -and $script:tenantSettings) + { + return # Value nut supported in Tenant Settings + } + elseif($settingValue.GlobalSettings -eq $false -and $script:tenantSettings -ne $true) + { + return # Value nut supported in Global Settings + } + + $value = Get-SettingValue $settingValue.Key -GlobalOnly:($script:tenantSettings -ne $true) + + if($settingValue.Type -eq "folder") + { + $settingObj = Add-SettingFolder $id $value + } + elseif($settingValue.Type -eq "Boolean") + { + $settingObj = Add-SettingCheckBox $id $value + } + elseif($settingValue.Type -eq "List") + { + $settingObj = Add-SettingComboBox $id $value $settingValue + } + else + { + $settingObj = Add-SettingTextBox $id $value + } + + if($settingObj) + { + Add-SettingsItem $settingObj $settingValue + # Find the control in the setting object that contains the actual value + # $settingObj might be a grid that contains the TextBox with the settings value + $ctrl = $settingObj.FindName($id) + if(($settingValue | Get-Member -MemberType NoteProperty -Name "Control")) + { + $settingValue.Control = $ctrl + } + else + { + $settingValue | Add-Member -MemberType NoteProperty -Name "Control" -Value $ctrl + } + } +} + +function Add-SettingTitle +{ + param($title, $marginTop = "0") + + $xaml = @" + +"@ + + #$global:spSettings.Children.Add([Windows.Markup.XamlReader]::Parse($xaml)) + Add-SettingsItem ([Windows.Markup.XamlReader]::Parse($xaml)) | Out-Null +} + +function Show-SettingsForm +{ + param([switch]$Tenant) + + $settingsStr = Get-Content ($global:AppRootFolder+ "\Xaml\SettingsForm.xaml") + + $settingsForm = [Windows.Markup.XamlReader]::Parse($settingsStr) + $global:settingControls = @() + $global:spSettings = $settingsForm.FindName("spSettings") + + $script:tenantSettings = ($Tenant -eq $true) + Add-XamlEvent $settingsForm "btnSave" "Add_Click" ({ + Save-AllSettings + }) + + Add-XamlEvent $settingsForm "btnClose" "Add_Click" ({ + $script:tenantSettings = $null + Show-ModalObject + }) + + if($JsonSettingsObj -or $script:tenantSettings -eq $true) + { + 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) + { + Add-SettingTitle $tmp.Title + foreach($settingObj in $tmp.Values) + { + Add-SettingValue $settingObj + } + } + + foreach($settingObj in $global:appSettingSections) + { + if(-not ($settingObj | Get-Member -MemberType NoteProperty -Name "Priority")) + { + $settingObj | Add-Member -MemberType NoteProperty -Name "Priority" -Value 100 + } + if($settingObj.Priority -lt 1) { $settingObj.Priority = 1} + } + + foreach($section in ($global:appSettingSections | Where-Object Id -ne "General" | Sort-Object -Property Priority,Title)) + { + if($section.Values.Count -eq 0) { continue } + Add-SettingTitle $section.Title 5 + foreach($settingObj in $section.Values) + { + Add-SettingValue $settingObj + } + } + Show-ModalObject $settingsForm +} + +function Add-DefaultSettings +{ + $global:appSettingSections = @() + + $script:lstColors = @() + $script:lstColors += [PSCustomObject]@{ + Name = "" + Value = "" + } + + foreach($color in ([System.Drawing.Color].GetProperties() | Where { $_.PropertyType -eq [System.Drawing.Color] } | Sort -Property Name | Select Name).Name) + { + $script:lstColors += [PSCustomObject]@{ + Name = $color + Value = $color + } + } + + $global:appSettingSections += (New-Object PSObject -Property @{ + Title = "General" + Id = "General" + Values = @() + }) + + Add-SettingsObject (New-Object PSObject -Property @{ + Title = "Log file" + Key = "LogFile" + Type = "File" + }) "General" + + Add-SettingsObject (New-Object PSObject -Property @{ + Title = "Max log file size" + Key = "LogFileSize" + Type = "Int" + DefaultValue = 1024 + }) "General" + + Add-SettingsObject (New-Object PSObject -Property @{ + Title = "Add errors to PowerShell output" + Key = "LogOutputError" + Type = "Boolean" + Description = "Write errors to the Error Output of the PS Host. If disabled, errors will be written as a Warning. Eg. disable this if automation should skip logging PowerShell errors." + DefaultValue = $true + }) "General" + + Add-SettingsObject (New-Object PSObject -Property @{ + Title = "Show stack error" + Key = "ShowStackTrace" + Type = "Boolean" + Description = "Write exception stack trace info to the log." + DefaultValue = $false + }) "General" + + Add-SettingsObject (New-Object PSObject -Property @{ + Title = "Debug" + Key = "Debug" + Type = "Boolean" + DefaultValue = $false + }) "General" + + Add-SettingsObject (New-Object PSObject -Property @{ + Title = "Hide No-access items" + Key = "HideNoAccess" + Type = "Boolean" + Description="Remove items from the menu if object permissions is missing. Default is to mark them with red" + DefaultValue = $false + }) "General" + + Add-SettingsObject (New-Object PSObject -Property @{ + Title = "Preview" + Key = "PreviewFeatures" + Type = "Boolean" + DefaultValue = $false + Description = "Enable features that are marked as Preview. This might require a restart and prompt for consent" + }) "General" + + Add-SettingsObject (New-Object PSObject -Property @{ + Title = "Check for updates" + Key = "CheckForUpdates" + Type = "Boolean" + DefaultValue = $true + Description = "Check GitHub if there is a later version available" + }) "General" + + Add-SettingsObject (New-Object PSObject -Property @{ + Title = "Menu Background color" + Key = "MenuBGColor" + Type = "List" + ItemsSource = $script:lstColors + DefaultValue = "" + }) "General" + + Add-SettingsObject (New-Object PSObject -Property @{ + Title = "Menu Foreground color" + Key = "MenuFGColor" + Type = "List" + ItemsSource = $script:lstColors + DefaultValue = "" + }) "General" + + Add-SettingsObject (New-Object PSObject -Property @{ + Title = "Show tenant name" + Key = "MenuShowOrganizationName" + Type = "Boolean" + DefaultValue = $true + Description = "Adds the organization name next to the login info on the menu bar" + }) "General" + + Add-SettingsObject (New-Object PSObject -Property @{ + Title = "Proxy URI" + Key = "ProxyURI" + Description = "Specify the URI for the proxy eg http://<server>:<port>" + }) "General" + +} + +function Add-SettingsObject +{ + param($obj, $section) + + $section = $global:appSettingSections | Where-Object Id -eq $section + if(-not $section) + { + Write-Log "Could not find section $section" 3 + return + } + + try + { + $section.Values += $obj + } + catch { } +} + +function Save-AllSettings +{ + Write-Status "Save settings" + $dt1 = Get-Date + $curHideNoAccess = Get-SettingValue "HideNoAccess" + + foreach($section in $global:appSettingSections) + { + foreach($settingObj in $section.Values) + { + if(-not $settingObj.Control) { continue } + if($settingObj.Control.IsEnabled -eq $false -and $script:tenantSettings) + { + Remove-TenantSetting $settingObj + continue + } + + $valueFound = $false + if($settingObj.Control.GetType().Name -eq "TextBox") + { + $value = $settingObj.Control.Text + if($settingObj.Type -eq "Int") + { + try + { + $value = [int]$value + } + catch + { + # Log or set invalid + $value = $settingObj.Value + } + } + $valueFound = $true + } + elseif($settingObj.Control.GetType().Name -eq "CheckBox") + { + $value = $settingObj.Control.IsChecked + $valueFound = $true + } + elseif($settingObj.Control.GetType().Name -eq "ComboBox") + { + Write-LogDebug "$($settingObj.Control.Text) | $($settingObj.Control.SelectedIndex)" + if($settingObj.Control.SelectedIndex -eq -1) + { + $value = $settingObj.Control.Text + } + else + { + $value = $settingObj.Control.SelectedValue + } + $valueFound = $true + } + + if($valueFound) + { + if($script:tenantSettings) + { + $subPath = ($global:Organization.Id + "\" + $settingObj.SubPath) + } + else + { + $subPath = $settingObj.SubPath + } + Save-Setting $subPath $settingObj.Key $value + } + } + } + + if($global:currentViewObject.ViewInfo.SaveSettings) + { + & $global:currentViewObject.ViewInfo.SaveSettings + } + + Initialize-Settings -Updated + + $newHideNoAccess = Get-SettingValue "HideNoAccess" + if($curHideNoAccess -ne $newHideNoAccess ) + { + Show-ViewMenu + } + + if($dt1.AddSeconds(1) -lt (Get-Date)) + { + Start-Sleep -Seconds 1 # It goes to quick...ToDo: Do this in a better way + } + Write-Status "" +} + +function Get-SettingValue +{ + param($Key, $defaultValue, [switch]$GlobalOnly, [switch]$TenantOnly, $TenantID) + + foreach($section in $global:appSettingSections) + { + $settingObj = $section.Values | Where Key -eq $Key + if($settingObj) { break } + } + + if(-not $defaultValue) { $defaultValue = $settingObj.DefaultValue } + + $value = $null + if(-not $TenantID) { $TenantID = $global:Organization.Id} + + if($GlobalOnly -ne $true -and $TenantID) + { + # Try get Tenant specific value first + $value = Get-Setting ($TenantID + "\" + $settingObj.SubPath) $settingObj.Key + } + + if($null -eq $value -and $TenantOnly -ne $true) + { + # Get global setting value if tenant value was not found + $value = Get-Setting $settingObj.SubPath $settingObj.Key $defaultValue + } + + if($value) + { + if($settingObj.Type -eq "Boolean") + { + $value = $value -eq $true -or $value -eq "true" + } + elseif($settingObj.Type -eq "Boolean") + { + try + { + $value = [int]$value + } + catch + { + if($settingObj.DefaultValue) + { + try + { + $value = [int]$settingObj.DefaultValue + } + catch { } + } + } + } + + # Keep last read value + if($settingObj -and ($settingObj | Get-Member -MemberType NoteProperty -Name "Value")) + { + $settingObj.Value = $value # Keep last read value + } + else + { + $settingObj | Add-Member -MemberType NoteProperty -Name "Value" -Value $value + } + } + $value +} + +#endregion + +#region Menu functions + +##################################################################################################### +# +# Menu functions +# +##################################################################################################### + +function Add-ViewObject +{ + param($viewObject) + + $global:viewObjects += New-Object PSObject -Property @{ ViewInfo = $viewObject; ViewItems = @() } +} + +function Add-ViewItem +{ + param($viewItem) + + $viewObject = $global:viewObjects | Where { $_.ViewInfo.Id -eq $viewItem.ViewID } + if(-not $viewObject) + { + if(($arrMenuInlcude -and $arrMenuInlcude -notcontains $viewItem.ViewID) -or ($arrMenuExlcude -and $arrMenuExlcude -contains $viewItem.ViewID)) { return } + + Write-Log "Could not find menu with id $($viewItem.ViewID). Item $($viewItem.Title) not added" 2 + return + } + + ### !!! ToDo: Should not be here... + if(-not ($viewItem.PSObject.Properties | Where Name -eq "ImportOrder")) + { + $viewItem | Add-Member -NotePropertyName "ImportOrder" -NotePropertyValue 1000 + } + + foreach($scope in $viewItem.Permissons) + { + 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"))) + { + $ctrl = Get-XamlObject ($global:AppRootFolder + "\Xaml\Icons\$((?? $viewItem.Icon $viewItem.Id)).xaml") + $viewItem | Add-Member -NotePropertyName "IconImage" -NotePropertyValue $ctrl + } + + $viewObject.ViewItems += $viewItem +} + +function Show-View +{ + param($viewId) + + if(($global:viewObjects | measure).Count -eq 0) + { + Write-Log "No View Objects loaded!" 3 + return + } + + if(-not $viewId) + { + # Use first View if not specified + # ToDo: Use last or default view + $viewId = $global:viewObjects[0].ViewInfo.Id + } + + if($global:currentViewObject.ViewInfo.ID -eq $viewId) { return } # Current view already selected + + # Get the View object + $viewObject = $global:viewObjects | Where { $_.ViewInfo.Id -eq $viewId } + if(-not $viewObject) + { + Write-Log "Could not find View with id $($viewId)" 3 + return + } + Write-Log "Change view to $($viewObject.ViewInfo.Title)" + + if($global:currentViewObject -ne $viewObject -and $global:currentViewObject.ViewInfo.Deactivating) + { + Write-Log "Deactivating View $($global:currentViewObject.ViewInfo.Title)" + & $global:currentViewObject.ViewInfo.Deactivating + } + + $global:currentViewObject = $viewObject + + Show-ViewMenu + + $lblMenuTitle.Content = $viewObject.ViewInfo.Title + + $grdViewPanel.Children.Clear() + + if($viewObject.ViewInfo.Authenticate) + { + $global:txtSplashText.Text = "Authenticate" + [System.Windows.Forms.Application]::DoEvents() + & $viewObject.ViewInfo.Authenticate + } + + if($viewObject.ViewInfo.Activating) + { + Write-Log "Activating View $($viewObject.ViewInfo.Title)" + & $viewObject.ViewInfo.Activating + } + + if($viewObject.ViewInfo.ViewPanel) + { + $grdViewPanel.Children.Add($viewObject.ViewInfo.ViewPanel) | Out-Null + } + + Set-MainTitle + + Show-AuthenticationInfo + + if($viewObject.ViewInfo.HideMenu -eq $true) + { + $global:grdViewItemMenu.Visibility = "Collapsed" + } + else + { + $global:grdViewItemMenu.Visibility = "Visible" + } + + if($viewObject.ViewInfo.Activated) + { + Write-Log "Activated View $($viewObject.ViewInfo.Title)" + & $viewObject.ViewInfo.Activated + } + + Invoke-ModuleFunction "Invoke-ViewActivated" +} + +function Show-ViewMenu +{ + $viewObject = $global:currentViewObject + + $viewItems = ?: ($viewObject.ViewInfo.Sort -ne $false) ($viewObject.ViewItems | Sort-Object -Property Title) ($viewObject.ViewItems) + + if((Get-SettingValue "HideNoAccess")) + { + $viewItems = $viewItems | Where { $_."@HasPermissions" -ne $false } + } + + $lstMenuItems.ItemsSource = @($viewItems) +} + +#endregion + +#region Main Window +function Set-MainTitle +{ + if(-not $global:window -or -not $global:currentViewObject.ViewInfo.Title) { return } + + Write-LogDebug "Set main title to $($global:currentViewObject.ViewInfo.Title)" + + $global:window.Title = ?? $global:currentViewObject.ViewInfo.Title "Cloud API PowerShell Management" +} + +function Get-MainWindow +{ + try + { + [xml]$xaml = Get-Content ($global:AppRootFolder + "\Xaml\MainWindow.xaml") + [xml]$styles = Get-Content ($global:AppRootFolder + "\Themes\Styles.xaml") + + ### Update relative path to full path for ResourceDictionary + [System.Xml.XmlNamespaceManager] $nsm = $xaml.NameTable; + $nsm.AddNamespace("s", 'http://schemas.microsoft.com/winfx/2006/xaml/presentation'); + foreach($rsdNode in ($xaml.SelectNodes("//s:ResourceDictionary[@Source]", $nsm))) + { + $rsdNode.Source = (Join-Path ($PSScriptRoot) ($rsdNode.Source)).ToString() + } + + # Add Styles + foreach($node in $styles.DocumentElement.ChildNodes) + { + $tmpNode = $xaml.CreateElement("Temp") + $tmpNode.InnerXml = $node.OuterXml + $xaml.Window.'Window.Resources'.ResourceDictionary.AppendChild($tmpNode.Style) | Out-Null + } + $global:window = [Windows.Markup.XamlReader]::Load((New-Object System.Xml.XmlNodeReader $xaml)) + } + catch + { + Write-LogError "Failed to initialize main window" $_.Exception + return + } + + # ToDo: Convert to a list for data binding + Add-XamlEvent $window "mnuSettings" "Add_Click" -scriptBlock ([scriptblock]{ Show-SettingsForm }) + Add-XamlEvent $window "mnuTenantSettings" "Add_Click" -scriptBlock ([scriptblock]{ Show-SettingsForm -Tenant }) + Add-XamlEvent $window "mnuUpdates" "Add_Click" -scriptBlock ([scriptblock]{ Show-UpdatesDialog }) + Add-XamlEvent $window "mnuAbout" "Add_Click" -scriptBlock ([scriptblock]{ Show-AboutDialog }) + Add-XamlEvent $window "mnuExit" "Add_Click" -scriptBlock ([scriptblock]{ + if([System.Windows.MessageBox]::Show("Are you sure you want to exit?", "Exit?", "YesNo", "Question") -eq "Yes") + { + $window.Close() + } + } + ) + + Add-XamlVariables $xaml $window + + $lstMenuItems.Add_SelectionChanged({ + if($global:currentViewObject.ViewInfo.ItemChanged) + { + & $global:currentViewObject.ViewInfo.ItemChanged + } + }) + + $global:grdPopup.add_MouseLeftButtonDown( { Hide-Popup } ) + + # ToDo: !!! Intune should not be default icon... + $iconFile = "$($global:AppRootFolder)\Intune.ico" + if([io.File]::Exists($iconFile)) + { + $Window.Icon = $iconFile + } + + $window.Add_Closed({ + }) + + $window.add_Loaded({ + $global:SplashScreen.Hide() + $global:window.Activate() + [System.Windows.Forms.Application]::DoEvents() + #$global:window.Topmost = $true + #$global:window.Topmost = $false + #$global:window.Focus() + + $global:MainAppStarted = $true + + if($global:FirstTimeRunning) + { + $script:welcomeForm = Get-XamlObject ($global:AppRootFolder + "\Xaml\Welcome.xaml") -AddVariables + + Add-XamlEvent $script:welcomeForm "gitHubLink" "Add_RequestNavigate" ({ Start-Process $_.Uri.AbsoluteUri; $_.Handled = $true }) + Add-XamlEvent $script:welcomeForm "licenseLink" "Add_RequestNavigate" ({ Start-Process $_.Uri.AbsoluteUri; $_.Handled = $true }) + Add-XamlEvent $script:welcomeForm "addCustomApp" "Add_RequestNavigate" ({ Start-Process $_.Uri.AbsoluteUri; $_.Handled = $true }) + + Add-XamlEvent $script:welcomeForm "chkAcceptConditions" "add_click" { + $global:btnAcceptConditions.IsEnabled = ($this.IsChecked -eq $true) + } + + Add-XamlEvent $script:welcomeForm "btnAcceptConditions" "add_click" { + Save-Setting "" "LicenseAccepted" "True" + Save-Setting "" "FirstTimeRunning" "False" + Save-Setting "" "AppChangeInformed" "true" + Show-ModalObject + + if($global:currentViewObject.ViewInfo.Authentication.ShowErrors) + { + & $global:currentViewObject.ViewInfo.Authentication.ShowErrors + } + } + + Add-XamlEvent $script:welcomeForm "btnCancel" "add_click" { + if([System.Windows.MessageBox]::Show("Conditions not accepted`n`nDo you want to close the application?", "Close App?", "YesNo", "Warning") -eq "Yes") + { + $window.Close() + } + } + + Show-ModalForm $window.Title $script:welcomeForm -HideButtons + } + else + { + if($global:informOldAzureApp -eq $true) + { + $appIdChangeInformed = Get-Setting "" "AppChangeInformed" "false" + if($appIdChangeInformed -ne "true") { + $script:oldAzureAppForm = Get-XamlObject ($global:AppRootFolder + "\Xaml\OldAzureApp.xaml") + + Add-XamlEvent $script:oldAzureAppForm "addCustomApp" "Add_RequestNavigate" ({ Start-Process $_.Uri.AbsoluteUri; $_.Handled = $true }) + + Add-XamlEvent $script:oldAzureAppForm "btnOK" "add_click" { + if((Get-XamlProperty $script:oldAzureAppForm "chkChangeApp" "IsChecked") -eq $true) { + Write-Log "Set default app ID to $($global:DefaultAzureApp)" + Save-Setting "EndpointManager" "EMAzureApp" $global:DefaultAzureApp + $script:azureAppChanged = $true + } + + if((Get-XamlProperty $script:oldAzureAppForm "chkSkippMessage" "IsChecked") -eq $true) { + Save-Setting "" "AppChangeInformed" "true" + } + Show-ModalObject + if($script:azureAppChanged -eq $true -and $global:currentViewObject) { + [System.Windows.Forms.Application]::DoEvents() + & $global:currentViewObject.ViewInfo.Authenticate + } + } + + Show-ModalForm $window.Title $script:oldAzureAppForm -HideButtons + } + } + + ###!!! Force login here + if($global:currentViewObject.ViewInfo.Authenticate) + { + # Skip for now...need additional code to skip previous login and force this based on setting. + #!!!& $global:currentViewObject.ViewInfo.Authenticate -Params (@{"Interactve"=$true}) + } + } + }) + + foreach($view in $global:viewObjects) + { + $subItem = [System.Windows.Controls.MenuItem]::new() + $subItem.Header = $view.ViewInfo.Title + $subItem.Tag = $view.ViewInfo.Id + $subItem.Add_Click({ + if($this.Tag) + { + Show-View $this.Tag + } + }) + $global:mnuViews.AddChild($subItem) | Out-Null + } + +} + +#endregion + +#region Module functions +function Invoke-ModuleFunction +{ + param($function, $arguments = $null) + + Write-Log "Trigger function $function" + + $params = @{} + if($arguments) + { + $params.Add("ArgumentList",$arguments) + } + foreach($module in $global:loadedModules) + { + # Get command with ExportedFunctions instead of Get-Command + $cmd = $module.ExportedFunctions[$function] + if($cmd) + { + Write-Log "Trigger $function in $($module.Name)" + Invoke-Command -ScriptBlock $cmd.ScriptBlock @params + } + else + { + #Write-Log "$function not found in $($module.Name)" 2 + } + } +} + +#endregion + +#region JWTToken + +### See JWT token documentation for more info: https://tools.ietf.org/html/rfc7519 +### AccessToken documentation https://docs.microsoft.com/en-us/azure/active-directory/develop/access-tokens +function Get-JWTtoken +{ + param($token) + + if(-not $token) { return } + + if(-not $token.StartsWith("eyJ")) + { + Write-Log "Invalid JWT token" 3; return + } + + # First part is the header. Second part is the payload. Third part is the signature + $arr = $token.Split(".") + + if($arr.Count -lt 2) { Write-Log "Invalid token" 3; return } + + $header = $arr[0].Replace('-', '+').Replace('_', '/') # change base64url to base64 + while ($header.Length % 4) { $header += "=" } # Add padding to match required length + + $payload = $arr[1].Replace('-', '+').Replace('_', '/') # change base64url to base64 + while ($payload.Length % 4) { $payload += "=" } # Add padding to match required length + + return (New-Object PSObject -Property @{ + Header=(([System.Text.Encoding]::UTF8.GetString(([System.Convert]::FromBase64String($header)))) | ConvertFrom-Json) + Payload=(([System.Text.Encoding]::UTF8.GetString(([System.Convert]::FromBase64String($payload)))) | ConvertFrom-Json) + }) +} +#endregion + +function Add-GridObject +{ + param($grid, $obj) + + $rd = [System.Windows.Controls.RowDefinition]::new() + $rd.Height = [double]::NaN + $obj.SetValue([System.Windows.Controls.Grid]::RowProperty,$grid.RowDefinitions.Count) | Out-Null + $grid.RowDefinitions.Add($rd) | Out-Null + $grid.Children.Add($obj) | Out-Null +} + +function Get-IsAdmin +{ + (New-Object Security.Principal.WindowsPrincipal ([Security.Principal.WindowsIdentity]::GetCurrent())).IsInRole([Security.Principal.WindowsBuiltinRole]::Administrator) +} + +function Get-NumericUpDownControl +{ + param($id, [decimal]$minValue = 0, [decimal]$maxValue = 9999, [int]$step = 1) + + try + { + [xml]$xaml = Get-Content ($global:AppRootFolder + "\Xaml\NumericUpDown.xaml") + $xamlObj = ([Windows.Markup.XamlReader]::Load((New-Object System.Xml.XmlNodeReader $xaml))) + + $xamlObj.Name = $id + $xamlObj.Children[0].Name = $id + "_TextBox" + $xamlObj.Children[1].Name = $id + "_UpButton" + $xamlObj.Children[1].Name = $id + "_DownButton" + + $settings = [PSCustomObject]@{ + MinValue = $minValue + MaxValue = $maxValue + Step = $step + _lastKnownValue = $null + } + + $xamlObj | Add-Member -MemberType NoteProperty -Name "Settings" -Value $settings + + $xamlObj.Children[0].Add_TextChanged({ + $val = $null + if([decimal]::TryParse($this.Parent.Children[0].Text, [ref]$val)) + { + $this.Parent.Settings._lastKnownValue = $val; + } + }) + + $xamlObj.Children[0].Add_LostFocus({ + $val = $null + if([decimal]::TryParse($this.Parent.Children[0].Text, [ref]$val)) + { + ; + } + elseif($this.Parent.Settings._lastKnownValue) + { + $val = $this.Parent.Settings._lastKnownValue + } + + if($val -ne $null) + { + if($val -gt $this.Parent.Settings.MaxValue) + { + $val = $this.Parent.Settings.MaxValue + } + elseif($val -lt $this.Parent.Settings.MinValue) + { + $val = $this.Parent.Settings.MinValue + } + $this.Parent.Children[0].Text = $val.ToString() + } + }) + + $xamlObj.Children[1].Add_Click({ + $val = $null + if([decimal]::TryParse($this.Parent.Children[0].Text, [ref]$val)) + { + $val = $val + $this.Parent.Settings.Step + if($val -gt $this.Parent.Settings.MaxValue) + { + $val = $this.Parent.Settings.MaxValue + } + $this.Parent.Children[0].Text = $val.ToString() + } + }) + + $xamlObj.Children[2].Add_Click({ + $val = $null + if([decimal]::TryParse($this.Parent.Children[0].Text, [ref]$val)) + { + $val = $val - $this.Parent.Settings.Step + if($val -lt $this.Parent.Settings.MinValue) + { + $val = $this.Parent.Settings.MinValue + } + $this.Parent.Children[0].Text = $val.ToString() + } + }) + + return $xamlObj + + } + catch + { + Write-LogError "Failed to create NumericUpDown control" $_.Exception + return $null + } + +} + +function Format-XML +{ + param([xml]$xml, $indent = 2) + + if(-not $xml) { return } + + #From: https://devblogs.microsoft.com/powershell/format-xml/ + $StringWriter = New-Object System.IO.StringWriter + $XmlWriter = New-Object System.XMl.XmlTextWriter $StringWriter + $xmlWriter.Formatting = "indented" + $xmlWriter.Indentation = $Indent + $xml.WriteContentTo($XmlWriter) + $XmlWriter.Flush() + $StringWriter.Flush() + $StringWriter.ToString() +} + +function Update-XmlFormatting { + [CmdletBinding()] + param( + [Parameter(Mandatory, ValueFromPipeline)] + [string]$Xml + ) + process { + + $Xml = $Xml -replace '<([^\s/>]+)([^>]*)\s/>' , '<$1$2/>' + + $Xml = ($Xml -split "`r?`n") | + Where-Object { $_.Trim().Length -gt 0 } | + ForEach-Object { $_ } | + Out-String + + $Xml = $Xml -replace "`r`n", "`n" + + return $Xml.Trim() + } +} + +function Show-LogView +{ + if($script:LogViewObject -and -not $script:LogViewObject.ViewPanel) + { + $viewPanel = Get-XamlObject ($global:AppRootFolder + "\Xaml\LogInfo.xaml") + + if(-not $viewPanel) { return } + + $script:LogViewObject.ViewPanel = $viewPanel + + Set-XamlProperty $viewPanel "dgLogInfo" "ItemsSource" $script:LogItems + + Add-XamlEvent $viewPanel "dgLogInfo" "add_selectionChanged" ({ + $obj = $this.Parent.FindName("txtLogInfo") + if($obj) + { + $obj.Parent.DataContext = $this.SelectedValue + } + }) + } +} + +function Get-Base64ScriptContent +{ + param($encodeContent, [switch]$RemoveSignature) + + if(-not $encodeContent) { return } + + try + { + $scriptContent = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($encodeContent)) + + if($RemoveSignature -eq $true) + { + $x = $scriptContent.IndexOf("# SIG # Begin signature block") + if($x -gt 0) + { + $scriptContent = $scriptContent.SubString(0,$x) + $scriptContent = $scriptContent + "# SIG # Begin signature block`nSignature data excluded..." + } + } + + $scriptContent + } + catch + { + + } +} + +function Get-ProxyURI +{ + if($null -eq $script:proxyURI) + { + $script:proxyUri = Get-SettingValue "ProxyURI" + } + + if($null -eq $script:proxyURI) + { + $script:proxyUri = "" + } + return $script:proxyURI +} + +function Start-DownloadFile +{ + param($sourceURL, $targetFile) + + Write-Log "Download file from $sourceURL" + if(-not $sourceURL) + { + return + } + + if(-not $targetFile) + { + Write-Log "Target file is missing" + return + } + + [void][System.Reflection.Assembly]::LoadWithPartialName("System.Web.Extensions") + $wc = New-Object System.Net.WebClient + $wc.Encoding = [System.Text.Encoding]::UTF8 + $proxyURI = Get-ProxyURI + if($proxyURI) + { + $wc.Proxy = [System.Net.WebProxy]::new($proxyURI) + } + + try + { + $title = $sourceURL.Split("/")[-1] + $title = $title.Split("/")[0] + } + catch + { + $title = $sourceURL + } + + try + { + Write-Status "Download file: `n$title" + $wc.DownloadFile($sourceURL, $targetFile) + Write-Log "File downloaded to $targetFile" + } + catch + { + Write-LogError "Failed to download file" $_.Exception + } + finally + { + $wc.Dispose() + } +} + +function Get-ASCIIBytes +{ + param($String) + + $bytes = [System.Text.Encoding]::ASCII.GetBytes($String) + + if ($bytes[0] -eq 0x2b -and $bytes[1] -eq 0x2f -and $bytes[2] -eq 0x76) + { [Text.Encoding]::UTF7.GetBytes($String) } + elseif ($bytes[0] -eq 0xff -and $bytes[1] -eq 0xfe) + { [Text.Encoding]::Unicode.GetBytes($String) } + elseif ($bytes[0] -eq 0xfe -and $bytes[1] -eq 0xff) + { [Text.Encoding]::BigEndianUnicode.GetBytes($String) } + elseif ($bytes[0] -eq 0x00 -and $bytes[1] -eq 0x00 -and $bytes[2] -eq 0xfe -and $bytes[3] -eq 0xff) + { [Text.Encoding]::UTF32.GetBytes($String) } + elseif ($bytes[0] -eq 0xef -and $bytes[1] -eq 0xbb -and $bytes[2] -eq 0xbf) + { [Text.Encoding]::UTF8.GetBytes($String) } + + $bytes +} + +function Get-DataGridValues +{ + param($dataGrid) + + $dgColumns = $dataGrid.Columns + + $properties = @() + + foreach($tmpCol in $dgColumns) + { + if(-not $tmpCol.Binding.Path.Path) { continue } + $propName = $tmpCol.Binding.Path.Path + $properties += @{n=$tmpCol.Header;e=([Scriptblock]::Create("`$_.$propName"))} + } + + ($dataGrid.ItemsSource | Select -Property $properties) +} + +function Get-GUIDs +{ + param($text) + + $regExpGuid = "[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}" + + $uniqueGuids = New-Object System.Collections.Generic.HashSet[String] + + # Use regular expressions to extract the GUIDs + [regex]::Matches($text, $regExpGuid) | ForEach-Object { $uniqueGuids.Add($_.Value) | Out-Null } + + $uniqueGuids +} + +New-Alias -Name ?? -value Invoke-Coalesce +New-Alias -Name ?: -value Invoke-IfTrue Export-ModuleMember -alias * -function * \ No newline at end of file diff --git a/Extensions/EndpointManager.psm1 b/Extensions/EndpointManager.psm1 index 46b2536..fee121a 100644 --- a/Extensions/EndpointManager.psm1 +++ b/Extensions/EndpointManager.psm1 @@ -1,4590 +1,4590 @@ -<# -.SYNOPSIS -Module for managing Intune objects - -.DESCRIPTION -This module is for the Endpoint Manager/Intune View. It manages Export/Import/Copy of Intune objects - -.NOTES - Author: Mikael Karlsson -#> -function Get-ModuleVersion -{ - '3.10.0.6' -} - -function Invoke-InitializeModule -{ - #Add settings - $global:appSettingSections += (New-Object PSObject -Property @{ - Title = "Endpoint Manager/Intune" - Id = "EndpointManager" - Values = @() - Priority = 10 - }) - - Add-SettingsObject (New-Object PSObject -Property @{ - Title = "Application" - Key = "EMAzureApp" - Type = "List" - SelectedValuePath = "ClientId" - ItemsSource = $global:MSGraphGlobalApps - DefaultValue = "" - SubPath = "EndpointManager" - }) "EndpointManager" - - Add-SettingsObject (New-Object PSObject -Property @{ - Title = "Application Id" - Key = "EMCustomAppId" - Type = "String" - DefaultValue = "" - SubPath = "EndpointManager" - }) "EndpointManager" - - Add-SettingsObject (New-Object PSObject -Property @{ - Title = "Redirect URL" - Key = "EMCustomAppRedirect" - Type = "String" - DefaultValue = "" - SubPath = "EndpointManager" - }) "EndpointManager" - - Add-SettingsObject (New-Object PSObject -Property @{ - Title = "Tenant Id" - Key = "EMCustomTenantId" - Type = "String" - DefaultValue = "" - SubPath = "EndpointManager" - }) "EndpointManager" - - Add-SettingsObject (New-Object PSObject -Property @{ - Title = "Authority" - Key = "EMCustomAuthority" - Type = "String" - DefaultValue = "" - SubPath = "EndpointManager" - }) "EndpointManager" - - Add-SettingsObject (New-Object PSObject -Property @{ - Title = "App packages folder" - Key = "EMIntuneAppPackages" - Type = "Folder" - Description = "Root folder where intune app packages are located" - SubPath = "EndpointManager" - }) "EndpointManager" - - Add-SettingsObject (New-Object PSObject -Property @{ - Title = "Save Encryption File" - Key = "EMSaveEncryptionFile" - Type = "Boolean" - Description = "Save encryption file when uploading an app. This can then be used to when downloading the app file." - SubPath = "EndpointManager" - }) "EndpointManager" - - Add-SettingsObject (New-Object PSObject -Property @{ - Title = "App download folder" - Key = "EMIntuneAppDownloadFolder" - Type = "Folder" - Description = "Folder where app packages will be downloaded and where encryption files will be saved" - SubPath = "EndpointManager" - }) "EndpointManager" - - Get-SettingValue "ProxyURI" - - if($global:FirstTimeRunning) { - Save-Setting "EndpointManager" "EMAzureApp" $global:DefaultAzureApp - } - - $currentAppID = Get-SettingValue "EMAzureApp" - $customAppID = Get-SettingValue "EMCustomAppId" - $global:informOldAzureApp = $false - - if(($global:OldAzureApps -is [Array] -and $currentAppID -in $global:OldAzureApps) -or (-not $currentAppID -and -not $customAppID)) - { - $global:informOldAzureApp = $true - 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 - - #Add menu group and items - $global:EMViewObject = (New-Object PSObject -Property @{ - Title = "Intune Manager" - Description = "Manages Intune environments. This view can be used for copying objects in an Intune environment. It can also be used for backing up an entire Intune environment and cloning the Intune environment into another tenant." - ID="IntuneGraphAPI" - ViewPanel = $viewPanel - AuthenticationID = "MSAL" - ItemChanged = { Show-GraphObjects -ObjectTypeChanged; Invoke-ModuleFunction "Invoke-GraphObjectsChanged"; Write-Status ""} - Deactivating = { Invoke-EMDeactivateView } - Activating = { Invoke-EMActivatingView } - Authentication = (Get-MSALAuthenticationObject) - Authenticate = { Invoke-EMAuthenticateToMSAL @args } - AppInfo = (Get-GraphAppInfo "EMAzureApp" $global:DefaultAzureApp "EM") - SaveSettings = { Invoke-EMSaveSettings } - - Permissions = @() - }) - - Add-ViewObject $global:EMViewObject - - Add-ViewItem (New-Object PSObject -Property @{ - Title = "Device Configuration" - Id = "DeviceConfiguration" - ViewID = "IntuneGraphAPI" - API = "/deviceManagement/deviceConfigurations" - QUERYLIST = "`$filter=not%20isof(%27microsoft.graph.windowsUpdateForBusinessConfiguration%27)%20and%20not%20isof(%27microsoft.graph.iosUpdateConfiguration%27)" - #ExportFullObject = $false - Permissons=@("DeviceManagementConfiguration.ReadWrite.All") - PropertiesToRemove = @("privacyAccessControls") - PostFileImportCommand = { Start-PostFileImportDeviceConfiguration @args } - PostCopyCommand = { Start-PostCopyDeviceConfiguration @args } - PostGetCommand = { Start-PostGetDeviceConfiguration @args } - GroupId = "DeviceConfiguration" - NavigationProperties=$true - }) - - Add-ViewItem (New-Object PSObject -Property @{ - Title = "Conditional Access" - Id = "ConditionalAccess" - ViewID = "IntuneGraphAPI" - API = "/identity/conditionalAccess/policies" - Permissons=@("Policy.Read.All","Policy.ReadWrite.ConditionalAccess","Application.Read.All") - Dependencies = @("NamedLocations","Applications","TermsOfUse","AuthenticationStrengths","AssignmentFilters") - GroupId = "ConditionalAccess" - ImportExtension = { Add-ConditionalAccessImportExtensions @args } - PreImportCommand = { Start-PreImportConditionalAccess @args } - PostExportCommand = { Start-PostExportConditionalAccess @args } - ExpandAssignmentsList = $false - SupportsPageSize = $false - }) - - Add-ViewItem (New-Object PSObject -Property @{ - Title = "Terms of use" - Id = "TermsOfUse" - ViewID = "IntuneGraphAPI" - ViewProperties = @("id", "displayName") - Expand = "files" - QUERYLIST = "`$expand=files" - API = "/identityGovernance/termsOfUse/agreements" - Permissons=@("Agreement.ReadWrite.All") - PreImportCommand = { Start-PreImportTermsOfUse @args } - PostExportCommand = { Start-PostExportTermsOfUse @args } - GroupId = "ConditionalAccess" - SupportsPageSize = $false - }) - - Add-ViewItem (New-Object PSObject -Property @{ - Title = "Named Locations" - Id = "NamedLocations" - ViewID = "IntuneGraphAPI" - API = "/identity/conditionalAccess/namedLocations" - Permissons=@("Policy.ReadWrite.ConditionalAccess") - ImportOrder = 50 - GroupId = "ConditionalAccess" - ExpandAssignmentsList = $false - SupportsPageSize = $false - }) - - Add-ViewItem (New-Object PSObject -Property @{ - Title = "Endpoint Security" - Id = "EndpointSecurity" - ViewID = "IntuneGraphAPI" - API = "/deviceManagement/intents" - PropertiesToRemove = @('Settings','@OData.Type') - PreImportCommand = { Start-PreImportEndpointSecurity @args } - PostListCommand = { Start-PostListEndpointSecurity @args } - PostExportCommand = { Start-PostExportEndpointSecurity @args } - PostFileImportCommand = { Start-PostFileImportEndpointSecurity @args } - PostGetCommand = { Start-PostGetEndpointSecurity @args } - #PreCopyCommand = { Start-PreCopyEndpointSecurity @args } - PostCopyCommand = { Start-PostCopyEndpointSecurity @args } - PreUpdateCommand = { Start-PreUpdateEndpointSecurity @args } - Permissons=@("DeviceManagementConfiguration.ReadWrite.All") - Dependencies = @("ReusableSettings") - GroupId = "EndpointSecurity" - SupportsPageSize = $false - }) - - Add-ViewItem (New-Object PSObject -Property @{ - Title = "Compliance Policies" - Id = "CompliancePolicies" - ViewID = "IntuneGraphAPI" - Expand = "scheduledActionsForRule(`$expand=scheduledActionConfigurations)" - API = "/deviceManagement/deviceCompliancePolicies" - Permissons=@("DeviceManagementConfiguration.ReadWrite.All") - Dependencies = @("Locations","Notifications","ComplianceScripts") - PostExportCommand = { Start-PostExportCompliancePolicies @args } - PreUpdateCommand = { Start-PreUpdateCompliancePolicies @args } - GroupId = "CompliancePolicies" - SupportsPageSize = $false - }) - - Add-ViewItem (New-Object PSObject -Property @{ - Title = "Compliance Policies - V2" - Id = "CompliancePoliciesV2" - ViewID = "IntuneGraphAPI" - API = "/deviceManagement/compliancePolicies" - NameProperty = "name" - PropertiesToRemove = @('settingCount') - ViewProperties = @("name","description","Id") - Expand="settings" - Permissons=@("DeviceManagementConfiguration.ReadWrite.All") - GroupId = "CompliancePolicies" - Icon = "CompliancePolicies" - }) - - - Add-ViewItem (New-Object PSObject -Property @{ - Title = "Compliance Scripts" - Id = "ComplianceScripts" - ViewID = "IntuneGraphAPI" - API = "/deviceManagement/deviceComplianceScripts" - PostImportCommand = { Start-PostImportComplianceScripts @args } - Permissons=@("DeviceManagementScripts.ReadWrite.All") - GroupId = "CompliancePolicies" - Icon = "Scripts" - ImportOrder = 80 - }) - - Add-ViewItem (New-Object PSObject -Property @{ - Title = "Intune Branding" - Id = "IntuneBranding" - API = "/deviceManagement/intuneBrandingProfiles" - ViewID = "IntuneGraphAPI" - NameProperty = "profileName" - ViewProperties = @("profileName", "displayName", "description", "id","isDefaultProfile") - PreImportCommand = { Start-PreImportIntuneBranding @args } - PostImportCommand = { Start-PostImportIntuneBranding @args } - PostGetCommand = { Start-PostGetIntuneBranding @args } - PostExportCommand = { Start-PostExportIntuneBranding @args } - PreDeleteCommand = { Start-PreDeleteIntuneBranding @args } - PreUpdateCommand = { Start-PreUpdateIntuneBranding @args } - Permissons=@("DeviceManagementApps.ReadWrite.All") - Icon = "Branding" - SkipRemoveProperties = @('Id') # Id is removed by PreImport. Required for default profile - PropertiesToRemoveForUpdate = @('isDefaultProfile','disableClientTelemetry') - GroupId = "TenantAdmin" - SupportsPageSize = $false - }) - - <# - # BUG in Graph? Cannot create default branding. Can only create it when importing another object - # Header required Accept-Language: sv-SE - # Documentation says to use Content-Language but that doesn't work - - # Could work with https://main.iam.ad.ext.azure.com/api/LoginTenantBrandings - - #> - - Add-ViewItem (New-Object PSObject -Property @{ - Title = "Azure Branding" - Id = "AzureBranding" - API = "/organization/%OrganizationId%/branding/localizations" - ViewID = "IntuneGraphAPI" - ViewProperties = @("Id") - PreImportCommand = { Start-PreImportAzureBranding @args } - PostListCommand = { Start-PostListAzureBranding @args } - ShowButtons = @("Export","View") - NameProperty = "Id" - Permissons=@("Organization.ReadWrite.All") - Icon = "Branding" - SkipRemoveProperties = @('Id') - GroupId = "Azure" - SkipAddIDOnExport = $true - ExpandAssignmentsList = $false - SupportsPageSize = $false - }) - - Add-ViewItem (New-Object PSObject -Property @{ - Title = "Enrollment Status Page" - Id = "EnrollmentStatusPage" - API = "/deviceManagement/deviceEnrollmentConfigurations" - ViewID = "IntuneGraphAPI" - PreImportCommand = { Start-PreImportESP @args } - PostExportCommand = { Start-PostExportESP @args } - PreDeleteCommand = { Start-PreDeleteEnrollmentRestrictions @args } # Note: Uses same PreDelete as restrictions - PreReplaceCommand = { Start-PreReplaceEnrollmentRestrictions @args } # Note: Uses same PreReplaceCommand as restrictions - PostReplaceCommand = { Start-PostReplaceEnrollmentRestrictions @args } # Note: Uses same PostReplaceCommand as restrictions - PreFilesImportCommand = { Start-PreFilesImportEnrollmentRestrictions @args } # Note: Uses same PreFilesImportCommand as restrictions - PreImportAssignmentsCommand = { Start-PreImportAssignmentsEnrollmentRestrictions @args } # Note: Uses same PreFilesImportCommand as restrictions - PostListCommand = { Start-PostListESP @args } - #PreUpdateCommand = { Start-PreUpdateEnrollmentRestrictions @args } # Note: Uses same PreUpdateCommand as restrictions - #QUERYLIST = "`$filter=endsWith(id,'Windows10EnrollmentCompletionPageConfiguration')" - Permissons=@("DeviceManagementServiceConfig.ReadWrite.All") - SkipRemoveProperties = @('Id') - Dependencies = @("Applications") - AssignmentsType = "enrollmentConfigurationAssignments" - PropertiesToRemoveForUpdate = @('priority') - GroupId = "WinEnrollment" - SupportsPageSize = $false - }) - - Add-ViewItem (New-Object PSObject -Property @{ - Title = "Enrollment Restrictions" - Id = "EnrollmentRestrictions" - API = "/deviceManagement/deviceEnrollmentConfigurations" - ViewID = "IntuneGraphAPI" - #QUERYLIST = "`$filter=not endsWith(id,'Windows10EnrollmentCompletionPageConfiguration')" - PostExportCommand = { Start-PostExportEnrollmentRestrictions @args } - PreImportCommand = { Start-PreImportEnrollmentRestrictions @args } - PreDeleteCommand = { Start-PreDeleteEnrollmentRestrictions @args } - PreReplaceCommand = { Start-PreReplaceEnrollmentRestrictions @args } - PostReplaceCommand = { Start-PostReplaceEnrollmentRestrictions @args } - PreFilesImportCommand = { Start-PreFilesImportEnrollmentRestrictions @args } - PostListCommand = { Start-PostListEnrollmentRestrictions @args } - PreImportAssignmentsCommand = { Start-PreImportAssignmentsEnrollmentRestrictions @args } - #PreUpdateCommand = { Start-PreUpdateEnrollmentRestrictions @args } - PropertiesToRemoveForUpdate = @('priority') - Permissons=@("DeviceManagementServiceConfig.ReadWrite.All") - SkipRemoveProperties = @('Id') - AssignmentsType = "enrollmentConfigurationAssignments" - GroupId = "EnrollmentRestrictions" - ViewProperties = @("displayName","platformType","description","Id") - SupportsPageSize = $false - }) - - Add-ViewItem (New-Object PSObject -Property @{ - Title = "Co-Management Settings" - Id = "CoManagementSettings" - ViewID = "IntuneGraphAPI" - API = "/deviceManagement/deviceEnrollmentConfigurations" - PostReplaceCommand = { Start-PostReplaceEnrollmentRestrictions @args } # Note: Uses same PostReplaceCommand as restrictions - PreFilesImportCommand = { Start-PreFilesImportEnrollmentRestrictions @args } # Note: Uses same PreFilesImportCommand as restrictions - PostListCommand = { Start-PostListCoManagementSettings @args } - PropertiesToRemoveForUpdate = @('priority') - Permissons=@("DeviceManagementServiceConfig.ReadWrite.All") - SkipRemoveProperties = @('Id') - GroupId = "WinEnrollment" - Icon = "EnrollmentStatusPage" - SupportsPageSize = $false - }) - - Add-ViewItem (New-Object PSObject -Property @{ - Title = "Administrative Templates" - Id = "AdministrativeTemplates" - API = "/deviceManagement/groupPolicyConfigurations" - ViewID = "IntuneGraphAPI" - PostGetCommand = { Start-PostGetAdministrativeTemplate @args } - PostExportCommand = { Start-PostExportAdministrativeTemplate @args } - PostCopyCommand = { Start-PostCopyAdministrativeTemplate @args } - PostFileImportCommand = { Start-PostFileImportAdministrativeTemplate @args } - PreImportCommand = { Start-PreImportAdministrativeTemplate @args } - LoadObject = { Start-LoadAdministrativeTemplate @args } - PropertiesToRemove = @("definitionValues","policyConfigurationIngestionType") - Permissons=@("DeviceManagementConfiguration.ReadWrite.All") - Icon="DeviceConfiguration" - GroupId = "DeviceConfiguration" - CompareValue = "CombinedValueWithLabel" - Dependencies = @("ADMXFiles") - }) - - Add-ViewItem (New-Object PSObject -Property @{ - Title = "Scripts (PowerShell)" - Id = "PowerShellScripts" - API = "/deviceManagement/deviceManagementScripts" - ViewID = "IntuneGraphAPI" - DetailExtension = { Add-ScriptExtensions @args } - ExportExtension = { Add-ScriptExportExtensions @args } - PostExportCommand = { Start-PostExportScripts @args } - Permissons=@("DeviceManagementScripts.ReadWrite.All") - AssignmentsType = "deviceManagementScriptAssignments" - Icon="Scripts" - GroupId = "Scripts" - }) - - Add-ViewItem (New-Object PSObject -Property @{ - Title = "Scripts (Shell)" - Id = "MacScripts" - API = "/deviceManagement/deviceShellScripts" - ViewID = "IntuneGraphAPI" - DetailExtension = { Add-ScriptExtensions @args } - ExportExtension = { Add-ScriptExportExtensions @args } - PostExportCommand = { Start-PostExportScripts @args } - Permissons=@("DeviceManagementScripts.ReadWrite.All") - AssignmentsType = "deviceManagementScriptAssignments" - Icon="Scripts" - GroupId = "Scripts" - }) - - Add-ViewItem (New-Object PSObject -Property @{ - Title = "Custom Attributes" - Id = "MacCustomAttributes" - API = "/deviceManagement/deviceCustomAttributeShellScripts" - ViewID = "IntuneGraphAPI" - Permissons=@("DeviceManagementScripts.ReadWrite.All") - AssignmentsType = "deviceManagementScriptAssignments" - Icon="CustomAttributes" - GroupId = "CustomAttributes" # MacOS Settings - DetailExtension = { Add-ScriptExtensions @args } - ExportExtension = { Add-ScriptExportExtensions @args } - PostExportCommand = { Start-PostExportScripts @args } - PropertiesToRemoveForUpdate = @('customAttributeName','customAttributeType','displayName') - #PreUpdateCommand = { Start-PreUpdateMacCustomAttributes @args } - }) - - Add-ViewItem (New-Object PSObject -Property @{ - Title = "Terms and Conditions" - Id = "TermsAndConditions" - API = "/deviceManagement/termsAndConditions" - ViewID = "IntuneGraphAPI" - Permissons=@("DeviceManagementServiceConfig.ReadWrite.All") - ExpandAssignments = $false # Not supported for this object type - PostExportCommand = { Start-PostExportTermsAndConditions @args } - PreImportAssignmentsCommand = { Start-PreImportAssignmentsTermsAndConditions @args } - GroupId = "TenantAdmin" - ExpandAssignmentsList = $false - SupportsPageSize = $false - }) - - Add-ViewItem (New-Object PSObject -Property @{ - Title = "App Protection" - Id = "AppProtection" - API = "/deviceAppManagement/managedAppPolicies" - ViewID = "IntuneGraphAPI" - PreGetCommand = { Start-GetAppProtection @args } - PostListCommand = { Start-PostListAppProtection @args } - PreImportCommand = { Start-PreImportAppProtection @args } - PostImportCommand = { Start-PostImportAppProtection @args } - PreImportAssignmentsCommand = { Start-PreImportAssignmentsAppProtection @args } - PreUpdateCommand = { Start-PreUpdateAppProtection @args } - ExportFullObject = $true - PropertiesToRemove = @('exemptAppLockerFiles') - PropertiesToRemoveForUpdate = @("protectedAppLockerFiles","version") # ToDo: !!! Add support for protectedAppLockerFiles? - Permissons=@("DeviceManagementApps.ReadWrite.All") - Dependencies = @("Applications") - GroupId = "AppProtection" - ExpandAssignmentsList = $false - SupportsPageSize = $false - }) - - # These are also included in the managedAppPolicies API - # So all custom commands will be handled by the same functions as App Protection - Add-ViewItem (New-Object PSObject -Property @{ - Title = "App Configuration (App)" - Id = "AppConfigurationManagedApp" - API = "/deviceAppManagement/targetedManagedAppConfigurations" - ViewID = "IntuneGraphAPI" - PreGetCommand = { Start-GetAppProtection @args } - PreImportCommand = { Start-PreImportAppProtection @args } - PostImportCommand = { Start-PostImportAppProtection @args } - PreImportAssignmentsCommand = { Start-PreImportAssignmentsAppProtection @args } - PreUpdateCommand = { Start-PreUpdateAppConfigurationApp @args } - Permissons=@("DeviceManagementApps.ReadWrite.All") - Dependencies = @("Applications") - Icon = "AppConfiguration" - GroupId = "AppConfiguration" - ExpandAssignmentsList = $false - SupportsPageSize = $false - }) - - Add-ViewItem (New-Object PSObject -Property @{ - Title = "App Configuration (Device)" - Id = "AppConfigurationManagedDevice" - API = "/deviceAppManagement/mobileAppConfigurations" - QUERYLIST = "`$filter=microsoft.graph.androidManagedStoreAppConfiguration/appSupportsOemConfig%20eq%20false%20or%20isof(%27microsoft.graph.androidManagedStoreAppConfiguration%27)%20eq%20false" - ViewID = "IntuneGraphAPI" - Permissons=@("DeviceManagementApps.ReadWrite.All") - Dependencies = @("Applications") - PreFilesImportCommand = { Start-PreFilesImportAppConfiguration @args } - PreImportAssignmentsCommand = { Start-PreImportAssignmentsAppConfiguration @args } - PostExportCommand = { Start-PostExportAppConfiguration @args } - Icon = "AppConfiguration" - GroupId = "AppConfiguration" - }) - - Add-ViewItem (New-Object PSObject -Property @{ - Title = "Applications" - Id = "Applications" - API = "/deviceAppManagement/mobileApps" - ViewID = "IntuneGraphAPI" - PropertiesToRemove = @('uploadState','publishingState','isAssigned','dependentAppCount','supersedingAppCount','supersededAppCount','committedContentVersion','isFeatured','size','categories') #,'minimumSupportedWindowsRelease' - QUERYLIST = "`$filter=(microsoft.graph.managedApp/appAvailability%20eq%20null%20or%20microsoft.graph.managedApp/appAvailability%20eq%20%27lineOfBusiness%27%20or%20isAssigned%20eq%20true)&`$orderby=displayName" - QuerySearch=$true - Permissons=@("DeviceManagementApps.ReadWrite.All") - AssignmentsType="mobileAppAssignments" - AssignmentProperties = @("@odata.type","target","settings","intent") - AssignmentTargetProperties = @("@odata.type","groupId","deviceAndAppManagementAssignmentFilterId","deviceAndAppManagementAssignmentFilterType") - ImportOrder = 60 - Expand="categories,assignments" # ODataMetadata is set to minimal so assignments can't be autodetected - ODataMetadata="minimal" # categories property not supported with ODataMetadata full - PostFileImportCommand = { Start-PostFileImportApplication @args } - PostCopyCommand = { Start-PostCopyApplication @args } - PreUpdateCommand = { Start-PreUpdateApplication @args } - PreImportCommand = { Start-PreImportCommandApplication @args } - DetailExtension = { Add-DetailExtensionApplications @args } - PreImportAssignmentsCommand = { Start-PreImportAssignmentsApplications @args } - PreDeleteCommand = { Start-PreDeleteApplications @args } - PostExportCommand = { Start-PostExportApplications @args } - PostListCommand = { Start-PostListApplications @args } - ExportExtension = { Add-ScriptExportApplications @args } - PostGetCommand = { Start-PostGetApplications @args } - PostImportCommand = { Start-PostImportApplications @args } - PostFilesImportCommand = { Start-PostFilesImportApplications @args } - GroupId = "Apps" - ScopeTagsReturnedInList = $false - }) - - Add-ViewItem (New-Object PSObject -Property @{ - Title = "Autopilot" - Id = "AutoPilot" - API = "/deviceManagement/windowsAutopilotDeploymentProfiles" - ViewID = "IntuneGraphAPI" - CopyDefaultName = "%displayName% Copy" # '-' is not allowed in the name - Permissons=@("DeviceManagementServiceConfig.ReadWrite.All") - PreImportAssignmentsCommand = { Start-PreImportAssignmentsAutoPilot @args } - PreDeleteCommand = { Start-PreDeleteAutoPilot @args } - PropertiesToRemoveForUpdate = @('managementServiceAppId') - GroupId = "WinEnrollment" - }) - - Add-ViewItem (New-Object PSObject -Property @{ - Title = "Policy Sets" - Id = "PolicySets" - API = "/deviceAppManagement/policySets" - ViewID = "IntuneGraphAPI" - Expand = "Items" - PreImportAssignmentsCommand = { Start-PreImportAssignmentsPolicySets @args } - PreImportCommand = { Start-PreImportPolicySets @args } - PreUpdateCommand = { Start-PreUpdatePolicySets @args } - PostListCommand = { Start-PostListPolicySets @args } - Permissons=@("DeviceManagementConfiguration.ReadWrite.All") - ImportOrder = 2000 # Policy Sets reference other objects so make sure it is imported last - Dependencies = @("Applications","AppConfiguration","AppProtection","AutoPilot","EnrollmentRestrictions","EnrollmentStatusPage","DeviceConfiguration","AdministrativeTemplates","SettingsCatalog","CompliancePolicies") - GroupId = "PolicySets" - ExpandAssignmentsList = $false # expand is not allowed, IsAssigned is set in PostListCommand - }) - - Add-ViewItem (New-Object PSObject -Property @{ - Title = "Update Policies" - Id = "UpdatePolicies" - ViewID = "IntuneGraphAPI" - API = "/deviceManagement/deviceConfigurations" - QUERYLIST = "`$filter=isof(%27microsoft.graph.windowsUpdateForBusinessConfiguration%27)%20or%20isof(%27microsoft.graph.iosUpdateConfiguration%27)" - #ExportFullObject = $false - Permissons=@("DeviceManagementConfiguration.ReadWrite.All") - GroupId = "WinUpdatePolicies" - PropertiesToRemoveForUpdate = @('version','qualityUpdatesPauseStartDate','featureUpdatesPauseStartDate','qualityUpdatesWillBeRolledBack','featureUpdatesWillBeRolledBack') - }) - - Add-ViewItem (New-Object PSObject -Property @{ - Title = "Feature Updates" - Id = "FeatureUpdates" - ViewID = "IntuneGraphAPI" - API = "/deviceManagement/windowsFeatureUpdateProfiles" - Permissons=@("DeviceManagementConfiguration.ReadWrite.All") - GroupId = "WinFeatureUpdates" - PropertiesToRemoveForUpdate = @('deployableContentDisplayName','endOfSupportDate') - #PreUpdateCommand = { Start-PreUpdateFeatureUpdates @args } - }) - - Add-ViewItem (New-Object PSObject -Property @{ - Title = "Quality Updates (Profiles)" - Id = "QualityUpdates" - ViewID = "IntuneGraphAPI" - API = "/deviceManagement/windowsQualityUpdateProfiles" - Permissons=@("DeviceManagementConfiguration.ReadWrite.All") - Icon = "UpdatePolicies" - GroupId = "WinQualityUpdates" - PropertiesToRemoveForUpdate = @('releaseDateDisplayName','deployableContentDisplayName') - SupportsPageSize = $false - }) - - Add-ViewItem (New-Object PSObject -Property @{ - Title = "Quality Updates (Policies)" - Id = "QualityUpdatePolicies" - ViewID = "IntuneGraphAPI" - API = "/deviceManagement/windowsQualityUpdatePolicies" - Permissons=@("DeviceManagementConfiguration.ReadWrite.All") - Icon = "UpdatePolicies" - GroupId = "WinQualityUpdates" - SupportsPageSize = $false - }) - - # Locations are not FULLY supported - # They will be imported but Compliance Policies will not be updated with new Location object after import - # ToDo: Add support Export/Import Location Settings - # Location object - Only used by Android Device Admins Compliance Policies - # - These should probably be migrated to Android Enterprise anyway. That is the recommendation by Google - # Property that needs to be updated on the Compliance Policy - # deviceManagement/managementConditionStatements/$obj.conditionStatementId - - # Location objects support removed from Intune - <# - Add-ViewItem (New-Object PSObject -Property @{ - Title = "Locations" - Id = "Locations" - ViewID = "IntuneGraphAPI" - API = "/deviceManagement/managementConditions" - Permissons=@("DeviceManagementConfiguration.ReadWrite.All") - PreImportCommand = { Start-PreImportLocations @args } - ImportOrder = 30 - GroupId = "CompliancePolicies" - }) - #> - - Add-ViewItem (New-Object PSObject -Property @{ - Title = "Settings Catalog" - Id = "SettingsCatalog" - ViewID = "IntuneGraphAPI" - API = "/deviceManagement/configurationPolicies" - PropertiesToRemove = @('settingCount') - Permissons=@("DeviceManagementConfiguration.ReadWrite.All") - NameProperty = "name" - ViewProperties = @("name","description","Id") - Expand="Settings" - Icon="DeviceConfiguration" - PreImportCommand = { Start-PreImportSettingsCatalog @args } - PostExportCommand = { Start-PostExportSettingsCatalog @args } - PreUpdateCommand = { Start-PreUpdateSettingsCatalog @args } - PostGetCommand = { Start-PostGetSettingsCatalog @args } - Dependencies = @("ReusableSettings") - GroupId = "DeviceConfiguration" - }) - - Add-ViewItem (New-Object PSObject -Property @{ - Title = "Inventory Policies" - Id = "InventoryPolicies" - ViewID = "IntuneGraphAPI" - API = "/deviceManagement/inventoryPolicies" - PropertiesToRemove = @('settingCount') - Permissons=@("DeviceManagementConfiguration.ReadWrite.All") - NameProperty = "name" - ViewProperties = @("name","description","Id") - Expand="Settings" - Icon="DeviceConfiguration" - GroupId = "DeviceConfiguration" - }) - - Add-ViewItem (New-Object PSObject -Property @{ - Title = "BIOS Configurations" - Id = "HardwareConfigurations" - ViewID = "IntuneGraphAPI" - DetailExtension = { Add-PolicyFileExtensions @args } - ExportExtension = { Add-PolicyFileExportExtensions @args } - PostExportCommand = { Start-PostExportPolicyFile @args } - PropertiesToRemoveForUpdate = @('version') - PolicyFileAttribute = "configurationFileContent" - API = "/deviceManagement/hardwareConfigurations" - Permissons=@("DeviceManagementConfiguration.ReadWrite.All") - Icon="DeviceConfiguration" - GroupId = "DeviceConfiguration" - SupportsPageSize = $false - }) - - Add-ViewItem (New-Object PSObject -Property @{ - Title = "Role Definitions" - Id = "RoleDefinitions" - ViewID = "IntuneGraphAPI" - API = "/deviceManagement/roleDefinitions" - QUERYLIST = "`$filter=isBuiltIn%20eq%20false" - PostExportCommand = { Start-PostExportRoleDefinitions @args } - PreImportCommand = { Start-PreImportRoleDefinitions @args } - PostFileImportCommand = { Start-PostFileImportRoleDefinitions @args } - Permissons=@("DeviceManagementRBAC.ReadWrite.All") - ImportOrder = 20 - #expand=roleassignments - PropertiesToRemoveForUpdate = @('isBuiltInRoleDefinition','isBuiltIn','roleAssignments') ### !!! ToDo: Add support for roleAssignments - GroupId = "TenantAdmin" - ExpandAssignments = $false - ExpandAssignmentsList = $false - }) - - Add-ViewItem (New-Object PSObject -Property @{ - Title = "Scope (Tags)" - Id = "ScopeTags" - ViewID = "IntuneGraphAPI" - API = "/deviceManagement/roleScopeTags" - QUERYLIST = "`$filter=isBuiltIn%20eq%20false" - Permissons=@("DeviceManagementRBAC.ReadWrite.All") - PostExportCommand = { Start-PostExportScopeTags @args } - PostGetCommand = { Start-PostGetScopeTags @args } - ImportOrder = 10 - DocumentAll = $true - GroupId = "TenantAdmin" - ExpandAssignmentsList = $false # Adds the assignmnets property but always empty - }) - - Add-ViewItem (New-Object PSObject -Property @{ - Title = "Notifications" - Id = "Notifications" - ViewID = "IntuneGraphAPI" - API = "/deviceManagement/notificationMessageTemplates" - Permissons=@("DeviceManagementServiceConfig.ReadWrite.All") - ImportOrder = 40 - Expand = "localizedNotificationMessages" - PreImportCommand = { Start-PreImportNotifications @args } - PostFileImportCommand = { Start-PostFileImportNotifications @args } - PostCopyCommand = { Start-PostCopyNotifications @args } - PropertiesToRemoveForUpdate = @('defaultLocale','localizedNotificationMessages') ### !!! ToDo: Add support for localizedNotificationMessages - GroupId = "CompliancePolicies" - ExpandAssignmentsList = $false - }) - - # This has some pre-reqs for working! - # Import is tested and verified in a tenant with Googple Play connection configured - # And the OEM app was dpwnloaded e.g. Knox Service Plugin - # Import failed in a tenant where Google Play was NOT configured - Add-ViewItem (New-Object PSObject -Property @{ - Title = "Android OEM Config" - Id = "AndroidOEMConfig" - ViewID = "IntuneGraphAPI" - QUERYLIST = "`$filter=microsoft.graph.androidManagedStoreAppConfiguration/appSupportsOemConfig%20eq%20true" - API = "/deviceAppManagement/mobileAppConfigurations" - PreImportAssignmentsCommand = { Start-PreImportAssignmentsAppConfiguration @args } - PreFilesImportCommand = { Start-PreFilesImportAppConfiguration @args } - PostExportCommand = { Start-PostExportAppConfiguration @args } - Permissons=@("DeviceManagementConfiguration.ReadWrite.All") - Icon="DeviceConfiguration" - Dependencies = @("Applications") - GroupId = "DeviceConfiguration" - }) - - # Copy/Export/Import not verified! - Add-ViewItem (New-Object PSObject -Property @{ - Title = "Apple Enrollment Types" - Id = "AppleEnrollmentTypes" - ViewID = "IntuneGraphAPI" - API = "/deviceManagement/appleUserInitiatedEnrollmentProfiles" - Permissons=@("DeviceManagementServiceConfig.ReadWrite.All") - PropertiesToRemoveForUpdate = @('platform') - GroupId = "AppleEnrollment" - }) - - Add-ViewItem (New-Object PSObject -Property @{ - Title = "Filters" - Id = "AssignmentFilters" - ViewID = "IntuneGraphAPI" - API = "/deviceManagement/assignmentFilters" - Permissons=@("DeviceManagementConfiguration.ReadWrite.All") - ImportOrder = 15 - GroupId = "TenantAdmin" - PropertiesToRemoveForUpdate = @('platform') - ExpandAssignmentsList = $false - PropertiesToRemove = @("payloads") - }) - - Add-ViewItem (New-Object PSObject -Property @{ - Title = "Health Scripts" - Id = "DeviceHealthScripts" - ViewID = "IntuneGraphAPI" - QUERYLIST = "`$filter=isGlobalScript%20eq%20false" # Looks like filters are not working for deviceHealthScripts - API = "/deviceManagement/deviceHealthScripts" - PreDeleteCommand = { Start-PreDeleteDeviceHealthScripts @args } - PreImportCommand = { Start-PreImportDeviceHealthScripts @args } - PreUpdateCommand = { Start-PreUpdateDeviceHealthScripts @args } - PostExportCommand = { Start-PostExportDeviceHealthScripts @args } - ExportExtension = { Add-ScriptExportExtensions @args } - Permissons=@("DeviceManagementScripts.ReadWrite.All") - GroupId = "EndpointAnalytics" - Icon = "Report" - AssignmentsType = "deviceHealthScriptAssignments" - AssignmentProperties = @("target","runSchedule","runRemediationScript") - PropertiesToRemoveForUpdate = @('version','isGlobalScript','highestAvailableVersion') - }) - - Add-ViewItem (New-Object PSObject -Property @{ - Title = "ADMX Files" - Id = "ADMXFiles" - ViewID = "IntuneGraphAPI" - NameProperty = "fileName" - API = "/deviceManagement/groupPolicyUploadedDefinitionFiles" - Permissons=@("DeviceManagementConfiguration.ReadWrite.All") - ImportOrder = 45 - GroupId = "DeviceConfiguration" - Icon = "DeviceConfiguration" - ExpandAssignmentsList = $false - PreFilesImportCommand = { Start-PreFilesImportADMXFiles @args } - PreImportCommand = { Start-PreImportADMXFiles @args } - PostImportCommand = { Start-PostImportADMXFiles @args } - PreDeleteCommand = { Start-PreDeleteADMXFiles @args } - ViewProperties = @("fileName","status","Id") - PropertiesToRemove = @("languageCodes","targetPrefix","targetNamespace","policyType","revision","status","uploadDateTime") - SupportsPageSize = $false - }) - - <# - Add-ViewItem (New-Object PSObject -Property @{ - Title = "iOS Enrollment Profile" - Id = "iOSDepProfile" - ViewID = "IntuneGraphAPI" - API = "/deviceManagement/depIOSEnrollmentProfile" - Permissons=@("DeviceManagementServiceConfig.ReadWrite.All") - GroupId = "DeviceConfiguration" - Icon = "DeviceConfiguration" - ExpandAssignmentsList = $false - ViewProperties = @("fileName","status","Id") - }) - #> - - Add-ViewItem (New-Object PSObject -Property @{ - Title = "Reusable Settings" - Id = "ReusableSettings" - ViewID = "IntuneGraphAPI" - API = "/deviceManagement/reusablePolicySettings" - PropertiesToRemove = @('Settings','@OData.Type') - PostGetCommand = { Start-PostGetReusableSettings @args } - ImportOrder = 70 - Permissons=@("DeviceManagementConfiguration.ReadWrite.All") - ExpandAssignmentsList = $false - SkipRemoveProperties = @("@OData.Type") - Icon = "EndpointSecurity" - GroupId = "EndpointSecurity" - }) - - Add-ViewItem (New-Object PSObject -Property @{ - Title = "Authentication Strengths" - Id = "AuthenticationStrengths" - ViewID = "IntuneGraphAPI" - API = "/identity/conditionalAccess/authenticationStrengths/policies" - PreImportCommand = { Start-PreImportCommandAuthenticationStrengths @args } - PropertiesToRemove = @() - ImportOrder = 45 - Permissons=@("Policy.ReadWrite.ConditionalAccess") - ExpandAssignmentsList = $false - Icon = "ConditionalAccess" - GroupId = "EndpointSecurity" - SupportsPageSize = $false - }) - - Add-ViewItem (New-Object PSObject -Property @{ - Title = "Authentication Context" - Id = "AuthenticationContext" - ViewID = "IntuneGraphAPI" - API = "/identity/conditionalAccess/authenticationContextClassReferences" - PropertiesToRemove = @("@odata.type") - SkipRemoveProperties = @('Id') - ImportOrder = 46 - PreImportCommand = { Start-PreImportCommandAuthenticationContext @args } - Permissons=@("Policy.ReadWrite.ConditionalAccess") - ExpandAssignmentsList = $false - Icon = "ConditionalAccess" - GroupId = "EndpointSecurity" - SupportsPageSize = $false - }) - - Add-ViewItem (New-Object PSObject -Property @{ - Title = "W365 Provisioning Policies" - Id = "W365ProvisioningPolicies" - ViewID = "IntuneGraphAPI" - API = "/deviceManagement/virtualEndpoint/provisioningPolicies" - Permissons=@("CloudPC.ReadWrite.All") - Icon = "Devices" - GroupId = "DeviceConfiguration" - }) - - Add-ViewItem (New-Object PSObject -Property @{ - Title = "W365 User Settings" - Id = "W365UserSettings" - ViewID = "IntuneGraphAPI" - API = "/deviceManagement/virtualEndpoint/userSettings" - Permissons = @("CloudPC.ReadWrite.All") - Icon = "Devices" - GroupId = "DeviceConfiguration" - }) - - Add-ViewItem (New-Object PSObject -Property @{ - Title = "Driver Update Profiles" - Id = "DriverUpdateProfiles" - ViewID = "IntuneGraphAPI" - API = "/deviceManagement/windowsDriverUpdateProfiles" - Permissons = @("DeviceManagementConfiguration.ReadWrite.All") - Icon = "UpdatePolicies" - GroupId = "WinDriverUpdatePolicies" - }) - - Add-ViewItem (New-Object PSObject -Property @{ - Title = "Device Categories" - Id = "DeviceCategories" - ViewID = "IntuneGraphAPI" - API = "/deviceManagement/deviceCategories" - QUERYLIST = "`$top=500" - Permissons = @("DeviceManagementConfiguration.ReadWrite.All") - GroupId = "DeviceConfiguration" - ExpandAssignmentsList = $false - }) - -} - -function Invoke-EMAuthenticateToMSAL -{ - param($params = @{}) - - $global:EMViewObject.AppInfo = Get-GraphAppInfo "EMAzureApp" $global:DefaultAzureApp "EM" - Set-MSALCurrentApp $global:EMViewObject.AppInfo - & $global:msalAuthenticator.Login -Account (?? $global:MSALToken.Account.UserName (Get-Setting "" "LastLoggedOnUser")) @params -} - -function Invoke-EMDeactivateView -{ - $tmp = $mnuMain.Items | Where Name -eq "EMBulk" - if($tmp) { $mnuMain.Items.Remove($tmp) } -} - -function Invoke-EMActivatingView -{ - Show-MSALError - - # Refresh values in case they have changed - $global:EMViewObject.AppInfo = (Get-GraphAppInfo "EMAzureApp" $global:DefaultAzureApp "EM") - if(-not $global:EMViewObject.Authentication) - { - $global:EMViewObject.Authentication = Get-MSALAuthenticationObject - } - - # Add View specific menus - Add-GraphBulkMenu -} - -function Invoke-EMSaveSettings -{ - $tmpApp = Get-GraphAppInfo "EMAzureApp" $global:DefaultAzureApp - - if($global:appObj.ClientID -ne $tmpApp.ClientId -and $global:MSALToken) - { - # The app has changed. Need to authenticate to the new app - Write-Status "Logging in to $((?? $global:appObj.Name "selected application"))" - $global:EMViewObject.AppInfo = $tmpApp - Set-MSALCurrentApp $global:EMViewObject.AppInfo - Clear-MSALCurentUserVaiables - Connect-MSALUser -Account $global:MSALToken.Account.Username - Write-Status "" - } - - Set-EMUIStatus -} - -function Invoke-GraphAuthenticationUpdated -{ - Set-EMUIStatus - - $script:CustomADMXDefinitions = $null -} - -function Set-EMUIStatus -{ - # Hide/Show Delete button - $allowDelete = Get-SettingValue "EMAllowDelete" - $global:btnDelete.Visibility = (?: ($allowDelete -eq $true) "Visible" "Collapsed") - - # Hide/Show Delete on Bulk menu - $allowBulkDelete = Get-SettingValue "EMAllowBulkDelete" - $mnuBulk = $mnuMain.Items | Where Name -eq "EMBulk" - - if($mnuBulk) - { - $mnuBulkDelete = $mnuBulk.Items | Where Name -eq "mnuBulkDelete" - if($mnuBulkDelete) - { - $mnuBulkDelete.Visibility = (?: ($allowBulkDelete -eq $true) "Visible" "Collapsed") - } - } -} - -function Set-EMViewPanel -{ - param($panel) - - # ToDo: Create View specific pannel and move this to graph - Add-XamlEvent $panel "btnView" "Add_Click" -scriptBlock ([scriptblock]{ - Show-GraphObjectInfo - }) - - Add-XamlEvent $panel "btnDelete" "Add_Click" -scriptBlock ([scriptblock]{ - Remove-GraphObjects - }) - - Add-XamlEvent $panel "btnCopy" "Add_Click" -scriptBlock ([scriptblock]{ - Copy-GraphObject - }) - - Add-XamlEvent $panel "btnExport" "Add_Click" -scriptBlock ([scriptblock]{ - Show-GraphExportForm - }) - - Add-XamlEvent $panel "btnImport" "Add_Click" -scriptBlock ([scriptblock]{ - Show-GraphImportForm - }) - - Add-XamlEvent $panel "txtFilter" "Add_LostFocus" ({ #param($obj, $e) - Invoke-FilterBoxChanged $this - #$e.Handled = $true - }) - - Add-XamlEvent $panel "txtFilter" "Add_GotFocus" ({ - if($this.Tag -eq "1" -and $this.Text -eq "Filter") { $this.Text = "" } - Invoke-FilterBoxChanged $this - }) - - Add-XamlEvent $panel "txtFilter" "Add_TextChanged" ({ - Invoke-FilterBoxChanged $this - }) - - Invoke-FilterBoxChanged ($panel.FindName("txtFilter")) - - $allowDelete = Get-SettingValue "EMAllowDelete" - Set-XamlProperty $panel "btnDelete" "Visibility" (?: ($allowDelete -eq $true) "Visible" "Collapsed") - - $global:dgObjects.add_selectionChanged({ - Invoke-ModuleFunction "Invoke-EMSelectedItemsChanged" - }) - - # ToDo: Move this to the view object - $dpd = [System.ComponentModel.DependencyPropertyDescriptor]::FromProperty([System.Windows.Controls.ItemsControl]::ItemsSourceProperty, [System.Windows.Controls.DataGrid]) - if($dpd) - { - $dpd.AddValueChanged($global:dgObjects, { - Set-XamlProperty $global:dgObjects.Parent "txtFilter" "Text" "" - $enabled = (?: ($null -eq $this.ItemsSource -or ($this.ItemsSource | measure).Count -eq 0) $false $true) - Set-XamlProperty $global:dgObjects.Parent "btnImport" "IsEnabled" $true # Always all Import if ObjectType allows it - Set-XamlProperty $global:dgObjects.Parent "btnExport" "IsEnabled" $enabled - }) - } - - $btnRefresh = Get-XamlObject ($global:AppRootFolder + "\Xaml\RefreshButton.xaml") - if($btnRefresh) - { - $btnRefresh.SetValue([System.Windows.Controls.Grid]::ColumnProperty,$grdTitle.ColumnDefinitions.Count - 1) - $btnRefresh.Margin = "0,0,5,3" - $btnRefresh.Cursor = "Hand" - $btnRefresh.Name = "btnRefresh" - $btnRefresh.Focusable = $false - $grdTitle.Children.Add($btnRefresh) | Out-Null - - $tooltip = [System.Windows.Controls.ToolTip]::new() - $tooltip.Content = "Refresh all objects" - [System.Windows.Controls.ToolTipService]::SetToolTip($btnRefresh, $tooltip) - - $panel.RegisterName($btnRefresh.Name, $btnRefresh) - - $tooltip = [System.Windows.Controls.ToolTip]::new() - $tooltip.Content = "Refresh objects" - - [System.Windows.Controls.ToolTipService]::SetToolTip($btnRefresh, $tooltip) - - $btnRefresh.Add_Click({ - $txtFilterText = $null - $txtFilter = $this.Parent.FindName("txtFilter") - if($txtFilter) { $txtFilterText = $txtFilter.Text } #= "" } - - Show-GraphObjects $txtFilterText - - if($txtFilterText -and $txtFilter) - { - $txtFilter.Text = $txtFilterText - Invoke-FilterBoxChanged $txtFilter - } - - Write-Status "" - }) - } - - $global:btnLoadAllPages.add_click({ - Write-Status "Loading $($global:curObjectType.Title) objects" - [array]$graphObjects = Get-GraphObjects -property $global:curObjectType.ViewProperties -objectType $global:curObjectType -AllPages - if(-not $global:dgObjects.Columns) - { - Show-GraphObjects -FromGraphObjects $graphObjects - } - else - { - $graphObjects | Where-Object { $_ -ne $null } | ForEach-Object { $global:dgObjects.ItemsSource.AddNewItem($_) | Out-Null } - } - $global:dgObjects.ItemsSource.CommitNew() - Set-GraphPagesButtonStatus - Invoke-FilterBoxChanged $global:txtFilter -ForceUpdate - Write-Status "" - }) - - $global:btnLoadNextPage.add_click({ - Write-Status "Loading $($global:curObjectType.Title) objects" - [array]$graphObjects = Get-GraphObjects -property $global:curObjectType.ViewProperties -objectType $global:curObjectType -SinglePage - if(-not $global:dgObjects.Columns) - { - Show-GraphObjects -FromGraphObjects $graphObjects - } - else - { - $graphObjects | Where-Object { $_ -ne $null } | ForEach-Object { $global:dgObjects.ItemsSource.AddNewItem($_) | Out-Null } - } - $global:dgObjects.ItemsSource.CommitNew() - Set-GraphPagesButtonStatus - Invoke-FilterBoxChanged $global:txtFilter - Write-Status "" - }) -} - -function Invoke-GraphObjectsChanged -{ - $btnRefresh = $global:EMViewObject.ViewPanel.FindName("btnRefresh") - - if($btnRefresh) - { - $tooltip = [System.Windows.Controls.ToolTipService]::GetToolTip($btnRefresh) - if($global:lstMenuItems.SelectedItem.QuerySearch -eq $true) - { - $tooltip.Content = "Refresh objects based on filter. Note: Only filtered objects will be returned. Clear filter and press refresh to reload other objects" - } - else - { - $tooltip.Content = "Refresh all objects" - } - } -} - -function Invoke-EMSelectedItemsChanged -{ - $hasSelectedItems = ($global:dgObjects.ItemsSource | Where IsSelected -eq $true) -or ($null -ne $global:dgObjects.SelectedItem) - Set-XamlProperty $global:dgObjects.Parent "btnView" "IsEnabled" $hasSelectedItems #(?: ($null -eq ($global:dgObjects.SelectedItem)) $false $true) - Set-XamlProperty $global:dgObjects.Parent "btnCopy" "IsEnabled" $hasSelectedItems #(?: ($null -eq $global:dgObjects.SelectedItem) $false $true) - Set-XamlProperty $global:dgObjects.Parent "btnDelete" "IsEnabled" $hasSelectedItems #(?: ($null -eq $global:dgObjects.SelectedItem -and $global:curObjectType.AllowDelete -ne $false) $false $true) -} - -function Invoke-FilterBoxChanged -{ - param($txtBox,[switch]$ForceUpdate) - - $filter = $null - - if($txtBox.Text.Trim() -eq "" -and $txtBox.IsFocused -eq $false) - { - $txtBox.FontStyle = "Italic" - $txtBox.Tag = 1 - $txtBox.Text = "Filter" - $txtBox.Foreground="Lightgray" - } - elseif($ForceUpdate -eq $true) - { - $dgObjects.ItemsSource.Filter = $dgObjects.ItemsSource.Filter - } - elseif($txtBox.Tag -eq "1" -and $txtBox.Text -eq "Filter" -and $txtBox.IsFocused -eq $false) - { - - } - else - { - $txtBox.FontStyle = "Normal" - $txtBox.Tag = $null - $txtBox.Foreground="Black" - $txtBox.Background="White" - - if($txtBox.Text) - { - $filter = { - param ($item) - - return ($null -ne ($item.PSObject.Properties | Where { $_.Name -notin @("IsSelected","Object", "ObjectType") -and $_.Value -match [regex]::Escape($txtBox.Text) })) - - foreach($prop in ($item.PSObject.Properties | Where { $_.Name -notin @("IsSelected","Object", "ObjectType")})) - { - if($prop.Value -match [regex]::Escape($txtBox.Text)) { return $true } - } - $false - } - } - } - - if($dgObjects.ItemsSource -is [System.Windows.Data.ListCollectionView] -and $txtBox.IsFocused -eq $true) - { - $dgObjects.ItemsSource.Filter = $filter - } - - $allObjectsCount = 0 - if($dgObjects.ItemsSource.SourceCollection) - { - $allObjectsCount = $dgObjects.ItemsSource.SourceCollection.Count - } - - $objCount = ($dgObjects.ItemsSource | measure).Count - if($objCount -gt 0) - { - $strAllObjectsInfo = "" - if($allObjectsCount -gt $objCount) - { - $strAllObjectsInfo = " ($($allObjectsCount))" - } - $global:txtEMObjects.Text = "Objects: $objCount$strAllObjectsInfo" - } - else - { - $global:txtEMObjects.Text = "" - } -} -#region Endpoint Security (Intents) functions - -function Start-PreImportEndpointSecurity -{ - param($obj, $objectType) - - @{ - "API"="deviceManagement/templates/$($obj.templateId)/createInstance" - } -} - -function Start-PostListEndpointSecurity -{ - param($objList, $objectType) - - if(-not $script:baseLineTemplates) - { - $script:baseLineTemplates = (Invoke-GraphRequest -Url "/deviceManagement/templates").Value - } - if(-not $script:baseLineTemplates) { return } - - foreach($obj in $objList) - { - if(-not $obj.Object.templateId) { continue } - if($obj.Object.templateId -ne $baseLineTemplate.Id) - { - $baseLineTemplate = $script:baseLineTemplates | Where Id -eq $obj.Object.templateId - } - - if($baseLineTemplate) - { - $obj | Add-Member -MemberType NoteProperty -Name "Type" -Value $baseLineTemplate.displayName - $obj | Add-Member -MemberType NoteProperty -Name "Category" -Value (?: ($baseLineTemplate.templateSubtype -eq "none") $baseLineTemplate.templateType $baseLineTemplate.templateSubtype) - } - - } - $objList -} - -function Start-PostExportEndpointSecurity -{ - param($obj, $objectType, $path) - - $fileName = (Get-GraphObjectName $obj $objectType).Trim('.') - if((Get-SettingValue "AddIDToExportFile") -eq $true -and $obj.Id) - { - $fileName = ($fileName + "_" + $obj.Id) - } - - $settings = Invoke-GraphRequest -Url "$($objectType.API)/$($obj.id)/settings" - $settingsJson = "{ `"settings`": $((ConvertTo-Json $settings.value -Depth 20 ))`n}" - $fileName = "$path\$((Remove-InvalidFileNameChars $fileName))_Settings.json" - Save-GraphObjectToFile $settingsJson $fileName -} - -function Start-PostFileImportEndpointSecurity -{ - param($obj, $objectType, $file) - - $settings = Get-EMSettingsObject $obj $objectType $file - if($settings) - { - Start-GraphPreImport $settings - Invoke-GraphRequest -Url "$($objectType.API)/$($obj.id)/updateSettings" -Body ($settings | ConvertTo-Json -Depth 50) -Method "POST" - } -} - -function Start-PreCopyEndpointSecurity -{ - param($obj, $objectType, $newName) - - $false - - # Intents has a createCopy method. Use "manual" copy to have one standard and making sure Copy works the same as Export/Import - # These objects supports duplicate in the portal - # Keep for reference - # - # $objData = "{`"displayName`":`"$($newName)`"}" - # - #Invoke-GraphRequest -Url "/deviceManagement/intents/$($obj.Id)/createCopy" -Content $objData -HttpMethod "POST" | Out-Null - #$true -} - -function Start-PostCopyEndpointSecurity -{ - param($objCopyFrom, $objNew, $objectType) - - $settings = Invoke-GraphRequest -Url "$($objectType.API)/$($objCopyFrom.id)/settings" -ODataMetadata "Skip" - if($settings) - { - $settingsObj = New-object PSObject @{ "Settings" = $settings.Value } - Invoke-GraphRequest -Url "$($objectType.API)/$($objNew.id)/updateSettings" -Body ($settingsObj | ConvertTo-Json -Depth 20) -Method "POST" - } -} - -function Start-PreUpdateEndpointSecurity -{ - param($obj, $objectType, $curObject, $fromObj) - - if(-not $fromObj.settings) { return } - - $strAPI = "/deviceManagement/intents/$($curObject.Object.id)/updateSettings" - - $curObject = Get-GraphObject $curObject.Object $objectType - - $curValues = @() - foreach($val in $curObject.Object.settings) - { - if($fromObj.settings | Where { $_.definitionId -eq $val.definitionId}) { continue } - - # Set all existing values to null - # Note: This will not remove them from the configured list just set them Not Configured - $curValues += [PSCustomObject]@{ - '@odata.type' = $val.'@odata.type' - definitionId = $val.definitionId - id = $val.id - valueJson = "null" - } - } - - $curValues += $fromObj.settings - - <# - if($curValues.Count -gt 0) - { - $tmpObj = [PSCustomObject]@{ - settings = $curValues - } - $json = ConvertTo-Json $tmpObj -Depth 20 - - # Set all existing values to null - # Note: This will not remove them from the configured list just set them Not Configured - Invoke-GraphRequest -Url $strAPI -Content $json -HttpMethod "POST" | Out-Null - } - #> - - $tmpObj = [PSCustomObject]@{ - settings = $curValues - } - Start-GraphPreImport $tmpObj.settings - - $json = ConvertTo-Json $tmpObj -Depth 20 - Invoke-GraphRequest -Url $strAPI -Content $json -HttpMethod "POST" | Out-Null - - Remove-Property $obj "templateId" -} - -function Start-PostGetEndpointSecurity -{ - param($obj, $objectType) - - Add-EndpointSecurityInfo $obj -} - -function local:Add-EndpointSecurityInfo -{ - param($obj, $baseLineTemplate = $null) - -} -#endregion - -#region - -function Start-PostFileImportDeviceConfiguration -{ - param($obj, $objectType, $importFile) - - if($obj.'@OData.Type' -like "#microsoft.graph.windows10GeneralConfiguration") - { - $tmpObj = Get-GraphObjectFromFile $importFile - - if(($tmpObj.privacyAccessControls | measure).Count -gt 0) - { - $privacyObj = [PSCustomObject]@{ - windowsPrivacyAccessControls = $tmpObj.privacyAccessControls - } - $json = $privacyObj | ConvertTo-Json -Depth 20 - $ret = Invoke-GraphRequest -Url "deviceManagement/deviceConfigurations('$($obj.Id)')/windowsPrivacyAccessControls" -Body $json -Method "POST" - } - } -} - -function Start-PostCopyDeviceConfiguration -{ - param($objCopyFrom, $objNew, $objectType) - - if($objCopyFrom.'@OData.Type' -like "#microsoft.graph.windows10GeneralConfiguration") - { - if(($objCopyFrom.privacyAccessControls | measure).Count -gt 0) - { - $privacyObj = [PSCustomObject]@{ - windowsPrivacyAccessControls = $objCopyFrom.privacyAccessControls - } - $json = $privacyObj | ConvertTo-Json -Depth 20 - Invoke-GraphRequest -Url "deviceManagement/deviceConfigurations('$($objNew.Id)')/windowsPrivacyAccessControls" -Body $json -Method "POST" | Out-null - } - } -} - -function Start-PostGetDeviceConfiguration -{ - param($obj, $objectType) - - if(($obj.Object.omaSettings | measure).Count -gt 0) - { - foreach($omaSetting in ($obj.Object.omaSettings | Where isEncrypted -eq $true)) - { - if($omaSetting.isEncrypted -eq $false) { continue } - - $xmlValue = Invoke-GraphRequest -Url "/deviceManagement/deviceConfigurations/$($obj.Object.Id)/getOmaSettingPlainTextValue(secretReferenceValueId='$($omaSetting.secretReferenceValueId)')" - if($xmlValue.Value) - { - $omaSetting.isEncrypted = $false - $omaSetting.secretReferenceValueId = $null - - if($omaSetting.'@odata.type' -eq "#microsoft.graph.omaSettingStringXml" -or - $omaSetting.'value@odata.type' -eq "#Binary") - { - $Bytes = [System.Text.Encoding]::UTF8.GetBytes($xmlValue.Value) - $omaSetting.value = [Convert]::ToBase64String($bytes) - } - else - { - $omaSetting.value = $xmlValue.Value - } - } - } - } -} - -#endregion - -#region Compliance Policy -function Start-PostExportCompliancePolicies -{ - param($obj, $objectType, $exportPath) - - foreach($scheduledActionsForRule in $obj.scheduledActionsForRule) - { - foreach($scheduledActionConfiguration in $scheduledActionsForRule.scheduledActionConfigurations) - { - foreach($notificationMessageCCGroup in $scheduledActionConfiguration.notificationMessageCCList) - { - Add-GroupMigrationObject $notificationMessageCCGroup - } - } - } -} - -function Start-PreUpdateCompliancePolicies -{ - param($obj, $objectType, $curObject, $fromObj) - - $strAPI = "/deviceManagement/deviceCompliancePolicies/$($curObject.Object.id)/scheduleActionsForRules" - - $tmpObj = [PSCustomObject]@{ - deviceComplianceScheduledActionForRules = $obj.scheduledActionsForRule - } - - $json = ConvertTo-Json $tmpObj -Depth 20 - Invoke-GraphRequest -Url $strAPI -Content $json -HttpMethod "POST" | Out-Null - - Remove-Property $obj "scheduledActionsForRule" -} - -#endregion - -function Start-PostImportComplianceScripts -{ - param($obj, $objectType, $file) - - $endTime = (Get-Date).AddMinutes(2) - - $found = $false - while($endTime -gt (Get-Date)) - { - $tmpObj = Invoke-GraphRequest -Url "$($objectType.API)/$($obj.Id)" -ErrorAction SilentlyContinue - if($tmpObj) { - $found = $true - break - } - Start-Sleep -Seconds 10 - } - - if(-not $found) - { - Write-LogError "Compliance script $($obj.Id) not found after import. Please check the import file." - return - } -} - -#region Intune Branding functions -function Start-PreImportIntuneBranding -{ - param($obj, $objectType) - - $ret = @{} - $global:brandingClone = $null - - if($obj.isDefaultProfile) - { - - # Looks like the ID is the same for all tenants so skip this for now - <# - $defObj = (Invoke-GraphRequest -Url "/deviceManagement/intuneBrandingProfiles?`$filter=isDefaultProfile eq true&`$select=id,displayName").Value[0] - if($defObj) - { - $obj.Id = $defObj.Id - } - #> - - $ret.Add("API",($objectType.API + "/" + $obj.Id)) - $ret.Add("Method","PATCH") # Default profile always exists so update it - - foreach($prop in @("profileName","isDefaultProfile","disableClientTelemetry","profileDescription")) - { - Remove-Property $obj $prop - } - - $ret - } - else - { - # Create new Branding profile does not support images data in the json - # Workaround: (as done by the portal) - # Create a new profile with basic info - # Patch the profile with all the info - - $global:brandingClone = $obj | ConvertTo-Json -Depth 20 | ConvertFrom-Json - - foreach($prop in ($obj.PSObject.Properties | Where {$_.Name -notin @("profileName","profileDescription","roleScopeTagIds")})) #"customPrivacyMessage" - { - Remove-Property $obj $prop.Name - } - } - Remove-Property $obj "Id" -} - -function Start-PostImportIntuneBranding -{ - param($obj, $objectType, $file) - - if($obj.isDefaultProfile -or -not $global:brandingClone) { return } - - foreach($prop in @("Id","isDefaultProfile","customPrivacyMessage","disableClientTelemetry")) #"isDefaultProfile","disableClientTelemetry" - { - Remove-Property $global:brandingClone $prop - } - $json = ($global:brandingClone | ConvertTo-Json -Depth 20) - Invoke-GraphRequest -Url "$($objectType.API)/$($obj.Id)" -Body $json -Method "PATCH" | Out-Null -} - -function Start-PostGetIntuneBranding -{ - param($obj, $objectType) - - foreach($imgType in @("themeColorLogo","lightBackgroundLogo","landingPageCustomizedImage")) - { - Write-LogDebug "Get $imgType for $($obj.Object.profileName)" - $imgJson = Invoke-GraphRequest -Url "$($objectType.API)/$($obj.Object.Id)/$imgType" - if($imgJson.Value) - { - $obj.Object.$imgType = $imgJson - } - } -} - -function Start-PostExportIntuneBranding -{ - param($obj, $objectType, $path) - - foreach($imgType in @("themeColorLogo","lightBackgroundLogo","landingPageCustomizedImage")) - { - if($obj.$imgType.Value) - { - $fileName = "$path\$((Get-GraphObjectName $obj $objectType))_$imgType.jpg" - [IO.File]::WriteAllBytes($fileName, [System.Convert]::FromBase64String($obj.$imgType.Value)) - } - } -} - -function Start-PreDeleteIntuneBranding -{ - param($obj, $objectType) - - if($obj.isDefaultProfile -eq $true) - { - @{ "Delete" = $false } - } -} - -function Start-PreUpdateIntuneBranding -{ - param($obj, $objectType, $curObject, $fromObj) - - if($curObject.Object.isDefaultProfile) - { - foreach($prop in @("profileName","isDefaultProfile","disableClientTelemetry","profileDescription")) - { - Remove-Property $obj $prop - } - } -} - -#endregion - -#region Azure Branding functions -function Start-PreImportAzureBranding -{ - param($obj, $objectType) - - Remove-Property $obj "@odata.Type" - - $ret = @{} - if($obj.Id -eq "0") - { - #$ret.Add("Method","PATCH") # Default profile always exists so update it - #$ret.Add("API",($objectType.API + "/0")) - } - - $ret.Add("API",($objectType.API + "/$($global:Organization.Id)/branding/localizations")) - - # This is NOT wat the documentation says - # Documentation says to use Content-Language - # Any place the documentation states to use Accept-Language is for Get operation - # https://docs.microsoft.com/en-us/graph/api/organizationalbrandingproperties-get?view=graph-rest-beta&tabs=http#request-headers - $ret.Add("AdditionalHeaders", @{ "Accept-Language" = $obj.Id }) - - $ret -} - -function Start-PostListAzureBranding -{ - param($objList, $objectType) - - foreach($obj in $objList) - { - if(-not $obj.Object.id) { continue } - try - { - if($obj.Object.id -eq "0") - { - $language = "Default" - } - else - { - $language = ([cultureinfo]::GetCultureInfo($obj.Object.id)).DisplayName - } - - $obj | Add-Member -MemberType NoteProperty -Name "Language" -Value $language - } - catch{} - } - $objList -} - -#endregion - -#region Script functions -function Add-ScriptExtensions -{ - param($form, $buttonPanel, $index = 0) - - $btnDownload = New-Object System.Windows.Controls.Button - $btnDownload.Content = 'Download' - $btnDownload.Name = 'btnDownload' - $btnDownload.Margin = "0,0,5,0" - $btnDownload.Width = "100" - - $btnDownload.Add_Click({ - Invoke-DownloadScript - }) - - $tmp = $form.FindName($buttonPanel) - if($tmp) - { - $tmp.Children.Insert($index, $btnDownload) - } - - $btnDownload = New-Object System.Windows.Controls.Button - $btnDownload.Content = 'Edit' - $btnDownload.Name = 'btnEdit' - $btnDownload.Margin = "0,0,5,0" - $btnDownload.Width = "100" - - $btnDownload.Add_Click({ - Invoke-EditScript - }) - - $tmp = $form.FindName($buttonPanel) - if($tmp) - { - $tmp.Children.Insert($index, $btnDownload) - } -} - -function Add-ScriptExportExtensions -{ - param($form, $buttonPanel, $index = 0) - - $ctrl = $form.FindName("chkExportScript") - if(-not $ctrl) - { - $xaml = @" - - -"@ - $label = [Windows.Markup.XamlReader]::Parse($xaml) - - $global:chkExportScript = [System.Windows.Controls.CheckBox]::new() - $global:chkExportScript.IsChecked = $true - $global:chkExportScript.VerticalAlignment = "Center" - $global:chkExportScript.Name = "chkExportScript" - - @($label, $global:chkExportScript) - } -} - -function Start-PostExportScripts -{ - param($obj, $objectType, $exportPath) - - if($obj.scriptContent -and $global:chkExportScript.IsChecked) - { - Write-Log "Export script $($obj.FileName)" - $fileName = [IO.Path]::Combine($exportPath, $obj.FileName) - [IO.File]::WriteAllBytes($fileName, ([System.Convert]::FromBase64String($obj.scriptContent))) - } -} - -function Invoke-DownloadScript -{ - if(-not $global:dgObjects.SelectedItem.Object.id) { return } - - $obj = (Get-GraphObject $global:dgObjects.SelectedItem $global:curObjectType).Object - Write-Status "" - - if($obj.scriptContent) - { - Write-Log "Download PowerShell script '$($obj.FileName)' from $($obj.displayName)" - - $dlgSave = New-Object -Typename System.Windows.Forms.SaveFileDialog - $dlgSave.InitialDirectory = Get-SettingValue "IntuneRootFolder" $env:Temp - $dlgSave.FileName = $obj.FileName - if($dlgSave.ShowDialog() -eq [System.Windows.Forms.DialogResult]::OK -and $dlgSave.Filename) - { - # Changed to WriteAllBytes to get rid of BOM characters from Custom Attribute file - [IO.File]::WriteAllBytes($dlgSave.FileName, ([System.Convert]::FromBase64String($obj.scriptContent))) - } - } -} - -function Invoke-EditScript -{ - if(-not $global:dgObjects.SelectedItem.Object.id) { return } - - $obj = (Get-GraphObject $global:dgObjects.SelectedItem $global:curObjectType) - Write-Status "" - if(-not $obj.Object.scriptContent) { return } - $script:currentScriptObject = $obj - - $script:editForm = Get-XamlObject ($global:AppRootFolder + "\Xaml\EditScriptDialog.xaml") - - if(-not $script:editForm) { return } - - Set-XamlProperty $script:editForm "txtEditScriptTitle" "Text" "Edit: $($obj.Object.displayName)" - - $scriptText = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($obj.Object.scriptContent)) - Set-XamlProperty $script:editForm "txtScriptText" "Text" $scriptText - - $script:currentModal = $null - if($global:grdModal.Children.Count -gt 0) - { - $script:currentModal = $global:grdModal.Children[0] - } - - Add-XamlEvent $script:editForm "btnSaveScriptEdit" "add_click" ({ - $scriptText = Get-XamlProperty $script:editForm "txtScriptText" "Text" - $pre = [System.Text.Encoding]::UTF8.GetPreamble() - $utfBOM = [System.Text.Encoding]::UTF8.GetString($pre) - if($scriptText.startsWith($utfBOM)) - { - # Remove UTF8 BOM bytes - $scriptText = $scriptText.Remove(0, $utfBOM.Length) - } - $bytes = [System.Text.Encoding]::UTF8.GetBytes($scriptText) - $encodedText = [Convert]::ToBase64String($bytes) - - if($script:currentScriptObject.Object.scriptContent -ne $encodedText) - { - # Save script - if(([System.Windows.MessageBox]::Show("Are you sure you want to update the script?`n`nObject:`n$($script:currentScriptObject.displayName)", "Update script?", "YesNo", "Warning")) -eq "Yes") - { - Write-Status "Update $($script:currentScriptObject.displayName)" - $obj = $script:currentScriptObject.Object | ConvertTo-Json -Depth 20 | ConvertFrom-Json - $obj.scriptContent = $encodedText - Start-GraphPreImport $obj $script:currentScriptObject.ObjectType - foreach($prop in $script:currentScriptObject.ObjectType.PropertiesToRemoveForUpdate) - { - Remove-Property $obj $prop - } - Remove-Property $obj "Assignments" - Remove-Property $obj "isAssigned" - - $json = ConvertTo-Json $obj -Depth 15 - - $objectUpdated = (Invoke-GraphRequest -Url "$($script:currentScriptObject.ObjectType.API)/$($script:currentScriptObject.Object.Id)" -Content $json -HttpMethod "PATCH") - if(-not $objectUpdated) - { - Write-Log "Failed to update script" 3 - [System.Windows.MessageBox]::Show("Failed to save the script object. See log for more information","Update failed!", "OK", "Error") - } - Write-Status "" - } - } - - $global:grdModal.Children.Clear() - if($script:currentModal) - { - $global:grdModal.Children.Add($script:currentModal) - } - [System.Windows.Forms.Application]::DoEvents() - }) - - Add-XamlEvent $script:editForm "btnCancelScriptEdit" "add_click" ({ - $global:grdModal.Children.Clear() - if($script:currentModal) - { - $global:grdModal.Children.Add($script:currentModal) - } - [System.Windows.Forms.Application]::DoEvents() - }) - - $global:grdModal.Children.Clear() - $script:editForm.SetValue([System.Windows.Controls.Grid]::RowProperty,1) - $script:editForm.SetValue([System.Windows.Controls.Grid]::ColumnProperty,1) - $global:grdModal.Children.Add($script:editForm) | Out-Null - [System.Windows.Forms.Application]::DoEvents() -} - -#endregion - -#region Policy File functions -function Add-PolicyFileExtensions -{ - param($form, $buttonPanel, $index = 0) - - $btnDownload = New-Object System.Windows.Controls.Button - $btnDownload.Content = 'Download' - $btnDownload.Name = 'btnDownload' - $btnDownload.Margin = "0,0,5,0" - $btnDownload.Width = "100" - - $btnDownload.Add_Click({ - Invoke-DownloadPolicyFile - }) - - $tmp = $form.FindName($buttonPanel) - if($tmp) - { - $tmp.Children.Insert($index, $btnDownload) - } - - $btnDownload = New-Object System.Windows.Controls.Button - $btnDownload.Content = 'Edit' - $btnDownload.Name = 'btnEdit' - $btnDownload.Margin = "0,0,5,0" - $btnDownload.Width = "100" - - $btnDownload.Add_Click({ - Invoke-EditPolicyFile - }) - - $tmp = $form.FindName($buttonPanel) - if($tmp) - { - $tmp.Children.Insert($index, $btnDownload) - } -} - -function Add-PolicyFileExportExtensions -{ - param($form, $buttonPanel, $index = 0) - - $ctrl = $form.FindName("chkExportPolicyFile") - if(-not $ctrl) - { - $xaml = @" - - -"@ - $label = [Windows.Markup.XamlReader]::Parse($xaml) - - $global:chkExportPolicyFile = [System.Windows.Controls.CheckBox]::new() - $global:chkExportPolicyFile.IsChecked = $true - $global:chkExportPolicyFile.VerticalAlignment = "Center" - $global:chkExportPolicyFile.Name = "chkExportPolicyFile" - - @($label, $global:chkExportPolicyFile) - } -} - -function Start-PostExportPolicyFile -{ - param($obj, $objectType, $exportPath) - - if($objectType.PolicyFileAttribute -and $obj.$($objectType.PolicyFileAttribute) -and $global:chkExportPolicyFile.IsChecked) - { - Write-Log "Export policy file from attribute $($obj.PolicyFileAttribute)" - $fileNameOut = ?? $obj.FileName "$($obj.PolicyFileAttribute).json" - $fileName = [IO.Path]::Combine($exportPath, $fileNameOut) - [IO.File]::WriteAllBytes($fileName, ([System.Convert]::FromBase64String($obj.$($objectType.PolicyFileAttribute)))) - } -} - -function Invoke-DownloadPolicyFile -{ - if(-not $global:dgObjects.SelectedItem.Object.id) { return } - - $obj = (Get-GraphObject $global:dgObjects.SelectedItem $global:curObjectType).Object - Write-Status "" - - if($global:curObjectType.PolicyFileAttribute -and $obj.$($global:curObjectType.PolicyFileAttribute)) - { - $fileNameOut = ?? $obj.FileName "$($obj.PolicyFileAttribute).json" - Write-Log "Download policy file '$($fileNameOut)' from $($obj.displayName)" - - $dlgSave = New-Object -Typename System.Windows.Forms.SaveFileDialog - $dlgSave.InitialDirectory = Get-SettingValue "IntuneRootFolder" $env:Temp - $dlgSave.FileName = $fileNameOut - if($dlgSave.ShowDialog() -eq [System.Windows.Forms.DialogResult]::OK -and $dlgSave.Filename) - { - # Changed to WriteAllBytes to get rid of BOM characters from Custom Attribute file - [IO.File]::WriteAllBytes($dlgSave.FileName, ([System.Convert]::FromBase64String($obj.$($global:curObjectType.PolicyFileAttribute)))) - } - } -} - -function Invoke-EditPolicyFile -{ - if(-not $global:dgObjects.SelectedItem.Object.id) { return } - - $obj = (Get-GraphObject $global:dgObjects.SelectedItem $global:curObjectType) - Write-Status "" - if(-not $global:curObjectType.PolicyFileAttribute -or -not $obj.Object.$($global:curObjectType.PolicyFileAttribute)) { return } - $script:currentScriptObject = $obj - - $script:editForm = Get-XamlObject ($global:AppRootFolder + "\Xaml\EditScriptDialog.xaml") - - if(-not $script:editForm) { return } - - Set-XamlProperty $script:editForm "txtEditScriptTitle" "Text" "Edit: $($obj.Object.displayName)" - - $scriptText = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($obj.Object.$($global:curObjectType.PolicyFileAttribute))) - Set-XamlProperty $script:editForm "txtScriptText" "Text" $scriptText - - $script:currentModal = $null - if($global:grdModal.Children.Count -gt 0) - { - $script:currentModal = $global:grdModal.Children[0] - } - - Add-XamlEvent $script:editForm "btnSaveScriptEdit" "add_click" ({ - $scriptText = Get-XamlProperty $script:editForm "txtScriptText" "Text" - $pre = [System.Text.Encoding]::UTF8.GetPreamble() - $utfBOM = [System.Text.Encoding]::UTF8.GetString($pre) - if($scriptText.startsWith($utfBOM)) - { - # Remove UTF8 BOM bytes - $scriptText = $scriptText.Remove(0, $utfBOM.Length) - } - $bytes = [System.Text.Encoding]::UTF8.GetBytes($scriptText) - $encodedText = [Convert]::ToBase64String($bytes) - - if($script:currentScriptObject.Object.scriptContent -ne $encodedText) - { - # Save script - if(([System.Windows.MessageBox]::Show("Are you sure you want to update the $($global:curObjectType.PolicyFileAttribute) attribute?`n`nObject:`n$($script:currentScriptObject.displayName)", "Update script?", "YesNo", "Warning")) -eq "Yes") - { - Write-Status "Update $($script:currentScriptObject.displayName)" - $obj = $script:currentScriptObject.Object | ConvertTo-Json -Depth 20 | ConvertFrom-Json - $obj.$($global:curObjectType.PolicyFileAttribute) = $encodedText - Start-GraphPreImport $obj $script:currentScriptObject.ObjectType - foreach($prop in $script:currentScriptObject.ObjectType.PropertiesToRemoveForUpdate) - { - Remove-Property $obj $prop - } - Remove-Property $obj "Assignments" - Remove-Property $obj "isAssigned" - - $json = ConvertTo-Json $obj -Depth 15 - - $objectUpdated = (Invoke-GraphRequest -Url "$($script:currentScriptObject.ObjectType.API)/$($script:currentScriptObject.Object.Id)" -Content $json -HttpMethod "PATCH") - if(-not $objectUpdated) - { - Write-Log "Failed to update script" 3 - [System.Windows.MessageBox]::Show("Failed to save the policy. See log for more information","Update failed!", "OK", "Error") - } - Write-Status "" - } - } - - $global:grdModal.Children.Clear() - if($script:currentModal) - { - $global:grdModal.Children.Add($script:currentModal) - } - [System.Windows.Forms.Application]::DoEvents() - }) - - Add-XamlEvent $script:editForm "btnCancelScriptEdit" "add_click" ({ - $global:grdModal.Children.Clear() - if($script:currentModal) - { - $global:grdModal.Children.Add($script:currentModal) - } - [System.Windows.Forms.Application]::DoEvents() - }) - - $global:grdModal.Children.Clear() - $script:editForm.SetValue([System.Windows.Controls.Grid]::RowProperty,1) - $script:editForm.SetValue([System.Windows.Controls.Grid]::ColumnProperty,1) - $global:grdModal.Children.Add($script:editForm) | Out-Null - [System.Windows.Forms.Application]::DoEvents() -} - -#endregion - -#region Terms and Conditions -function Start-PostExportTermsAndConditions -{ - param($obj, $objectType, $path) - - Add-EMAssignmentsToExportFile $obj $objectType $path -} - -function Start-PreImportAssignmentsTermsAndConditions -{ - param($obj, $objectType, $file, $assignments) - - Add-EMAssignmentsToObject $obj $objectType $file $assignments -} -#endregion - -#region App Protection functions - -function Start-GetAppProtection -{ - param($obj, $objectType) - - if(-not $obj."@odata.type") { return } - - Get-GraphMetaData - - $objectClass = $null - if($global:metaDataXML) - { - try - { - $tmp = $obj."@odata.type".Split('.')[-1] - $objectClass = Get-GraphObjectClassName $tmp - } - catch - { - - } - $expand = $null - if($objectClass -eq "windowsInformationProtectionPolicies") - { - $expand = "?`$expand=protectedAppLockerFiles,exemptAppLockerFiles" - } - - if($objectClass) - { - @{"API"="/deviceAppManagement/$objectClass/$($obj.Id)$expand"} - } - } -} - -function Start-PostListAppProtection -{ - param($objList, $objectType) - - # App Configurations for Managed Apps are included in App Protections e.g. the /deviceAppManagement/managedAppPolicies API - # For some reason, the $filter option is not supported to filter out these objects - # e.g. not isof(...) to excluded the type, not startsWith(id, 'A_') to exlude based on Id - # These filters generates a request error so filter them out manually in this function instead - # The portal is probably doing the same thing since these are included in the return but not in the UI - $objList | Where { $_.Object.'@OData.Type' -ne '#microsoft.graph.targetedManagedAppConfiguration' } -} - -function Start-PreImportAppProtection -{ - param($obj, $objectType) - - if(($obj.Apps | measure).Count -gt 0) - { - $global:ImportObjectInfo = @{ Apps=$obj.Apps } - } - else - { - $global:ImportObjectInfo = $null - } - - $global:ImportObjectClass = $null - if($obj."@odata.type") - { - try - { - $global:ImportObjectClass = Get-GraphObjectClassName ($obj."@odata.type".Split('.')[-1]) - } - catch {} - } - - Remove-Property $obj "apps" - Remove-Property $obj "apps@odata.context" - - try - { - $tmp = $obj."@odata.type".Split('.')[-1] - $objectClass = Get-GraphObjectClassName $tmp - if($objectClass) - { - @{"API"="/deviceAppManagement/$objectClass"} - } - } - catch {} -} - -function Start-PostImportAppProtection -{ - param($obj, $objectType, $file) - - if($global:ImportObjectInfo.Apps) - { - # No "@odata.type" on the created object so reload new object - #$newObject = (Invoke-GraphRequest "$($objectType.API)?`$filter=id eq '$($obj.Id)'").Value - $newObject = Invoke-GraphRequest "$($objectType.API)/$($obj.Id)" - if($newObject) - { - try - { - $tmp = $newObject."@odata.type".Split('.')[-1] - $objectClass = Get-GraphObjectClassName $tmp - - $apps = [PSCustomObject]@{ - appGroupType = $obj.appGroupType - apps = @($global:ImportObjectInfo.Apps) - } - $json = $apps | ConvertTo-Json -Depth 20 - - Invoke-GraphRequest -Url "/deviceAppManagement/$objectClass/$($obj.Id)/targetApps" -Content $json -HttpMethod POST | Out-Null - } - catch {} - } - } - $global:ImportObjectInfo = $null -} - -function Start-PreImportAssignmentsAppProtection -{ - param($obj, $objectType, $file, $assignments) - - if($global:ImportObjectClass) - { - @{"API"="/deviceAppManagement/$($global:ImportObjectClass)/$($obj.Id)/assign"} - } -} - -function Start-PreUpdateAppConfigurationApp -{ - param($obj, $objectType, $curObject, $fromObj) - - if($obj.Apps) - { - try - { - Write-Log "Update App Configuruation Apps" - - $apps = [PSCustomObject]@{ - appGroupType = $obj.appGroupType - apps = @($obj.Apps) - } - $json = $apps | ConvertTo-Json -Depth 20 - $objectClass = 'targetedManagedAppConfigurations' - - Invoke-GraphRequest -Url "/deviceAppManagement/$objectClass/$($curObject.Object.Id)/targetApps" -Content $json -HttpMethod POST | Out-Null - } - catch {} - } - - Remove-Property $obj "apps" -} - -function Start-PreUpdateAppProtection -{ - param($obj, $objectType, $curObject, $fromObj) - - if($curObject.Object.'@OData.Type' -eq "#microsoft.graph.windowsInformationProtectionPolicy") - { - $api = "/deviceAppManagement/windowsInformationProtectionPolicies/$($curObject.Object.Id)" - } - elseif($curObject.Object.'@OData.Type' -eq "#microsoft.graph.mdmWindowsInformationProtectionPolicy") - { - $api = "/deviceAppManagement/mdmWindowsInformationProtectionPolicies/$($curObject.Object.Id)" - } - elseif($curObject.Object.'@OData.Type' -eq "#microsoft.graph.iosManagedAppProtection") - { - $api = "/deviceAppManagement/iosManagedAppProtections/$($curObject.Object.Id)" - } - elseif($curObject.Object.'@OData.Type' -eq "#microsoft.graph.androidManagedAppProtection") - { - $api = "/deviceAppManagement/androidManagedAppProtections/$($curObject.Object.Id)" - } - else - { - return (Start-PreUpdateAppConfigurationApp $obj $objectType $curObject $fromObj) - } - - if($obj.Apps) - { - try - { - Write-Log "Update App Protection Apps" - - $apps = [PSCustomObject]@{ - appGroupType = $obj.appGroupType - apps = @($obj.Apps) - } - $json = $apps | ConvertTo-Json -Depth 20 - - Invoke-GraphRequest -Url "$api/targetApps" -Content $json -HttpMethod POST | Out-Null - } - catch {} - - Remove-Property $obj "apps" - } - - @{ "API" = $api } - -} -#endregion - -#region App Configuration -function Start-PostExportAppConfiguration -{ - param($obj, $objectType, $path) - - #Add-EMAssignmentsToExportFile $obj $objectType $path - - Write-Log "Export app config for $($objectType.Id) with OData.Type: $($obj.'@OData.Type')" - - if($obj.'@OData.Type' -eq "#microsoft.graph.androidManagedAppProtection" -or - $obj.'@OData.Type' -eq "#microsoft.graph.androidForWorkMobileAppConfiguration" -or - $obj.'@OData.Type' -eq "#microsoft.graph.androidManagedStoreAppConfiguration" -or - $obj.'@OData.Type' -eq "#microsoft.graph.iosMobileAppConfiguration") - { - $fileName = (Get-GraphObjectName $obj $objectType).Trim('.') - if((Get-SettingValue "AddIDToExportFile") -eq $true -and $obj.Id) - { - $fileName = ($fileName + "_" + $obj.Id) - } - $tmpObj = $null - $fileName = "$path\$((Remove-InvalidFileNameChars $fileName)).json" - if([IO.File]::Exists($fileName)) - { - $tmpObj = Get-GraphObjectFromFile $fileName - } - else - { - Write-Log "File not found: $fileName. Could not add App names." 3 - } - - if(($tmpObj.targetedMobileApps | measure).Count -gt 0) - { - Write-Log "Add target apps info" - $targetedApps = @() - foreach($appId in $tmpObj.targetedMobileApps) - { - $appObj = Invoke-GraphRequest -Url "/deviceAppManagement/mobileApps/$($appId)" #?`select=id,displayName" -ODataMetadata "Minimal" - if($appObj) - { - Write-Log "Add target app info $($appObj.displayName) ($($appObj.Id)) of type $($appObj.'@OData.Type')" - $targetedApps += $appObj.displayName + '|!|' + $appObj.Id + '|!|' + $appObj.'@OData.Type' - } - } - - if($targetedApps.Count -gt 0) - { - Write-Log "Add CustomRefTargetedApps property" - $tmpObj | Add-Member -MemberType NoteProperty -Name "#CustomRefTargetedApps" -Value ($targetedApps -join "|*|") - Write-Log "Save file $fileName" - Save-GraphObjectToFile $tmpObj $fileName - } - } - else - { - Write-Log "No target apps found" 2 - } - } -} - -function Start-PreFilesImportAppConfiguration -{ - param($objectType, $filesToImport) - - $targetedAppsObjects = $filesToImport | Where { $null -ne $_.Object."#CustomRefTargetedApps" } - - if(($targetedAppsObjects | measure).Count -gt 0) - { - Write-Log "Policies with Targeted Apps detected" - foreach($fileObject in $targetedAppsObjects) - { - Add-AppConfigurationTargets $objectType $fileObject - } - } - $filesToImport -} - -function local:Add-AppConfigurationTargets -{ - param($obj, $fileObj) - - if($fileObj.Object."#CustomRefTargetedApps" -and $fileObj.Object.targetedMobileApps) - { - Write-Log "Adding app target for $($fileObj.Object.displayName)" - - $targetedAppsInfo = $fileObj.Object."#CustomRefTargetedApps" - - $translatedTargetedApps = @() - - if($targetedAppsInfo) - { - foreach($targetedApp in ($targetedAppsInfo -split "[|][*][|]")) - { - $appName, $appId, $appType = $targetedApp -split "[|][!][|]" - if(-not $appName -or -not $appId) - { - Write-Log "App Name and Id is missing in string: $targetedApp" 2 - continue - } - $tmpApps = (Invoke-GraphRequest -Url "/deviceAppManagement/mobileApps?`$filter=displayName eq '$appName'").value - if(-not $tmpApps) - { - Write-Log "No application found with name $appName. $appId will not be translated and added to target list" 2 - continue - } - - Write-Log "Found $(($tmpApps | measure).Count) applications" 2 - foreach ($tmpApp in $tmpApps) { - Write-Log "Found '$($tmpApp.displayName)' ($($tmpApp.id)) of type $($($tmpApp.'@OData.Type'))" - } - - $tmpApp = $tmpApps | Where-Object '@OData.Type' -eq $appType - if(-not $tmpApp) - { - Write-Log "No $appName application found of type $appType. $appId will not be translated and added to target list" 2 - } - elseif(($tmpApp | measure).Count -gt 1) { - Write-Log "$(($tmpApp | measure).Count) applications found with name '$appName' of type $appType. $appId will not be translated and added to target list" 2 - } - else { - Write-Log "Found '$appName' with id $($tmpApp.Id) ($appType)" - $translatedTargetedApps += $tmpApp.Id - } - } - - if($translatedTargetedApps.Count -gt 0) { - Write-Log "Updating translated targeted apps" - $fileObj.Object.targetedMobileApps = $translatedTargetedApps - } - else { - Write-Log "Could not find targeted apps in the evnironment. Verify that they are added. Policy import might fail" 3 - } - } - } -} - -function Start-PreImportAssignmentsAppConfiguration -{ - param($obj, $objectType, $file, $assignments) - - @{"API"="/deviceAppManagement/mobileAppConfigurations/$($obj.Id)/microsoft.graph.managedDeviceMobileAppConfiguration/assign"} -} -#endregon - -#region Applications - -function Start-PostCopyApplication -{ - param($objCopyFrom, $objNew, $objectType) - - Start-ImportApp $objNew - Start-AddInstallScripts $objNew $objCopyFrom - Write-Status "" -} - -function Start-PostFileImportApplication -{ - param($obj, $objectType, $file) - - $tmpObj = Get-GraphObjectFromFile $file - - if(-not ($obj.PSObject.Properties | Where Name -eq '@odata.type')) - { - # Add @odata.type property if it is missing. Required by app package import - $obj | Add-Member -MemberType NoteProperty -Name '@odata.type' -Value $objectType.'@odata.type' - } - - $fi = [IO.FileInfo]$file - $tmpFilName = $fi.DirectoryName + "\" + $obj.FileName - - if([IO.File]::Exists($tmpFilName) -eq $false) - { - $tmpFilName = $null - } - - Start-ImportApp $obj $tmpFilName - Start-AddInstallScripts $obj $tmpObj -} - -function local:Start-ImportApp -{ - param($obj, $packageFile = $null) - - if(-not $obj.'@odata.type') { return } - - if($null -eq $packageFile) - { - $pkgPath = Get-SettingValue "EMIntuneAppPackages" - - if(-not $pkgPath -or [IO.Directory]::Exists($pkgPath) -eq $false) - { - Write-LogDebug "Package source directory is either missing or does not exist" 2 - return - } - - $packageFile = "$($pkgPath)\$($obj.fileName)" - } - $fi = [IO.FileInfo]$packageFile - - if($fi.Exists -eq $false) - { - Write-LogDebug "Package source file $($fi.FullName) not found" 2 - return - } - - Write-Status "Import appliction package file $($fi.FullName)" - Write-Log "Import application file '$($($fi.FullName))' for $($obj.displayName)" - - $appType = $obj.'@odata.type'.Trim('#') - - if($appType -eq "microsoft.graph.win32LobApp") - { - $fileEncryptionInfo = Copy-Win32LOBPackage $packageFile $obj - } - elseif($appType -eq "microsoft.graph.windowsMobileMSI") - { - $fileEncryptionInfo = Copy-MSILOB $packageFile $obj - } - elseif($appType -eq "microsoft.graph.windowsUniversalAppX") - { - $fileEncryptionInfo = Copy-MSIXLOB $packageFile $obj - } - elseif($appType -eq "microsoft.graph.iosLOBApp") - { - $fileEncryptionInfo = Copy-iOSLOB $packageFile $obj - } - elseif($appType -eq "microsoft.graph.androidLOBApp") - { - $fileEncryptionInfo = Copy-AndroidLOB $packageFile $obj - } - else - { - Write-Log "Unsupported application type $appType. File will not be uploaded" 2 - } - - if((Get-SettingValue "EMSaveEncryptionFile") -eq $true) - { - if($fileEncryptionInfo) - { - $jsonEncryptionInfo = $fileEncryptionInfo | ConvertTo-Json -Depth 10 - - $pkgPath = Get-SettingValue "EMIntuneAppDownloadFolder" (Get-SettingValue "EMIntuneAppPackages") - if($pkgPath -and [IO.Directory]::Exists($pkgPath)) - { - $obj = Invoke-GraphRequest -Url "/deviceAppManagement/mobileApps/$($obj.id)" -ODataMetadata "Minimal" - $fullPath = $pkgPath + "\$($obj.displayName)_$($obj.id)_$($obj.committedContentVersion).json" - $jsonEncryptionInfo | Out-File -FilePath $fullPath -Force -Encoding utf8 - } - } - } -} - -function local:Start-AddInstallScripts -{ - param($obj, $fromAppObj) - - if($fromAppObj -and ($fromAppObj.activeInstallScript."#ScriptInfo" -or $fromAppObj.activeUninstallScript."#ScriptInfo")) - { - Write-Log "Importing scripts for $($obj.displayName)" - - $scriptsAdded = $false - $jsonData = @{} - $jsonData."@odata.type" = "#microsoft.graph.win32LobApp" - $jsonData."committedContentVersion" = "1" - - foreach ($scriptType in @('activeInstallScript','activeUninstallScript')) { - $scriptInfo = $fromAppObj.$scriptType.'#ScriptInfo' - if (-not $scriptInfo) { continue } - - Write-Log "Add $($scriptType -replace '^active','') script: $($scriptInfo.displayName)" - - $json = [ordered]@{ - '@odata.type' = $scriptInfo.'@odata.type' - displayName = $scriptInfo.displayName - enforceSignatureCheck = $scriptInfo.enforceSignatureCheck - runAs32Bit = $scriptInfo.runAs32Bit - content = $scriptInfo.content - } | ConvertTo-Json -Depth 10 -Compress - - $scriptObject = Invoke-GraphRequest -Url "deviceAppManagement/mobileApps/$($obj.id)/microsoft.graph.win32LobApp/contentVersions/1/scripts" -Method POST -Content $json - - if ($scriptObject) { - $jsonData.$scriptType = @{ targetId = $scriptObject.Id } - $scriptsAdded = $true - } - } - - $i = 0 - while($true) - { - $scripts = Invoke-GraphRequest -Url "deviceAppManagement/mobileApps/$($obj.id)/microsoft.graph.win32LobApp/contentVersions/1/scripts" - if(-not $scripts) - { - Write-Log "Failed to retrieve scripts for app after adding. Skipping Install/Uninstall script config." 2 - return - } - - if(($scripts.value.state | Select -Unique) -eq "commitSuccess") - { - Write-Log "Scripts added successfully" - break - } - if($i -ge 12) - { - Write-Log "Install/Uninstall scripts are still not in pending state after waiting for 1 minute." 3 - return - } - - Write-Log "Waiting for scripts to be added..." - Start-Sleep -Seconds 5 - $i++ - } - - if($scriptsAdded) - { - Write-Log "Add script info to app" - $json = ConvertTo-Json $jsonData -Depth 10 - $status = Invoke-GraphRequest -Url "deviceAppManagement/mobileApps/$($obj.id)" -Method PATCH -Body $json - if($status -eq $true) - { - Write-Log "Install/Uninstall script info updated successfully" - } - else - { - Write-Log "Failed to update Install/Uninstall script info" 2 - } - } - } -} - -function Start-PreUpdateApplication -{ - param($obj, $objectType, $curObject, $fromObj) - - if($curObject.Object.'@OData.type' -eq "#microsoft.graph.windowsMobileMSI") - { - Remove-Property $obj "useDeviceContext" - } - elseif($curObject.Object.'@OData.type' -eq "#microsoft.graph.officeSuiteApp") - { - Remove-Property $obj "officeConfigurationXml" - Remove-Property $obj "officePlatformArchitecture" - Remove-Property $obj "developer" - Remove-Property $obj "owner" - Remove-Property $obj "publisher" - } - - Remove-Property $obj "appStoreUrl" -} - -function Start-PreImportCommandApplication -{ - param($obj, $objectType, $file, $assignments) - - if($obj.'@OData.Type' -in @('#microsoft.graph.microsoftStoreForBusinessApp','#microsoft.graph.androidStoreApp')) - { - Write-Log "App type '$($obj.'@OData.Type')' not supported for import" 2 - @{ "Import" = $false } - } - - if($obj.'@OData.Type' -eq '#microsoft.graph.officeSuiteApp') - { - if($obj.officeSuiteAppDefaultFileFormat -eq "notConfigured") - { - $obj.officeSuiteAppDefaultFileFormat = "officeOpenXMLFormat" - } - } - - if($obj.activeInstallScript) { $obj.activeInstallScript = $null } - if($obj.activeUninstallScript) { $obj.activeUninstallScript = $null } -} - -function Add-DetailExtensionApplications -{ - param($form, $buttonPanel, $index = 0) - - $btnUpload = New-Object System.Windows.Controls.Button - $btnUpload.Content = 'Upload' - $btnUpload.Name = 'btnUploadAppfile' - $btnUpload.Margin = "0,0,5,0" - $btnUpload.Width = "100" - - $btnUpload.Add_Click({ - if($global:dgObjects.SelectedItem.Object.publishingState -ne "notPublished") - { - # Only allow upload of not published apps - # Use portal to replace app file... - if(([System.Windows.MessageBox]::Show("Are you sure you want to upload a new file for the app?`n`nApplication:`n$($global:dgObjects.SelectedItem.Object.displayName)", "Update app file?", "YesNo", "Warning")) -ne "Yes") - { - return - } - } - - $pkgPath = Get-SettingValue "EMIntuneAppPackages" - - $of = [System.Windows.Forms.OpenFileDialog]::new() - $of.FileName = $global:dgObjects.SelectedItem.Object.fileName - $of.DefaultExt = "*.intunewin" - $of.Filter = "Intune Win32 (*.intunewin)|*.*" - $of.Multiselect = $false - - if($pkgPath -and [IO.Directory]::Exists($pkgPath)) - { - $of.InitialDirectory = $pkgPath - } - - if($of.ShowDialog() -eq "OK") - { - Write-Status "Import $($global:dgObjects.SelectedItem.Object.displayName) file" - Start-ImportApp $global:dgObjects.SelectedItem.Object $of.FileName - Write-Status "" - } - }) - - $tmp = $form.FindName($buttonPanel) - if($tmp) - { - $tmp.Children.Insert($index, $btnUpload) - } - - $btnDownload = New-Object System.Windows.Controls.Button - $btnDownload.Content = 'Download' - $btnDownload.Name = 'btnDownloadAppfile' - $btnDownload.Margin = "0,0,5,0" - $btnDownload.Width = "100" - - $btnDownload.Add_Click({ - Write-Status "Download file" - $obj = $global:dgObjects.SelectedItem.Object - #$obj = Invoke-GraphRequest -Url "/deviceAppManagement/mobileApps/$($obj.id)" - - $pkgPath = Get-SettingValue "EMIntuneAppDownloadFolder" (Get-SettingValue "EMIntuneAppPackages") - - $dlgSave = [System.Windows.Forms.SaveFileDialog]::new() - $dlgSave.InitialDirectory = $pkgPath - $dlgSave.FileName = ($obj.FileName + ".encrypted") - $dlgSave.DefaultExt = "*.encrypted" - $dlgSave.Filter = "Encrypted intunewin (*.encrypted)|*.encrypted|All files (*.*)|*.*" - - if($dlgSave.ShowDialog() -eq [System.Windows.Forms.DialogResult]::OK -and $dlgSave.Filename) - { - $contentFileObj = Start-DownloadAppContent $obj $dlgSave.FileName - - if([IO.File]::Exists($dlgSave.FileName)) - { - $fullPath = Find-AppEncryptionFile $obj $contentFileObj $pkgPath - if([IO.File]::Exists($fullPath) -eq $false) - { - if(([System.Windows.MessageBox]::Show("Could not find decryption file for $($obj.displayName)`nApp Id: $($obj.id)`nContent version $($obj.committedContentVersion)`n`nDo you want to browse for the file?", "Encryption file not found", "YesNo", "Warning")) -eq "Yes") - { - $of = [System.Windows.Forms.OpenFileDialog]::new() - $of.InitialDirectory = $pkgPath - $of.DefaultExt = "*.json" - $of.Filter = "Json (*.json)|*.json" - $of.Multiselect = $false - - if($of.ShowDialog() -eq "OK") - { - $fullPath = $of.FileName - } - } - } - - if([IO.File]::Exists($fullPath)) - { - Write-Status "Decrypting file" - $encryptionInfo = ConvertFrom-Json (Get-Content -Path $fullPath -Raw) - if($encryptionInfo.fileEncryptionInfo) - { - $encryptionInfo = $encryptionInfo.fileEncryptionInfo - } - $destination = $pkgPath + "\$($obj.FileName)" - Start-DecryptFile $dlgSave.Filename $destination $encryptionInfo.encryptionKey $encryptionInfo.initializationVector - try { [IO.File]::Delete($dlgSave.Filename) } - catch { - Write-LogError "Failed to delete exported encrypted file" $_.Exception - } - } - else - { - Write-Log "Decryption file for $($obj.displayName) not found. Skipping decryption" 2 - } - } - } - - Write-Status "" - }) - - $tmp = $form.FindName($buttonPanel) - if($tmp) - { - $tmp.Children.Insert($index, $btnDownload) - } -} - -function Find-AppEncryptionFile -{ - param($obj, $contentFileObj, $rootFolders) - - $search = @() - $search += "$($obj.displayName)_$($obj.id)_$($obj.committedContentVersion)" - $search += "$([IO.Path]::GetFileNameWithoutExtension($obj.fileName))_$($contentFileObj.size)" - $search += "$($obj.displayName)_$($contentFileObj.size)" - - foreach($rootFolder in $rootFolders) - { - foreach($searchName in $search) - { - $fullName = ($rootFolder + "\$($searchName).json") - if([IO.File]::Exists($fullName)) - { - return $fullName - } - } - } -} - -function Start-PreImportAssignmentsApplications -{ - param($obj, $objectType, $file, $assignments) - - if($obj.'@odata.type' -eq "#microsoft.graph.windowsMicrosoftEdgeApp") - { - foreach($assignment in $assignments) - { - Remove-Property $assignment.target "deviceAndAppManagementAssignmentFilterId" - Remove-Property $assignment.target "deviceAndAppManagementAssignmentFilterType" - } - @{"Assignments"=$assignments} - } - elseif($obj.'@odata.type' -eq "#microsoft.graph.winGetApp") - { - Write-LogDebug "Wait for app to be published" - $i = 2 - Start-Sleep -s ($i) - $x = 0 - while($x -lt 10) - { - ###!!! - $appInfo = Invoke-GraphRequest -Url "$($objectType.API)/$($obj.id)" -ODataMetadata "skip" - if($appInfo.publishingState -eq "Published") - { - Write-LogDebug "Application $($obj.displayName) is published" - return - } - Start-Sleep -s ($i) - $x++ - if($x -ge 5) { $i++ } - } - - Write-Log "Application '$($obj.displayName)' is not published. Skipping assignment" 2 - @{"Import"=$false} - } -} - -function Start-PreDeleteApplications -{ - param($obj, $objectType) - - if($obj.'@odata.type' -eq "#microsoft.graph.microsoftStoreForBusinessApp") - { - # Don't delete Microsoft Store for Business Apps - @{ "Delete" = $false } - } -} - -function Start-PostExportApplications -{ - param($obj, $objectType, $path) - - if($global:chkExportScript.IsChecked) - { - $fileName = Get-GraphObjectFile $obj $objectType - $fi = [IO.FileInfo]"$path\$fileName" - - try - { - foreach($rule in ($obj.detectionRules | Where '@OData.Type' -eq "#microsoft.graph.win32LobAppPowerShellScriptDetection")) - { - if($rule.ScriptContent) - { - [IO.File]::WriteAllBytes(("$path\$($fi.BaseName)_DetectionScript.ps1"), ([System.Convert]::FromBase64String($rule.ScriptContent))) - - } - } - - foreach($rule in $obj.requirementRules) - { - if($rule.'@OData.Type' -eq "#microsoft.graph.win32LobAppPowerShellScriptRequirement") - { - if($rule.ScriptContent) - { - [IO.File]::WriteAllBytes(("$path\$($fi.BaseName)_RequirementScript.ps1"), ([System.Convert]::FromBase64String($rule.ScriptContent))) - } - } - } - - if($obj.activeInstallScript.'#ScriptInfo'.displayName) - { - [IO.File]::WriteAllBytes(("$path\$($fi.BaseName)_$($obj.activeInstallScript.'#ScriptInfo'.displayName)"), ([System.Convert]::FromBase64String($obj.activeInstallScript.'#ScriptInfo'.content))) - } - - if($obj.activeUninstallScript.'#ScriptInfo'.displayName) - { - [IO.File]::WriteAllBytes(("$path\$($fi.BaseName)_$($obj.activeUninstallScript.'#ScriptInfo'.displayName)"), ([System.Convert]::FromBase64String($obj.activeUninstallScript.'#ScriptInfo'.content))) - } - } - catch - { - Write-LogError "Failed to export scripts" $_.Exception - } - } - - Save-Setting "Intune" "ExportAppFile" $global:chkExportApplicationFile.IsChecked - if($global:chkExportApplicationFile.IsChecked) - { - $encryptionSource = Get-SettingValue "EMIntuneAppDownloadFolder" (Get-SettingValue "EMIntuneAppPackages") - $pkgPath = $path - - if($pkgPath) - { - Write-Status "Download file" - - $exportFile = $pkgPath + "\$($obj.FileName).encrypted" - $contentFileObj = Start-DownloadAppContent $obj $exportFile -GetContentFileInfoOnly - $encryptionFile = Find-AppEncryptionFile $obj $contentFileObj $encryptionSource - if($encryptionFile -and [IO.File]::Exists($encryptionFile)) - { - Start-DownloadFile $contentFileObj.azureStorageUri $exportFile - - if([IO.File]::Exists($exportFile)) - { - Write-Status "Decrypting file" - $encryptionInfo = ConvertFrom-Json (Get-Content -Path $encryptionFile -Raw) - if($encryptionInfo.fileEncryptionInfo) - { - $encryptionInfo = $encryptionInfo.fileEncryptionInfo - } - $destination = $pkgPath + "\$($obj.FileName)" - Start-DecryptFile $exportFile $destination $encryptionInfo.encryptionKey $encryptionInfo.initializationVector - } - - try { [IO.File]::Delete($exportFile) } - catch { - Write-LogError "Failed to delete exported encrypted file" $_.Exception - } - } - else - { - Write-Log "Could not find encryption file" - } - } - } -} - -function Start-PostListApplications -{ - param($objList, $objectType) - - foreach($obj in ($objList | Where { $_.Object."@OData.Type" -eq "#microsoft.graph.winGetApp"})) - { - if($obj.Object.packageIdentifier -like "9*") - { - $installerType = "UWP" - } - elseif($obj.Object.packageIdentifier -like "X*") - { - $installerType = "Win32" - } - else - { - $objName = Get-GraphObjectName $obj.Object $objectType - Write-Log "Unknown package identifier for app $($objName): $($obj.Object.packageIdentifier)" 2 - $installerType = "Unknown" - } - $obj.Object | Add-Member -MemberType NoteProperty -Name "InstallerType" -Value $installerType - } - $objList -} - -function Add-ScriptExportApplications -{ - param($form, $buttonPanel, $index = 0) - - Add-ScriptExportExtensions $form $buttonPanel $index - - $ctrl = $form.FindName("chkExportApplicationFile") - if(-not $ctrl) - { - $xaml = @" - - -"@ - $label = [Windows.Markup.XamlReader]::Parse($xaml) - - $global:chkExportApplicationFile = [System.Windows.Controls.CheckBox]::new() - $global:chkExportApplicationFile.IsChecked = ((Get-Setting "Intune" "ExportAppFile" "false") -eq "true") - $global:chkExportApplicationFile.VerticalAlignment = "Center" - $global:chkExportApplicationFile.Name = "chkExportApplicationFile" - - @($label, $global:chkExportApplicationFile) - } -} - -function Start-PostGetApplications { - param($obj, $objectType) - - if(($obj.Object.dependentAppCount -as [int]) -gt 0 -or ($obj.Object.supersededAppCount -as [int]) -gt 0) { - $relationships = (Invoke-GraphRequest -Url "/deviceAppManagement/mobileApps/$($obj.Id)/relationships?`$filter=targetType%20eq%20microsoft.graph.mobileAppRelationshipType%27child%27").value - $dependencyApps = @() - $supersededApps = @() - foreach ($rel in $relationships) { - if ($rel."@odata.type" -eq "#microsoft.graph.mobileAppDependency") { - $dependencyApps += "$($rel.targetDisplayName)|!|$($rel.targetDisplayVersion)|!|$($rel.targetId)|!|$($rel.dependencyType)" - } - elseif ($rel."@odata.type" -eq "#microsoft.graph.mobileAppSupersedence") { - $supersededApps += "$($rel.targetDisplayName)|!|$($rel.targetDisplayVersion)|!|$($rel.targetId)|!|$($rel.supersedenceType)" - } - } - if ($dependencyApps.Count -gt 0) { - $obj.Object | Add-Member -MemberType NoteProperty -Name "#CustomRefDependency" -Value ($dependencyApps -join "|*|") - } - - if ($supersededApps.Count -gt 0) { - $obj.Object | Add-Member -MemberType NoteProperty -Name "#CustomRefSupersedence" -Value ($supersededApps -join "|*|") - } - } - - if($obj.Object.'@odata.type' -eq "#microsoft.graph.win32LobApp") - { - if($obj.Object.activeInstallScript.targetId -or $obj.Object.activeUninstallScript.targetId) - { - $scriptInfo = (Invoke-GraphRequest -Url "/deviceAppManagement/mobileApps/$($obj.Id)/microsoft.graph.win32LobApp/contentVersions/$($obj.Object.committedContentVersion)/scripts/").value - - foreach($script in $scriptInfo) { - $scriptFullInfo = (Invoke-GraphRequest -Url "/deviceAppManagement/mobileApps/$($obj.Id)/microsoft.graph.win32LobApp/contentVersions/$($obj.Object.committedContentVersion)/scripts/$($script.id)?`$select=Id,Content") - if($scriptFullInfo.Content) - { - $script.content = $scriptFullInfo.Content - } - - if($obj.Object.activeInstallScript.targetId -eq $script.id) - { - $tpObject = $obj.Object.activeInstallScript - } - elseif($obj.Object.activeUninstallScript.targetId -eq $script.id) - { - $tpObject = $obj.Object.activeUninstallScript - } - else - { - Write-Log "Script with id $($script.id) is not referenced by active install or uninstall script. Skipping." 2 - continue - } - $tpObject | Add-Member -MemberType NoteProperty -Name "#ScriptInfo" -Value $script -Force - } - } - } -} - -function Start-PostImportApplications -{ - param($obj, $objectType, $file) - - #$tmpObj = Get-GraphObjectFromFile $file -} - -function Start-PostFilesImportApplications -{ - param($objType, $importedObjects, $importedFiles) - - $refObjects = $importedFiles | Where { $null -ne $_.Object."#CustomRefDependency" -or $null -ne $_.Object."#CustomRefSupersedence" } - - if(($refObjects | measure).Count -gt 0) - { - Write-Log "Applicetions with Dependency or Supersedence detected" - foreach($file in $refObjects) - { - Add-ApplicationReferences $file.ImportedObject $file.Object - } - } -} - -function local:Add-ApplicationReferences -{ - param($obj, $fileObj) - - if($fileObj."#CustomRefDependency" -or $fileObj."#CustomRefSupersedence") - { - Write-Log "Adding app references for $($obj.displayName)" - - $depAppsInfo = $fileObj."#CustomRefDependency" - $supAppsInfo = $fileObj."#CustomRefSupersedence" - - $releationShips = [PSCustomObject]@{ - relationships = @() - } - - if($depAppsInfo) - { - foreach($depApp in ($depAppsInfo -split "[|][*][|]")) - { - $appName, $appVer, $appId, $appType = $depApp -split "[|][!][|]" - if(-not $appName -or -not $appVer) - { - Write-Log "Could not get Name and Version from string: $appApp" 2 - continue - } - $tmpApps = (Invoke-GraphRequest -Url "/deviceAppManagement/mobileApps?`$filter=displayName eq '$appName'").value - if(-not $tmpApps) - { - Write-Log "No application found with name $appName" 2 - continue - } - $tmpApp = $tmpApps | Where displayVersion -eq $appVer - if(-not $tmpApp) - { - Write-Log "No $appName application found with version $appVer" 2 - continue - } - elseif(($tmpApp | measure).Count -gt 1) - { - Write-Log "Multiple $appName applications found with version $appVer" 2 - continue - } - Write-Log "Add $appName ($appVer) to Dependency list" - $releationShips.relationships += [PSCustomObject]@{ - "@odata.type" = "#microsoft.graph.mobileAppDependency" - targetId = $tmpApp.Id - dependencyType = $appType - } - } - } - - if($supAppsInfo) - { - foreach($suppApp in ($supAppsInfo -split "[|][*][|]")) - { - $appName, $appVer, $appId, $appType = $suppApp -split "[|][!][|]" - if(-not $appName -or -not $appVer) - { - Write-Log "Could not get Name and Version from string: $suppApp" 2 - continue - } - $tmpApps = (Invoke-GraphRequest -Url "/deviceAppManagement/mobileApps?`$filter=displayName eq '$appName'").value - if(-not $tmpApps) - { - Write-Log "No application found with name $appName" 2 - continue - } - $tmpApp = $tmpApps | Where displayVersion -eq $appVer - if(-not $tmpApp) - { - Write-Log "No $appName application found with version $appVer" 2 - continue - } - elseif(-not ($tmpApp | measure).Count -gt 1) - { - Write-Log "Multiple $appName application found with version $appVer" 2 - continue - } - Write-Log "Add $appName ($appVer) to Supersedence list" - $releationShips.relationships += [PSCustomObject]@{ - "@odata.type" = "#microsoft.graph.mobileAppSupersedence" - targetId = $tmpApp.Id - supersedenceType = $appType - } - } - } - - if($releationShips.relationships.Count -gt 0) - { - $json = Update-JsonForEnvironment (ConvertTo-Json $releationShips -Depth 20) - - Write-Log "Update app references" - Invoke-GraphRequest -Url "/deviceAppManagement/mobileApps/$($obj.Id)/updateRelationships" -Method "POST" -Body $json - } - } -} - -#endregion - -#region Group Policy/Administrative Templates functions -function Get-GPOObjectSettings -{ - param($GPOObj) - - $gpoSettings = @() - - if ($GPOObj.policyConfigurationIngestionType -eq "unknown") { - $tmpObj = (Invoke-GraphRequest -Url "/deviceManagement/groupPolicyConfigurations?`$filter=id eq '$($GPOObj.id)'").value[0] - if ($tmpObj.policyConfigurationIngestionType) { - $GPOObj.policyConfigurationIngestionType = $tmpObj.policyConfigurationIngestionType - } - } - - # Get all configured policies in the Administrative Templates profile - $GPODefinitionValues = Invoke-GraphRequest -Url "/deviceManagement/groupPolicyConfigurations/$($GPOObj.id)/definitionValues?`$expand=definition" -ODataMetadata "skip" - foreach($definitionValue in $GPODefinitionValues.value) - { - # Get presentation values for the current settings (with presentation object included) - $presentationValues = Invoke-GraphRequest -Url "/deviceManagement/groupPolicyConfigurations/$($GPOObj.id)/definitionValues/$($definitionValue.id)/presentationValues?`$expand=presentation" -ODataMetadata "skip" - - # Set base policy settings - $obj = @{ - "enabled" = $definitionValue.enabled - "definition@odata.bind" = "$($global:graphURL)/deviceManagement/groupPolicyDefinitions('$($definitionValue.definition.id)')" - } - - if($definitionValue.definition.categoryPath) - { - $obj.Add("#Definition_Id", $definitionValue.definition.id) - $obj.Add("#Definition_displayName", $definitionValue.definition.displayName) - $obj.Add("#Definition_classType", $definitionValue.definition.classType) - $obj.Add("#Definition_categoryPath", $definitionValue.definition.categoryPath) - } - - if($presentationValues.value) - { - # Policy presentation values set e.g. a drop down list, check box, text box etc. - $obj.presentationValues = @() - - foreach ($presentationValue in $presentationValues.value) - { - # Add presentation@odata.bind property that links the value to the presentation object - $presentationValue | Add-Member -MemberType NoteProperty -Name "presentation@odata.bind" -Value "$($global:graphURL)/deviceManagement/groupPolicyDefinitions('$($definitionValue.definition.id)')/presentations('$($presentationValue.presentation.id)')" - - if($definitionValue.definition.categoryPath) - { - $presentationValue | Add-Member -MemberType NoteProperty -Name "#Presentation_Id" -Value $presentationValue.presentation.id - $presentationValue | Add-Member -MemberType NoteProperty -Name "#Presentation_Label" -Value $presentationValue.presentation.label - } - #Remove presentation object so it is not included in the export - Remove-ObjectProperty $presentationValue "presentation" - - #Optional removes. Import will igonre them - Remove-ObjectProperty $presentationValue "id" - Remove-ObjectProperty $presentationValue "lastModifiedDateTime" - Remove-ObjectProperty $presentationValue "createdDateTime" - - # Add presentation value to the list - $obj.presentationValues += $presentationValue - } - } - $gpoSettings += $obj - } - $gpoSettings -} - -function Import-GPOSetting -{ - param($obj, $settings) - - if($obj) - { - Write-Status "Import settings for $($obj.displayName)" - - $hasCustomADMX = $null -ne ($settings | Where { $null -ne $_.'#Definition_categoryPath' }) - - if($hasCustomADMX) - { - Write-Status "Import custom ADMX settings" - if(-not $script:CustomADMXDefinitions) - { - $tmpCustomCategories = Invoke-GraphRequest -Url "deviceManagement/groupPolicyCategories?`$expand=definitions(`$select=id, displayName, categoryPath, classType)&`$select=id, displayName&`$filter=ingestionSource eq 'custom'" -ODataMetadata "Minimal" - if($tmpCustomCategories.Value) - { - $script:CustomADMXDefinitions = @{} - foreach($tmpCat in $tmpCustomCategories.Value) - { - foreach($tmpDef in $tmpCat.definitions) - { - $key = ($tmpDef.displayName + $tmpDef.categoryPath + $tmpDef.classType).ToLower() - $val = [PSCustomObject]@{ - Definition = $tmpDef - Category = $tmpCat - Presentations = $null - } - try { - $script:CustomADMXDefinitions.Add($key, $val) - } - catch { - Write-Log "Failed to add '$($tmpDef.displayName)' in category '$($tmpDef.categoryPath)' of class $($tmpDef.classType)" 3 - } - } - } - } - } - } - - foreach($setting in $settings) - { - if($setting.'#Definition_categoryPath' -and $script:CustomADMXDefinitions -is [HashTable] -and $script:CustomADMXDefinitions.Count -gt 0) - { - $defVal = $null - $key = ($setting.'#Definition_displayName' + $setting.'#Definition_categoryPath' + $setting.'#Definition_classType').ToLower() - if($key -and $script:CustomADMXDefinitions.ContainsKey($key)) - { - $defVal = $script:CustomADMXDefinitions[$key] - } - elseif($key) - { - Write-Log "No custom ADMX definitiona found for setting $($setting.'#Definition_displayName')" 2 - } - else - { - Write-Log "Setting $($setting.'#Definition_displayName') does not have information to be imported in the environment" - } - - if($defVal) - { - $setting.'definition@odata.bind' = $setting.'definition@odata.bind' -replace $setting.'#Definition_Id', $defVal.Definition.Id - if(($setting.presentationValues | measure).Count -gt 0) - { - if(-not $defVal.Presentations) - { - $tmpPresentation = Invoke-GraphRequest -Url "deviceManagement/groupPolicyDefinitions/$($defVal.Definition.Id)/presentations" -ODataMetadata "Minimal" - if($tmpPresentation.value) - { - foreach($settingPresentation in $setting.presentationValues) - { - $tmpPresentationVal = $tmpPresentation.value | Where label -eq $settingPresentation.'#Presentation_Label' - if($tmpPresentationVal) - { - $settingPresentation.'presentation@odata.bind' = $settingPresentation.'presentation@odata.bind' -replace $setting.'#Definition_Id', $defVal.Definition.Id - $settingPresentation.'presentation@odata.bind' = $settingPresentation.'presentation@odata.bind' -replace $settingPresentation.'#Presentation_Id', $tmpPresentationVal.Id - } - else - { - Write-Log "Could not find a presentation value with label $($settingPresentation.'#Presentation_Label'). Setting will not be configured" 2 - continue - } - } - } - else - { - Write-Log "Could not find presentation for setting $($settingPresentation.'#Presentation_Label'). Setting will not be configured." 2 - continue - } - } - } - } - else - { - Write-Log "Settings might not be available if imported in another environment" 3 - } - } - elseif($setting.'#Definition_categoryPath') - { - Write-Log "Custom AMDX settings cannot be imported without ADMX file imported. Definitions not found" 2 - continue - } - - Start-GraphPreImport $setting - - if($true) - { - foreach($tmpProp in (($setting.PSObject.Properties | Where Name -like "#*").Name)) - { - Remove-Property $setting $tmpProp - } - - foreach($settingPresentation in $setting.presentationValues) - { - foreach($tmpProp in (($settingPresentation.PSObject.Properties | Where Name -like "#*").Name)) - { - Remove-Property $settingPresentation $tmpProp - } - } - } - - # Import each setting for the Administrative Template profile - Invoke-GraphRequest -Url "/deviceManagement/groupPolicyConfigurations/$($obj.id)/definitionValues" -Content (ConvertTo-Json $setting -Depth 20) -HttpMethod POST | Out-Null - } - } -} - -function Start-PostExportAdministrativeTemplate -{ - param($obj, $objectType, $path) - - $fileName = (Get-GraphObjectName $obj $objectType).Trim('.') - if((Get-SettingValue "AddIDToExportFile") -eq $true -and $obj.Id) - { - $fileName = ($fileName + "_" + $obj.Id) - } - - if($obj.definitionValues) - { - $settings = $obj.definitionValues - } - else - { - $settings = Get-GPOObjectSettings $obj - } - - $fileName = "$path\$((Remove-InvalidFileNameChars $fileName))_Settings.json" - Save-GraphObjectToFile $settings $fileName -} - -function Start-PostCopyAdministrativeTemplate -{ - param($objCopyFrom, $objNew, $objectType) - - $settings = Get-GPOObjectSettings $objCopyFrom - if($settings) - { - Import-GPOSetting $objNew $settings - } -} - -function Start-PostFileImportAdministrativeTemplate -{ - param($obj, $objectType, $file) - - $settings = Get-EMSettingsObject $obj $objectType $file -settingsProperty "definitionValues" -SettingsArray - if($settings) - { - $tmpObj = Get-GraphObjectFromFile $file - - Import-GPOSetting $obj $settings - } -} - -function Start-LoadAdministrativeTemplate -{ - param($fileName) - - if(-not $fileName) { return $null } - - $fi = [IO.FileInfo]$fileName - if($fi.Exists -eq $false) { return } - - $obj = Get-GraphObjectFromFile $fi.FullName - - if($obj.definitionValues) - { - return $obj - } - - $settingsFile = $fi.DirectoryName + "\" + $fi.BaseName + "_Settings.json" - - if([IO.File]::Exists($settingsFile)) - { - $definitionValues = Get-GraphObjectFromFile $settingsFile - - $obj | Add-Member Noteproperty -Name "definitionValues" -Value $definitionValues -Force - } - $obj -} - -function Start-PostGetAdministrativeTemplate -{ - param($obj, $objectType) - - $definitionValues = Get-GPOObjectSettings $obj.Object - if($definitionValues) - { - $obj.Object | Add-Member Noteproperty -Name "definitionValues" -Value $definitionValues -Force - } - <# - # Leave for now. This only loads the configured definition values and not the values specified. - # That would require enumerating each definition value which takes time. - $definitionValues = (Invoke-GraphRequest "deviceManagement/groupPolicyConfigurations('$($obj.Id)')/definitionValues?`$expand=definition(`$select=id,classType,displayName,policyType,groupPolicyCategoryId)" -ODataMetadata "minimal").value - - if($definitionValues) - { - $obj.Object | Add-Member Noteproperty -Name "definitionValues" -Value $definitionValues -Force - } - #> -} - -function Start-PreImportAdministrativeTemplate -{ - param($obj, $objectType, $file, $assignments) - - -} - -#endregion - -#region Policy Sets function - -function Start-PreImportAssignmentsPolicySets -{ - param($obj, $objectType, $file, $assignments) - - @{"API"="$($objectType.API)/$($obj.Id)/Update"} -} - -function Start-PreImportPolicySets -{ - param($obj, $objectType) - - @("items@odata.context","status","errorCode") | foreach { Remove-Property $obj $_ } - - # Properties to keep for items - $keepProperties = @("@odata.type","payloadId","intent","settings") - foreach($item in $obj.Items) - { - foreach($prop in ($item.PSObject.Properties | Where {$_.Name -notin $keepProperties})) - { - Remove-Property $item $prop.Name - } - #@("itemType","displayName","status","errorCode") | foreach { Remove-Property $item $_ } - } -} - -function Start-PreUpdatePolicySets -{ - param($obj, $objectType, $curObject, $fromObj) - - Start-PreImportPolicySets $obj $objectType - - $curObject = Get-GraphObject $curObject.Object $objectType - - # Update ref object in the json - # Used when importing in a different environment - $jsonObj = ConvertTo-Json $obj -Depth 15 - $updateObj = Update-JsonForEnvironment $jsonObj | ConvertFrom-Json - - $addedItems = @() - $updatedItems = @() - $deletedItems = @() - - foreach($item in $updateObj.items) - { - if(($curObject.Object.items | Where payloadId -eq $item.payloadId)) - { - $updatedItems += $item - } - else - { - $addedItems += $item - } - } - - foreach($item in $curObject.Object.items) - { - if(-not ($updateObj.Items | Where payloadId -eq $item.payloadId)) - { - $deletedItems += $item.id - } - } - - $updateItemObj = [PSCustomObject]@{ - addedPolicySetItems = $addedItems - deletedPolicySetItems = $deletedItems - updatedPolicySetItems = $updatedItems - } - - Write-Log "Update Policy Set items. Add: $($addedItems.Count), Update: $($updatedItems.Count), Delete: $($deletedItems.Count)" - - $updateApi = "/deviceAppManagement/policySets/$($curObject.Object.Id)/update" - $json = $updateItemObj | ConvertTo-Json -Depth 15 - - Invoke-GraphRequest -Url $updateApi -HttpMethod "POST" -Content $json - Remove-Property $obj "items" -} - -function Update-EMPolicySetAssignment -{ - param($assignment, $sourceObject, $newObject, $objectType) - - $api = "/deviceAppManagement/policySets/$($assignment.SourceId)?`$expand=assignments,items" - - $psObj = Invoke-GraphRequest -Url $api -ODataMetadata "Minimal" - - if(-not $psObj) - { - return - } - - $curItem = $psObj.Items | Where payloadId -eq $sourceObject.Id - - if(-not $curItem) - { - return - } - - $api = "/deviceAppManagement/policySets/$($assignment.SourceId)/update" - - $curItemClone = $curItem | ConvertTo-Json -Depth 20 | ConvertFrom-Json - $newItem = $curItem | ConvertTo-Json -Depth 20 | ConvertFrom-Json - $newItem.payloadId = $newObject.Id - if($newItem.guidedDeploymentTags -is [String] -and [String]::IsNullOrEmpty($newItem.guidedDeploymentTags)) - { - $newItem.guidedDeploymentTags = @() - } - - $keepProperties = @('@odata.type','payloadId','Settings','guidedDeploymentTags') - #itemType? e.g. #microsoft.graph.iosManagedAppProtection - #priority? - - foreach($prop in ($newItem.PSObject.Properties | Where {$_.Name -notin $keepProperties})) - { - Remove-Property $newItem $prop.Name - } - - $update = @{} - $update.Add('addedPolicySetItems',@($newItem)) - $update.Add('updatedPolicySetItems', @()) - $update.Add('deletedPolicySetItems',@($curItemClone.Id)) - - $json = $update | ConvertTo-Json -Depth 20 - - Write-Log "Update PolicySet $($psObj.displayName) - Replace: $((Get-GraphObjectName $newObject $objectType))" - - Invoke-GraphRequest -Url $api -HttpMethod "POST" -Content $json -} - -function Start-PostListPolicySets -{ - param($objList, $objectType) - - foreach($obj in $objList) - { - $obj | Add-Member -MemberType NoteProperty -Name "IsAssigned" -Value ($obj.Object.status -ne "notAssigned") - } - $objList -} -#endregion - -#endregion Locations -function Start-PreImportLocations -{ - param($obj, $objectType) - - if($obj.uniqueName) - { - $arr = $obj.uniqueName.Split('_') - if($arr.Length -ge 3) - { - # Locations requires a unique name so generate a new guid and change the uniqueName property - $obj.uniqueName = ($obj.uniqueName.Substring(0,$obj.uniqueName.Length-$arr[-1].Length) + [Guid]::NewGuid().Tostring("n")) - } - } -} -#endregion - -#region RoleDefinitions -function Start-PostExportRoleDefinitions -{ - param($obj, $objectType, $path) - - $fileName = (Get-GraphObjectName $obj $objectType).Trim('.') - if((Get-SettingValue "AddIDToExportFile") -eq $true -and $obj.Id) - { - $fileName = ($fileName + "_" + $obj.Id) - } - $tmpObj = $null - $fileName = "$path\$((Remove-InvalidFileNameChars $fileName)).json" - if([IO.File]::Exists($fileName)) - { - $tmpObj = Get-GraphObjectFromFile $fileName - } - else - { - Write-Log "File not found: $fileName. Could not get role assignments" 3 - } - - if(($tmpObj.RoleAssignments | measure).Count -gt 0) - { - $roleAssignmentsArr = @() - foreach($roleAssignment in $tmpObj.RoleAssignments) - { - $raObj = Invoke-GraphRequest -Url "/deviceManagement/roleAssignments/$($roleAssignment.Id)?`$expand=microsoft.graph.deviceAndAppManagementRoleAssignment/roleScopeTags" -ODataMetadata "Minimal" - if($raObj) - { - foreach($groupId in $raObj.resourceScopes) { Add-GroupMigrationObject $groupId } - foreach($groupId in $raObj.members) { Add-GroupMigrationObject $groupId } - $roleAssignmentsArr += $raObj - } - } - - if($roleAssignmentsArr.Count -gt 0) - { - $tmpObj.RoleAssignments = $roleAssignmentsArr - Save-GraphObjectToFile $tmpObj $fileName - } - } -} - -function Start-PreImportRoleDefinitions -{ - param($obj, $objectType) - - Remove-Property $obj "RoleAssignments" - Remove-Property $obj "RoleAssignments@odata.context" -} - -function Start-PostFileImportRoleDefinitions -{ - param($obj, $objectType, $file) - - $tmpObj = Get-GraphObjectFromFile $file - - $loadedScopeTags = $global:LoadedDependencyObjects["ScopeTags"] - if(($tmpObj.RoleAssignments | measure).Count -gt 0 -and ($loadedScopeTags | measure).Count -gt 0) - { - # Documentation way did not work so use the same way as the portal - # Should be created with /deviceManagement/roleDefinitions/{roleDefinitionId}/roleAssignments - foreach($roleAssignment in $tmpObj.RoleAssignments) - { - $roleAssignmentObj = New-object PSObject @{ - "description" = $roleAssignment.Description - "displayName"= $roleAssignment.DisplayName - "members" = $roleAssignment.members - "resourceScopes" = $roleAssignment.resourceScopes - "roleDefinition@odata.bind" = "https://graph.microsoft.com/beta/deviceManagement/roleDefinitions('$($obj.Id)')" - "roleScopeTags@odata.bind" = @() - } - - foreach($scopeTag in $roleAssignment.roleScopeTags) - { - $scopeMigObj = $loadedScopeTags | Where OriginalId -eq $scopeTag.Id - if(-not $scopeMigObj.Id) { continue } - $roleAssignmentObj."roleScopeTags@odata.bind" += "https://graph.microsoft.com/beta/deviceManagement/roleScopeTags('$($scopeMigObj.Id)')" - } - - # This will update GroupIds - $json = Update-JsonForEnvironment (ConvertTo-Json $roleAssignmentObj -Depth 20) - - Write-Log "Import Role Assignments" - Invoke-GraphRequest -Url "/deviceManagement/roleAssignments" -Body $json -Method "POST" - } - } -} -#endregion - -#region SettingsCatalog - -function Start-PreImportSettingsCatalog -{ - param($obj, $objectType) - - $returnHT = @{} - $updated = $false - - if($obj.templateReference.templateId) { - # I do not like this at all and it is a lazy but simple implementation... - # It turns out that settingInstanceTemplateId and settingValueTemplateId are case sensitive - # and there is ONE setting with a different casing in the Windows Baseline template. - # The export saves it with lowercase which causes the import to fail. - - Write-Log "Get template $($obj.templateReference.templateId)" - $templateObj = Invoke-GraphRequest -Url "/deviceManagement/configurationPolicyTemplates('$($obj.templateReference.templateId)')" - if($templateObj.lifecycleState -and $templateObj.lifecycleState -ne "active") { - Write-Log "Template '$($templateObj.displayName)' '$($templateObj.displayVersion)' is in '$($templateObj.lifecycleState)' state. Current state: $($templateObj.lifecycleState). Import might fail." 2 - } - #Todo: Should probably check for the latest active version and use that instead of the one in the templateReference - - if(-not $script:baseLineTemplate) { - $script:baseLineTemplate = @{} - } - if($script:baseLineTemplate.ContainsKey($obj.templateReference.templateId)) { - $templateReference = $script:baseLineTemplate[$obj.templateReference.templateId] - } - else { - Write-Log "Get template settings for '$($templateObj.displayName)' '$($templateObj.displayVersion)' ($($obj.templateReference.templateId))" - $templateReference = Invoke-GraphRequest -Url "/deviceManagement/configurationPolicyTemplates('$($obj.templateReference.templateId)')/settingTemplates?`$expand=settingDefinitions&top=1000" - $script:baseLineTemplate.Add($obj.templateReference.templateId, $templateReference) - } - - if($templateReference) { - $newObjJson = $obj | ConvertTo-Json -Depth 50 - $templateIDs = Get-GUIDs ($templateReference | ConvertTo-Json -Depth 50) - $objectIDs = Get-GUIDs ($obj.Settings | ConvertTo-Json -Depth 50) - $diff = Compare-Object $templateIDs $objectIDs -CaseSensitive - foreach($diffItem in ($diff | where SideIndicator -eq "=>")) { - $templateID = $templateIDs | Where { $_ -eq $diffItem.InputObject } - if($templateID) { - # Found but with different casing - $newObjJson = $newObjJson -replace $diffItem.InputObject, $templateID - $updated = $true - } - } - if($updated) { - $returnHT.Add("JSON", $newObjJson) - } - } - } - return $returnHT -} - -function Invoke-CheckSettingsCatalogIds -{ - param($obj, $templateReference) - - foreach($settingTemplate in $obj.value) { - if($settingTemplate.settingDefinitions) { - foreach($settingDefinition in $settingTemplate.settingDefinitions) { - if($settingDefinition.id -and $settingDefinition.id -ne $obj.Id) { - Write-Log "Setting definition ID $($settingDefinition.id) does not match the settings catalog ID $($obj.Id)" 2 - } - } - } - } -} - -function Start-PostExportSettingsCatalog -{ - param($obj, $objectType, $path) - - Add-EMAssignmentsToExportFile $obj $objectType $path -} - -function Start-PreUpdateSettingsCatalog -{ - param($obj, $objectType, $curObject, $fromObj) - - @{"Method"="PUT"} -} - -function Start-PostGetSettingsCatalog -{ - param($obj, $objectType) - - if(-not $obj.Object.Assignments) - { - $url = "$($objectType.API)/$($obj.id)/assignments" - $assignments = (Invoke-GraphRequest -Url $url).Value - if($assignments) - { - $obj.Object.Assignments = $assignments - } - } -} - -#endregion - -#region Notification functions -function Start-PreImportNotifications -{ - param($obj, $objectType) - - Remove-Property $obj "defaultLocale" - Remove-Property $obj "localizedNotificationMessages" - Remove-Property $obj "localizedNotificationMessages@odata.context" -} - -function Start-PostFileImportNotifications -{ - param($obj, $objectType, $file) - - $tmpObj = Get-GraphObjectFromFile $file - - foreach($localizedNotificationMessage in $tmpObj.localizedNotificationMessages) - { - Start-GraphPreImport $localizedNotificationMessage $objectType - Invoke-GraphRequest -Url "$($objectType.API)/$($obj.id)/localizedNotificationMessages" -Body ($localizedNotificationMessage | ConvertTo-Json -Depth 20) -Method "POST" - } -} - -function Start-PostCopyNotifications -{ - param($objCopyFrom, $objNew, $objectType) - - foreach($localizedNotificationMessage in $objCopyFrom.localizedNotificationMessages) - { - Start-GraphPreImport $localizedNotificationMessage $objectType - Invoke-GraphRequest -Url "$($objectType.API)/$($objNew.id)/localizedNotificationMessages" -Body ($localizedNotificationMessage | ConvertTo-Json -Depth 20) -Method "POST" - } -} -#endregion - -#region Enrollment Status Page functions -function Start-PreImportESP -{ - param($obj, $objectType) - - if($obj.Priority -eq 0) - { - $ret = @{} - $ret.Add("API","$($objectType.API)/$($obj.Id)") - $ret.Add("Method","PATCH") # Default profile always exists so update them - $ret - } - else - { - Remove-Property $obj "Id" - } -} - -function Start-PostExportESP -{ - param($obj, $objectType, $path) - - if($obj.Priority -eq 0) - { - Save-EMDefaultPolicy $obj $objectType $path - } -} - -function Start-PostListESP -{ - param($objList, $objectType) - - # endswith not working so filter them out - $objList | Where { $_.Object.'@OData.Type' -eq '#microsoft.graph.windows10EnrollmentCompletionPageConfiguration' } -} -#endregion - -#region Enrollment Restriction functions - -function Start-PostExportEnrollmentRestrictions -{ - param($obj, $objectType, $path) - - if($obj.Priority -eq 0) - { - Save-EMDefaultPolicy $obj $objectType $path - } -} - -function Start-PreImportEnrollmentRestrictions -{ - param($obj, $objectType) - - if($obj.Priority -eq 0) - { - $ret = @{} - $ret.Add("API","$($objectType.API)/$($obj.Id)") - $ret.Add("Method","PATCH") # Default profile always exists so update them - $ret - } - else - { - Remove-Property $obj "Id" - } - - if($obj.windowsMobileRestriction) - { - # Windows Phone operations are no longer supported - Remove-Property $obj "windowsMobileRestriction" - } -} - -function Start-PreDeleteEnrollmentRestrictions -{ - param($obj, $objectType) - - if($obj.Priority -eq 0) - { - @{ "Delete" = $false } - } -} - -function Start-PreReplaceEnrollmentRestrictions -{ - param($obj, $objectType, $sourceObj, $fromFile) - - if($sourceObj.Priority -eq 0) { @{ "Replace" = $false } } -} - -function Start-PostReplaceEnrollmentRestrictions -{ - param($obj, $objectType, $sourceObj, $fromFile) - - if($sourceObj.Priority -eq 0) { return } - - $api = "/deviceManagement/deviceEnrollmentConfigurations/$($obj.id)/setpriority" - - $priority = [PSCustomObject]@{ - priority = $sourceObj.Priority - } - $json = $priority | ConvertTo-Json -Depth 20 - - Write-Log "Update priority for $($obj.displayName) to $($sourceObj.Priority)" - Invoke-GraphRequest $api -HttpMethod "POST" -Content $json -} - -function Start-PreFilesImportEnrollmentRestrictions -{ - param($objectType, $filesToImport) - - $filesToImport | sort-object -property @{e={$_.Object.priority}} -} - -function Start-PreUpdateEnrollmentRestrictions -{ - param($obj, $objectType, $curObject, $fromObj) - - Remove-Property $obj "priority" -} - -function Start-PostListEnrollmentRestrictions -{ - param($objList, $objectType) - - # endswith not working so filter them out - $objList | Where { - ($_.Object.'@OData.Type' -eq '#microsoft.graph.deviceEnrollmentPlatformRestrictionConfiguration' -or - $_.Object.'@OData.Type' -eq '#microsoft.graph.deviceEnrollmentLimitConfiguration' -or - $_.Object.'@OData.Type' -eq '#microsoft.graph.deviceEnrollmentPlatformRestrictionsConfiguration') -and - $_.Object.id -notlike "*_PlatformRestrictions" -and $_.Object.platformType -ne "WindowsPhone" -and $_.Object.platformType -ne "AndroidAosp" - } -} - -function Start-PreImportAssignmentsEnrollmentRestrictions -{ - param($obj, $objectType, $file, $assignments) - - if($obj.Priority -eq 0) - { - # Skip Assignment for Default Policy - @{ "Import" = $false } - } -} - -#endregion - -#region -function Start-PostListCoManagementSettings -{ - param($objList, $objectType) - - # endswith not working so filter them out - $objList | Where { $_.Object.'@OData.Type' -eq '#microsoft.graph.deviceComanagementAuthorityConfiguration' } -} -#endregion - -#region ScopeTags -function Start-PostExportScopeTags -{ - param($obj, $objectType, $path) - - Add-EMAssignmentsToExportFile $obj $objectType $path -} - -function Start-PostGetScopeTags -{ - param($obj, $objectType) - - $strAPI = "$($objectType.API)/$($obj.Object.Id)/assignments" - $tmpObj = Invoke-GraphRequest -Url $strAPI - - if(($tmpObj.value | measure).count -gt 0) - { - $obj.Object.assignments = $tmpObj.value - } -} -#endregion - -#region AutoPilot -function Start-PreImportAssignmentsAutoPilot -{ - param($obj, $objectType, $file, $assignments) - - Add-EMAssignmentsToObject $obj $objectType $file $assignments -} - -function Start-PreDeleteAutoPilot -{ - param($obj, $objectType) - - Write-Log "Delete AutoPilot profile assignments" - - if(-not $obj.Assignments) - { - $tmpObj = (Get-GraphObject $obj $objectType).Object - } - else - { - $tmpObj = $obj - } - - foreach($assignment in $tmpObj.Assignments) - { - if($assignment.Source -ne "direct") { continue } - - $api = "/deviceManagement/windowsAutopilotDeploymentProfiles/$($obj.Id)/assignments/$($assignment.Id)" - - Invoke-GraphRequest $api -HttpMethod "DELETE" - } -} - -#endregion - -#region Health Scripts - -function Start-PreDeleteDeviceHealthScripts -{ - param($obj, $objectType) - - if($obj.isGlobalScript -eq $true) - { - @{ "Delete" = $false } - } -} - -function Start-PreImportDeviceHealthScripts -{ - param($obj, $objectType, $file, $assignments) - - if($obj.isGlobalScript -eq $true) - { - @{ "Import" = $false } - } -} - -function Start-PreUpdateDeviceHealthScripts -{ - param($obj, $objectType, $curObject, $fromObj) - - if($curObject.Object.isGlobalScript -eq $true) - { - @{ "Import" = $false } - } -} - -function Start-PostExportDeviceHealthScripts -{ - param($obj, $objectType, $path) - - if($global:chkExportScript.IsChecked) - { - $fileName = Get-GraphObjectFile $obj $objectType - $fi = [IO.FileInfo]"$path\$fileName" - - try - { - if($obj.detectionScriptContent) - { - [IO.File]::WriteAllBytes(("$path\$($fi.BaseName)_DetectionScript.ps1"), ([System.Convert]::FromBase64String($obj.detectionScriptContent))) - } - - if($obj.remediationScriptContent) - { - [IO.File]::WriteAllBytes(("$path\$($fi.BaseName)_RemediationScript.ps1"), ([System.Convert]::FromBase64String($obj.remediationScriptContent))) - } - } - catch - { - Write-LogError "Failed to export scripts" $_.Exception - } - } -} - -#endregion - -#region Generic functions - -function Save-EMDefaultPolicy -{ - param($obj, $objectType, $path) - - if($obj.Priority -eq 0) - { - try - { - $fileName = $obj.Id.Split('_')[1] - - if($fileName) - { - $oldFile = "$path\$((Get-GraphObjectName $obj $objectType)).json" - if([IO.File]::Exists($oldFile)) - { - # Clean up from old version of the script that used the wrong name for Default policies - try { [IO.File]::Delete($oldFile) | Out-Null } Catch {} - } - Save-GraphObjectToFile $obj "$path\$((Remove-InvalidFileNameChars $fileName)).json" - } - } - catch {} - } -} -function Get-EMSettingsObject -{ - param($obj, $objectType, $file, $settingsProperty = "settings", [switch]$SettingsArray) - - if($obj.$settingsProperty) { return $obj.$settingsProperty } - - $fi = [IO.FileInfo]$file - if($fi.Exists) - { - # Settings property removed during import so lets try exported file first - $tmpObj = Get-GraphObjectFromFile $fi.FullName - if($SettingsArray -eq $true) - { - # Only the an array of settings is expected - return $tmpObj.$settingsProperty - } - else - { - if($tmpObj.$settingsProperty) - { - # A property with the an array of settings is expected - return ([PSCustomObject]@{ - $settingsProperty = $tmpObj.$settingsProperty - }) - } - } - - Write-Log "Settings not included in export file. Try import from _Settings.json file" 2 - $settingsFile = $fi.DirectoryName + "\" + $fi.BaseName + "_Settings.json" - $fiSettings = [IO.FileInfo]$settingsFile - if($fiSettings.Exists -eq $false) - { - Write-Log "Settings file '$($fiSettings.FullName)' was not found" 2 - return - } - Get-GraphObjectFromFile $fiSettings.FullName - } - else - { - Write-Log "Settings not included in export file and _Settings.json file is missing." 3 - } -} - -function Add-EMAssignmentsToExportFile -{ - param($obj, $objectType, $path, $Url = "") - - if($global:chkExportAssignments.IsChecked -ne $true) { return } - - $fileName = (Get-GraphObjectName $obj $objectType).Trim('.') - if((Get-SettingValue "AddIDToExportFile") -eq $true -and $obj.Id) - { - $fileName = ($fileName + "_" + $obj.Id) - } - $fileName = "$path\$((Remove-InvalidFileNameChars $fileName)).json" - if([IO.File]::Exists($fileName) -eq $false) - { - Write-Log "File not found: $fileName. Could not add assignments to file" 3 - return - } - - $tmpObj = Get-GraphObjectFromFile $fileName - - if(-not $url) - { - $url = "$($objectType.API)/$($obj.id)/assignments" - } - $assignments = (Invoke-GraphRequest -Url $url -ODataMetadata "Minimal").Value - if($assignments) - { - if(-not ($tmpObj.PSObject.Properties | Where Name -eq "assignments")) - { - $tmpObj | Add-Member -MemberType NoteProperty -Name "assignments" -Value $assignments - } - else - { - $tmpObj.Assignments = $assignments - } - Save-GraphObjectToFile $tmpObj $fileName - } -} - -function Add-EMAssignmentsToObject -{ - param($obj, $objectType, $file, $assignments) - - # AutoPilot and TaC are using assignments and not assign like other object types - $api = "$($objectType.API)/$($obj.Id)/assignments" - - # These profiles don't support importing of multiple assignments with { "assignment" [...]} - # Each assignment must be imported separately - - foreach($assignment in $assignments) - { - if($assignment.Source -and $assignment.Source -ne "direct") { continue } - - foreach($prop in $assignment.PSObject.Properties) - { - if($prop.Name -in @("Target")) { continue } - Remove-Property $assignment $prop.Name - } - - foreach($prop in $assignment.target.PSObject.Properties) - { - if($prop.Name -in @("@odata.type","groupId")) { continue } - Remove-Property $assignment.target $prop.Name - } - - $json = Update-JsonForEnvironment ($assignment | ConvertTo-Json -Depth 20) - Invoke-GraphRequest -Url $api -Body $json -Method "POST" | Out-Null - } - @{"Import"=$false} -} - -#endregion - -#region Mac Custom Scripts - -function Start-PreUpdateMacCustomAttributes -{ - param($obj, $objectType, $curObject, $fromObj) - - foreach($prop in @('customAttributeName','customAttributeType','displayName')) - { - Remove-Property $obj $prop - } -} - -#endregion - -#region Mac Feature Updates -function Start-PreUpdateFeatureUpdates -{ - param($obj, $objectType, $curObject, $fromObj) - - foreach($prop in @('deployableContentDisplayName','endOfSupportDate')) - { - Remove-Property $obj $prop - } -} -#endregion - -#region Conditional Access -function Add-ConditionalAccessImportExtensions -{ - param($form, $buttonPanel, $index = 0) - - $xaml = @" - - -"@ - $label = [Windows.Markup.XamlReader]::Parse($xaml) - - $CAStates = @() - $CAStates += [PSCustomObject]@{ - Name = "As Exported - Change On to Report-only" - Value = "AsExportedReportOnly" - } - - $CAStates += [PSCustomObject]@{ - Name = "As Exported" - Value = "AsExported" - } - - $CAStates += [PSCustomObject]@{ - Name = "Report-only" - Value = "enabledForReportingButNotEnforced" - } - - $CAStates += [PSCustomObject]@{ - Name = "On" - Value = "enabled" - } - - $CAStates += [PSCustomObject]@{ - Name = "Off" - Value = "disabled" - } - - $defaultCAState = Get-SettingValue "ConditionalAccessState" - - $global:cbImportCAState = [System.Windows.Controls.ComboBox]::new() - $global:cbImportCAState.DisplayMemberPath = "Name" - $global:cbImportCAState.SelectedValuePath = "Value" - $global:cbImportCAState.ItemsSource = $CAStates - $global:cbImportCAState.SelectedValue = $defaultCAState - $global:cbImportCAState.Margin="0,5,0,0" - $global:cbImportCAState.HorizontalAlignment="Left" - $global:cbImportCAState.Width=250 - $global:cbImportCAState.Name = "cbImportCAState" - - @($label, $global:cbImportCAState) -} - -function Start-PreImportConditionalAccess -{ - param($obj, $objectType, $file, $assignments) - - if ($global:cbImportCAState.SelectedValue -and $global:cbImportCAState.SelectedValue -ne "AsExported") { - if ($global:cbImportCAState.SelectedValue -eq "AsExportedReportOnly" -and $obj.state -eq "enabled") { - Write-Log "Change Enabled policy to Report-only" - $obj.state = "enabledForReportingButNotEnforced" - } - else { - $obj.state = $global:cbImportCAState.SelectedValue - } - } - - if($obj.grantControls.authenticationStrength) - { - $obj.grantControls.operator = "AND" - $tmpObj = Get-GraphObjectFromFile $file - - $authSetting = [PSCustomObject]@{ - id = $tmpObj.grantControls.authenticationStrength.id - } - $obj.grantControls.authenticationStrength = $authSetting - } - - if($obj.sessionControls.disableResilienceDefaults -eq $false) - { - $obj.sessionControls.disableResilienceDefaults = $null - } - - # DeviceStates property is depricated - if(($obj.conditions.PSObject.Properties | Where Name -eq "DeviceStates")) - { - $obj.conditions.PSObject.Properties.Remove('DeviceStates') - } -} - -function Start-PostExportConditionalAccess -{ - param($obj, $objectType, $path) - - $ids = @() - foreach($id in ($obj.conditions.users.includeGroups + $obj.conditions.users.excludeGroups)) - { - if($id -in $ids) { continue } - elseif($id -eq "GuestsOrExternalUsers") { continue } - elseif($id -eq "All") { continue } - elseif($id -eq "None") { continue } - - $ids += $id - Add-GraphMigrationObject $id "/groups" "Group" - } - - foreach($id in ($obj.conditions.users.includeUsers +$obj.conditions.users.excludeUsers)) - { - if($id -in $ids) { continue } - elseif($id -eq "GuestsOrExternalUsers") { continue } - elseif($id -eq "All") { continue } - elseif($id -eq "None") { continue } - - $ids += $id - Add-GraphMigrationObject $id "/users" "User" - } - - <# - $roleIds = @() - foreach($id in ($obj.conditions.users.includeRoles + $obj.conditions.users.excludeRoles)) - { - if($id -in $ids) { continue } - $roleIds += $id - } - #> -} -#endregion - -#region Terms of use -function Start-PreImportTermsOfUse -{ - param($obj, $objectType, $file, $assignments) - - $pkgPath = Get-SettingValue "EMIntuneAppPackages" - - if(-not $pkgPath -or [IO.Directory]::Exists($pkgPath) -eq $false) - { - Write-Log "Intune app directory is either missing or does not exist" 2 - } - - try - { - $fi = [IO.FileInfo]$file - } catch {} - - foreach($file in $obj.Files) - { - $pdfFile = $null - - if($fi.Directory.FullName) - { - $pdfFile = "$($fi.Directory.FullName)\$($file.fileName)" - } - - if($null -eq $pdfFile -or [IO.File]::Exists($pdfFile) -eq $false) - { - $pdfFile = "$($pkgPath)\$($file.fileName)" - } - - if([IO.File]::Exists($pdfFile) -eq $false) - { - Write-Log "Terms of use file $($file.fileName) not found. The Terms of Use object will not be imported." 2 - @{"Import" = $false} - return - } - - Write-Log "Add file data: $pdfFile" - - $bytes = [IO.File]::ReadAllBytes($pdfFile) - $file.fileData = [PSCustomObject]@{ - data = [Convert]::ToBase64String($bytes) - } - } -} - -function Start-PostExportTermsOfUse -{ - param($obj, $objectType, $path) - - foreach($file in $obj.Files) - { - $url = "agreements/$($obj.id)/file/localizations('$($file.id)')/fileData/data" - $data = (Invoke-GraphRequest -Url $url -ODataMetadata "Minimal").Value - if($data) - { - Write-Log "Save file $($file.FileName)" - $fileName = "$path\$($file.FileName)" - [IO.File]::WriteAllBytes($fileName, [System.Convert]::FromBase64String($data)) - } - } -} - -#endregion - -#region ADMXFiles - -function Start-PreFilesImportADMXFiles -{ - param($objectType, $filesToImport) - - $filesToImport | sort-object -property @{e={$_.Object.lastModifiedDateTime}} -} - -function Start-PreImportADMXFiles -{ - param($obj, $objectType, $file, $assignments) - - $pkgPath = Get-SettingValue "EMIntuneAppPackages" - - if(-not $pkgPath -or [IO.Directory]::Exists($pkgPath) -eq $false) - { - Write-Log "Intune app directory is either missing or does not exist" 2 - $pkgPath = $null - } - - try - { - $fi = [IO.FileInfo]$file - } catch {} - - $admxFile = $null - - if($fi.Directory.FullName) - { - $admxFile = "$($fi.Directory.FullName)\$($obj.fileName)" - $admlFile = "$($fi.Directory.FullName)\$([io.path]::GetFileNameWithoutExtension($obj.fileName)).adml" - } - - if($null -ne $pkgPath -and ($null -eq $admxFile -or [IO.File]::Exists($admxFile) -eq $false -or [IO.File]::Exists($admxFile) -eq $false)) - { - Write-Log "$($obj.fileName) not foud in Export folder. Look in package path: $pkgPath" - $admxFile = "$($pkgPath)\$($obj.fileName)" - $admlFile = "$($pkgPath)\$([io.path]::GetFileNameWithoutExtension($obj.fileName)).adml" - } - - if([IO.File]::Exists($admxFile) -eq $false) - { - Write-Log "ADMX (or ADML) file $($obj.fileName) not found. The ADMXFile object will not be imported." 2 - @{"Import" = $false} - return - } - - #$bytes = [IO.File]::ReadAllBytes($admxFile) - $bytes = Get-ASCIIBytes ([IO.File]::ReadAllText($admxFile)) - $obj.content = [Convert]::ToBase64String($bytes) - - #$bytes = [IO.File]::ReadAllBytes($admlFile) - $bytes = Get-ASCIIBytes ([IO.File]::ReadAllText($admlFile)) - - $obj.groupPolicyUploadedLanguageFiles += [PSCustomObject]@{ - fileName = [io.path]::GetFileName($admlFile) - content = [Convert]::ToBase64String($bytes) - languageCode = (?? $obj.defaultLanguageCode "en-US") - } - $obj.defaultLanguageCode = "" -} - -function Start-PostImportADMXFiles -{ - param($obj, $objectType, $file) - - $script:CustomADMXDefinitions = $null -} - -function Start-PreDeleteADMXFiles -{ - param($obj, $objectType) - - Write-Status "Delete $($obj.fileName)" - $strAPI = ($objectType.API + "/$($obj.Id)/remove") - Write-Log "Delete $($objectType.Title) object $($obj.fileName)" - Invoke-GraphRequest -Url $strAPI -HttpMethod "POST" -ODataMetadata "none" | Out-Null - - @{ "Delete" = $false } -} - -#endregion - -#region Reusable Groups -function Start-PostGetReusableSettings -{ - param($obj, $objectType) - - $strAPI = "$($objectType.API)/$($obj.Object.Id)?`$select=settinginstance,displayname,description" - $tmpObj = Invoke-GraphRequest -Url $strAPI - - if($tmpObj.settingInstance) - { - $obj.Object | Add-Member Noteproperty -Name "settingInstance" -Value $tmpObj.settingInstance -Force - } -} - -#endregon - -#region Authentication Strength -function Start-PreImportCommandAuthenticationStrengths -{ - param($obj, $objectType, $file, $assignments) - - if($obj.policyType -ne "custom") - { - Write-Log "Built-in Authentication Strength objects cannot be imported" 2 - @{ "Import" = $false } - } -} -#endregion - -#region Authentication Strength -function Start-PreImportCommandAuthenticationContext -{ - param($obj, $objectType, $file, $assignments) - - #@{ "Method" = "PATCH" } - -} -#endregion - - +<# +.SYNOPSIS +Module for managing Intune objects + +.DESCRIPTION +This module is for the Endpoint Manager/Intune View. It manages Export/Import/Copy of Intune objects + +.NOTES + Author: Mikael Karlsson +#> +function Get-ModuleVersion +{ + '3.10.0.6' +} + +function Invoke-InitializeModule +{ + #Add settings + $global:appSettingSections += (New-Object PSObject -Property @{ + Title = "Endpoint Manager/Intune" + Id = "EndpointManager" + Values = @() + Priority = 10 + }) + + Add-SettingsObject (New-Object PSObject -Property @{ + Title = "Application" + Key = "EMAzureApp" + Type = "List" + SelectedValuePath = "ClientId" + ItemsSource = $global:MSGraphGlobalApps + DefaultValue = "" + SubPath = "EndpointManager" + }) "EndpointManager" + + Add-SettingsObject (New-Object PSObject -Property @{ + Title = "Application Id" + Key = "EMCustomAppId" + Type = "String" + DefaultValue = "" + SubPath = "EndpointManager" + }) "EndpointManager" + + Add-SettingsObject (New-Object PSObject -Property @{ + Title = "Redirect URL" + Key = "EMCustomAppRedirect" + Type = "String" + DefaultValue = "" + SubPath = "EndpointManager" + }) "EndpointManager" + + Add-SettingsObject (New-Object PSObject -Property @{ + Title = "Tenant Id" + Key = "EMCustomTenantId" + Type = "String" + DefaultValue = "" + SubPath = "EndpointManager" + }) "EndpointManager" + + Add-SettingsObject (New-Object PSObject -Property @{ + Title = "Authority" + Key = "EMCustomAuthority" + Type = "String" + DefaultValue = "" + SubPath = "EndpointManager" + }) "EndpointManager" + + Add-SettingsObject (New-Object PSObject -Property @{ + Title = "App packages folder" + Key = "EMIntuneAppPackages" + Type = "Folder" + Description = "Root folder where intune app packages are located" + SubPath = "EndpointManager" + }) "EndpointManager" + + Add-SettingsObject (New-Object PSObject -Property @{ + Title = "Save Encryption File" + Key = "EMSaveEncryptionFile" + Type = "Boolean" + Description = "Save encryption file when uploading an app. This can then be used to when downloading the app file." + SubPath = "EndpointManager" + }) "EndpointManager" + + Add-SettingsObject (New-Object PSObject -Property @{ + Title = "App download folder" + Key = "EMIntuneAppDownloadFolder" + Type = "Folder" + Description = "Folder where app packages will be downloaded and where encryption files will be saved" + SubPath = "EndpointManager" + }) "EndpointManager" + + Get-SettingValue "ProxyURI" + + if($global:FirstTimeRunning) { + Save-Setting "EndpointManager" "EMAzureApp" $global:DefaultAzureApp + } + + $currentAppID = Get-SettingValue "EMAzureApp" + $customAppID = Get-SettingValue "EMCustomAppId" + $global:informOldAzureApp = $false + + if(($global:OldAzureApps -is [Array] -and $currentAppID -in $global:OldAzureApps) -or (-not $currentAppID -and -not $customAppID)) + { + $global:informOldAzureApp = $true + 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 + + #Add menu group and items + $global:EMViewObject = (New-Object PSObject -Property @{ + Title = "Intune Manager" + Description = "Manages Intune environments. This view can be used for copying objects in an Intune environment. It can also be used for backing up an entire Intune environment and cloning the Intune environment into another tenant." + ID="IntuneGraphAPI" + ViewPanel = $viewPanel + AuthenticationID = "MSAL" + ItemChanged = { Show-GraphObjects -ObjectTypeChanged; Invoke-ModuleFunction "Invoke-GraphObjectsChanged"; Write-Status ""} + Deactivating = { Invoke-EMDeactivateView } + Activating = { Invoke-EMActivatingView } + Authentication = (Get-MSALAuthenticationObject) + Authenticate = { Invoke-EMAuthenticateToMSAL @args } + AppInfo = (Get-GraphAppInfo "EMAzureApp" $global:DefaultAzureApp "EM") + SaveSettings = { Invoke-EMSaveSettings } + + Permissions = @() + }) + + Add-ViewObject $global:EMViewObject + + Add-ViewItem (New-Object PSObject -Property @{ + Title = "Device Configuration" + Id = "DeviceConfiguration" + ViewID = "IntuneGraphAPI" + API = "/deviceManagement/deviceConfigurations" + QUERYLIST = "`$filter=not%20isof(%27microsoft.graph.windowsUpdateForBusinessConfiguration%27)%20and%20not%20isof(%27microsoft.graph.iosUpdateConfiguration%27)" + #ExportFullObject = $false + Permissons=@("DeviceManagementConfiguration.ReadWrite.All") + PropertiesToRemove = @("privacyAccessControls") + PostFileImportCommand = { Start-PostFileImportDeviceConfiguration @args } + PostCopyCommand = { Start-PostCopyDeviceConfiguration @args } + PostGetCommand = { Start-PostGetDeviceConfiguration @args } + GroupId = "DeviceConfiguration" + NavigationProperties=$true + }) + + Add-ViewItem (New-Object PSObject -Property @{ + Title = "Conditional Access" + Id = "ConditionalAccess" + ViewID = "IntuneGraphAPI" + API = "/identity/conditionalAccess/policies" + Permissons=@("Policy.Read.All","Policy.ReadWrite.ConditionalAccess","Application.Read.All") + Dependencies = @("NamedLocations","Applications","TermsOfUse","AuthenticationStrengths","AssignmentFilters") + GroupId = "ConditionalAccess" + ImportExtension = { Add-ConditionalAccessImportExtensions @args } + PreImportCommand = { Start-PreImportConditionalAccess @args } + PostExportCommand = { Start-PostExportConditionalAccess @args } + ExpandAssignmentsList = $false + SupportsPageSize = $false + }) + + Add-ViewItem (New-Object PSObject -Property @{ + Title = "Terms of use" + Id = "TermsOfUse" + ViewID = "IntuneGraphAPI" + ViewProperties = @("id", "displayName") + Expand = "files" + QUERYLIST = "`$expand=files" + API = "/identityGovernance/termsOfUse/agreements" + Permissons=@("Agreement.ReadWrite.All") + PreImportCommand = { Start-PreImportTermsOfUse @args } + PostExportCommand = { Start-PostExportTermsOfUse @args } + GroupId = "ConditionalAccess" + SupportsPageSize = $false + }) + + Add-ViewItem (New-Object PSObject -Property @{ + Title = "Named Locations" + Id = "NamedLocations" + ViewID = "IntuneGraphAPI" + API = "/identity/conditionalAccess/namedLocations" + Permissons=@("Policy.ReadWrite.ConditionalAccess") + ImportOrder = 50 + GroupId = "ConditionalAccess" + ExpandAssignmentsList = $false + SupportsPageSize = $false + }) + + Add-ViewItem (New-Object PSObject -Property @{ + Title = "Endpoint Security" + Id = "EndpointSecurity" + ViewID = "IntuneGraphAPI" + API = "/deviceManagement/intents" + PropertiesToRemove = @('Settings','@OData.Type') + PreImportCommand = { Start-PreImportEndpointSecurity @args } + PostListCommand = { Start-PostListEndpointSecurity @args } + PostExportCommand = { Start-PostExportEndpointSecurity @args } + PostFileImportCommand = { Start-PostFileImportEndpointSecurity @args } + PostGetCommand = { Start-PostGetEndpointSecurity @args } + #PreCopyCommand = { Start-PreCopyEndpointSecurity @args } + PostCopyCommand = { Start-PostCopyEndpointSecurity @args } + PreUpdateCommand = { Start-PreUpdateEndpointSecurity @args } + Permissons=@("DeviceManagementConfiguration.ReadWrite.All") + Dependencies = @("ReusableSettings") + GroupId = "EndpointSecurity" + SupportsPageSize = $false + }) + + Add-ViewItem (New-Object PSObject -Property @{ + Title = "Compliance Policies" + Id = "CompliancePolicies" + ViewID = "IntuneGraphAPI" + Expand = "scheduledActionsForRule(`$expand=scheduledActionConfigurations)" + API = "/deviceManagement/deviceCompliancePolicies" + Permissons=@("DeviceManagementConfiguration.ReadWrite.All") + Dependencies = @("Locations","Notifications","ComplianceScripts") + PostExportCommand = { Start-PostExportCompliancePolicies @args } + PreUpdateCommand = { Start-PreUpdateCompliancePolicies @args } + GroupId = "CompliancePolicies" + SupportsPageSize = $false + }) + + Add-ViewItem (New-Object PSObject -Property @{ + Title = "Compliance Policies - V2" + Id = "CompliancePoliciesV2" + ViewID = "IntuneGraphAPI" + API = "/deviceManagement/compliancePolicies" + NameProperty = "name" + PropertiesToRemove = @('settingCount') + ViewProperties = @("name","description","Id") + Expand="settings" + Permissons=@("DeviceManagementConfiguration.ReadWrite.All") + GroupId = "CompliancePolicies" + Icon = "CompliancePolicies" + }) + + + Add-ViewItem (New-Object PSObject -Property @{ + Title = "Compliance Scripts" + Id = "ComplianceScripts" + ViewID = "IntuneGraphAPI" + API = "/deviceManagement/deviceComplianceScripts" + PostImportCommand = { Start-PostImportComplianceScripts @args } + Permissons=@("DeviceManagementScripts.ReadWrite.All") + GroupId = "CompliancePolicies" + Icon = "Scripts" + ImportOrder = 80 + }) + + Add-ViewItem (New-Object PSObject -Property @{ + Title = "Intune Branding" + Id = "IntuneBranding" + API = "/deviceManagement/intuneBrandingProfiles" + ViewID = "IntuneGraphAPI" + NameProperty = "profileName" + ViewProperties = @("profileName", "displayName", "description", "id","isDefaultProfile") + PreImportCommand = { Start-PreImportIntuneBranding @args } + PostImportCommand = { Start-PostImportIntuneBranding @args } + PostGetCommand = { Start-PostGetIntuneBranding @args } + PostExportCommand = { Start-PostExportIntuneBranding @args } + PreDeleteCommand = { Start-PreDeleteIntuneBranding @args } + PreUpdateCommand = { Start-PreUpdateIntuneBranding @args } + Permissons=@("DeviceManagementApps.ReadWrite.All") + Icon = "Branding" + SkipRemoveProperties = @('Id') # Id is removed by PreImport. Required for default profile + PropertiesToRemoveForUpdate = @('isDefaultProfile','disableClientTelemetry') + GroupId = "TenantAdmin" + SupportsPageSize = $false + }) + + <# + # BUG in Graph? Cannot create default branding. Can only create it when importing another object + # Header required Accept-Language: sv-SE + # Documentation says to use Content-Language but that doesn't work + + # Could work with https://main.iam.ad.ext.azure.com/api/LoginTenantBrandings + + #> + + Add-ViewItem (New-Object PSObject -Property @{ + Title = "Azure Branding" + Id = "AzureBranding" + API = "/organization/%OrganizationId%/branding/localizations" + ViewID = "IntuneGraphAPI" + ViewProperties = @("Id") + PreImportCommand = { Start-PreImportAzureBranding @args } + PostListCommand = { Start-PostListAzureBranding @args } + ShowButtons = @("Export","View") + NameProperty = "Id" + Permissons=@("Organization.ReadWrite.All") + Icon = "Branding" + SkipRemoveProperties = @('Id') + GroupId = "Azure" + SkipAddIDOnExport = $true + ExpandAssignmentsList = $false + SupportsPageSize = $false + }) + + Add-ViewItem (New-Object PSObject -Property @{ + Title = "Enrollment Status Page" + Id = "EnrollmentStatusPage" + API = "/deviceManagement/deviceEnrollmentConfigurations" + ViewID = "IntuneGraphAPI" + PreImportCommand = { Start-PreImportESP @args } + PostExportCommand = { Start-PostExportESP @args } + PreDeleteCommand = { Start-PreDeleteEnrollmentRestrictions @args } # Note: Uses same PreDelete as restrictions + PreReplaceCommand = { Start-PreReplaceEnrollmentRestrictions @args } # Note: Uses same PreReplaceCommand as restrictions + PostReplaceCommand = { Start-PostReplaceEnrollmentRestrictions @args } # Note: Uses same PostReplaceCommand as restrictions + PreFilesImportCommand = { Start-PreFilesImportEnrollmentRestrictions @args } # Note: Uses same PreFilesImportCommand as restrictions + PreImportAssignmentsCommand = { Start-PreImportAssignmentsEnrollmentRestrictions @args } # Note: Uses same PreFilesImportCommand as restrictions + PostListCommand = { Start-PostListESP @args } + #PreUpdateCommand = { Start-PreUpdateEnrollmentRestrictions @args } # Note: Uses same PreUpdateCommand as restrictions + #QUERYLIST = "`$filter=endsWith(id,'Windows10EnrollmentCompletionPageConfiguration')" + Permissons=@("DeviceManagementServiceConfig.ReadWrite.All") + SkipRemoveProperties = @('Id') + Dependencies = @("Applications") + AssignmentsType = "enrollmentConfigurationAssignments" + PropertiesToRemoveForUpdate = @('priority') + GroupId = "WinEnrollment" + SupportsPageSize = $false + }) + + Add-ViewItem (New-Object PSObject -Property @{ + Title = "Enrollment Restrictions" + Id = "EnrollmentRestrictions" + API = "/deviceManagement/deviceEnrollmentConfigurations" + ViewID = "IntuneGraphAPI" + #QUERYLIST = "`$filter=not endsWith(id,'Windows10EnrollmentCompletionPageConfiguration')" + PostExportCommand = { Start-PostExportEnrollmentRestrictions @args } + PreImportCommand = { Start-PreImportEnrollmentRestrictions @args } + PreDeleteCommand = { Start-PreDeleteEnrollmentRestrictions @args } + PreReplaceCommand = { Start-PreReplaceEnrollmentRestrictions @args } + PostReplaceCommand = { Start-PostReplaceEnrollmentRestrictions @args } + PreFilesImportCommand = { Start-PreFilesImportEnrollmentRestrictions @args } + PostListCommand = { Start-PostListEnrollmentRestrictions @args } + PreImportAssignmentsCommand = { Start-PreImportAssignmentsEnrollmentRestrictions @args } + #PreUpdateCommand = { Start-PreUpdateEnrollmentRestrictions @args } + PropertiesToRemoveForUpdate = @('priority') + Permissons=@("DeviceManagementServiceConfig.ReadWrite.All") + SkipRemoveProperties = @('Id') + AssignmentsType = "enrollmentConfigurationAssignments" + GroupId = "EnrollmentRestrictions" + ViewProperties = @("displayName","platformType","description","Id") + SupportsPageSize = $false + }) + + Add-ViewItem (New-Object PSObject -Property @{ + Title = "Co-Management Settings" + Id = "CoManagementSettings" + ViewID = "IntuneGraphAPI" + API = "/deviceManagement/deviceEnrollmentConfigurations" + PostReplaceCommand = { Start-PostReplaceEnrollmentRestrictions @args } # Note: Uses same PostReplaceCommand as restrictions + PreFilesImportCommand = { Start-PreFilesImportEnrollmentRestrictions @args } # Note: Uses same PreFilesImportCommand as restrictions + PostListCommand = { Start-PostListCoManagementSettings @args } + PropertiesToRemoveForUpdate = @('priority') + Permissons=@("DeviceManagementServiceConfig.ReadWrite.All") + SkipRemoveProperties = @('Id') + GroupId = "WinEnrollment" + Icon = "EnrollmentStatusPage" + SupportsPageSize = $false + }) + + Add-ViewItem (New-Object PSObject -Property @{ + Title = "Administrative Templates" + Id = "AdministrativeTemplates" + API = "/deviceManagement/groupPolicyConfigurations" + ViewID = "IntuneGraphAPI" + PostGetCommand = { Start-PostGetAdministrativeTemplate @args } + PostExportCommand = { Start-PostExportAdministrativeTemplate @args } + PostCopyCommand = { Start-PostCopyAdministrativeTemplate @args } + PostFileImportCommand = { Start-PostFileImportAdministrativeTemplate @args } + PreImportCommand = { Start-PreImportAdministrativeTemplate @args } + LoadObject = { Start-LoadAdministrativeTemplate @args } + PropertiesToRemove = @("definitionValues","policyConfigurationIngestionType") + Permissons=@("DeviceManagementConfiguration.ReadWrite.All") + Icon="DeviceConfiguration" + GroupId = "DeviceConfiguration" + CompareValue = "CombinedValueWithLabel" + Dependencies = @("ADMXFiles") + }) + + Add-ViewItem (New-Object PSObject -Property @{ + Title = "Scripts (PowerShell)" + Id = "PowerShellScripts" + API = "/deviceManagement/deviceManagementScripts" + ViewID = "IntuneGraphAPI" + DetailExtension = { Add-ScriptExtensions @args } + ExportExtension = { Add-ScriptExportExtensions @args } + PostExportCommand = { Start-PostExportScripts @args } + Permissons=@("DeviceManagementScripts.ReadWrite.All") + AssignmentsType = "deviceManagementScriptAssignments" + Icon="Scripts" + GroupId = "Scripts" + }) + + Add-ViewItem (New-Object PSObject -Property @{ + Title = "Scripts (Shell)" + Id = "MacScripts" + API = "/deviceManagement/deviceShellScripts" + ViewID = "IntuneGraphAPI" + DetailExtension = { Add-ScriptExtensions @args } + ExportExtension = { Add-ScriptExportExtensions @args } + PostExportCommand = { Start-PostExportScripts @args } + Permissons=@("DeviceManagementScripts.ReadWrite.All") + AssignmentsType = "deviceManagementScriptAssignments" + Icon="Scripts" + GroupId = "Scripts" + }) + + Add-ViewItem (New-Object PSObject -Property @{ + Title = "Custom Attributes" + Id = "MacCustomAttributes" + API = "/deviceManagement/deviceCustomAttributeShellScripts" + ViewID = "IntuneGraphAPI" + Permissons=@("DeviceManagementScripts.ReadWrite.All") + AssignmentsType = "deviceManagementScriptAssignments" + Icon="CustomAttributes" + GroupId = "CustomAttributes" # MacOS Settings + DetailExtension = { Add-ScriptExtensions @args } + ExportExtension = { Add-ScriptExportExtensions @args } + PostExportCommand = { Start-PostExportScripts @args } + PropertiesToRemoveForUpdate = @('customAttributeName','customAttributeType','displayName') + #PreUpdateCommand = { Start-PreUpdateMacCustomAttributes @args } + }) + + Add-ViewItem (New-Object PSObject -Property @{ + Title = "Terms and Conditions" + Id = "TermsAndConditions" + API = "/deviceManagement/termsAndConditions" + ViewID = "IntuneGraphAPI" + Permissons=@("DeviceManagementServiceConfig.ReadWrite.All") + ExpandAssignments = $false # Not supported for this object type + PostExportCommand = { Start-PostExportTermsAndConditions @args } + PreImportAssignmentsCommand = { Start-PreImportAssignmentsTermsAndConditions @args } + GroupId = "TenantAdmin" + ExpandAssignmentsList = $false + SupportsPageSize = $false + }) + + Add-ViewItem (New-Object PSObject -Property @{ + Title = "App Protection" + Id = "AppProtection" + API = "/deviceAppManagement/managedAppPolicies" + ViewID = "IntuneGraphAPI" + PreGetCommand = { Start-GetAppProtection @args } + PostListCommand = { Start-PostListAppProtection @args } + PreImportCommand = { Start-PreImportAppProtection @args } + PostImportCommand = { Start-PostImportAppProtection @args } + PreImportAssignmentsCommand = { Start-PreImportAssignmentsAppProtection @args } + PreUpdateCommand = { Start-PreUpdateAppProtection @args } + ExportFullObject = $true + PropertiesToRemove = @('exemptAppLockerFiles') + PropertiesToRemoveForUpdate = @("protectedAppLockerFiles","version") # ToDo: !!! Add support for protectedAppLockerFiles? + Permissons=@("DeviceManagementApps.ReadWrite.All") + Dependencies = @("Applications") + GroupId = "AppProtection" + ExpandAssignmentsList = $false + SupportsPageSize = $false + }) + + # These are also included in the managedAppPolicies API + # So all custom commands will be handled by the same functions as App Protection + Add-ViewItem (New-Object PSObject -Property @{ + Title = "App Configuration (App)" + Id = "AppConfigurationManagedApp" + API = "/deviceAppManagement/targetedManagedAppConfigurations" + ViewID = "IntuneGraphAPI" + PreGetCommand = { Start-GetAppProtection @args } + PreImportCommand = { Start-PreImportAppProtection @args } + PostImportCommand = { Start-PostImportAppProtection @args } + PreImportAssignmentsCommand = { Start-PreImportAssignmentsAppProtection @args } + PreUpdateCommand = { Start-PreUpdateAppConfigurationApp @args } + Permissons=@("DeviceManagementApps.ReadWrite.All") + Dependencies = @("Applications") + Icon = "AppConfiguration" + GroupId = "AppConfiguration" + ExpandAssignmentsList = $false + SupportsPageSize = $false + }) + + Add-ViewItem (New-Object PSObject -Property @{ + Title = "App Configuration (Device)" + Id = "AppConfigurationManagedDevice" + API = "/deviceAppManagement/mobileAppConfigurations" + QUERYLIST = "`$filter=microsoft.graph.androidManagedStoreAppConfiguration/appSupportsOemConfig%20eq%20false%20or%20isof(%27microsoft.graph.androidManagedStoreAppConfiguration%27)%20eq%20false" + ViewID = "IntuneGraphAPI" + Permissons=@("DeviceManagementApps.ReadWrite.All") + Dependencies = @("Applications") + PreFilesImportCommand = { Start-PreFilesImportAppConfiguration @args } + PreImportAssignmentsCommand = { Start-PreImportAssignmentsAppConfiguration @args } + PostExportCommand = { Start-PostExportAppConfiguration @args } + Icon = "AppConfiguration" + GroupId = "AppConfiguration" + }) + + Add-ViewItem (New-Object PSObject -Property @{ + Title = "Applications" + Id = "Applications" + API = "/deviceAppManagement/mobileApps" + ViewID = "IntuneGraphAPI" + PropertiesToRemove = @('uploadState','publishingState','isAssigned','dependentAppCount','supersedingAppCount','supersededAppCount','committedContentVersion','isFeatured','size','categories') #,'minimumSupportedWindowsRelease' + QUERYLIST = "`$filter=(microsoft.graph.managedApp/appAvailability%20eq%20null%20or%20microsoft.graph.managedApp/appAvailability%20eq%20%27lineOfBusiness%27%20or%20isAssigned%20eq%20true)&`$orderby=displayName" + QuerySearch=$true + Permissons=@("DeviceManagementApps.ReadWrite.All") + AssignmentsType="mobileAppAssignments" + AssignmentProperties = @("@odata.type","target","settings","intent") + AssignmentTargetProperties = @("@odata.type","groupId","deviceAndAppManagementAssignmentFilterId","deviceAndAppManagementAssignmentFilterType") + ImportOrder = 60 + Expand="categories,assignments" # ODataMetadata is set to minimal so assignments can't be autodetected + ODataMetadata="minimal" # categories property not supported with ODataMetadata full + PostFileImportCommand = { Start-PostFileImportApplication @args } + PostCopyCommand = { Start-PostCopyApplication @args } + PreUpdateCommand = { Start-PreUpdateApplication @args } + PreImportCommand = { Start-PreImportCommandApplication @args } + DetailExtension = { Add-DetailExtensionApplications @args } + PreImportAssignmentsCommand = { Start-PreImportAssignmentsApplications @args } + PreDeleteCommand = { Start-PreDeleteApplications @args } + PostExportCommand = { Start-PostExportApplications @args } + PostListCommand = { Start-PostListApplications @args } + ExportExtension = { Add-ScriptExportApplications @args } + PostGetCommand = { Start-PostGetApplications @args } + PostImportCommand = { Start-PostImportApplications @args } + PostFilesImportCommand = { Start-PostFilesImportApplications @args } + GroupId = "Apps" + ScopeTagsReturnedInList = $false + }) + + Add-ViewItem (New-Object PSObject -Property @{ + Title = "Autopilot" + Id = "AutoPilot" + API = "/deviceManagement/windowsAutopilotDeploymentProfiles" + ViewID = "IntuneGraphAPI" + CopyDefaultName = "%displayName% Copy" # '-' is not allowed in the name + Permissons=@("DeviceManagementServiceConfig.ReadWrite.All") + PreImportAssignmentsCommand = { Start-PreImportAssignmentsAutoPilot @args } + PreDeleteCommand = { Start-PreDeleteAutoPilot @args } + PropertiesToRemoveForUpdate = @('managementServiceAppId') + GroupId = "WinEnrollment" + }) + + Add-ViewItem (New-Object PSObject -Property @{ + Title = "Policy Sets" + Id = "PolicySets" + API = "/deviceAppManagement/policySets" + ViewID = "IntuneGraphAPI" + Expand = "Items" + PreImportAssignmentsCommand = { Start-PreImportAssignmentsPolicySets @args } + PreImportCommand = { Start-PreImportPolicySets @args } + PreUpdateCommand = { Start-PreUpdatePolicySets @args } + PostListCommand = { Start-PostListPolicySets @args } + Permissons=@("DeviceManagementConfiguration.ReadWrite.All") + ImportOrder = 2000 # Policy Sets reference other objects so make sure it is imported last + Dependencies = @("Applications","AppConfiguration","AppProtection","AutoPilot","EnrollmentRestrictions","EnrollmentStatusPage","DeviceConfiguration","AdministrativeTemplates","SettingsCatalog","CompliancePolicies") + GroupId = "PolicySets" + ExpandAssignmentsList = $false # expand is not allowed, IsAssigned is set in PostListCommand + }) + + Add-ViewItem (New-Object PSObject -Property @{ + Title = "Update Policies" + Id = "UpdatePolicies" + ViewID = "IntuneGraphAPI" + API = "/deviceManagement/deviceConfigurations" + QUERYLIST = "`$filter=isof(%27microsoft.graph.windowsUpdateForBusinessConfiguration%27)%20or%20isof(%27microsoft.graph.iosUpdateConfiguration%27)" + #ExportFullObject = $false + Permissons=@("DeviceManagementConfiguration.ReadWrite.All") + GroupId = "WinUpdatePolicies" + PropertiesToRemoveForUpdate = @('version','qualityUpdatesPauseStartDate','featureUpdatesPauseStartDate','qualityUpdatesWillBeRolledBack','featureUpdatesWillBeRolledBack') + }) + + Add-ViewItem (New-Object PSObject -Property @{ + Title = "Feature Updates" + Id = "FeatureUpdates" + ViewID = "IntuneGraphAPI" + API = "/deviceManagement/windowsFeatureUpdateProfiles" + Permissons=@("DeviceManagementConfiguration.ReadWrite.All") + GroupId = "WinFeatureUpdates" + PropertiesToRemoveForUpdate = @('deployableContentDisplayName','endOfSupportDate') + #PreUpdateCommand = { Start-PreUpdateFeatureUpdates @args } + }) + + Add-ViewItem (New-Object PSObject -Property @{ + Title = "Quality Updates (Profiles)" + Id = "QualityUpdates" + ViewID = "IntuneGraphAPI" + API = "/deviceManagement/windowsQualityUpdateProfiles" + Permissons=@("DeviceManagementConfiguration.ReadWrite.All") + Icon = "UpdatePolicies" + GroupId = "WinQualityUpdates" + PropertiesToRemoveForUpdate = @('releaseDateDisplayName','deployableContentDisplayName') + SupportsPageSize = $false + }) + + Add-ViewItem (New-Object PSObject -Property @{ + Title = "Quality Updates (Policies)" + Id = "QualityUpdatePolicies" + ViewID = "IntuneGraphAPI" + API = "/deviceManagement/windowsQualityUpdatePolicies" + Permissons=@("DeviceManagementConfiguration.ReadWrite.All") + Icon = "UpdatePolicies" + GroupId = "WinQualityUpdates" + SupportsPageSize = $false + }) + + # Locations are not FULLY supported + # They will be imported but Compliance Policies will not be updated with new Location object after import + # ToDo: Add support Export/Import Location Settings + # Location object - Only used by Android Device Admins Compliance Policies + # - These should probably be migrated to Android Enterprise anyway. That is the recommendation by Google + # Property that needs to be updated on the Compliance Policy + # deviceManagement/managementConditionStatements/$obj.conditionStatementId + + # Location objects support removed from Intune + <# + Add-ViewItem (New-Object PSObject -Property @{ + Title = "Locations" + Id = "Locations" + ViewID = "IntuneGraphAPI" + API = "/deviceManagement/managementConditions" + Permissons=@("DeviceManagementConfiguration.ReadWrite.All") + PreImportCommand = { Start-PreImportLocations @args } + ImportOrder = 30 + GroupId = "CompliancePolicies" + }) + #> + + Add-ViewItem (New-Object PSObject -Property @{ + Title = "Settings Catalog" + Id = "SettingsCatalog" + ViewID = "IntuneGraphAPI" + API = "/deviceManagement/configurationPolicies" + PropertiesToRemove = @('settingCount') + Permissons=@("DeviceManagementConfiguration.ReadWrite.All") + NameProperty = "name" + ViewProperties = @("name","description","Id") + Expand="Settings" + Icon="DeviceConfiguration" + PreImportCommand = { Start-PreImportSettingsCatalog @args } + PostExportCommand = { Start-PostExportSettingsCatalog @args } + PreUpdateCommand = { Start-PreUpdateSettingsCatalog @args } + PostGetCommand = { Start-PostGetSettingsCatalog @args } + Dependencies = @("ReusableSettings") + GroupId = "DeviceConfiguration" + }) + + Add-ViewItem (New-Object PSObject -Property @{ + Title = "Inventory Policies" + Id = "InventoryPolicies" + ViewID = "IntuneGraphAPI" + API = "/deviceManagement/inventoryPolicies" + PropertiesToRemove = @('settingCount') + Permissons=@("DeviceManagementConfiguration.ReadWrite.All") + NameProperty = "name" + ViewProperties = @("name","description","Id") + Expand="Settings" + Icon="DeviceConfiguration" + GroupId = "DeviceConfiguration" + }) + + Add-ViewItem (New-Object PSObject -Property @{ + Title = "BIOS Configurations" + Id = "HardwareConfigurations" + ViewID = "IntuneGraphAPI" + DetailExtension = { Add-PolicyFileExtensions @args } + ExportExtension = { Add-PolicyFileExportExtensions @args } + PostExportCommand = { Start-PostExportPolicyFile @args } + PropertiesToRemoveForUpdate = @('version') + PolicyFileAttribute = "configurationFileContent" + API = "/deviceManagement/hardwareConfigurations" + Permissons=@("DeviceManagementConfiguration.ReadWrite.All") + Icon="DeviceConfiguration" + GroupId = "DeviceConfiguration" + SupportsPageSize = $false + }) + + Add-ViewItem (New-Object PSObject -Property @{ + Title = "Role Definitions" + Id = "RoleDefinitions" + ViewID = "IntuneGraphAPI" + API = "/deviceManagement/roleDefinitions" + QUERYLIST = "`$filter=isBuiltIn%20eq%20false" + PostExportCommand = { Start-PostExportRoleDefinitions @args } + PreImportCommand = { Start-PreImportRoleDefinitions @args } + PostFileImportCommand = { Start-PostFileImportRoleDefinitions @args } + Permissons=@("DeviceManagementRBAC.ReadWrite.All") + ImportOrder = 20 + #expand=roleassignments + PropertiesToRemoveForUpdate = @('isBuiltInRoleDefinition','isBuiltIn','roleAssignments') ### !!! ToDo: Add support for roleAssignments + GroupId = "TenantAdmin" + ExpandAssignments = $false + ExpandAssignmentsList = $false + }) + + Add-ViewItem (New-Object PSObject -Property @{ + Title = "Scope (Tags)" + Id = "ScopeTags" + ViewID = "IntuneGraphAPI" + API = "/deviceManagement/roleScopeTags" + QUERYLIST = "`$filter=isBuiltIn%20eq%20false" + Permissons=@("DeviceManagementRBAC.ReadWrite.All") + PostExportCommand = { Start-PostExportScopeTags @args } + PostGetCommand = { Start-PostGetScopeTags @args } + ImportOrder = 10 + DocumentAll = $true + GroupId = "TenantAdmin" + ExpandAssignmentsList = $false # Adds the assignmnets property but always empty + }) + + Add-ViewItem (New-Object PSObject -Property @{ + Title = "Notifications" + Id = "Notifications" + ViewID = "IntuneGraphAPI" + API = "/deviceManagement/notificationMessageTemplates" + Permissons=@("DeviceManagementServiceConfig.ReadWrite.All") + ImportOrder = 40 + Expand = "localizedNotificationMessages" + PreImportCommand = { Start-PreImportNotifications @args } + PostFileImportCommand = { Start-PostFileImportNotifications @args } + PostCopyCommand = { Start-PostCopyNotifications @args } + PropertiesToRemoveForUpdate = @('defaultLocale','localizedNotificationMessages') ### !!! ToDo: Add support for localizedNotificationMessages + GroupId = "CompliancePolicies" + ExpandAssignmentsList = $false + }) + + # This has some pre-reqs for working! + # Import is tested and verified in a tenant with Googple Play connection configured + # And the OEM app was dpwnloaded e.g. Knox Service Plugin + # Import failed in a tenant where Google Play was NOT configured + Add-ViewItem (New-Object PSObject -Property @{ + Title = "Android OEM Config" + Id = "AndroidOEMConfig" + ViewID = "IntuneGraphAPI" + QUERYLIST = "`$filter=microsoft.graph.androidManagedStoreAppConfiguration/appSupportsOemConfig%20eq%20true" + API = "/deviceAppManagement/mobileAppConfigurations" + PreImportAssignmentsCommand = { Start-PreImportAssignmentsAppConfiguration @args } + PreFilesImportCommand = { Start-PreFilesImportAppConfiguration @args } + PostExportCommand = { Start-PostExportAppConfiguration @args } + Permissons=@("DeviceManagementConfiguration.ReadWrite.All") + Icon="DeviceConfiguration" + Dependencies = @("Applications") + GroupId = "DeviceConfiguration" + }) + + # Copy/Export/Import not verified! + Add-ViewItem (New-Object PSObject -Property @{ + Title = "Apple Enrollment Types" + Id = "AppleEnrollmentTypes" + ViewID = "IntuneGraphAPI" + API = "/deviceManagement/appleUserInitiatedEnrollmentProfiles" + Permissons=@("DeviceManagementServiceConfig.ReadWrite.All") + PropertiesToRemoveForUpdate = @('platform') + GroupId = "AppleEnrollment" + }) + + Add-ViewItem (New-Object PSObject -Property @{ + Title = "Filters" + Id = "AssignmentFilters" + ViewID = "IntuneGraphAPI" + API = "/deviceManagement/assignmentFilters" + Permissons=@("DeviceManagementConfiguration.ReadWrite.All") + ImportOrder = 15 + GroupId = "TenantAdmin" + PropertiesToRemoveForUpdate = @('platform') + ExpandAssignmentsList = $false + PropertiesToRemove = @("payloads") + }) + + Add-ViewItem (New-Object PSObject -Property @{ + Title = "Health Scripts" + Id = "DeviceHealthScripts" + ViewID = "IntuneGraphAPI" + QUERYLIST = "`$filter=isGlobalScript%20eq%20false" # Looks like filters are not working for deviceHealthScripts + API = "/deviceManagement/deviceHealthScripts" + PreDeleteCommand = { Start-PreDeleteDeviceHealthScripts @args } + PreImportCommand = { Start-PreImportDeviceHealthScripts @args } + PreUpdateCommand = { Start-PreUpdateDeviceHealthScripts @args } + PostExportCommand = { Start-PostExportDeviceHealthScripts @args } + ExportExtension = { Add-ScriptExportExtensions @args } + Permissons=@("DeviceManagementScripts.ReadWrite.All") + GroupId = "EndpointAnalytics" + Icon = "Report" + AssignmentsType = "deviceHealthScriptAssignments" + AssignmentProperties = @("target","runSchedule","runRemediationScript") + PropertiesToRemoveForUpdate = @('version','isGlobalScript','highestAvailableVersion') + }) + + Add-ViewItem (New-Object PSObject -Property @{ + Title = "ADMX Files" + Id = "ADMXFiles" + ViewID = "IntuneGraphAPI" + NameProperty = "fileName" + API = "/deviceManagement/groupPolicyUploadedDefinitionFiles" + Permissons=@("DeviceManagementConfiguration.ReadWrite.All") + ImportOrder = 45 + GroupId = "DeviceConfiguration" + Icon = "DeviceConfiguration" + ExpandAssignmentsList = $false + PreFilesImportCommand = { Start-PreFilesImportADMXFiles @args } + PreImportCommand = { Start-PreImportADMXFiles @args } + PostImportCommand = { Start-PostImportADMXFiles @args } + PreDeleteCommand = { Start-PreDeleteADMXFiles @args } + ViewProperties = @("fileName","status","Id") + PropertiesToRemove = @("languageCodes","targetPrefix","targetNamespace","policyType","revision","status","uploadDateTime") + SupportsPageSize = $false + }) + + <# + Add-ViewItem (New-Object PSObject -Property @{ + Title = "iOS Enrollment Profile" + Id = "iOSDepProfile" + ViewID = "IntuneGraphAPI" + API = "/deviceManagement/depIOSEnrollmentProfile" + Permissons=@("DeviceManagementServiceConfig.ReadWrite.All") + GroupId = "DeviceConfiguration" + Icon = "DeviceConfiguration" + ExpandAssignmentsList = $false + ViewProperties = @("fileName","status","Id") + }) + #> + + Add-ViewItem (New-Object PSObject -Property @{ + Title = "Reusable Settings" + Id = "ReusableSettings" + ViewID = "IntuneGraphAPI" + API = "/deviceManagement/reusablePolicySettings" + PropertiesToRemove = @('Settings','@OData.Type') + PostGetCommand = { Start-PostGetReusableSettings @args } + ImportOrder = 70 + Permissons=@("DeviceManagementConfiguration.ReadWrite.All") + ExpandAssignmentsList = $false + SkipRemoveProperties = @("@OData.Type") + Icon = "EndpointSecurity" + GroupId = "EndpointSecurity" + }) + + Add-ViewItem (New-Object PSObject -Property @{ + Title = "Authentication Strengths" + Id = "AuthenticationStrengths" + ViewID = "IntuneGraphAPI" + API = "/identity/conditionalAccess/authenticationStrengths/policies" + PreImportCommand = { Start-PreImportCommandAuthenticationStrengths @args } + PropertiesToRemove = @() + ImportOrder = 45 + Permissons=@("Policy.ReadWrite.ConditionalAccess") + ExpandAssignmentsList = $false + Icon = "ConditionalAccess" + GroupId = "EndpointSecurity" + SupportsPageSize = $false + }) + + Add-ViewItem (New-Object PSObject -Property @{ + Title = "Authentication Context" + Id = "AuthenticationContext" + ViewID = "IntuneGraphAPI" + API = "/identity/conditionalAccess/authenticationContextClassReferences" + PropertiesToRemove = @("@odata.type") + SkipRemoveProperties = @('Id') + ImportOrder = 46 + PreImportCommand = { Start-PreImportCommandAuthenticationContext @args } + Permissons=@("Policy.ReadWrite.ConditionalAccess") + ExpandAssignmentsList = $false + Icon = "ConditionalAccess" + GroupId = "EndpointSecurity" + SupportsPageSize = $false + }) + + Add-ViewItem (New-Object PSObject -Property @{ + Title = "W365 Provisioning Policies" + Id = "W365ProvisioningPolicies" + ViewID = "IntuneGraphAPI" + API = "/deviceManagement/virtualEndpoint/provisioningPolicies" + Permissons=@("CloudPC.ReadWrite.All") + Icon = "Devices" + GroupId = "DeviceConfiguration" + }) + + Add-ViewItem (New-Object PSObject -Property @{ + Title = "W365 User Settings" + Id = "W365UserSettings" + ViewID = "IntuneGraphAPI" + API = "/deviceManagement/virtualEndpoint/userSettings" + Permissons = @("CloudPC.ReadWrite.All") + Icon = "Devices" + GroupId = "DeviceConfiguration" + }) + + Add-ViewItem (New-Object PSObject -Property @{ + Title = "Driver Update Profiles" + Id = "DriverUpdateProfiles" + ViewID = "IntuneGraphAPI" + API = "/deviceManagement/windowsDriverUpdateProfiles" + Permissons = @("DeviceManagementConfiguration.ReadWrite.All") + Icon = "UpdatePolicies" + GroupId = "WinDriverUpdatePolicies" + }) + + Add-ViewItem (New-Object PSObject -Property @{ + Title = "Device Categories" + Id = "DeviceCategories" + ViewID = "IntuneGraphAPI" + API = "/deviceManagement/deviceCategories" + QUERYLIST = "`$top=500" + Permissons = @("DeviceManagementConfiguration.ReadWrite.All") + GroupId = "DeviceConfiguration" + ExpandAssignmentsList = $false + }) + +} + +function Invoke-EMAuthenticateToMSAL +{ + param($params = @{}) + + $global:EMViewObject.AppInfo = Get-GraphAppInfo "EMAzureApp" $global:DefaultAzureApp "EM" + Set-MSALCurrentApp $global:EMViewObject.AppInfo + & $global:msalAuthenticator.Login -Account (?? $global:MSALToken.Account.UserName (Get-Setting "" "LastLoggedOnUser")) @params +} + +function Invoke-EMDeactivateView +{ + $tmp = $mnuMain.Items | Where Name -eq "EMBulk" + if($tmp) { $mnuMain.Items.Remove($tmp) } +} + +function Invoke-EMActivatingView +{ + Show-MSALError + + # Refresh values in case they have changed + $global:EMViewObject.AppInfo = (Get-GraphAppInfo "EMAzureApp" $global:DefaultAzureApp "EM") + if(-not $global:EMViewObject.Authentication) + { + $global:EMViewObject.Authentication = Get-MSALAuthenticationObject + } + + # Add View specific menus + Add-GraphBulkMenu +} + +function Invoke-EMSaveSettings +{ + $tmpApp = Get-GraphAppInfo "EMAzureApp" $global:DefaultAzureApp + + if($global:appObj.ClientID -ne $tmpApp.ClientId -and $global:MSALToken) + { + # The app has changed. Need to authenticate to the new app + Write-Status "Logging in to $((?? $global:appObj.Name "selected application"))" + $global:EMViewObject.AppInfo = $tmpApp + Set-MSALCurrentApp $global:EMViewObject.AppInfo + Clear-MSALCurentUserVaiables + Connect-MSALUser -Account $global:MSALToken.Account.Username + Write-Status "" + } + + Set-EMUIStatus +} + +function Invoke-GraphAuthenticationUpdated +{ + Set-EMUIStatus + + $script:CustomADMXDefinitions = $null +} + +function Set-EMUIStatus +{ + # Hide/Show Delete button + $allowDelete = Get-SettingValue "EMAllowDelete" + $global:btnDelete.Visibility = (?: ($allowDelete -eq $true) "Visible" "Collapsed") + + # Hide/Show Delete on Bulk menu + $allowBulkDelete = Get-SettingValue "EMAllowBulkDelete" + $mnuBulk = $mnuMain.Items | Where Name -eq "EMBulk" + + if($mnuBulk) + { + $mnuBulkDelete = $mnuBulk.Items | Where Name -eq "mnuBulkDelete" + if($mnuBulkDelete) + { + $mnuBulkDelete.Visibility = (?: ($allowBulkDelete -eq $true) "Visible" "Collapsed") + } + } +} + +function Set-EMViewPanel +{ + param($panel) + + # ToDo: Create View specific pannel and move this to graph + Add-XamlEvent $panel "btnView" "Add_Click" -scriptBlock ([scriptblock]{ + Show-GraphObjectInfo + }) + + Add-XamlEvent $panel "btnDelete" "Add_Click" -scriptBlock ([scriptblock]{ + Remove-GraphObjects + }) + + Add-XamlEvent $panel "btnCopy" "Add_Click" -scriptBlock ([scriptblock]{ + Copy-GraphObject + }) + + Add-XamlEvent $panel "btnExport" "Add_Click" -scriptBlock ([scriptblock]{ + Show-GraphExportForm + }) + + Add-XamlEvent $panel "btnImport" "Add_Click" -scriptBlock ([scriptblock]{ + Show-GraphImportForm + }) + + Add-XamlEvent $panel "txtFilter" "Add_LostFocus" ({ #param($obj, $e) + Invoke-FilterBoxChanged $this + #$e.Handled = $true + }) + + Add-XamlEvent $panel "txtFilter" "Add_GotFocus" ({ + if($this.Tag -eq "1" -and $this.Text -eq "Filter") { $this.Text = "" } + Invoke-FilterBoxChanged $this + }) + + Add-XamlEvent $panel "txtFilter" "Add_TextChanged" ({ + Invoke-FilterBoxChanged $this + }) + + Invoke-FilterBoxChanged ($panel.FindName("txtFilter")) + + $allowDelete = Get-SettingValue "EMAllowDelete" + Set-XamlProperty $panel "btnDelete" "Visibility" (?: ($allowDelete -eq $true) "Visible" "Collapsed") + + $global:dgObjects.add_selectionChanged({ + Invoke-ModuleFunction "Invoke-EMSelectedItemsChanged" + }) + + # ToDo: Move this to the view object + $dpd = [System.ComponentModel.DependencyPropertyDescriptor]::FromProperty([System.Windows.Controls.ItemsControl]::ItemsSourceProperty, [System.Windows.Controls.DataGrid]) + if($dpd) + { + $dpd.AddValueChanged($global:dgObjects, { + Set-XamlProperty $global:dgObjects.Parent "txtFilter" "Text" "" + $enabled = (?: ($null -eq $this.ItemsSource -or ($this.ItemsSource | measure).Count -eq 0) $false $true) + Set-XamlProperty $global:dgObjects.Parent "btnImport" "IsEnabled" $true # Always all Import if ObjectType allows it + Set-XamlProperty $global:dgObjects.Parent "btnExport" "IsEnabled" $enabled + }) + } + + $btnRefresh = Get-XamlObject ($global:AppRootFolder + "\Xaml\RefreshButton.xaml") + if($btnRefresh) + { + $btnRefresh.SetValue([System.Windows.Controls.Grid]::ColumnProperty,$grdTitle.ColumnDefinitions.Count - 1) + $btnRefresh.Margin = "0,0,5,3" + $btnRefresh.Cursor = "Hand" + $btnRefresh.Name = "btnRefresh" + $btnRefresh.Focusable = $false + $grdTitle.Children.Add($btnRefresh) | Out-Null + + $tooltip = [System.Windows.Controls.ToolTip]::new() + $tooltip.Content = "Refresh all objects" + [System.Windows.Controls.ToolTipService]::SetToolTip($btnRefresh, $tooltip) + + $panel.RegisterName($btnRefresh.Name, $btnRefresh) + + $tooltip = [System.Windows.Controls.ToolTip]::new() + $tooltip.Content = "Refresh objects" + + [System.Windows.Controls.ToolTipService]::SetToolTip($btnRefresh, $tooltip) + + $btnRefresh.Add_Click({ + $txtFilterText = $null + $txtFilter = $this.Parent.FindName("txtFilter") + if($txtFilter) { $txtFilterText = $txtFilter.Text } #= "" } + + Show-GraphObjects $txtFilterText + + if($txtFilterText -and $txtFilter) + { + $txtFilter.Text = $txtFilterText + Invoke-FilterBoxChanged $txtFilter + } + + Write-Status "" + }) + } + + $global:btnLoadAllPages.add_click({ + Write-Status "Loading $($global:curObjectType.Title) objects" + [array]$graphObjects = Get-GraphObjects -property $global:curObjectType.ViewProperties -objectType $global:curObjectType -AllPages + if(-not $global:dgObjects.Columns) + { + Show-GraphObjects -FromGraphObjects $graphObjects + } + else + { + $graphObjects | Where-Object { $_ -ne $null } | ForEach-Object { $global:dgObjects.ItemsSource.AddNewItem($_) | Out-Null } + } + $global:dgObjects.ItemsSource.CommitNew() + Set-GraphPagesButtonStatus + Invoke-FilterBoxChanged $global:txtFilter -ForceUpdate + Write-Status "" + }) + + $global:btnLoadNextPage.add_click({ + Write-Status "Loading $($global:curObjectType.Title) objects" + [array]$graphObjects = Get-GraphObjects -property $global:curObjectType.ViewProperties -objectType $global:curObjectType -SinglePage + if(-not $global:dgObjects.Columns) + { + Show-GraphObjects -FromGraphObjects $graphObjects + } + else + { + $graphObjects | Where-Object { $_ -ne $null } | ForEach-Object { $global:dgObjects.ItemsSource.AddNewItem($_) | Out-Null } + } + $global:dgObjects.ItemsSource.CommitNew() + Set-GraphPagesButtonStatus + Invoke-FilterBoxChanged $global:txtFilter + Write-Status "" + }) +} + +function Invoke-GraphObjectsChanged +{ + $btnRefresh = $global:EMViewObject.ViewPanel.FindName("btnRefresh") + + if($btnRefresh) + { + $tooltip = [System.Windows.Controls.ToolTipService]::GetToolTip($btnRefresh) + if($global:lstMenuItems.SelectedItem.QuerySearch -eq $true) + { + $tooltip.Content = "Refresh objects based on filter. Note: Only filtered objects will be returned. Clear filter and press refresh to reload other objects" + } + else + { + $tooltip.Content = "Refresh all objects" + } + } +} + +function Invoke-EMSelectedItemsChanged +{ + $hasSelectedItems = ($global:dgObjects.ItemsSource | Where IsSelected -eq $true) -or ($null -ne $global:dgObjects.SelectedItem) + Set-XamlProperty $global:dgObjects.Parent "btnView" "IsEnabled" $hasSelectedItems #(?: ($null -eq ($global:dgObjects.SelectedItem)) $false $true) + Set-XamlProperty $global:dgObjects.Parent "btnCopy" "IsEnabled" $hasSelectedItems #(?: ($null -eq $global:dgObjects.SelectedItem) $false $true) + Set-XamlProperty $global:dgObjects.Parent "btnDelete" "IsEnabled" $hasSelectedItems #(?: ($null -eq $global:dgObjects.SelectedItem -and $global:curObjectType.AllowDelete -ne $false) $false $true) +} + +function Invoke-FilterBoxChanged +{ + param($txtBox,[switch]$ForceUpdate) + + $filter = $null + + if($txtBox.Text.Trim() -eq "" -and $txtBox.IsFocused -eq $false) + { + $txtBox.FontStyle = "Italic" + $txtBox.Tag = 1 + $txtBox.Text = "Filter" + $txtBox.Foreground="Lightgray" + } + elseif($ForceUpdate -eq $true) + { + $dgObjects.ItemsSource.Filter = $dgObjects.ItemsSource.Filter + } + elseif($txtBox.Tag -eq "1" -and $txtBox.Text -eq "Filter" -and $txtBox.IsFocused -eq $false) + { + + } + else + { + $txtBox.FontStyle = "Normal" + $txtBox.Tag = $null + $txtBox.Foreground="Black" + $txtBox.Background="White" + + if($txtBox.Text) + { + $filter = { + param ($item) + + return ($null -ne ($item.PSObject.Properties | Where { $_.Name -notin @("IsSelected","Object", "ObjectType") -and $_.Value -match [regex]::Escape($txtBox.Text) })) + + foreach($prop in ($item.PSObject.Properties | Where { $_.Name -notin @("IsSelected","Object", "ObjectType")})) + { + if($prop.Value -match [regex]::Escape($txtBox.Text)) { return $true } + } + $false + } + } + } + + if($dgObjects.ItemsSource -is [System.Windows.Data.ListCollectionView] -and $txtBox.IsFocused -eq $true) + { + $dgObjects.ItemsSource.Filter = $filter + } + + $allObjectsCount = 0 + if($dgObjects.ItemsSource.SourceCollection) + { + $allObjectsCount = $dgObjects.ItemsSource.SourceCollection.Count + } + + $objCount = ($dgObjects.ItemsSource | measure).Count + if($objCount -gt 0) + { + $strAllObjectsInfo = "" + if($allObjectsCount -gt $objCount) + { + $strAllObjectsInfo = " ($($allObjectsCount))" + } + $global:txtEMObjects.Text = "Objects: $objCount$strAllObjectsInfo" + } + else + { + $global:txtEMObjects.Text = "" + } +} +#region Endpoint Security (Intents) functions + +function Start-PreImportEndpointSecurity +{ + param($obj, $objectType) + + @{ + "API"="deviceManagement/templates/$($obj.templateId)/createInstance" + } +} + +function Start-PostListEndpointSecurity +{ + param($objList, $objectType) + + if(-not $script:baseLineTemplates) + { + $script:baseLineTemplates = (Invoke-GraphRequest -Url "/deviceManagement/templates").Value + } + if(-not $script:baseLineTemplates) { return } + + foreach($obj in $objList) + { + if(-not $obj.Object.templateId) { continue } + if($obj.Object.templateId -ne $baseLineTemplate.Id) + { + $baseLineTemplate = $script:baseLineTemplates | Where Id -eq $obj.Object.templateId + } + + if($baseLineTemplate) + { + $obj | Add-Member -MemberType NoteProperty -Name "Type" -Value $baseLineTemplate.displayName + $obj | Add-Member -MemberType NoteProperty -Name "Category" -Value (?: ($baseLineTemplate.templateSubtype -eq "none") $baseLineTemplate.templateType $baseLineTemplate.templateSubtype) + } + + } + $objList +} + +function Start-PostExportEndpointSecurity +{ + param($obj, $objectType, $path) + + $fileName = (Get-GraphObjectName $obj $objectType).Trim('.') + if((Get-SettingValue "AddIDToExportFile") -eq $true -and $obj.Id) + { + $fileName = ($fileName + "_" + $obj.Id) + } + + $settings = Invoke-GraphRequest -Url "$($objectType.API)/$($obj.id)/settings" + $settingsJson = "{ `"settings`": $((ConvertTo-Json $settings.value -Depth 20 ))`n}" + $fileName = "$path\$((Remove-InvalidFileNameChars $fileName))_Settings.json" + Save-GraphObjectToFile $settingsJson $fileName +} + +function Start-PostFileImportEndpointSecurity +{ + param($obj, $objectType, $file) + + $settings = Get-EMSettingsObject $obj $objectType $file + if($settings) + { + Start-GraphPreImport $settings + Invoke-GraphRequest -Url "$($objectType.API)/$($obj.id)/updateSettings" -Body ($settings | ConvertTo-Json -Depth 50) -Method "POST" + } +} + +function Start-PreCopyEndpointSecurity +{ + param($obj, $objectType, $newName) + + $false + + # Intents has a createCopy method. Use "manual" copy to have one standard and making sure Copy works the same as Export/Import + # These objects supports duplicate in the portal + # Keep for reference + # + # $objData = "{`"displayName`":`"$($newName)`"}" + # + #Invoke-GraphRequest -Url "/deviceManagement/intents/$($obj.Id)/createCopy" -Content $objData -HttpMethod "POST" | Out-Null + #$true +} + +function Start-PostCopyEndpointSecurity +{ + param($objCopyFrom, $objNew, $objectType) + + $settings = Invoke-GraphRequest -Url "$($objectType.API)/$($objCopyFrom.id)/settings" -ODataMetadata "Skip" + if($settings) + { + $settingsObj = New-object PSObject @{ "Settings" = $settings.Value } + Invoke-GraphRequest -Url "$($objectType.API)/$($objNew.id)/updateSettings" -Body ($settingsObj | ConvertTo-Json -Depth 20) -Method "POST" + } +} + +function Start-PreUpdateEndpointSecurity +{ + param($obj, $objectType, $curObject, $fromObj) + + if(-not $fromObj.settings) { return } + + $strAPI = "/deviceManagement/intents/$($curObject.Object.id)/updateSettings" + + $curObject = Get-GraphObject $curObject.Object $objectType + + $curValues = @() + foreach($val in $curObject.Object.settings) + { + if($fromObj.settings | Where { $_.definitionId -eq $val.definitionId}) { continue } + + # Set all existing values to null + # Note: This will not remove them from the configured list just set them Not Configured + $curValues += [PSCustomObject]@{ + '@odata.type' = $val.'@odata.type' + definitionId = $val.definitionId + id = $val.id + valueJson = "null" + } + } + + $curValues += $fromObj.settings + + <# + if($curValues.Count -gt 0) + { + $tmpObj = [PSCustomObject]@{ + settings = $curValues + } + $json = ConvertTo-Json $tmpObj -Depth 20 + + # Set all existing values to null + # Note: This will not remove them from the configured list just set them Not Configured + Invoke-GraphRequest -Url $strAPI -Content $json -HttpMethod "POST" | Out-Null + } + #> + + $tmpObj = [PSCustomObject]@{ + settings = $curValues + } + Start-GraphPreImport $tmpObj.settings + + $json = ConvertTo-Json $tmpObj -Depth 20 + Invoke-GraphRequest -Url $strAPI -Content $json -HttpMethod "POST" | Out-Null + + Remove-Property $obj "templateId" +} + +function Start-PostGetEndpointSecurity +{ + param($obj, $objectType) + + Add-EndpointSecurityInfo $obj +} + +function local:Add-EndpointSecurityInfo +{ + param($obj, $baseLineTemplate = $null) + +} +#endregion + +#region + +function Start-PostFileImportDeviceConfiguration +{ + param($obj, $objectType, $importFile) + + if($obj.'@OData.Type' -like "#microsoft.graph.windows10GeneralConfiguration") + { + $tmpObj = Get-GraphObjectFromFile $importFile + + if(($tmpObj.privacyAccessControls | measure).Count -gt 0) + { + $privacyObj = [PSCustomObject]@{ + windowsPrivacyAccessControls = $tmpObj.privacyAccessControls + } + $json = $privacyObj | ConvertTo-Json -Depth 20 + $ret = Invoke-GraphRequest -Url "deviceManagement/deviceConfigurations('$($obj.Id)')/windowsPrivacyAccessControls" -Body $json -Method "POST" + } + } +} + +function Start-PostCopyDeviceConfiguration +{ + param($objCopyFrom, $objNew, $objectType) + + if($objCopyFrom.'@OData.Type' -like "#microsoft.graph.windows10GeneralConfiguration") + { + if(($objCopyFrom.privacyAccessControls | measure).Count -gt 0) + { + $privacyObj = [PSCustomObject]@{ + windowsPrivacyAccessControls = $objCopyFrom.privacyAccessControls + } + $json = $privacyObj | ConvertTo-Json -Depth 20 + Invoke-GraphRequest -Url "deviceManagement/deviceConfigurations('$($objNew.Id)')/windowsPrivacyAccessControls" -Body $json -Method "POST" | Out-null + } + } +} + +function Start-PostGetDeviceConfiguration +{ + param($obj, $objectType) + + if(($obj.Object.omaSettings | measure).Count -gt 0) + { + foreach($omaSetting in ($obj.Object.omaSettings | Where isEncrypted -eq $true)) + { + if($omaSetting.isEncrypted -eq $false) { continue } + + $xmlValue = Invoke-GraphRequest -Url "/deviceManagement/deviceConfigurations/$($obj.Object.Id)/getOmaSettingPlainTextValue(secretReferenceValueId='$($omaSetting.secretReferenceValueId)')" + if($xmlValue.Value) + { + $omaSetting.isEncrypted = $false + $omaSetting.secretReferenceValueId = $null + + if($omaSetting.'@odata.type' -eq "#microsoft.graph.omaSettingStringXml" -or + $omaSetting.'value@odata.type' -eq "#Binary") + { + $Bytes = [System.Text.Encoding]::UTF8.GetBytes($xmlValue.Value) + $omaSetting.value = [Convert]::ToBase64String($bytes) + } + else + { + $omaSetting.value = $xmlValue.Value + } + } + } + } +} + +#endregion + +#region Compliance Policy +function Start-PostExportCompliancePolicies +{ + param($obj, $objectType, $exportPath) + + foreach($scheduledActionsForRule in $obj.scheduledActionsForRule) + { + foreach($scheduledActionConfiguration in $scheduledActionsForRule.scheduledActionConfigurations) + { + foreach($notificationMessageCCGroup in $scheduledActionConfiguration.notificationMessageCCList) + { + Add-GroupMigrationObject $notificationMessageCCGroup + } + } + } +} + +function Start-PreUpdateCompliancePolicies +{ + param($obj, $objectType, $curObject, $fromObj) + + $strAPI = "/deviceManagement/deviceCompliancePolicies/$($curObject.Object.id)/scheduleActionsForRules" + + $tmpObj = [PSCustomObject]@{ + deviceComplianceScheduledActionForRules = $obj.scheduledActionsForRule + } + + $json = ConvertTo-Json $tmpObj -Depth 20 + Invoke-GraphRequest -Url $strAPI -Content $json -HttpMethod "POST" | Out-Null + + Remove-Property $obj "scheduledActionsForRule" +} + +#endregion + +function Start-PostImportComplianceScripts +{ + param($obj, $objectType, $file) + + $endTime = (Get-Date).AddMinutes(2) + + $found = $false + while($endTime -gt (Get-Date)) + { + $tmpObj = Invoke-GraphRequest -Url "$($objectType.API)/$($obj.Id)" -ErrorAction SilentlyContinue + if($tmpObj) { + $found = $true + break + } + Start-Sleep -Seconds 10 + } + + if(-not $found) + { + Write-LogError "Compliance script $($obj.Id) not found after import. Please check the import file." + return + } +} + +#region Intune Branding functions +function Start-PreImportIntuneBranding +{ + param($obj, $objectType) + + $ret = @{} + $global:brandingClone = $null + + if($obj.isDefaultProfile) + { + + # Looks like the ID is the same for all tenants so skip this for now + <# + $defObj = (Invoke-GraphRequest -Url "/deviceManagement/intuneBrandingProfiles?`$filter=isDefaultProfile eq true&`$select=id,displayName").Value[0] + if($defObj) + { + $obj.Id = $defObj.Id + } + #> + + $ret.Add("API",($objectType.API + "/" + $obj.Id)) + $ret.Add("Method","PATCH") # Default profile always exists so update it + + foreach($prop in @("profileName","isDefaultProfile","disableClientTelemetry","profileDescription")) + { + Remove-Property $obj $prop + } + + $ret + } + else + { + # Create new Branding profile does not support images data in the json + # Workaround: (as done by the portal) + # Create a new profile with basic info + # Patch the profile with all the info + + $global:brandingClone = $obj | ConvertTo-Json -Depth 20 | ConvertFrom-Json + + foreach($prop in ($obj.PSObject.Properties | Where {$_.Name -notin @("profileName","profileDescription","roleScopeTagIds")})) #"customPrivacyMessage" + { + Remove-Property $obj $prop.Name + } + } + Remove-Property $obj "Id" +} + +function Start-PostImportIntuneBranding +{ + param($obj, $objectType, $file) + + if($obj.isDefaultProfile -or -not $global:brandingClone) { return } + + foreach($prop in @("Id","isDefaultProfile","customPrivacyMessage","disableClientTelemetry")) #"isDefaultProfile","disableClientTelemetry" + { + Remove-Property $global:brandingClone $prop + } + $json = ($global:brandingClone | ConvertTo-Json -Depth 20) + Invoke-GraphRequest -Url "$($objectType.API)/$($obj.Id)" -Body $json -Method "PATCH" | Out-Null +} + +function Start-PostGetIntuneBranding +{ + param($obj, $objectType) + + foreach($imgType in @("themeColorLogo","lightBackgroundLogo","landingPageCustomizedImage")) + { + Write-LogDebug "Get $imgType for $($obj.Object.profileName)" + $imgJson = Invoke-GraphRequest -Url "$($objectType.API)/$($obj.Object.Id)/$imgType" + if($imgJson.Value) + { + $obj.Object.$imgType = $imgJson + } + } +} + +function Start-PostExportIntuneBranding +{ + param($obj, $objectType, $path) + + foreach($imgType in @("themeColorLogo","lightBackgroundLogo","landingPageCustomizedImage")) + { + if($obj.$imgType.Value) + { + $fileName = "$path\$((Get-GraphObjectName $obj $objectType))_$imgType.jpg" + [IO.File]::WriteAllBytes($fileName, [System.Convert]::FromBase64String($obj.$imgType.Value)) + } + } +} + +function Start-PreDeleteIntuneBranding +{ + param($obj, $objectType) + + if($obj.isDefaultProfile -eq $true) + { + @{ "Delete" = $false } + } +} + +function Start-PreUpdateIntuneBranding +{ + param($obj, $objectType, $curObject, $fromObj) + + if($curObject.Object.isDefaultProfile) + { + foreach($prop in @("profileName","isDefaultProfile","disableClientTelemetry","profileDescription")) + { + Remove-Property $obj $prop + } + } +} + +#endregion + +#region Azure Branding functions +function Start-PreImportAzureBranding +{ + param($obj, $objectType) + + Remove-Property $obj "@odata.Type" + + $ret = @{} + if($obj.Id -eq "0") + { + #$ret.Add("Method","PATCH") # Default profile always exists so update it + #$ret.Add("API",($objectType.API + "/0")) + } + + $ret.Add("API",($objectType.API + "/$($global:Organization.Id)/branding/localizations")) + + # This is NOT wat the documentation says + # Documentation says to use Content-Language + # Any place the documentation states to use Accept-Language is for Get operation + # https://docs.microsoft.com/en-us/graph/api/organizationalbrandingproperties-get?view=graph-rest-beta&tabs=http#request-headers + $ret.Add("AdditionalHeaders", @{ "Accept-Language" = $obj.Id }) + + $ret +} + +function Start-PostListAzureBranding +{ + param($objList, $objectType) + + foreach($obj in $objList) + { + if(-not $obj.Object.id) { continue } + try + { + if($obj.Object.id -eq "0") + { + $language = "Default" + } + else + { + $language = ([cultureinfo]::GetCultureInfo($obj.Object.id)).DisplayName + } + + $obj | Add-Member -MemberType NoteProperty -Name "Language" -Value $language + } + catch{} + } + $objList +} + +#endregion + +#region Script functions +function Add-ScriptExtensions +{ + param($form, $buttonPanel, $index = 0) + + $btnDownload = New-Object System.Windows.Controls.Button + $btnDownload.Content = 'Download' + $btnDownload.Name = 'btnDownload' + $btnDownload.Margin = "0,0,5,0" + $btnDownload.Width = "100" + + $btnDownload.Add_Click({ + Invoke-DownloadScript + }) + + $tmp = $form.FindName($buttonPanel) + if($tmp) + { + $tmp.Children.Insert($index, $btnDownload) + } + + $btnDownload = New-Object System.Windows.Controls.Button + $btnDownload.Content = 'Edit' + $btnDownload.Name = 'btnEdit' + $btnDownload.Margin = "0,0,5,0" + $btnDownload.Width = "100" + + $btnDownload.Add_Click({ + Invoke-EditScript + }) + + $tmp = $form.FindName($buttonPanel) + if($tmp) + { + $tmp.Children.Insert($index, $btnDownload) + } +} + +function Add-ScriptExportExtensions +{ + param($form, $buttonPanel, $index = 0) + + $ctrl = $form.FindName("chkExportScript") + if(-not $ctrl) + { + $xaml = @" + + +"@ + $label = [Windows.Markup.XamlReader]::Parse($xaml) + + $global:chkExportScript = [System.Windows.Controls.CheckBox]::new() + $global:chkExportScript.IsChecked = $true + $global:chkExportScript.VerticalAlignment = "Center" + $global:chkExportScript.Name = "chkExportScript" + + @($label, $global:chkExportScript) + } +} + +function Start-PostExportScripts +{ + param($obj, $objectType, $exportPath) + + if($obj.scriptContent -and $global:chkExportScript.IsChecked) + { + Write-Log "Export script $($obj.FileName)" + $fileName = [IO.Path]::Combine($exportPath, $obj.FileName) + [IO.File]::WriteAllBytes($fileName, ([System.Convert]::FromBase64String($obj.scriptContent))) + } +} + +function Invoke-DownloadScript +{ + if(-not $global:dgObjects.SelectedItem.Object.id) { return } + + $obj = (Get-GraphObject $global:dgObjects.SelectedItem $global:curObjectType).Object + Write-Status "" + + if($obj.scriptContent) + { + Write-Log "Download PowerShell script '$($obj.FileName)' from $($obj.displayName)" + + $dlgSave = New-Object -Typename System.Windows.Forms.SaveFileDialog + $dlgSave.InitialDirectory = Get-SettingValue "IntuneRootFolder" $env:Temp + $dlgSave.FileName = $obj.FileName + if($dlgSave.ShowDialog() -eq [System.Windows.Forms.DialogResult]::OK -and $dlgSave.Filename) + { + # Changed to WriteAllBytes to get rid of BOM characters from Custom Attribute file + [IO.File]::WriteAllBytes($dlgSave.FileName, ([System.Convert]::FromBase64String($obj.scriptContent))) + } + } +} + +function Invoke-EditScript +{ + if(-not $global:dgObjects.SelectedItem.Object.id) { return } + + $obj = (Get-GraphObject $global:dgObjects.SelectedItem $global:curObjectType) + Write-Status "" + if(-not $obj.Object.scriptContent) { return } + $script:currentScriptObject = $obj + + $script:editForm = Get-XamlObject ($global:AppRootFolder + "\Xaml\EditScriptDialog.xaml") + + if(-not $script:editForm) { return } + + Set-XamlProperty $script:editForm "txtEditScriptTitle" "Text" "Edit: $($obj.Object.displayName)" + + $scriptText = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($obj.Object.scriptContent)) + Set-XamlProperty $script:editForm "txtScriptText" "Text" $scriptText + + $script:currentModal = $null + if($global:grdModal.Children.Count -gt 0) + { + $script:currentModal = $global:grdModal.Children[0] + } + + Add-XamlEvent $script:editForm "btnSaveScriptEdit" "add_click" ({ + $scriptText = Get-XamlProperty $script:editForm "txtScriptText" "Text" + $pre = [System.Text.Encoding]::UTF8.GetPreamble() + $utfBOM = [System.Text.Encoding]::UTF8.GetString($pre) + if($scriptText.startsWith($utfBOM)) + { + # Remove UTF8 BOM bytes + $scriptText = $scriptText.Remove(0, $utfBOM.Length) + } + $bytes = [System.Text.Encoding]::UTF8.GetBytes($scriptText) + $encodedText = [Convert]::ToBase64String($bytes) + + if($script:currentScriptObject.Object.scriptContent -ne $encodedText) + { + # Save script + if(([System.Windows.MessageBox]::Show("Are you sure you want to update the script?`n`nObject:`n$($script:currentScriptObject.displayName)", "Update script?", "YesNo", "Warning")) -eq "Yes") + { + Write-Status "Update $($script:currentScriptObject.displayName)" + $obj = $script:currentScriptObject.Object | ConvertTo-Json -Depth 20 | ConvertFrom-Json + $obj.scriptContent = $encodedText + Start-GraphPreImport $obj $script:currentScriptObject.ObjectType + foreach($prop in $script:currentScriptObject.ObjectType.PropertiesToRemoveForUpdate) + { + Remove-Property $obj $prop + } + Remove-Property $obj "Assignments" + Remove-Property $obj "isAssigned" + + $json = ConvertTo-Json $obj -Depth 15 + + $objectUpdated = (Invoke-GraphRequest -Url "$($script:currentScriptObject.ObjectType.API)/$($script:currentScriptObject.Object.Id)" -Content $json -HttpMethod "PATCH") + if(-not $objectUpdated) + { + Write-Log "Failed to update script" 3 + [System.Windows.MessageBox]::Show("Failed to save the script object. See log for more information","Update failed!", "OK", "Error") + } + Write-Status "" + } + } + + $global:grdModal.Children.Clear() + if($script:currentModal) + { + $global:grdModal.Children.Add($script:currentModal) + } + [System.Windows.Forms.Application]::DoEvents() + }) + + Add-XamlEvent $script:editForm "btnCancelScriptEdit" "add_click" ({ + $global:grdModal.Children.Clear() + if($script:currentModal) + { + $global:grdModal.Children.Add($script:currentModal) + } + [System.Windows.Forms.Application]::DoEvents() + }) + + $global:grdModal.Children.Clear() + $script:editForm.SetValue([System.Windows.Controls.Grid]::RowProperty,1) + $script:editForm.SetValue([System.Windows.Controls.Grid]::ColumnProperty,1) + $global:grdModal.Children.Add($script:editForm) | Out-Null + [System.Windows.Forms.Application]::DoEvents() +} + +#endregion + +#region Policy File functions +function Add-PolicyFileExtensions +{ + param($form, $buttonPanel, $index = 0) + + $btnDownload = New-Object System.Windows.Controls.Button + $btnDownload.Content = 'Download' + $btnDownload.Name = 'btnDownload' + $btnDownload.Margin = "0,0,5,0" + $btnDownload.Width = "100" + + $btnDownload.Add_Click({ + Invoke-DownloadPolicyFile + }) + + $tmp = $form.FindName($buttonPanel) + if($tmp) + { + $tmp.Children.Insert($index, $btnDownload) + } + + $btnDownload = New-Object System.Windows.Controls.Button + $btnDownload.Content = 'Edit' + $btnDownload.Name = 'btnEdit' + $btnDownload.Margin = "0,0,5,0" + $btnDownload.Width = "100" + + $btnDownload.Add_Click({ + Invoke-EditPolicyFile + }) + + $tmp = $form.FindName($buttonPanel) + if($tmp) + { + $tmp.Children.Insert($index, $btnDownload) + } +} + +function Add-PolicyFileExportExtensions +{ + param($form, $buttonPanel, $index = 0) + + $ctrl = $form.FindName("chkExportPolicyFile") + if(-not $ctrl) + { + $xaml = @" + + +"@ + $label = [Windows.Markup.XamlReader]::Parse($xaml) + + $global:chkExportPolicyFile = [System.Windows.Controls.CheckBox]::new() + $global:chkExportPolicyFile.IsChecked = $true + $global:chkExportPolicyFile.VerticalAlignment = "Center" + $global:chkExportPolicyFile.Name = "chkExportPolicyFile" + + @($label, $global:chkExportPolicyFile) + } +} + +function Start-PostExportPolicyFile +{ + param($obj, $objectType, $exportPath) + + if($objectType.PolicyFileAttribute -and $obj.$($objectType.PolicyFileAttribute) -and $global:chkExportPolicyFile.IsChecked) + { + Write-Log "Export policy file from attribute $($obj.PolicyFileAttribute)" + $fileNameOut = ?? $obj.FileName "$($obj.PolicyFileAttribute).json" + $fileName = [IO.Path]::Combine($exportPath, $fileNameOut) + [IO.File]::WriteAllBytes($fileName, ([System.Convert]::FromBase64String($obj.$($objectType.PolicyFileAttribute)))) + } +} + +function Invoke-DownloadPolicyFile +{ + if(-not $global:dgObjects.SelectedItem.Object.id) { return } + + $obj = (Get-GraphObject $global:dgObjects.SelectedItem $global:curObjectType).Object + Write-Status "" + + if($global:curObjectType.PolicyFileAttribute -and $obj.$($global:curObjectType.PolicyFileAttribute)) + { + $fileNameOut = ?? $obj.FileName "$($obj.PolicyFileAttribute).json" + Write-Log "Download policy file '$($fileNameOut)' from $($obj.displayName)" + + $dlgSave = New-Object -Typename System.Windows.Forms.SaveFileDialog + $dlgSave.InitialDirectory = Get-SettingValue "IntuneRootFolder" $env:Temp + $dlgSave.FileName = $fileNameOut + if($dlgSave.ShowDialog() -eq [System.Windows.Forms.DialogResult]::OK -and $dlgSave.Filename) + { + # Changed to WriteAllBytes to get rid of BOM characters from Custom Attribute file + [IO.File]::WriteAllBytes($dlgSave.FileName, ([System.Convert]::FromBase64String($obj.$($global:curObjectType.PolicyFileAttribute)))) + } + } +} + +function Invoke-EditPolicyFile +{ + if(-not $global:dgObjects.SelectedItem.Object.id) { return } + + $obj = (Get-GraphObject $global:dgObjects.SelectedItem $global:curObjectType) + Write-Status "" + if(-not $global:curObjectType.PolicyFileAttribute -or -not $obj.Object.$($global:curObjectType.PolicyFileAttribute)) { return } + $script:currentScriptObject = $obj + + $script:editForm = Get-XamlObject ($global:AppRootFolder + "\Xaml\EditScriptDialog.xaml") + + if(-not $script:editForm) { return } + + Set-XamlProperty $script:editForm "txtEditScriptTitle" "Text" "Edit: $($obj.Object.displayName)" + + $scriptText = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($obj.Object.$($global:curObjectType.PolicyFileAttribute))) + Set-XamlProperty $script:editForm "txtScriptText" "Text" $scriptText + + $script:currentModal = $null + if($global:grdModal.Children.Count -gt 0) + { + $script:currentModal = $global:grdModal.Children[0] + } + + Add-XamlEvent $script:editForm "btnSaveScriptEdit" "add_click" ({ + $scriptText = Get-XamlProperty $script:editForm "txtScriptText" "Text" + $pre = [System.Text.Encoding]::UTF8.GetPreamble() + $utfBOM = [System.Text.Encoding]::UTF8.GetString($pre) + if($scriptText.startsWith($utfBOM)) + { + # Remove UTF8 BOM bytes + $scriptText = $scriptText.Remove(0, $utfBOM.Length) + } + $bytes = [System.Text.Encoding]::UTF8.GetBytes($scriptText) + $encodedText = [Convert]::ToBase64String($bytes) + + if($script:currentScriptObject.Object.scriptContent -ne $encodedText) + { + # Save script + if(([System.Windows.MessageBox]::Show("Are you sure you want to update the $($global:curObjectType.PolicyFileAttribute) attribute?`n`nObject:`n$($script:currentScriptObject.displayName)", "Update script?", "YesNo", "Warning")) -eq "Yes") + { + Write-Status "Update $($script:currentScriptObject.displayName)" + $obj = $script:currentScriptObject.Object | ConvertTo-Json -Depth 20 | ConvertFrom-Json + $obj.$($global:curObjectType.PolicyFileAttribute) = $encodedText + Start-GraphPreImport $obj $script:currentScriptObject.ObjectType + foreach($prop in $script:currentScriptObject.ObjectType.PropertiesToRemoveForUpdate) + { + Remove-Property $obj $prop + } + Remove-Property $obj "Assignments" + Remove-Property $obj "isAssigned" + + $json = ConvertTo-Json $obj -Depth 15 + + $objectUpdated = (Invoke-GraphRequest -Url "$($script:currentScriptObject.ObjectType.API)/$($script:currentScriptObject.Object.Id)" -Content $json -HttpMethod "PATCH") + if(-not $objectUpdated) + { + Write-Log "Failed to update script" 3 + [System.Windows.MessageBox]::Show("Failed to save the policy. See log for more information","Update failed!", "OK", "Error") + } + Write-Status "" + } + } + + $global:grdModal.Children.Clear() + if($script:currentModal) + { + $global:grdModal.Children.Add($script:currentModal) + } + [System.Windows.Forms.Application]::DoEvents() + }) + + Add-XamlEvent $script:editForm "btnCancelScriptEdit" "add_click" ({ + $global:grdModal.Children.Clear() + if($script:currentModal) + { + $global:grdModal.Children.Add($script:currentModal) + } + [System.Windows.Forms.Application]::DoEvents() + }) + + $global:grdModal.Children.Clear() + $script:editForm.SetValue([System.Windows.Controls.Grid]::RowProperty,1) + $script:editForm.SetValue([System.Windows.Controls.Grid]::ColumnProperty,1) + $global:grdModal.Children.Add($script:editForm) | Out-Null + [System.Windows.Forms.Application]::DoEvents() +} + +#endregion + +#region Terms and Conditions +function Start-PostExportTermsAndConditions +{ + param($obj, $objectType, $path) + + Add-EMAssignmentsToExportFile $obj $objectType $path +} + +function Start-PreImportAssignmentsTermsAndConditions +{ + param($obj, $objectType, $file, $assignments) + + Add-EMAssignmentsToObject $obj $objectType $file $assignments +} +#endregion + +#region App Protection functions + +function Start-GetAppProtection +{ + param($obj, $objectType) + + if(-not $obj."@odata.type") { return } + + Get-GraphMetaData + + $objectClass = $null + if($global:metaDataXML) + { + try + { + $tmp = $obj."@odata.type".Split('.')[-1] + $objectClass = Get-GraphObjectClassName $tmp + } + catch + { + + } + $expand = $null + if($objectClass -eq "windowsInformationProtectionPolicies") + { + $expand = "?`$expand=protectedAppLockerFiles,exemptAppLockerFiles" + } + + if($objectClass) + { + @{"API"="/deviceAppManagement/$objectClass/$($obj.Id)$expand"} + } + } +} + +function Start-PostListAppProtection +{ + param($objList, $objectType) + + # App Configurations for Managed Apps are included in App Protections e.g. the /deviceAppManagement/managedAppPolicies API + # For some reason, the $filter option is not supported to filter out these objects + # e.g. not isof(...) to excluded the type, not startsWith(id, 'A_') to exlude based on Id + # These filters generates a request error so filter them out manually in this function instead + # The portal is probably doing the same thing since these are included in the return but not in the UI + $objList | Where { $_.Object.'@OData.Type' -ne '#microsoft.graph.targetedManagedAppConfiguration' } +} + +function Start-PreImportAppProtection +{ + param($obj, $objectType) + + if(($obj.Apps | measure).Count -gt 0) + { + $global:ImportObjectInfo = @{ Apps=$obj.Apps } + } + else + { + $global:ImportObjectInfo = $null + } + + $global:ImportObjectClass = $null + if($obj."@odata.type") + { + try + { + $global:ImportObjectClass = Get-GraphObjectClassName ($obj."@odata.type".Split('.')[-1]) + } + catch {} + } + + Remove-Property $obj "apps" + Remove-Property $obj "apps@odata.context" + + try + { + $tmp = $obj."@odata.type".Split('.')[-1] + $objectClass = Get-GraphObjectClassName $tmp + if($objectClass) + { + @{"API"="/deviceAppManagement/$objectClass"} + } + } + catch {} +} + +function Start-PostImportAppProtection +{ + param($obj, $objectType, $file) + + if($global:ImportObjectInfo.Apps) + { + # No "@odata.type" on the created object so reload new object + #$newObject = (Invoke-GraphRequest "$($objectType.API)?`$filter=id eq '$($obj.Id)'").Value + $newObject = Invoke-GraphRequest "$($objectType.API)/$($obj.Id)" + if($newObject) + { + try + { + $tmp = $newObject."@odata.type".Split('.')[-1] + $objectClass = Get-GraphObjectClassName $tmp + + $apps = [PSCustomObject]@{ + appGroupType = $obj.appGroupType + apps = @($global:ImportObjectInfo.Apps) + } + $json = $apps | ConvertTo-Json -Depth 20 + + Invoke-GraphRequest -Url "/deviceAppManagement/$objectClass/$($obj.Id)/targetApps" -Content $json -HttpMethod POST | Out-Null + } + catch {} + } + } + $global:ImportObjectInfo = $null +} + +function Start-PreImportAssignmentsAppProtection +{ + param($obj, $objectType, $file, $assignments) + + if($global:ImportObjectClass) + { + @{"API"="/deviceAppManagement/$($global:ImportObjectClass)/$($obj.Id)/assign"} + } +} + +function Start-PreUpdateAppConfigurationApp +{ + param($obj, $objectType, $curObject, $fromObj) + + if($obj.Apps) + { + try + { + Write-Log "Update App Configuruation Apps" + + $apps = [PSCustomObject]@{ + appGroupType = $obj.appGroupType + apps = @($obj.Apps) + } + $json = $apps | ConvertTo-Json -Depth 20 + $objectClass = 'targetedManagedAppConfigurations' + + Invoke-GraphRequest -Url "/deviceAppManagement/$objectClass/$($curObject.Object.Id)/targetApps" -Content $json -HttpMethod POST | Out-Null + } + catch {} + } + + Remove-Property $obj "apps" +} + +function Start-PreUpdateAppProtection +{ + param($obj, $objectType, $curObject, $fromObj) + + if($curObject.Object.'@OData.Type' -eq "#microsoft.graph.windowsInformationProtectionPolicy") + { + $api = "/deviceAppManagement/windowsInformationProtectionPolicies/$($curObject.Object.Id)" + } + elseif($curObject.Object.'@OData.Type' -eq "#microsoft.graph.mdmWindowsInformationProtectionPolicy") + { + $api = "/deviceAppManagement/mdmWindowsInformationProtectionPolicies/$($curObject.Object.Id)" + } + elseif($curObject.Object.'@OData.Type' -eq "#microsoft.graph.iosManagedAppProtection") + { + $api = "/deviceAppManagement/iosManagedAppProtections/$($curObject.Object.Id)" + } + elseif($curObject.Object.'@OData.Type' -eq "#microsoft.graph.androidManagedAppProtection") + { + $api = "/deviceAppManagement/androidManagedAppProtections/$($curObject.Object.Id)" + } + else + { + return (Start-PreUpdateAppConfigurationApp $obj $objectType $curObject $fromObj) + } + + if($obj.Apps) + { + try + { + Write-Log "Update App Protection Apps" + + $apps = [PSCustomObject]@{ + appGroupType = $obj.appGroupType + apps = @($obj.Apps) + } + $json = $apps | ConvertTo-Json -Depth 20 + + Invoke-GraphRequest -Url "$api/targetApps" -Content $json -HttpMethod POST | Out-Null + } + catch {} + + Remove-Property $obj "apps" + } + + @{ "API" = $api } + +} +#endregion + +#region App Configuration +function Start-PostExportAppConfiguration +{ + param($obj, $objectType, $path) + + #Add-EMAssignmentsToExportFile $obj $objectType $path + + Write-Log "Export app config for $($objectType.Id) with OData.Type: $($obj.'@OData.Type')" + + if($obj.'@OData.Type' -eq "#microsoft.graph.androidManagedAppProtection" -or + $obj.'@OData.Type' -eq "#microsoft.graph.androidForWorkMobileAppConfiguration" -or + $obj.'@OData.Type' -eq "#microsoft.graph.androidManagedStoreAppConfiguration" -or + $obj.'@OData.Type' -eq "#microsoft.graph.iosMobileAppConfiguration") + { + $fileName = (Get-GraphObjectName $obj $objectType).Trim('.') + if((Get-SettingValue "AddIDToExportFile") -eq $true -and $obj.Id) + { + $fileName = ($fileName + "_" + $obj.Id) + } + $tmpObj = $null + $fileName = "$path\$((Remove-InvalidFileNameChars $fileName)).json" + if([IO.File]::Exists($fileName)) + { + $tmpObj = Get-GraphObjectFromFile $fileName + } + else + { + Write-Log "File not found: $fileName. Could not add App names." 3 + } + + if(($tmpObj.targetedMobileApps | measure).Count -gt 0) + { + Write-Log "Add target apps info" + $targetedApps = @() + foreach($appId in $tmpObj.targetedMobileApps) + { + $appObj = Invoke-GraphRequest -Url "/deviceAppManagement/mobileApps/$($appId)" #?`select=id,displayName" -ODataMetadata "Minimal" + if($appObj) + { + Write-Log "Add target app info $($appObj.displayName) ($($appObj.Id)) of type $($appObj.'@OData.Type')" + $targetedApps += $appObj.displayName + '|!|' + $appObj.Id + '|!|' + $appObj.'@OData.Type' + } + } + + if($targetedApps.Count -gt 0) + { + Write-Log "Add CustomRefTargetedApps property" + $tmpObj | Add-Member -MemberType NoteProperty -Name "#CustomRefTargetedApps" -Value ($targetedApps -join "|*|") + Write-Log "Save file $fileName" + Save-GraphObjectToFile $tmpObj $fileName + } + } + else + { + Write-Log "No target apps found" 2 + } + } +} + +function Start-PreFilesImportAppConfiguration +{ + param($objectType, $filesToImport) + + $targetedAppsObjects = $filesToImport | Where { $null -ne $_.Object."#CustomRefTargetedApps" } + + if(($targetedAppsObjects | measure).Count -gt 0) + { + Write-Log "Policies with Targeted Apps detected" + foreach($fileObject in $targetedAppsObjects) + { + Add-AppConfigurationTargets $objectType $fileObject + } + } + $filesToImport +} + +function local:Add-AppConfigurationTargets +{ + param($obj, $fileObj) + + if($fileObj.Object."#CustomRefTargetedApps" -and $fileObj.Object.targetedMobileApps) + { + Write-Log "Adding app target for $($fileObj.Object.displayName)" + + $targetedAppsInfo = $fileObj.Object."#CustomRefTargetedApps" + + $translatedTargetedApps = @() + + if($targetedAppsInfo) + { + foreach($targetedApp in ($targetedAppsInfo -split "[|][*][|]")) + { + $appName, $appId, $appType = $targetedApp -split "[|][!][|]" + if(-not $appName -or -not $appId) + { + Write-Log "App Name and Id is missing in string: $targetedApp" 2 + continue + } + $tmpApps = (Invoke-GraphRequest -Url "/deviceAppManagement/mobileApps?`$filter=displayName eq '$appName'").value + if(-not $tmpApps) + { + Write-Log "No application found with name $appName. $appId will not be translated and added to target list" 2 + continue + } + + Write-Log "Found $(($tmpApps | measure).Count) applications" 2 + foreach ($tmpApp in $tmpApps) { + Write-Log "Found '$($tmpApp.displayName)' ($($tmpApp.id)) of type $($($tmpApp.'@OData.Type'))" + } + + $tmpApp = $tmpApps | Where-Object '@OData.Type' -eq $appType + if(-not $tmpApp) + { + Write-Log "No $appName application found of type $appType. $appId will not be translated and added to target list" 2 + } + elseif(($tmpApp | measure).Count -gt 1) { + Write-Log "$(($tmpApp | measure).Count) applications found with name '$appName' of type $appType. $appId will not be translated and added to target list" 2 + } + else { + Write-Log "Found '$appName' with id $($tmpApp.Id) ($appType)" + $translatedTargetedApps += $tmpApp.Id + } + } + + if($translatedTargetedApps.Count -gt 0) { + Write-Log "Updating translated targeted apps" + $fileObj.Object.targetedMobileApps = $translatedTargetedApps + } + else { + Write-Log "Could not find targeted apps in the evnironment. Verify that they are added. Policy import might fail" 3 + } + } + } +} + +function Start-PreImportAssignmentsAppConfiguration +{ + param($obj, $objectType, $file, $assignments) + + @{"API"="/deviceAppManagement/mobileAppConfigurations/$($obj.Id)/microsoft.graph.managedDeviceMobileAppConfiguration/assign"} +} +#endregon + +#region Applications + +function Start-PostCopyApplication +{ + param($objCopyFrom, $objNew, $objectType) + + Start-ImportApp $objNew + Start-AddInstallScripts $objNew $objCopyFrom + Write-Status "" +} + +function Start-PostFileImportApplication +{ + param($obj, $objectType, $file) + + $tmpObj = Get-GraphObjectFromFile $file + + if(-not ($obj.PSObject.Properties | Where Name -eq '@odata.type')) + { + # Add @odata.type property if it is missing. Required by app package import + $obj | Add-Member -MemberType NoteProperty -Name '@odata.type' -Value $objectType.'@odata.type' + } + + $fi = [IO.FileInfo]$file + $tmpFilName = $fi.DirectoryName + "\" + $obj.FileName + + if([IO.File]::Exists($tmpFilName) -eq $false) + { + $tmpFilName = $null + } + + Start-ImportApp $obj $tmpFilName + Start-AddInstallScripts $obj $tmpObj +} + +function local:Start-ImportApp +{ + param($obj, $packageFile = $null) + + if(-not $obj.'@odata.type') { return } + + if($null -eq $packageFile) + { + $pkgPath = Get-SettingValue "EMIntuneAppPackages" + + if(-not $pkgPath -or [IO.Directory]::Exists($pkgPath) -eq $false) + { + Write-LogDebug "Package source directory is either missing or does not exist" 2 + return + } + + $packageFile = "$($pkgPath)\$($obj.fileName)" + } + $fi = [IO.FileInfo]$packageFile + + if($fi.Exists -eq $false) + { + Write-LogDebug "Package source file $($fi.FullName) not found" 2 + return + } + + Write-Status "Import appliction package file $($fi.FullName)" + Write-Log "Import application file '$($($fi.FullName))' for $($obj.displayName)" + + $appType = $obj.'@odata.type'.Trim('#') + + if($appType -eq "microsoft.graph.win32LobApp") + { + $fileEncryptionInfo = Copy-Win32LOBPackage $packageFile $obj + } + elseif($appType -eq "microsoft.graph.windowsMobileMSI") + { + $fileEncryptionInfo = Copy-MSILOB $packageFile $obj + } + elseif($appType -eq "microsoft.graph.windowsUniversalAppX") + { + $fileEncryptionInfo = Copy-MSIXLOB $packageFile $obj + } + elseif($appType -eq "microsoft.graph.iosLOBApp") + { + $fileEncryptionInfo = Copy-iOSLOB $packageFile $obj + } + elseif($appType -eq "microsoft.graph.androidLOBApp") + { + $fileEncryptionInfo = Copy-AndroidLOB $packageFile $obj + } + else + { + Write-Log "Unsupported application type $appType. File will not be uploaded" 2 + } + + if((Get-SettingValue "EMSaveEncryptionFile") -eq $true) + { + if($fileEncryptionInfo) + { + $jsonEncryptionInfo = $fileEncryptionInfo | ConvertTo-Json -Depth 10 + + $pkgPath = Get-SettingValue "EMIntuneAppDownloadFolder" (Get-SettingValue "EMIntuneAppPackages") + if($pkgPath -and [IO.Directory]::Exists($pkgPath)) + { + $obj = Invoke-GraphRequest -Url "/deviceAppManagement/mobileApps/$($obj.id)" -ODataMetadata "Minimal" + $fullPath = $pkgPath + "\$($obj.displayName)_$($obj.id)_$($obj.committedContentVersion).json" + $jsonEncryptionInfo | Out-File -FilePath $fullPath -Force -Encoding utf8 + } + } + } +} + +function local:Start-AddInstallScripts +{ + param($obj, $fromAppObj) + + if($fromAppObj -and ($fromAppObj.activeInstallScript."#ScriptInfo" -or $fromAppObj.activeUninstallScript."#ScriptInfo")) + { + Write-Log "Importing scripts for $($obj.displayName)" + + $scriptsAdded = $false + $jsonData = @{} + $jsonData."@odata.type" = "#microsoft.graph.win32LobApp" + $jsonData."committedContentVersion" = "1" + + foreach ($scriptType in @('activeInstallScript','activeUninstallScript')) { + $scriptInfo = $fromAppObj.$scriptType.'#ScriptInfo' + if (-not $scriptInfo) { continue } + + Write-Log "Add $($scriptType -replace '^active','') script: $($scriptInfo.displayName)" + + $json = [ordered]@{ + '@odata.type' = $scriptInfo.'@odata.type' + displayName = $scriptInfo.displayName + enforceSignatureCheck = $scriptInfo.enforceSignatureCheck + runAs32Bit = $scriptInfo.runAs32Bit + content = $scriptInfo.content + } | ConvertTo-Json -Depth 10 -Compress + + $scriptObject = Invoke-GraphRequest -Url "deviceAppManagement/mobileApps/$($obj.id)/microsoft.graph.win32LobApp/contentVersions/1/scripts" -Method POST -Content $json + + if ($scriptObject) { + $jsonData.$scriptType = @{ targetId = $scriptObject.Id } + $scriptsAdded = $true + } + } + + $i = 0 + while($true) + { + $scripts = Invoke-GraphRequest -Url "deviceAppManagement/mobileApps/$($obj.id)/microsoft.graph.win32LobApp/contentVersions/1/scripts" + if(-not $scripts) + { + Write-Log "Failed to retrieve scripts for app after adding. Skipping Install/Uninstall script config." 2 + return + } + + if(($scripts.value.state | Select -Unique) -eq "commitSuccess") + { + Write-Log "Scripts added successfully" + break + } + if($i -ge 12) + { + Write-Log "Install/Uninstall scripts are still not in pending state after waiting for 1 minute." 3 + return + } + + Write-Log "Waiting for scripts to be added..." + Start-Sleep -Seconds 5 + $i++ + } + + if($scriptsAdded) + { + Write-Log "Add script info to app" + $json = ConvertTo-Json $jsonData -Depth 10 + $status = Invoke-GraphRequest -Url "deviceAppManagement/mobileApps/$($obj.id)" -Method PATCH -Body $json + if($status -eq $true) + { + Write-Log "Install/Uninstall script info updated successfully" + } + else + { + Write-Log "Failed to update Install/Uninstall script info" 2 + } + } + } +} + +function Start-PreUpdateApplication +{ + param($obj, $objectType, $curObject, $fromObj) + + if($curObject.Object.'@OData.type' -eq "#microsoft.graph.windowsMobileMSI") + { + Remove-Property $obj "useDeviceContext" + } + elseif($curObject.Object.'@OData.type' -eq "#microsoft.graph.officeSuiteApp") + { + Remove-Property $obj "officeConfigurationXml" + Remove-Property $obj "officePlatformArchitecture" + Remove-Property $obj "developer" + Remove-Property $obj "owner" + Remove-Property $obj "publisher" + } + + Remove-Property $obj "appStoreUrl" +} + +function Start-PreImportCommandApplication +{ + param($obj, $objectType, $file, $assignments) + + if($obj.'@OData.Type' -in @('#microsoft.graph.microsoftStoreForBusinessApp','#microsoft.graph.androidStoreApp')) + { + Write-Log "App type '$($obj.'@OData.Type')' not supported for import" 2 + @{ "Import" = $false } + } + + if($obj.'@OData.Type' -eq '#microsoft.graph.officeSuiteApp') + { + if($obj.officeSuiteAppDefaultFileFormat -eq "notConfigured") + { + $obj.officeSuiteAppDefaultFileFormat = "officeOpenXMLFormat" + } + } + + if($obj.activeInstallScript) { $obj.activeInstallScript = $null } + if($obj.activeUninstallScript) { $obj.activeUninstallScript = $null } +} + +function Add-DetailExtensionApplications +{ + param($form, $buttonPanel, $index = 0) + + $btnUpload = New-Object System.Windows.Controls.Button + $btnUpload.Content = 'Upload' + $btnUpload.Name = 'btnUploadAppfile' + $btnUpload.Margin = "0,0,5,0" + $btnUpload.Width = "100" + + $btnUpload.Add_Click({ + if($global:dgObjects.SelectedItem.Object.publishingState -ne "notPublished") + { + # Only allow upload of not published apps + # Use portal to replace app file... + if(([System.Windows.MessageBox]::Show("Are you sure you want to upload a new file for the app?`n`nApplication:`n$($global:dgObjects.SelectedItem.Object.displayName)", "Update app file?", "YesNo", "Warning")) -ne "Yes") + { + return + } + } + + $pkgPath = Get-SettingValue "EMIntuneAppPackages" + + $of = [System.Windows.Forms.OpenFileDialog]::new() + $of.FileName = $global:dgObjects.SelectedItem.Object.fileName + $of.DefaultExt = "*.intunewin" + $of.Filter = "Intune Win32 (*.intunewin)|*.*" + $of.Multiselect = $false + + if($pkgPath -and [IO.Directory]::Exists($pkgPath)) + { + $of.InitialDirectory = $pkgPath + } + + if($of.ShowDialog() -eq "OK") + { + Write-Status "Import $($global:dgObjects.SelectedItem.Object.displayName) file" + Start-ImportApp $global:dgObjects.SelectedItem.Object $of.FileName + Write-Status "" + } + }) + + $tmp = $form.FindName($buttonPanel) + if($tmp) + { + $tmp.Children.Insert($index, $btnUpload) + } + + $btnDownload = New-Object System.Windows.Controls.Button + $btnDownload.Content = 'Download' + $btnDownload.Name = 'btnDownloadAppfile' + $btnDownload.Margin = "0,0,5,0" + $btnDownload.Width = "100" + + $btnDownload.Add_Click({ + Write-Status "Download file" + $obj = $global:dgObjects.SelectedItem.Object + #$obj = Invoke-GraphRequest -Url "/deviceAppManagement/mobileApps/$($obj.id)" + + $pkgPath = Get-SettingValue "EMIntuneAppDownloadFolder" (Get-SettingValue "EMIntuneAppPackages") + + $dlgSave = [System.Windows.Forms.SaveFileDialog]::new() + $dlgSave.InitialDirectory = $pkgPath + $dlgSave.FileName = ($obj.FileName + ".encrypted") + $dlgSave.DefaultExt = "*.encrypted" + $dlgSave.Filter = "Encrypted intunewin (*.encrypted)|*.encrypted|All files (*.*)|*.*" + + if($dlgSave.ShowDialog() -eq [System.Windows.Forms.DialogResult]::OK -and $dlgSave.Filename) + { + $contentFileObj = Start-DownloadAppContent $obj $dlgSave.FileName + + if([IO.File]::Exists($dlgSave.FileName)) + { + $fullPath = Find-AppEncryptionFile $obj $contentFileObj $pkgPath + if([IO.File]::Exists($fullPath) -eq $false) + { + if(([System.Windows.MessageBox]::Show("Could not find decryption file for $($obj.displayName)`nApp Id: $($obj.id)`nContent version $($obj.committedContentVersion)`n`nDo you want to browse for the file?", "Encryption file not found", "YesNo", "Warning")) -eq "Yes") + { + $of = [System.Windows.Forms.OpenFileDialog]::new() + $of.InitialDirectory = $pkgPath + $of.DefaultExt = "*.json" + $of.Filter = "Json (*.json)|*.json" + $of.Multiselect = $false + + if($of.ShowDialog() -eq "OK") + { + $fullPath = $of.FileName + } + } + } + + if([IO.File]::Exists($fullPath)) + { + Write-Status "Decrypting file" + $encryptionInfo = ConvertFrom-Json (Get-Content -Path $fullPath -Raw) + if($encryptionInfo.fileEncryptionInfo) + { + $encryptionInfo = $encryptionInfo.fileEncryptionInfo + } + $destination = $pkgPath + "\$($obj.FileName)" + Start-DecryptFile $dlgSave.Filename $destination $encryptionInfo.encryptionKey $encryptionInfo.initializationVector + try { [IO.File]::Delete($dlgSave.Filename) } + catch { + Write-LogError "Failed to delete exported encrypted file" $_.Exception + } + } + else + { + Write-Log "Decryption file for $($obj.displayName) not found. Skipping decryption" 2 + } + } + } + + Write-Status "" + }) + + $tmp = $form.FindName($buttonPanel) + if($tmp) + { + $tmp.Children.Insert($index, $btnDownload) + } +} + +function Find-AppEncryptionFile +{ + param($obj, $contentFileObj, $rootFolders) + + $search = @() + $search += "$($obj.displayName)_$($obj.id)_$($obj.committedContentVersion)" + $search += "$([IO.Path]::GetFileNameWithoutExtension($obj.fileName))_$($contentFileObj.size)" + $search += "$($obj.displayName)_$($contentFileObj.size)" + + foreach($rootFolder in $rootFolders) + { + foreach($searchName in $search) + { + $fullName = ($rootFolder + "\$($searchName).json") + if([IO.File]::Exists($fullName)) + { + return $fullName + } + } + } +} + +function Start-PreImportAssignmentsApplications +{ + param($obj, $objectType, $file, $assignments) + + if($obj.'@odata.type' -eq "#microsoft.graph.windowsMicrosoftEdgeApp") + { + foreach($assignment in $assignments) + { + Remove-Property $assignment.target "deviceAndAppManagementAssignmentFilterId" + Remove-Property $assignment.target "deviceAndAppManagementAssignmentFilterType" + } + @{"Assignments"=$assignments} + } + elseif($obj.'@odata.type' -eq "#microsoft.graph.winGetApp") + { + Write-LogDebug "Wait for app to be published" + $i = 2 + Start-Sleep -s ($i) + $x = 0 + while($x -lt 10) + { + ###!!! + $appInfo = Invoke-GraphRequest -Url "$($objectType.API)/$($obj.id)" -ODataMetadata "skip" + if($appInfo.publishingState -eq "Published") + { + Write-LogDebug "Application $($obj.displayName) is published" + return + } + Start-Sleep -s ($i) + $x++ + if($x -ge 5) { $i++ } + } + + Write-Log "Application '$($obj.displayName)' is not published. Skipping assignment" 2 + @{"Import"=$false} + } +} + +function Start-PreDeleteApplications +{ + param($obj, $objectType) + + if($obj.'@odata.type' -eq "#microsoft.graph.microsoftStoreForBusinessApp") + { + # Don't delete Microsoft Store for Business Apps + @{ "Delete" = $false } + } +} + +function Start-PostExportApplications +{ + param($obj, $objectType, $path) + + if($global:chkExportScript.IsChecked) + { + $fileName = Get-GraphObjectFile $obj $objectType + $fi = [IO.FileInfo]"$path\$fileName" + + try + { + foreach($rule in ($obj.detectionRules | Where '@OData.Type' -eq "#microsoft.graph.win32LobAppPowerShellScriptDetection")) + { + if($rule.ScriptContent) + { + [IO.File]::WriteAllBytes(("$path\$($fi.BaseName)_DetectionScript.ps1"), ([System.Convert]::FromBase64String($rule.ScriptContent))) + + } + } + + foreach($rule in $obj.requirementRules) + { + if($rule.'@OData.Type' -eq "#microsoft.graph.win32LobAppPowerShellScriptRequirement") + { + if($rule.ScriptContent) + { + [IO.File]::WriteAllBytes(("$path\$($fi.BaseName)_RequirementScript.ps1"), ([System.Convert]::FromBase64String($rule.ScriptContent))) + } + } + } + + if($obj.activeInstallScript.'#ScriptInfo'.displayName) + { + [IO.File]::WriteAllBytes(("$path\$($fi.BaseName)_$($obj.activeInstallScript.'#ScriptInfo'.displayName)"), ([System.Convert]::FromBase64String($obj.activeInstallScript.'#ScriptInfo'.content))) + } + + if($obj.activeUninstallScript.'#ScriptInfo'.displayName) + { + [IO.File]::WriteAllBytes(("$path\$($fi.BaseName)_$($obj.activeUninstallScript.'#ScriptInfo'.displayName)"), ([System.Convert]::FromBase64String($obj.activeUninstallScript.'#ScriptInfo'.content))) + } + } + catch + { + Write-LogError "Failed to export scripts" $_.Exception + } + } + + Save-Setting "Intune" "ExportAppFile" $global:chkExportApplicationFile.IsChecked + if($global:chkExportApplicationFile.IsChecked) + { + $encryptionSource = Get-SettingValue "EMIntuneAppDownloadFolder" (Get-SettingValue "EMIntuneAppPackages") + $pkgPath = $path + + if($pkgPath) + { + Write-Status "Download file" + + $exportFile = $pkgPath + "\$($obj.FileName).encrypted" + $contentFileObj = Start-DownloadAppContent $obj $exportFile -GetContentFileInfoOnly + $encryptionFile = Find-AppEncryptionFile $obj $contentFileObj $encryptionSource + if($encryptionFile -and [IO.File]::Exists($encryptionFile)) + { + Start-DownloadFile $contentFileObj.azureStorageUri $exportFile + + if([IO.File]::Exists($exportFile)) + { + Write-Status "Decrypting file" + $encryptionInfo = ConvertFrom-Json (Get-Content -Path $encryptionFile -Raw) + if($encryptionInfo.fileEncryptionInfo) + { + $encryptionInfo = $encryptionInfo.fileEncryptionInfo + } + $destination = $pkgPath + "\$($obj.FileName)" + Start-DecryptFile $exportFile $destination $encryptionInfo.encryptionKey $encryptionInfo.initializationVector + } + + try { [IO.File]::Delete($exportFile) } + catch { + Write-LogError "Failed to delete exported encrypted file" $_.Exception + } + } + else + { + Write-Log "Could not find encryption file" + } + } + } +} + +function Start-PostListApplications +{ + param($objList, $objectType) + + foreach($obj in ($objList | Where { $_.Object."@OData.Type" -eq "#microsoft.graph.winGetApp"})) + { + if($obj.Object.packageIdentifier -like "9*") + { + $installerType = "UWP" + } + elseif($obj.Object.packageIdentifier -like "X*") + { + $installerType = "Win32" + } + else + { + $objName = Get-GraphObjectName $obj.Object $objectType + Write-Log "Unknown package identifier for app $($objName): $($obj.Object.packageIdentifier)" 2 + $installerType = "Unknown" + } + $obj.Object | Add-Member -MemberType NoteProperty -Name "InstallerType" -Value $installerType + } + $objList +} + +function Add-ScriptExportApplications +{ + param($form, $buttonPanel, $index = 0) + + Add-ScriptExportExtensions $form $buttonPanel $index + + $ctrl = $form.FindName("chkExportApplicationFile") + if(-not $ctrl) + { + $xaml = @" + + +"@ + $label = [Windows.Markup.XamlReader]::Parse($xaml) + + $global:chkExportApplicationFile = [System.Windows.Controls.CheckBox]::new() + $global:chkExportApplicationFile.IsChecked = ((Get-Setting "Intune" "ExportAppFile" "false") -eq "true") + $global:chkExportApplicationFile.VerticalAlignment = "Center" + $global:chkExportApplicationFile.Name = "chkExportApplicationFile" + + @($label, $global:chkExportApplicationFile) + } +} + +function Start-PostGetApplications { + param($obj, $objectType) + + if(($obj.Object.dependentAppCount -as [int]) -gt 0 -or ($obj.Object.supersededAppCount -as [int]) -gt 0) { + $relationships = (Invoke-GraphRequest -Url "/deviceAppManagement/mobileApps/$($obj.Id)/relationships?`$filter=targetType%20eq%20microsoft.graph.mobileAppRelationshipType%27child%27").value + $dependencyApps = @() + $supersededApps = @() + foreach ($rel in $relationships) { + if ($rel."@odata.type" -eq "#microsoft.graph.mobileAppDependency") { + $dependencyApps += "$($rel.targetDisplayName)|!|$($rel.targetDisplayVersion)|!|$($rel.targetId)|!|$($rel.dependencyType)" + } + elseif ($rel."@odata.type" -eq "#microsoft.graph.mobileAppSupersedence") { + $supersededApps += "$($rel.targetDisplayName)|!|$($rel.targetDisplayVersion)|!|$($rel.targetId)|!|$($rel.supersedenceType)" + } + } + if ($dependencyApps.Count -gt 0) { + $obj.Object | Add-Member -MemberType NoteProperty -Name "#CustomRefDependency" -Value ($dependencyApps -join "|*|") + } + + if ($supersededApps.Count -gt 0) { + $obj.Object | Add-Member -MemberType NoteProperty -Name "#CustomRefSupersedence" -Value ($supersededApps -join "|*|") + } + } + + if($obj.Object.'@odata.type' -eq "#microsoft.graph.win32LobApp") + { + if($obj.Object.activeInstallScript.targetId -or $obj.Object.activeUninstallScript.targetId) + { + $scriptInfo = (Invoke-GraphRequest -Url "/deviceAppManagement/mobileApps/$($obj.Id)/microsoft.graph.win32LobApp/contentVersions/$($obj.Object.committedContentVersion)/scripts/").value + + foreach($script in $scriptInfo) { + $scriptFullInfo = (Invoke-GraphRequest -Url "/deviceAppManagement/mobileApps/$($obj.Id)/microsoft.graph.win32LobApp/contentVersions/$($obj.Object.committedContentVersion)/scripts/$($script.id)?`$select=Id,Content") + if($scriptFullInfo.Content) + { + $script.content = $scriptFullInfo.Content + } + + if($obj.Object.activeInstallScript.targetId -eq $script.id) + { + $tpObject = $obj.Object.activeInstallScript + } + elseif($obj.Object.activeUninstallScript.targetId -eq $script.id) + { + $tpObject = $obj.Object.activeUninstallScript + } + else + { + Write-Log "Script with id $($script.id) is not referenced by active install or uninstall script. Skipping." 2 + continue + } + $tpObject | Add-Member -MemberType NoteProperty -Name "#ScriptInfo" -Value $script -Force + } + } + } +} + +function Start-PostImportApplications +{ + param($obj, $objectType, $file) + + #$tmpObj = Get-GraphObjectFromFile $file +} + +function Start-PostFilesImportApplications +{ + param($objType, $importedObjects, $importedFiles) + + $refObjects = $importedFiles | Where { $null -ne $_.Object."#CustomRefDependency" -or $null -ne $_.Object."#CustomRefSupersedence" } + + if(($refObjects | measure).Count -gt 0) + { + Write-Log "Applicetions with Dependency or Supersedence detected" + foreach($file in $refObjects) + { + Add-ApplicationReferences $file.ImportedObject $file.Object + } + } +} + +function local:Add-ApplicationReferences +{ + param($obj, $fileObj) + + if($fileObj."#CustomRefDependency" -or $fileObj."#CustomRefSupersedence") + { + Write-Log "Adding app references for $($obj.displayName)" + + $depAppsInfo = $fileObj."#CustomRefDependency" + $supAppsInfo = $fileObj."#CustomRefSupersedence" + + $releationShips = [PSCustomObject]@{ + relationships = @() + } + + if($depAppsInfo) + { + foreach($depApp in ($depAppsInfo -split "[|][*][|]")) + { + $appName, $appVer, $appId, $appType = $depApp -split "[|][!][|]" + if(-not $appName -or -not $appVer) + { + Write-Log "Could not get Name and Version from string: $appApp" 2 + continue + } + $tmpApps = (Invoke-GraphRequest -Url "/deviceAppManagement/mobileApps?`$filter=displayName eq '$appName'").value + if(-not $tmpApps) + { + Write-Log "No application found with name $appName" 2 + continue + } + $tmpApp = $tmpApps | Where displayVersion -eq $appVer + if(-not $tmpApp) + { + Write-Log "No $appName application found with version $appVer" 2 + continue + } + elseif(($tmpApp | measure).Count -gt 1) + { + Write-Log "Multiple $appName applications found with version $appVer" 2 + continue + } + Write-Log "Add $appName ($appVer) to Dependency list" + $releationShips.relationships += [PSCustomObject]@{ + "@odata.type" = "#microsoft.graph.mobileAppDependency" + targetId = $tmpApp.Id + dependencyType = $appType + } + } + } + + if($supAppsInfo) + { + foreach($suppApp in ($supAppsInfo -split "[|][*][|]")) + { + $appName, $appVer, $appId, $appType = $suppApp -split "[|][!][|]" + if(-not $appName -or -not $appVer) + { + Write-Log "Could not get Name and Version from string: $suppApp" 2 + continue + } + $tmpApps = (Invoke-GraphRequest -Url "/deviceAppManagement/mobileApps?`$filter=displayName eq '$appName'").value + if(-not $tmpApps) + { + Write-Log "No application found with name $appName" 2 + continue + } + $tmpApp = $tmpApps | Where displayVersion -eq $appVer + if(-not $tmpApp) + { + Write-Log "No $appName application found with version $appVer" 2 + continue + } + elseif(-not ($tmpApp | measure).Count -gt 1) + { + Write-Log "Multiple $appName application found with version $appVer" 2 + continue + } + Write-Log "Add $appName ($appVer) to Supersedence list" + $releationShips.relationships += [PSCustomObject]@{ + "@odata.type" = "#microsoft.graph.mobileAppSupersedence" + targetId = $tmpApp.Id + supersedenceType = $appType + } + } + } + + if($releationShips.relationships.Count -gt 0) + { + $json = Update-JsonForEnvironment (ConvertTo-Json $releationShips -Depth 20) + + Write-Log "Update app references" + Invoke-GraphRequest -Url "/deviceAppManagement/mobileApps/$($obj.Id)/updateRelationships" -Method "POST" -Body $json + } + } +} + +#endregion + +#region Group Policy/Administrative Templates functions +function Get-GPOObjectSettings +{ + param($GPOObj) + + $gpoSettings = @() + + if ($GPOObj.policyConfigurationIngestionType -eq "unknown") { + $tmpObj = (Invoke-GraphRequest -Url "/deviceManagement/groupPolicyConfigurations?`$filter=id eq '$($GPOObj.id)'").value[0] + if ($tmpObj.policyConfigurationIngestionType) { + $GPOObj.policyConfigurationIngestionType = $tmpObj.policyConfigurationIngestionType + } + } + + # Get all configured policies in the Administrative Templates profile + $GPODefinitionValues = Invoke-GraphRequest -Url "/deviceManagement/groupPolicyConfigurations/$($GPOObj.id)/definitionValues?`$expand=definition" -ODataMetadata "skip" + foreach($definitionValue in $GPODefinitionValues.value) + { + # Get presentation values for the current settings (with presentation object included) + $presentationValues = Invoke-GraphRequest -Url "/deviceManagement/groupPolicyConfigurations/$($GPOObj.id)/definitionValues/$($definitionValue.id)/presentationValues?`$expand=presentation" -ODataMetadata "skip" + + # Set base policy settings + $obj = @{ + "enabled" = $definitionValue.enabled + "definition@odata.bind" = "$($global:graphURL)/deviceManagement/groupPolicyDefinitions('$($definitionValue.definition.id)')" + } + + if($definitionValue.definition.categoryPath) + { + $obj.Add("#Definition_Id", $definitionValue.definition.id) + $obj.Add("#Definition_displayName", $definitionValue.definition.displayName) + $obj.Add("#Definition_classType", $definitionValue.definition.classType) + $obj.Add("#Definition_categoryPath", $definitionValue.definition.categoryPath) + } + + if($presentationValues.value) + { + # Policy presentation values set e.g. a drop down list, check box, text box etc. + $obj.presentationValues = @() + + foreach ($presentationValue in $presentationValues.value) + { + # Add presentation@odata.bind property that links the value to the presentation object + $presentationValue | Add-Member -MemberType NoteProperty -Name "presentation@odata.bind" -Value "$($global:graphURL)/deviceManagement/groupPolicyDefinitions('$($definitionValue.definition.id)')/presentations('$($presentationValue.presentation.id)')" + + if($definitionValue.definition.categoryPath) + { + $presentationValue | Add-Member -MemberType NoteProperty -Name "#Presentation_Id" -Value $presentationValue.presentation.id + $presentationValue | Add-Member -MemberType NoteProperty -Name "#Presentation_Label" -Value $presentationValue.presentation.label + } + #Remove presentation object so it is not included in the export + Remove-ObjectProperty $presentationValue "presentation" + + #Optional removes. Import will igonre them + Remove-ObjectProperty $presentationValue "id" + Remove-ObjectProperty $presentationValue "lastModifiedDateTime" + Remove-ObjectProperty $presentationValue "createdDateTime" + + # Add presentation value to the list + $obj.presentationValues += $presentationValue + } + } + $gpoSettings += $obj + } + $gpoSettings +} + +function Import-GPOSetting +{ + param($obj, $settings) + + if($obj) + { + Write-Status "Import settings for $($obj.displayName)" + + $hasCustomADMX = $null -ne ($settings | Where { $null -ne $_.'#Definition_categoryPath' }) + + if($hasCustomADMX) + { + Write-Status "Import custom ADMX settings" + if(-not $script:CustomADMXDefinitions) + { + $tmpCustomCategories = Invoke-GraphRequest -Url "deviceManagement/groupPolicyCategories?`$expand=definitions(`$select=id, displayName, categoryPath, classType)&`$select=id, displayName&`$filter=ingestionSource eq 'custom'" -ODataMetadata "Minimal" + if($tmpCustomCategories.Value) + { + $script:CustomADMXDefinitions = @{} + foreach($tmpCat in $tmpCustomCategories.Value) + { + foreach($tmpDef in $tmpCat.definitions) + { + $key = ($tmpDef.displayName + $tmpDef.categoryPath + $tmpDef.classType).ToLower() + $val = [PSCustomObject]@{ + Definition = $tmpDef + Category = $tmpCat + Presentations = $null + } + try { + $script:CustomADMXDefinitions.Add($key, $val) + } + catch { + Write-Log "Failed to add '$($tmpDef.displayName)' in category '$($tmpDef.categoryPath)' of class $($tmpDef.classType)" 3 + } + } + } + } + } + } + + foreach($setting in $settings) + { + if($setting.'#Definition_categoryPath' -and $script:CustomADMXDefinitions -is [HashTable] -and $script:CustomADMXDefinitions.Count -gt 0) + { + $defVal = $null + $key = ($setting.'#Definition_displayName' + $setting.'#Definition_categoryPath' + $setting.'#Definition_classType').ToLower() + if($key -and $script:CustomADMXDefinitions.ContainsKey($key)) + { + $defVal = $script:CustomADMXDefinitions[$key] + } + elseif($key) + { + Write-Log "No custom ADMX definitiona found for setting $($setting.'#Definition_displayName')" 2 + } + else + { + Write-Log "Setting $($setting.'#Definition_displayName') does not have information to be imported in the environment" + } + + if($defVal) + { + $setting.'definition@odata.bind' = $setting.'definition@odata.bind' -replace $setting.'#Definition_Id', $defVal.Definition.Id + if(($setting.presentationValues | measure).Count -gt 0) + { + if(-not $defVal.Presentations) + { + $tmpPresentation = Invoke-GraphRequest -Url "deviceManagement/groupPolicyDefinitions/$($defVal.Definition.Id)/presentations" -ODataMetadata "Minimal" + if($tmpPresentation.value) + { + foreach($settingPresentation in $setting.presentationValues) + { + $tmpPresentationVal = $tmpPresentation.value | Where label -eq $settingPresentation.'#Presentation_Label' + if($tmpPresentationVal) + { + $settingPresentation.'presentation@odata.bind' = $settingPresentation.'presentation@odata.bind' -replace $setting.'#Definition_Id', $defVal.Definition.Id + $settingPresentation.'presentation@odata.bind' = $settingPresentation.'presentation@odata.bind' -replace $settingPresentation.'#Presentation_Id', $tmpPresentationVal.Id + } + else + { + Write-Log "Could not find a presentation value with label $($settingPresentation.'#Presentation_Label'). Setting will not be configured" 2 + continue + } + } + } + else + { + Write-Log "Could not find presentation for setting $($settingPresentation.'#Presentation_Label'). Setting will not be configured." 2 + continue + } + } + } + } + else + { + Write-Log "Settings might not be available if imported in another environment" 3 + } + } + elseif($setting.'#Definition_categoryPath') + { + Write-Log "Custom AMDX settings cannot be imported without ADMX file imported. Definitions not found" 2 + continue + } + + Start-GraphPreImport $setting + + if($true) + { + foreach($tmpProp in (($setting.PSObject.Properties | Where Name -like "#*").Name)) + { + Remove-Property $setting $tmpProp + } + + foreach($settingPresentation in $setting.presentationValues) + { + foreach($tmpProp in (($settingPresentation.PSObject.Properties | Where Name -like "#*").Name)) + { + Remove-Property $settingPresentation $tmpProp + } + } + } + + # Import each setting for the Administrative Template profile + Invoke-GraphRequest -Url "/deviceManagement/groupPolicyConfigurations/$($obj.id)/definitionValues" -Content (ConvertTo-Json $setting -Depth 20) -HttpMethod POST | Out-Null + } + } +} + +function Start-PostExportAdministrativeTemplate +{ + param($obj, $objectType, $path) + + $fileName = (Get-GraphObjectName $obj $objectType).Trim('.') + if((Get-SettingValue "AddIDToExportFile") -eq $true -and $obj.Id) + { + $fileName = ($fileName + "_" + $obj.Id) + } + + if($obj.definitionValues) + { + $settings = $obj.definitionValues + } + else + { + $settings = Get-GPOObjectSettings $obj + } + + $fileName = "$path\$((Remove-InvalidFileNameChars $fileName))_Settings.json" + Save-GraphObjectToFile $settings $fileName +} + +function Start-PostCopyAdministrativeTemplate +{ + param($objCopyFrom, $objNew, $objectType) + + $settings = Get-GPOObjectSettings $objCopyFrom + if($settings) + { + Import-GPOSetting $objNew $settings + } +} + +function Start-PostFileImportAdministrativeTemplate +{ + param($obj, $objectType, $file) + + $settings = Get-EMSettingsObject $obj $objectType $file -settingsProperty "definitionValues" -SettingsArray + if($settings) + { + $tmpObj = Get-GraphObjectFromFile $file + + Import-GPOSetting $obj $settings + } +} + +function Start-LoadAdministrativeTemplate +{ + param($fileName) + + if(-not $fileName) { return $null } + + $fi = [IO.FileInfo]$fileName + if($fi.Exists -eq $false) { return } + + $obj = Get-GraphObjectFromFile $fi.FullName + + if($obj.definitionValues) + { + return $obj + } + + $settingsFile = $fi.DirectoryName + "\" + $fi.BaseName + "_Settings.json" + + if([IO.File]::Exists($settingsFile)) + { + $definitionValues = Get-GraphObjectFromFile $settingsFile + + $obj | Add-Member Noteproperty -Name "definitionValues" -Value $definitionValues -Force + } + $obj +} + +function Start-PostGetAdministrativeTemplate +{ + param($obj, $objectType) + + $definitionValues = Get-GPOObjectSettings $obj.Object + if($definitionValues) + { + $obj.Object | Add-Member Noteproperty -Name "definitionValues" -Value $definitionValues -Force + } + <# + # Leave for now. This only loads the configured definition values and not the values specified. + # That would require enumerating each definition value which takes time. + $definitionValues = (Invoke-GraphRequest "deviceManagement/groupPolicyConfigurations('$($obj.Id)')/definitionValues?`$expand=definition(`$select=id,classType,displayName,policyType,groupPolicyCategoryId)" -ODataMetadata "minimal").value + + if($definitionValues) + { + $obj.Object | Add-Member Noteproperty -Name "definitionValues" -Value $definitionValues -Force + } + #> +} + +function Start-PreImportAdministrativeTemplate +{ + param($obj, $objectType, $file, $assignments) + + +} + +#endregion + +#region Policy Sets function + +function Start-PreImportAssignmentsPolicySets +{ + param($obj, $objectType, $file, $assignments) + + @{"API"="$($objectType.API)/$($obj.Id)/Update"} +} + +function Start-PreImportPolicySets +{ + param($obj, $objectType) + + @("items@odata.context","status","errorCode") | foreach { Remove-Property $obj $_ } + + # Properties to keep for items + $keepProperties = @("@odata.type","payloadId","intent","settings") + foreach($item in $obj.Items) + { + foreach($prop in ($item.PSObject.Properties | Where {$_.Name -notin $keepProperties})) + { + Remove-Property $item $prop.Name + } + #@("itemType","displayName","status","errorCode") | foreach { Remove-Property $item $_ } + } +} + +function Start-PreUpdatePolicySets +{ + param($obj, $objectType, $curObject, $fromObj) + + Start-PreImportPolicySets $obj $objectType + + $curObject = Get-GraphObject $curObject.Object $objectType + + # Update ref object in the json + # Used when importing in a different environment + $jsonObj = ConvertTo-Json $obj -Depth 15 + $updateObj = Update-JsonForEnvironment $jsonObj | ConvertFrom-Json + + $addedItems = @() + $updatedItems = @() + $deletedItems = @() + + foreach($item in $updateObj.items) + { + if(($curObject.Object.items | Where payloadId -eq $item.payloadId)) + { + $updatedItems += $item + } + else + { + $addedItems += $item + } + } + + foreach($item in $curObject.Object.items) + { + if(-not ($updateObj.Items | Where payloadId -eq $item.payloadId)) + { + $deletedItems += $item.id + } + } + + $updateItemObj = [PSCustomObject]@{ + addedPolicySetItems = $addedItems + deletedPolicySetItems = $deletedItems + updatedPolicySetItems = $updatedItems + } + + Write-Log "Update Policy Set items. Add: $($addedItems.Count), Update: $($updatedItems.Count), Delete: $($deletedItems.Count)" + + $updateApi = "/deviceAppManagement/policySets/$($curObject.Object.Id)/update" + $json = $updateItemObj | ConvertTo-Json -Depth 15 + + Invoke-GraphRequest -Url $updateApi -HttpMethod "POST" -Content $json + Remove-Property $obj "items" +} + +function Update-EMPolicySetAssignment +{ + param($assignment, $sourceObject, $newObject, $objectType) + + $api = "/deviceAppManagement/policySets/$($assignment.SourceId)?`$expand=assignments,items" + + $psObj = Invoke-GraphRequest -Url $api -ODataMetadata "Minimal" + + if(-not $psObj) + { + return + } + + $curItem = $psObj.Items | Where payloadId -eq $sourceObject.Id + + if(-not $curItem) + { + return + } + + $api = "/deviceAppManagement/policySets/$($assignment.SourceId)/update" + + $curItemClone = $curItem | ConvertTo-Json -Depth 20 | ConvertFrom-Json + $newItem = $curItem | ConvertTo-Json -Depth 20 | ConvertFrom-Json + $newItem.payloadId = $newObject.Id + if($newItem.guidedDeploymentTags -is [String] -and [String]::IsNullOrEmpty($newItem.guidedDeploymentTags)) + { + $newItem.guidedDeploymentTags = @() + } + + $keepProperties = @('@odata.type','payloadId','Settings','guidedDeploymentTags') + #itemType? e.g. #microsoft.graph.iosManagedAppProtection + #priority? + + foreach($prop in ($newItem.PSObject.Properties | Where {$_.Name -notin $keepProperties})) + { + Remove-Property $newItem $prop.Name + } + + $update = @{} + $update.Add('addedPolicySetItems',@($newItem)) + $update.Add('updatedPolicySetItems', @()) + $update.Add('deletedPolicySetItems',@($curItemClone.Id)) + + $json = $update | ConvertTo-Json -Depth 20 + + Write-Log "Update PolicySet $($psObj.displayName) - Replace: $((Get-GraphObjectName $newObject $objectType))" + + Invoke-GraphRequest -Url $api -HttpMethod "POST" -Content $json +} + +function Start-PostListPolicySets +{ + param($objList, $objectType) + + foreach($obj in $objList) + { + $obj | Add-Member -MemberType NoteProperty -Name "IsAssigned" -Value ($obj.Object.status -ne "notAssigned") + } + $objList +} +#endregion + +#endregion Locations +function Start-PreImportLocations +{ + param($obj, $objectType) + + if($obj.uniqueName) + { + $arr = $obj.uniqueName.Split('_') + if($arr.Length -ge 3) + { + # Locations requires a unique name so generate a new guid and change the uniqueName property + $obj.uniqueName = ($obj.uniqueName.Substring(0,$obj.uniqueName.Length-$arr[-1].Length) + [Guid]::NewGuid().Tostring("n")) + } + } +} +#endregion + +#region RoleDefinitions +function Start-PostExportRoleDefinitions +{ + param($obj, $objectType, $path) + + $fileName = (Get-GraphObjectName $obj $objectType).Trim('.') + if((Get-SettingValue "AddIDToExportFile") -eq $true -and $obj.Id) + { + $fileName = ($fileName + "_" + $obj.Id) + } + $tmpObj = $null + $fileName = "$path\$((Remove-InvalidFileNameChars $fileName)).json" + if([IO.File]::Exists($fileName)) + { + $tmpObj = Get-GraphObjectFromFile $fileName + } + else + { + Write-Log "File not found: $fileName. Could not get role assignments" 3 + } + + if(($tmpObj.RoleAssignments | measure).Count -gt 0) + { + $roleAssignmentsArr = @() + foreach($roleAssignment in $tmpObj.RoleAssignments) + { + $raObj = Invoke-GraphRequest -Url "/deviceManagement/roleAssignments/$($roleAssignment.Id)?`$expand=microsoft.graph.deviceAndAppManagementRoleAssignment/roleScopeTags" -ODataMetadata "Minimal" + if($raObj) + { + foreach($groupId in $raObj.resourceScopes) { Add-GroupMigrationObject $groupId } + foreach($groupId in $raObj.members) { Add-GroupMigrationObject $groupId } + $roleAssignmentsArr += $raObj + } + } + + if($roleAssignmentsArr.Count -gt 0) + { + $tmpObj.RoleAssignments = $roleAssignmentsArr + Save-GraphObjectToFile $tmpObj $fileName + } + } +} + +function Start-PreImportRoleDefinitions +{ + param($obj, $objectType) + + Remove-Property $obj "RoleAssignments" + Remove-Property $obj "RoleAssignments@odata.context" +} + +function Start-PostFileImportRoleDefinitions +{ + param($obj, $objectType, $file) + + $tmpObj = Get-GraphObjectFromFile $file + + $loadedScopeTags = $global:LoadedDependencyObjects["ScopeTags"] + if(($tmpObj.RoleAssignments | measure).Count -gt 0 -and ($loadedScopeTags | measure).Count -gt 0) + { + # Documentation way did not work so use the same way as the portal + # Should be created with /deviceManagement/roleDefinitions/{roleDefinitionId}/roleAssignments + foreach($roleAssignment in $tmpObj.RoleAssignments) + { + $roleAssignmentObj = New-object PSObject @{ + "description" = $roleAssignment.Description + "displayName"= $roleAssignment.DisplayName + "members" = $roleAssignment.members + "resourceScopes" = $roleAssignment.resourceScopes + "roleDefinition@odata.bind" = "https://graph.microsoft.com/beta/deviceManagement/roleDefinitions('$($obj.Id)')" + "roleScopeTags@odata.bind" = @() + } + + foreach($scopeTag in $roleAssignment.roleScopeTags) + { + $scopeMigObj = $loadedScopeTags | Where OriginalId -eq $scopeTag.Id + if(-not $scopeMigObj.Id) { continue } + $roleAssignmentObj."roleScopeTags@odata.bind" += "https://graph.microsoft.com/beta/deviceManagement/roleScopeTags('$($scopeMigObj.Id)')" + } + + # This will update GroupIds + $json = Update-JsonForEnvironment (ConvertTo-Json $roleAssignmentObj -Depth 20) + + Write-Log "Import Role Assignments" + Invoke-GraphRequest -Url "/deviceManagement/roleAssignments" -Body $json -Method "POST" + } + } +} +#endregion + +#region SettingsCatalog + +function Start-PreImportSettingsCatalog +{ + param($obj, $objectType) + + $returnHT = @{} + $updated = $false + + if($obj.templateReference.templateId) { + # I do not like this at all and it is a lazy but simple implementation... + # It turns out that settingInstanceTemplateId and settingValueTemplateId are case sensitive + # and there is ONE setting with a different casing in the Windows Baseline template. + # The export saves it with lowercase which causes the import to fail. + + Write-Log "Get template $($obj.templateReference.templateId)" + $templateObj = Invoke-GraphRequest -Url "/deviceManagement/configurationPolicyTemplates('$($obj.templateReference.templateId)')" + if($templateObj.lifecycleState -and $templateObj.lifecycleState -ne "active") { + Write-Log "Template '$($templateObj.displayName)' '$($templateObj.displayVersion)' is in '$($templateObj.lifecycleState)' state. Current state: $($templateObj.lifecycleState). Import might fail." 2 + } + #Todo: Should probably check for the latest active version and use that instead of the one in the templateReference + + if(-not $script:baseLineTemplate) { + $script:baseLineTemplate = @{} + } + if($script:baseLineTemplate.ContainsKey($obj.templateReference.templateId)) { + $templateReference = $script:baseLineTemplate[$obj.templateReference.templateId] + } + else { + Write-Log "Get template settings for '$($templateObj.displayName)' '$($templateObj.displayVersion)' ($($obj.templateReference.templateId))" + $templateReference = Invoke-GraphRequest -Url "/deviceManagement/configurationPolicyTemplates('$($obj.templateReference.templateId)')/settingTemplates?`$expand=settingDefinitions&top=1000" + $script:baseLineTemplate.Add($obj.templateReference.templateId, $templateReference) + } + + if($templateReference) { + $newObjJson = $obj | ConvertTo-Json -Depth 50 + $templateIDs = Get-GUIDs ($templateReference | ConvertTo-Json -Depth 50) + $objectIDs = Get-GUIDs ($obj.Settings | ConvertTo-Json -Depth 50) + $diff = Compare-Object $templateIDs $objectIDs -CaseSensitive + foreach($diffItem in ($diff | where SideIndicator -eq "=>")) { + $templateID = $templateIDs | Where { $_ -eq $diffItem.InputObject } + if($templateID) { + # Found but with different casing + $newObjJson = $newObjJson -replace $diffItem.InputObject, $templateID + $updated = $true + } + } + if($updated) { + $returnHT.Add("JSON", $newObjJson) + } + } + } + return $returnHT +} + +function Invoke-CheckSettingsCatalogIds +{ + param($obj, $templateReference) + + foreach($settingTemplate in $obj.value) { + if($settingTemplate.settingDefinitions) { + foreach($settingDefinition in $settingTemplate.settingDefinitions) { + if($settingDefinition.id -and $settingDefinition.id -ne $obj.Id) { + Write-Log "Setting definition ID $($settingDefinition.id) does not match the settings catalog ID $($obj.Id)" 2 + } + } + } + } +} + +function Start-PostExportSettingsCatalog +{ + param($obj, $objectType, $path) + + Add-EMAssignmentsToExportFile $obj $objectType $path +} + +function Start-PreUpdateSettingsCatalog +{ + param($obj, $objectType, $curObject, $fromObj) + + @{"Method"="PUT"} +} + +function Start-PostGetSettingsCatalog +{ + param($obj, $objectType) + + if(-not $obj.Object.Assignments) + { + $url = "$($objectType.API)/$($obj.id)/assignments" + $assignments = (Invoke-GraphRequest -Url $url).Value + if($assignments) + { + $obj.Object.Assignments = $assignments + } + } +} + +#endregion + +#region Notification functions +function Start-PreImportNotifications +{ + param($obj, $objectType) + + Remove-Property $obj "defaultLocale" + Remove-Property $obj "localizedNotificationMessages" + Remove-Property $obj "localizedNotificationMessages@odata.context" +} + +function Start-PostFileImportNotifications +{ + param($obj, $objectType, $file) + + $tmpObj = Get-GraphObjectFromFile $file + + foreach($localizedNotificationMessage in $tmpObj.localizedNotificationMessages) + { + Start-GraphPreImport $localizedNotificationMessage $objectType + Invoke-GraphRequest -Url "$($objectType.API)/$($obj.id)/localizedNotificationMessages" -Body ($localizedNotificationMessage | ConvertTo-Json -Depth 20) -Method "POST" + } +} + +function Start-PostCopyNotifications +{ + param($objCopyFrom, $objNew, $objectType) + + foreach($localizedNotificationMessage in $objCopyFrom.localizedNotificationMessages) + { + Start-GraphPreImport $localizedNotificationMessage $objectType + Invoke-GraphRequest -Url "$($objectType.API)/$($objNew.id)/localizedNotificationMessages" -Body ($localizedNotificationMessage | ConvertTo-Json -Depth 20) -Method "POST" + } +} +#endregion + +#region Enrollment Status Page functions +function Start-PreImportESP +{ + param($obj, $objectType) + + if($obj.Priority -eq 0) + { + $ret = @{} + $ret.Add("API","$($objectType.API)/$($obj.Id)") + $ret.Add("Method","PATCH") # Default profile always exists so update them + $ret + } + else + { + Remove-Property $obj "Id" + } +} + +function Start-PostExportESP +{ + param($obj, $objectType, $path) + + if($obj.Priority -eq 0) + { + Save-EMDefaultPolicy $obj $objectType $path + } +} + +function Start-PostListESP +{ + param($objList, $objectType) + + # endswith not working so filter them out + $objList | Where { $_.Object.'@OData.Type' -eq '#microsoft.graph.windows10EnrollmentCompletionPageConfiguration' } +} +#endregion + +#region Enrollment Restriction functions + +function Start-PostExportEnrollmentRestrictions +{ + param($obj, $objectType, $path) + + if($obj.Priority -eq 0) + { + Save-EMDefaultPolicy $obj $objectType $path + } +} + +function Start-PreImportEnrollmentRestrictions +{ + param($obj, $objectType) + + if($obj.Priority -eq 0) + { + $ret = @{} + $ret.Add("API","$($objectType.API)/$($obj.Id)") + $ret.Add("Method","PATCH") # Default profile always exists so update them + $ret + } + else + { + Remove-Property $obj "Id" + } + + if($obj.windowsMobileRestriction) + { + # Windows Phone operations are no longer supported + Remove-Property $obj "windowsMobileRestriction" + } +} + +function Start-PreDeleteEnrollmentRestrictions +{ + param($obj, $objectType) + + if($obj.Priority -eq 0) + { + @{ "Delete" = $false } + } +} + +function Start-PreReplaceEnrollmentRestrictions +{ + param($obj, $objectType, $sourceObj, $fromFile) + + if($sourceObj.Priority -eq 0) { @{ "Replace" = $false } } +} + +function Start-PostReplaceEnrollmentRestrictions +{ + param($obj, $objectType, $sourceObj, $fromFile) + + if($sourceObj.Priority -eq 0) { return } + + $api = "/deviceManagement/deviceEnrollmentConfigurations/$($obj.id)/setpriority" + + $priority = [PSCustomObject]@{ + priority = $sourceObj.Priority + } + $json = $priority | ConvertTo-Json -Depth 20 + + Write-Log "Update priority for $($obj.displayName) to $($sourceObj.Priority)" + Invoke-GraphRequest $api -HttpMethod "POST" -Content $json +} + +function Start-PreFilesImportEnrollmentRestrictions +{ + param($objectType, $filesToImport) + + $filesToImport | sort-object -property @{e={$_.Object.priority}} +} + +function Start-PreUpdateEnrollmentRestrictions +{ + param($obj, $objectType, $curObject, $fromObj) + + Remove-Property $obj "priority" +} + +function Start-PostListEnrollmentRestrictions +{ + param($objList, $objectType) + + # endswith not working so filter them out + $objList | Where { + ($_.Object.'@OData.Type' -eq '#microsoft.graph.deviceEnrollmentPlatformRestrictionConfiguration' -or + $_.Object.'@OData.Type' -eq '#microsoft.graph.deviceEnrollmentLimitConfiguration' -or + $_.Object.'@OData.Type' -eq '#microsoft.graph.deviceEnrollmentPlatformRestrictionsConfiguration') -and + $_.Object.id -notlike "*_PlatformRestrictions" -and $_.Object.platformType -ne "WindowsPhone" -and $_.Object.platformType -ne "AndroidAosp" + } +} + +function Start-PreImportAssignmentsEnrollmentRestrictions +{ + param($obj, $objectType, $file, $assignments) + + if($obj.Priority -eq 0) + { + # Skip Assignment for Default Policy + @{ "Import" = $false } + } +} + +#endregion + +#region +function Start-PostListCoManagementSettings +{ + param($objList, $objectType) + + # endswith not working so filter them out + $objList | Where { $_.Object.'@OData.Type' -eq '#microsoft.graph.deviceComanagementAuthorityConfiguration' } +} +#endregion + +#region ScopeTags +function Start-PostExportScopeTags +{ + param($obj, $objectType, $path) + + Add-EMAssignmentsToExportFile $obj $objectType $path +} + +function Start-PostGetScopeTags +{ + param($obj, $objectType) + + $strAPI = "$($objectType.API)/$($obj.Object.Id)/assignments" + $tmpObj = Invoke-GraphRequest -Url $strAPI + + if(($tmpObj.value | measure).count -gt 0) + { + $obj.Object.assignments = $tmpObj.value + } +} +#endregion + +#region AutoPilot +function Start-PreImportAssignmentsAutoPilot +{ + param($obj, $objectType, $file, $assignments) + + Add-EMAssignmentsToObject $obj $objectType $file $assignments +} + +function Start-PreDeleteAutoPilot +{ + param($obj, $objectType) + + Write-Log "Delete AutoPilot profile assignments" + + if(-not $obj.Assignments) + { + $tmpObj = (Get-GraphObject $obj $objectType).Object + } + else + { + $tmpObj = $obj + } + + foreach($assignment in $tmpObj.Assignments) + { + if($assignment.Source -ne "direct") { continue } + + $api = "/deviceManagement/windowsAutopilotDeploymentProfiles/$($obj.Id)/assignments/$($assignment.Id)" + + Invoke-GraphRequest $api -HttpMethod "DELETE" + } +} + +#endregion + +#region Health Scripts + +function Start-PreDeleteDeviceHealthScripts +{ + param($obj, $objectType) + + if($obj.isGlobalScript -eq $true) + { + @{ "Delete" = $false } + } +} + +function Start-PreImportDeviceHealthScripts +{ + param($obj, $objectType, $file, $assignments) + + if($obj.isGlobalScript -eq $true) + { + @{ "Import" = $false } + } +} + +function Start-PreUpdateDeviceHealthScripts +{ + param($obj, $objectType, $curObject, $fromObj) + + if($curObject.Object.isGlobalScript -eq $true) + { + @{ "Import" = $false } + } +} + +function Start-PostExportDeviceHealthScripts +{ + param($obj, $objectType, $path) + + if($global:chkExportScript.IsChecked) + { + $fileName = Get-GraphObjectFile $obj $objectType + $fi = [IO.FileInfo]"$path\$fileName" + + try + { + if($obj.detectionScriptContent) + { + [IO.File]::WriteAllBytes(("$path\$($fi.BaseName)_DetectionScript.ps1"), ([System.Convert]::FromBase64String($obj.detectionScriptContent))) + } + + if($obj.remediationScriptContent) + { + [IO.File]::WriteAllBytes(("$path\$($fi.BaseName)_RemediationScript.ps1"), ([System.Convert]::FromBase64String($obj.remediationScriptContent))) + } + } + catch + { + Write-LogError "Failed to export scripts" $_.Exception + } + } +} + +#endregion + +#region Generic functions + +function Save-EMDefaultPolicy +{ + param($obj, $objectType, $path) + + if($obj.Priority -eq 0) + { + try + { + $fileName = $obj.Id.Split('_')[1] + + if($fileName) + { + $oldFile = "$path\$((Get-GraphObjectName $obj $objectType)).json" + if([IO.File]::Exists($oldFile)) + { + # Clean up from old version of the script that used the wrong name for Default policies + try { [IO.File]::Delete($oldFile) | Out-Null } Catch {} + } + Save-GraphObjectToFile $obj "$path\$((Remove-InvalidFileNameChars $fileName)).json" + } + } + catch {} + } +} +function Get-EMSettingsObject +{ + param($obj, $objectType, $file, $settingsProperty = "settings", [switch]$SettingsArray) + + if($obj.$settingsProperty) { return $obj.$settingsProperty } + + $fi = [IO.FileInfo]$file + if($fi.Exists) + { + # Settings property removed during import so lets try exported file first + $tmpObj = Get-GraphObjectFromFile $fi.FullName + if($SettingsArray -eq $true) + { + # Only the an array of settings is expected + return $tmpObj.$settingsProperty + } + else + { + if($tmpObj.$settingsProperty) + { + # A property with the an array of settings is expected + return ([PSCustomObject]@{ + $settingsProperty = $tmpObj.$settingsProperty + }) + } + } + + Write-Log "Settings not included in export file. Try import from _Settings.json file" 2 + $settingsFile = $fi.DirectoryName + "\" + $fi.BaseName + "_Settings.json" + $fiSettings = [IO.FileInfo]$settingsFile + if($fiSettings.Exists -eq $false) + { + Write-Log "Settings file '$($fiSettings.FullName)' was not found" 2 + return + } + Get-GraphObjectFromFile $fiSettings.FullName + } + else + { + Write-Log "Settings not included in export file and _Settings.json file is missing." 3 + } +} + +function Add-EMAssignmentsToExportFile +{ + param($obj, $objectType, $path, $Url = "") + + if($global:chkExportAssignments.IsChecked -ne $true) { return } + + $fileName = (Get-GraphObjectName $obj $objectType).Trim('.') + if((Get-SettingValue "AddIDToExportFile") -eq $true -and $obj.Id) + { + $fileName = ($fileName + "_" + $obj.Id) + } + $fileName = "$path\$((Remove-InvalidFileNameChars $fileName)).json" + if([IO.File]::Exists($fileName) -eq $false) + { + Write-Log "File not found: $fileName. Could not add assignments to file" 3 + return + } + + $tmpObj = Get-GraphObjectFromFile $fileName + + if(-not $url) + { + $url = "$($objectType.API)/$($obj.id)/assignments" + } + $assignments = (Invoke-GraphRequest -Url $url -ODataMetadata "Minimal").Value + if($assignments) + { + if(-not ($tmpObj.PSObject.Properties | Where Name -eq "assignments")) + { + $tmpObj | Add-Member -MemberType NoteProperty -Name "assignments" -Value $assignments + } + else + { + $tmpObj.Assignments = $assignments + } + Save-GraphObjectToFile $tmpObj $fileName + } +} + +function Add-EMAssignmentsToObject +{ + param($obj, $objectType, $file, $assignments) + + # AutoPilot and TaC are using assignments and not assign like other object types + $api = "$($objectType.API)/$($obj.Id)/assignments" + + # These profiles don't support importing of multiple assignments with { "assignment" [...]} + # Each assignment must be imported separately + + foreach($assignment in $assignments) + { + if($assignment.Source -and $assignment.Source -ne "direct") { continue } + + foreach($prop in $assignment.PSObject.Properties) + { + if($prop.Name -in @("Target")) { continue } + Remove-Property $assignment $prop.Name + } + + foreach($prop in $assignment.target.PSObject.Properties) + { + if($prop.Name -in @("@odata.type","groupId")) { continue } + Remove-Property $assignment.target $prop.Name + } + + $json = Update-JsonForEnvironment ($assignment | ConvertTo-Json -Depth 20) + Invoke-GraphRequest -Url $api -Body $json -Method "POST" | Out-Null + } + @{"Import"=$false} +} + +#endregion + +#region Mac Custom Scripts + +function Start-PreUpdateMacCustomAttributes +{ + param($obj, $objectType, $curObject, $fromObj) + + foreach($prop in @('customAttributeName','customAttributeType','displayName')) + { + Remove-Property $obj $prop + } +} + +#endregion + +#region Mac Feature Updates +function Start-PreUpdateFeatureUpdates +{ + param($obj, $objectType, $curObject, $fromObj) + + foreach($prop in @('deployableContentDisplayName','endOfSupportDate')) + { + Remove-Property $obj $prop + } +} +#endregion + +#region Conditional Access +function Add-ConditionalAccessImportExtensions +{ + param($form, $buttonPanel, $index = 0) + + $xaml = @" + + +"@ + $label = [Windows.Markup.XamlReader]::Parse($xaml) + + $CAStates = @() + $CAStates += [PSCustomObject]@{ + Name = "As Exported - Change On to Report-only" + Value = "AsExportedReportOnly" + } + + $CAStates += [PSCustomObject]@{ + Name = "As Exported" + Value = "AsExported" + } + + $CAStates += [PSCustomObject]@{ + Name = "Report-only" + Value = "enabledForReportingButNotEnforced" + } + + $CAStates += [PSCustomObject]@{ + Name = "On" + Value = "enabled" + } + + $CAStates += [PSCustomObject]@{ + Name = "Off" + Value = "disabled" + } + + $defaultCAState = Get-SettingValue "ConditionalAccessState" + + $global:cbImportCAState = [System.Windows.Controls.ComboBox]::new() + $global:cbImportCAState.DisplayMemberPath = "Name" + $global:cbImportCAState.SelectedValuePath = "Value" + $global:cbImportCAState.ItemsSource = $CAStates + $global:cbImportCAState.SelectedValue = $defaultCAState + $global:cbImportCAState.Margin="0,5,0,0" + $global:cbImportCAState.HorizontalAlignment="Left" + $global:cbImportCAState.Width=250 + $global:cbImportCAState.Name = "cbImportCAState" + + @($label, $global:cbImportCAState) +} + +function Start-PreImportConditionalAccess +{ + param($obj, $objectType, $file, $assignments) + + if ($global:cbImportCAState.SelectedValue -and $global:cbImportCAState.SelectedValue -ne "AsExported") { + if ($global:cbImportCAState.SelectedValue -eq "AsExportedReportOnly" -and $obj.state -eq "enabled") { + Write-Log "Change Enabled policy to Report-only" + $obj.state = "enabledForReportingButNotEnforced" + } + else { + $obj.state = $global:cbImportCAState.SelectedValue + } + } + + if($obj.grantControls.authenticationStrength) + { + $obj.grantControls.operator = "AND" + $tmpObj = Get-GraphObjectFromFile $file + + $authSetting = [PSCustomObject]@{ + id = $tmpObj.grantControls.authenticationStrength.id + } + $obj.grantControls.authenticationStrength = $authSetting + } + + if($obj.sessionControls.disableResilienceDefaults -eq $false) + { + $obj.sessionControls.disableResilienceDefaults = $null + } + + # DeviceStates property is depricated + if(($obj.conditions.PSObject.Properties | Where Name -eq "DeviceStates")) + { + $obj.conditions.PSObject.Properties.Remove('DeviceStates') + } +} + +function Start-PostExportConditionalAccess +{ + param($obj, $objectType, $path) + + $ids = @() + foreach($id in ($obj.conditions.users.includeGroups + $obj.conditions.users.excludeGroups)) + { + if($id -in $ids) { continue } + elseif($id -eq "GuestsOrExternalUsers") { continue } + elseif($id -eq "All") { continue } + elseif($id -eq "None") { continue } + + $ids += $id + Add-GraphMigrationObject $id "/groups" "Group" + } + + foreach($id in ($obj.conditions.users.includeUsers +$obj.conditions.users.excludeUsers)) + { + if($id -in $ids) { continue } + elseif($id -eq "GuestsOrExternalUsers") { continue } + elseif($id -eq "All") { continue } + elseif($id -eq "None") { continue } + + $ids += $id + Add-GraphMigrationObject $id "/users" "User" + } + + <# + $roleIds = @() + foreach($id in ($obj.conditions.users.includeRoles + $obj.conditions.users.excludeRoles)) + { + if($id -in $ids) { continue } + $roleIds += $id + } + #> +} +#endregion + +#region Terms of use +function Start-PreImportTermsOfUse +{ + param($obj, $objectType, $file, $assignments) + + $pkgPath = Get-SettingValue "EMIntuneAppPackages" + + if(-not $pkgPath -or [IO.Directory]::Exists($pkgPath) -eq $false) + { + Write-Log "Intune app directory is either missing or does not exist" 2 + } + + try + { + $fi = [IO.FileInfo]$file + } catch {} + + foreach($file in $obj.Files) + { + $pdfFile = $null + + if($fi.Directory.FullName) + { + $pdfFile = "$($fi.Directory.FullName)\$($file.fileName)" + } + + if($null -eq $pdfFile -or [IO.File]::Exists($pdfFile) -eq $false) + { + $pdfFile = "$($pkgPath)\$($file.fileName)" + } + + if([IO.File]::Exists($pdfFile) -eq $false) + { + Write-Log "Terms of use file $($file.fileName) not found. The Terms of Use object will not be imported." 2 + @{"Import" = $false} + return + } + + Write-Log "Add file data: $pdfFile" + + $bytes = [IO.File]::ReadAllBytes($pdfFile) + $file.fileData = [PSCustomObject]@{ + data = [Convert]::ToBase64String($bytes) + } + } +} + +function Start-PostExportTermsOfUse +{ + param($obj, $objectType, $path) + + foreach($file in $obj.Files) + { + $url = "agreements/$($obj.id)/file/localizations('$($file.id)')/fileData/data" + $data = (Invoke-GraphRequest -Url $url -ODataMetadata "Minimal").Value + if($data) + { + Write-Log "Save file $($file.FileName)" + $fileName = "$path\$($file.FileName)" + [IO.File]::WriteAllBytes($fileName, [System.Convert]::FromBase64String($data)) + } + } +} + +#endregion + +#region ADMXFiles + +function Start-PreFilesImportADMXFiles +{ + param($objectType, $filesToImport) + + $filesToImport | sort-object -property @{e={$_.Object.lastModifiedDateTime}} +} + +function Start-PreImportADMXFiles +{ + param($obj, $objectType, $file, $assignments) + + $pkgPath = Get-SettingValue "EMIntuneAppPackages" + + if(-not $pkgPath -or [IO.Directory]::Exists($pkgPath) -eq $false) + { + Write-Log "Intune app directory is either missing or does not exist" 2 + $pkgPath = $null + } + + try + { + $fi = [IO.FileInfo]$file + } catch {} + + $admxFile = $null + + if($fi.Directory.FullName) + { + $admxFile = "$($fi.Directory.FullName)\$($obj.fileName)" + $admlFile = "$($fi.Directory.FullName)\$([io.path]::GetFileNameWithoutExtension($obj.fileName)).adml" + } + + if($null -ne $pkgPath -and ($null -eq $admxFile -or [IO.File]::Exists($admxFile) -eq $false -or [IO.File]::Exists($admxFile) -eq $false)) + { + Write-Log "$($obj.fileName) not foud in Export folder. Look in package path: $pkgPath" + $admxFile = "$($pkgPath)\$($obj.fileName)" + $admlFile = "$($pkgPath)\$([io.path]::GetFileNameWithoutExtension($obj.fileName)).adml" + } + + if([IO.File]::Exists($admxFile) -eq $false) + { + Write-Log "ADMX (or ADML) file $($obj.fileName) not found. The ADMXFile object will not be imported." 2 + @{"Import" = $false} + return + } + + #$bytes = [IO.File]::ReadAllBytes($admxFile) + $bytes = Get-ASCIIBytes ([IO.File]::ReadAllText($admxFile)) + $obj.content = [Convert]::ToBase64String($bytes) + + #$bytes = [IO.File]::ReadAllBytes($admlFile) + $bytes = Get-ASCIIBytes ([IO.File]::ReadAllText($admlFile)) + + $obj.groupPolicyUploadedLanguageFiles += [PSCustomObject]@{ + fileName = [io.path]::GetFileName($admlFile) + content = [Convert]::ToBase64String($bytes) + languageCode = (?? $obj.defaultLanguageCode "en-US") + } + $obj.defaultLanguageCode = "" +} + +function Start-PostImportADMXFiles +{ + param($obj, $objectType, $file) + + $script:CustomADMXDefinitions = $null +} + +function Start-PreDeleteADMXFiles +{ + param($obj, $objectType) + + Write-Status "Delete $($obj.fileName)" + $strAPI = ($objectType.API + "/$($obj.Id)/remove") + Write-Log "Delete $($objectType.Title) object $($obj.fileName)" + Invoke-GraphRequest -Url $strAPI -HttpMethod "POST" -ODataMetadata "none" | Out-Null + + @{ "Delete" = $false } +} + +#endregion + +#region Reusable Groups +function Start-PostGetReusableSettings +{ + param($obj, $objectType) + + $strAPI = "$($objectType.API)/$($obj.Object.Id)?`$select=settinginstance,displayname,description" + $tmpObj = Invoke-GraphRequest -Url $strAPI + + if($tmpObj.settingInstance) + { + $obj.Object | Add-Member Noteproperty -Name "settingInstance" -Value $tmpObj.settingInstance -Force + } +} + +#endregon + +#region Authentication Strength +function Start-PreImportCommandAuthenticationStrengths +{ + param($obj, $objectType, $file, $assignments) + + if($obj.policyType -ne "custom") + { + Write-Log "Built-in Authentication Strength objects cannot be imported" 2 + @{ "Import" = $false } + } +} +#endregion + +#region Authentication Strength +function Start-PreImportCommandAuthenticationContext +{ + param($obj, $objectType, $file, $assignments) + + #@{ "Method" = "PATCH" } + +} +#endregion + + Export-ModuleMember -alias * -function * \ No newline at end of file