src/Public/Collect/Invoke-FotonSoftwareCollection.ps1

# Invoke-FotonSoftwareCollection
# Live facts only. Called from Invoke-FotonTriage when that exists.
# Endpoint collect. Rebuild dist/Foton-Collect.ps1 after edits.

function Invoke-FotonSoftwareCollection {
    Write-FotonStatus 'Collecting installed software, drivers, and browser extensions' 'Step'

    Invoke-FotonCollection -Category 'Software' -Name 'installed_software' -Format 'Csv' -Description 'Installed programs from all registry views' -Script {
        $rows = @()
        $paths = @(
            'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*',
            'HKLM:\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*'
        )
        foreach ($p in $paths) {
            try {
                Get-ItemProperty -Path $p -ErrorAction SilentlyContinue | ForEach-Object {
                    if (-not $_.DisplayName) { return }
                    $rows += [PSCustomObject]@{
                        Scope           = 'Machine'
                        User            = ''
                        DisplayName     = $_.DisplayName
                        DisplayVersion  = $_.DisplayVersion
                        Publisher       = $_.Publisher
                        InstallDate     = $_.InstallDate
                        InstallLocation = $_.InstallLocation
                        UninstallString = $_.UninstallString
                    }
                }
            }
            catch { }
        }
        foreach ($hiveRoot in (Get-FotonUserHiveRoot)) {
            $up = Join-Path $hiveRoot.Root 'Software\Microsoft\Windows\CurrentVersion\Uninstall\*'
            try {
                Get-ItemProperty -Path $up -ErrorAction SilentlyContinue | ForEach-Object {
                    if (-not $_.DisplayName) { return }
                    $rows += [PSCustomObject]@{
                        Scope           = 'User'
                        User            = $hiveRoot.UserName
                        DisplayName     = $_.DisplayName
                        DisplayVersion  = $_.DisplayVersion
                        Publisher       = $_.Publisher
                        InstallDate     = $_.InstallDate
                        InstallLocation = $_.InstallLocation
                        UninstallString = $_.UninstallString
                    }
                }
            }
            catch { }
        }
        $rows | Sort-Object DisplayName
    } | Out-Null

    Invoke-FotonCollection -Category 'Software' -Name 'drivers' -Format 'Csv' -Description 'Kernel and file system drivers with signature state' -Script {
        $rows = @()
        foreach ($d in (Get-CimInstance Win32_SystemDriver -ErrorAction SilentlyContinue)) {
            $path = $d.PathName
            if ($path) { $path = [Environment]::ExpandEnvironmentVariables($path.Trim('"')) }
            $sig = Get-FotonSignatureInfo -Path $path
            $rows += [PSCustomObject]@{
                Name            = $d.Name
                DisplayName     = $d.DisplayName
                State           = $d.State
                StartMode       = $d.StartMode
                PathName        = $path
                SignatureStatus = $sig.Status
                Signer          = $sig.Signer
                SHA256          = (Get-FotonSafeHash -Path $path)
            }
        }
        $rows | Sort-Object Name
    } | Out-Null

    Invoke-FotonCollection -Category 'Software' -Name 'browser_extensions' -Format 'Csv' -Description 'Chromium-family extension folders and Firefox profile add-ons' -Script {
        $rows = @()
        $browsers = @(
            @{ Name = 'Chrome'; Sub = 'AppData\Local\Google\Chrome\User Data' },
            @{ Name = 'Edge'; Sub = 'AppData\Local\Microsoft\Edge\User Data' },
            @{ Name = 'Brave'; Sub = 'AppData\Local\BraveSoftware\Brave-Browser\User Data' }
        )
        foreach ($userProfile in (Get-FotonUserProfile)) {
            if (-not $userProfile.Exists) { continue }
            foreach ($b in $browsers) {
                $base = Join-Path $userProfile.ProfilePath $b.Sub
                if (-not (Test-Path -LiteralPath $base)) { continue }
                foreach ($bp in (Get-ChildItem -LiteralPath $base -Directory -ErrorAction SilentlyContinue)) {
                    $extDir = Join-Path $bp.FullName 'Extensions'
                    if (-not (Test-Path -LiteralPath $extDir)) { continue }
                    foreach ($ext in (Get-ChildItem -LiteralPath $extDir -Directory -ErrorAction SilentlyContinue)) {
                        $name = ''
                        $version = ''
                        $perms = ''
                        try {
                            $manifest = Get-ChildItem -LiteralPath $ext.FullName -Recurse -Filter 'manifest.json' -ErrorAction SilentlyContinue | Select-Object -First 1
                            if ($manifest) {
                                $json = Get-Content -LiteralPath $manifest.FullName -Raw -ErrorAction Stop | ConvertFrom-Json
                                $name = [string]$json.name
                                $version = [string]$json.version
                                if ($json.permissions) { $perms = ($json.permissions -join ', ') }
                            }
                        }
                        catch { }
                        $rows += [PSCustomObject]@{
                            User          = $userProfile.UserName
                            Browser       = $b.Name
                            BrowserProfile = $bp.Name
                            ExtensionId   = $ext.Name
                            Name          = $name
                            Version       = $version
                            Permissions   = $perms
                            InstalledUtc  = $ext.CreationTimeUtc.ToString('yyyy-MM-dd HH:mm:ss')
                        }
                    }
                }
            }
            $ffBase = Join-Path $userProfile.ProfilePath 'AppData\Roaming\Mozilla\Firefox\Profiles'
            if (Test-Path -LiteralPath $ffBase) {
                foreach ($fp in (Get-ChildItem -LiteralPath $ffBase -Directory -ErrorAction SilentlyContinue)) {
                    $extPath = Join-Path $fp.FullName 'extensions'
                    if (-not (Test-Path -LiteralPath $extPath)) { continue }
                    foreach ($x in (Get-ChildItem -LiteralPath $extPath -ErrorAction SilentlyContinue)) {
                        $rows += [PSCustomObject]@{
                            User = $userProfile.UserName; Browser = 'Firefox'; BrowserProfile = $fp.Name
                            ExtensionId = $x.Name; Name = ''; Version = ''; Permissions = ''
                            InstalledUtc = $x.CreationTimeUtc.ToString('yyyy-MM-dd HH:mm:ss')
                        }
                    }
                }
            }
        }
        $rows
    } | Out-Null

    Invoke-FotonCollection -Category 'Software' -Name 'remote_access_tool_indicators' -Format 'Csv' -Description 'Presence of common remote access / tunneling tooling' -Script {
        $known = @($script:FotonSession.RemoteAccessToolNames)
        $rows = @()
        $installed = $script:FotonSession.Data['installed_software']
        foreach ($app in $installed) {
            $n = ("{0} {1}" -f $app.DisplayName, $app.Publisher).ToLower()
            foreach ($k in $known) {
                if ($n.Contains($k)) {
                    $rows += [PSCustomObject]@{
                        Source = 'InstalledSoftware'; Match = $k; Detail = $app.DisplayName
                        Publisher = $app.Publisher; Extra = $app.InstallLocation
                    }
                    break
                }
            }
        }
        $procs = $script:FotonSession.Data['running_processes']
        foreach ($p in $procs) {
            $n = ("{0} {1}" -f $p.Name, $p.Path).ToLower()
            foreach ($k in $known) {
                if ($n.Contains($k)) {
                    $rows += [PSCustomObject]@{
                        Source = 'RunningProcess'; Match = $k; Detail = $p.Name
                        Publisher = $p.Signer; Extra = $p.Path
                    }
                    break
                }
            }
        }
        $svcs = $script:FotonSession.Data['services']
        foreach ($s in $svcs) {
            $n = ("{0} {1} {2}" -f $s.Name, $s.DisplayName, $s.PathName).ToLower()
            foreach ($k in $known) {
                if ($n.Contains($k)) {
                    $rows += [PSCustomObject]@{
                        Source = 'Service'; Match = $k; Detail = $s.DisplayName
                        Publisher = $s.Signer; Extra = $s.PathName
                    }
                    break
                }
            }
        }
        $rows
    } | Out-Null

    Invoke-FotonCollection -Category 'Software' -Name 'appx_packages' -Format 'Csv' -Description 'Installed Store/AppX packages' -Script {
        # -AllUsers requires elevation. Standard-user coverage is this token's packages only.
        if ($script:FotonSession.IsElevated) {
            Get-AppxPackage -AllUsers -ErrorAction Stop |
                Select-Object Name, PackageFullName, Publisher, Version, InstallLocation, SignatureKind, Status
        }
        else {
            Get-AppxPackage -ErrorAction Stop |
                Select-Object Name, PackageFullName, Publisher, Version, InstallLocation, SignatureKind, Status
        }
    } | Out-Null

    # Deep only: PnP device inventory + DriverStore FileRepository listing (not full binary tree copy).
    if ($script:FotonSession.Deep) {
        Invoke-FotonCollection -Category 'Software' -Name 'pnp_devices' -Format 'Csv' -Description 'Deep: Win32_PnPEntity inventory (Name, DeviceID, class, service). No binary copy.' -Script {
            $rows = @()
            foreach ($d in (Get-CimInstance Win32_PnPEntity -ErrorAction SilentlyContinue)) {
                $rows += [PSCustomObject]@{
                    Name         = $d.Name
                    DeviceID     = $d.DeviceID
                    Manufacturer = $d.Manufacturer
                    Status       = $d.Status
                    PNPClass     = $d.PNPClass
                    Service      = $d.Service
                }
            }
            $rows | Sort-Object DeviceID
        } | Out-Null

        Invoke-FotonCollection -Category 'Software' -Name 'driverstore_inventory' -Format 'Csv' -Description 'Deep: DriverStore\FileRepository inventory. SHA-256 for .sys/.inf/.cat only. No full tree copy into the package.' -Script {
            $repo = Join-Path $env:SystemRoot 'System32\DriverStore\FileRepository'
            $rows = @()
            if (-not (Test-Path -LiteralPath $repo)) {
                return @([PSCustomObject]@{
                        Path         = $repo
                        SizeBytes    = 0
                        Extension    = ''
                        ModifiedUtc  = ''
                        SHA256       = ''
                        Note         = 'DriverStore FileRepository not found. Visibility gap.'
                    })
            }
            $hashExt = @('.sys', '.inf', '.cat')
            Get-ChildItem -LiteralPath $repo -Recurse -File -Force -ErrorAction SilentlyContinue | ForEach-Object {
                $ext = $_.Extension
                $hash = ''
                foreach ($h in $hashExt) {
                    if ($ext.Equals($h, [StringComparison]::OrdinalIgnoreCase)) {
                        $hash = Get-FotonSafeHash -Path $_.FullName
                        break
                    }
                }
                $rows += [PSCustomObject]@{
                    Path        = $_.FullName
                    SizeBytes   = $_.Length
                    Extension   = $ext
                    ModifiedUtc = $_.LastWriteTimeUtc.ToString('yyyy-MM-dd HH:mm:ss')
                    SHA256      = $hash
                    Note        = ''
                }
            }
            $rows | Sort-Object Path
        } | Out-Null
    }
}