diff --git a/README.md b/README.md index d45d261..6a67cfb 100644 --- a/README.md +++ b/README.md @@ -127,7 +127,7 @@ Supports JetBrains IDEs and other ACP-compatible editors. See the [ACP setup gui ## Built-in Protection -Crust ships with **14 security rules** and **19 DLP token-detection patterns** out of the box: +Crust ships with **14 security rules** and **34 DLP token-detection patterns** out of the box: | Category | What's Protected | |----------|-----------------| @@ -138,12 +138,13 @@ Crust ships with **14 security rules** and **19 DLP token-detection patterns** o | **Package Tokens** | npm, pip, Cargo, Composer, NuGet, Gem auth tokens | | **Git Credentials** | `.git-credentials`, `.config/git/credentials` | | **Persistence** | Shell RC files, `authorized_keys` | -| **DLP Token Detection** | Content-based scanning for real API keys and tokens (AWS, GitHub, Stripe, OpenAI, Anthropic, and [14 more](docs/how-it-works.md#dlp-secret-detection)) | +| **DLP Token Detection** | Content-based scanning for real API keys and tokens (AWS, GitHub, Stripe, OpenAI, Anthropic, and [23 more](docs/how-it-works.md#dlp-secret-detection)) | | **Key Exfiltration** | Content-based PEM private key detection | +| **Crypto Wallets** | BIP39 mnemonics, xprv/WIF keys (checksum-validated), wallet directories for 16 chains | | **Self-Protection** | Agents cannot read, modify, or disable Crust itself | | **Dangerous Commands** | `eval`/`exec` with dynamic code execution | -All rules are open source: [`internal/rules/builtin/security.yaml`](internal/rules/builtin/security.yaml) (path rules) and [`internal/rules/dlp.go`](internal/rules/dlp.go) (DLP patterns) +All rules are open source: [`internal/rules/builtin/security.yaml`](internal/rules/builtin/security.yaml) (path rules), [`internal/rules/dlp.go`](internal/rules/dlp.go) (DLP patterns), and [`internal/rules/dlp_crypto.go`](internal/rules/dlp_crypto.go) (crypto key detection) ## Custom Rules @@ -183,7 +184,7 @@ Crust inspects tool calls at multiple layers: 2. **Layer 1 (Response Scan)**: Scans tool calls in the LLM's response before they execute — blocks new dangerous actions in real-time. 3. **Stdio Proxy** ([MCP](docs/mcp.md) / [ACP](docs/acp.md)): Wraps MCP servers or ACP agents as a stdio proxy, intercepting security-relevant JSON-RPC messages in both directions — including DLP scanning of server responses for leaked secrets. -All modes apply a [16-step evaluation pipeline](docs/how-it-works.md) — input sanitization, Unicode normalization, obfuscation detection, DLP secret scanning, path-based rules, and fallback content matching — each step in microseconds. +All modes apply a [17-step evaluation pipeline](docs/how-it-works.md) — input sanitization, Unicode normalization, obfuscation detection, DLP secret scanning, path-based rules, and fallback content matching — each step in microseconds. All activity is logged locally to encrypted storage. diff --git a/docs/how-it-works.md b/docs/how-it-works.md index d851f0f..a1af957 100644 --- a/docs/how-it-works.md +++ b/docs/how-it-works.md @@ -11,7 +11,7 @@ Agent Request ──▶ [Layer 0: History Scan] ──▶ LLM ──▶ [Layer 1 (14-30μs) (14-30μs) "Bad agent detected" "Action blocked" -Layer 1 Rule Evaluation (16 steps): +Layer 1 Rule Evaluation (17 steps): 1. Sanitize tool name → strip null bytes, control chars 2. Extract paths, commands, content from tool arguments 3. Normalize Unicode → NFKC, strip invisible chars and confusables @@ -20,14 +20,15 @@ Layer 1 Rule Evaluation (16 steps): 6. Block evasive commands (fork bombs, unparseable shell) 7. Self-protection → block management API access (hardcoded) 8. Block management API via Unix socket / named pipe - 9. DLP Secret Detection → block real API keys/tokens + 9. DLP Secret Detection → API keys/tokens + crypto keys (BIP39, xprv, WIF) 10. Filter bare shell globs (not real paths) 11. Normalize paths → expand ~, env vars 12. Expand globs against real filesystem - 13. Block /proc access (hardcoded) - 14. Resolve symlinks → match both original and resolved - 15. Operation-based rules → path/command/host matching - 16. Fallback rules (content-only) → raw JSON matching for ANY tool + 13. Resolve symlinks → match both original and resolved + 14. Block /proc access (hardcoded, after symlink resolution) + 15. Block crypto wallet access (hardcoded, after symlink resolution) + 16. Operation-based rules → path/command/host matching + 17. Fallback rules (content-only) → raw JSON matching for ANY tool ``` **Layer 0 (Request History):** Scans tool_calls in conversation history. Catches "bad agent" patterns where malicious actions already occurred in past turns. @@ -101,6 +102,10 @@ Layer 1 Rule Evaluation (16 steps): | ACP agent reads `.env` via IDE | - | - | - | ✅ Blocked | | ACP agent reads SSH keys via IDE | - | - | - | ✅ Blocked | | ACP agent runs `cat /etc/shadow` | - | - | - | ✅ Blocked | +| BIP39 mnemonic in content | - | ✅ Blocked (crypto DLP) | ✅ Blocked (DLP) | ✅ Blocked (DLP) | +| xprv/WIF private key in content | - | ✅ Blocked (crypto DLP) | ✅ Blocked (DLP) | ✅ Blocked (DLP) | +| Access `~/.bitcoin/wallet.dat` | - | ✅ Blocked (hardcoded) | - | - | +| Symlink to crypto wallet dir | - | ✅ Blocked (post-symlink) | - | - | --- @@ -138,7 +143,7 @@ The pre-filter runs before the shell parser (step 5) and catches encoding-based ## DLP Secret Detection -Step 9 of the evaluation pipeline runs hardcoded DLP (Data Loss Prevention) patterns against all operations. These patterns detect real API keys and tokens by their format, regardless of file path or tool name. +Step 9 of the evaluation pipeline runs DLP (Data Loss Prevention) patterns against all operations. These patterns detect real API keys, tokens, and cryptocurrency secrets by their format, regardless of file path or tool name. In stdio proxy modes (MCP Gateway, ACP Wrap, Auto-detect), DLP also scans **server/agent responses** before they reach the client. This catches secrets leaked by the subprocess — for example, an MCP server returning file content that contains an AWS access key. The response is replaced with a JSON-RPC error so the secret never reaches the client. @@ -152,17 +157,48 @@ In stdio proxy modes (MCP Gateway, ACP Wrap, Auto-detect), DLP also scans **serv | Google | API keys (`AIza...`) | | SendGrid | API keys (`SG....`) | | Heroku | API keys (`heroku_...`) | -| OpenAI | Project keys (`sk-proj-...`) | +| OpenAI | Project keys, admin keys (`sk-proj-...`, `sk-admin-...`) | | Anthropic | API keys (`sk-ant-api03-...`) | | Shopify | Shared secrets, access tokens (`shpss_...`, `shpat_...`) | | Databricks | Access tokens (`dapi...`) | | PyPI | Upload tokens (`pypi-...`) | | npm | Auth tokens (`npm_...`) | | age | Secret keys (`AGE-SECRET-KEY-...`) | +| Private keys | PEM format (RSA, EC, DSA, OpenSSH, Ed25519) | +| HuggingFace | API tokens (`hf_...`) | +| Groq | API keys (`gsk_...`) | +| Vercel | Tokens (`vercel_...`) | +| Supabase | Service keys (`sbp_...`) | +| DigitalOcean | PATs, OAuth tokens (`dop_v1_...`, `doo_v1_...`) | +| HashiCorp Vault | Tokens (`hvs....`) | +| Linear | API keys (`lin_api_...`) | +| Postman | API keys (`PMAK-...`) | +| Replicate | API tokens (`r8_...`) | +| Twilio | API keys (`SK...`) | +| Doppler | Tokens (`dp.st....`) | +| Firebase | Cloud Messaging keys (`AAAA...:...`) | -Patterns are sourced from [gitleaks v8.24](https://github.com/gitleaks/gitleaks), curated for blocking (not warning). See `internal/rules/dlp.go` for the full list. +Tier 1 patterns (34 hardcoded) are sourced from [gitleaks v8.24](https://github.com/gitleaks/gitleaks) and extended for newer services. See `internal/rules/dlp.go` for the full list. -In addition, [gitleaks](https://github.com/gitleaks/gitleaks) is used as a secondary scanner if installed, providing coverage for additional token formats beyond the hardcoded set. +Tier 2: [gitleaks](https://github.com/gitleaks/gitleaks) is used as a secondary scanner, providing 200+ additional token formats. Install with `brew install gitleaks` or `go install github.com/gitleaks/gitleaks/v8@latest`. + +### Cryptocurrency Key Detection + +Step 9 also runs crypto-specific DLP with **cryptographic validation** — not just regex matching. This eliminates false positives by verifying checksums. + +| Type | Detection | Validation | +|------|-----------|------------| +| BIP39 mnemonic | Sliding window (12/15/18/21/24 words) | Embedded 2048-word wordlist | +| Extended private key | `[xyzt]prv` prefix match | base58check checksum via btcutil | +| WIF private key | `[5KL]` prefix match | base58check checksum + version byte (0x80/0xEF) | + +BIP39 mnemonics are the universal seed phrase standard used by Bitcoin, Ethereum, Solana, Cardano, Cosmos, Polkadot, and most other chains. See `internal/rules/dlp_crypto.go` for the implementation. + +### Crypto Wallet Path Protection + +Step 15 blocks access to cryptocurrency wallet directories. Paths are computed at init using OS-specific data directories (e.g., `~/Library/Application Support/Bitcoin/` on macOS, `~/.bitcoin/` on Linux, `%LOCALAPPDATA%\Bitcoin` on Windows). This check runs **after symlink resolution** (step 13) so symlink bypasses are caught. + +Protected chains: Bitcoin, Litecoin, Dogecoin, Dash, Ethereum, Electrum, Monero, Zcash, Cardano, Cosmos, Polkadot, Avalanche, Tron, Solana, Sui, Aptos. --- @@ -184,10 +220,11 @@ The rule engine can protect against various attack vectors: |----------|----------| | Credentials | .env, SSH keys, cloud creds, tokens, DLP secret detection | | System | `/etc/passwd`, `/etc/shadow`, binaries, kernel modules, boot | +| Crypto Wallets | BIP39 mnemonics, xprv/WIF keys, wallet.dat, keystore (16 chains) | | Persistence | Shell RC, cron, systemd, git hooks | | Privilege Escalation | Sudoers, PAM, LD_PRELOAD | | Container Escape | Docker/containerd sockets | | Network | Internal networks, cloud metadata | -See `internal/rules/builtin/security.yaml` for actual built-in rules. +See `internal/rules/builtin/security.yaml` for path rules, `internal/rules/dlp.go` for token patterns, and `internal/rules/dlp_crypto.go` for crypto key detection. diff --git a/go.mod b/go.mod index 502b59c..a5b0656 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.24.13 require ( github.com/Microsoft/go-winio v0.6.2 + github.com/btcsuite/btcd/btcutil v1.1.6 github.com/charmbracelet/bubbles v1.0.0 github.com/charmbracelet/bubbletea v1.3.10 github.com/charmbracelet/huh v0.8.0 diff --git a/go.sum b/go.sum index 292435b..91aab27 100644 --- a/go.sum +++ b/go.sum @@ -2,12 +2,36 @@ github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII= github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= github.com/aymanbagabas/go-udiff v0.3.1 h1:LV+qyBQ2pqe0u42ZsUEtPiCaUoqgA9gYRDs3vj1nolY= github.com/aymanbagabas/go-udiff v0.3.1/go.mod h1:G0fsKmG+P6ylD0r6N/KgQD/nWzgfnl8ZBcNLgcbrw8E= +github.com/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ= +github.com/btcsuite/btcd v0.22.0-beta.0.20220111032746-97732e52810c/go.mod h1:tjmYdS6MLJ5/s0Fj4DbLgSbDHbEqLJrtnHecBFkdz5M= +github.com/btcsuite/btcd v0.23.5-0.20231215221805-96c9fd8078fd/go.mod h1:nm3Bko6zh6bWP60UxwoT5LzdGJsQJaPo6HjduXq9p6A= +github.com/btcsuite/btcd v0.24.2/go.mod h1:5C8ChTkl5ejr3WHj8tkQSCmydiMEPB0ZhQhehpq7Dgg= +github.com/btcsuite/btcd/btcec/v2 v2.1.0/go.mod h1:2VzYrv4Gm4apmbVVsSq5bqf1Ec8v56E48Vt0Y/umPgA= +github.com/btcsuite/btcd/btcec/v2 v2.1.3/go.mod h1:ctjw4H1kknNJmRN4iP1R7bTQ+v3GJkZBd6mui8ZsAZE= +github.com/btcsuite/btcd/btcutil v1.0.0/go.mod h1:Uoxwv0pqYWhD//tfTiipkxNfdhG9UrLwaeswfjfdF0A= +github.com/btcsuite/btcd/btcutil v1.1.0/go.mod h1:5OapHB7A2hBBWLm48mmw4MOHNJCcUBTwmWH/0Jn8VHE= +github.com/btcsuite/btcd/btcutil v1.1.5/go.mod h1:PSZZ4UitpLBWzxGd5VGOrLnmOjtPP/a6HaFo12zMs00= +github.com/btcsuite/btcd/btcutil v1.1.6 h1:zFL2+c3Lb9gEgqKNzowKUPQNb8jV7v5Oaodi/AYFd6c= +github.com/btcsuite/btcd/btcutil v1.1.6/go.mod h1:9dFymx8HpuLqBnsPELrImQeTQfKBQqzqGbbV3jK55aE= +github.com/btcsuite/btcd/chaincfg/chainhash v1.0.0/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc= +github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc= +github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc= +github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f/go.mod h1:TdznJufoqS23FtqVCzL0ZqgP5MqXbb4fg/WgDys70nA= +github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg= +github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd/go.mod h1:HHNXQzUsZCxOoE+CPiyCTO6x34Zs86zZUiwtpXoGdtg= +github.com/btcsuite/goleveldb v0.0.0-20160330041536-7834afc9e8cd/go.mod h1:F+uVaaLLH7j4eDXPRvw78tMflu7Ie2bzYOH4Y8rRKBY= +github.com/btcsuite/goleveldb v1.0.0/go.mod h1:QiK9vBlgftBg6rWQIj6wFzbPfRjiykIEhBH4obrXJ/I= +github.com/btcsuite/snappy-go v0.0.0-20151229074030-0bdef8d06723/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc= +github.com/btcsuite/snappy-go v1.0.0/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc= +github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY= +github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs= github.com/bytedance/sonic v1.14.0 h1:/OfKt8HFw0kh2rj8N0F6C/qPGRESq0BbaNZgcNXXzQQ= github.com/bytedance/sonic v1.14.0/go.mod h1:WoEbx8WTcFJfzCe0hbmyTGrfjt8PzNEBdxlNUO24NhA= github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA= @@ -54,13 +78,19 @@ github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU= github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s= github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE= +github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeCxkaw7y45JueMRL4DIyJDKs= +github.com/decred/dcrd/lru v1.0.0/go.mod h1:mxKOwFd7lFjN2GZYsiz/ecgqR6kkYAl+0pz0tEMk218= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/gabriel-vasile/mimetype v1.4.12 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCKbn0w9jyqw= @@ -85,13 +115,30 @@ github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw= github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= +github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= +github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlTRt3OuAQ= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/kelseyhightower/envconfig v1.4.0 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dvMUtDTo2cv8= github.com/kelseyhightower/envconfig v1.4.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg= +github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4= github.com/klauspost/compress v1.18.3 h1:9PJRvfbmTabkOX8moIpXPbMMbYN60bWImDDU7L+/6zw= github.com/klauspost/compress v1.18.3/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= @@ -126,6 +173,15 @@ github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk= github.com/mutecomm/go-sqlcipher/v4 v4.4.2 h1:eM10bFtI4UvibIsKr10/QT7Yfz+NADfjZYh0GKrXUNc= github.com/mutecomm/go-sqlcipher/v4 v4.4.2/go.mod h1:mF2UmIpBnzFeBdu/ypTDb/LdbS0nk0dfSN1WUsWTjMA= +github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= +github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= +github.com/onsi/gomega v1.4.1/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= +github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= +github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= @@ -148,11 +204,14 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc= github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA= @@ -163,27 +222,62 @@ go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko= go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o= golang.org/x/arch v0.20.0 h1:dx1zTU0MAE98U+TQ8BLl7XsJbgze2WnNKF/8tGp/Q6c= golang.org/x/arch v0.20.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk= +golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU= golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0= golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI= golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo= +golang.org/x/net v0.0.0-20180719180050-a680a1efc54d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200813134508-3edf25e44fcc/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY= golang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw= google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/rules/dlp.go b/internal/rules/dlp.go index 5863b70..08f18fe 100644 --- a/internal/rules/dlp.go +++ b/internal/rules/dlp.go @@ -3,8 +3,10 @@ package rules import "regexp" // Hardcoded DLP (Data Loss Prevention) token detection patterns. -// Compiled at init, checked on all operations. -// Sourced from gitleaks v8.24, curated for blocking (not warning). +// Compiled at init, checked on all operations (Tier 1). +// Core patterns sourced from gitleaks v8.24, extended for newer AI/cloud services. +// Curated for blocking (not warning) — each pattern must have a distinctive prefix +// to avoid false positives. type dlpPattern struct { name string @@ -140,5 +142,109 @@ var dlpPatterns = []dlpPattern{ message: "Cannot write age secret key — potential credential leak", }, + // Private keys (PEM format) — fires on ALL operations, not just writes. + // Catches RSA, EC, DSA, OpenSSH, Ed25519, and generic PRIVATE KEY headers. + { + name: "builtin:dlp-private-key", + re: regexp.MustCompile(`-----BEGIN[A-Z ]* PRIVATE KEY-----`), + message: "Cannot expose private key — potential credential leak", + }, + + // HuggingFace + { + name: "builtin:dlp-huggingface-token", + re: regexp.MustCompile(`hf_[A-Za-z0-9]{34,}`), + message: "Cannot write HuggingFace token — potential credential leak", + }, + + // Groq + { + name: "builtin:dlp-groq-api-key", + re: regexp.MustCompile(`gsk_[A-Za-z0-9]{48,}`), + message: "Cannot write Groq API key — potential credential leak", + }, + + // Vercel + { + name: "builtin:dlp-vercel-token", + re: regexp.MustCompile(`vercel_[A-Za-z0-9]{20,}`), + message: "Cannot write Vercel token — potential credential leak", + }, + + // Supabase + { + name: "builtin:dlp-supabase-key", + re: regexp.MustCompile(`sbp_[a-f0-9]{40,}`), + message: "Cannot write Supabase key — potential credential leak", + }, + + // DigitalOcean + { + name: "builtin:dlp-digitalocean-pat", + re: regexp.MustCompile(`dop_v1_[a-f0-9]{64}`), + message: "Cannot write DigitalOcean token — potential credential leak", + }, + { + name: "builtin:dlp-digitalocean-oauth", + re: regexp.MustCompile(`doo_v1_[a-f0-9]{64}`), + message: "Cannot write DigitalOcean OAuth token — potential credential leak", + }, + + // HashiCorp Vault + { + name: "builtin:dlp-hashicorp-vault", + re: regexp.MustCompile(`hvs\.[A-Za-z0-9_\-]{24,}`), + message: "Cannot write HashiCorp Vault token — potential credential leak", + }, + + // Linear + { + name: "builtin:dlp-linear-api-key", + re: regexp.MustCompile(`lin_api_[A-Za-z0-9]{40,}`), + message: "Cannot write Linear API key — potential credential leak", + }, + + // Postman + { + name: "builtin:dlp-postman-api-key", + re: regexp.MustCompile(`PMAK-[A-Za-z0-9]{24,}`), + message: "Cannot write Postman API key — potential credential leak", + }, + + // Replicate + { + name: "builtin:dlp-replicate-api-token", + re: regexp.MustCompile(`r8_[A-Za-z0-9]{36,}`), + message: "Cannot write Replicate API token — potential credential leak", + }, + + // Twilio + { + name: "builtin:dlp-twilio-api-key", + re: regexp.MustCompile(`SK[a-f0-9]{32}`), + message: "Cannot write Twilio API key — potential credential leak", + }, + + // Doppler + { + name: "builtin:dlp-doppler-token", + re: regexp.MustCompile(`dp\.st\.[a-zA-Z0-9_\-]{40,}`), + message: "Cannot write Doppler token — potential credential leak", + }, + + // OpenAI admin key + { + name: "builtin:dlp-openai-admin-key", + re: regexp.MustCompile(`sk-admin-[A-Za-z0-9_\-]{40,}`), + message: "Cannot write OpenAI admin key — potential credential leak", + }, + + // Firebase Cloud Messaging + { + name: "builtin:dlp-firebase-key", + re: regexp.MustCompile(`AAAA[A-Za-z0-9_\-]{7}:[A-Za-z0-9_\-]{140,}`), + message: "Cannot write Firebase key — potential credential leak", + }, + // Add new patterns above this line. } diff --git a/internal/rules/dlp_crypto.go b/internal/rules/dlp_crypto.go new file mode 100644 index 0000000..4eede79 --- /dev/null +++ b/internal/rules/dlp_crypto.go @@ -0,0 +1,643 @@ +package rules + +import ( + "os" + "path/filepath" + "regexp" + "runtime" + "strings" + "unicode" + + "github.com/btcsuite/btcd/btcutil/base58" +) + +// Crypto DLP: detects cryptocurrency secrets using cryptographic validation. +// - BIP39 mnemonics: sliding window over embedded 2048-word English wordlist +// - Extended private keys (xprv/yprv/zprv/tprv): regex + base58check checksum +// - WIF private keys (5/K/L prefix): regex + base58check checksum + version byte +// - Wallet path protection: hardcoded directory check using OS-specific data dirs + +// cryptoDLPMatch holds a crypto DLP detection result. +type cryptoDLPMatch struct { + name string + message string +} + +// scanCrypto checks content for cryptocurrency secrets. +// Returns the first match found, or nil if clean. +func scanCrypto(content string) *cryptoDLPMatch { + if content == "" { + return nil + } + if m := scanBIP39Mnemonic(content); m != nil { + return m + } + if m := scanExtendedPrivateKey(content); m != nil { + return m + } + if m := scanWIFKey(content); m != nil { + return m + } + return nil +} + +// --- BIP39 Mnemonic Detection --- + +// bip39ValidLengths are the valid BIP39 mnemonic lengths. +var bip39ValidLengths = []int{12, 15, 18, 21, 24} + +// scanBIP39Mnemonic detects BIP39 seed phrases using a sliding window. +// All 2048 words are embedded — no external dependency needed. +func scanBIP39Mnemonic(content string) *cryptoDLPMatch { + // Extract lowercase words, filtering non-alpha tokens. + words := extractLowerWords(content) + if len(words) < 12 { + return nil + } + + for _, windowSize := range bip39ValidLengths { + if len(words) < windowSize { + break + } + for i := 0; i <= len(words)-windowSize; i++ { + allMatch := true + for j := range windowSize { + if !bip39Wordlist[words[i+j]] { + // Skip ahead: no point checking windows that include this non-BIP39 word. + i += j + allMatch = false + break + } + } + if allMatch { + return &cryptoDLPMatch{ + name: "builtin:dlp-crypto-bip39-mnemonic", + message: "Cannot expose BIP39 mnemonic seed phrase — potential cryptocurrency key leak", + } + } + } + } + return nil +} + +// extractLowerWords splits content into lowercase alphabetic words. +func extractLowerWords(s string) []string { + var words []string + fields := strings.FieldsFunc(s, func(r rune) bool { + return !unicode.IsLetter(r) + }) + for _, f := range fields { + w := strings.ToLower(f) + if len(w) >= 3 && len(w) <= 8 { // BIP39 words are 3-8 chars + words = append(words, w) + } + } + return words +} + +// --- Extended Private Key Detection (xprv/yprv/zprv/tprv) --- + +// xprvRegex matches base58-encoded extended private keys. +var xprvRegex = regexp.MustCompile(`[xyzt]prv[1-9A-HJ-NP-Za-km-z]{107,112}`) + +// scanExtendedPrivateKey detects Bitcoin HD extended private keys with checksum validation. +func scanExtendedPrivateKey(content string) *cryptoDLPMatch { + matches := xprvRegex.FindAllString(content, 5) // limit to 5 candidates + for _, match := range matches { + _, _, err := base58.CheckDecode(match) + if err == nil { + return &cryptoDLPMatch{ + name: "builtin:dlp-crypto-xprv", + message: "Cannot expose extended private key (xprv/yprv/zprv/tprv) — potential cryptocurrency key leak", + } + } + } + return nil +} + +// --- WIF Private Key Detection --- + +// wifRegex matches WIF-encoded Bitcoin private keys. +// Uncompressed: starts with 5, 51 chars total. +// Compressed: starts with K or L, 52 chars total. +var wifRegex = regexp.MustCompile(`[5KL][1-9A-HJ-NP-Za-km-z]{50,51}`) + +// scanWIFKey detects Bitcoin WIF private keys with checksum + version byte validation. +func scanWIFKey(content string) *cryptoDLPMatch { + matches := wifRegex.FindAllString(content, 5) // limit to 5 candidates + for _, match := range matches { + decoded, version, err := base58.CheckDecode(match) + if err != nil { + continue + } + // WIF version byte: 0x80 for mainnet, 0xEF for testnet. + if version != 0x80 && version != 0xEF { + continue + } + // WIF payload: 32 bytes (uncompressed) or 33 bytes (compressed, ends with 0x01). + if len(decoded) == 32 || (len(decoded) == 33 && decoded[32] == 0x01) { + return &cryptoDLPMatch{ + name: "builtin:dlp-crypto-wif", + message: "Cannot expose WIF private key — potential cryptocurrency key leak", + } + } + } + return nil +} + +// --- Crypto Wallet Path Protection --- + +// cryptoWalletDirs are computed once at init using OS-specific data directories. +// Checked after symlink resolution (step 15) so symlink bypasses are caught. +var cryptoWalletDirs []string + +// cryptoDataDir returns the OS-specific data directory for a cryptocurrency. +// Follows the same convention as Bitcoin Core and most crypto wallets: +// - Linux/FreeBSD: ~/.chainname (lowercase, dot prefix) +// - macOS: ~/Library/Application Support/Chainname (title case) +// - Windows: %LOCALAPPDATA%\Chainname (title case) +func cryptoDataDir(home, chain string) string { + upper := string(unicode.ToUpper(rune(chain[0]))) + chain[1:] + lower := string(unicode.ToLower(rune(chain[0]))) + chain[1:] + + switch runtime.GOOS { + case "windows": + appData := os.Getenv("LOCALAPPDATA") + if appData == "" { + appData = os.Getenv("APPDATA") + } + if appData != "" { + return filepath.Join(appData, upper) + } + case "darwin": + if home != "" { + return filepath.Join(home, "Library", "Application Support", upper) + } + default: + if home != "" { + return filepath.Join(home, "."+lower) + } + } + return "" +} + +func init() { + home, err := os.UserHomeDir() + if err != nil { + home = "" + } + + // All major chains that follow the standard data directory convention. + for _, chain := range []string{ + "bitcoin", "litecoin", "dogecoin", "dash", // Bitcoin forks + "ethereum", "electrum", "monero", "zcash", // Major chains + "cardano", "cosmos", "polkadot", // PoS chains + "avalanche", "tron", // Other popular + } { + if dir := cryptoDataDir(home, chain); dir != "" { + cryptoWalletDirs = append(cryptoWalletDirs, dir) + } + } + + // Solana (non-standard locations). + if home != "" { + cryptoWalletDirs = append(cryptoWalletDirs, + filepath.Join(home, ".solana"), + filepath.Join(home, ".config", "solana"), + ) + // Sui, Aptos (newer chains with non-standard locations). + cryptoWalletDirs = append(cryptoWalletDirs, + filepath.Join(home, ".sui"), + filepath.Join(home, ".aptos"), + ) + } +} + +// hasCryptoWalletPath checks if any path is inside a crypto wallet directory. +func hasCryptoWalletPath(paths []string) (bool, string) { + for _, p := range paths { + cleaned := filepath.Clean(p) // normalize separators (/ → \ on Windows) + for _, dir := range cryptoWalletDirs { + if strings.HasPrefix(cleaned, dir+string(filepath.Separator)) || cleaned == dir { + return true, p + } + } + } + return false, "" +} + +// --- Embedded BIP39 English Wordlist (2048 words) --- +// Source: https://github.com/bitcoin/bips/blob/master/bip-0039/english.txt + +var bip39Wordlist = map[string]bool{ + "abandon": true, "ability": true, "able": true, "about": true, "above": true, + "absent": true, "absorb": true, "abstract": true, "absurd": true, "abuse": true, + "access": true, "accident": true, "account": true, "accuse": true, "achieve": true, + "acid": true, "acoustic": true, "acquire": true, "across": true, "act": true, + "action": true, "actor": true, "actress": true, "actual": true, "adapt": true, + "add": true, "addict": true, "address": true, "adjust": true, "admit": true, + "adult": true, "advance": true, "advice": true, "aerobic": true, "affair": true, + "afford": true, "afraid": true, "again": true, "age": true, "agent": true, + "agree": true, "ahead": true, "aim": true, "air": true, "airport": true, + "aisle": true, "alarm": true, "album": true, "alcohol": true, "alert": true, + "alien": true, "all": true, "alley": true, "allow": true, "almost": true, + "alone": true, "alpha": true, "already": true, "also": true, "alter": true, + "always": true, "amateur": true, "amazing": true, "among": true, "amount": true, + "amused": true, "analyst": true, "anchor": true, "ancient": true, "anger": true, + "angle": true, "angry": true, "animal": true, "ankle": true, "announce": true, + "annual": true, "another": true, "answer": true, "antenna": true, "antique": true, + "anxiety": true, "any": true, "apart": true, "apology": true, "appear": true, + "apple": true, "approve": true, "april": true, "arch": true, "arctic": true, + "area": true, "arena": true, "argue": true, "arm": true, "armed": true, + "armor": true, "army": true, "around": true, "arrange": true, "arrest": true, + "arrive": true, "arrow": true, "art": true, "artefact": true, "artist": true, //nolint:misspell // official BIP39 word + "artwork": true, "ask": true, "aspect": true, "assault": true, "asset": true, + "assist": true, "assume": true, "asthma": true, "athlete": true, "atom": true, + "attack": true, "attend": true, "attitude": true, "attract": true, "auction": true, + "audit": true, "august": true, "aunt": true, "author": true, "auto": true, + "autumn": true, "average": true, "avocado": true, "avoid": true, "awake": true, + "aware": true, "away": true, "awesome": true, "awful": true, "awkward": true, + "axis": true, "baby": true, "bachelor": true, "bacon": true, "badge": true, + "bag": true, "balance": true, "balcony": true, "ball": true, "bamboo": true, + "banana": true, "banner": true, "bar": true, "barely": true, "bargain": true, + "barrel": true, "base": true, "basic": true, "basket": true, "battle": true, + "beach": true, "bean": true, "beauty": true, "because": true, "become": true, + "beef": true, "before": true, "begin": true, "behave": true, "behind": true, + "believe": true, "below": true, "belt": true, "bench": true, "benefit": true, + "best": true, "betray": true, "better": true, "between": true, "beyond": true, + "bicycle": true, "bid": true, "bike": true, "bind": true, "biology": true, + "bird": true, "birth": true, "bitter": true, "black": true, "blade": true, + "blame": true, "blanket": true, "blast": true, "bleak": true, "bless": true, + "blind": true, "blood": true, "blossom": true, "blouse": true, "blue": true, + "blur": true, "blush": true, "board": true, "boat": true, "body": true, + "boil": true, "bomb": true, "bone": true, "bonus": true, "book": true, + "boost": true, "border": true, "boring": true, "borrow": true, "boss": true, + "bottom": true, "bounce": true, "box": true, "boy": true, "bracket": true, + "brain": true, "brand": true, "brass": true, "brave": true, "bread": true, + "breeze": true, "brick": true, "bridge": true, "brief": true, "bright": true, + "bring": true, "brisk": true, "broccoli": true, "broken": true, "bronze": true, + "broom": true, "brother": true, "brown": true, "brush": true, "bubble": true, + "buddy": true, "budget": true, "buffalo": true, "build": true, "bulb": true, + "bulk": true, "bullet": true, "bundle": true, "bunker": true, "burden": true, + "burger": true, "burst": true, "bus": true, "business": true, "busy": true, + "butter": true, "buyer": true, "buzz": true, "cabbage": true, "cabin": true, + "cable": true, "cactus": true, "cage": true, "cake": true, "call": true, + "calm": true, "camera": true, "camp": true, "can": true, "canal": true, + "cancel": true, "candy": true, "cannon": true, "canoe": true, "canvas": true, + "canyon": true, "capable": true, "capital": true, "captain": true, "car": true, + "carbon": true, "card": true, "cargo": true, "carpet": true, "carry": true, + "cart": true, "case": true, "cash": true, "casino": true, "castle": true, + "casual": true, "cat": true, "catalog": true, "catch": true, "category": true, + "cattle": true, "caught": true, "cause": true, "caution": true, "cave": true, + "ceiling": true, "celery": true, "cement": true, "census": true, "century": true, + "cereal": true, "certain": true, "chair": true, "chalk": true, "champion": true, + "change": true, "chaos": true, "chapter": true, "charge": true, "chase": true, + "chat": true, "cheap": true, "check": true, "cheese": true, "chef": true, + "cherry": true, "chest": true, "chicken": true, "chief": true, "child": true, + "chimney": true, "choice": true, "choose": true, "chronic": true, "chuckle": true, + "chunk": true, "churn": true, "cigar": true, "cinnamon": true, "circle": true, + "citizen": true, "city": true, "civil": true, "claim": true, "clap": true, + "clarify": true, "claw": true, "clay": true, "clean": true, "clerk": true, + "clever": true, "click": true, "client": true, "cliff": true, "climb": true, + "clinic": true, "clip": true, "clock": true, "clog": true, "close": true, + "cloth": true, "cloud": true, "clown": true, "club": true, "clump": true, + "cluster": true, "clutch": true, "coach": true, "coast": true, "coconut": true, + "code": true, "coffee": true, "coil": true, "coin": true, "collect": true, + "color": true, "column": true, "combine": true, "come": true, "comfort": true, + "comic": true, "common": true, "company": true, "concert": true, "conduct": true, + "confirm": true, "congress": true, "connect": true, "consider": true, "control": true, + "convince": true, "cook": true, "cool": true, "copper": true, "copy": true, + "coral": true, "core": true, "corn": true, "correct": true, "cost": true, + "cotton": true, "couch": true, "country": true, "couple": true, "course": true, + "cousin": true, "cover": true, "coyote": true, "crack": true, "cradle": true, + "craft": true, "cram": true, "crane": true, "crash": true, "crater": true, + "crawl": true, "crazy": true, "cream": true, "credit": true, "creek": true, + "crew": true, "cricket": true, "crime": true, "crisp": true, "critic": true, + "crop": true, "cross": true, "crouch": true, "crowd": true, "crucial": true, + "cruel": true, "cruise": true, "crumble": true, "crunch": true, "crush": true, + "cry": true, "crystal": true, "cube": true, "culture": true, "cup": true, + "cupboard": true, "curious": true, "current": true, "curtain": true, "curve": true, + "cushion": true, "custom": true, "cute": true, "cycle": true, "dad": true, + "damage": true, "damp": true, "dance": true, "danger": true, "daring": true, + "dash": true, "daughter": true, "dawn": true, "day": true, "deal": true, + "debate": true, "debris": true, "decade": true, "december": true, "decide": true, + "decline": true, "decorate": true, "decrease": true, "deer": true, "defense": true, + "define": true, "defy": true, "degree": true, "delay": true, "deliver": true, + "demand": true, "demise": true, "denial": true, "dentist": true, "deny": true, + "depart": true, "depend": true, "deposit": true, "depth": true, "deputy": true, + "derive": true, "describe": true, "desert": true, "design": true, "desk": true, + "despair": true, "destroy": true, "detail": true, "detect": true, "develop": true, + "device": true, "devote": true, "diagram": true, "dial": true, "diamond": true, + "diary": true, "dice": true, "diesel": true, "diet": true, "differ": true, + "digital": true, "dignity": true, "dilemma": true, "dinner": true, "dinosaur": true, + "direct": true, "dirt": true, "disagree": true, "discover": true, "disease": true, + "dish": true, "dismiss": true, "disorder": true, "display": true, "distance": true, + "divert": true, "divide": true, "divorce": true, "dizzy": true, "doctor": true, + "document": true, "dog": true, "doll": true, "dolphin": true, "domain": true, + "donate": true, "donkey": true, "donor": true, "door": true, "dose": true, + "double": true, "dove": true, "draft": true, "dragon": true, "drama": true, + "drastic": true, "draw": true, "dream": true, "dress": true, "drift": true, + "drill": true, "drink": true, "drip": true, "drive": true, "drop": true, + "drum": true, "dry": true, "duck": true, "dumb": true, "dune": true, + "during": true, "dust": true, "dutch": true, "duty": true, "dwarf": true, + "dynamic": true, "eager": true, "eagle": true, "early": true, "earn": true, + "earth": true, "easily": true, "east": true, "easy": true, "echo": true, + "ecology": true, "economy": true, "edge": true, "edit": true, "educate": true, + "effort": true, "egg": true, "eight": true, "either": true, "elbow": true, + "elder": true, "electric": true, "elegant": true, "element": true, "elephant": true, + "elevator": true, "elite": true, "else": true, "embark": true, "embody": true, + "embrace": true, "emerge": true, "emotion": true, "employ": true, "empower": true, + "empty": true, "enable": true, "enact": true, "end": true, "endless": true, + "endorse": true, "enemy": true, "energy": true, "enforce": true, "engage": true, + "engine": true, "enhance": true, "enjoy": true, "enlist": true, "enough": true, + "enrich": true, "enroll": true, "ensure": true, "enter": true, "entire": true, + "entry": true, "envelope": true, "episode": true, "equal": true, "equip": true, + "era": true, "erase": true, "erode": true, "erosion": true, "error": true, + "erupt": true, "escape": true, "essay": true, "essence": true, "estate": true, + "eternal": true, "ethics": true, "evidence": true, "evil": true, "evoke": true, + "evolve": true, "exact": true, "example": true, "excess": true, "exchange": true, + "excite": true, "exclude": true, "excuse": true, "execute": true, "exercise": true, + "exhaust": true, "exhibit": true, "exile": true, "exist": true, "exit": true, + "exotic": true, "expand": true, "expect": true, "expire": true, "explain": true, + "expose": true, "express": true, "extend": true, "extra": true, "eye": true, + "eyebrow": true, "fabric": true, "face": true, "faculty": true, "fade": true, + "faint": true, "faith": true, "fall": true, "false": true, "fame": true, + "family": true, "famous": true, "fan": true, "fancy": true, "fantasy": true, + "farm": true, "fashion": true, "fat": true, "fatal": true, "father": true, + "fatigue": true, "fault": true, "favorite": true, "feature": true, "february": true, + "federal": true, "fee": true, "feed": true, "feel": true, "female": true, + "fence": true, "festival": true, "fetch": true, "fever": true, "few": true, + "fiber": true, "fiction": true, "field": true, "figure": true, "file": true, + "film": true, "filter": true, "final": true, "find": true, "fine": true, + "finger": true, "finish": true, "fire": true, "firm": true, "first": true, + "fiscal": true, "fish": true, "fit": true, "fitness": true, "fix": true, + "flag": true, "flame": true, "flash": true, "flat": true, "flavor": true, + "flee": true, "flight": true, "flip": true, "float": true, "flock": true, + "floor": true, "flower": true, "fluid": true, "flush": true, "fly": true, + "foam": true, "focus": true, "fog": true, "foil": true, "fold": true, + "follow": true, "food": true, "foot": true, "force": true, "forest": true, + "forget": true, "fork": true, "fortune": true, "forum": true, "forward": true, + "fossil": true, "foster": true, "found": true, "fox": true, "fragile": true, + "frame": true, "frequent": true, "fresh": true, "friend": true, "fringe": true, + "frog": true, "front": true, "frost": true, "frown": true, "frozen": true, + "fruit": true, "fuel": true, "fun": true, "funny": true, "furnace": true, + "fury": true, "future": true, "gadget": true, "gain": true, "galaxy": true, + "gallery": true, "game": true, "gap": true, "garage": true, "garbage": true, + "garden": true, "garlic": true, "garment": true, "gas": true, "gasp": true, + "gate": true, "gather": true, "gauge": true, "gaze": true, "general": true, + "genius": true, "genre": true, "gentle": true, "genuine": true, "gesture": true, + "ghost": true, "giant": true, "gift": true, "giggle": true, "ginger": true, + "giraffe": true, "girl": true, "give": true, "glad": true, "glance": true, + "glare": true, "glass": true, "glide": true, "glimpse": true, "globe": true, + "gloom": true, "glory": true, "glove": true, "glow": true, "glue": true, + "goat": true, "goddess": true, "gold": true, "good": true, "goose": true, + "gorilla": true, "gospel": true, "gossip": true, "govern": true, "gown": true, + "grab": true, "grace": true, "grain": true, "grant": true, "grape": true, + "grass": true, "gravity": true, "great": true, "green": true, "grid": true, + "grief": true, "grit": true, "grocery": true, "group": true, "grow": true, + "grunt": true, "guard": true, "guess": true, "guide": true, "guilt": true, + "guitar": true, "gun": true, "gym": true, "habit": true, "hair": true, + "half": true, "hammer": true, "hamster": true, "hand": true, "happy": true, + "harbor": true, "hard": true, "harsh": true, "harvest": true, "hat": true, + "have": true, "hawk": true, "hazard": true, "head": true, "health": true, + "heart": true, "heavy": true, "hedgehog": true, "height": true, "hello": true, + "helmet": true, "help": true, "hen": true, "hero": true, "hidden": true, + "high": true, "hill": true, "hint": true, "hip": true, "hire": true, + "history": true, "hobby": true, "hockey": true, "hold": true, "hole": true, + "holiday": true, "hollow": true, "home": true, "honey": true, "hood": true, + "hope": true, "horn": true, "horror": true, "horse": true, "hospital": true, + "host": true, "hotel": true, "hour": true, "hover": true, "hub": true, + "huge": true, "human": true, "humble": true, "humor": true, "hundred": true, + "hungry": true, "hunt": true, "hurdle": true, "hurry": true, "hurt": true, + "husband": true, "hybrid": true, "ice": true, "icon": true, "idea": true, + "identify": true, "idle": true, "ignore": true, "ill": true, "illegal": true, + "illness": true, "image": true, "imitate": true, "immense": true, "immune": true, + "impact": true, "impose": true, "improve": true, "impulse": true, "inch": true, + "include": true, "income": true, "increase": true, "index": true, "indicate": true, + "indoor": true, "industry": true, "infant": true, "inflict": true, "inform": true, + "inhale": true, "inherit": true, "initial": true, "inject": true, "injury": true, + "inmate": true, "inner": true, "innocent": true, "input": true, "inquiry": true, + "insane": true, "insect": true, "inside": true, "inspire": true, "install": true, + "intact": true, "interest": true, "into": true, "invest": true, "invite": true, + "involve": true, "iron": true, "island": true, "isolate": true, "issue": true, + "item": true, "ivory": true, "jacket": true, "jaguar": true, "jar": true, + "jazz": true, "jealous": true, "jeans": true, "jelly": true, "jewel": true, + "job": true, "join": true, "joke": true, "journey": true, "joy": true, + "judge": true, "juice": true, "jump": true, "jungle": true, "junior": true, + "junk": true, "just": true, "kangaroo": true, "keen": true, "keep": true, + "ketchup": true, "key": true, "kick": true, "kid": true, "kidney": true, + "kind": true, "kingdom": true, "kiss": true, "kit": true, "kitchen": true, + "kite": true, "kitten": true, "kiwi": true, "knee": true, "knife": true, + "knock": true, "know": true, "lab": true, "label": true, "labor": true, + "ladder": true, "lady": true, "lake": true, "lamp": true, "language": true, + "laptop": true, "large": true, "later": true, "latin": true, "laugh": true, + "laundry": true, "lava": true, "law": true, "lawn": true, "lawsuit": true, + "layer": true, "lazy": true, "leader": true, "leaf": true, "learn": true, + "leave": true, "lecture": true, "left": true, "leg": true, "legal": true, + "legend": true, "leisure": true, "lemon": true, "lend": true, "length": true, + "lens": true, "leopard": true, "lesson": true, "letter": true, "level": true, + "liar": true, "liberty": true, "library": true, "license": true, "life": true, + "lift": true, "light": true, "like": true, "limb": true, "limit": true, + "link": true, "lion": true, "liquid": true, "list": true, "little": true, + "live": true, "lizard": true, "load": true, "loan": true, "lobster": true, + "local": true, "lock": true, "logic": true, "lonely": true, "long": true, + "loop": true, "lottery": true, "loud": true, "lounge": true, "love": true, + "loyal": true, "lucky": true, "luggage": true, "lumber": true, "lunar": true, + "lunch": true, "luxury": true, "lyrics": true, "machine": true, "mad": true, + "magic": true, "magnet": true, "maid": true, "mail": true, "main": true, + "major": true, "make": true, "mammal": true, "man": true, "manage": true, + "mandate": true, "mango": true, "mansion": true, "manual": true, "maple": true, + "marble": true, "march": true, "margin": true, "marine": true, "market": true, + "marriage": true, "mask": true, "mass": true, "master": true, "match": true, + "material": true, "math": true, "matrix": true, "matter": true, "maximum": true, + "maze": true, "meadow": true, "mean": true, "measure": true, "meat": true, + "mechanic": true, "medal": true, "media": true, "melody": true, "melt": true, + "member": true, "memory": true, "mention": true, "menu": true, "mercy": true, + "merge": true, "merit": true, "merry": true, "mesh": true, "message": true, + "metal": true, "method": true, "middle": true, "midnight": true, "milk": true, + "million": true, "mimic": true, "mind": true, "minimum": true, "minor": true, + "minute": true, "miracle": true, "mirror": true, "misery": true, "miss": true, + "mistake": true, "mix": true, "mixed": true, "mixture": true, "mobile": true, + "model": true, "modify": true, "mom": true, "moment": true, "monitor": true, + "monkey": true, "monster": true, "month": true, "moon": true, "moral": true, + "more": true, "morning": true, "mosquito": true, "mother": true, "motion": true, + "motor": true, "mountain": true, "mouse": true, "move": true, "movie": true, + "much": true, "muffin": true, "mule": true, "multiply": true, "muscle": true, + "museum": true, "mushroom": true, "music": true, "must": true, "mutual": true, + "myself": true, "mystery": true, "myth": true, "naive": true, "name": true, + "napkin": true, "narrow": true, "nasty": true, "nation": true, "nature": true, + "near": true, "neck": true, "need": true, "negative": true, "neglect": true, + "neither": true, "nephew": true, "nerve": true, "nest": true, "net": true, + "network": true, "neutral": true, "never": true, "news": true, "next": true, + "nice": true, "night": true, "noble": true, "noise": true, "nominee": true, + "noodle": true, "normal": true, "north": true, "nose": true, "notable": true, + "note": true, "nothing": true, "notice": true, "novel": true, "now": true, + "nuclear": true, "number": true, "nurse": true, "nut": true, "oak": true, + "obey": true, "object": true, "oblige": true, "obscure": true, "observe": true, + "obtain": true, "obvious": true, "occur": true, "ocean": true, "october": true, + "odor": true, "off": true, "offer": true, "office": true, "often": true, + "oil": true, "okay": true, "old": true, "olive": true, "olympic": true, + "omit": true, "once": true, "one": true, "onion": true, "online": true, + "only": true, "open": true, "opera": true, "opinion": true, "oppose": true, + "option": true, "orange": true, "orbit": true, "orchard": true, "order": true, + "ordinary": true, "organ": true, "orient": true, "original": true, "orphan": true, + "ostrich": true, "other": true, "outdoor": true, "outer": true, "output": true, + "outside": true, "oval": true, "oven": true, "over": true, "own": true, + "owner": true, "oxygen": true, "oyster": true, "ozone": true, "pact": true, + "paddle": true, "page": true, "pair": true, "palace": true, "palm": true, + "panda": true, "panel": true, "panic": true, "panther": true, "paper": true, + "parade": true, "parent": true, "park": true, "parrot": true, "party": true, + "pass": true, "patch": true, "path": true, "patient": true, "patrol": true, + "pattern": true, "pause": true, "pave": true, "payment": true, "peace": true, + "peanut": true, "pear": true, "peasant": true, "pelican": true, "pen": true, + "penalty": true, "pencil": true, "people": true, "pepper": true, "perfect": true, + "permit": true, "person": true, "pet": true, "phone": true, "photo": true, + "phrase": true, "physical": true, "piano": true, "picnic": true, "picture": true, + "piece": true, "pig": true, "pigeon": true, "pill": true, "pilot": true, + "pink": true, "pioneer": true, "pipe": true, "pistol": true, "pitch": true, + "pizza": true, "place": true, "planet": true, "plastic": true, "plate": true, + "play": true, "please": true, "pledge": true, "pluck": true, "plug": true, + "plunge": true, "poem": true, "poet": true, "point": true, "polar": true, + "pole": true, "police": true, "pond": true, "pony": true, "pool": true, + "popular": true, "portion": true, "position": true, "possible": true, "post": true, + "potato": true, "pottery": true, "poverty": true, "powder": true, "power": true, + "practice": true, "praise": true, "predict": true, "prefer": true, "prepare": true, + "present": true, "pretty": true, "prevent": true, "price": true, "pride": true, + "primary": true, "print": true, "priority": true, "prison": true, "private": true, + "prize": true, "problem": true, "process": true, "produce": true, "profit": true, + "program": true, "project": true, "promote": true, "proof": true, "property": true, + "prosper": true, "protect": true, "proud": true, "provide": true, "public": true, + "pudding": true, "pull": true, "pulp": true, "pulse": true, "pumpkin": true, + "punch": true, "pupil": true, "puppy": true, "purchase": true, "purity": true, + "purpose": true, "purse": true, "push": true, "put": true, "puzzle": true, + "pyramid": true, "quality": true, "quantum": true, "quarter": true, "question": true, + "quick": true, "quit": true, "quiz": true, "quote": true, "rabbit": true, + "raccoon": true, "race": true, "rack": true, "radar": true, "radio": true, + "rail": true, "rain": true, "raise": true, "rally": true, "ramp": true, + "ranch": true, "random": true, "range": true, "rapid": true, "rare": true, + "rate": true, "rather": true, "raven": true, "raw": true, "razor": true, + "ready": true, "real": true, "reason": true, "rebel": true, "rebuild": true, + "recall": true, "receive": true, "recipe": true, "record": true, "recycle": true, + "reduce": true, "reflect": true, "reform": true, "refuse": true, "region": true, + "regret": true, "regular": true, "reject": true, "relax": true, "release": true, + "relief": true, "rely": true, "remain": true, "remember": true, "remind": true, + "remove": true, "render": true, "renew": true, "rent": true, "reopen": true, + "repair": true, "repeat": true, "replace": true, "report": true, "require": true, + "rescue": true, "resemble": true, "resist": true, "resource": true, "response": true, + "result": true, "retire": true, "retreat": true, "return": true, "reunion": true, + "reveal": true, "review": true, "reward": true, "rhythm": true, "rib": true, + "ribbon": true, "rice": true, "rich": true, "ride": true, "ridge": true, + "rifle": true, "right": true, "rigid": true, "ring": true, "riot": true, + "ripple": true, "risk": true, "ritual": true, "rival": true, "river": true, + "road": true, "roast": true, "robot": true, "robust": true, "rocket": true, + "romance": true, "roof": true, "rookie": true, "room": true, "rose": true, + "rotate": true, "rough": true, "round": true, "route": true, "royal": true, + "rubber": true, "rude": true, "rug": true, "rule": true, "run": true, + "runway": true, "rural": true, "sad": true, "saddle": true, "sadness": true, + "safe": true, "sail": true, "salad": true, "salmon": true, "salon": true, + "salt": true, "salute": true, "same": true, "sample": true, "sand": true, + "satisfy": true, "satoshi": true, "sauce": true, "sausage": true, "save": true, + "say": true, "scale": true, "scan": true, "scare": true, "scatter": true, + "scene": true, "scheme": true, "school": true, "science": true, "scissors": true, + "scorpion": true, "scout": true, "scrap": true, "screen": true, "script": true, + "scrub": true, "sea": true, "search": true, "season": true, "seat": true, + "second": true, "secret": true, "section": true, "security": true, "seed": true, + "seek": true, "segment": true, "select": true, "sell": true, "seminar": true, + "senior": true, "sense": true, "sentence": true, "series": true, "service": true, + "session": true, "settle": true, "setup": true, "seven": true, "shadow": true, + "shaft": true, "shallow": true, "share": true, "shed": true, "shell": true, + "sheriff": true, "shield": true, "shift": true, "shine": true, "ship": true, + "shiver": true, "shock": true, "shoe": true, "shoot": true, "shop": true, + "short": true, "shoulder": true, "shove": true, "shrimp": true, "shrug": true, + "shuffle": true, "shy": true, "sibling": true, "sick": true, "side": true, + "siege": true, "sight": true, "sign": true, "silent": true, "silk": true, + "silly": true, "silver": true, "similar": true, "simple": true, "since": true, + "sing": true, "siren": true, "sister": true, "situate": true, "six": true, + "size": true, "skate": true, "sketch": true, "ski": true, "skill": true, + "skin": true, "skirt": true, "skull": true, "slab": true, "slam": true, + "sleep": true, "slender": true, "slice": true, "slide": true, "slight": true, + "slim": true, "slogan": true, "slot": true, "slow": true, "slush": true, + "small": true, "smart": true, "smile": true, "smoke": true, "smooth": true, + "snack": true, "snake": true, "snap": true, "sniff": true, "snow": true, + "soap": true, "soccer": true, "social": true, "sock": true, "soda": true, + "soft": true, "solar": true, "soldier": true, "solid": true, "solution": true, + "solve": true, "someone": true, "song": true, "soon": true, "sorry": true, + "sort": true, "soul": true, "sound": true, "soup": true, "source": true, + "south": true, "space": true, "spare": true, "spatial": true, "spawn": true, + "speak": true, "special": true, "speed": true, "spell": true, "spend": true, + "sphere": true, "spice": true, "spider": true, "spike": true, "spin": true, + "spirit": true, "split": true, "spoil": true, "sponsor": true, "spoon": true, + "sport": true, "spot": true, "spray": true, "spread": true, "spring": true, + "spy": true, "square": true, "squeeze": true, "squirrel": true, "stable": true, + "stadium": true, "staff": true, "stage": true, "stairs": true, "stamp": true, + "stand": true, "start": true, "state": true, "stay": true, "steak": true, + "steel": true, "stem": true, "step": true, "stereo": true, "stick": true, + "still": true, "sting": true, "stock": true, "stomach": true, "stone": true, + "stool": true, "story": true, "stove": true, "strategy": true, "street": true, + "strike": true, "strong": true, "struggle": true, "student": true, "stuff": true, + "stumble": true, "style": true, "subject": true, "submit": true, "subway": true, + "success": true, "such": true, "sudden": true, "suffer": true, "sugar": true, + "suggest": true, "suit": true, "summer": true, "sun": true, "sunny": true, + "sunset": true, "super": true, "supply": true, "supreme": true, "sure": true, + "surface": true, "surge": true, "surprise": true, "surround": true, "survey": true, + "suspect": true, "sustain": true, "swallow": true, "swamp": true, "swap": true, + "swarm": true, "swear": true, "sweet": true, "swift": true, "swim": true, + "swing": true, "switch": true, "sword": true, "symbol": true, "symptom": true, + "syrup": true, "system": true, "table": true, "tackle": true, "tag": true, + "tail": true, "talent": true, "talk": true, "tank": true, "tape": true, + "target": true, "task": true, "taste": true, "tattoo": true, "taxi": true, + "teach": true, "team": true, "tell": true, "ten": true, "tenant": true, + "tennis": true, "tent": true, "term": true, "test": true, "text": true, + "thank": true, "that": true, "theme": true, "then": true, "theory": true, + "there": true, "they": true, "thing": true, "this": true, "thought": true, + "three": true, "thrive": true, "throw": true, "thumb": true, "thunder": true, + "ticket": true, "tide": true, "tiger": true, "tilt": true, "timber": true, + "time": true, "tiny": true, "tip": true, "tired": true, "tissue": true, + "title": true, "toast": true, "tobacco": true, "today": true, "toddler": true, + "toe": true, "together": true, "toilet": true, "token": true, "tomato": true, + "tomorrow": true, "tone": true, "tongue": true, "tonight": true, "tool": true, + "tooth": true, "top": true, "topic": true, "topple": true, "torch": true, + "tornado": true, "tortoise": true, "toss": true, "total": true, "tourist": true, + "toward": true, "tower": true, "town": true, "toy": true, "track": true, + "trade": true, "traffic": true, "tragic": true, "train": true, "transfer": true, + "trap": true, "trash": true, "travel": true, "tray": true, "treat": true, + "tree": true, "trend": true, "trial": true, "tribe": true, "trick": true, + "trigger": true, "trim": true, "trip": true, "trophy": true, "trouble": true, + "truck": true, "true": true, "truly": true, "trumpet": true, "trust": true, + "truth": true, "try": true, "tube": true, "tuition": true, "tumble": true, + "tuna": true, "tunnel": true, "turkey": true, "turn": true, "turtle": true, + "twelve": true, "twenty": true, "twice": true, "twin": true, "twist": true, + "two": true, "type": true, "typical": true, "ugly": true, "umbrella": true, + "unable": true, "unaware": true, "uncle": true, "uncover": true, "under": true, + "undo": true, "unfair": true, "unfold": true, "unhappy": true, "uniform": true, + "unique": true, "unit": true, "universe": true, "unknown": true, "unlock": true, + "until": true, "unusual": true, "unveil": true, "update": true, "upgrade": true, + "uphold": true, "upon": true, "upper": true, "upset": true, "urban": true, + "urge": true, "usage": true, "use": true, "used": true, "useful": true, + "useless": true, "usual": true, "utility": true, "vacant": true, "vacuum": true, + "vague": true, "valid": true, "valley": true, "valve": true, "van": true, + "vanish": true, "vapor": true, "various": true, "vast": true, "vault": true, + "vehicle": true, "velvet": true, "vendor": true, "venture": true, "venue": true, + "verb": true, "verify": true, "version": true, "very": true, "vessel": true, + "veteran": true, "viable": true, "vibrant": true, "vicious": true, "victory": true, + "video": true, "view": true, "village": true, "vintage": true, "violin": true, + "virtual": true, "virus": true, "visa": true, "visit": true, "visual": true, + "vital": true, "vivid": true, "vocal": true, "voice": true, "void": true, + "volcano": true, "volume": true, "vote": true, "voyage": true, "wage": true, + "wagon": true, "wait": true, "walk": true, "wall": true, "walnut": true, + "want": true, "warfare": true, "warm": true, "warrior": true, "wash": true, + "wasp": true, "waste": true, "water": true, "wave": true, "way": true, + "wealth": true, "weapon": true, "wear": true, "weasel": true, "weather": true, + "web": true, "wedding": true, "weekend": true, "weird": true, "welcome": true, + "west": true, "wet": true, "whale": true, "what": true, "wheat": true, + "wheel": true, "when": true, "where": true, "whip": true, "whisper": true, + "wide": true, "width": true, "wife": true, "wild": true, "will": true, + "win": true, "window": true, "wine": true, "wing": true, "wink": true, + "winner": true, "winter": true, "wire": true, "wisdom": true, "wise": true, + "wish": true, "witness": true, "wolf": true, "woman": true, "wonder": true, + "wood": true, "wool": true, "word": true, "work": true, "world": true, + "worry": true, "worth": true, "wrap": true, "wreck": true, "wrestle": true, + "wrist": true, "write": true, "wrong": true, "yard": true, "year": true, + "yellow": true, "you": true, "young": true, "youth": true, "zebra": true, + "zero": true, "zone": true, "zoo": true, +} diff --git a/internal/rules/dlp_crypto_test.go b/internal/rules/dlp_crypto_test.go new file mode 100644 index 0000000..ceb389c --- /dev/null +++ b/internal/rules/dlp_crypto_test.go @@ -0,0 +1,283 @@ +package rules + +import ( + "path/filepath" + "runtime" + "strings" + "testing" +) + +// BIP32 test vector 1 — well-known master extended private key. +const testXprv = "xprv9s21ZrQH143K3QTDL4LXw2F7HEK3wJUD2nW2nRk4stbPy6cq3jPPqjiChkVvvNKmPGJxWUtg6LnF5kejMRNNU3TGtRBeJgk33yuGBxrMPHi" + +// Known WIF private key from Bitcoin wiki (uncompressed, mainnet). +const testWIF = "5HueCGU8rMjxEXxiPuD5BDku4MkFqeZyd4dZ1jvhTVqvbTLvyTJ" + +// Standard BIP39 12-word test mnemonic. +const testMnemonic12 = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about" + +// Standard BIP39 15-word test mnemonic. +const testMnemonic15 = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about" + +// Standard BIP39 18-word test mnemonic. +const testMnemonic18 = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about" + +// Standard BIP39 24-word test mnemonic. +const testMnemonic24 = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon art" + +func TestCryptoDLPDetection(t *testing.T) { + tests := []struct { + name string + content string + wantNil bool + wantID string + }{ + // --- BIP39 Mnemonic Detection --- + { + name: "12-word BIP39 mnemonic", + content: testMnemonic12, + wantID: "builtin:dlp-crypto-bip39-mnemonic", + }, + { + name: "24-word BIP39 mnemonic", + content: testMnemonic24, + wantID: "builtin:dlp-crypto-bip39-mnemonic", + }, + { + name: "15-word BIP39 mnemonic", + content: testMnemonic15, + wantID: "builtin:dlp-crypto-bip39-mnemonic", + }, + { + name: "18-word BIP39 mnemonic", + content: testMnemonic18, + wantID: "builtin:dlp-crypto-bip39-mnemonic", + }, + { + name: "mnemonic embedded in code", + content: `const seed = "` + testMnemonic12 + `"`, + wantID: "builtin:dlp-crypto-bip39-mnemonic", + }, + { + name: "mnemonic with surrounding text", + content: "Here is my wallet backup: " + testMnemonic12 + " — keep this safe!", + wantID: "builtin:dlp-crypto-bip39-mnemonic", + }, + { + name: "mnemonic with newlines between words", + content: strings.ReplaceAll(testMnemonic12, " ", "\n"), + wantID: "builtin:dlp-crypto-bip39-mnemonic", + }, + + // --- Extended Private Key Detection --- + { + name: "xprv key (BIP32 test vector 1)", + content: testXprv, + wantID: "builtin:dlp-crypto-xprv", + }, + { + name: "xprv embedded in JSON", + content: `{"master_key": "` + testXprv + `"}`, + wantID: "builtin:dlp-crypto-xprv", + }, + + // --- WIF Key Detection --- + { + name: "WIF key (uncompressed)", + content: testWIF, + wantID: "builtin:dlp-crypto-wif", + }, + { + name: "WIF key embedded in config", + content: "PRIVATE_KEY=" + testWIF, + wantID: "builtin:dlp-crypto-wif", + }, + + // --- No Detection (clean content) --- + { + name: "empty content", + content: "", + wantNil: true, + }, + { + name: "normal English text", + content: "The quick brown fox jumps over the lazy dog.", + wantNil: true, + }, + { + name: "Go source code", + content: `func main() { fmt.Println("hello world") }`, + wantNil: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := scanCrypto(tt.content) + if tt.wantNil { + if result != nil { + t.Errorf("scanCrypto() = %q, want nil", result.name) + } + return + } + if result == nil { + t.Fatal("scanCrypto() = nil, want match") + } + if result.name != tt.wantID { + t.Errorf("scanCrypto().name = %q, want %q", result.name, tt.wantID) + } + if result.message == "" { + t.Error("scanCrypto().message is empty") + } + }) + } +} + +func TestCryptoDLPFalsePositives(t *testing.T) { + tests := []struct { + name string + content string + }{ + { + name: "11 BIP39 words (below minimum)", + content: "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon", + }, + { + name: "12 common English words (not all BIP39)", + content: "the quick brown fox jumps over the lazy dog near the house", + }, + { + name: "xprv-like string with bad checksum", + content: "xprvAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", + }, + { + name: "random K-prefixed string (not valid WIF)", + content: "KzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzZ", + }, + { + name: "base58 alphabet soup", + content: "5" + strings.Repeat("1", 50), + }, + { + name: "xprv in variable name only", + content: `var xprvKeyPath = "/some/path"`, + }, + { + name: "Bitcoin address (not a private key)", + content: "1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa", + }, + { + name: "words from BIP39 mixed with non-BIP39", + content: "abandon ability xylophone microphone telephone abandon ability xylophone microphone telephone abandon ability", + }, + { + name: "short WIF-like prefix in URL", + content: "https://example.com/5K/path/to/resource", + }, + { + name: "code with abandon in comments", + content: "// We should abandon this approach and start over with a new design pattern", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := scanCrypto(tt.content) + if result != nil { + t.Errorf("scanCrypto() = %q, want nil (false positive)", result.name) + } + }) + } +} + +func TestCryptoWalletPaths(t *testing.T) { + // Verify that cryptoWalletDirs was populated at init. + if len(cryptoWalletDirs) == 0 { + t.Fatal("cryptoWalletDirs is empty — init() failed") + } + + // Should contain at least bitcoin and ethereum. + hasBitcoin := false + hasEthereum := false + for _, dir := range cryptoWalletDirs { + lower := strings.ToLower(dir) + if strings.Contains(lower, "bitcoin") { + hasBitcoin = true + } + if strings.Contains(lower, "ethereum") { + hasEthereum = true + } + } + if !hasBitcoin { + t.Error("cryptoWalletDirs missing bitcoin directory") + } + if !hasEthereum { + t.Error("cryptoWalletDirs missing ethereum directory") + } + + // Test hasCryptoWalletPath. + t.Run("direct access", func(t *testing.T) { + // Use the first wallet dir as test target. + testDir := cryptoWalletDirs[0] + testPath := filepath.Join(testDir, "wallet.dat") + + blocked, path := hasCryptoWalletPath([]string{testPath}) + if !blocked { + t.Errorf("hasCryptoWalletPath(%q) = false, want true", testPath) + } + if path != testPath { + t.Errorf("matched path = %q, want %q", path, testPath) + } + }) + + t.Run("unrelated path", func(t *testing.T) { + blocked, _ := hasCryptoWalletPath([]string{"/tmp/safe/file.txt"}) + if blocked { + t.Error("hasCryptoWalletPath(/tmp/safe/file.txt) = true, want false") + } + }) + + t.Run("directory itself", func(t *testing.T) { + testDir := cryptoWalletDirs[0] + blocked, _ := hasCryptoWalletPath([]string{testDir}) + if !blocked { + t.Errorf("hasCryptoWalletPath(%q) = false, want true (directory itself)", testDir) + } + }) +} + +func TestCryptoWalletPathsOSSpecific(t *testing.T) { + // Verify paths follow OS conventions via btcutil.AppDataDir. + switch runtime.GOOS { + case "darwin": + // macOS: ~/Library/Application Support/Bitcoin + found := false + for _, dir := range cryptoWalletDirs { + if strings.Contains(dir, "Library/Application Support") && strings.Contains(dir, "itcoin") { + found = true + break + } + } + if !found { + t.Errorf("macOS: expected Library/Application Support path for Bitcoin, got: %v", cryptoWalletDirs) + } + case "linux": + // Linux: ~/.bitcoin + found := false + for _, dir := range cryptoWalletDirs { + if strings.Contains(dir, ".bitcoin") { + found = true + break + } + } + if !found { + t.Errorf("Linux: expected .bitcoin path, got: %v", cryptoWalletDirs) + } + } +} + +func TestBIP39WordlistCount(t *testing.T) { + if len(bip39Wordlist) != 2048 { + t.Errorf("bip39Wordlist has %d words, want 2048", len(bip39Wordlist)) + } +} diff --git a/internal/rules/dlp_scanner.go b/internal/rules/dlp_scanner.go index 80e1821..0612e58 100644 --- a/internal/rules/dlp_scanner.go +++ b/internal/rules/dlp_scanner.go @@ -10,8 +10,8 @@ import ( "time" ) -// DLPScanner provides optional Tier 2 secret detection via an external -// gitleaks binary. Disabled gracefully when gitleaks is not installed. +// DLPScanner provides Tier 2 secret detection via an external gitleaks binary. +// Recommended dependency — logs a warning with install instructions if missing. type DLPScanner struct { binaryPath string available bool @@ -37,7 +37,8 @@ func NewDLPScanner() *DLPScanner { path, err := exec.LookPath("gitleaks") if err != nil { - log.Info("DLP Tier 2 disabled: gitleaks not found in PATH") + log.Warn("DLP Tier 2 disabled: gitleaks not found — install for full secret detection (200+ patterns)") + log.Warn(" brew install gitleaks OR go install github.com/gitleaks/gitleaks/v8@latest") return s } diff --git a/internal/rules/dlp_test.go b/internal/rules/dlp_test.go index 5b92067..3a71610 100644 --- a/internal/rules/dlp_test.go +++ b/internal/rules/dlp_test.go @@ -54,6 +54,62 @@ func TestDLPFalsePositives(t *testing.T) { // Short dapi-like string (not 32 hex chars) {"dapi-like but too short", "Write", `{"file_path":"/tmp/test.py","content":"dapibus = 'lorem ipsum'"}`}, + // PEM: certificates and public keys are NOT private keys + {"PEM certificate (not private key)", "Write", + `{"file_path":"/tmp/test.pem","content":"-----BEGIN CERTIFICATE-----\nMIIBkTCB...\n-----END CERTIFICATE-----"}`}, + {"PEM public key (not private key)", "Write", + `{"file_path":"/tmp/test.pem","content":"-----BEGIN PUBLIC KEY-----\nMIIBIjAN...\n-----END PUBLIC KEY-----"}`}, + // Short HuggingFace-like token + {"hf_ too short", "Write", + `{"file_path":"/tmp/test.py","content":"hf_short"}`}, + // Short Groq-like key + {"gsk_ too short", "Write", + `{"file_path":"/tmp/test.py","content":"gsk_short"}`}, + // Short Twilio-like key (SK but not 32 hex) + {"SK too short for Twilio", "Write", + `{"file_path":"/tmp/test.py","content":"SKU = 'product-123'"}`}, + // Short r8_ token + {"r8_ too short", "Write", + `{"file_path":"/tmp/test.py","content":"r8_short"}`}, + // vercel_ in normal code (version string, not a token) + {"vercel_ in version context", "Write", + `{"file_path":"/tmp/test.js","content":"const vercel_sdk = '1.0'"}`}, + // "BEGIN CERTIFICATE" is not a private key + {"PEM certificate request", "Write", + `{"file_path":"/tmp/test.pem","content":"-----BEGIN CERTIFICATE REQUEST-----\nMIIBkTCB...\n-----END CERTIFICATE REQUEST-----"}`}, + // hvs. but too short + {"hvs. too short", "Write", + `{"file_path":"/tmp/test.env","content":"hvs.short"}`}, + // dp.st. but too short + {"dp.st. too short", "Write", + `{"file_path":"/tmp/test.env","content":"dp.st.short"}`}, + // lin_api_ but too short + {"lin_api_ too short", "Write", + `{"file_path":"/tmp/test.env","content":"lin_api_short"}`}, + // PMAK- but too short + {"PMAK- too short", "Write", + `{"file_path":"/tmp/test.env","content":"PMAK-short"}`}, + // sbp_ but too short + {"sbp_ too short", "Write", + `{"file_path":"/tmp/test.env","content":"sbp_short"}`}, + // Normal Go code with SK in variable name + {"SK in Go variable name", "Write", + `{"file_path":"/tmp/test.go","content":"func SKip() {}"}`}, + // Firebase-like but too short + {"AAAA prefix but too short", "Write", + `{"file_path":"/tmp/test.py","content":"AAAA_PADDING = True"}`}, + // Normal code mentioning private key in comments + {"private key in comment text", "Write", + `{"file_path":"/tmp/test.py","content":"# Generate a private key using openssl"}`}, + // dop_v1_ but too short + {"dop_v1_ too short", "Write", + `{"file_path":"/tmp/test.env","content":"dop_v1_short"}`}, + // Normal Python f-string with hf_ substring + {"hf_ in variable name", "Write", + `{"file_path":"/tmp/test.py","content":"half_width = 100"}`}, + // gsk_ as Go package prefix + {"gsk_ in package name", "Write", + `{"file_path":"/tmp/test.go","content":"import gsk_utils"}`}, } for _, tc := range cases { @@ -267,6 +323,87 @@ func TestDLPSecretDetection(t *testing.T) { "Write", `{"file_path":"/home/user/project/key.txt","content":"` + "AGE-SECRET-KEY-" + "1QQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQ" + `"}`, }, + // --- Expanded patterns --- + { + "DLP: RSA private key in any operation", + "Bash", + `{"command":"cat /tmp/key.pem"}` + "\n-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBA...\n-----END RSA PRIVATE KEY-----", + }, + { + "DLP: OpenSSH private key", + "Write", + `{"file_path":"/home/user/project/deploy_key","content":"-----BEGIN OPENSSH PRIVATE KEY-----\nb3BlbnNzaC1rZXktdjEAAAA...\n-----END OPENSSH PRIVATE KEY-----"}`, + }, + { + "DLP: writing HuggingFace token", + "Write", + `{"file_path":"/home/user/project/.env","content":"HF_TOKEN=` + "hf_" + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefgh" + `"}`, + }, + { + "DLP: writing Groq API key", + "Write", + `{"file_path":"/home/user/project/.env","content":"GROQ_KEY=` + "gsk_" + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwx" + `"}`, + }, + { + "DLP: writing Vercel token", + "Write", + `{"file_path":"/home/user/project/.env","content":"VERCEL_TOKEN=` + "vercel_" + "ABCDEFGHIJKLMNOPQRSTUVWXYZab" + `"}`, + }, + { + "DLP: writing Supabase key", + "Write", + `{"file_path":"/home/user/project/.env","content":"SUPABASE_KEY=` + "sbp_" + "aabbccddeeff00112233445566778899aabbccddeeff" + `"}`, + }, + { + "DLP: writing DigitalOcean PAT", + "Write", + `{"file_path":"/home/user/project/.env","content":"DO_TOKEN=` + "dop_v1_" + "aabbccddeeff00112233445566778899aabbccddeeff00112233445566778899aabb" + `"}`, + }, + { + "DLP: writing DigitalOcean OAuth token", + "Write", + `{"file_path":"/home/user/project/.env","content":"DO_OAUTH=` + "doo_v1_" + "aabbccddeeff00112233445566778899aabbccddeeff00112233445566778899aabb" + `"}`, + }, + { + "DLP: writing HashiCorp Vault token", + "Write", + `{"file_path":"/home/user/project/.env","content":"VAULT_TOKEN=` + "hvs." + "ABCDEFghijklmnopqrstuvwx0123" + `"}`, + }, + { + "DLP: writing Linear API key", + "Write", + `{"file_path":"/home/user/project/.env","content":"LINEAR_KEY=` + "lin_api_" + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnop" + `"}`, + }, + { + "DLP: writing Postman API key", + "Write", + `{"file_path":"/home/user/project/.env","content":"POSTMAN_KEY=` + "PMAK-" + "ABCDEFGHIJKLMNOPQRSTUVWXYZab" + `"}`, + }, + { + "DLP: writing Replicate API token", + "Write", + `{"file_path":"/home/user/project/.env","content":"REPLICATE_TOKEN=` + "r8_" + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnop" + `"}`, + }, + { + "DLP: writing Twilio API key", + "Write", + `{"file_path":"/home/user/project/.env","content":"TWILIO_KEY=` + "SK" + "aabbccddeeff00112233445566778899" + `"}`, + }, + { + "DLP: writing Doppler token", + "Write", + `{"file_path":"/home/user/project/.env","content":"DOPPLER_TOKEN=` + "dp.st." + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrst" + `"}`, + }, + { + "DLP: writing OpenAI admin key", + "Write", + `{"file_path":"/home/user/project/.env","content":"OPENAI_ADMIN=` + "sk-admin-" + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrst" + `"}`, + }, + { + "DLP: writing Firebase key", + "Write", + `{"file_path":"/home/user/project/.env","content":"FIREBASE_KEY=` + "AAAA" + "ABCDEFG" + ":" + strings.Repeat("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnop", 4) + `"}`, + }, } for _, tc := range cases { diff --git a/internal/rules/engine.go b/internal/rules/engine.go index 8009d12..6272573 100644 --- a/internal/rules/engine.go +++ b/internal/rules/engine.go @@ -430,6 +430,17 @@ func (e *Engine) Evaluate(call ToolCall) MatchResult { } } + // Tier 3: Crypto-specific DLP (checksum-validated). + if m := scanCrypto(dlpContent); m != nil { + return MatchResult{ + Matched: true, + RuleName: m.name, + Severity: SeverityCritical, + Action: ActionBlock, + Message: m.message, + } + } + if findings := e.dlpScanner.Scan(dlpContent); len(findings) > 0 { f := findings[0] msg := "Blocked secret — " + f.Description @@ -460,8 +471,12 @@ func (e *Engine) Evaluate(call ToolCall) MatchResult { // Step 12: Expand globs against real filesystem (e.g. ~/.e* → ~/.env). normalizedPaths = expandFileGlobs(normalizedPaths) - // Step 13: Block /proc access (hardcoded; checked before symlink resolution). - if blocked, path := hasProcPath(normalizedPaths); blocked { + // Step 13: Resolve symlinks — match both original and resolved paths. + resolvedPaths := e.normalizer.resolveSymlinks(normalizedPaths) + allPaths := mergeUnique(normalizedPaths, resolvedPaths) + + // Step 14: Block /proc access (hardcoded; after symlink resolution to catch symlink bypasses). + if blocked, path := hasProcPath(allPaths); blocked { return MatchResult{ Matched: true, RuleName: "builtin:protect-proc", @@ -471,18 +486,25 @@ func (e *Engine) Evaluate(call ToolCall) MatchResult { } } - // Step 14: Resolve symlinks — match both original and resolved paths. - resolvedPaths := e.normalizer.resolveSymlinks(normalizedPaths) - allPaths := mergeUnique(normalizedPaths, resolvedPaths) + // Step 15: Block crypto wallet access (hardcoded; after symlink resolution to catch symlink bypasses). + if blocked, path := hasCryptoWalletPath(allPaths); blocked { + return MatchResult{ + Matched: true, + RuleName: "builtin:protect-crypto-wallet", + Severity: SeverityCritical, + Action: ActionBlock, + Message: fmt.Sprintf("Cannot access %s — crypto wallet directory", path), + } + } - // Step 15: Evaluate operation-based rules (for known tools). + // Step 16: Evaluate operation-based rules (for known tools). if info.Operation != OpNone { if result := e.evaluateOperationRules(rules, info, allPaths, call.Name); result.Matched { return result } } - // Step 16: Fallback content-only rules — matches raw JSON of any tool. + // Step 17: Fallback content-only rules — matches raw JSON of any tool. contentForRules := info.RawJSON if contentForRules == "" { contentForRules = info.Content diff --git a/internal/rules/fuzz_test.go b/internal/rules/fuzz_test.go index 31ba0b2..d37bdc3 100644 --- a/internal/rules/fuzz_test.go +++ b/internal/rules/fuzz_test.go @@ -345,7 +345,7 @@ func FuzzBuiltinRuleBypass(f *testing.F) { f.Add("Write", `{"file_path":"/home/user/.zshrc","content":"backdoor"}`) // protect-ssh-authorized-keys f.Add("Write", `{"file_path":"/home/user/.ssh/authorized_keys","content":"ssh-rsa AAAA..."}`) - // detect-private-key-write (constructed to avoid gitleaks false positive) + // detect-private-key-write / builtin:dlp-private-key pkHeader := "-----BEGIN " + "RSA PRIVATE KEY-----" f.Add("Write", `{"file_path":"/tmp/key","content":"`+pkHeader+`"}`) // builtin:protect-crust-api (hardcoded, all loopback forms)