Skip to content

ci: add diff-scoped gitleaks secret scanning - #116

Merged
github-actions[bot] merged 2 commits into
mainfrom
claude/daily-repo-scan-v8fvqs
Jul 27, 2026
Merged

ci: add diff-scoped gitleaks secret scanning#116
github-actions[bot] merged 2 commits into
mainfrom
claude/daily-repo-scan-v8fvqs

Conversation

@dizhaky

@dizhaky dizhaky commented Jul 27, 2026

Copy link
Copy Markdown
Owner

What does this PR do?

This repo had no secret scanning of any kind — no gitleaks/trufflehog
workflow, no .pre-commit-config.yaml — while shipping 1Password and Bitwarden
credential-bootstrap paths. supply-chain-audit.yml covers attack-pattern
indicators only (.pth files, base64+exec) and is pull_request-scoped, so
direct pushes to main had no coverage at all. Surfaced by the 2026-07-25 daily
repo scan.

The scan is diff-scoped — it never rescans history, so the placeholder
credentials already sitting in test fixtures don't fire on unrelated changes.
That's the design choice that keeps the check worth reading, and it mirrors the
warning already in supply-chain-audit.yml: a scanner that fires on every PR
trains reviewers to ignore it.

Related Issue

Follow-up from the 2026-07-25 daily repo scan (no tracking issue filed).

Type of Change

  • 🔒 Security fix

Changes Made

  • .github/workflows/secret-scan.yml — gitleaks on every push and PR.
    gitleaks pinned by version and tarball SHA256; actions/checkout SHA-pinned
    per the Dependency Pinning Policy. On failure, prints what to do next.
  • .gitleaks.toml — custom rules + a narrow, commented allowlist.
  • AGENTS.md — a Secret Scanning section next to the Dependency Pinning
    Policy. Deliberately not added to SECURITY.md §2: that section documents
    the runtime trust boundary and is careful about what counts as one. A CI
    hygiene check is not a boundary.
  • docs/system-log/2026-07-27.md — session entry.

Two findings worth your attention

1. gitleaks' stock ruleset has no Anthropic rule at all.

A well-formed sk-ant-api03-… canary passes straight through the default
config. For an agent framework whose primary job is calling Claude, that's the
single most likely credential to leak. Added explicit anthropic-api-key,
anthropic-admin-key and openrouter-api-key rules — OpenRouter otherwise
falls to the entropy-based generic-api-key, which misses a key appearing
without a nearby keyword.

I confirmed this is a stock-ruleset gap and not something my allowlist caused:
the Anthropic miss reproduces identically with no config at all.

2. No committed credentials exist — and that's now a real claim.

The 2026-07-25 scan said this based on a narrow pattern grep. A full-tree
gitleaks scan surfaced 647 findings in tracked files the grep missed. All
647 were triaged; all are benign:

Count What
~534 Two vendored upstream unsloth reference docs (third-party prose/code samples)
~90 Test fixtures
~17 Discord/Mattermost user + channel IDs and Bearer <placeholder> in docs examples
6 Published OAuth client IDs, each already carrying an in-source comment on why it's non-confidential
1 gemini-cli's public desktop client secret (Google's MIT repo, upstream link in agent/google_oauth.py)
1 The -----BEGIN … PRIVATE KEY----- pattern inside agent/redact.py — the module whose job is recognising secrets

How to Test

  1. Positive control — drop a file with a well-formed provider credential
    into any tracked path and commit it, then run the workflow's command:
    gitleaks detect --config .gitleaks.toml --redact --exit-code 1 --log-opts="HEAD~1..HEAD".
    Exits 1.
  2. Negative control — same command on a commit that touches nothing
    sensitive. Exits 0, despite 82 pre-existing findings in the tree.
  3. python3 -c "import yaml; yaml.safe_load(open('.github/workflows/secret-scan.yml'))"

Verification

  • Allowlist tuning takes the tracked tree from 647 → 82 findings. All 82
    residual are unmarked placeholder strings inside tests/; none fire in CI
    because the scan is diff-scoped. I did not blanket-exempt tests/ — test
    fixtures are exactly where a real key gets pasted by accident.
  • Positive control, 7/7: a canary with well-formed Anthropic, OpenRouter,
    OpenAI, GitHub PAT, AWS, Slack-bot and generic 48-char credentials is fully
    detected with the config applied.
  • End-to-end against the real CI command: clean commit exits 0, canary
    commit exits 1 with 7 leaks. Canary commit reset; nothing left in the tree.
  • Workflow YAML parses. The pinned gitleaks SHA256 was computed from the
    actual release artifact
    , not transcribed.
  • ruff check . clean (no Python touched). The advisory lint bot reports 0 new
    ruff and 0 new ty diagnostics vs base.

Retracting a sibling finding from the same scan

The second commit here corrects the system log. The 2026-07-25 scan reported
"version = "0.15.0" since 2026-07-02 with no RELEASE_v0.15.0.md; every
release from v0.2.0 through v0.14.0 has one"
as a gap. It isn't one.

