Files
vulncheck/tests/test_vm_tools_bulletins.py
T
vulncheck 7f063482d1 feat(app-scan): XenServer VM Tools CVEs from Citrix bulletins
CVE-2025-27462, -27463 and -27464 (XSA-468) are in the XenServer VM Tools
for Windows, but the record names only "Xen / Windows PV drivers / all"
and NVD left them Deferred with no configuration, so nothing bounded the
tools a Windows inventory lists ("XenServer VM Tools" / XenServer / 9.6.0).
Citrix's bulletin CTX692748 does: "All versions BEFORE 9.4.1", Linux guests
not affected.

The tools' "What's new" on docs.xenserver.com links each security release to
its bulletin (9.4.1 -> CTX692748), so it is the list, read the way XenCenter's
notes are; the fetch loop is now shared. A bulletin counts only if its VM
Tools bound is the release the notes link it from and its "Affected Versions"
names no other product; every CVE it lists is then the tools'. Severity comes
from the bulletin page's JSON, where Citrix now renders it.

Matched are "XenServer VM Tools" and "Citrix VM Tools" (before the 9.3.2
rebrand), whose version is the installer's, on Windows hosts only. The
Management Agent entries are not read: 9.4.2 ships agent 9.3.2.95. Findings
close when the tools update; a feed outage keeps the last parse. Known limit:
drivers updated through Windows Update alone leave the tools' entry old.
2026-10-24 14:35:04 +02:00

257 lines
12 KiB
Python

