test(canary): verify the pinned gitleaks release
This commit is contained in:
@@ -2,7 +2,7 @@
|
|||||||
# digest, etc.) without coupling every dev push to upstream registry
|
# digest, etc.) without coupling every dev push to upstream registry
|
||||||
# availability.
|
# availability.
|
||||||
#
|
#
|
||||||
# Opt-in via CLAUDE_BOTTLE_RUN_CANARIES=1 so the same files can be run
|
# Opt-in via BOT_BOTTLE_RUN_CANARIES=1 so the same files can be run
|
||||||
# locally with the same gating.
|
# locally with the same gating.
|
||||||
|
|
||||||
name: canaries
|
name: canaries
|
||||||
@@ -25,4 +25,7 @@ jobs:
|
|||||||
# No actions/setup-python: canaries are stdlib unittest on the image's
|
# No actions/setup-python: canaries are stdlib unittest on the image's
|
||||||
# system Python 3.12 (older act_runner mishandles setup-python's PATH).
|
# system Python 3.12 (older act_runner mishandles setup-python's PATH).
|
||||||
- name: Run canaries
|
- name: Run canaries
|
||||||
run: python3 -m unittest discover -t . -s tests/canaries -v
|
run: |
|
||||||
|
python3 -m scripts.unittest_gate \
|
||||||
|
-t . -s tests/canaries -v \
|
||||||
|
--minimum-executed 1 --fail-on-skip
|
||||||
|
|||||||
@@ -0,0 +1,85 @@
|
|||||||
|
"""Canary: the pinned gitleaks release remains downloadable and executable.
|
||||||
|
|
||||||
|
The gateway Dockerfile verifies this archive during an image build. Repeating
|
||||||
|
the upstream check weekly keeps registry/release drift out of normal pull
|
||||||
|
requests while proving that the pinned URL, architecture checksum, archive
|
||||||
|
shape, and binary still agree.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import hashlib
|
||||||
|
import os
|
||||||
|
import platform
|
||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
import tarfile
|
||||||
|
import tempfile
|
||||||
|
import unittest
|
||||||
|
import urllib.request
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[2]
|
||||||
|
DOCKERFILE = ROOT / "Dockerfile.gateway"
|
||||||
|
|
||||||
|
|
||||||
|
def _docker_arg(text: str, name: str) -> str:
|
||||||
|
match = re.search(rf"^ARG {re.escape(name)}=(\S+)$", text, re.MULTILINE)
|
||||||
|
if match is None:
|
||||||
|
raise AssertionError(f"Dockerfile.gateway has no concrete ARG {name}")
|
||||||
|
return match.group(1)
|
||||||
|
|
||||||
|
|
||||||
|
@unittest.skipUnless(
|
||||||
|
os.environ.get("BOT_BOTTLE_RUN_CANARIES") == "1",
|
||||||
|
"canary suite is opt-in; set BOT_BOTTLE_RUN_CANARIES=1 to run",
|
||||||
|
)
|
||||||
|
class TestGitleaksRelease(unittest.TestCase):
|
||||||
|
def test_pinned_archive_checksum_and_binary(self) -> None:
|
||||||
|
dockerfile = DOCKERFILE.read_text(encoding="utf-8")
|
||||||
|
version = _docker_arg(dockerfile, "GITLEAKS_VERSION")
|
||||||
|
machine = platform.machine().lower()
|
||||||
|
architectures = {
|
||||||
|
"x86_64": ("linux_x64", "GITLEAKS_SHA256_AMD64"),
|
||||||
|
"amd64": ("linux_x64", "GITLEAKS_SHA256_AMD64"),
|
||||||
|
"aarch64": ("linux_arm64", "GITLEAKS_SHA256_ARM64"),
|
||||||
|
"arm64": ("linux_arm64", "GITLEAKS_SHA256_ARM64"),
|
||||||
|
}
|
||||||
|
if machine not in architectures:
|
||||||
|
self.fail(f"unsupported canary runner architecture: {machine}")
|
||||||
|
asset, checksum_arg = architectures[machine]
|
||||||
|
expected_checksum = _docker_arg(dockerfile, checksum_arg)
|
||||||
|
url = (
|
||||||
|
"https://github.com/gitleaks/gitleaks/releases/download/"
|
||||||
|
f"v{version}/gitleaks_{version}_{asset}.tar.gz"
|
||||||
|
)
|
||||||
|
|
||||||
|
with tempfile.TemporaryDirectory(prefix="bot-bottle-gitleaks-canary.") as tmp:
|
||||||
|
archive = Path(tmp) / "gitleaks.tar.gz"
|
||||||
|
urllib.request.urlretrieve(url, archive)
|
||||||
|
self.assertEqual(
|
||||||
|
expected_checksum,
|
||||||
|
hashlib.sha256(archive.read_bytes()).hexdigest(),
|
||||||
|
"the pinned upstream archive no longer matches Dockerfile.gateway",
|
||||||
|
)
|
||||||
|
with tarfile.open(archive, "r:gz") as bundle:
|
||||||
|
member = bundle.getmember("gitleaks")
|
||||||
|
source = bundle.extractfile(member)
|
||||||
|
if source is None:
|
||||||
|
self.fail("gitleaks archive member is not a regular file")
|
||||||
|
binary = Path(tmp) / "gitleaks"
|
||||||
|
binary.write_bytes(source.read())
|
||||||
|
binary.chmod(0o755)
|
||||||
|
result = subprocess.run(
|
||||||
|
[str(binary), "version"],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
check=False,
|
||||||
|
)
|
||||||
|
self.assertEqual(0, result.returncode, result.stderr)
|
||||||
|
self.assertIn(version, result.stdout + result.stderr)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
Reference in New Issue
Block a user