feat(app-scan): Dell SupportAssist via NVD and cvelistV5
Intune lists "Dell SupportAssist" 3.14.0.91 (Dell Inc.), and nothing matched CVE-2024-52535 (symlink attack in the remediation component). Both sources split it by edition: NVD as dell:supportassist_for_home_pcs < 4.6.2 and dell:supportassist_for_business_pcs < 4.5.1, cvelistV5 as "Dell / SupportAssist for Home PCs" and "... for Business PCs". The editions share version numbers but not bounds, so the name decides: plain "Dell SupportAssist" is Home, Business registers as "Dell SupportAssist for Business PCs" (whole_name, so the companion rule keeps it). Remediation, OS Recovery Plugin and SupportAssistAgent stay out. Dell's newer records write the floor as "N/A". That reads as a version which compares to nothing, so the cvelistV5 half never saw them. For the two SupportAssist keys it is now read as 0, the way NVD reads the same records; other keys keep the word-floor rule. Index key bumped to v60.
This commit is contained in:
@@ -2042,6 +2042,23 @@ say which platform its OpenSSL search path is (macOS not checked yet,
|
||||
FileZilla-style). KeePass Password Safe,
|
||||
KeePassDX and KeePassXC-Browser are not matched.
|
||||
|
||||
**Dell SupportAssist** (`dell:supportassist_for_home_pcs` /
|
||||
`dell:supportassist_for_business_pcs` on NVD, keys `dell-supportassist-home` /
|
||||
`dell-supportassist-business` on cvelistV5). Field case 2026-11-02: Intune
|
||||
lists "Dell SupportAssist" 3.14.0.91 (Dell Inc.) and nothing matched
|
||||
CVE-2024-52535 (Home `< 4.6.2`, Business `< 4.5.1`). The two editions share
|
||||
version numbers (Home 3.0 .. 3.14 then 4.x, Business 3.0 .. 3.5 then 4.x) but
|
||||
not bounds, so the name decides: plain "Dell SupportAssist" is Home, Business
|
||||
registers as "Dell SupportAssist for Business PCs" (uninstaller listings, not
|
||||
yet seen in the field; a Business install under the plain name would be judged
|
||||
by Home bounds). cvelistV5 records say
|
||||
"SupportAssist for Home PCs" (older: "SupportAssist Client Consumer") and
|
||||
"... for Business PCs"; the newer ones write the floor as `"N/A"`, which these
|
||||
two keys read as `0` (NVD reads the same records with no floor). Elsewhere a
|
||||
word floor still compares to nothing. CVE-2022-34389 ("SupportAssist ", both
|
||||
editions' bounds in one string) stays on NVD only. Remediation, OS Recovery
|
||||
Plugin and SupportAssistAgent are not matched.
|
||||
|
||||
**UltraVNC** (`uvnc:ultravnc` on NVD, no cvelistV5 row). Field case
|
||||
2026-10-21: Wazuh and Intune list "UltraVnc" 1.3.8.1 and 1.2.2.2 (uvnc bvba).
|
||||
NVD holds 37 CVEs, all ranges but one: the Kaspersky batch (CVE-2018-15361,
|
||||
|
||||
@@ -116,6 +116,14 @@ GIT_NAME_RE = r"^git(?: for windows)?$"
|
||||
# KeePassXC-Browser extension number their own releases.
|
||||
KEEPASSXC_NAME_RE = r"^keepassxc(?: \d+(?:\.\d+)*)?$"
|
||||
|
||||
# Dell SupportAssist, shared with the cvelistV5 rows. Two editions, overlapping
|
||||
# version lines, separate bounds (CVE-2024-52535: Home < 4.6.2, Business
|
||||
# < 4.5.1): plain "Dell SupportAssist" (3.14.0.91, Dell Inc., in Intune) is the
|
||||
# Home edition, Business registers under its own name. Anchored: Remediation,
|
||||
# OS Recovery Plugin and SupportAssistAgent number their own releases.
|
||||
SUPPORTASSIST_HOME_NAME_RE = r"^dell supportassist(?: for home pcs)?$"
|
||||
SUPPORTASSIST_BUSINESS_NAME_RE = r"^dell supportassist for business pcs$"
|
||||
|
||||
# NVD compares the queried version itself, and reads 5.5.0 as past "up to
|
||||
# (including) 5.5": SoapUI 5.5.0 got no CVE-2019-12180. A CPE listed here
|
||||
# (few CVEs) is asked for without a version, and _in_range judges every entry.
|
||||
@@ -451,6 +459,10 @@ _REGISTRY: List[tuple] = [
|
||||
# NVD analyses the GitHub CNA's records late; cvelistV5 (key "keepassxc")
|
||||
# sees them first. Windows only: a distro "keepassxc" hides backports.
|
||||
_cpe(KEEPASSXC_NAME_RE, "keepassxc:keepassxc"),
|
||||
# Dell SupportAssist, one CPE per edition, see SUPPORTASSIST_HOME_NAME_RE.
|
||||
# whole_name: the companion rule would cut "... for Business PCs".
|
||||
_cpe(SUPPORTASSIST_HOME_NAME_RE, "dell:supportassist_for_home_pcs"),
|
||||
_cpe(SUPPORTASSIST_BUSINESS_NAME_RE, "dell:supportassist_for_business_pcs", whole_name=True),
|
||||
_cpe(r"oracle vm virtualbox|virtualbox", "oracle:vm_virtualbox"),
|
||||
_cpe(r"(?<!\w)gimp(?!\w)", "gimp:gimp"),
|
||||
# .NET (6/7/8/9) and .NET Framework share the SAME NVD CPE
|
||||
|
||||
@@ -44,7 +44,7 @@ _ZIP_PATH = "/tmp/truevuln-cvelistv5-cache.zip"
|
||||
_ZIP_URL = "https://github.com/CVEProject/cvelistV5/archive/refs/heads/main.zip"
|
||||
_ZIP_TTL = 12 * 3600
|
||||
_ZIP_MIN_BYTES = 100_000_000 # sanity floor; the real ZIP is ~557 MB
|
||||
_INDEX_SETTING = "cvelistv5_product_index_v59" # v59: Authenticator for iOS
|
||||
_INDEX_SETTING = "cvelistv5_product_index_v60" # v60: Dell SupportAssist
|
||||
_INDEX_TTL = timedelta(hours=26) # rebuilt nightly; a missed night still serves
|
||||
|
||||
# Curated registry: name-regex (installed software) → cvelistV5 (vendor,
|
||||
@@ -458,6 +458,18 @@ _REGISTRY: List[dict] = [
|
||||
# version ("2.7.11", CVE-2026-4158). Windows hosts only.
|
||||
{"key": "keepassxc", "re": cpe.KEEPASSXC_NAME_RE,
|
||||
"pairs": [("keepassxreboot", "keepassxc"), ("keepassxc", "keepassxc")]},
|
||||
# Dell SupportAssist, same names as the CPE rows, one key per edition.
|
||||
# Dell writes "SupportAssist for Home PCs" (older: "SupportAssist Client
|
||||
# Consumer") and "SupportAssist for Business PCs", bound in `lessThan`
|
||||
# after version "N/A" (CVE-2024-52535). "SupportAssist " with both
|
||||
# editions' bounds in one string (CVE-2022-34389) is not paired.
|
||||
# "whole_name": the companion rule would cut "... for Business PCs".
|
||||
{"key": "dell-supportassist-home", "re": cpe.SUPPORTASSIST_HOME_NAME_RE,
|
||||
"pairs": [("dell", "supportassist for home pcs"),
|
||||
("dell", "supportassist client consumer")]},
|
||||
{"key": "dell-supportassist-business", "re": cpe.SUPPORTASSIST_BUSINESS_NAME_RE,
|
||||
"whole_name": True,
|
||||
"pairs": [("dell", "supportassist for business pcs")]},
|
||||
# LibreOffice. NVD analyses TDF's records late (the 26.2.5 batch,
|
||||
# CVE-2026-63266 ff., sat at "Received" with no CPE), so this is the path
|
||||
# that sees the fresh ones. Each record bounds one series ("26.2" ..
|
||||
@@ -1772,6 +1784,15 @@ def build_product_index(db: Session, force_fresh: bool = False) -> dict:
|
||||
aff = intel_proset_affected(aff)
|
||||
if key in ("veeam-br", "veeam-one", "veeam-agent-windows"):
|
||||
aff = veeam_affected(aff, _KEY_ENTRY[key].get("zero_floor"))
|
||||
if key.startswith("dell-supportassist-"):
|
||||
# Dell writes the open floor as "N/A" (CVE-2024-52535:
|
||||
# "N/A" .. < 4.6.2, "4.6.1 and prior"; NVD: < 4.6.2).
|
||||
# Elsewhere a word floor compares to nothing, see test_dbeaver.
|
||||
aff = dict(aff, versions=[
|
||||
dict(v, version="0") if isinstance(v, dict)
|
||||
and (v.get("status") or "affected") == "affected"
|
||||
and str(v.get("version") or "").strip().upper() == "N/A" else v
|
||||
for v in aff.get("versions") or []])
|
||||
if key == "aruba-switch" or (key == "aruba-os" and any(
|
||||
isinstance(v, dict)
|
||||
and _AOSS_PROSE_RE.search(str(v.get("version") or ""))
|
||||
|
||||
@@ -0,0 +1,210 @@
|
||||
"""Dell SupportAssist — run: venv/bin/python -m pytest tests/test_dell_supportassist.py
|
||||
|
||||
Field case, no row matched:
|
||||
Intune "Dell SupportAssist" 3.14.0.91 (Dell Inc.)
|
||||
|
||||
Dell ships two editions with overlapping version lines: Home PCs (3.0 .. 3.14,
|
||||
then 4.x) and Business PCs (3.0 .. 3.5, then 4.x). The bounds differ per
|
||||
edition (CVE-2024-52535: Home < 4.6.2, Business < 4.5.1), so the name decides:
|
||||
plain "Dell SupportAssist" is the Home edition, the Business edition shows as
|
||||
"Dell SupportAssist for Business PCs".
|
||||
NVD dell:supportassist_for_home_pcs / dell:supportassist_for_business_pcs
|
||||
cvelistV5 "Dell / SupportAssist for Home PCs" (older: "SupportAssist Client
|
||||
Consumer") and "Dell / SupportAssist for Business PCs"
|
||||
|
||||
Fixtures are the real NVD cpeMatch entries and cvelistV5 blocks, trimmed.
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import tempfile
|
||||
import zipfile
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
from app.services import app_cve_scanner_service as cpe # noqa: E402
|
||||
from app.services import cvelistv5_scan_service as c5 # noqa: E402
|
||||
|
||||
HOME = "cpe:2.3:a:dell:supportassist_for_home_pcs"
|
||||
BIZ = "cpe:2.3:a:dell:supportassist_for_business_pcs"
|
||||
NAME = "Dell SupportAssist"
|
||||
BIZ_NAME = "Dell SupportAssist for Business PCs"
|
||||
WIN = "Microsoft Windows 11 Enterprise"
|
||||
|
||||
|
||||
def _m(base, **bounds):
|
||||
return dict(criteria=base + ":*:*:*:*:*:*:*:*", vulnerable=True, **bounds)
|
||||
|
||||
|
||||
def _rec(cve, *matches):
|
||||
return {"cve": {"id": cve, "descriptions": [{"lang": "en", "value": cve}],
|
||||
"configurations": [{"nodes": [{"operator": "OR", "cpeMatch": list(matches)}]}]}}
|
||||
|
||||
|
||||
NVD = {
|
||||
HOME: [
|
||||
_rec("CVE-2023-25535", _m(HOME, versionEndExcluding="3.13.2.19")),
|
||||
_rec("CVE-2023-44283", _m(HOME, versionStartIncluding="3.0", versionEndExcluding="3.14.2"),
|
||||
_m(BIZ, versionStartIncluding="3.0", versionEndExcluding="3.5.0")),
|
||||
_rec("CVE-2024-52535", _m(BIZ, versionEndExcluding="4.5.1"),
|
||||
_m(HOME, versionEndExcluding="4.6.2")),
|
||||
],
|
||||
BIZ: [
|
||||
_rec("CVE-2023-44283", _m(HOME, versionStartIncluding="3.0", versionEndExcluding="3.14.2"),
|
||||
_m(BIZ, versionStartIncluding="3.0", versionEndExcluding="3.5.0")),
|
||||
_rec("CVE-2024-52535", _m(BIZ, versionEndExcluding="4.5.1"),
|
||||
_m(HOME, versionEndExcluding="4.6.2")),
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
class _Asset:
|
||||
id = 1
|
||||
|
||||
def __init__(self, operating_system=WIN):
|
||||
self.operating_system = operating_system
|
||||
|
||||
|
||||
class _Resp:
|
||||
status_code = 200
|
||||
|
||||
def __init__(self, body):
|
||||
self._body = body
|
||||
|
||||
def json(self):
|
||||
return self._body
|
||||
|
||||
|
||||
def _scan(monkeypatch, version, name=NAME):
|
||||
"""The real NVD query and scan; only HTTP and the cache are faked."""
|
||||
asked = []
|
||||
|
||||
class Client:
|
||||
def __init__(self, **kw):
|
||||
pass
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, *a):
|
||||
return False
|
||||
|
||||
def get(self, url, headers=None):
|
||||
vms = url.split("virtualMatchString=")[1].split("&")[0]
|
||||
asked.append(vms)
|
||||
recs = next((r for base, r in NVD.items() if vms.startswith(base + ":")), [])
|
||||
return _Resp({"vulnerabilities": recs, "totalResults": len(recs)})
|
||||
|
||||
got = []
|
||||
monkeypatch.setattr(cpe.httpx, "Client", Client)
|
||||
monkeypatch.setattr(cpe.time, "sleep", lambda s: None)
|
||||
monkeypatch.setattr(cpe, "_cache_get", lambda *a: None)
|
||||
monkeypatch.setattr(cpe, "_cache_put", lambda *a: None)
|
||||
monkeypatch.setattr(cpe, "_upsert", lambda db, asset, n, v, c, new_ids, **kw:
|
||||
got.append((c["cve"], c["fixed"])))
|
||||
cpe.scan_asset_packages(None, _Asset(), [{"name": name, "version": version,
|
||||
"vendor": "Dell Inc."}], touched=set())
|
||||
return sorted(got), asked
|
||||
|
||||
|
||||
def test_resolves_names():
|
||||
for name in (NAME, "dell supportassist", "Dell SupportAssist for Home PCs"):
|
||||
assert (cpe.resolve_product(name) or {}).get("cpe") == HOME, name
|
||||
assert c5.resolve(name) == "dell-supportassist-home", name
|
||||
assert (cpe.resolve_product(BIZ_NAME) or {}).get("cpe") == BIZ
|
||||
assert c5.resolve(BIZ_NAME) == "dell-supportassist-business"
|
||||
# The other SupportAssist parts number their own releases.
|
||||
for other in ("Dell SupportAssist Remediation", "Dell SupportAssist OS Recovery Plugin for Dell Update",
|
||||
"Dell SupportAssistAgent", "SupportAssist Recovery Assistant"):
|
||||
assert (cpe.resolve_product(other) or {}).get("cpe") not in (HOME, BIZ), other
|
||||
assert not (c5.resolve(other) or "").startswith("dell-supportassist"), other
|
||||
|
||||
|
||||
def test_nvd_home(monkeypatch):
|
||||
got, asked = _scan(monkeypatch, "3.14.0.91")
|
||||
assert asked == [HOME + ":3.14.0.91"]
|
||||
# Each CVE with the Home bound, never the Business one.
|
||||
assert got == [("CVE-2023-44283", "3.14.2"), ("CVE-2024-52535", "4.6.2")]
|
||||
# Business 4.5.1 is fixed; Home 4.5.1 is not.
|
||||
assert _scan(monkeypatch, "4.5.1")[0] == [("CVE-2024-52535", "4.6.2")]
|
||||
assert _scan(monkeypatch, "4.6.2")[0] == []
|
||||
|
||||
|
||||
def test_nvd_business(monkeypatch):
|
||||
got, asked = _scan(monkeypatch, "4.5.0", BIZ_NAME)
|
||||
assert asked == [BIZ + ":4.5.0"]
|
||||
assert got == [("CVE-2024-52535", "4.5.1")]
|
||||
assert _scan(monkeypatch, "4.5.1", BIZ_NAME)[0] == []
|
||||
assert _scan(monkeypatch, "3.4.0", BIZ_NAME)[0] == [
|
||||
("CVE-2023-44283", "3.5.0"), ("CVE-2024-52535", "4.5.1")]
|
||||
|
||||
|
||||
def _c5(cve, *affected):
|
||||
return {"cveMetadata": {"cveId": cve}, "containers": {"cna": {"affected": [
|
||||
{"vendor": "Dell", "product": p, "defaultStatus": "unaffected", "versions": [v]}
|
||||
for p, v in affected]}}}
|
||||
|
||||
|
||||
def _lt(bound, start="N/A"):
|
||||
return {"version": start, "lessThan": bound, "versionType": "semver", "status": "affected"}
|
||||
|
||||
|
||||
def _le(bound):
|
||||
return {"version": "0", "lessThanOrEqual": bound, "versionType": "semver", "status": "affected"}
|
||||
|
||||
|
||||
C5 = [
|
||||
_c5("CVE-2024-52535", ("SupportAssist for Home PCs", _lt("4.6.2")),
|
||||
("SupportAssist for Business PCs", _lt("4.5.1"))),
|
||||
_c5("CVE-2023-44283", ("SupportAssist for Home PCs", _le("3.14.1")),
|
||||
("SupportAssist for Business PCs", _le("3.4.1"))),
|
||||
_c5("CVE-2023-25535", ("SupportAssist Client Consumer", _lt("3.13.2.19", "0"))),
|
||||
_c5("CVE-2025-36612", ("SupportAssist for Business PCs", _lt("4.9.0"))),
|
||||
# Home and Business bounds in one string, no edition: not paired.
|
||||
_c5("CVE-2022-34389", ("SupportAssist ", _le("3.11.1, 3.2"))),
|
||||
]
|
||||
|
||||
|
||||
def _build_index(monkeypatch):
|
||||
"""Run the REAL index build over a ZIP shaped like cvelistV5's."""
|
||||
fd, path = tempfile.mkstemp(suffix=".zip")
|
||||
os.close(fd)
|
||||
with zipfile.ZipFile(path, "w") as zf:
|
||||
for rec in C5:
|
||||
cve = rec["cveMetadata"]["cveId"]
|
||||
zf.writestr(f"cvelistV5-main/cves/{cve[4:8]}/x/{cve}.json", json.dumps(rec))
|
||||
monkeypatch.setattr(c5, "_ZIP_PATH", path)
|
||||
monkeypatch.setattr(c5, "_ensure_zip", lambda force=False: True)
|
||||
monkeypatch.setattr(c5, "_store_index", lambda db, index: None)
|
||||
for merge in ("_merge_mozilla_mfsa", "_merge_chrome_releases", "_merge_zoom_bulletins"):
|
||||
monkeypatch.setattr(c5, merge, lambda *a, **kw: None)
|
||||
try:
|
||||
return c5.build_product_index(None)
|
||||
finally:
|
||||
os.unlink(path)
|
||||
|
||||
|
||||
def _scan_c5(monkeypatch, index, version, name=NAME):
|
||||
"""Run the REAL scan_asset, capturing what it would upsert."""
|
||||
found = []
|
||||
monkeypatch.setattr(cpe, "_upsert", lambda db, asset, n, v, c, new_ids, **kw: (
|
||||
found.append((c["cve"], c["fixed"])), new_ids.append(len(new_ids))))
|
||||
c5.scan_asset(None, _Asset(), [{"name": name, "version": version, "vendor": "Dell Inc."}],
|
||||
index, touched=set())
|
||||
return sorted(found)
|
||||
|
||||
|
||||
def test_cvelistv5(monkeypatch):
|
||||
index = _build_index(monkeypatch)
|
||||
assert {e["cve"] for e in index["dell-supportassist-home"]} == {
|
||||
"CVE-2024-52535", "CVE-2023-44283", "CVE-2023-25535"}
|
||||
assert {e["cve"] for e in index["dell-supportassist-business"]} == {
|
||||
"CVE-2024-52535", "CVE-2023-44283", "CVE-2025-36612"}
|
||||
# The field case: Home bounds only.
|
||||
assert _scan_c5(monkeypatch, index, "3.14.0.91") == [
|
||||
("CVE-2023-44283", ">3.14.1"), ("CVE-2024-52535", "4.6.2")]
|
||||
assert _scan_c5(monkeypatch, index, "4.5.1") == [("CVE-2024-52535", "4.6.2")]
|
||||
assert _scan_c5(monkeypatch, index, "4.6.2") == []
|
||||
assert _scan_c5(monkeypatch, index, "4.5.0", BIZ_NAME) == [
|
||||
("CVE-2024-52535", "4.5.1"), ("CVE-2025-36612", "4.9.0")]
|
||||
assert _scan_c5(monkeypatch, index, "4.5.1", BIZ_NAME) == [("CVE-2025-36612", "4.9.0")]
|
||||
Reference in New Issue
Block a user