Private/Collectors.ExecutionArtifacts.ps1

function ConvertFrom-ICRot13 {
    [CmdletBinding()]
    param(
        [AllowEmptyString()]
        [string]$Value
    )

    $characters = $Value.ToCharArray()
    for ($index = 0; $index -lt $characters.Length; $index++) {
        $code = [int]$characters[$index]
        if ($code -ge 65 -and $code -le 90) {
            $characters[$index] = [char]((($code - 65 + 13) % 26) + 65)
        }
        elseif ($code -ge 97 -and $code -le 122) {
            $characters[$index] = [char]((($code - 97 + 13) % 26) + 97)
        }
    }
    return -join $characters
}

function ConvertFrom-ICFileTimeUtc {
    [CmdletBinding()]
    param(
        [AllowNull()]
        [object]$FileTime
    )

    try {
        $value = [int64]$FileTime
        if ($value -le 0) {
            return $null
        }
        $timestamp = [datetime]::FromFileTimeUtc($value)
        if ($timestamp.Year -lt 1990 -or $timestamp -gt [datetime]::UtcNow.AddDays(1)) {
            return $null
        }
        return $timestamp.ToString('o')
    }
    catch {
        return $null
    }
}

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

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

    $prefetchRecords = New-Object System.Collections.ArrayList
    $prefetchCopied = 0
    $prefetchFound = 0
    $prefetchDirectory = Join-Path $env:SystemRoot 'Prefetch'
    if (Test-Path -LiteralPath $prefetchDirectory -PathType Container) {
        try {
            $prefetchFiles = @(
                Get-ChildItem -LiteralPath $prefetchDirectory -Filter '*.pf' -File -Force -ErrorAction Stop |
                    Sort-Object LastWriteTimeUtc -Descending
            )
            $prefetchFound = $prefetchFiles.Count
            $destinationDirectory = Join-Path $Context.RootPath 'evidence\execution\prefetch'
            foreach ($prefetchFile in $prefetchFiles) {
                $record = Get-ICFileMetadata -Path $prefetchFile.FullName
                $copied = $false
                $capsulePath = $null
                if ($prefetchCopied -lt $maximumPrefetchFiles -and [int64]$prefetchFile.Length -le $maximumArtifactBytes) {
                    try {
                        [void][System.IO.Directory]::CreateDirectory($destinationDirectory)
                        $destinationPath = Join-Path $destinationDirectory (ConvertTo-ICSafeFileName -Value $prefetchFile.Name)
                        [System.IO.File]::Copy($prefetchFile.FullName, $destinationPath, $false)
                        Add-ICOutputFiles -List $files -Path @($destinationPath)
                        $capsulePath = Get-ICRelativePath -BasePath $Context.RootPath -Path $destinationPath
                        $copied = $true
                        $prefetchCopied++
                    }
                    catch { Add-ICCollectorWarning -List $warnings -Message "Prefetch copy '$($prefetchFile.Name)': $($_.Exception.Message)" }
                }
                $record | Add-Member -NotePropertyName Copied -NotePropertyValue $copied -Force
                $record | Add-Member -NotePropertyName CapsulePath -NotePropertyValue $capsulePath -Force
                [void]$prefetchRecords.Add($record)
            }
            if ($prefetchFound -gt $prefetchCopied) {
                Add-ICCollectorWarning -List $warnings -Message ("Prefetch copy bounded: {0} of {1} file(s) copied (MaximumPrefetchFiles={2}, MaximumArtifactFileBytes={3})." -f $prefetchCopied, $prefetchFound, $maximumPrefetchFiles, $maximumArtifactBytes)
            }
        }
        catch { Add-ICCollectorWarning -List $warnings -Message "Prefetch inventory: $($_.Exception.Message)" }
    }
    else {
        Add-ICCollectorWarning -List $warnings -Message "Prefetch directory '$prefetchDirectory' is unavailable; prefetch can be disabled on this system."
    }
    Add-ICOutputFiles -List $files -Path (Export-ICCollectorData -Context $Context -Collector ExecutionArtifacts -RelativePath 'evidence/execution/prefetch-files.json' -Data @($prefetchRecords) -Csv)

    $appCompatCacheBytes = 0
    try {
        $appCompatKey = 'HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager\AppCompatCache'
        $appCompatRecord = [ordered]@{
            SourceKey   = $appCompatKey
            ValueName   = 'AppCompatCache'
            LengthBytes = $null
            SHA256      = $null
            CapsulePath = $null
        }
        $properties = Get-ItemProperty -LiteralPath $appCompatKey -ErrorAction Stop
        $rawCache = Get-ICPropertyValue -InputObject $properties -Name 'AppCompatCache'
        if ($rawCache -is [byte[]] -and $rawCache.Length -gt 0) {
            $binaryPath = Join-Path $Context.RootPath 'evidence\execution\appcompatcache.bin'
            [void][System.IO.Directory]::CreateDirectory((Split-Path -Parent $binaryPath))
            [System.IO.File]::WriteAllBytes($binaryPath, $rawCache)
            Add-ICOutputFiles -List $files -Path @($binaryPath)
            $appCompatCacheBytes = $rawCache.Length
            $appCompatRecord.LengthBytes = $rawCache.Length
            $appCompatRecord.SHA256 = (Get-FileHash -LiteralPath $binaryPath -Algorithm SHA256 -ErrorAction Stop).Hash.ToLowerInvariant()
            $appCompatRecord.CapsulePath = Get-ICRelativePath -BasePath $Context.RootPath -Path $binaryPath
        }
        else {
            $reason = if ($null -eq $rawCache) {
                "value 'AppCompatCache' was not found under '$appCompatKey'"
            }
            elseif ($rawCache -is [byte[]]) {
                "value 'AppCompatCache' exists but is empty"
            }
            else {
                "value 'AppCompatCache' has unexpected type '$($rawCache.GetType().FullName)'"
            }
            Add-ICCollectorWarning -List $warnings -Message "AppCompatCache snapshot: $reason."
        }
        Add-ICOutputFiles -List $files -Path (Export-ICCollectorData -Context $Context -Collector ExecutionArtifacts -RelativePath 'evidence/execution/appcompatcache.json' -Data ([pscustomobject]$appCompatRecord))
    }
    catch { Add-ICCollectorWarning -List $warnings -Message "AppCompatCache snapshot: $($_.Exception.Message)" }

    $bamEntries = New-Object System.Collections.ArrayList
    foreach ($bamRoot in @(
        'HKLM:\SYSTEM\CurrentControlSet\Services\bam\State\UserSettings',
        'HKLM:\SYSTEM\CurrentControlSet\Services\bam\UserSettings'
    )) {
        if (-not (Test-Path -LiteralPath $bamRoot)) {
            continue
        }
        try {
            foreach ($sidKey in Get-ChildItem -LiteralPath $bamRoot -ErrorAction Stop) {
                $sid = Split-Path -Leaf $sidKey.Name
                try {
                    $properties = Get-ItemProperty -LiteralPath $sidKey.PSPath -ErrorAction Stop
                    foreach ($property in $properties.PSObject.Properties) {
                        if ($property.Name -notmatch '\\' -or $property.Value -isnot [byte[]] -or $property.Value.Length -lt 8) {
                            continue
                        }
                        [void]$bamEntries.Add([pscustomobject][ordered]@{
                            Sid              = $sid
                            ExecutablePath   = [string]$property.Name
                            LastExecutionUtc = ConvertFrom-ICFileTimeUtc -FileTime ([System.BitConverter]::ToInt64($property.Value, 0))
                            SourceKey        = [string]$sidKey.Name
                        })
                    }
                }
                catch { Add-ICCollectorWarning -List $warnings -Message "BAM settings '$sid': $($_.Exception.Message)" }
            }
        }
        catch { Add-ICCollectorWarning -List $warnings -Message "BAM root '$bamRoot': $($_.Exception.Message)" }
        break
    }
    if ($bamEntries.Count -eq 0) {
        Add-ICCollectorWarning -List $warnings -Message 'Background Activity Moderator (BAM) execution records are unavailable on this system.'
    }
    Add-ICOutputFiles -List $files -Path (Export-ICCollectorData -Context $Context -Collector ExecutionArtifacts -RelativePath 'evidence/execution/bam-entries.json' -Data @($bamEntries | Sort-Object Sid, ExecutablePath) -Csv)

    $userAssistEntries = 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
            }
            $userAssistRoot = "Registry::HKEY_USERS\$sidName\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\UserAssist"
            if (-not (Test-Path -LiteralPath $userAssistRoot)) {
                continue
            }
            try {
                foreach ($namespaceKey in Get-ChildItem -LiteralPath $userAssistRoot -ErrorAction Stop) {
                    $countPath = Join-Path $namespaceKey.PSPath 'Count'
                    if (-not (Test-Path -LiteralPath $countPath)) {
                        continue
                    }
                    $properties = Get-ItemProperty -LiteralPath $countPath -ErrorAction Stop
                    foreach ($property in $properties.PSObject.Properties) {
                        if ($property.Name -match '^PS(Path|ParentPath|ChildName|Drive|Provider)$' -or $property.Value -isnot [byte[]]) {
                            continue
                        }
                        $bytes = [byte[]]$property.Value
                        $runCount = if ($bytes.Length -ge 8) { [System.BitConverter]::ToInt32($bytes, 4) } else { $null }
                        $lastExecution = if ($bytes.Length -ge 68) { ConvertFrom-ICFileTimeUtc -FileTime ([System.BitConverter]::ToInt64($bytes, 60)) } else { $null }
                        [void]$userAssistEntries.Add([pscustomobject][ordered]@{
                            Sid              = $sidName
                            GuidNamespace    = Split-Path -Leaf $namespaceKey.Name
                            DecodedName      = ConvertFrom-ICRot13 -Value ([string]$property.Name)
                            RunCount         = $runCount
                            LastExecutionUtc = $lastExecution
                            ValueLength      = $bytes.Length
                        })
                    }
                }
            }
            catch { Add-ICCollectorWarning -List $warnings -Message "UserAssist hive '$sidName': $($_.Exception.Message)" }
        }
    }
    catch { Add-ICCollectorWarning -List $warnings -Message "UserAssist enumeration: $($_.Exception.Message)" }
    Add-ICOutputFiles -List $files -Path (Export-ICCollectorData -Context $Context -Collector ExecutionArtifacts -RelativePath 'evidence/execution/userassist-entries.json' -Data @($userAssistEntries | Sort-Object Sid, DecodedName) -Csv)

    return New-ICCollectorResultData -OutputFiles @($files) -Warnings @($warnings) -Metrics ([ordered]@{
        PrefetchFilesFound  = $prefetchFound
        PrefetchFilesCopied = $prefetchCopied
        AppCompatCacheBytes = $appCompatCacheBytes
        BamEntries          = $bamEntries.Count
        UserAssistEntries   = $userAssistEntries.Count
    })
}