function Invoke-ServiceImagePathAudit { <# .SYNOPSIS Scans, analyzes, and optionally repairs Windows service ImagePath values for unquoted paths with spaces. .DESCRIPTION Single entry-point for the classic workflow: - Scan: Retrieve service ImagePath values from HKLM:\SYSTEM\CurrentControlSet\Services - Analyze: Identify unquoted service paths with spaces and generate a FixedKey suggestion - Repair: Apply FixedKey back to the registry for items marked BadKey="Yes" - ScanAnalyze: Scan then Analyze (default) - ScanFix: Scan then Analyze then Repair Supports -WhatIf and -Confirm for repairs. .PARAMETER Operation What to do: - Scan Output ComputerName/Key/ImagePath records - Analyze Add BadKey/FixedKey to incoming records - Repair Write FixedKey back for incoming records where BadKey="Yes" - ScanAnalyze Scan then Analyze (default) - ScanFix Scan then Analyze then Repair .PARAMETER ComputerName One or more computer names to scan (used by Scan/ScanAnalyze/ScanFix). Defaults to the local computer. .PARAMETER InputObject Pipeline input (used by Analyze/Repair). Expected properties: - Analyze: ComputerName, Key, ImagePath - Repair: ComputerName, Key, BadKey, FixedKey .PARAMETER ShowProgress Show progress bars during scanning/analyzing/repair. Default is enabled if you do not specify it. .EXAMPLE Invoke-ServiceImagePathAudit Scans and analyzes the local computer (default Operation is ScanAnalyze). .EXAMPLE Invoke-ServiceImagePathAudit -Operation Scan -ComputerName Server1,Server2 Scans Server1 and Server2 and returns raw ImagePath records. .EXAMPLE Invoke-ServiceImagePathAudit -ComputerName Server1 | Where-Object BadKey -eq 'Yes' Scans and analyzes Server1 and filters to vulnerable entries. .EXAMPLE Invoke-ServiceImagePathAudit -Operation ScanFix -ComputerName Server1 -WhatIf Shows what would be repaired on Server1 without making changes. .EXAMPLE Import-Csv .\scan.csv | Invoke-ServiceImagePathAudit -Operation Analyze Analyzes previously exported scan results. .EXAMPLE Import-Csv .\scan.csv | Invoke-ServiceImagePathAudit -Operation Repair -WhatIf Dry-run repairs from previously analyzed records. .NOTES Save as UTF-8 (no BOM). Remote scan/repair requires Remote Registry access and permissions to read/write HKLM on targets. #> [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'High', DefaultParameterSetName = 'ScanSet')] param( [Parameter(Position = 0)] [ValidateSet('Scan','Analyze','Repair','ScanAnalyze','ScanFix')] [string]$Operation = 'ScanAnalyze', [Parameter(ParameterSetName = 'ScanSet')] [Alias('Name','Computer','Server','__ServerName')] [string[]]$ComputerName = $env:COMPUTERNAME, [Parameter(ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true, ParameterSetName = 'PipelineSet')] $InputObject, [switch]$ShowProgress ) begin { # Default ShowProgress to ON if user didn't specify it if (-not $PSBoundParameters.ContainsKey('ShowProgress')) { $ShowProgress = $true } function _Scan { param([string[]]$Targets) foreach ($computer in $Targets) { $collection = @() if ($ShowProgress) { Write-Progress -Id 1 -Activity "Scanning services on $computer" -Status "Connecting..." Write-Progress -Id 2 -Activity "Parsing results" -Status "Waiting..." -PercentComplete 0 } $result = & reg.exe QUERY "\\$computer\HKLM\SYSTEM\CurrentControlSet\Services" /v ImagePath /s 2>&1 if (-not $result -or $result[0] -match 'ERROR|Denied') { if ($ShowProgress) { Write-Progress -Id 1 -Activity "Scanning services on $computer" -Status "Connection Failed" Write-Progress -Id 2 -Activity "Parsing results" -Completed Write-Progress -Id 1 -Activity "Scanning services on $computer" -Completed } $collection += [pscustomobject]@{ ComputerName = $computer Status = "REG Failed" Key = "Unavailable" ImagePath = "Unavailable" } $collection continue } if ($ShowProgress) { Write-Progress -Id 1 -Activity "Scanning services on $computer" -Status "Connected" } $lines = $result | Where-Object { $_ -and $_.Trim() -ne '' } $currentKey = $null $i = 0 foreach ($line in $lines) { $i++ if ($ShowProgress) { $pct = [math]::Min([math]::Round(($i / [math]::Max($lines.Count, 1)) * 100), 100) Write-Progress -Id 2 -Activity "Parsing results" -Status "Reading $computer" -PercentComplete $pct } if ($line -match '^HKEY_') { $currentKey = $line.Trim() continue } if ($line -match '^\s*ImagePath\s+REG_\w+\s+(?.+)$') { $collection += [pscustomobject]@{ ComputerName = $computer Status = "Retrieved" Key = $currentKey ImagePath = $Matches['val'].Trim() } } } if ($ShowProgress) { Write-Progress -Id 2 -Activity "Parsing results" -Completed Write-Progress -Id 1 -Activity "Scanning services on $computer" -Completed } $collection } } function _AnalyzeOne { param($Obj) $outObj = $Obj | Select-Object * $badpath = $false $examine = $outObj.ImagePath if ($ShowProgress) { Write-Progress -Activity "Analyzing ImagePath" -Status "Checking $($outObj.ComputerName)\$($outObj.Key)" } if ($outObj.Key -eq "Unavailable" -or $examine -eq "Unavailable" -or [string]::IsNullOrWhiteSpace($examine)) { $outObj | Add-Member NoteProperty BadKey "Unknown" -Force $outObj | Add-Member NoteProperty FixedKey "Can't Fix" -Force return $outObj } # Ignore already-quoted or special \?? prefixes if (-not $examine.StartsWith('"') -and -not $examine.StartsWith("\??")) { if ($examine.Contains(" ")) { # If we see flagged args, try to isolate a path portion if ($examine.Contains("-") -or $examine.Contains("/")) { $split = $examine -split " -", 0, "simplematch" $split = $split[0] -split " /", 0, "simplematch" $newpath = $split[0].Trim() if ($newpath.Contains(" ")) { $eval = $newpath -Replace '".*"', '' $detunflagged = $eval -split "\\", 0, "simplematch" if ($detunflagged[-1].Contains(" ")) { $fixarg = $detunflagged[-1] -split " ", 0, "simplematch" $quoteexe = $fixarg[0] + '"' $examine = $examine.Replace($fixarg[0], $quoteexe) $examine = '"' + $examine.Trim('"') + '"' $badpath = $true } $examine = $examine.Replace($newpath, '"' + $newpath + '"') $badpath = $true } } else { # No flagged args, either just a bad path or an unflagged argument scenario $eval = $examine -Replace '".*"', '' $detunflagged = $eval -split "\\", 0, "simplematch" if ($detunflagged[-1].Contains(" ")) { $fixarg = $detunflagged[-1] -split " ", 0, "simplematch" $quoteexe = $fixarg[0] + '"' $examine = $examine.Replace($fixarg[0], $quoteexe) $examine = '"' + $examine.Trim('"') + '"' $badpath = $true } else { $examine = '"' + $examine.Trim('"') + '"' $badpath = $true } } } } if (-not $badpath) { $outObj | Add-Member NoteProperty BadKey "No" -Force $outObj | Add-Member NoteProperty FixedKey "N/A" -Force return $outObj } while ($examine.EndsWith('""')) { $examine = $examine.Substring(0, $examine.Length - 1) } $outObj | Add-Member NoteProperty BadKey "Yes" -Force $outObj | Add-Member NoteProperty FixedKey $examine -Force return $outObj } function _RepairOne { param($Obj) $outObj = $Obj | Select-Object * if ($outObj.BadKey -ne 'Yes') { return $outObj } $target = "\\$($outObj.ComputerName)\$($outObj.Key)" $data = $outObj.FixedKey if ($ShowProgress) { Write-Progress -Activity "Repairing ImagePath" -Status "Fixing $($outObj.ComputerName)\$($outObj.Key)" } if ([string]::IsNullOrWhiteSpace($data) -or $data -eq 'N/A' -or $data -eq "Can't Fix") { $outObj.Status = "Skipped (no FixedKey)" return $outObj } if ($PSCmdlet.ShouldProcess($target, "Set ImagePath to: $data")) { try { $args = @( 'ADD', $target, '/v', 'ImagePath', '/t', 'REG_EXPAND_SZ', '/d', $data, '/f' ) $output = & reg.exe @args 2>&1 if ($LASTEXITCODE -eq 0) { $outObj.Status = "Fixed" } else { $msg = ($output | Out-String).Trim() if ([string]::IsNullOrWhiteSpace($msg)) { $msg = "reg.exe exit code $LASTEXITCODE" } $outObj.Status = "Failed: $msg" } } catch { $outObj.Status = "Failed: $($_.Exception.Message)" } } else { $outObj.Status = "WhatIf" } return $outObj } } process { switch ($Operation) { 'Scan' { if ($PSCmdlet.ParameterSetName -ne 'ScanSet') { throw "Operation 'Scan' requires -ComputerName (not pipeline input)." } _Scan -Targets $ComputerName } 'Analyze' { if ($PSCmdlet.ParameterSetName -ne 'PipelineSet') { throw "Operation 'Analyze' requires pipeline input (-InputObject)." } if ($null -ne $InputObject) { _AnalyzeOne -Obj $InputObject } } 'Repair' { if ($PSCmdlet.ParameterSetName -ne 'PipelineSet') { throw "Operation 'Repair' requires pipeline input (-InputObject)." } if ($null -ne $InputObject) { _RepairOne -Obj $InputObject } } 'ScanAnalyze' { if ($PSCmdlet.ParameterSetName -ne 'ScanSet') { throw "Operation 'ScanAnalyze' requires -ComputerName (not pipeline input)." } _Scan -Targets $ComputerName | ForEach-Object { _AnalyzeOne -Obj $_ } } 'ScanFix' { if ($PSCmdlet.ParameterSetName -ne 'ScanSet') { throw "Operation 'ScanFix' requires -ComputerName (not pipeline input)." } _Scan -Targets $ComputerName | ForEach-Object { _AnalyzeOne -Obj $_ } | ForEach-Object { _RepairOne -Obj $_ } } } } }