Vulnerability/Get-VulnerabilityPriority.ps1
|
function Get-VulnerabilityPriority { <# .SYNOPSIS Prioritizes assessment CVEs with EPSS and CISA KEV evidence. .DESCRIPTION Adds deterministic triage points to NVD and offline vulnerability findings. The heuristic combines a severity impact proxy, EPSS probability, and an asset multiplier. KEV membership overrides the score to 100/Critical. Missing evidence remains null and produces an honest Unscored state. These triage points are local queue-weighting policy, not a probability of exploitation or a measure of business risk. .PARAMETER InputObject Assessment result returned by Get-VulnerabilityAssessment. .PARAMETER AssetCriticality DomainController (1.25), Server (1.10), Workstation (1.00), or a custom finite numeric multiplier from 0.5 through 2.0. The default is 1.0. .PARAMETER EpssMode Auto uses a fresh full CSV cache, one API batch for small lookups, or the daily CSV for larger lookups. Api and Csv force the online refresh route. .PARAMETER CachePath Per-user feed cache directory. Defaults to LOCALAPPDATA on Windows. WSA_VULNERABILITY_PRIORITY_CACHE_PATH can override it for automation. .PARAMETER CacheMaxAgeHours Cache lifetime from 1 through 168 hours. An entry is stale at exactly this age. The default is 24 hours. .PARAMETER Offline Forbids all HTTP calls and uses only eligible cached data. This switch does not implicitly permit stale data. .PARAMETER AllowStaleCache Allows the last known-good expired cache when offline or after a failed refresh. Per-feed and per-row sources identify stale evidence. .PARAMETER ForceRefresh Attempts a live refresh even when a fresh cache exists. A valid fresh cache can still be used if that refresh fails. Offline remains authoritative. .EXAMPLE Get-VulnerabilityAssessment -ExactCVEMatch | Get-VulnerabilityPriority -AssetCriticality DomainController .EXAMPLE $assessment | Get-VulnerabilityPriority -Offline -AllowStaleCache -CachePath C:\AuditCache .OUTPUTS The original assessment with additive PrioritizedVulnerabilities, PrioritySummary, and PriorityMetadata properties. Existing NVD objects also receive additive EPSS, KEV, and priority fields. #> [CmdletBinding()] param( [Parameter(Mandatory = $true, ValueFromPipeline = $true)] [AllowNull()] [object]$InputObject, [Parameter()] [AllowNull()] [object]$AssetCriticality, [Parameter()] [ValidateSet('Auto', 'Api', 'Csv')] [string]$EpssMode = 'Auto', [Parameter()] [AllowNull()] [string]$CachePath, [Parameter()] [ValidateRange(1, 168)] [int]$CacheMaxAgeHours = 24, [Parameter()] [switch]$Offline, [Parameter()] [switch]$AllowStaleCache, [Parameter()] [switch]$ForceRefresh ) begin { $resolvedAsset = Resolve-WsaAssetCriticality -AssetCriticality $AssetCriticality $resolvedCachePath = Get-WsaPriorityCacheRoot -CachePath $CachePath $inputItems = New-Object 'System.Collections.Generic.List[object]' } process { if ($null -ne $InputObject) { $inputItems.Add($InputObject) } } end { $states = New-Object 'System.Collections.Generic.List[object]' $uniqueCveSet = @{} foreach ($item in $inputItems) { $candidates = New-Object 'System.Collections.Generic.List[object]' $deduplicated = @{} $applications = @() if ((Test-WsaPriorityProperty -InputObject $item -Name 'Software') -and $null -ne $item.Software) { $applications = @($item.Software) } foreach ($application in $applications) { $software = '' if ((Test-WsaPriorityProperty -InputObject $application -Name 'Name') -and $null -ne $application.Name) { $software = [string]$application.Name } elseif ((Test-WsaPriorityProperty -InputObject $application -Name 'DisplayName') -and $null -ne $application.DisplayName) { $software = [string]$application.DisplayName } $version = $null if ((Test-WsaPriorityProperty -InputObject $application -Name 'Version') -and $null -ne $application.Version) { $version = [string]$application.Version } elseif ((Test-WsaPriorityProperty -InputObject $application -Name 'DisplayVersion') -and $null -ne $application.DisplayVersion) { $version = [string]$application.DisplayVersion } $collections = @( [PSCustomObject]@{ Property = 'ExactCVEs'; MatchType = 'ExactCPE' }, [PSCustomObject]@{ Property = 'RealCVEs'; MatchType = 'Keyword' } ) foreach ($collection in $collections) { $propertyName = $collection.Property if (-not (Test-WsaPriorityProperty -InputObject $application -Name $propertyName) -or $null -eq $application.$propertyName) { continue } foreach ($cve in @($application.$propertyName)) { if ($null -eq $cve) { continue } $rawId = $null if ((Test-WsaPriorityProperty -InputObject $cve -Name 'CVEId') -and $null -ne $cve.CVEId) { $rawId = ([string]$cve.CVEId).Trim() } $validCve = Test-WsaCveId -CveId $rawId $normalizedId = if ($validCve) { $rawId.ToUpperInvariant() } else { $rawId } $severity = $null if ((Test-WsaPriorityProperty -InputObject $cve -Name 'Severity') -and $null -ne $cve.Severity) { $severity = [string]$cve.Severity } $cvssScore = $null if ((Test-WsaPriorityProperty -InputObject $cve -Name 'CvssScore') -and $null -ne $cve.CvssScore) { $cvssScore = $cve.CvssScore } $candidate = [PSCustomObject]@{ CVEId = $normalizedId Software = $software SoftwareVersion = $version MatchType = $collection.MatchType Severity = $severity CvssScore = $cvssScore SourceObjects = @($cve) } $dedupKey = 'NVD|' + $software.ToUpperInvariant() + '|' + $normalizedId if ($deduplicated.ContainsKey($dedupKey)) { $existing = $candidates[$deduplicated[$dedupKey]] $existing.SourceObjects = @($existing.SourceObjects) + @($cve) if ($collection.MatchType -eq 'ExactCPE' -and $existing.MatchType -ne 'ExactCPE') { $existing.MatchType = 'ExactCPE' $existing.Severity = $severity $existing.CvssScore = $cvssScore $existing.SoftwareVersion = $version } } else { $deduplicated[$dedupKey] = $candidates.Count $candidates.Add($candidate) } if ($validCve) { $uniqueCveSet[$normalizedId] = $true } } } } $offlineFindings = @() if ((Test-WsaPriorityProperty -InputObject $item -Name 'Vulnerabilities') -and $null -ne $item.Vulnerabilities) { $offlineFindings = @($item.Vulnerabilities) } foreach ($finding in $offlineFindings) { if ($null -eq $finding) { continue } $software = '' if ((Test-WsaPriorityProperty -InputObject $finding -Name 'Software') -and $null -ne $finding.Software) { $software = [string]$finding.Software } $version = $null if ((Test-WsaPriorityProperty -InputObject $finding -Name 'CurrentVersion') -and $null -ne $finding.CurrentVersion) { $version = [string]$finding.CurrentVersion } $severity = $null if ((Test-WsaPriorityProperty -InputObject $finding -Name 'Severity') -and $null -ne $finding.Severity) { $severity = [string]$finding.Severity } $cvssScore = $null if ((Test-WsaPriorityProperty -InputObject $finding -Name 'CvssScore') -and $null -ne $finding.CvssScore) { $cvssScore = $finding.CvssScore } $cveText = $null if ((Test-WsaPriorityProperty -InputObject $finding -Name 'CVE') -and $null -ne $finding.CVE) { $cveText = [string]$finding.CVE } $rawIds = if ($null -eq $cveText) { @('') } else { @($cveText.Split(',')) } foreach ($rawIdValue in $rawIds) { $rawId = $rawIdValue.Trim() $validCve = Test-WsaCveId -CveId $rawId $normalizedId = if ($validCve) { $rawId.ToUpperInvariant() } else { $rawId } $dedupKey = 'Offline|' + $software.ToUpperInvariant() + '|' + $normalizedId if ($deduplicated.ContainsKey($dedupKey)) { continue } $candidate = [PSCustomObject]@{ CVEId = $normalizedId Software = $software SoftwareVersion = $version MatchType = 'OfflineVersion' Severity = $severity CvssScore = $cvssScore SourceObjects = @() } $deduplicated[$dedupKey] = $candidates.Count $candidates.Add($candidate) if ($validCve) { $uniqueCveSet[$normalizedId] = $true } } } $states.Add([PSCustomObject]@{ InputObject = $item Candidates = $candidates }) } $uniqueCves = @($uniqueCveSet.Keys | Sort-Object) if ($uniqueCves.Count -gt 0) { try { $epssFeed = Get-WsaEpssFeed -CveIds $uniqueCves -CachePath $resolvedCachePath -Mode $EpssMode -CacheMaxAgeHours $CacheMaxAgeHours -Offline:$Offline -AllowStaleCache:$AllowStaleCache -ForceRefresh:$ForceRefresh } catch { $epssFeed = New-WsaEpssFeedResult -Error $_.Exception.Message } try { $kevFeed = Get-WsaKevFeed -CachePath $resolvedCachePath -CacheMaxAgeHours $CacheMaxAgeHours -Offline:$Offline -AllowStaleCache:$AllowStaleCache -ForceRefresh:$ForceRefresh } catch { $kevFeed = New-WsaKevFeedResult -Error $_.Exception.Message } } else { $epssFeed = New-WsaEpssFeedResult $kevFeed = New-WsaKevFeedResult } foreach ($state in $states) { $rows = New-Object 'System.Collections.Generic.List[object]' foreach ($candidate in $state.Candidates) { $validCve = Test-WsaCveId -CveId $candidate.CVEId $epssEntry = $null $epssSource = 'Unavailable' if ($validCve) { if ($epssFeed.Entries.ContainsKey($candidate.CVEId)) { $epssEntry = $epssFeed.Entries[$candidate.CVEId] $epssSource = $epssFeed.Source } elseif ($epssFeed.Source -ne 'Unavailable' -and $epssFeed.CoveredCves.ContainsKey($candidate.CVEId)) { $epssSource = 'NotScored' } } $kevEntry = $null $kevSource = if ($validCve) { $kevFeed.Source } else { 'Unavailable' } $kevStatus = $null $kevDateAdded = $null $kevDueDate = $null $kevRequiredAction = $null $kevRansomware = $null $kevRansomwareStatus = 'Unavailable' $kevCatalogVersion = $null if ($validCve -and $kevSource -ne 'Unavailable') { $kevCatalogVersion = $kevFeed.CatalogVersion if ($kevFeed.Entries.ContainsKey($candidate.CVEId)) { $kevEntry = $kevFeed.Entries[$candidate.CVEId] $kevStatus = $true $kevDateAdded = $kevEntry.DateAdded $kevDueDate = $kevEntry.DueDate $kevRequiredAction = $kevEntry.RequiredAction $kevRansomwareStatus = $kevEntry.RansomwareStatus if ($kevEntry.RansomwareStatus -eq 'Known') { $kevRansomware = $true } } else { $kevStatus = $false $kevRansomware = $false $kevRansomwareStatus = 'NotListed' } } $priority = Resolve-WsaPriorityScore -Severity $candidate.Severity -EpssScore $(if ($null -ne $epssEntry) { $epssEntry.EpssScore } else { $null }) -KevStatus $kevStatus -AssetMultiplier $resolvedAsset.AssetMultiplier -EpssSource $epssSource -KevSource $kevSource -ValidCve $validCve $row = [PSCustomObject]@{ CVEId = $candidate.CVEId Software = $candidate.Software SoftwareVersion = $candidate.SoftwareVersion MatchType = $candidate.MatchType Severity = $candidate.Severity CvssScore = $candidate.CvssScore EpssScore = if ($null -ne $epssEntry) { [double]$epssEntry.EpssScore } else { $null } EpssPercentile = if ($null -ne $epssEntry) { [double]$epssEntry.EpssPercentile } else { $null } EpssScoreDate = if ($null -ne $epssEntry) { $epssEntry.EpssScoreDate } else { $null } KevStatus = $kevStatus KevDateAdded = $kevDateAdded KevDueDate = $kevDueDate KevRequiredAction = $kevRequiredAction KevRansomware = $kevRansomware KevRansomwareStatus = $kevRansomwareStatus KevCatalogVersion = $kevCatalogVersion AssetCriticality = $resolvedAsset.AssetCriticality AssetMultiplier = [double]$resolvedAsset.AssetMultiplier PriorityScore = $priority.PriorityScore PriorityTier = $priority.PriorityTier PriorityStatus = $priority.PriorityStatus UnscoredReason = $priority.UnscoredReason ScoreSource = $priority.ScoreSource EpssSource = $epssSource KevSource = $kevSource } $rows.Add($row) foreach ($sourceObject in $candidate.SourceObjects) { foreach ($property in $row.PSObject.Properties) { if ($property.Name -notin @('CVEId', 'Severity', 'CvssScore')) { $sourceObject | Add-Member -NotePropertyName $property.Name -NotePropertyValue $property.Value -Force } } } } $sortedRows = @($rows | Sort-Object ` @{ Expression = { if ($_.KevStatus -eq $true) { 0 } else { 1 } } }, ` @{ Expression = { if ($_.KevStatus -eq $true -and $null -ne $_.KevDueDate) { $_.KevDueDate } else { [DateTime]::MaxValue } } }, ` @{ Expression = { if ($_.KevRansomware -eq $true) { 0 } else { 1 } } }, ` @{ Expression = { if ($null -eq $_.PriorityScore) { 1 } else { 0 } } }, ` @{ Expression = { $_.PriorityScore }; Descending = $true }, ` @{ Expression = { if ($null -eq $_.EpssScore) { 1 } else { 0 } } }, ` @{ Expression = { $_.EpssScore }; Descending = $true }, ` @{ Expression = { $_.Software } }, ` @{ Expression = { $_.CVEId } }) $kevCves = @($sortedRows | Where-Object { $_.KevStatus -eq $true } | ForEach-Object { $_.CVEId } | Sort-Object -Unique) $summary = [PSCustomObject]@{ TotalFindings = $sortedRows.Count UniqueCves = $uniqueCves.Count ScoredFindings = @($sortedRows | Where-Object { $_.PriorityStatus -eq 'Scored' }).Count UnscoredFindings = @($sortedRows | Where-Object { $_.PriorityStatus -eq 'Unscored' }).Count CriticalFindings = @($sortedRows | Where-Object { $_.PriorityTier -eq 'Critical' }).Count HighFindings = @($sortedRows | Where-Object { $_.PriorityTier -eq 'High' }).Count MediumFindings = @($sortedRows | Where-Object { $_.PriorityTier -eq 'Medium' }).Count LowFindings = @($sortedRows | Where-Object { $_.PriorityTier -eq 'Low' }).Count KevFindings = @($sortedRows | Where-Object { $_.KevStatus -eq $true }).Count UniqueKevCves = $kevCves.Count InvalidCveFindings = @($sortedRows | Where-Object { $_.UnscoredReason -eq 'InvalidCve' }).Count } $metadata = [PSCustomObject]@{ GeneratedAtUtc = (Get-WsaPriorityUtcNow).ToUniversalTime().ToString('o', [Globalization.CultureInfo]::InvariantCulture) FormulaType = 'Deterministic triage-points heuristic' Formula = 'Round(Min(100, AssetMultiplier * (SeverityImpact + 60 * EPSS)), 2); KEV override = 100' AssetCriticality = $resolvedAsset.AssetCriticality AssetMultiplier = [double]$resolvedAsset.AssetMultiplier EpssMode = $EpssMode CachePath = $resolvedCachePath CacheMaxAgeHours = $CacheMaxAgeHours Offline = $Offline.IsPresent AllowStaleCache = $AllowStaleCache.IsPresent ForceRefresh = $ForceRefresh.IsPresent RequestedCves = @($uniqueCves) EpssSource = $epssFeed.Source EpssScoreDate = $epssFeed.ScoreDate EpssModelVersion = $epssFeed.ModelVersion EpssFetchedAtUtc = $epssFeed.FetchedAtUtc EpssError = $epssFeed.Error KevSource = $kevFeed.Source KevCatalogVersion = $kevFeed.CatalogVersion KevDateReleased = $kevFeed.DateReleased KevFetchedAtUtc = $kevFeed.FetchedAtUtc KevError = $kevFeed.Error } $state.InputObject | Add-Member -NotePropertyName PrioritizedVulnerabilities -NotePropertyValue @($sortedRows) -Force $state.InputObject | Add-Member -NotePropertyName PrioritySummary -NotePropertyValue $summary -Force $state.InputObject | Add-Member -NotePropertyName PriorityMetadata -NotePropertyValue $metadata -Force Write-Output $state.InputObject } } } |