On 29.10.2026 at 04:34 the remediation catch-up made one call to /cvrf/v3.0/updates, got 429 Too Many Requests and aborted, although both documents it needed were cached by the 01:30 index build. MSRC documents no quota, offers no API key (v3.0 is anonymous) and sends neither Retry-After nor rate-limit headers, so the throttling has to be absorbed on our side. - Every CVRF call goes through _get. It retries 429, 5xx and network errors after 20/60/180 s (or Retry-After), and other 4xx raise at once. MsrcUnavailable stops both document walks instead of hammering on. - A failed /updates falls back to the last listing that worked. Documents never leave /updates, so an index built from it cannot lose a month. - CVRF documents stay cached per revision for 6 days instead of 4 h. The 01:30 build downloaded all 18 documents (20-30 MB each, uncompressed) every night. Now it only downloads the revised ones. - The on-demand Update Guide call no longer caches a 429/5xx as "CVE not in MSRC" until restart. - Failed documents in the ingest and scan errors are logged as warnings; every caller dropped them from its log line.
197 lines
7.4 KiB
Python
197 lines
7.4 KiB
Python
"""MSRC Edge findings must not flip open/patched, and only Edge's newest build
|
|
counts when the inventory lists it several times.
|
|
|
|
Run: python tests/test_msrc_edge_flap.py
|
|
|
|
Field reports 09./10.10.2026:
|
|
1. Wazuh-only Server 2016, one Edge 152.0.4191.62, fix 154.0.4258.37. At
|
|
09.10. 12:08:03, 15 MSRC-only Edge CVEs went open → patched with "MSRC scan:
|
|
… is now at or past the fixed build", then patched → open with "MSRC
|
|
fixed-build scan reports this CVE again". build_product_index skipped a
|
|
CVRF document that failed to load and stored the rest, and the package
|
|
reconcile read every CVE missing from that index as fixed.
|
|
2. Intune-only: "Microsoft Edge" 154.0.4258.62 + 152.0.4191.53 +
|
|
"Microsoft.MicrosoftEdge.Stable" 152.0.4191.53. Intune's app inventory
|
|
lists the device-wide Edge at 154.x; the 152 entries are stale per-user
|
|
registrations, and the scan flagged the 152 one.
|
|
3. Intune-only: "Microsoft Edge" 153.0.4234.32 (stale) next to the MSIX
|
|
"Microsoft.MicrosoftEdge.Stable" 154.0.4258.37, the real build.
|
|
"""
|
|
import json
|
|
import os
|
|
import sys
|
|
import types
|
|
|
|
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
os.environ.setdefault("JWT_SECRET_KEY", "test-key-test-key-test-key-test-key")
|
|
|
|
for _name in ("lxml", "lxml.html"):
|
|
sys.modules.setdefault(_name, types.ModuleType(_name))
|
|
sys.modules["lxml"].html = sys.modules["lxml.html"]
|
|
|
|
import httpx # noqa: E402
|
|
from sqlalchemy import create_engine # noqa: E402
|
|
from sqlalchemy.orm import sessionmaker # noqa: E402
|
|
|
|
import app.models # noqa: F401,E402
|
|
from app.models.base import Base # noqa: E402
|
|
from app.models.asset import Asset, AssetSource, AssetStatus # noqa: E402
|
|
from app.models.vulnerability import Vulnerability # noqa: E402
|
|
from app.services import msrc_scan_service as m, msrc_service # noqa: E402
|
|
|
|
CVE, FIX = "CVE-2026-95385", "154.0.4258.37"
|
|
OCT = {"ProductTree": {"FullProductName": [
|
|
{"ProductID": "11655", "Value": "Microsoft Edge (Chromium-based)"}]},
|
|
"Vulnerability": [{"CVE": CVE, "Remediations": [
|
|
{"Description": {"Value": "Release Notes"}, "ProductID": ["11655"],
|
|
"Type": 2, "FixedBuild": FIX}]}]}
|
|
FAIL = set()
|
|
|
|
|
|
class _Resp:
|
|
status_code = 200
|
|
text = ""
|
|
|
|
def __init__(self, payload):
|
|
self._p = payload
|
|
|
|
def raise_for_status(self):
|
|
pass
|
|
|
|
def json(self):
|
|
return self._p
|
|
|
|
|
|
class _FakeHttpx:
|
|
def __init__(self, *a, **k):
|
|
pass
|
|
|
|
def __enter__(self):
|
|
return self
|
|
|
|
def __exit__(self, *a):
|
|
pass
|
|
|
|
def get(self, url, headers=None, **k):
|
|
if url.endswith("/updates"):
|
|
return _Resp({"value": [{"ID": "2026-Sep"}, {"ID": "2026-Oct"}]})
|
|
if any(url.endswith(f"/cvrf/{d}") for d in FAIL):
|
|
raise httpx.ReadTimeout("timed out")
|
|
if url.endswith("/cvrf/2026-Oct"):
|
|
return _Resp(OCT)
|
|
return _Resp({"ProductTree": {"FullProductName": []}, "Vulnerability": []})
|
|
|
|
|
|
def _edge(version, name="Microsoft Edge"):
|
|
return {"name": name, "version": version, "vendor": "Microsoft Corporation"}
|
|
|
|
|
|
def demo():
|
|
engine = create_engine("sqlite://")
|
|
Base.metadata.create_all(engine)
|
|
db = sessionmaker(bind=engine)()
|
|
|
|
def host(name):
|
|
a = Asset(hostname=name, operating_system="Microsoft Windows Server 2016 Standard",
|
|
os_version="10.0.14393.9418", source=AssetSource.WAZUH,
|
|
wazuh_agent_id=name, status=AssetStatus.ACTIVE)
|
|
db.add(a)
|
|
db.flush()
|
|
return a
|
|
|
|
def app_scan(a, packages, index):
|
|
"""What the app scan's MSRC package pass does per asset."""
|
|
touched = set()
|
|
m.scan_asset_packages(db, a, packages, index, [], touched=touched)
|
|
m.resolve_stale_packages(db, a, touched, packages)
|
|
db.commit()
|
|
|
|
def status(a):
|
|
v = (db.query(Vulnerability)
|
|
.filter(Vulnerability.asset_id == a.id, Vulnerability.cve_id == CVE)
|
|
.first())
|
|
return v.status.value if v else None
|
|
|
|
old_client, httpx.Client = httpx.Client, _FakeHttpx
|
|
old_waits, msrc_service._RETRY_WAITS = msrc_service._RETRY_WAITS, (0, 0, 0) # the timeout is retried
|
|
try:
|
|
full = m.build_product_index(db, months_back=2)
|
|
assert [e["cve"] for e in full["edge"]] == [CVE], full
|
|
|
|
# ---- 1. a document that fails to load never becomes the index ----
|
|
FAIL.add("2026-Oct")
|
|
try:
|
|
m.build_product_index(db, months_back=2)
|
|
raise AssertionError("a partial index build must raise")
|
|
except RuntimeError as e:
|
|
assert "2026-Oct" in str(e), e
|
|
finally:
|
|
FAIL.clear()
|
|
assert m.load_index(db) == full, "the complete cached index is kept"
|
|
finally:
|
|
httpx.Client = old_client
|
|
msrc_service._RETRY_WAITS = old_waits
|
|
|
|
srv = host("srv-2016")
|
|
pkgs = [_edge("152.0.4191.62")]
|
|
app_scan(srv, pkgs, full)
|
|
assert status(srv) == "open"
|
|
# Even an index that lacks the CVE (fetched fine, MSRC-side gap) must not
|
|
# close it while the host is still below the fix recorded on the finding.
|
|
for _ in range(3):
|
|
app_scan(srv, pkgs, {"edge": []})
|
|
assert status(srv) == "open", "absent from the index is not fixed"
|
|
app_scan(srv, pkgs, full)
|
|
assert status(srv) == "open"
|
|
|
|
# Every detection still has a path to close: updated past the fix …
|
|
app_scan(srv, [_edge("154.0.4258.62")], full)
|
|
assert status(srv) == "patched"
|
|
# … or uninstalled, even when the index no longer carries the CVE.
|
|
other = host("srv-b")
|
|
app_scan(other, [_edge("152.0.4191.62")], full)
|
|
assert status(other) == "open"
|
|
app_scan(other, [{"name": "7-Zip", "version": "24.09"}], {"edge": []})
|
|
assert status(other) == "patched"
|
|
|
|
# ---- 2. Intune: newest Edge build counts, stale entries do not ----
|
|
c2 = host("intune-2")
|
|
app_scan(c2, [_edge("154.0.4258.62"), _edge("152.0.4191.53"),
|
|
_edge("152.0.4191.53", "Microsoft.MicrosoftEdge.Stable")], full)
|
|
assert status(c2) is None, "Edge is at 154.0.4258.62"
|
|
|
|
# ---- 3. the MSIX registration can carry the current build ----
|
|
c3 = host("intune-3")
|
|
app_scan(c3, [_edge("153.0.4234.32"),
|
|
_edge(FIX, "Microsoft.MicrosoftEdge.Stable")], full)
|
|
assert status(c3) is None, "Edge is at 154.0.4258.37"
|
|
|
|
# A host that IS below the fix is still flagged, whichever entry says so.
|
|
c4 = host("intune-4")
|
|
app_scan(c4, [_edge("153.0.4234.32"),
|
|
_edge("152.0.4191.53", "Microsoft.MicrosoftEdge.Stable")], full)
|
|
v = (db.query(Vulnerability)
|
|
.filter(Vulnerability.asset_id == c4.id, Vulnerability.cve_id == CVE).one())
|
|
assert v.status.value == "open" and v.package_version == "153.0.4234.32", \
|
|
(v.status, v.package_version)
|
|
# The MSIX alone never flags: it is a per-user copy, not the install.
|
|
c5 = host("intune-5")
|
|
app_scan(c5, [_edge("152.0.4191.53", "Microsoft.MicrosoftEdge.Stable")], full)
|
|
assert status(c5) is None
|
|
# WebView2 and the Edge DevTools client are other products.
|
|
pairs = m.installed_products([
|
|
_edge("152.0.4191.99", "Microsoft Edge WebView2 Runtime"),
|
|
_edge("1000.25128.1000.0", "Microsoft.MicrosoftEdgeDevToolsClient"),
|
|
_edge("153.0.4234.32")])
|
|
assert [(p["key"], v) for p, v in pairs] == [("edge", "153.0.4234.32")], pairs
|
|
|
|
print("msrc edge flap OK")
|
|
|
|
|
|
def test_msrc_edge_flap():
|
|
demo()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
demo()
|