diff --git a/.gitea/workflows/ci.yaml b/.gitea/workflows/ci.yaml index 1fcf272..63a5431 100644 --- a/.gitea/workflows/ci.yaml +++ b/.gitea/workflows/ci.yaml @@ -28,6 +28,8 @@ env: REGISTRY: gitea.oci-oci.duckdns.org IMAGE_NS: gitea_admin UV_VERSION: "0.5.11" + GITLEAKS_VERSION: "8.21.2" + GITLEAKS_SHA256: "654c935542c89f565aabe7bf7c6c500830f116c114f0aeb509d2460c1ac2e6da" jobs: # --- stage 1: lint -- fast, fails first --------------------------------------------- @@ -155,12 +157,37 @@ jobs: run: uv run --with 'bandit[toml]' bandit -c pyproject.toml -r libs services -ll - name: gitleaks (secret scan) - # Pinned by digest and run directly, so the command is the documented one rather - # than a marketplace action's opinion of it. + # A downloaded binary, not `docker run -v "$PWD:/repo"`. + # + # The container form was scanning NOTHING and passing. `-v "$PWD:/repo"` is + # interpreted by the dind sidecar's daemon, which cannot see the job container's + # checkout, so gitleaks got an empty mount and logged: + # + # ERR [git] fatal: not a git repository (or any parent up to mount point /) + # ERR failed to scan Git repository error="stderr is not empty" + # INF scan completed in 35.2ms + # INF no leaks found <- exit 0 + # + # It reported success without looking, which is worse than having no scanner: the + # gate was green and meaningless. The 35ms runtime was the tell — a real history + # scan of this repo takes seconds. + # + # The `--log-opts` + commit-count assertion below is the guard against that class + # of failure returning. A scanner that cannot fail is not a gate. run: | - docker run --rm -v "$PWD:/repo" -w /repo \ - ghcr.io/gitleaks/gitleaks:v8.21.2@sha256:0e99e8821643ea5b235718642b93bb32486af9c8162c8b8731f7cbdc951a7f46 \ - detect --no-banner --source /repo + set -euo pipefail + curl -fsSL -o /tmp/gitleaks.tgz \ + "https://github.com/gitleaks/gitleaks/releases/download/v${GITLEAKS_VERSION}/gitleaks_${GITLEAKS_VERSION}_linux_arm64.tar.gz" + echo "${GITLEAKS_SHA256} /tmp/gitleaks.tgz" | sha256sum -c - + tar -xzf /tmp/gitleaks.tgz -C /tmp gitleaks + install -m 0755 /tmp/gitleaks /usr/local/bin/gitleaks + + # Prove there is a repository to scan before trusting the verdict. + commits=$(git rev-list --count HEAD) + echo "scanning $commits commits" + test "$commits" -gt 0 + + gitleaks detect --no-banner --source . --redact --exit-code 1 - name: pip-audit (dependency CVEs) # --strict fails on an audit error rather than shrugging and reporting clean. diff --git a/scripts/bump-digests.sh b/scripts/bump-digests.sh index 8907936..7e3f437 100755 --- a/scripts/bump-digests.sh +++ b/scripts/bump-digests.sh @@ -22,21 +22,50 @@ set -euo pipefail SERVICES=(api worker reconciler) CHART_VALUES="deploy/chart/values.yaml" -# yq, pinned by digest. Not python+pyyaml: a yaml round-trip strips every comment in -# values.yaml, and those comments are the only thing explaining why the digests are there. -# yq edits in place and leaves the rest of the file alone. -YQ_IMAGE="mikefarah/yq:4.44.6@sha256:b1d117c609ba990436ad1649299e2f6c378f62cb562caf30b6f2fb6144713422" - WORKDIR="$(mktemp -d)" cleanup() { rm -rf "${WORKDIR}" } trap cleanup EXIT -yq() { - docker run --rm -v "${PWD}:/work" -w /work -u "$(id -u):$(id -g)" "${YQ_IMAGE}" "$@" +# yq as a checksum-pinned binary, NOT `docker run -v "$PWD:/work"`. +# +# The container form cannot see the checkout on the Gitea runner: the -v source path is +# resolved by the dind sidecar's daemon, not by the job container, so yq reported +# "stat deploy/chart/values.yaml: no such file or directory" for a file that plainly +# exists. The same boundary silently broke helm and gitleaks. +# +# Not python+pyyaml: a yaml round-trip strips every comment in values.yaml, and those +# comments are the only thing explaining why the digests are there. yq edits in place. +YQ_VERSION="4.44.6" +YQ_SHA256="9477ac3cc447b6c083986129e35af8122eb2b938fe55c9c3e40436fb966e5813" + +ensure_yq() { + if command -v yq >/dev/null 2>&1; then + return + fi + local arch + case "$(uname -m)" in + aarch64|arm64) arch=arm64 ;; + x86_64) arch=amd64 ;; + *) echo "!! unsupported arch $(uname -m)" >&2; exit 1 ;; + esac + curl -fsSL -o "${WORKDIR}/yq" \ + "https://github.com/mikefarah/yq/releases/download/v${YQ_VERSION}/yq_linux_${arch}" + # Only the arm64 digest is pinned; this runner is Ampere. On another arch the download + # is still version-pinned, and the mismatch is reported rather than silently accepted. + if [ "${arch}" = "arm64" ]; then + echo "${YQ_SHA256} ${WORKDIR}/yq" | sha256sum -c - + else + echo "warning: no pinned checksum for ${arch}; version-pinned only" >&2 + fi + chmod +x "${WORKDIR}/yq" + PATH="${WORKDIR}:${PATH}" + export PATH } +ensure_yq + echo "==> resolving digests for tag ${IMAGE_TAG}" for svc in "${SERVICES[@]}"; do image="${REGISTRY}/${IMAGE_NS}/svcforge-${svc}"