Repository navigation
fix(rules): scope RE001 to credentials exposed to code the job cannot vouch for - #966
Conversation
… vouch for RE001 fired on every job that named any `secrets.*` without harden-runner. That made it noise: 18 of 18 jobs here, 18 in standards, 5 in echidna, and the gate cannot tell the one that matters from the rest. A job is now reported only when it holds a real credential (a secret other than GITHUB_TOKEN, which is the ambient `github.token` every job holds) AND has an exposure: an untrusted-input trigger (pull_request_target, issue_comment, issues, workflow_run, ...), a checkout or git fetch of a pull-request or triggering-run head (naming the head SHA in env/if/ concurrency is not one), a piped installer, or a dependency install/build (`npm ci`, `cargo build`, `mix deps.get`, ...). Severity is :high when the credential and the code exposure share one step, :warn when they share only the job, since an earlier step can still poison what a later credentialed step runs. Measured with the old and new rule: repo old new remaining hypatia 18 2 estate-rescan rescan (warn), tests integration-tests (high) standards 18 2 hypatia-scan-reusable scan, mirror-radicle (both warn) echidna 5 0 Tests: 12 new RE001 cases (each exposure kind, the ambient token, decoys for a step name, `permissions: issues:`, a head SHA in env/if/concurrency and a comment on `on:`, same-step escalation, mixed ambient and real secret); 8 of them fail against the old rule. Existing fixtures gain an `npm ci` step after the credential so they keep testing the runner, hardening and comment logic rather than passing vacuously. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01J4QvDfStHgMdZGTeqaBgM1
|
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 2 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 |
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. |
|
Add Carrot credits or activate Agent usage billing to use Autofix |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 52a11782ea
ℹ️ 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".
…ntions it Four precision gaps Codex found on #966: - Secret names are case-insensitive, so `secrets.github_token` is the ambient token too; the comparison now upcases the name. - An inline trigger map (`on: {push: {paths: [issues/**]}}`) contributed every word as an event, so `issues` from a path filter read as an issues trigger. Only the map's top-level keys are events now. - Exposures were matched on any non-`name:` line, so an `env:` value or a YAML comment mentioning `npm ci` counted as executed. Fetched code is now matched only in a step's `run:` script (inline or block scalar, comments removed); a head counts as fetched on a checkout's `ref:`/`repository:` or a git checkout/fetch/switch inside `run:`. - That script is matched joined, so a folded `run: >-` that splits `npm` and `ci` across lines is recognised. Five new tests (one per gap, plus inline-map top-level keys); the four gap tests fail against the previous commit. Findings on hypatia, standards and echidna are unchanged (2 / 2 / 0). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01J4QvDfStHgMdZGTeqaBgM1
…are `-` steps Four more gaps from Codex's review of #966: - A credential in the job's own `env:` is in every step's environment, so a step that builds dependencies holds it directly: :high, pointing at the job-level reference. - A job-level `if:` made only of `github.event_name == '<event>'` tests joined by `||` restricts which of the workflow's untrusted events reach the job. Any other condition is not interpreted and the job is judged on all of them. - `workflow_run` names its head `head_sha` / `head_branch` / `head_repository`, not `head.*`; those count as a head checkout now. - `step_indent/2` required whitespace after the step dash, so a step opened by a bare `-` was not found. It now accepts a bare dash, as the step chunker already did; harden-runner detection shares the helper. Four new tests, each failing against the previous commit. Findings on hypatia, standards and echidna are unchanged (2 / 2 / 0). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01J4QvDfStHgMdZGTeqaBgM1
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 652ac168b7
ℹ️ 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".
- A step that sets the same variable as a job-level `env:` secret replaces it, so that step does not hold the credential: no :high from it. - `ref:`/`repository:` naming a head is a checkout only on an `actions/checkout` step; a reporting action's input merely names it. - A git checkout/fetch/switch of the head is matched within one command of the joined `run:` script, so a folded `run: >-` that splits it across lines is caught, and `git fetch origin main && echo $HEAD_REF` is not. - `on: &anchor` followed by a block is read as that block. Five new tests, each failing against the previous commit. Findings on hypatia, standards and echidna are unchanged (2 / 2 / 0). Not changed: telling an install command from the same words inside quoted shell data (`echo 'run npm ci'`) needs shell parsing; stripping quoted strings would hide `bash -c "npm ci"` and break KYAML inline `run:` values. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01J4QvDfStHgMdZGTeqaBgM1
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 496f6f600a
ℹ️ 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".
- A flow trigger mapping that continues past the `on:` line is read until
its brackets balance.
- A piped installer is matched within one command of the joined script
(`[^|;&]*` instead of `[^\n|]*`), so a folded `curl … | bash` split across
lines is caught.
- Only the step's own `run:` property is its script; an `env:` variable named
`run` is a nested key and is ignored.
- A checkout's `ref:`/`repository:` is recognised in flow form
(`with: {ref: …}`) as well as block form.
Four new tests, each failing against the previous commit. Findings on
hypatia, standards and echidna are unchanged (2 / 2 / 0).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J4QvDfStHgMdZGTeqaBgM1
Overriding one job-level secret in a step left the step holding the others, but only the first job-level credential was considered, so the direct exposure was downgraded to :warn. Every job-level credential is now checked against the step's overrides. New test fails against the previous commit; findings on hypatia, standards and echidna are unchanged (2 / 2 / 0). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01J4QvDfStHgMdZGTeqaBgM1
…968) ## Summary Part of the stack to turn `governance / Validate Hypatia Baseline` green (see #966). Under the risk-scoped RE001, `estate-rescan` is one of the two real findings left in this repo. It is also a concrete credential exposure: - The `rescan` job built `panic-attack` from an **unpinned `HEAD`** and ran it over about 300 repositories. - `HYPATIA_DISPATCH_PAT` was used in the same job. - `Checkout verisimdb-data` cloned with the PAT **in the URL**, so the PAT sat in that clone's `.git/config`, readable by every later step, the freshly built scanner included. The workflow is now three jobs: | job | credential | runs fetched code | egress | |---|---|---|---| | `preflight` (writability probe, unchanged logic) | PAT | no (`gh api` only) | harden-runner `block`: `api.github.com` | | `scan` | **none**: the repo list is public metadata, read with the ambient `github.token` | yes: `panic-attack` **pinned at `ac68bba`** (its current HEAD) | unrestricted | | `publish` (commit + two squash-merged PRs, unchanged logic) | PAT | no | harden-runner `block`: `github.com`, `api.github.com`, the artifact service and its blob storage | The scans move from `scan` to `publish` as an artifact with 1-day retention. **Behaviour preserved:** - Dry runs skip `preflight` and `publish`. - A run that scans nothing is still a green no-op: no artifact and no publish. - The run still fails fast if the PAT can't write. ### `actions.lock` The lock is tool-owned. As in #958, a one-off workflow on this branch (run 40433509902) did the regeneration: - It ran `gh actions-lock` v0.2.0 plus the standards `333b06a` repair chain. - `update-actions-lock.sh --verify-local`, `check-lock-sync.sh` and the governance gate's `check-actions-lock-gate.sh` all passed. - It printed the lock's diff and sha256. I applied that diff, and the file matches the run's `LOCK-SHA256` exactly: `42e5e208…a286`. The one-off is removed in the same commit. The regenerated lock changes three things: 1. **This PR's entries:** `harden-runner@v2.22.2` → `dc2769a`, `upload-artifact@v7.0.2`, `download-artifact@v8.0.2` for `estate-rescan.yml`. 2. **Main's lock is stale:** #958 squash-merged without its lock commit. Main's workflows call `metadatastician/standards@333b06a`, but the lock still records `hyperpolymath/standards@333b06a`. The tool re-keys all five callers.⚠️ This overlaps the session unblocking estate CI after the standards move. Whichever lands first, the other PR should take main and regenerate. 3. It records the leftover `zz-one-off-lock-regen.yml` from #936, which is still in the tree. ## Type of change - [x] 🔒 Security / least privilege - [x] 🔧 Build / CI / tooling ## How has this been verified? - `actionlint` 1.7.7 with shellcheck 0.9.0 is clean, except SC2020 on the moved `tr ' ,' '\n\n'` line, which is identical on `main` and intentional. - Hypatia's own rules on this branch, compiled standalone: - RE001 v2 no longer reports `estate-rescan`. The one RE001 left is `tests.yml`, which #967 removes. - WorkflowHardening shows only the 4 pre-existing `:info` WH007. - The 4 WH004 hits on the new `uses:` cleared once the lock recorded them. - **Not yet verified:** - The two `block` allow-lists only meet real traffic on the first live (non-dry) run. A wrong entry would fail loudly at publish, not corrupt data. - A `workflow_dispatch` dry run on this branch would exercise `scan` and the artifact upload without publishing. I haven't triggered one. - Governance CI is pending on the allow-list fix. ## Notes for reviewers - **Bumping `panic-attack`** is now a deliberate one-line change (`PA_SHA`) instead of following HEAD. That trades freshness for a reviewable supply chain. - **Beyond this repo:** - gitbot-fleet's dispatch-runner fires `repository_dispatch: estate-rescan` and is unaffected; - the `verisimdb-data` contract is unchanged. --- _Generated by [Claude Code](https://claude.ai/code/session_01J4QvDfStHgMdZGTeqaBgM1)_
…tests (#967) ## Summary Part of the stack to turn `governance / Validate Hypatia Baseline` green (see #966). `Integration Tests` passed `secrets.TEST_GITHUB_TOKEN` into the env of `cargo test --workspace` and `mix test --include integration`. Every dependency's build script and test therefore ran with the credential in reach. Under the risk-scoped RE001 (#966) this is the repo's only **`:high`** finding: the credential and the dependency build share a step. **Nothing reads the token:** - No `.rs`, `.ex`, `.exs` or `.sh` file names `TEST_GITHUB_TOKEN`. - `integration/src/lib.rs` reads only `ARANGODB_*`, `DRAGONFLY_URL` and `MOCK_{GITHUB,GITLAB,BITBUCKET}_URL`. This PR deletes the two env lines. That is least privilege, adds no new action and needs no lock change. **Deviation from the agreed plan:** the plan was to isolate the token and also add harden-runner. With no consumer there is nothing to isolate. After this change the job holds no credential, so it has nothing for harden-runner to protect, and adding it would only mean maintaining an egress allow-list for docker, cargo and hex. ## Type of change - [x] 🔒 Security / least privilege - [x] 🔧 Build / CI / tooling ## How has this been verified? - `grep -rIl TEST_GITHUB` over the tree (excluding deps, `_build`, target) finds only `tests.yml`. - `actionlint` 1.7.7 with shellcheck 0.9.0 gives the same 24 messages as on `main`, all pre-existing. - RE001 v2 (#966) on this branch reports one finding: `estate-rescan.yml`, which #968 fixes. Before this change it reported two. - **Pending CI:** this PR's own `Integration Tests` run is the end-to-end check that nothing needed the token. ## Notes for reviewers - **Owner follow-up, outside the repo:** once this merges, the `TEST_GITHUB_TOKEN` repository secret can be deleted in Settings → Secrets. - **Noticed, not changed:** the workflow sets `ARANGODB_USER`, but `integration/src/lib.rs` reads `ARANGODB_USERNAME`. It falls back to its default, so the variable is ignored. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01J4QvDfStHgMdZGTeqaBgM1
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 2194d63b0e
ℹ️ 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".
| case Regex.run(~r/^\s*([A-Za-z_][A-Za-z0-9_]*)\s*:/, job_line) do | ||
| [_, key] -> | ||
| Enum.any?(step, fn {line, _} -> Regex.match?(~r/^\s*#{key}\s*:/, line) end) |
There was a problem hiding this comment.
Parse flow-style job env bindings before overrides
When a job declares credentials using a flow mapping such as env: {NPM_TOKEN: ${{ secrets.NPM_TOKEN }}}, this regex extracts env as the variable name. If an npm ci step has any step-level env: block—even one containing only an unrelated variable—the later search finds that env: header and incorrectly concludes that NPM_TOKEN was overridden. The step still inherits the secret, but RE001 is downgraded from :high to :warn; extract each binding from the flow mapping and compare its actual key.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Valid, and it's the same class as the flow-step finding: a flow env: {…} is opaque to the line reader. The #971 model reads env bindings as {key, value, line}, so override matching compares real keys. Tracked there. #966 is paused while its RE001 scope is reconciled with #964.
Generated by Claude Code
| case Enum.split_while(step, fn {line, _} -> | ||
| case Regex.run(~r/^(\s*(?:-\s+)?)run:/, line) do | ||
| [_, prefix] -> String.length(prefix) != key_column | ||
| _ -> true | ||
| end |
There was a problem hiding this comment.
Accept flow-style step maps when extracting run
A valid flow-style step such as - {run: npm ci, env: {TOKEN: "${{ secrets.NPM_TOKEN }}"}} is grouped as a step, but this search requires run: to appear directly after the sequence prefix and therefore never recognizes {run:. step_code_exposures/1 consequently sees no fetched code and RE001 emits no finding despite the secret being supplied to the install command; parse the top-level keys of a flow-style step mapping as well.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Valid YAML shape, and not fixed here. A flow-style step map is a collection the line reader can't see into. With the kyq parser behind #971 it's an ordinary map, and the prototype reads - {run: …, env: {…}} structurally. Tracked in #971's corpus. #966 is paused while its RE001 scope is reconciled with #964.
Generated by Claude Code
|
|
||
| # Code fetched at run time: piped installers, package installs, and builds | ||
| # or test runs that execute dependencies' build scripts. | ||
| @re001_fetched_code ~r/(?:curl|wget)\b[^|;&]*\|\s*(?:sudo\s+)?(?:ba|z)?sh\b|\b(?:npm|pnpm|yarn)\s+(?:i|install|ci)\b|\bnpx\s|\bpip3?\s+install\b|\b(?:pipx|uvx)\s|\bcargo\s+(?:install|build|test|run)\b|\bgo\s+(?:install|build|test|run)\b|\bgem\s+install\b|\bbundle\s+install\b|\bmix\s+(?:deps\.get|compile|test)\b|\bdeno\s+(?:run|install)\b|\bbun\s+(?:i|install|run)\b|\bopam\s+install\b|\bcabal\s+(?:install|build|test)\b|\bstack\s+(?:build|test)\b/ |
There was a problem hiding this comment.
Collapse shell line continuations before matching installs
On an Ubuntu runner, a literal script written as npm \ followed by ci on the next line executes npm ci, but the raw joined script still contains the backslash between npm and ci, so this pattern does not match it. A credentialed job whose only exposure uses this common shell continuation is therefore omitted from RE001; normalize backslash-newline continuations before applying the command patterns.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Valid, and not fixed here. This is a shell-level shape, \ continuation, that the line reader doesn't model. #971 moves workflow rules onto a parsed model with the metadatastician/kyq KYAML reader. A kyq-backed prototype already matches this rule on hypatia, standards and echidna. Continuations belong to that model's run normalisation, and a fixture for this case goes into #971's corpus. #966 is also paused while its RE001 scope is reconciled with #964 (ADR-009).
Generated by Claude Code
| [_, condition] <- Regex.run(~r/^\s*if:\s*(.*?)\s*$/, line), | ||
| condition = condition |> String.replace(~r/^\$\{\{\s*|\s*\}\}$/, ""), | ||
| test = ~S"github\.event_name\s*==\s*'([a-z_]+)'", | ||
| true <- Regex.match?(~r/^\s*#{test}(?:\s*\|\|\s*#{test})*\s*$/, condition) do |
There was a problem hiding this comment.
Unquote YAML scalars before parsing job event gates
The remaining quoted-scalar case provides fresh evidence that event-gated jobs are still misclassified: with if: "${{ github.event_name == 'push' }}", YAML removes the outer quotes before GitHub evaluates the same push-only condition, but this raw-text parser leaves them in place, fails both anchored regexes, and exposes the job to every workflow event. In a workflow also triggered by issue_comment, that produces a false RE001 finding for a job that cannot run on the untrusted event.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Valid. YAML removes the quotes before GitHub evaluates the condition, and the line reader doesn't. Under #971's kyq model, if arrives as the resolved scalar, so the gate parser sees the unquoted condition. Tracked there. #966 is paused while its RE001 scope is reconciled with #964.
Generated by Claude Code
| Enum.find(job_lines, fn {line, _} -> | ||
| Regex.match?(~r/\$\{\{\s*secrets\.[A-Za-z_][A-Za-z0-9_]*/, line) | ||
| end) | ||
| credential_lines = Enum.filter(job_lines, &credential_line?/1) |
There was a problem hiding this comment.
Include workflow-level credentials in each job
When a real secret is declared in the workflow's top-level env: and a local job runs npm ci, this job-only filter finds no credential and RE001 emits nothing, even though GitHub makes workflow-level environment variables available to the steps of every job (workflow syntax). RE007 may separately warn about broad secret scope, but it does not report the direct fetched-code exposure or missing hardener; carry top-level credential bindings into each job while respecting job and step overrides.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Valid, and it is a semantic gap rather than a shape: workflow-level env: secrets reach every job's steps, and this rule ignores them. I'm leaving it open because it interacts with #964 (ADR-009), which rewrites the same function and decides which secret-reading jobs are in scope. The fix (inherit top-level bindings, respecting job and step overrides) lands in whichever implementation survives that reconciliation.
Generated by Claude Code
| defp re001_exposures(steps, untrusted_events) do | ||
| code = Enum.flat_map(steps, &step_code_exposures/1) |
There was a problem hiding this comment.
Respect step order for cross-step exposures
When a credential exists only in an early step and the sole npm ci or head checkout occurs later, this aggregation still reports a :warn exposure merely because both occur somewhere in the job. Step-scoped environment values apply only while that step executes, and each step runs in its own process (workflow syntax), so later untrusted code cannot poison an already-completed credential consumer; only treat separate steps as an exposure when the untrusted-code step can affect a subsequent credential-bearing step.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Valid refinement. Untrusted code can only poison credential-bearing steps that run after it, so a credential used only before the first exposure isn't exposed across steps. That changes what :warn means, so I'm leaving it open for the same reason as the workflow-env thread: the #964 (ADR-009) reconciliation decides RE001's scope and tiers. Step order is a natural field once the #971 model lands.
Generated by Claude Code
#262) ## Summary This moves the template's pinned Hypatia commit from `7c10e5d` (#261) to `be98f74`, the merge commit of hyperpolymath/hypatia#970. It brings in four scanner changes: - **Secret-scanning alerts:** fetches follow pagination, time out after 30 s per request, and report an error instead of a truncated list. - **SC-010 (local Scorecard licence check):** accepts `LICENSE.adoc` and REUSE `LICENSES/`, so it agrees with root hygiene. Before, one scan could report both "licence present" and "No LICENSE file found". - **REUSE `LICENSES/`:** a file counts only if its name is an SPDX identifier (vendored list, SPDX 3.29.0) or a `LicenseRef-` id. A stray `LICENSES/notes.txt` no longer passes. - **RE001:** now scoped to credentials exposed to code the job cannot vouch for (hyperpolymath/hypatia#966). ## Changes - `.github/workflows/hypatia-scan.yml`: `HYPATIA_SHA`. The cache key follows it. - `.github/workflows/static-analysis-gate.yml`: the escript cache key and the fetch. - `.github/workflows/dogfood-gate.yml`: the checkout `ref` and the oracle cache key. `7c10e5d` appeared nowhere else in the repository. ## RSR Quality Checklist ### Required - [x] Tests pass: this PR's own `Hypatia Security Scan`, `static-analysis-gate` and `dogfood-gate` runs exercise the new pin. Those are the checks that matter here. - [x] Code is formatted (pin values only; no structural change) - [x] Linter is clean (all three workflows still parse) - [x] No banned language patterns - [x] No `unsafe` blocks without `// SAFETY:` comments - [x] No banned functions - [x] SPDX licence headers present (unchanged files) - [x] No secrets, credentials, or `.env` files included ### As Applicable None apply: this is a pin bump with no change to state, integrations, architecture or dependencies. ## Testing - Every `7c10e5d` occurrence is replaced (`grep` finds none left), and the three workflows parse. - The new scanner's REUSE and licence behaviour is unit-tested in hyperpolymath/hypatia#970: 84 of 84 pass locally, and CI there was green before merge. - I could not run the full scan locally, because hex.pm is unreachable from this environment. The scan runs on this PR are the end-to-end check. **Downstream:** repositories synced from the template, including echo-types since hyperpolymath/echo-types#356, keep `7c10e5d` until they re-sync. --- _Generated by [Claude Code](https://claude.ai/code/session_01P4yFkxW5Heb8p2YW7ZYs9F)_ Co-authored-by: Claude <noreply@anthropic.com>
The merge of main (#966) into this branch, 7b1e5f9, left re001_missing_harden_runner/1 spliced from both versions; the module did not compile. This rebuilds it from main's version with #964 on top, as the two sessions agreed and the owner ruled on 2026-10-27: - Scope (ADR-009): every locally run job that reads a real secret. - Satisfied by harden-runner, or by a deed entry whose :secrets equal the secrets the job reads, exposed or not. - An undeclared job is :high when the credential shares a step with fetched or untrusted code (#966's step, env and trigger model), and :warn otherwise, with or without an exposure. The exposure list is in the reason and in detail.exposures. - Drift and stale stay :warn. A computed-key read stays undeclarable. - Secrets are read by #964's ${{ }} reader; #966's credential_line? is gone, so the exposure model sees the same secrets the deed is compared against (index form, any case, folded expressions). Tests: main's #966 suite runs unchanged except where it asserted no finding for a job without an exposure: those 9 now assert a :warn finding with no exposures, which still checks that each decoy is not mistaken for one. #964's deed suite is restored. 145 tests pass (research_extensions_test + deed_test), kyaml_workflow_test 10/10. RE001 on hypatia, standards, template, choreographic-types: 9/15/0/1. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01J4QvDithtn5Xk5MbSPm4Px
## Summary Step 2 of the stack that turns `governance / Validate Hypatia Baseline` green, after #966, #967 and #968. Both of hypatia's scanner callers pinned `hypatia-ref` to `23dc74d`. A rule fix merged to main therefore reached hypatia's own gate only after a hand bump. #966's risk-scoped RE001 is on main now, but the gate would still build the old rule. This PR replaces the literal with: ```yaml hypatia-ref: ${{ github.event.pull_request.base.sha || github.sha }} ``` | event | scanner commit | |---|---| | `pull_request` | the base-branch commit (main) | | `push`, `schedule`, `workflow_dispatch` | the commit being run | - **Never the PR head.** A PR cannot weaken the rules that gate it, which is the property the fixed pin was protecting. - **Always a 40-hex SHA**, so the reusables' validation passes without change. `HEAD` canary mode is not used, and nothing calls `git ls-remote`. - **Scope:** only hypatia's own callers change. Every other estate repo keeps the reusable's vetted default, so a bad main can't turn the estate red. That was the 09-12..14 and 09-26..30 outage class the default exists for. - **Escape hatch:** if main ships a broken rule, the PR that fixes it can pin a known-good SHA here temporarily. Only the `with:` input and its comment change. `uses:`, the allow-list and `actions.lock` are untouched. ## How has this been verified? - `actionlint` 1.7.7 is clean on both files. - **Differential self-scan:** all 15 `lib/rules` modules with `scan/1,2` were run over `origin/main` and this branch, keeping findings on the two files. Both give the same 10 findings (the ones already on main), with none added or removed. - **Not yet verified:** how GitHub evaluates the expression at run time. Governance still hits `startup_failure` on every PR until the Actions allow-list accepts `metadatastician/standards`, which is handled separately. `Hypatia Security Scan` can confirm the `pull_request` branch of the expression as soon as it runs here. ## Beyond this repo Moving the reusables' own `hypatia-ref` default past `51ab649`, so other repos get #966 too, is a standards decision. This PR does not make it. --- _Generated by [Claude Code](https://claude.ai/code/session_01J4QvDfStHgMdZGTeqaBgM1)_
…_TOKEN (standards ADR-009) (#964) ## Summary This implements the consumer side of metadatastician/standards#1352 (ADR-009). The estate declares egress in the repo deed instead of adopting `step-security/harden-runner` everywhere. It sits on top of #966, which merged first: #966's exposure model is kept as RE001's severity layer. ## RE001 after this PR - **Scope (ADR-009):** every locally run job that reads a real secret, including one it inherits from the workflow's top-level `env:`. `secrets.GITHUB_TOKEN` is the job token and doesn't count. - **Satisfied by:** harden-runner (owner/repo matched case-insensitively, as GitHub resolves it), or an entry in the `(egress …)` clause of `<repo>_chora.deed` whose `:secrets` equal the secrets the job reads. A declaration satisfies an exposed job too (owner ruling, 2026-10-27). Declarations are checked even for a hardened job. - **Severity:** a job is `:high` when the credential shares a step with fetched or untrusted code (#966's step, env and trigger model), otherwise `:warn`, with or without an exposure (owner ruling, 2026-10-27). A hardened job is always `:warn`. The exposure list is in the reason and in `detail.exposures`. - **Findings:** - undeclared job: the job's severity; - **drift** (declared and read secrets differ): the job's severity, so an incomplete declaration never lowers a finding; - **computed key** (`secrets[env.X]`, `toJSON(secrets)`, `secrets.*`): the job's severity, and no declaration can satisfy it; - **enforcement mismatch**: the deed declares `harden-runner-audit` or `harden-runner-block` but the job has no harden-runner step, or declares `harden-runner-block` while the step runs `egress-policy: audit` (or the default, which is audit); - **stale** declaration (job gone, runs elsewhere, or reads no real secret): `:warn`, reported against the deed. - **Secret reading:** across every `${{ … }}` in the job, even one that a folded scalar splits over lines. The `secrets.NAME` and `secrets['NAME']` forms both count, and the context matches case-insensitively. String literals are skipped, and only the top-level context counts. #966's exposure model uses the same reader. - **An unreadable deed** declares nothing, and the finding says why. `Hypatia.Deed` (`lib/hypatia/deed.ex`) reads `deed.abnf` v1.2.1 (metadatastician/standards#1359, merged) and the egress clause profile. It accepts a deed only if both the grammar and `deed_lint.js` would, including the gate's filename-stem, dispatch and boolean-word checks. ## Merge with #966 Your merge of main into this branch (`7b1e5f9`) left `re001_missing_harden_runner/1` spliced from both versions, so the module did not compile. `020da4b` rebuilds RE001 from main's version with this PR on top. #966's test suite runs unchanged except for 9 tests that asserted *no finding* for a job without an exposure. Under the ADR-009 ruling, those jobs are now `:warn` findings, so those tests assert `[%{severity: :warn, detail: %{exposures: []}}]`. That still checks that each decoy is not mistaken for an exposure. ## Testing - `research_extensions_test` + `deed_test`: **153 tests, 0 failures**. `kyaml_workflow_test`: 10/10. `mix format --check-formatted` is clean. - **Caveat:** hex.pm is unreachable from the session, so I compiled the touched modules standalone under Elixir 1.14. `research_extensions_wiring_test` (CLI/SARIF) runs first in CI. - **Differential fuzzing:** apg-js compiled from `deed.abnf` v1.2.1 and the reader agree on 20,043 mutated deeds, apart from the semantic checks. - **RE001 on real trees:** hypatia 9, standards 15, rsr-template-repo 0 (its deed declares its three jobs), choreographic-types 1 (`push-email-notify`: needs a deed). **Known limits** (follow-up: a parsed workflow model, #971): RE001 reads workflows line by line. A job written as a flow mapping is not seen, so a declaration for one reads as stale, and a secret named only in an inline YAML comment counts as read. Both errors make RE001 report more, never less. --- _Generated by [Claude Code](https://claude.ai/code/session_01J4QvDithtn5Xk5MbSPm4Px)_
) ## Summary Step 3 of the governance-gate stack, after #966, #967, #968 and #973. Under RE001 as #964 (standards ADR-009) now ships it, every job that reads a real secret is in scope. On main that is **9 `:warn` findings**. Once #973 makes hypatia's gate run main's rules, they hold it red at the `info` threshold. This PR clears them by the safety-triangle order: **eliminate** the credential where we can, then **control** what remains. | job | secret | action | |---|---|---| | `security-policy` / `secret-gitleaks` | `GITLEAKS_LICENSE` | **Eliminated.** gitleaks-action needs a licence only for an organization owner. Run 40442141312 logs `[hyperpolymath] is an individual user. No license key is required.` and shows the secret empty, so it was never set. | | `inbox-steward-intake` / `analyze-patterns` | `FARM_PAT` | Its read-only checkout no longer takes the PAT and keeps no credential (`persist-credentials: false`). The `gh api` read still uses the PAT, so the job is **hardened** with GitHub + API. | | `inbox-steward-intake` / `dispatch-to-farm` | `FARM_PAT` | Dropped a checkout nothing read. **Hardened**, API only. | | `inbox-steward-intake` / `record-report`, `update-ruleset` | `FARM_PAT` | **Hardened**, GitHub + API (push, `gh pr create`). | | `ci-health-sweep` / `sweep` | `HYPATIA_DISPATCH_PAT` | **Hardened**, GitHub + API. Every call in `scripts/ci-health/*.sh` is `gh`. | | `hypatia-remediation-sweep` / `sweep` | `HYPATIA_DISPATCH_PAT` | **Hardened**, API plus the artifact service and its blob storage. | | `roadmap-sync` / `sync` | App private key | **Hardened**, API only. It already used an App token (substitution done before this PR). | | `push-email-notify` / `notify` | `SMTP_*` | **Hardened**: the action's release download, plus `${{ secrets.SMTP_HOST }}:${{ secrets.SMTP_PORT \|\| '465' }}`. The mail host is itself a secret, so it stays masked. | All hardening is `step-security/harden-runner@v2.22.2` with `egress-policy: block`, the same pin #968 introduced. `actions.lock` holds the tool's bytes from run 40443523409 (sha256 `74ebf8be…`, `GATE_RC=0`); it was not hand-written. ## How has this been verified? - **RE001, #964's version** (`653d09c`) on this tree: 9 → **0**. Main's version: 0 → 0. - **Differential self-scan**, all 15 rule modules over `origin/main` and this branch, keeping findings on the six touched workflows: - with #964's rules, only those 9 RE001 findings go and nothing is added; - with main's rules, there is no change; - WH004 is 0 once the lock is in. - **actionlint 1.7.7:** the same 85 issues as main, none new. - **Not verified, and the real risk:** a `block` allow-list that misses a host breaks its job at run time. These jobs run on schedule, dispatch or push, so PR CI can't exercise them. I worked out each list from the job's code (every `gh`/`git`/action call), not by observation. I propose one `workflow_dispatch` per job with its own `dry_run`, before or just after merge, and reading harden-runner's egress report. ## Not done here (needs the owner or another repo) - **Substitution:** the four `FARM_PAT` jobs and the two `HYPATIA_DISPATCH_PAT` sweeps could each move to a GitHub App installation token scoped to the repos they touch, as roadmap-sync already does. That means creating Apps, which this session can't do. - **Attestation:** a `hypatia_chora.deed` with `(egress …)` entries (`:enforcement harden-runner-block`), once #964 lands, agreed with the #964 session. - **Org move:** if the repo moves to an organization (as standards did), gitleaks will need `GITLEAKS_LICENSE` again. --- _Generated by [Claude Code](https://claude.ai/code/session_01J4QvDfStHgMdZGTeqaBgM1)_
Summary
Step 1 of the stack that turns
governance / Validate Hypatia Baselinegreen. Every finding that gate keeps on main is RE001: 18 of 18, measured on run 39701221313 (main at9a5db53, scanner23dc74d).RE001 fired on any job that named any
secrets.*without harden-runner, so it reported every secret-using job and could not tell the risky ones apart. It now treats this as an information-flow question: does a real credential share a job with code the job cannot vouch for?A job is reported when all of these hold:
GITHUB_TOKEN, in any casing;pull_request_target,issue_comment,issues,workflow_run, …). A job-levelif:made only ofgithub.event_name == '…'tests narrows which events reach the job;ref:/repository:on anactions/checkoutstep (block or flow form, includingworkflow_run.head_sha/head_branch/head_repository), or one git checkout/fetch/switch command naming the head;npm ci,cargo build/test,mix deps.get, …);Only executable content counts: the step's own
run:script (inline, literal or folded, with comments removed), notenv:values, comments, or anenv:key namedrun.Severity is
:highwhen the credential and the code exposure share a step. That includes a job-levelenv:secret the step doesn't override. It is:warnwhen they share only the job.estate-rescanrescan (warn),testsintegration-tests (high)mirror-radicle(high),hypatia-scan-reusablescan (warn)The two hypatia findings that remain are real; #968 and #967 fix them.
Type of change
How has this been verified?
test/research_extensions_test.exs+test/kyaml_workflow_test.exsgive 84/84.elixirc --warnings-as-errorsis clean, andmix formatis clean on the touched files.Integration Tests, which runs the fullmix test --include integrationand so covers the wiring test.metadatastician/standards.Notes for reviewers
bash -c "npm ci";git clonefollowed by running the clone is not recognised.hypatia-refpin ingovernance.yml, so this lands in the gate only via the follow-up pin bump.hypatia-scan-reusableandmirror-radicleare worth a look in standards itself.Checklist
@doc).🤖 Generated with Claude Code
https://claude.ai/code/session_01J4QvDfStHgMdZGTeqaBgM1