fix(firefox): take the CVE list from Mozilla's MFSA, not cvelistV5 ranges

Of the 23 CVEs in mfsa2026-74 (announced 18.08.) TrueVuln had found two on
20.08. Not a cache problem — the yml was fetched 19.08. 15:39 — the MFSA index
was only ever used to fill in severity and description, never to find a CVE.

Detection came from cvelistV5, which states Firefox INVERSELY: no affected
range, only one "unaffected" floor per maintained train (115.39 / 140.14 /
153.1 / 154). Nothing in the record marks which floor is the release train and
which are ESR, so _ranges_from_affected only inverts the single-floor shape —
anything else is skipped rather than flag a regular Firefox 121 for an ESR-only
advisory. CVE-2026-74975 and -74989 have one floor; the other 21 have two to
four and produced no range at all. Whether such a CVE was found then came down
to NVD having published a CPE for it, which is why two assets on the same
Firefox build reported different CVE sets.

Mozilla's own advisory says it in one line — fixed_in: [Firefox 154] — so the
MFSA index now decides the Firefox ranges and cvelistV5 keeps everything else:

- mozilla_advisory_service: derive ff_fix (mainline, non-ESR/iOS/Thunderbird)
  and esr_fix per CVE; merge fixed_in across advisories instead of letting the
  first file listed win, so the regular-vs-ESR verdict no longer depends on
  alphabetical order; parse the indented "  - Firefox 137.0.2" form (5 of 178
  advisories, previously an empty fixed_in); never overwrite a good index with
  an empty one and serve it stale when a rebuild fails.
- cvelistv5_scan_service: _merge_mozilla_mfsa replaces the Firefox entries for
  every CVE Mozilla has ruled on, drops the ESR/iOS/Thunderbird-only ones, and
  spares an install sitting at or above its own ESR train's fix. Index setting
  bumped to v28 so the cache rebuilds.

