feat(scan): report a fix floor when the advisory names no patched build
Adobe writes CVE-2026-48294 as "lessThanOrEqual 26.5.2.2", so no fixed build exists to show and the finding read "not announced" — which hid something the tester rightly pointed out twice: if 26.5.2.2 and older are affected, anything above it is not. That is a valid statement, and an admin can act on "newer than 26.5.2.2" even when the vendor named no build. Stored as ">26.5.2.2" and rendered as "not announced — need newer than 26.5.2.2". The prefix carries both facts at once, which is why it is not written as a bare version: fixed_version also drives the "patch available" badge, and lessThanOrEqual means the CNA named no patched release, often because none has shipped. has_fix reads the prefix and does not claim one exists. A later record with a real lessThan replaces the floor. No schema change: the existing column carries it, and nothing compares fixed_version numerically (only equality, in has_fix).
This commit is contained in:
@@ -81,6 +81,13 @@ class VulnerabilityPackage(Base, TimestampMixin):
|
||||
is the affected version itself."""
|
||||
if not self.fixed_version:
|
||||
return False
|
||||
# ">26.5.2.2" is a floor, not a build: it comes from a lessThanOrEqual
|
||||
# bound, which says "this and older are affected" and names no patched
|
||||
# release — often because none has shipped. The value is worth showing
|
||||
# (an admin can act on "newer than 26.5.2.2"), but claiming a patch is
|
||||
# available would send them looking for something that may not exist.
|
||||
if self.fixed_version.startswith(">"):
|
||||
return False
|
||||
if not self.package_version:
|
||||
return True # fix known, install unknown — best to show "patch available"
|
||||
return self.fixed_version != self.package_version
|
||||
|
||||
@@ -446,7 +446,30 @@ def _wildcard_floor(raw: str) -> Optional[str]:
|
||||
# iOS bounds — same shape — so nothing genuine is lost.
|
||||
_STRICT_SCHEME_KEYS = {"adobe-acrobat", "teams"}
|
||||
|
||||
def _fix_target(lt):
|
||||
def _fix_target(lt, lte=None):
|
||||
"""The fix to show. A lessThan bound names a real build; a lessThanOrEqual
|
||||
only bounds the damage, so it is reported as a floor: ">26.5.2.2".
|
||||
|
||||
The tester asked for this twice and he is right: if "26.5.2.2 and earlier"
|
||||
are affected, then anything above it is not, and that is a valid, useful
|
||||
statement — an admin can act on "newer than 26.5.2.2" even when the vendor
|
||||
named no build. Adobe writes CVE-2026-48294 exactly that way.
|
||||
|
||||
It is kept out of the plain fixed_version because that field also drives
|
||||
the "patch available" badge, and lessThanOrEqual means the CNA named no
|
||||
patched build — often because none had shipped yet. The ">" prefix carries
|
||||
both facts: the actionable floor is visible, and has_fix (see
|
||||
VulnerabilityPackage) reads the prefix and does not claim a patch exists.
|
||||
A later record with a real lessThan replaces it.
|
||||
"""
|
||||
real = _fix_build(lt)
|
||||
if real:
|
||||
return real
|
||||
floor = _fix_build(lte)
|
||||
return f">{floor}" if floor else None
|
||||
|
||||
|
||||
def _fix_build(lt):
|
||||
"""A lessThan bound only becomes a fixed_version if it names a real build.
|
||||
|
||||
CVE-2026-21710 lists Node 20.20.1, 22.22.1, 24.14.0 and 25.8.1 as concrete
|
||||
@@ -1276,7 +1299,8 @@ def scan_asset(db: Session, asset, packages: list, index: dict,
|
||||
# Only lessThan is a real fix target; lessThanOrEqual means that
|
||||
# version is still affected (no published fix) → leave fixed empty.
|
||||
c = {"cve": entry["cve"], "cvss": entry.get("cvss"), "severity": entry.get("sev"),
|
||||
"fixed": _fix_target(entry.get("lt")), "desc": entry.get("desc")}
|
||||
"fixed": _fix_target(entry.get("lt"), entry.get("lte")),
|
||||
"desc": entry.get("desc")}
|
||||
try:
|
||||
before = len(new_ids)
|
||||
cpe._upsert(db, asset, name, eff_ver, c, new_ids, touched=touched,
|
||||
|
||||
@@ -31,6 +31,13 @@ function formatFixedVersion(v?: string | null): string {
|
||||
if (/^[0-9a-f]{12,64}$/i.test(s) && !/[.:~_+]/.test(s)) {
|
||||
return `upstream commit ${s.slice(0, 12)}`;
|
||||
}
|
||||
// ">26.5.2.2" is a floor, not a build. The advisory bounded the damage
|
||||
// ("this version and older are affected") without naming a patched
|
||||
// release, so say what can actually be acted on instead of printing a
|
||||
// version number nobody can look up.
|
||||
if (s.startsWith('>')) {
|
||||
return `not announced — need newer than ${s.slice(1).trim()}`;
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
|
||||
@@ -70,5 +70,39 @@ def demo():
|
||||
print("ok RVTools and Veeam ONE (server only) resolve and bound correctly")
|
||||
|
||||
|
||||
def fix_floor():
|
||||
"""lessThanOrEqual names no patched build — report it as a floor.
|
||||
|
||||
Adobe writes CVE-2026-48294 as "lessThanOrEqual 26.5.2.2", so no build is
|
||||
named. "not announced" alone hid an actionable fact: anything above
|
||||
26.5.2.2 is not affected. It is shown as ">26.5.2.2" without claiming a
|
||||
patch exists.
|
||||
"""
|
||||
from app.services.cvelistv5_scan_service import _fix_target, _fix_build
|
||||
from app.models.vulnerability_package import VulnerabilityPackage
|
||||
|
||||
# A real lessThan still wins and is unchanged.
|
||||
assert _fix_target("4.8.1", None) == "4.8.1"
|
||||
assert _fix_target("13.1.0.7034", "13.0") == "13.1.0.7034"
|
||||
# Only a lessThanOrEqual → floor.
|
||||
assert _fix_target(None, "26.5.2.2") == ">26.5.2.2"
|
||||
# Wildcards are not builds on either side (Node's "4.*").
|
||||
assert _fix_target("4.*", None) is None
|
||||
assert _fix_target(None, "12.x.x") is None
|
||||
assert _fix_build("4.*") is None
|
||||
assert _fix_target(None, None) is None
|
||||
|
||||
# The floor must NOT read as "patch available".
|
||||
p = VulnerabilityPackage(package_name="Adobe Acrobat extension (Chrome)",
|
||||
package_version="23.8.0.1", fixed_version=">26.5.2.2")
|
||||
assert p.has_fix is False
|
||||
real = VulnerabilityPackage(package_name="RVTools", package_version="4.7.1",
|
||||
fixed_version="4.8.1")
|
||||
assert real.has_fix is True
|
||||
|
||||
print("ok lessThanOrEqual reported as a floor, without a patch claim")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
demo()
|
||||
fix_floor()
|
||||
|
||||
Reference in New Issue
Block a user