Skip to content

Commit f6aad8b

Browse files
authored
Merge pull request #104 from DFanso/dev
Feat: Add redaction patterns for cloud credentials
2 parents d1227f6 + dd2a098 commit f6aad8b

4 files changed

Lines changed: 325 additions & 15 deletions

File tree

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,3 +28,5 @@ HACKTOBERFEST_SETUP.md
2828
PR_DESCRIPTION.md
2929
commit
3030
test.txt
31+
AGENTS.md
32+
commit~

cmd/cli/root.go

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,3 @@
1-
/*
2-
Copyright © 2025 NAME HERE <EMAIL ADDRESS>
3-
*/
41
package cmd
52

63
import (

internal/scrubber/scrubber.go

Lines changed: 190 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,50 @@ var (
4444
Redact: "${1}=\"[REDACTED_AWS_SECRET]\"",
4545
},
4646

47+
// Azure Credentials
48+
{
49+
Name: "Azure Client Secret",
50+
Pattern: regexp.MustCompile(`(?i)(azure[_-]?client[_-]?secret|AZURE_CLIENT_SECRET)\s*[=:]\s*["\']?([a-zA-Z0-9_\-\.~]{20,})["\']?`),
51+
Redact: "${1}=\"[REDACTED_AZURE_CLIENT_SECRET]\"",
52+
},
53+
{
54+
Name: "Azure Subscription Key",
55+
Pattern: regexp.MustCompile(`(?i)(azure[_-]?subscription[_-]?key|AZURE_SUBSCRIPTION_KEY)\s*[=:]\s*["\']?([a-zA-Z0-9]{32})["\']?`),
56+
Redact: "${1}=\"[REDACTED_AZURE_SUBSCRIPTION_KEY]\"",
57+
},
58+
{
59+
Name: "Azure Storage Key",
60+
Pattern: regexp.MustCompile(`(?i)(azure[_-]?storage[_-]?key|AZURE_STORAGE_KEY|AccountKey)\s*[=:]\s*["\']?([a-zA-Z0-9/+=]{88})["\']?`),
61+
Redact: "${1}=\"[REDACTED_AZURE_STORAGE_KEY]\"",
62+
},
63+
{
64+
Name: "Azure Service Principal",
65+
Pattern: regexp.MustCompile(`(?i)(azure[_-]?service[_-]?principal|AZURE_SERVICE_PRINCIPAL)\s*[=:]\s*["\']?([a-f0-9-]{36})["\']?`),
66+
Redact: "${1}=\"[REDACTED_AZURE_SERVICE_PRINCIPAL]\"",
67+
},
68+
69+
// Google Cloud Credentials
70+
{
71+
Name: "Google Cloud Service Account Key",
72+
Pattern: regexp.MustCompile(`(?i)(gcp[_-]?service[_-]?account[_-]?key|GOOGLE_APPLICATION_CREDENTIALS)\s*[=:]\s*["\']?([a-zA-Z0-9_\-\.@]+\.json)["\']?`),
73+
Redact: "${1}=\"[REDACTED_GCP_SA_KEY]\"",
74+
},
75+
{
76+
Name: "Google Cloud API Key",
77+
Pattern: regexp.MustCompile(`(?i)(gcp[_-]?api[_-]?key|GOOGLE_CLOUD_API_KEY)\s*[=:]\s*["\']?([a-zA-Z0-9_\-]{39})["\']?`),
78+
Redact: "${1}=\"[REDACTED_GCP_API_KEY]\"",
79+
},
80+
{
81+
Name: "Google Cloud OAuth Client",
82+
Pattern: regexp.MustCompile(`(?i)(gcp[_-]?oauth[_-]?client[_-]?secret|GOOGLE_OAUTH_CLIENT_SECRET)\s*[=:]\s*["\']?([a-zA-Z0-9_\-]{24})["\']?`),
83+
Redact: "${1}=\"[REDACTED_GCP_OAUTH_SECRET]\"",
84+
},
85+
{
86+
Name: "Google Cloud JSON Credentials",
87+
Pattern: regexp.MustCompile(`(?s)"type":\s*"service_account".*?"private_key":\s*"-----BEGIN PRIVATE KEY-----.*?-----END PRIVATE KEY-----`),
88+
Redact: "\"type\": \"service_account\",\n\"private_key\": \"[REDACTED_GCP_PRIVATE_KEY]\"",
89+
},
90+
4791
// Database Credentials
4892
{
4993
Name: "Database URL with Password",
@@ -88,12 +132,155 @@ var (
88132
Redact: "${1}=\"[REDACTED_SLACK_TOKEN]\"",
89133
},
90134

91-
// Private Keys
135+
// Payment & Communication APIs
136+
{
137+
Name: "Stripe API Key",
138+
Pattern: regexp.MustCompile(`(?i)(stripe[_-]?api[_-]?key|STRIPE_API_KEY)\s*[=:]\s*["\']?(sk_live_[a-zA-Z0-9]{24})["\']?`),
139+
Redact: "${1}=\"[REDACTED_STRIPE_KEY]\"",
140+
},
141+
{
142+
Name: "Stripe Publishable Key",
143+
Pattern: regexp.MustCompile(`(?i)(stripe[_-]?publishable[_-]?key|STRIPE_PUBLISHABLE_KEY)\s*[=:]\s*["\']?(pk_live_[a-zA-Z0-9]{24})["\']?`),
144+
Redact: "${1}=\"[REDACTED_STRIPE_PUBLISHABLE_KEY]\"",
145+
},
146+
{
147+
Name: "Twilio API Key",
148+
Pattern: regexp.MustCompile(`(?i)(twilio[_-]?api[_-]?key|TWILIO_API_KEY)\s*[=:]\s*["\']?(SK[a-f0-9]{32})["\']?`),
149+
Redact: "${1}=\"[REDACTED_TWILIO_API_KEY]\"",
150+
},
151+
{
152+
Name: "Twilio Auth Token",
153+
Pattern: regexp.MustCompile(`(?i)(twilio[_-]?auth[_-]?token|TWILIO_AUTH_TOKEN)\s*[=:]\s*["\']?([a-f0-9]{32})["\']?`),
154+
Redact: "${1}=\"[REDACTED_TWILIO_AUTH_TOKEN]\"",
155+
},
156+
{
157+
Name: "Twilio Account SID",
158+
Pattern: regexp.MustCompile(`(?i)(twilio[_-]?account[_-]?sid|TWILIO_ACCOUNT_SID)\s*[=:]\s*["\']?(AC[a-f0-9]{32})["\']?`),
159+
Redact: "${1}=\"[REDACTED_TWILIO_ACCOUNT_SID]\"",
160+
},
161+
162+
// Cloud & Infrastructure APIs
163+
{
164+
Name: "DigitalOcean API Key",
165+
Pattern: regexp.MustCompile(`(?i)(digitalocean[_-]?api[_-]?key|DIGITALOCEAN_API_KEY)\s*[=:]\s*["\']?([a-f0-9]{64})["\']?`),
166+
Redact: "${1}=\"[REDACTED_DIGITALOCEAN_KEY]\"",
167+
},
168+
{
169+
Name: "Heroku API Key",
170+
Pattern: regexp.MustCompile(`(?i)(heroku[_-]?api[_-]?key|HEROKU_API_KEY)\s*[=:]\s*["\']?([a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12})["\']?`),
171+
Redact: "${1}=\"[REDACTED_HEROKU_KEY]\"",
172+
},
173+
{
174+
Name: "Vercel API Key",
175+
Pattern: regexp.MustCompile(`(?i)(vercel[_-]?api[_-]?key|VERCEL_API_KEY)\s*[=:]\s*["\']?([a-zA-Z0-9_\-]{24})["\']?`),
176+
Redact: "${1}=\"[REDACTED_VERCEL_KEY]\"",
177+
},
178+
{
179+
Name: "Netlify API Key",
180+
Pattern: regexp.MustCompile(`(?i)(netlify[_-]?api[_-]?key|NETLIFY_API_KEY)\s*[=:]\s*["\']?([a-zA-Z0-9_\-]{40})["\']?`),
181+
Redact: "${1}=\"[REDACTED_NETLIFY_KEY]\"",
182+
},
183+
184+
// Database & Analytics APIs
92185
{
93-
Name: "Private Key",
94-
Pattern: regexp.MustCompile(`(?s)(-----BEGIN (?:RSA |EC |DSA |OPENSSH )?PRIVATE KEY-----).*?(-----END (?:RSA |EC |DSA |OPENSSH )?PRIVATE KEY-----)`),
186+
Name: "MongoDB Atlas API Key",
187+
Pattern: regexp.MustCompile(`(?i)(mongodb[_-]?api[_-]?key|MONGODB_API_KEY)\s*[=:]\s*["\']?([a-f0-9]{24}-[a-f0-9]{24})["\']?`),
188+
Redact: "${1}=\"[REDACTED_MONGODB_API_KEY]\"",
189+
},
190+
{
191+
Name: "SendGrid API Key",
192+
Pattern: regexp.MustCompile(`(?i)(sendgrid[_-]?api[_-]?key|SENDGRID_API_KEY)\s*[=:]\s*["\']?(SG\.[a-zA-Z0-9_\-\.]{22,}\.[a-zA-Z0-9_\-\.]{43})["\']?`),
193+
Redact: "${1}=\"[REDACTED_SENDGRID_KEY]\"",
194+
},
195+
{
196+
Name: "Mailgun API Key",
197+
Pattern: regexp.MustCompile(`(?i)(mailgun[_-]?api[_-]?key|MAILGUN_API_KEY)\s*[=:]\s*["\']?(key-[a-f0-9]{32})["\']?`),
198+
Redact: "${1}=\"[REDACTED_MAILGUN_KEY]\"",
199+
},
200+
201+
// Social & Auth APIs
202+
{
203+
Name: "Facebook App Secret",
204+
Pattern: regexp.MustCompile(`(?i)(facebook[_-]?app[_-]?secret|FACEBOOK_APP_SECRET)\s*[=:]\s*["\']?([a-f0-9]{32})["\']?`),
205+
Redact: "${1}=\"[REDACTED_FACEBOOK_SECRET]\"",
206+
},
207+
{
208+
Name: "Twitter API Key",
209+
Pattern: regexp.MustCompile(`(?i)(twitter[_-]?api[_-]?key|TWITTER_API_KEY)\s*[=:]\s*["\']?([a-zA-Z0-9]{25})["\']?`),
210+
Redact: "${1}=\"[REDACTED_TWITTER_API_KEY]\"",
211+
},
212+
{
213+
Name: "Twitter API Secret",
214+
Pattern: regexp.MustCompile(`(?i)(twitter[_-]?api[_-]?secret|TWITTER_API_SECRET)\s*[=:]\s*["\']?([a-zA-Z0-9]{50})["\']?`),
215+
Redact: "${1}=\"[REDACTED_TWITTER_API_SECRET]\"",
216+
},
217+
{
218+
Name: "LinkedIn Client Secret",
219+
Pattern: regexp.MustCompile(`(?i)(linkedin[_-]?client[_-]?secret|LINKEDIN_CLIENT_SECRET)\s*[=:]\s*["\']?([a-zA-Z0-9]{16})["\']?`),
220+
Redact: "${1}=\"[REDACTED_LINKEDIN_SECRET]\"",
221+
},
222+
223+
// SSH Keys and Private Keys in various formats
224+
{
225+
Name: "RSA Private Key",
226+
Pattern: regexp.MustCompile(`(?s)(-----BEGIN RSA PRIVATE KEY-----).*?(-----END RSA PRIVATE KEY-----)`),
227+
Redact: "${1}\n[REDACTED_RSA_PRIVATE_KEY]\n${2}",
228+
},
229+
{
230+
Name: "EC Private Key",
231+
Pattern: regexp.MustCompile(`(?s)(-----BEGIN EC PRIVATE KEY-----).*?(-----END EC PRIVATE KEY-----)`),
232+
Redact: "${1}\n[REDACTED_EC_PRIVATE_KEY]\n${2}",
233+
},
234+
{
235+
Name: "DSA Private Key",
236+
Pattern: regexp.MustCompile(`(?s)(-----BEGIN DSA PRIVATE KEY-----).*?(-----END DSA PRIVATE KEY-----)`),
237+
Redact: "${1}\n[REDACTED_DSA_PRIVATE_KEY]\n${2}",
238+
},
239+
{
240+
Name: "OpenSSH Private Key",
241+
Pattern: regexp.MustCompile(`(?s)(-----BEGIN OPENSSH PRIVATE KEY-----).*?(-----END OPENSSH PRIVATE KEY-----)`),
242+
Redact: "${1}\n[REDACTED_OPENSSH_PRIVATE_KEY]\n${2}",
243+
},
244+
{
245+
Name: "PKCS#8 Private Key",
246+
Pattern: regexp.MustCompile(`(?s)(-----BEGIN PRIVATE KEY-----).*?(-----END PRIVATE KEY-----)`),
95247
Redact: "${1}\n[REDACTED_PRIVATE_KEY]\n${2}",
96248
},
249+
{
250+
Name: "PGP Private Key",
251+
Pattern: regexp.MustCompile(`(?s)(-----BEGIN PGP PRIVATE KEY BLOCK-----).*?(-----END PGP PRIVATE KEY BLOCK-----)`),
252+
Redact: "${1}\n[REDACTED_PGP_PRIVATE_KEY]\n${2}",
253+
},
254+
{
255+
Name: "SSH Public Key (RSA)",
256+
Pattern: regexp.MustCompile(`(?i)(ssh[_-]?rsa[_-]?public[_-]?key|SSH_RSA_PUBLIC_KEY)\s*[=:]\s*["\']?(ssh-rsa [a-zA-Z0-9/+=]+ [a-zA-Z0-9._@-]+)["\']?`),
257+
Redact: "${1}=\"[REDACTED_SSH_RSA_PUBLIC_KEY]\"",
258+
},
259+
{
260+
Name: "SSH Public Key (ED25519)",
261+
Pattern: regexp.MustCompile(`(?i)(ssh[_-]?ed25519[_-]?public[_-]?key|SSH_ED25519_PUBLIC_KEY)\s*[=:]\s*["\']?(ssh-ed25519 [a-zA-Z0-9+/]+ [a-zA-Z0-9._@-]+)["\']?`),
262+
Redact: "${1}=\"[REDACTED_SSH_ED25519_PUBLIC_KEY]\"",
263+
},
264+
{
265+
Name: "SSH Public Key (ECDSA)",
266+
Pattern: regexp.MustCompile(`(?i)(ssh[_-]?ecdsa[_-]?public[_-]?key|SSH_ECDSA_PUBLIC_KEY)\s*[=:]\s*["\']?(ecdsa-sha2-[a-zA-Z0-9-]+ [a-zA-Z0-9/+=]+ [a-zA-Z0-9._@-]+)["\']?`),
267+
Redact: "${1}=\"[REDACTED_SSH_ECDSA_PUBLIC_KEY]\"",
268+
},
269+
{
270+
Name: "SSH Public Key (Generic)",
271+
Pattern: regexp.MustCompile(`(?i)(ssh[_-]?public[_-]?key|SSH_PUBLIC_KEY)\s*[=:]\s*["\']?(ssh-[a-zA-Z0-9-]+ [a-zA-Z0-9/+=]+ [a-zA-Z0-9._@-]+)["\']?`),
272+
Redact: "${1}=\"[REDACTED_SSH_PUBLIC_KEY]\"",
273+
},
274+
{
275+
Name: "SSH Authorized Keys",
276+
Pattern: regexp.MustCompile(`(?i)(ssh[_-]?authorized[_-]?keys|SSH_AUTHORIZED_KEYS)\s*[=:]\s*["\']?(ssh-[a-zA-Z0-9-]+ [a-zA-Z0-9/+=]+ [a-zA-Z0-9._@-]+)["\']?`),
277+
Redact: "${1}=\"[REDACTED_SSH_AUTHORIZED_KEYS]\"",
278+
},
279+
{
280+
Name: "SSH Private Key File Content",
281+
Pattern: regexp.MustCompile(`(?s)(-----BEGIN [A-Z ]+PRIVATE KEY-----\n)([A-Za-z0-9+/=\n]+)(\n-----END [A-Z ]+PRIVATE KEY-----)`),
282+
Redact: "${1}[REDACTED_SSH_PRIVATE_KEY_CONTENT]${3}",
283+
},
97284

98285
// JWT Tokens
99286
{

internal/scrubber/scrubber_test.go

Lines changed: 133 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@ ghijklmnopqrstuvwxyz
134134
if strings.Contains(result, "MIIEpAIBAAKCAQEA") || strings.Contains(result, "ghijklmnopqrstuvwxyz") {
135135
t.Errorf("ScrubDiff() failed to redact private key.\nOutput: %s", result)
136136
}
137-
if !strings.Contains(result, "[REDACTED_PRIVATE_KEY]") {
137+
if !strings.Contains(result, "[REDACTED_RSA_PRIVATE_KEY]") {
138138
t.Errorf("ScrubDiff() did not add redaction marker.\nOutput: %s", result)
139139
}
140140
}
@@ -397,29 +397,153 @@ func TestScrubEmailInCredentials(t *testing.T) {
397397
}
398398

399399
func TestScrubCreditCard(t *testing.T) {
400+
tests := []struct {
401+
name string
402+
input string
403+
expected string
404+
}{
405+
{
406+
name: "Credit card with spaces",
407+
input: "4111 1111 1111 1111",
408+
expected: "[REDACTED_CREDIT_CARD]",
409+
},
410+
{
411+
name: "Credit card with dashes",
412+
input: "4111-1111-1111-1111",
413+
expected: "[REDACTED_CREDIT_CARD]",
414+
},
415+
{
416+
name: "Credit card no separators",
417+
input: "4111111111111111",
418+
expected: "[REDACTED_CREDIT_CARD]",
419+
},
420+
}
421+
422+
for _, tt := range tests {
423+
t.Run(tt.name, func(t *testing.T) {
424+
result := ScrubDiff(tt.input)
425+
if !strings.Contains(result, "[REDACTED_CREDIT_CARD]") {
426+
t.Errorf("ScrubDiff() failed to redact credit card.\nInput: %s\nOutput: %s", tt.input, result)
427+
}
428+
})
429+
}
430+
}
431+
432+
func TestScrubAzureCredentials(t *testing.T) {
400433
tests := []struct {
401434
name string
402435
input string
403436
}{
404437
{
405-
name: "Credit card with spaces",
406-
input: `card: "4532 1234 5678 9010"`,
438+
name: "Azure Client Secret",
439+
input: `AZURE_CLIENT_SECRET="abc123def456ghi789jkl012mno345pqr"`,
407440
},
408441
{
409-
name: "Credit card with dashes",
410-
input: `4532-1234-5678-9010`,
442+
name: "Azure Subscription Key",
443+
input: `azure_subscription_key: "abcdefghijklmnopqrstuvwxyz123456"`,
411444
},
412445
{
413-
name: "Credit card no separators",
414-
input: `4532123456789010`,
446+
name: "Azure Storage Key",
447+
input: `AccountKey="abcdefghijklmnopqrstuvwxyz1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890123456789012345678901234567890123456789012345678901234567890"`,
415448
},
416449
}
417450

418451
for _, tt := range tests {
419452
t.Run(tt.name, func(t *testing.T) {
420453
result := ScrubDiff(tt.input)
421-
if strings.Contains(result, "4532") && strings.Contains(result, "9010") {
422-
t.Errorf("ScrubDiff() failed to redact credit card.\nInput: %s\nOutput: %s", tt.input, result)
454+
if !strings.Contains(result, "[REDACTED_AZURE") {
455+
t.Errorf("ScrubDiff() failed to redact Azure credentials.\nInput: %s\nOutput: %s", tt.input, result)
456+
}
457+
})
458+
}
459+
}
460+
461+
func TestScrubGoogleCloudCredentials(t *testing.T) {
462+
tests := []struct {
463+
name string
464+
input string
465+
}{
466+
{
467+
name: "Google Cloud Service Account Key",
468+
input: `GOOGLE_APPLICATION_CREDENTIALS="my-service-account-key.json"`,
469+
},
470+
{
471+
name: "Google Cloud API Key",
472+
input: `gcp_api_key="abcdefghijklmnopqrstuvwxyz1234567890123456789"`,
473+
},
474+
{
475+
name: "Google Cloud JSON Credentials",
476+
input: `"type": "service_account",\n"private_key": "-----BEGIN PRIVATE KEY-----\nMIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC5\n-----END PRIVATE KEY-----"`,
477+
},
478+
}
479+
480+
for _, tt := range tests {
481+
t.Run(tt.name, func(t *testing.T) {
482+
result := ScrubDiff(tt.input)
483+
if !strings.Contains(result, "[REDACTED_") {
484+
t.Errorf("ScrubDiff() failed to redact Google Cloud credentials.\nInput: %s\nOutput: %s", tt.input, result)
485+
}
486+
})
487+
}
488+
}
489+
490+
func TestScrubAdditionalAPIKeys(t *testing.T) {
491+
tests := []struct {
492+
name string
493+
input string
494+
}{
495+
{
496+
name: "Stripe API Key",
497+
input: `STRIPE_API_KEY="sk_live_1234567890abcdefghijklmnopqrstuvwxyz"`,
498+
},
499+
{
500+
name: "Twilio Auth Token",
501+
input: `twilio_auth_token: "1234567890abcdef1234567890abcdef"`,
502+
},
503+
{
504+
name: "DigitalOcean API Key",
505+
input: `digitalocean_api_key="1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef"`,
506+
},
507+
{
508+
name: "SendGrid API Key",
509+
input: `SENDGRID_API_KEY="SG.1234567890abcdef1234567890abcdef.1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef12345678"`,
510+
},
511+
}
512+
513+
for _, tt := range tests {
514+
t.Run(tt.name, func(t *testing.T) {
515+
result := ScrubDiff(tt.input)
516+
if !strings.Contains(result, "[REDACTED_") {
517+
t.Errorf("ScrubDiff() failed to redact additional API keys.\nInput: %s\nOutput: %s", tt.input, result)
518+
}
519+
})
520+
}
521+
}
522+
523+
func TestScrubSSHKeys(t *testing.T) {
524+
tests := []struct {
525+
name string
526+
input string
527+
}{
528+
{
529+
name: "OpenSSH Private Key",
530+
input: `-----BEGIN OPENSSH PRIVATE KEY-----\nb3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAlwAAAAdzc2gtcn\nNhAAAAAwEAAQAAAIEA2K8Qv8a4a5K6B2J8RiQzL1h4w0F2R3B4C5D6E7F8G9H0I1J2K3L4\n-----END OPENSSH PRIVATE KEY-----`,
531+
},
532+
{
533+
name: "SSH RSA Public Key",
534+
input: `ssh_rsa_public_key="ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDCeStA8tG2r5a user@hostname"`,
535+
},
536+
{
537+
name: "SSH ED25519 Public Key",
538+
input: `SSH_ED25519_PUBLIC_KEY="ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGbWQrKyUBUHvL3+d1H5Q8UQrBw6M5J user@example.com"`,
539+
},
540+
}
541+
542+
for _, tt := range tests {
543+
t.Run(tt.name, func(t *testing.T) {
544+
result := ScrubDiff(tt.input)
545+
if !strings.Contains(result, "[REDACTED_") {
546+
t.Errorf("ScrubDiff() failed to redact SSH keys.\nInput: %s\nOutput: %s", tt.input, result)
423547
}
424548
})
425549
}

0 commit comments

Comments
 (0)