Collect-RemediationLogs.ps1
|
<#PSScriptInfo
.VERSION 2.0.0 .GUID 4db7d295-a70a-4313-aeeb-909de8d2880b .AUTHOR Jeff Gilbert .RELEASENOTES 10.13.26 v1.0.3 Initial release. 10.23.26 v2.0.0 Added -System switch to download PsExec and enable cached log collection with required SYSTEM privileges. #> <# .SYNOPSIS Collects Microsoft Intune Management Extension and Remediation troubleshooting information from a Windows endpoint. .DESCRIPTION Collects: - Remediation-related Intune Management Extension logs - Custom Intune Remediation logs [$customLogPath] - Custom registry checkpoint paths [$CheckpointPath] - IME service information - HealthScripts cache inventory - Relevant registry information - DeviceManagement Enterprise Diagnostics event log - Device and enrollment summary - Recent IME log entries containing remediation-related terms Run from an elevated PowerShell session. Creates a timestamped ZIP file in .\CollectedRemediationLogs relative to the current directory by default. Use -System to download PsExec, verify its Microsoft signature, and relaunch this script interactively as SYSTEM. The relaunch preserves OutputPath, DaysOfEvents, CustomLogPath, and CheckpointPath and automatically enables IncludeCachedScripts. If already running as SYSTEM, no download or relaunch occurs; specify -IncludeCachedScripts explicitly to include cached script contents. Without -System, cached script contents are excluded. Specifying -IncludeCachedScripts without -System causes the script to stop. .PARAMETER OutputPath Directory where the final ZIP file will be created. Defaults to .\CollectedRemediationLogs relative to the current directory. The directory is created if it does not exist. Relative paths are resolved before relaunching as SYSTEM. .PARAMETER DaysOfEvents Number of days of DeviceManagement Enterprise Diagnostics events to include in the CSV export. Defaults to 3; valid values are 1 through 90. The native EVTX export is not limited by this setting. .PARAMETER CustomLogPath Directory containing custom remediation logs. Defaults to C:\Logs\IR. Relative paths are resolved before relaunching as SYSTEM. .PARAMETER CheckpointPath PowerShell registry path containing custom remediation checkpoints. Defaults to HKLM:\SOFTWARE\Remediations. Specify your company's actual registry path, such as HKLM:\SOFTWARE\Contoso\Remediations. .PARAMETER IncludeCachedScripts Copies the HealthScripts cache, including DetectionScript.ps1 and RemediationScript.ps1 files. Requires -System, even when the current process already runs as SYSTEM. Automatically enabled when -System relaunches the script from a non-SYSTEM session. Use carefully because cached files can contain sensitive information and internal implementation details. .PARAMETER System When not already running as SYSTEM, downloads PsExec.zip from https://live.sysinternals.com/Files/PsExec.zip to a temporary directory, verifies the Microsoft signature on PsExec64.exe, and relaunches this script with PsExec -accepteula -s -i using Windows PowerShell. Requires an elevated PowerShell session; does not request elevation. Accepts the Sysinternals PsExec license agreement and automatically enables cached-script collection during relaunch. Waits for collection to finish, then removes the temporary PsExec directory. If already running as SYSTEM, continues collection without relaunching. .EXAMPLE .\Collect-RemediationLogs.ps1 Collects diagnostics with three days of events in the CSV export and writes the ZIP to .\CollectedRemediationLogs. Excludes cached script contents. .EXAMPLE .\Collect-RemediationLogs.ps1 -OutputPath "C:\Support" -DaysOfEvents 7 Writes the ZIP to C:\Support and includes seven days of events in the CSV. .EXAMPLE .\Collect-RemediationLogs.ps1 -System From an elevated non-SYSTEM session, relaunches as SYSTEM and automatically includes cached script contents. .EXAMPLE .\Collect-RemediationLogs.ps1 -System -IncludeCachedScripts Explicitly includes cached script contents, including when the calling process is already running as SYSTEM. .EXAMPLE .\Collect-RemediationLogs.ps1 -System -CustomLogPath "C:\Logs\Contoso" -CheckpointPath "HKLM:\SOFTWARE\Contoso\Remediations" Collects custom logs and registry checkpoints from the specified paths. .NOTES Requires Windows. The SYSTEM relaunch requires internet access to live.sysinternals.com and permission to run PsExec and create its service. Review the resulting ZIP before sharing it because logs, registry exports, and optional cached scripts can contain sensitive information. #> [CmdletBinding()] param ( [Parameter()] [ValidateNotNullOrEmpty()] #[string]$OutputPath = "$env:windir\Temp", [string]$OutputPath = ".\CollectedRemediationLogs", [Parameter()] [ValidateRange(1, 90)] [int]$DaysOfEvents = 3, [Parameter()] [switch]$IncludeCachedScripts, [Parameter()] [switch]$System, [Parameter()] [ValidateNotNullOrEmpty()] [string]$CustomLogPath = 'C:\Logs\IR', [Parameter()] [ValidateNotNullOrEmpty()] [string]$CheckpointPath = 'HKLM:\SOFTWARE\Remediations' ) Set-StrictMode -Version Latest $ErrorActionPreference = 'Stop' $currentIdentity = [System.Security.Principal.WindowsIdentity]::GetCurrent() $principal = [System.Security.Principal.WindowsPrincipal]::new($currentIdentity) if (-not $currentIdentity.IsSystem -and -not $principal.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator)) { throw 'This script requires an elevated PowerShell session. Run PowerShell as administrator and try again.' } if ($IncludeCachedScripts -and -not $System) { throw 'The -IncludeCachedScripts switch requires the -System switch.' } if ($System -and -not $currentIdentity.IsSystem) { $IncludeCachedScripts = $true Write-Host 'Relaunching script as SYSTEM...' $psExecDirectory = Join-Path $env:TEMP ('IntuneRemediation-PsExec-' + [guid]::NewGuid()) try { New-Item -Path $psExecDirectory -ItemType Directory | Out-Null $psExecArchive = Join-Path $psExecDirectory 'PsExec.zip' Write-Host 'Downloading PsExec to relaunch log collection as SYSTEM.' Invoke-WebRequest ` -Uri 'https://live.sysinternals.com/Files/PsExec.zip' ` -OutFile $psExecArchive ` -UseBasicParsing Expand-Archive -LiteralPath $psExecArchive -DestinationPath $psExecDirectory $psExecPath = Join-Path $psExecDirectory 'PsExec64.exe' $signature = Get-AuthenticodeSignature -LiteralPath $psExecPath if ($signature.Status -ne 'Valid' -or $signature.SignerCertificate.Subject -notmatch 'O=Microsoft Corporation(?:,|$)') { throw 'PsExec64.exe does not have a valid Microsoft signature.' } $psExecArguments = @( '-accepteula', '-s', '-i', 'powershell.exe', '-NoProfile', '-File', $PSCommandPath, '-System', '-OutputPath', $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($OutputPath), '-DaysOfEvents', $DaysOfEvents.ToString(), '-CustomLogPath', $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($CustomLogPath), '-CheckpointPath', $CheckpointPath ) if ($IncludeCachedScripts) { $psExecArguments += '-IncludeCachedScripts' } & $psExecPath @psExecArguments if ($LASTEXITCODE -ne 0) { throw "PsExec or the SYSTEM collection process returned exit code $LASTEXITCODE." } } finally { if (Test-Path -LiteralPath $psExecDirectory) { Remove-Item -LiteralPath $psExecDirectory -Recurse -Force -ErrorAction SilentlyContinue } } return } $timestamp = Get-Date -Format 'yyyyMMdd_HHmmss' $computerName = $env:COMPUTERNAME $collectionName = "IntuneRemediationLogs_${computerName}_${timestamp}" $workingPath = Join-Path -Path $env:TEMP -ChildPath $collectionName $zipPath = Join-Path -Path $OutputPath -ChildPath "$collectionName.zip" $imeLogPath = 'C:\ProgramData\Microsoft\IntuneManagementExtension\Logs' $healthScriptsPath = 'C:\Windows\IMECache\HealthScripts' $remediationImeLogPattern = '^(HealthScripts|AgentExecutor|IntuneManagementExtension)(-\d{8}-\d{6})?\.log$' $imeRegistryPath = 'HKLM:\SOFTWARE\Microsoft\IntuneManagementExtension' $sideCarRegistryPath = 'HKLM:\SOFTWARE\Microsoft\IntuneManagementExtension\SideCarPolicies\Scripts' $eventLogName = 'Microsoft-Windows-DeviceManagement-Enterprise-Diagnostics-Provider/Admin' function Write-CollectionLog { param ( [Parameter(Mandatory)] [string]$Message, [Parameter()] [ValidateSet('INFO', 'WARN', 'ERROR')] [string]$Level = 'INFO' ) $entry = '[{0}] [{1}] {2}' -f ` (Get-Date -Format 'yyyy-MM-dd HH:mm:ss'), $Level, $Message Write-Host $entry if (Test-Path -LiteralPath $workingPath) { $entry | Out-File ` -FilePath (Join-Path $workingPath 'Collection.log') ` -Append ` -Encoding utf8 } } function Export-RegistryTree { param ( [Parameter(Mandatory)] [string]$PowerShellPath, [Parameter(Mandatory)] [string]$RegExePath, [Parameter(Mandatory)] [string]$OutputFile ) if (-not (Test-Path -LiteralPath $PowerShellPath)) { Write-CollectionLog ` -Message "Registry path not present: $PowerShellPath" ` -Level WARN return } try { & reg.exe export $RegExePath $OutputFile /y | Out-Null if ($LASTEXITCODE -ne 0) { throw "reg.exe returned exit code $LASTEXITCODE." } Write-CollectionLog "Exported registry path: $RegExePath" } catch { Write-CollectionLog ` -Message "Could not export $RegExePath. $($_.Exception.Message)" ` -Level ERROR } } try { if (-not (Test-Path -LiteralPath $OutputPath)) { New-Item ` -Path $OutputPath ` -ItemType Directory ` -Force | Out-Null } if (Test-Path -LiteralPath $workingPath) { Remove-Item ` -LiteralPath $workingPath ` -Recurse ` -Force } New-Item ` -Path $workingPath ` -ItemType Directory ` -Force | Out-Null Write-CollectionLog "Starting Intune Remediation log collection." # ------------------------------------------------------------ # Collection directories # ------------------------------------------------------------ $imeDestination = Join-Path $workingPath 'IntuneManagementExtension-Logs' $customDestination = Join-Path $workingPath 'Custom-Remediation-Logs' $registryDestination = Join-Path $workingPath 'Registry' $eventDestination = Join-Path $workingPath 'EventLogs' $cacheDestination = Join-Path $workingPath 'HealthScripts-Cache' foreach ($directory in @( $imeDestination, $customDestination, $registryDestination, $eventDestination, $cacheDestination )) { New-Item ` -Path $directory ` -ItemType Directory ` -Force | Out-Null } # ------------------------------------------------------------ # IME logs # ------------------------------------------------------------ $remediationImeLogs = @() if (Test-Path -LiteralPath $imeLogPath) { $remediationImeLogs = @( Get-ChildItem ` -LiteralPath $imeLogPath ` -File ` -ErrorAction Stop | Where-Object Name -Match $remediationImeLogPattern ) if ($remediationImeLogs.Count -gt 0) { $remediationImeLogs | Copy-Item ` -Destination $imeDestination ` -Force ` -ErrorAction Stop Write-CollectionLog ` "Collected $($remediationImeLogs.Count) remediation-related IME logs." } else { Write-CollectionLog ` -Message "No remediation-related IME logs were found in: $imeLogPath" ` -Level WARN } } else { Write-CollectionLog ` -Message "IME log folder was not found: $imeLogPath" ` -Level WARN } # ------------------------------------------------------------ # Custom remediation logs # ------------------------------------------------------------ if (Test-Path -LiteralPath $customLogPath) { Copy-Item ` -LiteralPath $customLogPath ` -Destination $customDestination ` -Recurse ` -Force ` -ErrorAction Continue Write-CollectionLog "Collected custom remediation logs." } else { Write-CollectionLog ` -Message "Custom remediation log folder was not found." ` -Level WARN } # ------------------------------------------------------------ # IME service information # ------------------------------------------------------------ $serviceOutput = Join-Path $workingPath 'IntuneManagementExtension-Service.txt' try { $imeService = Get-CimInstance ` -ClassName Win32_Service ` -Filter "Name='IntuneManagementExtension'" if ($null -ne $imeService) { $imeService | Select-Object ` Name, DisplayName, State, StartMode, StartName, ProcessId, PathName | Format-List | Out-File ` -FilePath $serviceOutput ` -Encoding utf8 } else { 'The IntuneManagementExtension service was not found.' | Out-File ` -FilePath $serviceOutput ` -Encoding utf8 } } catch { $_ | Out-String | Out-File ` -FilePath $serviceOutput ` -Encoding utf8 } # ------------------------------------------------------------ # Device summary # ------------------------------------------------------------ $deviceOutput = Join-Path $workingPath 'Device-Summary.txt' $operatingSystem = Get-CimInstance Win32_OperatingSystem $computerSystem = Get-CimInstance Win32_ComputerSystem $bios = Get-CimInstance Win32_BIOS [pscustomobject]@{ ComputerName = $computerName Manufacturer = $computerSystem.Manufacturer Model = $computerSystem.Model SerialNumber = $bios.SerialNumber OperatingSystem = $operatingSystem.Caption OSVersion = $operatingSystem.Version OSBuild = $operatingSystem.BuildNumber LastBootTime = $operatingSystem.LastBootUpTime PowerShellVersion = $PSVersionTable.PSVersion.ToString() CollectionTime = Get-Date CollectionUser = [System.Security.Principal.WindowsIdentity]::GetCurrent().Name Is64BitOperatingSystem = [System.Environment]::Is64BitOperatingSystem Is64BitProcess = [System.Environment]::Is64BitProcess } | Format-List | Out-File ` -FilePath $deviceOutput ` -Encoding utf8 # ------------------------------------------------------------ # Scheduled tasks related to Enterprise Management # ------------------------------------------------------------ $taskOutput = Join-Path $workingPath 'EnterpriseManagement-ScheduledTasks.txt' try { Get-ScheduledTask -ErrorAction Stop | Where-Object { $_.TaskPath -like '\Microsoft\Windows\EnterpriseMgmt*' -or $_.TaskName -match 'Intune|Enterprise' } | Select-Object ` TaskPath, TaskName, State, Description | Sort-Object TaskPath, TaskName | Format-Table -AutoSize | Out-String -Width 300 | Out-File ` -FilePath $taskOutput ` -Encoding utf8 } catch { $_ | Out-String | Out-File ` -FilePath $taskOutput ` -Encoding utf8 } # ------------------------------------------------------------ # HealthScripts cache inventory # ------------------------------------------------------------ $inventoryCsv = Join-Path $cacheDestination 'HealthScripts-Inventory.csv' if (Test-Path -LiteralPath $healthScriptsPath) { $scriptFiles = Get-ChildItem ` -LiteralPath $healthScriptsPath ` -File ` -Recurse ` -ErrorAction Continue $inventory = foreach ($file in $scriptFiles) { [pscustomobject]@{ RelativePath = $file.FullName.Substring( $healthScriptsPath.Length ).TrimStart('\') FileName = $file.Name Length = $file.Length LastWriteTime = $file.LastWriteTime } } $inventory | Export-Csv ` -LiteralPath $inventoryCsv ` -NoTypeInformation ` -Encoding utf8 if ($IncludeCachedScripts) { Copy-Item ` -LiteralPath $healthScriptsPath ` -Destination $cacheDestination ` -Recurse ` -Force ` -ErrorAction Continue Write-CollectionLog ` "Collected HealthScripts inventory and cached script files." } else { Write-CollectionLog ` "Collected HealthScripts inventory. Script contents were excluded. Use -System to pull cached script contents." } } else { Write-CollectionLog ` -Message "HealthScripts cache was not found: $healthScriptsPath" ` -Level WARN } # ------------------------------------------------------------ # Registry exports # ------------------------------------------------------------ Export-RegistryTree ` -PowerShellPath $imeRegistryPath ` -RegExePath 'HKLM\SOFTWARE\Microsoft\IntuneManagementExtension' ` -OutputFile ( Join-Path $registryDestination ` 'IntuneManagementExtension.reg' ) Export-RegistryTree ` -PowerShellPath $sideCarRegistryPath ` -RegExePath ` 'HKLM\SOFTWARE\Microsoft\IntuneManagementExtension\SideCarPolicies\Scripts' ` -OutputFile ( Join-Path $registryDestination ` 'SideCarPolicies-Scripts.reg' ) Export-RegistryTree ` -PowerShellPath $checkpointPath ` -RegExePath $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($CheckpointPath) ` -OutputFile ( Join-Path $registryDestination ` 'Company-IntuneRemediations.reg' ) # ------------------------------------------------------------ # DeviceManagement Enterprise Diagnostics event log # ------------------------------------------------------------ $startTime = (Get-Date).AddDays(-$DaysOfEvents) try { $events = Get-WinEvent ` -FilterHashtable @{ LogName = $eventLogName StartTime = $startTime } ` -ErrorAction Stop $events | Select-Object ` TimeCreated, Id, LevelDisplayName, ProviderName, Message | Export-Csv ` -LiteralPath ( Join-Path $eventDestination ` 'DeviceManagement-Enterprise-Diagnostics.csv' ) ` -NoTypeInformation ` -Encoding utf8 Write-CollectionLog ` "Collected $($events.Count) DeviceManagement events." } catch { Write-CollectionLog ` -Message "Could not collect DeviceManagement events. $($_.Exception.Message)" ` -Level WARN } # Export the native event log when possible. try { $evtxOutput = Join-Path $eventDestination ` 'DeviceManagement-Enterprise-Diagnostics.evtx' & wevtutil.exe epl $eventLogName $evtxOutput if ($LASTEXITCODE -ne 0) { throw "wevtutil.exe returned exit code $LASTEXITCODE." } } catch { Write-CollectionLog ` -Message "Could not export the EVTX file. $($_.Exception.Message)" ` -Level WARN } # ------------------------------------------------------------ # Remediation-focused log extracts # ------------------------------------------------------------ $extractOutput = Join-Path $workingPath 'Remediation-Relevant-Log-Entries.txt' $searchTerms = @( 'HealthScripts', 'Remediation', 'DetectionScript', 'RemediationScript', 'exit code', 'SideCarPolicies', 'error', 'exception', 'failed' ) foreach ($logFile in $remediationImeLogs) { "============================================================" | Out-File ` -FilePath $extractOutput ` -Append ` -Encoding utf8 "SOURCE: $($logFile.FullName)" | Out-File ` -FilePath $extractOutput ` -Append ` -Encoding utf8 "============================================================" | Out-File ` -FilePath $extractOutput ` -Append ` -Encoding utf8 Select-String ` -LiteralPath $logFile.FullName ` -Pattern $searchTerms ` -SimpleMatch ` -ErrorAction Continue | Select-Object ` Path, LineNumber, Line | Format-Table -Wrap | Out-String -Width 400 | Out-File ` -FilePath $extractOutput ` -Append ` -Encoding utf8 } # ------------------------------------------------------------ # Package manifest # ------------------------------------------------------------ Get-ChildItem ` -LiteralPath $workingPath ` -File ` -Recurse | Select-Object ` FullName, Length, LastWriteTime | Export-Csv ` -LiteralPath ( Join-Path $workingPath 'Collection-Manifest.csv' ) ` -NoTypeInformation ` -Encoding utf8 # ------------------------------------------------------------ # Compress the package # ------------------------------------------------------------ if (Test-Path -LiteralPath $zipPath) { Remove-Item ` -LiteralPath $zipPath ` -Force } Compress-Archive ` -Path (Join-Path $workingPath '*') ` -DestinationPath $zipPath ` -CompressionLevel Optimal ` -Force Write-CollectionLog "Collection completed successfully." Write-Host Write-Host "Output package: $zipPath" -ForegroundColor Green Get-Item -LiteralPath $zipPath | Select-Object FullName, Length, LastWriteTime # ------------------------------------------------------------ # Grab the help file # ------------------------------------------------------------ $Uri = "https://raw.githubusercontent.com/jeffgilb/Intune-Remediation-Framework/main/Troubleshooting/troubleshooting.md" $OutFile = "$OutputPath\troubleshooting.md" Invoke-WebRequest -Uri $Uri -OutFile $OutFile Write-Host "Troubleshooting help file downloaded to $OutFile" -ForegroundColor Green if ($system -eq $true) { Write-Host pause # Pause to display output for SYSTEM mode before the console closes. } } catch { Write-CollectionLog ` -Message "Collection failed. $($_.Exception.Message)" ` -Level ERROR throw } finally { if (Test-Path -LiteralPath $workingPath) { Remove-Item ` -LiteralPath $workingPath ` -Recurse ` -Force ` -ErrorAction SilentlyContinue } } |