Public/vss/Get-ShadowCopy.ps1

#Requires -Version 5.1
function Get-ShadowCopy {
    <#
        .SYNOPSIS
            List existing Volume Shadow Copies on local or remote Windows computers
 
        .DESCRIPTION
            Retrieves all Volume Shadow Copy snapshots using Win32_ShadowCopy via CIM.
            Supports filtering by drive letter, remote execution via Invoke-RemoteOrLocal,
            and pipeline input for multiple computer names.
 
        .PARAMETER ComputerName
            One or more computer names to query. Defaults to the local machine.
 
        .PARAMETER DriveLetter
            Single drive letter (A-Z) to filter shadow copies for a specific volume.
 
        .PARAMETER Credential
            PSCredential object for remote authentication.
 
        .EXAMPLE
            Get-ShadowCopy
 
            Lists all shadow copies on the local computer.
 
        .EXAMPLE
            Get-ShadowCopy -ComputerName 'SRV01' -DriveLetter 'C'
 
            Lists shadow copies for the C: drive on remote server SRV01.
 
        .EXAMPLE
            'SRV01', 'SRV02' | Get-ShadowCopy -Credential (Get-Credential)
 
            Lists all shadow copies on SRV01 and SRV02 using alternate credentials.
 
        .OUTPUTS
            PSWinOps.ShadowCopy
            Returns objects with ComputerName, ShadowCopyId, DriveLetter, VolumeName,
            CreationTime, DeviceObject, ProviderName, State, and Timestamp properties.
 
        .NOTES
            Author: Franck SALLET
            Version: 1.0.0
            Last Modified: 2026-04-10
            Requires: PowerShell 5.1+ / Windows only
            Requires: Administrator privileges for remote queries
 
        .LINK
            https://github.com/k9fr4n/PSWinOps
 
        .LINK
            https://learn.microsoft.com/en-us/windows/win32/cimwin32prov/win32-shadowcopy
    #>

    [CmdletBinding()]
    [OutputType('PSWinOps.ShadowCopy')]
    param(
        [Parameter(Mandatory = $false, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
        [ValidateNotNullOrEmpty()]
        [Alias('CN', 'Name', 'DNSHostName')]
        [string[]]$ComputerName = $env:COMPUTERNAME,

        [Parameter(Mandatory = $false)]
        [ValidatePattern('^[A-Za-z]$')]
        [string]$DriveLetter,

        [Parameter(Mandatory = $false)]
        [ValidateNotNull()]
        [System.Management.Automation.PSCredential]
        [System.Management.Automation.Credential()]
        $Credential
    )

    begin {
        Write-Verbose -Message "[$($MyInvocation.MyCommand)] Starting"

        # VSS_SNAPSHOT_STATE enum — https://learn.microsoft.com/en-us/windows/win32/api/vss/ne-vss-vss_snapshot_state
        $stateMap = @{
            0  = 'Unknown'
            1  = 'Preparing'
            2  = 'ProcessingPrepare'
            3  = 'Prepared'
            4  = 'ProcessingPreCommit'
            5  = 'PreCommitted'
            6  = 'ProcessingCommit'
            7  = 'Committed'
            8  = 'ProcessingPostCommit'
            9  = 'ProcessingPreFinalCommit'
            10 = 'PreFinalCommitted'
            11 = 'ProcessingFinalCommit'
            12 = 'Created'
            13 = 'Aborted'
            14 = 'Deleted'
        }

        $driveLetterArg = if ($PSBoundParameters.ContainsKey('DriveLetter')) {
            $DriveLetter 
        } else {
            '' 
        }

        $scriptBlock = {
            param([string]$FilterDriveLetter)

            $volumeIndex = @{}
            foreach ($vol in (Get-CimInstance -ClassName Win32_Volume -ErrorAction SilentlyContinue)) {
                if ($vol.DeviceID) {
                    $normalizedId = $vol.DeviceID.TrimEnd('\').ToLower()
                    if ($vol.DriveLetter) {
                        $volumeIndex[$normalizedId] = $vol.DriveLetter.TrimEnd(':')
                    } elseif ($vol.Label) {
                        $shortLabel = if ($vol.Label.Length -gt 8) {
                            $vol.Label.Substring(0, 8) 
                        } else {
                            $vol.Label 
                        }
                        $volumeIndex[$normalizedId] = "[$shortLabel]"
                    } elseif ($vol.DeviceID -match '\{([^}]+)\}') {
                        $volumeIndex[$normalizedId] = $Matches[1].Substring(0, 8)
                    }
                }
            }

            $targetDeviceId = ''
            if ($FilterDriveLetter -ne '') {
                $filterExpression = "DriveLetter='$($FilterDriveLetter):'"
                $targetVolume = Get-CimInstance -ClassName Win32_Volume -Filter $filterExpression -ErrorAction SilentlyContinue
                if ($targetVolume) {
                    $targetDeviceId = $targetVolume.DeviceID.TrimEnd('\').ToLower()
                } else {
                    return
                }
            }

            $shadows = Get-CimInstance -ClassName Win32_ShadowCopy -ErrorAction Stop

            foreach ($shadow in $shadows) {
                $normalizedVolName = $shadow.VolumeName.TrimEnd('\').ToLower()

                if ($targetDeviceId -ne '' -and $normalizedVolName -ne $targetDeviceId) {
                    continue
                }
                $resolvedDrive = if ($volumeIndex.ContainsKey($normalizedVolName)) {
                    $volumeIndex[$normalizedVolName]
                } else {
                    '?'
                }

                [PSCustomObject]@{
                    ShadowCopyId = $shadow.ID
                    DriveLetter  = $resolvedDrive
                    VolumeName   = $shadow.VolumeName
                    CreationTime = $shadow.InstallDate
                    DeviceObject = $shadow.DeviceObject
                    ProviderName = $shadow.ProviderName
                    StateCode    = $shadow.State
                }
            }
        }
    }

    process {
        foreach ($machine in $ComputerName) {
            Write-Verbose -Message "[$($MyInvocation.MyCommand)] Querying shadow copies on '$machine'"

            try {
                $invokeParams = @{
                    ComputerName = $machine
                    ScriptBlock  = $scriptBlock
                    ArgumentList = @($driveLetterArg)
                }
                if ($PSBoundParameters.ContainsKey('Credential')) {
                    $invokeParams['Credential'] = $Credential
                }

                $raw = Invoke-RemoteOrLocal @invokeParams

                foreach ($item in $raw) {
                    $mappedState = if ($stateMap.ContainsKey([int]$item.StateCode)) {
                        $stateMap[[int]$item.StateCode]
                    } else {
                        'Unknown'
                    }

                    [PSCustomObject]@{
                        PSTypeName   = 'PSWinOps.ShadowCopy'
                        ComputerName = $machine
                        ShadowCopyId = $item.ShadowCopyId
                        DriveLetter  = $item.DriveLetter
                        VolumeName   = $item.VolumeName
                        CreationTime = $item.CreationTime
                        DeviceObject = $item.DeviceObject
                        ProviderName = $item.ProviderName
                        State        = $mappedState
                        Timestamp    = Get-Date -Format 'yyyy-MM-dd HH:mm:ss'
                    }
                }
            } catch {
                Write-Error -Message "[$($MyInvocation.MyCommand)] Failed on '${machine}': $_"
                continue
            }
        }
    }

    end {
        Write-Verbose -Message "[$($MyInvocation.MyCommand)] Completed"
    }
}