Cybersecurity-Projects/PROJECTS/intermediate/secrets-scanner/internal/rules/builtin_test.go

555 lines
16 KiB
Go

/*
©AngelaMos | 2026
builtin_test.go
Tests for rules/builtin.go
Tests:
RegisterBuiltins loads at least 70 rules
All rules have non-empty ID, description, keywords, and a non-nil pattern with no duplicates
Pattern match correctness for 50+ services (AWS, GitHub, GitLab, GCP, Azure, Stripe,
Twilio, Slack, JWT, SSH/PGP keys, DB connection strings, Shopify, npm, PyPI, Docker,
Vault, DigitalOcean, Grafana, Databricks, HuggingFace, Supabase, and more)
MatchKeywords routes content to the correct rule by keyword
No false positives on benign code patterns (imports, constants, comments, loops)
*/
package rules
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestRegisterBuiltins(t *testing.T) {
t.Parallel()
reg := NewRegistry()
RegisterBuiltins(reg)
assert.GreaterOrEqual(t, reg.Len(), 70)
}
func TestBuiltinRuleIDs(t *testing.T) {
t.Parallel()
reg := NewRegistry()
RegisterBuiltins(reg)
seen := make(map[string]bool)
for _, r := range reg.All() {
assert.NotEmpty(t, r.ID)
assert.NotEmpty(t, r.Description)
assert.NotEmpty(t, r.Keywords)
assert.NotNil(t, r.Pattern)
assert.False(t, seen[r.ID], "duplicate ID: %s", r.ID)
seen[r.ID] = true
}
}
func TestBuiltinPatternMatches(t *testing.T) { //nolint:funlen,gocognit
t.Parallel()
tests := map[string]struct {
ruleID string
input string
wantMatch bool
wantGroup string
}{
"aws access key": { //nolint:gosec
ruleID: "aws-access-key-id",
input: `aws_key = "AKIAIOSFODNN7EXAMPLE"`,
wantMatch: true,
wantGroup: "AKIAIOSFODNN7EXAMPLE",
},
"aws access key ABIA prefix": {
ruleID: "aws-access-key-id",
input: `ABIAIOSFODNN7EXAMPL0`,
wantMatch: true,
wantGroup: "ABIAIOSFODNN7EXAMPL0",
},
"aws access key too short": {
ruleID: "aws-access-key-id",
input: `AKIA1234`,
wantMatch: false,
},
"github pat classic": {
ruleID: "github-pat-classic",
input: `ghp_ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghij`,
wantMatch: true,
wantGroup: "ghp_ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghij",
},
"github pat fine-grained": {
ruleID: "github-pat-fine",
input: `github_pat_abcdefghijABCDEFGHIJKL_abcdefghij0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789ABC`,
wantMatch: true,
},
"github oauth": {
ruleID: "github-oauth-token",
input: `gho_ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghij`,
wantMatch: true,
wantGroup: "gho_ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghij",
},
"github app token": {
ruleID: "github-app-token",
input: `ghs_ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghij`,
wantMatch: true,
wantGroup: "ghs_ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghij",
},
"github refresh token": {
ruleID: "github-refresh-token",
input: `ghr_ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghij`,
wantMatch: true,
wantGroup: "ghr_ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghij",
},
"gitlab pat": {
ruleID: "gitlab-pat",
input: `glpat-xYz1234567890AbCdEfGh`,
wantMatch: true,
wantGroup: "glpat-xYz1234567890AbCdEfGh",
},
"gitlab pipeline trigger": {
ruleID: "gitlab-pipeline-trigger",
input: `glptt-abcdefghijklmnopqrstuvwxyz0123456789ABCD`,
wantMatch: true,
wantGroup: "glptt-abcdefghijklmnopqrstuvwxyz0123456789ABCD",
},
"gitlab runner token": {
ruleID: "gitlab-runner-token",
input: `glrt-xYz1234567890AbCdEfGh`,
wantMatch: true,
wantGroup: "glrt-xYz1234567890AbCdEfGh",
},
"gcp api key": {
ruleID: "gcp-api-key",
input: `AIzaSyDabcdefghij1234567890KLMNOPQRSTUV`,
wantMatch: true,
},
"gcp service account": {
ruleID: "gcp-service-account",
input: `"type" : "service_account"`,
wantMatch: true,
},
"gcp oauth secret": {
ruleID: "gcp-oauth-client-secret",
input: `GOCSPX-aBcDeFgHiJkLmNoPqRsTuVwXyZ01`,
wantMatch: true,
},
"azure storage key": {
ruleID: "azure-storage-key",
input: `AccountKey=` +
`abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz`,
wantMatch: true,
},
"slack bot token": {
ruleID: "slack-bot-token",
input: `xoxb-1234567890-9876543210-AbCdEfGhIjKlMnOpQrStUvWx`,
wantMatch: true,
},
"slack webhook": { //nolint:gosec
ruleID: "slack-webhook",
input: `https://hooks.slack.com/services/T01234567/B01234567/AbCdEfGhIjKlMnOpQrStUvWx`,
wantMatch: true,
},
"stripe live key": { //nolint:gosec
ruleID: "stripe-live-secret",
input: `sk_live_4eC39HqLyjWDarjtT1zdp7dc`,
wantMatch: true,
wantGroup: "sk_live_4eC39HqLyjWDarjtT1zdp7dc",
},
"stripe test key": {
ruleID: "stripe-test-secret",
input: `sk_test_4eC39HqLyjWDarjtT1zdp7dc`, //nolint:gosec
wantMatch: true,
wantGroup: "sk_test_4eC39HqLyjWDarjtT1zdp7dc", //nolint:gosec
},
"stripe restricted live": { //nolint:gosec
ruleID: "stripe-live-restricted",
input: `rk_live_4eC39HqLyjWDarjtT1zdp7dc`,
wantMatch: true,
wantGroup: "rk_live_4eC39HqLyjWDarjtT1zdp7dc",
},
"stripe webhook secret": {
ruleID: "stripe-webhook-secret",
input: `whsec_MfKBGsXP8r7B2cGnQ9jT6KxL12AbCdEf`,
wantMatch: true,
},
"twilio api key": {
ruleID: "twilio-api-key",
input: `SK1234567890abcdef1234567890abcdef`,
wantMatch: true,
wantGroup: "SK1234567890abcdef1234567890abcdef",
},
"twilio account sid": {
ruleID: "twilio-account-sid",
input: `AC1234567890abcdef1234567890abcdef`,
wantMatch: true,
wantGroup: "AC1234567890abcdef1234567890abcdef",
},
"sendgrid api key": {
ruleID: "sendgrid-api-key",
input: `SG.aBcDeFgHiJkLmNoPqRsTuw.xYzAbCdEfGhIjKlMnOpQrStUvWxYzAbCdEfGhIjKlMn`,
wantMatch: true,
},
"shopify access token": {
ruleID: "shopify-access-token",
input: `shpat_abcdef0123456789abcdef0123456789`,
wantMatch: true,
wantGroup: "shpat_abcdef0123456789abcdef0123456789",
},
"shopify custom app": {
ruleID: "shopify-custom-app",
input: `shpca_abcdef0123456789abcdef0123456789`,
wantMatch: true,
wantGroup: "shpca_abcdef0123456789abcdef0123456789",
},
"shopify private app": {
ruleID: "shopify-private-app",
input: `shppa_abcdef0123456789abcdef0123456789`,
wantMatch: true,
wantGroup: "shppa_abcdef0123456789abcdef0123456789",
},
"shopify shared secret": {
ruleID: "shopify-shared-secret",
input: `shpss_abcdef0123456789abcdef0123456789`,
wantMatch: true,
wantGroup: "shpss_abcdef0123456789abcdef0123456789",
},
"npm access token": {
ruleID: "npm-access-token",
input: `npm_aBcDeFgHiJkLmNoPqRsTuVwXyZ0123456789`,
wantMatch: true,
},
"pypi token": {
ruleID: "pypi-api-token",
input: `pypi-AgEIcHlwaS5vcmcCJDU5NTk5YTJhLWIwN2QtNDRkZi1iM` +
`jIxLTk2OWU4YmViZDM3NgACKlszLCJjODA0ZWE1OC1kMjFiLTQzMjMtYWR` +
`mNy0xZjQ4MGQ`,
wantMatch: true,
},
"rubygems api key": {
ruleID: "rubygems-api-key",
input: `rubygems_abcdef0123456789abcdef0123456789abcdef0123456789`,
wantMatch: true,
},
"docker hub pat": {
ruleID: "docker-hub-pat",
input: `dckr_pat_aBcDeFgHiJkLmNoPqRsTuVwXyZ01`,
wantMatch: true,
},
"jwt": {
ruleID: "jwt-token",
input: `eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.` +
`eyJzdWIiOiIxMjM0NTY3ODkwIn0.` +
`dozjgNryP4J3jVmNHl0w5N_XgL0n3I9PlFUP0THsR8U`,
wantMatch: true,
},
"rsa private key": { //nolint:gosec
ruleID: "ssh-private-key-rsa",
input: `-----BEGIN RSA PRIVATE KEY-----`,
wantMatch: true,
},
"openssh private key": {
ruleID: "ssh-private-key-openssh",
input: `-----BEGIN OPENSSH PRIVATE KEY-----`, //nolint:gosec
wantMatch: true,
},
"ec private key": { //nolint:gosec
ruleID: "ssh-private-key-ec",
input: `-----BEGIN EC PRIVATE KEY-----`,
wantMatch: true,
},
"dsa private key": { //nolint:gosec
ruleID: "ssh-private-key-dsa",
input: `-----BEGIN DSA PRIVATE KEY-----`,
wantMatch: true,
},
"pgp private key": { //nolint:gosec
ruleID: "pgp-private-key",
input: `-----BEGIN PGP PRIVATE KEY BLOCK-----`,
wantMatch: true,
},
"pkcs8 private key": {
ruleID: "private-key-pkcs8",
input: `-----BEGIN PRIVATE KEY-----`,
wantMatch: true,
},
"generic password": {
ruleID: "generic-password",
input: `password = "my$ecretP@ss99"`,
wantMatch: true,
wantGroup: "my$ecretP@ss99",
},
"generic password no match on short": {
ruleID: "generic-password",
input: `password = "abc"`,
wantMatch: false,
},
"generic secret": {
ruleID: "generic-secret",
input: `secret_key = "xK9mP2vL5nQ8jR3tB7"`,
wantMatch: true,
wantGroup: "xK9mP2vL5nQ8jR3tB7",
},
"generic api key": {
ruleID: "generic-api-key",
input: `api_key = "aK9mP2vL5nQ8jR3tB7wX4cD"`,
wantMatch: true,
wantGroup: "aK9mP2vL5nQ8jR3tB7wX4cD",
},
"generic token": {
ruleID: "generic-token",
input: `access_token = "xK9mP2vL5nQ8jR3tB7wY"`,
wantMatch: true,
wantGroup: "xK9mP2vL5nQ8jR3tB7wY",
},
"postgres connection": { //nolint:gosec
ruleID: "postgres-connection",
input: `postgresql://admin:s3cr3t@db.example.com:5432/mydb`,
wantMatch: true,
},
"mysql connection": { //nolint:gosec
ruleID: "mysql-connection",
input: `mysql://root:password123@127.0.0.1:3306/app`,
wantMatch: true,
},
"mongodb connection": { //nolint:gosec
ruleID: "mongodb-connection",
input: `mongodb+srv://user:p4ssw0rd@cluster0.abc.mongodb.net/db`,
wantMatch: true,
},
"redis connection": { //nolint:gosec
ruleID: "redis-connection",
input: `redis://default:s3cret@redis.example.com:6379/0`,
wantMatch: true,
},
"hashicorp vault token": {
ruleID: "hashicorp-vault-token",
input: `hvs.CAESIGH3YzJfaBcDeFgHiJkLmNoPqR`,
wantMatch: true,
},
"digitalocean pat": {
ruleID: "digitalocean-pat",
input: `dop_v1_abcdef01234567890abcdef01234567890abcdef01234567890abcdef0123456`,
wantMatch: true,
wantGroup: "dop_v1_abcdef01234567890abcdef01234567890abcdef01234567890abcdef0123456",
},
"linear api key": {
ruleID: "linear-api-key",
input: `lin_api_aBcDeFgHiJkLmNoPqRsTuVwXyZaBcDeFgHiJkLmN`,
wantMatch: true,
},
"age secret key": {
ruleID: "age-secret-key",
input: `AGE-SECRET-KEY-1QQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQ`,
wantMatch: true,
},
"doppler token": {
ruleID: "doppler-token",
input: `dp.st.aBcDeFgHiJkLmNoPqRsTuVwXyZaBcDeFgHiJkLmN`,
wantMatch: true,
},
"grafana sa token": {
ruleID: "grafana-api-key",
input: `glsa_aBcDeFgHiJkLmNoPqRsTuVwXyZaBcDeF_12345678`,
wantMatch: true,
},
"grafana cloud token": {
ruleID: "grafana-cloud-token",
input: `glc_aBcDeFgHiJkLmNoPqRsTuVwXyZaBcDeF1`,
wantMatch: true,
},
"databricks token": {
ruleID: "databricks-token",
input: `dapi1234567890abcdef1234567890abcdef`,
wantMatch: true,
wantGroup: "dapi1234567890abcdef1234567890abcdef",
},
"huggingface token": {
ruleID: "huggingface-token",
input: `hf_aBcDeFgHiJkLmNoPqRsTuVwXyZaBcDeFgHiJ`,
wantMatch: true,
wantGroup: "hf_aBcDeFgHiJkLmNoPqRsTuVwXyZaBcDeFgHiJ",
},
"netlify token": {
ruleID: "netlify-token",
input: `nfp_aBcDeFgHiJkLmNoPqRsTuVwXyZaBcDeFgHiJkLmN`,
wantMatch: true,
},
"postman api key": {
ruleID: "postman-api-key",
input: `PMAK-abcdef0123456789abcdef01-abcdef0123456789abcdef0123456789ab`,
wantMatch: true,
},
"figma pat": {
ruleID: "figma-pat",
input: `figd_aBcDeFgHiJkLmNoPqRsTuVwXyZaBcDeFgHiJkLmN`,
wantMatch: true,
},
"flyio token": {
ruleID: "flyio-token",
input: `fo1_aBcDeFgHiJkLmNoPqRsTuVwXyZaBcDeFgHiJkLmN`,
wantMatch: true,
},
"planetscale token": {
ruleID: "planetscale-token",
input: `pscale_tkn_aBcDeFgHiJkLmNoPqRsTuVwXyZaBcDeFgHiJkLmN`,
wantMatch: true,
},
"replicate token": {
ruleID: "replicate-api-token",
input: `r8_aBcDeFgHiJkLmNoPqRsTuVwXyZaBcDeFgHiJkLmNoPqR`,
wantMatch: true,
},
"sentry auth token": {
ruleID: "sentry-auth-token",
input: `sntrys_aBcDeFgHiJkLmNoPqRsTuVwXyZaBcDeFgHiJkLmNoPqRsTuVwXyZaBcDeFgHi`,
wantMatch: true,
},
"atlassian token": {
ruleID: "atlassian-api-token",
input: `ATATTaBcDeFgHiJkLmNoPqRsTuVwXyZaBcDeFgHiJkLmNoPqRsTuVwX`,
wantMatch: true,
},
"render api key": {
ruleID: "render-api-key",
input: `rnd_aBcDeFgHiJkLmNoPqRsTuVwXyZaBcDeFgHi`,
wantMatch: true,
},
"launchdarkly sdk key": {
ruleID: "launchdarkly-sdk-key",
input: `sdk-a1b2c3d4-e5f6-7890-abcd-ef1234567890`,
wantMatch: true,
wantGroup: "sdk-a1b2c3d4-e5f6-7890-abcd-ef1234567890",
},
"launchdarkly api key": {
ruleID: "launchdarkly-api-key",
input: `api-a1b2c3d4-e5f6-7890-abcd-ef1234567890`,
wantMatch: true,
wantGroup: "api-a1b2c3d4-e5f6-7890-abcd-ef1234567890",
},
"okta api token": {
ruleID: "okta-api-token",
input: `OKTA_API_TOKEN = "00ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijkl"`,
wantMatch: true,
wantGroup: "00ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijkl",
},
"okta ssws header": {
ruleID: "okta-api-token",
input: `SSWS = "00xK9mP2vL5nQ8jR3tB7wX4cD6eF8gH0iJ2kL"`,
wantMatch: true,
wantGroup: "00xK9mP2vL5nQ8jR3tB7wX4cD6eF8gH0iJ2kL",
},
"supabase service key": {
ruleID: "supabase-service-key",
input: `SUPABASE_SERVICE_ROLE_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJvbGUiOiJzZXJ2aWNlX3JvbGUifQ.xxxxxxxx`,
wantMatch: true,
wantGroup: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJvbGUiOiJzZXJ2aWNlX3JvbGUifQ.xxxxxxxx",
},
"supabase service key quoted": {
ruleID: "supabase-service-key",
input: `SUPABASE_SERVICE_ROLE_KEY="eyJhbGciOiJIUzI1NiJ9.eyJyb2xlIjoic2VydmljZV9yb2xlIn0.sig123"`,
wantMatch: true,
},
"no match on normal text": {
ruleID: "aws-access-key-id",
input: `this is just normal code`,
wantMatch: false,
},
}
reg := NewRegistry()
RegisterBuiltins(reg)
for name, tc := range tests {
t.Run(name, func(t *testing.T) {
t.Parallel()
rule, ok := reg.Get(tc.ruleID)
require.True(t, ok, "rule %s not found", tc.ruleID)
match := rule.Pattern.FindStringSubmatch(tc.input)
if tc.wantMatch {
require.NotNil(t, match,
"expected pattern match for rule %s", tc.ruleID)
if tc.wantGroup != "" && rule.SecretGroup > 0 {
require.Greater(t, len(match), rule.SecretGroup)
assert.Equal(t, tc.wantGroup,
match[rule.SecretGroup])
}
} else {
assert.Nil(t, match,
"unexpected match for rule %s", tc.ruleID)
}
})
}
}
func TestBuiltinKeywordMatches(t *testing.T) {
t.Parallel()
reg := NewRegistry()
RegisterBuiltins(reg)
tests := map[string]struct {
content string
wantIDs []string
}{
"aws content": { //nolint:gosec
content: "found AKIAIOSFODNN7EXAMPLE in config",
wantIDs: []string{"aws-access-key-id"},
},
"github content": {
content: "token = ghp_abc123",
wantIDs: []string{"github-pat-classic"},
},
"stripe content": {
content: "key = sk_live_abc123",
wantIDs: []string{"stripe-live-secret"},
},
"private key content": { //nolint:gosec
content: "-----BEGIN RSA PRIVATE KEY-----",
wantIDs: []string{"ssh-private-key-rsa"},
},
"postgres content": { //nolint:gosec
content: "DATABASE_URL=postgres://user:pass@host/db",
wantIDs: []string{"postgres-connection"},
},
}
for name, tc := range tests {
t.Run(name, func(t *testing.T) {
t.Parallel()
matched := reg.MatchKeywords(tc.content)
ids := make([]string, len(matched))
for i, r := range matched {
ids[i] = r.ID
}
for _, wantID := range tc.wantIDs {
assert.Contains(t, ids, wantID)
}
})
}
}
func TestBuiltinNoFalsePositives(t *testing.T) {
t.Parallel()
reg := NewRegistry()
RegisterBuiltins(reg)
benignInputs := []string{
`import os`,
`func main() {}`,
`const x = 42`,
`// this is a comment`,
`print("hello world")`,
`if err != nil { return err }`,
`for i := 0; i < 10; i++ {}`,
}
for _, rule := range reg.All() {
for _, input := range benignInputs {
match := rule.Pattern.FindString(input)
assert.Empty(t, match,
"rule %s false positive on: %s", rule.ID, input)
}
}
}