From 9644f6c3a3dc65154b4edfcb9842029b9a9e8584 Mon Sep 17 00:00:00 2001 From: cyy Date: Sat, 28 Feb 2026 10:02:23 +0800 Subject: [PATCH 1/5] feat: expand DLP to 34 patterns, add 15 new token detectors Add private key PEM (all operations), HuggingFace, Groq, Vercel, Supabase, DigitalOcean, HashiCorp Vault, Linear, Postman, Replicate, Twilio, Doppler, OpenAI admin, Firebase patterns. Promote gitleaks from optional to recommended with install instructions on missing. --- README.md | 4 +- docs/how-it-works.md | 23 ++++-- internal/rules/dlp.go | 110 ++++++++++++++++++++++++++- internal/rules/dlp_scanner.go | 7 +- internal/rules/dlp_test.go | 137 ++++++++++++++++++++++++++++++++++ internal/rules/fuzz_test.go | 2 +- 6 files changed, 270 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index d45d261..18511a3 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,7 +138,7 @@ 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 | | **Self-Protection** | Agents cannot read, modify, or disable Crust itself | | **Dangerous Commands** | `eval`/`exec` with dynamic code execution | diff --git a/docs/how-it-works.md b/docs/how-it-works.md index d851f0f..53a72f8 100644 --- a/docs/how-it-works.md +++ b/docs/how-it-works.md @@ -152,17 +152,30 @@ 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-...`) | - -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. - -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. +| 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...:...`) | + +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. + +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`. --- 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_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/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) From bca7985a72ee9bc447d440656711de57eabbfae9 Mon Sep 17 00:00:00 2001 From: cyy Date: Sat, 28 Feb 2026 10:48:30 +0800 Subject: [PATCH 2/5] feat: add crypto wallet DLP protection with checksum validation - BIP39 mnemonic detection (embedded 2048-word wordlist, sliding window) - Extended private key (xprv/yprv/zprv/tprv) with base58check validation - WIF private key with base58check + version byte validation - Hardcoded wallet path protection for 15+ chains via btcutil.AppDataDir() - Fix /proc symlink bypass: moved hardcoded checks after symlink resolution - Pipeline reordered: symlink resolution now step 13, hardcoded checks 14-15 --- go.mod | 5 + go.sum | 98 +++++ internal/rules/dlp_crypto.go | 610 ++++++++++++++++++++++++++++++ internal/rules/dlp_crypto_test.go | 266 +++++++++++++ internal/rules/engine.go | 36 +- 5 files changed, 1008 insertions(+), 7 deletions(-) create mode 100644 internal/rules/dlp_crypto.go create mode 100644 internal/rules/dlp_crypto_test.go diff --git a/go.mod b/go.mod index 502b59c..b48416c 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 @@ -27,6 +28,9 @@ require ( require ( github.com/atotto/clipboard v0.1.4 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect + github.com/btcsuite/btcd v0.24.2 // indirect + github.com/btcsuite/btcd/btcec/v2 v2.1.3 // indirect + github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0 // indirect github.com/bytedance/sonic v1.14.0 // indirect github.com/bytedance/sonic/loader v0.3.0 // indirect github.com/catppuccin/go v0.3.0 // indirect @@ -40,6 +44,7 @@ require ( github.com/clipperhouse/stringish v0.1.1 // indirect github.com/clipperhouse/uax29/v2 v2.5.0 // indirect github.com/cloudwego/base64x v0.1.6 // indirect + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect github.com/gabriel-vasile/mimetype v1.4.12 // indirect diff --git a/go.sum b/go.sum index 292435b..471a2af 100644 --- a/go.sum +++ b/go.sum @@ -2,12 +2,39 @@ 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 h1:aLmxPguqxza+4ag8R1I2nnJjSu2iFn/kqtHTIImswcY= +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 h1:xM/n3yIhHAhHy04z4i43C8p4ehixJZMsnrVJkgl+MTE= +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 h1:59Kx4K6lzOW5w6nFlA0v5+lk/6sjybR934QNHSJZPTQ= +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 +81,20 @@ 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 h1:YLtO71vCjJRCBcrPMtQ9nqBsqpA1m5sE92cU+pd5Mcc= +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 +119,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 +177,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 +208,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 +226,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_crypto.go b/internal/rules/dlp_crypto.go new file mode 100644 index 0000000..8d937c3 --- /dev/null +++ b/internal/rules/dlp_crypto.go @@ -0,0 +1,610 @@ +package rules + +import ( + "os" + "path/filepath" + "regexp" + "strings" + "unicode" + + "github.com/btcsuite/btcd/btcutil" + "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 via btcutil.AppDataDir() + +// 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) >= 2 && len(w) <= 8 { // BIP39 words are 3-8 chars, allow 2 for safety + 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 via btcutil.AppDataDir(). +// Checked after symlink resolution (step 14b) so symlink bypasses are caught. +var cryptoWalletDirs []string + +func init() { + home, err := os.UserHomeDir() + if err != nil { + home = "" + } + + // All major chains that follow the AppDataDir 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 + } { + cryptoWalletDirs = append(cryptoWalletDirs, btcutil.AppDataDir(chain, false)) + } + + // 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 { + for _, dir := range cryptoWalletDirs { + if strings.HasPrefix(p, dir+string(filepath.Separator)) || p == 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..69c869e --- /dev/null +++ b/internal/rules/dlp_crypto_test.go @@ -0,0 +1,266 @@ +package rules + +import ( + "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 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: "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 := 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/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 From 1e57373da1a77f8bbc23c0d239b8c1ad04eb277e Mon Sep 17 00:00:00 2001 From: cyy Date: Sat, 28 Feb 2026 10:59:53 +0800 Subject: [PATCH 3/5] docs: add crypto wallet protection, update pipeline to 17 steps - Pipeline reordered: symlink resolution before hardcoded checks - Add crypto DLP section (BIP39, xprv, WIF with checksum validation) - Add crypto wallet path protection for 16 chains - Add 4 new rows to attack coverage table - Add Crypto Wallets to protection categories --- README.md | 5 +++-- docs/how-it-works.md | 40 ++++++++++++++++++++++++++++++++-------- 2 files changed, 35 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 18511a3..6a67cfb 100644 --- a/README.md +++ b/README.md @@ -140,10 +140,11 @@ Crust ships with **14 security rules** and **34 DLP token-detection patterns** o | **Persistence** | Shell RC files, `authorized_keys` | | **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 53a72f8..39e7ced 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. @@ -177,6 +182,24 @@ Tier 1 patterns (34 hardcoded) are sourced from [gitleaks v8.24](https://github. 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 via `btcutil.AppDataDir()` for OS-correct locations (e.g., `~/Library/Application Support/Bitcoin/` on macOS, `~/.bitcoin/` on Linux). 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. + --- ## Built-in Rule Principles @@ -197,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. From 88e2da98174433782838df0ab9bea4521678f2aa Mon Sep 17 00:00:00 2001 From: cyy Date: Sat, 28 Feb 2026 15:19:21 +0800 Subject: [PATCH 4/5] fix: tighten BIP39 word filter, add defensive checks and test coverage MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Word length filter 2-8 → 3-8 (matches actual BIP39 range) - Skip empty AppDataDir() results defensively - Add 15-word and 18-word BIP39 mnemonic test cases --- internal/rules/dlp_crypto.go | 6 ++++-- internal/rules/dlp_crypto_test.go | 16 ++++++++++++++++ 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/internal/rules/dlp_crypto.go b/internal/rules/dlp_crypto.go index 8d937c3..b46f554 100644 --- a/internal/rules/dlp_crypto.go +++ b/internal/rules/dlp_crypto.go @@ -88,7 +88,7 @@ func extractLowerWords(s string) []string { }) for _, f := range fields { w := strings.ToLower(f) - if len(w) >= 2 && len(w) <= 8 { // BIP39 words are 3-8 chars, allow 2 for safety + if len(w) >= 3 && len(w) <= 8 { // BIP39 words are 3-8 chars words = append(words, w) } } @@ -164,7 +164,9 @@ func init() { "cardano", "cosmos", "polkadot", // PoS chains "avalanche", "tron", // Other popular } { - cryptoWalletDirs = append(cryptoWalletDirs, btcutil.AppDataDir(chain, false)) + if dir := btcutil.AppDataDir(chain, false); dir != "" { + cryptoWalletDirs = append(cryptoWalletDirs, dir) + } } // Solana (non-standard locations). diff --git a/internal/rules/dlp_crypto_test.go b/internal/rules/dlp_crypto_test.go index 69c869e..7072a4e 100644 --- a/internal/rules/dlp_crypto_test.go +++ b/internal/rules/dlp_crypto_test.go @@ -15,6 +15,12 @@ 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" @@ -36,6 +42,16 @@ func TestCryptoDLPDetection(t *testing.T) { 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 + `"`, From afdb6ded3db2ebccc5abd57c1eac604ec660ffad Mon Sep 17 00:00:00 2001 From: cyy Date: Sat, 28 Feb 2026 15:48:15 +0800 Subject: [PATCH 5/5] refactor: replace btcutil.AppDataDir with Go stdlib for wallet path discovery Use runtime.GOOS + os.UserHomeDir() instead of btcutil.AppDataDir() for computing crypto wallet directories. Removes direct btcutil import (base58 sub-package retained for key validation). Also fixes Windows path separator normalization with filepath.Clean. --- docs/how-it-works.md | 2 +- go.mod | 4 --- go.sum | 4 --- internal/rules/dlp_crypto.go | 45 ++++++++++++++++++++++++++----- internal/rules/dlp_crypto_test.go | 3 ++- 5 files changed, 41 insertions(+), 17 deletions(-) diff --git a/docs/how-it-works.md b/docs/how-it-works.md index 39e7ced..a1af957 100644 --- a/docs/how-it-works.md +++ b/docs/how-it-works.md @@ -196,7 +196,7 @@ BIP39 mnemonics are the universal seed phrase standard used by Bitcoin, Ethereum ### Crypto Wallet Path Protection -Step 15 blocks access to cryptocurrency wallet directories. Paths are computed at init via `btcutil.AppDataDir()` for OS-correct locations (e.g., `~/Library/Application Support/Bitcoin/` on macOS, `~/.bitcoin/` on Linux). This check runs **after symlink resolution** (step 13) so symlink bypasses are caught. +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. diff --git a/go.mod b/go.mod index b48416c..a5b0656 100644 --- a/go.mod +++ b/go.mod @@ -28,9 +28,6 @@ require ( require ( github.com/atotto/clipboard v0.1.4 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect - github.com/btcsuite/btcd v0.24.2 // indirect - github.com/btcsuite/btcd/btcec/v2 v2.1.3 // indirect - github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0 // indirect github.com/bytedance/sonic v1.14.0 // indirect github.com/bytedance/sonic/loader v0.3.0 // indirect github.com/catppuccin/go v0.3.0 // indirect @@ -44,7 +41,6 @@ require ( github.com/clipperhouse/stringish v0.1.1 // indirect github.com/clipperhouse/uax29/v2 v2.5.0 // indirect github.com/cloudwego/base64x v0.1.6 // indirect - github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect github.com/gabriel-vasile/mimetype v1.4.12 // indirect diff --git a/go.sum b/go.sum index 471a2af..91aab27 100644 --- a/go.sum +++ b/go.sum @@ -12,10 +12,8 @@ github.com/aymanbagabas/go-udiff v0.3.1/go.mod h1:G0fsKmG+P6ylD0r6N/KgQD/nWzgfnl 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 h1:aLmxPguqxza+4ag8R1I2nnJjSu2iFn/kqtHTIImswcY= 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 h1:xM/n3yIhHAhHy04z4i43C8p4ehixJZMsnrVJkgl+MTE= 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= @@ -24,7 +22,6 @@ github.com/btcsuite/btcd/btcutil v1.1.6 h1:zFL2+c3Lb9gEgqKNzowKUPQNb8jV7v5Oaodi/ 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 h1:59Kx4K6lzOW5w6nFlA0v5+lk/6sjybR934QNHSJZPTQ= 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= @@ -86,7 +83,6 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs 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 h1:YLtO71vCjJRCBcrPMtQ9nqBsqpA1m5sE92cU+pd5Mcc= 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= diff --git a/internal/rules/dlp_crypto.go b/internal/rules/dlp_crypto.go index b46f554..4eede79 100644 --- a/internal/rules/dlp_crypto.go +++ b/internal/rules/dlp_crypto.go @@ -4,10 +4,10 @@ import ( "os" "path/filepath" "regexp" + "runtime" "strings" "unicode" - "github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/btcutil/base58" ) @@ -15,7 +15,7 @@ import ( // - 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 via btcutil.AppDataDir() +// - Wallet path protection: hardcoded directory check using OS-specific data dirs // cryptoDLPMatch holds a crypto DLP detection result. type cryptoDLPMatch struct { @@ -147,24 +147,54 @@ func scanWIFKey(content string) *cryptoDLPMatch { // --- Crypto Wallet Path Protection --- -// cryptoWalletDirs are computed once at init via btcutil.AppDataDir(). -// Checked after symlink resolution (step 14b) so symlink bypasses are caught. +// 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 AppDataDir convention. + // 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 := btcutil.AppDataDir(chain, false); dir != "" { + if dir := cryptoDataDir(home, chain); dir != "" { cryptoWalletDirs = append(cryptoWalletDirs, dir) } } @@ -186,8 +216,9 @@ func init() { // 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(p, dir+string(filepath.Separator)) || p == dir { + if strings.HasPrefix(cleaned, dir+string(filepath.Separator)) || cleaned == dir { return true, p } } diff --git a/internal/rules/dlp_crypto_test.go b/internal/rules/dlp_crypto_test.go index 7072a4e..ceb389c 100644 --- a/internal/rules/dlp_crypto_test.go +++ b/internal/rules/dlp_crypto_test.go @@ -1,6 +1,7 @@ package rules import ( + "path/filepath" "runtime" "strings" "testing" @@ -218,7 +219,7 @@ func TestCryptoWalletPaths(t *testing.T) { t.Run("direct access", func(t *testing.T) { // Use the first wallet dir as test target. testDir := cryptoWalletDirs[0] - testPath := testDir + "/wallet.dat" + testPath := filepath.Join(testDir, "wallet.dat") blocked, path := hasCryptoWalletPath([]string{testPath}) if !blocked {