functions/Get-XdrVulnerabilityManagementVulnerabilities.ps1

function Get-XdrVulnerabilityManagementVulnerabilities {
    <#
    .SYNOPSIS
        Retrieves vulnerabilities from Vulnerability Management.

    .DESCRIPTION
        Gets vulnerability information from Threat and Vulnerability Management (TVM) in Microsoft Defender XDR.
        This function includes caching support with a 30-minute TTL to reduce API calls.
        By default, only returns vulnerabilities that impact at least one asset, ordered by number of impacted assets (descending).

    .PARAMETER Top
        Limits the number of results returned. Valid range is 1-10000.

    .PARAMETER Force
        Bypasses the cache and forces a fresh retrieval from the API.

    .PARAMETER AllVulnerabilities
        Returns all vulnerabilities without filtering. By default, only vulnerabilities impacting at least one asset are returned.

    .PARAMETER CountOnly
        Returns only the total count of vulnerabilities (numOfResults).

    .PARAMETER Summary
        Returns the vulnerability summary from the dedicated summary endpoint. This provides detailed counts including
        total vulnerabilities, exploitable, critical, zero-day, and unpatchable vulnerabilities without retrieving full vulnerability data.

    .EXAMPLE
        Get-XdrVulnerabilityManagementVulnerabilities
        Retrieves vulnerabilities impacting at least one asset, ordered by impact (most impacted first).

    .EXAMPLE
        Get-XdrVulnerabilityManagementVulnerabilities -Top 100
        Retrieves the first 100 vulnerabilities impacting at least one asset.

    .EXAMPLE
        Get-XdrVulnerabilityManagementVulnerabilities -Force
        Forces a fresh retrieval of vulnerabilities impacting at least one asset, bypassing the cache.

    .EXAMPLE
        Get-XdrVulnerabilityManagementVulnerabilities -AllVulnerabilities
        Retrieves all vulnerabilities without filtering, including those with zero impacted assets.

    .EXAMPLE
        Get-XdrVulnerabilityManagementVulnerabilities -CountOnly
        Returns only the total number of vulnerabilities.

    .EXAMPLE
        Get-XdrVulnerabilityManagementVulnerabilities -Summary
        Returns detailed vulnerability summary statistics including total, exploitable, critical, zero-day, and unpatchable counts.

    .OUTPUTS
        System.Object[]
        Returns an array of vulnerability objects from TVM.

    .OUTPUTS
        System.Int64
        When -CountOnly is specified, returns the total count as an integer.

    .OUTPUTS
        System.Management.Automation.PSCustomObject
        When -Summary is specified, returns a summary object with vulnerability statistics.
    #>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns', '')]
    [CmdletBinding(DefaultParameterSetName = 'Default')]
    [OutputType([System.Object[]])]
    [OutputType([System.Int64], ParameterSetName = 'CountOnly')]
    [OutputType([System.Management.Automation.PSCustomObject], ParameterSetName = 'Summary')]
    param (
        [Parameter(ParameterSetName = 'Default')]
        [ValidateRange(1, 10000)]
        [int]$Top,

        [Parameter()]
        [switch]$Force,

        [Parameter(ParameterSetName = 'Default')]
        [switch]$AllVulnerabilities,

        [Parameter(ParameterSetName = 'CountOnly')]
        [switch]$CountOnly,

        [Parameter(ParameterSetName = 'Summary')]
        [switch]$Summary
    )

    begin {
        Update-XdrConnectionSettings

        # Helper function for paginated requests with filter support
        function Invoke-PaginatedRequest {
            param(
                [string]$BaseUri,
                [hashtable]$Headers,
                [string]$FilterString = "",
                [int]$MaxResults = 0,
                [string]$DisplayName = "items"
            )

            $allResults = [System.Collections.Generic.List[object]]::new()
            $pageIndex = 1
            $totalResults = 0
            $maxPages = 1000  # Safety limit

            do {
                $uri = "$BaseUri`?pageIndex=$pageIndex$FilterString"
                Write-Verbose "Fetching page $pageIndex"

                try {
                    $response = Invoke-RestMethod -Uri $uri -Method Get -ContentType "application/json" -WebSession $script:session -Headers $Headers
                } catch {
                    Write-Error "Failed to retrieve $DisplayName (page $pageIndex): $_"
                    break
                }

                if ($pageIndex -eq 1) {
                    $totalResults = $response.numOfResults
                    $script:responseMetadata = $response.meta
                    $targetCount = if ($MaxResults -gt 0 -and $MaxResults -lt $totalResults) { $MaxResults } else { $totalResults }
                    Write-Information "Total $DisplayName available: $totalResults$(if ($MaxResults -gt 0 -and $MaxResults -lt $totalResults) { " (retrieving top $MaxResults)" })" -InformationAction Continue

                    # Warn if dataset is very large
                    if ($targetCount -gt 10000) {
                        Write-Warning "This will retrieve $targetCount $DisplayName which may take several minutes."
                    }
                }

                if ($response.results) {
                    foreach ($item in $response.results) {
                        $allResults.Add($item)
                        if ($MaxResults -gt 0 -and $allResults.Count -ge $MaxResults) { break }
                    }
                }

                # Progress bar for large result sets
                if ($targetCount -gt 25) {
                    $percentComplete = [math]::Min(100, [math]::Round(($allResults.Count / $targetCount) * 100))
                    Write-Progress -Activity "Retrieving $DisplayName" -Status "$($allResults.Count) of $targetCount" -PercentComplete $percentComplete
                }

                $pageIndex++
            } while ($allResults.Count -lt $targetCount -and $response.results.Count -gt 0 -and $pageIndex -le $maxPages)

            if ($targetCount -gt 25) {
                Write-Progress -Activity "Retrieving $DisplayName" -Completed
            }

            return @{
                Results    = $allResults.ToArray()
                TotalCount = $totalResults
            }
        }
    }

    process {
        $BaseUri = "https://security.microsoft.com/apiproxy/mtp/tvm/analytics/vulnerabilities"

        # Create TVM headers
        $tvmHeaders = $script:headers.Clone()
        $tvmHeaders["api-version"] = "1.0"

        # Handle Summary separately - dedicated endpoint
        if ($Summary) {
            $config = @{
                CacheKey    = "XdrVulnerabilityManagementVulnerabilities_Summary"
                Endpoint    = "$BaseUri/summary"
                DisplayName = "vulnerability summary"
            }

            $currentCacheValue = Get-XdrCache -CacheKey $config.CacheKey -ErrorAction SilentlyContinue
            if (-not $Force -and $currentCacheValue.NotValidAfter -gt (Get-Date)) {
                Write-Verbose "Using cached $($config.DisplayName)"
                return $currentCacheValue.Value
            } elseif ($Force) {
                Write-Verbose "Force parameter specified, bypassing cache"
                Clear-XdrCache -CacheKey $config.CacheKey
            }

            Write-Verbose "Retrieving $($config.DisplayName)"
            try {
                $result = Invoke-RestMethod -Uri $config.Endpoint -Method Get -ContentType "application/json" -WebSession $script:session -Headers $tvmHeaders

                # Handle null result
                if ($null -eq $result) {
                    Write-Verbose "No $($config.DisplayName) found"
                    return $null
                }

                # Display summary information
                Write-Information "Vulnerability Summary:" -InformationAction Continue
                Write-Information " Total Vulnerabilities: $($result.totalVulnerabilityCount)" -InformationAction Continue
                Write-Information " In Organization: $($result.vulnerabilityInOrgCount)" -InformationAction Continue
                Write-Information " Exploitable: $($result.exploitableVulnerabilitiesInOrgCount)" -InformationAction Continue
                Write-Information " Critical: $($result.criticalVulnerabilitiesInOrgCount)" -InformationAction Continue
                Write-Information " Zero-Day: $($result.zeroDayVulnerabilitiesInOrgCount)" -InformationAction Continue
                Write-Information " Unpatchable: $($result.unpatchableVulnerabilitiesInOrgCount)" -InformationAction Continue
                Write-Information " Partially Patchable: $($result.partiallyPatchableVulnerabilitiesInOrgCount)" -InformationAction Continue

                Set-XdrCache -CacheKey $config.CacheKey -Value $result -TTLMinutes 30
                return $result
            } catch {
                Write-Error "Failed to retrieve $($config.DisplayName): $_"
                return
            }
        }

        # Configuration for paginated vulnerabilities endpoint
        $cacheKey = if ($AllVulnerabilities) {
            "XdrVulnerabilityManagementVulnerabilities_All"
        } else {
            "XdrVulnerabilityManagementVulnerabilities_Filtered"
        }

        $displayName = if ($AllVulnerabilities) { "vulnerabilities (all)" } else { "vulnerabilities (impacting assets)" }

        # Build filter string (default: only vulnerabilities impacting at least one asset)
        $filterString = if (-not $AllVulnerabilities) {
            "&`$filter=(numOfImpactedAssets+gt+0)&`$orderby=numOfImpactedAssets+desc"
        } else {
            ""
        }

        # Check cache
        $currentCacheValue = Get-XdrCache -CacheKey $cacheKey -ErrorAction SilentlyContinue
        if (-not $Force -and $currentCacheValue.NotValidAfter -gt (Get-Date)) {
            Write-Verbose "Using cached $displayName"

            if ($CountOnly) {
                Write-Information "Total $displayName`: $($currentCacheValue.Value.numOfResults)" -InformationAction Continue
                return [int64]$currentCacheValue.Value.numOfResults
            }

            $results = $currentCacheValue.Value.results
            if ($Top -gt 0 -and $Top -lt $results.Count) {
                $results = $results | Select-Object -First $Top
            }
            Write-Information "Retrieved $($results.Count) $displayName from cache" -InformationAction Continue
            return $results
        } elseif ($Force) {
            Write-Verbose "Force parameter specified, bypassing cache"
            Clear-XdrCache -CacheKey $cacheKey
        }

        Write-Verbose "Retrieving $displayName"

        # For CountOnly, just fetch first page to get the count
        if ($CountOnly) {
            try {
                $Uri = "$BaseUri`?pageIndex=1$filterString"
                $response = Invoke-RestMethod -Uri $Uri -Method Get -ContentType "application/json" -WebSession $script:session -Headers $tvmHeaders
                Write-Information "Total $displayName`: $($response.numOfResults)" -InformationAction Continue
                return [int64]$response.numOfResults
            } catch {
                Write-Error "Failed to retrieve $displayName count: $_"
                return
            }
        }

        # Fetch paginated results
        $paginatedResult = Invoke-PaginatedRequest -BaseUri $BaseUri -Headers $tvmHeaders -FilterString $filterString -MaxResults $Top -DisplayName $displayName

        # Cache complete results (without Top limit) if we fetched everything
        if ($Top -eq 0 -or $Top -ge ($(if ($null -ne $paginatedResult.TotalCount) { $paginatedResult.TotalCount } else { 0 }))) {
            $completeResponse = [PSCustomObject]@{
                numOfResults = $(if ($null -ne $paginatedResult.TotalCount) { $paginatedResult.TotalCount } else { 0 })
                meta         = $script:responseMetadata
                results      = $(if ($null -ne $paginatedResult.Results) { $paginatedResult.Results } else { @() })
            }
            Set-XdrCache -CacheKey $cacheKey -Value $completeResponse -TTLMinutes 30
        }

        # Return empty array if results are null
        if ($null -eq $paginatedResult.Results -or $paginatedResult.Results.Count -eq 0) {
            return
        }
        return $paginatedResult.Results
    }

    end {

    }
}