Skip to content

feat(secret-scanning): dedicated token, fail-loud sweep, secret-alerts command - #969

Merged
hyperpolymath merged 3 commits into
mainfrom
claude/kind-clarke-aqpe7a-secret-sweep
Oct 27, 2026
Merged

hyperpolymath merged 3 commits into
mainfrom
claude/kind-clarke-aqpe7a-secret-sweep

Conversation

@hyperpolymath

Copy link
Copy Markdown
Owner

Summary

A workflow's own GITHUB_TOKEN can't read secret-scanning alerts, because no workflow permission covers them. So the per-repo Hypatia scan never sees them (raised by Codex on hyperpolymath/rsr-template-repo#258).

The owner decided these alerts will be collected by one central sweep, run from a private repo, using a dedicated read-only token, and not by adding a secret to every repo. The results stay private: an "open leaked token in repo X" list must not land in the public verisimdb-data store or in a public repo's Actions logs.

This PR adds what that sweep needs on the Hypatia side. Existing callers behave exactly as before.

Changes

lib/rules/secret_scanning_alerts.ex

  • Token: reads HYPATIA_SECRET_ALERTS_TOKEN first, then GITHUB_TOKEN. The dedicated token never has to be exported under the general name, and an empty value counts as unset.
  • sweep/2: fetches the alerts in one API call and returns {:error, reason} when they can't be fetched.
    • The existing SSA001–004 functions still log the failure and return [], as the other API-backed rules do.
    • In an estate sweep, that [] would make a refused token or a repo with secret scanning disabled look clean. sweep/2 never does.
  • Clearer errors: curl no longer uses -f, which threw away GitHub's error body. Errors now carry the HTTP status and GitHub's message, for example GitHub API 404: Secret scanning is disabled on this repository.
  • Pure finding builders: they take the alert list as input, so they're tested from fixtures without the network. The SSA001–004 rules, severities and messages are unchanged.

lib/hypatia/cli.ex

  • hypatia secret-alerts --owner X --repo Y: prints one JSON line (status: ok with findings, or status: error with the reason).
    • Exit 0 when the alerts were read, with or without findings.
    • Exit 3 when they couldn't be read.
    • Exit 2 for bad arguments.

Not in this PR

The private sweep workflow itself is not here. It belongs in a private repo, and this session can't attach hyperpolymath/.git-private-farm: repo names starting with . are refused. The owner still has to decide where it goes.

Testing

Hex isn't reachable from this environment, so I ran the test files standalone with plain Elixir 1.14. A minimal local JSON shim stood in for Jason; it isn't committed. CI's full mix test runs with real Jason.

  • test/secret_scanning_alerts_test.exs: 16 of 16 pass, 6 existing and 10 new. The new tests cover:
    • which token wins, including an empty dedicated token;
    • sweep/2 returning an error without a token;
    • response parsing for 200, 404, 403, a non-JSON 502 and a non-list 200;
    • SSA001–004 built from fixture alerts, with the stale threshold and accepted resolutions.
  • test/cli_test.exs: the 3 new secret-alerts tests and the help tests pass. One existing test needs rule modules my harness can't load. It fails the same way on unmodified main in this harness, so it isn't affected by this change.
  • Formatting: mix format --check-formatted passes locally. Two existing assertions that the local formatter rewrapped are restored to their original form, so test/cli_test.exs only gains lines.

Generated by Claude Code

…alerts` command

A workflow's GITHUB_TOKEN cannot read secret-scanning alerts (no workflow
permission covers them), so per-repo scans never see them. They will be
gathered by one central sweep from a private repo, with a dedicated
read-only token. This adds what that sweep needs, without changing any
existing caller.

- Token: `HYPATIA_SECRET_ALERTS_TOKEN` is read first, then GITHUB_TOKEN,
  so the dedicated token never has to be exported under the general name.
- `sweep/2`: one API call; returns {:error, reason} when the alerts cannot
  be fetched. The per-rule SSA001-004 functions log and return [] on a
  fetch failure, which an estate sweep would read as "clean".
- Errors now carry the HTTP status and GitHub's message (curl no longer
  uses -f, which discarded the body), e.g. "GitHub API 404: Secret
  scanning is disabled on this repository."
- The finding builders are pure functions of the alert list, so they are
  tested from fixtures without the network.
- `hypatia secret-alerts --owner X --repo Y`: one JSON line; exit 0 when
  the alerts were read, 3 when they could not be.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P4yFkxW5Heb8p2YW7ZYs9F
The local Elixir 1.14 formatter rejoined two pre-existing assertions;
CI formats with 1.19.4. Restore them so this change only adds lines.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P4yFkxW5Heb8p2YW7ZYs9F
@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: 04f9371a-1cd9-49d1-97b7-b031ec60f79a
Commits

Reviewing files that changed from the base of the PR and between 5b08933 and a9c3d47.

Files selected for processing: 4
  • lib/hypatia/cli.ex
  • lib/rules/secret_scanning_alerts.ex
  • test/cli_test.exs
  • test/secret_scanning_alerts_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:58
@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:14:50.952963Z a9c3d47 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.

@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: cc9c20d74c

ℹ️ 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 +259 to +260
case fetch_alerts(owner, repo) do
{:ok, alerts} -> {:ok, result(findings_from_alerts(owner, repo, alerts))}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Paginate the repository alert request

For repositories with more than 100 historical alerts, fetch_alerts/2 returns only the newest 100 because GitHub caps per_page at 100 and defaults to the first page (API documentation). Nevertheless, sweep/2 treats this partial list as complete and emits status: "ok", so older open, stale, or improperly dismissed alerts are silently omitted and totals are understated. Fetch subsequent pages or follow GitHub's pagination cursors before building the findings.

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. This PR had already merged, so the fix is in #970. The fetch now follows Link: rel="next" pages, but only on api.github.com. Past 50 pages it returns an error rather than a truncated ok.


Generated by Claude Code

Comment on lines +327 to +330
[
"-s",
"-w",
"\n%{http_code}",

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 Bound the alert API transfer time

When GitHub or an intermediate network connection accepts the request but stops delivering data, this synchronous curl invocation has neither --max-time nor another transfer deadline, so System.cmd/3 can block indefinitely instead of letting sweep/2 return an error and the CLI exit 3. This can stall the entire central estate sweep on a single repository; the curl manual specifically describes --max-time as preventing batch jobs from hanging for hours. Add a finite transfer timeout and let the existing nonzero-exit path report it.

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. Fixed in #970: each request now has --max-time 30, and a timeout (curl exit 28) is reported as an error, so secret-alerts exits 3 instead of hanging the sweep.


Generated by Claude Code

@hyperpolymath
hyperpolymath merged commit 78be831 into main Oct 27, 2026
46 checks passed
@hyperpolymath
hyperpolymath deleted the claude/kind-clarke-aqpe7a-secret-sweep branch October 27, 2026 02:13
hyperpolymath added a commit that referenced this pull request Oct 27, 2026
…LICENSE.adoc (#970)

## Summary

This fixes three defects from #963 and
#969. Codex flagged them after those PRs had
already merged. It also tightens the REUSE licence rule after
CodeRabbit's review here.

## Changes

`lib/rules/secret_scanning_alerts.ex` (from #969)
- **Pagination:** the fetch read only the first page of 100 alerts, so
`sweep/2` returned `status: ok` with older open, stale or badly
dismissed alerts missing. It now follows GitHub's `Link: rel="next"`
header until the last page.
- It only follows links on `api.github.com`, because the token goes with
every request.
- Past 50 pages (5,000 alerts) it returns an error rather than a
truncated list.
- **Timeout:** the curl call had no deadline, so one stalled repository
could hang an estate-wide sweep. Each request now has `--max-time 30`,
and a timeout (curl exit 28) is reported as `GitHub API request timed
out after 30s`.
- **Header parsing:** responses are read with `curl -i` so the `Link`
header is available. If an HTTPS proxy adds its own "Connection
established" header block first, it's skipped.

`lib/scorecard_ingestor.ex` and `lib/rules/root_hygiene.ex` (from #963)
- **SC-010 now agrees with root hygiene.** The local Scorecard licence
check still rejected `LICENSE.adoc` and REUSE `LICENSES/`, so a single
scan reported both "licence present" (root hygiene) and "No LICENSE file
found" (SC-010). It now accepts `LICENSE.adoc` and calls root hygiene's
REUSE check.
- **`RootHygiene.reuse_licenses?/1` is now public.** The REUSE rule has
one definition instead of two copies that could drift apart.

REUSE strictness (e5f5a7a, owner decision on CodeRabbit's finding)
- **The problem:** a `LICENSES/` file used to count if it was any
non-empty `<name>.<ext>`. So `LICENSES/notes.txt` alone passed both
licence checks.
- **The fix:** its stem must now be on the SPDX License List or be a
REUSE `LicenseRef-` id. Matching is exact.
- **Why a list, not a filename rule:** real identifiers such as `curl`
and `blessing` are lowercase, so no heuristic separates them from
`notes`. The list is vendored instead:
- `priv/spdx/license-ids.txt` holds SPDX License List 3.29.0: 740
identifiers, deprecated ones included. Its header records the source
tag.
- `scripts/refresh-spdx-license-ids.sh <tag>` regenerates the file and
refuses a suspiciously short download.
- `Hypatia.SPDX` reads the file at compile time, so the escript needs no
`priv/` at run time.
- **Owner action:** the vendored list is third-party data. I have not
added a `.reuse/dep5` entry for it, because licence records are yours to
make.

## Testing

Run with plain Elixir 1.14 and a local JSON stand-in, as for #969. Hex
isn't reachable from here, and CI's `mix test` uses the real Jason.

- **Overall:** 84 of 84 pass across `secret_scanning_alerts_test.exs`,
`root_hygiene_test.exs`, `scorecard_ingestor_license_test.exs`,
`scorecard_ingestor_security_policy_test.exs` (regression guard) and the
new `spdx_test.exs`.
- **Behaviour tests fail on the old code.** The 10 tests from the first
commit fail against `main`. The two `notes.txt` tests fail against the
previous `reuse_licenses?/1`.
- `parse_page/1`: the next link, the last page, the proxy header block,
and an error page.
- `collect_pages/2`: two pages joined, a next link to another host
refused, a failed later page failing the whole fetch, the page cap
raising an error, and exactly 50 requests before the cap.
- SC-010: `LICENSE.adoc`, REUSE and `LicenseRef-` pass. A
`.gitkeep`-only `LICENSES/` and a `notes.txt` fail.
- `Hypatia.SPDX`: listed, lowercase, deprecated and `LicenseRef-` ids
count; other names and case variants do not.
- **Formatting:** `mix format --check-formatted` passes on every changed
file. `shellcheck` passes on the refresh script.

---
_Generated by [Claude
Code](https://claude.ai/code/session_01P4yFkxW5Heb8p2YW7ZYs9F)_
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