ProvAgentTroubleshooter.psm1

<#
Disclaimer: The scripts are not supported under any Microsoft standard support program or service.
The scripts are provided AS IS without warranty of any kind. Microsoft further disclaims all implied
warranties including, without limitation, any implied warranties of merchantability or of fitness for a
particular purpose. The entire risk arising out of the use or performance of the scripts and
documentation remains with you. In no event shall Microsoft, its authors, or anyone else involved in the
creation, production, or delivery of the scripts be liable for any damages whatsoever (including, without
limitation, damages for loss of business profits, business interruption, loss of business information, or
other pecuniary loss) arising out of the use of or inability to use the scripts or documentation,
even if Microsoft has been advised of the possibility of such damages.
#>


#-------------------------------------------------------------------------------------------------------------------------------------
#
# Copyright © 2025 Microsoft Corporation. All rights reserved.
#
#-------------------------------------------------------------------------------------------------------------------------------------
#
# NAME: Entra ID Provisioning Agent Troubleshooting PowerShell Module
#
#-------------------------------------------------------------------------------------------------------------------------------------

<#
.SYNOPSIS
    Collects logs and diagnostic data for Microsoft Entra ID Provisioning Agent troubleshooting.
.DESCRIPTION
    This module configures tracing, collects logs, exports event logs, gathers system and registry info, and compresses
    all data that helps in troubleshooting Microsoft Entra ID Provisioning Agent.
.NOTES
    Author: [Akos Regi & Tariq Jaber]
    Version: 0.0.3
    Date: 2025-05-20
#>


# Color for progress Write-Host messages
$ProgressColor = 'Cyan'

# List of required files to check before compressing
$RequiredFiles = @(
    'Application.evtx',
    'build.txt',
    'CAPI2.evtx',
    'CLOUDSYNC-AADConnectProvisioningAgentServiceReg.txt',
    'credman.txt',
    'CurrentUser-CA-store.txt',
    'CurrentUser-My-store.txt',
    'CurrentUser-Root-store.txt',
    'env.txt',
    'GPComputer.html',
    'GPReport_User.html',
    'ipconfig-info.txt',
    'kdc.etl',
    'kerb.etl',
    'kerb.evtx',
    'lanmanserver-key.txt',
    'lanmanworkstation-key.txt',
    'ldap.etl',
    'LocalMachine-CA-store.txt',
    'LocalMachine-My-store.txt',
    'LocalMachine-Root-store.txt',
    'lsa-key.txt',
    'machine.config',
    'msinfo32.nfo',
    'Netlogon-key.txt',
    'netlogon.log',
    'netmon.cab',
    'netmon.etl',
    'netsetup.log',
    'NTDS.txt',
    'ntlm.etl',
    'Policies-key.txt',
    'schannel-key.txt',
    'Security.evtx',
    'ssl.etl',
    'start-tasklist.txt',
    'stop-tasklist.txt',
    'System.evtx',
    'whoami.txt',
    'winhttp.txt'
)

function Test-FileReady {
    param([string]$Path)
    if (!(Test-Path $Path)) { return $false }
    try {
        $stream = [System.IO.File]::Open($Path, 'Open', 'Read', 'ReadWrite')
        $stream.Close()
        return $true
    } catch {
        return $false
    }
}

<#
.SYNOPSIS
    Collects logs and diagnostic data for Microsoft Provisioning Agent troubleshooting.
.DESCRIPTION
    Collects logs, event logs, system info, and compresses them for support.
.EXAMPLE
    Start-ProvAgentLogCollection
    Start-ProvAgentLogCollection -OutputFolder 'C:\temp' -LogFolder 'C:\temp\Logs'
#>

