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:
@@ -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] = []
|
||||
|
||||
Reference in New Issue
Block a user