Repository navigation
fix(ci): pin test service and job containers by digest (RE004) - #945
Conversation
Governance's Validate Hypatia Baseline and the Hypatia Security Scan build the scanner at the reusables' default hypatia-ref, 51ab649. That commit's ActionsLock parser accepts only v0.0.2, so this repo's tool-owned v0.0.3 actions.lock fails closed as two high invalid_actions_lock findings on every run. Pass hypatia-ref aa34a4c (main after #938, which adds v0.0.3 support) as an input. The `uses:` refs are unchanged, so actions.lock is untouched. This is the value standards#1315 moves the defaults to; drop the input once the standards callers are re-pinned past that PR. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CYR5bf5FBLS7cXQK72aVNM
…credential WH013 reported a write as starved whenever the file had no `contents: write` grant, including jobs that write with their own credential. That grant only governs GITHUB_TOKEN, so a PAT or app-token write cannot be starved by a read-only grant. Measured false positives: - hypatia estate-rescan.yml: PAT clone of verisimdb-data and push (high) - hypatia inbox-steward-intake.yml: checkout with FARM_PAT (high) - standards github-backup-mirror-reusable.yml: PAT push to the backup repo Writes are now judged per job. A job is credentialed when `token:`, `GH_TOKEN:` or `GITHUB_TOKEN:` is set to a secret other than secrets.GITHUB_TOKEN, or to a step-minted outputs.token. Starvation and its masked :high arm consider only GITHUB_TOKEN jobs. A credentialed write that is masked (2>/dev/null, || true, || echo) is still reported, at :warn with fix recipe unmask-credentialed-write, since a credential that expires would fail it silently. Tests: 5 new cases; 61/61 pass, and 3 of the new cases fail against the previous module. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CYR5bf5FBLS7cXQK72aVNM
…943) ## Summary PR 3 of the baseline-gate stack, stacked on #942. Fixes the true positive left after WH013's precision fix: four real defects in `.github/workflows/inbox-steward-intake.yml`. | Defect | Effect | Fix | |---|---|---| | `update-ruleset` appended ` :: TODO: Apply rule update: …` lines to **`.hypatia-baseline.json`** | Corrupts the JSON the Governance baseline gate reads, on every critical/high suggestion | Suggestions are surfaced as `::notice` annotations only. The `data/ruleset-updates/*.json` record (`applied: false`) is unchanged. | | Every `git commit` / `git push origin HEAD` was masked (`2>/dev/null \|\| true`, `\|\| echo "::warning::…"`) and went **straight to main** | A refused write stayed green and bypassed review and signing | New `scripts/open-data-pr.sh`: branch, PR, squash auto-merge (the `estate-rescan.yml` pattern). Every failure exits non-zero. An empty change is skipped with `git diff --cached --quiet`. | | `record-report` committed only when `inputs.dry_run == "false"`; the input is **empty on `repository_dispatch`** | Every real dispatch ran as a dry run, while `update-ruleset` (`!= 'true'`) did write | Only an explicit `'true'` is a dry run, matching `update-ruleset`. | | The `.git-private-farm` dispatch discarded stderr and turned a refusal into a warning | A failed hand-off stayed green | Fails the job. | Context: none of this has produced data. `data/inbox-steward-reports/` and `data/ruleset-updates/` don't exist on main, and the last 17 runs (to 2026-07-05) all failed. ## Type of change - [x] 🐛 Bug fix - [x] 🔧 Build / CI / tooling ## How has this been verified? - `wh013_permission_starved_write/1` (with #942) on this tree returns `[]`, down from 1 `:warn` `unmask-credentialed-write` finding. - `actionlint` 1.7.7: the same 14 messages before and after (diffed). All 14 are pre-existing and sit in the `summary` job; see notes. - `shellcheck scripts/open-data-pr.sh`: clean. - I sandbox-ran `open-data-pr.sh` against a bare local remote with a stub `gh`: - publish pushes `inbox-steward/123-report` and calls `gh pr create … --base main` then `gh pr merge 77 --squash --auto --delete-branch` (rc 0); - with nothing to commit it exits 0 without a PR; - an unreachable remote gives rc 128. ## Checklist - [ ] My commits are **signed**. The session's signing shim is platform-mediated. - [x] I ran the project's own checks/tests locally and they pass. - [x] New files carry the correct SPDX (`scripts/open-data-pr.sh`: MPL-2.0). - [x] Docs are updated, and no public claim now overstates what the code does. - [x] I have not introduced a soundness hole. ## Notes for reviewers - **Needs:** `FARM_PAT` with Contents **and** Pull requests write on this repo, plus *Allow auto-merge* enabled. If either is missing, the job now fails visibly instead of warning. - **Pre-existing, not fixed here:** the `summary` job builds its step summary with `${{ … && echo '…' || echo '…' }}`. That is shell inside an expression, and actionlint rejects it ("parser did not reach end of input"). Left out to keep this PR to the write path. --- _Generated by [Claude Code](https://claude.ai/code/session_01CYR5bf5FBLS7cXQK72aVNM)_ Co-authored-by: Claude <noreply@anthropic.com>
RE005 flagged every `|| true` in a test-named step. Of hypatia's three findings, one was a rule false positive and two were masks that could go: - tests.yml:763 `trap 'kill "$STRESS_PID" 2>/dev/null || true' EXIT`. The EXIT trap runs after the step's status is decided, and under `set -e` an unmasked failing kill turns a PASSING step red (measured: rc 1). The rule now skips `trap …` lines. The same false positive is in echidna server-boot-gate.yml. - tests.yml:192 `$(find … -exec grep … | grep -v … || true)` also masked grep's exit 2, so an error passed the check (measured with a missing directory: old step PASS, new step rc 2). Rewritten with grep -r and an explicit status test (1 = no match = pass; >1 fails), comment lines removed with sed. - build-gossamer-gui.yml:189 diagnostic `ls … 2>/dev/null || true` right before `exit 1`: the mask hid the diagnosis and changed nothing. Tests: 2 new RE005 cases (trap ignored; a trap does not hide a real swallow); 42/42 pass, and the trap case fails against the previous module. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CYR5bf5FBLS7cXQK72aVNM
Five container references were pinned by mutable tag: - tests.yml (x2): arangodb:3.11 -> arangodb:3.11@sha256:e83f3705… - tests.yml (x2): docker.dragonflydb.io/dragonflydb/dragonfly:latest -> ghcr.io/dragonflydb/dragonfly:v1.40.2@sha256:18afa43e… - escript-soundness.yml: elixir:1.17-otp-27 -> elixir:1.17-otp-27@sha256:c65f0bdd… Digests are the multi-arch OCI index digests. elixir's was read from Docker Hub and from ECR Public's docker/library mirror, and both agree. arangodb's is from the ECR mirror because Docker Hub answered 429 to this session. Dragonfly's is from ghcr.io, upstream's GitHub registry, because docker.dragonflydb.io is not reachable from here. `latest` is replaced by the newest release tag, v1.40.2, so the pinned version is named. A digest that does not resolve fails the pull loudly. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CYR5bf5FBLS7cXQK72aVNM
|
Warning Review limit reachedYou've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository. Next included review available in 11 minutes. View limit details
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
- A file whose jobs the extractor cannot split is judged as one body and
is never credentialed. One job's PAT can no longer excuse another job's
write.
- `jobs: # comment` now splits jobs, so that fallback is rarer.
- A workflow-level `env:` credential (`GH_TOKEN: ${{ secrets.X }}`)
credentials every job.
- `secrets['NAME']` counts as an own credential;
`secrets['GITHUB_TOKEN']` does not.
- masked_write?/1 also sees `gh release create|upload|edit`.
Tests: 4 new cases; 64/64 pass, and the 4 fail against the previous
commit. WH013 on hypatia, standards and echidna: no findings, and no
workflow there has a commented `jobs:` header.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CYR5bf5FBLS7cXQK72aVNM
) ## Summary PR 4 of the baseline-gate stack, stacked on #942 (which now also carries #943). It clears all 3 RE005 findings at their source: one false positive in the rule, and two masks removed from workflows. | Finding | Verdict | Fix | |---|---|---| | `tests.yml:763` `trap 'kill "$STRESS_PID" 2>/dev/null \|\| true' EXIT` | **Rule false positive.** The EXIT trap runs after the step's status is decided. Under `set -e`, an *unmasked* failing `kill` turns a **passing** step red (measured: rc 1). The `\|\| true` is what keeps it honest. | RE005 skips `trap …` lines (`lib/rules/research_extensions.ex`). The same false positive is in echidna `server-boot-gate.yml:40`. | | `tests.yml:192` `$(find … -exec grep … \| grep -v … \|\| true)` | **Real, small mask.** It also swallowed grep's exit **2**: with a missing directory the old step reports PASS. | Rewritten as `grep -r --include … --exclude-dir=rules` with an explicit status test (1 = no match = pass, >1 fails). Comment lines are removed with `sed`, which can't exit 1 on "all filtered". | | `build-gossamer-gui.yml:189` `ls … 2>/dev/null \|\| true` right before `exit 1` | The mask did nothing except hide the diagnosis | Mask dropped | ## Type of change - [x] 🐛 Bug fix (a rule false positive, plus two CI masks) ## How has this been verified? - **Rule:** I compiled `research_extensions.ex` with `scanner_suppression.ex`, `cicd_rules.ex` and `security_errors.ex` standalone (Elixir 1.14; hex.pm is unreachable here) and ran `test/research_extensions_test.exs`: - **42/42** pass; - control: the new trap test fails against the previous module, and the "a trap does not hide a real swallow" test passes on both; - `mix format --check-formatted` passes. - **Blast radius (`re005_test_swallows_exit/1`, old → new):** - hypatia: 3 → **0**; - standards: 0 → 0; - echidna: 2 → 1 (the trap in `server-boot-gate.yml` clears; `live-provers.yml:475` is a real finding and remains). - **The `tests.yml` step**, extracted with `yq` and run on this tree: - clean → rc 0 PASS; - planted `believe_me` in `src/` → rc 1 FAIL; - planted in `lib/rules/` → PASS (excluded, as before); - `-- believe_me` comment → PASS; - a missing search directory → **rc 2 FAIL**, where the old step reports PASS. - `actionlint` 1.7.7: identical message sets before and after on both files (6 pre-existing in `tests.yml`, 0 in `build-gossamer-gui.yml`). ## Checklist - [ ] My commits are **signed**. The session's signing shim is platform-mediated. - [x] I ran the project's own checks/tests locally and they pass (scoped as above). - [x] New files carry the correct SPDX (none added). - [x] Docs are updated (the RE005 `@doc` states the trap rule). - [x] I have not introduced a soundness hole. Only whole `trap …` lines are skipped, and a real swallow elsewhere in the same step still fires (test). --- _Generated by [Claude Code](https://claude.ai/code/session_01CYR5bf5FBLS7cXQK72aVNM)_ Co-authored-by: Claude <noreply@anthropic.com>
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
…001) (#946) ## Summary PR 6 of the baseline-gate stack, stacked on #945. This is the first RE001 group: the 5 jobs that hold a token but talk **only to the GitHub API**. Each now starts with `step-security/harden-runner@v2.22.2`, using `egress-policy: block` and the allowlist `api.github.com:443`, `github.com:443`. | Workflow / job | Credential | Network surface (from the steps) | |---|---|---| | `labels.yml` sync | `GITHUB_TOKEN` | `gh label` / `gh api` | | `label-triage.yml` triage | `GITHUB_TOKEN` | `gh issue` / `gh label` / `gh api` | | `dependabot-automerge.yml` automerge | `GITHUB_TOKEN` | `dependabot/fetch-metadata`, `gh pr` | | `roadmap-sync.yml` sync | App private key | `actions/create-github-app-token`, `gh api` (REST + GraphQL) | | `ci-health-sweep.yml` sweep | `HYPATIA_DISPATCH_PAT` | `actions/checkout`, `scripts/ci-health/*.sh` (`gh repo list`, `gh issue`) | RE001 on this tree: **18 → 13**. RE002 (audit mode on a protected branch): 0. ## ⚠ Blocked on an owner step before merge `actions.lock` is tool-owned, and this session has no valid `gh` token. **Run `gh actions-lock` on this branch** to add the `step-security/harden-runner@v2.22.2` record (tag commit `dc2769ab…`, from `git ls-remote`). Until then these five workflows fail at *Set up job*, and Governance's *Actions lockfile verify* is red on this PR. ## Type of change - [x] 🔧 Build / CI / tooling (egress containment) ## How has this been verified? - `actionlint` 1.7.7: identical message counts before and after on all 5 files (0/0/1/3/0, all pre-existing). - `re001_missing_harden_runner/1` on this tree: the 5 jobs clear, 13 remain. `re002_harden_runner_audit_mode/1`: 0. - **Not yet verified:** a block-mode run of each job. The allowlist is derived from the steps, not measured. A job that needs another endpoint now **fails loudly** on its next run. These are scheduled, dispatch or event workflows, so after the lock regen please `workflow_dispatch` `labels`, `ci-health-sweep` and `roadmap-sync` once. I'll widen any list from the blocked-endpoint annotations. ## Checklist - [ ] My commits are **signed**. The session's signing shim is platform-mediated. - [x] I ran the project's own checks/tests locally and they pass. - [x] New files carry the correct SPDX (none added). - [x] Docs are updated, and no public claim now overstates what the code does. - [x] I have not introduced a soundness hole. ## Notes for reviewers - **Privacy:** harden-runner's agent reports run telemetry to StepSecurity's service, as raised when this option was chosen. - **Remaining 13 RE001 jobs** need a measured endpoint list (toolchains, registries, SMTP, artifact storage): clusterfuzzlite ×2, escript-soundness, estate-rescan, hypatia-remediation-sweep, inbox-steward-intake ×4, push-email-notify, release, security-policy, tests. Plan: one audit-mode measurement run per group, then block mode with the measured list. --- _Generated by [Claude Code](https://claude.ai/code/session_01CYR5bf5FBLS7cXQK72aVNM)_ Co-authored-by: Claude <noreply@anthropic.com>
|
Heads-up before merging: #946 is now part of this PR. #946 merged into this branch ( If this merges up the stack (into #942, then main) as it is:
Needed first: a Generated by Claude Code |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 259115355b
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| steps: | ||
| # Egress allowlist (RE001): this job talks only to the GitHub API. | ||
| - name: Harden runner | ||
| uses: step-security/harden-runner@v2.22.2 |
There was a problem hiding this comment.
Regenerate the lockfile for the new action references
Regenerate and commit .github/workflows/actions.lock alongside the new action references in ci-health-sweep.yml, dependabot-automerge.yml, and roadmap-sync.yml. At this commit the lock still lists only each workflow's previous actions and contains no step-security/harden-runner@v2.22.2 dependency, so the repository's path-scoped lock verification rejects these mutable references and can leave the affected workflows at startup_failure.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Agreed. This is the open blocker for ci-health-sweep.yml, dependabot-automerge.yml and roadmap-sync.yml. actions.lock is tool-owned, and this session has no gh credential, so the owner has to regenerate it with gh actions-lock on this branch before this PR merges. It can't be fixed from inside this PR, so the thread stays open until the lock commit lands.
Generated by Claude Code
| steps: | ||
| # Egress allowlist (RE001): this job talks only to the GitHub API. | ||
| - name: Harden runner | ||
| uses: step-security/harden-runner@v2.22.2 |
| steps: | ||
| # Egress allowlist (RE001): this job talks only to the GitHub API. | ||
| - name: Harden runner | ||
| uses: step-security/harden-runner@v2.22.2 |
| steps: | ||
| # Egress allowlist (RE001): this job talks only to the GitHub API. | ||
| - name: Harden runner | ||
| uses: step-security/harden-runner@v2.22.2 |
labels.yml and label-triage.yml are dispatched to estate repositories whose actions.lock does not list them. Their headers say "NO `uses:` ANYWHERE, DELIBERATELY": any action makes those copies fail at startup. #946 added step-security/harden-runner to both. That is removed here, and both files are byte-identical to main again. Their RE001 findings return. They need a different answer than an action (an owner decision, raised on the PR). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CYR5bf5FBLS7cXQK72aVNM
`governance / Actions lockfile verify` fails on this PR, and on main since #945. Of the verifier's findings, only the three `step-security/harden-runner@v2.22.2` "not-pinned" records are not accepted by standards' wrapper. #948 removes those steps. This is the same three-file change; it no-ops once #948 (or a lock regeneration) lands on main. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CYR5bf5FBLS7cXQK72aVNM
`governance / Actions lockfile verify` fails on this PR, and on main since #945. Of the verifier's findings, only the three `step-security/harden-runner@v2.22.2` "not-pinned" records are not accepted by standards' wrapper. #948 removes those steps. This is the same three-file change; it no-ops once #948 (or a lock regeneration) lands on main. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CYR5bf5FBLS7cXQK72aVNM
## Summary **Regression fix for main.** #945 brought in `step-security/harden-runner@v2.22.2` (from #946) in 3 workflows, but `actions.lock` has **no record** for it. On main, `grep -c harden-runner .github/workflows/actions.lock` gives 0. GitHub rejects unlocked actions at startup, so: - **Dependabot Auto-Merge ends in `startup_failure` on every PR** since 13:34 UTC, including Dependabot's own #937 (runs 39431585232, 39431759488, 39432798311). - `ci-health-sweep` and `roadmap-sync` will fail the same way on their next scheduled or dispatched run. This PR restores `ci-health-sweep.yml`, `dependabot-automerge.yml` and `roadmap-sync.yml` byte-for-byte to their state before #945 (`551b4c4`), removing only the 8-line harden-runner step from each. **Alternative:** if you'd rather keep the hardening, close this PR and run `gh actions-lock` on main. That adds the record (tag `v2.22.2` → `dc2769ab…`) and fixes the same failure. Either fixes main; this PR is the one I can push without a `gh` credential. The RE001 findings for these 3 jobs come back with this PR. Re-adding harden-runner and the lock regeneration should land in **one** change. ## Type of change - [x] 🐛 Bug fix (CI regression) - [x] 🔧 Build / CI / tooling ## How has this been verified? - `git diff 551b4c4 -- <3 files>` on this branch is empty: the files are identical to their pre-#945 state. - `actionlint` 1.7.7 gives 0, 1 and 3 messages, the same pre-existing counts as before #945. - On main, `dependabot-automerge.yml` runs `startup_failure` on each of its last 3 runs (listed above). This PR's own Dependabot Auto-Merge run is the end-to-end check. ## Checklist - [ ] My commits are **signed**. The session's signing shim is platform-mediated. - [x] I ran the project's own checks/tests locally and they pass. - [x] New files carry the correct SPDX (none added). - [x] Docs are updated, and no public claim now overstates what the code does. - [x] I have not introduced a soundness hole. --- _Generated by [Claude Code](https://claude.ai/code/session_01CYR5bf5FBLS7cXQK72aVNM)_ Co-authored-by: Claude <noreply@anthropic.com>
## Summary This is the last step of the baseline-gate stack. `governance.yml` and `hypatia-scan.yml` pass `hypatia-ref: aa34a4c` (#940), which predates the rule fixes that clear this repo's own false positives. This PR moves both to **hypatia `main` at `23dc74d`**, which contains: - #938: ActionsLock accepts v0.0.3 locks (already in `aa34a4c`); - #942: WH013 judges writes per job and skips PAT/app-token writes. This clears `estate-rescan.yml`; `inbox-steward-intake.yml` was also repaired in #943; - #947: RE005 skips `trap` handlers, and job extraction survives comments between jobs. `uses:` refs are unchanged, so `actions.lock` is untouched. ## What this PR's governance run will show This PR's own `governance / Validate Hypatia Baseline` run is the measurement: it builds the scanner at `23dc74d`. The jump from `aa34a4c` also brings #931's new rules (WF026, WF027, PIN-003), which haven't been measured on this repo yet. hex.pm is blocked in my session, so I can't build the escript to pre-run them. **Expected remaining findings, by class:** - **RE001 (harden-runner):** most of the original 18 jobs, including the 2 dispatched label workflows, which must stay action-free. - These need an owner decision; the harden-runner steps were removed again in #948 because the lock has no record. - **WH013, RE004, RE005, `invalid_actions_lock`:** expected 0. I'll post the measured list here once the run completes. ## Type of change - [x] 🔧 Build / CI / tooling ## How has this been verified? - `actionlint` 1.7.7 is clean on both files. - `23dc74d` is `origin/main` at the time of writing. `git log aa34a4c..23dc74d` contains #942, #947, #945, #931, #941 and #936. ## Checklist - [ ] My commits are **signed**. The session's signing shim is platform-mediated. - [x] I ran the project's own checks/tests locally and they pass. - [x] New files carry the correct SPDX (none added). - [x] Docs are updated (the pin comment names what it carries). - [x] I have not introduced a soundness hole. --- _Generated by [Claude Code](https://claude.ai/code/session_01CYR5bf5FBLS7cXQK72aVNM)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
Summary
PR 5 of the baseline-gate stack, stacked on #944. It clears all 5 RE004 findings by pinning each container by its multi-arch OCI index digest.
tests.yml×2arangodb:3.11arangodb:3.11@sha256:e83f3705…1c1f16tests.yml×2docker.dragonflydb.io/dragonflydb/dragonfly:latestghcr.io/dragonflydb/dragonfly:v1.40.2@sha256:18afa43e…4065c9escript-soundness.ymlelixir:1.17-otp-27elixir:1.17-otp-27@sha256:c65f0bdd…467e08Type of change
How has this been verified?
docker/librarymirror return the same index digest. That agreement is also the cross-check that the mirror is digest-identical.ghcr.io/dragonflydb/dragonfly.docker.dragonflydb.iois not reachable from this session.actionlint1.7.7: the same 6 pre-existing messages ontests.yml, none onescript-soundness.yml.Checklist
Notes for reviewers
Two judgment calls to confirm or reverse:
docker.dragonflydb.io→ghcr.io(upstream's GitHub registry). On ghcr,latestandv1.40.2are different builds, and I couldn't read their labels (the blob host is blocked here). So I pinned the newest named release, v1.40.2, rather than freezing an unnamedlatestbuild. If you'd rather keepdocker.dragonflydb.io, its digest needs resolving from a host that can reach it.github-actionsecosystem doesn't bump service-container digests. Refresh one withcrane digest <image>:<tag>(ordocker buildx imagetools inspect).Generated by Claude Code