23 views +0 -0

Cove System State backup error

Cove: System State backup error: There is no data available for the backup. Please check your settings and environment

https://me.n-able.com/s/article/Cove-System-State-backup-error-There-is-no-data-available-for-the-backup-Please-check-your-settings-and-environment

.\Invoke-CoveBackupDiag.ps1 # basic run .\Invoke-CoveBackupDiag.ps1 -CollectSupportBundle # also makes a zip for N-able support .\Invoke-CoveBackupDiag.ps1 -SkipEventLogs # skip slow event log scan .\Invoke-CoveBackupDiag.ps1 -LookbackDays 30 # widen event/log window

<#
.SYNOPSIS
  Cove (N-able) System State backup diagnostic script.

.DESCRIPTION
  Comprehensive read-only diagnostic for the Cove Data Protection error:
    "There is no data available for the backup. Please check your settings
     and environment."

  Covers all 14 root causes documented in N-able KB
  https://me.n-able.com/s/article/Cove-System-State-backup-error-There-is-no-data-available-for-the-backup-Please-check-your-settings-and-environment
  plus its sub-articles.

  Checks performed:
    1.  Core VSS services (VSS, swprv, CryptSvc, BackupFP) state & start type
    2.  VSS writers state (vssadmin list writers) with writer->service mapping
    3.  VSS shadow storage configuration (vssadmin list shadowstorage/shadows)
    4.  Disk free space, especially System Reserved / EFI / Recovery
    5.  Disk initialization / offline / RAW / Unknown partition style
    6.  Volume filesystem types (flags ReFS/FAT/exFAT/removable)
    7.  BitLocker conversion state (manage-bde -status)
    8.  GPT partition labels for non-ASCII / XML-unsafe chars
    9.  Service ImagePath sanity (UNC, mapped drives, missing exe)
   10.  ProfileList orphan SIDs / .bak keys / missing ProfileImagePath
   11.  Registry ACL on HKLM\SYSTEM\CurrentControlSet\Services\VSS\Diag
   12.  SentinelOne presence + VSS-writer interference
   13.  Competing backup agents (Veeam, Macrium, Acronis, AOMEI, WSB)
   14.  Recent VSS / volsnap / BackupFP event log errors
   15.  Cove BackupFP log scan for known error signatures

  Output:
    - Console summary with ERROR / WARN / INFO severity.
    - Transcript log: <OutputFolder>\CoveDiag-<host>-<timestamp>.log
    - Findings JSON: <OutputFolder>\CoveDiag-<host>-<timestamp>.json
    - Optional support bundle (zip) for N-able with -CollectSupportBundle

.PARAMETER OutputFolder
  Folder for logs/findings/support bundle. Default %TEMP%\CoveDiag.

.PARAMETER LookbackDays
  Days of Event log history to scan. Default 7.

.PARAMETER CollectSupportBundle
  Also gather VSS metadata, Application/System event logs, and Backup Manager
  logs into a single zip for upload to N-able support.

.PARAMETER SkipEventLogs
  Skip the (slow) event log scan.

.EXAMPLE
  .\Invoke-CoveBackupDiag.ps1

.EXAMPLE
  .\Invoke-CoveBackupDiag.ps1 -CollectSupportBundle -OutputFolder C:\Temp\CoveDiag

.NOTES
  Run as Administrator. Tested on Windows Server 2016+/Windows 10+
  with Windows PowerShell 5.1. Read-only — makes no changes to the system.
#>
[CmdletBinding()]
param(
    [string]$OutputFolder = (Join-Path $env:TEMP 'CoveDiag'),
    [int]$LookbackDays = 7,
    [switch]$CollectSupportBundle,
    [switch]$SkipEventLogs
)

$ErrorActionPreference = 'Continue'
$ProgressPreference    = 'SilentlyContinue'

# -----------------------------------------------------------------------------
# Bootstrap
# -----------------------------------------------------------------------------

if (-not (Test-Path $OutputFolder)) {
    New-Item -ItemType Directory -Path $OutputFolder -Force | Out-Null
}

$stamp        = (Get-Date).ToString('yyyyMMdd-HHmmss')
$hostName     = $env:COMPUTERNAME
$transcript   = Join-Path $OutputFolder ("CoveDiag-{0}-{1}.log" -f $hostName, $stamp)
$findingsPath = Join-Path $OutputFolder ("CoveDiag-{0}-{1}.json" -f $hostName, $stamp)
$bundlePath   = Join-Path $OutputFolder ("CoveDiag-{0}-{1}-bundle.zip" -f $hostName, $stamp)

Start-Transcript -Path $transcript -Force | Out-Null

# Admin check
$principal = New-Object Security.Principal.WindowsPrincipal([Security.Principal.WindowsIdentity]::GetCurrent())
$isAdmin   = $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
if (-not $isAdmin) {
    Write-Host "WARNING: Not running as Administrator. Several checks (registry ACL, VSS writers, event logs) will be incomplete." -ForegroundColor Yellow
}

# Findings collector
$script:Findings = New-Object System.Collections.Generic.List[object]

function Add-Finding {
    param(
        [ValidateSet('ERROR','WARN','INFO','OK')]
        [string]$Severity,
        [string]$Category,
        [string]$Title,
        [string]$Detail,
        [string]$Remediation,
        [string]$KbRef
    )
    $obj = [pscustomobject]@{
        Severity    = $Severity
        Category    = $Category
        Title       = $Title
        Detail      = $Detail
        Remediation = $Remediation
        KbRef       = $KbRef
        Timestamp   = (Get-Date).ToString('s')
    }
    $script:Findings.Add($obj) | Out-Null

    $color = switch ($Severity) {
        'ERROR' { 'Red' }
        'WARN'  { 'Yellow' }
        'OK'    { 'Green' }
        default { 'Gray' }
    }
    Write-Host ("  [{0}] {1}: {2}" -f $Severity, $Category, $Title) -ForegroundColor $color
    if ($Detail)      { Write-Host ("        {0}" -f $Detail)      -ForegroundColor DarkGray }
    if ($Remediation) { Write-Host ("        Fix: {0}" -f $Remediation) -ForegroundColor DarkCyan }
}

function Write-Section {
    param([string]$Title)
    Write-Host ""
    Write-Host ("=" * 78) -ForegroundColor Cyan
    Write-Host $Title -ForegroundColor Cyan
    Write-Host ("=" * 78) -ForegroundColor Cyan
}

