Files
vulncheck/tests/test_autodesk_year.py
T
vulncheck 974de08b3c fix(autodesk): match AutoCAD by release, not by release year
Two halves of the same blind spot, both on a stock AutoCAD LT 2026
(build 25.1.60.0):

MISSED. The cvelistV5 registry never carried year_ver, so that path
compared the MSI build 25.1.60.0 against "2026.0.0 lessThan 2026.1.2"
and matched nothing. It is the only path that can see these CVEs at
all — CVE-2026-7405, -7406 (ADSK-SA-2026-0012) and -17550 sit at NVD
with vulnStatus "Received" and zero cpeMatch entries, so the CPE path
is blind to them by construction. Four confirmed CVEs were invisible.

NEVER CLEARS. The year came from the product name and the build was
thrown away, so every 2026 install scanned as a bare "2026" — below
every 2026.x fix bound forever. A host patched to 2026.1.2 keeps all
seven of its autocad_lt findings with no version it could reach to
drop them. Three of those seven carry a bare-year CPE at NVD with no
range at all.

Both are fixed by resolving the build to the release Autodesk actually
bounds by: 25.1.60.0 -> 2026.0.0, 25.1.172.0 -> 2026.1.2. The table is
keyed by the build's major.minor, which has to agree with the year in
the name — that one check keeps it off products numbered some other
way, so Navisworks Freedom 2025 (22.0.1411.23) still falls back to the
bare year instead of landing on an AutoCAD row.

Hypothesis that held: the year was doing two jobs it cannot do at
once — pick the release line, and say where inside it the host sits.
2026-08-13 14:04:46 +02:00

137 lines
6.2 KiB
Python

