Public/Software/Get-FleetSoftware.ps1

function Get-FleetSoftware {
    <#
    .SYNOPSIS
        Retrieves software inventory from FleetDM
     
    .DESCRIPTION
        Gets software inventory information from FleetDM. Can retrieve all software or filter by various criteria.
        Software inventory includes installed applications, versions, and vulnerability information.
     
    .PARAMETER Id
        The specific software ID to retrieve
     
    .PARAMETER Name
        Filter software by name (partial match)
     
    .PARAMETER Version
        Filter software by version
     
    .PARAMETER Cve
        Filter software by CVE (Common Vulnerabilities and Exposures) ID
     
    .PARAMETER VulnerableOnly
        Only return software with known vulnerabilities
 
    .PARAMETER FleetId
        Fleet Premium. Limits software to hosts in this fleet. Use 0 for unassigned hosts.
 
    .PARAMETER AvailableForInstall
        Only return software that has an installer. Installers require Fleet Premium.
 
    .PARAMETER SelfService
        Fleet Premium. Only return self-service software.
 
    .PARAMETER MinCvssScore
        Fleet Premium. Minimum CVSS v3 score, exclusive of scores below this value.
 
    .PARAMETER MaxCvssScore
        Fleet Premium. Maximum CVSS v3 score.
 
    .PARAMETER Exploit
        Fleet Premium. Only return software with a CISA known exploited vulnerability.
 
    .NOTES
        FleetId, SelfService, MinCvssScore, MaxCvssScore, and Exploit require Fleet Premium.
        https://fleetdm.com/docs/rest-api/rest-api#list-software-versions
     
    .PARAMETER Page
        Page number for pagination (0-based)
     
    .PARAMETER PerPage
        Number of results per page (default: 100)
     
    .PARAMETER OrderKey
        Sort results by this field (name, hosts_count, cve_published, cve_resolved)
     
    .PARAMETER OrderDirection
        Sort direction (asc or desc)
     
    .EXAMPLE
        Get-FleetSoftware
         
        Gets all software inventory
     
    .EXAMPLE
        Get-FleetSoftware -Name "Chrome"
         
        Gets all software with "Chrome" in the name
     
    .EXAMPLE
        Get-FleetSoftware -VulnerableOnly
         
        Gets only software with known vulnerabilities
     
    .EXAMPLE
        Get-FleetSoftware -Cve "CVE-2023-1234"
         
        Gets software affected by specific CVE
     
    .LINK
        https://fleetdm.com/docs/using-fleet/rest-api#list-software
    #>

    [CmdletBinding(DefaultParameterSetName = 'List')]
    param(
        [Parameter(ParameterSetName = 'ById', Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)]
        [ValidateRange(1, [int]::MaxValue)]
        [int]$Id,
        
        [Parameter(ParameterSetName = 'List')]
        [string]$Name,
        
        [Parameter(ParameterSetName = 'List')]
        [string]$Version,
        
        [Parameter(ParameterSetName = 'List')]
        [string]$Cve,
        
        [Parameter(ParameterSetName = 'List')]
        [switch]$VulnerableOnly,

        [Parameter(ParameterSetName = 'List')]
        [Parameter(ParameterSetName = 'ById')]
        [ValidateRange(0, [int]::MaxValue)]
        [int]$FleetId,

        [Parameter(ParameterSetName = 'List')]
        [switch]$AvailableForInstall,

        [Parameter(ParameterSetName = 'List')]
        [switch]$SelfService,

        [Parameter(ParameterSetName = 'List')]
        [ValidateRange(0, 10)]
        [double]$MinCvssScore,

        [Parameter(ParameterSetName = 'List')]
        [ValidateRange(0, 10)]
        [double]$MaxCvssScore,

        [Parameter(ParameterSetName = 'List')]
        [switch]$Exploit,
        
        [Parameter(ParameterSetName = 'List')]
        [int]$Page,
        
        [Parameter(ParameterSetName = 'List')]
        [ValidateRange(1, 500)]
        [int]$PerPage = 100,
        
        [Parameter(ParameterSetName = 'List')]
        [ValidateSet('name', 'hosts_count', 'cve_published', 'cvss_score', 'epss_probability', 'cisa_known_exploit')]
        [string]$OrderKey,
        
        [Parameter(ParameterSetName = 'List')]
        [ValidateSet('asc', 'desc')]
        [string]$OrderDirection = 'asc'
    )
    
    process {
        if ($PSCmdlet.ParameterSetName -eq 'ById') {
            # Get specific software
            $endpoint = "software/versions/$Id"
            
            Write-Verbose "Retrieving software with ID: $Id"
            
            try {
                $versionQuery = @{}
                if ($PSBoundParameters.ContainsKey('FleetId')) {
                    $versionQuery['fleet_id'] = $FleetId
                }
                $result = Invoke-FleetDMRequest -Endpoint $endpoint -Method GET -QueryParameters $versionQuery
                
                # Add custom type for formatting
                if ($result.software) {
                    $result.software.PSObject.TypeNames.Insert(0, 'FleetDM.Software')
                    return $result.software
                }
                else {
                    Write-Warning "No software data returned for ID: $Id"
                    return $null
                }
            }
            catch {
                if ($_.Exception.Message -like "*not found*") {
                    Write-Warning "Software with ID $Id not found"
                    return $null
                }
                throw
            }
        }
        else {
            # List software with filters
            # available_for_install and self_service are software title filters.
            $useTitles = $AvailableForInstall -or $SelfService
            $endpoint = if ($useTitles) { 'software/titles' } else { 'software/versions' }
            $queryParams = @{
                per_page = $PerPage
            }
            
            # Add optional parameters
            if ($PSBoundParameters.ContainsKey('Page')) {
                $queryParams['page'] = $Page
            }
            
            if ($VulnerableOnly) {
                $queryParams['vulnerable'] = 'true'
            }
            
            if ($OrderKey) {
                $queryParams['order_key'] = $OrderKey
                $queryParams['order_direction'] = $OrderDirection
            }
            
            # Server-side filtering parameters
            if ($Name) {
                $queryParams['query'] = $Name
            }
            
            if ($Version) {
                Write-Verbose "Version filtering will be applied client-side"
            }
            
            if ($Cve) {
                if ($useTitles) {
                    # Titles have no cve query parameter. MatchQuery searches the title name and the CVE id.
                    if (-not $Name) {
                        $queryParams['query'] = $Cve
                    }
                }
                else {
                    $queryParams['cve'] = $Cve
                    if (-not $Name) {
                        # List software versions searches CVE text through query.
                        $queryParams['query'] = $Cve
                    }
                }
            }

            if ($PSBoundParameters.ContainsKey('FleetId')) {
                $queryParams['fleet_id'] = $FleetId
            }

            if ($AvailableForInstall) {
                $queryParams['available_for_install'] = 'true'
            }

            if ($SelfService) {
                $queryParams['self_service'] = 'true'
            }

            if ($PSBoundParameters.ContainsKey('MinCvssScore')) {
                $queryParams['min_cvss_score'] = $MinCvssScore
            }

            if ($PSBoundParameters.ContainsKey('MaxCvssScore')) {
                $queryParams['max_cvss_score'] = $MaxCvssScore
            }

            if ($Exploit) {
                $queryParams['exploit'] = 'true'
            }
            
            Write-Verbose "Query parameters: $($queryParams | ConvertTo-Json -Compress)"
            
            # Determine if we should follow pagination
            $followPagination = -not $PSBoundParameters.ContainsKey('Page')
            
            if ($followPagination) {
                $result = Invoke-FleetDMRequest -Endpoint $endpoint -QueryParameters $queryParams -FollowPagination
            }
            else {
                $response = Invoke-FleetDMRequest -Endpoint $endpoint -QueryParameters $queryParams
                # Handle response structure - API returns object with software array
                if ($useTitles -and $response -and $null -ne $response.software_titles) {
                    $result = $response.software_titles
                }
                elseif ($response -and $null -ne $response.software) {
                    $result = $response.software
                }
                else {
                    $result = @()
                }
            }
            
            # Apply client-side filtering if needed.
            # Title search uses one query string. When -Name already occupies it, -Cve
            # must still be applied to versions[].vulnerabilities.
            if ($useTitles -and $Cve -and $result) {
                $result = @($result | Where-Object {
                    @($_.versions.vulnerabilities | ForEach-Object { if ($_ -is [string]) { $_ } else { $_.cve } }) -like "*$Cve*"
                })
                Write-Verbose "Filtered to $($result.Count) software titles matching CVE '*$Cve*'"
            }

            if ($Version -and $result) {
                if ($useTitles) {
                    $result = @($result | Where-Object {
                        @($_.versions) | Where-Object { $_.version -like "*$Version*" }
                    })
                }
                else {
                    $result = $result | Where-Object { $_.version -like "*$Version*" }
                }
                Write-Verbose "Filtered to $($result.Count) software items matching version '*$Version*'"
            }
            
            # Add custom type for formatting. Wrap a single object so Windows
            # PowerShell 5.1 still applies calculated properties.
            if ($result) {
                if ($result -isnot [System.Collections.IList]) {
                    $result = @($result)
                }
                for ($i = 0; $i -lt $result.Count; $i++) {
                    $result[$i].PSObject.TypeNames.Insert(0, 'FleetDM.Software')
                    
                    # Title rows keep vulnerabilities on versions[], not on the title.
                    $vulnerabilities = @()
                    if ($useTitles) {
                        foreach ($softwareVersion in @($result[$i].versions)) {
                            if ($softwareVersion.vulnerabilities) {
                                $vulnerabilities += @($softwareVersion.vulnerabilities)
                            }
                        }
                    }
                    elseif ($result[$i].vulnerabilities) {
                        $vulnerabilities = @($result[$i].vulnerabilities)
                    }

                    # Add calculated properties
                    if ($vulnerabilities.Count -gt 0) {
                        $cveCount = $vulnerabilities.Count
                        Add-Member -InputObject $result[$i] -MemberType NoteProperty -Name 'cve_count' -Value $cveCount -Force
                        
                        # Title CVE ids are strings and have no severity. Version rows are objects.
                        $vulnerabilityObjects = @($vulnerabilities | Where-Object { $_ -isnot [string] })
                        $severities = @($vulnerabilityObjects | Where-Object { $_.severity } | ForEach-Object { $_.severity })
                        $highestSeverity = 'none'
                        if ($severities.Count -eq 0 -and $vulnerabilityObjects.Count -gt 0) {
                            # Has vulnerability objects but no severity info
                            $highestSeverity = 'medium'
                        }
                        elseif ($severities -contains 'critical') { $highestSeverity = 'critical' }
                        elseif ($severities -contains 'high') { $highestSeverity = 'high' }
                        elseif ($severities -contains 'medium') { $highestSeverity = 'medium' }
                        elseif ($severities -contains 'low') { $highestSeverity = 'low' }
                        
                        Add-Member -InputObject $result[$i] -MemberType NoteProperty -Name 'highest_severity' -Value $highestSeverity -Force
                    }
                    else {
                        Add-Member -InputObject $result[$i] -MemberType NoteProperty -Name 'cve_count' -Value 0 -Force
                        Add-Member -InputObject $result[$i] -MemberType NoteProperty -Name 'highest_severity' -Value 'none' -Force
                    }
                }
            }
            
            Write-Verbose "Retrieved $($result.Count) software items"
            
            return $result
        }
    }
}