Skip to main content
Glama
README.md
# πŸ›‘οΈ crv-leak-scan

**Find the secrets you already leaked β€” before attackers do. Then let your AI fix them.**

`crv-leak-scan` rips through a Git repository β€” working tree **and** commit
history β€” hunting for Web3 RPC endpoints, crypto private keys, seed phrases,
cloud tokens, AI keys, and database credentials. Keys that were "deleted" in a
later commit are still in history. Attackers clone history. So does this tool.

Three ways to run it:

- **CLI** β€” scan, mask, remediate-by-hand, exit codes for CI: `npx crv-leak-scan`
- **Pre-commit shield** β€” block leaking commits locally: `--install-hook`
- **MCP server** β€” an AI agent drives the whole detect β†’ remediate β†’ purge β†’
  verify pipeline autonomously: `--mcp`

One binary. Two runtime dependencies (`picocolors`, the MCP SDK). Everything
else is Node stdlib. Scans the last 100 commits of a modern repo in **under
two seconds**.

```bash
npx crv-leak-scan
```

---

## Why

Every leaked key follows the same lifecycle:

1. A dev pastes a key into `config.ts` "just for local testing".
2. It gets committed.
3. Someone notices and deletes the line.
4. Everyone believes it's gone.

It is not gone. `git log` remembers. Bots scrape every public commit on GitHub
within **minutes**, specifically grepping for the prefixes below. `crv-leak-scan`
is the counter-move: scan locally, rotate early, purge properly.

## Quick start

```bash
# Scan working tree + last 100 commits of the current repo
npx crv-leak-scan

# Everything since day one, across all refs
npx crv-leak-scan --full-history

# Wire the pre-commit shield (blocks leaking commits locally)
npx crv-leak-scan --install-hook

# Check your setup: git, hook, wordlist integrity, detector self-test
npx crv-leak-scan --doctor
```

Real output β€” a key committed once and deleted later, found in history:

```
πŸ”΄ [CRITICAL] Alchemy Web3 RPC Endpoint
   File   : config.ts:1
   Origin : git history (commit 64db0602562a02ec707177c2b3d97a7d499ebc7f)
   Secret : https://eth-mainnet.g.alchemy.com/v2/kQ7z...****
   Fix    : Delete the key in the Alchemy dashboard (Apps β†’ API Keys), create a
            replacement, and review the app's usage stats for abuse. Purge the
            secret from history with `git filter-repo` (BFG also works), then
            force-push and have all clones re-clone.

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
  Scanned   1 file Β· 2 commits Β· 0.1s
  Findings  2 (2 unique secrets)
  Severity  πŸ”΄ 2 critical
  By type   Alchemy Web3 RPC Endpoint Γ—1 Β· Infura Web3 RPC Endpoint Γ—1
────────────────────────────────────────────────────────────────
❌ FAIL β€” 2 leaks at or above "critical".
   1. Revoke/rotate every key above in its provider dashboard β€” assume abuse.
   2. Purge history: `git filter-repo --replace-text expressions.txt`, then force-push.
   3. Prevent: move real secrets to a gitignored `.env` and add `.crvignore` for fixtures.
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
```

**Secrets are never printed.** Every finding is masked (`sk_live_...9a4f`,
`https://mainnet.infura.io/v3/4a9f...****`) β€” safe to paste in Slack, CI logs,
or a standup screen.

## What it detects

| Class | Detector | Severity |
|---|---|---|
| Web3 RPC | Alchemy (`*.g.alchemy.com/v2/…`) | πŸ”΄ critical |
| Web3 RPC | Infura (`/v3/<32-hex>`) | πŸ”΄ critical |
| Web3 RPC | QuickNode (`*.quiknode.pro/<token>`) | πŸ”΄ critical |
| Web3 RPC | Ankr (`rpc.ankr.com/<project>`) | πŸ”΄ critical |
| Web3 RPC | Tatum (`/v3/blockchain/node/…/<uuid>`) | πŸ”΄ critical |
| Crypto | Ethereum private key, `0x`-prefixed 64-hex | πŸ”΄ critical |
| Crypto | Ethereum private key, bare 64-hex (entropy + context gated) | πŸ”΄ critical |
| Crypto | BIP-39 seed phrase β€” 12 or 24 consecutive wordlist words | πŸ”΄ critical |
| Crypto | Solana private key (base58, 87–88 chars) | πŸ”΄ critical |
| Cloud/AI | OpenAI `sk-proj-…` and legacy `sk-…` keys | πŸ”΄ critical |
| Cloud/AI | Anthropic `sk-ant-…` keys | πŸ”΄ critical |
| Cloud/AI | Google / Gemini `AIzaSy…` keys | 🟑 high |
| Cloud | AWS Access Key ID (`AKIA`/`ASIA…`) | 🟑 high |
| Cloud | AWS Secret Access Key (40-char, keyword-gated) | πŸ”΄ critical |
| Payments | Stripe **live** secret keys (`sk_live_`, `rk_live_`) | πŸ”΄ critical |
| VCS | GitHub PATs (`ghp_`/`gho_`/`ghu_`/`ghs_`/`ghr_`, `github_pat_`) | πŸ”΄ critical |
| Database | Postgres / MySQL / MongoDB / Redis / AMQP URIs with passwords | 🟑 high |

