Generating a Corrupt Rollup Package Report Without Microsoft TSS

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:

  1. Timestamp — from the first 23 characters of the log line
  2. Type — either "Marked Corrupt" or "Failed to Resolve"
  3. KB Number — using regex from the format Package_for_KB<digits>, e.g., KB5005565
  4. 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:

  1. A .csv file I can open in Excel for sorting and filtering
  2. 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.

Previous Post Next Post

نموذج الاتصال