plugin-catalog CI: enforce rule 17 — scan pinned entries for unscannable payloads

The admission gate ran the install scanner (dangerous fails) but nothing refused
a compiled binary, committed bytecode, or an archive hiding either — the shapes
the scanner cannot read. This adds that enforcement to pinned-source-validate.

- scripts/scan_plugin_payloads.py (stdlib-only, so the no-install validator and
  third-party CI can run it): flags compiled binaries by magic header (ELF,
  Mach-O, PE/MZ, WebAssembly — not by extension, which a rename defeats),
  .pyc/.pyo bytecode (incl. sourceless/hash-forged caches that run instead of
  the .py beside them), and archives whose members are not reviewable source.
  Plain-text source bundles and inert assets (fonts, images) pass.
- plugin-catalog-ci.yml: after `hermes plugins validate`, each changed entry is
  scanned; a finding fails it. An entry may list `attested_binaries:` paths,
  each verified with `gh attestation verify --repo <repo> --source-digest <sha>`
  against the pinned commit; verified paths are dropped before the scan, any
  other binary still fails. Repo-controlled filenames are newline-stripped in
  the log so none can forge a workflow command.
- validate_plugin_catalog.py: `attested_binaries` is a known key, shape-checked
  as a list of relative paths.
- Tests: unit tests for the detector and two new workflow-driven bypass cases
  (bundled .exe, committed .pyc) in the confinement harness, which runs the real
  `run:` block.

Full-content re-sweep of all 543 pins: only tabro carries a native binary (it is
delisted in the companion PR). Three entries (crawl4ai, hermes-vision-inline,
mermail) commit __pycache__/*.pyc build litter and would fail rule 17 on their
next pin bump — pre-existing, not touched here.
This commit is contained in:
Teknium
2026-10-11 02:34:57 -07:00
parent efd17a70b5
commit cdce56faf2
5 changed files with 385 additions and 0 deletions

View File

@@ -76,6 +76,7 @@ KNOWN_KEYS = {
"title",
"onboarding",
"known_issues",
"attested_binaries",
}
# Cosmetic labels attached to the pin. ``version`` is never parsed; ``image`` and ``screenshots``
# may only point at GitHub so the Desktop catalog browser and the docs site never fetch from
@@ -236,9 +237,30 @@ def validate_entry(data: object) -> tuple[list[str], list[str]]:
):
errors.append(f"capabilities.{key} must be a list of strings")
# attested_binaries: relative paths to compiled files that carry a GitHub
# build-provenance attestation (README rule 17). The CI gate verifies the
# attestation; here we only shape-check the paths.
_check_attested_binaries(data.get("attested_binaries"), errors)
return errors, warnings
def _check_attested_binaries(attested: object, errors: list[str]) -> None:
"""Shape-check the optional ``attested_binaries`` list of relative paths."""
if attested is None:
return
if not isinstance(attested, list) or not all(isinstance(p, str) for p in attested):
errors.append("attested_binaries must be a list of strings")
return
for p in attested:
if not re.fullmatch(r"[A-Za-z0-9._/-]+", p) or any(
seg in ("", ".", "..") for seg in p.split("/")
):
errors.append(
f"attested_binaries path {p!r} must be a relative path without '.' or '..' segments"
)
def validate_removed(data: object) -> tuple[list[str], list[str]]:
"""Validate the removed.yaml document. Returns (errors, warnings)."""
errors: list[str] = []