### False-positive elimination (the part most scanners get wrong)

Three layers, applied in order:

1. **Placeholder rejection** β€” `YOUR_API_KEY`, `CHANGEME`, `0x000…000`,
   `deadbeef` repeats, sequential constants, `xxxxx` runs, `${TEMPLATE}`
   variables, and the canonical dev mnemonics (`test test … junk`,
   all-`abandon`) are silently ignored.
2. **Shannon entropy gate** β€” formatless hex/base64 candidates must clear
   **> 3.8 bits/char** (`H(X) = -Ξ£ P(x)Β·logβ‚‚ P(x)`). A true random key sits
   near its alphabet's ceiling; `aaaa…` and `123456…` sit at zero.
3. **Context gate** β€” bare 64-char hex (indistinguishable from transaction
   hashes by shape) only fires near key-flavored keywords (`private key`,
   `wallet`, `signer`, …). `0x`-prefixed keys fire on shape alone.

## Full history, on your terms

| Flag | Behavior |
|---|---|
| *(default)* | Working tree + **last 100 commits** β€” sub-2-second feel |
| `--depth <n>` | Last *n* commits |
| `--full-history` | Every commit, every ref β€” day one to HEAD |
| `--no-history` | Working tree only |
| `--staged` | Only what this commit would introduce (used by the hook) |

Untracked and uncommitted files are always scanned. Deleted keys come back as
findings with the exact commit SHA, ready to feed to `git filter-repo`.

## The pre-commit shield

```bash
npx crv-leak-scan --install-hook
```

Writes a clearly-marked block into `.git/hooks/pre-commit` (respects
`core.hooksPath`, never clobbers an existing hook, idempotent, reversible with
`--uninstall-hook`). From then on:

- clean commit β†’ scan runs in milliseconds, commit proceeds
- leaking commit β†’ **blocked before it ever reaches GitHub**

```text
βœ… CLEAN β€” no secrets found (staged changes (git diff --cached)).   β†’ commit lands
❌ FAIL β€” 1 leak … STRIPE_SECRET="sk_live_…"                        β†’ commit blocked
```