# VSS writer -> Windows service short name (from N-able KB)
$script:WriterServiceMap = @{
    'ASR Writer'                        = 'VSS'
    'BITS Writer'                       = 'BITS'
    'COM+ REGDB Writer'                 = 'VSS'
    'DFS Replication service writer'    = 'DFSR'
    'DHCP Jet Writer'                   = 'DHCPServer'
    'FRS Writer'                        = 'NtFrs'
    'FSRM writer'                       = 'srmsvc'
    'IIS Config Writer'                 = 'AppHostSvc'
    'IIS Metabase Writer'               = 'IISADMIN'
    'Microsoft Exchange Writer'         = 'MSExchangeIS'
    'Microsoft Hyper-V VSS Writer'      = 'vmms'
    'NTDS'                              = 'NTDS'
    'Registry Writer'                   = 'VSS'
    'Shadow Copy Optimization Writer'   = 'VSS'
    'SqlServerWriter'                   = 'SQLWriter'
    'System Writer'                     = 'CryptSvc'
    'TermServLicensing'                 = 'TermServLicensing'
    'WINS Jet Writer'                   = 'WINS'
    'WMI Writer'                        = 'Winmgmt'
}

Write-Host ""
Write-Host "========================================================================" -ForegroundColor Cyan
Write-Host "  Cove System State Backup Diagnostic" -ForegroundColor Cyan
Write-Host "  Host:        $hostName" -ForegroundColor Cyan
Write-Host "  Timestamp:   $stamp" -ForegroundColor Cyan
Write-Host "  Output:      $OutputFolder" -ForegroundColor Cyan
Write-Host "  Admin:       $isAdmin" -ForegroundColor Cyan
Write-Host "========================================================================" -ForegroundColor Cyan

# -----------------------------------------------------------------------------
# Check 1: Core VSS services
# -----------------------------------------------------------------------------
Write-Section "Check 1/15 — Core VSS service state"

$coreSvcs = 'VSS','swprv','CryptSvc','BackupFP'
foreach ($svcName in $coreSvcs) {
    $svc = Get-Service -Name $svcName -ErrorAction SilentlyContinue
    if (-not $svc) {
        if ($svcName -eq 'BackupFP') {
            Add-Finding -Severity 'WARN' -Category 'Services' `
                -Title "Backup Manager service (BackupFP) not installed" `
                -Detail "Cove Backup Manager service not detected. If this server is supposed to run Cove backups, the agent may be missing or corrupt." `
                -Remediation "Reinstall the Cove Backup Manager."
        } else {
            Add-Finding -Severity 'ERROR' -Category 'Services' `
                -Title "Service '$svcName' not found" `
                -Detail "Critical service is missing on this system." `
                -Remediation "VSS subsystem broken. Run 'sfc /scannow' and 'DISM /Online /Cleanup-Image /RestoreHealth'."
        }
        continue
    }

    $startType = (Get-CimInstance Win32_Service -Filter "Name='$svcName'").StartMode
    $status    = $svc.Status

    if ($svcName -eq 'VSS') {
        if ($startType -eq 'Disabled') {
            Add-Finding -Severity 'ERROR' -Category 'Services' `
                -Title "VSS service is Disabled" `
                -Detail "Status: $status, StartMode: $startType" `
                -Remediation "Run: sc config VSS start= demand" `
                -KbRef "VSS-snapshot-not-found-VSS-service-is-stopped"
        } else {
            Add-Finding -Severity 'OK' -Category 'Services' `
                -Title "VSS service OK ($status / $startType)"
        }
    } elseif ($svcName -eq 'CryptSvc') {
        # CryptSvc backs the System Writer
        if ($status -ne 'Running') {
            Add-Finding -Severity 'ERROR' -Category 'Services' `
                -Title "CryptSvc (System Writer backing service) not running" `
                -Detail "Status: $status, StartMode: $startType. The System Writer used by System State backup depends on CryptSvc." `
                -Remediation "Restart-Service CryptSvc" `
                -KbRef "VSS-error-0x800423f4-RegOpenKeyExW"
        } else {
            Add-Finding -Severity 'OK' -Category 'Services' `
                -Title "CryptSvc OK ($status / $startType)"
        }
    } else {
        Add-Finding -Severity 'OK' -Category 'Services' `
            -Title "$svcName ($status / $startType)"
    }
}

# -----------------------------------------------------------------------------
# Check 2: VSS Writers
# -----------------------------------------------------------------------------
Write-Section "Check 2/15 — VSS Writers state (vssadmin list writers)"

try {
    $writersRaw = vssadmin list writers 2>&1 | Out-String
    if ($LASTEXITCODE -ne 0 -or $writersRaw -match 'Error') {
        Add-Finding -Severity 'ERROR' -Category 'VSSWriters' `
            -Title "vssadmin list writers failed" `
            -Detail ($writersRaw -split "`n" | Select-Object -First 5) -join " | " `
            -Remediation "Run 'net stop vss; net start vss', then retry."
    } else {
        # Parse: "Writer name: 'X'" / "State: [n] Y" / "Last error: Z"
        $blocks = [regex]::Split($writersRaw, "(?ms)^Writer name: ")
        $badWriters = 0
        foreach ($block in $blocks) {
            if ($block -match "^'([^']+)'.*?State:\s*\[\d+\]\s*(\S+).*?Last error:\s*(.+?)\r?\n" -or
                $block -match "^'([^']+)'") {
                $wName = $Matches[1]
                $wState = if ($Matches.Count -gt 2) { $Matches[2] } else { '' }
                $wErr   = if ($Matches.Count -gt 3) { $Matches[3].Trim() } else { '' }

                if ($wErr -and $wErr -ne 'No error') {
                    $badWriters++
                    $svcHint = $script:WriterServiceMap[$wName]
                    $fix = if ($svcHint) {
                        "Restart-Service $svcHint  (then re-run vssadmin list writers)"
                    } else {
                        "Identify the service hosting this writer and restart it; see KB Cove-Windows-VSS-Writers-and-Corresponding-Service-Names"
                    }
                    Add-Finding -Severity 'ERROR' -Category 'VSSWriters' `
                        -Title "Writer in failed state: $wName" `
                        -Detail "State: $wState | Last error: $wErr" `
                        -Remediation $fix `
                        -KbRef "Cove-Windows-VSS-Writers-and-Corresponding-Service-Names"
                }
            }
        }
        if ($badWriters -eq 0) {
            Add-Finding -Severity 'OK' -Category 'VSSWriters' `
                -Title "All VSS writers report 'No error'"
        }
    }
} catch {
    Add-Finding -Severity 'WARN' -Category 'VSSWriters' `
        -Title "Could not enumerate VSS writers" -Detail $_.Exception.Message
}

# -----------------------------------------------------------------------------
# Check 3: Shadow Storage
# -----------------------------------------------------------------------------
Write-Section "Check 3/15 — Shadow Storage configuration"

try {
    $storageRaw = vssadmin list shadowstorage 2>&1 | Out-String
    if ($storageRaw -match 'No shadow') {
        Add-Finding -Severity 'WARN' -Category 'ShadowStorage' `
            -Title "No shadow storage associations exist yet" `
            -Detail "Windows will use default 10% on first VSS snapshot. On systems with very small System Reserved, this may fail." `
            -KbRef "0x8004231f-Insufficient-storage"
    } else {
        # Parse "Used Shadow Copy Storage space: X (Y%)" / "Allocated" / "Maximum"
        $shadowPattern = '(?ms)For volume:\s*\(([^)]+)\).*?Used Shadow Copy Storage space:\s*(\S+\s+\S+)\s*\(([\d.]+)%\).*?Allocated Shadow Copy Storage space:\s*(\S+\s+\S+).*?Maximum Shadow Copy Storage space:\s*(\S+\s*\S*)'
        $entries = [regex]::Matches($storageRaw, $shadowPattern)
        foreach ($m in $entries) {
            $vol  = $m.Groups[1].Value
            $used = $m.Groups[2].Value
            $pct  = [double]$m.Groups[3].Value
            $alloc = $m.Groups[4].Value
            $max   = $m.Groups[5].Value
            if ($pct -ge 90) {
                Add-Finding -Severity 'ERROR' -Category 'ShadowStorage' `
                    -Title "Shadow storage almost full on $vol (${pct}%)" `
                    -Detail "Used $used / Allocated $alloc / Max $max" `
                    -Remediation "vssadmin resize shadowstorage /for=$vol /on=$vol /maxsize=20%   (or delete old: vssadmin delete shadows /for=$vol /oldest)" `
                    -KbRef "0x8004231f-Insufficient-storage"
            } elseif ($pct -ge 75) {
                Add-Finding -Severity 'WARN' -Category 'ShadowStorage' `
                    -Title "Shadow storage usage high on $vol (${pct}%)" `
                    -Detail "Used $used / Allocated $alloc / Max $max"
            } else {
                Add-Finding -Severity 'OK' -Category 'ShadowStorage' `
                    -Title "Shadow storage on $vol healthy (${pct}% used)"
            }
        }
    }

    # Existing shadows (just informational)
    $shadowsRaw = vssadmin list shadows 2>&1 | Out-String
    $shadowCount = ([regex]::Matches($shadowsRaw, 'Shadow Copy ID:')).Count
    Add-Finding -Severity 'INFO' -Category 'ShadowStorage' `
        -Title "Existing shadow copies on system: $shadowCount"
} catch {
    Add-Finding -Severity 'WARN' -Category 'ShadowStorage' `
        -Title "Could not query shadow storage" -Detail $_.Exception.Message
}

