From def21275194eb4fe6ef05cc4e820feb0b1058554 Mon Sep 17 00:00:00 2001 From: Rostyslav Kachan Date: Thu, 27 Aug 2026 15:49:37 +0200 Subject: [PATCH] chore: Extend /vulnerabilities response to return fixed versions RHINENG-29137 --- .github/workflows/lint.yml | 2 +- .github/workflows/test.yml | 2 +- vmaas/patches_test.go | 2 +- vmaas/types.go | 16 +++-- vmaas/vulnerabilities.go | 67 +++++++++--------- vmaas/vulnerabilities_test.go | 8 +-- vmaas/vulnerabilities_update_cves_test.go | 83 +++++++++++++++++++++++ 7 files changed, 134 insertions(+), 46 deletions(-) create mode 100644 vmaas/vulnerabilities_update_cves_test.go diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index de33050..f907a99 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -11,7 +11,7 @@ jobs: os: [ubuntu-latest] runs-on: ${{ matrix.os }} steps: - - run: sudo apt install -y librpm-dev rpm + - run: sudo apt-get update && sudo apt-get install -y librpm-dev rpm - uses: actions/setup-go@v5 with: go-version: ${{ matrix.go }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 1d1aeea..3b5c3dd 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -13,7 +13,7 @@ jobs: os: [ubuntu-latest] runs-on: ${{ matrix.os }} steps: - - run: sudo apt install -y librpm-dev rpm + - run: sudo apt-get update && sudo apt-get install -y librpm-dev rpm # cache downloaded modules and build cache - uses: actions/cache@v4 with: diff --git a/vmaas/patches_test.go b/vmaas/patches_test.go index 018e860..27d2130 100644 --- a/vmaas/patches_test.go +++ b/vmaas/patches_test.go @@ -56,6 +56,6 @@ func TestPatches(t *testing.T) { // Verify that errata are extracted correctly expectedErrata := []string{"RHSA-2023-001", "RHSA-2023-002", "RHSA-2023-003"} - assert.Equal(t, expectedErrata, result.Errata) + assert.ElementsMatch(t, expectedErrata, result.Errata) assert.Equal(t, cache.DBChange.LastChange, result.LastChange) } diff --git a/vmaas/types.go b/vmaas/types.go index c82b8f9..6326dda 100644 --- a/vmaas/types.go +++ b/vmaas/types.go @@ -243,10 +243,11 @@ type Updates struct { type Vulnerability string type VulnerabilityDetail struct { - CVE string `json:"cve"` - Packages map[string]bool - Errata map[string]bool - Affected []AffectedPackage `json:"affected,omitempty"` + CVE string `json:"cve"` + Packages map[string]bool + Errata map[string]bool + Affected []AffectedPackage `json:"affected,omitempty"` + AffectedNames map[string]bool `json:"-"` } // marshal VulnerabilityDetail Packages and Errata as json arrays for backward compatibility @@ -275,9 +276,10 @@ func (d VulnerabilityDetail) MarshalJSON() ([]byte, error) { } type AffectedPackage struct { - Name string `json:"package_name"` - EVRA string `json:"evra"` - Cpe CpeLabel `json:"cpe"` + Name string `json:"package_name"` + EVRA string `json:"evra"` + FixedEVRA string `json:"fixed_evra,omitempty"` + Cpe CpeLabel `json:"cpe,omitempty"` ModuleStreamPtrs } diff --git a/vmaas/vulnerabilities.go b/vmaas/vulnerabilities.go index 0491445..abfc081 100644 --- a/vmaas/vulnerabilities.go +++ b/vmaas/vulnerabilities.go @@ -94,6 +94,11 @@ func evaluate(c *Cache, opts *options, request *Request) (*VulnerabilitiesCvesDe // 2. evaluate CVEs from Repositories // if CVE is already in Unpatched list -> skip it updates := processed.evaluateRepositories(c, opts) + pkgByString := make(map[string]Package, len(processed.Packages)) + for _, p := range processed.Packages { + nameID := c.Packagename2ID[p.Nevra.Name] + pkgByString[p.Pkg] = Package{Nevra: p.Nevra, String: p.Pkg, NameID: nameID} + } seenPkgErratum := map[packageErratum]bool{} tmpManualCves := map[string]VulnerabilityDetail{} for pkg, upDetail := range updates.UpdateList { @@ -103,14 +108,15 @@ func evaluate(c *Cache, opts *options, request *Request) (*VulnerabilitiesCvesDe continue } seenPkgErratum[pe] = true + pkgDetail := pkgByString[pkg] for _, cve := range c.ErratumDetails[update.Erratum].CVEs { if _, inUnpatchedCves := cves.UnpatchedCves[cve]; inUnpatchedCves { continue } if update.manuallyFixable { - updateCves(tmpManualCves, cve, Package{String: pkg}, []string{update.Erratum}, "", nil) + updateCves(tmpManualCves, cve, pkgDetail, []string{update.Erratum}, "", nil, "") } else { - updateCves(cves.Cves, cve, Package{String: pkg}, []string{update.Erratum}, "", nil) + updateCves(cves.Cves, cve, pkgDetail, []string{update.Erratum}, "", nil, update.EVRA) } } } @@ -140,9 +146,9 @@ func evaluateUnpatchedCves(c *Cache, products []ProductsPackage, cves *Vulnerabi for _, cve := range getCveStrings(c, csafCves.Unfixed) { cpe := c.CpeID2Label[product.CpeID] if module.Module != "" { - updateCves(cves.UnpatchedCves, cve.String, pp.Package, nil, cpe, &module) + updateCves(cves.UnpatchedCves, cve.String, pp.Package, nil, cpe, &module, "") } else { - updateCves(cves.UnpatchedCves, cve.String, pp.Package, nil, cpe, nil) + updateCves(cves.UnpatchedCves, cve.String, pp.Package, nil, cpe, nil, "") } } } @@ -209,9 +215,9 @@ func updateManualCvesFromProducts(c *Cache, pkg Package, productID CSAFProductID CSAFProductID: productID, }] if module.Module != "" { - updateCves(cves.ManualCves, cve.String, pkg, []string{erratum}, cpe, &module) + updateCves(cves.ManualCves, cve.String, pkg, []string{erratum}, cpe, &module, "") } else { - updateCves(cves.ManualCves, cve.String, pkg, []string{erratum}, cpe, nil) + updateCves(cves.ManualCves, cve.String, pkg, []string{erratum}, cpe, nil, "") } } } @@ -463,29 +469,34 @@ func cpes2products(c *Cache, variants []VariantSuffix, cpes []CpeID, nameID Name return pp } +func buildAffectedPackage(pkg Package, cpe CpeLabel, module *ModuleStream, fixedEVRA string) AffectedPackage { + affectedPackage := AffectedPackage{ + Name: pkg.Name, + EVRA: pkg.EVRAStringE(true), + FixedEVRA: fixedEVRA, + Cpe: cpe, + } + if module != nil && module.Module != "" { + affectedPackage.Module = &module.Module + affectedPackage.Stream = &module.Stream + } + return affectedPackage +} + func updateCves(cves map[string]VulnerabilityDetail, cve string, pkg Package, errata []string, cpe CpeLabel, - module *ModuleStream, + module *ModuleStream, fixedEVRA string, ) { if _, has := cves[cve]; !has { cveDetail := VulnerabilityDetail{ - CVE: cve, - Packages: map[string]bool{pkg.String: true}, - Errata: map[string]bool{}, + CVE: cve, + Packages: map[string]bool{pkg.String: true}, + Errata: map[string]bool{}, + Affected: []AffectedPackage{buildAffectedPackage(pkg, cpe, module, fixedEVRA)}, + AffectedNames: map[string]bool{pkg.Name: true}, } for _, erratum := range errata { cveDetail.Errata[erratum] = true } - if len(cpe) > 0 { - cveDetail.Affected = []AffectedPackage{{ - Name: pkg.Name, - EVRA: pkg.EVRAStringE(true), - Cpe: cpe, - }} - if module != nil { - cveDetail.Affected[0].Module = &module.Module - cveDetail.Affected[0].Stream = &module.Stream - } - } cves[cve] = cveDetail return } @@ -495,17 +506,9 @@ func updateCves(cves map[string]VulnerabilityDetail, cve string, pkg Package, er for _, erratum := range errata { vulnDetail.Errata[erratum] = true } - if len(cpe) > 0 { - affectedPackage := AffectedPackage{ - Name: pkg.Name, - EVRA: pkg.EVRAStringE(true), - Cpe: cpe, - } - if module != nil { - affectedPackage.Module = &module.Module - affectedPackage.Stream = &module.Stream - } - vulnDetail.Affected = append(vulnDetail.Affected, affectedPackage) + if !vulnDetail.AffectedNames[pkg.Name] { + vulnDetail.Affected = append(vulnDetail.Affected, buildAffectedPackage(pkg, cpe, module, fixedEVRA)) + vulnDetail.AffectedNames[pkg.Name] = true } cves[cve] = vulnDetail } diff --git a/vmaas/vulnerabilities_test.go b/vmaas/vulnerabilities_test.go index 8faa750..5ccb8dd 100644 --- a/vmaas/vulnerabilities_test.go +++ b/vmaas/vulnerabilities_test.go @@ -329,16 +329,15 @@ func TestManualCvesNewerRelease(t *testing.T) { assert.Contains(t, cves.ManualCves["CVE-1"].Errata, "RHSA-1") assert.Contains(t, cves.ManualCves["CVE-1"].Errata, "RHSA-3") assert.Contains(t, cves.ManualCves["CVE-1"].Errata, "RHSA-6") - assert.Len(t, cves.ManualCves["CVE-1"].Affected, 2) + assert.Len(t, cves.ManualCves["CVE-1"].Affected, 1) assert.Equal(t, cves.ManualCves["CVE-1"].Affected[0].Cpe, currentReleaseCPE) - assert.Equal(t, cves.ManualCves["CVE-1"].Affected[1].Cpe, newerReleaseCPE) - assert.Equal(t, *cves.ManualCves["CVE-1"].Affected[1].Module, ms.Module) - assert.Equal(t, *cves.ManualCves["CVE-1"].Affected[1].Stream, ms.Stream) + assert.Empty(t, cves.ManualCves["CVE-1"].Affected[0].FixedEVRA) // CVE-2 is reported from newer release CPE (CSAF) and is fixed by RHSA-4 assert.Len(t, cves.ManualCves["CVE-2"].Errata, 1) assert.Contains(t, cves.ManualCves["CVE-2"].Errata, "RHSA-4") assert.Len(t, cves.ManualCves["CVE-2"].Affected, 1) assert.Equal(t, cves.ManualCves["CVE-2"].Affected[0].Cpe, newerReleaseCPE) + assert.Empty(t, cves.ManualCves["CVE-2"].Affected[0].FixedEVRA) assert.Equal(t, *cves.ManualCves["CVE-2"].Affected[0].Module, ms.Module) assert.Equal(t, *cves.ManualCves["CVE-2"].Affected[0].Stream, ms.Stream) // CVE-3 is reported from current release CPE (CSAF) and is fixed by RHSA-2 @@ -346,6 +345,7 @@ func TestManualCvesNewerRelease(t *testing.T) { assert.Contains(t, cves.ManualCves["CVE-3"].Errata, "RHSA-2") assert.Len(t, cves.ManualCves["CVE-3"].Affected, 1) assert.Equal(t, cves.ManualCves["CVE-3"].Affected[0].Cpe, currentReleaseCPE) + assert.Empty(t, cves.ManualCves["CVE-3"].Affected[0].FixedEVRA) // CVE-4 is reported from newer release CPE (Repos) and is fixed by RHSA-8 assert.Len(t, cves.ManualCves["CVE-4"].Errata, 1) assert.Contains(t, cves.ManualCves["CVE-4"].Errata, "RHSA-8") diff --git a/vmaas/vulnerabilities_update_cves_test.go b/vmaas/vulnerabilities_update_cves_test.go new file mode 100644 index 0000000..193dc5b --- /dev/null +++ b/vmaas/vulnerabilities_update_cves_test.go @@ -0,0 +1,83 @@ +package vmaas + +import ( + "testing" + + "github.com/redhatinsights/vmaas-lib/vmaas/utils" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestUpdateCvesFixedEVRA(t *testing.T) { + pkg := Package{ + Nevra: utils.Nevra{Name: "openssl", Epoch: 0, Version: "3.0.7", Release: "1.el9", Arch: "x86_64"}, + String: "openssl-0:3.0.7-1.el9.x86_64", + } + cpe := CpeLabel("cpe:/o:redhat:enterprise_linux:9") + + t.Run("repo path records affected with fixed_evra and empty cpe", func(t *testing.T) { + cves := map[string]VulnerabilityDetail{} + updateCves(cves, "CVE-2024-0001", pkg, []string{"RHSA-2024:0001"}, "", nil, "0:3.0.7-5.el9.x86_64") + + require.Len(t, cves["CVE-2024-0001"].Affected, 1) + assert.Equal(t, "openssl", cves["CVE-2024-0001"].Affected[0].Name) + assert.Equal(t, "0:3.0.7-1.el9.x86_64", cves["CVE-2024-0001"].Affected[0].EVRA) + assert.Equal(t, "0:3.0.7-5.el9.x86_64", cves["CVE-2024-0001"].Affected[0].FixedEVRA) + assert.Empty(t, cves["CVE-2024-0001"].Affected[0].Cpe) + }) + + t.Run("unpatched cve has no fixed_evra", func(t *testing.T) { + cves := map[string]VulnerabilityDetail{} + updateCves(cves, "CVE-2024-0002", pkg, nil, cpe, nil, "") + + require.Len(t, cves["CVE-2024-0002"].Affected, 1) + assert.Empty(t, cves["CVE-2024-0002"].Affected[0].FixedEVRA) + assert.Equal(t, cpe, cves["CVE-2024-0002"].Affected[0].Cpe) + }) + + t.Run("csaf manual records fixed_evra with cpe", func(t *testing.T) { + cves := map[string]VulnerabilityDetail{} + updateCves(cves, "CVE-2024-0003", pkg, []string{"RHSA-1"}, cpe, nil, "0:3.0.7-5.el9.x86_64") + + require.Len(t, cves["CVE-2024-0003"].Affected, 1) + assert.Equal(t, "0:3.0.7-5.el9.x86_64", cves["CVE-2024-0003"].Affected[0].FixedEVRA) + assert.Equal(t, cpe, cves["CVE-2024-0003"].Affected[0].Cpe) + }) + + t.Run("same package deduplicates keeping first fixed_evra", func(t *testing.T) { + cves := map[string]VulnerabilityDetail{} + updateCves(cves, "CVE-2024-0004", pkg, []string{"RHSA-1"}, "", nil, "0:3.0.7-4.el9.x86_64") + updateCves(cves, "CVE-2024-0004", pkg, []string{"RHSA-2"}, "", nil, "0:3.0.7-6.el9.x86_64") + + require.Len(t, cves["CVE-2024-0004"].Affected, 1) + assert.Equal(t, "0:3.0.7-4.el9.x86_64", cves["CVE-2024-0004"].Affected[0].FixedEVRA) + assert.Len(t, cves["CVE-2024-0004"].Errata, 2) + }) + + t.Run("affected always recorded even without cpe and fixed_evra", func(t *testing.T) { + cves := map[string]VulnerabilityDetail{} + updateCves(cves, "CVE-2024-0005", Package{String: "test-pkg"}, []string{"RHSA-1"}, "", nil, "") + + require.Len(t, cves["CVE-2024-0005"].Affected, 1) + assert.Empty(t, cves["CVE-2024-0005"].Affected[0].FixedEVRA) + assert.Empty(t, cves["CVE-2024-0005"].Affected[0].Cpe) + }) +} + +func TestUpdateCvesDeduplication(t *testing.T) { + t.Run("different packages are not deduplicated", func(t *testing.T) { + pkg := Package{ + Nevra: utils.Nevra{Name: "openssl", Epoch: 0, Version: "3.0.7", Release: "1.el9", Arch: "x86_64"}, + String: "openssl-0:3.0.7-1.el9.x86_64", + } + pkg2 := Package{ + Nevra: utils.Nevra{Name: "libssl", Epoch: 0, Version: "3.0.7", Release: "1.el9", Arch: "x86_64"}, + String: "libssl-0:3.0.7-1.el9.x86_64", + } + cves := map[string]VulnerabilityDetail{} + updateCves(cves, "CVE-2024-0006", pkg, []string{"RHSA-1"}, "", nil, "0:3.0.7-5.el9.x86_64") + updateCves(cves, "CVE-2024-0006", pkg2, []string{"RHSA-1"}, "", nil, "0:3.0.7-5.el9.x86_64") + + require.Len(t, cves["CVE-2024-0006"].Affected, 2) + }) +}