Emergency bypass: `git commit --no-verify` (then fix it, don't make a habit of it).

## Silencing intentional fixtures

Tests contain fake keys. Two precision tools β€” silence **one line** or **one
path**, never the whole tool:

```ts
// one line:
const mockRpc = "https://eth-mainnet.g.alchemy.com/v2/kQ7zR2vXw8mN4bC1dT6yH5uL0pEa9fJ3"; // crv-ignore
```

```gitignore
# .crvignore (gitignore-flavored: *, **, ?, !negation, trailing / = dir)
test/fixtures/
**/mock-secrets.ts
*.snap
!test/fixtures/keep-me.ts
```

## The AI-driven remediation pipeline (MCP)

`npx crv-leak-scan --mcp` turns the scanner into a
[Model Context Protocol](https://modelcontextprotocol.io) server. Point any
MCP client at it:

```json
{
  "mcpServers": {
    "crv-leak-scan": {
      "command": "npx",
      "args": ["-y", "crv-leak-scan", "--mcp"]
    }
  }
}
```

The connected agent gets five deterministic tools and one system prompt β€”
**the CRV OpSec Guardian** β€” that instructs it to run the pipeline end to end,
without human error:

| Step | Tool | What it does |
|---|---|---|
| 1 Β· Detect | `crv_scan_staged` / `crv_scan_workspace` | Regex + Shannon-entropy scan; returns `{file, line, type, rawSecret, suggestedEnvName, inGitHistory}` per leak |
| 2 Β· Remediate | `crv_auto_remediate` | Three atomic actions: write the secret to `.env`, force `.env` into `.gitignore`, and refactor the source line to read the environment β€” `process.env.X` (TS/JS), `os.getenv("X")` (Python), `std::env::var("X")` (Rust), `os.Getenv("X")` (Go), `${X}` (shell/YAML) |
| 3 Β· Purge | `crv_generate_purge_command` | Builds the exact `git filter-repo --replace-text` plan; the expressions file is written **inside `.git/`** so raw secrets never touch shell history or process lists |
| 4 Β· Verify | `crv_verify_clean` | Re-audits workspace or staged diff β†’ `{clean: true, status: "Ready to commit safely"}` |
| β€” | `crv_get_pipeline_instructions` | Returns the full guardian instructions to the agent |

The same guide ships as an MCP prompt (`crv_pipeline_guide`) and a resource
(`crv://pipeline-guide`), and as server instructions on initialize β€” so the
agent knows the playbook before its first tool call:

> *"You are the CRV OpSec Guardian… 1. First invoke `crv_scan_staged`. 2. If
> leaks are detected, DO NOT commit β€” call `crv_auto_remediate` to move
> secrets into .env and update .gitignore. 3. Purge history with
> `crv_generate_purge_command` when `inGitHistory` is set. 4. Call
> `crv_verify_clean` to confirm 100% security. 5. Report the fix."*

### Security boundary (read this)

Raw secrets flow **only** over the local stdio MCP channel β€” the agent needs
them to relocate values into `.env`, and it could read the file anyway.
Everything human-facing (terminal, `--json`, `--sarif`) stays masked, and
remediation results never echo secret material back. `.env` is skipped by the
worktree scan (it's the designated local store) but **never** by staged or
history scans β€” committing a `.env` is precisely the leak this tool catches.
And the prime rule the guardian enforces: moving a key to `.env` protects the
repo, not the key β€” anything already pushed must be rotated.

## CI/CD

### GitHub Actions (scan everything, gate the merge)

```yaml
name: leak-scan
on: [push, pull_request]
jobs:
  scan:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0        # full history β€” the whole point
      - uses: actions/setup-node@v4
        with:
          node-version: 20
      - run: npx --yes crv-leak-scan@latest --full-history .
```

Exit code `1` fails the job when anything at/above `--fail-on` (default
`critical`) is found.

### Machine-readable output

```bash
npx crv-leak-scan --json  > leak-report.json   # pipe to Slack, Datadog, a bot…
npx crv-leak-scan --sarif > leak-report.sarif  # GitHub code scanning
```

`--sarif` emits [SARIF 2.1.0](https://docs.github.com/en/code-security/code-scanning/integrating-with-code-scanning/uploading-a-sarif-file-to-github) β€”
upload it with `github/codeql-action/upload-sarif` and findings appear in the
repository's **Security tab** with rule metadata and fingerprints.

### Pre-commit (CI belt-and-suspenders)

```yaml
      - run: npx --yes crv-leak-scan@latest --staged .
```

## CLI reference

```
Usage: npx crv-leak-scan [path] [options]

Scan modes:   --staged | --depth <n> | --full-history | --no-history
Setup:        --install-hook | --uninstall-hook | --doctor | --mcp
Output:       --json | --sarif | --fail-on critical|high|medium | --quiet | --no-color
Other:        -v/--version Β· -h/--help

Exit codes:   0 clean Β· 1 leaks found Β· 2 usage/runtime error
```

## Architecture

```
src/
β”œβ”€β”€ cli.ts        argument parsing, orchestration, exit codes, --mcp wiring
β”œβ”€β”€ scanner.ts    worktree walk Β· streaming git log -p parser Β· staged diff
β”œβ”€β”€ detectors.ts  17 regex detectors + BIP-39 scanner Β· masking Β· remediation
β”œβ”€β”€ entropy.ts    Shannon entropy
β”œβ”€β”€ wordlist.ts   embedded BIP-39 English wordlist (2048, verified)
β”œβ”€β”€ ignore.ts     .crvignore patterns + inline crv-ignore suppression
β”œβ”€β”€ hook.ts       pre-commit install/uninstall (core.hooksPath aware)
β”œβ”€β”€ remediate.ts  .env write Β· .gitignore guard Β· per-language env accessor
β”œβ”€β”€ purge.ts      git-filter-repo plan + safe expressions file
β”œβ”€β”€ mcp.ts        MCP server: 5 tools + guardian prompt (stdio transport)
β”œβ”€β”€ report.ts     terminal / JSON / SARIF renderers
β”œβ”€β”€ selftest.ts   per-detector canaries + placeholder battery
└── doctor.ts     --doctor diagnostics
```

The history pass **streams** `git log -p` line by line β€” flat memory on repos
with gigabytes of history. Binary files are NUL-sniffed and skipped. The BIP-39
wordlist is structurally verified at build time (2048 words, unique, sorted β€”
`npm run wordlist` regenerates it from the canonical source).

```bash
npm install && npm test    # 134 tests: detectors, boundaries, git history, hook, CLI, MCP e2e
```

## Honest limitations

- A scanner finds leaks; it cannot **un-leak**. Anything pushed to a public
  remote must be treated as compromised the moment it left your machine β€”
  rotate first, purge second.
- Default mode reads the last 100 commits. That's a speed default, not a
  security guarantee β€” run `--full-history` in CI.
- Merge-commit-only introductions, quoted exotic git paths, and base64-body
  secrets without recognizable shapes are known blind spots.

---

**Built by CRV OpSec.** Scan early, rotate ruthlessly, purge properly.

MIT β€” see [LICENSE](LICENSE).