Private/Collectors.Devices.ps1

function ConvertTo-ICMountedDeviceValue {
    [CmdletBinding()]
    param(
        [AllowNull()]
        [object]$Data
    )

    if ($Data -isnot [byte[]] -or $Data.Length -eq 0) {
        return [pscustomobject]@{ Decoded = $null; HexPrefix = $null }
    }

    $bytes = [byte[]]$Data
    $decoded = $null
    if (($bytes.Length % 2) -eq 0) {
        try {
            $candidate = [System.Text.Encoding]::Unicode.GetString($bytes)
            if ($candidate -match '^[\x20-\x7E]+$') {
                $decoded = $candidate
            }
        }
        catch { $decoded = $null }
    }

    $prefixLength = [math]::Min($bytes.Length, 64)
    $hexPrefix = -join (@($bytes[0..($prefixLength - 1)]) | ForEach-Object { $_.ToString('x2') })
    return [pscustomobject]@{ Decoded = $decoded; HexPrefix = $hexPrefix }
}

function Get-ICDeviceEvidence {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [object]$Context
    )

    $warnings = New-Object System.Collections.ArrayList
    $files = New-Object System.Collections.ArrayList
    $maximumArtifactBytes = [int64](Get-ICPropertyValue -InputObject $Context.Configuration -Name 'MaximumArtifactFileBytes' -Default 33554432L)

    $usbDevices = New-Object System.Collections.ArrayList
    $usbRoot = 'HKLM:\SYSTEM\CurrentControlSet\Enum\USBSTOR'
    if (Test-Path -LiteralPath $usbRoot) {
        try {
            foreach ($deviceClassKey in Get-ChildItem -LiteralPath $usbRoot -ErrorAction Stop) {
                foreach ($instanceKey in Get-ChildItem -LiteralPath $deviceClassKey.PSPath -ErrorAction SilentlyContinue) {
                    try {
                        $properties = Get-ItemProperty -LiteralPath $instanceKey.PSPath -ErrorAction Stop
                        $hardwareId = @(Get-ICPropertyValue -InputObject $properties -Name 'HardwareID' -Default @())
                        [void]$usbDevices.Add([pscustomobject][ordered]@{
                            DeviceClass  = Split-Path -Leaf $deviceClassKey.Name
                            SerialNumber = Split-Path -Leaf $instanceKey.Name
                            FriendlyName = [string](Get-ICPropertyValue -InputObject $properties -Name 'FriendlyName')
                            DeviceDesc   = [string](Get-ICPropertyValue -InputObject $properties -Name 'DeviceDesc')
                            Manufacturer = [string](Get-ICPropertyValue -InputObject $properties -Name 'Mfg')
                            Service      = [string](Get-ICPropertyValue -InputObject $properties -Name 'Service')
                            ContainerID  = [string](Get-ICPropertyValue -InputObject $properties -Name 'ContainerID')
                            HardwareID   = (@($hardwareId) -join '; ')
                            RegistryKey  = [string]$instanceKey.Name
                        })
                    }
                    catch { Add-ICCollectorWarning -List $warnings -Message "USBSTOR instance '$($instanceKey.Name)': $($_.Exception.Message)" }
                }
            }
        }
        catch { Add-ICCollectorWarning -List $warnings -Message "USBSTOR enumeration: $($_.Exception.Message)" }
    }
    Add-ICOutputFiles -List $files -Path (Export-ICCollectorData -Context $Context -Collector Devices -RelativePath 'evidence/devices/usb-storage.json' -Data @($usbDevices | Sort-Object DeviceClass, SerialNumber) -Csv)

    $mountedDevices = New-Object System.Collections.ArrayList
    try {
        $properties = Get-ItemProperty -LiteralPath 'HKLM:\SYSTEM\MountedDevices' -ErrorAction Stop
        foreach ($property in $properties.PSObject.Properties) {
            if ($property.Name -match '^PS(Path|ParentPath|ChildName|Drive|Provider)$') {
                continue
            }
            $value = ConvertTo-ICMountedDeviceValue -Data $property.Value
            [void]$mountedDevices.Add([pscustomobject][ordered]@{
                Name        = [string]$property.Name
                Decoded     = $value.Decoded
                HexPrefix   = $value.HexPrefix
                LengthBytes = if ($property.Value -is [byte[]]) { ([byte[]]$property.Value).Length } else { $null }
            })
        }
    }
    catch { Add-ICCollectorWarning -List $warnings -Message "MountedDevices snapshot: $($_.Exception.Message)" }
    Add-ICOutputFiles -List $files -Path (Export-ICCollectorData -Context $Context -Collector Devices -RelativePath 'evidence/devices/mounted-devices.json' -Data @($mountedDevices | Sort-Object Name) -Csv)

    $mountPoints = New-Object System.Collections.ArrayList
    try {
        foreach ($hive in Get-ChildItem -LiteralPath 'Registry::HKEY_USERS' -ErrorAction Stop) {
            $sidName = Split-Path -Leaf $hive.Name
            if ($sidName -match '_Classes$') {
                continue
            }
            $mountPointRoot = "Registry::HKEY_USERS\$sidName\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\MountPoints2"
            if (-not (Test-Path -LiteralPath $mountPointRoot)) {
                continue
            }
            try {
                foreach ($mountKey in Get-ChildItem -LiteralPath $mountPointRoot -ErrorAction Stop) {
                    [void]$mountPoints.Add([pscustomobject][ordered]@{
                        Sid        = $sidName
                        MountPoint = Split-Path -Leaf $mountKey.Name
                    })
                }
            }
            catch { Add-ICCollectorWarning -List $warnings -Message "MountPoints2 hive '$sidName': $($_.Exception.Message)" }
        }
    }
    catch { Add-ICCollectorWarning -List $warnings -Message "MountPoints2 enumeration: $($_.Exception.Message)" }
    Add-ICOutputFiles -List $files -Path (Export-ICCollectorData -Context $Context -Collector Devices -RelativePath 'evidence/devices/mountpoints.json' -Data @($mountPoints | Sort-Object Sid, MountPoint) -Csv)

    $portableDevices = New-Object System.Collections.ArrayList
    $portableRoot = 'HKLM:\SOFTWARE\Microsoft\Windows Portable Devices\Devices'
    if (Test-Path -LiteralPath $portableRoot) {
        try {
            foreach ($deviceKey in Get-ChildItem -LiteralPath $portableRoot -ErrorAction Stop) {
                $friendlyName = $null
                try {
                    $properties = Get-ItemProperty -LiteralPath $deviceKey.PSPath -ErrorAction Stop
                    $friendlyName = [string](Get-ICPropertyValue -InputObject $properties -Name 'FriendlyName')
                }
                catch { $friendlyName = $null }
                [void]$portableDevices.Add([pscustomobject][ordered]@{
                    DeviceKey    = Split-Path -Leaf $deviceKey.Name
                    FriendlyName = $friendlyName
                })
            }
        }
        catch { Add-ICCollectorWarning -List $warnings -Message "Windows Portable Devices enumeration: $($_.Exception.Message)" }
    }
    Add-ICOutputFiles -List $files -Path (Export-ICCollectorData -Context $Context -Collector Devices -RelativePath 'evidence/devices/portable-devices.json' -Data @($portableDevices | Sort-Object DeviceKey) -Csv)

    $setupApiCopied = $false
    $setupApiPath = Join-Path $env:SystemRoot 'INF\setupapi.dev.log'
    $setupApiRecord = Get-ICFileMetadata -Path $setupApiPath
    if ((Get-ICPropertyValue -InputObject $setupApiRecord -Name 'Error')) {
        Add-ICCollectorWarning -List $warnings -Message "Device setup log '$setupApiPath' is unavailable."
    }
    elseif ([int64]$setupApiRecord.Length -gt $maximumArtifactBytes) {
        Add-ICCollectorWarning -List $warnings -Message ("Device setup log copy bounded: '{0}' ({1} bytes) exceeds MaximumArtifactFileBytes={2}; metadata was recorded without the file content." -f $setupApiPath, $setupApiRecord.Length, $maximumArtifactBytes)
    }
    else {
        try {
            $destinationPath = Join-Path $Context.RootPath 'evidence\devices\setupapi.dev.log'
            [void][System.IO.Directory]::CreateDirectory((Split-Path -Parent $destinationPath))
            [System.IO.File]::Copy($setupApiPath, $destinationPath, $false)
            Add-ICOutputFiles -List $files -Path @($destinationPath)
            $setupApiCopied = $true
        }
        catch { Add-ICCollectorWarning -List $warnings -Message "Device setup log copy: $($_.Exception.Message)" }
    }
    $setupApiRecord | Add-Member -NotePropertyName Copied -NotePropertyValue $setupApiCopied -Force
    Add-ICOutputFiles -List $files -Path (Export-ICCollectorData -Context $Context -Collector Devices -RelativePath 'evidence/devices/setupapi-log.json' -Data $setupApiRecord)

    return New-ICCollectorResultData -OutputFiles @($files) -Warnings @($warnings) -Metrics ([ordered]@{
        UsbStorageDevices   = $usbDevices.Count
        MountedDeviceValues = $mountedDevices.Count
        UserMountPoints     = $mountPoints.Count
        PortableDevices     = $portableDevices.Count
        SetupApiLogCopied   = $setupApiCopied
    })
}