Compare commits

..
4 changed files with 62 additions and 5 deletions
+1 -1
View File
@@ -48,7 +48,7 @@ keeps every weekly edition looking the same regardless of which model writes the
lists **failed sources**, the count is **not** authoritative: render the cve cell as `?` lists **failed sources**, the count is **not** authoritative: render the cve cell as `?`
(unknown), never `none` — a blank that reads as "clean" is exactly how two CVSS-9.8 gitea (unknown), never `none` — a blank that reads as "clean" is exactly how two CVSS-9.8 gitea
RCEs were reported as "none" on 2026-08-07. RCEs were reported as "none" on 2026-08-07.
- The table's **CVEs fixed** column counts known CVEs addressed by the available recipe upgrade. - The table's **CVEs** column counts known CVEs addressed by the available recipe upgrade.
A zero for an up-to-date recipe means there is no upgrade window to credit; it does not A zero for an up-to-date recipe means there is no upgrade window to credit; it does not
certify that the running images are free of vulnerabilities. If an upgrade exists but its certify that the running images are free of vulnerabilities. If an upgrade exists but its
advisory scan is absent, a source failed, or an image window was unmatched, use `?`. advisory scan is absent, a source failed, or an image window was unmatched, use `?`.
+1 -2
View File
@@ -217,8 +217,7 @@ def _table(rows, repo_url=None):
return "" return ""
# TESTS = the CI/test verdict (GREEN/FAILED/STALE/…); STATUS = the PR's LIVE state, fetched # TESTS = the CI/test verdict (GREEN/FAILED/STALE/…); STATUS = the PR's LIVE state, fetched
# client-side (open vs ✓ for any not-open state) by the inline script in _page(). # client-side (open vs ✓ for any not-open state) by the inline script in _page().
head = ("<tr><th>Recipe</th><th>Change</th><th>TESTS</th>" head = ("<tr><th>Recipe</th><th>Change</th><th>TESTS</th><th>CVEs</th>"
'<th title="Known CVEs fixed by the available recipe upgrade; zero does not certify the current image as vulnerability-free">CVEs fixed</th>'
"<th>CI</th><th>PR</th><th>STATUS</th><th>Notes</th></tr>") "<th>CI</th><th>PR</th><th>STATUS</th><th>Notes</th></tr>")
trs = [] trs = []
for r in rows: for r in rows:
+43 -2
View File
@@ -313,6 +313,39 @@ def _release_fallback_repos(registry: str, repo: str) -> list[tuple[str, str]]:
return cands return cands
def _oci_tag_exists(host: str, repo: str, tag: str) -> bool:
"""Verify a release candidate is actually published as an image manifest."""
hdrs = {"User-Agent": UA,
"Accept": "application/vnd.oci.image.index.v1+json, "
"application/vnd.docker.distribution.manifest.list.v2+json, "
"application/vnd.oci.image.manifest.v1+json",
**_oci_bearer(host, repo)}
url = f"https://{host}/v2/{repo}/manifests/{urllib.parse.quote(tag, safe='')}"
try:
with urllib.request.urlopen(urllib.request.Request(url, headers=hdrs, method="HEAD"),
timeout=TIMEOUT) as response:
return response.status == 200
except urllib.error.HTTPError as e:
if e.code == 404:
return False
raise
def _release_manifest_fallback(registry: str, repo: str, current: str) -> tuple[list[str], str] | None:
"""For huge GHCR registries, use release tags only after checking actual manifests."""
for owner, name in _release_fallback_repos(registry, repo):
releases = github_release_tags(owner, name)
if current not in releases:
continue # incomplete release window; it cannot establish a current baseline
candidates = sorted((tag for tag in set(releases)
if shape(tag) == shape(current) and vkey(tag) >= vkey(current)),
key=vkey, reverse=True)
for tag in candidates:
if _oci_tag_exists(registry, repo, tag):
return [current, tag], f"github-releases+registry-manifest:{owner}/{name}"
return None
_TAG_CACHE: dict[tuple[str, str], tuple[list[str], str | None]] = {} _TAG_CACHE: dict[tuple[str, str], tuple[list[str], str | None]] = {}
@@ -349,8 +382,16 @@ def resolve(ref: str) -> dict:
"upgrade_available": False} "upgrade_available": False}
tags, err = list_tags(info["registry"], info["repo"]) tags, err = list_tags(info["registry"], info["repo"])
if err: if err:
out["status"] = f"error: {err}" try:
return out fallback = (_release_manifest_fallback(info["registry"], info["repo"], info["tag"])
if "tag listing truncated" in err else None)
except Exception as e: # noqa: BLE001 — a failed fallback must stay unresolved
out["status"] = f"error: registry {err}; release fallback {type(e).__name__}: {e}"
return out
if not fallback:
out["status"] = f"error: {err}"
return out
tags, out["source"] = fallback
out["tags_seen"] = len(set(tags)) out["tags_seen"] = len(set(tags))
# INTEGRITY CHECK: the tag we are currently running MUST appear in the listing. If it does not, # INTEGRITY CHECK: the tag we are currently running MUST appear in the listing. If it does not,
# the listing is incomplete and any "newest" derived from it is a guess — ghcr paginates to tens # the listing is incomplete and any "newest" derived from it is a guess — ghcr paginates to tens
+17
View File
@@ -33,6 +33,23 @@ class TestResolverCoverage(unittest.TestCase):
row = resolver.resolve("example:1.2.3") row = resolver.resolve("example:1.2.3")
self.assertTrue(row["status"].startswith("unverifiable:")) self.assertTrue(row["status"].startswith("unverifiable:"))
def test_huge_ghcr_listing_uses_only_published_release_manifests(self):
ref = "ghcr.io/immich-app/immich-machine-learning:v3.3.5"
with mock.patch.object(resolver, "list_tags", return_value=([], "RuntimeError: tag listing truncated")), \
mock.patch.object(resolver, "github_release_tags",
return_value=["v3.3.6", "v3.3.5"]), \
mock.patch.object(resolver, "_oci_tag_exists", side_effect=lambda h, r, t: t == "v3.3.5"):
row = resolver.resolve(ref)
self.assertEqual(row["status"], "ok")
self.assertFalse(row["upgrade_available"]) # release exists; image does not yet
self.assertIn("registry-manifest", row["source"])
with mock.patch.object(resolver, "list_tags", return_value=([], "RuntimeError: tag listing truncated")), \
mock.patch.object(resolver, "github_release_tags",
return_value=["v3.3.6", "v3.3.5"]), \
mock.patch.object(resolver, "_oci_tag_exists", return_value=True):
upgraded = resolver.resolve(ref)
self.assertTrue(upgraded["upgrade_available"])
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()