Verified against all 178 announce/2025+2026 advisories and the live cvelistV5
records: all 23 mfsa2026-74 CVEs now resolve to "below 154", identically for
153.0.4 and 147.0.4. Regression test: tests/test_firefox_mfsa_source.py
This commit is contained in:
2026-08-21 08:01:53 +02:00
parent 1b1ff11ca1
commit a099fef3b8
3 changed files with 333 additions and 22 deletions
+96 -1
View File
@@ -42,7 +42,7 @@ logger = logging.getLogger(__name__)
_ZIP_PATH = "/tmp/truevuln-cvelistv5-cache.zip"
_ZIP_URL = "https://github.com/CVEProject/cvelistV5/archive/refs/heads/main.zip"
_ZIP_TTL = 12 * 3600
_INDEX_SETTING = "cvelistv5_product_index_v27" # v27: ESXi + vCenter
_INDEX_SETTING = "cvelistv5_product_index_v28" # v28: Firefox from MFSA
_INDEX_TTL = timedelta(hours=26) # rebuilt nightly; a missed night still serves
# Curated registry: name-regex (installed software) → cvelistV5 (vendor,
@@ -836,6 +836,31 @@ def _affected(installed: str, start: Optional[str], lt: Optional[str],
return False
def _esr_patched(installed: str, esr_fixes: List[str]) -> bool:
"""True when the install sits on an ESR train at or above THAT train's fix.
An ESR build is below the mainline fix by construction — 140.14 < 154 — so
the mainline bound alone would flag a fully patched ESR host with every CVE
of every release since 140. The install is only spared when its own major
train is one Mozilla named: 153.0.4 (mainline) against ESR 153.1 is still
affected, 153.1.0 (ESR) is not.
resolve() already refuses installs whose NAME says ESR; this catches the
ones where it does not (Intune / Defender report the app as plain
"Firefox").
"""
it = cpe._vtuple(installed)
if not it:
return False
for f in esr_fixes:
ft = cpe._vtuple(f)
if ft and it[0] == ft[0]:
c = cpe._vcmp(installed, f)
if c is not None and c >= 0:
return True
return False
# ---------- index build + cache ----------
def _ensure_zip(force: bool = False) -> bool:
"""Make sure the shared cvelistV5 ZIP is on disk.
@@ -892,6 +917,7 @@ def build_product_index(db: Session, force_fresh: bool = False) -> dict:
if not _ensure_zip(force=force_fresh):
return load_index(db) or {}
index: Dict[str, list] = {}
ff_meta: Dict[str, dict] = {}
seen: set = set()
scanned = 0
parsed = 0
@@ -935,6 +961,10 @@ def build_product_index(db: Session, force_fresh: bool = False) -> dict:
continue
plats = [str(p).lower().strip() for p in (aff.get("platforms") or [])]
prod = (aff.get("product") or "").strip()
if key == "firefox" and cve_id not in ff_meta:
# Score + text for the MFSA merge below, taken while the
# record is already open (Mozilla's yml carries neither).
ff_meta[cve_id] = {"cvss": cvss, "sev": sev, "desc": desc}
if key == "sap":
for _v in (aff.get("versions") or []):
if not isinstance(_v, dict):
@@ -994,12 +1024,75 @@ def build_product_index(db: Session, force_fresh: bool = False) -> dict:
{"cve": cve_id, "start": start, "lt": lt, "lte": lte,
"plats": plats, "cvss": cvss, "sev": sev, "prod": prod,
"desc": desc})
_merge_mozilla_mfsa(db, index, ff_meta)
_store_index(db, index)
logger.info("cvelistv5-scan: index built (%d files scanned, %d parsed) → %d products, %d ranges",
scanned, parsed, len(index), sum(len(v) for v in index.values()))
return index
def _merge_mozilla_mfsa(db: Session, index: Dict[str, list],
ff_meta: Dict[str, dict]) -> None:
"""Let Mozilla's own MFSA advisories decide the Firefox ranges.
cvelistV5 states Firefox INVERSELY — no affected range at all, only a list
of "unaffected" floors, one per maintained train. mfsa2026-74 lands as
unaffected 115.39 lte 115.* ESR 115
unaffected 140.14 lte 140.* ESR 140
unaffected 153.1 lte 153.* ESR 153
unaffected 154 lte * release
and _ranges_from_affected can only invert that when there is exactly ONE
floor, because nothing in the record says which of the four is the release
train and which are ESR (read naively, "below 140.14" flags a regular
Firefox 121 for an ESR-only advisory). So every multi-train record — 21 of
the 23 CVEs in mfsa2026-74 — produced no range and was never indexed. The
two that were are exactly the two the tester saw arrive (CVE-2026-74975,
-74989); the rest depended on NVD publishing a CPE, which is why coverage
looked random from one CVE to the next.
The MFSA yml answers it outright: one advisory, one `fixed_in` train.
"Firefox 154" → every mainline Firefox below 154 is affected by every CVE
the advisory lists. "Firefox ESR 140.14" / "Firefox for iOS 152.4" /
"Thunderbird 154" → regular desktop Firefox is not affected at all, so the
CVE is REMOVED from the Firefox index (this also retires the false-positive
class the single-floor inversion could produce for ESR-only advisories).
Mozilla wins for every CVE it has ruled on; anything it has not (older CVEs,
other CNAs) keeps whatever cvelistV5 derived. No MFSA index (offline, rate
limited) → the index is left exactly as it was.
"""
try:
from app.services import mozilla_advisory_service as mfsa
moz = mfsa.get_index(db)
except Exception as e:
logger.warning("cvelistv5-scan: MFSA merge skipped (%s) — Firefox falls "
"back to cvelistV5 ranges", e)
return
if not moz:
logger.warning("cvelistv5-scan: MFSA index empty — Firefox falls back to "
"cvelistV5 ranges")
return
before = index.get("firefox") or []
kept = [e for e in before if e.get("cve") not in moz]
added = 0
for cve_id, data in sorted(moz.items()):
fix = data.get("ff_fix")
if not fix:
continue # ESR / iOS / Thunderbird only → not a desktop Firefox CVE
meta = ff_meta.get(cve_id) or {}
kept.append({"cve": cve_id, "start": None, "lt": fix, "lte": None,
"plats": [], "cvss": meta.get("cvss"),
"sev": meta.get("sev") or data.get("sev"),
"prod": "Firefox", "esr": data.get("esr_fix") or [],
"desc": meta.get("desc") or data.get("title")})
added += 1
index["firefox"] = kept
logger.info("cvelistv5-scan: MFSA decided %d Firefox CVEs (was %d cvelistV5 "
"ranges, %d kept as-is)", added, len(before), len(kept) - added)
def _store_index(db: Session, index: dict) -> None:
from app.models.setting import Setting
payload = json.dumps({"built_at": datetime.now().isoformat(), "index": index})
@@ -1613,6 +1706,8 @@ def scan_asset(db: Session, asset, packages: list, index: dict,
if not _affected(cver, entry.get("start"), entry.get("lt"),
entry.get("lte"), scheme_strict=strict):
continue
if entry.get("esr") and _esr_patched(cver, entry["esr"]):
continue # patched on its own ESR train (Firefox only)
if not _platform_ok(fam, entry.get("plats")):
continue # CVE is for a different OS platform (e.g. Teams-for-Mac)
# Only lessThan is a real fix target; lessThanOrEqual means that
+105 -21
View File
@@ -9,9 +9,15 @@ SEVERITY + description source, not a CVSS source. It fills the gap where fresh
Firefox CVEs have no score yet in NVD/cvelistV5.
`fixed_in` is the AUTHORITATIVE regular-vs-ESR discriminator (an advisory whose
fixed_in lists only "Firefox ESR …" does not affect regular Firefox) — stored
here for the Firefox-scan ESR exclusion, cf. [[ghsa-unreviewed-no-version-range]]
sibling reference on why cvelistV5 alone can't tell them apart.
fixed_in lists only "Firefox ESR …" does not affect regular Firefox), and since
20.08.2026 it is also the PRIMARY Firefox detection source: cvelistv5_scan_
service asks this index for the mainline fix version of every Firefox CVE and
indexes "below that → affected". cvelistV5 states Firefox inversely, one
"unaffected" floor per maintained train, and cannot say which floor is the
release train — so 21 of the 23 CVEs in mfsa2026-74 produced no range at all
and were never found. See _merge_mozilla_mfsa there, and
[[ghsa-unreviewed-no-version-range]] on why cvelistV5 alone can't tell the
trains apart.
The parser is deliberately line-based (no PyYAML dependency): the MFSA schema is
regular — top-level `fixed_in:` list, then an `advisories:` map of
@@ -45,7 +51,11 @@ _IMPACT_TO_SEV = {
_CVE_KEY = re.compile(r"^ (CVE-\d{4}-\d+):\s*$")
_IMPACT = re.compile(r"^ impact:\s*([A-Za-z]+)")
_TITLE = re.compile(r"^ title:\s*(.+?)\s*$")
_LIST_ITEM = re.compile(r"^-\s*(.+?)\s*$")
# Mozilla writes the fixed_in list both flush-left and indented two spaces
# (" - Firefox 137.0.2"); 5 of the 178 2025/2026 advisories use the indented
# form. Anchoring at column 0 silently produced an EMPTY fixed_in for those —
# no train, so no regular-vs-ESR verdict and no Firefox fix version at all.
_LIST_ITEM = re.compile(r"^\s*-\s*(.+?)\s*$")
def _gh_headers(db: Session) -> dict:
@@ -70,6 +80,42 @@ def _is_esr_only(fixed_in: List[str]) -> bool:
return all("esr" in f.lower() for f in ff)
# "Firefox 154", "Firefox 153.0.3" → mainline. Deliberately anchored so the
# other three shapes Mozilla writes into `fixed_in` are NOT mainline desktop:
# "Firefox ESR 140.14" (own train, own fix), "Firefox for iOS 152.4" (WebKit —
# different codebase, different CVEs) and "Thunderbird 154" (different product).
# Verified against all 178 announce/2025+2026 advisories: those four forms plus
# "Focus/Klar for iOS" are the only ones that occur, one entry per advisory.
_FF_MAINLINE = re.compile(r"^firefox\s+(\d+(?:\.\d+)*)$", re.I)
_FF_ESR = re.compile(r"^firefox\s+esr\s+(\d+(?:\.\d+)*)$", re.I)
def _esr_firefox_fixes(fixed_in: List[str]) -> List[str]:
"""The ESR builds that also carry the fix, e.g. ["115.39", "140.14",
"153.1"]. Used to spare an ESR install the mainline verdict: 140.14 is
below the mainline fix 154 but is not vulnerable — see _esr_patched in the
scanner. Needed because an inventory does not always spell out "ESR"
(Intune/Defender report the app as plain "Firefox")."""
return [m.group(1) for m in
(_FF_ESR.match((f or "").strip()) for f in fixed_in or []) if m]
def _mainline_firefox_fix(fixed_in: List[str]) -> Optional[str]:
"""The mainline (non-ESR, non-iOS) Firefox release that carries the fix, or
None when the advisory only ships an ESR / Thunderbird / iOS build — in
which case regular desktop Firefox is not affected at all.
Highest wins: a CVE listed in two mainline advisories is only fixed once
the later one is installed."""
vs = [m.group(1) for m in
(_FF_MAINLINE.match((f or "").strip()) for f in fixed_in or []) if m]
if not vs:
return None
return max(vs, key=lambda v: tuple(int(x) for x in v.split(".")))
def _parse_yml(text: str) -> Tuple[List[str], Dict[str, dict]]:
"""Line-parse one MFSA yml → (fixed_in list, {cve: {impact, title}})."""
fixed_in: List[str] = []
@@ -144,23 +190,41 @@ def build_index(db: Session, years: Optional[List[int]] = None) -> Dict[str, dic
if rr.status_code != 200:
continue
fixed_in, cves = _parse_yml(rr.text)
esr_only = _is_esr_only(fixed_in)
for cve_id, data in cves.items():
sev = _IMPACT_TO_SEV.get(data.get("impact") or "")
# First writer wins per CVE (a CVE can appear in several
# MFSAs for different products; the Firefox one is fine).
if cve_id not in index:
index[cve_id] = {
"sev": sev,
"title": data.get("title"),
"fixed_in": fixed_in,
"esr_only": esr_only,
}
# MERGE across advisories, don't let the first writer
# win. One flaw is announced once per product train —
# CVE-2026-74934 is in mfsa2026-74 (Firefox 154) AND
# mfsa2026-75 (Firefox ESR 115.39). Keeping only the
# first made the regular-vs-ESR verdict depend on the
# alphabetical order of the file listing, so a CVE that
# happened to be seen in its ESR advisory first looked
# ESR-only and was dropped from the Firefox scan.
e = index.setdefault(cve_id, {"sev": None, "title": None,
"fixed_in": []})
if e["sev"] is None:
e["sev"] = sev
if not e["title"]:
e["title"] = data.get("title")
for fx in fixed_in:
if fx not in e["fixed_in"]:
e["fixed_in"].append(fx)
except Exception as e:
logger.debug("MFSA: parse %s failed: %s", f.get("name"), e)
_store(db, index)
logger.info("MFSA index built: %d CVEs across years %s", len(index), years)
for entry in index.values():
entry["esr_only"] = _is_esr_only(entry["fixed_in"])
entry["ff_fix"] = _mainline_firefox_fix(entry["fixed_in"])
entry["esr_fix"] = _esr_firefox_fixes(entry["fixed_in"])
if index:
_store(db, index)
logger.info("MFSA index built: %d CVEs across years %s", len(index), years)
else:
# A rate-limited or offline build must not overwrite a good index with
# {} and then stamp it fresh for 24h — that blanks Firefox detection
# for a whole day. Leave the cache alone; get_index serves it stale.
logger.warning("MFSA index build returned nothing — keeping the cached index")
return index
@@ -175,13 +239,14 @@ def _store(db: Session, index: Dict[str, dict]) -> None:
db.commit()
def load_index(db: Session) -> Optional[Dict[str, dict]]:
def load_index(db: Session, allow_stale: bool = False) -> Optional[Dict[str, dict]]:
ts = db.query(Setting).filter(Setting.key == INDEX_TS_SETTING).first()
row = db.query(Setting).filter(Setting.key == INDEX_SETTING).first()
if not ts or not row or not row.value:
return None
try:
if datetime.now() - datetime.fromisoformat(ts.value) > timedelta(hours=TTL_HOURS):
if (not allow_stale
and datetime.now() - datetime.fromisoformat(ts.value) > timedelta(hours=TTL_HOURS)):
return None
return json.loads(row.value)
except (ValueError, json.JSONDecodeError):
@@ -189,15 +254,21 @@ def load_index(db: Session) -> Optional[Dict[str, dict]]:
def get_index(db: Session) -> Dict[str, dict]:
"""Cached index, lazily (re)built when absent/stale. Build failure → {}."""
"""Cached index (TTL 24h), lazily (re)built when absent/stale.
A failed rebuild falls back to the STALE cache rather than to {}: yesterday's
Firefox advisories are a far better answer than none, and {} silently turns
the Firefox scan off."""
idx = load_index(db)
if idx is not None:
return idx
try:
return build_index(db)
built = build_index(db)
if built:
return built
except Exception as e:
logger.warning("MFSA index build failed: %s", e)
return {}
return load_index(db, allow_stale=True) or {}
def apply_mozilla_severity(db: Session, cve_ids: List[str]) -> int:
@@ -270,4 +341,17 @@ advisories:
assert _is_esr_only(["Firefox ESR 115.13"]) is True # ESR only
assert _is_esr_only(["Thunderbird 128"]) is False # no firefox
assert _IMPACT_TO_SEV["moderate"] == "medium"
# The mainline-vs-ESR verdict the Firefox scan hangs on.
assert _mainline_firefox_fix(["Firefox 154"]) == "154"
assert _mainline_firefox_fix(["Firefox 153.0.3"]) == "153.0.3"
assert _mainline_firefox_fix(["Firefox ESR 140.14"]) is None # ESR train
assert _mainline_firefox_fix(["Firefox for iOS 152.4"]) is None # WebKit
assert _mainline_firefox_fix(["Thunderbird 154"]) is None # other product
# Merged across advisories: the mainline fix must survive the ESR entry.
assert _mainline_firefox_fix(["Firefox ESR 115.39", "Firefox 154"]) == "154"
assert _mainline_firefox_fix(["Firefox 153", "Firefox 154"]) == "154" # highest wins
assert _esr_firefox_fixes(["Firefox 154", "Firefox ESR 140.14",
"Firefox ESR 153.1", "Thunderbird 154"]) == ["140.14", "153.1"]
# Indented fixed_in list — 5 of the 178 2025/2026 advisories write it this way.
assert _parse_yml("fixed_in:\n - Firefox 137.0.2\ntitle: x\n")[0] == ["Firefox 137.0.2"]
print("mozilla_advisory_service self-check OK")
+132
View File
@@ -0,0 +1,132 @@
"""Firefox CVEs come from Mozilla's MFSA — run: python tests/test_firefox_mfsa_source.py
Why this exists (tester, 20.08.2026): of the 23 CVEs in mfsa2026-74 (announced
18.08.), TrueVuln had found two. Not a cache problem — the MFSA yml was fetched
on 19.08. 15:39 — but a parsing one, in the OTHER source:
cvelistV5 states Firefox inversely. There is no affected range at all, only a
list of "unaffected" floors, one per maintained train:
CVE-2026-74990 115.39 lte 115.* | 140.14 lte 140.* | 153.1 lte 153.* | 154 lte *
CVE-2026-74989 154 lte *
Nothing in the record marks which floor is the release train and which are ESR,
so `_ranges_from_affected` only inverts the single-floor shape — anything else
is skipped rather than risk flagging a regular Firefox 121 for an ESR-only
advisory. CVE-2026-74975 and -74989 have one floor; the other 21 have two to
four and produced no range at all. Whether such a CVE was found at all then
came down to NVD having published a CPE for it, which is why two assets on the
SAME Firefox build reported different CVE sets.
Mozilla's own advisory says it in one line — `fixed_in: [Firefox 154]` — so the
MFSA index now decides the Firefox ranges, and cvelistV5 keeps everything else.
"""
import os
import sys
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from app.services import mozilla_advisory_service as mfsa
from app.services import cvelistv5_scan_service as c5
# ---- verbatim from cvelistV5 (fetched 21.08.2026) --------------------------
MULTI_TRAIN = { # CVE-2026-74990 and 20 siblings in mfsa2026-74
"vendor": "Mozilla", "product": "Firefox", "versions": [
{"status": "unaffected", "version": "115.39", "lessThanOrEqual": "115.*"},
{"status": "unaffected", "version": "140.14", "lessThanOrEqual": "140.*"},
{"status": "unaffected", "version": "153.1", "lessThanOrEqual": "153.*"},
{"status": "unaffected", "version": "154", "lessThanOrEqual": "*"}]}
SINGLE_TRAIN = { # CVE-2026-74989 — one of the two that DID arrive
"vendor": "Mozilla", "product": "Firefox", "versions": [
{"status": "unaffected", "version": "154", "lessThanOrEqual": "*"}]}
ESR_ONLY = { # CVE-2026-16361, mfsa2026-70 (Firefox ESR 140.13) — no mainline fix
"vendor": "Mozilla", "product": "Firefox", "versions": [
{"status": "unaffected", "version": "115.38", "lessThanOrEqual": "115.*"},
{"status": "unaffected", "version": "140.13", "lessThanOrEqual": "*"}]}
# ---- verbatim from the MFSA ymls ------------------------------------------
MFSA_INDEX = {
# mfsa2026-74 "Firefox 154" + mfsa2026-75 "Firefox ESR 115.39" (merged)
"CVE-2026-74990": {"sev": "high", "title": "Memory safety bugs",
"fixed_in": ["Firefox 154", "Firefox ESR 115.39"],
"ff_fix": "154"},
"CVE-2026-74989": {"sev": "medium", "title": "Memory safety bugs",
"fixed_in": ["Firefox 154"], "ff_fix": "154"},
# mfsa2026-70 — ESR only, so regular Firefox is not affected
"CVE-2026-16361": {"sev": "critical", "title": "Invalid pointer",
"fixed_in": ["Firefox ESR 140.13"], "ff_fix": None},
}
def _hits(aff, version):
return any(c5._affected(version, s, lt, lte)
for s, lt, lte in c5._ranges_from_affected(aff))
def _index_after_merge(monkeypatched_moz, firefox_entries):
"""Run the merge with a stubbed MFSA index (no network, no DB)."""
real = mfsa.get_index
mfsa.get_index = lambda db: monkeypatched_moz
try:
index = {"firefox": list(firefox_entries)}
c5._merge_mozilla_mfsa(None, index, {})
return index["firefox"]
finally:
mfsa.get_index = real
def demo():
# --- the cause, still true: cvelistV5 alone cannot bound a multi-train record
assert c5._ranges_from_affected(MULTI_TRAIN) == [], "must stay unbounded"
assert c5._ranges_from_affected(ESR_ONLY) == []
assert c5._ranges_from_affected(SINGLE_TRAIN) == [(None, "154", None)]
assert _hits(SINGLE_TRAIN, "153.0.4") and _hits(SINGLE_TRAIN, "147.0.4")
# --- mainline fix extraction, the whole regular-vs-ESR decision
assert mfsa._mainline_firefox_fix(["Firefox 154"]) == "154"
assert mfsa._mainline_firefox_fix(["Firefox ESR 140.13"]) is None
assert mfsa._mainline_firefox_fix(["Firefox for iOS 152.4"]) is None
assert mfsa._mainline_firefox_fix(["Thunderbird 154"]) is None
# --- after the merge: every mfsa2026-74 CVE is indexed with lt=154
entries = _index_after_merge(MFSA_INDEX,
[{"cve": "CVE-2026-74989", "start": None,
"lt": "154", "lte": None, "plats": []}])
by_cve = {e["cve"]: e for e in entries}
assert set(by_cve) == {"CVE-2026-74990", "CVE-2026-74989"}, by_cve
for cve in ("CVE-2026-74990", "CVE-2026-74989"):
assert by_cve[cve]["lt"] == "154"
# Same verdict on both installed builds the tester compared — this is
# what makes two assets on one version report one identical CVE set.
for ver in ("153.0.4", "147.0.4"):
assert c5._affected(ver, None, "154", None), (cve, ver)
# …and a patched build is not flagged.
assert not c5._affected("154.0", None, "154", None)
assert not c5._affected("154.0.1", None, "154", None)
# --- an ESR build is not flagged for a mainline fix it already carries
assert c5._esr_patched("140.14.0", ["115.39", "140.14", "153.1"]) # ESR, patched
assert c5._esr_patched("153.1.0", ["153.1"]) # ESR, patched
assert not c5._esr_patched("153.0.4", ["153.1"]) # mainline 153 → affected
assert not c5._esr_patched("140.0.4", ["140.14"]) # old 140 → affected
assert not c5._esr_patched("147.0.4", ["115.39", "140.14", "153.1"]) # no ESR train
# --- ESR-only CVE is REMOVED, even if cvelistV5 had guessed a range for it
stale = [{"cve": "CVE-2026-16361", "start": None, "lt": "140.13",
"lte": None, "plats": []}]
after = {e["cve"] for e in _index_after_merge(MFSA_INDEX, stale)}
assert "CVE-2026-16361" not in after, "ESR-only CVE must not reach desktop Firefox"
# --- a CVE Mozilla has NOT ruled on keeps its cvelistV5 range untouched
older = [{"cve": "CVE-2019-11708", "start": None, "lt": "67.0.4",
"lte": None, "plats": []}]
assert _index_after_merge(MFSA_INDEX, older)[0]["cve"] == "CVE-2019-11708"
# --- no MFSA index (offline / rate limited) → nothing is touched
assert _index_after_merge({}, older) == older
print("firefox MFSA source self-check OK")
if __name__ == "__main__":
demo()