# -----------------------------------------------------------------------------
# Check 4: Disk free space (especially System Reserved)
# -----------------------------------------------------------------------------
Write-Section "Check 4/15 — Disk free space"

try {
    $vols = Get-Volume -ErrorAction SilentlyContinue | Where-Object { $_.DriveType -eq 'Fixed' }
    foreach ($v in $vols) {
        if ($null -eq $v.Size -or $v.Size -eq 0) { continue }
        $freeGB  = [math]::Round($v.SizeRemaining / 1GB, 2)
        $totalGB = [math]::Round($v.Size / 1GB, 2)
        $freePct = [math]::Round(($v.SizeRemaining / $v.Size) * 100, 1)
        $label   = if ($v.DriveLetter) { "$($v.DriveLetter):" } elseif ($v.FileSystemLabel) { $v.FileSystemLabel } else { '<unnamed>' }

        # System Reserved / Recovery partitions are often tiny — need ~50MB+ free
        $isSystem = ($label -match 'System Reserved|Recovery|EFI' -or
                     ($v.Size -lt 1GB -and $totalGB -lt 1))
        if ($isSystem) {
            if ($v.SizeRemaining -lt 50MB) {
                Add-Finding -Severity 'ERROR' -Category 'DiskSpace' `
                    -Title "System partition '$label' has < 50MB free" `
                    -Detail "Free: ${freeGB}GB / Total: ${totalGB}GB (${freePct}%)" `
                    -Remediation "Free space on this partition or redirect shadow storage off it: vssadmin add shadowstorage /for=$label /on=C: /maxsize=2GB" `
                    -KbRef "BitLocker-0x8004231f"
            } elseif ($freePct -lt 15) {
                Add-Finding -Severity 'WARN' -Category 'DiskSpace' `
                    -Title "System partition '$label' has only ${freePct}% free" `
                    -Detail "Free: ${freeGB}GB / Total: ${totalGB}GB"
            } else {
                Add-Finding -Severity 'OK' -Category 'DiskSpace' `
                    -Title "System partition '$label' free: ${freeGB}GB (${freePct}%)"
            }
        } else {
            if ($freePct -lt 10) {
                Add-Finding -Severity 'ERROR' -Category 'DiskSpace' `
                    -Title "Volume '$label' nearly full (${freePct}% free)" `
                    -Detail "Free: ${freeGB}GB / Total: ${totalGB}GB" `
                    -Remediation "Free at least 15% on '$label' before next backup." `
                    -KbRef "0x8004231f-Insufficient-storage"
            } elseif ($freePct -lt 15) {
                Add-Finding -Severity 'WARN' -Category 'DiskSpace' `
                    -Title "Volume '$label' low on space (${freePct}% free)" `
                    -Detail "Free: ${freeGB}GB / Total: ${totalGB}GB"
            } else {
                Add-Finding -Severity 'OK' -Category 'DiskSpace' `
                    -Title "Volume '$label' free: ${freeGB}GB (${freePct}%)"
            }
        }
    }
} catch {
    Add-Finding -Severity 'WARN' -Category 'DiskSpace' `
        -Title "Could not enumerate volumes" -Detail $_.Exception.Message
}

# -----------------------------------------------------------------------------
# Check 5: Disk initialization
# -----------------------------------------------------------------------------
Write-Section "Check 5/15 — Disk initialization / offline / RAW"

try {
    $disks = Get-Disk -ErrorAction SilentlyContinue
    foreach ($d in $disks) {
        $line = "Disk $($d.Number): '$($d.FriendlyName)' Status=$($d.OperationalStatus) Style=$($d.PartitionStyle) Offline=$($d.IsOffline)"
        if ($d.OperationalStatus -ne 'Online' -or
            $d.PartitionStyle -in @('RAW','Unknown') -or
            $d.IsOffline -eq $true) {
            Add-Finding -Severity 'ERROR' -Category 'DiskInit' `
                -Title "Disk $($d.Number) not ready for VSS" `
                -Detail $line `
                -Remediation "Either initialize it (Initialize-Disk -Number $($d.Number) -PartitionStyle GPT) or detach it before running System State backup." `
                -KbRef "Uninitialized-Disks-or-Unallocated-Partitions"
        } else {
            Add-Finding -Severity 'OK' -Category 'DiskInit' -Title $line
        }
    }
} catch {
    Add-Finding -Severity 'WARN' -Category 'DiskInit' `
        -Title "Could not enumerate disks" -Detail $_.Exception.Message
}

# -----------------------------------------------------------------------------
# Check 6: Volume filesystem types
# -----------------------------------------------------------------------------
Write-Section "Check 6/15 — Volume filesystem support"

try {
    $vols = Get-Volume -ErrorAction SilentlyContinue | Where-Object { $_.DriveType -eq 'Fixed' -and $_.Size -gt 0 }
    foreach ($v in $vols) {
        $label = if ($v.DriveLetter) { "$($v.DriveLetter):" } elseif ($v.FileSystemLabel) { $v.FileSystemLabel } else { '<unnamed>' }
        switch -Regex ($v.FileSystemType) {
            'NTFS' { Add-Finding -Severity 'OK' -Category 'Filesystem' -Title "$label NTFS (snapshot-capable)" }
            'ReFS' {
                Add-Finding -Severity 'WARN' -Category 'Filesystem' `
                    -Title "$label is ReFS" `
                    -Detail "ReFS volumes have limited VSS support; some application writers will not snapshot." `
                    -Remediation "If this volume contains data needed by System State backup, consider relocating to NTFS." `
                    -KbRef "0x8004230c-Making-shadow-copies-not-supported"
            }
            'FAT|exFAT' {
                Add-Finding -Severity 'ERROR' -Category 'Filesystem' `
                    -Title "$label is $($v.FileSystemType) — VSS not supported" `
                    -Detail "Shadow copies are only supported on NTFS." `
                    -Remediation "Move data off this volume or exclude it from System State backup." `
                    -KbRef "0x8004230c-Making-shadow-copies-not-supported"
            }
            default {
                Add-Finding -Severity 'WARN' -Category 'Filesystem' `
                    -Title "$label has unusual filesystem '$($v.FileSystemType)'"
            }
        }
    }
} catch {
    Add-Finding -Severity 'WARN' -Category 'Filesystem' `
        -Title "Could not check filesystem types" -Detail $_.Exception.Message
}

# -----------------------------------------------------------------------------
# Check 7: BitLocker state
# -----------------------------------------------------------------------------
Write-Section "Check 7/15 — BitLocker conversion state"

$bdeAvailable = Get-Command manage-bde -ErrorAction SilentlyContinue
if (-not $bdeAvailable) {
    Add-Finding -Severity 'INFO' -Category 'BitLocker' -Title "manage-bde not available — BitLocker check skipped"
} else {
    try {
        $bdeRaw = manage-bde -status 2>&1 | Out-String
        # Look for any drive that is still encrypting/decrypting
        if ($bdeRaw -match 'Encryption in Progress|Decryption in Progress') {
            Add-Finding -Severity 'ERROR' -Category 'BitLocker' `
                -Title "BitLocker is mid-conversion" `
                -Detail "An encrypt/decrypt is still in progress. VSS snapshots during conversion frequently fail with 0x8004231f." `
                -Remediation "Wait until conversion completes, then retry: manage-bde -status" `
                -KbRef "BitLocker-0x8004231f"
        } elseif ($bdeRaw -match 'Protection Status:\s*Protection On') {
            $encrypted = ([regex]::Matches($bdeRaw, 'Protection Status:\s*Protection On')).Count
            Add-Finding -Severity 'INFO' -Category 'BitLocker' `
                -Title "BitLocker enabled on $encrypted volume(s)" `
                -Detail "Verify System Reserved/Recovery partitions have headroom; small ones cause 0x8004231f." `
                -KbRef "BitLocker-0x8004231f"
        } else {
            Add-Finding -Severity 'OK' -Category 'BitLocker' -Title "No active BitLocker protection detected"
        }
    } catch {
        Add-Finding -Severity 'WARN' -Category 'BitLocker' `
            -Title "Could not query BitLocker state" -Detail $_.Exception.Message
    }
}

# -----------------------------------------------------------------------------
# Check 8: GPT partition labels (XML safety)
# -----------------------------------------------------------------------------
Write-Section "Check 8/15 — Partition labels XML-safe"

try {
    $parts = Get-CimInstance -Namespace root/Microsoft/Windows/Storage -ClassName MSFT_Partition -ErrorAction SilentlyContinue
    if ($parts) {
        foreach ($p in $parts) {
            $name = $p.Name
            if (-not $name) { continue }
            # Flag non-printable or non-ASCII characters that can break XML parsing
            if ($name -match '[^\x20-\x7E]') {
                $bytes = [System.Text.Encoding]::Unicode.GetBytes($name)
                $hex   = ($bytes | ForEach-Object { '{0:X2}' -f $_ }) -join ' '
                Add-Finding -Severity 'ERROR' -Category 'PartitionLabel' `
                    -Title "Partition Disk $($p.DiskNumber)/$($p.PartitionNumber) label has non-ASCII chars" `
                    -Detail "Name='$name'  (UTF-16 bytes: $hex)" `
                    -Remediation "Rename the partition to ASCII-only using GParted live ISO. Common offender: Recovery (WinRE) partition." `
                    -KbRef "Fail-to-parse-XML-file"
            }
        }
        Add-Finding -Severity 'OK' -Category 'PartitionLabel' `
            -Title "$($parts.Count) partition labels scanned for XML-unsafe chars"
    } else {
        Add-Finding -Severity 'INFO' -Category 'PartitionLabel' `
            -Title "Could not enumerate MSFT_Partition (older OS?)"
    }
} catch {
    Add-Finding -Severity 'WARN' -Category 'PartitionLabel' `
        -Title "Partition label check failed" -Detail $_.Exception.Message
}

# -----------------------------------------------------------------------------
# Check 9: Service ImagePath sanity (UNC, mapped, missing files)
# -----------------------------------------------------------------------------
Write-Section "Check 9/15 — Service ImagePath UNC/missing-file scan"

try {
    $services = Get-CimInstance Win32_Service -ErrorAction SilentlyContinue
    $bad = 0
    foreach ($s in $services) {
        if (-not $s.PathName) { continue }

        # Strip leading quotes and arguments to isolate the executable
        $raw = $s.PathName.Trim()
        $exe = $null
        if ($raw -match '^"([^"]+)"') {
            $exe = $Matches[1]
        } elseif ($raw -match '^(\S+)') {
            $exe = $Matches[1]
        }
        if (-not $exe) { continue }

        # UNC path?
        if ($exe -match '^\\\\') {
            $bad++
            Add-Finding -Severity 'ERROR' -Category 'ServiceImagePath' `
                -Title "Service '$($s.Name)' has UNC ImagePath" `
                -Detail "PathName: $($s.PathName)" `
                -Remediation "System State backup rejects UNC service paths. Edit HKLM\SYSTEM\CurrentControlSet\Services\$($s.Name)\ImagePath to a local path, or 'sc delete $($s.Name)' if dead." `
                -KbRef "System-State-unable-to-perform-backup-from-a-network-share"
            continue
        }
        # Mapped drive letter (non-system letter)?
        if ($exe -match '^([D-Z]):\\') {
            $drv = $Matches[1] + ':'
            $vol = Get-Volume -DriveLetter $Matches[1] -ErrorAction SilentlyContinue
            if (-not $vol -or $vol.DriveType -in 'Network','Removable') {
                $bad++
                Add-Finding -Severity 'WARN' -Category 'ServiceImagePath' `
                    -Title "Service '$($s.Name)' ImagePath on $drv (non-local or absent drive)" `
                    -Detail "PathName: $($s.PathName)" `
                    -Remediation "Confirm $drv is a local fixed drive present at boot." `
                    -KbRef "System-State-unable-to-perform-backup-from-a-network-share"
            }
        }

        # Skip kernel/driver pseudo-paths
        if ($exe -match '^(\\SystemRoot\\|System32\\DRIVERS\\|\\\?\?\\)') { continue }

        # Missing file?
        # Resolve environment variables
        $resolved = [Environment]::ExpandEnvironmentVariables($exe)
        if (-not (Test-Path -LiteralPath $resolved -ErrorAction SilentlyContinue)) {
            # Try without "system32" prefix interpretation
            $bad++
            Add-Finding -Severity 'ERROR' -Category 'ServiceImagePath' `
                -Title "Service '$($s.Name)' ImagePath points to missing file" `
                -Detail "PathName: $($s.PathName)  (resolved: $resolved)" `
                -Remediation "Either restore the file or 'sc delete $($s.Name)'. Export the registry key first." `
                -KbRef "Failed-to-recognize-volume-name-in-path"
        }
    }
    if ($bad -eq 0) {
        Add-Finding -Severity 'OK' -Category 'ServiceImagePath' `
            -Title "All $($services.Count) service ImagePaths look local + present"
    }
} catch {
    Add-Finding -Severity 'WARN' -Category 'ServiceImagePath' `
        -Title "Service ImagePath scan failed" -Detail $_.Exception.Message
}

# -----------------------------------------------------------------------------
# Check 10: ProfileList (orphan SIDs, .bak keys)
# -----------------------------------------------------------------------------
Write-Section "Check 10/15 — ProfileList for orphan SIDs / .bak keys"

try {
    $profilePath = 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList'
    $profiles = Get-ChildItem $profilePath -ErrorAction Stop
    $bad = 0
    foreach ($p in $profiles) {
        $sid  = $p.PSChildName
        $pip  = (Get-ItemProperty -Path $p.PSPath -ErrorAction SilentlyContinue).ProfileImagePath

        # .bak suffix indicates an orphan from profile rebuild
        if ($sid -match '\.bak$') {
            $bad++
            Add-Finding -Severity 'ERROR' -Category 'ProfileList' `
                -Title "Orphan .bak profile key: $sid" `
                -Detail "ProfileImagePath: $pip" `
                -Remediation "Backup then delete: reg export 'HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList' C:\Temp\ProfileList.reg ; reg delete 'HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList\$sid' /f" `
                -KbRef "0x800423f4-ConvertStringSidToSid"
            continue
        }

        # ProfileImagePath missing or non-existent (skip S-1-5-18/19/20 — system accounts)
        if ($sid -match '^S-1-5-(18|19|20)$') { continue }
        if ($pip -and -not (Test-Path -LiteralPath $pip -ErrorAction SilentlyContinue)) {
            $bad++
            Add-Finding -Severity 'WARN' -Category 'ProfileList' `
                -Title "Profile SID $sid points to missing folder" `
                -Detail "ProfileImagePath: $pip" `
                -Remediation "If this user is decommissioned, remove the SID entry (export the key first)." `
                -KbRef "0x800423f4-ConvertStringSidToSid"
            continue
        }

        # SID resolvability
        try {
            $obj = New-Object System.Security.Principal.SecurityIdentifier($sid)
            $obj.Translate([System.Security.Principal.NTAccount]) | Out-Null
        } catch {
            $bad++
            Add-Finding -Severity 'WARN' -Category 'ProfileList' `
                -Title "SID $sid in ProfileList does not resolve" `
                -Detail "ProfileImagePath: $pip" `
                -Remediation "Likely a deleted domain user. Clean up if the profile folder is also gone." `
                -KbRef "0x800423f4-ConvertStringSidToSid"
        }
    }
    if ($bad -eq 0) {
        Add-Finding -Severity 'OK' -Category 'ProfileList' `
            -Title "ProfileList clean ($($profiles.Count) entries)"
    }
} catch {
    Add-Finding -Severity 'WARN' -Category 'ProfileList' `
        -Title "ProfileList enumeration failed" -Detail $_.Exception.Message
}

# -----------------------------------------------------------------------------
# Check 11: ACL on HKLM\SYSTEM\CurrentControlSet\Services\VSS\Diag
# -----------------------------------------------------------------------------
Write-Section "Check 11/15 — Registry ACL on VSS\Diag"

try {
    $vssDiag = 'HKLM:\SYSTEM\CurrentControlSet\Services\VSS\Diag'
    if (Test-Path $vssDiag) {
        $acl  = Get-Acl -Path $vssDiag -ErrorAction Stop
        $hasSystemFull = $false
        $hasAdminFull  = $false
        foreach ($ace in $acl.Access) {
            if ($ace.IdentityReference.Value -match 'NT AUTHORITY\\SYSTEM' -and
                $ace.RegistryRights -match 'FullControl' -and
                $ace.AccessControlType -eq 'Allow') { $hasSystemFull = $true }
            if ($ace.IdentityReference.Value -match 'BUILTIN\\Administrators' -and
                $ace.RegistryRights -match 'FullControl' -and
                $ace.AccessControlType -eq 'Allow') { $hasAdminFull = $true }
        }
        if (-not $hasSystemFull) {
            Add-Finding -Severity 'ERROR' -Category 'RegistryACL' `
                -Title "SYSTEM lacks Full Control on HKLM\SYSTEM\CurrentControlSet\Services\VSS\Diag" `
                -Detail "VSS coordinator runs as SYSTEM and must write diagnostic entries here." `
                -Remediation "In regedit: right-click the Diag key -> Permissions -> SYSTEM -> Full Control. Or use SubInACL/Set-Acl." `
                -KbRef "0x800423f4-RegOpenKeyExW"
        } elseif (-not $hasAdminFull) {
            Add-Finding -Severity 'WARN' -Category 'RegistryACL' `
                -Title "Administrators lacks Full Control on VSS\Diag" `
                -Remediation "Grant BUILTIN\Administrators Full Control on the Diag key."
        } else {
            Add-Finding -Severity 'OK' -Category 'RegistryACL' `
                -Title "VSS\Diag ACL OK (SYSTEM + Administrators have Full Control)"
        }
    } else {
        Add-Finding -Severity 'WARN' -Category 'RegistryACL' `
            -Title "VSS\Diag registry key missing" `
            -Detail "Expected at HKLM\SYSTEM\CurrentControlSet\Services\VSS\Diag" `
            -Remediation "Recreate by restarting VSS service or VSS service repair."
    }
} catch {
    Add-Finding -Severity 'WARN' -Category 'RegistryACL' `
        -Title "ACL check failed" -Detail $_.Exception.Message
}

# -----------------------------------------------------------------------------
# Check 12: SentinelOne presence
# -----------------------------------------------------------------------------
Write-Section "Check 12/15 — SentinelOne presence and VSS-writer interference"

$s1Found = $false
$s1Hits  = @()

$s1Services = Get-Service -Name 'Sentinel*' -ErrorAction SilentlyContinue
if ($s1Services) { $s1Found = $true; $s1Hits += "Services: $($s1Services.Name -join ', ')" }

$s1Path = 'C:\Program Files\SentinelOne'
if (Test-Path $s1Path) {
    $s1Found = $true
    $agentDirs = Get-ChildItem $s1Path -Directory -ErrorAction SilentlyContinue | Where-Object { $_.Name -match 'Sentinel Agent' }
    if ($agentDirs) { $s1Hits += "Agent dir: $($agentDirs[0].FullName)" }
}

if (Test-Path 'HKLM:\SOFTWARE\Sentinel Labs') { $s1Found = $true; $s1Hits += "Registry: HKLM\SOFTWARE\Sentinel Labs" }

if ($s1Found) {
    $s1Fix = 'Disable S1 VSS writers (requires per-endpoint passphrase from S1 console): "C:\Program Files\SentinelOne\Sentinel Agent <ver>\SentinelCtl.exe" config -p agent.vssConfig.agentVssWriters -v false -k "<passphrase>" ; same with agent.vssConfig.enableResearchDataCollectorVssWriter ; reboot. Also exclude BackupFP.exe and %ProgramData%\MXB\Backup Manager in S1 policy.'
    Add-Finding -Severity 'WARN' -Category 'SentinelOne' `
        -Title "SentinelOne is installed" `
        -Detail ($s1Hits -join ' | ') `
        -Remediation $s1Fix `
        -KbRef "Sentinel-One-AV-is-installed"
} else {
    Add-Finding -Severity 'OK' -Category 'SentinelOne' -Title "SentinelOne not detected"
}

# -----------------------------------------------------------------------------
# Check 13: Competing backup agents
# -----------------------------------------------------------------------------
Write-Section "Check 13/15 — Competing backup agents"

$competitorRegex = 'veeam|macrium|acronis|aomei|reflect|wbengine|backupexec|arcserve|carbonite'
$competitors = Get-Service -ErrorAction SilentlyContinue |
    Where-Object { $_.Name -match $competitorRegex -or $_.DisplayName -match $competitorRegex }

if ($competitors) {
    foreach ($c in $competitors) {
        Add-Finding -Severity 'WARN' -Category 'CompetingBackup' `
            -Title "Competing backup service detected: $($c.DisplayName) ($($c.Name))" `
            -Detail "Status: $($c.Status). Other VSS-using backup agents can race or delete shadows mid-job." `
            -Remediation "Ensure schedules don't overlap with Cove. Stop the service during Cove backup window if a conflict is confirmed." `
            -KbRef "VSS-snapshot-not-found-VSS-service-is-stopped"
    }
} else {
    Add-Finding -Severity 'OK' -Category 'CompetingBackup' -Title "No competing backup agents detected"
}

# -----------------------------------------------------------------------------
# Check 14: Event log scan
# -----------------------------------------------------------------------------
Write-Section "Check 14/15 — Recent Event log VSS / volsnap / BackupFP errors"

if ($SkipEventLogs) {
    Add-Finding -Severity 'INFO' -Category 'EventLog' -Title "Event log scan skipped (-SkipEventLogs)"
} else {
    $startTime = (Get-Date).AddDays(-$LookbackDays)

    # VSS errors (App log)
    try {
        $vssEvents = Get-WinEvent -FilterHashtable @{
            LogName='Application'; ProviderName='VSS'; Level=1,2,3; StartTime=$startTime
        } -ErrorAction SilentlyContinue -MaxEvents 100
        if ($vssEvents) {
            $byId = $vssEvents | Group-Object Id | Sort-Object Count -Descending
            foreach ($g in $byId) {
                $first = $g.Group | Select-Object -First 1
                $msgSnip = ($first.Message -split "`r?`n" | Where-Object { $_.Trim() } | Select-Object -First 2) -join ' / '
                $kbHint = switch ($g.Name) {
                    '8193'  {
                        if ($first.Message -match 'ConvertStringSidToSid') { '0x800423f4-ConvertStringSidToSid' }
                        elseif ($first.Message -match 'RegOpenKeyExW')      { '0x800423f4-RegOpenKeyExW' }
                        else                                                { 'check-windows-app-system-logs' }
                    }
                    '8194'  { 'check-windows-app-system-logs' }
                    '8228'  { 'Fail-to-parse-XML-file' }
                    '12289' { 'check-windows-app-system-logs' }
                    '12302' { 'check-windows-app-system-logs' }
                    default { '' }
                }
                Add-Finding -Severity 'ERROR' -Category 'EventLog' `
                    -Title "VSS Event ID $($g.Name) seen $($g.Count) time(s) in last $LookbackDays day(s)" `
                    -Detail $msgSnip `
                    -KbRef $kbHint
            }
        } else {
            Add-Finding -Severity 'OK' -Category 'EventLog' `
                -Title "No VSS errors in last $LookbackDays day(s)"
        }
    } catch {
        Add-Finding -Severity 'INFO' -Category 'EventLog' `
            -Title "Could not query Application/VSS events" -Detail $_.Exception.Message
    }

    # volsnap (System log)
    try {
        $volsnap = Get-WinEvent -FilterHashtable @{
            LogName='System'; ProviderName='volsnap'; Level=1,2,3; StartTime=$startTime
        } -ErrorAction SilentlyContinue -MaxEvents 50
        if ($volsnap) {
            $byId = $volsnap | Group-Object Id | Sort-Object Count -Descending
            foreach ($g in $byId) {
                $first = $g.Group | Select-Object -First 1
                $msgSnip = ($first.Message -split "`r?`n" | Where-Object { $_.Trim() } | Select-Object -First 2) -join ' / '
                $kbHint = switch ($g.Name) {
                    '25'    { '0x8004231f-Insufficient-storage' }
                    '27'    { '0x8004231f-Insufficient-storage' }
                    '33'    { '0x8004231f-Insufficient-storage' }
                    '36'    { '0x8004231f-Insufficient-storage' }
                    default { '' }
                }
                Add-Finding -Severity 'ERROR' -Category 'EventLog' `
                    -Title "volsnap Event ID $($g.Name) seen $($g.Count) time(s)" `
                    -Detail $msgSnip `
                    -KbRef $kbHint
            }
        } else {
            Add-Finding -Severity 'OK' -Category 'EventLog' `
                -Title "No volsnap errors in last $LookbackDays day(s)"
        }
    } catch {
        Add-Finding -Severity 'INFO' -Category 'EventLog' `
            -Title "Could not query System/volsnap events" -Detail $_.Exception.Message
    }

    # BackupFP crashes
    try {
        $appErr = Get-WinEvent -FilterHashtable @{
            LogName='Application'; ProviderName='Application Error'; StartTime=$startTime
        } -ErrorAction SilentlyContinue -MaxEvents 50 | Where-Object { $_.Message -match 'BackupFP' }
        if ($appErr) {
            Add-Finding -Severity 'WARN' -Category 'EventLog' `
                -Title "BackupFP.exe crashes detected ($($appErr.Count))" `
                -Detail ($appErr[0].Message -split "`r?`n" | Select-Object -First 1)
        }
    } catch {}
}

# -----------------------------------------------------------------------------
# Check 15: Cove BackupFP log scan
# -----------------------------------------------------------------------------
Write-Section "Check 15/15 — Cove Backup Manager log scan"

$mxbLogDir = 'C:\ProgramData\MXB\Backup Manager\logs'
$mxbLogDirExists = $false
try { $mxbLogDirExists = Test-Path -LiteralPath $mxbLogDir -ErrorAction Stop } catch {}
if (-not $mxbLogDirExists) {
    Add-Finding -Severity 'INFO' -Category 'CoveLogs' -Title "Cove logs not found at $mxbLogDir (or access denied)"
} else {
    try {
        $recent = Get-ChildItem $mxbLogDir -File -ErrorAction SilentlyContinue |
                  Where-Object { $_.LastWriteTime -gt (Get-Date).AddDays(-$LookbackDays) } |
                  Sort-Object LastWriteTime -Descending

        $signatures = @{
            'There is no data available for the backup'                = 'Main symptom'
            'Failed to recognize volume name in path'                  = 'Failed-to-recognize-volume-name-in-path'
            'Fail to parse XML file'                                   = 'Fail-to-parse-XML-file'
            '0x8004231f'                                               = '0x8004231f-Insufficient-storage'
            '0x800423f4'                                               = '0x800423f4-(see-RegOpenKeyExW-or-ConvertStringSidToSid)'
            '0x800423f0'                                               = '0x800423f0-subset-of-volumes'
            '0x8004230c'                                               = '0x8004230c-Making-shadow-copies-not-supported'
            'VSS snapshot not found'                                   = 'VSS-snapshot-not-found'
            'Volume Shadow Copy Service error'                         = 'check-windows-app-system-logs'
        }
        $hits = @()
        foreach ($f in $recent | Select-Object -First 20) {
            $content = Get-Content -LiteralPath $f.FullName -Tail 2000 -ErrorAction SilentlyContinue
            if (-not $content) { continue }
            $joined = $content -join "`n"
            foreach ($sig in $signatures.Keys) {
                if ($joined -match [regex]::Escape($sig)) {
                    $hits += [pscustomobject]@{
                        File = $f.Name
                        Signature = $sig
                        Kb = $signatures[$sig]
                    }
                }
            }
        }
        if ($hits) {
            $byKb = $hits | Group-Object Signature
            foreach ($g in $byKb) {
                $first = $g.Group | Select-Object -First 1
                Add-Finding -Severity 'ERROR' -Category 'CoveLogs' `
                    -Title "Cove log contains: '$($g.Name)'" `
                    -Detail ("Seen in: " + (($g.Group | Select-Object -ExpandProperty File -Unique) -join ', ')) `
                    -KbRef $first.Kb
            }
        } else {
            Add-Finding -Severity 'OK' -Category 'CoveLogs' `
                -Title "No known error signatures in recent Cove logs ($($recent.Count) files scanned)"
        }
    } catch {
        Add-Finding -Severity 'WARN' -Category 'CoveLogs' `
            -Title "Cove log scan failed" -Detail $_.Exception.Message
    }
}

# -----------------------------------------------------------------------------
# Summary
# -----------------------------------------------------------------------------
Write-Section "SUMMARY"

$errors = @($script:Findings | Where-Object Severity -eq 'ERROR')
$warns  = @($script:Findings | Where-Object Severity -eq 'WARN')
$oks    = @($script:Findings | Where-Object Severity -eq 'OK')

Write-Host ""
Write-Host ("  ERRORS:   {0}" -f $errors.Count) -ForegroundColor Red
Write-Host ("  WARNINGS: {0}" -f $warns.Count)  -ForegroundColor Yellow
Write-Host ("  OK:       {0}" -f $oks.Count)    -ForegroundColor Green
Write-Host ""

if ($errors.Count -gt 0) {
    Write-Host "TOP PRIORITY ACTIONS:" -ForegroundColor Red
    $errors | Select-Object -First 10 | ForEach-Object {
        Write-Host ("  - [{0}] {1}" -f $_.Category, $_.Title) -ForegroundColor Red
        if ($_.Remediation) {
            Write-Host ("       Fix: {0}" -f $_.Remediation) -ForegroundColor White
        }
        if ($_.KbRef) {
            Write-Host ("       KB:  {0}" -f $_.KbRef) -ForegroundColor DarkGray
        }
    }
    Write-Host ""
}

# Persist findings JSON
$script:Findings | ConvertTo-Json -Depth 5 | Out-File -FilePath $findingsPath -Encoding utf8
Write-Host ("Findings JSON: {0}" -f $findingsPath) -ForegroundColor Cyan
Write-Host ("Transcript:    {0}" -f $transcript)   -ForegroundColor Cyan

# -----------------------------------------------------------------------------
# Optional support bundle
# -----------------------------------------------------------------------------
if ($CollectSupportBundle) {
    Write-Section "Collecting support bundle for N-able"

    $bundleStage = Join-Path $OutputFolder ("bundle-" + $stamp)
    New-Item -ItemType Directory -Path $bundleStage -Force | Out-Null

    # 1. vssadmin outputs
    Write-Host "  - vssadmin list writers"
    vssadmin list writers 2>&1 | Out-File (Join-Path $bundleStage 'vssadmin-writers.txt') -Encoding utf8
    Write-Host "  - vssadmin list shadowstorage"
    vssadmin list shadowstorage 2>&1 | Out-File (Join-Path $bundleStage 'vssadmin-shadowstorage.txt') -Encoding utf8
    Write-Host "  - vssadmin list shadows"
    vssadmin list shadows 2>&1 | Out-File (Join-Path $bundleStage 'vssadmin-shadows.txt') -Encoding utf8
    Write-Host "  - vssadmin list providers"
    vssadmin list providers 2>&1 | Out-File (Join-Path $bundleStage 'vssadmin-providers.txt') -Encoding utf8

    # 2. diskshadow detailed writer metadata (read-only: no create/add commands)
    Write-Host "  - diskshadow list writers metadata"
    $dsScript = Join-Path $bundleStage 'diskshadow.in'
    "list writers metadata`r`nlist writers detailed`r`nexit" |
        Out-File -Encoding ascii $dsScript
    diskshadow /s $dsScript 2>&1 | Out-File (Join-Path $bundleStage 'diskshadow-writers.txt') -Encoding utf8

    # 3. Event log exports
    Write-Host "  - Event logs (Application, System)"
    $lookbackMs   = $LookbackDays * 86400000
    $evtAppPath   = Join-Path $bundleStage 'Application.evtx'
    $evtSysPath   = Join-Path $bundleStage 'System.evtx'
    $evtQuery     = '*[System[TimeCreated[timediff(@SystemTime) <= ' + $lookbackMs + ']]]'
    & wevtutil epl Application $evtAppPath "/q:$evtQuery" 2>&1 | Out-Null
    & wevtutil epl System      $evtSysPath "/q:$evtQuery" 2>&1 | Out-Null

    # 4. Disk / volume info
    Write-Host "  - disk / volume / bitlocker info"
    Get-Disk | Format-List * | Out-File (Join-Path $bundleStage 'disks.txt') -Encoding utf8
    Get-Volume | Format-List * | Out-File (Join-Path $bundleStage 'volumes.txt') -Encoding utf8
    Get-Partition | Format-List * | Out-File (Join-Path $bundleStage 'partitions.txt') -Encoding utf8
    if ($bdeAvailable) { manage-bde -status 2>&1 | Out-File (Join-Path $bundleStage 'bitlocker.txt') -Encoding utf8 }

    # 5. Services / WriterServiceMap
    Get-CimInstance Win32_Service | Select-Object Name, DisplayName, State, StartMode, PathName |
        Export-Csv -Path (Join-Path $bundleStage 'services.csv') -NoTypeInformation

    # 6. Cove logs (recent only — full dump can be huge)
    if (Test-Path $mxbLogDir) {
        Write-Host "  - Cove Backup Manager logs (last $LookbackDays day(s))"
        $logsTarget = Join-Path $bundleStage 'CoveLogs'
        New-Item -ItemType Directory -Path $logsTarget -Force | Out-Null
        Get-ChildItem $mxbLogDir -File -ErrorAction SilentlyContinue |
            Where-Object { $_.LastWriteTime -gt (Get-Date).AddDays(-$LookbackDays) } |
            Copy-Item -Destination $logsTarget -Force -ErrorAction SilentlyContinue
    }

    # 7. Findings + transcript
    Copy-Item $findingsPath $bundleStage -Force
    Copy-Item $transcript   $bundleStage -Force -ErrorAction SilentlyContinue

    # 8. Zip
    Write-Host "  - Compressing bundle..."
    if (Test-Path $bundlePath) { Remove-Item $bundlePath -Force }
    Add-Type -AssemblyName System.IO.Compression.FileSystem
    [System.IO.Compression.ZipFile]::CreateFromDirectory($bundleStage, $bundlePath)
    Remove-Item $bundleStage -Recurse -Force

    Write-Host ""
    Write-Host "  Support bundle: $bundlePath" -ForegroundColor Green
    Write-Host "  Upload this file to N-able support per:" -ForegroundColor Green
    Write-Host "    https://me.n-able.com/s/article/Cove-How-to-collect-VSS-Volume-Shadow-Copy-metadata" -ForegroundColor Green
}

Write-Host ""
Stop-Transcript | Out-Null

# Exit code: 2 if errors, 1 if warnings only, 0 if clean
if ($errors.Count -gt 0) { exit 2 }
elseif ($warns.Count -gt 0) { exit 1 }
else { exit 0 }