function Start-ProvAgentLogCollection {
    [CmdletBinding()]

    param(
        [string]$OutputFolder = 'C:\temp',
        [string]$LogFolder = ("C:\temp\ProvAgentLogs_{0}_UTC" -f (Get-Date -Format 'yyyyMMdd-HHmm'))
    )

    # Add current date/time to zip file name if not provided, and ensure it's inside $LogFolder
    $dateTimeStr = (Get-Date -Format 'yyyyMMdd-HHmm')
    if ([string]::IsNullOrWhiteSpace($ZipFile)) {
        $ZipFile = Join-Path $LogFolder "MicrosoftTraceLogs_$dateTimeStr.zip"
    } elseif ($ZipFile -notmatch '\\[0-9]{8}-[0-9]{4}\.zip$') {
        $baseName = [System.IO.Path]::GetFileNameWithoutExtension($ZipFile)
        $ZipFile = Join-Path $LogFolder ("{0}_{1}.zip" -f $baseName, $dateTimeStr)
    } else {
        $ZipFile = Join-Path $LogFolder ([System.IO.Path]::GetFileName($ZipFile))
    }

    Write-Host 'Starting Microsoft Provisioning Agent log collection...'
    Write-Host "[+] Checking/creating output folder: $OutputFolder" -ForegroundColor $ProgressColor
    if (!(Test-Path $OutputFolder)) { New-Item -Path $OutputFolder -ItemType Directory -Force | Out-Null }
    Write-Host "[+] Preparing log folder: $LogFolder" -ForegroundColor $ProgressColor
    if (Test-Path $LogFolder) { Remove-Item "$LogFolder\*" -Force -Recurse -ErrorAction SilentlyContinue }
    else { New-Item -Path $LogFolder -ItemType Directory -Force | Out-Null }

    # Set debug flags (environment variables)
    Write-Host '[+] Setting debug environment variables' -ForegroundColor $ProgressColor
    $env:KdcDebugFlags = '0xfffff'
    $env:ldapDebugFlags = '0x1FFFDFF3'
    $env:NtlmDebugFlags = '0x1ffDf'
    $env:SslDebugFlags = '0xffffffff'
    $env:KerbDebugFlags = '0x6ffffff'

    # Configure registry for tracing
    $regPaths = @(
        'HKLM:\System\CurrentControlSet\Services\ldap\Tracing\AADConnectProvisioningAgent.exe',
        'HKLM:\System\CurrentControlSet\Services\ldap\Tracing\AADConnectProvisioningAgentWizard.exe',
        'HKLM:\System\CurrentControlSet\Services\ldap\Tracing\powershell.exe'
    )
    Write-Host '[+] Configuring registry for tracing' -ForegroundColor $ProgressColor
    foreach ($reg in $regPaths) {
        if (!(Test-Path $reg)) { New-Item -Path $reg -Force | Out-Null }
    }
    Set-ItemProperty -Path 'HKLM:\SYSTEM\CurrentControlSet\Control\LSA' -Name 'SPMInfoLevel' -Value 0xC03E3F -Type DWord -Force
    Set-ItemProperty -Path 'HKLM:\SYSTEM\CurrentControlSet\Control\LSA' -Name 'LogToFile' -Value 1 -Type DWord -Force
    Set-ItemProperty -Path 'HKLM:\SYSTEM\CurrentControlSet\Control\LSA' -Name 'NegEventMask' -Value 0xF -Type DWord -Force

    # Start ETW tracing
    Write-Host '[+] Starting ETW tracing...' -ForegroundColor $ProgressColor
    $logman = 'logman.exe'
    Write-Host '[+] Running: logman start kdc ...' -ForegroundColor $ProgressColor
    & $logman start kdc -p '{1BBA8B19-7F31-43c0-9643-6E911F79A06B}' $env:KdcDebugFlags -o "$LogFolder\kdc.etl" -ets
    Write-Host '[+] Running: logman start kerb ...' -ForegroundColor $ProgressColor
    & $logman start kerb -p '{6B510852-3583-4e2d-AFFE-A67F9F223438}' $env:KerbDebugFlags -o "$LogFolder\kerb.etl" -ets
    Write-Host '[+] Running: logman start ldap ...' -ForegroundColor $ProgressColor
    & $logman start ldap -p '{099614A5-5DD7-4788-8BC9-E29F43DB28FC}' $env:ldapDebugFlags -o "$LogFolder\ldap.etl" -ets
    Write-Host '[+] Running: logman start ntlm ...' -ForegroundColor $ProgressColor
    & $logman start ntlm -p '{5BBB6C18-AA45-49b1-A15F-085F7ED0AA90}' $env:NtlmDebugFlags -o "$LogFolder\ntlm.etl" -ets
    Write-Host '[+] Running: logman start ssl ...' -ForegroundColor $ProgressColor
    & $logman start ssl -p '{37D2C3CD-C5D4-4587-8531-4696C44244C8}' $env:SslDebugFlags -o "$LogFolder\ssl.etl" -ets

    # Set nltest debug flag
    Write-Host '[+] Setting nltest debug flag' -ForegroundColor $ProgressColor
    nltest /dbflag:0x2fffffff | Out-Null

    # Enable and clear event logs
    Write-Host '[+] Enabling and clearing event logs (CAPI2, Kerberos)' -ForegroundColor $ProgressColor
    wevtutil.exe set-log Microsoft-Windows-CAPI2/Operational /enabled:true
    wevtutil.exe clear-log Microsoft-Windows-CAPI2/Operational
    wevtutil.exe set-log Microsoft-Windows-Kerberos/Operational /enabled:true
    wevtutil.exe clear-log Microsoft-Windows-Kerberos/Operational

    # Start network trace
    Write-Host '[+] Starting network trace (netsh)' -ForegroundColor $ProgressColor
    netsh trace start traceFile="$LogFolder\netmon.etl" capture=yes | Out-Null

    # Flush DNS, purge tickets
    Write-Host '[+] Flushing DNS and purging Kerberos tickets' -ForegroundColor $ProgressColor
    ipconfig /flushdns | Out-Null
    klist purge | Out-Null
    klist -li 0x3e7 purge | Out-Null

    # Collect initial info
    Write-Host '[+] Collecting initial system and network info' -ForegroundColor $ProgressColor
    whoami /all | Out-File "$LogFolder\whoami.txt"
    netsh winhttp show proxy | Out-File "$LogFolder\winhttp.txt"
    tasklist /svc | Out-File "$LogFolder\start-tasklist.txt"

    Write-Host '[*] Please reproduce the issue now...' -BackgroundColor Yellow -ForegroundColor Black
    Write-Host ("[*] Current UTC time: {0}" -f ((Get-Date).ToUniversalTime().ToString('yyyy-MM-dd HH:mm:ss'))) -ForegroundColor $ProgressColor
    Write-Host '[*] Waiting 30 seconds or for the issue to be reproduced...' -BackgroundColor Yellow -ForegroundColor Black
    for ($i = 0; $i -lt 30; $i++) {
        $char = if ($i % 2 -eq 0) { '/' } else { '\' }
        Write-Host -NoNewline "`r$char"
        Start-Sleep -Seconds 1
    }
    Write-Host "`r " -NoNewline  # Clear spinner character
    Write-Host '[*] 30 seconds elapsed.' -BackgroundColor Yellow -ForegroundColor Blue
    Read-Host '[*] If the issue was reproduced, press Enter to stop tracing.' 
    Write-Host ("[*] Current UTC time: {0}" -f ((Get-Date).ToUniversalTime().ToString('yyyy-MM-dd HH:mm:ss'))) -ForegroundColor $ProgressColor
    Write-Host '[+] Stopping ETW tracing and collecting logs...' -ForegroundColor $ProgressColor
    Write-Host '[+] Running: logman stop kerb ...' -ForegroundColor $ProgressColor
    & $logman stop kerb -ets
    Write-Host '[+] Running: logman stop kdc ...' -ForegroundColor $ProgressColor
    & $logman stop kdc -ets
    Write-Host '[+] Running: logman stop ldap ...' -ForegroundColor $ProgressColor
    & $logman stop ldap -ets
    Write-Host '[+] Running: logman stop ntlm ...' -ForegroundColor $ProgressColor
    & $logman stop ntlm -ets
    Write-Host '[+] Running: logman stop ssl ...' -ForegroundColor $ProgressColor
    & $logman stop ssl -ets

    # Remove registry keys
    Write-Host '[+] Cleaning up registry keys and debug flags' -ForegroundColor $ProgressColor
    foreach ($reg in $regPaths) {
        Remove-Item -Path $reg -Force -ErrorAction SilentlyContinue
    }
    Remove-ItemProperty -Path 'HKLM:\SYSTEM\CurrentControlSet\Control\LSA' -Name 'SPMInfoLevel' -ErrorAction SilentlyContinue
    Remove-ItemProperty -Path 'HKLM:\SYSTEM\CurrentControlSet\Control\LSA' -Name 'LogToFile' -ErrorAction SilentlyContinue
    Remove-ItemProperty -Path 'HKLM:\SYSTEM\CurrentControlSet\Control\LSA' -Name 'NegEventMask' -ErrorAction SilentlyContinue
    nltest /dbflag:0x0 | Out-Null

    # Export event logs
    Write-Host '[+] Exporting event logs (Application, System, Security, Kerberos, CAPI2)' -ForegroundColor $ProgressColor
    wevtutil.exe export-log Application "$LogFolder\Application.evtx" /overwrite:true
    wevtutil.exe export-log System "$LogFolder\System.evtx" /overwrite:true
    wevtutil.exe export-log Security "$LogFolder\Security.evtx" /overwrite:true
    wevtutil.exe set-log Microsoft-Windows-Kerberos/Operational /enabled:false
    wevtutil.exe export-log Microsoft-Windows-Kerberos/Operational "$LogFolder\kerb.evtx" /overwrite:true
    wevtutil.exe export-log Microsoft-Windows-CAPI2/Operational "$LogFolder\CAPI2.evtx" /overwrite:true

    # Certificate store info
    Write-Host '[+] Exporting certificate store information' -ForegroundColor $ProgressColor
    $stores = @(
        @{ Name = "My"; Location = "LocalMachine" },
        @{ Name = "My"; Location = "CurrentUser" },
        @{ Name = "Root"; Location = "LocalMachine" },
        @{ Name = "Root"; Location = "CurrentUser" },
        @{ Name = "CA"; Location = "LocalMachine" },
        @{ Name = "CA"; Location = "CurrentUser" }
    )
    foreach ($store in $stores) {
        $locSwitch = if ($store.Location -eq "CurrentUser") { "-user" } else { "" }
        $fileName = "$LogFolder\$($store.Location)-$($store.Name)-store.txt"
        Write-Host "[+] -- Exporting cert store: $($store.Location)\$($store.Name)" -ForegroundColor $ProgressColor
        certutil.exe -silent -v $locSwitch -store $store.Name > $fileName
    }

    # Network info
    Write-Host '[+] Collecting network credentials and IP configuration' -ForegroundColor $ProgressColor
    cmdkey.exe /list > "$LogFolder\credman.txt"
    ipconfig /all > "$LogFolder\ipconfig-info.txt"
    Write-Host '[+] Stopping network trace (netsh)' -ForegroundColor $ProgressColor
    netsh trace stop | Out-Null

    # Copy log files
    $copyFiles = @(
        "$env:windir\debug\netlogon.log",
        "$env:windir\debug\netlogon.bak",
        "$env:windir\system32\lsass.log",
        "$env:windir\debug\netsetup.log",
        "$env:windir\Microsoft.NET\Framework64\v4.0.30319\Config\machine.config"
    )
    Write-Host '[+] Copying log files (netlogon, lsass, netsetup, machine.config)' -ForegroundColor $ProgressColor
    foreach ($file in $copyFiles) {
        if (Test-Path $file) { Copy-Item $file $LogFolder -Force }
    }

    # Environment and registry info
    Write-Host '[+] Collecting environment and registry information' -ForegroundColor $ProgressColor
    Get-ChildItem Env: | Out-File "$LogFolder\env.txt"
    reg query "HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion" /v BuildLabEx > "$LogFolder\build.txt"
    reg query "HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Lsa" /s > "$LogFolder\lsa-key.txt"
    reg query "HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies" /s > "$LogFolder\Policies-key.txt"
    reg query "HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\LanmanServer" /s > "$LogFolder\lanmanserver-key.txt"
    reg query "HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\LanmanWorkstation" /s > "$LogFolder\lanmanworkstation-key.txt"
    reg query "HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\Netlogon" /s > "$LogFolder\Netlogon-key.txt"
    reg query "HKEY_LOCAL_MACHINE\System\CurrentControlSet\Services\NTDS" /s > "$LogFolder\NTDS.txt"
    reg query "HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL" /s >  "$LogFolder\schannel-key.txt"

    # DLL version info
    $dlls = @(
        'kerberos.dll','lsasrv.dll','netlogon.dll','kdcsvc.dll','msv1_0.dll','schannel.dll','dpapisrv.dll','basecsp.dll','scksp.dll','bcrypt.dll','bcryptprimitives.dll','ncrypt.dll','ncryptprov.dll','cryptsp.dll','rsaenh.dll','Cryptdll.dll'
    )
    Write-Host '[+] Collecting DLL version information' -ForegroundColor $ProgressColor
    foreach ($dll in $dlls) {
        $dllPath = Join-Path $env:SystemRoot "System32\$dll"
        if (Test-Path $dllPath) {
            (Get-Item $dllPath).VersionInfo | Select-Object FileName,FileVersion | Out-File -Append "$LogFolder\build.txt"
        }
    }

    # More info
    Write-Host '[+] Collecting additional system information (tasklist, msinfo32, gpresult)' -ForegroundColor $ProgressColor
    tasklist /svc | Out-File "$LogFolder\stop-tasklist.txt"
    msinfo32 /nfo "$LogFolder\msinfo32.nfo"
    gpresult /H "$LogFolder\GPReport_User.html"
    gpresult /Scope COMPUTER /H "$LogFolder\GPComputer.html"

    # Registry: AADConnectProvisioningAgent
    Write-Host '[+] Collecting AADConnectProvisioningAgent registry information' -ForegroundColor $ProgressColor
    reg query "HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\AADConnectProvisioningAgent" /s > "$LogFolder\$env:COMPUTERNAME-AADConnectProvisioningAgentServiceReg.txt"

    Write-Host '[+] Waiting for all required files to be ready for compression...' -ForegroundColor $ProgressColor
    $waitTimeout = 180 # seconds
    $waitInterval = 10 # seconds
    $startTime = Get-Date
    $notReady = @()
    do {
        $notReady = @()
        foreach ($file in $RequiredFiles) {
            $fullPath = Join-Path $LogFolder $file
            if (-not (Test-FileReady $fullPath)) {
                $notReady += $file
            }
        }
        if ($notReady.Count -gt 0) {
            Write-Host ("[+] Waiting for files: {0}" -f ($notReady -join ', ')) -ForegroundColor Yellow
            Start-Sleep -Seconds $waitInterval
        }
    } while ($notReady.Count -gt 0 -and ((Get-Date) - $startTime).TotalSeconds -lt $waitTimeout)
    while ($notReady.Count -gt 0) {
        Write-Host ("[!] Warning: Some files are still not ready after waiting: {0}" -f ($notReady -join ', ')) -ForegroundColor Red
        Write-Host "Some files are not ready. Type 'W' to wait another $waitTimeout seconds, or 'C' to continue and compress what is available (you may need to manually collect missing files later)"
        $choice = Read-Host "Enter your choice (W/C)"
        $choice = $choice.Trim().ToUpper()
        if ($choice -eq 'C') {
            Write-Host "[!] Proceeding to compress available files. You may need to manually collect and send missing files to support." -ForegroundColor Yellow
            break
        }
        # Default to waiting if 'W' or nothing/invalid entered
        Write-Host "[+] Waiting another $waitTimeout seconds for files to be ready..." -ForegroundColor Yellow
        $startTime = Get-Date
        do {
            $notReady = @()
            foreach ($file in $RequiredFiles) {
                $fullPath = Join-Path $LogFolder $file
                if (-not (Test-FileReady $fullPath)) {
                    $notReady += $file
                }
            }
            if ($notReady.Count -gt 0) {
                Write-Host ("[+] Waiting for files: {0}" -f ($notReady -join ', ')) -ForegroundColor Yellow
                Start-Sleep -Seconds $waitInterval
            }
        } while ($notReady.Count -gt 0 -and ((Get-Date) - $startTime).TotalSeconds -lt $waitTimeout)
    }

    Write-Host '[+] Compressing logs to ZIP archive...' -ForegroundColor $ProgressColor
    $compressSuccess = $false
    $compressAttempts = 0
    do {
        try {
            if (Test-Path $ZipFile) { Remove-Item $ZipFile -Force }
            Compress-Archive -Path $LogFolder -DestinationPath $ZipFile -Force
            $compressSuccess = $true
        } catch {
            $compressAttempts++
            $errMsg = $_.Exception.Message
            if ($errMsg -like '*because it is being used by another process*') {
                Write-Host ("[+] Waiting for files to be released by other processes before compressing (attempt $compressAttempts)...") -ForegroundColor Yellow
                Start-Sleep -Seconds 2
            } else {
                throw $_
            }
        }
    } while (-not $compressSuccess -and $compressAttempts -lt 10)
    if (-not $compressSuccess) {
        Write-Host '[!] Failed to compress logs after multiple attempts.' -ForegroundColor Red
    } else {
        Write-Host "[+] Log collection complete. Please provide the file: $ZipFile to support." -ForegroundColor Yellow
    }
    # Ask for to manually Compress and collect Azure AD Connect Provisioning Agent folder
    $aadProvFolder = "C:\ProgramData\Microsoft\Azure AD Connect Provisioning Agent"
    write-host ''
    Write-Host "[*] Please manually compress and share the Azure AD Connect Provisioning Agent folder: $aadProvFolder" -ForegroundColor Yellow

}

Export-ModuleMember -Function Start-ProvAgentLogCollection