"""Autodesk release versioning — run: python tests/test_autodesk_year.py
Autodesk versions its products by RELEASE (2026, 2026.1.2) and both NVD and
cvelistV5 state their bounds that way. The inventory reports the internal build
instead — AutoCAD LT 2026 installs as 25.1.60.0 — and comparing a build number
against a release matches nothing, which is why these were invisible.
The release year is taken from the NAME. Where there is none, nothing is
scanned: guessing a year would be worse than missing the product.
Where the build maps to a known update it is resolved to the precise release,
because the year alone can never CLEAR a host: "2026" sits below every 2026.x
fix bound forever, so a host patched to 2026.1.2 would keep every 2026 finding
it ever had.
"""
import os
import sys
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from app.services.app_cve_scanner_service import resolve_product, _effective_version
def _ver(name, version="25.1.60.0"):
e = resolve_product(name)
return (e or {}).get("key"), (_effective_version(name, version, e) if e else None)
def demo():
# Verbatim inventory rows from the tester's estate. 25.1.60.0 is the
# 2026 first-customer-ship build, i.e. no update applied.
assert _ver("AutoCAD LT 2026 - Deutsch (German)") == \
("cpe:autodesk:autocad_lt", "2026.0.0")
assert _ver("Autodesk AutoCAD LT 2026 - Deutsch (German)") == \
("cpe:autodesk:autocad_lt", "2026.0.0")
# LT and full AutoCAD are separate CPE products.
assert _ver("AutoCAD 2025")[0] == "cpe:autodesk:autocad"
assert _ver("AutoCAD LT 2026")[0] == "cpe:autodesk:autocad_lt"
# No year in the name → no version to scan with; the caller skips it.
assert _ver("AutoCAD LT Private")[1] == ""
# Companions ship alongside and are not the application. Each carries its
# own unrelated version (Autodesk Access 2.21.0.559, CER 7.2.4.927, …).
for helper in ("AutoCAD Open in Desktop", "Autodesk Access", "Autodesk CER",
"Autodesk Genuine Service", "Autodesk Identity Manager"):
assert resolve_product(helper) is None, helper
print("autodesk year OK")
def demo_family():
"""The wider Autodesk family, from the tester's real inventory.
Two AutoCAD LT generations sit on one host, and every product ships
language packs as separate entries with the SAME version as the
application — Navisworks Freedom 2025 has twelve. Each would collect the
product's full CVE set, exactly like Adobe's dictionary pack did.
"""
assert _ver("AutoCAD LT 2022 - Deutsch (German)", "24.1.51.0") == \
("cpe:autodesk:autocad_lt", "2022.0.0")
assert _ver("AutoCAD LT 2025 - Deutsch (German)", "25.0.58.0") == \
("cpe:autodesk:autocad_lt", "2025.0.0")
# The viewers, both blind spots until now. DWG TrueView is built from
# AutoCAD and carries AutoCAD's build numbers — 24.3.119.0 is 2024.1.
assert _ver("Autodesk DWG TrueView 2024 - English", "24.3.119.0") == \
("cpe:autodesk:dwg_trueview", "2024.1")
# Navisworks numbers its builds some other way (22.0.x for the 2025
# release), so no row claims it and the bare year is what it scans with.
assert _ver("Autodesk Navisworks Freedom 2025", "22.0.1411.23") == \
("cpe:autodesk:navisworks_freedom", "2025")
# Language packs and shared components are not the applications.
for other in ("AutoCAD LT 2022 Language Pack - Deutsch (German)",
"Autodesk Navisworks Freedom 2025 - Language Pack Deutsch (German)",
"Autodesk Navisworks Freedom 2025 - English Language Pack",
"Autodesk Navisworks Freedom 2025 - Module linguistique Fran\u00e7ais (French)",
"Autodesk Material Library 2022",
"Autodesk Material Library Base Resolution Image Library 2022",
"Autodesk Single Sign On Component"):
assert resolve_product(other) is None, other
print("autodesk family OK")
def demo_advisory():
"""ADSK-SA-2026-0012 end to end, through the path that actually carries it.
CVE-2026-7405 / -7406 sit at NVD with vulnStatus "Received" and zero
cpeMatch entries, so the CPE path is blind to them by construction —
cvelistV5 is the only source, and it states "AutoCAD LT 2026.0.0 lessThan
2026.1.2". That path resolved no year at all until now: it compared the
raw build 25.1.60.0 against a release range and matched nothing.
"""
from app.services import cvelistv5_scan_service as c5
def scanned(name, version):
key = c5.resolve(name)
entry = c5._KEY_ENTRY.get(key) or {}
return key, _effective_version(name, version, entry)
def affected(version):
# Verbatim from cves/2026/7xxx/CVE-2026-7405.json, product "AutoCAD LT".
return any(c5._affected(version, start, lt, None)
for start, lt in (("2027.0.0", "2027.1.0"),
("2026.0.0", "2026.1.2")))
tester = ("Autodesk AutoCAD LT 2026 - Deutsch (German)", "25.1.60.0")
key, ver = scanned(*tester)
assert key == "autodesk-autocad-lt", key
assert ver == "2026.0.0", ver
assert affected(ver), "the tester's stock 2026 install IS affected"
# …and the update Autodesk names as the mitigation clears it. This is the
# half a bare "2026" can never do.
assert scanned(tester[0], "25.1.172.0")[1] == "2026.1.2"
assert not affected("2026.1.2")
# Every step in between stays affected.
for build, rel in (("25.1.74.0", "2026.0.1"), ("25.1.122.0", "2026.1"),
("25.1.164.0", "2026.1.1")):
assert scanned(tester[0], build)[1] == rel, build
assert affected(rel), build
# A build newer than the table resolves to the newest row it is above —
# over-reporting rather than hiding, deliberately.
assert scanned(tester[0], "25.1.180.0")[1] == "2026.1.2"
# 2027 GA is in the same advisory; 2025 and 2022 are not.
assert affected(scanned("Autodesk AutoCAD LT 2027", "26.0.60.0")[1])
assert not affected(scanned(tester[0].replace("2026", "2025"), "25.0.58.0")[1])
print("autodesk advisory OK")
if __name__ == "__main__":
demo()
demo_family()
demo_advisory()