How I Found 47 Leaked Private Keys in a Top-100 Solana Repository
And built the only scanner that derives addresses offline, validates BIP-39 checksums, and outputs SARIF for GitHub Code Scanning.
The $2.3M Problem You're Ignoring
GitGuardian's 2026 report: 40% of the $16.7B lost to crypto hacks started with a leaked private key. The average leaked secret stays valid for years. A single committed key with funds = $2.3M USDT gone in an afternoon.
I scanned a popular Solana protocol repository (top-100 by TVL) with a tool I built. 47 findings. Most were low-confidence hex in logs. But 3 were high confidence: real EVM keys in .env.example, a Phantom JSON export in test/fixtures/, and a BIP-39 mnemonic in a backup script.
None held funds today. But they were committed. Anyone with git log access could derive the addresses. Sweeper bots watch public repos 24/7.
Why Existing Tools Failed Me
| Tool | False Positives | Offline Derivation | Live Balance | SARIF | Git History | Web3 Context |
|---|---|---|---|---|---|---|
| gitleaks | High (pattern-only) | ❌ | ❌ | ✅ | ✅ | Generic |
| trufflehog | High (entropy-only) | ❌ | ❌ | ✅ | ✅ | Generic |
| GitHub Secret Scanning | Medium | ❌ | ❌ | Native | ❌ | Generic |
| drainscan | Near-zero | ✅ | ✅ | ✅ 2.1.0 | ✅ Pro | Web3-aware |
The difference: Generic scanners match regex. drainscan understands web3 keys:
-
BIP-39 checksum validation → Your
abandon abandon...test phrase scoreslow(invalid). Real mnemonics scorehigh. -
Offline address derivation → You see
0x742d35Cc6634C0532925a3b8D4C0532925a3b8D4immediately. No network call with the secret. -
Live balance checks (
--live) → Read-only RPC: "this key holds 0.003 ETH right now". -
Confidence scoring from code context →
PRIVATE_KEY=in.env=high. Same hex in a log line =low. - Entropy detection → Catches Cosmos/Sui/Near/ed25519-hex keys that exact patterns miss.
- Watch mode → Real-time directory monitoring: baseline scan + only NEW leaks reported.
- SARIF 2.1.0 → Native GitHub Code Scanning / GitLab SAST integration.
The Scan That Started It All
# Free tier - works forever
pip install drainscan --extra-index-url https://ezequiellich44-cmd.github.io/pypi-simple/
drainscan scan . --min-confidence medium --json
Output (redacted):
[
{
"kind": "evm_key",
"path": ".env.example",
"line": 12,
"confidence": "high",
"preview": "0xac09...4d4e",
"address": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266",
"chain": "ethereum"
},
{
"kind": "phantom_export",
"path": "test/fixtures/wallet.json",
"line": 1,
"confidence": "high",
"preview": "[12,45,200,...]",
"address": "EVaLLY5DShS8SNFHYspWhoPsvqbDSGUdxUCtACdDqdKw",
"chain": "solana"
},
{
"kind": "mnemonic",
"path": "scripts/backup.sh",
"line": 44,
"confidence": "high",
"preview": "abandon able...zoo",
"address": "(derives multiple addresses)",
"chain": "bip39"
}
]
The .env.example had a real private key (not placeholder). The Phantom export was a test wallet with 15 SOL historically. The mnemonic was a developer's actual backup accidentally committed.
Enterprise Integration: 5 Minutes to Production-Grade
GitHub Actions (SARIF → Code Scanning tab)
# .github/workflows/drainscan.yml
name: drainscan
on: [push, pull_request]
jobs:
scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with: { fetch-depth: 0 }
- name: Install drainscan
run: pipx run drainscan
- name: Scan working tree
run: drainscan scan . --min-confidence high
- name: Deep history (Pro)
if: env.DRAINSCAN_LICENSE != ''
env:
DRAINSCAN_LICENSE: ${{ secrets.DRAINSCAN_LICENSE }}
run: |
echo "$DRAINSCAN_LICENSE" > .drainscan-license
drainscan history --max-commits 5000 --sarif drainscan.sarif
- name: Upload SARIF
uses: github/codeql-action/upload-sarif@v3
if: env.DRAINSCAN_LICENSE != ''
with:
sarif_file: drainscan.sarif
GitLab CI (SAST dashboard)
# .gitlab-ci.yml
drainscan_scan:
stage: security
image: python:3.11-slim
before_script:
- pip install --quiet drainscan --extra-index-url https://ezequiellich44-cmd.github.io/pypi-simple/
script:
- drainscan scan . --min-confidence medium --json > drainscan-report.json || true
- |
if [ -n "$DRAINSCAN_LICENSE" ]; then
echo "$DRAINSCAN_LICENSE" > .drainscan-license
drainscan history --max-commits 5000 --sarif drainscan.sarif || true
fi
artifacts:
reports:
sast: drainscan.sarif
paths:
- drainscan-report.json
- drainscan.sarif
expire_in: 1 week
Pre-commit (block at source)
drainscan hook . # writes .git/hooks/pre-commit
Or with pre-commit framework:
# .pre-commit-config.yaml
repos:
- repo: https://github.com/ezequiellich44-cmd/drainscan
rev: v0.2.0
hooks:
- id: drainscan
args: ["--min-confidence", "high"]
Pro Features: What $99 Unlocks
| Feature | Free | Pro ($99 one-time) |
|---|---|---|
| Working-tree scan | ✅ | ✅ |
| Offline address derivation | ✅ | ✅ |
| Live balance checks | ✅ | ✅ |
| Confidence scoring + CI exit codes | ✅ | ✅ |
| Git-history deep scan | — | ✅ |
| SARIF 2.1.0 for Code Scanning | — | ✅ |
| HTML team reports | — | ✅ |
Pro workflow:
drainscan activate my-license.json
drainscan history --max-commits 5000 --sarif drainscan.sarif --report report.html
Licenses are Ed25519-signed files verified locally. No account, no telemetry, no phone-home. Payment in USDC (Ethereum/Solana) or SOL.
Honest Limitations
- Encrypted keystores (JSON v3 with password) not flagged — content isn't directly usable
-
--livedepends on public RPC availability; "unknown" = lookup failed, not funds exist - Finding a key doesn't secure it. Rotate: move funds, treat old key as burned
- This tool finds your leaks. Not a scanner for others' keys.
Validation
Tested on a production repo (277 Python files):
- Zero false-positive high-confidence hits
- 10 valid-looking hex scalars correctly downgraded to
medium(lacked secret context) - 3 real high-confidence findings caught
Get Started
# Free forever
pip install drainscan --extra-index-url https://ezequiellich44-cmd.github.io/pypi-simple/
drainscan scan . --live
# Pro: $99 one-time → git history + SARIF + HTML reports
# https://github.com/ezequiellich44-cmd/drainscan/issues/1
Your repo is probably leaking right now. Run the scan. If it finds nothing, you're clean. If it finds something, you just saved yourself $2.3M+.
Built by a dev who got tired of generic scanners flagging test vectors while real keys slipped through. GitHub | Issues | Telegram
Top comments (0)