All 13 RELEASE_*.md files arrived in a single commit (3beff00,
2026-07-01) — they are upstream NousResearch/hermes-agent release notes
vendored wholesale into this fork, not documents authored here; their contents
cite upstream PR numbers in the NousResearch#21000NousResearch#26000 range and "215 community
contributors". This fork's 0.15.0 is a local bump (5b95b76, "bump version
to 0.15.0 to resolve 3 Dependabot CVEs"
), not an upstream release.

So there is no fork-local release process that skipped a step, and authoring a
RELEASE_v0.15.0.md here would fabricate an upstream release document. The
original finding pattern-matched on filenames without checking provenance.
No action needed.

Follow-ups not in this PR

  • A .pre-commit-config.yaml running the same config would catch leaks before
    they reach a remote, but it changes every contributor's local workflow — your
    call.
  • ci-auto-healer.yml fires on a */30 cron regardless of whether anything is
    failing — ~48 mostly no-op runs/day, which is what made the relevant
    Tests/Lint history hard to read during the scan. The workflow_run
    trigger already covers the immediate case, so the cron is a safety net that
    could run hourly. Left alone: that's a judgement call about CI economics, not
    a defect.

This repo had no secret scanning of any kind — no gitleaks/trufflehog
workflow, no pre-commit config — while shipping 1Password and Bitwarden
credential-bootstrap paths. supply-chain-audit.yml covers attack-pattern
indicators only (.pth files, base64+exec) and is pull_request-scoped.

The scan is diff-scoped: it never rescans history, so pre-existing
placeholder credentials in test fixtures don't fire on unrelated changes.
gitleaks is pinned by version and tarball SHA256; checkout is SHA-pinned
per the dependency pinning policy.

Two things worth flagging:

gitleaks' stock ruleset has no Anthropic rule at all — a well-formed
sk-ant-api03 canary passes straight through the default config. For an
agent framework whose primary job is calling Claude, that is the most
likely credential to leak, so anthropic-api-key, anthropic-admin-key and
openrouter-api-key are added explicitly. OpenRouter otherwise falls to
the entropy-based generic-api-key, which misses a key appearing without
a nearby keyword.

No committed credentials exist. A full-tree scan surfaced 647 findings
in tracked files; all were triaged and all are benign — ~534 in two
vendored upstream unsloth reference docs, the rest published OAuth
client IDs, gemini-cli's public desktop client secret, Discord and
Mattermost IDs in docs examples, and test fixtures.

Verification: allowlist tuning takes the tracked tree 647 -> 82, all 82
residual being unmarked placeholders in tests/ that a diff scan never
reaches. A canary with seven well-formed provider credentials is caught
7/7. End-to-end against the real CI command: clean commit exits 0,
canary commit exits 1.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012689txgT12g2hjRczcUZi8
@github-actions

Copy link
Copy Markdown

🔎 Lint report: claude/daily-repo-scan-v8fvqs vs origin/main

ruff

Total: 0 on HEAD, 0 on base (➖ 0)

🆕 New issues: none

✅ Fixed issues: none

Unchanged: 0 pre-existing issues carried over.

ty (type checker)

Total: 9231 on HEAD, 9231 on base (➖ 0)

🆕 New issues: none

✅ Fixed issues: none

Unchanged: 5099 pre-existing issues carried over.

Diagnostics are surfaced as warnings — this check never fails the build.

All 13 RELEASE_*.md files landed in one commit (3beff00) — they are upstream
NousResearch release notes vendored into this fork, not documents authored
here. This fork's 0.15.0 is a local CVE bump, not an upstream release, so
there is no missing artifact and authoring one would fabricate an upstream
release document.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012689txgT12g2hjRczcUZi8
@dizhaky
dizhaky marked this pull request as ready for review July 27, 2026 20:03
@github-actions
github-actions Bot merged commit 052207d into main Jul 27, 2026
24 checks passed
@github-actions
github-actions Bot deleted the claude/daily-repo-scan-v8fvqs branch July 27, 2026 20:03

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

ℹ️ 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".


concurrency:
group: secret-scan-${{ github.ref }}
cancel-in-progress: true

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 Preserve every push when cancelling concurrent scans

When two pushes target the same ref in quick succession, this cancels the first run, while the replacement run is bounded below by the second event's BEFORE_SHA..AFTER_SHA. Because gitleaks documents --log-opts as options passed to Git log in its v8.24.3 CLI documentation, the replacement excludes the first push's commits. If that first push introduced a secret and the second leaves it untouched, no completed workflow reports it; queue each push or widen the replacement scan to include the cancelled range.

AGENTS.md reference: AGENTS.md:L336-L339

Useful? React with 👍 / 👎.

"${SCAN[@]}" --log-opts="${BEFORE_SHA}..${AFTER_SHA}"
elif git rev-parse HEAD~1 >/dev/null 2>&1; then
echo "Scanning latest commit (parent SHA unavailable after a rewrite)"
"${SCAN[@]}" --log-opts="HEAD~1..HEAD"

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 Scan all commits on a newly created branch

When BEFORE_SHA is the all-zero value for a newly created ref, a multi-commit initial push reaches this fallback and HEAD~1..HEAD scans only its final commit. Under gitleaks' documented --log-opts Git-log semantics, a credential introduced in an earlier pushed commit and left unchanged in the tip is therefore missed; use a merge-base or another baseline covering every newly reachable commit.

AGENTS.md reference: AGENTS.md:L336-L339

Useful? React with 👍 / 👎.

&& [ "$BEFORE_SHA" != "0000000000000000000000000000000000000000" ] \
&& git cat-file -e "${BEFORE_SHA}^{commit}" 2>/dev/null; then
echo "Scanning push diff: ${BEFORE_SHA}..${AFTER_SHA}"
"${SCAN[@]}" --log-opts="${BEFORE_SHA}..${AFTER_SHA}"

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 Include merge-commit patches in every pushed range

When a pushed range contains a merge commit, the supplied log options do not enable merge diffs; Git documents the default --diff-merges mode as off, so the merge commit has no patch in the stream gitleaks scans. A credential added only while resolving a merge conflict therefore appears in neither parent commit and is missed even though the merge is inside BEFORE_SHA..AFTER_SHA; include an appropriate --diff-merges mode or separately scan the resulting tree diff.

AGENTS.md reference: AGENTS.md:L336-L339

Useful? React with 👍 / 👎.

Comment thread .gitleaks.toml
# --- Marked placeholders in tests and docs ---
# Deliberately requires an explicit marker word on the line. A high-entropy
# string with no marker still fires, including inside tests/.
'''(?i)(fake|dummy|placeholder|example|changeme|change-me|not-?a-?real|your[-_](api[-_])?key)''',

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 Scope placeholder markers to the credential they exempt

Because the global allowlist targets the entire line, this unbounded marker regex suppresses every finding whenever any field or comment on that line contains fake, dummy, example, and similar substrings. For example, an actual provider key added to a one-line JSON fixture that also has an unrelated "description": "example response" field is silently ignored, so the exemption is not limited to marked placeholder credentials; couple the marker to the credential assignment or matched value instead.

AGENTS.md reference: AGENTS.md:L348-L350

Useful? React with 👍 / 👎.

dizhaky pushed a commit that referenced this pull request Aug 1, 2026
Clears the two follow-ups named in #116.

.pre-commit-config.yaml runs the same gitleaks against the staged diff,
pinned to the upstream repo at a commit SHA per the Dependency Pinning
Policy. Opt-in — nothing runs until a contributor runs `pre-commit
install`. CI remains the enforcement boundary; this only moves the check
to where the remedy is an amend rather than a history rewrite plus a
credential rotation.

ci-auto-healer.yml drops from */30 to hourly at :23. The workflow_run
triggers already fire the moment a watched workflow completes, so the
cron is only a backstop for missed completion events. At */30 it ran
~48x/day almost always to print "No failed runs found", which is what
buried the real Tests/Lint history in the run list during the scan that
prompted this work. Off-minute is deliberate: :00 is GitHub's most
contended scheduler slot.

Verified the hook rather than just linting it: staged canary of seven
well-formed provider credentials exits 1 and reports all seven,
including the custom Anthropic and OpenRouter rules — confirming it
reads .gitleaks.toml and not the built-in default. Clean tree exits 0.
Both edited workflows re-parsed; the pinned rev came from
`git ls-remote --tags`, not from memory.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012689txgT12g2hjRczcUZi8
github-actions Bot pushed a commit that referenced this pull request Aug 1, 2026
)

Clears the two follow-ups named in #116.

.pre-commit-config.yaml runs the same gitleaks against the staged diff,
pinned to the upstream repo at a commit SHA per the Dependency Pinning
Policy. Opt-in — nothing runs until a contributor runs `pre-commit
install`. CI remains the enforcement boundary; this only moves the check
to where the remedy is an amend rather than a history rewrite plus a
credential rotation.

ci-auto-healer.yml drops from */30 to hourly at :23. The workflow_run
triggers already fire the moment a watched workflow completes, so the
cron is only a backstop for missed completion events. At */30 it ran
~48x/day almost always to print "No failed runs found", which is what
buried the real Tests/Lint history in the run list during the scan that
prompted this work. Off-minute is deliberate: :00 is GitHub's most
contended scheduler slot.

Verified the hook rather than just linting it: staged canary of seven
well-formed provider credentials exits 1 and reports all seven,
including the custom Anthropic and OpenRouter rules — confirming it
reads .gitleaks.toml and not the built-in default. Clean tree exits 0.
Both edited workflows re-parsed; the pinned rev came from
`git ls-remote --tags`, not from memory.


Claude-Session: https://claude.ai/code/session_012689txgT12g2hjRczcUZi8

Co-authored-by: Claude <noreply@anthropic.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.

2 participants