From f5613442bd93f9d06e61012b1fc0e64247ecbe4b Mon Sep 17 00:00:00 2001
From: Mikael Karlsson <43226266+Micke-K@users.noreply.github.com>
Date: Mon, 11 Dec 2023 18:58:13 +1100
Subject: [PATCH] 3.9.3
---
CloudAPIPowerShellManagement.psd1 | 4 +-
Core.psm1 | 35 +-
Extensions/Documentation.psm1 | 27 +-
Extensions/DocumentationHTML.psm1 | 4 +-
Extensions/DocumentationMD.psm1 | 4 +-
Extensions/DocumentationWord.psm1 | 6 +-
Extensions/EndpointManager.psm1 | 90 +++--
Extensions/IntuneAppManagement.psm1 | 18 +-
Extensions/IntuneFilterUsage.psm1 | 329 ++++++++++++++++++
Extensions/MSALAuthentication.psm1 | 6 +-
Extensions/MSGraph.psm1 | 114 +++---
ReleaseNotes.md | 52 +++
Scripts/Export-ExcryptionKeys.ps1 | 159 +++++++++
...EndpointManagerToolsIntuneAssignments.xaml | 15 +-
Xaml/IntuneToolsFiterUsage.xaml | 54 +++
15 files changed, 824 insertions(+), 93 deletions(-)
create mode 100644 Extensions/IntuneFilterUsage.psm1
create mode 100644 Scripts/Export-ExcryptionKeys.ps1
create mode 100644 Xaml/IntuneToolsFiterUsage.xaml
diff --git a/CloudAPIPowerShellManagement.psd1 b/CloudAPIPowerShellManagement.psd1
index 906e02e..91d7a57 100644
--- a/CloudAPIPowerShellManagement.psd1
+++ b/CloudAPIPowerShellManagement.psd1
@@ -12,7 +12,7 @@
RootModule = 'CloudAPIPowerShellManagement.psm1'
# Version number of this module.
-ModuleVersion = '3.9.2'
+ModuleVersion = '3.9.3'
# Supported PSEditions
# CompatiblePSEditions = @()
@@ -69,7 +69,7 @@ Description = 'Management of Intune and Azure via Cloud APIs like Microsoft Grap
NestedModules = @()
# Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export.
-FunctionsToExport = @("Initialize-CloudAPIManagement","Initialize-CloudAPIManagement")
+FunctionsToExport = @("Initialize-CloudAPIManagement")
# Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export.
# CmdletsToExport = @()
diff --git a/Core.psm1 b/Core.psm1
index aa975ad..ad069b3 100644
--- a/Core.psm1
+++ b/Core.psm1
@@ -11,7 +11,7 @@ This module handles the WPF UI
function Get-ModuleVersion
{
- '3.9.2'
+ '3.9.3'
}
function Initialize-Window
@@ -2069,7 +2069,7 @@ function Add-SettingsObject
$section.Values += $obj
}
catch { }
-}
+}
function Save-AllSettings
{
@@ -2797,7 +2797,17 @@ function Start-DownloadFile
try
{
- Write-Status "Download file: `n$sourceURL"
+ $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"
}
@@ -2831,6 +2841,25 @@ function Get-ASCIIBytes
$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)
+}
+
+
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/Documentation.psm1 b/Extensions/Documentation.psm1
index a7b03df..3c01aae 100644
--- a/Extensions/Documentation.psm1
+++ b/Extensions/Documentation.psm1
@@ -20,7 +20,7 @@ $global:documentationProviders = @()
function Get-ModuleVersion
{
- '2.0.2'
+ '2.0.3'
}
function Invoke-InitializeModule
@@ -228,6 +228,7 @@ function Get-ObjectDocumentation
$script:applicabilityRules = @()
$script:objectAssignments = @()
$script:objectScripts = @()
+ $script:admxCategories = $null
$script:ObjectTypeFullTable = @{} # Hash table with objects that should be documented in a single table eg ScopeTags
@@ -384,6 +385,7 @@ function Invoke-ObjectDocumentation
$global:catRecommendedSettings = $null
$global:intentCategoryDefs = $null
$global:cfgCategories = $null
+ $script:admxCategories = $null
$script:DocumentationLanguage = "en"
$script:objectSeparator = [System.Environment]::NewLine
@@ -879,7 +881,8 @@ function Invoke-TranslateADMXObject
{
if(-not $definitionValue.definition -and $definitionValues.'definition@odata.bind')
{
- $definition = Invoke-GraphRequest -Url $definitionValue.'definition@odata.bind' -ODataMetadata "minimal" @params
+ $url = $definitionValue.'definition@odata.bind' -replace $global:graphURL, ("https://$((?? $global:MSALGraphEnvironment "graph.microsoft.com"))/beta")
+ $definition = Invoke-GraphRequest -Url $url -ODataMetadata "minimal" @params
if($definition)
{
$definitionValue | Add-Member -MemberType NoteProperty -Name "definition" -Value $definition
@@ -1924,7 +1927,8 @@ function Get-LanguageString
if(-not $script:languageStrings)
{
- $fileContent = Get-Content ($global:AppRootFolder + "\Documentation\Strings-$($script:DocumentationLanguage).json") -Encoding UTF8
+ $lng = ?? $script:DocumentationLanguage "en"
+ $fileContent = Get-Content ($global:AppRootFolder + "\Documentation\Strings-$($lng).json") -Encoding UTF8
$script:languageStrings = $fileContent | ConvertFrom-Json
}
@@ -4432,7 +4436,7 @@ function local:Invoke-StartDocumentatiom
# Add each object to the documentation
foreach($curGroupId in ($sourceList.ObjectType | Select GroupID -Unique).GroupID)
{
- # New object group e.g. Script, Tennant, Device Configuration
+ # New object group e.g. Script, Tenant, Device Configuration
# A group matches a menu item in the protal but can contain multiple object types
if($global:cbDocumentationType.SelectedItem.NewObjectGroup)
{
@@ -5040,4 +5044,19 @@ function Set-TableObjects
{
$script:ObjectTypeFullTable.Add($objectInfo.ObjectType.Id, $objectInfo)
}
+}
+
+function Get-PolicyTypeName
+{
+ param($type, $default = $null)
+
+ $categoryObj = Get-TranslationFiles $type
+
+ if($null -eq $categoryObj) { return $default }
+
+ $lngStr = Get-LanguageString "PolicyType.$($categoryObj.PolicyTypeLanguageId)"
+
+ if($lngStr) { return $lngStr }
+
+ return $defult
}
\ No newline at end of file
diff --git a/Extensions/DocumentationHTML.psm1 b/Extensions/DocumentationHTML.psm1
index 08f979e..fb4affd 100644
--- a/Extensions/DocumentationHTML.psm1
+++ b/Extensions/DocumentationHTML.psm1
@@ -1,6 +1,6 @@
function Get-ModuleVersion
{
- '1.0.0'
+ '1.0.1'
}
function Invoke-InitializeModule
@@ -374,7 +374,7 @@ function Invoke-HTMLProcessItem
$isFilterAssignment = $false
foreach($assignment in $documentedObj.Assignments)
{
- if(($assignment.target.PSObject.Properties | Where Name -eq "deviceAndAppManagementAssignmentFilterType"))
+ if(($assignment.PSObject.Properties | Where Name -eq "FilterMode"))
{
$isFilterAssignment = $true
break
diff --git a/Extensions/DocumentationMD.psm1 b/Extensions/DocumentationMD.psm1
index 8946f68..e78d485 100644
--- a/Extensions/DocumentationMD.psm1
+++ b/Extensions/DocumentationMD.psm1
@@ -1,6 +1,6 @@
function Get-ModuleVersion
{
- '1.1.0'
+ '1.1.1'
}
function Invoke-InitializeModule
@@ -331,7 +331,7 @@ function Invoke-MDProcessItem
$isFilterAssignment = $false
foreach($assignment in $documentedObj.Assignments)
{
- if(($assignment.target.PSObject.Properties | Where Name -eq "deviceAndAppManagementAssignmentFilterType"))
+ if(($assignment.PSObject.Properties | Where Name -eq "FilterMode"))
{
$isFilterAssignment = $true
break
diff --git a/Extensions/DocumentationWord.psm1 b/Extensions/DocumentationWord.psm1
index cf4f2b2..047b45d 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.5.0'
+ '1.6.0'
}
function Invoke-InitializeModule
@@ -600,7 +600,7 @@ function Invoke-WordProcessItem
$isFilterAssignment = $false
foreach($assignment in $documentedObj.Assignments)
{
- if(($assignment.target.PSObject.Properties | Where Name -eq "deviceAndAppManagementAssignmentFilterType"))
+ if(($assignment.PSObject.Properties | Where Name -eq "FilterMode"))
{
$isFilterAssignment = $true
break
@@ -752,7 +752,7 @@ function Add-DocTableItems
$range = $script:doc.application.selection.range
- $script:docTable = $script:doc.Tables.Add($range, ($items.Count + 1), $properties.Count, [Microsoft.Office.Interop.Word.WdDefaultTableBehavior]::wdWord9TableBehavior, [Microsoft.Office.Interop.Word.WdAutoFitBehavior]::wdAutoFitWindow)
+ $script:docTable = $script:doc.Tables.Add($range, (($items | measure).Count + 1), $properties.Count, [Microsoft.Office.Interop.Word.WdDefaultTableBehavior]::wdWord9TableBehavior, [Microsoft.Office.Interop.Word.WdAutoFitBehavior]::wdAutoFitWindow)
$script:docTable.ApplyStyleHeadingRows = $true
Set-DocObjectStyle $script:docTable $global:txtWordTableStyle.Text | Out-null
diff --git a/Extensions/EndpointManager.psm1 b/Extensions/EndpointManager.psm1
index 3843620..7ef4aad 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.9.2'
+ '3.9.3'
}
function Invoke-InitializeModule
@@ -703,6 +703,7 @@ function Invoke-InitializeModule
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")
@@ -848,6 +849,8 @@ function Invoke-EMSaveSettings
function Invoke-GraphAuthenticationUpdated
{
Set-EMUIStatus
+
+ $script:CustomADMXDefinitions = $null
}
function Set-EMUIStatus
@@ -2023,10 +2026,9 @@ function local:Start-ImportApp
if((Get-SettingValue "EMSaveEncryptionFile") -eq $true)
{
- #$fileEncryptionInfo = $fileEncryptionInfo | where { $null -ne $_.fileEncryptionInfo }
if($fileEncryptionInfo)
{
- $jsonEncryptionInfo = $fileEncryptionInfo.fileEncryptionInfo | ConvertTo-Json -Depth 10
+ $jsonEncryptionInfo = $fileEncryptionInfo | ConvertTo-Json -Depth 10
$pkgPath = Get-SettingValue "EMIntuneAppDownloadFolder" (Get-SettingValue "EMIntuneAppPackages")
if($pkgPath -and [IO.Directory]::Exists($pkgPath))
@@ -2144,11 +2146,11 @@ function Add-DetailExtensionApplications
$dlgSave.FileName = ($obj.FileName + ".encrypted")
if($dlgSave.ShowDialog() -eq [System.Windows.Forms.DialogResult]::OK -and $dlgSave.Filename)
{
- Start-DownloadAppContent $obj $dlgSave.FileName
+ $contentFileObj = Start-DownloadAppContent $obj $dlgSave.FileName
if([IO.File]::Exists($dlgSave.FileName))
{
- $fullPath = $pkgPath + "\$($obj.displayName)_$($obj.id)_$($obj.committedContentVersion).json"
+ $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")
@@ -2170,8 +2172,16 @@ function Add-DetailExtensionApplications
{
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
{
@@ -2188,7 +2198,28 @@ function Add-DetailExtensionApplications
{
$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
@@ -2278,7 +2309,7 @@ function Start-PostExportApplications
Save-Setting "Intune" "ExportAppFile" $global:chkExportApplicationFile.IsChecked
if($global:chkExportApplicationFile.IsChecked)
{
- $encryptioSource = Get-SettingValue "EMIntuneAppDownloadFolder" (Get-SettingValue "EMIntuneAppPackages")
+ $encryptionSource = Get-SettingValue "EMIntuneAppDownloadFolder" (Get-SettingValue "EMIntuneAppPackages")
$pkgPath = $path
if($pkgPath)
@@ -2286,27 +2317,32 @@ function Start-PostExportApplications
Write-Status "Download file"
$exportFile = $pkgPath + "\$($obj.FileName).encrypted"
- $encryptionFile = $encryptioSource + "\$($obj.displayName)_$($obj.id)_$($obj.committedContentVersion).json"
+ $contentFileObj = Start-DownloadAppContent $obj $exportFile -GetContentFileInfoOnly
+ $encryptionFile = Find-AppEncryptionFile $obj $contentFileObj $encryptionSource
if($encryptionFile -and [IO.File]::Exists($encryptionFile))
{
- Start-DownloadAppContent $obj $exportFile
+ 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 "Filed to delete exported encrypted file" $_.Exception
+ Write-LogError "Failed to delete exported encrypted file" $_.Exception
}
}
else
{
- Write-Log "Cound not file encryption file `"$($obj.displayName)_$($obj.id)_$($obj.committedContentVersion).json`""
+ Write-Log "Cound not file encryption file"
}
}
}
@@ -2537,7 +2573,7 @@ function Get-GPOObjectSettings
"definition@odata.bind" = "$($global:graphURL)/deviceManagement/groupPolicyDefinitions('$($definitionValue.definition.id)')"
}
- if($GPOObj.policyConfigurationIngestionType -eq "Custom")
+ if($definitionValue.definition.categoryPath)
{
$obj.Add("#Definition_Id", $definitionValue.definition.id)
$obj.Add("#Definition_displayName", $definitionValue.definition.displayName)
@@ -2555,7 +2591,7 @@ function Get-GPOObjectSettings
# 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($GPOObj.policyConfigurationIngestionType -eq "Custom")
+ 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
@@ -2579,15 +2615,15 @@ function Get-GPOObjectSettings
function Import-GPOSetting
{
- param($obj, $settings, [switch]$CustomADMX)
+ param($obj, $settings)
if($obj)
{
Write-Status "Import settings for $($obj.displayName)"
- $isCustomADMX = $CustomADMX -eq $true
+ $hasCustomADMX = $null -ne ($settings | Where { $null -ne $_.'#Definition_categoryPath' })
- if($isCustomADMX)
+ if($hasCustomADMX)
{
Write-Status "Import custom ADMX settings"
if(-not $script:CustomADMXDefinitions)
@@ -2606,7 +2642,12 @@ function Import-GPOSetting
Category = $tmpCat
Presentations = $null
}
- $script:CustomADMXDefinitions.Add($key, $val)
+ try {
+ $script:CustomADMXDefinitions.Add($key, $val)
+ }
+ catch {
+ Write-Log "Failed to add '$($tmpDef.displayName)' in category '$($tmpDef.categoryPath)' of class $($tmpDef.classType)" 3
+ }
}
}
}
@@ -2615,7 +2656,7 @@ function Import-GPOSetting
foreach($setting in $settings)
{
- if($isCustomADMX -and $script:CustomADMXDefinitions -is [HashTable] -and $script:CustomADMXDefinitions.Count -gt 0)
+ 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()
@@ -2670,7 +2711,7 @@ function Import-GPOSetting
Write-Log "Settings might not be available if imported in another environment" 3
}
}
- elseif($isCustomADMX)
+ elseif($setting.'#Definition_categoryPath')
{
Write-Log "Custom AMDX settings cannot be imported without ADMX file imported. Definitions not found" 2
continue
@@ -2678,7 +2719,7 @@ function Import-GPOSetting
Start-GraphPreImport $setting
- if($true) #$isCustomADMX)
+ if($true)
{
foreach($tmpProp in (($setting.PSObject.Properties | Where Name -like "#*").Name))
{
@@ -2743,7 +2784,7 @@ function Start-PostFileImportAdministrativeTemplate
{
$tmpObj = Get-GraphObjectFromFile $file
- Import-GPOSetting $obj $settings -CustomADMX:($tmpObj.policyConfigurationIngestionType -eq "Custom")
+ Import-GPOSetting $obj $settings
}
}
@@ -3468,6 +3509,8 @@ function Add-EMAssignmentsToExportFile
{
param($obj, $objectType, $path, $Url = "")
+ if($global:chkExportAssignments.IsChecked -ne $true) { return }
+
$fileName = (Get-GraphObjectName $obj $objectType)
if((Get-SettingValue "AddIDToExportFile") -eq $true -and $obj.Id)
{
@@ -3811,6 +3854,13 @@ function Start-PreImportADMXFiles
$obj.defaultLanguageCode = ""
}
+function Start-PostImportADMXFiles
+{
+ param($obj, $objectType, $file)
+
+ $script:CustomADMXDefinitions = $null
+}
+
function Start-PreDeleteADMXFiles
{
param($obj, $objectType)
diff --git a/Extensions/IntuneAppManagement.psm1 b/Extensions/IntuneAppManagement.psm1
index 0d31d8b..7f33087 100644
--- a/Extensions/IntuneAppManagement.psm1
+++ b/Extensions/IntuneAppManagement.psm1
@@ -10,7 +10,7 @@ This module manages Application objects in Intune e.g. uploading application fil
#>
function Get-ModuleVersion
{
- '3.9.2'
+ '3.9.3'
}
#########################################################################################
@@ -718,7 +718,7 @@ function Start-DecryptFile
function Start-DownloadAppContent
{
- param($obj, $destinationFile)
+ param($obj, $destinationFile, [switch]$GetContentFileInfoOnly)
# Not use but kept for reference. File can be download but it will be encrypted
if([IO.File]::Exists($destinationFile))
@@ -756,5 +756,17 @@ function Start-DownloadAppContent
}
}
}
- Start-DownloadFile $contentFile.azureStorageUri $destinationFile
+
+ if($contentFile.azureStorageUri)
+ {
+ if($GetContentFileInfoOnly -ne $true)
+ {
+ Start-DownloadFile $contentFile.azureStorageUri $destinationFile
+ }
+ return $contentFile
+ }
+ else
+ {
+ Write-Log "Could not find file object for app $($obj.displayName) ($($appId))" 2
+ }
}
\ No newline at end of file
diff --git a/Extensions/IntuneFilterUsage.psm1 b/Extensions/IntuneFilterUsage.psm1
new file mode 100644
index 0000000..5336d27
--- /dev/null
+++ b/Extensions/IntuneFilterUsage.psm1
@@ -0,0 +1,329 @@
+<#
+.SYNOPSIS
+Module for listing Intune assignment filter usage
+
+.DESCRIPTION
+
+.NOTES
+ Author: Mikael Karlsson
+#>
+function Get-ModuleVersion
+{
+ '1.0.0'
+}
+
+function Invoke-InitializeModule
+{
+ Add-EMToolsViewItem (New-Object PSObject -Property @{
+ Title = "Intune Filter Usage"
+ Id = "IntuneFilterUsage"
+ ViewID = "EMTools"
+ Permissons=@("DeviceManagementConfiguration.ReadWrite.All")
+ Icon="DeviceConfiguration"
+ ShowViewItem = { Show-IntuneToolsFilterUsage }
+ })
+}
+
+function Show-IntuneToolsFilterUsage
+{
+ if(-not $script:frmIntuneFilterUsage)
+ {
+ $script:frmIntuneFilterUsage = Get-XamlObject ($global:AppRootFolder + "\Xaml\IntuneToolsFiterUsage.xaml") #-AddVariables
+
+ if(-not $script:frmIntuneFilterUsage) { return }
+
+ Add-XamlEvent $script:frmIntuneFilterUsage "btnGetIntuneFilterUsage" "add_click" ({
+ Write-Status "Get Intune Filter Usage"
+ Get-EMIntuneFilterUsage
+ Write-Status ""
+ })
+
+ Add-XamlEvent $script:frmIntuneFilterUsage "btnIntuneFilterUsageCopy" "add_click" ({
+ $dgValues = Get-DataGridValues ($script:frmIntuneFilterUsage.FindName("dgIntuneFilterUsage"))
+ $dgValues | ConvertTo-Csv -NoTypeInformation | Set-Clipboard
+ })
+
+ Add-XamlEvent $script:frmIntuneFilterUsage "btnIntuneFilterUsagesSave" "add_click" ({
+
+ $dlgSave = New-Object -Typename System.Windows.Forms.SaveFileDialog
+ $dlgSave.FileName = $obj.FileName
+ $dlgSave.DefaultExt = "*.csv"
+ $dlgSave.Filter = "CSV (*.csv)|*.csv|All files (*.*)| *.*"
+ if($dlgSave.ShowDialog() -eq [System.Windows.Forms.DialogResult]::OK -and $dlgSave.Filename)
+ {
+ $dgValues = Get-DataGridValues ($script:frmIntuneFilterUsage.FindName("dgIntuneFilterUsage"))
+ $dgValues | ConvertTo-Csv -NoTypeInformation | Out-File -LiteralPath $dlgSave.Filename -Encoding UTF8 -Force
+ }
+ })
+ }
+
+ $global:grdToolsMain.Children.Clear()
+ $global:grdToolsMain.Children.Add($frmIntuneFilterUsage)
+}
+
+function Get-DataGridValues_old
+{
+ param($dataGrid)
+
+ $dgColumns = $dataGrid.Columns
+ #$dgColumns = Get-XamlProperty $script:frmIntuneFilterUsage "dgIntuneFilterUsage" "Columns"
+
+ $properties = @()
+
+ foreach($tmpCol in $dgColumns)
+ {
+ $propName = $tmpCol.Binding.Path.Path
+ $properties += @{n=$tmpCol.Header;e=([Scriptblock]::Create("`$_.$propName"))}
+ }
+
+ ($script:objFilterUsage | Select -Property $properties)
+}
+
+function Get-EMIntuneFilterUsage
+{
+ param($rootDir)
+
+ Write-Status "Gather Intune Filter Information"
+
+ Set-XamlProperty $script:frmIntuneFilterUsage "dgIntuneFilterUsage" "ItemsSource" $null
+
+ $objectType = Get-GraphObjectType "AssignmentFilters"
+
+ $loadedGroups = @{}
+ $loadedGroups.Add("adadadad-808e-44e2-905a-0b7873a8a531","All Devices")
+ $loadedGroups.Add("acacacac-9df4-4c7d-9d50-4ef0226f57a9","All Users")
+
+ $script:objFilters = (Invoke-GraphRequest -Url $objectType.API).Value
+
+ $script:objFilterUsage = @()
+ $groupIDs = @()
+
+ foreach($filter in $script:objFilters)
+ {
+ Write-Status "Get payloads for filter $($filter.displayName)"
+
+ $payloads = (Invoke-GraphRequest -Url "$($objectType.API)/$($filter.ID)/payloads").value
+
+ $batchObjs = @()
+ foreach($payload in $payloads)
+ {
+ $guid = (New-Guid).Guid
+
+ $payloadsObj = @{
+ Payload = $payload
+ ID = $guid
+ Requests = @()
+ }
+
+ if($groupIDs -notcontains $payload.groupId)
+ {
+ $groupIDs += $payload.groupId
+ }
+
+ $batchObjs += $payloadsObj
+
+ if($payload.payloadType -eq "win32app")
+ {
+ $payloadsObj.Requests += [ordered]@{
+ id = "$($guid)_deviceHealthScripts"
+ method = "GET"
+ url = "/deviceManagement/deviceHealthScripts/$($payload.payloadId)/?`$select=displayName,isGlobalScript"
+ headers = @{"x-ms-command-name"="AssignmentFilterPayloadProxy_resolvePayloadNames_BatchItem"}
+ }
+ }
+ elseif($payload.payloadType -eq "application")
+ {
+ $payloadsObj.Requests += [ordered]@{
+ id = "$($guid)_mobileApps"
+ method = "GET"
+ url = "//deviceAppManagement/mobileApps/$($payload.payloadId)/?`$select=displayName"
+ headers = @{"x-ms-command-name"="AssignmentFilterPayloadProxy_resolvePayloadNames_BatchItem"}
+ }
+ }
+ else
+ {
+ $payloadsObj.Requests += [ordered]@{
+ id = "$($guid)_deviceCompliancePolicies"
+ method = "GET"
+ url = "/deviceManagement/deviceCompliancePolicies/$($payload.payloadId)/?`$select=displayName"
+ headers = @{"x-ms-command-name"="AssignmentFilterPayloadProxy_resolvePayloadNames_BatchItem"}
+ }
+
+ $payloadsObj.Requests += [ordered]@{
+ id = "$($guid)_deviceConfigurations"
+ method = "GET"
+ url = "/deviceManagement/deviceConfigurations/$($payload.payloadId)/?`$select=displayName"
+ headers = @{"x-ms-command-name"="AssignmentFilterPayloadProxy_resolvePayloadNames_BatchItem"}
+ }
+
+ $payloadsObj.Requests += [ordered]@{
+ id = "$($guid)_mobileAppConfigurations"
+ method = "GET"
+ url = "/deviceAppManagement/mobileAppConfigurations/$($payload.payloadId)/?`$select=displayName"
+ headers = @{"x-ms-command-name"="AssignmentFilterPayloadProxy_resolvePayloadNames_BatchItem"}
+ }
+ }
+ }
+
+ if($batchObjs.Count -gt 0)
+ {
+ $objName = Get-GraphObjectName $filter $objectType
+ $responses = Invoke-GraphBatchRequest $batchObjs.Requests $objName -SkipWarnings
+ <#
+ $batchObj = [ordered]@{
+ requests = @($batchObjs.Requests)
+ }
+
+ $responses = (Invoke-GraphRequest -Url "`$batch" -Body ($batchObj | ConvertTo-Json -Depth 50 -Compress) -Method "POST").responses
+ #>
+ foreach($response in ($responses | Where Status -eq 200))
+ {
+ $payload = ($batchObjs | Where { $response.id -like "$($_.ID)*"}).Payload
+
+ if($payload.assignmentFilterType -eq "Include")
+ {
+ $filterType = "Include"
+ }
+ else
+ {
+ $filterType = "Exclude"
+ }
+
+ $typeStr = $null
+ if($payload.payloadType -eq "application")
+ {
+ $typeStr = Get-LanguageString "AppType.windowsClassicApp"
+ }
+ elseif($payload.payloadType -eq "win32app")
+ {
+ $typeStr = "Proactive Remediations"
+ }
+ else
+ {
+ $typeStr = (Get-PolicyTypeName $response.body.'@odata.type' $payload.payloadType)
+ }
+
+ if(-not $typeStr) { $typeStr = $payload.payloadType}
+
+ $script:objFilterUsage += [PSCustomObject]@{
+ FiterObject = $filter
+ PayloadObject = $payload
+ FilterName = $filter.displayName
+ PolicyName = $response.body.displayName
+ Type = $response.body.'@odata.type'
+ PayloadType = $typeStr
+ Mode = $filterType
+ GroupID = $payload.groupId
+ GroupName = $payload.groupId
+ }
+ }
+ }
+ }
+
+ if($groupIDs.Count -gt 0)
+ {
+ $guid = (New-Guid).Guid
+ $groupObjs = @()
+ $x = 1
+ foreach($groupID in $groupIDs)
+ {
+ if($loadedGroups.ContainsKey($groupID)) { continue }
+ $groupObjs += [ordered]@{
+ id= "$($guid)_$x"
+ method="GET"
+ url="/groups/$($groupID)/?`$select=displayName,id"
+ headers = @{"x-ms-command-name"="AssignmentFilterPayloadProxy_resolvePayloadGroupAssignments_BatchItem"}
+ }
+ $x++
+ }
+
+ if($groupObjs.Count -gt 0)
+ {
+ $responses = Invoke-GraphBatchRequest $groupObjs "Groups"
+ <#
+ $batchObj = [ordered]@{
+ requests = @($groupObjs)
+ }
+
+ $responses = (Invoke-GraphRequest -Url "`$batch" -Body ($batchObj | ConvertTo-Json -Depth 50 -Compress) -Method "POST").responses
+ #>
+ foreach($response in ($responses | Where Status -eq 200))
+ {
+ if($response.body.displayName -and $response.body.id -and $loadedGroups.ContainsKey($response.body.id) -eq $false)
+ {
+ $loadedGroups.Add($response.body.id, $response.body.displayName)
+ }
+ }
+ }
+
+ foreach($groupID in $loadedGroups.Keys)
+ {
+ $filterObj = $script:objFilterUsage | WHere GroupID -eq $groupID
+ if($filterObj -and $loadedGroups[$groupID])
+ {
+ $filterObj.GroupName = $loadedGroups[$groupID]
+ }
+ }
+ }
+
+ Add-XamlEvent $script:frmIntuneFilterUsage "txtIntuneFilterUsageFilter" "Add_LostFocus" ({
+ Invoke-IntueFilterUsageBoxChanged $this
+ })
+
+ Add-XamlEvent $script:frmIntuneFilterUsage "txtIntuneFilterUsageFilter" "Add_GotFocus" ({
+ if($this.Tag -eq "1" -and $this.Text -eq "Filter") { $this.Text = "" }
+ Invoke-IntueFilterUsageBoxChanged $this ($script:frmIntuneFilterUsage.FindName("dgIntuneFilterUsage"))
+ })
+
+ Add-XamlEvent $script:frmIntuneFilterUsage "txtIntuneFilterUsageFilter" "Add_TextChanged" ({
+ Invoke-IntueFilterUsageBoxChanged $this ($script:frmIntuneFilterUsage.FindName("dgIntuneFilterUsage"))
+ })
+
+ Invoke-IntueFilterUsageBoxChanged ($script:frmIntuneFilterUsage.FindName("txtIntuneFilterUsageFilter")) ($script:frmIntuneFilterUsage.FindName("dgIntuneFilterUsage"))
+
+ $ocList = [System.Collections.ObjectModel.ObservableCollection[object]]::new(@($script:objFilterUsage))
+
+ Set-XamlProperty $script:frmIntuneFilterUsage "dgIntuneFilterUsage" "ItemsSource" ([System.Windows.Data.CollectionViewSource]::GetDefaultView($ocList))
+}
+
+function Invoke-IntueFilterUsageBoxChanged
+{
+ param($txtBox, $dgObject)
+
+ $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($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 ($item.FilterName -match [regex]::Escape($txtBox.Text) -or $item.PolicyName -match [regex]::Escape($txtBox.Text) -or $item.GroupName -match [regex]::Escape($txtBox.Text) )
+ }
+ }
+ }
+
+ if($dgObject.ItemsSource -is [System.Windows.Data.ListCollectionView] -and $txtBox.IsFocused -eq $true)
+ {
+ # This causes odd behaviour with focus e.g. and item has to be clicked twice to be selected
+ $dgObject.ItemsSource.Filter = $filter
+ #$dgObject.ItemsSource.Refresh()
+ }
+}
diff --git a/Extensions/MSALAuthentication.psm1 b/Extensions/MSALAuthentication.psm1
index d48a956..c6c2c6f 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.2'
+ '3.9.3'
}
$global:msalAuthenticator = $null
@@ -770,7 +770,7 @@ function Get-MSALApp
[void] $appBuilder.WithClientName("CloudAPIPowerShellManagement")
[void] $appBuilder.WithClientVersion($PSVersionTable.PSVersion)
- Add-MSALProxy $appBuilder
+ Add-MSALProxy $appBuilder
# Ceck if correct version...
#$appBuilder.WithMultiCloudSupport($true)
@@ -1148,7 +1148,7 @@ function Connect-MSALUser
#########################################################################################################
try
{
- Write-Log "Get tennant list"
+ Write-Log "Get tenant list"
# Can we reuse the app used for login?
$appBuilder = [Microsoft.Identity.Client.PublicClientApplicationBuilder]::Create($global:appObj.ClientID)
diff --git a/Extensions/MSGraph.psm1 b/Extensions/MSGraph.psm1
index 8b6a273..00e5966 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.9.2'
+ '3.9.3'
}
$global:MSGraphGlobalApps = @(
@@ -278,6 +278,7 @@ function Invoke-GraphAuthenticationUpdated
$global:MigrationTableCacheId = $null
$global:LoadedDependencyObjects = $null
$global:migFileObj = $null
+ $global:AADObjectCache = $null
}
function Invoke-SettingsUpdated
@@ -2719,7 +2720,7 @@ function Add-GroupMigrationObject
}
}
else
- {
+ {
Write-Log "No group found with ID $($groupId). It might be deleted." 2
}
}
@@ -2738,7 +2739,7 @@ function Add-GraphMigrationObject
# Check if object is already processed
$graphObj = Get-GraphMigrationObject $objId
- if(-not $graphObj)
+ if(-not $graphObj -and ($global:AADObjectCache.ContainsKey($objId) -eq $false))
{
# Get object info
$graphObj = Invoke-GraphRequest "$($grapAPI)/$objId" -ODataMetadata "none" -NoError
@@ -2764,7 +2765,8 @@ function Add-GraphMigrationObject
}
else
{
- Write-Log "No $objTypeName found with ID $($groupId). It might be deleted." 2
+ if($global:AADObjectCache.ContainsKey($objId) -eq $false) { $global:AADObjectCache.Add($objId, $null) }
+ Write-Log "No $objTypeName found with ID $($objId). It might be deleted." 2
}
}
@@ -3198,7 +3200,7 @@ function Export-GraphObject
[IO.Directory]::CreateDirectory($exportFolder) | Out-Null
}
- if($chkExportAssignments.IsChecked -ne $true -and $obj.Assignments)
+ if($global:chkExportAssignments.IsChecked -ne $true -and $obj.Assignments)
{
Remove-Property $obj "Assignments"
}
@@ -3436,10 +3438,9 @@ function Get-GraphBatchObjects
{
param($objects, $txtNameFilter)
- $curBatch = 1
$batchResults = @()
$batchArr = @()
- $batchTotal = 0
+ $skipped = 0
$objectType = $null
foreach($obj in $objects)
@@ -3449,7 +3450,7 @@ function Get-GraphBatchObjects
if($objName -and $txtNameFilter -and $objName -notmatch [RegEx]::Escape($txtNameFilter))
{
- $batchTotal++
+ $skipped++
}
else
{
@@ -3460,31 +3461,74 @@ function Get-GraphBatchObjects
url = (Get-GraphObject $obj.Object $obj.ObjectType -GetAPI)
headers = @{"Accept"="application/json;odata.metadata=$ometadata"}
}
- }
+ }
+ }
+
+ if($batchArr.Count -eq 0) { return }
- if($batchArr.Count -eq 20 -or ($batchTotal + $batchArr.Count -eq $objects.Count))
+ $batchResults = (Invoke-GraphBatchRequest $batchArr $objectType.Title).body
+
+ if($batchResults.Count -ne ($objects.Count - $skipped))
+ {
+ Write-Log "Not all batch objects returned. Expected $($objects.Count - $skipped) but only got $($batchResults.Count)"
+ }
+
+ if($objectType -and $batchResults.Count -gt 0)
+ {
+ $batchResultsTmp = $batchResults
+ $batchResults = Add-GraphObjectProperties $batchResultsTmp $objectType -property $objectType.ViewProperties
+
+ $curObj = 1
+ foreach($obj in $batchResults)
+ {
+ if($obj.Object -and $obj.ObjectType.PostGetCommand)
+ {
+ Write-Status "Run PostGetCommand - $((Get-GraphObjectName $obj.Object $obj.ObjectType)) ($($curObj)/$(@($batchResults).Count))" -Force
+ & $obj.ObjectType.PostGetCommand $obj $obj.ObjectType
+ }
+ $curObj++
+ }
+ }
+ $batchResults
+}
+
+function Invoke-GraphBatchRequest
+{
+ param($batchObjects, $batchType, [switch]$SkipWarnings, [switch]$IncludedFailed)
+
+ $batchArr = @()
+ $batchResults = @()
+ $batchTotal = 0
+ $curBatch = 1
+
+ foreach($obj in $batchObjects)
+ {
+ $batchArr += $obj
+
+ if($batchArr.Count -eq 20 -or (($batchTotal + $batchArr.Count) -eq $batchObjects.Count))
{
$batchObj = [PSCustomObject]@{
- requests = $batchArr
- }
+ requests = @($batchArr)
+ }
+
+ Write-Status "Get batch $curBatch $batchType" -Force
- Write-Status "Get batch $curBatch $($obj.ObjectType.Title)" -Force
$batchTotal += $batchArr.Count
$json = $batchObj | ConvertTo-Json -Depth 50
-
$maxRetryCount = 10
$curRetry = 0
do
- {
+ {
$retry = $false
$retryArr = @()
$retryAfter = 0
- $tmpResults = Invoke-GraphRequest -Url "`$batch" -Content $json -HttpMethod "POST" -Batch #-Url $api -property $obj.ObjectType.ViewProperties -objectType $obj.ObjectType -
+ $tmpResults = Invoke-GraphRequest -Url "`$batch" -Body $json -Method "POST"
+
foreach($batchResult in ($tmpResults.responses | Sort -Property Id))
{
- if($batchResult.Status -ne "200" -or -not $batchResult.body)
- {
+ if($batchResult.Status -ge 300 -or -not $batchResult.body)
+ {
$reqObj = $batchObj.requests | where id -eq $batchResult.Id
if($batchResult.Status -eq 429 -and $reqObj)
{
@@ -3500,11 +3544,19 @@ function Get-GraphBatchObjects
}
else
{
- Write-Log "Batch result $($batchResult.Status) for URL $($reqObj.URL). Skipping..." 2
+ if($SkipWarnings -ne $true)
+ {
+ Write-Log "Batch result $($batchResult.Status) for URL $($reqObj.URL). Skipping..." 2
+ }
+
+ if($IncludedFailed -eq $true)
+ {
+ $batchResults += $batchResult
+ }
}
continue
}
- $batchResults += $batchResult.body
+ $batchResults += $batchResult
}
if($retryArr.Count -gt 0)
@@ -3521,7 +3573,7 @@ function Get-GraphBatchObjects
$retry = $true
$tmpBatchObj = [PSCustomObject]@{
requests = $retryArr
- }
+ }
$json = $tmpBatchObj | ConvertTo-Json -Depth 50
Start-Sleep -Seconds $retryAfter
}
@@ -3533,27 +3585,11 @@ function Get-GraphBatchObjects
}
}
- if($batchResults.Count -ne $objects.Count)
+ if($batchResults.Count -ne $batchObjects.Count -and $SkipWarnings -ne $true)
{
- Write-Log "Not all batch objects returned. Expected $($objects.Count) but only got $($batchResults.Count)"
+ Write-Log "Not all batch objects returned. Expected $($batchObjects.Count) but only got $($batchResults.Count)" 2
}
- if($objectType -and $batchResults.Count -gt 0)
- {
- $batchResultsTmp = $batchResults
- $batchResults = Add-GraphObjectProperties $batchResultsTmp $objectType -property $objectType.ViewProperties
-
- $curObj = 1
- foreach($obj in $batchResults)
- {
- if($obj.Object -and $obj.ObjectType.PostGetCommand)
- {
- Write-Status "Get full info - $((Get-GraphObjectName $obj.Object $obj.ObjectType)) ($($curObj)/$(@($batchResults).Count))" -Force
- & $obj.ObjectType.PostGetCommand $obj $obj.ObjectType
- }
- $curObj++
- }
- }
$batchResults
}
diff --git a/ReleaseNotes.md b/ReleaseNotes.md
index 8e00e8f..4f0ae9c 100644
--- a/ReleaseNotes.md
+++ b/ReleaseNotes.md
@@ -1,4 +1,56 @@
# Release Notes
+## 3.9.2 - 2023-12-11
+
+**New features**
+
+- **New tool - Get Assignment Filter usage**
+ - List all policies and assignments with a Filter defined
+ Based on [Issue 141](https://github.com/Micke-K/IntuneManagement/issues/141)
+ **NOTE:** Start the tool from: Views -> Intune Tools -> Intune Filter Usage
+
+- **Batch Export of App Content Encryption Key from Intunewin files**
+ This script can export encryption keys from existing intunewin files
+ Example:
+ Export-EncrytionKeys -RootFolder C:\Intune\Packages -ExportFolder C:\Intune\Download
+ This will export the encryption key information for each .intunewinfiles under C:\Intune\Packages
+ One json file will be created (for each .intunwinfile) in the C:\Intune\Download folder
+ File name will be **<*IntunewinFileBaseName*>_<*UnencryptedFileSize*>.json**
+ Do **NOT** rename this file since the script will search for that file when downloading or exporting App content
+ The script will not require authentication and it will have no knowledge of apps in Intune
+ Filename and unencrypted file size is used as the identifier to match app content in Intune with encryption file
+ **Important notes:**
+ Exported and decrypted .intunewin files are not supported to use for import at the moment.
+ These files are just the "zip" version of the source and can be unzipped with any zip extraction tool
+ The .intunewin file used for import has the "zip" version of the file and an xml with the encryption information +
+ additional file information eg. msi properties, file size etc.
+ Use the exported unencrypted "zip" version to restore the original files. Re-run the packaging tool if it should be re-used as applications content
+
+ Please report any issues or create a discussion if there are any questions
+ Script is located: **<*RootFolder*>\Scripts\Export-EncrytionKeys.ps1**
+
+
+
+**Fixes**
+- **Export**
+ - Fixed issue where Assignments were included in export even if 'Export Assignments' was unchecked
+ Based on [Issue 171](https://github.com/Micke-K/IntuneManagement/issues/171)
+
+- **Documentation**
+ - Fixed issue where filter was not documented on some policies
+ - Fixed issue with Word Output provider if a policy only had one settings
+
+- **Custom ADMX Files**
+ - Fixed bug with migrating custom policies between environments. Cache was not cleared when swapping tenants or imported additional ADMX files
+ - Fixed documentention issue with Administrative template policies in GCC environment. Name and Category was missing
+ Based on [Issue 174](https://github.com/Micke-K/IntuneManagement/issues/174)
+ - Custom ADMX based policies was missing properties when swapping tenant
+ Based on [Issue 124](https://github.com/Micke-K/IntuneManagement/issues/124)
+
+- **Generic**
+ - Fixed logging issues when processing objects with a group that was deleted. ID was not reported
+ - Generic Batch request function created to support other batch requests eg Groups
+
+
## 3.9.2 - 2023-10-17
**New features**
diff --git a/Scripts/Export-ExcryptionKeys.ps1 b/Scripts/Export-ExcryptionKeys.ps1
new file mode 100644
index 0000000..d80a1af
--- /dev/null
+++ b/Scripts/Export-ExcryptionKeys.ps1
@@ -0,0 +1,159 @@
+<#
+ Export encryption keys from .intunewin files.
+ This can be used when downloading intunewin files from Intune.
+
+ This is a prt of the IntuneManage GitHub Repository
+ https://github.com/Micke-K/IntuneManagement/
+ (c) Mikael Karlsson MIT License - https://github.com/Micke-K/IntuneManagement/blob/master/LICENSE
+
+ Exprot file name will be _.json
+ Do NOT rename the exported file. The script will try to find excryption file based on the generated name.
+
+ Encryption information is file specific. If the same .intunewin file is imported in multiple tenants,
+ the same ecryption file can be used to decrypt it when downloading or exporting the app content.
+
+ .Sample
+ Export-EncrytionKeys -RootFolder C:\Intune\Packages -ExportFolder C:\Intune\Download
+ This will search C:\Intune\Packages and all subfolder for .intunewin files and export
+ the encryption keys to the C:\Intune\Download.
+#>
+param(
+ [Alias("RF")]
+ # Root folder where intunewin files are located.
+ $RootFolder,
+ [Alias("EF")]
+ # Folder where encryption files should be exported to
+ # If this is empty, the encryption file will be saved to the same folder as the intunewin file
+ $ExportFolder)
+
+function Export-IntunewinFileObject
+{
+ param($file, $objectName, $toFile)
+
+ try
+ {
+ Add-Type -Assembly System.IO.Compression.FileSystem
+
+ $zip = [IO.Compression.ZipFile]::OpenRead($file)
+
+ $zip.Entries | where { $_.Name -like $objectName } | foreach {
+
+ [System.IO.Compression.ZipFileExtensions]::ExtractToFile($_, $toFile, $true)
+ }
+
+ $zip.Dispose()
+ return $true
+ }
+ catch
+ {
+ Write-Warning "Failed to get info from $file. Error: $($_.Exception.Message)"
+ return $false
+ }
+
+}
+
+function Export-EncryptionKeys
+{
+ param(
+ [Parameter(ValueFromPipeline=$true)]
+ $fileInfo,
+ $exportFolder = $fileInfo.DirectoryName
+ )
+
+ begin
+ {
+ }
+
+ process
+ {
+ if($fileInfo -isnot [IO.FileInfo]) { return }
+
+ if(-not $exportFolder) { $exportFolder = $fileInfo.DirectoryName }
+
+ $tmpFile = [IO.Path]::GetTempFileName()
+
+ if((Export-IntunewinFileObject $fileInfo.FullName "detection.xml" $tmpFile) -ne $true)
+ {
+ return
+ }
+
+ $tmpFI = [IO.FileInfo]$tmpFile
+
+ try
+ {
+ if($tmpFI.Length -eq 0)
+ {
+ throw "Detection.xml not exported"
+ }
+ [xml]$DetectionXML = Get-Content $tmpFile
+ }
+ catch
+ {
+ Write-Warning "Failed to export detection.xml file. Error: $($_.Exception.Message)"
+ return
+ }
+ finally
+ {
+ Remove-Item -Path $tmpFile -Force | Out-Null
+ }
+
+ # Get encryption info from detection.xml and build encryptionInfo object
+
+ $encryptionInfo = @{}
+ $encryptionInfo.encryptionKey = $DetectionXML.ApplicationInfo.EncryptionInfo.EncryptionKey
+ $encryptionInfo.macKey = $DetectionXML.ApplicationInfo.EncryptionInfo.macKey
+ $encryptionInfo.initializationVector = $DetectionXML.ApplicationInfo.EncryptionInfo.initializationVector
+ $encryptionInfo.mac = $DetectionXML.ApplicationInfo.EncryptionInfo.mac
+ $encryptionInfo.profileIdentifier = "ProfileVersion1"
+ $encryptionInfo.fileDigest = $DetectionXML.ApplicationInfo.EncryptionInfo.fileDigest
+ $encryptionInfo.fileDigestAlgorithm = $DetectionXML.ApplicationInfo.EncryptionInfo.fileDigestAlgorithm
+
+ $fileData = @{}
+ $fileData.Name = $DetectionXML.ApplicationInfo.Name
+ $fileData.UnencryptedContentSize = $DetectionXML.ApplicationInfo.UnencryptedContentSize
+ $fileData.SetupFile = $DetectionXML.ApplicationInfo.SetupFile
+
+ $msiInfo = @{}
+ if($DetectionXML.ApplicationInfo.MsiInfo)
+ {
+ $msiInfo.MsiPublisher = $DetectionXML.ApplicationInfo.MsiInfo.MsiPublisher
+ $msiInfo.MsiProductCode = $DetectionXML.ApplicationInfo.MsiInfo.Publisher
+ $msiInfo.MsiProductVersion = $DetectionXML.ApplicationInfo.MsiInfo.MsiProductVersion
+ $msiInfo.MsiPackageCode = $DetectionXML.ApplicationInfo.MsiInfo.MsiPackageCode
+ $msiInfo.MsiUpgradeCode = $DetectionXML.ApplicationInfo.MsiInfo.MsiUpgradeCode
+ $msiInfo.MsiIsMachineInstall = $DetectionXML.ApplicationInfo.MsiInfo.MsiIsMachineInstall
+ $msiInfo.MsiIsUserInstall = $DetectionXML.ApplicationInfo.MsiInfo.MsiIsUserInstall
+ $msiInfo.MsiIncludesServices = $DetectionXML.ApplicationInfo.MsiInfo.MsiIncludesServices
+ $msiInfo.MsiIncludesODBCDataSource = $DetectionXML.ApplicationInfo.MsiInfo.MsiIncludesODBCDataSource
+ $msiInfo.MsiContainsSystemRegistryKeys = $DetectionXML.ApplicationInfo.MsiInfo.MsiContainsSystemRegistryKeys
+ $msiInfo.MsiContainsSystemFolders = $DetectionXML.ApplicationInfo.MsiInfo.MsiContainsSystemFolders
+ }
+ # Create mobileAppContentFile object for the file
+ $fileEncryptionInfo = @{}
+ $fileEncryptionInfo.fileEncryptionInfo = $encryptionInfo
+ $fileEncryptionInfo.fileData = $fileData
+ if($msiInfo.Count -gt 0)
+ {
+ $fileEncryptionInfo.MsiInfo = $msiInfo
+ }
+
+ $json = $fileEncryptionInfo | ConvertTo-Json -Depth 10
+
+ if([IO.Directory]::Exists($exportFolder) -eq $false)
+ {
+ md $exportFolder | Out-Null
+ }
+
+ $fileName = $exportFolder + "\$($fileInfo.BaseName)_$($DetectionXML.ApplicationInfo.UnencryptedContentSize).json"
+
+ Write-Host "Save encryption for $($fileInfo.BaseName) file $fileName"
+ $json | Out-File -FilePath $fileName -Force -Encoding utf8
+ }
+
+ end
+ {
+ }
+
+}
+
+Get-ChildItem -Path $RootFolder -Filter "*.intunewin" -Recurse | Export-EncryptionKeys -exportFolder $ExportFolder
\ No newline at end of file
diff --git a/Xaml/EndpointManagerToolsIntuneAssignments.xaml b/Xaml/EndpointManagerToolsIntuneAssignments.xaml
index 058b8ef..76f2cf4 100644
--- a/Xaml/EndpointManagerToolsIntuneAssignments.xaml
+++ b/Xaml/EndpointManagerToolsIntuneAssignments.xaml
@@ -41,22 +41,13 @@
-
+
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file