Invoke-IntuneODC.ps1
|
<#PSScriptInfo .VERSION 1.0.1 .GUID d6cbaed4-c787-438a-9ca2-88688b443c67 .AUTHOR Jeff Gilbert .COMPANYNAME Community .DESCRIPTION This script is intended for deployment through Microsoft Intune Remediations to collect Intune ODC diagnostic information from managed Windows devices. .RELEASENOTES v1.0.0 - 08.04.2026 - Initial release v1.0.1 - 08.06.2026 - Metadata updates #> <# .DESCRIPTION Collects Intune ODC logs and uploads them to a central repository. The script performs the following actions: • Forces execution in 64-bit PowerShell when necessary • Creates a unique Run ID for tracking purposes • Downloads the latest Intune ODC configuration and collection script from Microsoft • Executes the Intune ODC collector • Locates the generated diagnostic ZIP package • Uploads the ZIP file to a designated network share • Records execution details in the registry • Writes CMTrace-compatible logs • Cleans up temporary working files after completion Execution status is recorded both in the local log file and a registry checkpoint that can be used for troubleshooting, reporting, and remediation tracking. .PARAMETER logFilePath Local directory where the CMTrace-compatible log file is stored. Example: C:\Logs\IntuneRemediations .PARAMETER logName Friendly name used for: • Log file name • Registry checkpoint key name • Status reporting Example: IntuneODC .PARAMETER regPath Registry root path used to store remediation checkpoint information. Example: HKLM:\SOFTWARE\IntuneRemediations .PARAMETER WorkingDirRoot Temporary working directory used for downloading and executing the Intune ODC collector. Example: C:\ProgramData\Microsoft\IntuneODC .PARAMETER UploadShare UNC path where collected ODC ZIP files will be uploaded. The executing device must have write access to this location. Example: \\SERVER\SHARE\IntuneODC .NOTES Requirements: • PowerShell 5.1 or later • Windows device managed by Intune • Internet access to Microsoft download endpoints • Write access to the configured upload share • Access to create files in the configured log directory Microsoft ODC Download Endpoints: https://aka.ms/intuneXML https://aka.ms/intunePS1 Registry Checkpoint Location: HKLM:\SOFTWARE\IntuneRemediations\<LogName> Stored Registry Values: RemediationBeginTimeUtc RemediationEndTimeUtc RemediationRuntime RemediationExitCode RunId ScriptVersion Log File Location: <logFilePath>\<logName>.log .EXAMPLE Deploy through Microsoft Intune Remediations. The script downloads the latest Intune ODC collector, gathers diagnostic data, uploads the resulting ZIP package to the configured network share, and records execution results in the registry. .EXAMPLE Run locally for troubleshooting. PS C:\> .\Invoke-IntuneODC.ps1 Collects Intune diagnostics and uploads the generated ZIP file to the configured upload share. .OUTPUTS These are displayed in the Intune Admin center for remediation results: Success: Intune ODC logs successfully collected to <UploadShare> Failure: Intune ODC log collection failed. See <LogFile> for details. .EXITCODES 0 Log collection completed successfully and ZIP uploaded. 1 One or more steps failed, including: • Upload share validation • Download failures • ODC execution failures • ZIP discovery failures • Upload failures .LINK https://learn.microsoft.com/mem/intune/ .LINK https://learn.microsoft.com/troubleshoot/mem/intune/ #> #region userVariables # !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! THINGS YOU NEED TO CHANGE !!!!!!!!!!!!!!!!!!!!!!!!!!! # What do you want to call this log collection script? This will be used in the registry path and log file names. $logName = "IntuneODC" # Where do you want to store the log file? $logFilePath = "C:\Logs\IntuneRemediations\" # Building out the log file path here to avoid issues with the log file being created in the wrong location. $logFile = Join-Path $logFilePath "$($logName).log" # Where do you want the script to write the remediation results to the registry. $regPath = 'HKLM:\SOFTWARE\IntuneRemediations' # What working directory do you want to use for the Intune ODC script? This is where the script will download and run the ODC script. $WorkingDirRoot = "$env:ProgramData\Microsoft\IntuneODC" # Where do you want to upload the collected logs? Make sure this share is reachable and writable by the device running this script. Use UNC path format (e.g., \\server\share). $UploadShare = "\\SERVER\SHARE\IntuneODC" #endregion #!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! DO NOT MODIFY BELOW THIS LINE !!!!!!!!!!!!!!!!!!!!!!!!!! #region prep $ErrorActionPreference = 'Stop' #-------------------------------------- Functions ------------------------------------- function Log { [CmdletBinding()] Param( [Parameter(Mandatory = $true, Position = 0)] [ValidateNotNullOrEmpty()] [string]$Message, [Parameter(Mandatory = $false, Position = 1)] [string]$Component = "IntuneODC", [Parameter(Mandatory = $false, Position = 2)] [ValidateSet(1, 2, 3)] [int]$Type = 1 ) $now = Get-Date $time = $now.ToString('HH:mm:ss.ffffff', [System.Globalization.CultureInfo]::InvariantCulture) $date = $now.ToString('MM-dd-yyyy', [System.Globalization.CultureInfo]::InvariantCulture) $escapedMessage = [System.Security.SecurityElement]::Escape($Message) $scriptName = if ($PSCommandPath) { Split-Path -Path $PSCommandPath -Leaf } else { $null } $stack = Get-PSCallStack if ($stack.Count -gt 1) { $caller = $stack[1].Command if ($caller -and $scriptName -and ($caller -ne $scriptName)) { $Component = $caller } elseif ($caller -and -not $scriptName) { $Component = $caller } } $logLine = ('<![LOG[{0}]LOG]!><time="{1}" date="{2}" component="{3}" context="" type="{4}" thread="" file="">' -f ` $escapedMessage, $time, $date, $Component, $Type) try { $dir = Split-Path -Path $LogFile -Parent if ($dir -and -not (Test-Path -LiteralPath $dir)) { New-Item -ItemType Directory -Path $dir -Force | Out-Null } Add-Content -LiteralPath $LogFile -Value $logLine -Encoding UTF8 } catch { Write-Error -Message ("Failed to write log to '{0}': {1}" -f $LogFile, $_.Exception.Message) return } switch ($Type) { 1 { Write-Host $Message -ForegroundColor Gray } 2 { Write-Host $Message -ForegroundColor Yellow } 3 { Write-Host $Message -ForegroundColor Red } default { Write-Host $Message } } } function Write-RemediationResult { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [string]$LogName, [Parameter(Mandatory = $false)] [ValidateNotNullOrEmpty()] [string]$ScriptVersion, [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [string]$RunId, [Parameter(Mandatory = $true)] [datetime]$StartTime, [Parameter(Mandatory = $false)] [datetime]$EndTime = (Get-Date), [Parameter(Mandatory = $false)] [int]$ExitCode = 0 ) try { if (-not (Test-Path -LiteralPath $regPath)) { New-Item -Path $regPath -Force | Out-Null } $baseKey = Join-Path $regPath "$LogName" if (-not (Test-Path -LiteralPath $baseKey)) { New-Item -Path $baseKey -Force | Out-Null } $duration = ($EndTime).ToUniversalTime() - ($StartTime).ToUniversalTime() $properties = [ordered]@{ RemediationBeginTimeUtc = $StartTime.ToUniversalTime().ToString("o") RemediationEndTimeUtc = $EndTime.ToUniversalTime().ToString("o") RemediationRuntime = $duration.ToString("hh\:mm\:ss") RemediationExitCode = $ExitCode.ToString() RunId = $RunId ScriptVersion = $ScriptVersion } foreach ($property in $properties.GetEnumerator()) { New-ItemProperty -Path $baseKey -Name $property.Key -Value $property.Value -PropertyType String -Force | Out-Null } Log "Registry checkpoint updated: $baseKey" Log "RunTime: $($duration.ToString('hh\:mm\:ss'))" Log "Remediation Exit Code = $($ExitCode)" } catch { Log "Failed to write remediation registry result: $($_.Exception.Message)" -Type 2 } } # If running as 32-bit PowerShell on x64 Windows, relaunch in 64-bit PowerShell. if ("$env:PROCESSOR_ARCHITEW6432" -ne "ARM64") { if (Test-Path "$($env:WINDIR)\SysNative\WindowsPowerShell\v1.0\powershell.exe") { & "$($env:WINDIR)\SysNative\WindowsPowerShell\v1.0\powershell.exe" ` -ExecutionPolicy Bypass ` -NoProfile ` -File "$PSCommandPath" exit $LASTEXITCODE } } $script:ExitCode = 0 $script:RunId = [guid]::NewGuid() $script:StartTime = Get-Date if (-not (Test-Path $WorkingDirRoot)) { New-Item -Path $WorkingDirRoot -ItemType Directory -Force | Out-Null } $WorkingDir = Join-Path $WorkingDirRoot $script:RunId New-Item -Path $WorkingDir -ItemType Directory -Force | Out-Null if (-not (Test-Path $logFilePath)) { New-Item -Path $logFilePath -ItemType Directory -Force | Out-Null } #endregion #region main # --------------------------------------- Main ---------------------------------------- Log "--------------------------- BEGIN INTUNE ODC LOG COLLECTION --------------------------" Log "RunId = $($script:RunId)" Log "WorkingDir = $WorkingDir" Write-RemediationResult ` -LogName $logName ` -RunId $script:RunId ` -StartTime $script:StartTime ` -EndTime (Get-Date) ` -ExitCode 0 try { Log "Preflight: validating upload share availability: $UploadShare" if (-not (Test-Path -LiteralPath $UploadShare)) { throw "Upload share is not found: $UploadShare" } $preflightFile = Join-Path $UploadShare ("IntuneODC-preflight-{0}-{1}.tmp" -f $env:COMPUTERNAME, [guid]::NewGuid().ToString('N')) Set-Content -LiteralPath $preflightFile -Value ("preflight {0:o}" -f (Get-Date)) -Encoding UTF8 -Force Remove-Item -LiteralPath $preflightFile -Force -ErrorAction SilentlyContinue Log "Preflight passed" $XMLFile = Join-Path $WorkingDir "Intune.xml" $PS1File = Join-Path $WorkingDir "IntuneODCStandAlone.ps1" Log "Downloading Intune XML" Invoke-WebRequest -Uri "https://aka.ms/intuneXML" -OutFile $XMLFile -UseBasicParsing Log "Downloading ODC script" Invoke-WebRequest -Uri "https://aka.ms/intunePS1" -OutFile $PS1File -UseBasicParsing if (Test-Path $PS1File) { $Match = Select-String -Path $PS1File -Pattern '^\s*\$ODCversion\s*=\s*"([^"]+)"' if ($Match) { $ODCversion = $Match.Matches[0].Groups[1].Value } else { $ODCversion = "Unknown" } } else { $ODCversion = "FileNotFound" } # What version of the Intune ODC script did we just download? Log "Detected Intune ODC Version: $ODCversion" Log "Starting $PS1File" $odcProcess = Start-Process ` -FilePath powershell.exe ` -ArgumentList "-ExecutionPolicy Bypass -NoProfile -File `"$PS1File`"" ` -WorkingDirectory $WorkingDir ` -Wait ` -PassThru Log "ODC ExitCode = $($odcProcess.ExitCode)" if ($odcProcess.ExitCode -ne 0) { throw "ODC script exited with code $($odcProcess.ExitCode)" } Log "ODC execution completed" Log "Directory contents after ODC execution" Get-ChildItem ` -Path $WorkingDir ` -Recurse ` -Force ` -ErrorAction SilentlyContinue | Sort-Object FullName | ForEach-Object { Log $_.FullName } $ZipFile = Get-ChildItem ` -Path $WorkingDir ` -Filter "*CollectedData*.zip" ` -File ` -ErrorAction SilentlyContinue | Sort-Object LastWriteTime -Descending | Select-Object -First 1 if (-not $ZipFile) { $ZipFile = Get-ChildItem ` -Path $WorkingDir ` -Filter "*.zip" ` -File ` -ErrorAction SilentlyContinue | Sort-Object LastWriteTime -Descending | Select-Object -First 1 } if (-not $ZipFile) { Log "No ZIP files found in working directory." -Type 3 Get-ChildItem ` -Path $WorkingDir ` -Recurse ` -Force ` -ErrorAction SilentlyContinue | ForEach-Object { Log $_.FullName -Type 2 } throw "Expected ODC ZIP not found in $WorkingDir" } Log "Found ZIP: $($ZipFile.FullName)" $ComputerFolder = Join-Path $UploadShare $env:COMPUTERNAME if (-not (Test-Path -LiteralPath $ComputerFolder)) { New-Item -Path $ComputerFolder -ItemType Directory -Force | Out-Null } $Destination = Join-Path $ComputerFolder "$($env:COMPUTERNAME)_$(Get-Date -Format 'yyyyMMdd_HHmmss').zip" Copy-Item -Path $ZipFile.FullName -Destination $Destination -Force Log "Uploaded ZIP to $Destination" } catch { Log "ERROR: $($_.Exception.Message)" -Type 3 $script:ExitCode = 1 } finally { $endTime = Get-Date Write-RemediationResult ` -LogName $logName ` -ScriptVersion $ODCversion ` -RunId $script:RunId ` -StartTime $script:StartTime ` -EndTime $endTime ` -ExitCode $script:ExitCode try { Remove-Item -Path $WorkingDir -Recurse -Force -ErrorAction SilentlyContinue Log "Removed working directory: $WorkingDir" } catch { Log "Cleanup failure: $($_.Exception.Message)" -Type 2 } Log "---------------------------- END INTUNE ODC LOG COLLECTION ---------------------------" } # This is what shows up in the Intune remediation status message. It is not a log file, so keep it short and simple. if ($script:ExitCode -eq 0) { Write-Output "Intune ODC logs successfully collected to $UploadShare" } else { Write-Output "Intune ODC log collection failed. See $LogFile for details." } #endregion exit $script:ExitCode |