Skip to content

Commit 33659ff

Browse files
committed
Refresh exact builder header and pip vendor evidence
1 parent 1a6f9be commit 33659ff

9 files changed

Lines changed: 6549 additions & 19 deletions

‎.github/dapper-headers.json‎

Lines changed: 6225 additions & 0 deletions
Large diffs are not rendered by default.

‎.github/scripts/enforce-builder-vulnerabilities.py‎

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,23 @@ def violations(report, ledger, boundary, headers):
2727
if (headers.get("package") != "linux-libc-dev"
2828
or headers.get("version") != HEADER_VERSION or headers.get("purl") != HEADER_PURL):
2929
raise ValueError("Changed exact header review")
30+
refresh = headers.get("mediumRefresh", {})
31+
refreshed = {}
32+
for entry in refresh.get("entries", []):
33+
identifier = entry.get("id")
34+
official = ("https://raw.githubusercontent.com/canonical/ubuntu-security-notices/"
35+
+ str(refresh.get("officialSnapshot")) + "/osv/cve/2026/UBUNTU-"
36+
+ str(identifier) + ".json")
37+
if (identifier in refreshed or identifier not in headers.get("ids", [])
38+
or entry.get("package") != "linux-libc-dev" or entry.get("version") != HEADER_VERSION
39+
or entry.get("purl") != HEADER_PURL or entry.get("severity") != "MEDIUM"
40+
or entry.get("decision") != "UNFIXED_MEDIUM_HEADER_ONLY"
41+
or entry.get("officialFixedVersions") != []
42+
or entry.get("officialRangeEvents") != [{"introduced": "0"}]
43+
or entry.get("officialUrl") != official
44+
or not entry.get("rawSha256") or not entry.get("target")):
45+
raise ValueError("Invalid exact medium header refresh")
46+
refreshed[identifier] = entry
3047
reviewed = {}
3148
for entry in ledger.get("unfixed", []):
3249
key = (entry["id"], entry["package"], entry["version"], entry["purl"])
@@ -54,7 +71,8 @@ def violations(report, ledger, boundary, headers):
5471
if boundary != expected:
5572
raise ValueError("Missing exact installed header-only proof")
5673
if (not fixed and key[0] in headers.get("ids", [])
57-
and key[2] == HEADER_VERSION and key[3] == HEADER_PURL):
74+
and key[2] == HEADER_VERSION and key[3] == HEADER_PURL
75+
and (key[0] not in refreshed or finding.get("Severity") == "MEDIUM")):
5876
continue
5977
severity = finding.get("Severity", "UNKNOWN")
6078
entry = reviewed.get(key)

‎.github/scripts/test_builder_vulnerability_policy.py‎

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import importlib.util
2+
import json
3+
from copy import deepcopy
24
import unittest
35
from pathlib import Path
46
spec = importlib.util.spec_from_file_location("gate", Path(__file__).with_name("enforce-builder-vulnerabilities.py"))
@@ -69,6 +71,28 @@ def test_fixed_header_rejected(self):
6971
self.header()["FixedVersion"] = "7.0.0-99"; self.assertTrue(self.check())
7072
def test_new_header_advisory_rejected(self):
7173
self.header()["VulnerabilityID"] = "NEW"; self.assertTrue(self.check())
74+
def refreshed_header(self):
75+
self.headers = json.loads((Path(__file__).resolve().parents[1] / "dapper-headers.json").read_text())
76+
f = self.header()
77+
f.update(VulnerabilityID=self.headers["mediumRefresh"]["entries"][0]["id"], Severity="MEDIUM")
78+
return f
79+
def test_refreshed_exact_medium_header(self):
80+
self.refreshed_header()
81+
self.assertEqual(len(self.headers["mediumRefresh"]["entries"]), 222)
82+
self.assertEqual(self.check(), [])
83+
def test_refreshed_headers_cannot_allow_high_unknown_or_available_fix(self):
84+
for severity in ("LOW", "HIGH", "CRITICAL", "UNKNOWN"):
85+
self.refreshed_header()["Severity"] = severity
86+
with self.subTest(severity=severity): self.assertTrue(self.check())
87+
self.refreshed_header()["FixedVersion"] = "7.0.0-99"
88+
self.assertTrue(self.check())
89+
def test_refreshed_review_fixed_or_package_drift_rejected(self):
90+
self.refreshed_header()
91+
original = deepcopy(self.headers)
92+
for key, value in (("officialFixedVersions", ["7.0.0-99"]), ("purl", "pkg:generic/other@1")):
93+
self.headers = deepcopy(original)
94+
self.headers["mediumRefresh"]["entries"][0][key] = value
95+
with self.subTest(key=key), self.assertRaises(ValueError): self.check()
7296
def test_header_version_or_purl_drift_rejected(self):
7397
for field in ("InstalledVersion", "PkgIdentifier"):
7498
f = self.header()

