diff --git a/.gitignore b/.gitignore index 09487cd..9348176 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,5 @@ /CloudAPIPowerShellManagement.Log /CloudAPIPowerShellManagement.Lo_ /Documentation/Get-LanguageStrings.ps1 +/*.csv diff --git a/Core.psm1 b/Core.psm1 index 47cb84d..5a1d3a9 100644 --- a/Core.psm1 +++ b/Core.psm1 @@ -12,7 +12,7 @@ This module handles the WPF UI function Get-ModuleVersion { - '3.0.1' + '3.1.2' } function Start-CoreApp @@ -363,6 +363,24 @@ function Get-XamlObject } } +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 Dialogs @@ -429,6 +447,65 @@ function Show-AboutDialog 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 + } + + #Get-Module | Where Name -eq "Core" + $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() } + } + + <# + $latest = Invoke-RestMethod "https://api.github.com/repos/Micke-K/IntuneManagement/releases/latest" + if($latest) + { + + } + #> + + $content = Invoke-RestMethod "https://api.github.com/repos/Micke-K/IntuneManagement/contents/ReleaseNotes.md" + if($content) + { + $txt = [System.Text.Encoding]::ASCII.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 Show-InputDialog { param( @@ -664,6 +741,9 @@ function Get-Folder function Remove-Property { param($obj, $prop) + + if(-not $prop) { return } + if(($obj | GM -MemberType NoteProperty -Name $prop)) { Write-LogDebug "Remove property $prop" @@ -692,6 +772,28 @@ function Get-GridCheckboxColumn $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) + + $fileName +} + #endregion #region Reg functions @@ -708,7 +810,7 @@ function Save-Setting $regPath = Get-RegPath $SubPath if((Test-Path $regPath) -eq $false) { - New-Item (Get-RegPath $SubPath) -ErrorAction SilentlyContinue + New-Item (Get-RegPath $SubPath) -Force -ErrorAction SilentlyContinue | Out-Null } New-ItemProperty -Path $regPath -Name $Key -Value $Value -Type $Type -Force | Out-Null } @@ -1201,6 +1303,8 @@ function Show-View & $global:currentViewObject.ViewInfo.Deactivating } + $global:currentViewObject = $viewObject + $viewItems = ?: ($viewObject.ViewInfo.Sort -ne $false) ($viewObject.ViewItems | Sort-Object -Property Title) ($viewObject.ViewItems) $lblMenuTitle.Content = $viewObject.ViewInfo.Title @@ -1227,8 +1331,6 @@ function Show-View $grdViewPanel.Children.Add($viewObject.ViewInfo.ViewPanel) | Out-Null } - $global:currentViewObject = $viewObject - Set-MainTitle Show-AuthenticationInfo @@ -1286,6 +1388,7 @@ function Get-MainWindow # ToDo: Convert to a list for data binding Add-XamlEvent $window "mnuSettings" "Add_Click" -scriptBlock ([scriptblock]{ Show-SettingsForm }) + 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") diff --git a/Documentation/Strings-cs.json b/Documentation/Strings-cs.json index 99e93d6..2a50d9c 100644 Binary files a/Documentation/Strings-cs.json and b/Documentation/Strings-cs.json differ diff --git a/Documentation/Strings-de.json b/Documentation/Strings-de.json index 7dfe60d..37cdcef 100644 Binary files a/Documentation/Strings-de.json and b/Documentation/Strings-de.json differ diff --git a/Documentation/Strings-en.json b/Documentation/Strings-en.json index 7ce0ae3..9efa1b4 100644 Binary files a/Documentation/Strings-en.json and b/Documentation/Strings-en.json differ diff --git a/Documentation/Strings-es.json b/Documentation/Strings-es.json index 70281f1..e7e2700 100644 Binary files a/Documentation/Strings-es.json and b/Documentation/Strings-es.json differ diff --git a/Documentation/Strings-fr.json b/Documentation/Strings-fr.json index 4e7a078..14e0239 100644 Binary files a/Documentation/Strings-fr.json and b/Documentation/Strings-fr.json differ diff --git a/Documentation/Strings-hu.json b/Documentation/Strings-hu.json index 2fb6004..6fe057d 100644 Binary files a/Documentation/Strings-hu.json and b/Documentation/Strings-hu.json differ diff --git a/Documentation/Strings-it.json b/Documentation/Strings-it.json index ec00304..e6c6c26 100644 Binary files a/Documentation/Strings-it.json and b/Documentation/Strings-it.json differ diff --git a/Documentation/Strings-ja.json b/Documentation/Strings-ja.json index 51cce29..2ec37b3 100644 Binary files a/Documentation/Strings-ja.json and b/Documentation/Strings-ja.json differ diff --git a/Documentation/Strings-ko.json b/Documentation/Strings-ko.json index d9f86a6..682dfc2 100644 Binary files a/Documentation/Strings-ko.json and b/Documentation/Strings-ko.json differ diff --git a/Documentation/Strings-nl.json b/Documentation/Strings-nl.json index 0b17c45..6254fcf 100644 Binary files a/Documentation/Strings-nl.json and b/Documentation/Strings-nl.json differ diff --git a/Documentation/Strings-pl.json b/Documentation/Strings-pl.json index 10a553c..6c80135 100644 Binary files a/Documentation/Strings-pl.json and b/Documentation/Strings-pl.json differ diff --git a/Documentation/Strings-pt.json b/Documentation/Strings-pt.json index a68d099..9efa1b4 100644 Binary files a/Documentation/Strings-pt.json and b/Documentation/Strings-pt.json differ diff --git a/Documentation/Strings-ru.json b/Documentation/Strings-ru.json index 99c4fdd..e1cb47b 100644 Binary files a/Documentation/Strings-ru.json and b/Documentation/Strings-ru.json differ diff --git a/Documentation/Strings-sv.json b/Documentation/Strings-sv.json index cc56ef9..ee19119 100644 Binary files a/Documentation/Strings-sv.json and b/Documentation/Strings-sv.json differ diff --git a/Documentation/Strings-tr.json b/Documentation/Strings-tr.json index 5ebc220..b1ec607 100644 Binary files a/Documentation/Strings-tr.json and b/Documentation/Strings-tr.json differ diff --git a/Documentation/Strings-zh-chs.json b/Documentation/Strings-zh-chs.json index a68d099..9efa1b4 100644 Binary files a/Documentation/Strings-zh-chs.json and b/Documentation/Strings-zh-chs.json differ diff --git a/Documentation/Strings-zh-cht.json b/Documentation/Strings-zh-cht.json index a68d099..9efa1b4 100644 Binary files a/Documentation/Strings-zh-cht.json and b/Documentation/Strings-zh-cht.json differ diff --git a/Documentation/Strings-zh-hans.json b/Documentation/Strings-zh-hans.json index dde5a2b..f690e8c 100644 Binary files a/Documentation/Strings-zh-hans.json and b/Documentation/Strings-zh-hans.json differ diff --git a/Documentation/Strings-zh-hant.json b/Documentation/Strings-zh-hant.json index d7e3f9d..5318f21 100644 Binary files a/Documentation/Strings-zh-hant.json and b/Documentation/Strings-zh-hant.json differ diff --git a/Documentation/Strings-zh.json b/Documentation/Strings-zh.json index a68d099..9efa1b4 100644 Binary files a/Documentation/Strings-zh.json and b/Documentation/Strings-zh.json differ diff --git a/Extensions/Compare.psm1 b/Extensions/Compare.psm1 index 1154299..1e466f2 100644 --- a/Extensions/Compare.psm1 +++ b/Extensions/Compare.psm1 @@ -11,13 +11,102 @@ Objects can be compared based on Properties or Documentatation info. function Get-ModuleVersion { - '1.0.1' + '1.0.2' } function Invoke-InitializeModule { - # Make sure we add the default Output types - Add-OutputType + $global:comparisonTypes = $null + $global:compareProviders = $null + $script:CompareProviderOptionsCache = $null + + $script:defaultCompareProps = [Collections.Generic.List[String]]@('ObjectName', 'Id', 'Type', 'Category', 'SubCategory', 'Property', 'Value1', 'Value2', 'Match') + + # Make sure we add the default providers + Add-CompareProvider + Add-ComparisonTypes + + $script:saveType = @( + [PSCustomObject]@{ + Name="One file for each object type" + Value="objectType" + }, + [PSCustomObject]@{ + Name="One file for all objects" + Value="all" + } + ) +} + +function Add-CompareProvider +{ + param($compareProvider) + + if(-not $global:compareProviders) + { + $global:compareProviders = @() + } + + if($global:compareProviders.Count -eq 0) + { + $global:compareProviders += [PSCustomObject]@{ + Name = "Exported File" + Value = "export" + ObjectCompare = { Compare-ObjectsBasedonProperty @args } + BulkCompare = { Start-BulkCompareExportObjects @args } + ProviderOptions = "CompareExportOptions" + Activate = { Invoke-ActivateCompareExportObjects @args } + } + + $global:compareProviders += [PSCustomObject]@{ + Name = "Named Objects" + Value = "name" + BulkCompare = { Start-BulkCompareNamedObjects @args } + ProviderOptions = "CompareNamedOptions" + Activate = { Invoke-ActivateCompareNamesObjects @args } + RemoveProperties = @("Id") + } + + $global:compareProviders += [PSCustomObject]@{ + Name = "Existing objects" + Value = "existing" + Compare = { Compare-ObjectsBasedonDocumentation @args } + } + } + + if(!$compareProvider) { return } + + $global:compareProviders += $compareProvider +} + +function Add-ComparisonTypes +{ + param($comparisonType) + + if(-not $global:comparisonTypes) + { + $global:comparisonTypes = @() + } + + if($global:comparisonTypes.Count -eq 0) + { + $global:comparisonTypes += [PSCustomObject]@{ + Name = "Property" + Value = "property" + Compare = { Compare-ObjectsBasedonProperty @args } + RemoveProperties = @('Category','SubCategory') + } + + $global:comparisonTypes += [PSCustomObject]@{ + Name = "Documentation" + Value = "doc" + Compare = { Compare-ObjectsBasedonDocumentation @args } + } + } + + if(!$comparisonType) { return } + + $global:comparisonTypes += $comparisonType } function Invoke-ShowMainWindow @@ -42,6 +131,510 @@ function Invoke-ShowMainWindow $global:spSubMenu.Children.Insert(0, $button) } +function Invoke-ViewActivated +{ + if($global:currentViewObject.ViewInfo.ID -ne "IntuneGraphAPI") { return } + + $tmp = $mnuMain.Items | Where Name -eq "EMBulk" + if($tmp) + { + $tmp.AddChild(([System.Windows.Controls.Separator]::new())) | Out-Null + $subItem = [System.Windows.Controls.MenuItem]::new() + $subItem.Header = "_Compare" + $subItem.Add_Click({Show-CompareBulkForm}) + $tmp.AddChild($subItem) + } +} + +function Show-CompareBulkForm +{ + $script:form = Get-XamlObject ($global:AppRootFolder + "\Xaml\BulkCompare.xaml") -AddVariables + if(-not $script:form) { return } + + $global:cbCompareProvider.ItemsSource = @(($global:compareProviders | Where BulkCompare -ne $null)) + $global:cbCompareProvider.SelectedValue = (Get-Setting "Compare" "Provider" "export") + + $global:cbCompareSave.ItemsSource = @($script:saveType) + $global:cbCompareSave.SelectedValue = (Get-Setting "Compare" "SaveType" "objectType") + + $global:cbCompareType.ItemsSource = $global:comparisonTypes | Where ShowOnBulk -ne $false + $global:cbCompareType.SelectedValue = (Get-Setting "Compare" "Type" "property") + + $script:compareObjects = @() + foreach($objType in $global:lstMenuItems.ItemsSource) + { + if(-not $objType.Title) { continue } + + $script:compareObjects += New-Object PSObject -Property @{ + Title = $objType.Title + Selected = $true + ObjectType = $objType + } + } + + $column = Get-GridCheckboxColumn "Selected" + $global:dgObjectsToCompare.Columns.Add($column) + + $column.Header.IsChecked = $true # All items are checked by default + $column.Header.add_Click({ + foreach($item in $global:dgObjectsToCompare.ItemsSource) + { + $item.Selected = $this.IsChecked + } + $global:dgObjectsToCompare.Items.Refresh() + } + ) + + # Add Object type column + $binding = [System.Windows.Data.Binding]::new("Title") + $column = [System.Windows.Controls.DataGridTextColumn]::new() + $column.Header = "Object type" + $column.IsReadOnly = $true + $column.Binding = $binding + $global:dgObjectsToCompare.Columns.Add($column) + + $global:dgObjectsToCompare.ItemsSource = $script:compareObjects + + Add-XamlEvent $script:form "btnClose" "add_click" { + $script:form = $null + Show-ModalObject + } + + Add-XamlEvent $script:form "btnStartCompare" "add_click" { + Write-Status "Compare objects" + Save-Setting "Compare" "Provider" $global:cbCompareProvider.SelectedValue + Save-Setting "Compare" "Type" $global:cbCompareType.SelectedValue + if($global:cbCompareProvider.SelectedItem.BulkCompare) + { + & $global:cbCompareProvider.SelectedItem.BulkCompare + } + Write-Status "" + } + + $global:cbCompareProvider.Add_SelectionChanged({ + Set-CompareProviderOptions $this + }) + + Set-CompareProviderOptions $global:cbCompareProvider + + Show-ModalForm "Bulk Compare Objects" $script:form -HideButtons +} + +function Set-CompareProviderOptions +{ + param($control) + + $providerOptions = $null + $firstTime = $false + if($control.SelectedItem.ProviderOptions) + { + if($script:CompareProviderOptionsCache -isnot [Hashtable]) { $script:CompareProviderOptionsCache = @{} } + if($script:CompareProviderOptionsCache.Keys -contains $control.SelectedValue) + { + $providerOptions = $script:CompareProviderOptionsCache[$control.SelectedValue] + } + else + { + $providerOptions = Get-XamlObject ($global:AppRootFolder + "\Xaml\$($control.SelectedItem.ProviderOptions).xaml") -AddVariables + if($providerOptions) + { + $firstTime = $true + $script:CompareProviderOptionsCache.Add($control.SelectedValue, $providerOptions) + } + else + { + Write-Log "Failed to create options for $($control.SelectedItem.Name)" 3 + } + } + $global:ccContentProviderOptions.Content = $providerOptions + } + else + { + $global:ccContentProviderOptions.Content = $null + } + $global:ccContentProviderOptions.Visibility = (?: ($global:ccContentProviderOptions.Content -eq $null) "Collapsed" "Visible") + + if($control.SelectedItem.Activate) + { + if($firstTime) + { + Write-Log "Initialize $($global:cbCompareProvider.SelectedItem.Name) provider options" + } + + & $control.SelectedItem.Activate $providerOptions $firstTime + } +} + +function Invoke-ActivateCompareExportObjects +{ + param($providerOptions, $firstTime) + + if($firstTime) + { + $path = Get-Setting "" "LastUsedFullPath" + if($path) + { + $path = [IO.Directory]::GetParent($path).FullName + } + Set-XamlProperty $providerOptions "txtExportPath" "Text" (?? $path (Get-SettingValue "RootFolder")) + + Add-XamlEvent $providerOptions "browseExportPath" "add_click" ({ + $folder = Get-Folder (Get-XamlProperty $this.Parent "txtExportPath" "Text") "Select root folder for compare" + if($folder) + { + Set-XamlProperty $this.Parent "txtExportPath" "Text" $folder + } + }) + } +} + +function Invoke-ActivateCompareNamesObjects +{ + param($providerOptions, $firstTime) + + if($providerOptions -and $firstTime) + { + Set-XamlProperty $providerOptions "txtCompareSource" "Text" (Get-Setting "Compare" "CompareSource" "") + Set-XamlProperty $providerOptions "txtCompareWith" "Text" (Get-Setting "Compare" "CompareWith" "") + + Set-XamlProperty $providerOptions "txtSavePath" "Text" (Get-Setting "Compare" "SavePath" "") + Add-XamlEvent $providerOptions "browseSavePath" "add_click" ({ + $folder = Get-Folder (Get-XamlProperty $this.Parent "txtSavePath" "Text") "Select folder" + if($folder) + { + Set-XamlProperty $this.Parent "txtSavePath" "Text" $folder + } + }) + } +} + +function Start-BulkCompareNamedObjects +{ + Write-Log "****************************************************************" + Write-Log "Start bulk Named Objects compare" + Write-Log "****************************************************************" + $compareObjectsResult = @() + + $compareSource = (Get-XamlProperty $global:ccContentProviderOptions.Content "txtCompareSource" "Text") + $compareWith = (Get-XamlProperty $global:ccContentProviderOptions.Content "txtCompareWith" "Text") + + if(-not $compareSource -or -not $compareWith) + { + [System.Windows.MessageBox]::Show("Both source and compare name patterns must be specified", "Error", "OK", "Error") + return + } + + Save-Setting "Compare" "CompareSource" $compareSource + Save-Setting "Compare" "CompareWith" $compareWith + + Invoke-BulkCompareNamedObjects $compareSource $compareWith + + Write-Log "****************************************************************" + Write-Log "Bulk compare Named Objects finished" + Write-Log "****************************************************************" + Write-Status "" +} + +function Invoke-BulkCompareNamedObjects +{ + param($sourcePattern, $comparePattern) + + $outputType = $global:cbCompareSave.SelectedValue + + Save-Setting "Compare" "SaveType" $outputType + + $compResultValues = @() + $compareObjectsResult = @() + + $outputFolder = (Get-XamlProperty $global:ccContentProviderOptions.Content "txtSavePath" "Text") + if(-not $outputFolder) + { + $outputFolder = Expand-FileName "%MyDocuments%" + } + + $compareProps = $script:defaultCompareProps + + foreach($removeProp in $global:cbCompareProvider.SelectedItem.RemoveProperties) + { + $compareProps.Remove($removeProp) | Out-Null + } + + foreach($removeProp in $global:cbCompareType.SelectedItem.RemoveProperties) + { + $compareProps.Remove($removeProp) | Out-Null + } + + foreach($item in ($global:dgObjectsToCompare.ItemsSource | where Selected -eq $true)) + { + Write-Status "Compare $($item.ObjectType.Title) objects" -Force -SkipLog + Write-Log "----------------------------------------------------------------" + Write-Log "Compare $($item.ObjectType.Title) objects" + Write-Log "----------------------------------------------------------------" + + $url = $item.ObjectType.API + if($item.ObjectType.QUERYLIST) + { + $url = "$($url.Trim())?$($item.ObjectType.QUERYLIST.Trim())" + } + + $graphObjects = @(Get-GraphObjects -Url $url -property $item.ObjectType.ViewProperties -objectType $item.ObjectType) + + $nameProp = ?? $item.ObjectType.NameProperty "displayName" + + foreach($graphObj in ($graphObjects | Where { $_.Object."$($nameProp)" -imatch [regex]::Escape($sourcePattern) })) + { + $sourceName = $graphObj.Object."$($nameProp)" + $compareName = $sourceName -ireplace [regex]::Escape($sourcePattern),$comparePattern + + $compareObj = $graphObjects | Where { $_.Object."$($nameProp)" -eq $compareName -and $_.Object.'@OData.Type' -eq $graphObj.Object.'@OData.Type' } + + if(($compareObj | measure).Count -gt 1) + { + Write-Log "Multiple objects found with name $compareName. Compare will not be performed" 2 + continue + } + elseif($compareObj) + { + $sourceObj = Get-GraphObject $graphObj.Object $graphObj.ObjectType + $compareObj = Get-GraphObject $compareObj.Object $compareObj.ObjectType + $compareProperties = Compare-Objects $sourceObj.Object $compareObj.Object $sourceObj.ObjectType + } + else + { + # Add objects that are exported but deleted + Write-Log "Object '$((Get-GraphObjectName $graphObj.Object $graphObj.ObjectType))' with id $($graphObj.Object.Id) has no matching object with the compate pattern" 2 + $compareProperties = @([PSCustomObject]@{ + Object1Value = (Get-GraphObjectName $graphObj.Object $graphObj.ObjectType) + Object2Value = $null + Match = $false + }) + } + + $compareObjectsResult += [PSCustomObject]@{ + Object1 = $sourceObj.Object + Object2 = $compareObj.Object + ObjectType = $item.ObjectType + Id = $sourceObj.Object.Id + Result = $compareProperties + } + } + + if($outputType -eq "objectType") + { + $compResultValues = @() + } + + foreach($compObj in @($compareObjectsResult | Where { $_.ObjectType.Id -eq $item.ObjectType.Id })) + { + $objName = Get-GraphObjectName (?? $compObj.Object1 $compObj.Object2) $item.ObjectType + foreach($compValue in $compObj.Result) + { + $compResultValues += [PSCustomObject]@{ + ObjectName = $objName + Id = $compObj.Id + Type = $compObj.ObjectType.Title + ODataType = $compObj.Object1.'@OData.Type' + Property = $compValue.PropertyName + Value1 = $compValue.Object1Value + Value2 = $compValue.Object2Value + Category = $compValue.Category + SubCategory = $compValue.SubCategory + Match = $compValue.Match + } + } + } + + if($outputType -eq "objectType") + { + $fileName = Remove-InvalidFileNameChars (Expand-FileName "Compare-$($graphObj.ObjectType.Id)-$sourcePattern-$comparePattern-%DateTime%.csv") + Save-BulkCompareResults $compResultValues (Join-Path $outputFolder $fileName) $compareProps + } + } + #$fileName = Expand-FileName $fileName + + if($compareObjectsResult.Count -eq 0) + { + [System.Windows.MessageBox]::Show("No objects were comparced. Verify name patterns", "Error", "OK", "Error") + } + elseif($outputType -eq "all") + { + $fileName = Remove-InvalidFileNameChars (Expand-FileName "Compare-$sourcePattern-$comparePattern-%DateTime%.csv") + Save-BulkCompareResults $compResultValues (Join-Path $outputFolder $fileName) $compareProps + } +} + +function Start-BulkCompareExportObjects +{ + Write-Log "****************************************************************" + Write-Log "Start bulk Exported Objects compare" + Write-Log "****************************************************************" + $compareObjectsResult = @() + $rootFolder = (Get-XamlProperty $global:ccContentProviderOptions.Content "txtExportPath" "Text") + + $compareProps = $script:defaultCompareProps + + foreach($removeProp in $global:cbCompareProvider.SelectedItem.RemoveProperties) + { + $compareProps.Remove($removeProp) | Out-Null + } + + foreach($removeProp in $global:cbCompareType.SelectedItem.RemoveProperties) + { + $compareProps.Remove($removeProp) | Out-Null + } + + if(-not $rootFolder) + { + [System.Windows.MessageBox]::Show("Root folder must be specified", "Error", "OK", "Error") + return + } + + if([IO.Directory]::Exists($rootFolder) -eq $false) + { + [System.Windows.MessageBox]::Show("Root folder $rootFolder does not exist", "Error", "OK", "Error") + return + } + + $outputType = $global:cbCompareSave.SelectedValue + Save-Setting "Compare" "SaveType" $outputType + + $compResultValues = @() + + foreach($item in ($global:dgObjectsToCompare.ItemsSource | where Selected -eq $true)) + { + Write-Status "Compare $($item.ObjectType.Title) objects" -Force -SkipLog + Write-Log "----------------------------------------------------------------" + Write-Log "Compare $($item.ObjectType.Title) objects" + Write-Log "----------------------------------------------------------------" + + $folder = Join-Path $rootFolder $item.ObjectType.Id + + if([IO.Directory]::Exists($folder)) + { + Save-Setting "" "LastUsedFullPath" $folder + + $url = $item.ObjectType.API + if($item.ObjectType.QUERYLIST) + { + $url = "$($url.Trim())?$($item.ObjectType.QUERYLIST.Trim())" + } + + $graphObjects = @(Get-GraphObjects -Url $url -property $item.ObjectType.ViewProperties -objectType $item.ObjectType) + + foreach ($fileObj in @(Get-GraphFileObjects $folder -ObjectType $item.ObjectType)) + { + if(-not $fileObj.Object.Id) + { + Write-Log "Object from file '$($fileObj.FullName)' has no Id property. Compare not supported" 2 + continue + } + $curObject = $graphObjects | Where { $_.Object.Id -eq $fileObj.Object.Id } + + if(-not $curObject) + { + # Add objects that are exported but deleted + Write-Log "Object '$((Get-GraphObjectName $fileObj.Object $fileObj.ObjectType))' with id $($fileObj.Object.Id) not found in Intune. Deleted?" 2 + $compareProperties = @([PSCustomObject]@{ + Object1Value = $null + Object2Value = (Get-GraphObjectName $fileObj.Object $item.ObjectType) + Match = $false + }) + } + else + { + $sourceObj = Get-GraphObject $curObject.Object $curObject.ObjectType + $compareProperties = Compare-Objects $sourceObj.Object $fileObj.Object $item.ObjectType + } + + $compareObjectsResult += [PSCustomObject]@{ + Object1 = $curObject.Object + Object2 = $fileObj.Object + ObjectType = $item.ObjectType + Id = $fileObj.Object.Id + Result = $compareProperties + } + } + + foreach($graphObj in $graphObjects) + { + # Add objects that are not exported + if(($compareObjectsResult | Where { $_.Id -eq $graphObj.Id})) { continue } + + $compareObjectsResult += [PSCustomObject]@{ + Object1 = $curObject.Object + Object2 = $null + ObjectType = $item.ObjectType + Id = $graphObj.Id + Result = @([PSCustomObject]@{ + Object1Value = (Get-GraphObjectName $graphObj.Object $item.ObjectType) + Object2Value = $null + Match = $false + }) + } + } + + if($outputType -eq "objectType") + { + $compResultValues = @() + } + + foreach($compObj in @($compareObjectsResult | Where { $_.ObjectType.Id -eq $item.ObjectType.Id })) + { + $objName = Get-GraphObjectName (?? $compObj.Object1 $compObj.Object2) $item.ObjectType + foreach($compValue in $compObj.Result) + { + $compResultValues += [PSCustomObject]@{ + ObjectName = $objName + Id = $compObj.Id + Type = $compObj.ObjectType.Title + ODataType = $compObj.Object1.'@OData.Type' + Property = $compValue.PropertyName + Value1 = $compValue.Object1Value + Value2 = $compValue.Object2Value + Category = $compValue.Category + SubCategory = $compValue.SubCategory + Match = $compValue.Match + } + } + } + + if($outputType -eq "objectType") + { + Save-BulkCompareResults $compResultValues (Join-Path $rootFolder "Compare_$(((Get-Date).ToString("yyyyMMdd-HHmm"))).csv") $compareProps + } + } + else + { + Write-Log "Folder $folder not found. Skipping import" 2 + } + } + + if($outputType -eq "all" -and $compResultValues.Count -gt 0) + { + Save-BulkCompareResults $compResultValues (Join-Path $folder "Compare_$(((Get-Date).ToString("yyyyMMDD-HHmm"))).csv") $compareProps + } + + Write-Log "****************************************************************" + Write-Log "Bulk compare Exported Objects finished" + Write-Log "****************************************************************" + Write-Status "" + if($compareObjectsResult.Count -eq 0) + { + [System.Windows.MessageBox]::Show("No objects were comparced. Verify folder and exported files", "Error", "OK", "Error") + } +} + +function Save-BulkCompareResults +{ + param($compResultValues, $file, $props) + + if($compResultValues.Count -gt 0) + { + Write-Log "Save bulk comare results to $file" + $compResultValues | Select -Property $props | ConvertTo-Csv -NoTypeInformation | Out-File $file -Force -Encoding UTF8 + } +} + function Show-CompareForm { param($objInfo) @@ -49,12 +642,15 @@ function Show-CompareForm $script:cmpForm = Get-XamlObject ($global:AppRootFolder + "\Xaml\CompareForm.xaml") -AddVariables if(-not $script:cmpForm) { return } + $script:cmpForm.Tag = $objInfo + $script:copareSource = $objInfo - $global:cbCompareType.ItemsSource = ("[ { Name: `"Property`",Value: `"property`" }, { Name: `"Documentation`",Value: `"doc`" }]" | ConvertFrom-Json) + $global:cbCompareType.ItemsSource = $global:comparisonTypes | Where ShowOnObject -ne $false $global:cbCompareType.SelectedValue = (Get-Setting "Compare" "Type" "property") $global:txtIntuneObject.Text = (Get-GraphObjectName $objInfo.Object $objInfo.ObjectType) + $global:txtIntuneObject.Tag = $objInfo Add-XamlEvent $script:cmpForm "btnClose" "add_click" { $script:cmpForm = $null @@ -65,7 +661,7 @@ function Show-CompareForm Write-Status "Compare objects" Save-Setting "Compare" "Type" $global:cbCompareType.SelectedValue $script:currentObjName = "" - Invoke-CompareObjects + Start-CompareExportObject Write-Status "" } @@ -80,22 +676,42 @@ function Show-CompareForm $sf.Filter = "CSV (*.csv)|*.csv|All files (*.*)| *.*" if($sf.ShowDialog() -eq "OK") { - $csvInfo = $global:dgCompareInfo.ItemsSource | Select PropertyName,Object1Value,Object2Value,Category,SubCategory,Match | ConvertTo-Csv -NoTypeInformation + $csvInfo = Get-CompareCsvInfo $global:dgCompareInfo.ItemsSource $script:cmpForm.Tag $csvInfo | Out-File $sf.FileName -Force -Encoding UTF8 } } Add-XamlEvent $script:cmpForm "btnCompareCopy" "add_click" { - - $global:dgCompareInfo.ItemsSource | Select PropertyName,Object1Value,Object2Value,Category,SubCategory,Match | ConvertTo-Csv -NoTypeInformation | Set-Clipboard + + (Get-CompareCsvInfo $global:dgCompareInfo.ItemsSource $script:cmpForm.Tag) | Set-Clipboard } Add-XamlEvent $script:cmpForm "browseCompareObject" "add_click" { + + $path = Get-Setting "" "LastUsedFullPath" + if($path) + { + $path = [IO.Directory]::GetParent($path).FullName + if($global:txtIntuneObject.Tag.ObjectType) + { + $objectTypePath = [IO.Path]::Combine($path, $global:txtIntuneObject.Tag.ObjectType.Id) + if([IO.Direcotry]::Exists($objectTypePath)) + { + $path = $objectTypePath + } + } + } + + $path = (?: ($global:lastCompareFile -eq $null) $path ([IO.FileInfo]$global:lastCompareFile).DirectoryName) + $of = [System.Windows.Forms.OpenFileDialog]::new() $of.Multiselect = $false $of.Filter = "Json files (*.json)|*.json" - $of.InitialDirectory = (?: ($global:lastCompareFile -eq $null) (Get-Setting "" "LastUsedRoot") ([IO.FileInfo]$global:lastCompareFile).DirectoryName) - + if($path) + { + $of.InitialDirectory = $path + } + if($of.ShowDialog()) { Set-XamlProperty $script:cmpForm "txtCompareFile" "Text" $of.FileName @@ -110,18 +726,55 @@ function Show-CompareForm Show-ModalForm "Compare Intune Objects" $script:cmpForm -HideButtons } -function Invoke-CompareObjects +function Get-CompareCsvInfo +{ + param($comareInfo, $objInfo) + + $compResultValues = @() + $objName = Get-GraphObjectName $objInfo.Object $objInfo.ObjectType + foreach($compValue in $comareInfo) + { + $compResultValues += [PSCustomObject]@{ + ObjectName = $objName + Id = $objInfo.Object.Id + Type = $objInfo.ObjectType.Title + ODataType = $objInfo.Object.'@OData.Type' + Property = $compValue.PropertyName + Value1 = $compValue.Object1Value + Value2 = $compValue.Object2Value + Category = $compValue.Category + SubCategory = $compValue.SubCategory + Match = $compValue.Match + } + } + + $compareProps = $script:defaultCompareProps + + # !!! Not supported yet + #foreach($removeProp in $global:cbCompareProvider.SelectedItem.RemoveProperties) + #{ + # $compareProps.Remove($removeProp) | Out-Null + #} + + foreach($removeProp in $global:cbCompareType.SelectedItem.RemoveProperties) + { + $compareProps.Remove($removeProp) | Out-Null + } + $compResultValues | Select -Property $compareProps | ConvertTo-Csv -NoTypeInformation +} + +function Start-CompareExportObject { if(-not $script:copareSource) { return } if(-not $global:txtCompareFile.Text) { - [System.Windows.MessageBox]::Show("No file selected", "Comapre", "OK", "Error") + [System.Windows.MessageBox]::Show("No file selected", "Compare", "OK", "Error") return } elseif([IO.File]::Exists($global:txtCompareFile.Text) -eq $false) { - [System.Windows.MessageBox]::Show("File '$($global:txtCompareFile.Text)' not found", "Comapre", "OK", "Error") + [System.Windows.MessageBox]::Show("File '$($global:txtCompareFile.Text)' not found", "Compare", "OK", "Error") return } @@ -138,7 +791,7 @@ function Invoke-CompareObjects } catch { - [System.Windows.MessageBox]::Show("Failed to convert json file '$($global:txtCompareFile.Text)'", "Comapre", "OK", "Error") + [System.Windows.MessageBox]::Show("Failed to convert json file '$($global:txtCompareFile.Text)'", "Compare", "OK", "Error") return } @@ -148,24 +801,45 @@ function Invoke-CompareObjects if($obj.Object."@OData.Type" -ne $compareObj."@OData.Type") { - if(([System.Windows.MessageBox]::Show("The object types does not match.`n`nDo you to compare the objects?", "Comapre", "YesNo", "Warning")) -eq "No") + if(([System.Windows.MessageBox]::Show("The object types does not match.`n`nDo you to compare the objects?", "Compare", "YesNo", "Warning")) -eq "No") { return } - } - + } + + $compareResult = Compare-Objects $obj.Object $compareObj $obj.ObjectType + + $global:dgCompareInfo.ItemsSource = $compareResult +} + +function Compare-Objects +{ + param($obj1, $obj2, $objectType) + $script:compareProperties = @() - if($global:cbCompareType.SelectedValue -eq "property") + if($global:cbCompareType.SelectedItem.Compare) { - Compare-ObjectsBasedonProperty $obj.Object $compareObj $obj.ObjectType + $compareResult = & $global:cbCompareType.SelectedItem.Compare $obj1 $obj2 $objectType + } + else + { + Write-Log "Selected comparison type ($($global:cbCompareType.SelectedItem.Name)) does not have a Compare property specified" 3 + } + <# + elseif($global:cbCompareType.SelectedValue -eq "property") + { + $compareResult = Compare-ObjectsBasedonProperty $obj1 $obj2 $objectType } elseif($global:cbCompareType.SelectedValue -eq "doc") { - Compare-ObjectsBasedonDocumentation $obj $compareObj + $compareResult = Compare-ObjectsBasedonDocumentation $obj1 $obj2 $objectType } - $global:dgCompareInfo.ItemsSource = $script:compareProperties + #> + + $compareResult } + function Set-ColumnVisibility { param($showCategory = $false, $showSubCategory = $false) @@ -208,7 +882,7 @@ function Compare-ObjectsBasedonProperty { param($obj1, $obj2, $objectType) - Write-Status "Compare properties" + Write-Status "Compare objects based on property values" Set-ColumnVisibility $false @@ -262,14 +936,16 @@ function Compare-ObjectsBasedonProperty $val1 = ($obj1.$propName | ConvertTo-Json -Depth 10) $val2 = ($obj2.$propName | ConvertTo-Json -Depth 10) Add-CompareProperty $propName $val1 $val2 - } + } + + $script:compareProperties } function Get-CompareCustomColumnsDoc { - param($objInfo) + param($obj) - if($objInfo.Object.'@OData.Type' -eq "#microsoft.graph.deviceEnrollmentPlatformRestrictionsConfiguration") + if($obj.'@OData.Type' -eq "#microsoft.graph.deviceEnrollmentPlatformRestrictionsConfiguration") { Set-ColumnVisibility $true $true } @@ -281,23 +957,34 @@ function Get-CompareCustomColumnsDoc function Compare-ObjectsBasedonDocumentation { - param($obj1, $obj2) + param($obj1, $obj2, $objectType) + + Write-Status "Compare objects based on documentation values" - Get-CompareCustomColumnsDoc $obj + Get-CompareCustomColumnsDoc $obj1 # ToDo: set this based on configuration value $script:assignmentOutput = "simpleFullCompare" - $docObj1 = Invoke-ObjectDocumentation $obj1 + $docObj1 = Invoke-ObjectDocumentation ([PSCustomObject]@{ + Object = $obj1 + ObjectType = $objectType + }) - $obj2 | Add-Member Noteproperty -Name "@CompareObject" -Value $true -Force + $obj2 | Add-Member Noteproperty -Name "@ObjectFromFile" -Value $true -Force $docObj2 = Invoke-ObjectDocumentation ([PSCustomObject]@{ Object = $obj2 - ObjectType = $obj1.ObjectType + ObjectType = $objectType }) - $settingsValue = ?? $obj1.ObjectType.CompareValue "Value" + $settingsValue = ?? $objectType.CompareValue "Value" + + if($docObj1.BasicInfo -and -not ($docObj1.BasicInfo | where Value -eq $obj1.Id)) + { + # Make sure the Id property is included + Add-CompareProperty "Id" $obj1.Id $obj2.Id $docObj1.BasicInfo[0].Category + } foreach ($prop in $docObj1.BasicInfo) { @@ -412,6 +1099,28 @@ function Compare-ObjectsBasedonDocumentation Add-CompareProperty $applicabilityRule.Property $val1 $val2 $applicabilityRule.Category } + $complianceActionsAdded = @() + foreach($complianceAction in $docObj1.ComplianceActions) + { + $complianceAction2 = $docObj2.ComplianceActions | Where { $_.IdStr -eq $complianceAction.IdStr } + $complianceActionsAdded += $complianceAction.IdStr + $val1 = ($complianceAction.Action + [environment]::NewLine + $complianceAction.Schedule + [environment]::NewLine + $complianceAction.MessageTemplateId + [environment]::NewLine + $complianceAction.EmailCCIds) + $val2 = ($complianceAction2.Action + [environment]::NewLine + $complianceAction2.Schedule + [environment]::NewLine + $complianceAction2.MessageTemplateId + [environment]::NewLine + $complianceAction2.EmailCCIds) + + Add-CompareProperty $complianceAction.Category $val1 $val2 + } + + foreach($complianceAction in $docObj2.ComplianceActions) + { + if(($complianceAction.IdStr) -in $complianceActionsAdded) { continue } + $complianceAction2 = $docObj1.ComplianceActions | Where { $_.IdStr -eq $complianceAction.IdStr } + $complianceActionsAdded += $complianceAction.IdStr + $val2 = ($complianceAction.Action + [environment]::NewLine + $complianceAction.Schedule + [environment]::NewLine + $complianceAction.MessageTemplateId + [environment]::NewLine + $complianceAction.EmailCCIds) + $val1 = ($complianceAction2.Action + [environment]::NewLine + $complianceAction2.Schedule + [environment]::NewLine + $complianceAction2.MessageTemplateId + [environment]::NewLine + $complianceAction2.EmailCCIds) + + Add-CompareProperty $complianceAction.Category $val1 $val2 + } + $script:assignmentStr = Get-LanguageString "TableHeaders.assignment" $script:groupsAdded = @() @@ -449,6 +1158,8 @@ function Compare-ObjectsBasedonDocumentation { Add-AssignmentInfo $docObj2 $docObj1 $assignment -ReversedValue } + + $script:compareProperties } function Add-AssignmentInfo diff --git a/Extensions/Copy.psm1 b/Extensions/Copy.psm1 new file mode 100644 index 0000000..b177ae6 --- /dev/null +++ b/Extensions/Copy.psm1 @@ -0,0 +1,173 @@ +function Get-ModuleVersion +{ + '1.0.0' +} + +function Invoke-InitializeModule +{ + +} + + +function Invoke-ViewActivated +{ + if($global:currentViewObject.ViewInfo.ID -ne "IntuneGraphAPI") { return } + + $tmp = $mnuMain.Items | Where Name -eq "EMBulk" + if($tmp) + { + $tmp.AddChild(([System.Windows.Controls.Separator]::new())) | Out-Null + $subItem = [System.Windows.Controls.MenuItem]::new() + $subItem.Header = "Cop_y" + $subItem.Add_Click({Show-CopyBulkForm}) + $tmp.AddChild($subItem) + } +} + +function Show-CopyBulkForm +{ + $script:form = Get-XamlObject ($global:AppRootFolder + "\Xaml\BulkCopy.xaml") -AddVariables + if(-not $script:form) { return } + + $global:txtCopyFromPattern.Text = Get-Setting "Copy" "CopyFromPattern" + $global:txtCopyToPattern.Text = Get-Setting "Copy" "CopyToPattern" + + $script:copyObjects = @() + foreach($objType in $global:lstMenuItems.ItemsSource) + { + if(-not $objType.Title) { continue } + + $script:copyObjects += New-Object PSObject -Property @{ + Title = $objType.Title + Selected = $true + ObjectType = $objType + } + } + + $column = Get-GridCheckboxColumn "Selected" + $global:dgObjectsToCopy.Columns.Add($column) + + $column.Header.IsChecked = $true # All items are checked by default + $column.Header.add_Click({ + foreach($item in $global:dgObjectsToCopy.ItemsSource) + { + $item.Selected = $this.IsChecked + } + $global:dgObjectsToCopy.Items.Refresh() + } + ) + + # Add Object type column + $binding = [System.Windows.Data.Binding]::new("Title") + $column = [System.Windows.Controls.DataGridTextColumn]::new() + $column.Header = "Object type" + $column.IsReadOnly = $true + $column.Binding = $binding + $global:dgObjectsToCopy.Columns.Add($column) + + $global:dgObjectsToCopy.ItemsSource = $script:copyObjects + + Add-XamlEvent $script:form "btnClose" "add_click" { + $script:form = $null + Show-ModalObject + } + + Add-XamlEvent $script:form "btnStartCopy" "add_click" { + Write-Status "Copy objects" + Start-BulkCopyObjects + Write-Status "" + } + + Show-ModalForm "Bulk Copy Objects" $script:form -HideButtons +} + +function Start-BulkCopyObjects +{ + Write-Log "****************************************************************" + Write-Log "Start bulk copy" + Write-Log "****************************************************************" + + $copyFrom = $global:txtCopyFromPattern.Text + $copyTo = $global:txtCopyToPattern.Text + + if(-not $copyFrom -or -not $copyTo) + { + [System.Windows.MessageBox]::Show("Both name patterns must be specified", "Error", "OK", "Error") + return + } + + Save-Setting "Copy" "CopyFromPattern" $global:txtCopyFromPattern.Text + Save-Setting "Copy" "CopyToPattern" $global:txtCopyToPattern.Text + + foreach($item in ($global:dgObjectsToCopy.ItemsSource | where Selected -eq $true)) + { + Write-Status "Copy $($item.ObjectType.Title) objects" -Force -SkipLog + Write-Log "----------------------------------------------------------------" + Write-Log "Copy $($item.ObjectType.Title) objects" + Write-Log "----------------------------------------------------------------" + + $url = $item.ObjectType.API + if($item.ObjectType.QUERYLIST) + { + $url = "$($url.Trim())?$($item.ObjectType.QUERYLIST.Trim())" + } + + $graphObjects = @(Get-GraphObjects -Url $url -property $item.ObjectType.ViewProperties -objectType $item.ObjectType) + + $nameProp = ?? $item.ObjectType.NameProperty "displayName" + + foreach($graphObj in ($graphObjects | Where { $_.Object."$($nameProp)" -imatch [regex]::Escape($copyFrom) })) + { + $sourceName = $graphObj.Object."$($nameProp)" + $copyName = $sourceName -ireplace [regex]::Escape($copyFrom),$copyTo + + $copyObj = $graphObjects | Where { $_.Object."$($nameProp)" -eq $copyName -and $_.Object.'@OData.Type' -eq $graphObj.Object.'@OData.Type' } + + if(($copyObj | measure).Count -gt 0) + { + Write-Log "Object with name $copyName already exists. $sourceName will not be copied" 2 + continue + } + else + { + Write-Status "Create $copyName from $sourceName" -Force + + if($graphObj.ObjectType.PreCopyCommand) + { + if((& $graphObj.ObjectType.PreCopyCommand $graphObj.Object $graphObj.ObjectType $copyName)) + { + continue + } + } + + $copyFromObj = (Get-GraphObject $graphObj.Object $graphObj.ObjectType -SkipAssignments).Object + + # Convert to Json and back to clone the object + $obj = ConvertTo-Json $copyFromObj -Depth 10 | ConvertFrom-Json + if($obj) + { + # Import new profile + Set-GraphObjectName $obj $graphObj.ObjectType $copyName + + $newObj = Import-GraphObject $obj $graphObj.ObjectType + if($newObj) + { + if($graphObj.ObjectType.PostCopyCommand) + { + & $graphObj.ObjectType.PostCopyCommand $copyFromObj $newObj $graphObj.ObjectType + } + } + else + { + Write-log "Failed to copy $sourceName" 3 + } + } + } + } + } + + Write-Log "****************************************************************" + Write-Log "Bulk copy finished" + Write-Log "****************************************************************" + Write-Status "" +} \ No newline at end of file diff --git a/Extensions/Documentation.psm1 b/Extensions/Documentation.psm1 index 518ad5c..ab65f6b 100644 --- a/Extensions/Documentation.psm1 +++ b/Extensions/Documentation.psm1 @@ -20,7 +20,7 @@ $global:documentationProviders = @() function Get-ModuleVersion { - '1.0.1' + '1.0.2' } function Invoke-InitializeModule @@ -124,7 +124,13 @@ function Get-ObjectDocumentation { param($documentationObj) - Write-Status "Get documentation info for $((Get-GraphObjectName $documentationObj.Object $documentationObj.ObjectType)) ($($documentationObj.ObjectType.Title))" + $additionalInfo = "" + if($documentationObj.Object.'@ObjectFromFile' -eq $true) + { + $additionalInfo = " - From File" + } + + Write-Status "Get documentation info for $((Get-GraphObjectName $documentationObj.Object $documentationObj.ObjectType)) ($($documentationObj.ObjectType.Title))$additionalInfo" $status = $null $inputType = "Settings" @@ -225,24 +231,8 @@ function Get-ObjectDocumentation { $inputType = "Property" $processed = $true - <# - if([IO.File]::Exists(($global:AppRootFolder + "\Documentation\ObjectInfo\$($obj.'@OData.Type').json"))) - { - # Process object based on OData type - $processed = Invoke-TranslateCustomProfileObject $obj "$($obj.'@OData.Type')" - } - elseif($objectType -and [IO.File]::Exists(($global:AppRootFolder + "\Documentation\ObjectInfo\#$($objectType.Id).json"))) - { - # Process object based on Intune Object Type ($objectType) - # '#' is added to front of name to distinguish manually created files from generated files - $processed = Invoke-TranslateCustomProfileObject $obj "#$($objectType.Id)" - } - else - { - # Process objects based on generated Category Files and ObjectCategories.json - $processed = Invoke-TranslateProfileObject $obj - } - #> + + $processed = Invoke-TranslateProfileObject $obj if($processed -eq $false) @@ -685,7 +675,7 @@ function Invoke-TranslateADMXObject $categoryObj = $script:admxCategories | Where { $definitionValue.definition.id -in ($_.definitions.id) } $category = $script:admxCategories.definitions | Where { $definitionValue.definition.id -in ($_.id) } # Get presentation values for the current settings (with presentation object included) - if($definitionValue.presentationValues -or $obj.'@CompareObject' -eq $true) #$definitionValue.'definition@odata.bind') + if($definitionValue.presentationValues -or $obj.'@ObjectFromFile' -eq $true) #$definitionValue.'definition@odata.bind') { # Documenting exported json #$presentationValues = (Invoke-GraphRequest -Url "$($definitionValue.'definition@odata.bind')/presentations?`$expand=presentation" -ODataMetadata "minimal").value @@ -1039,7 +1029,7 @@ function Invoke-TranslateIntentObject foreach($category in ($categories | Sort -Property displayName)) { # Get settings for the category. This will put them in the correct order... - if($obj.'@CompareObject' -ne $true) + if($obj.'@ObjectFromFile' -ne $true) { $settings = (Invoke-GraphRequest "/deviceManagement/intents/$($obj.Id)/categories/$($category.Id)/settings?`$expand=Microsoft.Graph.DeviceManagementComplexSettingInstance/Value" -ODataMetadata "minimal" @params).Value } @@ -2662,7 +2652,9 @@ function Invoke-TranslateScheduledActionType foreach($actionConfig in $actionRule.scheduledActionConfigurations) { $notificationTemplate = $null + $notificationTemplateId = $null $additionalNotifications = $null + $additionalNotificationsList = $null if($actionConfig.actionType -eq "notification") { @@ -2694,6 +2686,7 @@ function Invoke-TranslateScheduledActionType if($actionConfig.notificationTemplateId -ne [Guid]::Empty) { $notificationTemplate = Get-LanguageString "ScheduledAction.Notification.selected" + $notificationTemplateId = $actionConfig.notificationTemplateId } else { @@ -2703,6 +2696,7 @@ function Invoke-TranslateScheduledActionType if($actionConfig.notificationMessageCCList.Count -gt 0) { $additionalNotifications = ((Get-LanguageString "ScheduledAction.Notification.numSelected") -f $actionConfig.notificationMessageCCList.Count) + $additionalNotificationsList = $actionConfig.notificationMessageCCList -join "," } else { @@ -2710,11 +2704,26 @@ function Invoke-TranslateScheduledActionType } } + $objClone = $actionConfig | ConvertTo-Json -Depth 10 | ConvertFrom-Json + + Remove-Property $objClone "Id" + foreach($prop in $objClone.PSObject.Properties) + { + if($prop.Name -like "*@odata*") + { + Remove-Property $objClone $prop.Name + } + } + + # ToDo: Resolve MessageTemplateId and EmailCCIds to actual object names $script:objectComplianceActionData += New-Object PSObject -Property @{ + IdStr = ($objClone | ConvertTo-Json -Depth 10 -Compress) Action = $actionType Schedule = $schedule MessageTemplate = $notificationTemplate + MessageTemplateId = $notificationTemplateId EmailCC = $additionalNotifications + EmailCCIds = $additionalNotificationsList Category=$category RawJsonValue=($actionConfig | ConvertTo-Json -Depth 20 -Compress) } @@ -3455,11 +3464,10 @@ function Show-DocumentationForm $tmpArr += $script:objectSettingsData | Select -Property $script:settingsProperties | ConvertTo-Csv -NoTypeInformation } - if($script:objectSettingsData.Count -gt 0) + if($script:applicabilityRules.Count -gt 0) { $tmpArr += $script:applicabilityRules | Select -Property Rule,Property,Value,Category | ConvertTo-Csv -NoTypeInformation - } - + } if($script:objectComplianceActionData.Count -gt 0) { diff --git a/Extensions/DocumentationCustom.psm1 b/Extensions/DocumentationCustom.psm1 index c652694..07d6c82 100644 --- a/Extensions/DocumentationCustom.psm1 +++ b/Extensions/DocumentationCustom.psm1 @@ -10,7 +10,7 @@ This module will also document some objects based on PowerShell functions function Get-ModuleVersion { - '1.0.0' + '1.0.1' } function Invoke-InitializeModule @@ -68,7 +68,14 @@ function Invoke-CDDocumentObject return [PSCustomObject]@{ Properties = @("Name","Value","Category","SubCategory") } - } + } + elseif($type -eq '#microsoft.graph.policySet') + { + Invoke-CDDocumentPolicySet $documentationObj + return [PSCustomObject]@{ + Properties = @("Name","Value","Category","SubCategory") + } + } } function Get-CDAllManagedApps @@ -2040,4 +2047,102 @@ function Invoke-CDDocumentConditionalAccess EntityKey = "persistentBrowser" }) } -} \ No newline at end of file +} + +#region Document Policy Sets +function Invoke-CDDocumentPolicySet +{ + param($documentationObj) + + $obj = $documentationObj.Object + $objectType = $documentationObj.ObjectType + + $script:objectSeparator = ?? $global:cbDocumentationObjectSeparator.SelectedValue ([System.Environment]::NewLine) + $script:propertySeparator = ?? $global:cbDocumentationPropertySeparator.SelectedValue "," + + ################################################### + # Basic info + ################################################### + + Add-BasicDefaultValues $obj $objectType + Add-BasicPropertyValue (Get-LanguageString "TableHeaders.configurationType") (Get-LanguageString "SettingDetails.appConfiguration") + + + ################################################### + # Basic info + ################################################### + + $addedSettings = @() + + $policySetSettings = ( + [PSCustomObject]@{ + Types = @( + @('#microsoft.graph.mobileAppPolicySetItem','appTitle'), + @('#microsoft.graph.targetedManagedAppConfigurationPolicySetItem','appConfigurationTitle'), + @('#microsoft.graph.managedAppProtectionPolicySetItem','appProtectionTitle'), + @('#microsoft.graph.iosLobAppProvisioningConfigurationPolicySetItem','iOSAppProvisioningTitle')) + Category = (Get-LanguageString "PolicySet.appManagement") + }, + [PSCustomObject]@{ + Types = @( + @('#microsoft.graph.deviceConfigurationPolicySetItem','deviceConfigurationTitle'), + @('#microsoft.graph.deviceCompliancePolicyPolicySetItem','deviceComplianceTitle'), + @('#microsoft.graph.deviceManagementScriptPolicySetItem','powershellScriptTitle')) + Category = (Get-LanguageString "PolicySet.deviceManagement") + }, + [PSCustomObject]@{ + Types = @( + @('#microsoft.graph.enrollmentRestrictionsConfigurationPolicySetItem','deviceTypeRestrictionTitle'), + @('#microsoft.graph.windowsAutopilotDeploymentProfilePolicySetItem','windowsAutopilotDeploymentProfileTitle'), + @('#microsoft.graph.windows10EnrollmentCompletionPageConfigurationPolicySetItem','enrollmentStatusSettingTitle')) + Category = (Get-LanguageString "PolicySet.deviceEnrollment") + } + ) + + foreach($policySettingType in $policySetSettings) + { + foreach($subType in $policySettingType.Types) + { + foreach($setting in ($obj.items | where '@OData.Type' -eq $subType[0])) + { + if($setting.status -eq "error") + { + Write-Log "Skipping missing $($subType[0]) type with id $($setting.id). Error code: $($setting.errorCode)" + continue + } + + Add-CustomSettingObject ([PSCustomObject]@{ + Name = $setting.displayName + Value = (Get-CDDocumentPolicySetValue $setting) + EntityKey = $setting.id + Category = $policySettingType.Category + SubCategory = (Get-LanguageString "PolicySet.$($subType[1])") + }) + } + } + } +} + +function Get-CDDocumentPolicySetValue +{ + param($policySetItem) + + if($policySetItem.'@OData.Type' -eq '#microsoft.graph.enrollmentRestrictionsConfigurationPolicySetItem' -or + $policySetItem.'@OData.Type' -eq '#microsoft.graph.windows10EnrollmentCompletionPageConfigurationPolicySetItem') + { + return $policySetItem.Priority + } + elseif($policySetItem.'@OData.Type' -eq '#microsoft.graph.windowsAutopilotDeploymentProfilePolicySetItem') + { + if($policySetItem.itemType -eq '#microsoft.graph.azureADWindowsAutopilotDeploymentProfile') + { + return (Get-LanguageString "Autopilot.DirectoryService.azureAD") + } + elseif($policySetItem.itemType -eq '#microsoft.graph.activeDirectoryWindowsAutopilotDeploymentProfile') + { + return (Get-LanguageString "Autopilot.DirectoryService.activeDirectoryAD") + } + } + # ToDo: Add support for all PolicySet items +} +#endregion \ No newline at end of file diff --git a/Extensions/DocumentationWord.psm1 b/Extensions/DocumentationWord.psm1 index faad5cf..a778f55 100644 --- a/Extensions/DocumentationWord.psm1 +++ b/Extensions/DocumentationWord.psm1 @@ -3,7 +3,7 @@ #https://docs.microsoft.com/en-us/office/vba/api/overview/word function Get-ModuleVersion { - '1.0.0' + '1.0.1' } function Invoke-InitializeModule @@ -225,24 +225,12 @@ function Invoke-WordPostProcessItems $script:doc.TablesOfFigures | ForEach-Object -Process { $_.Update() | Out-Null } $fileName = $global:txtWordDocumentName.Text - - [Environment]::SetEnvironmentVariable("Date",(Get-Date).ToString("yyyy-MM-dd"),[System.EnvironmentVariableTarget]::Process) - [Environment]::SetEnvironmentVariable("Organization",$global:Organization.displayName,[System.EnvironmentVariableTarget]::Process) - if(-not $fileName) { $fileName = "%MyDocuments%\%Organization%-%Date%.docx" } - $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("Organization",$null,[System.EnvironmentVariableTarget]::Process) - $fileName + $fileName = Expand-FileName $fileName $format = [Microsoft.Office.Interop.Word.WdSaveFormat]::wdFormatDocumentDefault diff --git a/Extensions/EndpointManager.psm1 b/Extensions/EndpointManager.psm1 index fa72890..1ef27eb 100644 --- a/Extensions/EndpointManager.psm1 +++ b/Extensions/EndpointManager.psm1 @@ -10,7 +10,7 @@ This module is for the Endpoint Manager/Intune View. It manages Export/Import/Co #> function Get-ModuleVersion { - '3.1.2' + '3.1.3' } function Invoke-InitializeModule @@ -562,7 +562,7 @@ function Invoke-EMAuthenticateToMSAL { $global:EMViewObject.AppInfo = Get-GraphAppInfo "EMAzureApp" "d1ddf0e4-d672-4dae-b554-9d5bdfd93547" Set-MSALCurrentApp $global:EMViewObject.AppInfo - & $global:msalAuthenticator.Login -Account (?? $global:MSALToken.Account.UserName (Get-Setting "" "LastLoggedOnUser")) -Permissions $global:EMViewObject.Permissions + & $global:msalAuthenticator.Login -Account (?? $global:MSALToken.Account.UserName (Get-Setting "" "LastLoggedOnUser")) } function Invoke-EMDeactivateView @@ -1178,7 +1178,7 @@ 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 some $filter options is not supported to filter out these objects + # 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 fiter 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 diff --git a/Extensions/EndpointManagerInfo.psm1 b/Extensions/EndpointManagerInfo.psm1 index e674a19..bf94e44 100644 --- a/Extensions/EndpointManagerInfo.psm1 +++ b/Extensions/EndpointManagerInfo.psm1 @@ -10,7 +10,7 @@ This module is for the Endpoint Info View. It shows read-only objects in Intune #> function Get-ModuleVersion { - '3.1.1' + '3.1.2' } function Invoke-InitializeModule @@ -100,6 +100,6 @@ function Invoke-EMInfoAuthenticateToMSAL $usr = (?? $global:MSALToken.Account.UserName (Get-Setting "" "LastLoggedOnUser")) if($usr) { - & $global:msalAuthenticator.Login -Account $usr -Permissions $global:EMInfoViewObject.Permissions + & $global:msalAuthenticator.Login -Account $usr } } \ No newline at end of file diff --git a/Extensions/MSALAuthentication.psm1 b/Extensions/MSALAuthentication.psm1 index 92a4d58..ea1bb82 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.0.1' + '3.0.2' } $global:msalAuthenticator = $null @@ -520,9 +520,7 @@ function Connect-MSALUser [switch] $Interactive, - $Account, - - $Permissions = @() # Addidional permissions required by the current view object + $Account ) # No login during first time the app is started @@ -563,7 +561,7 @@ function Connect-MSALUser $global:MSALToken = $null } - if((Get-SettingValue "UseDefaultPermissions") -eq $true) + if((Get-SettingValue "UseDefaultPermissions") -eq $true -or ($global:currentViewObject.ViewInfo.Permissions | measure).Count -eq 0) { [string[]] $Scopes = "https://graph.microsoft.com/.default" $useDefaultPermissions = $true @@ -582,11 +580,7 @@ function Connect-MSALUser $reqScopes += "RoleManagement.Read.Directory" } - if($Permissions.Count -gt 0) - { - $script:curViewPermissions = $Permissions - } - $reqScopes += $script:curViewPermissions + $script:curViewPermissions = $global:currentViewObject.ViewInfo.Permissions foreach($tmpScope in $script:curViewPermissions) { diff --git a/Extensions/MSGraph.psm1 b/Extensions/MSGraph.psm1 index 5349315..c8d0480 100644 --- a/Extensions/MSGraph.psm1 +++ b/Extensions/MSGraph.psm1 @@ -10,7 +10,7 @@ This module manages Microsoft Grap fuctions like calling APIs, managing graph ob #> function Get-ModuleVersion { - '3.1.1' + '3.1.2' } $global:MSGraphGlobalApps = @( @@ -622,6 +622,7 @@ function Show-GraphExportForm Set-XamlProperty $script:exportForm "txtExportPath" "Text" (?? (Get-Setting "" "LastUsedRoot") (Get-SettingValue "RootFolder")) Set-XamlProperty $script:exportForm "chkAddObjectType" "IsChecked" (Get-SettingValue "AddObjectType") Set-XamlProperty $script:exportForm "chkAddCompanyName" "IsChecked" (Get-SettingValue "AddCompanyName") + Set-XamlProperty $script:exportForm "chkExportAssignments" "IsChecked" (Get-SettingValue "ExportAssignments") Set-XamlProperty $script:exportForm "btnExportSelected" "IsEnabled" ($global:dgObjects.SelectedItem -ne $null) if(($global:dgObjects.ItemsSource | Where IsSelected -eq $true).Count -gt 0) @@ -672,6 +673,7 @@ function Show-GraphBulkExportForm Set-XamlProperty $script:exportForm "txtExportPath" "Text" (?? (Get-Setting "" "LastUsedRoot") (Get-SettingValue "RootFolder")) Set-XamlProperty $script:exportForm "chkAddCompanyName" "IsChecked" (Get-SettingValue "AddCompanyName") + Set-XamlProperty $script:exportForm "chkExportAssignments" "IsChecked" (Get-SettingValue "ExportAssignments") Add-XamlEvent $script:exportForm "browseExportPath" "add_click" ({ $folder = Get-Folder (Get-XamlProperty $script:exportForm "txtExportPath" "Text") "Select root folder for export" @@ -1147,11 +1149,20 @@ function Get-GraphFileObjects $fileArr = @() foreach($file in (Get-Item -path "$path\*.json" @params)) { + if($ObjectType.LoadObject) + { + $graphObj = & $ObjectType.LoadObject $file.FullName + } + else + { + $graphObj = (ConvertFrom-Json (Get-Content $file.FullName -Raw)) + } + $obj = New-Object PSObject -Property @{ FileName = $file.Name FileInfo = $file Selected = $SelectedStatus - Object = (ConvertFrom-Json (Get-Content $file.FullName -Raw)) + Object = $graphObj ObjectType = $ObjectType } @@ -1818,13 +1829,7 @@ function Export-GraphObject if($chkExportAssignments.IsChecked -ne $true -and $obj.Assignments) { - ### ToDo: Fix full support for including Assignments. $extend=Assignments might not work - ### E.g. Check AutoPilot - Remove-Property $obj $Assignments - } - elseif($chkExportAssignments.IsChecked -eq $true -and -not $obj.Assignments) - { - + Remove-Property $obj "Assignments" } $obj | ConvertTo-Json -Depth 10 | Out-File ([IO.Path]::Combine($exportFolder, (Remove-InvalidFileNameChars "$((Get-GraphObjectName $obj $objectType)).json"))) diff --git a/README.md b/README.md index 5096934..9c0243e 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,29 @@ Objects can be compared based on property values or documented values. The property value method is a quick way to compare objects but it will only show the names and values of the native Intune object. This is not a good comparison method for Settings objects since they have all the settings in one property. -The documentation method is a bit slower but will show the values as they are stated in the Intune portal. This is the recommended way to compare objects but note that this is only supported on object types that supports documentation. +The documentation method is a bit slower but will show the values as they are stated in the Intune portal. This is the recommended way to compare objects but note that this is only supported on object types that supports documentation. + +Bulk compare is supported. This can be performed in two ways: + +* **Export File** - This will read each exported file and compare it with the existing object + + The result file will be stored in the exported folder structure. Either in the Object Type folder or the parent folder depending on the Save as setting. + + **Note:** This cannot be used with files exported from a different environment since it used the Id as identifier + +* **Named Objects** - Compare file based on patterns + + This can be used in where a pattern is used separate objects between different environments e.g. [Test] Policy 1 vs [Prod] Policy 1. + + Output files are by default stored in the My Documents folder. + +The output CSV can either be one file for ALL objects or one file for each Object Type. + +## Bulk Copy + +Bulk copy can be used to clone objects based on a name pattern. This can be used in the same scenario as Bulk Compare where the object names includes an environment identifier. The application will identify all objects matching the source pattern and copy each object with a new name matching the 'Copy object name pattern'. The object will not be copied if it detects that an object already exists with the new name. + +**Note:** Assignments will NOT be copied. ## Change log diff --git a/ReleaseNotes.md b/ReleaseNotes.md index e3b3982..c2bb6a1 100644 --- a/ReleaseNotes.md +++ b/ReleaseNotes.md @@ -1,5 +1,27 @@ # Release Notes +## 3.1.3 - 2021-07-05 + +**New features** + +- Bulk Compare + - Compare with exported files + - Compare with existing objects based on name patterns +- Bulk Copy + - Copy existing objects based on name patterns +- Support for documenting PolicySets +- Release Notes check - Check if there are any updates by comparing the local version of ReleaseNotes.md with the GitHub version + +**Fixes** + +* Fixed bug that caused an exception when exporting objects with an assignment and the 'Export Assignment' option disabled. + + See [Issue 16](https://github.com/Micke-K/IntuneManagement/issues/16) for more info + +* Export Assignments in Bulk Export and Object Export did not get default value from Settings + +* Fixed issue where the required permissions were not passed during authentication + ## 3.1.2 - 2021-06-20 **New features** diff --git a/Xaml/BulkCompare.xaml b/Xaml/BulkCompare.xaml new file mode 100644 index 0000000..3023400 --- /dev/null +++ b/Xaml/BulkCompare.xaml @@ -0,0 +1,79 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Xaml/CompareNamedOptions.xaml b/Xaml/CompareNamedOptions.xaml new file mode 100644 index 0000000..efd60c4 --- /dev/null +++ b/Xaml/CompareNamedOptions.xaml @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Xaml/MainWindow.xaml b/Xaml/MainWindow.xaml index 1ac0116..ec2d7f0 100644 --- a/Xaml/MainWindow.xaml +++ b/Xaml/MainWindow.xaml @@ -34,6 +34,7 @@ + diff --git a/Xaml/UpdatesDialog.xaml b/Xaml/UpdatesDialog.xaml new file mode 100644 index 0000000..4b06cdc --- /dev/null +++ b/Xaml/UpdatesDialog.xaml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + The local and GitHub versions of ReleaseNotes.md match. + + + The local and GitHub versions of ReleaseNotes.md does not match. Download the latest version from + + GitHub + . + +