Skip to content

fix(rules): scope RE001 to credentials exposed to code the job cannot vouch for - #966

Merged
hyperpolymath merged 7 commits into
mainfrom
claude/re001-risk-scoped
Oct 27, 2026
Merged

hyperpolymath merged 7 commits into
mainfrom
claude/re001-risk-scoped

Conversation

@hyperpolymath

@hyperpolymath hyperpolymath commented Oct 27, 2026 •

Copy link
Copy Markdown
Owner

Summary

Step 1 of the stack that turns governance / Validate Hypatia Baseline green. Every finding that gate keeps on main is RE001: 18 of 18, measured on run 39701221313 (main at 9a5db53, scanner 23dc74d).

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:

  • it references a secret other than GITHUB_TOKEN, in any casing;
  • it has at least one exposure:
    • an untrusted-input trigger (pull_request_target, issue_comment, issues, workflow_run, …). A job-level if: made only of github.event_name == '…' tests narrows which events reach the job;
    • a head checkout: ref:/repository: on an actions/checkout step (block or flow form, including workflow_run.head_sha/head_branch/head_repository), or one git checkout/fetch/switch command naming the head;
    • a piped installer, or a dependency install/build (npm ci, cargo build/test, mix deps.get, …);
  • it installs no harden-runner.

Only executable content counts: the step's own run: script (inline, literal or folded, with comments removed), not env: values, comments, or an env: key named run.

Severity is :high when the credential and the code exposure share a step. That includes a job-level env: secret the step doesn't override. It is :warn when they share only the job.

repo old new remaining
hypatia 18 2 estate-rescan rescan (warn), tests integration-tests (high)
metadatastician/standards 18 2 mirror-radicle (high), hypatia-scan-reusable scan (warn)
hyperpolymath/echidna 5 0

The two hypatia findings that remain are real; #968 and #967 fix them.

Type of change

  • 🐛 Bug fix (scanner precision)

How has this been verified?

  • Standalone (Elixir 1.14; hex.pm is unreachable here): test/research_extensions_test.exs + test/kyaml_workflow_test.exs give 84/84.
  • Negative controls: each test added for Codex's four review rounds fails against the commit before its fix. 8 of the first 12 new tests fail against the old rule.
  • Build checks: elixirc --warnings-as-errors is clean, and mix format is clean on the touched files.
  • CI on the previous heads: all green, including Integration Tests, which runs the full mix test --include integration and so covers the wiring test.
  • Pending: governance can't start on any PR until the Actions allow-list accepts metadatastician/standards.

Notes for reviewers

  • Known limits (decided):
    • install words inside quoted shell data still count as an exposure; the maintainer kept that trade-off rather than miss bash -c "npm ci";
    • git clone followed by running the clone is not recognised.
  • Root cause of the review churn: the 22 Codex findings were all YAML shapes that a line reader gets wrong. design: one parsed workflow model for every workflow rule #971 proposes one parsed workflow model for every workflow rule, with a differential experiment before any build. Further shape findings here belong there.
  • Pin: the gate builds hypatia at the hypatia-ref pin in governance.yml, so this lands in the gate only via the follow-up pin bump.
  • Beyond this repo: every estate repo scans with this rule. Standards' hypatia-scan-reusable and mirror-radicle are worth a look in standards itself.

Checklist

  • My commits are signed. The session's signing shim is platform-mediated.
  • I ran the project's own checks/tests locally and they pass (scoped as above).
  • New files carry the correct SPDX (none added).
  • Docs are updated (moduledoc and @doc).
  • I have not introduced a soundness hole: every dropped finding falls into a class listed above, and the dropped classes are measured.

🤖 Generated with Claude Code

https://claude.ai/code/session_01J4QvDfStHgMdZGTeqaBgM1

… 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
@coderabbitai

coderabbitai Bot commented Oct 27, 2026 •

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

You'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.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

Learn how review limits work.

Review configuration:

Run configuration
  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: Organization UI
  • Review profile: ASSERTIVE
  • Plan: Advanced
  • Run ID: 96fa4ed8-2bd0-47d8-83ec-8d74948a2c72
Commits

Reviewing files that changed from the base of the PR and between 7c10e5d and 2194d63.

Files selected for processing: 4
  • lib/rules/research_extensions.ex
  • test/kyaml_workflow_test.exs
  • test/research_extensions_test.exs
  • test/research_extensions_wiring_test.exs
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@hyperpolymath
hyperpolymath marked this pull request as ready for review October 27, 2026 01:33
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 27, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-27T02:20:41.436072Z 2194d63 New commits
ℹ️ 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" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@coderabbitai

coderabbitai Bot commented Oct 27, 2026

Copy link
Copy Markdown
Contributor

Add Carrot credits or activate Agent usage billing to use Autofix

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread lib/rules/research_extensions.ex Outdated
Comment thread lib/rules/research_extensions.ex Outdated
Comment thread lib/rules/research_extensions.ex Outdated
Comment thread lib/rules/research_extensions.ex Outdated
Comment thread lib/rules/research_extensions.ex
Comment thread lib/rules/research_extensions.ex Outdated
Comment thread lib/rules/research_extensions.ex Outdated
Comment thread lib/rules/research_extensions.ex
…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

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread lib/rules/research_extensions.ex Outdated
Comment thread lib/rules/research_extensions.ex Outdated
Comment thread lib/rules/research_extensions.ex Outdated
Comment thread lib/rules/research_extensions.ex Outdated
Comment thread lib/rules/research_extensions.ex Outdated
- 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

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread lib/rules/research_extensions.ex Outdated
Comment thread lib/rules/research_extensions.ex Outdated
Comment thread lib/rules/research_extensions.ex Outdated
Comment thread lib/rules/research_extensions.ex Outdated
Comment thread lib/rules/research_extensions.ex Outdated
- 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
hyperpolymath added a commit that referenced this pull request Oct 27, 2026
…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)_
hyperpolymath added a commit that referenced this pull request Oct 27, 2026
…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

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment on lines +442 to +444
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)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment on lines +393 to +397
case Enum.split_while(step, fn {line, _} ->
case Regex.run(~r/^(\s*(?:-\s+)?)run:/, line) do
[_, prefix] -> String.length(prefix) != key_column
_ -> true
end

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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/

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment on lines +480 to +483
[_, 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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment on lines +349 to +350
defp re001_exposures(steps, untrusted_events) do
code = Enum.flat_map(steps, &step_code_exposures/1)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

hyperpolymath added a commit to hyperpolymath/rsr-template-repo that referenced this pull request Oct 27, 2026
#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>
hyperpolymath pushed a commit that referenced this pull request Oct 27, 2026
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
hyperpolymath added a commit that referenced this pull request Oct 27, 2026
## 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)_
hyperpolymath added a commit that referenced this pull request Oct 27, 2026
…_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)_
hyperpolymath added a commit that referenced this pull request Oct 27, 2026
)

## 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)_
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants