bug(redhat): picking the highest fixed version across minor releases reports already-patched packages
Description
Red Hat publishes a separate errata per minor release for the same CVE, and in the RHEL 9 mainstream data these sit under one and the same CPE. Trivy keeps only one of them — the one with the highest fixed version — so a package installed at exactly the version Red Hat published as the fix for its own minor is still reported.
Example from CVE-2026-31431, where both errata are published under the generic mainstream CPE, with no EUS involved:
2026-05-04 RHSA-2026:13565 cpe:/o:redhat:enterprise_linux:9 kernel-0:5.14.0-611.54.1.el9_7
2026-05-19 RHSA-2026:19225 cpe:/o:redhat:enterprise_linux:9 kernel-0:5.14.0-687.5.3.el9_8A redhat-oval.Get("kernel", …) lookup with the RHEL 9 content sets returns both entries, and only the el9_8 one survives.
Reproduction
This needs neither rootfs nor missing content manifests — it reproduces on a public image whose manifest lists the ordinary mainstream content sets.
trivy image registry.access.redhat.com/ubi9/ubi:9.7 --scanners vuln --pkg-types osImage sha256:e9a31af6530caffa3551f266c51a0d43b602e8f76a0dc12826dbeebceb487c92, Trivy 0.74.0, DB from 2026-09-07 — 16 findings where the installed version is the published fix:
CVE-2025-14087 glib2 installed=2.68.4-18.el9_7.2 reported=2.68.4-19.el9_8.1 (RHSA-2026:19361)
also fixed in 2.68.4-18.el9_7.2 (RHSA-2026:15971)
CVE-2026-29111 systemd installed=252-55.el9_7.9 reported=252-67.el9_8.2 (RHSA-2026:19213)
also fixed in 252-55.el9_7.9 (RHSA-2026:13677)
CVE-2026-4519 python3 installed=3.9.25-3.el9_7.3 reported=3.9.25-7.el9_8 (RHSA-2026:19216)
also fixed in 3.9.25-3.el9_7.2 (RHSA-2026:6766)Plus CVE-2026-4786, CVE-2026-6100, CVE-2026-0865 for python3/python3-libs, CVE-2025-14512 for glib2, CVE-2026-29111 for systemd-libs/systemd-pam/systemd-rpm-macros, CVE-2026-4878 for libcap, CVE-2026-34982 for vim-minimal.
The same check on ubi9/ubi:9.6 finds three more:
CVE-2025-53905 vim-minimal 8.2.2637-22.el9_6.1 RHSA-2025:17742 (reported as 8.2.2637-23.el9_7)
CVE-2025-53906 vim-minimal 8.2.2637-22.el9_6.1 RHSA-2025:17742
CVE-2025-6965 sqlite-libs 3.34.1-8.el9_6 RHSA-2025:11992 (reported as 3.34.1-9.el9_7)Root cause
Deduplication per CVE-ID keeps the advisory with the highest fixed version, regardless of which minor release that version belongs to.
Why the data looks like this
Red Hat cannot separate these by content set: a content set is one and the same repository serving every minor. In repository-to-cpe.json, rhel-9-for-x86_64-baseos-rpms carries a single set of CPEs for the whole major release, and the minors appear only in repository paths, which are not part of an image manifest.
rhel-9-for-x86_64-baseos-rpms
cpes: [cpe:/o:redhat:enterprise_linux:9::baseos, …]
repo_relative_urls: content/dist/rhel9/9.1/… 9.7/… 9.8/… 9/…In OVAL, RHEL 9 has exactly one mainstream stream, rhel-9.oval.xml. Per-minor streams exist only for EUS, AUS and E4S, and only for even minors, so there is no separate stream for 9.7 at all — an el9_7 errata lands in mainstream.
This matches Red Hat's stated position in the CPE guidelines: a generic CPE for the whole major release on RHEL 9 and earlier, minor versions in mainstream CPEs starting with RHEL 10.
Migrating to CSAF VEX does not solve this on its own. Red Hat distinguishes the errata at the product_id level, but both branches sit under one product_name and share a CPE:
BaseOS-9.7.0.Z.MAIN : kernel-0:5.14.0-611.54.1.el9_7 → cpe:/o:redhat:enterprise_linux:9::baseos
BaseOS-9.8.0.Z.MAIN.EUS : kernel-0:5.14.0-687.5.3.el9_8 → cpe:/o:redhat:enterprise_linux:9::baseosIn pkg/vulnsrc/redhat-csaf the product_id string is not preserved: relates_to_product_reference is reduced to its CPE, the stored entry holds only AffectedCPEIndices, and entries differing only by CPE are additionally merged. The resulting shape matches OVAL, so the same choice between two entries remains. Keeping the minor from product_id during the migration would make a correct solution possible without parsing the version string.
Why the highest version is picked today
The logic came in with #8063 for #8061. Judging by the test added at the time ("CVE-ID and RHSA-ID for same vulnerability"), the goal was to let an entry with a fix win over a CVE-level entry with an empty fixed version. That requirement still stands and must be preserved.
Between two real fixed versions the maximum is not an arbiter. Across 50 common RHEL 9 packages in the current DB, 195 CVEs have more than one fixed version, and 192 of those differ only by minor. The remaining three carry two versions within one branch, and Red Hat marks both as fixed:
CVE-2025-6020 pam BaseOS-9.6.0.Z.MAIN.EUS 1.5.1-25.el9_6 and 1.5.1-26.el9_6
CVE-2025-68800 kernel BaseOS-9.7.0.Z.MAIN 611.38.1.el9_7 and 611.41.1.el9_7
CVE-2026-43284 kernel BaseOS-9.8.0.Z.MAIN.EUS 687.5.3.el9_8 and 687.10.1.el9_8Two entries in one branch therefore mean two builds each carrying the fix, not a sequence from a partial fix to a complete one.
Possible solution
One possible solution (not thoroughly verified) is comparing dist tags: among the advisories that carry a fix, keep those whose dist tag matches the dist tag of the installed package, and compare the installed version against the lowest version within that branch; if nothing matches, keep the current behaviour. The requirement from #8061 — an advisory with a non-empty fixed version wins over one without — should be expressed separately and explicitly.
Running such a rule against the images removes exactly the findings listed above and adds none:
ubi9/ubi:9.7 16 findings drop, none appear
ubi9/ubi:9.6 3 findings drop, none appearKnown gaps in that approach, from a first pass over the data:
- Packages from AppStream modules do not carry an
.elN_Mtag at all — their versions look like1:18.14.2-2.module+el9.2.0.z+18497+a402347c— sonodejs,php,ruby,postgresql,mariadb,nginxand the rest of the modular stack would keep the current behaviour unless that form is handled too. - About 9% of the distinct fixed versions in the sample carry no minor in the tag (
0:2.34-100.el9), so they never match a branch. - Whichever advisory is selected also supplies the severity and the RHSA ID, and advisories for one CVE can disagree:
CVE-2024-36880onkernelis HIGH in one and LOW in another. This affects--severityand--exit-code, not just which findings appear. - The case of an installed minor with no advisory of its own, next to an advisory from an earlier minor that already covers the installed version, is not addressed. Selecting the highest advisory at or below the installed minor would cover it, but would reintroduce a false positive for
kernelCVE-2026-43284, where two builds carrying the fix live in the same branch. - The two images above are minimal UBI: no kernel, no modular packages, no EUS content, so they do not exercise the riskiest cases.
Note that the dist tag is used here only to choose between advisories already matched by the same CPE. It cannot be used to identify the subscription stream (EUS/AUS/E4S).
Residual risk: the rule relies on a fixed version listed by Red Hat genuinely resolving the CVE for its product. If a fix turns out to be incomplete after its minor has left mainstream support, there is no stream left to ship a second errata to — but that is a property of trusting Red Hat's per-product data in general, not a consequence of this change.
Discussed in https://github.com/aquasecurity/trivy/discussions/11120
Source: aquasecurity/trivy