/* ·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━· : : : █▀ █ █▀▀ · Blazing-fast pentesting suite : : ▄█ █ █▀ · BSD 3-Clause License : : : : (c) 2022-2026 vmfunc, xyzeva, : : lunchcat alumni & contributors : : : ·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━· */ package js import ( "fmt" "strings" "testing" ) // the fake tokens below are assembled from two fragments on purpose: a contiguous // provider token literal in a committed file trips github push-protection (and // every other secret scanner) even though it's a test fixture. splitting it // keeps the literal out of source while ScanSecrets still sees the joined value. const ( fakeAWSKey = "AKIA" + "IOSFODNN7EXAMPLE" fakeAWSSecret = "wJalrXUtnFEMI/K7MDENG/" + "bPxRfiCYEXAMPLEKEY" fakeGitHub = "ghp_" + "aB3dEfGh1jKlMn0pQrStUvWxYz012345abcd" fakeSlack = "xoxb-" + "123456789012-abcdefABCDEF1234567890ab" fakeStripe = "sk_live_" + "0000000000000000000000" fakeGoogle = "AIza" + "SyA1B2C3D4E5F6G7H8I9J0K1L2M3N4O5P6Q" fakeGeneric = "x9Kq2Lm7Pz4Rt6Wv8Bn3Cd5Fg1Hj0As" fakePEM = "-----BEGIN RSA PRIVATE " + "KEY-----\nMIIEpAIB..." ) // fakes for the rebuilt rules; the derived ones end in a non-word char to // exercise the dropped trailing word boundaries. var ( fakeStripeRestricted = "rk_live_" + "0000000000000000000000" fakeGitHubPAT = "github_pat_" + strings.Repeat("a1B2c3D4", 10) + "ab" fakeSlackApp = "xapp-1-" + "A01B23C45D6-1234567890-abcdefABCDEF" fakeEncryptedPEM = "-----BEGIN ENCRYPTED PRIVATE " + "KEY-----\nMIIFDjBA..." fakeAWSSecretSlash = fakeAWSSecret[:len(fakeAWSSecret)-1] + "/" fakeGoogleDash = fakeGoogle[:len(fakeGoogle)-1] + "-" fakeGitLabPAT = "glpat-" + "AbCdEf1234567890GhIj" fakeAnthropic = "sk-ant-api03-" + strings.Repeat("aB3", 31) + "AA" fakeNPM = "npm_" + strings.Repeat("a1B2c3", 6) fakeGoogleOAuth = "GOCSPX-" + strings.Repeat("aB3d", 7) fakeStripeWebhook = "whsec_" + strings.Repeat("aB3d", 8) fakeShopify = "shpat_" + strings.Repeat("0a1b", 8) fakeSendGrid = "SG." + strings.Repeat("aB3", 7) + "a." + strings.Repeat("aB3", 14) + "a" fakeSlackHook = "hooks.slack.com/services/T00000000/B00000000/" + strings.Repeat("aB3", 8) fakeAnthropicBad = "sk-ant-api03-" + strings.Repeat("aB3", 31) + "XX" ) func TestScanSecrets(t *testing.T) { tests := []struct { name string content string wantRule string // rule expected on the first match, "" means no match wantNone bool }{ { name: "aws access key id", content: fmt.Sprintf(`const k = %q;`, fakeAWSKey), wantRule: "aws access key id", }, { name: "github personal token", content: fmt.Sprintf(`token: %q`, fakeGitHub), wantRule: "github token", }, { name: "slack bot token", content: fmt.Sprintf(`slack=%q`, fakeSlack), wantRule: "slack token", }, { name: "stripe live secret key", content: fmt.Sprintf(`var sk = %q;`, fakeStripe), wantRule: "stripe secret key", }, { name: "google api key", content: fmt.Sprintf(`apiKey: %q`, fakeGoogle), wantRule: "google api key", }, { name: "pem private key header", content: fakePEM, wantRule: "private key", }, { name: "generic high-entropy api key assignment", content: fmt.Sprintf(`apikey = %q`, fakeGeneric), wantRule: "generic secret assignment", }, { name: "aws secret with entropy", content: fmt.Sprintf(`aws_secret_access_key=%q`, fakeAWSSecret), wantRule: "aws secret access key", }, { // low-entropy assignment is a placeholder, not a real secret. name: "low entropy generic assignment not flagged", content: `password = "aaaaaaaaaaaaaaaaaaaaaaaa"`, wantNone: true, }, { // a repetitive placeholder is low-entropy and must not trip the gate. name: "low entropy repeated pattern not flagged", content: `token = "abababababababababababab"`, wantNone: true, }, { name: "stripe restricted live key", content: fmt.Sprintf(`var rk = %q;`, fakeStripeRestricted), wantRule: "stripe secret key", }, { name: "github fine-grained pat", content: fmt.Sprintf(`pat: %q`, fakeGitHubPAT), wantRule: "github fine-grained pat", }, { name: "slack app-level token", content: fmt.Sprintf(`slack=%q`, fakeSlackApp), wantRule: "slack token", }, { name: "encrypted pem private key header", content: fakeEncryptedPEM, wantRule: "private key", }, { // value ends in / so the old trailing \b dropped the match. name: "aws secret ending in slash", content: fmt.Sprintf(`aws_secret_access_key=%q`, fakeAWSSecretSlash), wantRule: "aws secret access key", }, { // same as above, dash-ending case. name: "google api key ending in dash", content: fmt.Sprintf(`apiKey: %q`, fakeGoogleDash), wantRule: "google api key", }, { // publishable pk_ keys are public by design, not a finding. name: "stripe publishable key not flagged", content: `pub = "pk_live_0000000000000000000000"`, wantNone: true, }, { name: "stripe test key not flagged", content: `k = "sk_test_0000000000000000000000"`, wantNone: true, }, { // the rk_live substring inside spark_live must not match (word boundary). name: "spark_live substring not flagged", content: `sparkCfg = "spark_live_aBcDeF1234567890XY"`, wantNone: true, }, { name: "digitless camelcase generic not flagged", content: `token = "getUserAccountSettings"`, wantNone: true, }, { name: "gitlab personal access token", content: fmt.Sprintf(`token: %q`, fakeGitLabPAT), wantRule: "gitlab personal access token", }, { name: "anthropic api key", content: fmt.Sprintf(`key = %q`, fakeAnthropic), wantRule: "anthropic api key", }, { name: "npm access token", content: fmt.Sprintf(`_authToken=%q`, fakeNPM), wantRule: "npm access token", }, { name: "google oauth client secret", content: fmt.Sprintf(`client_secret: %q`, fakeGoogleOAuth), wantRule: "google oauth client secret", }, { name: "stripe webhook secret", content: fmt.Sprintf(`endpointSecret = %q`, fakeStripeWebhook), wantRule: "stripe webhook secret", }, { name: "shopify access token", content: fmt.Sprintf(`shopify=%q`, fakeShopify), wantRule: "shopify access token", }, { name: "sendgrid api key", content: fmt.Sprintf(`SENDGRID_API_KEY=%q`, fakeSendGrid), wantRule: "sendgrid api key", }, { name: "slack webhook url", content: fmt.Sprintf(`url = "https://%s"`, fakeSlackHook), wantRule: "slack webhook url", }, { // classic glpat tokens are 20 chars; a short stub is not a finding. name: "gitlab token too short not flagged", content: `pub = "glpat-abc"`, wantNone: true, }, { // anthropic keys end in a fixed AA pad; a different tail is not a key. name: "anthropic key wrong pad not flagged", content: fmt.Sprintf(`k = %q`, fakeAnthropicBad), wantNone: true, }, { name: "no secrets in plain code", content: `function add(a, b) { return a + b; }`, wantNone: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := ScanSecrets(tt.content, "https://example.com/app.js") if tt.wantNone { if len(got) != 0 { t.Fatalf("expected no matches, got %+v", got) } return } if len(got) == 0 { t.Fatalf("expected a %q match, got none", tt.wantRule) } if got[0].Rule != tt.wantRule { t.Errorf("rule = %q, want %q", got[0].Rule, tt.wantRule) } if got[0].Match == "" { t.Error("match value is empty") } if got[0].Source != "https://example.com/app.js" { t.Errorf("source = %q, want the passed url", got[0].Source) } }) } } func TestScanSecretsDedupesWithinSource(t *testing.T) { // the same key referenced twice in one file is one finding. content := fmt.Sprintf(`a = %q; b = %q;`, fakeAWSKey, fakeAWSKey) got := ScanSecrets(content, "https://example.com/app.js") if len(got) != 1 { t.Fatalf("expected 1 deduped match, got %d: %+v", len(got), got) } } func TestShannonEntropy(t *testing.T) { tests := []struct { name string input string // random-ish strings clear the generic gate, repetitive ones don't. wantHigh bool }{ {name: "empty is zero", input: "", wantHigh: false}, {name: "repeated char is low", input: "aaaaaaaaaaaaaaaa", wantHigh: false}, {name: "random blob is high", input: fakeGeneric, wantHigh: true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := shannonEntropy(tt.input) if tt.wantHigh && got < genericMinEntropy { t.Errorf("entropy %f below generic gate %f", got, genericMinEntropy) } if !tt.wantHigh && got >= genericMinEntropy { t.Errorf("entropy %f unexpectedly cleared generic gate %f", got, genericMinEntropy) } }) } }