How osv-scanner Determines and Assigns Severity Ratings to Detected Vulnerabilities
osv-scanner parses CVSS vectors and Ubuntu severity strings from the OSV backend, calculates numeric base scores using the pandatix/go-cvss library, selects the highest score when multiple severities exist, and converts these values into standardized Critical/High/Medium/Low ratings for consistent reporting.
When scanning manifests and dependencies for security issues, the google/osv-scanner source code processes raw severity data through a structured pipeline to ensure accurate risk prioritization. This system handles multiple CVSS versions and vendor-specific ratings, ultimately surfacing a single, authoritative severity rating for each detected vulnerability.
Parsing CVSS Vectors and Severity Types
The severity determination process begins in internal/utility/severity/severity.go within the CalculateScore function. This function receives a *osvschema.Severity object from the OSV database and inspects its type to determine the parsing strategy.
The implementation uses a type switch on severity.GetType() to select the appropriate parser:
- CVSS_V2 vectors are parsed using
github.com/pandatix/go-cvss/20 - CVSS_V3 entries (including 3.0 and 3.1) use
github.com/pandatix/go-cvss/30or31 - CVSS_V4 vectors are handled by
github.com/pandatix/go-cvss/40 - Ubuntu severity strings are taken verbatim via
severity.GetScore() - UNSPECIFIED types return no score
// internal/utility/severity/severity.go
func CalculateScore(severity *osvschema.Severity) (float64, string, error) {
// Switch on severity.GetType()
// vec, err := gocvss30.ParseVector(severity.GetScore())
// score := vec.BaseScore()
// rating, err := gocvss30.Rating(score)
// ...
// case osvschema.Severity_Ubuntu:
// rating = severity.GetScore()
return score, rating, err
}
The function returns a numeric base score (0.0–10.0), a textual rating (e.g., "HIGH"), and an error if vector parsing fails.
Calculating the Overall Severity Score
Vulnerabilities in the OSV database may contain multiple Severity objects from different sources (e.g., NVD, vendor advisories). The CalculateOverallScore function, also in internal/utility/severity/severity.go, resolves these into a single authoritative rating.
This function iterates over the slice of severity objects, calls CalculateScore on each entry, and retains the maximum numeric score. The rating associated with that highest score is returned as the definitive severity for the vulnerability.
// internal/utility/severity/severity.go
func CalculateOverallScore(severities []*osvschema.Severity) (float64, string, error) {
maxScore := -1.0
maxRating := string(UnknownRating)
for _, s := range severities {
score, rating, err := CalculateScore(s)
if err != nil { return -1, string(UnknownRating), err }
if score > maxScore {
maxScore, maxRating = score, rating
}
}
return maxScore, maxRating, nil
}
Aggregating Severity by Vulnerability Group
When constructing output results, osv-scanner groups related vulnerabilities. In internal/output/table.go, the MaxSeverity helper function determines the worst severity for each group by iterating over vulnerability IDs and extracting their associated severity lists.
This function calls CalculateOverallScore for each vulnerability, tracks the highest value across the group, and formats the result to one decimal place (e.g., "9.8"). The formatted string is stored in GroupInfo.MaxSeverity.
// internal/output/table.go
func MaxSeverity(group models.GroupInfo, pkg models.PackageVulns) string {
var maxSeverity float64 = -1
for _, vulnID := range group.IDs {
var severities []*osvschema.Severity
for _, vuln := range pkg.Vulnerabilities {
if vuln.GetId() == vulnID {
severities = vuln.GetSeverity()
}
}
score, _, _ := severity.CalculateOverallScore(severities)
maxSeverity = max(maxSeverity, score)
}
if maxSeverity < 0 { return "" }
return fmt.Sprintf("%.1f", maxSeverity)
}
The assignment of this value occurs in pkg/osvscanner/vulnerability_result.go during result construction.
Converting Scores to Standardized Ratings
The final conversion happens in internal/output/output_result.go, where the numeric string stored in GroupInfo.MaxSeverity is transformed into a human-readable rating label. The CalculateRating function parses the float value and requests the official CVSS v3 rating classification (Critical, High, Medium, or Low).
If the rating cannot be determined or the score is invalid, the function returns severity.UnknownRating, and the UI renders the severity as "N/A".
// internal/output/output_result.go (excerpt)
vuln.SeverityScore = group.MaxSeverity
vuln.SeverityRating, _ = severity.CalculateRating(vuln.SeverityScore)
if vuln.SeverityRating == severity.UnknownRating {
vuln.SeverityScore = "N/A"
}
These SeverityScore and SeverityRating fields propagate to all reporters (SARIF, JSON, HTML, and table output), ensuring consistency across formats.
Practical Code Examples
Parsing a Single CVSS Entry
s := &osvschema.Severity{
Type: osvschema.Severity_CVSS_V3,
Score: "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H",
}
score, rating, err := severity.CalculateScore(s)
// score => 9.8, rating => "CRITICAL"
Selecting the Highest Severity Among Multiple Sources
sevs := []*osvschema.Severity{sev1, sev2, sev3}
maxScore, maxRating, _ := severity.CalculateOverallScore(sevs)
// maxScore = highest numeric CVSS base score
// maxRating = rating belonging to that score
Integrating Severity into Output Results
group.MaxSeverity = output.MaxSeverity(groupInfo, pkgVulns) // e.g. "9.8"
vuln := VulnResult{
ID: "OSV-2023-123",
SeverityScore: group.MaxSeverity,
}
vuln.SeverityRating, _ = severity.CalculateRating(vuln.SeverityScore)
// vuln.SeverityRating == severity.CriticalRating
Summary
- Severity parsing occurs in
internal/utility/severity/severity.go, whereCalculateScoredelegates to the appropriate CVSS library based on the severity type. - Maximum severity selection uses
CalculateOverallScoreto compare multiple ratings and return the highest numeric score. - Group aggregation happens in
internal/output/table.goviaMaxSeverity, which formats the final score to one decimal place. - Rating conversion in
internal/output/output_result.gotranslates numeric scores into standardized Critical/High/Medium/Low labels, falling back to "N/A" for unknown values. - Consistent output is ensured across all reporters (table, SARIF, JSON, HTML) by reading from the unified result structure.
Frequently Asked Questions
What CVSS versions does osv-scanner support?
According to the source code in internal/utility/severity/severity.go, osv-scanner supports CVSS v2, CVSS v3.0/v3.1, and CVSS v4 through the pandatix/go-cvss library. It also handles Ubuntu severity strings and gracefully manages UNSPECIFIED types by returning no score.
How does osv-scanner handle multiple severity ratings for one vulnerability?
When a vulnerability contains several severity entries from different sources, the CalculateOverallScore function iterates through all entries, calculates the numeric score for each, and selects the maximum value as the definitive severity. This ensures the worst-case scenario is reported.
Why does my vulnerability show "N/A" for severity?
A severity displays as "N/A" when severity.CalculateRating returns UnknownRating in internal/output/output_result.go. This occurs when the OSV entry has an UNSPECIFIED type, when CVSS vector parsing fails, or when the score falls outside expected ranges.
Which file contains the core severity calculation logic?
The primary severity logic resides in internal/utility/severity/severity.go, which contains the CalculateScore and CalculateOverallScore functions. Supporting logic for formatting and group aggregation exists in internal/output/table.go and internal/output/output_result.go.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →