When I needed to extract a report of corrupt or unresolved Windows update packages, I didn’t want to rely on Microsoft’s TSS tool. I wanted a clear, controllable way to parse the cbs.log and surface exactly what had failed. The TSS tool does this by running servicing scans and bundling the filtered results into a single report file — I’ve replicated that logic using PowerShell alone.
Servicing Commands I Run Before the Script
The CBS log doesn’t contain corruption data by default — it logs it when the system is scanned. That means I need to populate it by running the following:
sfc /scannow
DISM /Online /Cleanup-Image /ScanHealth
The STC scan checks and logs integrity violations of system files.
The ScanHealth switch in DISM tells Windows to scan the component store (WinSxS) for corruption. It writes detailed package failures and corruption entries to the cbs.log without making changes to the system — which is what I want for reporting purposes.
If I’m working on a non-critical system, like a dev box or test VM, I sometimes go a step further and run:
DISM /Online /Cleanup-Image /StartComponentCleanup /ResetBase
This cleans up superseded updates and resets the base servicing layer. However, this should never be run on production machines. It removes the ability to uninstall individual updates and may lead to unrecoverable errors during future updates or servicing stack changes. I only use this to prep custom images or shrink containers.
How the PowerShell Script Works
After I’ve generated the data with the servicing commands above, I use a PowerShell script to replicate the part of the TSS tool that gathers corrupt package data into a single report.
Here’s how I structured and built the script for the curious.
Define CBS Log Location
The CBS log always lives at:
$logFile = "C:\Windows\Logs\CBS\CBS.log"
This is the primary system log for component-based servicing and is updated any time I run sfc
or DISM
.
Check That the File Exists
Before I proceed, I check whether the file is actually present:
if (!(Test-Path $logFile)) {
Write-Host "CBS.log not found at $logFile"
exit
}
If the file doesn’t exist, there’s no data to parse, so the script stops cleanly.
Pattern Matching With Select-String
To find the corrupt packages, I search for two specific phrases:
- Marking te package as corrupt
- Failed to resolve package
These are the same lines that TSS filters out for its reports. I use Select-String
to get just the log lines that contain these phrases:
$matches = Select-String -Path $logFile -Pattern "Marking package as
corrupt|Failed to resolve package"
Extract Timestamp, KB ID, and Error Type
For each matched line, I extract:
- Timestamp — from the first 23 characters of the log line
- Type — either
"Marked Corrupt"
or"Failed to Resolve"
- KB Number — using regex from the format
Package_for_KB<digits>
, e.g.,KB5005565
- Raw Log Line — the full original line for traceability
This logic is written inside a foreach
block that processes each log entry and returns a structured object:
[PSCustomObject]@{
Timestamp = $timestamp
Type = $type
KB = $kb
RawLine = $logText
}
If no KB number is found, I assign unknown so that the report still includes the entry for manual inspection.
Output Format
I wanted results in two formats:
- A .csv file I can open in Excel for sorting and filtering
- A .html file for browser viewing or sharing with teams
This is handled using Export-Csv
and ConvertTo-Html
. Both files are written to my desktop:
$csvPath = "Corrupt_Packages_Report.csv"
$htmlPath = "Corrupt_Packages_Report.html"
Moving to - The Full Script
Here’s the complete version of the script, ready to run after sfc
and DISM /ScanHealth
have been executed:
# Define path to CBS log
$logFile = "C:\Windows\Logs\CBS\CBS.log"
# Validate file existence
if (!(Test-Path $logFile)) {
Write-Host "CBS.log not found at $logFile"
exit
}
# Look for corruption indicators
$matches = Select-String -Path $logFile -Pattern "Marking package as
corrupt|Failed to resolve package"
# Parse and extract useful data
$report = foreach ($line in $matches) {
$logText = $line.Line.Trim()
$timestamp = $logText.Substring(0,23)
$type = if ($logText -like "*Marking package*") { "Marked Corrupt" }
else { "Failed to Resolve" }
if ($logText -match "Package_for_KB(\d{7})") {
$kb = $matches[1]
} else {
$kb = "Unknown"
}
[PSCustomObject]@{
Timestamp = $timestamp
Type = $type
KB = $kb
RawLine = $logText
}
}
# Output paths
$csvPath = "$env:USERPROFILE\Desktop\Corrupt_Packages_Report.csv"
$htmlPath = "$env:USERPROFILE\Desktop\Corrupt_Packages_Report.html"
# Save to CSV
$report | Export-Csv -Path $csvPath -NoTypeInformation -Encoding UTF8
# Save to HTML
$report | ConvertTo-Html -Title "Component Corruption Report" -PreContent
"<h2>Corrupt Rollup Packages Detected in CBS.log</h2>" |
Out-File -FilePath $htmlPath -Encoding UTF8
Write-Host "Report generated:"
Write-Host "CSV: $csvPath"
Write-Host "HTML: $htmlPath"
This gives me a clean, portable, and auditable report of which Windows update packages are corrupt or unresolved — without needing to hunt manually through 100MB+ log files or run full diagnostic bundles.