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