diff --git a/Get-IntuneManagementExtensionDiagnostics.ps1 b/Get-IntuneManagementExtensionDiagnostics.ps1
new file mode 100644
index 0000000..378e677
--- /dev/null
+++ b/Get-IntuneManagementExtensionDiagnostics.ps1
@@ -0,0 +1,5903 @@
+<#PSScriptInfo
+
+.VERSION 3.0
+
+.GUID e0307766-d5a7-4704-a578-5ff1fb315a26
+
+.AUTHOR Petri.Paavola@yodamiitti.fi
+
+.COMPANYNAME Yodamiitti Oy
+
+.COPYRIGHT Petri.Paavola@yodamiitti.fi
+
+.TAGS Intune Windows Autopilot troubleshooting log analyzer
+
+.LICENSEURI
+
+.PROJECTURI https://github.com/petripaavola/Get-IntuneManagementExtensionDiagnostics
+
+.ICONURI
+
+.EXTERNALMODULEDEPENDENCIES
+
+.REQUIREDSCRIPTS
+
+.EXTERNALSCRIPTDEPENDENCIES
+
+.RELEASENOTES
+Version 1.0: Original published version
+Version 1.1: Win32App and WinGetApp Required/Available and Install/Uninstall intent is detected right
+ Win32App Supersedence should be recognized (first uninstall and then install)
+ Win32App failed (un)install process is detected
+ Win32App Download Statistics table added
+ Added export to text files
+Version 2.0: Huge new feature is to create html report
+ Html report is primary reporting and console observed timeline is secondary
+ All future development will be done to html report
+ Console timeline will be available for example for OOBE troubleshooting scenarios
+ Added App detection events to timeline
+ Html report entries support HoverOn ToolTips which include more information
+Version 2.3: Updated script to use Microsoft.Graph.Authentication module to download data from Graph API
+Version 2.4: Fix to process new log files AppWorkload.log
+Version 3.0: Support for Intune Device Preparation.
+ Shows Remediation and PowerShell platform script contents and script output (if available) in (hover on) ToolTip with -Online option
+ Copy any ToolTip data including PowerShell scripts
+ -DoNotDownloadClearTextRemediationScriptsToReport - New option not to download Remediation script in clear text
+#>
+
+<#
+.Synopsis
+ This script analyzes Microsoft Intune Management Extension (IME) log(s) and creates timeline report from found actions.
+
+.DESCRIPTION
+ This script analyzes Microsoft Intune Management Extension (IME) log(s) and creates timeline report from found log events.
+
+ Report is saved to HTML file. Events are also shown in Powershell console window.
+
+ Timeline report includes information about Intune Win32App, WinGetApp, Powershell scripts, Remedation scripts and custom Compliance Policy scripts events. Windows Autopilot ESP phases are also shown on timeline.
+
+ Script also includes really capable Log Viewer UI if scripts is started with parameter -ShowLogViewerUI
+
+ LogViewerUI (Out-GridView) looks a lot like cmtrace.exe tool but it is better because all found log actions are added to log for easier debugging.
+
+ LogViewerUI has good search and filtering capabilities. Try to filter known log entries in Timeline: Add criteria -> ProcessRunTime -> is not empty.
+
+ What really differentiates this LogViewer from other tools is it's capability to convert GUIDs to known names
+ try parameter -ConvertAllKnownGuidsToClearText and you can see for example real application names instead of GUIDs on log events.
+
+ Selecting last line (RELOAD) and OK will reload log file.
+
+ Script can merge multiple log files so especially in LogViewerUI you can see Powershell command outputs from AgentExecutor.log
+
+ Powershell command outputs and errors can be also shown in Timeline view with parameters -ShowStdOutInReport and -ShowErrorsInReport
+ This shows instantly what is possible problem in Powershell scripts.
+
+
+ Possible Microsoft 365 App and MSI Line-of-Business Apps (maybe change to Win32App ;) installations are not seen by this report because they are not installed with Intune Management Agent.
+
+
+ Author:
+ Petri.Paavola@yodamiitti.fi
+ Senior Modern Management Principal
+ Microsoft MVP - Windows and Intune
+
+ 2024-09-17
+
+ https://github.com/petripaavola/Get-IntuneManagementExtensionDiagnostics
+
+.PARAMETER Online
+Download Powershell, Remediation and custom Compliance policy scripts to get displayName to Timeline report
+Install Microsoft Graph module with command: Install-Module -Name Microsoft.Graph.Authentication -Scope CurrentUser
+
+.PARAMETER LogFile
+Specify log file fullpath
+
+.PARAMETER LogFilesFolder
+Specify folder where to check log files. Will show UI where you can select what logs to process
+
+.PARAMETER LogStartDateTime
+Specify date and time to start log entries. For example -
+
+.PARAMETER LogEndDateTime
+Specify date and time to stop log entries
+
+.PARAMETER ShowLogViewerUI
+Shows graphical LogViewerUI where all log events are easily browsed, searched and filtered in graphical UI
+This parameter will always show file selection UI and event selection UI.
+
+.PARAMETER LogViewerUI
+Shows graphical LogViewerUI where all log events are easily browsed, searched and filtered in graphical UI
+This parameter will always show file selection UI and event selection UI.
+
+.PARAMETER AllLogEvents
+Process all found log events.
+Selecting this parameter will disable UI which asks date/time/hour selection for logs (use for silent commands or scripts)
+This is default option (aka silent and no selection UI shown)
+
+.PARAMETER LogEventsSelectionUI
+Selecting this parameter will enable UI which asks date/time/hour selection for logs
+
+.PARAMETER AllLogFiles
+Process all found supported log file(s) automatically. This includes *AgentExecutor*.log, *IntuneManagementExtension*.log and *AppWorkload*.log
+Selecting this parameter will disable UI which asks which log files to process (use for silent commands or scripts)
+This is default option (aka silent and no selection UI shown)
+
+.PARAMETER LogFilesSelectionUI
+Selecting this parameter will enable UI which asks which log files to process
+
+.PARAMETER Today
+Show log entries from today (from midnight)
+
+.PARAMETER ShowAllTimelineEvents
+Shows more entries in report. This option will show starting messages for events which are not shown by default
+
+.PARAMETER ShowStdOutInReport
+Show script StdOut in events. This shows for example what Remediation script will return back to Intune
+
+.PARAMETER ShowErrorsInReport
+This will show found error messages from Powershell scripts. Note that Powershell script may succeed and still have errors shown here.
+
+.PARAMETER ShowErrorsSummary
+Show separate all errors summary after Timeline.
+
+.PARAMETER ConvertAllKnownGuidsToClearText
+This parameter replaces all known GUIDs to cleartext in LogViewerUI. Known GUIDs are Win32Apps and WinGetApps by default.
+With -Online option also Powershell scripts, Proactive Remediation scripts and custom Compliance script will get name shown in UI.
+Often this parameter helps a lot debugging log entries in LogViewerUI
+
+.PARAMETER LongRunningPowershellNotifyThreshold
+Threshold (seconds) after Timeline report will show warning message for long running Powershell scripts. Default value is 180 seconds.
+
+.PARAMETER ExportTextFileName
+Export Timeline information and possible Powershell script error to text file.
+This expects either text filename or fullpath to textfile.
+
+.PARAMETER FindAllLongRunningPowershellScripts
+Poweruser option to try to find all long running Powershell scripts over threshold which default is 180 seconds
+This option could find long running scripts which we don't even have event in our report.
+
+.PARAMETER DoNotOpenReportAutomatically
+Do not open html report file automatically in browser
+
+.PARAMETER DoNotDownloadClearTextRemediationScriptsToReport
+Do not download Remediation scripts in clear text to the report
+This could be used for security measures.
+Note that PowerShell script is always shown in report ToolTip in clear text because clear text PowerShell script is in log files
+
+
+.EXAMPLE
+ .\Get-IntuneManagementExtensionDiagnostics.ps1
+.EXAMPLE
+ .\Get-IntuneManagementExtensionDiagnostics.ps1 -Online
+.EXAMPLE
+ .\Get-IntuneManagementExtensionDiagnostics.ps1 -Online -AllLogEvents -AllLogFiles
+.EXAMPLE
+ .\Get-IntuneManagementExtensionDiagnostics.ps1 -Online -AllLogEvents -ShowAllTimelineEvents
+.EXAMPLE
+ .\Get-IntuneManagementExtensionDiagnostics.ps1 -Online -ShowLogViewerUI
+.EXAMPLE
+ .\Get-IntuneManagementExtensionDiagnostics.ps1 -Online -ShowLogViewerUI -ConvertAllKnownGuidsToClearText
+.EXAMPLE
+ .\Get-IntuneManagementExtensionDiagnostics.ps1 -Online -Today
+.EXAMPLE
+ .\Get-IntuneManagementExtensionDiagnostics.ps1 -Online -LogStartDateTime "10.3.2023 5.00:00" -LogEndDateTime "11.3.2023 23.00:00"
+.EXAMPLE
+ .\Get-IntuneManagementExtensionDiagnostics.ps1 "C:\ProgramData\Microsoft\IntuneManagementExtension\Logs\IntuneManagementExtension.log"
+.EXAMPLE
+ .\Get-IntuneManagementExtensionDiagnostics.ps1 -LogFile "C:\ProgramData\Microsoft\IntuneManagementExtension\Logs\IntuneManagementExtension.log"
+.EXAMPLE
+ .\Get-IntuneManagementExtensionDiagnostics.ps1 -LogFilesFolder "C:\temp\MDMDiagReport"
+.EXAMPLE
+ .\Get-IntuneManagementExtensionDiagnostics.ps1 -Online -ShowStdOutInReport
+.EXAMPLE
+ .\Get-IntuneManagementExtensionDiagnostics.ps1 -Online -ShowErrorsInReport
+.EXAMPLE
+ .\Get-IntuneManagementExtensionDiagnostics.ps1 -Online -ShowErrorsSummary
+.EXAMPLE
+ .\Get-IntuneManagementExtensionDiagnostics.ps1 -Online -ExportTextFileName ExportTextFile.txt
+.EXAMPLE
+ .\Get-IntuneManagementExtensionDiagnostics.ps1 -Online -ExportTextFileName C:\temp\ExportTextFile.txt
+.EXAMPLE
+ .\Get-IntuneManagementExtensionDiagnostics.ps1 -AllLogEntries -AllLogFiles -ExportTextFileName C:\temp\ExportTextFile.txt
+.EXAMPLE
+ Get-ChildItem "C:\ProgramData\Microsoft\IntuneManagementExtension\Logs\IntuneManagementExtension.log" | .\Get-IntuneManagementExtensionDiagnostics.ps1 -AllLogEntries -Online
+.INPUTS
+ Script accepts File object as input. This would be same than specifying Parameter -LogFile
+.OUTPUTS
+ None
+.NOTES
+ You can download current version of this script from PowershellGallery with command
+
+ Save-Script Get-IntuneManagementExtensionDiagnostics -Path ./
+.LINK
+ https://github.com/petripaavola/Get-IntuneManagementExtensionDiagnostics
+#>
+
+[CmdletBinding()]
+Param(
+ [Parameter(Mandatory=$false,
+ HelpMessage = 'Enter Intune IME log file fullpath',
+ ValueFromPipeline=$true,
+ ValueFromPipelineByPropertyName=$true)]
+ [Alias("FullName")]
+ [String]$LogFile = $null,
+ [Parameter(Mandatory=$false,
+ HelpMessage = 'Enter Intune IME log files folder path',
+ ValueFromPipeline=$false,
+ ValueFromPipelineByPropertyName=$false)]
+ [String]$LogFilesFolder = $null,
+ [Parameter(Mandatory=$false)]
+ [Switch]$Online,
+ [Parameter(Mandatory=$false,
+ HelpMessage = 'Enter Start DateTime for log entries (for example "10.3.2023 5:00:00")',
+ ValueFromPipeline=$false,
+ ValueFromPipelineByPropertyName=$false)]
+ $LogStartDateTime = $null,
+ [Parameter(Mandatory=$false,
+ HelpMessage = 'Enter End DateTime for log entries (for example "11.3.2023 23.00:00")',
+ ValueFromPipeline=$false,
+ ValueFromPipelineByPropertyName=$false)]
+ $LogEndDateTime = $null,
+ [Parameter(Mandatory=$false)]
+ [Switch]$ShowLogViewerUI,
+ [Parameter(Mandatory=$false)]
+ [Switch]$LogViewerUI,
+ [Parameter(Mandatory=$false)]
+ [Switch]$AllLogEntries=$true,
+ [Parameter(Mandatory=$false)]
+ [Switch]$LogEntriesSelectionUI,
+ [Parameter(Mandatory=$false)]
+ [Switch]$AllLogFiles=$true,
+ [Parameter(Mandatory=$false)]
+ [Switch]$LogFilesSelectionUI,
+ [Parameter(Mandatory=$false)]
+ [Switch]$Today,
+ [Parameter(Mandatory=$false)]
+ [Switch]$ShowAllTimelineEvents,
+ [Parameter(Mandatory=$false)]
+ [Switch]$ShowStdOutInReport,
+ [Parameter(Mandatory=$false)]
+ [Switch]$ShowErrorsInReport,
+ [Parameter(Mandatory=$false)]
+ [Switch]$ShowErrorsSummary,
+ [Parameter(Mandatory=$false)]
+ [Switch]$ConvertAllKnownGuidsToClearText,
+ [Parameter(Mandatory=$false,
+ HelpMessage = 'Threshold seconds to highlight long running Powershell scripts',
+ ValueFromPipeline=$true,
+ ValueFromPipelineByPropertyName=$true)]
+ [int]$LongRunningPowershellNotifyThreshold = 180,
+ [Parameter(Mandatory=$false,
+ HelpMessage = 'Enter text (.txt) filename to export info to',
+ ValueFromPipeline=$false,
+ ValueFromPipelineByPropertyName=$false)]
+ [String]$ExportTextFileName=$null,
+ [Parameter(Mandatory=$false)]
+ [String]$ExportHTMLReportPath=$null,
+ [Parameter(Mandatory=$false)]
+ [Switch]$FindAllLongRunningPowershellScripts,
+ [Parameter(Mandatory=$false)]
+ [Switch]$DoNotOpenReportAutomatically,
+ [Parameter(Mandatory=$false)]
+ [Switch]$DoNotDownloadClearTextRemediationScriptsToReport
+)
+
+
+$ScriptVersion = "3.0"
+$TimeOutBetweenGraphAPIRequests = 300
+
+
+Write-Host "Get-IntuneManagementExtensionDiagnostics.ps1 $ScriptVersion" -ForegroundColor Cyan
+Write-Host "Author: Petri.Paavola@yodamiitti.fi / Microsoft MVP - Windows and Intune"
+Write-Host ""
+
+
+$ExportHTML=$True
+
+# Make script to not show start selection UIs by default
+# This should be fixed in the code
+# but this was quicker hack to make script silent by default
+
+$AllLogEntries=$true
+$AllLogFiles = $true
+
+if($LogEntriesSelectionUI) {
+ $AllLogEntries=$false
+}
+if($LogFilesSelectionUI) {
+ $AllLogFiles = $false
+}
+
+
+# Show selection UIs with LogViewerUI because we would like to
+# limit as less as events possible to save memory and speed up Out-GridView
+
+# With LogViewerUI show Events selection UI always
+# With LogViewerUI show file selection UI always
+if($ShowLogViewerUI -or $LogViewerUI) {
+ $LogEntriesSelectionUI = $true
+ $LogFilesSelectionUI = $true
+
+ $AllLogEntries=$false
+ $AllLogFiles = $false
+
+ Write-Host "Parameter -ShowLogViewerUI selected. Script will show log file and event selection UIs."
+}
+
+
+
+# Set variables automatically if we are in Windows Autopilot ESP (Enrollment Status Page)
+# Idea is that user can just run the script without checking Parameters first
+if($env:UserName -eq 'defaultUser0') {
+
+ Write-Host "Detected running in Windows Autopilot Enrollment Status Page (ESP)" -ForegroundColor Yellow
+
+ #$LOGFile='C:\ProgramData\Microsoft\IntuneManagementExtension\Logs\IntuneManagementExtension.log'
+
+ # Do not open HTML report to browser
+ $DoNotOpenReportAutomatically = $true
+
+ if((-not $LogFilesFolder) -or (-not $LOGFile)) {
+ Write-Host "Configuring parameters automatically"
+ Write-Host "Selected: All log files from default Intune IME logs folder"
+ Write-Host "Selected: Do not open HTML report automatically"
+ Write-Host
+
+ $LogFilesFolder = 'C:\ProgramData\Microsoft\IntuneManagementExtension\Logs'
+
+ if(-not (Test-Path $LogFilesFolder)) {
+ Write-Host "Log folder does not exist yet: $LogFilesFolder"
+ Write-Host "Try again in a moment..." -ForegroundColor Yellow
+ Write-Host ""
+ Exit 0
+ }
+ }
+
+ # Save Computer Name
+ $ComputerNameForReport = $env:ComputerName
+
+ # Process all found supported log files
+ # This will not show file selection UI
+ $AllLogFiles=$True
+
+ # Process all log entries
+ # This will not show time selection UI
+ $AllLogEntries=$True
+
+ # Show all entries in Timeline
+ # This especially useful if some script or application hangs for a long time
+ # so then you can see start entry for that script or application and you know what is current running Intune deployment
+ $ShowAllTimelineEvents=$True
+
+ # Do not download Remediation scripts in clear text to Report
+ # Reason is that many may not event know that script did also html report
+ # In OOBE usually the console view is enough and html file may be left to the device disk
+ $DoNotDownloadClearTextRemediationScriptsToReport=$True
+
+ if(-not (Test-Path 'C:\ProgramData\Microsoft\IntuneManagementExtension\Logs\IntuneManagementExtension.log')) {
+ Write-Host "Log file does not exist yet: C:\ProgramData\Microsoft\IntuneManagementExtension\Logs\IntuneManagementExtension.log" -ForegroundColor Yellow
+ Write-Host "Try again in a moment..." -ForegroundColor Yellow
+ Write-Host ""
+ Exit 0
+ }
+}
+
+
+# Hashtable with ids and names
+$IdHashtable = @{}
+
+# Save timeline objects to this List
+$observedTimeline = [System.Collections.Generic.List[PSObject]]@()
+
+# Save application download statistics to this list
+$ApplicationDownloadStatistics = [System.Collections.Generic.List[PSObject]]@()
+
+# Save filtered oud applications to to this list
+$ApplicationAssignmentFilterApplied = [System.Collections.Generic.List[PSObject]]@()
+
+# TimeLine entry index
+# This might be used in HTML table for sorting entries
+$Script:observedTimeLineIndexToHTMLTable=0
+
+
+################ Functions ################
+
+# This is aligned with Michael Niehaus's Get-AutopilotDiagnostics script just in case
+# region Functions
+ Function RecordStatusToTimeline {
+ param
+ (
+ [Parameter(Mandatory=$true)] [String] $date,
+ [Parameter(Mandatory=$true)] [String] $status,
+ [Parameter(Mandatory=$false)] [String] $type,
+ [Parameter(Mandatory=$false)] [String] $intent,
+ [Parameter(Mandatory=$false)] [String] $detail,
+ [Parameter(Mandatory=$false)] $seconds,
+ [Parameter(Mandatory=$false)] [String] $logEntry,
+ [Parameter(Mandatory=$false)] [String] $color,
+ [Parameter(Mandatory=$false)] [String] $DetailToolTip
+ )
+
+ $Script:observedTimeLineIndexToHTMLTable++
+
+ # Round seconds to full seconds
+ if($seconds) {
+ $seconds = [math]::Round($seconds)
+ }
+
+ $observedTimeline.add([PSCustomObject]@{
+ 'Index' = $Script:observedTimeLineIndexToHTMLTable
+ 'Date' = $date
+ 'Status' = $status
+ 'Type' = $type
+ 'Intent' = $intent
+ 'Detail' = $detail
+ 'Seconds' = $seconds
+ 'LogEntry' = $logEntry
+ 'Color' = $color
+ 'DetailToolTip' = $DetailToolTip
+
+ })
+ }
+
+ Function Get-AppIntent {
+ Param(
+ $AppId
+ )
+
+ $intent = 'Unknown Intent'
+
+ if($AppId) {
+ if($IdHashtable.ContainsKey($AppId)) {
+ $AppPolicy=$IdHashtable[$AppId]
+
+ if($AppPolicy.Intent) {
+ Switch ($AppPolicy.Intent)
+ {
+ 0 { $intent = 'Not Targeted' }
+ 1 { $intent = 'Available Install' }
+ 3 { $intent = 'Required Install' }
+ 4 { $intent = 'Required Uninstall' }
+ default { $intent = 'Unknown Intent' }
+ }
+ }
+ }
+ }
+
+ return $intent
+ }
+
+ Function Get-AppIntentNameForNumber {
+ Param(
+ $IntentNumber
+ )
+
+ Switch ($IntentNumber)
+ {
+ 0 { $intent = 'Not Targeted' }
+ 1 { $intent = 'Available Install' }
+ 3 { $intent = 'Required Install' }
+ 4 { $intent = 'Required Uninstall' }
+ default { $intent = 'Unknown Intent' }
+ }
+
+ return $intent
+ }
+
+ Function Get-AppDetectionResultForNumber {
+ Param(
+ $DetectionNumber
+ )
+
+ Switch ($DetectionNumber)
+ {
+ 0 { $DetectionState = 'Unknown' }
+ 1 { $DetectionState = 'Detected' }
+ 2 { $DetectionState = 'Not Detected' }
+ 3 { $DetectionState = 'Unknown' }
+ 4 { $DetectionState = 'Unknown' }
+ 5 { $DetectionState = 'Unknown' }
+ default { $DetectionState = 'Unknown' }
+ }
+
+ return $DetectionState
+ }
+
+
+ Function Get-AppName {
+ Param(
+ $AppId
+ )
+
+ $AppName = $null
+
+ if($AppId) {
+ if($IdHashtable.ContainsKey($AppId)) {
+ $AppPolicy=$IdHashtable[$AppId]
+
+ if($AppPolicy.Name) {
+ $AppName = $AppPolicy.Name
+ }
+ }
+ }
+
+ return $AppName
+ }
+
+
+ Function Get-AppType {
+ Param(
+ $AppId
+ )
+
+ $AppType = 'App'
+
+ if($AppId) {
+ if($IdHashtable.ContainsKey($AppId)) {
+ $AppPolicy=$IdHashtable[$AppId]
+
+ if($AppPolicy.InstallerData) {
+ # This should be New Store App
+ $AppType = 'WinGetApp'
+ } else {
+ # This should be Win32App
+ $AppType = 'Win32App'
+ }
+ }
+ }
+
+ return $AppType
+ }
+
+ Function Convert-AppDetectionValuesToHumanReadable {
+ Param(
+ $DetectionRulesObject
+ )
+
+ # Object has DetectionType and DetectionText objects
+ # Object is array of objects
+ <#
+ [
+ {
+ "DetectionType": 2,
+ "DetectionText": {
+ "Path": "C:\\Program Files (x86)\\Foo",
+ "FileOrFolderName": "bar.exe",
+ "Check32BitOn64System": true,
+ "DetectionType": 1,
+ "Operator": 0,
+ "DetectionValue": null
+ }
+ }
+ ]
+ #>
+
+ foreach($DetectionRule in $DetectionRulesObject) {
+
+
+ # Change DetectionText properties values to text
+
+ # DetectionType: Registry
+ if($DetectionRule.DetectionType -eq 0) {
+
+ # Registry Detection Type values
+ # https://learn.microsoft.com/en-us/graph/api/resources/intune-apps-win32lobappregistrydetectiontype?view=graph-rest-beta
+ Switch ($DetectionRule.DetectionText.DetectionType) {
+ 0 { $DetectionRule.DetectionText.DetectionType = 'Not configure' }
+ 1 { $DetectionRule.DetectionText.DetectionType = 'Value exists' }
+ 2 { $DetectionRule.DetectionText.DetectionType = 'Value does not exist' }
+ 3 { $DetectionRule.DetectionText.DetectionType = 'String comparison' }
+ 4 { $DetectionRule.DetectionText.DetectionType = 'Integer comparison' }
+ 5 { $DetectionRule.DetectionText.DetectionType = 'Version comparison' }
+ }
+
+ # Registry Detection Operation values
+ # https://learn.microsoft.com/en-us/graph/api/resources/intune-apps-win32lobappruleoperator?view=graph-rest-beta
+ Switch ($DetectionRule.DetectionText.Operator) {
+ 0 { $DetectionRule.DetectionText.Operator = 'Not configured' }
+ 1 { $DetectionRule.DetectionText.Operator = 'Equals' }
+ 2 { $DetectionRule.DetectionText.Operator = 'Not equal to' }
+ 4 { $DetectionRule.DetectionText.Operator = 'Greater than' }
+ 5 { $DetectionRule.DetectionText.Operator = 'Greater than or equal to' }
+ 8 { $DetectionRule.DetectionText.Operator = 'Less than' }
+ 9 { $DetectionRule.DetectionText.Operator = 'Less than or equal to' }
+ }
+ }
+
+
+ # DetectionType: File
+ if($DetectionRule.DetectionType -eq 2) {
+
+ # File Detection Type values
+ # https://learn.microsoft.com/en-us/graph/api/resources/intune-apps-win32lobappfilesystemdetectiontype?view=graph-rest-beta
+ Switch ($DetectionRule.DetectionText.DetectionType) {
+ 0 { $DetectionRule.DetectionText.DetectionType = 'Not configure' }
+ 1 { $DetectionRule.DetectionText.DetectionType = 'File or folder exists' }
+ 2 { $DetectionRule.DetectionText.DetectionType = 'Date modified' }
+ 3 { $DetectionRule.DetectionText.DetectionType = 'Date created' }
+ 4 { $DetectionRule.DetectionText.DetectionType = 'String (version)' }
+ 5 { $DetectionRule.DetectionText.DetectionType = 'Size in MB' }
+ 6 { $DetectionRule.DetectionText.DetectionType = 'File or folder does not exist' }
+ }
+
+ # File Detection Operator values
+ # https://learn.microsoft.com/en-us/graph/api/resources/intune-apps-win32lobappdetectionoperator?view=graph-rest-beta
+ Switch ($DetectionRule.DetectionText.Operator) {
+ 0 { $DetectionRule.DetectionText.Operator = 'Not configured' }
+ 1 { $DetectionRule.DetectionText.Operator = 'Equals' }
+ 2 { $DetectionRule.DetectionText.Operator = 'Not equal to' }
+ 4 { $DetectionRule.DetectionText.Operator = 'Greater than' }
+ 5 { $DetectionRule.DetectionText.Operator = 'Greater than or equal to' }
+ 8 { $DetectionRule.DetectionText.Operator = 'Less than' }
+ 9 { $DetectionRule.DetectionText.Operator = 'Less than or equal to' }
+ }
+ }
+
+
+ # DetectionType: Custom script
+ if($DetectionRule.DetectionType -eq 3) {
+
+ # Convert base64 script to clear text
+ #$DetectionRule.DetectionText.ScriptBody
+
+ # Decode Base64 content
+ $b = [System.Convert]::FromBase64String("$($DetectionRule.DetectionText.ScriptBody)")
+ $DetectionRule.DetectionText.ScriptBody = [System.Text.Encoding]::UTF8.GetString($b)
+
+ }
+
+
+ <#
+ # Change DetectionType value to text
+ Switch ($DetectionRule.DetectionType) {
+ 0 { $DetectionRule.DetectionType = 'Registry' }
+ 1 { $DetectionRule.DetectionType = 'MSI' }
+ 2 { $DetectionRule.DetectionType = 'File' }
+ 3 { $DetectionRule.DetectionType = 'Custom script' }
+ default { $DetectionRule.DetectionType = $DetectionRule.DetectionType }
+ }
+ #>
+
+ # Add new property with DetectionType value as text
+ Switch ($DetectionRule.DetectionType) {
+ 0 { $DetectionRule | Add-Member -MemberType noteProperty -Name DetectionTypeAsText -Value 'Registry' }
+ 1 { $DetectionRule | Add-Member -MemberType noteProperty -Name DetectionTypeAsText -Value 'MSI' }
+ 2 { $DetectionRule | Add-Member -MemberType noteProperty -Name DetectionTypeAsText -Value 'File' }
+ 3 { $DetectionRule | Add-Member -MemberType noteProperty -Name DetectionTypeAsText -Value 'Custom script' }
+ default { $DetectionRule | Add-Member -MemberType noteProperty -Name DetectionTypeAsText -Value $DetectionRule.DetectionType }
+ }
+
+ }
+
+ return $DetectionRulesObject
+ }
+
+
+ function Invoke-MgGraphRequestGetAllPages {
+ param (
+ [Parameter(Mandatory = $true)]
+ [String]$uri
+ )
+
+ $MgGraphRequest = $null
+ $AllMSGraphRequest = $null
+
+ Start-Sleep -Milliseconds $TimeOutBetweenGraphAPIRequests
+
+ try {
+
+ # Save results to this variable
+ $allGraphAPIData = @()
+
+ do {
+
+ $MgGraphRequest = $null
+ $MgGraphRequest = Invoke-MgGraphRequest -Uri $uri -Method 'Get' -OutputType PSObject -ContentType "application/json"
+
+ if($MgGraphRequest) {
+
+ # Test if object has attribute named Value (whether value is null or not)
+ #if((Get-Member -inputobject $MgGraphRequest -name 'Value' -Membertype Properties) -and (Get-Member -inputobject $MgGraphRequest -name '@odata.context' -Membertype Properties)) {
+ if(Get-Member -inputobject $MgGraphRequest -name 'Value' -Membertype Properties) {
+ # Value property exists
+ $allGraphAPIData += $MgGraphRequest.Value
+
+ # Check if we have value starting https:// in attribute @odate.nextLink
+ # and check that $Top= parameter was NOT used. With $Top= parameter we can limit search results
+ # but that almost always results .nextLink being present if there is more data than specified with top
+ # If we specified $Top= ourselves then we don't want to fetch nextLink values
+ #
+ # So get GraphAllPages if there is valid nextlink and $Top= was NOT used in url originally
+ if (($MgGraphRequest.'@odata.nextLink' -like 'https://*') -and (-not ($uri.Contains('$top=')))) {
+ # Save nextLink url to variable and rerun do-loop
+ $uri = $MgGraphRequest.'@odata.nextLink'
+ Start-Sleep -Milliseconds $TimeOutBetweenGraphAPIRequests
+
+ # Continue to next round in Do-loop
+ Continue
+
+ } else {
+ # We dont have nextLink value OR
+ # $top= exists so we return what we got from first round
+ #return $allGraphAPIData
+ $uri = $null
+ }
+
+ } else {
+ # Sometimes we get results without Value-attribute (eg. getting user details)
+ # We will return all we got as is
+ # because there should not be nextLink page in this case ???
+ return $MgGraphRequest
+ }
+ } else {
+ # Invoke-MGGraphRequest failed so we return false
+ return $null
+ }
+
+ } while ($uri) # Always run once and continue if there is nextLink value
+
+
+ # We should not end here but just in case
+ return $allGraphAPIData
+
+ } catch {
+ Write-Error "There was error with MGGraphRequest with url $url!"
+ return $null
+ }
+ }
+
+
+ function Get-IntunePowershellScriptContentInCleartext {
+ Param(
+ [Parameter(Mandatory=$true)]
+ [String]$PowershellScriptPolicyId
+ )
+
+ # Check if we already have value
+ if($IdHashtable[$PowershellScriptPolicyId].scriptContentClearText) {
+ # Powershell script in clear text already exists in HashTable object
+ # So we can return it
+
+ return $IdHashtable[$PowershellScriptPolicyId].scriptContentClearText
+
+ } else {
+ # Property scriptContentClearText does NOT exist in HashTable object
+
+ # Download Powershell script in cleartext if -Online parameter has been specified
+ # PowerShell script is in clear text in IME log files so this is always shown
+ # And later version might get PowerShell script from IME log instead from Graph
+ if($Online) {
+
+ #Write-Verbose "Downloading Powershell script: $PowershellScriptPolicyId"
+
+ $uri = "https://graph.microsoft.com/beta/deviceManagement/deviceManagementScripts/$PowershellScriptPolicyId"
+ $IntunePowershellScriptInformation = Invoke-MgGraphRequestGetAllPages -Uri $uri
+
+ if($IntunePowershellScriptInformation) {
+ #Write-Verbose "Done" -ForegroundColor Green
+
+ if($IntunePowershellScriptInformation.scriptContent) {
+
+ Try {
+ # Convert Intune Powershell script base64 content to clear text
+ $b = [System.Convert]::FromBase64String("$($IntunePowershellScriptInformation.scriptContent)")
+ $IntuneScriptContentInClearText = [System.Text.Encoding]::UTF8.GetString($b)
+ } catch {
+ # Some fatal error converting base64 to cleartext
+ Write-Error "Error converting Intune Powershell script base64 to cleartext" -ForegroundColor Red
+ return 'N/A'
+ }
+
+ # Add new property to HashTable object if it doesn't already exist
+ if(-not $IdHashtable[$PowershellScriptPolicyId].scriptContentClearText) {
+
+ $IdHashtable[$PowershellScriptPolicyId] | Add-Member -MemberType noteProperty -Name scriptContentClearText -Value $IntuneScriptContentInClearText
+ } else {
+ $IdHashtable[$PowershellScriptPolicyId].scriptContentClearText = $IntuneScriptContentInClearText
+ }
+
+ return $IntuneScriptContentInClearText
+ } else {
+ # Did not get scriptContent information
+ # We should never get here if we got anything successfully from Graph API
+ Write-Verbose "Failed to download Powershell scriptContent property" -ForegroundColor Yellow
+ return 'N/A'
+ }
+ } else {
+ # Could not get Intune Powershell information from Intune
+ # Doing nothing
+ Write-Host "Failed to download Powershell script from Intune" -ForegroundColor Yellow
+ return 'N/A'
+ }
+ } else {
+ # -Online not selected and we didn't have value so return $null
+ return 'N/A'
+ }
+ }
+ # We should not get here
+ return 'N/A'
+ }
+
+
+ function Get-IntuneRemediationDetectionScriptContentInCleartext {
+ Param(
+ [Parameter(Mandatory=$true)]
+ [String]$ScriptPolicyId
+ )
+
+
+ # Check if we already have value
+ if($IdHashtable[$ScriptPolicyId].detectionScriptContentClearText) {
+ # Powershell script in clear text already exists in HashTable object
+ # So we can return it
+
+ return $IdHashtable[$ScriptPolicyId].detectionScriptContentClearText
+
+ } else {
+ # Property detectionScriptContent does NOT exist in HashTable object
+
+ # Download Remediation Detection script in cleartext if -Online parameter has been specified
+ # and -DoNotDownloadClearTextRemediationScriptsToReport is NOT specified
+ if($Online -and (-not $DoNotDownloadClearTextRemediationScriptsToReport)) {
+
+ #Write-Verbose "Downloading Remediation Detection script: $ScriptPolicyId"
+
+ $uri = "https://graph.microsoft.com/beta/deviceManagement/deviceHealthScripts/$ScriptPolicyId"
+ #Write-Verbose "URI: $uri"
+ $IntuneRemediationScriptInformation = Invoke-MgGraphRequestGetAllPages -Uri $uri
+
+ if($IntuneRemediationScriptInformation) {
+ #Write-Verbose "Done" -ForegroundColor Green
+
+ if($IntuneRemediationScriptInformation.detectionScriptContent) {
+
+ Try {
+ # Convert Intune Remediation Detection script base64 content to clear text
+ $b = [System.Convert]::FromBase64String("$($IntuneRemediationScriptInformation.detectionScriptContent)")
+ $IntuneScriptContentInClearText = [System.Text.Encoding]::UTF8.GetString($b)
+ } catch {
+ # Some fatal error converting base64 to cleartext
+ Write-Error "Error converting Intune Remediation Detection script base64 to cleartext" -ForegroundColor Red
+ return 'N/A'
+ }
+
+ # Add new property to HashTable object if it doesn't already exist
+ if(-not $IdHashtable[$ScriptPolicyId].detectionScriptContentClearText) {
+
+ $IdHashtable[$ScriptPolicyId] | Add-Member -MemberType noteProperty -Name detectionScriptContentClearText -Value $IntuneScriptContentInClearText
+ } else {
+ $IdHashtable[$PowershellScriptPolicyId].detectionScriptContentClearText = $IntuneScriptContentInClearText
+ }
+
+ return $IntuneScriptContentInClearText
+ } else {
+ # Did not get detectionScriptContent information
+ # We should never get here if we got anything successfully from Graph API
+ Write-Verbose "Failed to download Remediation Detect detectionScriptContent property" -ForegroundColor Yellow
+ return 'N/A'
+ }
+ } else {
+ # Could not get Intune Remediation Detect information from Intune
+ # Doing nothing
+ Write-Host "Failed to download Remediation Detect script from Intune" -ForegroundColor Yellow
+ return 'N/A'
+ }
+ } else {
+ # -Online not selected and we didn't have value so return $null
+ return 'N/A'
+ }
+ }
+ # We should not get here
+ return 'N/A'
+ }
+
+
+ function Get-IntuneRemediationRemediateScriptContentInCleartext {
+ Param(
+ [Parameter(Mandatory=$true)]
+ [String]$ScriptPolicyId
+ )
+
+
+ # Check if we already have value
+ if($IdHashtable[$ScriptPolicyId].RemediateScriptContentClearText) {
+ # Powershell script in clear text already exists in HashTable object
+ # So we can return it
+
+ return $IdHashtable[$ScriptPolicyId].RemediateScriptContentClearText
+
+ } else {
+ # Property remediationScriptContent does NOT exist in HashTable object
+
+ # Download Remediation Remediate script in cleartext if -Online parameter has been specified
+ # and -DoNotDownloadClearTextRemediationScriptsToReport is NOT specified
+ if($Online -and (-not $DoNotDownloadClearTextRemediationScriptsToReport)) {
+
+ #Write-Verbose "Downloading Remediation Remediate script: $ScriptPolicyId"
+
+ $uri = "https://graph.microsoft.com/beta/deviceManagement/deviceHealthScripts/$ScriptPolicyId"
+ $IntuneRemediationScriptInformation = Invoke-MgGraphRequestGetAllPages -Uri $uri
+
+ if($IntuneRemediationScriptInformation) {
+ #Write-Verbose "Done" -ForegroundColor Green
+
+ if($IntuneRemediationScriptInformation.remediationScriptContent) {
+
+ Try {
+ # Convert Intune Remediation Detection script base64 content to clear text
+ $b = [System.Convert]::FromBase64String("$($IntuneRemediationScriptInformation.remediationScriptContent)")
+ $IntuneScriptContentInClearText = [System.Text.Encoding]::UTF8.GetString($b)
+ } catch {
+ # Some fatal error converting base64 to cleartext
+ Write-Error "Error converting Intune Remediation Detection script base64 to cleartext" -ForegroundColor Red
+ return 'N/A'
+ }
+
+ # Add new property to HashTable object if it doesn't already exist
+ if(-not $IdHashtable[$ScriptPolicyId].RemediateScriptContentClearText) {
+
+ $IdHashtable[$ScriptPolicyId] | Add-Member -MemberType noteProperty -Name RemediateScriptContentClearText -Value $IntuneScriptContentInClearText
+ } else {
+ $IdHashtable[$PowershellScriptPolicyId].RemediateScriptContentClearText = $IntuneScriptContentInClearText
+ }
+
+ return $IntuneScriptContentInClearText
+ } else {
+ # Did not get remediationScriptContent information
+ # We should never get here if we got anything successfully from Graph API
+ Write-Verbose "Failed to download Powershell remediationScriptContent property" -ForegroundColor Yellow
+ return 'N/A'
+ }
+ } else {
+ # Could not get Intune Remediation script information from Intune
+ # Doing nothing
+ Write-Host "Failed to download Remediation Remediate script from Intune" -ForegroundColor Yellow
+ return 'N/A'
+ }
+ } else {
+ # -Online not selected and we didn't have value so return $null
+ return 'N/A'
+ }
+ }
+ # We should not get here
+ return 'N/A'
+ }
+
+
+ function Get-IntuneCustomComplianceScriptContentInCleartext {
+ Param(
+ [Parameter(Mandatory=$true)]
+ [String]$ScriptPolicyId
+ )
+
+
+ # Check if we already have value
+ if($IdHashtable[$ScriptPolicyId].detectionScriptContentClearText) {
+ # Powershell script in clear text already exists in HashTable object
+ # So we can return it
+
+ return $IdHashtable[$ScriptPolicyId].detectionScriptContentClearText
+
+ } else {
+ # Property detectionScriptContent does NOT exist in HashTable object
+
+ # Download Custom Compliance script in cleartext if -Online parameter has been specified
+ # and -DoNotDownloadClearTextRemediationScriptsToReport is NOT specified
+ if($Online -and (-not $DoNotDownloadClearTextRemediationScriptsToReport)) {
+
+ #Write-Verbose "Downloading Custom Compliance script: $ScriptPolicyId"
+
+ $uri = "https://graph.microsoft.com/beta/deviceManagement/deviceComplianceScripts/$ScriptPolicyId"
+ $IntuneRemediationScriptInformation = Invoke-MgGraphRequestGetAllPages -Uri $uri
+
+ if($IntuneRemediationScriptInformation) {
+ #Write-Verbose "Done" -ForegroundColor Green
+
+ if($IntuneRemediationScriptInformation.detectionScriptContent) {
+
+ Try {
+ # Convert Intune Custom Compliance script base64 content to clear text
+ $b = [System.Convert]::FromBase64String("$($IntuneRemediationScriptInformation.detectionScriptContent)")
+ $IntuneScriptContentInClearText = [System.Text.Encoding]::UTF8.GetString($b)
+ } catch {
+ # Some fatal error converting base64 to cleartext
+ Write-Error "Error converting Intune Custom Compliance script base64 to cleartext" -ForegroundColor Red
+ return 'N/A'
+ }
+
+ # Add new property to HashTable object if it doesn't already exist
+ if(-not $IdHashtable[$ScriptPolicyId].detectionScriptContentClearText) {
+
+ $IdHashtable[$ScriptPolicyId] | Add-Member -MemberType noteProperty -Name detectionScriptContentClearText -Value $IntuneScriptContentInClearText
+ } else {
+ $IdHashtable[$PowershellScriptPolicyId].detectionScriptContentClearText = $IntuneScriptContentInClearText
+ }
+
+ return $IntuneScriptContentInClearText
+ } else {
+ # Did not get detectionScriptContent information
+ # We should never get here if we got anything successfully from Graph API
+ Write-Verbose "Failed to download Custom Compliance detectionScriptContent property" -ForegroundColor Yellow
+ return 'N/A'
+ }
+ } else {
+ # Could not get Intune Custom Compliance information from Intune
+ # Doing nothing
+ Write-Host "Failed to download Custom Compliance script from Intune" -ForegroundColor Yellow
+ return 'N/A'
+ }
+ } else {
+ # -Online not selected and we didn't have value so return $null
+ return 'N/A'
+ }
+ }
+ # We should not get here
+ return 'N/A'
+ }
+
+
+
+ ### HTML Report helper functions ###
+
+ function Fix-HTMLSyntax {
+ Param(
+ $html
+ )
+
+ $html = $html.Replace('<', '<')
+ $html = $html.Replace('>', '>')
+ $html = $html.Replace('"', '"')
+
+ return $html
+ }
+
+ function Fix-HTMLColumns {
+ Param(
+ $html
+ )
+
+ # Rename column headers
+ $html = $html -replace '
@odata.type
','
App type
'
+ $html = $html -replace '
displayname
','
App name
'
+ $html = $html -replace '
assignmentIntent
','
Assignment Intent
'
+ $html = $html -replace '
assignmentTargetGroupDisplayName
','
Target Group
'
+ $html = $html -replace '
assignmentFilterDisplayName
','
Filter name
'
+ $html = $html -replace '
FilterIncludeExclude
','
Filter Intent
'
+ $html = $html -replace '
publisher
','
Publisher
'
+ $html = $html -replace '
productVersion
','
Version
'
+ $html = $html -replace '
filename
','
Filename
'
+ $html = $html -replace '
createdDateTime
','
Created
'
+ $html = $html -replace '
lastModifiedDateTime
','
Modified
'
+
+ return $html
+ }
+
+
+ function return-ObjectPropertiesAsAlignedString {
+ Param(
+ $object
+ )
+ # Calculate the maximum length of property names for alignment
+ $maxWidth = ($object.PSObject.Properties.Name | Measure-Object -Maximum -Property Length).Maximum
+
+ # Build the formatted string
+ $result = $object.PSObject.Properties | ForEach-Object {
+ "{0,-$maxWidth} : {1}" -f $_.Name, $_.Value
+ }
+
+ # Convert array to single string
+ $output = $result -join "`n"
+ return $output
+ }
+
+# endregion Functions
+
+################ Functions ################
+
+Write-Host "Starting Get-IntuneManagementExtensionDiagnostics`n"
+
+# If LogFilePath is not specified then show log files in Out-GridView
+# from folder C:\ProgramData\Microsoft\intunemanagementextension\Logs
+if($LOGFile) {
+
+ if(-not (Test-Path $LOGFile)) {
+ Write-Host "Log file does not exist: $LOGFile" -ForegroundColor Yellow
+ Write-Host "Script will exit" -ForegroundColor Yellow
+ Write-Host ""
+ Exit 0
+ }
+ $SelectedLogFiles = Get-ChildItem -Path $LOGFile
+
+} else {
+
+ if($LogFilesFolder) {
+
+ if(-not (Test-Path $LogFilesFolder)) {
+ Write-Host "LogFilesFolder: $LogFilesFolder does not exist" -ForegroundColor Yellow
+ Write-Host "Script will exit" -ForegroundColor Yellow
+ exit 0
+ }
+
+ # Sort files: new files first and IntuneManagementExtension before AgentExecutor
+
+ # ORIGINAL
+ #$LogFiles = Get-ChildItem -Path $LogFilesFolder -Filter *.log | Where-Object { ($_.Name -like '*IntuneManagementExtension*.log') -or ($_.Name -like '*AgentExecutor*.log') -or ($_.Name -like '*AppWorkload*.log') } | Sort-Object -Property Name -Descending | Sort-Object -Property LastWriteTime -Descending
+
+ # Added -Depth 2 so you can point to Intune Diagnostics package root folder
+ $LogFiles = Get-ChildItem -Path $LogFilesFolder -Filter *.log -Depth 2 | Where-Object { ($_.Name -like '*IntuneManagementExtension*.log') -or ($_.Name -like '*AgentExecutor*.log') -or ($_.Name -like '*AppWorkload*.log') } | Sort-Object -Property Name -Descending | Sort-Object -Property LastWriteTime -Descending
+
+
+ } else {
+ # Running report using local computer's Intune log files
+
+ # Save Computer Name
+ $ComputerNameForReport = $env:ComputerName
+
+ # Sort files: new files first and IntuneManagementExtension before AgentExecutor
+ $LogFiles = Get-ChildItem -Path 'C:\ProgramData\Microsoft\intunemanagementextension\Logs' -Filter *.log | Where-Object { ($_.Name -like '*IntuneManagementExtension*.log') -or ($_.Name -like '*AgentExecutor*.log') -or ($_.Name -like '*AppWorkload*.log') } | Sort-Object -Property Name -Descending | Sort-Object -Property LastWriteTime -Descending
+
+ }
+
+ # Show log files in Out-GridView
+ # This variable is automatically configured in ESP
+ if(-not $SelectedLogFiles) {
+ if($AllLogFiles) {
+ $SelectedLogFiles = $LogFiles
+ } else {
+ $SelectedLogFiles = $LogFiles | Out-GridView -Title 'Select log file to show in Out-GridView from path C:\ProgramData\Microsoft\intunemanagementextension\Logs' -OutputMode Multiple
+ }
+ }
+
+ if(-not $SelectedLogFiles) {
+ Write-Host "No log file(s) selected. Script will exit!`n" -ForegroundColor Yellow
+ Exit 0
+ }
+}
+
+
+# Check whether we show time selection on Out-GridView or not
+if(((-not $LogStartDateTime) -and (-not $LogEndDateTime)) -and (-not $AllLogEntries) -and (-not $Today)) {
+
+ $LogStartEndTimeOutGridviewEntries = [System.Collections.Generic.List[PSObject]]@()
+
+ $LogStartEndTimeOutGridviewEntries.add([PSCustomObject]@{
+ 'Log Start and End time' = 'All log entries';
+ 'LogStartDateTimeObject' = Get-Date 1/1/1900;
+ 'LogEndDateTimeObject' = (Get-Date).AddYears(1000);
+ })
+
+ $LogStartEndTimeOutGridviewEntries.add([PSCustomObject]@{
+ 'Log Start and End time' = 'Current day from midnight';
+ 'LogStartDateTimeObject' = Get-Date -Hour 0 -Minute 0 -Second 0;
+ 'LogEndDateTimeObject' = (Get-Date).AddYears(1000);
+ })
+
+ $LogStartEndTimeOutGridviewEntries.add([PSCustomObject]@{
+ 'Log Start and End time' = 'Last 5 minutes';
+ 'LogStartDateTimeObject' = (Get-Date).AddMinutes(-5);
+ 'LogEndDateTimeObject' = (Get-Date).AddYears(1000);
+ })
+
+ $LogStartEndTimeOutGridviewEntries.add([PSCustomObject]@{
+ 'Log Start and End time' = 'Last 15 minutes';
+ 'LogStartDateTimeObject' = (Get-Date).AddMinutes(-15);
+ 'LogEndDateTimeObject' = (Get-Date).AddYears(1000);
+ })
+
+ $LogStartEndTimeOutGridviewEntries.add([PSCustomObject]@{
+ 'Log Start and End time' = 'Last 30 minutes';
+ 'LogStartDateTimeObject' = (Get-Date).AddMinutes(-30);
+ 'LogEndDateTimeObject' = (Get-Date).AddYears(1000);
+ })
+
+ $LogStartEndTimeOutGridviewEntries.add([PSCustomObject]@{
+ 'Log Start and End time' = 'Last 1 hour';
+ 'LogStartDateTimeObject' = (Get-Date).AddHours(-1);
+ 'LogEndDateTimeObject' = (Get-Date).AddYears(1000);
+ })
+
+ $LogStartEndTimeOutGridviewEntries.add([PSCustomObject]@{
+ 'Log Start and End time' = 'Last 2 hours';
+ 'LogStartDateTimeObject' = (Get-Date).AddHours(-2);
+ 'LogEndDateTimeObject' = (Get-Date).AddYears(1000);
+ })
+
+ $LogStartEndTimeOutGridviewEntries.add([PSCustomObject]@{
+ 'Log Start and End time' = 'Last 6 hours';
+ 'LogStartDateTimeObject' = (Get-Date).AddHours(-6);
+ 'LogEndDateTimeObject' = (Get-Date).AddYears(1000);
+ })
+
+ $LogStartEndTimeOutGridviewEntries.add([PSCustomObject]@{
+ 'Log Start and End time' = 'Last 24 hours';
+ 'LogStartDateTimeObject' = (Get-Date).AddHours(-24);
+ 'LogEndDateTimeObject' = (Get-Date).AddYears(1000);
+ })
+
+ $LogStartEndTimeOutGridviewEntries.add([PSCustomObject]@{
+ 'Log Start and End time' = 'Last 7 days';
+ 'LogStartDateTimeObject' = (Get-Date).AddDays(-7);
+ 'LogEndDateTimeObject' = (Get-Date).AddYears(1000);
+ })
+
+ $LogStartEndTimeOutGridviewEntries.add([PSCustomObject]@{
+ 'Log Start and End time' = 'First 30 minutes';
+ 'LogStartDateTimeObject' = Get-Date 1/1/1900;
+ 'LogEndDateTimeObject' = 'Analyzing DateTime later';
+ })
+
+ $LogStartEndTimeOutGridviewEntries.add([PSCustomObject]@{
+ 'Log Start and End time' = 'First 1 hour';
+ 'LogStartDateTimeObject' = Get-Date 1/1/1900;
+ 'LogEndDateTimeObject' = 'Analyzing DateTime later';
+ })
+
+ $LogStartEndTimeOutGridviewEntries.add([PSCustomObject]@{
+ 'Log Start and End time' = 'First 2 hours';
+ 'LogStartDateTimeObject' = Get-Date 1/1/1900;
+ 'LogEndDateTimeObject' = 'Analyzing DateTime later';
+ })
+
+ $LogStartEndTimeOutGridviewEntries.add([PSCustomObject]@{
+ 'Log Start and End time' = 'First 6 hours';
+ 'LogStartDateTimeObject' = Get-Date 1/1/1900;
+ 'LogEndDateTimeObject' = 'Analyzing DateTime later';
+ })
+
+ $LogStartEndTimeOutGridviewEntries.add([PSCustomObject]@{
+ 'Log Start and End time' = 'First 24 hours';
+ 'LogStartDateTimeObject' = Get-Date 1/1/1900;
+ 'LogEndDateTimeObject' = 'Analyzing DateTime later';
+ })
+
+ # Show predefined values in Out-GridView
+ $SelectedTimeFrameObject = $LogStartEndTimeOutGridviewEntries | Out-GridView -Title 'Select timeframe to show log entries' -OutputMode Single
+
+ if($SelectedTimeFrameObject) {
+ $LogStartDateTimeObject = $SelectedTimeFrameObject.LogStartDateTimeObject
+ $LogEndDateTimeObject = $SelectedTimeFrameObject.LogEndDateTimeObject
+
+ } else {
+ Write-Host "No timeframe selected." -ForegroundColor Red
+ Write-Host "Script will exit"
+ Exit 0
+ }
+
+
+} else {
+
+ # Set default time which can be changed depenging on -Parameters
+
+ # Use StartTime from year 1900
+ $LogStartDateTimeObject = Get-Date 1/1/1900
+
+ # Use EndTime +1000 years from today
+ $LogEndDateTimeObject = (Get-Date).AddYears(1000)
+
+ if($Today) {
+ $LogStartDateTimeObject = Get-Date -Hour 0 -Minute 0 -Second 0
+ $LogEndDateTimeObject = (Get-Date).AddYears(1000)
+ }
+
+ if($AllLogEntries) {
+ # Use StartTime from year 1900
+ $LogStartDateTimeObject = Get-Date 1/1/1900
+
+ # Use EndTime +1000 years from today
+ $LogEndDateTimeObject = (Get-Date).AddYears(1000)
+ }
+
+ # Check $LogStartDateTime can be converted to Powershell DateTime object
+ if($LogStartDateTime) {
+ # Try converting given parameter to Powershell DateTime object
+
+ Try {
+ $LogStartDateTimeObject = Get-Date $LogStartDateTime
+ } catch {
+ Write-Host "Given parameter LogStartDateTime is not in valid DateTime format." -ForegroundColor Red
+ Write-Host "Script will exit"
+ Exit 0
+ }
+ }
+
+ # Check $LogEndDateTime can be converted to Powershell DateTime object
+ if($LogEndDateTime) {
+ # Try converting given parameter to Powershell DateTime object
+
+ Try {
+ $LogEndDateTimeObject = Get-Date $LogEndDateTime
+ } catch {
+ Write-Host "Given parameter LogEndDateTime is not in valid DateTime format." -ForegroundColor Red
+ Write-Host "Script will exit"
+ Exit 0
+ }
+ }
+}
+
+
+# Download Powershell scripts, Proactive Remediation scripts and custom Compliance Policy scripts
+if ($Online) {
+
+ $GraphAuthenticationModule = $null
+ $MgContext = $null
+
+ # Test if we are in Enrollment Status Page (ESP) phase
+ # Detect defaultuser0 loggedon
+ if($env:UserName -eq 'defaultUser0') {
+
+ # Make sure we can connect
+ $GraphAuthenticationModule = Import-Module Microsoft.Graph.Authentication -PassThru -ErrorAction Ignore
+ if (-not $GraphAuthenticationModule) {
+
+ Write-Host "Installing module Microsoft.Graph.Authentication"
+ Install-Module Microsoft.Graph.Authentication -Force
+ $Success = $?
+
+ if($Success) {
+ Write-Host "Success`n" -ForegroundColor Green
+
+ Write-Host "Import module Microsoft.Graph.Authentication"
+ $GraphAuthenticationModule = Import-Module Microsoft.Graph.Authentication -PassThru -ErrorAction Ignore
+
+ if($GraphAuthenticationModule) {
+ # Module imported successfully
+ Write-Host "Success`n" -ForegroundColor Green
+ } else {
+ # Failed to import module
+ Write-Host "Failed to import module! Skip downloading script names...`n" -ForegroundColor Red
+ }
+ } else {
+ Write-Host "Failed to install module! Skip downloading script names...`n" -ForegroundColor Red
+ }
+ }
+
+ if($GraphAuthenticationModule) {
+
+ Write-Host "Connect to Microsoft Graph API"
+
+ $scopes = "DeviceManagementConfiguration.Read.All"
+ $MgGraph = Connect-MgGraph -scopes $scopes
+ $Success = $?
+
+ if ($Success -and $MgGraph) {
+ Write-Host "Success`n" -ForegroundColor Green
+
+ # Get MgGraph session details
+ $MgContext = Get-MgContext
+
+ if($MgContext) {
+
+ $TenantId = $MgContext.TenantId
+ $AdminUserUPN = $MgContext.Account
+
+ Write-Host "Connected to Intune tenant:`n$TenantId`n$AdminUserUPN`n"
+
+ } else {
+ Write-Host "Error getting MgContext information! Skip downloading script names..." -ForegroundColor Red
+ }
+
+ } else {
+ Write-Host "Could not connect to Graph API! Skip downloading script names..." -ForegroundColor Red
+ }
+
+ } else {
+ Write-Host "Could not connect to Graph API! Skip downloading script names..." -ForegroundColor Red
+ }
+
+ } else {
+ Write-Host "Connecting to Intune using module Microsoft.Graph.Authentication"
+
+ Write-Host "Import module Microsoft.Graph.Authentication"
+ Import-Module Microsoft.Graph.Authentication
+ $Success = $?
+
+ if($Success) {
+ # Module imported successfully
+ Write-Host "Success`n" -ForegroundColor Green
+ } else {
+ Write-Host "Failed" -ForegroundColor Red
+ Write-Host "Make sure you have installed module Microsoft.Graph.Authentication"
+ Write-Host "You can install module without admin rights to your user account with command:`n`nInstall-Module -Name Microsoft.Graph.Authentication -Scope CurrentUser" -ForegroundColor Yellow
+ Write-Host "`nor you can install machine-wide module with with admin rights using command:`nInstall-Module -Name Microsoft.Graph.Authentication"
+ Write-Host ""
+ Exit 1
+ }
+
+
+ Write-Host "Connect to Microsoft Graph API"
+
+ $scopes = "DeviceManagementConfiguration.Read.All"
+ $MgGraph = Connect-MgGraph -scopes $scopes
+ $Success = $?
+
+ if ($Success -and $MgGraph) {
+ Write-Host "Success`n" -ForegroundColor Green
+
+ # Get MgGraph session details
+ $MgContext = Get-MgContext
+
+ if($MgContext) {
+
+ $TenantId = $MgContext.TenantId
+ $AdminUserUPN = $MgContext.Account
+
+ Write-Host "Connected to Intune tenant:`n$TenantId`n$AdminUserUPN`n"
+
+ } else {
+ Write-Host "Error getting MgContext information!`nScript will exit!" -ForegroundColor Red
+ Exit 1
+ }
+
+ } else {
+ Write-Host "Could not connect to Graph API!" -ForegroundColor Red
+ Exit 1
+ }
+ }
+
+ # Download Intune scripts information if we have connection to Microsoft Graph API
+ if($MgContext) {
+
+ Write-Host "Download Intune Powershell scripts"
+ # Get PowerShell Scripts
+ $uri = 'https://graph.microsoft.com/beta/deviceManagement/deviceManagementScripts'
+ $AllIntunePowershellScripts = Invoke-MgGraphRequestGetAllPages -Uri $uri
+
+ if($AllIntunePowershellScripts) {
+ Write-Host "Done" -ForegroundColor Green
+
+ # Add Name Property to object
+ $AllIntunePowershellScripts | Foreach-Object { $_ | Add-Member -MemberType noteProperty -Name name -Value $_.displayName }
+
+ # Add all PowershellScripts to Hashtable
+ $AllIntunePowershellScripts | Foreach-Object { $id = $_.id; $value=$_; $IdHashtable["$id"] = $value }
+
+ # Save locally for debugging
+ #$AllIntunePowershellScripts | ConvertTo-Json -Depth 4 | Out-File -FilePath "$PSScriptRoot\AllIntunePowershellScripts.json" -Force
+
+ } else {
+ Write-Error "Did not find Intune Powershell scripts"
+ }
+
+ Start-Sleep -MilliSeconds 500
+
+ Write-Host "Download Intune Remediations Scripts"
+ # Get Proactive Remediations Scripts
+ $uri = 'https://graph.microsoft.com/beta/deviceManagement/deviceHealthScripts'
+ $AllIntuneProactiveRemediationsScripts = Invoke-MgGraphRequestGetAllPages -Uri $uri
+
+ if($AllIntuneProactiveRemediationsScripts) {
+ Write-Host "Done" -ForegroundColor Green
+
+ # Add Name Property to object
+ $AllIntuneProactiveRemediationsScripts | Foreach-Object { $_ | Add-Member -MemberType noteProperty -Name name -Value $_.displayName }
+
+ # Add policyType 6
+ # Which is Remediation script type
+ $AllIntuneProactiveRemediationsScripts | Foreach-Object { $_ | Add-Member -MemberType noteProperty -Name policyType -Value 6 }
+
+ # Add all PowershellScripts to Hashtable
+ $AllIntuneProactiveRemediationsScripts | Foreach-Object { $id = $_.id; $value=$_; $IdHashtable["$id"] = $value }
+
+ # Save locally for debugging
+ #$AllIntuneProactiveRemediationsScripts | ConvertTo-Json -Depth 4 | Out-File -FilePath "$PSScriptRoot\AllIntuneProactiveRemediationsScripts.json" -Force
+
+ } else {
+ Write-Error "Did not find Intune Remediation scripts"
+ }
+
+ Start-Sleep -MilliSeconds 500
+
+ Write-Host "Download Intune Windows Device Compliance custom Scripts"
+ # Get Windows Device Compliance custom Scripts
+ $uri = 'https://graph.microsoft.com/beta/deviceManagement/deviceComplianceScripts'
+ $AllIntuneCustomComplianceScripts = Invoke-MgGraphRequestGetAllPages -Uri $uri
+
+ if($AllIntuneCustomComplianceScripts) {
+ Write-Host "Done" -ForegroundColor Green
+
+ # Add Name Property to object
+ $AllIntuneCustomComplianceScripts | Foreach-Object { $_ | Add-Member -MemberType noteProperty -Name name -Value $_.displayName }
+
+ # Add policyType 8
+ # Which is Custom Compliance script type
+ $AllIntuneCustomComplianceScripts | Foreach-Object { $_ | Add-Member -MemberType noteProperty -Name policyType -Value 8 }
+
+ # Add all PowershellScripts to Hashtable
+ $AllIntuneCustomComplianceScripts | Foreach-Object { $id = $_.id; $value=$_; $IdHashtable["$id"] = $value }
+
+ # Save locally for debugging
+ #$AllIntuneCustomComplianceScripts | ConvertTo-Json -Depth 4 | Out-File -FilePath "$PSScriptRoot\AllIntuneCustomComplianceScripts.json" -Force
+
+ } else {
+ Write-Error "Did not find Intune Windows custom Compliance scripts"
+ }
+
+ Start-Sleep -MilliSeconds 500
+
+ Write-Host "Download Intune Filters"
+ $uri = 'https://graph.microsoft.com/beta/deviceManagement/assignmentFilters?$select=*'
+ $AllIntuneFilters = Invoke-MgGraphRequestGetAllPages -Uri $uri
+
+ if($AllIntuneFilters) {
+ Write-Host "Done" -ForegroundColor Green
+
+ # Add all Filters to Hashtable
+ $AllIntuneFilters | Foreach-Object { $id = $_.id; $value=$_; $IdHashtable["$id"] = $value }
+ } else {
+ Write-Error "Did not find Intune filters"
+ }
+
+ } else {
+ Write-Host "Not connected to Microsoft Intune, skip downloading script names...." -ForegroundColor Yellow
+ }
+}
+Write-Host ""
+
+
+# Run Report and/or Out-GridView in loop as long as user selects last line which will reload log file
+# Otherwise Out-GridView will exit if something else is selected than last line (reload line)
+do {
+
+ # Create Generic list where log entry custom objects are added
+ $LogEntryList = [System.Collections.Generic.List[PSObject]]@()
+
+ # Go through all selected log file(s)
+ Foreach ($SelectedLogFile in $SelectedLogFiles) {
+
+ $LogFilePath = $SelectedLogFile.FullName
+ $LogFileName = $SelectedLogFile.Name
+
+ Write-Host "Processing file $LogFileName"
+
+ # Initialize variables
+ $LineNumber=1
+ $MultilineLogEntryStartsArrayIndex=0
+ $MultilineLogEntryStartFound=$False
+
+ $Log = Get-Content -Path $LogFilePath
+
+ # This matches for cmtrace type logs
+ # Test with https://regex101.com
+ # String: