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 { } } |