# How osv-scanner Determines and Assigns Severity Ratings to Detected Vulnerabilities

> Learn how osv-scanner assigns severity ratings. It parses CVSS vectors and Ubuntu severities, calculates scores using go-cvss, and converts them to Critical/High/Medium/Low for clear reporting.

- Repository: [Google/osv-scanner](https://github.com/google/osv-scanner)
- Tags: deep-dive
- Published: 2026-04-25

---

**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`](https://github.com/google/osv-scanner/blob/main/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/30` or `31`
- **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

```go
// 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`](https://github.com/google/osv-scanner/blob/main/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.

```go
// 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`](https://github.com/google/osv-scanner/blob/main/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`.

```go
// 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`](https://github.com/google/osv-scanner/blob/main/pkg/osvscanner/vulnerability_result.go) during result construction.

## Converting Scores to Standardized Ratings

The final conversion happens in [`internal/output/output_result.go`](https://github.com/google/osv-scanner/blob/main/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"**.

```go
// 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

```go
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

```go
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

```go
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`](https://github.com/google/osv-scanner/blob/main/internal/utility/severity/severity.go), where `CalculateScore` delegates to the appropriate CVSS library based on the severity type.
- **Maximum severity selection** uses `CalculateOverallScore` to compare multiple ratings and return the highest numeric score.
- **Group aggregation** happens in [`internal/output/table.go`](https://github.com/google/osv-scanner/blob/main/internal/output/table.go) via `MaxSeverity`, which formats the final score to one decimal place.
- **Rating conversion** in [`internal/output/output_result.go`](https://github.com/google/osv-scanner/blob/main/internal/output/output_result.go) translates 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`](https://github.com/google/osv-scanner/blob/main/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`](https://github.com/google/osv-scanner/blob/main/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`](https://github.com/google/osv-scanner/blob/main/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`](https://github.com/google/osv-scanner/blob/main/internal/output/table.go) and [`internal/output/output_result.go`](https://github.com/google/osv-scanner/blob/main/internal/output/output_result.go).