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 }