diff --git a/Get-IntuneManagementExtensionDiagnostics.ps1 b/Get-IntuneManagementExtensionDiagnostics.ps1 new file mode 100644 index 0000000..5512eef --- /dev/null +++ b/Get-IntuneManagementExtensionDiagnostics.ps1 @@ -0,0 +1,2869 @@ +<#PSScriptInfo + +.VERSION 1.0 + +.GUID ab5a8b63-97d5-4b1a-a4ab-6dcedbab1eb7 + +.AUTHOR Petri.Paavola@yodamiitti.fi + +.COMPANYNAME Yodamiitti Oy + +.COPYRIGHT Petri.Paavola@yodamiitti.fi + +.TAGS Intune Windows Autopilot + +.LICENSEURI + +.PROJECTURI https://github.com/petripaavola/Get-IntuneManagementExtensionDiagnostics + +.ICONURI + +.EXTERNALMODULEDEPENDENCIES + +.REQUIREDSCRIPTS + +.EXTERNALSCRIPTDEPENDENCIES + +.RELEASENOTES +Version 1.0: Original published version + +#> + +<# +.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 actions. + + Timeline report includes information about Intune Win32App, WinGetApp, Powershell scripts, Proactive 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 -> ProcessruntTime -> is not empty + + 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 -ShowStdOutInTimeline and -ShowErrorsInTimeline + This shows instantly what is problem in Powershell scripts + + + Author: + Petri.Paavola@yodamiitti.fi + Senior Modern Management Principal + Microsoft MVP - Windows and Devices for IT + + 2023-03-07 + + https://github.com/petripaavola/Get-IntuneManagementExtensionDiagnostics + +.PARAMETER Online +Download Powershell, Proactive Remediation and custom Compliance policy scripts to get displayName to Timeline report + +.PARAMETER ShowLogViewerUI +Shows graphical LogViewerUI where all log entries are easily browsed, searched and filtered + +.EXAMPLE + .\Get-IntuneManagementExtensionDiagnostics.ps1 +.EXAMPLE + .\Get-IntuneManagementExtensionDiagnostics.ps1 -Online +.EXAMPLE + .\Get-IntuneManagementExtensionDiagnostics.ps1 -Online -AllLogEntries -ShowAllTimelineEntries +.EXAMPLE + .\Get-IntuneManagementExtensionDiagnostics.ps1 -Online -ShowLogViewerUI +.EXAMPLE + .\Get-IntuneManagementExtensionDiagnostics.ps1 -Online -Today +.EXAMPLE + .\Get-IntuneManagementExtensionDiagnostics.ps1 -Online -LogStartDateTime 5.3.2023 5.00:00 -LogEndDateTime 7.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" + +.INPUTS +.OUTPUTS + None +.NOTES +.LINK + https://github.com/petripaavola +#> + +[CmdletBinding()] +Param( + [Parameter(Mandatory=$false, + HelpMessage = 'Enter Intune IME log file fullpath', + ValueFromPipeline=$true, + ValueFromPipelineByPropertyName=$true)] + [Alias("FullName")] + [String]$LOGFile = $null, + [Parameter(Mandatory=$false)] + [Switch]$Online, + [Parameter(Mandatory=$false, + HelpMessage = 'Enter Intune IME log files folder path', + ValueFromPipeline=$true, + ValueFromPipelineByPropertyName=$true)] + [String]$LogFilesFolder = $null, + [Parameter(Mandatory=$false, + HelpMessage = 'Enter Start DateTime for log entries (for example 1.3.2023 18:00:00)', + ValueFromPipeline=$true, + ValueFromPipelineByPropertyName=$true)] + $LogStartDateTime = $null, + [Parameter(Mandatory=$false, + HelpMessage = 'Enter End DateTime for log entries (for example 1.3.2023 19:00:00)', + ValueFromPipeline=$true, + ValueFromPipelineByPropertyName=$true)] + $LogEndDateTime = $null, + [Parameter(Mandatory=$false, + HelpMessage = 'Threshold seconds to highlight long running Powershell scripts', + ValueFromPipeline=$true, + ValueFromPipelineByPropertyName=$true)] + [int]$LongRunningPowershellNotifyThreshold = 180, + [Parameter(Mandatory=$false)] + [Switch]$ShowLogViewerUI, + [Parameter(Mandatory=$false)] + [Switch]$AllLogEntries, + [Parameter(Mandatory=$false)] + [Switch]$Today, + [Parameter(Mandatory=$false)] + [Switch]$ShowAllTimelineEntries, + [Parameter(Mandatory=$false)] + [Switch]$ShowStdOutInTimeline, + [Parameter(Mandatory=$false)] + [Switch]$ShowErrorsInTimeline, + [Parameter(Mandatory=$false)] + [Switch]$ShowErrorsSummary, + [Parameter(Mandatory=$false)] + [Switch]$ConvertAllKnownGuidsToClearText + +) + + +Write-Host "Get-IntuneManagementExtensionDiagnostics.ps1 v1.0" -ForegroundColor Cyan +Write-Host "Author: Petri.Paavola@yodamiitti.fi / Microsoft MVP - Windows and Devices for IT" +Write-Host "" + + + +# Set variables 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' + + if((-not $LogFilesFolder) -or (-not $LOGFile)) { + Write-Host "Automatically configuring parameters" + Write-Host + + $IntuneLogsPath = 'C:\ProgramData\Microsoft\IntuneManagementExtension\Logs' + if(Test-Path $IntuneLogsPath) { + $SelectedLogFiles = Get-ChildItem -Path $IntuneLogsPath -Filter *.log | Where-Object { ($_.Name -like 'IntuneManagementExtension*.log') -or ($_.Name -like 'AgentExecutor*.log') } + } else { + Write-Host "Log folder does not exist yet: $IntuneLogsPath" + Write-Host "Try again in a moment..." -ForegroundColor Yellow + Write-Host "" + Exit 0 + } + } + + $AllLogEntries=$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 + } +} + + +# Work in Progress +<# + [Parameter(Mandatory=$false)] + [Switch]$ExportJson +#> + + +# Hashtable with ids and names +$IdHashtable = @{} + +# Save timeline objects to this List +$observedTimeline = [System.Collections.Generic.List[PSObject]]@() + + +# 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] $detail, + [Parameter(Mandatory=$false)] $seconds, + [Parameter(Mandatory=$false)] [String] $logEntry, + [Parameter(Mandatory=$false)] [String] $color + + ) + + $observedTimeline.add([PSCustomObject]@{ + 'Date' = $date + 'Status' = $status + 'Detail' = $detail + 'Seconds' = $seconds + 'LogEntry' = $logEntry + 'Color' = $color + }) + } +# endregion 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) { + # Sort files: new files first and IntuneManagementExtension before AgentExecutor + $LogFiles = Get-ChildItem -Path $LogFilesFolder -Filter *.log | Where-Object { ($_.Name -like 'IntuneManagementExtension*.log') -or ($_.Name -like 'AgentExecutor*.log') } | Sort-Object -Property Name -Descending | Sort-Object -Property LastWriteTime -Descending + } else { + # 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') } | 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) { + $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' = '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'; + }) + + + # Show predefined values in Out-GridView + $SelectedTimeFrameObject = $LogStartEndTimeOutGridviewEntries | Out-GridView -Title 'Select timeframe to show log entries' -OutputMode Multiple + + 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) { + + # Test if we are in Enrollment Status Page (ESP) phase + # Detect defaultuser0 loggedon + if($env:UserName -eq 'defaultUser0') { + + # Make sure we can connect + $module = Import-Module Microsoft.Graph.Intune -PassThru -ErrorAction Ignore + if (-not $module) { + Write-Host "Installing module Microsoft.Graph.Intune" + Install-Module Microsoft.Graph.Intune -Force + } + Import-Module Microsoft.Graph.Intune + + $MSGraphEnvironment = Connect-MSGraph + Write-Host "Connected to tenant $($graph.TenantId)" + + + } else { + Write-Host "Connecting to Intune using Powershell Intune-Module" + Update-MSGraphEnvironment -SchemaVersion 'beta' + $Success = $? + + if (-not $Success) { + Write-Host "Failed to update MSGraph Environment schema to Beta!`n" -ForegroundColor Red + Write-Host "Make sure you have installed Intune Powershell module" + Write-Host "`nYou can install Intune module to your user account with command:" + Write-Host "Install-Module -Name Microsoft.Graph.Intune -Scope CurrentUser`n" -ForegroundColor Yellow + Write-Host "`nor you can install machine-wide Intune module with command:`nInstall-Module -Name Microsoft.Graph.Intune" + Write-Host "More information: https://github.com/microsoft/Intune-PowerShell-SDK" + Exit 1 + } + + $MSGraphEnvironment = Connect-MSGraph + $Success = $? + + if ($Success -and $MSGraphEnvironment) { + $TenantId = $MSGraphEnvironment.tenantId + $AdminUserUPN = $MSGraphEnvironment.upn + + Write-Host "Connected to tenant $TenantId as user $AdminUserUPN" + + } else { + Write-Host "Could not connect to MSGraph!" -ForegroundColor Red + Write-Host "Will not download information from Microsoft Intune" -ForegroundColor Yellow + Write-Host "Script will exit." + Exit 0 + } + } + + if($MSGraphEnvironment) { + Write-Host "Download Intune Powershell scripts" + # Get PowerShell Scripts + $url = 'https://graph.microsoft.com/beta/deviceManagement/deviceManagementScripts' + $MSGraphRequest = Invoke-MSGraphRequest -Url $url -HttpMethod 'GET' + $Success = $? + + if (-not ($Success)) { + Write-Error "Error downloading Intune Powershell scripts" + #return $null + } else { + $AllIntunePowershellScripts = Get-MSGraphAllPages -SearchResult $MSGraphRequest + $Success = $? + if($Success) { + 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 } + } else { + Write-Error "Error downloading All Intune Powershell scripts" + } + } + + Start-Sleep 1 + + Write-Host "Download Intune Proactive Remediations Scripts" + # Get Proactive Remediations Scripts + $url = 'https://graph.microsoft.com/beta/deviceManagement/deviceHealthScripts' + $MSGraphRequest = Invoke-MSGraphRequest -Url $url -HttpMethod 'GET' + $Success = $? + + if (-not ($Success)) { + Write-Error "Error downloading Intune Proactive Remediations scripts" + #return $null + } else { + $AllIntuneProactiveRemediationsScripts = Get-MSGraphAllPages -SearchResult $MSGraphRequest + $Success = $? + if($Success) { + Write-Host "Done" -ForegroundColor Green + + # Add Name Property to object + $AllIntuneProactiveRemediationsScripts | Foreach-Object { $_ | Add-Member -MemberType noteProperty -Name name -Value $_.displayName } + + # Add all PowershellScripts to Hashtable + $AllIntuneProactiveRemediationsScripts | Foreach-Object { $id = $_.id; $value=$_; $IdHashtable["$id"] = $value } + } else { + Write-Error "Error downloading All Intune Proactive Remediations scripts" + } + } + + Start-Sleep 1 + + Write-Host "Download Intune Windows Device Compliance custom Scripts" + # Get Windows Device Compliance custom Scripts + $url = 'https://graph.microsoft.com/beta/deviceManagement/deviceComplianceScripts' + $MSGraphRequest = Invoke-MSGraphRequest -Url $url -HttpMethod 'GET' + $Success = $? + + if (-not ($Success)) { + Write-Error "Error downloading Intune Windows custom Compliance scripts" + #return $null + } else { + $AllIntuneCustomComplianceScripts = Get-MSGraphAllPages -SearchResult $MSGraphRequest + $Success = $? + if($Success) { + Write-Host "Done" -ForegroundColor Green + + # Add Name Property to object + $AllIntuneCustomComplianceScripts | Foreach-Object { $_ | Add-Member -MemberType noteProperty -Name name -Value $_.displayName } + + # Add all PowershellScripts to Hashtable + $AllIntuneCustomComplianceScripts | Foreach-Object { $id = $_.id; $value=$_; $IdHashtable["$id"] = $value } + } else { + Write-Error "Error downloading All Intune Windows custom Compliance scripts" + } + } + } 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: + + # This matches single line full log entry + $SingleLineRegex = '^\<\!\[LOG\[(.*)]LOG\].*\$' + + # Start of multiline log entry + $FirstLineOfMultiLineLogRegex = '^\<\!\[LOG\[(.*)$' + + # End of multiline log entry + $LastLineOfMultiLineLogRegex = '^(.*)\]LOG\]\!>\$' + + + # Process each log file line one by one + Foreach ($CurrentLogEntry in $Log) { + + # Get data from CurrentLogEntry + if($CurrentLogEntry -Match $SingleLineRegex) { + # This matches single line log entry + + # Regex found match + $LogMessage = $Matches[1].Trim() + + $Hour = $Matches[2] + $Minute = $Matches[3] + $Second = $Matches[4] + + $MilliSecondFull = $Matches[5] + # Cut milliseconds to 0-999 + # Time unit is so small that we don't even bother to round the value + $MilliSecond = $MilliSecondFull.Substring(0,3) + + $Month = $Matches[6] + $Day = $Matches[7] + $Year = $Matches[8] + + $Component = $Matches[9] + $Context = $Matches[10] + $Type = $Matches[11] + $Thread = $Matches[12] + $File = $Matches[13] + + $Param = @{ + Hour=$Hour + Minute=$Minute + Second=$Second + MilliSecond=$MilliSecond + Year=$Year + Month=$Month + Day=$Day + } + + $LogEntryDateTime = Get-Date @Param + #Write-Host "DEBUG `$LogEntryDateTime: $LogEntryDateTime" -ForegroundColor Yellow + + # This works for humans but does not sort + #$DateTimeToLogFile = "$($Hour):$($Minute):$($Second).$MilliSecondFull $Day/$Month/$Year" + + # This does sorting right way + $DateTimeToLogFile = "$Year-$Month-$Day $($Hour):$($Minute):$($Second).$MilliSecondFull" + + # Create Powershell custom object and add it to list + $LogEntryList.add([PSCustomObject]@{ + 'Index' = $null; + 'LogEntryDateTime' = $LogEntryDateTime; + 'FileName' = $LogFileName; + 'Line' = $LineNumber; + 'DateTime' = $DateTimeToLogFile; + 'Multiline' = ''; + 'ProcessRunTime' = $ProcessRunTime; + 'Message' = $LogMessage; + 'Component' = $Component; + 'Context' = $Context; + 'Type' = $Type; + 'Thread' = $Thread; + 'File' = $File + }) + + } elseif ($CurrentLogEntry -Match $FirstLineOfMultiLineLogRegex) { + # This is start of multiline log entry + # Single line regex did not get results so we are dealing multiline case separately here + + #Write-Host "DEBUG Start of multiline regex: $CurrentLogEntry" -ForegroundColor Yellow + + $MultilineLogEntryStartFound=$True + + # Regex found match + $LogMessage = $Matches[1].Trim() + + $LogEntryDateTime = '' + $DateTimeToLogFile = '' + $Component = '' + $Context = '' + $Type = '' + $Thread = '' + $File = '' + + # Create Powershell custom object and add it to list + $LogEntryList.add([PSCustomObject]@{ + 'Index' = $null; + 'LogEntryDateTime' = $LogEntryDateTime; + 'FileName' = $LogFileName; + 'Line' = $LineNumber; + 'DateTime' = $DateTimeToLogFile; + 'Multiline' = ''; + 'ProcessRunTime' = ''; + 'Message' = $LogMessage; + 'Component' = $Component; + 'Context' = $Context; + 'Type' = $Type; + 'Thread' = $Thread; + 'File' = $File + }) + + $MultilineLogEntryStartsArrayIndex = $LogEntryList.Count - 1 + + } elseif ($CurrentLogEntry -Match $LastLineOfMultiLineLogRegex) { + # This is end of multiline log entry + # Single line regex did not get results so we are dealing multiline case separately here + + # Regex found match + $LogMessage = $Matches[1].Trim() + + $Hour = $Matches[2] + $Minute = $Matches[3] + $Second = $Matches[4] + + $MilliSecondFull = $Matches[5] + # Cut milliseconds to 0-999 + # Time unit is so small that we don't even bother to round the value + $MilliSecond = $MilliSecondFull.Substring(0,3) + + $Month = $Matches[6] + $Day = $Matches[7] + $Year = $Matches[8] + + $Component = $Matches[9] + $Context = $Matches[10] + $Type = $Matches[11] + $Thread = $Matches[12] + $File = $Matches[13] + + $Param = @{ + Hour=$Hour + Minute=$Minute + Second=$Second + MilliSecond=$MilliSecond + Year=$Year + Month=$Month + Day=$Day + } + + $LogEntryDateTime = Get-Date @Param + #Write-Host "DEBUG `$LogEntryDateTime: $LogEntryDateTime" -ForegroundColor Yellow + + # This works for humans but does not sort + #$DateTimeToLogFile = "$($Hour):$($Minute):$($Second).$MilliSecondFull $Day/$Month/$Year" + + # This does sorting right way + $DateTimeToLogFile = "$Year-$Month-$Day $($Hour):$($Minute):$($Second).$MilliSecondFull" + + if($MultilineLogEntryStartFound) { + $Multiline = '---->' + } else { + $Multiline = '' + } + + # Create Powershell custom object and add it to list + $LogEntryList.add([PSCustomObject]@{ + 'Index' = $null; + 'LogEntryDateTime' = $LogEntryDateTime; + 'FileName' = $LogFileName; + 'Line' = $LineNumber; + 'DateTime' = $DateTimeToLogFile; + 'Multiline' = $Multiline; + 'ProcessRunTime' = ''; + 'Message' = $LogMessage; + 'Component' = $Component; + 'Context' = $Context; + 'Type' = $Type; + 'Thread' = $Thread; + 'File' = $File + }) + + # Process previous lines and add DateTime, Multiline, Component, Context, Type, Thread and File + # There can be corrupted log entries so there can be multiline entries without proper multiline start log entry + # So we will add information until we find log entry which has DateTime value + + $MultilineLogEntryEndsArrayIndex = $LogEntryList.Count - 1 + + # Add data to starting multiline log entry object + # DateTime, Component, Context, Type, Thread, File information + $LogEntryList[$MultilineLogEntryStartsArrayIndex].LogEntryDateTime = $LogEntryDateTime + $LogEntryList[$MultilineLogEntryStartsArrayIndex].DateTime = $DateTimeToLogFile + $LogEntryList[$MultilineLogEntryStartsArrayIndex].Multiline = '<----' + $LogEntryList[$MultilineLogEntryStartsArrayIndex].Component = $Component + $LogEntryList[$MultilineLogEntryStartsArrayIndex].Context = $Context + $LogEntryList[$MultilineLogEntryStartsArrayIndex].Type = $Type + $LogEntryList[$MultilineLogEntryStartsArrayIndex].Thread = $Thread + $LogEntryList[$MultilineLogEntryStartsArrayIndex].File = $File + + # Add data to multiline entries which are not end or start entries + #if($MultilineLogEntryEndsArrayIndex - $MultilineLogEntryStartsArrayIndex -gt 1) { + # Multiline entry is more than 2 lines + + # Add DateTime, Component, Context, Type, Thread, File information + # to Multiline log entry from last to first + #for($i=$MultilineLogEntryEndsArrayIndex - 1; $i -gt $MultilineLogEntryStartsArrayIndex ; $i--) { + + # Add information to previous log entries if they don't have DateTime-value + # Stop when we find DateTime-value + $i = $MultilineLogEntryEndsArrayIndex -1 + While($LogEntryList[$i].DateTime -eq $null) { + #Write-Host "DEBUG: Multiline add data: `$MultilineLogEntryStartsArrayIndex=$MultilineLogEntryStartsArrayIndex `$MultilineLogEntryEndsArrayIndex=$MultilineLogEntryEndsArrayIndex `$i=$i `$Message=$($LogEntryList[$i].Message)" + + $LogEntryList[$i].LogEntryDateTime = $LogEntryDateTime + $LogEntryList[$i].DateTime = $DateTimeToLogFile + $LogEntryList[$i].Multiline = ' --- ' + $LogEntryList[$i].Component = $Component + $LogEntryList[$i].Context = $Context + $LogEntryList[$i].Type = $Type + $LogEntryList[$i].Thread = $Thread + $LogEntryList[$i].File = $File + + $i-- + } + + } else { + # We didn't catch log entry with our regex + # This should be multiline log entry but not first or last line in that log entry + # This can also be some line that should be matched with (other) regex + + #Write-Host "DEBUG: $CurrentLogEntry" -ForegroundColor Yellow + + # Create Powershell custom object and add it to list + $LogEntryList.add([PSCustomObject]@{ + 'Index' = $null; + 'LogEntryDateTime' = $null; + 'FileName' = $LogFileName; + 'Line' = $LineNumber; + 'DateTime' = $null; + 'Multiline' = ''; + 'ProcessRunTime' = $null; + 'Message' = $CurrentLogEntry; + 'Component' = ''; + 'Context' = ''; + 'Type' = ''; + 'Thread' = ''; + 'File' = '' + }) + } + + $LineNumber++ + + } # Foreach end single log file line by line foreach + + } # Foreach end all log files + Write-Host "" + + # Check if there was log entries/lines which were not detected + $DroppedLines = $LogEntryList | Where-Object DateTime -eq $null + if($DroppedLines) { + Write-Host "These lines were detected not to have right syntax so they were dropped" -ForegroundColor Yellow + Foreach($Line in $DroppedLines) { + Write-Host "$($Line.FileName) Line $($Line.Line) $($Line.Message)" + + # FIXME Copy timestamp from previous log entry + + } + Write-Host "`n" + } + + + # Take last $LogEntryDateTime and add 1 seconds to it + $ReloadDate = (Get-Date).AddHours(1) + $ReloadDateTime = "$($ReloadDate.Year)-$($ReloadDate.Month)-$($ReloadDate.Day) $($ReloadDate.TimeOfDay)" + + # Add last entry which will reload log file if selected + $LogEntryList.add([PSCustomObject]@{ + 'Index' = $null; + 'LogEntryDateTime' = $ReloadDate; + 'FileName' = ''; + 'Line' = ''; + 'DateTime' = $ReloadDateTime; + 'Multiline' = ''; + 'ProcessRunTime' = ''; + 'Message' = 'RELOAD LOG FILE(S). Select this line and OK button from right botton corner'; + 'Component' = ''; + 'Context' = ''; + 'Type' = ''; + 'Thread' = ''; + 'File' = '' + }) + + + + # Check if First x minutes/hours was selected + if($LogEndDateTimeObject -eq 'Analyzing DateTime later') { + # Sort log entries by DateTime and secondary by Line number + # Find first log entry with LogEntryDateTime value + # There is some rare case there could be log entry with empty value + #Foreach ($LogEntryObject in $LogEntryList | Sort-Object -Property DateTime, Line) { + Foreach ($LogEntryObject in $LogEntryList) { + Try { + $FirstLogEntryTimeDate = Get-Date $LogEntryObject.LogEntryDateTime + Break + } catch { + + } + } + + # And figure out LogEndDateTime based on first log entry timestamp + if($SelectedTimeFrameObject.'Log Start and End time' -eq 'First 30 minutes') { + $LogEndDateTimeObject = ($FirstLogEntryTimeDate).AddMinutes(30) + } + + if($SelectedTimeFrameObject.'Log Start and End time' -eq 'First 1 hour') { + $LogEndDateTimeObject = ($FirstLogEntryTimeDate).AddHours(1) + } + + if($SelectedTimeFrameObject.'Log Start and End time' -eq 'First 2 hours') { + $LogEndDateTimeObject = ($FirstLogEntryTimeDate).AddHours(2) + } + + if($SelectedTimeFrameObject.'Log Start and End time' -eq 'First 6 hours') { + $LogEndDateTimeObject = ($FirstLogEntryTimeDate).AddHours(6) + } + + Write-Host "Log entries End time: $LogEndDateTimeObject`n" + } + + Write-Host "Creating report" + Write-Host "Log entries Start time: $LogStartDateTimeObject" + Write-Host "Log entries End time: $LogEndDateTimeObject`n" + + + # Filter and Sort objects + # We need to sort with DateTime AND Line + # because there can are multiple log entries with same timestamp + #$LogEntryList = $LogEntryList | Sort-Object -Property DateTime, Line + $LogEntryList = $LogEntryList | Where-Object { ($_.LogEntryDateTime -gt $LogStartDateTimeObject) -and ($_.LogEntryDateTime -lt $LogEndDateTimeObject) } | Sort-Object -Property DateTime, Line + + # Add Index property value + # This is used for sorting when doing analyzing in Out-GridView + $index = 1 + Foreach($LogEntry in $LogEntryList) { + + # Use this to find out if we did catch all lines + # Look for the first object index and line numbers that if they match + #Write-host "$index $($LogEntry.Line) $($LogEntry.Message)" + + # DateTime is empty if there was corrupted log entries + # We should set last known timestamp to those but this is just in case we still missed something + if($LogEntry.DateTime) { + $LogEntry.Index = $index + $index++ + } + } + + + # region analyze log entries + ########################## + # Analyze log entries + # + # Try to find end and start log entries for Powershell, Win32App and WinGetApp installations + # We go backwards because if there is process end log entry + # then there has to be start log entry (going upwards) for the same process (same thread in this case) + # + # Start process could be "orphan" if process is stopped without end log entry + # so it is hard to detect these cases (going from up to down) + + $DetectedInEspPhase=$False + $skipDeviceStatusPage='NotFoundYet' + $skipUserStatusPage='NotFoundYet' + $skipDeviceStatusPageIndex=$null + $skipUserStatusPageIndex=$null + $NotInEspInSessionFound=$false + $UserESPFinished=$false + $UserESPFinishedNotFoundWarningMessageShown=$false + + $PreviousLoggedOnUser='NotInitializedYet' + $LoggedOnUserDefaultUser0Shown=$false + + $PowershellScriptStartLogEntryFound=$false + $PowershellScriptStartLogEntryIndex=$null + + # Array + $PowershellScriptErrorLogs = @() + + + $i = 0 + Do { + # Clear variables in every loop round + $LoggedOnUser=$false + + #Write-Host "`$i=$i" + #Write-Host "$($LogEntryList[$i].Message)" + + + # region Find Username and UserSID + if($LogEntryList[$i].Message -Like '`[Win32App`] Expected usersid for session *') { + $Matches=$null + if($LogEntryList[$i].Message -Match '^\[Win32App\] Expected usersid for session (.*) with name (.*) is (.*)$') { + $UserSession = $Matches[1] + $UserName = $Matches[2] + $UserSID = $Matches[3] + + #Write-Host "DEBUG: $($LogEntryList[$i].Message)" + + # Check if we have UserSID object in Hashtable UserId object + if($IdHashtable.ContainsKey($UserSId)) { + $IdHashtable[$UserSId].userSID = $UserSID + $IdHashtable[$UserSId].name = $UserName + $IdHashtable[$UserSId].displayName = $UserName + $IdHashtable[$UserSId].session = $UserSession + + $userId = $IdHashtable[$UserSId].userId + if($userId) { + # Check if we have UserID object in Hashtable + if($IdHashtable.ContainsKey($UserId)) { + # Add Name-value. We don't even check existing value + # because we are already here. + $IdHashtable[$UserId].userSID = $UserSID + $IdHashtable[$UserId].name = $UserName + $IdHashtable[$UserId].displayName = $UserName + $IdHashtable[$UserId].session = $UserSession + } + } + } else { + # UserSID object does not exist in Hashtable + # Create object which we will add to Hashtable + $UserSIDCustomObjectProperties = @{ + id = $null + userId = $null + userSID = $UserSID + name = $UserName + displayName = $UserName + session = $UserSession + } + $UserSIDCustomObject = New-Object -TypeName PSObject -Prop $UserSIDCustomObjectProperties + + # Create new UserId hashtable entry + $IdHashtable.Add($UserSId , $UserSIDCustomObject) + } + + #Write-Host "DEBUG: We should have name Property now`n$($IdHashtable | ConvertTo-Json)" + } + + # Cleanup variables + $UserSession = $null + $UserName = $null + $UserSID= $null + $UserSIDCustomObject = $null + } + # endregion Find UserName and UserSID + + +<# # This code works, but there was log entries with mismatched UserID and UserSID (from different users) + # so took another approach to map UserID, UserSID and UserName + + + # region Find UserId and UserSID + if($LogEntryList[$i].Message -Like '`[Win32App`] EspPreparation starts for userId: *') { + $Matches=$null + if($LogEntryList[$i].Message -Match '^\[Win32App\] EspPreparation starts for userId: (.*) userSID: (.*)$') { + $UserId = $Matches[1] + $UserSID = $Matches[2] + + Write-Host "DEBUG: $($LogEntryList[$i].Message)" + + # Add information to $IdHashtable UserId key + if($IdHashtable.ContainsKey($UserId)) { + + # Add UserID and UserSID -values. We don't even check existing value + # because we are already here. + $IdHashtable[$UserId].Id = $UserId + $IdHashtable[$UserId].userId = $UserId + $IdHashtable[$UserId].userSID = $UserSID + + } else { + # Hashtable does NOT have UserId object + + if($IdHashtable.ContainsKey($UserSId)) { + $UserName = $IdHashtable[$UserSId].name + $Session = $IdHashtable[$UserSId].session + } else { + $UserName = $UserId + $Session = $null + } + + # Create object which we will add to Hashtable + $UserIdCustomObjectProperties = @{ + id = $UserId + userId = $UserId + userSID = $UserSID + name = $UserName + session = $Session + } + + $UserIdCustomObject = New-Object -TypeName PSObject -Prop $UserIdCustomObjectProperties + + # Create new UserId hashtable entry + $IdHashtable.Add($UserId , $UserIdCustomObject) + } + + # Add information to $IdHashtable UserSID key + if($IdHashtable.ContainsKey($UserSID)) { + + if($IdHashtable.ContainsKey($UserId)) { + $UserName = $IdHashtable[$UserId].name + $Session = $IdHashtable[$UserId].session + } else { + $UserName = $UserId + $Session = $null + } + + # Add UserId-value. We don't even check existing value + # because we are already here. + $IdHashtable[$UserSId].id = $UserId + $IdHashtable[$UserSId].userId = $UserId + $IdHashtable[$UserSId].userSId = $UserSId + $IdHashtable[$UserSId].name = $UserName + $IdHashtable[$UserSId].session = $Session + + } else { + # UserSID object does not exist in Hashtable + # Create object which we will add to Hashtable + $UserSIDCustomObjectProperties = @{ + id = $UserId + userId = $UserId + userSID = $UserSID + name = $UserName + session = $Session + } + $UserSIDCustomObject = New-Object -TypeName PSObject -Prop $UserSIDCustomObjectProperties + + # Create new UserId hashtable entry + $IdHashtable.Add($UserSId , $UserSIDCustomObject) + } + } + + Write-Host "DEBUG: We should have UserID and UserSID`n$($IdHashtable | ConvertTo-Json)" + + # Cleanup variables + $UserIdCustomObject = $null + $UserSIDCustomObject = $null + $UserId = $null + $UserSID = $null + + } + # endregion Find UserId and UserSID +#> + + # region EMS Agent Started/Stopped/received shutdown signal + # Excluded -or ($LogEntryList[$i].Message -eq 'EMS Agent received shutdown signal') + if(($LogEntryList[$i].Message -eq 'EMS Agent Started') -or ($LogEntryList[$i].Message -eq 'EMS Agent Stopped')) { + + $LogEntryList[$i].ProcessRunTime = "$($LogEntryList[$i].Message)" + #Write-Host "$($LogEntryList[$i].DateTime) $($LogEntryList[$i].Message) ($($LogEntryList[$i].FileName) line $($LogEntryList[$i].Line))" + + RecordStatusToTimeline -date $LogEntryList[$i].DateTime -status 'Info' -detail $LogEntryList[$i].Message -logEntry "Line $($LogEntryList[$i].Line)" + + # Continue to next line in Do-while -loop + $i++ + Continue + } + # endregion EMS Agent Started/Stopped/received shutdown signal + + # region Find ESP SkipESPStatusPage(s) + if($skipDeviceStatusPage -eq 'NotFoundYet') { + $Matches=$null + if($LogEntryList[$i].Message -Match '^\[Win32App\] CheckDeviceOnlyFromFirstSyncReg set skipDeviceStatusPage: (.*)$') { + $skipDeviceStatusPage = $Matches[1] + $LogEntryList[$i].ProcessRunTime = "Skip Device ESP: $skipDeviceStatusPage" + + $skipDeviceStatusPageIndex=$i + + # Continue to next line in Do-while -loop + $i++ + Continue + } + } + + # region Find ESP SkipESPStatusPage(s) + if($skipUserStatusPage -eq 'NotFoundYet') { + $Matches=$null + if($LogEntryList[$i].Message -Match '^\[Win32App\] CheckDeviceOnlyFromFirstSyncReg set skipUserStatusPage: (.*)$') { + $skipUserStatusPage = $Matches[1] + $LogEntryList[$i].ProcessRunTime = "Skip User ESP : $skipUserStatusPage" + + $skipUserStatusPageIndex=$i + + # Continue to next line in Do-while -loop + $i++ + Continue + } + } + # endregion Find ESP SkipESPStatusPage(s) + + + # region Find ESP (Enrollment Status Page) phases + if(-not $DetectedInEspPhase) { + if($LogEntryList[$i].Message -eq '[Win32App] In EspPhase: DeviceSetup') { + $DetectedInEspPhase = $True + + # This is here because these lines exists later after ESP + # So only showing this information if we are in ESP + if($skipDeviceStatusPage -eq 'True') { + + RecordStatusToTimeline -date $LogEntryList[$skipDeviceStatusPageIndex].DateTime -status "Info" -detail "Skip Device ESP: $skipDeviceStatusPage" -logEntry "Line $($LogEntryList[$skipDeviceStatusPageIndex].Line)" -color 'Yellow' + + } else { + + RecordStatusToTimeline -date $LogEntryList[$skipDeviceStatusPageIndex].DateTime -status "Info" -detail "Skip Device ESP: $skipDeviceStatusPage" -logEntry "Line $($LogEntryList[$skipDeviceStatusPageIndex].Line)" + } + + # This is here because these lines exists later after ESP + # So only showing this information if we are in ESP + if($skipUserStatusPage -eq 'True') { + + RecordStatusToTimeline -date $LogEntryList[$skipUserStatusPageIndex].DateTime -status "Info" -detail "Skip User ESP: $skipUserStatusPage" -logEntry "Line $($LogEntryList[$skipUserStatusPageIndex].Line)" -color 'Yellow' + } else { + + RecordStatusToTimeline -date $LogEntryList[$skipUserStatusPageIndex].DateTime -status "Info" -detail "Skip User ESP: $skipUserStatusPage" -logEntry "Line $($LogEntryList[$skipUserStatusPageIndex].Line)" + } + + $LogEntryList[$i].ProcessRunTime = "[Win32App] In Device ESP phase detected" + + RecordStatusToTimeline -date $LogEntryList[$i].DateTime -status "Info" -detail "######################## Device ESP phase ########################" -logEntry "Line $($LogEntryList[$i].Line)" + + # Continue to next line in Do-while -loop + $i++ + Continue + } + } + # endregion Find ESP (Enrollment Status Page) phases + + # region User ESP finished + if(-not $UserESPFinished) { + if($LogEntryList[$i].Message -like '`[Win32App`] In EspPhase: AccountSetup. All Win32App has been added. Set SidecarReady: True*') { + $UserESPFinished = $True + + $LogEntryList[$i].ProcessRunTime = "User ESP finished successfully" + + RecordStatusToTimeline -date $LogEntryList[$i].DateTime -status "Info" -detail "######################## User ESP finished ########################" -logEntry "Line $($LogEntryList[$i].Line)" + + # Continue to next line in Do-while -loop + $i++ + Continue + } + } + # endregion User ESP finished + + + # region NotInEsp in session + if(-not $NotInEspInSessionFound) { + if($LogEntryList[$i].Message -eq '[Win32App] The EspPhase: NotInEsp in session') { + $NotInEspInSessionFound = $True + + $LogEntryList[$i].ProcessRunTime = "[Win32App] The EspPhase: NotInEsp in session" + + RecordStatusToTimeline -date $LogEntryList[$i].DateTime -status "Info" -detail "######################## Not in ESP detected ########################" -logEntry "Line $($LogEntryList[$i].Line)" + + # Continue to next line in Do-while -loop + $i++ + Continue + } + } + # endregion NotInEsp in session + + # region NotInESP found but did not find User ESP Finished + # Show warning on this case. Did User ESP fail ??? + if((-not $UserESPFinishedNotFoundWarningMessageShown) -and ($DetectedInEspPhase) -and ((-not $UserESPFinished) -and ($NotInEspInSessionFound))) { + $UserESPFinishedNotFoundWarningMessageShown=$True + + RecordStatusToTimeline -date $LogEntryList[$i].DateTime -status "Warning" -detail "Did not detect User ESP finished. This is usually falsepositive warning." -logEntry "Line $($LogEntryList[$i].Line)" -Color 'Yellow' + + } + # endregion NotInESP found but did not find User ESP Finished + + # region Find Logged on user + if($LogEntryList[$i].Message -Like 'After impersonation: *') { + $Matches=$null + if($LogEntryList[$i].Message -Match '^After impersonation: (.*)$') { + $LoggedOnUser = $Matches[1] + + #Write-Host "DEBUG: `$FirstLoggedOnUser`=$FirstLoggedOnUser" + # Skip if defaultuser0 found (we are in device ESP phase still) + if(-not $LoggedOnUserDefaultUser0Shown) { + if($LoggedOnUser -like '*\defaultuser0') { + $LoggedOnUserDefaultUser0Shown = $True + + RecordStatusToTimeline -date $LogEntryList[$i].DateTime -status "User Logon" -detail "Logged on user : $LoggedOnUser" -logEntry "Line $($LogEntryList[$i].Line)" -Color 'Yellow' + + # Set LoggedOnUser information to ProcessRuntime property + $LogEntryList[$i].ProcessRunTime = "Logged on user: $LoggedOnUser" + + $i++ + Continue + } + } elseif (($LoggedOnUser -notlike '*\defaultuser0') -and ($LoggedOnUser -ne $PreviousLoggedOnUser)) { + $PreviousLoggedOnUser=$LoggedOnUser + + RecordStatusToTimeline -date $LogEntryList[$i].DateTime -status "User Logon" -detail "Logged on user : $LoggedOnUser" -logEntry "Line $($LogEntryList[$i].Line)" -Color 'Yellow' + + $LogEntryList[$i].ProcessRunTime = "Logged on user: $LoggedOnUser" + + } + + + # Not sure if we want to run this everytime or not + # Now running it every time we find this log entry + if($True) { + # Try to find user session from previous log entry + if($LogEntryList[$i-1].Message -Match '^Starting impersonation, session id = (.*)$') { + $ImpersonationSessionId = $Matches[1] + + #Write-Host "Found ImpersonationSessionId $ImpersonationSessionId" + + # Try to find UserId from either of following lines + # [PowerShell] Processing user session 3, user id = cdc3177c-df70-45e3-a469-4ef1be2b8345 + # [PowerShell] Get 3 policies for user cdc3177c-df70-45e3-a469-4ef1be2b8345 in session 3 + $b = $i + 1 + While(($LogEntryList[$b].Message -notlike 'After impersonation: *') -and ($b -lt $LogEntryList.Count)) { + #Write-Host "DEBUG: Processing line: $($LogEntryList[$b].Message)" + + if(($LogEntryList[$i].Thread -eq $LogEntryList[$b].Thread) -and (($LogEntryList[$b].Message -Match '^\[PowerShell\] Processing user session (.*), user id = (.*)$') -or ($LogEntryList[$b].Message -Match '^\[PowerShell\] Get (.*) policies for user (.*) in session (.*)$') -or ($LogEntryList[$b].Message -Match '^\[Win32App\] Got (.*) Win32App(s) for user (.*) in session (.*)$'))) { + #Write-Host "DEBUG: Found matching line: $($LogEntryList[$b].Message)" + + # Save to temporary variables because next -Match will overwrite existing $Matches -variable + $Matches1 = $Matches[1] + $Matches2 = $Matches[2] + + # Either one is GUID so we need to test which $Matches variable is valid GUID and which is sessionId + if($Matches1 -Match '[[0-9a-fA-F]{8}-([0-9a-fA-F]{4}-){3}[0-9a-fA-F]{12}') { + $UserId = $Matches1 + $UserSession = $Matches2 + } elseif ($Matches2 -Match '[[0-9a-fA-F]{8}-([0-9a-fA-F]{4}-){3}[0-9a-fA-F]{12}') { + $UserSession = $Matches1 + $UserId = $Matches2 + } else { + # Something strange happened and we did not get GUID + # We should not be here + $UserSession = $null + $UserId = $null + } + + #Write-Host "`$UserId=$UserId" + #Write-Host "`$UserSession=$UserSession" + #Write-Host "`$ImpersonationSessionId=$ImpersonationSessionId" + + if($ImpersonationSessionId -eq $UserSession) { + # We are quite sure we have right id for UserName + + # UserSID object does not exist in Hashtable + # Create object which we will add to Hashtable + if($IdHashtable.ContainsKey($UserId)) { + $IdHashtable[$UserId].id = $UserId + $IdHashtable[$UserId].userId = $UserId + $IdHashtable[$UserId].name = $LoggedOnUser + $IdHashtable[$UserId].displayName = $LoggedOnUser + $IdHashtable[$UserId].session = $UserSession + + } else { + $UserIdCustomObjectProperties = @{ + id = $UserId + userId = $UserId + userSID = $null + name = $LoggedOnUser + displayName = $LoggedOnUser + session = $UserSession + } + $UserIdCustomObject = New-Object -TypeName PSObject -Prop $UserIdCustomObjectProperties + + # Create new UserId hashtable entry + $IdHashtable.Add($UserId , $UserIdCustomObject) + } + + #Write-Host "DEBUG: We should have UserID and Username`n$($IdHashtable | ConvertTo-Json)" + } + + # Break out from While-loop + Break + } + $b++ + } + } + } + + + # Continue to next line in Do-while -loop + $i++ + Continue + } + } + # endregion Find Logged on user + + + # region Find Powershell Script Start log entry + if($LogEntryList[$i].Message -like '`[PowerShell`] Processing policy with id = *') { + if($LogEntryList[$i].Message -Match '^\[PowerShell\] Processing policy with id = (.*) for user (.*)$') { + $PowershellScriptPolicyIdStart = $Matches[1] + $PowershellScriptForUserIdStart = $Matches[2] + + $PowershellScriptStartLogEntryIndex=$i + + # This variables is probably not needed because + # we can detect end and start log entries separately + $PowershellScriptStartLogEntryFound=$True + + if($PowershellScriptForUserIdStart -eq '00000000-0000-0000-0000-000000000000') { + $PowershellScriptForUserNameStart = 'System' + } else { + + # Get real name if found from Hashtable + if($IdHashtable.ContainsKey($PowershellScriptForUserIdStart)) { + $Name = $IdHashtable[$PowershellScriptForUserIdStart].name + if($Name) { + $PowershellScriptForUserNameStart = $IdHashtable[$PowershellScriptForUserIdStart].name + } else { + $PowershellScriptForUserNameStart = $PowershellScriptForUserIdStart + } + } else { + $PowershellScriptForUserNameStart = $PowershellScriptForUserIdStart + } + } + + # Get Powershell script name if parameter -Online was specified + if($IdHashtable.ContainsKey($PowershellScriptPolicyIdStart)) { + $PowershellScriptPolicyNameStart = $IdHashtable[$PowershellScriptPolicyIdStart].name + } else { + $PowershellScriptPolicyNameStart = $PowershellScriptPolicyIdStart + } + + if($ShowAllTimelineEntries) { + + RecordStatusToTimeline -date $LogEntryList[$i].DateTime -status "Process / Powershell" -detail "Powershell script : $PowershellScriptPolicyNameStart for user $PowershellScriptForUserNameStart" -logEntry "Line $($LogEntryList[$i].Line)" + } + + # Set Powershell Script Start information to ProcessRuntime property + $LogEntryList[$i].ProcessRunTime = "Processing Powershell script ($PowershellScriptPolicyNameStart) for user $PowershellScriptForUserNameStart" + + # Continue to next line in Do-while -loop + $i++ + Continue + } + } + # endregion Find Powershell Script Start log entry + + # region Find Powershell Script End log entry + if($LogEntryList[$i].Message -like '`[PowerShell`] User Id = *') { + if($LogEntryList[$i].Message -Match '^\[PowerShell\] User Id = (.*), Policy id = (.*), policy result = (.*)$') { + $PowershellScriptForUserIdEndProcess = $Matches[1] + $PowershellScriptPolicyIdEndProcess = $Matches[2] + $PowershellScriptPolicyResultEndProcess = $Matches[3] + + # This variables is probably not needed because + # we can detect end and start log entries separately + $PowershellScriptEndLogEntryFound=$True + + if($PowershellScriptForUserIdEndProcess -eq '00000000-0000-0000-0000-000000000000') { + $PowershellScriptForUserNameEndProcess = 'System' + } else { + $PowershellScriptForUserNameEndProcess = $PowershellScriptForUserIdEndProcess + + # Get real name if found from Hashtable + if($IdHashtable.ContainsKey($PowershellScriptForUserIdEndProcess)) { + $Name = $IdHashtable[$PowershellScriptForUserIdEndProcess].name + if($Name) { + $PowershellScriptForUserNameEndProcess = $IdHashtable[$PowershellScriptForUserIdEndProcess].name + } + } + } + + # Get Powershell script name if parameter -Online was specified + if($IdHashtable.ContainsKey($PowershellScriptPolicyIdEndProcess)) { + $PowershellScriptPolicyNameEnd = $IdHashtable[$PowershellScriptPolicyIdEndProcess].name + } else { + $PowershellScriptPolicyNameEnd = $PowershellScriptPolicyIdEndProcess + } + + # Get Process runtime + if($PowershellScriptStartLogEntryIndex) { + $PowershellScriptStartTime = $LogEntryList[$PowershellScriptStartLogEntryIndex].LogEntryDateTime + $PowershellScriptEndTime = $LogEntryList[$i].LogEntryDateTime + + $PowershellScriptRunTime = New-Timespan $PowershellScriptStartTime $PowershellScriptEndTime + $PowershellScriptRunTimeTotalSeconds = $PowershellScriptRunTime.TotalSeconds + + + } else { + $PowershellScriptRunTimeTotalSeconds = 'N/A' + } + + if($PowershellScriptPolicyResultEndProcess -eq 'Success') { + + RecordStatusToTimeline -date $LogEntryList[$i].DateTime -status "Success / Powershell" -detail "Powershell script : $PowershellScriptPolicyNameEnd for user $PowershellScriptForUserNameEndProcess" -Seconds $PowershellScriptRunTimeTotalSeconds -logEntry "Line $($LogEntryList[$i].Line)" -Color 'Green' + + # Check if Powershell script runtime is longer than configured (default 180 seconds) + if($PowershellScriptRunTimeTotalSeconds -gt $LongRunningPowershellNotifyThreshold) { + + RecordStatusToTimeline -date $LogEntryList[$i].DateTime -status "Warning" -detail " Long Powershell script runtime found" -Seconds $PowershellScriptRunTimeTotalSeconds -logEntry "Line $($LogEntryList[$i].Line)" -Color 'Yellow' + } + + # Set Powershell Script End information to ProcessRuntime property + $LogEntryList[$i].ProcessRunTime = "Succeeded Powershell script ($PowershellScriptPolicyNameEnd) for user $PowershellScriptForUserNameEndProcess (Runtime: $PowershellScriptRunTimeTotalSeconds seconds)" + + } elseif ($PowershellScriptPolicyResultEndProcess -eq 'Failed') { + + RecordStatusToTimeline -date $LogEntryList[$i].DateTime -status "Failed / Powershell" -detail "Powershell script : $PowershellScriptPolicyNameEnd for user $PowershellScriptForUserNameEndProcess" -Seconds $PowershellScriptRunTimeTotalSeconds -logEntry "Line $($LogEntryList[$i].Line)" -Color 'Red' + + # Set Powershell Script End information to ProcessRuntime property + $LogEntryList[$i].ProcessRunTime = "Failed Powershell script ($PowershellScriptPolicyNameEnd) for user $PowershellScriptForUserNameEndProcess (Runtime: $PowershellScriptRunTimeTotalSeconds seconds)" + + # Check if there is error message for failed Powershell script + + # Check if next log entry exists + if($i + 1 -le $LogEntryList.Count - 1) { + if($LogEntryList[$i+1].Message -Match '^\[PowerShell\] Fail, the details are (.*)$') { + $PowershellFailJsonString = $Matches[1] + + # Try to convert JsonString + Try { + $PowershellFailFromJson=$null + $PowershellFailFromJson = $PowershellFailJsonString | ConvertFrom-Json + if($ExecutionMsg=$PowershellFailFromJson.ExecutionMsg) { + if($ExecutionMsg) { + # This makes time line harder to read so moved Error message after timeline + #Write-Host "$($LogEntryList[$i+1].DateTime) Powershell script error message $ExecutionMsg ($($LogEntryList[$i].FileName) line $($LogEntryList[$i+1].Line))" -ForegroundColor Red + + $PowershellScriptErrorLogs += "$($LogEntryList[$i].DateTime) End Powershell script ($PowershellScriptPolicyNameEnd) for user $PowershellScriptForUserNameEndProcess Success: $PowershellScriptPolicyResultEndProcess (Runtime: $PowershellScriptRunTimeTotalSeconds seconds) ($($LogEntryList[$i].FileName) line $($LogEntryList[$i].Line))" + $PowershellScriptErrorLogs += "$($LogEntryList[$i+1].DateTime) Powershell script error message ($($LogEntryList[$i].FileName) line $($LogEntryList[$i+1].Line))" + + # Split Error Message by newline and enter error message line by line + $ExecutionMsg.Split("`n") | Foreach-Object { + $ErrorMessageLine = $_ + #$PowershellScriptErrorLogs += "`t`t`t $ErrorMessageLine" + $PowershellScriptErrorLogs += "$ErrorMessageLine" + } + + if($ShowErrorsInTimeline) { + $ExecutionMsg.Split("`n") | Foreach-Object { + $ErrorMessageLine = $_ + #Write-Host "`t`t`t $ErrorMessageLine" -ForegroundColor Red + + RecordStatusToTimeline -date $LogEntryList[$i].DateTime -status "ErrorLog" -detail "$ErrorMessageLine" -logEntry "$($LogEntryList[$i].FileName) line $($LogEntryList[$i].Line)" -Color 'Red' + } + } else { + + RecordStatusToTimeline -date $LogEntryList[$i].DateTime -status "Info" -detail "Errors found. Use Parameter -ShowErrorsInTimeline to show errors on Timeline" -logEntry "Line $($LogEntryList[$i].Line)" -Color 'Yellow' + + } + } + } + } catch { + # Did not find Powershell error message + } + } + } + + } else { + + RecordStatusToTimeline -date $LogEntryList[$i].DateTime -status "Script ended" -detail "Powershell script $PowershellScriptPolicyNameEnd for user $PowershellScriptForUserNameEndProcess ($PowershellScriptRunTimeTotalSeconds seconds)" -logEntry "Line $($LogEntryList[$i].Line)" -Color 'Yellow' + } + #Write-Host "" + + $PowershellScriptStartLogEntryIndex=$null + + # Continue to next line in Do-while -loop + $i++ + Continue + } + } + # endregion Find Powershell Script End log entry + + +<# + # Remove me from Production + # region Find Powershell processess + # Try to find Powershell process End log entry + if($LogEntryList[$i].Message -like '*execution is done*') { + $ProcessStartTime = $null + $ProcessEndTime = $LogEntryList[$i].LogEntryDateTime + $Thread = $LogEntryList[$i].Thread + Write-Verbose "Found Powershell process Ending message (Line $($LogEntryList[$i].Line) Thread $Thread): $($LogEntryList[$i].Message)" + + # Try to find Powershell process Start log entry (upwards) + $b = $i - 1 + for($b = $i - 1; $b -ge 0; $b-- ) { + if(($LogEntryList[$b].Message -like '*process id =*') -and ($LogEntryList[$b].Thread -eq $Thread)) { + Write-Verbose "Found Powershell process Starting message (Line $($LogEntryList[$b].Line) Thread $($LogEntryList[$b].Thread)): $($LogEntryList[$b].Message)" + $ProcessStartTime = $LogEntryList[$b].LogEntryDateTime + $ProcessRunTime = New-Timespan $ProcessStartTime $ProcessEndTime + $ProcessRunTimeTotalSeconds = $ProcessRunTime.TotalSeconds + $ProcessRunTimeText = "{0,-16} {1,8:n1}" -f "End Powershell", $ProcessRunTimeTotalSeconds + + # Set RunTime to Powershell script Install Start log entry + $LogEntryList[$b].ProcessRunTime = "{0,-16} {1,8:n1}" -f "Start Powershell", $ProcessRunTimeTotalSeconds + + # Set RunTime to Powershell script Install End log entry + $LogEntryList[$i].ProcessRunTime = "{0,-16} {1,8:n1}" -f "End Powershell", $ProcessRunTimeTotalSeconds + + + # Show warning for long running Powershell script +# if($ProcessRunTimeTotalSeconds -ge $LongRunningPowershellNotifyThreshold) { +# Write-Host "$($LogEntryList[$b].DateTime) Long running ($ProcessRunTimeTotalSeconds seconds) Powershell script ($LogFileName line $($LogEntryList[$b].Line)): $($LogEntryList[$b].Message)" -ForegroundColor Yellow +# } + + + # Break out from for-loop + Break + } + } # For-loop end + Write-Verbose "" + + # Cleanup variables + $ProcessStartTime = $null + $ProcessEndTime = $null + $Thread = $null + $ProcessRunTimeTotalSeconds = $null + $ProcessRunTimeText = $null + } + # endregion Find Powershell processess + # Remove me from Production +#> + + ############################################### + + # region Find Proactive Remediation Detect script type - Proactive Remediation or custom Compliance + if($LogEntryList[$i].Message -like '`[HS`] ProcessScript PolicyId:*') { + # Proactive Remediation script = policyType 6 + # custom Compliance script = policyType 8 + + #Write-Host "Proactive Remediation Detect script type found" -ForegroundColor Yellow + $Matches=$null + if($LogEntryList[$i].Message -Match '^\[HS\] ProcessScript PolicyId: (.*) PolicyType: (.*)$') { + $PolicyId = $Matches[1] + $PolicyType = $Matches[2] + + # Check if PolicyId is in Hashtable + if($IdHashtable.ContainsKey($PolicyId)) { + # This can exist if -Online parameter was used + + if($IdHashtable[$PolicyId] | Get-Member policyType){ + # policyType Property exists in Hashtable object + $IdHashtable[$PolicyId].policyType = $PolicyType + } else { + # policyType Property does NOT exist in Hashtable object + # Add new Property + $IdHashtable[$PolicyId] | Add-Member -MemberType noteProperty -Name policyType -Value $PolicyType + } + + } else { + # Add new Hashtable id entry + $PolicyObjectProperties = @{ + id = $PolicyId + policyType = $PolicyType + } + $PolicyCustomObject = New-Object -TypeName PSObject -Prop $PolicyObjectProperties + + # Create new UserId hashtable entry + $IdHashtable.Add($PolicyId , $PolicyCustomObject) + } + } + } + # endregion Find Proactive Remediation Detect script type - Proactive Remediation or custom Compliance + + # region Try to find Proactive Detection script run + if($LogEntryList[$i].Message -like '`[HS`] the pre-rem*diation detection script compliance result for*') { + #Write-Host "Proactive Remediation or custom Compliance Script Detect found" -ForegroundColor Yellow + $Matches=$null + if($LogEntryList[$i].Message -Match '^\[HS\] the pre-remdiation detection script compliance result for (.*) is (.*)$') { + $PolicyIdEnd = $Matches[1] + $PreRemediationDetectResult = $Matches[2] + + # Use this PolicyId on next phase -> Remediate + # Because there we don't have 2 log entries with PolicyId + $ProactiveRemediationPolicyId = $PolicyIdStart + + $ProcessStartTime = $null + $DetectionLogEntryEndTime = $LogEntryList[$i].LogEntryDateTime + $Thread = $LogEntryList[$i].Thread + + # Try to find displayName + if($IdHashtable.ContainsKey($PolicyIdEnd)) { + if($IdHashtable[$PolicyIdEnd].displayName) { + $ProactiveRemediationScriptName = $IdHashtable[$PolicyIdEnd].displayName + } else { + $ProactiveRemediationScriptName = $PolicyIdEnd + } + } + + # Try to find policyType + if($IdHashtable.ContainsKey($PolicyIdEnd)) { + $ProactiveRemediationScriptPolicyType = $IdHashtable[$PolicyIdEnd].policyType + } + + if($ProactiveRemediationScriptPolicyType -eq 6) { + $ProactiveRemediationScriptPolicyTypeName = 'Proactive Remediation' + } elseif ($ProactiveRemediationScriptPolicyType -eq 8) { + $ProactiveRemediationScriptPolicyTypeName = 'Custom Compliance' + } else { + # We should never get here because we have seen polityType log entry before this log entry + # Proactive Remediation script type is unknown + $ProactiveRemediationScriptPolicyTypeName = 'Proactive Remediation script (unknown type)' + } + + # Check if next log entry is either + # [HS] remediation is not optional, kick off remediation script + # [HS] no remediation script, skip remediation script + if(($i+1 -lt $LogEntryList.Count) -and (($LogEntryList[$i+1].Message -like '`[HS`] remediation is not optional, kick off remediation script') -or ($LogEntryList[$i+1].Message -like '`[HS`] no remediation script, skip remediation script'))) { + $RemediationDetectPostActionMessageIndex = $i + 1 + } else { + $RemediationDetectPostActionMessageIndex = $null + } + + Write-Verbose "Found $ProactiveRemediationScriptPolicyTypeName ($ProactiveRemediationScriptName) Detect Ending message ($($LogEntryList[$i].FileName) Line $($LogEntryList[$i].Line) Thread $Thread): $($LogEntryList[$i].Message)" + + $ProactiveRemediationDetectStdOutput = $null + $ProactiveRemediationDetectStdOutIndex = $null + $ProactiveRemediationDetectErrorOutput = $null + $ProactiveRemediationDetectErrorOutputIndex = $null + + $PowershellScriptStartTime = $null + $PowershellScriptEndTime = $null + $PowershellScriptStartIndex = $null + $PowershellScriptEndIndex = $null + + $ProactiveRemediationDetectProcessId = $null + $ProcessRunTimeTotalSeconds = $null + $ProactiveRemediationDetectRunAs = 'N/A' + + # Try to find Proactive Remediation Detect Start log entry (going upwards) + $b = $i - 1 + for($b = $i - 1; $b -ge 0; $b-- ) { + if(($LogEntryList[$b].Message -Match '^\[HS\] std output =(.*)$') -and ($LogEntryList[$b].Thread -eq $Thread)) { + $ProactiveRemediationDetectStdOutput = ($Matches[1]).Trim() + $ProactiveRemediationDetectStdOutIndex = $b + # Continue to previous log entry + Continue + } + + if(($LogEntryList[$b].Message -Match '^\[HS\] err output =(.*)$') -and ($LogEntryList[$b].Thread -eq $Thread)) { + $ProactiveRemediationDetectErrorOutput = ($Matches[1]).Trim() + $ProactiveRemediationDetectErrorOutputIndex = $b + # Continue to previous log entry + Continue + } + + if(($LogEntryList[$b].Message -Match '^Powershell execution is done, exitCode = (.*)$') -and ($LogEntryList[$b].Thread -eq $Thread)) { + $ProactiveRemediationDetectExitCode = $Matches[1] + + # Set RunTime to Powershell script Install End log entry + $LogEntryList[$b].ProcessRunTime = "End Proactive Detect Powershell (ExitCode=$ProactiveRemediationDetectExitCode)" + + $PowershellScriptEndTime = $LogEntryList[$b].LogEntryDateTime + $PowershellScriptEndIndex = $b + + # Continue to previous log entry + Continue + } + + if(($LogEntryList[$b].Message -Match '^process id = (.*)$') -and ($LogEntryList[$b].Thread -eq $Thread)) { + $ProactiveRemediationDetectProcessId = $Matches[1] + + # Set RunTime to Powershell script Install End log entry + $LogEntryList[$b].ProcessRunTime = "Start Proactive Detect Powershell" + + $PowershellScriptStartTime = $LogEntryList[$b].LogEntryDateTime + + # Calculate Powershell script runtime + if($PowershellScriptStartTime -and $PowershellScriptEndTime) { + $PowershellScriptRuntime = New-Timespan $PowershellScriptStartTime $PowershellScriptEndTime + $ProcessRunTimeTotalSeconds = $PowershellScriptRuntime.TotalSeconds + } else { + $ProcessRunTimeTotalSeconds = 'N/A' + } + + $PowershellScriptStartIndex = $b + + # This if seems not to work + # Left here if it would be fixed at some time + if($ProactiveRemediationScriptPolicyType -eq 8) { + # Custom Compliance Script + + # Set RunTime to Powershell script Start log entry + $LogEntryList[$b].ProcessRunTime = "{0,-16} {1,8:n1}" -f "Start Custom Compliance Powershell", $ProcessRunTimeTotalSeconds + + # Add RunTime to Powershell script End log entry + $LogEntryList[$PowershellScriptEndIndex].ProcessRunTime = "{0,-16} {1,8:n1}" -f "End Custom Compliance Powershell", $ProcessRunTimeTotalSeconds + } else { + # Proactive Remediation script + + # Set RunTime to Powershell script Start log entry + $LogEntryList[$b].ProcessRunTime = "{0,-16} {1,8:n1}" -f "Start Proactive Detect Powershell", $ProcessRunTimeTotalSeconds + + # Add RunTime to Powershell script End log entry + $LogEntryList[$PowershellScriptEndIndex].ProcessRunTime = "{0,-16} {1,8:n1}" -f "End Proactive Detect Powershell", $ProcessRunTimeTotalSeconds + } + + # Continue to previous log entry + Continue + } + + if(($LogEntryList[$b].Message -Match '^\[HS\] The policy needs be run as (.*)$') -and ($LogEntryList[$b].Thread -eq $Thread)) { + $ProactiveRemediationDetectRunAs = $Matches[1] + + # Continue to previous log entry + Continue + } + + # Custom Compliance policy name is shorter than Proactive Remediations + # Add padding so we can align texts + # And Yes, I should use -f formatting on these but maybe next time :) + if($ProactiveRemediationScriptPolicyType -eq 8) { + $Padding = ' ' + } else { + $Padding = '' + } + + if(($LogEntryList[$b].Message -Match '^\[HS\] Processing policy with id = (.*)$') -and ($LogEntryList[$b].Thread -eq $Thread)) { + $PolicyIdStart = $Matches[1] + + # $b is startIndex + + if($PolicyIdStart -eq $PolicyIdEnd) { + # We found matching starting log entry + $DetectionLogEntryStartTime = $LogEntryList[$b].LogEntryDateTime + if($DetectionLogEntryStartTime -and $DetectionLogEntryEndTime) { + $ProactiveDetectionRuntime = New-Timespan $DetectionLogEntryStartTime $DetectionLogEntryEndTime + $ProcessRunTimeTotalSeconds = $ProactiveDetectionRuntime.TotalSeconds + } else { + $ProcessRunTimeTotalSeconds = 'N/A' + } + + + + # Start message to timeline + if($ShowAllTimelineEntries) { + RecordStatusToTimeline -date $LogEntryList[$b].DateTime -status "Process / Detect" -detail "$ProactiveRemediationScriptPolicyTypeName $($Padding): $ProactiveRemediationScriptName" -Seconds $ProcessRunTimeTotalSeconds -logEntry "Line $($LogEntryList[$b].Line)" + } + + # Set RunTime log entry + $LogEntryList[$b].ProcessRunTime = "Processing $ProactiveRemediationScriptPolicyTypeName Detect script ($ProactiveRemediationScriptName) (run as $ProactiveRemediationDetectRunAs)" + + + # End message to Timeline + if($PreRemediationDetectResult -eq 'True') { + + RecordStatusToTimeline -date $LogEntryList[$i].DateTime -status "Success / Detect" -detail "$ProactiveRemediationScriptPolicyTypeName $($Padding): $ProactiveRemediationScriptName" -Seconds $ProcessRunTimeTotalSeconds -Color 'Green' -logEntry "Line $($LogEntryList[$i].Line)" + + # Set RunTime log entry + $LogEntryList[$i].ProcessRunTime = "Succeeded $ProactiveRemediationScriptPolicyTypeName Detect script ($ProactiveRemediationScriptName) (Runtime: $ProcessRunTimeTotalSeconds sec)" + + } else { + + RecordStatusToTimeline -date $LogEntryList[$i].DateTime -status "Failed / Detect" -detail "$ProactiveRemediationScriptPolicyTypeName $($Padding): $ProactiveRemediationScriptName" -Seconds $ProcessRunTimeTotalSeconds -Color 'Yellow' -logEntry "Line $($LogEntryList[$i].Line)" + + $LogEntryList[$i].ProcessRunTime = "Failed $ProactiveRemediationScriptPolicyTypeName Detect script ($ProactiveRemediationScriptName) (Runtime: $ProcessRunTimeTotalSeconds sec)" + } + + + # Script StdOut to Timeline if configured and if exists + if($ShowStdOutInTimeline -and $ProactiveRemediationDetectStdOutIndex) { + + RecordStatusToTimeline -date $LogEntryList[$ProactiveRemediationDetectStdOutIndex].DateTime -status "StdOut" -detail "$ProactiveRemediationDetectStdOutput" -logEntry "Line $($LogEntryList[$ProactiveRemediationDetectStdOutIndex].Line)" -Color 'Yellow' + + $LogEntryList[$ProactiveRemediationDetectStdOutIndex].ProcessRunTime = "Powershell Detect StdOut" + } + + # Script Error to Timeline if configured and if exists + if($ShowErrorsInTimeline -and $ProactiveRemediationDetectErrorOutputIndex) { + + RecordStatusToTimeline -date $LogEntryList[$ProactiveRemediationDetectErrorOutputIndex].DateTime -status "ErrorLog" -detail "$ProactiveRemediationDetectErrorOutput" -logEntry "Line $($LogEntryList[$ProactiveRemediationDetectStdOutIndex].Line)" -Color 'Yellow' + + $LogEntryList[$ProactiveRemediationDetectErrorOutputIndex].ProcessRunTime = "Powershell Detect ErrorLog" + + } + + # Post-End message to Timeline if exists + if($RemediationDetectPostActionMessageIndex) { + + RecordStatusToTimeline -date $LogEntryList[$RemediationDetectPostActionMessageIndex].DateTime -status "Info" -detail " $($LogEntryList[$RemediationDetectPostActionMessageIndex].Message)" -logEntry "Line $($LogEntryList[$RemediationDetectPostActionMessageIndex].Line)" -Color 'Yellow' + } + + + } else { + # We missed Proactive Remediation Detect start log entry + # We should never get here + + if($PreRemediationDetectResult -eq 'True') { + + RecordStatusToTimeline -date $LogEntryList[$i].DateTime -status "Success / Detect" -detail "$ProactiveRemediationScriptPolicyTypeName $($Padding): $ProactiveRemediationScriptName" -Color 'Green' -logEntry "Line $($LogEntryList[$i].Line)" + + # Set RunTime log entry + $LogEntryList[$i].ProcessRunTime = "Succeeded $ProactiveRemediationScriptPolicyTypeName Detect script ($ProactiveRemediationScriptName)" + + } else { + RecordStatusToTimeline -date $LogEntryList[$i].DateTime -status "Failed / Detect" -detail "$ProactiveRemediationScriptPolicyTypeName $($Padding): $ProactiveRemediationScriptName" -Color 'Yellow' -logEntry "Line $($LogEntryList[$i].Line)" + + $LogEntryList[$i].ProcessRunTime = "Failed/NotCompliant $ProactiveRemediationScriptPolicyTypeName Detect script ($ProactiveRemediationScriptName)" + } + + # Script StdOut to Timeline if configured and if exists + if($ShowStdOutInTimeline -and $ProactiveRemediationDetectStdOutIndex) { + + RecordStatusToTimeline -date $LogEntryList[$ProactiveRemediationDetectStdOutIndex].DateTime -status "StdOut" -detail "$ProactiveRemediationDetectStdOutput" -logEntry "Line $($LogEntryList[$ProactiveRemediationDetectStdOutIndex].Line)" -Color 'Yellow' + + $LogEntryList[$ProactiveRemediationDetectStdOutIndex].ProcessRunTime = "Powershell Detect StdOut" + } + + # Script Error to Timeline if configured and if exists + if($ShowErrorsInTimeline -and $ProactiveRemediationDetectErrorOutputIndex) { + + RecordStatusToTimeline -date $LogEntryList[$ProactiveRemediationDetectErrorOutputIndex].DateTime -status "ErrorLog" -detail "$ProactiveRemediationDetectErrorOutput" -logEntry "Line $($LogEntryList[$ProactiveRemediationDetectStdOutIndex].Line)" -Color 'Yellow' + + $LogEntryList[$ProactiveRemediationDetectErrorOutputIndex].ProcessRunTime = "Powershell Detect ErrorLog" + + } + + # Post-End message to Timeline if exists + if($RemediationDetectPostActionMessageIndex) { + + RecordStatusToTimeline -date $LogEntryList[$RemediationDetectPostActionMessageIndex].DateTime -status "Info" -detail " $($LogEntryList[$RemediationDetectPostActionMessageIndex].Message)" -logEntry "Line $($LogEntryList[$RemediationDetectPostActionMessageIndex].Line)" -Color 'Yellow' + } + + Break + } + + # We are done + Write-Host + + # Break out from For-loop + Break + } + + + if($LogEntryList[$b - 1].Message -like '`[HS`] the pre-remdiation detection script compliance result for*') { + # We missed Proactive Remediation Detect start log entry + + #Write-Host "Missed Proactive Remedation Detect script start log entry" -ForegroundColor Red + + if($PreRemediationDetectResult -eq 'True') { + + RecordStatusToTimeline -date $LogEntryList[$i].DateTime -status "Success / Detect" -detail "$ProactiveRemediationScriptPolicyTypeName $($Padding): $ProactiveRemediationScriptName" -Color 'Green' -logEntry "Line $($LogEntryList[$i].Line)" + + # Set RunTime log entry + $LogEntryList[$i].ProcessRunTime = "Succeeded $ProactiveRemediationScriptPolicyTypeName Detection script ($ProactiveRemediationScriptName)" + + } else { + RecordStatusToTimeline -date $LogEntryList[$i].DateTime -status "Failed / Not Compliant" -detail "$ProactiveRemediationScriptPolicyTypeName $($Padding): $ProactiveRemediationScriptName" -Color 'Yellow' -logEntry "Line $($LogEntryList[$i].Line)" + + $LogEntryList[$i].ProcessRunTime = "Failed/NotCompliant $ProactiveRemediationScriptPolicyTypeName Detection script ($ProactiveRemediationScriptName)" + } + + Break + } + + } + } # For-loop end + Write-Verbose "" + + } + # endregion Try to find Proactive Detection script run + + + # region Try to find Proactive Remediate script run + if($LogEntryList[$i].Message -like '`[HS`] remediation script exit code is*') { + #Write-Host "DEBUG: Proactive Remediation Remediate Script found" -ForegroundColor Yellow + $Matches=$null + if($LogEntryList[$i].Message -Match '\[HS\] remediation script exit code is (.*)$') { + $RemediationScriptExitCode = $Matches[1] + + #Write-Host "DEBUG: $($LogEntryList[$i].Message)" + + $ProcessStartTime = $null + $RemediationScriptLogEntryEndTime = $LogEntryList[$i].LogEntryDateTime + $Thread = $LogEntryList[$i].Thread + + + $RemediationScriptStdOutput = $null + $RemediationScriptStdOutputIndex = $null + $RemediationScriptErrorOutput = $null + $RemediationScriptErrorOutputIndex = $null + + $PowershellScriptStartTime = $null + $PowershellScriptEndTime = $null + $PowershellScriptStartIndex = $null + $PowershellScriptEndIndex = $null + + $RemediationPowershellScriptExitCode = $null + + $RemediationPowershellScriptProcessId = $null + $ProcessRunTimeTotalSeconds = $null + $RemediationScriptRunAs = 'N/A' + + # Try to find Proactive Remediation Remediate Start log entry (going upwards) + $b = $i - 1 + for($b = $i - 1; $b -ge 0; $b-- ) { + + #Write-Host "DEBUG: $($LogEntryList[$b].Message)" + + if(($LogEntryList[$b].Message -Match '^\[HS\] std output =(.*)$') -and ($LogEntryList[$b].Thread -eq $Thread)) { + $RemediationScriptStdOutput = ($Matches[1]).Trim() + $RemediationScriptStdOutputIndex = $b + # Continue to previous log entry + Continue + } + + if(($LogEntryList[$b].Message -Match '^\[HS\] err output =(.*)$') -and ($LogEntryList[$b].Thread -eq $Thread)) { + $RemediationScriptErrorOutput = ($Matches[1]).Trim() + $RemediationScriptErrorOutputIndex = $b + # Continue to previous log entry + Continue + } + + if(($LogEntryList[$b].Message -Match '^Powershell execution is done, exitCode = (.*)$') -and ($LogEntryList[$b].Thread -eq $Thread)) { + $RemediationPowershellScriptExitCode = $Matches[1] + + # Set RunTime to Powershell script Install End log entry + $LogEntryList[$b].ProcessRunTime = "End Proactive Remediate Powershell (ExitCode=$RemediationPowershellScriptExitCode)" + + $PowershellScriptEndTime = $LogEntryList[$b].LogEntryDateTime + $PowershellScriptEndIndex = $b + + # Continue to previous log entry + Continue + } + + if(($LogEntryList[$b].Message -Match '^process id = (.*)$') -and ($LogEntryList[$b].Thread -eq $Thread)) { + $RemediationPowershellScriptProcessId = $Matches[1] + + $PowershellScriptStartTime = $LogEntryList[$b].LogEntryDateTime + + # Calculate Powershell script runtime + if($PowershellScriptStartTime -and $PowershellScriptEndTime) { + $PowershellScriptRuntime = New-Timespan $PowershellScriptStartTime $PowershellScriptEndTime + $ProcessRunTimeTotalSeconds = $PowershellScriptRuntime.TotalSeconds + } else { + $ProcessRunTimeTotalSeconds = 'N/A' + } + + $PowershellScriptStartIndex = $b + + # Set RunTime to Powershell script Start log entry + $LogEntryList[$b].ProcessRunTime = "{0,-16} {1,8:n1}" -f "Start Proactive Remediate Powershell", $ProcessRunTimeTotalSeconds + + # Add RunTime to Powershell script End log entry + $LogEntryList[$PowershellScriptEndIndex].ProcessRunTime = "{0,-16} {1,8:n1}" -f "End Proactive Remediate Powershell", $ProcessRunTimeTotalSeconds + + # Continue to previous log entry + Continue + } + + if(($LogEntryList[$b].Message -Match '^\[HS\] The policy needs be run as (.*)$') -and ($LogEntryList[$b].Thread -eq $Thread)) { + $RemediationScriptRunAs = $Matches[1] + + # Continue to previous log entry + Continue + } + + if(($LogEntryList[$b].Message -Match '^\[HS\] Processing policy with id = (.*)$') -and ($LogEntryList[$b].Thread -eq $Thread)) { + $PolicyIdStart = $Matches[1] + + # $b is startIndex + + # Try to find displayName + if($IdHashtable.ContainsKey($PolicyIdStart)) { + if($IdHashtable[$PolicyIdStart].displayName) { + $ProactiveRemediationScriptName = $IdHashtable[$PolicyIdStart].displayName + } else { + $ProactiveRemediationScriptName = $PolicyIdStart + } + } + + + $RemediationScriptStartTime = $LogEntryList[$b].LogEntryDateTime + if($RemediationScriptStartTime -and $RemediationScriptLogEntryEndTime) { + $ProactiveRemediateRuntime = New-Timespan $RemediationScriptStartTime $RemediationScriptLogEntryEndTime + $ProcessRunTimeTotalSeconds = $ProactiveDetectionRuntime.TotalSeconds + } else { + $ProcessRunTimeTotalSeconds = 'N/A' + } + + #Write-Host "DEBUG: `$ProcessRunTimeTotalSeconds=$ProcessRunTimeTotalSeconds" + + $Padding = '' + + # Start message to Timeline + if($ShowAllTimelineEntries) { + RecordStatusToTimeline -date $LogEntryList[$b].DateTime -status "Process / Remediate" -detail "$ProactiveRemediationScriptPolicyTypeName $($Padding): $ProactiveRemediationScriptName (run as $RemediationScriptRunAs)" -logEntry "Line $($LogEntryList[$b].Line)" + } + + # Set RunTime log entry + $LogEntryList[$b].ProcessRunTime = "Processing $ProactiveRemediationScriptPolicyTypeName Remediate script ($ProactiveRemediationScriptName) (run as $RemediationScriptRunAs)" + + # End message to Timeline + if($RemediationScriptExitCode -eq 0) { + + RecordStatusToTimeline -date $LogEntryList[$i].DateTime -status "Success / Remediate" -detail "$ProactiveRemediationScriptPolicyTypeName $($Padding): $ProactiveRemediationScriptName" -Seconds $ProcessRunTimeTotalSeconds -Color 'Green' -logEntry "Line $($LogEntryList[$i].Line)" + + # Set RunTime log entry + $LogEntryList[$i].ProcessRunTime = "Succeeded $ProactiveRemediationScriptPolicyTypeName Detection script ($ProactiveRemediationScriptName) (Runtime: $ProcessRunTimeTotalSeconds sec)" + + } else { + + RecordStatusToTimeline -date $LogEntryList[$i].DateTime -status "Failed / Remediate" -detail "$ProactiveRemediationScriptPolicyTypeName $($Padding): $ProactiveRemediationScriptName" -Seconds $ProcessRunTimeTotalSeconds -Color 'Red' -logEntry "Line $($LogEntryList[$i].Line)" + + $LogEntryList[$i].ProcessRunTime = "Failed $ProactiveRemediationScriptPolicyTypeName Remediate script ($ProactiveRemediationScriptName) (Runtime: $ProcessRunTimeTotalSeconds sec)" + } + + # Script StdOut to Timeline if configured and if exists + if($ShowStdOutInTimeline -and $RemediationScriptStdOutputIndex) { + + RecordStatusToTimeline -date $LogEntryList[$RemediationScriptStdOutputIndex].DateTime -status "StdOut" -detail "$RemediationScriptStdOutput" -logEntry "Line $($LogEntryList[$RemediationScriptStdOutputIndex].Line)" -Color 'Yellow' + + $LogEntryList[$RemediationScriptStdOutputIndex].ProcessRunTime = "Powershell Remediate StdOut" + } + + # Script Error to Timeline if configured and if exists + if($ShowErrorsInTimeline -and $RemediationScriptErrorOutputIndex) { + + RecordStatusToTimeline -date $LogEntryList[$RemediationScriptErrorOutputIndex].DateTime -status "ErrorLog" -detail "$RemediationScriptErrorOutput" -logEntry "Line $($LogEntryList[$RemediationScriptErrorOutputIndex].Line)" -Color 'Yellow' + + $LogEntryList[$RemediationScriptErrorOutput].ProcessRunTime = "Powershell Remediate ErrorLog" + + } + + # Break out from For-loop + Break + } + + } # For-loop end + + + if($LogEntryList[$b - 1].Message -like '`[HS`] the pre-remdiation detection script compliance result for*') { + # We missed Proactive Remediation Detect start log entry + + #Write-Host "Missed Proactive Remedation Detect script start log entry" -ForegroundColor Red + + if($PreRemediationDetectResult -eq 'True') { + + RecordStatusToTimeline -date $LogEntryList[$i].DateTime -status "Succeeded" -detail "$ProactiveRemediationScriptPolicyTypeName $($Padding): $ProactiveRemediationScriptName" -Color 'Green' -logEntry "Line $($LogEntryList[$i].Line)" + + # Set RunTime log entry + $LogEntryList[$i].ProcessRunTime = "Succeeded $ProactiveRemediationScriptPolicyTypeName Detection script ($ProactiveRemediationScriptName)" + + } else { + RecordStatusToTimeline -date $LogEntryList[$i].DateTime -status "Failed / Not Compliant" -detail "$ProactiveRemediationScriptPolicyTypeName $($Padding): $ProactiveRemediationScriptName" -Color 'Yellow' -logEntry "Line $($LogEntryList[$i].Line)" + + $LogEntryList[$i].ProcessRunTime = "Failed/NotCompliant $ProactiveRemediationScriptPolicyTypeName Detection script ($ProactiveRemediationScriptName)" + } + + # Script StdOut to Timeline if configured and if exists + if($ShowStdOutInTimeline -and $RemediationScriptStdOutputIndex) { + + RecordStatusToTimeline -date $LogEntryList[$RemediationScriptStdOutputIndex].DateTime -status "StdOut" -detail "$RemediationScriptStdOutput" -logEntry "Line $($LogEntryList[$RemediationScriptStdOutputIndex].Line)" -Color 'Yellow' + + $LogEntryList[$RemediationScriptStdOutputIndex].ProcessRunTime = "Powershell Remediate StdOut" + } + + # Script Error to Timeline if configured and if exists + if($ShowErrorsInTimeline -and $RemediationScriptErrorOutputIndex) { + + RecordStatusToTimeline -date $LogEntryList[$RemediationScriptErrorOutputIndex].DateTime -status "ErrorLog" -detail "$RemediationScriptErrorOutput" -logEntry "Line $($LogEntryList[$RemediationScriptErrorOutputIndex].Line)" -Color 'Yellow' + + $LogEntryList[$RemediationScriptErrorOutput].ProcessRunTime = "Powershell Remediate ErrorLog" + + } + + Break + } + } + } + # endregion Try to find Proactive Remediate script run + + # region Try to find Proactive PostDetection script run + if($LogEntryList[$i].Message -like '`[HS`] the post detection script result for *') { + #Write-Host "Proactive Remediation PostDetect found" -ForegroundColor Yellow + + $Matches=$null + if($LogEntryList[$i].Message -Match '^\[HS\] the post detection script result for (.*) is (.*)$') { + $PolicyIdEnd = $Matches[1] + $PostRemediationDetectResult = $Matches[2] + + $ProcessStartTime = $null + $DetectionLogEntryEndTime = $LogEntryList[$i].LogEntryDateTime + $Thread = $LogEntryList[$i].Thread + + # Try to find displayName + if($IdHashtable.ContainsKey($PolicyIdEnd)) { + if($IdHashtable[$PolicyIdEnd].displayName) { + $ProactiveRemediationScriptName = $IdHashtable[$PolicyIdEnd].displayName + } else { + $ProactiveRemediationScriptName = $PolicyIdEnd + } + } + + # Try to find policyType + if($IdHashtable.ContainsKey($PolicyIdEnd)) { + $ProactiveRemediationScriptPolicyType = $IdHashtable[$PolicyIdEnd].policyType + } + + if($ProactiveRemediationScriptPolicyType -eq 6) { + $ProactiveRemediationScriptPolicyTypeName = 'Proactive Remediation' + } elseif ($ProactiveRemediationScriptPolicyType -eq 8) { + $ProactiveRemediationScriptPolicyTypeName = 'Custom Compliance' + } else { + # We should never get here because we have seen polityType log entry before this log entry + # Proactive Remediation script type is unknown + $ProactiveRemediationScriptPolicyTypeName = 'Proactive Remediation script (unknown type)' + } + + Write-Verbose "Found $ProactiveRemediationScriptPolicyTypeName ($ProactiveRemediationScriptName) Detect Ending message ($($LogEntryList[$i].FileName) Line $($LogEntryList[$i].Line) Thread $Thread): $($LogEntryList[$i].Message)" + + $ProactiveRemediationPostDetectStdOutput = $null + $ProactiveRemediationPostDetectStdOutIndex = $null + $ProactiveRemediationPostDetectErrorOutput = $null + $ProactiveRemediationPostDetectErrorOutputIndex = $null + + $PowershellScriptStartTime = $null + $PowershellScriptEndTime = $null + $PowershellScriptStartIndex = $null + $PowershellScriptEndIndex = $null + + $ProactiveRemediationPostDetectProcessId = $null + $ProcessRunTimeTotalSeconds = $null + $ProactiveRemediationPostDetectRunAs = 'N/A' + + # Try to find Proactive Remediation Detect Start log entry (going upwards) + $b = $i - 1 + for($b = $i - 1; $b -ge 0; $b-- ) { + if(($LogEntryList[$b].Message -Match '^\[HS\] std output =(.*)$') -and ($LogEntryList[$b].Thread -eq $Thread)) { + $ProactiveRemediationPostDetectStdOutput = ($Matches[1]).Trim() + $ProactiveRemediationPostDetectStdOutIndex = $b + # Continue to previous log entry + Continue + } + + if(($LogEntryList[$b].Message -Match '^\[HS\] err output =(.*)$') -and ($LogEntryList[$b].Thread -eq $Thread)) { + $ProactiveRemediationPostDetectErrorOutput = ($Matches[1]).Trim() + $ProactiveRemediationPostDetectErrorOutputIndex = $b + # Continue to previous log entry + Continue + } + + if(($LogEntryList[$b].Message -Match '^Powershell execution is done, exitCode = (.*)$') -and ($LogEntryList[$b].Thread -eq $Thread)) { + $ProactiveRemediationDetectExitCode = $Matches[1] + + # Set RunTime to Powershell script Install End log entry + $LogEntryList[$b].ProcessRunTime = "End Proactive PostDetect Powershell (ExitCode=$ProactiveRemediationDetectExitCode)" + + $PowershellScriptEndTime = $LogEntryList[$b].LogEntryDateTime + $PowershellScriptEndIndex = $b + + # Continue to previous log entry + Continue + } + + if(($LogEntryList[$b].Message -Match '^process id = (.*)$') -and ($LogEntryList[$b].Thread -eq $Thread)) { + $ProactiveRemediationPostDetectProcessId = $Matches[1] + + # Set RunTime to Powershell script Install End log entry + $LogEntryList[$b].ProcessRunTime = "Start Proactive PostDetect Powershell" + + $PowershellScriptStartTime = $LogEntryList[$b].LogEntryDateTime + + # Calculate Powershell script runtime + if($PowershellScriptStartTime -and $PowershellScriptEndTime) { + $PowershellScriptRuntime = New-Timespan $PowershellScriptStartTime $PowershellScriptEndTime + $ProcessRunTimeTotalSeconds = $PowershellScriptRuntime.TotalSeconds + } else { + $ProcessRunTimeTotalSeconds = 'N/A' + } + + $PowershellScriptStartIndex = $b + + # Set RunTime to Powershell script Start log entry + $LogEntryList[$b].ProcessRunTime = "{0,-16} {1,8:n1}" -f "Start Proactive PostDetect Powershell", $ProcessRunTimeTotalSeconds + + # Add RunTime to Powershell script End log entry + $LogEntryList[$PowershellScriptEndIndex].ProcessRunTime = "{0,-16} {1,8:n1}" -f "End Proactive PostDetect Powershell", $ProcessRunTimeTotalSeconds + + # Continue to previous log entry + Continue + } + + if(($LogEntryList[$b].Message -Match '^\[HS\] The policy needs be run as (.*)$') -and ($LogEntryList[$b].Thread -eq $Thread)) { + $ProactiveRemediationPostDetectRunAs = $Matches[1] + + # Continue to previous log entry + Continue + } + + if(($LogEntryList[$b].Message -Match '^\[HS\] Processing policy with id = (.*)$') -and ($LogEntryList[$b].Thread -eq $Thread)) { + $PolicyIdStart = $Matches[1] + + # $b is startIndex + + if($PolicyIdStart -eq $PolicyIdEnd) { + # We found matching starting log entry + $DetectionLogEntryStartTime = $LogEntryList[$b].LogEntryDateTime + if($DetectionLogEntryStartTime -and $DetectionLogEntryEndTime) { + $ProactiveDetectionRuntime = New-Timespan $DetectionLogEntryStartTime $DetectionLogEntryEndTime + $ProcessRunTimeTotalSeconds = $ProactiveDetectionRuntime.TotalSeconds + } else { + $ProcessRunTimeTotalSeconds = 'N/A' + } + + $Padding = '' + + # Start message to Timeline + if($ShowAllTimelineEntries) { + + RecordStatusToTimeline -date $LogEntryList[$b].DateTime -status "Process / PostDetect" -detail "$ProactiveRemediationScriptPolicyTypeName $($Padding): $ProactiveRemediationScriptName (run as $ProactiveRemediationPostDetectRunAs)" -logEntry "Line $($LogEntryList[$b].Line)" + } + + # Set RunTime log entry + $LogEntryList[$b].ProcessRunTime = "Processing $ProactiveRemediationScriptPolicyTypeName PostDetect script ($ProactiveRemediationScriptName) (run as $ProactiveRemediationPostDetectRunAs)" + + + # End message to Timeline + if($PostRemediationDetectResult -eq 'True') { + + RecordStatusToTimeline -date $LogEntryList[$i].DateTime -status "Success / PostDetect" -detail "$ProactiveRemediationScriptPolicyTypeName $($Padding): $ProactiveRemediationScriptName" -Seconds $ProcessRunTimeTotalSeconds -Color 'Green' -logEntry "Line $($LogEntryList[$i].Line)" + + # Set RunTime log entry + $LogEntryList[$i].ProcessRunTime = "Succeeded $ProactiveRemediationScriptPolicyTypeName PostDetect script ($ProactiveRemediationScriptName) (Runtime: $ProcessRunTimeTotalSeconds sec)" + + } else { + + RecordStatusToTimeline -date $LogEntryList[$i].DateTime -status "Failed / PostDetect" -detail "$ProactiveRemediationScriptPolicyTypeName $($Padding): $ProactiveRemediationScriptName" -Seconds $ProcessRunTimeTotalSeconds -Color 'Yellow' -logEntry "Line $($LogEntryList[$i].Line)" + + $LogEntryList[$i].ProcessRunTime = "Failed $ProactiveRemediationScriptPolicyTypeName PostDetect script ($ProactiveRemediationScriptName) (Runtime: $ProcessRunTimeTotalSeconds sec)" + } + + # Script StdOut to Timeline if configured and if exists + if($ShowStdOutInTimeline -and $ProactiveRemediationPostDetectStdOutIndex) { + + RecordStatusToTimeline -date $LogEntryList[$ProactiveRemediationPostDetectStdOutIndex].DateTime -status "StdOut" -detail "$ProactiveRemediationPostDetectStdOutput" -logEntry "Line $($LogEntryList[$ProactiveRemediationPostDetectStdOutIndex].Line)" -Color 'Yellow' + + $LogEntryList[$ProactiveRemediationPostDetectStdOutIndex].ProcessRunTime = "Powershell PostDetect StdOut" + } + + # Script Error to Timeline if configured and if exists + if($ShowErrorsInTimeline -and $ProactiveRemediationPostDetectErrorOutputIndex) { + + RecordStatusToTimeline -date $LogEntryList[$ProactiveRemediationPostDetectErrorOutputIndex].DateTime -status "ErrorLog" -detail "$ProactiveRemediationPostDetectErrorOutput" -logEntry "Line $($LogEntryList[$ProactiveRemediationPostDetectStdOutIndex].Line)" -Color 'Yellow' + + $LogEntryList[$ProactiveRemediationPostDetectErrorOutputIndex].ProcessRunTime = "Powershell PostDetect ErrorLog" + + } + + + } else { + # We missed Proactive Remediation Detect start log entry + # We should never get here + + $Padding = '' + if($PostRemediationDetectResult -eq 'True') { + + RecordStatusToTimeline -date $LogEntryList[$i].DateTime -status "Success / PostDetect" -detail "$ProactiveRemediationScriptPolicyTypeName $($Padding): $ProactiveRemediationScriptName" -Color 'Green' -logEntry "Line $($LogEntryList[$i].Line)" + + # Set RunTime log entry + $LogEntryList[$i].ProcessRunTime = "Succeeded $ProactiveRemediationScriptPolicyTypeName PostDetect script ($ProactiveRemediationScriptName)" + + } else { + RecordStatusToTimeline -date $LogEntryList[$i].DateTime -status "Failed / PostDetect" -detail "$ProactiveRemediationScriptPolicyTypeName $($Padding): $ProactiveRemediationScriptName" -Color 'Yellow' -logEntry "Line $($LogEntryList[$i].Line)" + + $LogEntryList[$i].ProcessRunTime = "Failed/NotCompliant $ProactiveRemediationScriptPolicyTypeName PostDetect script ($ProactiveRemediationScriptName)" + } + + + # Script StdOut to Timeline if configured and if exists + if($ShowStdOutInTimeline -and $ProactiveRemediationPostDetectStdOutIndex) { + + RecordStatusToTimeline -date $LogEntryList[$ProactiveRemediationPostDetectStdOutIndex].DateTime -status "StdOut" -detail "$ProactiveRemediationPostDetectStdOutput" -logEntry "Line $($LogEntryList[$ProactiveRemediationPostDetectStdOutIndex].Line)" -Color 'Yellow' + + $LogEntryList[$ProactiveRemediationPostDetectStdOutIndex].ProcessRunTime = "Powershell PostDetect StdOut" + } + + # Script Error to Timeline if configured and if exists + if($ShowErrorsInTimeline -and $ProactiveRemediationPostDetectErrorOutputIndex) { + + RecordStatusToTimeline -date $LogEntryList[$ProactiveRemediationPostDetectErrorOutputIndex].DateTime -status "ErrorLog" -detail "$ProactiveRemediationPostDetectErrorOutput" -logEntry "Line $($LogEntryList[$ProactiveRemediationPostDetectStdOutIndex].Line)" -Color 'Yellow' + + $LogEntryList[$ProactiveRemediationPostDetectErrorOutputIndex].ProcessRunTime = "Powershell PostDetect ErrorLog" + + } + + Break + } + + # We are done + Write-Host + + # Break out from For-loop + Break + } + + $Padding = '' + if($LogEntryList[$b - 1].Message -like '`[HS`] the pre-remdiation detection script compliance result for*') { + # We missed Proactive Remediation Detect start log entry + + #Write-Host "Missed Proactive Remedation Detect script start log entry" -ForegroundColor Red + + if($PostRemediationDetectResult -eq 'True') { + + RecordStatusToTimeline -date $LogEntryList[$i].DateTime -status "Success / PostDetect" -detail "$ProactiveRemediationScriptPolicyTypeName $($Padding): $ProactiveRemediationScriptName" -Color 'Green' -logEntry "Line $($LogEntryList[$i].Line)" + + # Set RunTime log entry + $LogEntryList[$i].ProcessRunTime = "Succeeded $ProactiveRemediationScriptPolicyTypeName PostDetect script ($ProactiveRemediationScriptName)" + + } else { + RecordStatusToTimeline -date $LogEntryList[$i].DateTime -status "Failed / PostDetect" -detail "$ProactiveRemediationScriptPolicyTypeName $($Padding): $ProactiveRemediationScriptName" -Color 'Yellow' -logEntry "Line $($LogEntryList[$i].Line)" + + $LogEntryList[$i].ProcessRunTime = "Failed/NotCompliant $ProactiveRemediationScriptPolicyTypeName PostDetect script ($ProactiveRemediationScriptName)" + } + + # Script StdOut to Timeline if configured and if exists + if($ShowStdOutInTimeline -and $ProactiveRemediationPostDetectStdOutIndex) { + + RecordStatusToTimeline -date $LogEntryList[$ProactiveRemediationPostDetectStdOutIndex].DateTime -status "StdOut" -detail "$ProactiveRemediationPostDetectStdOutput" -logEntry "Line $($LogEntryList[$ProactiveRemediationPostDetectStdOutIndex].Line)" -Color 'Yellow' + + $LogEntryList[$ProactiveRemediationPostDetectStdOutIndex].ProcessRunTime = "Powershell PostDetect StdOut" + } + + # Script Error to Timeline if configured and if exists + if($ShowErrorsInTimeline -and $ProactiveRemediationPostDetectErrorOutputIndex) { + + RecordStatusToTimeline -date $LogEntryList[$ProactiveRemediationPostDetectErrorOutputIndex].DateTime -status "ErrorLog" -detail "$ProactiveRemediationPostDetectErrorOutput" -logEntry "Line $($LogEntryList[$ProactiveRemediationPostDetectStdOutIndex].Line)" -Color 'Yellow' + + $LogEntryList[$ProactiveRemediationPostDetectErrorOutputIndex].ProcessRunTime = "Powershell PostDetect ErrorLog" + + } + + Break + } + + } + } # For-loop end + Write-Verbose "" + + } + # endregion Try to find Proactive PostDetection script run + + + + ############################################### + + # region Find Win32App installations + # Check if this is ending message for Win32 application installation + if($LogEntryList[$i].Message -like '`[Win32App`] Installation is done, collecting result') { + $Win32AppInstallEndIndex = $i + + $ProcessStartTime = $null + $ProcessEndTime = $LogEntryList[$i].LogEntryDateTime + $Thread = $LogEntryList[$i].Thread + Write-Verbose "Found Win32 App install end process message (Line $($LogEntryList[$i].Line) Thread $Thread): $($LogEntryList[$i].Message)" + + $Win32AppExitCode='N/A' + $Win32AppInstallSuccess='N/A' + + # Check if we can find exitCode and Success/failed information in next 20 lines + for($b = $Win32AppInstallEndIndex + 1; $b -lt $Win32AppInstallEndIndex + 20; $b++ ) { + $Matches=$null + + # Run this if first because it is much faster than run -Match for every line + if($Win32AppExitCode -eq 'N/A') { + if(($LogEntryList[$b].Message -Match '^\[Win32App\] lpExitCode (.*)$') -and ($LogEntryList[$b].Thread -eq $Thread)) { + $Win32AppExitCode = $Matches[1] + Write-Verbose "Found Win32App install exitCode: $Win32AppExitCode" + + # Continue For-loop to next log entry + Continue + } + } + + # Run this if first because it is much faster than run -Match for every line + if($Win32AppInstallSuccess -eq 'N/A') { + if(($LogEntryList[$b].Message -Match '^\[Win32App\] lpExitCode is defined as (.*)$') -and ($LogEntryList[$b].Thread -eq $Thread)) { + $Win32AppInstallSuccess = $Matches[1] + Write-Verbose "Found Win32App install success: $Win32AppInstallSuccess" + + # Break out from For-loop + Break + } + } + } # For-loop end + + # Check if we can find previous LogEntry that matches same process thread start message + $TemporaryObjectIndex = $Win32AppInstallEndIndex - 1 + Do { + if(($LogEntryList[$TemporaryObjectIndex].Message -like '`[Win32App`] process id =*') -and ($LogEntryList[$TemporaryObjectIndex].Thread -eq $Thread)) { + # Message matches Win32App install start message + $ProcessStartTime = $LogEntryList[$TemporaryObjectIndex].LogEntryDateTime + $ProcessRunTime = New-Timespan $ProcessStartTime $ProcessEndTime + $ProcessRunTimeTotalSeconds = $ProcessRunTime.TotalSeconds + $ProcessRunTime = "{0,-16} {1,8:n1}" -f "End Win32App", $ProcessRunTimeTotalSeconds + + Write-Verbose "Found Win32 App install Start process message (Line $($LogEntryList[$TemporaryObjectIndex].Line)): $($LogEntryList[$TemporaryObjectIndex].Message)" + + # Set RunTime to Win32App Install Start log entry + $LogEntryList[$TemporaryObjectIndex].ProcessRunTime = "{0,-16} {1,8:n1}" -f "Start Win32App", $ProcessRunTimeTotalSeconds + + # Set RunTime to Powershell script Install End log entry + $LogEntryList[$i].ProcessRunTime = "{0,-16} {1,8:n1}" -f "End Win32App", $ProcessRunTimeTotalSeconds + + Write-Verbose "Runtime: $ProcessRunTimeTotalSeconds" + + Break + } else { + # Message does not match Win32App install start message + } + $TemporaryObjectIndex-- + } While ($TemporaryObjectIndex -ge 0) + + # Try to find Win32App name if we have start and end time + $Win32AppId = 'N/A' + $Win32AppName = 'N/A' + if($ProcessStartTime -and $ProcessEndTime) { + $Win32AppInstallStartIndex = $TemporaryObjectIndex + + $Win32AppNameRegex = '\[StatusService\] Downloading app \(id = (.*), name (.*)\).*$' + + Do { + if($LogEntryList[$TemporaryObjectIndex].Message -Match $Win32AppNameRegex) { + if($LogEntryList[$TemporaryObjectIndex].Thread -eq $Thread) { + + # Message matches Win32App Name message + $Win32AppId = $Matches[1] + $Win32AppName = $Matches[2] + $ProcessRunTime = "$ProcessRunTime $Win32AppName" + + Write-Verbose "Win32App Name $Win32AppName" + + #Write-Host "DEBUG: Found Win32 App install Start process message (Line $($LogEntryList[$TemporaryObjectIndex].Line)): $($LogEntryList[$TemporaryObjectIndex].Message)`n" + + # Set Win32App Name to Win32App Install Start entry + $LogEntryList[$Win32AppInstallStartIndex].ProcessRunTime = "$($LogEntryList[$Win32AppInstallStartIndex].ProcessRunTime) $Win32AppName" + + # Set Win32App Name to Win32App Install End entry + $LogEntryList[$i].ProcessRunTime = "$($LogEntryList[$i].ProcessRunTime) $Win32AppName (ExitCode $Win32AppExitCode $Win32AppInstallSuccess)" + + # Add AppId and name to Hashtable + if($IdHashtable.ContainsKey($Win32AppId)) { + $IdHashtable[$Win32AppId].id = $Win32AppId + $IdHashtable[$Win32AppId].name = $Win32AppName + $IdHashtable[$Win32AppId].displayName = $Win32AppName + + } else { + $AppIdCustomObjectProperties = @{ + id = $Win32AppId + name = $Win32AppName + displayName = $Win32AppName + } + $AppIdCustomObject = New-Object -TypeName PSObject -Prop $AppIdCustomObjectProperties + + # Create new UserId hashtable entry + $IdHashtable.Add($Win32AppId , $AppIdCustomObject) + } + + Break + } else { + # Found Win32App Name but Thread does not match + # Aborting Name find + Break + } + } else { + # Message does not match Win32App Name + } + $TemporaryObjectIndex-- + } While ($TemporaryObjectIndex -ge 0) + + # Change text color depending on Win32App install Success status + if($Win32AppInstallSuccess -eq 'Success') { + + RecordStatusToTimeline -date $LogEntryList[$Win32AppInstallEndIndex].DateTime -status "Success / Win32App" -detail "[Win32App] App install: $Win32AppName (ExitCode $Win32AppExitCode $Win32AppInstallSuccess)" -logEntry "Line $($LogEntryList[$Win32AppInstallEndIndex].Line)" -Seconds $ProcessRunTimeTotalSeconds -Color 'Green' + + } else { + + RecordStatusToTimeline -date $LogEntryList[$Win32AppInstallEndIndex].DateTime -status "Failed / Win32App" -detail "[Win32App] App install: $Win32AppName (ExitCode $Win32AppExitCode $Win32AppInstallSuccess)" -logEntry "Line $($LogEntryList[$Win32AppInstallEndIndex].Line)" -Seconds $ProcessRunTimeTotalSeconds -Color 'Red' + } + } + + + if(($TemporaryObjectIndex -eq 0) -and (-not $ProcessStartTime)) { + # We did not find Win32App install start time + Write-Host "Did NOT find matching Win32App install start time for Win32App install in line $LineNumber (Thread=$Thead)`n" -ForegroundColor Yellow + } + + # Cleanup variables + $ProcessStartTime = $null + $ProcessEndTime = $null + $Thread = $null + $ProcessRunTimeTotalSeconds = $null + $ProcessRunTimeText = $null + } + # endregion Find Win32App installations + + # region Find WinGet Application install + if($LogEntryList[$i].Message -like '`[Win32App`]`[V3Processor`] Processing subgraph with app ids:*') { + if($LogEntryList[$i].Message -Match '^\[Win32App\]\[V3Processor\] Processing subgraph with app ids: (.*)$') { + $WinGetAppId = $Matches[1] + $WinGetProcessingStartIndex = $i + + $Thread = $LogEntryList[$i].Thread + $WinGetInstallationStartedDateTime = $LogEntryList[$i].DateTime + + $WinGetDownloadFirstEntryFound=$false + + #Write-Host "DEBUG: Found WinGet installation start. `$i=$i" + + # Find next WinGet installation log entries + # Now we go down (to end) + for($b = $i + 1; $b -lt $LogEntryList.Count; $b++ ) { + + # Use this to debug WinGet log entries + #Write-Host "DEBUG: $b - $($LogEntryList[$b].Message)" + + if(($LogEntryList[$b].Message -Match '^\[Win32App\]\[WinGetApp\]\[WinGetAppExecutionExecutor\] Starting execution of app with id: (.*) and context: (.*)\.$') -and ($LogEntryList[$b].Thread -eq $Thread)) { + $TempAppId = $Matches[1] + $WinGetInstallContext = $Matches[2] + + #Write-Host "DEBUG: Found WinGet id ($TempAppId) and context ($WinGetInstallContext)" + + if($TempAppId -ne $WinGetAppId) { + # We got different AppIds + # Something is big time wrong here + + #Write-Host "DEBUG: WinGet Application Id mismatch in log entries. Aborting WinGet AppId: $WinGetAppId" + + # Break For-loop + Break + } + + # Continue For-loop to next round + Continue + } + + + if(-not $WinGetDownloadFirstEntryFound) { + if(($LogEntryList[$b].Message -Match '^\[StatusService\] Downloading app .id = (.*), name (.*). via WinGet, bytes (.*)\/(.*) for user (.*)$')) { + $WinGetDownloadFirstEntryFound=$True + + $WinGetDownloadStartDateTime = $LogEntryList[$b].DateTime + + $AppId = $Matches[1] + $WinGetAppName = $Matches[2] + $WinGetAppBytes = $Matches[4] + $WinGetUserId = $Matches[5] + + # Add WinGetAppId and name to Hashtable + if($IdHashtable.ContainsKey($AppId)) { + $IdHashtable[$AppId].id = $AppId + $IdHashtable[$AppId].name = $WinGetAppName + $IdHashtable[$AppId].displayName = $WinGetAppName + } else { + $AppIdCustomObjectProperties = @{ + id = $AppId + name = $WinGetAppName + displayName = $WinGetAppName + } + $AppIdCustomObject = New-Object -TypeName PSObject -Prop $AppIdCustomObjectProperties + + # Create new UserId hashtable entry + $IdHashtable.Add($AppId , $AppIdCustomObject) + } + + + # Get real UserName if found from Hashtable + if($IdHashtable.ContainsKey($WinGetUserId)) { + $Name = $IdHashtable[$WinGetUserId].name + if($Name) { + $WinGetUserName = $IdHashtable[$WinGetUserId].name + } else { + $WinGetUserName = $WinGetUserId + } + } + + $LogEntryList[$b].ProcessRunTime = " Start Download ($WinGetAppName) for user $WinGetUserName" + + #Write-Host "DEBUG: Found WinGet first download entry" + + # Continue For-loop to next round + Continue + } + } + + $DownloadCompleteString1 = 'State transition processed - Previous State:Download In Progress received event: Download Finished which took state machine into Download Complete state.' + $DownloadCompleteString2 = 'Processing state transition - Current State:Download Complete With Event: Continue Install.' + if(($LogEntryList[$b].Message -eq $DownloadCompleteString1) -or ($LogEntryList[$b].Message -eq $DownloadCompleteString2)) { + + $WinGetDownloadEnddDateTime = $LogEntryList[$b].DateTime + + $LogEntryList[$b].ProcessRunTime = " Download complete ($WinGetAppName)" + + #Write-Host "DEBUG: Found WinGet download complete" + + # Continue For-loop to next round + Continue + } + + + $WinGetInstallStartString = 'State transition processed - Previous State:Download Complete received event: Continue Install which took state machine into Install In Progress Download Complete state.' + if(($LogEntryList[$b].Message -eq $WinGetInstallStartString)) { + # -and ($LogEntryList[$b].Thread -eq $Thread) + $WinGetInstallStartdDateTime = $LogEntryList[$b].DateTime + $LogEntryList[$b].ProcessRunTime = " Start Install process ($WinGetAppName)" + #Write-Host "WinGet Found install start" + + } + + + if(($LogEntryList[$b].Message -Match '^\[WinGet\] Install Operation Result has arrived (.*)$')) { + $WinGetInstallResult = $Matches[1] + + # Calculate install time + $ProcessStartTime = $LogEntryList[$i].LogEntryDateTime + $ProcessEndTime = $LogEntryList[$b].LogEntryDateTime + $ProcessRunTime = New-Timespan $ProcessStartTime $ProcessEndTime + $ProcessRunTimeTotalSeconds = $ProcessRunTime.TotalSeconds + + $WinGetInstallEndDateTime = $LogEntryList[$b].DateTime + $LogEntryList[$i].ProcessRunTime = "Start WinGet processing App ($WinGetAppName)" + + if($WinGetInstallResult -eq 'OK') { + RecordStatusToTimeline -date $LogEntryList[$b].DateTime -status "Success / WinGet" -detail "[WinGet] App install: $WinGetAppName for user $WinGetUserName" -Seconds $ProcessRunTimeTotalSeconds -Color 'Green' -logEntry "Line $($LogEntryList[$b].Line)" + + $LogEntryList[$b].ProcessRunTime = "Stop WinGet Install Success ($WinGetAppName) for user $WinGetUserName ($ProcessRunTimeTotalSeconds)" + + } else { + RecordStatusToTimeline -date $LogEntryList[$b].DateTime -status "Failed / WinGet" -detail "[WinGet] App install: $WinGetAppName for user $WinGetUserName" -Seconds $ProcessRunTimeTotalSeconds -Color 'Red' -logEntry "Line $($LogEntryList[$b].Line)" + + $LogEntryList[$b].ProcessRunTime = "Stop WinGet Install Failed ($WinGetAppName) for user $WinGetUserName ($ProcessRunTimeTotalSeconds)" + } + + #Write-Host "WinGet Found install OK" + + # Break For-loop + Break + + } + + if(((($LogEntryList[$b].Message -eq '[Win32App][V3Processor] Done processing subgraph.') -or ` + ($LogEntryList[$b].Message -like '`[Win32App`]`[V3Processor`] Done processing * subgraphs.') -or ` + ($LogEntryList[$b].Message -eq '[Win32App][V3Processor] All apps in the subgraph are not applicable due to assignment filters. Processing will be skipped.')))` + -and ($LogEntryList[$b].Thread -eq $Thread)) { + + #Write-Host "DEBUG: Found WinGet Done Processing subgraph log entry" + Write-Verbose "Subgraph $WinGetAppId did not include WinGet Application install ($($LogEntryList[$i].FileName) Line $($LogEntryList[$i].Line))" + + # Break For-loop + Break + } + + + # Check that we didn't go over end log of this installation process + # This is just fail safe check. We should not get here + if($LogEntryList[$b].Message -like '`[Win32App`]`[V3Processor`] Processing subgraph with app ids:*') { + # New WinGet installation started. + # We didn't catch all log entries to process WinGet installation + Write-Verbose "Could not detect WinGet installation for WinGet AppId: $WinGetAppId (Line $($LogEntryList[$i].Line))" + + # Break For-loop + Break + } + + + } + } + + } # Region ends here + # region Find WinGet Application install + + $i++ + } While ($i -lt $LogEntryList.Count) + # endregion analyze log entries + + + # This is aligned with Michael Niehaus's Get-AutopilotDiagnostics script just in case + Write-Host "" + Write-Host "OBSERVED TIMELINE" -ForegroundColor Magenta + $observedTimeline | + Format-Table @{ + label= 'Date' + Expression = { $_.Date.Split('.')[0] } + }, + @{ + Label = "Status" + Expression = + { + Switch ($_.Color) + { + 'Red' { $color = "91"; break } + 'Yellow' { $color = '93'; break } + 'Green' { $color = "92"; break } + default { $color = "0" } + } + $e = [char]27 + "$e[${color}m$($_.Status)$e[0m" + } + }, + @{ + Label = "Detail" + Expression = + { + Switch ($_.Color) + { + 'Red' { $color = "91"; break } + #'Yellow' { $color = '93'; break } + 'Green' { $color = "92"; break } + default { $color = "0" } + } + $e = [char]27 + "$e[${color}m$($_.Detail)$e[0m" + } + }, + @{ + label= 'Seconds' + Expression = { "{0,6:N0}" -f $_.Seconds } + },logEntry + + Write-Host "" + + + # Print $IdHastable + #Write-Host "DEBUG: `$IdHastable:`n$($IdHashtable | ConvertTo-Json)" + + + # Command line Parameter -ShowErrorsSummary + if($ShowErrorsSummary) { + if($PowershellScriptErrorLogs) { + Write-Host "`n`n`n`n" + Write-Host "####################################" + Write-Host "Powershell scripts error logs found:" + Write-Host "$($PowershellScriptErrorLogs -Join "`n")" + Write-Host "`n" + } + } + + # Command line Parameter -ConvertAllKnownGuidsToClearText + if($ConvertAllKnownGuidsToClearText) { + Foreach($HashtableKey in $IdHashtable.Keys) { + #Write-Host "Processing Hashtable object $($IdHashtable[$HashtableKey].id)" + + Foreach($LogEntry in $LogEntryList) { + $Logentry.Message = ($Logentry.Message).Replace($HashtableKey, "'$($IdHashtable[$HashtableKey].name)'") + } + + Foreach($LogEntry in $LogEntryList | Where-Object ProcessRuntime -ne $null) { + $Logentry.ProcessRuntime = ($Logentry.ProcessRuntime).Replace($HashtableKey, "'$($IdHashtable[$HashtableKey].name)'") + } + } + } + +<# # This on work in progress so disabled at this time + # Export data to json file + if($ExportJson) { + Write-Host "Export all processed data to json file: IntuneManagementExtensionDiagnostics_export.json" + [array]$ExportJson = $LogEntryList | Select-Object -Property Index, FileName, Line, DateTime, Multiline, ProcessRunTime, Message, Component, Context, Type, Thread, File + #$ExportJson = $LogEntryList | Select-Object -Property Index, FileName, Line, DateTime, Multiline, ProcessRunTime, Message, Component, Context, Type, Thread, File #| Sort-Object -Property Index + $ExportJson | ConvertTo-Json | Out-File -Filepath ./IntuneManagementExtensionDiagnostics_export.json -Force + Write-Host "Done" + } +#> + + if($ShowLogViewerUI) { + Write-Host "Show logs in Out-GridView" + + # Show log in Out-GridView + if($SelectedLogFiles -is [Array]) { + # Sort log entries and add index number + # This is needed with multiple files so log entries + # can be sorted based on this index column + + # With multiple log files we sort log entries by date + #$SelectedLines = $LogEntryList | Where-Object { ($_.LogEntryDateTime -gt $LogStartDateTimeObject) -and ($_.LogEntryDateTime -lt $LogEndDateTimeObject) } | Select-Object -Property Index, FileName, Line, DateTime, Multiline, ProcessRunTime, Message, Component, Context, Type, Thread, File | Sort-Object -Property Index | Out-GridView -Title "Intune IME Log Viewer $($SelectedLogFiles.Name -join " ")" -OutputMode Multiple + + $SelectedLines = $LogEntryList | Select-Object -Property Index, FileName, Line, DateTime, Multiline, ProcessRunTime, Message, Component, Context, Type, Thread, File | Sort-Object -Property Index | Out-GridView -Title "Intune IME Log Viewer $($SelectedLogFiles.Name -join " ")" -OutputMode Multiple + } else { + # With single log file sorting is ok by default (line numbers) + #$SelectedLines = $LogEntryList | Where-Object { ($_.LogEntryDateTime -gt $LogStartDateTimeObject) -and ($_.LogEntryDateTime -lt $LogEndDateTimeObject) } | Select-Object -Property Index, FileName, Line, DateTime, Multiline, ProcessRunTime, Message, Component, Context, Type, Thread, File | Out-GridView -Title "Intune IME Log Viewer $LogFilePath" -OutputMode Multiple + + $SelectedLines = $LogEntryList | Select-Object -Property Index, FileName, Line, DateTime, Multiline, ProcessRunTime, Message, Component, Context, Type, Thread, File | Out-GridView -Title "Intune IME Log Viewer $LogFilePath" -OutputMode Multiple + } + } else { + + Write-Host "`nTip: Use Parameter -ShowLogViewerUI to get LogViewerUI for graphical log viewing/debugging`n" -ForegroundColor Cyan + + # Not showing Out-GridView so we need to make sure Do-While loop will exit + $SelectedLines = $null + Break + } + +} While ($SelectedLines.Message -eq 'RELOAD LOG FILE(S). Select this line and OK button from right botton corner') + +#Write-Host "Script end"