"""XenServer VM Tools for Windows CVEs from the tools' release notes and Citrix bulletins — run: python -m pytest tests/test_vm_tools_bulletins.py
Field report (24.10.2026): CVE-2025-27462, -27463 and -27464 (XSA-468) are in
the XenServer VM Tools for Windows. The record and NVD name only
"Xen / Windows PV drivers / all" (NVD: Deferred, no configuration), which bounds
nothing. Citrix's bulletin CTX692748 does:
The following supported versions of XenServer VM Tools for Windows are
affected: All versions BEFORE 9.4.1
The tools' "What's new" on docs.xenserver.com links the release to it:
9.4.1 — All customers who are affected by the issues described in
CTX692748 - XenServer and Citrix Hypervisor Security Bulletin should update
to this version of the tools or later.
The Windows inventory reads "XenServer VM Tools" / XenServer / 9.6.0.
Markup below is verbatim from docs.xenserver.com and support.citrix.com
(fetched 24.10.2026), trimmed to the parts the parser reads.
"""
import os
import sys
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from app.services import app_cve_scanner_service as cpe
from app.services import cvelistv5_scan_service as c5
from app.services import vm_tools_bulletin_service as vtb
def _release(version, body, h="h4"):
"""The newest release is an <h3> "XenServer VM Tools for Windows 9.6.1",
every older one an <h4> "9.4.1"."""
title = f"XenServer VM Tools for Windows {version}" if h == "h3" else version
return (f'<div id="{version.replace(".", "")}" class="topic">\n'
f'<{h} class="title">{title}</{h}>\n<div class="body">\n'
f'<div class="p">\nThis set of tools contains the following component versions:\n'
f'</div>\n{body}</div>\n</div>\n')
def _components(installer, agent):
return ('<ul class="ul">\n'
f'<li class="li">\n<div class="p">\nInstaller: {installer}\n</div></li>\n'
f'<li class="li">\n<div class="p">\nManagement Agent: {agent}\n</div></li>\n'
'</ul>\n')
NOTES = (
_release("9.6.1", _components("9.6.1.12", "9.6.1.16"), h="h3")
+ '<h3 class="title" id="earlier-releases">Earlier releases</h3>\n'
+ _release("9.4.2", _components("9.4.2", "9.3.2.95"))
+ _release("9.4.1", _components("9.4.1", "9.3.1.80")
+ '<div class="p">\nAll customers who are affected by the issues described in '
'<a class="xref" data-has-usertext="true" href="https://support.citrix.com/article/'
'CTX692748" data-attr-href="https://support.citrix.com/article/CTX692748" '
'data-attr-format="html" data-attr-scope="external" target="_blank" '
'rel="noopener noreferrer">CTX692748 - XenServer and Citrix Hypervisor Security '
'Bulletin</a> should update to this version of the tools or later.\n</div>\n')
+ '<div id="fixed-issues-in-941" class="topic">\n<h4 class="title">Fixed issues in 9.4.1'
'</h4>\n<div class="body">\n<div class="p">\nSome Windows VMs can experience unexpected '
'hard reboots.\n</div></div></div>\n'
+ _release("9.4.0", _components("9.4.0", "9.3.0.65")
+ '<div class="p">For more information, see the support article <a class="xref" '
'href="https://support.citrix.com/article/CTX678047">Static IP loss when updating '
'to VM Tools for Windows 9.3.3 or earlier</a>.</div>\n'))
def _s(text):
return (f'<p><span style="font-size: 11pt;font-family: Calibri, sans-serif;color: #000000;">'
f'{text}</span></p>\n')
def _bulletin(cves, affected, sev="High"):
lis = "".join(f'<li style="font-size: 11pt;font-family: Calibri, sans-serif;color: #000000;">'
f'\n<p><span style="font-size: 11pt;">{c}</span></p>\n</li>' for c in cves)
return (
'<div class="article-detail-card-content wolken-h5">\n'
'<p><h3><span style="font-size: 11pt;font-family: Calibri, sans-serif;color: #000000;">'
'Description of Problem</span></h3>\n'
+ _s("Issues have been identified in the XenServer VM Tools for Windows that allow an "
"attacker who has the ability to execute arbitrary unprivileged code within a guest "
"Windows VM to compromise that same VM. This issue affects Windows VMs running on "
"either XenServer 8.4 and Citrix Hypervisor 8.2 CU1 LTSR.")
+ _s("These issues have the following identifiers:")
+ f'<ul style="margin-top: 0px;margin-bottom: 0px;padding-inline-start: 48px;">{lis}</ul>\n'
'<h3 style="margin-top: 0pt;margin-bottom: 10pt;"><span style="font-size: 11pt;'
'font-family: Calibri, sans-serif;color: #000000;">Affected Versions</span></h3>\n'
+ affected
+ _s("Linux guest VMs are not affected by this issue.")
+ '<h3><span style="font-size: 11pt;font-family: Calibri, sans-serif;color: #000000;">'
'What Customers Should Do</span></h3>\n'
+ _s("We have released updated versions of the guest tools installer at https://support."
"citrix.com/s/article/CTX235403-updates-to-xenserver-vm-tools-for-windows-for-"
"xenserver-and-citrix-hypervisor and we are also making them available through "
"Windows Update.")
+ '</div>\n'
# The severity is not in the article markup; the page renders it from
# this JSON (trimmed), after the list of every possible value.
'<script type="text/javascript">var articleDetails = JSON.parse(\'{"kbFlexMapValueList":'
'[{"articleId":692748,"attributeId":12,"attributeName":"Severity","attributeTypeId":2,'
'"attributeTypeName":"LOV","flexAttributeLOVList":[{"lovId":6688,"lovName":"Critical"},'
'{"lovId":6690,"lovName":"High"},{"lovId":6691,"lovName":"Medium"}],"kbFlexMapId":6,'
f'"kbFlexMapValId":20988,"lovId":6690,"lovName":"{sev}","mandatory":false}},'
'{"attributeName":"Language","kbFlexMapValId":61150,"lovId":6693,'
'"lovName":"English (en_US)"}]}\');</script>\n')
AFFECTED = (_s("The following supported versions of XenServer VM Tools for Windows are affected:")
+ '<ul style="margin-top: 0px;margin-bottom: 0px;padding-inline-start: 48px;"><li '
'style="font-size: 11pt;font-family: Calibri, sans-serif;color: #000000;"><span '
'style="font-size: 11pt;">All versions BEFORE 9.4.1</span></li></ul>\n<p> </p>\n')
CTX692748 = _bulletin(["CVE-2025-27462", "CVE-2025-27463", "CVE-2025-27464"], AFFECTED)
# ---- 1. parse -----------------------------------------------------------------
def test_release_notes_name_the_security_releases():
"""9.4.0 links a support article, not a bulletin."""
assert vtb.parse_release_notes(NOTES) == [("9.4.1", "CTX692748")]
def test_newest_release_is_read_too():
top = _release("9.6.2", _components("9.6.2", "9.6.2.3") + '<div class="p">This release '
'fixes the issues described in <a href="https://support.citrix.com/article/'
'CTX699999">CTX699999 - XenServer Security Bulletin</a>.</div>', h="h3")
assert vtb.parse_release_notes(top + NOTES) == [("9.6.2", "CTX699999"),
("9.4.1", "CTX692748")]
def test_bulletin_page():
es = vtb.parse_bulletin("CTX692748", CTX692748, "9.4.1")
assert [e["cve"] for e in es] == ["CVE-2025-27462", "CVE-2025-27463", "CVE-2025-27464"]
for e in es:
assert (e["start"], e["lt"], e["lte"], e["sev"], e["ctx"]) == \
(None, "9.4.1", None, "high", "CTX692748")
assert e["desc"].startswith("Issues have been identified in the XenServer VM Tools for "
"Windows")
assert e["desc"].endswith("Source: Citrix Security Bulletin CTX692748.")
def test_bulletin_that_disagrees_with_the_release_notes_decides_nothing():
assert vtb.parse_bulletin("CTX692748", CTX692748, "9.4.2") == []
def test_bulletin_that_also_bounds_another_product_decides_nothing():
"""Which of its CVEs are the tools' and which the host's is prose."""
page = _bulletin(["CVE-2099-0001", "CVE-2099-0002"], AFFECTED + _s(
"The following supported versions of XenServer are affected: XenServer 8.4 and "
"Citrix Hypervisor 8.2 CU1 LTSR without the hotfixes listed below."))
assert vtb.parse_bulletin("CTX699999", page, "9.4.1") == []
def test_severity_from_markup_too():
"""The XenCenter bulletins still write "Severity: Medium" in the article."""
page = '<p><strong>Severity:</strong> Medium</p>' + CTX692748.split("<script")[0]
assert {e["sev"] for e in vtb.parse_bulletin("CTX692748", page, "9.4.1")} == {"medium"}
# ---- 2. scan ------------------------------------------------------------------
class _Asset:
id = 1
hostname = "vm-test"
operating_system = "Microsoft Windows Server 2022 Standard"
def _scan(monkeypatch, packages, os_name="Microsoft Windows Server 2022 Standard"):
monkeypatch.setattr(vtb, "load", lambda db: {
"CTX692748": vtb.parse_bulletin("CTX692748", CTX692748, "9.4.1")})
index = {}
c5._merge_vm_tools_bulletins(None, index)
got = []
monkeypatch.setattr(cpe, "_upsert", lambda db, asset, n, v, c, new_ids, **kw:
got.append((n, v, c["cve"], c["fixed"], c["severity"], c.get("refs"))))
monkeypatch.setattr(_Asset, "operating_system", os_name)
c5.scan_asset(None, _Asset(), packages, index, touched=set())
return got
def test_field_report(monkeypatch):
got = _scan(monkeypatch, [{"name": "XenServer VM Tools", "version": "9.4.0",
"vendor": "XenServer"}])
ref = '["https://support.citrix.com/external/article/CTX692748"]'
assert got == [("XenServer VM Tools", "9.4.0", cve, "9.4.1", "high", ref)
for cve in ("CVE-2025-27462", "CVE-2025-27463", "CVE-2025-27464")], got
# the screenshot's install, the fix itself, and the installer's four-part build
for ver in ("9.6.0", "9.4.1", "9.6.1.12"):
assert _scan(monkeypatch, [{"name": "XenServer VM Tools", "version": ver}]) == [], ver
def test_name_before_the_rebrand(monkeypatch):
"""9.3.2 renamed "Citrix VM Tools" to "XenServer VM Tools"."""
got = _scan(monkeypatch, [{"name": "Citrix VM Tools", "version": "9.3.1"}])
assert {g[2] for g in got} == {"CVE-2025-27462", "CVE-2025-27463", "CVE-2025-27464"}
def test_management_agent_is_not_the_tools_release(monkeypatch):
"""9.4.2 ships Management Agent 9.3.2.95: its version is not the tools'."""
for name in ("XenServer Windows Management Agent", "Citrix XenServer Windows Management Agent",
"Citrix Hypervisor PV Tools"):
assert _scan(monkeypatch, [{"name": name, "version": "9.3.2.95"}]) == [], name
def test_windows_only(monkeypatch):
assert _scan(monkeypatch, [{"name": "XenServer VM Tools", "version": "9.4.0"}],
os_name="Ubuntu 24.04") == []
# ---- 3. fetch -----------------------------------------------------------------
class _Resp:
def __init__(self, text, status=200):
self.text, self.status_code = text, status
def raise_for_status(self):
if self.status_code != 200:
raise OSError(self.status_code)
def _client(pages):
class Client:
def __init__(self, **kw):
pass
def __enter__(self):
return self
def __exit__(self, *a):
return False
def get(self, url):
if url == vtb.RELEASE_NOTES:
return pages["notes"]
return pages.get(url.rsplit("/", 1)[1]) or _Resp("", 403)
return Client
def test_build_index(monkeypatch):
import httpx
stored = {}
monkeypatch.setattr(vtb, "_store", lambda db, idx: stored.update(idx))
monkeypatch.setattr(vtb, "load", lambda db: {"CTX600000": ["kept"]})
monkeypatch.setattr(httpx, "Client", _client(
{"notes": _Resp(NOTES), "CTX692748": _Resp(CTX692748)}))
idx = vtb.build_index(None)
assert sorted(idx) == ["CTX600000", "CTX692748"], idx
assert len(idx["CTX692748"]) == 3
assert stored == idx
def test_unreachable_serves_last_parse(monkeypatch):
"""A bulletin that drops out would close its findings as "patched"."""
import httpx
monkeypatch.setattr(vtb, "load", lambda db: {"CTX692748": ["last"]})
monkeypatch.setattr(httpx, "Client", _client({"notes": _Resp("", 503)}))
assert vtb.build_index(None) == {"CTX692748": ["last"]}
monkeypatch.setattr(httpx, "Client", _client({"notes": _Resp(NOTES)})) # bulletin 403
assert vtb.build_index(None) == {"CTX692748": ["last"]}