crv-leak-scan
by colby999
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).
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues