modules/BackupDR/BackupDR.psm1
|
<#
BackupDR.psm1 - backup/DR solution detection and VSS writer status (read-only). Produces: BackupDiscovery, VssWriters. Never runs or modifies backups. #> function Get-DiscoveryModuleMetadata { [pscustomobject]@{ ModuleName='BackupDR'; DisplayName='Backup & DR'; Category='Backup'; Version='1.0.0' DefaultInFast=$true; DefaultInDeep=$true; RequiresAdmin=$false; RequiresDomainContext=$false RequiresRole=$null; EstimatedImpact='Low'; CanRunAsSystem=$true ProducesDatasets=@('BackupDiscovery','VssWriters') ProducesRisks=$true; ProducesFollowUpQuestions=$true; SupportsDeepMode=$true; SupportsComplianceLens=$true } } function Test-DiscoveryPrerequisites { param([object]$Context) [pscustomobject]@{ ModuleName='BackupDR'; CanRun=$true; Status='Ready'; Reason=''; Limitations=@() } } function Invoke-DiscoveryCollection { param([object]$Context) $backup=[System.Collections.Generic.List[object]]::new(); $vss=[System.Collections.Generic.List[object]]::new() $patterns = @( @{ Product='Veeam'; Pattern='(?i)veeam' }, @{ Product='Datto'; Pattern='(?i)datto' }, @{ Product='Acronis'; Pattern='(?i)acronis' }, @{ Product='StorageCraft/ShadowProtect'; Pattern='(?i)storagecraft|shadowprotect' }, @{ Product='Azure Backup (MARS)'; Pattern='(?i)Microsoft Azure Recovery Services|OBEngine' }, @{ Product='Cove/N-able'; Pattern='(?i)\bcove\b|N-able|SolarWinds Backup' }, @{ Product='Carbonite'; Pattern='(?i)carbonite' }, @{ Product='Windows Server Backup'; Pattern='(?i)Windows Server Backup|wbengine' }, @{ Product='Commvault'; Pattern='(?i)commvault' }, @{ Product='Unitrends'; Pattern='(?i)unitrends' }, @{ Product='Barracuda'; Pattern='(?i)barracuda' }, @{ Product='Rubrik'; Pattern='(?i)rubrik' } ) try { $svcRows = @(if ($Context.DataSets.Contains('Services')) { @($Context.DataSets['Services'].Rows) } else { @() }) $appRows = @(if ($Context.DataSets.Contains('InstalledApplications')) { @($Context.DataSets['InstalledApplications'].Rows) } else { @() }) foreach ($p in $patterns) { $ev=@() $sh = @($svcRows | Where-Object { $_.DisplayName -match $p.Pattern -or $_.Name -match $p.Pattern }) $ah = @($appRows | Where-Object { $_.DisplayName -match $p.Pattern }) if ($sh.Count) { $ev += ('service:' + $sh[0].Name) } if ($ah.Count) { $ev += ('app:' + $ah[0].DisplayName) } if ($ev.Count) { $backup.Add([pscustomobject]@{ Product=$p.Product; Evidence=($ev -join '; '); Confidence='Likely' }) } } } catch { Add-Limitation -Context $Context -Module 'BackupDR' -Message 'Backup product detection failed.' -Reason $_.Exception.Message | Out-Null } if ($backup.Count -gt 0) { Add-Unknown -Context $Context -Unknown 'Backup software detected but recent success/restore not confirmed.' -WhyItMatters 'A backup agent does not prove backups are current or restorable.' -Module 'BackupDR' -RecommendedValidationQuestion 'When was the last successful backup and last tested restore?' | Out-Null } # ---- VSS writers (read-only) ---- try { $r = Invoke-CommandLineSafe -FilePath 'vssadmin.exe' -Arguments @('list','writers') -TimeoutSeconds 45 if ($r.Succeeded -and $r.StdOut) { $name=''; $state=''; $lastErr='' foreach ($ln in ($r.StdOut -split "`r?`n")) { if ($ln -match "Writer name:\s*'([^']+)'") { if ($name) { $vss.Add([pscustomobject]@{ Writer=$name; State=$state; LastError=$lastErr }) }; $name=$Matches[1]; $state=''; $lastErr='' } elseif ($ln -match 'State:\s*(.+)$') { $state=$Matches[1].Trim() } elseif ($ln -match 'Last error:\s*(.+)$') { $lastErr=$Matches[1].Trim() } } if ($name) { $vss.Add([pscustomobject]@{ Writer=$name; State=$state; LastError=$lastErr }) } } else { Add-Limitation -Context $Context -Module 'BackupDR' -Message 'vssadmin list writers unavailable (may require elevation).' | Out-Null } } catch { Add-Limitation -Context $Context -Module 'BackupDR' -Message 'VSS writer query failed.' -Reason $_.Exception.Message | Out-Null } return ,@{ BackupDiscovery=@($backup); VssWriters=@($vss) } } function ConvertTo-DiscoveryDatasets { param([object]$Context, $RawData) if (-not $RawData) { $RawData = @{} } $get = { param($k) if ($RawData[$k]) { @($RawData[$k]) } else { @() } } Add-DataSet -Context $Context -Name 'BackupDiscovery' -Description 'Detected backup/DR solutions.' -Rows (& $get 'BackupDiscovery') -Visibility 'Both' -SourceModule 'BackupDR' | Out-Null Add-DataSet -Context $Context -Name 'VssWriters' -Description 'VSS writer status.' -Rows (& $get 'VssWriters') -Visibility 'Internal' -SourceModule 'BackupDR' | Out-Null } function Get-DiscoveryFollowUpQuestions { param([object]$Context) Add-FollowUpQuestion -Context $Context -Category 'Backup / restore' -Module 'BackupDR' -Audience 'Both' -Question 'How is this server backed up, where are the backups stored, and when was a restore last successfully tested?' | Out-Null } Export-ModuleMember -Function 'Get-DiscoveryModuleMetadata','Test-DiscoveryPrerequisites','Invoke-DiscoveryCollection','ConvertTo-DiscoveryDatasets','Get-DiscoveryFollowUpQuestions' |