diff --git a/Core.psm1 b/Core.psm1 index 4aaa738..5b1f98c 100644 --- a/Core.psm1 +++ b/Core.psm1 @@ -1112,7 +1112,7 @@ function Get-Folder { $dlgCOFD.InitialDirectory = $path } - if($dlgCOFD.ShowDialog($window) = [Microsoft.WindowsAPICodePack.Dialogs.CommonFileDialogResult]::Ok) + if($dlgCOFD.ShowDialog($window) -eq [Microsoft.WindowsAPICodePack.Dialogs.CommonFileDialogResult]::Ok) { $dlgCofd.FileName } @@ -1410,7 +1410,7 @@ function Remove-Setting try { $temp = Get-Item -LiteralPath $regPath -ErrorAction SilentlyContinue - if(($temp.Property -contains $Key)) + if(($temp -and $temp.Property -contains $Key)) { Remove-ItemProperty -Path $regPath -Name $Key -Force -ErrorAction Stop } @@ -1424,7 +1424,7 @@ function Remove-Setting function Get-Setting { - param($SubPath = "", $Key = "", $defautValue) + param($SubPath = "", $Key = "", $defaultValue) if(-not $key) { @@ -1489,7 +1489,7 @@ function Get-Setting if(-not $val) { - $defautValue + $defaultValue } else { diff --git a/Documentation.md b/Documentation.md index e3c9358..2dbc1ad 100644 --- a/Documentation.md +++ b/Documentation.md @@ -101,6 +101,75 @@ The json files contains a definition of each property to document. This includes The `DocumentationCustom.psm1` file also takes care of custom documentation for some object types e.g. Conditional Access. App Configuration policies etc. These objects are documented via a PowerShell function in the script. +**dataTypes Reference** + +The following `dataType` values are supported in property-based ObjectInfo json files: + +| Value | Name | Description | +|---|---|---| +| `0` | Boolean | Translated via the `booleanActions` field | +| `1` | Base64 | Certificate or binary data decoded to a UTF-8 string; filename used if `filenameEntityKey` is set | +| `2` | Multiline String | Multiline text or XML decoded from Base64 if needed; stored to a payload file if `filenameEntityKey` is set | +| `3` | Image | Binary image; outputs "Image file" when a value is present | +| `4` | Linked Certificate | Certificate resolved via OData navigation link | +| `5` | Complex Options | Iterates child `complexOptions` as a sub-section | +| `6` | Complex Option (sub-property) | Iterates `complexOptions` resolved via `entityKey` sub-property | +| `7` | OMA DateTime | Raw OMA DateTime string | +| `8` | Sub-category | Sets the current sub-category label from `nameResourceKey` | +| `9` | Label / OData Type | Skipped — UI label or `@odata.type` property | +| `10` | Information Box | Skipped — UI-only information box | +| `11` | App Picker | Application picker | +| `12` | Multiline String Array | Array of strings joined with the object separator | +| `13` | Multi Option | Multiple selection; translated via `Invoke-TranslateMultiOption` | +| `14` | Int32 | 32-bit integer | +| `15` | Int64 | 64-bit integer | +| `16` | Option | Single-select option; translated via `Invoke-TranslateOption` | +| `19` | Option (variant) | Single-select option; translated via `Invoke-TranslateOption` | +| `20` | String | Plain string | +| `21` | Table | Sub-table; translated via `Invoke-TranslateTable` | +| `22` | Scale Value | Numeric value combined with a unit from `scaleOptions` (e.g. "4 Years") | +| `99` | Reserved | No-op placeholder | +| `100` | Duration | Custom `Edm.Duration` formatting via `Invoke-TranslateDuration` | +| `101` | Static Label | Language string taken from the `value` field | +| `102` | Culture Name | Language/culture name string | +| `103` | Boolean (hide on false) | Boolean via `booleanActions`; hides child properties when `false` | +| `104` | Multi Option Boolean | Multi-option via boolean; hides children when `false` | +| `105` | Multi Option Boolean (inverted) | Like `104` but treats `false` as the selected state | +| `106` | Language Array | Array of culture/language names joined with the object separator | +| `107` | Static Value | Literal string taken directly from the `value` field | +| `108` | String with Format | String formatted using the `formatStringKey` language template | +| `200` | Multi Option (language key) | Translated string looked up via `entityKey` as a language string ID | + +**booleanActions Reference** + +The `booleanActions` field controls the text shown for `true`/`false` values when `dataType` is `0`, `103`, `104`, or `105`. + +Values `0`–`9` and `107`–`108` only display a custom label when `true`; a `false` value falls through to *Not Configured*. + +| Value | `true` → | `false` → | +|---|---|---| +| `0` | Allow | *(Not Configured)* | +| `1` | Require | *(Not Configured)* | +| `2` | Enable | *(Not Configured)* | +| `3` | Block | *(Not Configured)* | +| `4` | Configured | *(Not Configured)* | +| `5` | Disable | *(Not Configured)* | +| `6` | Limit | *(Not Configured)* | +| `7` | Show | *(Not Configured)* | +| `8` | Hide | *(Not Configured)* | +| `9` | Yes | *(Not Configured)* | +| `100` | Block | Allow | +| `101` | Require | Not Required | +| `102` | Enable | Disable | +| `107` | Show | *(Not Configured)* | +| `108` | Hide | *(Not Configured)* | +| `109` | Yes | No | +| `110` | No | Yes | +| `120` | On | Off | +| `200` | Allow | Block | +| `201` | Not Required | Require | +| `220` | Off | On | + **Language Support** The Settings based objects get their language strings from Graph APIs with a few exceptions. diff --git a/Extensions/Compare.psm1 b/Extensions/Compare.psm1 index dc25900..eac05b3 100644 --- a/Extensions/Compare.psm1 +++ b/Extensions/Compare.psm1 @@ -553,7 +553,7 @@ function Invoke-BulkCompareNamedObjects if($compareObjectsResult.Count -eq 0) { - [System.Windows.MessageBox]::Show("No objects were comparced. Verify name patterns", "Error", "OK", "Error") + [System.Windows.MessageBox]::Show("No objects were compared. Verify name patterns", "Error", "OK", "Error") } elseif($outputType -eq "all") { @@ -966,7 +966,7 @@ function Start-BulkCompareExportIntuneToNamedExportedObjects Write-Status "" if($compareObjectsResult.Count -eq 0) { - [System.Windows.MessageBox]::Show("No objects were comparced. Verify folder and exported files", "Error", "OK", "Error") + [System.Windows.MessageBox]::Show("No objects were compared. Verify folder and exported files", "Error", "OK", "Error") } } @@ -1438,7 +1438,7 @@ function Add-CompareProperty $compare.Match = $null } - Write-Host "Add property $($compare.PropertyName)" + Write-LogDebug "Add property $($compare.PropertyName)" $script:compareProperties += $compare } diff --git a/Extensions/Documentation.psm1 b/Extensions/Documentation.psm1 index 207b772..d47a375 100644 --- a/Extensions/Documentation.psm1 +++ b/Extensions/Documentation.psm1 @@ -1700,7 +1700,7 @@ function Add-IntentSettingObjectToList { $passConstraint = ($dependencyItemObj.RawValue -ne $null -and $dependencyItemObj.RawValue.ToString() -ne "NotConfigured" -and $dependencyItemObj.RawValue.ToString() -ne "False") } - if(-not $passConstraint) { break ]} + if(-not $passConstraint) { break } } if(-not $passConstraint) @@ -4424,7 +4424,7 @@ function local:Invoke-StartDocumentatiom $script:migTable = $null $script:scopeTags = $null - $diSource = $nul + $diSource = $null $global:intentCategories = $null $global:catRecommendedSettings = $null $global:intentCategoryDefs = $null @@ -4486,7 +4486,7 @@ function local:Invoke-StartDocumentatiom $tmpObj | Add-Member Noteproperty -Name "GroupName" -Value (Get-ObjectTypeGroupName $tmpObj.ObjectType) -Force } - if($groupSourceList.Count -eq 0) { contnue } + if($groupSourceList.Count -eq 0) { continue } if($curObjectType.GroupId -eq "EndpointSecurity") { diff --git a/Extensions/DocumentationHTML.psm1 b/Extensions/DocumentationHTML.psm1 index e650b30..86c2979 100644 --- a/Extensions/DocumentationHTML.psm1 +++ b/Extensions/DocumentationHTML.psm1 @@ -74,7 +74,7 @@ function Invoke-HTMLPreProcessItems Write-Log "CSS file not specified. Using default" 2 $HTMLCssFile = $defaultCSSFile } - elseif([IO.File]::Exists($HTMLCssFile) -eq -$false) + elseif([IO.File]::Exists($HTMLCssFile) -eq $false) { Write-Log "CSS file $($HTMLCssFile) not found. Using default" 2 $HTMLCssFile = $defaultCSSFile diff --git a/Extensions/DocumentationJson.psm1 b/Extensions/DocumentationJson.psm1 new file mode 100644 index 0000000..88644f7 --- /dev/null +++ b/Extensions/DocumentationJson.psm1 @@ -0,0 +1,265 @@ +function Get-ModuleVersion +{ + '1.0.0' +} + +function Invoke-InitializeModule +{ + Add-OutputType ([PSCustomObject]@{ + Name = "Json" + Value = "json" + OutputOptions = (Add-JsonOptionsControl) + PreProcess = { Invoke-JsonPreProcessItems @args } + NewObjectGroup = { Invoke-JsonNewObjectGroup @args } + NewObjectType = { Invoke-JsonNewObjectType @args } + Process = { Invoke-JsonProcessItem @args } + PostProcess = { Invoke-JsonPostProcessItems @args } + }) +} + +function Add-JsonOptionsControl +{ + $script:jsonForm = Get-XamlObject ($global:AppRootFolder + "\Xaml\DocumentationJsonOptions.xaml") -AddVariables + + Set-XamlProperty $script:jsonForm "txtJsonFileName" "Text" (Get-Setting "Documentation" "JsonDocumentName" "") + Set-XamlProperty $script:jsonForm "chkJsonOpenDocument" "IsChecked" (Get-Setting "Documentation" "JsonOpenFile" $true) + $outputFileTypes = '[ { "Name": "Single file", "Value": "Full" }, { "Name": "One file per object type", "Value": "ObjectType" } ]' | ConvertFrom-Json + Set-XamlProperty $script:jsonForm "cbJsonOutputFile" "ItemsSource" $outputFileTypes + Set-XamlProperty $script:jsonForm "cbJsonOutputFile" "SelectedValue" (Get-Setting "Documentation" "JsonOutputFileType" "Full") + + Add-XamlEvent $script:jsonForm "browseJsonFileName" "add_click" { + $sf = [System.Windows.Forms.SaveFileDialog]::new() + $sf.DefaultExt = "json" + $sf.Filter = "Json (*.json)|*.json|All files (*.*)|*.*" + if ($sf.ShowDialog() -eq "OK") + { + Set-XamlProperty $script:jsonForm "txtJsonFileName" "Text" $sf.FileName + Save-Setting "Documentation" "JsonDocumentName" $sf.FileName + } + } + + $script:jsonForm +} + +function Invoke-JsonPreProcessItems +{ + $script:jsonAllObjects = [System.Collections.Generic.List[object]]::new() + $script:jsonCurrentTypeObjects = [System.Collections.Generic.List[object]]::new() + $script:jsonCurrentTypeName = $null + + $script:jsonOutputType = Get-XamlProperty $script:jsonForm "cbJsonOutputFile" "SelectedValue" "Full" + + $jsonFileName = Get-XamlProperty $script:jsonForm "txtJsonFileName" "Text" "" + + Save-Setting "Documentation" "JsonDocumentName" $jsonFileName + Save-Setting "Documentation" "JsonOpenFile" (Get-XamlProperty $script:jsonForm "chkJsonOpenDocument" "IsChecked") + Save-Setting "Documentation" "JsonOutputFileType" $script:jsonOutputType + + $script:jsonOutFile = Expand-FileName (?? $jsonFileName "%MyDocuments%\%Organization%-%Date%.json") + $script:jsonDocumentPath = [IO.Path]::GetDirectoryName($script:jsonOutFile) +} + +function Invoke-JsonNewObjectGroup +{ + param($groupId) + # Groups are not used in flat Json output +} + +function Invoke-JsonNewObjectType +{ + param($objectTypeName) + + if ($script:jsonOutputType -eq "ObjectType" -and + $script:jsonCurrentTypeName -and + $script:jsonCurrentTypeObjects.Count -gt 0) + { + Save-JsonTypeFile $script:jsonCurrentTypeName $script:jsonCurrentTypeObjects + } + + $script:jsonCurrentTypeName = $objectTypeName + $script:jsonCurrentTypeObjects = [System.Collections.Generic.List[object]]::new() +} + +function Invoke-JsonProcessItem +{ + param($obj, $objectType, $documentedObj) + + if (-not $documentedObj -or -not $obj -or -not $objectType) { return } + + $objName = Get-GraphObjectName $obj $objectType + + try + { + $jsonObj = [ordered]@{ + objectType = $objectType.Title + name = $objName + } + + # BasicInfo as a flat key/value object + if ($documentedObj.BasicInfo.Count -gt 0) + { + $basicInfo = [ordered]@{} + foreach ($item in $documentedObj.BasicInfo) + { + if ($item.Name) { $basicInfo[$item.Name] = $item.Value } + } + $jsonObj.basicInfo = $basicInfo + } + + # Detailed settings as an array + if ($documentedObj.FilteredSettings.Count -gt 0) + { + $settings = [System.Collections.Generic.List[object]]::new() + foreach ($item in $documentedObj.FilteredSettings) + { + $setting = [ordered]@{ name = $item.Name; value = $item.Value } + if ($item.Category) { $setting.category = $item.Category } + if ($item.SubCategory) { $setting.subCategory = $item.SubCategory } + $settings.Add($setting) + } + $jsonObj.settings = $settings + } + + # Compliance actions + if (($documentedObj.ComplianceActions | Measure-Object).Count -gt 0) + { + $actions = [System.Collections.Generic.List[object]]::new() + foreach ($item in $documentedObj.ComplianceActions) + { + $actions.Add([ordered]@{ + action = $item.Action + schedule = $item.Schedule + messageTemplate = $item.MessageTemplate + emailCC = $item.EmailCC + }) + } + $jsonObj.complianceActions = $actions + } + + # Applicability rules + if (($documentedObj.ApplicabilityRules | Measure-Object).Count -gt 0) + { + $rules = [System.Collections.Generic.List[object]]::new() + foreach ($item in $documentedObj.ApplicabilityRules) + { + $rules.Add([ordered]@{ + rule = $item.Rule + property = $item.Property + value = $item.Value + }) + } + $jsonObj.applicabilityRules = $rules + } + + # Custom tables — each becomes a top-level array keyed by the last segment of the language ID + foreach ($customTable in ($documentedObj.CustomTables | Sort-Object -Property Order)) + { + if (-not $customTable.Values -or ($customTable.Values | Measure-Object).Count -eq 0) { continue } + + $tableKey = if ($customTable.LanguageId) { $customTable.LanguageId.Split('.')[-1] } else { "customTable" } + $tableArr = [System.Collections.Generic.List[object]]::new() + + foreach ($item in $customTable.Values) + { + $tableObj = [ordered]@{} + foreach ($col in $customTable.Columns) + { + $colName = $col.Split('.')[-1] + $tableObj[$colName] = "$($item.$colName)" + } + $tableArr.Add($tableObj) + } + $jsonObj[$tableKey] = $tableArr + } + + # Assignments + if (($documentedObj.Assignments | Measure-Object).Count -gt 0) + { + $assignments = [System.Collections.Generic.List[object]]::new() + $hasRawIntent = $null -ne $documentedObj.Assignments[0].RawIntent + + foreach ($item in $documentedObj.Assignments) + { + if ($hasRawIntent) + { + $assignObj = [ordered]@{ + groupMode = $item.GroupMode + group = $item.Group + } + if ($null -ne $item.Filter) { $assignObj.filter = $item.Filter } + if ($null -ne $item.FilterMode) { $assignObj.filterMode = $item.FilterMode } + if ($item.Settings) + { + $settingsObj = [ordered]@{} + foreach ($key in $item.Settings.Keys) + { + if ($key -in @("Category","RawIntent")) { continue } + $settingsObj[$key] = $item.Settings[$key] + } + $assignObj.settings = $settingsObj + } + } + else + { + $assignObj = [ordered]@{ group = $item.Group } + if ($item.PSObject.Properties.Name -contains "Filter") { $assignObj.filter = $item.Filter } + if ($item.PSObject.Properties.Name -contains "FilterMode") { $assignObj.filterMode = $item.FilterMode } + } + $assignments.Add($assignObj) + } + $jsonObj.assignments = $assignments + } + + # Scripts — included when "Document scripts" is checked in the documentation form + if ($global:chkIncludeScripts.IsChecked -and ($documentedObj.Scripts | Measure-Object).Count -gt 0) + { + $scripts = [System.Collections.Generic.List[object]]::new() + foreach ($scriptItem in $documentedObj.Scripts) + { + if (-not $scriptItem.ScriptContent) { continue } + $scripts.Add([ordered]@{ + caption = $scriptItem.Caption + content = $scriptItem.ScriptContent + }) + } + if ($scripts.Count -gt 0) { $jsonObj.scripts = $scripts } + } + + $script:jsonCurrentTypeObjects.Add($jsonObj) + if ($script:jsonOutputType -ne "ObjectType") { $script:jsonAllObjects.Add($jsonObj) } + } + catch + { + Write-LogError "Failed to process object $objName" $_.Exception + } +} + +function Invoke-JsonPostProcessItems +{ + $openFile = (Get-XamlProperty $script:jsonForm "chkJsonOpenDocument" "IsChecked") -eq $true + + if ($script:jsonOutputType -eq "ObjectType") + { + if ($script:jsonCurrentTypeName -and $script:jsonCurrentTypeObjects.Count -gt 0) + { + Save-JsonTypeFile $script:jsonCurrentTypeName $script:jsonCurrentTypeObjects + } + Write-Log "Json documentation saved to folder: $($script:jsonDocumentPath)" + } + else + { + $jsonContent = ConvertTo-Json -InputObject @($script:jsonAllObjects) -Depth 20 + Save-DocumentationFile $jsonContent $script:jsonOutFile -OpenFile:$openFile + } +} + +function Save-JsonTypeFile +{ + param($typeName, $objects) + + $safeTypeName = Remove-InvalidFileNameChars ($typeName.Replace(" ", "_")) + $typeFileName = [IO.Path]::Combine($script:jsonDocumentPath, "$safeTypeName.json") + $jsonContent = ConvertTo-Json -InputObject @($objects) -Depth 20 + Save-DocumentationFile $jsonContent $typeFileName + Write-Log "Saved $($objects.Count) objects to $typeFileName" +} diff --git a/Extensions/DocumentationMD.psm1 b/Extensions/DocumentationMD.psm1 index 3d44495..bc4c3f2 100644 --- a/Extensions/DocumentationMD.psm1 +++ b/Extensions/DocumentationMD.psm1 @@ -83,7 +83,7 @@ function Invoke-MDPreProcessItems Write-Log "CSS file not specified. Using default" 2 $MDCssFile = $defaultCSSFile } - elseif([IO.File]::Exists($MDCssFile) -eq -$false) + elseif([IO.File]::Exists($MDCssFile) -eq $false) { Write-Log "CSS file $($MDCssFile) not found. Using default" 2 $MDCssFile = $defaultCSSFile diff --git a/Extensions/DocumentationWord.psm1 b/Extensions/DocumentationWord.psm1 index 8323270..a157ac3 100644 --- a/Extensions/DocumentationWord.psm1 +++ b/Extensions/DocumentationWord.psm1 @@ -291,8 +291,8 @@ function Invoke-WordPreProcessItems } $script:wordStyles = @() - $script:doc.Styles | foreach { - $script:wordStyles += [PSCUstomObject]@{ + $script:doc.Styles | foreach { + $script:wordStyles += [PSCustomObject]@{ Name=$_.NameLocal Type=$_.Type Style=$_ diff --git a/Extensions/EndpointManager.psm1 b/Extensions/EndpointManager.psm1 index 382cb1a..75341c2 100644 --- a/Extensions/EndpointManager.psm1 +++ b/Extensions/EndpointManager.psm1 @@ -509,7 +509,7 @@ function Invoke-InitializeModule ImportOrder = 60 Expand="categories,assignments" # ODataMetadata is set to minimal so assignments can't be autodetected ODataMetadata="minimal" # categories property not supported with ODataMetadata full - PostFileImportCommand = { Start-PostFileImportApplication @args } + PostFileImportCommand = { Start-PostFileImportApplication @args } PostCopyCommand = { Start-PostCopyApplication @args } PreUpdateCommand = { Start-PreUpdateApplication @args } PreImportCommand = { Start-PreImportCommandApplication @args } @@ -654,7 +654,7 @@ function Invoke-InitializeModule ViewProperties = @("name","description","Id") Expand="Settings" Icon="DeviceConfiguration" - GroupId = "DeviceConfiguration" + GroupId = "DeviceConfiguration" }) Add-ViewItem (New-Object PSObject -Property @{ @@ -2353,7 +2353,7 @@ function local:Add-AppConfigurationTargets $appName, $appId, $appType = $targetedApp -split "[|][!][|]" if(-not $appName -or -not $appId) { - Write-Log "App Name and Id is missing in string: $appApp" 2 + Write-Log "App Name and Id is missing in string: $targetedApp" 2 continue } $tmpApps = (Invoke-GraphRequest -Url "/deviceAppManagement/mobileApps?`$filter=displayName eq '$appName'").value @@ -2814,6 +2814,8 @@ function Start-PreImportAssignmentsApplications function Start-PreDeleteApplications { + param($obj, $objectType) + if($obj.'@odata.type' -eq "#microsoft.graph.microsoftStoreForBusinessApp") { # Don't delete Microsoft Store for Business Apps @@ -3077,9 +3079,9 @@ function local:Add-ApplicationReferences Write-Log "No $appName application found with version $appVer" 2 continue } - elseif(-not ($tmpApp | measure).Count -gt 1) + elseif(($tmpApp | measure).Count -gt 1) { - Write-Log "Multiple $appName application found with version $appVer" 2 + Write-Log "Multiple $appName applications found with version $appVer" 2 continue } Write-Log "Add $appName ($appVer) to Dependency list" @@ -3098,7 +3100,7 @@ function local:Add-ApplicationReferences $appName, $appVer, $appId, $appType = $suppApp -split "[|][!][|]" if(-not $appName -or -not $appVer) { - Write-Log "Could not get Name and Version from string: $appApp" 2 + Write-Log "Could not get Name and Version from string: $suppApp" 2 continue } $tmpApps = (Invoke-GraphRequest -Url "/deviceAppManagement/mobileApps?`$filter=displayName eq '$appName'").value diff --git a/Extensions/IntuneAppManagement.psm1 b/Extensions/IntuneAppManagement.psm1 index ef4b2fa..2c541f5 100644 --- a/Extensions/IntuneAppManagement.psm1 +++ b/Extensions/IntuneAppManagement.psm1 @@ -790,7 +790,7 @@ function Start-DownloadAppContent { foreach($file in $contentFiles.value) { - if($contentFiles.value[-1].Id -eq $file.id) { continune } + if($contentFiles.value[-1].Id -eq $file.id) { continue } # NOT happy about this. file objects are not always returned in the order of upload. $contentFile = Invoke-GraphRequest -Url "/deviceAppManagement/mobileApps/$appId/$appType/contentVersions/$contentVerId/files/$($file.Id)" -NoError diff --git a/Extensions/IntuneFilterUsage.psm1 b/Extensions/IntuneFilterUsage.psm1 index cea5528..e92e324 100644 --- a/Extensions/IntuneFilterUsage.psm1 +++ b/Extensions/IntuneFilterUsage.psm1 @@ -348,7 +348,7 @@ function Get-EMIntuneFilterUsage foreach($groupID in $loadedGroups.Keys) { - $filterObjs = $script:objFilterUsage | WHere GroupID -eq $groupID + $filterObjs = $script:objFilterUsage | Where GroupID -eq $groupID if($filterObjs -and $loadedGroups[$groupID]) { foreach($filterObj in $filterObjs) { diff --git a/Extensions/MSALAuthentication.psm1 b/Extensions/MSALAuthentication.psm1 index 56acc23..32fc7d2 100644 --- a/Extensions/MSALAuthentication.psm1 +++ b/Extensions/MSALAuthentication.psm1 @@ -1116,7 +1116,6 @@ function Connect-MSALUser } else { - $lastUser = $lastUserId = Get-Setting "" "LastLoggedOnUserId" if($lastUserId) { diff --git a/Extensions/MSGraph.psm1 b/Extensions/MSGraph.psm1 index b8348a6..50c20ad 100644 --- a/Extensions/MSGraph.psm1 +++ b/Extensions/MSGraph.psm1 @@ -586,7 +586,8 @@ function Invoke-GraphRequest do { $ret = Invoke-RestMethod -Uri $Url -Method $HttpMethod @params - if($? -eq $false) + $callSucceeded = $? + if($callSucceeded -eq $false) { throw $global:error[0] } @@ -622,7 +623,6 @@ function Invoke-GraphRequest if($_.Exception.Response.StatusCode -eq 429 -and $retryCount -le $retryMax) { # NOT OK - Should use the date property but could not replicate the issue - $retryCount++ $retryRequest = $true Write-Log "429 - Too many requests received. Wait 5 s before retry" 2 Start-Sleep -Seconds 5 @@ -2244,7 +2244,7 @@ function Start-GraphObjectImport ImportedObject = $importedObj } } - $arrImportedObjects = $importedObj + $arrImportedObjects += $importedObj $importedObjects++ $importedObjectsCurType++ diff --git a/README.md b/README.md index 8124130..2b43bc9 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,8 @@ This PowerShell application is based on the foundation modules CloudAPIPowerShel **Security note:** Since the scripts are not signed, a warning might be display when running it and files might be blocked. The script will unblock all files. This is to avoid issues that it fails to load the MSAL library etc. If there are any security concerns, the PowerShell code can be reviewed and the DLL files can be downloaded manually from Microsoft repositories, see links below. The DLL files *CAN* be removed but MSAL is a pre-requisite for authentication. The script will try to find the DLL in the Az or MSAL.PS module if not found in the script root directory. DLL files are included to reduce dependencies. +**Development release:** Changes will be uploaded to the [Development](https://github.com/Micke-K/IntuneManagement/tree/Development) branch. Changes and fixes will be implement to this branch. Any PRs must be merging thatto this branch. + ## Starting the App Before starting the app: @@ -274,7 +276,8 @@ Start-WithJson.cmd is included as an example on how to start the script with jso * Scripts (PowerShell and Shell scripts, supports download of script) * Terms and Conditions * Terms of Use -* Update Policies +* Windows Update Policies +* Windows Updates Profiles * Co-management Settings [^1]: ADMX file must be located in the export folder or in the folder defined in Settings. Administrative Template policies based on ADMX files must be imported after the ADMX file is imported. diff --git a/ReleaseNotes.md b/ReleaseNotes.md index c3cf92b..5203a93 100644 --- a/ReleaseNotes.md +++ b/ReleaseNotes.md @@ -1,4 +1,52 @@ # Release Notes +## 3.10.3 - 2026-05-11 + +**Fixes** + +- **Import/Export**
+ - Added support for Install/Uninstall scripts on Win32 apps
+ It now supports importing Win32 apps that are configured with Install/Uninstall scripts.
+ **Note:** The Win32 app must be exported with 3.10.1.18 or later since the Install/Uninstall script info is not availble with default json export.
+ - Categories failed to import
+ Based on [Issue 381](https://github.com/Micke-K/IntuneManagement/issues/381)
+ - Orderchanges after export
+ This caused issues when using the export for trackiing changes e.g. upload json to Github.
+ Export changed the order of the properties causing a change in the json.
+ This can be enabled in settings: **Sort Json properties**
+ Based on [Issue 378](https://github.com/Micke-K/IntuneManagement/issues/378)
+ - Importing registry values caused 2016281112 error
+ Import was successful and reigstry value was correct but could caused a remediation error.
+ It looks like CSP formats the XML before applying it, causing it not to match the XML in the policy.
+ Based on [Issue 380](https://github.com/Micke-K/IntuneManagement/issues/380)
+- Added support for Windows Quality Updates (Policies)
+ Based on [Issue 387](https://github.com/Micke-K/IntuneManagement/issues/387)
+ + +- **Documentation**
+ - Added Json output provider
+ - Added support for Insall/Uninstall scriupt for Win32 apps
+ - Added support for Strict Open XML document output
+ Based on [Issue 375](https://github.com/Micke-K/IntuneManagement/issues/375)
+ - Fixed issue with boolean values when documenting App Configuration policies
+ Based on [Issue 383](https://github.com/Micke-K/IntuneManagement/issues/383)
+ - Country names missing when documenting a Named Locations.
+ Based on [Issue 400](https://github.com/Micke-K/IntuneManagement/issues/400)
+ - Uninstall on device removal says Not Configured on Deployemnts
+ Based on [Issue 402](https://github.com/Micke-K/IntuneManagement/issues/402)
+ - iOS Single sign-on app extension config is empty
+ Based on [Issue 403](https://github.com/Micke-K/IntuneManagement/issues/403)
+ - Android App Config is not showing the correct Profile Type in Basic info
+ Based on [Issue 404](https://github.com/Micke-K/IntuneManagement/issues/404)
+ +- **Generic**
+ - Added support for configuring when the cache is cleared
+ Cache is is cleared before a bulk export.
+ If can now be changed in Settings with the **Clear Cache Before Export** and **Clear all objects from cache**.
+ This can reaload all values if doing multiple manual imports but can also cause the imports to take longer.
+ Based on [Issue 306](https://github.com/Micke-K/IntuneManagement/issues/306)
+ - Lots of minor fixes
+ + ## 3.10.2 - 2025-10-14 **Fixes** diff --git a/Scripts/Export-ExcryptionKeys.ps1 b/Scripts/Export-ExcryptionKeys.ps1 index d80a1af..06913bb 100644 --- a/Scripts/Export-ExcryptionKeys.ps1 +++ b/Scripts/Export-ExcryptionKeys.ps1 @@ -117,7 +117,7 @@ function Export-EncryptionKeys if($DetectionXML.ApplicationInfo.MsiInfo) { $msiInfo.MsiPublisher = $DetectionXML.ApplicationInfo.MsiInfo.MsiPublisher - $msiInfo.MsiProductCode = $DetectionXML.ApplicationInfo.MsiInfo.Publisher + $msiInfo.MsiProductCode = $DetectionXML.ApplicationInfo.MsiInfo.MsiProductCode $msiInfo.MsiProductVersion = $DetectionXML.ApplicationInfo.MsiInfo.MsiProductVersion $msiInfo.MsiPackageCode = $DetectionXML.ApplicationInfo.MsiInfo.MsiPackageCode $msiInfo.MsiUpgradeCode = $DetectionXML.ApplicationInfo.MsiInfo.MsiUpgradeCode @@ -141,7 +141,7 @@ function Export-EncryptionKeys if([IO.Directory]::Exists($exportFolder) -eq $false) { - md $exportFolder | Out-Null + New-Item -ItemType Directory -Path $exportFolder -Force | Out-Null } $fileName = $exportFolder + "\$($fileInfo.BaseName)_$($DetectionXML.ApplicationInfo.UnencryptedContentSize).json" diff --git a/Xaml/DocumentationJsonOptions.xaml b/Xaml/DocumentationJsonOptions.xaml index e25b417..ed4e921 100644 --- a/Xaml/DocumentationJsonOptions.xaml +++ b/Xaml/DocumentationJsonOptions.xaml @@ -1,5 +1,5 @@ + HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Name="grdJsonOptions"> @@ -23,22 +23,36 @@ Full path to the name of the output file Default value is %MyDocuments%\%Organization%-%Date%.json - Special folders and environmentvariables can be used + Special folders and environment variables can be used - + - + - - - - + + + + - + - - + + - \ No newline at end of file + + + + + + + + + +