DependencyTrack / DependencyTrack/dependency-track
NVD mirror ignores CVSS metric type and picks CNA/ADP rating over NIST Primary rating
- Dominant language
- Java
- Stars
- 4.2k
- Forks
- 811
- Avg merge
- 8h 39m
- Merged PRs (30d)
- 237
Description
### Current Behavior
Since upgrading from Dependency-Track 4.x to 5.0.3, CVEs that carry multiple CVSS v3.1 metrics in the NVD (NIST Primary + CNA/ADP Secondary ratings) frequently end up with a Secondary rating stored in Dependency-Track instead of NIST's Primary rating. Which rating wins depends on the order of the `cvssMetricV31[]` array returned by the NVD API, so it appears random across CVEs.
### Example 1: CVE-2023-2976 (wrong)
NVD API `cvssMetricV31[]` order: `Google CNA (5.5)`, `nvd@nist.gov Primary (7.1)`, `CISA-ADP (5.5)`
- NIST Primary: 7.1 HIGH, `CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:N`
- Google CNA (Secondary): 5.5 MEDIUM, `CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N`
- CISA-ADP (Secondary): 5.5 MEDIUM, same vector as Google
Dependency-Track stores: **5.5 / `.../C:H/I:N/A:N`** (the Google CNA rating), attributed to source NVD.
### Example 2: CVE-2026-42581 (correct, but only by coincidence)
NVD API `cvssMetricV31[]` order: `security-advisories@github.com (5.8)`, `nvd@nist.gov Primary (9.8)`, `redhat-SADP (7.2)`
Dependency-Track stores: **9.8** (correct).
## Root Cause
`vuln-data-source/nvd/src/main/java/org/dependencytrack/vulndatasource/nvd/ModelConverter.java` (in 5.0.x: `NvdToCyclonedxParser` in the mirror-service) converts every entry of `cvssMetricV31[]` into a CycloneDX rating and assigns its source via `determineMetricSource()`:
```java
if ("nvd@nist.gov".equals(source)) {
return SOURCE_NVD;
} else if ("security-advisories@github.com".equals(source)) {
return SOURCE_GITHUB;
}
// Fall back to NVD if we don't recognize the source.
return SOURCE_NVD;
```
Every unrecognized source (Google, CISA-ADP, redhat-SADP, VulnCheck, any other CNA) is labelled as source `NVD`. The metric `type` (`Primary` / `Secondary`) is not considered at all.
`BovModelConverter.compareRatings()` (5.0.x: `ModelConverterCdxToVuln.compareRatings()`) then prefers ratings whose source equals the vulnerability source (`NVD`). Since all non-GitHub ratings are now labelled `NVD` and share the same method, they tie, the stable sort keeps NVD API order, and the first entry wins.
This explains both examples: for CVE-2023-2976 the Google CNA rating is first in the array and is mislabelled as NVD, so it beats NIST. For CVE-2026-42581 the first entry is GitHub, which is recognized and demoted, so NIST's rating is the first `NVD`-labelled one.
The behaviour regressed compared to 4.x. In `dependency-track` (`4.14.x`), `parser/nvd/api20/ModelConverter.java` sorts the metrics by type before picking the first one:
```java
metrics.getCvssMetricV31().sort(comparingInt(metric -> metric.getType().ordinal()));
```
That sort was not carried over to the v5 converter.
## Proposed Fix
Either (or both):
1. Sort `cvssMetricV2/V30/V31/V40` by `type` (Primary first) before converting, as 4.x does, so the Primary rating is first among ties.
2. Only map `nvd@nist.gov` to source `NVD` in `determineMetricSource()`, and give all other sources a distinct source name (e.g. the raw source string), so `compareRatings()` can demote them. Optionally carry the `type` into the rating so the comparator can prefer Primary explicitly.
Longer term it would be useful to persist all ratings per source (CycloneDX `ratings[]`), but that is a larger change.
## Environment
- Dependency-Track version: 5.0.3 (hyades mirror-service v0.6.0)
- Vulnerability sources: NVD (REST API mirror), Trivy analyzer
- The code path is unchanged on `main` (5.7.0-alpha), so newer versions are affected as well.
### Steps to Reproduce
1. Run Dependency-Track 5.0.3 with NVD mirroring via the NVD REST API enabled.
2. Let the mirror import CVE-2023-2976.
3. `GET /api/v1/vulnerability/source/NVD/vuln/CVE-2023-2976` and compare `cvssV3Vector` / `cvssV3BaseScore` with the NIST Primary metric from `https://services.nvd.nist.gov/rest/json/cves/2.0?cveId=CVE-2023-2976`.
### Expected Behavior
When the NVD provides a `Primary` (NIST) CVSS metric, that metric should be the one stored in Dependency-Track. Secondary ratings should only be used as a fallback when no Primary rating exists.
### Dependency-Track Version
5.x
### Browser
Google Chrome
### Checklist
- [x] I have read and understand the [contributing guidelines](https://github.com/DependencyTrack/dependency-track/blob/main/CONTRIBUTING.md#filing-issues)
- [x] I have checked the [existing issues](https://github.com/DependencyTrack/dependency-track/issues) for whether this defect was already reported
Contributor guide
Research direction
Start with vuln-data-source/nvd/src/main/java/org/dependencytrack/vulndatasource/nvd/ModelConverter.java and trace how cvssMetricV2/V30/V31/V40 entries are converted and sourced. Compare the v5 behavior with the 4.x sorting in parser/nvd/api20/ModelConverter.java, then reproduce CVE-2023-2976 through the NVD API path. Done means a Primary rating is selected before Secondary ratings, with Secondary used only when no Primary exists.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- java
- Domain
- backend, security
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 75/100