‎scripts/install-python-security-tools‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ else
3030
fi
3131
"$scratch/venv/bin/python" /opt/python-security/scripts/test-pip-overlay.py
3232
if [ -n "${PYTHON_SECURITY_VIRTUALENV_PYTHON:-}" ]; then
33-
"$PYTHON_SECURITY_VIRTUALENV_PYTHON" -m virtualenv --no-download --no-periodic-update --pip="$expected" --extra-search-dir=/usr/share/python-wheels "$scratch/virtualenv"
33+
"$PYTHON_SECURITY_VIRTUALENV_PYTHON" -m virtualenv --app-data "$scratch/virtualenv-app-data" --no-download --no-periodic-update --pip="$expected" --extra-search-dir=/usr/share/python-wheels "$scratch/virtualenv"
3434
"$scratch/virtualenv/bin/python" /opt/python-security/scripts/test-pip-overlay.py
3535
fi
3636
printf 'PYTHON_SECURITY_TOOLS_OK pip=%s certifi=2026.10.19 ensurepip=%s\n' "$expected" "$ensurepip_status"

‎scripts/pip-security/README.md‎

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,9 +67,23 @@ bash verify-pip-runtime.sh --virtualenv /usr/share/python-wheels
6767

6868
`check-pip-vendor-slice.py` 會比對實際載入的全部 299 個 vendor 檔案與
6969
固定 wheel,拒絕額外檔案、其他同版本完整 distribution。只有取得此證據後,
70-
`filter-pip-vendor-findings.py` 才可對上述三個 CVE(含 msgpack GHSA 別名)
70+
`filter-pip-vendor-findings.py` 才可對已驗證的 CVE(含 msgpack GHSA 別名)
7171
產生綁定該映像的 OpenVEX 與有效掃描檔。原始掃描必須另行保存;未知 CVE、
7272
其他版本、非 SBOM 套件來源與 urllib3 finding 全部保留,不做泛用版本排除。
73-
執行 `python3 test-pip-vex.py` 可驗證以上六組正反控制。
73+
執行 `python3 test-pip-vex.py` 可驗證以上正反控制。
74+
75+
新增公告仍限同一個 wheel 與實際 vendor slice:
76+
77+
- [CVE-2026-114613/GHSA-j586-36cw-2gc2](https://github.com/msgpack/msgpack-python/security/advisories/GHSA-j586-36cw-2gc2)
78+
的 C extension 不在固定 wheel,實際載入 pure-Python fallback。
79+
- [GHSA-xmj3-9gf7-h52w](https://github.com/pypa/setuptools/security/advisories/GHSA-xmj3-9gf7-h52w)
80+
所需 `setuptools/wheel.py` 不在此 slice。
81+
82+
每個 Compose builder 都須重新驗證 299 個 live 檔案、固定 seed wheel、
83+
SHA-256、脆弱模組缺席及整個檔案系統中相關 wheel/SBOM/distribution 來源。
84+
只有已列明的 Compose 路徑且內容完全一致才可承接此證據;未知位置的同版
85+
SBOM、額外完整套件或 seed copy 會拒絕。負向 fixtures 包含 sys.path 外完整
86+
distribution、外來 CycloneDX/SPDX、不同 bytes 與未知 wheel。虛擬環境驗證的
87+
app-data 放在其自有暫存目錄,不留下另一份未追蹤 seed。
7488

7589
官方政策:https://pip.pypa.io/en/stable/development/vendoring-policy/

‎scripts/pip-security/check-pip-vendor-slice.py‎

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,16 +9,27 @@
99
import zipfile
1010
import pip
1111
from pip._vendor import msgpack, urllib3
12+
from vendor_source_inventory import inventory
1213

1314
EXPECTED_SHA = "be5f63d537bbe611dc422fabb6740a44b5997888b592888df5091727f023802a"
15+
BOM_SHA = "3581cb8f0a6128c19ccec4e9aeab0d3ac8212fd19bd19d485b2e24fd4dc10f2a"
1416
FINDINGS = {
1517
"GHSA-6v7p-g79w-8964": ("msgpack", "1.1.2", "C/Cython unpacker is absent; runtime selects pure Python fallback"),
1618
"CVE-2026-57585": ("msgpack", "1.1.2", "C/Cython unpacker is absent; runtime selects pure Python fallback"),
19+
"CVE-2026-114613": ("msgpack", "1.1.2", "_unpacker.pyx/unpack.h strict-map-key C extension is absent; fallback.py is not affected"),
20+
"GHSA-j586-36cw-2gc2": ("msgpack", "1.1.2", "_unpacker.pyx/unpack.h strict-map-key C extension is absent; fallback.py is not affected"),
1721
"CVE-2025-47273": ("setuptools", "70.3.0", "setuptools/package_index.py is absent; only pkg_resources is vendored"),
1822
"CVE-2026-59890": ("setuptools", "70.3.0", "setuptools/command/egg_info.py and FileList are absent"),
23+
"GHSA-xmj3-9gf7-h52w": ("setuptools", "70.3.0", "setuptools/wheel.py Wheel._fix_namespace_packages is absent; only pkg_resources is vendored"),
1924
}
2025

2126

27+
def require_absent_vulnerable_modules(names):
28+
names = tuple(names)
29+
assert not any(name.startswith("pip/_vendor/msgpack/_cmsgpack") for name in names), "Native msgpack module present"
30+
assert not any(name.startswith("pip/_vendor/setuptools/") for name in names), "Full setuptools module present"
31+
32+
2233
def verify(wheel):
2334
assert hashlib.sha256(wheel.read_bytes()).hexdigest() == EXPECTED_SHA
2435
assert pip.__version__ == "26.2.2+pasturestack.1"
@@ -36,8 +47,7 @@ def verify(wheel):
3647
expected_files.add(name.removeprefix("pip/_vendor/"))
3748
checked += 1
3849
assert not any(name.startswith("pip/_vendor/") and name.endswith((".so", ".pyd")) for name in names)
39-
assert not any(name.startswith("pip/_vendor/setuptools/") for name in names)
40-
assert not any(name.startswith("pip/_vendor/msgpack/_cmsgpack") for name in names)
50+
require_absent_vulnerable_modules(names)
4151
def live_files(directory, prefix=""):
4252
for entry in directory.iterdir():
4353
if entry.name == "__pycache__":
@@ -47,17 +57,20 @@ def live_files(directory, prefix=""):
4757
yield from live_files(entry, relative + "/")
4858
else:
4959
yield relative
50-
assert set(live_files(live.joinpath("_vendor"))) == expected_files, "Unexpected live vendor files"
60+
observed_files = set(live_files(live.joinpath("_vendor")))
61+
require_absent_vulnerable_modules("pip/_vendor/" + name for name in observed_files)
62+
assert observed_files == expected_files, "Unexpected live vendor files"
5163
# A separate full setuptools installation cannot inherit the vendor-slice VEX.
5264
for distribution in importlib.metadata.distributions():
5365
if distribution.metadata.get("Name", "").lower() == "setuptools":
5466
assert distribution.version != "70.3.0", "A full setuptools 70.3.0 distribution is installed"
5567
if distribution.metadata.get("Name", "").lower() == "msgpack":
5668
assert distribution.version != "1.1.2", "A separate msgpack 1.1.2 distribution is installed"
57-
return {"verifiedWheelSha256": EXPECTED_SHA, "pip": pip.__version__,
69+
return {"verifiedWheelSha256": EXPECTED_SHA, "verifiedWheelPath": str(wheel.resolve()), "pip": pip.__version__,
5870
"urllib3": urllib3.__version__, "vendorFilesByteCompared": checked,
5971
"msgpackImplementation": msgpack.Unpacker.__module__,
60-
"setuptoolsPackageAbsent": True, "scope": "this exact pip vendor slice only",
72+
"setuptoolsPackageAbsent": True, "vulnerableModulesAbsent": ["msgpack._cmsgpack", "setuptools.wheel"],
73+
"scope": "this exact pip vendor slice only",
6174
"notAffected": [{"id": key, "package": value[0], "version": value[1],
6275
"justification": "vulnerable_code_not_present", "detail": value[2]}
6376
for key, value in FINDINGS.items()]}
@@ -81,6 +94,7 @@ def live_files(directory, prefix=""):
8194
else:
8295
with ensurepip._get_pip_whl_path_ctx() as wheel:
8396
result = verify(Path(wheel))
97+
result["vendorSourceInventory"] = inventory(Path("/"), EXPECTED_SHA, BOM_SHA)
8498
data = json.dumps(result, indent=2) + "\n"
8599
if args.output:
86100
args.output.write_text(data)

‎scripts/pip-security/filter-pip-vendor-findings.py‎

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,21 +5,39 @@
55
from datetime import datetime, timezone
66
import json
77
from pathlib import Path
8+
from vendor_source_inventory import SBOM_PATHS, WHEEL_PATHS
89

910
WHEEL_SHA = "be5f63d537bbe611dc422fabb6740a44b5997888b592888df5091727f023802a"
11+
WHEEL_NAME = "pip-26.2.2+pasturestack.1-py3-none-any.whl"
12+
BOM_SHA = "3581cb8f0a6128c19ccec4e9aeab0d3ac8212fd19bd19d485b2e24fd4dc10f2a"
1013
RULES = {
1114
"GHSA-6v7p-g79w-8964": ("msgpack", "1.1.2"),
1215
"CVE-2026-57585": ("msgpack", "1.1.2"),
16+
"CVE-2026-114613": ("msgpack", "1.1.2"),
17+
"GHSA-j586-36cw-2gc2": ("msgpack", "1.1.2"),
1318
"CVE-2025-47273": ("setuptools", "70.3.0"),
1419
"CVE-2026-59890": ("setuptools", "70.3.0"),
20+
"GHSA-xmj3-9gf7-h52w": ("setuptools", "70.3.0"),
1521
}
1622

1723

1824
def apply(raw, proof, subject):
25+
inventory = proof.get("vendorSourceInventory", {})
26+
sources = inventory.get("sources", [])
27+
valid_sources = (sources and len({item.get("path") for item in sources}) == len(sources) and
28+
any(item.get("kind") == "sbom" for item in sources) and
29+
any(item.get("kind") == "wheel" and item.get("path") == proof.get("verifiedWheelPath") for item in sources) and
30+
all((item.get("kind") == "wheel" and item.get("path") in WHEEL_PATHS and item.get("sha256") == WHEEL_SHA) or
31+
(item.get("kind") == "sbom" and item.get("path") in SBOM_PATHS and item.get("sha256") == BOM_SHA)
32+
for item in sources))
1933
valid = (proof["verifiedWheelSha256"] == WHEEL_SHA and
34+
proof.get("verifiedWheelPath") in WHEEL_PATHS and
2035
proof["pip"] == "26.2.2+pasturestack.1" and proof["urllib3"] == "2.8.0" and
2136
proof["msgpackImplementation"] == "pip._vendor.msgpack.fallback" and
22-
proof["setuptoolsPackageAbsent"] is True and proof["vendorFilesByteCompared"] == 299)
37+
proof["setuptoolsPackageAbsent"] is True and proof["vendorFilesByteCompared"] == 299 and
38+
proof.get("vulnerableModulesAbsent") == ["msgpack._cmsgpack", "setuptools.wheel"] and
39+
inventory.get("complete") is True and inventory.get("noForeignMatchingDistribution") is True and
40+
inventory.get("expectedBomSha256") == BOM_SHA and valid_sources)
2341
if not valid:
2442
raise ValueError("Exact installed pip vendor proof is required")
2543
reasons = {item["id"]: item for item in proof["notAffected"]}
@@ -31,17 +49,20 @@ def apply(raw, proof, subject):
3149
key = item.get("VulnerabilityID")
3250
expected = RULES.get(key)
3351
actual = (item.get("PkgName"), item.get("InstalledVersion"))
52+
purl = "pkg:pypi/" + str(actual[0]) + "@" + str(actual[1])
3453
packages = [pkg for pkg in section.get("Packages", []) if
3554
(pkg.get("Name"), pkg.get("Version")) == actual]
3655
reason = reasons.get(key, {})
3756
eligible = (expected == actual and key in reasons and
3857
(reason.get("package"), reason.get("version")) == actual and
3958
section.get("Type") == "python-pkg" and packages and
40-
all(pkg.get("AnalyzedBy") == "sbom" for pkg in packages))
59+
all(pkg.get("AnalyzedBy") == "sbom" for pkg in packages) and
60+
(item.get("PkgIdentifier") or {}).get("PURL") == purl and
61+
all(not pkg.get("FilePath") and not pkg.get("PkgPath") for pkg in packages) and
62+
not item.get("PkgPath"))
4163
if not eligible:
4264
retained.append(item)
4365
continue
44-
purl = "pkg:pypi/" + actual[0] + "@" + actual[1]
4566
statements.append({
4667
"vulnerability": {"name": key},
4768
"products": [{"@id": subject, "subcomponents": [{"@id": purl}]}],

0 commit comments

Comments
 (0)