Core/Test-SystemIntegrity.ps1
|
function Test-SystemIntegrity { <# .SYNOPSIS Tests Windows system file integrity .DESCRIPTION Runs SFC and DISM commands to check and verify system file integrity .PARAMETER RepairFiles Attempt to repair corrupted files if found .PARAMETER DetailedScan Perform a more thorough system scan .EXAMPLE Test-SystemIntegrity Test-SystemIntegrity -RepairFiles -DetailedScan .OUTPUTS PSCustomObject with integrity check results #> [CmdletBinding()] param( [Parameter()] [switch]$RepairFiles, [Parameter()] [switch]$DetailedScan ) begin { # Check for admin privileges $isAdmin = ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) if (-not $isAdmin) { throw "This function requires Administrator privileges. Please run PowerShell as Administrator." } Write-Host "Starting System Integrity Check..." -ForegroundColor Cyan $results = [PSCustomObject]@{ ScanDate = Get-Date -Format "yyyy-MM-dd HH:mm:ss" ComputerName = $env:COMPUTERNAME SFCResult = $null DISMResult = $null CorruptedFiles = @() RepairAttempted = $false RepairSuccessful = $false WindowsUpdateServices = @() # Added this property } } process { try { # Run SFC scan Write-Host "Running System File Checker (SFC)..." -ForegroundColor Yellow $scanStart = Get-Date $sfcOutput = & sfc /scannow 2>&1 | Out-String $sfcExitCode = $LASTEXITCODE # SFC output text is fully localized, so it is NEVER used for the # verdict; the exit code and the CBS.log '[SR]'/'Cannot repair' # tokens (written in English by the SFC engine regardless of UI # language), filtered to the current scan window, are the only # locale-invariant sources $cbsLogPath = "$env:windir\Logs\CBS\CBS.log" $cbsLogAvailable = Test-Path $cbsLogPath $cannotRepairLines = @() $cbsTimestampsParseable = $false if ($cbsLogAvailable) { $cbsTail = @(Get-Content $cbsLogPath -Tail 500 -ErrorAction SilentlyContinue) $cbsTailFilter = Select-CbsTailForCurrentScan -Lines $cbsTail -Since $scanStart $cbsTimestampsParseable = $cbsTailFilter.TimestampsParseable $cannotRepairLines = @($cbsTailFilter.Lines | Where-Object { $_ -match '\[SR\]' -and $_ -match 'Cannot repair' }) } # Localized SFC console text as a verbose hint only - never a verdict input if ($sfcOutput -match 'Windows Resource Protection') { $sfcHint = @($sfcOutput -split '\r?\n' | Where-Object { $_ -match 'Windows Resource Protection' } | Select-Object -First 1) if ($sfcHint.Count -gt 0) { Write-Verbose "SFC output hint (localized, informational only): $($sfcHint[0].Trim())" } } if ($sfcExitCode -ne 0) { # Covers the 'could not perform the requested operation' case - # SFC exits non-zero there, no text matching needed $results.SFCResult = "SFC could not complete scan - may need to run in Safe Mode" Write-Warning "SFC could not complete the scan (exit code $sfcExitCode)" } elseif ($cannotRepairLines.Count -gt 0) { $results.SFCResult = "Corrupt files found" if (-not $cbsTimestampsParseable) { $results.SFCResult = "Corrupt files found (possible stale CBS.log entries)" } $results.CorruptedFiles = $cannotRepairLines if ($RepairFiles) { Write-Host "Attempting to repair corrupt files..." -ForegroundColor Yellow $results.RepairAttempted = $true } } elseif ($cbsLogAvailable -and $cbsTimestampsParseable) { # Exit code 0 and the current-scan CBS.log window was actually # examined (timestamps parsed) with no 'Cannot repair' markers $results.SFCResult = "No integrity violations found" Write-Host "SFC: No integrity violations found" -ForegroundColor Green } else { # Inconclusive: CBS.log missing or its timestamps unparseable - # never report 'healthy' from a parse miss $results.SFCResult = "Scan completed - check CBS.log for details" } # Run DISM if detailed scan requested if ($DetailedScan) { Write-Host "Running DISM health check..." -ForegroundColor Yellow # Check image health $dismCheck = & dism /Online /Cleanup-Image /CheckHealth 2>&1 | Out-String $dismCheckExitCode = $LASTEXITCODE # DISM output text is fully localized and CheckHealth returns 0 for # both 'healthy' and 'repairable', so neither the output text nor # the exit code alone can separate those; servicing-stack CBS.log # markers (component-store corruption, written in English by the # engine), filtered to the current scan window, are the only # locale-invariant source. English DISM text is never matched. $storeCorruptionLines = @() $dismTimestampsParseable = $false if ($cbsLogAvailable) { $dismTail = @(Get-Content $cbsLogPath -Tail 500 -ErrorAction SilentlyContinue) $dismTailFilter = Select-CbsTailForCurrentScan -Lines $dismTail -Since $scanStart $dismTimestampsParseable = $dismTailFilter.TimestampsParseable $storeCorruptionLines = @($dismTailFilter.Lines | Where-Object { $_ -match '(?i)component store' -and $_ -match '(?i)corrupt' }) } # Localized DISM console text as a verbose hint only - never a verdict input if ($dismCheck -match '(?i)component store') { $dismHint = @($dismCheck -split '\r?\n' | Where-Object { $_ -match '(?i)component store' } | Select-Object -First 1) if ($dismHint.Count -gt 0) { Write-Verbose "DISM output hint (localized, informational only): $($dismHint[0].Trim())" } } $repairable = ($storeCorruptionLines.Count -gt 0) if ($dismCheckExitCode -ne 0) { $results.DISMResult = "DISM CheckHealth failed (exit code $dismCheckExitCode) - review CBS.log" Write-Warning "DISM CheckHealth failed with exit code $dismCheckExitCode" } elseif ($repairable) { $results.DISMResult = "Repairable corruption detected" if (-not $dismTimestampsParseable -and $storeCorruptionLines.Count -gt 0) { $results.DISMResult = "Repairable corruption detected (possible stale CBS.log entries)" } if ($RepairFiles) { Write-Host "Attempting DISM repair..." -ForegroundColor Yellow $null = & dism /Online /Cleanup-Image /RestoreHealth 2>&1 | Out-String $dismRepairExitCode = $LASTEXITCODE if ($dismRepairExitCode -eq 0) { $results.RepairSuccessful = $true $results.DISMResult = "Repaired successfully" Write-Host "DISM repair completed successfully" -ForegroundColor Green } else { $results.DISMResult = "Repair failed (exit code $dismRepairExitCode) - review CBS.log" Write-Warning "DISM RestoreHealth failed with exit code $dismRepairExitCode" } } } elseif ($cbsLogAvailable -and $dismTimestampsParseable) { # Exit code 0 and the current-scan CBS.log window was actually # examined (timestamps parsed) with no corruption markers $results.DISMResult = "Healthy" Write-Host "DISM: System image is healthy" -ForegroundColor Green } else { # Inconclusive: CBS.log missing or its timestamps unparseable - # never report 'healthy' from a parse miss $results.DISMResult = "Check completed - review CBS.log" } } # Check Windows Update components Write-Host "Checking Windows Update components..." -ForegroundColor Yellow $serviceNames = @('wuauserv', 'cryptSvc', 'bits', 'msiserver') $services = @() foreach ($serviceName in $serviceNames) { $service = Get-Service -Name $serviceName -ErrorAction SilentlyContinue if ($service) { $services += [PSCustomObject]@{ Name = $service.Name DisplayName = $service.DisplayName Status = $service.Status StartType = $service.StartType } } } $results.WindowsUpdateServices = $services } catch { Write-Error "Error during system integrity check: $_" throw } } end { return $results } } function Select-CbsTailForCurrentScan { [CmdletBinding()] param( [Parameter()] [AllowEmptyCollection()] [string[]]$Lines = @(), [Parameter(Mandatory = $true)] [datetime]$Since ) $windowStart = $Since.AddMinutes(-5) $currentScanLines = @() $timestampsParseable = $false foreach ($line in $Lines) { if ($null -eq $line) { continue } if ($line -match '^(?<ts>\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})') { try { $lineTime = [datetime]::ParseExact($Matches.ts, 'yyyy-MM-dd HH:mm:ss', [System.Globalization.CultureInfo]::InvariantCulture) $timestampsParseable = $true if ($lineTime -ge $windowStart) { $currentScanLines += $line } } catch { } } } $filteredLines = @($Lines) if ($timestampsParseable) { $filteredLines = @($currentScanLines) } [PSCustomObject]@{ Lines = $filteredLines TimestampsParseable = $timestampsParseable } } |