diff --git a/changes/44800-ollama-resolved-in-version b/changes/44800-ollama-resolved-in-version new file mode 100644 index 0000000000..a447dfd2dc --- /dev/null +++ b/changes/44800-ollama-resolved-in-version @@ -0,0 +1 @@ +- Fixed missing `resolved_in_version` for CVE-2025-63389 on Ollama (resolved in v0.12.4), which was absent because the NVD record only provides a `versionEndIncluding` constraint. diff --git a/server/vulnerabilities/nvd/sync/cve_syncer.go b/server/vulnerabilities/nvd/sync/cve_syncer.go index ecbc422661..c496c185a2 100644 --- a/server/vulnerabilities/nvd/sync/cve_syncer.go +++ b/server/vulnerabilities/nvd/sync/cve_syncer.go @@ -553,6 +553,23 @@ func transformVuln(year int, item nvdapi.CVEItem) nvdapi.CVEItem { } } + // NVD lists ollama as vulnerable through (and including) v0.12.3 via versionEndIncluding with no + // versionEndExcluding, so resolved_in_version comes back empty. The fix shipped in the next + // release, v0.12.4. Supply versionEndExcluding here so Fleet reports the resolved version. + // See https://github.com/fleetdm/fleet/issues/44800. + if item.CVE.ID != nil && *item.CVE.ID == "CVE-2025-63389" { + for configID := range item.CVE.Configurations { + for nodeID := range item.CVE.Configurations[configID].Nodes { + for matchID := range item.CVE.Configurations[configID].Nodes[nodeID].CPEMatch { + match := &item.CVE.Configurations[configID].Nodes[nodeID].CPEMatch[matchID] + if strings.Contains(match.Criteria, ":ollama:ollama:") && match.VersionEndExcluding == nil { + match.VersionEndExcluding = new("0.12.4") + } + } + } + } + } + return item } diff --git a/server/vulnerabilities/nvd/sync/cve_syncer_test.go b/server/vulnerabilities/nvd/sync/cve_syncer_test.go index fa6ffb94e8..026a6bc4e7 100644 --- a/server/vulnerabilities/nvd/sync/cve_syncer_test.go +++ b/server/vulnerabilities/nvd/sync/cve_syncer_test.go @@ -30,6 +30,64 @@ var ( api20CVEDir = filepath.Join("testdata", "cve", "api_2.0") ) +func TestTransformVuln(t *testing.T) { + t.Parallel() + + // makeItem builds a minimal CVEItem with a single CPE match carrying the given criteria and + // versionEndIncluding/versionEndExcluding constraints. + makeItem := func(cveID, criteria string, endIncluding, endExcluding *string) nvdapi.CVEItem { + return nvdapi.CVEItem{ + CVE: nvdapi.CVE{ + ID: &cveID, + Configurations: []nvdapi.Config{ + { + Nodes: []nvdapi.Node{ + { + CPEMatch: []nvdapi.CVECPEMatch{ + { + Vulnerable: true, + Criteria: criteria, + VersionEndIncluding: endIncluding, + VersionEndExcluding: endExcluding, + }, + }, + }, + }, + }, + }, + }, + } + } + + endExcludingOf := func(item nvdapi.CVEItem) *string { + return item.CVE.Configurations[0].Nodes[0].CPEMatch[0].VersionEndExcluding + } + + const ollamaCPE = "cpe:2.3:a:ollama:ollama:*:*:*:*:*:*:*:*" + + t.Run("CVE-2025-63389 gets a resolved version when NVD provides only versionEndIncluding", func(t *testing.T) { + got := transformVuln(2025, makeItem("CVE-2025-63389", ollamaCPE, new("0.12.3"), nil)) + require.NotNil(t, endExcludingOf(got)) + require.Equal(t, "0.12.4", *endExcludingOf(got)) + }) + + t.Run("CVE-2025-63389 does not clobber an existing versionEndExcluding", func(t *testing.T) { + got := transformVuln(2025, makeItem("CVE-2025-63389", ollamaCPE, new("0.12.3"), new("0.12.9"))) + require.NotNil(t, endExcludingOf(got)) + require.Equal(t, "0.12.9", *endExcludingOf(got)) + }) + + t.Run("CVE-2025-63389 override does not apply to other products", func(t *testing.T) { + got := transformVuln(2025, makeItem("CVE-2025-63389", "cpe:2.3:a:acme:widget:*:*:*:*:*:*:*:*", new("0.12.3"), nil)) + require.Nil(t, endExcludingOf(got)) + }) + + t.Run("unrelated CVE is left unchanged", func(t *testing.T) { + got := transformVuln(2025, makeItem("CVE-2025-00000", ollamaCPE, new("0.12.3"), nil)) + require.Nil(t, endExcludingOf(got)) + }) +} + func TestStoreCVEsLegacyFormat(t *testing.T) { t.Parallel() year := 2023