Skip to content

security(repo): enable secret scanning and push protection (#485) - #503

Open
michealross wants to merge 1 commit into
Sub-Rosa-Issue:mainfrom
michealross:security/485-enable-secret-scanning-push-protection
Open

michealross wants to merge 1 commit into
Sub-Rosa-Issue:mainfrom
michealross:security/485-enable-secret-scanning-push-protection

Conversation

@michealross

Copy link
Copy Markdown

Fixes #485

Why

The secret-protection features for this repository are configured in the repository settings, not in the tree: secret scanning, push protection, non-provider pattern detection and validity checks were all off, so a credential could be pushed — and merged into main — with nothing stopping it.

This PR delivers everything that can live in the repository, plus the tooling that turns the settings on and proves they are on:

  1. A gate that covers main and incoming pull requests — .github/workflows/secret-scan.yml runs gitleaks on every push to main and on every pull request, with a pinned release verified against its published checksum, always with --redact, and uploads the SARIF result to code scanning so findings land in the Security tab next to native alerts.
  2. A reproducible way to read and set the settings — scripts/check-secret-protection.mjs (pnpm secrets:check / pnpm secrets:enable) reads the security_and_analysis block of the repository and enables the four protections one request at a time, so a protection the plan does not offer is reported as not available instead of failing the run.

Required maintainer action (one command)

The four settings live in GitHub's configuration for the repository and need Administration: write. A contributor token cannot do it: PATCH /repos/{owner}/{repo} answers 403 Resource not accessible by integration, and security_and_analysis is only returned to repository admins, so the current state cannot even be read from a contributor token.

export GITHUB_TOKEN=<token with Administration: write on Sub-Rosa-Issue/sub-rosa-issue>
pnpm secrets:enable     # turns the four protections on
pnpm secrets:check      # confirms the state read back from the API

Expected output of pnpm secrets:check:

[PASS] Secret scanning (secret_scanning) — enabled
[PASS] Push protection (secret_scanning_push_protection) — enabled
[PASS] Non-provider pattern detection (secret_scanning_non_provider_patterns) — enabled
[PASS] Validity checks (secret_scanning_validity_checks) — enabled
PASS: secret protection is enabled for Sub-Rosa-Issue/sub-rosa-issue

non-provider pattern detection and validity checks depend on the plan: when GitHub does not offer them the script prints N/A and still exits 0, because the criterion is "where available". A required protection that is offered but off always fails the run.

The same thing can be done from the web UI (Settings → Code security and analysis → Secret scanning → Enable, and the same for Push protection); the script just makes it reproducible and verifiable.

What each protection covers

Protection Coverage
Secret scanning Scans the history of the default branch and every new push; one alert per detected credential.
Push protection Blocks the push itself, on any branch — including the branch behind an incoming pull request — before it can reach main.
Non-provider pattern detection Adds patterns for credentials that have no provider partner (generic API keys, private keys).
Validity checks Marks each alert active / inactive / unknown, so a live credential can be told apart from a revoked one.

Initial scan — no credential found, nothing to rotate

The initial scan of the full history reachable from main (402 commits) with the rules the CI gate uses found no credential. The only matches were generic-api-key hits on public, non-secret values — Stellar contract IDs, Stellar account IDs and SHA-256 hashes in fixtures, mainnet artifacts and demo traces. Those are exactly the values allowlisted in .gitleaks.toml, so no rotation and no history rewrite are needed.

When native secret scanning is switched on it will raise its own alerts for some of those same values, because a generic pattern cannot tell a contract ID from a key. They can be resolved as false positives. No secret value is reproduced in this PR, in the issue, or in any log: the gate runs gitleaks with --redact, and the SARIF report carries the rule, the file and the line only.

Files

File What it does
.github/workflows/secret-scan.yml gitleaks on pushes to main and on pull requests; redacted; SARIF → code scanning
.gitleaks.toml default rules + a minimal allowlist (paths × value shapes) for the non-credential values in this repo
scripts/check-secret-protection.mjs read/enable the four settings through the API; non-zero exit when a required one is off or unreported
scripts/check-secret-protection.test.mjs unit tests against a fake API server (enabled, disabled, unavailable, unreadable, enable flow)
docs/SECRET_PROTECTION.md the settings, how to enable and verify them, what they cover, and how to review alerts safely
package.json secrets:check, secrets:enable, secrets:test
.github/workflows/coverage.yml runs pnpm secrets:test so the new script stays covered
README.md, docs/CI.md, scripts/check-links.js link the new doc and keep the link check covering it

Review notes

  • The allowlist is deliberately narrow: it pairs specific file paths with specific value shapes (C… contract IDs, G… account IDs, 64-hex hashes). A real credential dropped into one of those files matches none of them and still fails the job — checked locally with a freshly generated token value, which was still reported.
  • The gate needs no repository secret: it only reads the repository, and its only write is the SARIF upload to code scanning.
  • secrets:check prints states only. It never prints the token and never prints a secret value.
  • Reviewing an alert without exposing it: gh api "repos/OWNER/REPO/secret-scanning/alerts?hide_secret=true".

Verification

  • pnpm secrets:test — 9 tests, all pass.
  • node scripts/check-links.js — 46 local links checked, OK.
  • gitleaks detect --source . --config .gitleaks.toml --redact over the history and over the working tree — no leaks found.

…Issue#485)

GitHub's secret-protection features live in the repository settings, not in
the tree, so this change adds the in-repo half of the protection plus the
tooling that makes the settings verifiable and reproducible:

- .github/workflows/secret-scan.yml: gitleaks gate on pushes to main and on
  incoming pull requests. Pinned release verified against the published
  checksum, always run with --redact, and the SARIF result is uploaded to
  code scanning so findings show up in the Security tab.
- .gitleaks.toml: default gitleaks rules with a minimal allowlist for the
  values in this repo that look like credentials but are not (Stellar
  contract IDs, account IDs and SHA-256 hashes in fixtures, mainnet
  artifacts and demo traces). The allowlist pairs file paths with value
  shapes, so a real credential in those files does not match it and still
  fails the job.
- scripts/check-secret-protection.mjs: reads, and with --enable turns on,
  secret_scanning, secret_scanning_push_protection,
  secret_scanning_non_provider_patterns and secret_scanning_validity_checks
  through the GitHub API. It reports every protection and exits non-zero
  when a required one is off or not reported at all; optional protections
  the plan does not offer are reported as not available instead of failing.
- scripts/check-secret-protection.test.mjs: unit tests against a fake API
  server (enabled, disabled, unavailable, unreadable, enable flow).
- docs/SECRET_PROTECTION.md: the settings, how to enable and verify them,
  what each one covers for main and incoming pull requests, and how to
  review alerts without posting secret values.

The settings themselves need Administration: write on the repository, which
a contributor token does not have, so a maintainer runs `pnpm secrets:enable`
and then `pnpm secrets:check` to confirm the state.

The initial scan of the full history (402 commits) with these rules found no
credential: the only matches were generic-api-key hits on public, non-secret
values, all of them allowlisted in .gitleaks.toml. No rotation and no
history rewrite are needed, and no secret value is reproduced here.

🤖 Generated with Codebuff
Co-Authored-By: Codebuff <noreply@codebuff.com>
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.

security(high/easy): enable secret scanning and push protection

1 participant