src/Public/Collect/Invoke-FotonSystemCollection.ps1

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

function Invoke-FotonSystemCollection {
    Write-FotonStatus 'Collecting system identity, configuration, and boot state' 'Step'

    Invoke-FotonCollection -Category 'System' -Name 'os_info' -Description 'Operating system build, install date, uptime' -Script {
        $os = Get-CimInstance Win32_OperatingSystem -ErrorAction Stop
        $cs = Get-CimInstance Win32_ComputerSystem -ErrorAction SilentlyContinue
        $bios = Get-CimInstance Win32_BIOS -ErrorAction SilentlyContinue
        [PSCustomObject]@{
            Hostname           = $env:COMPUTERNAME
            Caption            = $os.Caption
            Version            = $os.Version
            BuildNumber        = $os.BuildNumber
            DisplayVersion     = (Get-FotonRegValue 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion' 'DisplayVersion')
            UBR                = (Get-FotonRegValue 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion' 'UBR')
            Architecture       = $os.OSArchitecture
            InstallDateUtc     = $os.InstallDate.ToUniversalTime().ToString('yyyy-MM-dd HH:mm:ss')
            LastBootUtc        = $os.LastBootUpTime.ToUniversalTime().ToString('yyyy-MM-dd HH:mm:ss')
            UptimeDays         = [math]::Round(((Get-Date) - $os.LastBootUpTime).TotalDays, 2)
            SerialNumber       = $os.SerialNumber
            RegisteredUser     = $os.RegisteredUser
            SystemDirectory    = $os.SystemDirectory
            Domain             = if ($cs) { $cs.Domain } else { '' }
            PartOfDomain       = if ($cs) { $cs.PartOfDomain } else { $null }
            Manufacturer       = if ($cs) { $cs.Manufacturer } else { '' }
            Model              = if ($cs) { $cs.Model } else { '' }
            TotalPhysicalMemGB = if ($cs) { [math]::Round($cs.TotalPhysicalMemory / 1GB, 2) } else { $null }
            BiosVersion        = if ($bios) { ($bios.BIOSVersion -join ' ') } else { '' }
            SmbiosBiosVersion  = if ($bios) { [string]$bios.SMBIOSBIOSVersion } else { '' }
            BiosReleaseDateUtc = if ($bios -and $bios.ReleaseDate) { $bios.ReleaseDate.ToUniversalTime().ToString('yyyy-MM-dd HH:mm:ss') } else { '' }
            # SMBIOS uses 255 (0xFF) for "no embedded controller version"; emitting
            # "255.255" would read as a real version.
            EmbeddedControllerVersion = if (-not $bios -or $null -eq $bios.EmbeddedControllerMajorVersion) { '' } elseif ([int]$bios.EmbeddedControllerMajorVersion -eq 255) { 'NotReported' } else { '{0}.{1}' -f $bios.EmbeddedControllerMajorVersion, $bios.EmbeddedControllerMinorVersion }
            BiosSerial         = if ($bios) { $bios.SerialNumber } else { '' }
            TimeZone           = [System.TimeZoneInfo]::Local.Id
            UtcOffset          = [System.TimeZoneInfo]::Local.GetUtcOffset((Get-Date)).ToString()
        }
    } | Out-Null

    Invoke-FotonCollection -Category 'System' -Name 'entra_workplace_join' -Format 'Csv' -Description 'Entra/Azure AD join, workplace join, and MDM enrollment from local registry (no dsregcmd, no Graph)' -Script {
        Get-FotonCloudJoinRows
    } | Out-Null

    Invoke-FotonCollection -Category 'System' -Name 'time_sync' -Format 'Txt' -Description 'NTP/time source configuration (timestamp reliability)' -Script {
        $out = @()
        $out += '--- w32tm /query /status ---'
        $out += (& w32tm.exe /query /status 2>&1 | Out-String)
        $out += '--- w32tm /query /configuration ---'
        $out += (& w32tm.exe /query /configuration 2>&1 | Out-String)
        $out += '--- Time Service ---'
        $out += (Get-Service W32Time -ErrorAction SilentlyContinue | Select-Object Name, Status, StartType | Out-String)
        $out
    } | Out-Null

    Invoke-FotonCollection -Category 'System' -Name 'hotfixes' -Format 'Csv' -Description 'Installed updates and patch dates' -Script {
        Get-HotFix -ErrorAction Stop |
            Select-Object HotFixID, Description, InstalledBy, @{n = 'InstalledOn'; e = { if ($_.InstalledOn) { $_.InstalledOn.ToString('yyyy-MM-dd') } else { '' } } } |
            Sort-Object InstalledOn -Descending
    } | Out-Null

    Invoke-FotonCollection -Category 'System' -Name 'boot_and_firmware' -Description 'Secure Boot, TPM, BitLocker, Firmware-class devices (staged UEFI updates), boot configuration' -Script {
        $secureBoot = 'Unknown'
        try { $secureBoot = [string](Confirm-SecureBootUEFI -ErrorAction Stop) } catch { $secureBoot = 'NotSupportedOrLegacyBIOS' }
        $tpm = $null
        try { $tpm = Get-CimInstance -Namespace 'root\cimv2\security\microsofttpm' -ClassName Win32_Tpm -ErrorAction Stop } catch { }
        $bde = @()
        try {
            $bde = @(Get-CimInstance -Namespace 'root\cimv2\security\microsoftvolumeencryption' -ClassName Win32_EncryptableVolume -ErrorAction Stop |
                    Select-Object DriveLetter, ProtectionStatus, ConversionStatus, EncryptionMethod)
        }
        catch { }
        # Windows Update ships UEFI capsules as Firmware-class devices. A device whose
        # name carries a newer version than SMBIOS, in an error state, is a staged
        # update waiting for the next reboot - that is how a BIOS change shows up
        # before it applies.
        $fw = @()
        try {
            $drivers = @{}
            foreach ($d in @(Get-CimInstance Win32_PnPSignedDriver -Filter "DeviceClass='FIRMWARE'" -ErrorAction Stop)) {
                if ($d.DeviceID) { $drivers[[string]$d.DeviceID] = $d }
            }
            foreach ($p in @(Get-CimInstance Win32_PnPEntity -Filter "PNPClass='Firmware'" -ErrorAction Stop)) {
                $drv = $drivers[[string]$p.DeviceID]
                $fw += [PSCustomObject]@{
                    Name          = [string]$p.Name
                    DeviceID      = [string]$p.DeviceID
                    Status        = [string]$p.Status
                    ProblemCode   = $p.ConfigManagerErrorCode
                    DriverVersion = if ($drv) { [string]$drv.DriverVersion } else { '' }
                    InfName       = if ($drv) { [string]$drv.InfName } else { '' }
                }
            }
        }
        catch { }
        [PSCustomObject]@{
            SecureBootEnabled = $secureBoot
            TpmPresent        = ($null -ne $tpm)
            TpmEnabled        = if ($tpm) { $tpm.IsEnabled_InitialValue } else { $null }
            TpmActivated      = if ($tpm) { $tpm.IsActivated_InitialValue } else { $null }
            TpmVersion        = if ($tpm) { $tpm.SpecVersion } else { '' }
            BitLockerVolumes  = $bde
            FirmwareDevices   = $fw
            BcdOutput         = (& bcdedit.exe /enum 2>&1 | Out-String)
        }
    } | Out-Null

    Invoke-FotonCollection -Category 'System' -Name 'disks_and_volumes' -Format 'Csv' -Description 'Logical volumes, free space, filesystem type' -Script {
        Get-CimInstance Win32_LogicalDisk -ErrorAction Stop | Select-Object DeviceID, DriveType, FileSystem, VolumeName,
        @{n = 'SizeGB'; e = { [math]::Round($_.Size / 1GB, 2) } },
        @{n = 'FreeGB'; e = { [math]::Round($_.FreeSpace / 1GB, 2) } }, VolumeSerialNumber
    } | Out-Null

    Invoke-FotonCollection -Category 'System' -Name 'environment_variables' -Format 'Csv' -Description 'Process/machine environment variables' -Script {
        Get-ChildItem Env: | Select-Object Name, Value
    } | Out-Null

    Invoke-FotonCollection -Category 'System' -Name 'system_restore_and_shadowcopies' -Format 'Txt' -Description 'Volume shadow copies (deletion is a ransomware indicator)' -RequiresElevation -Script {
        $out = @()
        $out += '--- vssadmin list shadows ---'
        $out += (& vssadmin.exe list shadows 2>&1 | Out-String)
        $out += '--- vssadmin list shadowstorage ---'
        $out += (& vssadmin.exe list shadowstorage 2>&1 | Out-String)
        $out
    } | Out-Null
}