Get-ClientIntunePolicyResult.ps1
<#PSScriptInfo .VERSION 1.0.2 .GUID 640fc87a-88f7-4550-b6d5-a8fccffbecf9 .AUTHOR @AndrewZtrhgf .COMPANYNAME .COPYRIGHT .TAGS Intune Report HTML MDM .LICENSEURI .PROJECTURI https://doitpsway.com/get-a-better-intune-policy-report-part-3 .ICONURI .EXTERNALMODULEDEPENDENCIES .REQUIREDSCRIPTS .EXTERNALSCRIPTDEPENDENCIES .RELEASENOTES .PRIVATEDATA #> <# .SYNOPSIS Script for getting gpresult like report but for locally applied Intune policies. For more information check https://doitpsway.com/get-a-better-intune-policy-report-part-3. Result can be PowerShell object or HTML report. .DESCRIPTION Script for getting report of all locally applied Intune policies. The result can be PSObject or HTML report. In comparison to the built-in Intune HTML report, this one contains ALL policies, merged into logic groups with extra information like error, last execution time etc (see below). For more information check my blog post https://doitpsway.com/get-a-better-intune-policy-report-part-3. How my Intune HTML report can look like: https://cdn.hashnode.com/res/hashnode/image/upload/v1636618134545/3A2AR-KrQ.png How built-in Intune HTML report looks like: https://cdn.hashnode.com/res/hashnode/image/upload/v1636617956515/LWhmZrJZN.png .PARAMETER intuneXMLReport (optional) PowerShell object returned by ConvertFrom-MDMDiagReportXML function. .PARAMETER asHTML Switch for returning HTML report instead of PowerShell object. PSWriteHTML module is needed! .PARAMETER HTMLReportPath (optional) Where the HTML report should be stored. Default is "IntunePolicyReport.html" in user profile. .PARAMETER getDataFromIntune Switch for getting additional data (policy names) from Intune itself. Microsoft.Graph.Intune module is needed! .PARAMETER credential Credentials for connecting to Intune. Account that has at least READ permissions has to be used. .PARAMETER showEnrollmentIDs Switch for showing EnrollmentIDs in the result. .PARAMETER showURLs Switch for showing policy/setting URLs in the result. Makes this function a little slower, because every URL is tested that it exists. .EXAMPLE .\Get-ClientIntunePolicyResult.ps1 Will return PowerShell object containing Intune policy processing report data. .EXAMPLE .\Get-ClientIntunePolicyResult.ps1 -showURLs -asHTML Will return HTML page containing Intune policy processing report data. URLs to policies/settings will be included. .EXAMPLE $intuneREADCred = Get-Credential .\Get-ClientIntunePolicyResult.ps1 -showURLs -asHTML -getDataFromIntune -credential $intuneREADCred Will return HTML page containing Intune policy processing report data. URLs to policies/settings will be included same as Intune policies names (if available). .NOTES Author: Ondrej Sebela (ztrhgf@seznam.cz) URL: https://doitpsway.com/get-a-better-intune-policy-report-part-3 #> param ( $intuneXMLReport, [switch] $asHTML, [string] $HTMLReportPath = (Join-Path $env:USERPROFILE "IntunePolicyReport.html"), [switch] $getDataFromIntune, [System.Management.Automation.PSCredential] $credential, [switch] $showEnrollmentIDs, [switch] $showURLs ) #region prepare if ($asHTML) { if (!(Get-Module 'PSWriteHtml') -and (!(Get-Module 'PSWriteHtml' -ListAvailable))) { throw "Module PSWriteHtml is missing. To get it use command: Install-Module PSWriteHtml -Scope CurrentUser" } [Void][System.IO.Directory]::CreateDirectory((Split-Path $HTMLReportPath -Parent)) } if ($getDataFromIntune) { if (!(Get-Module 'Microsoft.Graph.Intune') -and !(Get-Module 'Microsoft.Graph.Intune' -ListAvailable)) { throw "Module 'Microsoft.Graph.Intune' is required. To install it call: Install-Module 'Microsoft.Graph.Intune' -Scope CurrentUser" } if ($credential) { $null = Connect-MSGraph -Credential $credential -ErrorAction Stop # $header = New-GraphAPIAuthHeader -credential $credential -ErrorAction Stop } else { $null = Connect-MSGraph -ErrorAction Stop # $header = New-GraphAPIAuthHeader -ErrorAction Stop } # $PSDefaultParameterValues = @{'Invoke-RestMethod:Headers' = $header } Write-Verbose "Getting Intune data" # filtering by ID is quite same as slow as getting all data # Invoke-MSGraphRequest -Url 'https://graph.microsoft.com/beta/deviceAppManagement/mobileApps?$filter=(id%20eq%20%2756695a77-925a-4df0-be79-24ed039afa86%27)' $intuneRemediationScript = Invoke-MSGraphRequest -Url "https://graph.microsoft.com/beta/deviceManagement/deviceHealthScripts" | Get-MSGraphAllPages $intuneScript = Invoke-MSGraphRequest -Url "https://graph.microsoft.com/beta/deviceManagement/deviceManagementScripts" | Get-MSGraphAllPages $intuneApp = Invoke-MSGraphRequest -Url "https://graph.microsoft.com/beta/deviceAppManagement/mobileApps" | Get-MSGraphAllPages } # get the core Intune data if (!$intuneXMLReport) { $param = @{} if ($showEnrollmentIDs) { $param.showEnrollmentIDs = $true } if ($showURLs) { $param.showURLs = $true } Write-Verbose "Getting local Intune data via ConvertFrom-MDMDiagReportXML" $intuneXMLReport = ConvertFrom-MDMDiagReportXML @param } #endregion prepare #region helper functions if (!(Get-Command 'ConvertFrom-MDMDiagReportXML' -ErrorAction SilentlyContinue)) { function ConvertFrom-MDMDiagReportXML { <# .SYNOPSIS Function for converting Intune XML report generated by MdmDiagnosticsTool.exe to a PowerShell object. .DESCRIPTION Function for converting Intune XML report generated by MdmDiagnosticsTool.exe to a PowerShell object. There is also option to generate HTML report instead. .PARAMETER MDMDiagReport Path to MDMDiagReport.xml. If not specified, new report will be generated and used. .PARAMETER asHTML Switch for outputting results as a HTML page instead of PowerShell object. PSWriteHtml module is required! .PARAMETER HTMLReportPath Path to html file where HTML report should be stored. Default is '<yourUserProfile>\IntuneReport.html'. .PARAMETER showEnrollmentIDs Switch for adding EnrollmentID property i.e. property containing Enrollment ID of given policy. From my point of view its useless :). .PARAMETER showURLs Switch for adding PolicyURL and PolicySettingsURL properties i.e. properties containing URL with Microsoft documentation for given CSP. Make running the function slower! Because I test each URL and shows just existing ones. .EXAMPLE $intuneReport = ConvertFrom-MDMDiagReportXML $intuneReport | Out-GridView Generates new Intune report, converts it into PowerShell object and output it using Out-GridView. .EXAMPLE ConvertFrom-MDMDiagReportXML -asHTML -showURLs Generates new Intune report (policies documentation URL included), converts it into HTML web page and opens it. #> [CmdletBinding()] param ( [ValidateScript( { if ($_ -match "\.xml$") { $true } else { throw "$_ is not a valid path to MDM xml report" } })] [string] $MDMDiagReport, [switch] $asHTML, [ValidateScript( { if ($_ -match "\.html$") { $true } else { throw "$_ is not a valid path to html file. Enter something like 'C:\destination\intune.html'" } })] [string] $HTMLReportPath = (Join-Path $env:USERPROFILE "IntuneReport.html"), [switch] $showEnrollmentIDs, [switch] $showURLs ) if ($asHTML) { # array of results that will be in the end transformed into HTML report $results = @() if (!(Get-Module 'PSWriteHtml') -and (!(Get-Module 'PSWriteHtml' -ListAvailable))) { throw "Module PSWriteHtml is missing. To get it use command: Install-Module PSWriteHtml -Scope CurrentUser" } # create parent directory if not exists [Void][System.IO.Directory]::CreateDirectory((Split-Path $HTMLReportPath -Parent)) } if (!$MDMDiagReport) { ++$reportNotSpecified $MDMDiagReport = "$env:PUBLIC\Documents\MDMDiagnostics\MDMDiagReport.xml" } $MDMDiagReportFolder = Split-Path $MDMDiagReport -Parent # generate XML report if necessary if ($reportNotSpecified) { Write-Verbose "Generating '$MDMDiagReport'..." Start-Process MdmDiagnosticsTool.exe -Wait -ArgumentList "-out `"$MDMDiagReportFolder`"" -NoNewWindow } if (!(Test-Path $MDMDiagReport -PathType Leaf)) { Write-Verbose "'$MDMDiagReport' doesn't exist, generating..." Start-Process MdmDiagnosticsTool.exe -Wait -ArgumentList "-out `"$MDMDiagReportFolder`"" -NoNewWindow } Write-Verbose "Converting '$MDMDiagReport' to XML object" [xml]$xml = Get-Content $MDMDiagReport -Raw -ErrorAction Stop $userEnrollmentID = Get-ScheduledTask -TaskName "*pushlaunch*" -TaskPath "\Microsoft\Windows\EnterpriseMgmt\*" | Select-Object -ExpandProperty TaskPath | Split-Path -Leaf Write-Verbose "Your EnrollmentID is $userEnrollmentID" #region helper functions function Test-URLStatus { param ($URL) try { $response = [System.Net.WebRequest]::Create($URL).GetResponse() $status = $response.StatusCode $response.Close() if ($status -eq 'OK') { return $true } else { return $false } } catch { return $false } } function _translateStatus { param ([int] $statusCode) $statusMessage = "" switch ($statusCode) { '10' { $statusMessage = "Initialized" } '20' { $statusMessage = "Download In Progress" } '25' { $statusMessage = "Pending Download Retry" } '30' { $statusMessage = "Download Failed" } '40' { $statusMessage = "Download Completed" } '48' { $statusMessage = "Pending User Session" } '50' { $statusMessage = "Enforcement In Progress" } '55' { $statusMessage = "Pending Enforcement Retry" } '60' { $statusMessage = "Enforcement Failed" } '70' { $statusMessage = "Enforcement Completed" } default { $statusMessage = $statusCode } } return $statusMessage } #endregion helper functions if ($showURLs) { $clientIsOnline = Test-URLStatus 'https://google.com' } #region enrollments Write-Verbose "Getting Enrollments (MDMEnterpriseDiagnosticsReport.Resources.Enrollment)" $enrollment = $xml.MDMEnterpriseDiagnosticsReport.Resources.Enrollment | % { ConvertFrom-XML $_ } if ($enrollment) { Write-Verbose "Processing Enrollments" $enrollment | % { <# <Resources> <Enrollment> <EnrollmentID>5AFCD0A0-321F-4635-B3EB-2EBD28A0FD9A</EnrollmentID> <Scope> <ResourceTarget>device</ResourceTarget> <Resources> <Type>default</Type> <ResourceName>./device/Vendor/MSFT/DeviceManageability/Provider/WMI_Bridge_Server</ResourceName> <ResourceName>2</ResourceName> <ResourceName>./device/Vendor/MSFT/VPNv2/K_AlwaysOn_VPN</ResourceName> </Resources> </Scope> #> $policy = $_ $enrollmentId = $_.EnrollmentId $policy.Scope | % { $policyScope = $_.ResourceTarget -replace "device", "Device" foreach ($policyAreaName in $_.Resources.ResourceName) { # some policies have just number instead of any name..I don't know what it means so I ignore them if ($policyAreaName -match "^\d+$") { continue } # get rid of MSI installations (I have them with details in separate section) if ($policyAreaName -match "/Vendor/MSFT/EnterpriseDesktopAppManagement/MSI") { continue } # get rid of useless data if ($policyAreaName -match "device/Vendor/MSFT/DeviceManageability/Provider/WMI_Bridge_Server") { continue } Write-Verbose "`nEnrollment '$enrollmentId' applied to '$policyScope' configures resource '$policyAreaName'" #region get policy settings details $settingDetails = $null #TODO zjistit co presne to nastavuje # - policymanager.configsource.policyscope.Area <# <ErrorLog> <Component>ConfigManager</Component> <SubComponent> <Name>BitLocker</Name> <Error>-2147024463</Error> <Metadata1>CmdType_Set</Metadata1> <Metadata2>./Device/Vendor/MSFT/BitLocker/RequireDeviceEncryption</Metadata2> <Time>2021-09-23 07:07:05.463</Time> </SubComponent> #> Write-Verbose "Getting Errors (MDMEnterpriseDiagnosticsReport.Diagnostics.ErrorLog)" # match operator used for metadata2 because for example WIFI networks are saved there as ./Vendor/MSFT/WiFi/Profile/<wifiname> instead of ./Vendor/MSFT/WiFi/Profile foreach ($errorRecord in $xml.MDMEnterpriseDiagnosticsReport.Diagnostics.ErrorLog) { $component = $errorRecord.component $errorRecord.subComponent | % { $subComponent = $_ if ($subComponent.name -eq $policyAreaName -or $subComponent.Metadata2 -match [regex]::Escape($policyAreaName)) { $settingDetails = $subComponent | Select-Object @{n = 'Component'; e = { $component } }, @{n = 'SubComponent'; e = { $subComponent.Name } }, @{n = 'SettingName'; e = { $policyAreaName } }, Error, @{n = 'Time'; e = { Get-Date $subComponent.Time } } break } } } if (!$settingDetails) { # try more "relaxed" search if ($policyAreaName -match "/") { # it is just common setting, try to find it using last part of the policy name $policyAreaNameID = ($policyAreaName -split "/")[-1] Write-Verbose "try to find just ID part ($policyAreaNameID) of the policy name in MDMEnterpriseDiagnosticsReport.Diagnostics.ErrorLog" # I don't search substring of policy name in Metadata2 because there can be multiple similar policies (./user/Vendor/MSFT/VPNv2/VPN_Backup vs ./device/Vendor/MSFT/VPNv2/VPN_Backup) foreach ($errorRecord in $xml.MDMEnterpriseDiagnosticsReport.Diagnostics.ErrorLog) { $component = $errorRecord.component $errorRecord.subComponent | % { $subComponent = $_ if ($subComponent.name -eq $policyAreaNameID) { $settingDetails = $subComponent | Select-Object @{n = 'Component'; e = { $component } }, @{n = 'SubComponent'; e = { $subComponent.Name } }, @{n = 'SettingName'; e = { $policyAreaName } }, Error, @{n = 'Time'; e = { Get-Date $subComponent.Time } } break } } } } else { Write-Verbose "'$policyAreaName' doesn't contains '/'" } if (!$settingDetails) { Write-Verbose "No additional data was found for '$policyAreaName' (it means it was successfully applied)" } } #endregion get policy settings details # get CSP policy URL if available if ($showURLs) { if ($policyAreaName -match "/") { $pName = ($policyAreaName -split "/")[-2] } else { $pName = $policyAreaName } $policyURL = "https://docs.microsoft.com/en-us/windows/client-management/mdm/$pName-csp" # check that URL exists if ($clientIsOnline) { if (!(Test-URLStatus $policyURL)) { # URL doesn't exist if ($policyAreaName -match "/") { # sometimes name of the CSP is not second from the end but third $pName = ($policyAreaName -split "/")[-3] $policyURL = "https://docs.microsoft.com/en-us/windows/client-management/mdm/$pName-csp" if (!(Test-URLStatus $policyURL)) { $policyURL = $null } } else { $policyURL = "https://docs.microsoft.com/en-us/windows/client-management/mdm/policy-csp-$pName" if (!(Test-URLStatus $policyURL)) { $policyURL = $null } } } } } #region return retrieved data $property = [ordered] @{ Scope = $policyScope PolicyName = $policyAreaName SettingName = $policyAreaName SettingDetails = $settingDetails } if ($showEnrollmentIDs) { $property.EnrollmentId = $enrollmentId } if ($showURLs) { $property.PolicyURL = $policyURL } $result = New-Object -TypeName PSObject -Property $property if ($asHTML) { $results += $result } else { $result } #endregion return retrieved data } } } } #endregion enrollments #region policies Write-Verbose "Getting Policies (MDMEnterpriseDiagnosticsReport.PolicyManager.ConfigSource)" $policyManager = $xml.MDMEnterpriseDiagnosticsReport.PolicyManager.ConfigSource | % { ConvertFrom-XML $_ } # filter out useless knobs $policyManager = $policyManager | ? { $_.policyScope.Area.PolicyAreaName -ne 'knobs' } if ($policyManager) { Write-Verbose "Processing Policies" # get policies metadata Write-Verbose "Getting Policies Area metadata (MDMEnterpriseDiagnosticsReport.PolicyManager.AreaMetadata)" $policyAreaNameMetadata = $xml.MDMEnterpriseDiagnosticsReport.PolicyManager.AreaMetadata # get admx policies metadata # there are duplicities, so pick just last one Write-Verbose "Getting Policies ADMX metadata (MDMEnterpriseDiagnosticsReport.PolicyManager.IngestedAdmxPolicyMetadata)" $admxPolicyAreaNameMetadata = $xml.MDMEnterpriseDiagnosticsReport.PolicyManager.IngestedAdmxPolicyMetadata | % { ConvertFrom-XML $_ } Write-Verbose "Getting Policies winning provider (MDMEnterpriseDiagnosticsReport.PolicyManager.CurrentPolicies.CurrentPolicyValues)" $winningProviderPolicyAreaNameMetadata = $xml.MDMEnterpriseDiagnosticsReport.PolicyManager.CurrentPolicies.CurrentPolicyValues | % { $_.psobject.properties | ? { $_.Name -Match "_WinningProvider$" } | Select-Object Name, Value } $policyManager | % { $policy = $_ $enrollmentId = $_.EnrollmentId $policy.policyScope | % { $policyScope = $_.PolicyScope -replace "device", "Device" $_.Area | % { <# <ConfigSource> <EnrollmentId>AB068787-67D2-4F7C-AA87-A9127A87411F</EnrollmentId> <PolicyScope> <PolicyScope>Device</PolicyScope> <Area> <PolicyAreaName>BitLocker</PolicyAreaName> <AllowWarningForOtherDiskEncryption>0</AllowWarningForOtherDiskEncryption> <AllowWarningForOtherDiskEncryption_LastWrite>1</AllowWarningForOtherDiskEncryption_LastWrite> <RequireDeviceEncryption>1</RequireDeviceEncryption> #> $policyAreaName = $_.PolicyAreaName Write-Verbose "`nEnrollment '$enrollmentId' applied to '$policyScope' configures area '$policyAreaName'" $policyAreaSetting = $_ | Select-Object -Property * -ExcludeProperty 'PolicyAreaName', "*_LastWrite" $policyAreaSettingName = $policyAreaSetting | Get-Member -MemberType NoteProperty | Select-Object -ExpandProperty name if ($policyAreaSettingName.count -eq 1 -and $policyAreaSettingName -eq "*") { # bug? when there is just PolicyAreaName and none other object than probably because of exclude $policyAreaSettingName instead of be null returns one empty object '*' $policyAreaSettingName = $null $policyAreaSetting = $null } #region get policy settings details $settingDetails = @() if ($policyAreaSetting) { Write-Verbose "`tIt configures these settings:" # $policyAreaSetting is object, so I have to iterate through its properties foreach ($setting in $policyAreaSetting.PSObject.Properties) { $settingName = $setting.Name $settingValue = $setting.Value # PolicyAreaName property was already picked up so now I will ignore it if ($settingName -eq "PolicyAreaName") { continue } Write-Verbose "`t`t- $settingName ($settingValue)" # makes test of url slow # if ($clientIsOnline) { # if (!(Test-URLStatus $policyDetailsURL)) { # # URL doesn't exist # $policyDetailsURL = $null # } # } if ($showURLs) { if ($policyAreaName -match "~Policy~OneDriveNGSC") { # doesn't have policy csp url $policyDetailsURL = $null } else { $policyDetailsURL = "https://docs.microsoft.com/en-us/windows/client-management/mdm/policy-csp-$policyAreaName#$(($policyAreaName).tolower())-$(($settingName).tolower())" } } # define base object $property = [ordered]@{ "SettingName" = $settingName "Value" = $settingValue "DefaultValue" = $null "PolicyType" = '*unknown*' "RegKey" = '*unknown*' "RegValueName" = '*unknown*' "SourceAdmxFile" = $null "WinningProvider" = $null } if ($showURLs) { $property.PolicyDetailsURL = $policyDetailsURL } $additionalData = $policyAreaNameMetadata | ? PolicyAreaName -EQ $policyAreaName | Select-Object -ExpandProperty PolicyMetadata | ? PolicyName -EQ $settingName | Select-Object PolicyType, Value, RegKeyPathRedirect, RegValueNameRedirect if ($additionalData) { Write-Verbose "Additional data for '$settingName' was found in policyAreaNameMetadata" <# <PolicyMetadata> <PolicyName>RecoveryEnvironmentAuthentication</PolicyName> <Behavior>49</Behavior> <highrange>2</highrange> <lowrange>0</lowrange> <mergealgorithm>3</mergealgorithm> <policytype>4</policytype> <RegKeyPathRedirect>Software\Policies\Microsoft\WinRE</RegKeyPathRedirect> <RegValueNameRedirect>WinREAuthenticationRequirement</RegValueNameRedirect> <value>0</value> </PolicyMetadata> #> $property.DefaultValue = $additionalData.Value $property.PolicyType = $additionalData.PolicyType $property.RegKey = $additionalData.RegKeyPathRedirect $property.RegValueName = $additionalData.RegValueNameRedirect } else { # no additional data was found in policyAreaNameMetadata # trying to get them from admxPolicyAreaNameMetadata <# <IngestedADMXPolicyMetaData> <EnrollmentId>11120759-7CE3-4683-AB59-46C27FF40D35</EnrollmentId> <AreaName> <ADMXIngestedAreaName>OneDriveNGSCv2~Policy~OneDriveNGSC</ADMXIngestedAreaName> <PolicyMetadata> <PolicyName>BlockExternalSync</PolicyName> <SourceAdmxFile>OneDriveNGSCv2</SourceAdmxFile> <Behavior>224</Behavior> <MergeAlgorithm>3</MergeAlgorithm> <RegKeyPathRedirect>SOFTWARE\Policies\Microsoft\OneDrive</RegKeyPathRedirect> <RegValueNameRedirect>BlockExternalSync</RegValueNameRedirect> <PolicyType>1</PolicyType> <AdmxMetadataDevice>30313D0100000000323D000000000000</AdmxMetadataDevice> </PolicyMetadata> #> $additionalData = ($admxPolicyAreaNameMetadata.AreaName | ? { $_.ADMXIngestedAreaName -eq $policyAreaName }).PolicyMetadata | ? { $_.PolicyName -EQ $settingName } | select -First 1 # sometimes there are duplicities in results if ($additionalData) { Write-Verbose "Additional data for '$settingName' was found in admxPolicyAreaNameMetadata" $property.PolicyType = $additionalData.PolicyType $property.RegKey = $additionalData.RegKeyPathRedirect $property.RegValueName = $additionalData.RegValueNameRedirect $property.SourceAdmxFile = $additionalData.SourceAdmxFile } else { Write-Verbose "No additional data found for $settingName" } } $winningProvider = $winningProviderPolicyAreaNameMetadata | ? Name -EQ "$settingName`_WinningProvider" | Select-Object -ExpandProperty Value if ($winningProvider) { if ($winningProvider -eq $userEnrollmentID) { $winningProvider = 'Intune' } $property.WinningProvider = $winningProvider } $settingDetails += New-Object -TypeName PSObject -Property $property } } else { Write-Verbose "`tIt doesn't contain any settings" } #endregion get policy settings details # get CSP policy URL if available if ($showURLs) { if ($policyAreaName -match "/") { $pName = ($policyAreaName -split "/")[-2] } else { $pName = $policyAreaName } $policyURL = "https://docs.microsoft.com/en-us/windows/client-management/mdm/$pName-csp" # check that URL exists if ($clientIsOnline) { if (!(Test-URLStatus $policyURL)) { # URL doesn't exist if ($policyAreaName -match "/") { # sometimes name of the CSP is not second from the end but third $pName = ($policyAreaName -split "/")[-3] $policyURL = "https://docs.microsoft.com/en-us/windows/client-management/mdm/$pName-csp" if (!(Test-URLStatus $policyURL)) { $policyURL = $null } } else { $policyURL = "https://docs.microsoft.com/en-us/windows/client-management/mdm/policy-csp-$pName" if (!(Test-URLStatus $policyURL)) { $policyURL = $null } } } } } #region return retrieved data $property = [ordered] @{ Scope = $policyScope PolicyName = $policyAreaName SettingName = $policyAreaSettingName SettingDetails = $settingDetails } if ($showEnrollmentIDs) { $property.EnrollmentId = $enrollmentId } if ($showURLs) { $property.PolicyURL = $policyURL } $result = New-Object -TypeName PSObject -Property $property if ($asHTML) { $results += $result } else { $result } #endregion return retrieved data } } } } #endregion policies #region installations Write-Verbose "Getting MSI installations (MDMEnterpriseDiagnosticsReport.EnterpriseDesktopAppManagementinfo.MsiInstallations)" $installation = $xml.MDMEnterpriseDiagnosticsReport.EnterpriseDesktopAppManagementinfo.MsiInstallations | % { ConvertFrom-XML $_ } if ($installation) { Write-Verbose "Processing MSI installations" $settingDetails = @() $installation.TargetedUser | % { <# <MsiInstallations> <TargetedUser> <UserSid>S-0-0-00-0000000000-0000000000-000000000-000</UserSid> <Package> <Type>MSI</Type> <Details> <PackageId>{23170F69-40C1-2702-1900-000001000000}</PackageId> <DownloadInstall>Ready</DownloadInstall> <ProductCode>{23170F69-40C1-2702-1900-000001000000}</ProductCode> <ProductVersion>19.00.00.0</ProductVersion> <ActionType>1</ActionType> <Status>70</Status> <JobStatusReport>1</JobStatusReport> <LastError>0</LastError> <BITSJobId></BITSJobId> <DownloadLocation></DownloadLocation> <CurrentDownloadUrlIndex>0</CurrentDownloadUrlIndex> <CurrentDownloadUrl></CurrentDownloadUrl> <FileHash>A7803233EEDB6A4B59B3024CCF9292A6FFFB94507DC998AA67C5B745D197A5DC</FileHash> <CommandLine>ALLUSERS=1</CommandLine> <AssignmentType>1</AssignmentType> <EnforcementTimeout>30</EnforcementTimeout> <EnforcementRetryIndex>0</EnforcementRetryIndex> <EnforcementRetryCount>5</EnforcementRetryCount> <EnforcementRetryInterval>3</EnforcementRetryInterval> <LocURI>./Device/Vendor/MSFT/EnterpriseDesktopAppManagement/MSI/{23170F69-40C1-2702-1900-000001000000}/DownloadInstall</LocURI> <ServerAccountID>11120759-7CE3-4683-FB59-46C27FF40D35</ServerAccountID> </Details> #> $scope = $_.UserSid if ($scope -eq 'S-0-0-00-0000000000-0000000000-000000000-000') { $scope = 'Device' } $type = $_.Package.Type $details = $_.Package.details $details | % { Write-Verbose "`t$($_.PackageId) of type $type" # define base object $property = [ordered]@{ "Scope" = $scope "Type" = $type "Status" = _translateStatus $_.Status "LastError" = $_.LastError "ProductVersion" = $_.ProductVersion "CommandLine" = $_.CommandLine "RetryIndex" = $_.EnforcementRetryIndex "MaxRetryCount" = $_.EnforcementRetryCount "PackageId" = $_.PackageId -replace "{" -replace "}" } $settingDetails += New-Object -TypeName PSObject -Property $property } } #region return retrieved data $property = [ordered] @{ Scope = $null PolicyName = "SoftwareInstallation" # made up! SettingName = $null SettingDetails = $settingDetails } if ($showEnrollmentIDs) { $property.EnrollmentId = $null } if ($showURLs) { $property.PolicyURL = $null } # this property only to have same properties for all returned objects $result = New-Object -TypeName PSObject -Property $property if ($asHTML) { $results += $result } else { $result } #endregion return retrieved data } #endregion installations #region convert results to HTML and output if ($asHTML -and $results) { Write-Verbose "Converting to HTML" # split the results $resultsWithSettings = @() $resultsWithoutSettings = @() $results | % { if ($_.settingDetails) { $resultsWithSettings += $_ } else { $resultsWithoutSettings += $_ } } New-HTML -TitleText "Intune Report" -Online -FilePath $HTMLReportPath -ShowHTML { # it looks better to have headers and content in center New-HTMLTableStyle -TextAlign center New-HTMLSection -HeaderText 'Intune Report' -Direction row -HeaderBackGroundColor Black -HeaderTextColor White -HeaderTextSize 20 { if ($resultsWithoutSettings) { New-HTMLSection -HeaderText "Policies without settings details" -HeaderTextAlignment left -CanCollapse -BackgroundColor DeepSkyBlue -HeaderBackGroundColor DeepSkyBlue -HeaderTextSize 10 -HeaderTextColor EgyptianBlue -Direction row { #region prepare data # exclude some not significant or needed properties # SettingName is empty (or same as PolicyName) # settingDetails is empty $excludeProperty = @('SettingName', 'SettingDetails') if (!$showEnrollmentIDs) { $excludeProperty += 'EnrollmentId' } if (!$showURLs) { $excludeProperty += 'PolicyURL' } $resultsWithoutSettings = $resultsWithoutSettings | Select-Object -Property * -exclude $excludeProperty # sort $resultsWithoutSettings = $resultsWithoutSettings | Sort-Object -Property Scope, PolicyName #endregion prepare data # render policies New-HTMLSection -HeaderText 'Policy' -HeaderBackGroundColor Wedgewood -BackgroundColor White { New-HTMLTable -DataTable $resultsWithoutSettings -WordBreak 'break-all' -DisableInfo -HideButtons -DisablePaging -FixedHeader -FixedFooter } } } if ($resultsWithSettings) { New-HTMLSection -HeaderText "Policies with settings details" -HeaderTextAlignment left -CanCollapse -BackgroundColor DeepSkyBlue -HeaderBackGroundColor DeepSkyBlue -HeaderTextSize 10 -HeaderTextColor EgyptianBlue -Direction row { # sort $resultsWithSettings = $resultsWithSettings | Sort-Object -Property Scope, PolicyName $resultsWithSettings | % { $policy = $_ $policySetting = $_.settingDetails #region prepare data # exclude some not significant or needed properties # SettingName is useless in HTML report from my point of view # settingDetails will be shown in separate table, omit here if ($showEnrollmentIDs) { $excludeProperty = 'SettingName', 'SettingDetails' } else { $excludeProperty = 'SettingName', 'SettingDetails', 'EnrollmentId' } $policy = $policy | Select-Object -Property * -ExcludeProperty $excludeProperty #endregion prepare data New-HTMLSection -HeaderText $policy.PolicyName -HeaderTextAlignment left -CanCollapse -BackgroundColor White -HeaderBackGroundColor White -HeaderTextSize 12 -HeaderTextColor EgyptianBlue { # render main policy New-HTMLSection -HeaderText 'Policy' -HeaderBackGroundColor Wedgewood -BackgroundColor White { New-HTMLTable -DataTable $policy -WordBreak 'break-all' -HideFooter -DisableInfo -HideButtons -DisablePaging -DisableSearch -DisableOrdering } # render policy settings details if ($policySetting) { if (@($policySetting).count -eq 1) { $detailsHTMLTableParam = @{ DisableSearch = $true DisableOrdering = $true } } else { $detailsHTMLTableParam = @{} } New-HTMLSection -HeaderText 'Policy settings' -HeaderBackGroundColor PictonBlue -BackgroundColor White { New-HTMLTable @detailsHTMLTableParam -DataTable $policySetting -WordBreak 'break-all' -AllProperties -FixedHeader -HideFooter -DisableInfo -HideButtons -DisablePaging -WarningAction SilentlyContinue { New-HTMLTableCondition -Name 'WinningProvider' -ComparisonType string -Operator 'ne' -Value 'Intune' -BackgroundColor Red -Color White #-Row New-HTMLTableCondition -Name 'LastError' -ComparisonType number -Operator 'ne' -Value 0 -BackgroundColor Red -Color White # -Row New-HTMLTableCondition -Name 'Error' -ComparisonType number -Operator 'ne' -Value 0 -BackgroundColor Red -Color White # -Row } } } } # hack for getting new line between sections New-HTMLText -Text '.' -Color DeepSkyBlue } } } } # end of main HTML section } } #endregion convert results to HTML and output } } function Get-InstalledSoftware { $RegistryLocation = 'SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\', 'SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\' $HashProperty = @{} $SelectProperty = @('DisplayName', 'DisplayVersion', 'UninstallString') $RegBase = [Microsoft.Win32.RegistryKey]::OpenRemoteBaseKey([Microsoft.Win32.RegistryHive]::LocalMachine, $env:COMPUTERNAME) foreach ($CurrentReg in $RegistryLocation) { if ($RegBase) { $RegBase.OpenSubKey($CurrentReg).GetSubKeyNames() | % { foreach ($CurrentProperty in $SelectProperty) { $HashProperty.$CurrentProperty = ($RegBase.OpenSubKey("$CurrentReg$_")).GetValue($CurrentProperty) } New-Object -TypeName PSCustomObject -Property $HashProperty | Select-Object -Property $SelectProperty | ? { $_.ProgramName -notlike "*Update for Microsoft*" -and $_.ProgramName -notlike "Security Update*" } } } } } function _getIntuneScript { param ([string] $scriptID) $intuneScript | ? id -EQ $scriptID } function _getIntuneApp { param ([string] $appID) $intuneApp | ? id -EQ $appID } function _getRemediationScript { param ([string] $scriptID) $intuneRemediationScript | ? id -EQ $scriptID } #endregion helper functions #region enrich SoftwareInstallation section if ($intuneXMLReport | ? PolicyName -EQ 'SoftwareInstallation') { Write-Verbose "Modifying 'SoftwareInstallation' section" # list of installed MSI applications $installedMSI = Get-InstalledSoftware if ($installedMSI) { $intuneXMLReport = $intuneXMLReport | % { if ($_.PolicyName -EQ 'SoftwareInstallation') { $softwareInstallation = $_ $softwareInstallationSettingDetails = $softwareInstallation.SettingDetails | ? { $_ } | % { $item = $_ $packageId = $item.PackageId Write-Verbose "`tPackageId $packageId" Add-Member -InputObject $item -MemberType NoteProperty -Force -Name DisplayName -Value ($installedMSI | ? UninstallString -Match ([regex]::Escape($packageId)) | select -Last 1 -ExpandProperty DisplayName) #return modified MSI object (put Displayname as a second property) $item | select -Property Scope, DisplayName, Type, Status, LastError, ProductVersion, CommandLine, RetryIndex, MaxRetryCount, PackageId } # save results back to original object $softwareInstallation.SettingDetails = $softwareInstallationSettingDetails # return modified object $softwareInstallation } else { # no change necessary $_ } } } } #endregion enrich SoftwareInstallation section #region Win32App # https://oliverkieselbach.com/2018/10/02/part-3-deep-dive-microsoft-intune-management-extension-win32-apps/ # HKLM\SOFTWARE\Microsoft\IntuneManagementExtension\Apps\ doesn't exists? Write-Verbose "Processing 'Win32App' section" $settingDetails = Get-ChildItem "HKLM:\SOFTWARE\Microsoft\IntuneManagementExtension\Win32Apps" | % { $userAzureObjectID = Split-Path $_.Name -Leaf $userWin32AppRoot = $_.PSPath $win32AppIDList = Get-ChildItem $userWin32AppRoot | select -ExpandProperty PSChildName | % { $_ -replace "_\d+$" } | select -Unique $win32AppIDList | % { $win32AppID = $_ Write-Verbose "`tID $win32AppID" $newestWin32AppRecord = Get-ChildItem $userWin32AppRoot | ? PSChildName -Match ([regex]::escape($win32AppID)) | Sort-Object -Descending -Property PSChildName | select -First 1 $lastUpdatedTimeUtc = Get-ItemPropertyValue $newestWin32AppRecord.PSPath -Name LastUpdatedTimeUtc $complianceStateMessage = Get-ItemPropertyValue "$($newestWin32AppRecord.PSPath)\ComplianceStateMessage" -Name ComplianceStateMessage | ConvertFrom-Json $enforcementStateMessage = Get-ItemPropertyValue "$($newestWin32AppRecord.PSPath)\EnforcementStateMessage" -Name EnforcementStateMessage | ConvertFrom-Json if ($userAzureObjectID -eq '00000000-0000-0000-0000-000000000000') { $scope = 'Device' } else { $scope = $userAzureObjectID } $lastError = $complianceStateMessage.ErrorCode if (!$lastError) { $lastError = 0 } # because of HTML conditional formatting ($null means that cell will have red background) if ($getDataFromIntune) { $property = [ordered]@{ "Scope" = $scope "UserAzureObjectID" = $userAzureObjectID # I want to make it more clear that this is ID of the targeted AzureAD user "DisplayName" = (_getIntuneApp $win32AppID).DisplayName "Id" = $win32AppID "LastUpdatedTimeUtc" = $lastUpdatedTimeUtc # "Status" = $complianceStateMessage.ComplianceState "ProductVersion" = $complianceStateMessage.ProductVersion "LastError" = $lastError } } else { # no 'DisplayName' property $property = [ordered]@{ "Scope" = $scope "UserAzureObjectID" = $userAzureObjectID # I want to make it more clear that this is ID of the targeted AzureAD user "Id" = $win32AppID "LastUpdatedTimeUtc" = $lastUpdatedTimeUtc # "Status" = $complianceStateMessage.ComplianceState "ProductVersion" = $complianceStateMessage.ProductVersion "LastError" = $lastError } } if ($showURLs) { $property.IntuneWin32AppURL = "https://endpoint.microsoft.com/#blade/Microsoft_Intune_Apps/SettingsMenu/0/appId/$win32AppID" } New-Object -TypeName PSObject -Property $property } } if ($settingDetails) { $property = [ordered]@{ "Scope" = $null # scope is specified at the particular items level "PolicyName" = 'SoftwareInstallation Win32App' # my custom made # SettingName = 'Win32App' # my custom made "SettingDetails" = $settingDetails } if ($showURLs) { $property.PolicyURL = "https://endpoint.microsoft.com/#blade/Microsoft_Intune_DeviceSettings/AppsWindowsMenu/windowsApps" } $intuneXMLReport += New-Object -TypeName PSObject -Property $property } #endregion Win32App #region add Scripts section # https://oliverkieselbach.com/2018/02/12/part-2-deep-dive-microsoft-intune-management-extension-powershell-scripts/ Write-Verbose "Processing 'Script' section" $settingDetails = Get-ChildItem "HKLM:\SOFTWARE\Microsoft\IntuneManagementExtension\Policies" | % { $userAzureObjectID = Split-Path $_.Name -Leaf if ($userAzureObjectID -eq '00000000-0000-0000-0000-000000000000') { $userAzureObjectID = $null $scope = 'Device' } else { $scope = $userAzureObjectID } Get-ChildItem $_.PSPath | % { $scriptRegPath = $_.PSPath $scriptID = Split-Path $_.Name -Leaf Write-Verbose "`tID $scriptID" $scriptRegData = Get-ItemProperty $scriptRegPath # get output of the invoked script if ($scriptRegData.ResultDetails) { $resultDetails = $scriptRegData.ResultDetails | ConvertFrom-Json | select -ExpandProperty ExecutionMsg } else { $resultDetails = $null } if ($getDataFromIntune) { $property = [ordered]@{ "Scope" = $scope "UserAzureObjectID" = $userAzureObjectID # I want to make it more clear that this is ID of the targeted AzureAD user "DisplayName" = (_getIntuneScript $scriptID).DisplayName "Id" = $scriptID "Result" = $scriptRegData.Result "ErrorCode" = $scriptRegData.ErrorCode "DownloadAndExecuteCount" = $scriptRegData.DownloadCount "LastUpdatedTimeUtc" = $scriptRegData.LastUpdatedTimeUtc "RunAsAccount" = $scriptRegData.RunAsAccount "ResultDetails" = $resultDetails } } else { # no 'DisplayName' property $property = [ordered]@{ "Scope" = $scope "UserAzureObjectID" = $userAzureObjectID # I want to make it more clear that this is ID of the targeted AzureAD user "Id" = $scriptID "Result" = $scriptRegData.Result "ErrorCode" = $scriptRegData.ErrorCode "DownloadAndExecuteCount" = $scriptRegData.DownloadCount "LastUpdatedTimeUtc" = $scriptRegData.LastUpdatedTimeUtc "RunAsAccount" = $scriptRegData.RunAsAccount "ResultDetails" = $resultDetails } } if ($showURLs) { $property.IntuneScriptURL = "https://endpoint.microsoft.com/#blade/Microsoft_Intune_DeviceSettings/ConfigureWMPolicyMenuBlade/properties/policyId/$scriptID/policyType/0" } New-Object -TypeName PSObject -Property $property } } if ($settingDetails) { $property = [ordered]@{ "Scope" = $null # scope is specified at the particular items level "PolicyName" = 'Script' # my custom made "SettingName" = $null "SettingDetails" = $settingDetails } if ($showURLs) { $property.PolicyURL = "https://endpoint.microsoft.com/#blade/Microsoft_Intune_DeviceSettings/DevicesMenu/powershell" } $intuneXMLReport += New-Object -TypeName PSObject -Property $property } #endregion add Scripts section #region remediation script Write-Verbose "Processing 'Remediation Script' section" $settingDetails = Get-ChildItem "HKLM:\SOFTWARE\Microsoft\IntuneManagementExtension\SideCarPolicies\Scripts\Reports" | % { $userAzureObjectID = Split-Path $_.Name -Leaf $userRemScriptRoot = $_.PSPath # $lastFullReportTimeUTC = Get-ItemPropertyValue $userRemScriptRoot -Name LastFullReportTimeUTC $remScriptIDList = Get-ChildItem $userRemScriptRoot | select -ExpandProperty PSChildName | % { $_ -replace "_\d+$" } | select -Unique $remScriptIDList | % { $remScriptID = $_ Write-Verbose "`tID $remScriptID" $newestRemScriptRecord = Get-ChildItem $userRemScriptRoot | ? PSChildName -Match ([regex]::escape($remScriptID)) | Sort-Object -Descending -Property PSChildName | select -First 1 $result = Get-ItemPropertyValue "$($newestRemScriptRecord.PSPath)\Result" -Name Result | ConvertFrom-Json $lastExecution = Get-ItemPropertyValue "HKLM:\SOFTWARE\Microsoft\IntuneManagementExtension\SideCarPolicies\Scripts\Execution\$userAzureObjectID\$($newestRemScriptRecord.PSChildName)" -Name LastExecution if ($userAzureObjectID -eq '00000000-0000-0000-0000-000000000000') { $scope = 'Device' } else { $scope = $userAzureObjectID } if ($getDataFromIntune) { $property = [ordered]@{ "Scope" = $scope "UserAzureObjectID" = $userAzureObjectID # I want to make it more clear that this is ID of the targeted AzureAD user "DisplayName" = (_getRemediationScript $remScriptID).DisplayName "Id" = $remScriptID "LastError" = $result.ErrorCode "LastExecution" = $lastExecution # LastFullReportTimeUTC = $lastFullReportTimeUTC "InternalVersion" = $result.InternalVersion "PreRemediationDetectScriptOutput" = $result.PreRemediationDetectScriptOutput "PreRemediationDetectScriptError" = $result.PreRemediationDetectScriptError "RemediationScriptErrorDetails" = $result.RemediationScriptErrorDetails "PostRemediationDetectScriptOutput" = $result.PostRemediationDetectScriptOutput "PostRemediationDetectScriptError" = $result.PostRemediationDetectScriptError "RemediationExitCode" = $result.Info.RemediationExitCode "FirstDetectExitCode" = $result.Info.FirstDetectExitCode "LastDetectExitCode" = $result.Info.LastDetectExitCode "ErrorDetails" = $result.Info.ErrorDetails } } else { # no 'DisplayName' property $property = [ordered]@{ "Scope" = $scope "UserAzureObjectID" = $userAzureObjectID # I want to make it more clear that this is ID of the targeted AzureAD user "Id" = $remScriptID "LastError" = $result.ErrorCode "LastExecution" = $lastExecution # LastFullReportTimeUTC = $lastFullReportTimeUTC "InternalVersion" = $result.InternalVersion "PreRemediationDetectScriptOutput" = $result.PreRemediationDetectScriptOutput "PreRemediationDetectScriptError" = $result.PreRemediationDetectScriptError "RemediationScriptErrorDetails" = $result.RemediationScriptErrorDetails "PostRemediationDetectScriptOutput" = $result.PostRemediationDetectScriptOutput "PostRemediationDetectScriptError" = $result.PostRemediationDetectScriptError "RemediationExitCode" = $result.Info.RemediationExitCode "FirstDetectExitCode" = $result.Info.FirstDetectExitCode "LastDetectExitCode" = $result.Info.LastDetectExitCode "ErrorDetails" = $result.Info.ErrorDetails } } New-Object -TypeName PSObject -Property $property } } if ($settingDetails) { $property = [ordered]@{ "Scope" = $null # scope is specified at the particular items level "PolicyName" = 'RemediationScript' # my custom made "SettingName" = $null # my custom made "SettingDetails" = $settingDetails } if ($showURLs) { $property.PolicyURL = "https://endpoint.microsoft.com/#blade/Microsoft_Intune_Enrollment/UXAnalyticsMenu/proactiveRemediations" } $intuneXMLReport += New-Object -TypeName PSObject -Property $property } #endregion remediation script #region output the results (as object or HTML report) if ($asHTML -and $intuneXMLReport) { Write-Verbose "Converting to '$HTMLReportPath'" # split the results $resultsWithSettings = @() $resultsWithoutSettings = @() $intuneXMLReport | % { if ($_.settingDetails) { $resultsWithSettings += $_ } else { $resultsWithoutSettings += $_ } } New-HTML -TitleText "Intune Report" -Online -FilePath $HTMLReportPath -ShowHTML { # it looks better to have headers and content in center New-HTMLTableStyle -TextAlign center New-HTMLSection -HeaderText 'Intune Report' -Direction row -HeaderBackGroundColor Black -HeaderTextColor White -HeaderTextSize 20 { if ($resultsWithoutSettings) { New-HTMLSection -HeaderText "Policies without settings details" -HeaderTextAlignment left -CanCollapse -BackgroundColor DeepSkyBlue -HeaderBackGroundColor DeepSkyBlue -HeaderTextSize 10 -HeaderTextColor EgyptianBlue -Direction row { #region prepare data # exclude some not significant or needed properties # SettingName is empty (or same as PolicyName) # settingDetails is empty $excludeProperty = @('SettingName', 'SettingDetails') if (!$showEnrollmentIDs) { $excludeProperty += 'EnrollmentId' } if (!$showURLs) { $excludeProperty += 'PolicyURL' } $resultsWithoutSettings = $resultsWithoutSettings | Select-Object -Property * -exclude $excludeProperty # sort $resultsWithoutSettings = $resultsWithoutSettings | Sort-Object -Property Scope, PolicyName #endregion prepare data # render policies New-HTMLSection -HeaderText 'Policy' -HeaderBackGroundColor Wedgewood -BackgroundColor White { New-HTMLTable -DataTable $resultsWithoutSettings -WordBreak 'break-all' -DisableInfo -HideButtons -DisablePaging -FixedHeader -FixedFooter } } } if ($resultsWithSettings) { # sort $resultsWithSettings = $resultsWithSettings | Sort-Object -Property Scope, PolicyName New-HTMLSection -HeaderText "Policies with settings details" -HeaderTextAlignment left -CanCollapse -BackgroundColor DeepSkyBlue -HeaderBackGroundColor DeepSkyBlue -HeaderTextSize 10 -HeaderTextColor EgyptianBlue -Direction row { $resultsWithSettings | % { $policy = $_ $policySetting = $_.settingDetails #region prepare data # exclude some not significant or needed properties # SettingName is useless in HTML report from my point of view # settingDetails will be shown in separate table, omit here $excludeProperty = @('SettingName', 'SettingDetails') if (!$showEnrollmentIDs) { $excludeProperty += 'EnrollmentId' } if (!$showURLs) { $excludeProperty += 'PolicyURL' } $policy = $policy | Select-Object -Property * -ExcludeProperty $excludeProperty #endregion prepare data New-HTMLSection -HeaderText $policy.PolicyName -HeaderTextAlignment left -CanCollapse -BackgroundColor White -HeaderBackGroundColor White -HeaderTextSize 12 -HeaderTextColor EgyptianBlue { # render main policy New-HTMLSection -HeaderText 'Policy' -HeaderBackGroundColor Wedgewood -BackgroundColor White { New-HTMLTable -DataTable $policy -WordBreak 'break-all' -HideFooter -DisableInfo -HideButtons -DisablePaging -DisableSearch -DisableOrdering } # render policy settings details if ($policySetting) { if (@($policySetting).count -eq 1) { $detailsHTMLTableParam = @{ DisableSearch = $true DisableOrdering = $true } } else { $detailsHTMLTableParam = @{} } New-HTMLSection -HeaderText 'Policy settings' -HeaderBackGroundColor PictonBlue -BackgroundColor White { New-HTMLTable @detailsHTMLTableParam -DataTable $policySetting -WordBreak 'break-all' -AllProperties -FixedHeader -HideFooter -DisableInfo -HideButtons -DisablePaging -WarningAction SilentlyContinue { New-HTMLTableCondition -Name 'WinningProvider' -ComparisonType string -Operator 'ne' -Value 'Intune' -BackgroundColor Red -Color White #-Row New-HTMLTableCondition -Name 'LastError' -ComparisonType number -Operator 'ne' -Value 0 -BackgroundColor Red -Color White # -Row New-HTMLTableCondition -Name 'Error' -ComparisonType number -Operator 'ne' -Value 0 -BackgroundColor Red -Color White # -Row New-HTMLTableCondition -Name 'ErrorCode' -ComparisonType number -Operator 'ne' -Value 0 -BackgroundColor Red -Color White # -Row } } } } # hack for getting new line between sections New-HTMLText -Text '.' -Color DeepSkyBlue } } } } # end of main HTML section } } else { Write-Verbose "Returning PowerShell object" return $intuneXMLReport } #endregion output the results (as object or HTML report) |