From a6c5cb3a06a099f8cf3c7fb36a129fd05ba3e9bc Mon Sep 17 00:00:00 2001 From: Gavin Borges Date: Tue, 25 Aug 2026 12:14:20 -0400 Subject: [PATCH] security(deps): block Dependabot from editing the hand-maintained render lockfile (A5) environments/requirements-ci-render.txt is tier 4 (CONTRIBUTING.md's dependency-tier table): hand-maintained via pip download + pip hash, no .in source. tools/check_lockfile_freshness.py cannot see it - it only walks LOCKFILE_PAIRS, which this file is deliberately not part of. .github/dependabot.yml's ignore: list is the only lever Dependabot itself offers, and it is keyed by package name, so it can only ever be reactive: it protects a transitive dependency once someone has already been bitten by Dependabot reaching it and named it. Three names (fastjsonschema, nbformat, pygments) reached this file in a single 2026-08-24 cycle despite two direct entries (ipykernel, nbclient) and one earlier transitive one (platformdirs) already on that list. A name-keyed guard cannot protect a target reached transitively by a name it has not seen yet. Adds a step to dependency-lockfile-check.yml that blocks the actor from touching this path at all, independent of which package moved: if the PR author is dependabot[bot] and the diff touches environments/requirements-ci-render.txt, the check fails with a message pointing at the correct fix. Uses pull_request.user.login rather than github.actor, matching the reliability reasoning pr-review-check.yml already documents for its own TRUSTED_BOTS check. fetch-depth: 0 added to the checkout step so the diff against origin/$BASE_REF resolves; the pattern (git diff "origin/$BASE_REF...HEAD") mirrors pii_scan.yml's own established, working diff-scoping. Validated: python -c 'yaml.safe_load(...)' confirms the file parses and the new if: condition folds to the expected single-line expression; the grep match/no-match logic was self-tested against both a touching and a non-touching file list. Signed-off-by: Gavin Borges --- .../workflows/dependency-lockfile-check.yml | 31 +++++++++++++++++++ CHANGELOG.md | 17 ++++++++++ 2 files changed, 48 insertions(+) diff --git a/.github/workflows/dependency-lockfile-check.yml b/.github/workflows/dependency-lockfile-check.yml index c475332..acc62f6 100644 --- a/.github/workflows/dependency-lockfile-check.yml +++ b/.github/workflows/dependency-lockfile-check.yml @@ -38,6 +38,7 @@ jobs: - name: Checkout code uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: + fetch-depth: 0 persist-credentials: false - name: Set up Python @@ -52,3 +53,33 @@ jobs: # direct pins, CVE-floor violations, or any unhashed entry. See # tools/check_lockfile_freshness.py for the exact rules. run: python tools/check_lockfile_freshness.py --check + + - name: Block Dependabot from editing the hand-maintained render lockfile + # environments/requirements-ci-render.txt is tier 4 (CONTRIBUTING.md's + # dependency-tier table): hand-maintained via `pip download` + `pip hash`, + # with no `.in` source, so the freshness check above cannot see it at all - + # it only walks LOCKFILE_PAIRS, which this file is deliberately not part + # of. dependabot.yml's `ignore:` list is the only lever Dependabot itself + # offers, and it is keyed by PACKAGE NAME, so it can only ever be + # reactive: it protects a transitive dependency once someone has already + # been bitten by Dependabot reaching it and named it. Three names + # (fastjsonschema, nbformat, pygments) reached this file in a single + # 2026-08-24 cycle despite two direct entries (ipykernel, nbclient) and + # one earlier transitive one (platformdirs) already being on that list - + # a name-keyed guard cannot protect a target reached transitively by a + # name it has not seen yet. This step is the path-scoped guard instead: + # it blocks the actor from touching this path at all, independent of + # which package moved, so the next unnamed transitive dependency cannot + # slip through the same way. `pull_request.user.login`, not + # `github.actor`, per the same reasoning pr-review-check.yml documents + # for its own bot allowlist. + if: >- + github.event_name == 'pull_request' && + github.event.pull_request.user.login == 'dependabot[bot]' + run: | + if git diff --name-only "origin/${{ github.base_ref }}...HEAD" \ + | grep -qx 'environments/requirements-ci-render.txt'; then + echo "::error file=environments/requirements-ci-render.txt::Dependabot must not modify this hand-maintained, no-.in-source tier-4 lockfile (CONTRIBUTING.md dependency tier 4). Close this PR, add the newly-reached package name(s) to the ignore: list in .github/dependabot.yml, and update this file by hand following its own header recipe (pip download + pip hash) in a separate PR." >&2 + exit 1 + fi + echo "environments/requirements-ci-render.txt untouched - PASSED." diff --git a/CHANGELOG.md b/CHANGELOG.md index b6262d3..2bb15db 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,23 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), ## [Unreleased] ### Security +- **A path-scoped guard blocks Dependabot from ever editing + `environments/requirements-ci-render.txt` again (A5).** That file is tier 4 + (`CONTRIBUTING.md`'s dependency-tier table): hand-maintained via `pip download` + `pip hash`, + with no `.in` source, so `tools/check_lockfile_freshness.py` cannot see it - it only walks + `LOCKFILE_PAIRS`, which this file is deliberately not part of. `.github/dependabot.yml`'s + `ignore:` list is the only lever Dependabot itself offers, and it is keyed by **package name**, + so it can only ever be reactive: it protects a transitive dependency once someone has already + been bitten by Dependabot reaching it and named it. Three names (`fastjsonschema`, `nbformat`, + `pygments`) reached this file in a single 2026-08-24 cycle despite two direct entries + (`ipykernel`, `nbclient`) and one earlier transitive one (`platformdirs`) already on that list - + a name-keyed guard cannot protect a target reached transitively by a name it has not seen yet. + A new step in `.github/workflows/dependency-lockfile-check.yml` blocks the actor from touching + this path at all, independent of which package moved: if the PR author is `dependabot[bot]` + and the diff touches `environments/requirements-ci-render.txt`, the check fails with a message + pointing at the correct fix (add the package name to the ignore list, then update the file by + hand in a separate PR). Uses `pull_request.user.login`, not `github.actor`, for the same + reliability reason `pr-review-check.yml` already documents for its own bot allowlist. - **`persist-credentials: false` added to 19 `actions/checkout` steps across 12 workflow files.** The repository has **21** checkout steps across **14** files (`security.yml` holds 6 and `ci.yml` 3, which is why the file count and the step count differ). `scorecard.yml`