github.com/google/osv-scalibr@v0.4.1/veles/secrets/stripeapikeys/detector_test.go (about) 1 // Copyright 2025 Google LLC 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 // Copyright 2025 Google LLC 16 // 17 // Licensed under the Apache License, Version 2.0 (the "License"); 18 // you may not use this file except in compliance with the License. 19 // You may obtain a copy of the License at 20 // 21 // http://www.apache.org/licenses/LICENSE-2.0 22 // 23 // Unless required by applicable law or agreed to in writing, software 24 // distributed under the License is distributed on an "AS IS" BASIS, 25 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 26 // See the License for the specific language governing permissions and 27 // limitations under the License. 28 29 package stripeapikeys_test 30 31 import ( 32 "fmt" 33 "strings" 34 "testing" 35 36 "github.com/google/go-cmp/cmp" 37 "github.com/google/go-cmp/cmp/cmpopts" 38 "github.com/google/osv-scalibr/veles" 39 stripeapikeys "github.com/google/osv-scalibr/veles/secrets/stripeapikeys" 40 ) 41 42 var ( 43 // Example valid Stripe API and Webhook secrets. 44 detectorSK = "sk_live_" + strings.Repeat("a", 99) 45 detectorRK = "rk_live_" + strings.Repeat("a", 99) 46 detectorWHSEC = "whsec_UOTBUgpYjyLswPFMxvzo4PyUxleOAiJd" 47 ) 48 49 // TestSecretKeyDetector_truePositives tests SK detection. 50 func TestSecretKeyDetector_truePositives(t *testing.T) { 51 engine, err := veles.NewDetectionEngine( 52 []veles.Detector{stripeapikeys.NewSecretKeyDetector()}, 53 ) 54 if err != nil { 55 t.Fatal(err) 56 } 57 58 cases := []struct { 59 name string 60 input string 61 want []veles.Secret 62 }{{ 63 name: "simple matching string", 64 input: detectorSK, 65 want: []veles.Secret{ 66 stripeapikeys.StripeSecretKey{Key: detectorSK}, 67 }, 68 }, { 69 name: "match at end of string", 70 input: `STRIPE_KEY=` + detectorSK, 71 want: []veles.Secret{ 72 stripeapikeys.StripeSecretKey{Key: detectorSK}, 73 }, 74 }, { 75 name: "match in quotes", 76 input: `key="` + detectorSK + `"`, 77 want: []veles.Secret{ 78 stripeapikeys.StripeSecretKey{Key: detectorSK}, 79 }, 80 }, { 81 name: "multiple matches", 82 input: detectorSK + "\n" + detectorSK, 83 want: []veles.Secret{ 84 stripeapikeys.StripeSecretKey{Key: detectorSK}, 85 stripeapikeys.StripeSecretKey{Key: detectorSK}, 86 }, 87 }, { 88 name: "larger_input_containing_key", 89 input: fmt.Sprintf("config:\n api_key: %s\n", 90 detectorSK), 91 want: []veles.Secret{ 92 stripeapikeys.StripeSecretKey{Key: detectorSK}, 93 }, 94 }, { 95 name: "potential match longer than max key length", 96 input: detectorSK + "Extra", 97 want: []veles.Secret{ 98 stripeapikeys.StripeSecretKey{Key: detectorSK}, 99 }, 100 }} 101 102 for _, tc := range cases { 103 t.Run(tc.name, func(t *testing.T) { 104 got, err := engine.Detect(t.Context(), 105 strings.NewReader(tc.input)) 106 if err != nil { 107 t.Errorf("Detect() error: %v, want nil", err) 108 } 109 if diff := cmp.Diff(tc.want, got, 110 cmpopts.EquateEmpty()); diff != "" { 111 t.Errorf("Detect() diff (-want +got):\n%s", 112 diff) 113 } 114 }) 115 } 116 } 117 118 // TestSecretKeyDetector_trueNegatives tests SK false negatives. 119 func TestSecretKeyDetector_trueNegatives(t *testing.T) { 120 engine, err := veles.NewDetectionEngine( 121 []veles.Detector{stripeapikeys.NewSecretKeyDetector()}, 122 ) 123 if err != nil { 124 t.Fatal(err) 125 } 126 127 cases := []struct { 128 name string 129 input string 130 want []veles.Secret 131 }{{ 132 name: "empty input", 133 input: "", 134 }, { 135 name: "short key should not match", 136 input: detectorSK[:len(detectorSK)-90], 137 }, { 138 name: "invalid_character_in_key_should_not_match", 139 input: "sk_live_" + strings.ReplaceAll( 140 detectorSK[8:], "a", "!", 141 ), 142 }, { 143 name: "incorrect prefix should not match", 144 input: "pk_live_" + detectorSK[8:], 145 }, { 146 name: "prefix missing underscore should not match", 147 input: "sk-live_" + detectorSK[8:], // removes the underscore 148 }} 149 150 for _, tc := range cases { 151 t.Run(tc.name, func(t *testing.T) { 152 got, err := engine.Detect(t.Context(), 153 strings.NewReader(tc.input)) 154 if err != nil { 155 t.Errorf("Detect() error: %v, want nil", err) 156 } 157 if diff := cmp.Diff(tc.want, got, 158 cmpopts.EquateEmpty()); diff != "" { 159 t.Errorf("Detect() diff (-want +got):\n%s", 160 diff) 161 } 162 }) 163 } 164 } 165 166 // TestRestrictedKeyDetector_truePositives tests RK detection. 167 func TestRestrictedKeyDetector_truePositives(t *testing.T) { 168 engine, err := veles.NewDetectionEngine( 169 []veles.Detector{stripeapikeys.NewRestrictedKeyDetector()}, 170 ) 171 if err != nil { 172 t.Fatal(err) 173 } 174 175 cases := []struct { 176 name string 177 input string 178 want []veles.Secret 179 }{{ 180 name: "simple matching string", 181 input: detectorRK, 182 want: []veles.Secret{ 183 stripeapikeys.StripeRestrictedKey{Key: detectorRK}, 184 }, 185 }, { 186 name: "match at end of string", 187 input: `STRIPE_RK=` + detectorRK, 188 want: []veles.Secret{ 189 stripeapikeys.StripeRestrictedKey{Key: detectorRK}, 190 }, 191 }, { 192 name: "match in quotes", 193 input: `secret="` + detectorRK + `"`, 194 want: []veles.Secret{ 195 stripeapikeys.StripeRestrictedKey{Key: detectorRK}, 196 }, 197 }, { 198 name: "multiple matches", 199 input: detectorRK + " " + detectorRK, 200 want: []veles.Secret{ 201 stripeapikeys.StripeRestrictedKey{Key: detectorRK}, 202 stripeapikeys.StripeRestrictedKey{Key: detectorRK}, 203 }, 204 }, { 205 name: "larger_input_containing_key", 206 input: fmt.Sprintf("token:\n value: %s\n", 207 detectorRK), 208 want: []veles.Secret{ 209 stripeapikeys.StripeRestrictedKey{Key: detectorRK}, 210 }, 211 }, { 212 name: "potential match longer than max key length", 213 input: detectorRK + "Extra", 214 want: []veles.Secret{ 215 stripeapikeys.StripeRestrictedKey{Key: detectorRK}, 216 }, 217 }} 218 219 for _, tc := range cases { 220 t.Run(tc.name, func(t *testing.T) { 221 got, err := engine.Detect(t.Context(), 222 strings.NewReader(tc.input)) 223 if err != nil { 224 t.Errorf("Detect() error: %v, want nil", err) 225 } 226 if diff := cmp.Diff(tc.want, got, 227 cmpopts.EquateEmpty()); diff != "" { 228 t.Errorf("Detect() diff (-want +got):\n%s", 229 diff) 230 } 231 }) 232 } 233 } 234 235 // TestRestrictedKeyDetector_trueNegatives tests RK false negatives. 236 func TestRestrictedKeyDetector_trueNegatives(t *testing.T) { 237 engine, err := veles.NewDetectionEngine( 238 []veles.Detector{stripeapikeys.NewRestrictedKeyDetector()}, 239 ) 240 if err != nil { 241 t.Fatal(err) 242 } 243 244 cases := []struct { 245 name string 246 input string 247 want []veles.Secret 248 }{{ 249 name: "empty input", 250 input: "", 251 }, { 252 name: "short key should not match", 253 input: detectorRK[:len(detectorRK)-90], 254 }, { 255 name: "invalid_character_in_key_should_not_match", 256 input: "rk_live_" + strings.ReplaceAll( 257 detectorRK[8:], "a", "#", 258 ), 259 }, { 260 name: "incorrect prefix should not match", 261 input: "sk_live_" + detectorRK[8:], 262 }, { 263 name: "prefix missing underscore should not match", 264 input: "rk-live_" + detectorRK[8:], // removes the underscore 265 }} 266 267 for _, tc := range cases { 268 t.Run(tc.name, func(t *testing.T) { 269 got, err := engine.Detect(t.Context(), 270 strings.NewReader(tc.input)) 271 if err != nil { 272 t.Errorf("Detect() error: %v, want nil", err) 273 } 274 if diff := cmp.Diff(tc.want, got, 275 cmpopts.EquateEmpty()); diff != "" { 276 t.Errorf("Detect() diff (-want +got):\n%s", 277 diff) 278 } 279 }) 280 } 281 } 282 283 // TestWebhookSecretDetector_truePositives tests WHSEC detection. 284 func TestWebhookSecretDetector_truePositives(t *testing.T) { 285 engine, err := veles.NewDetectionEngine( 286 []veles.Detector{stripeapikeys.NewWebhookSecretDetector()}, 287 ) 288 if err != nil { 289 t.Fatal(err) 290 } 291 292 cases := []struct { 293 name string 294 input string 295 want []veles.Secret 296 }{{ 297 name: "simple matching string", 298 input: detectorWHSEC, 299 want: []veles.Secret{ 300 stripeapikeys.StripeWebhookSecret{Key: detectorWHSEC}, 301 }, 302 }, { 303 name: "match at end of string", 304 input: `STRIPE_WHSEC=` + detectorWHSEC, 305 want: []veles.Secret{ 306 stripeapikeys.StripeWebhookSecret{Key: detectorWHSEC}, 307 }, 308 }, { 309 name: "match in quotes", 310 input: `secret="` + detectorWHSEC + `"`, 311 want: []veles.Secret{ 312 stripeapikeys.StripeWebhookSecret{Key: detectorWHSEC}, 313 }, 314 }, { 315 name: "multiple matches", 316 input: detectorWHSEC + " " + detectorWHSEC, 317 want: []veles.Secret{ 318 stripeapikeys.StripeWebhookSecret{Key: detectorWHSEC}, 319 stripeapikeys.StripeWebhookSecret{Key: detectorWHSEC}, 320 }, 321 }, { 322 name: "larger_input_containing_key", 323 input: fmt.Sprintf("token:\n value: %s\n", 324 detectorWHSEC), 325 want: []veles.Secret{ 326 stripeapikeys.StripeWebhookSecret{Key: detectorWHSEC}, 327 }, 328 }, { 329 name: "potential match longer than max key length", 330 input: detectorWHSEC + strings.Repeat("a", 500), 331 want: []veles.Secret{ 332 stripeapikeys.StripeWebhookSecret{Key: detectorWHSEC}, 333 }, 334 }} 335 336 for _, tc := range cases { 337 t.Run(tc.name, func(t *testing.T) { 338 got, err := engine.Detect(t.Context(), 339 strings.NewReader(tc.input)) 340 if err != nil { 341 t.Errorf("Detect() error: %v, want nil", err) 342 } 343 if diff := cmp.Diff(tc.want, got, 344 cmpopts.EquateEmpty()); diff != "" { 345 t.Errorf("Detect() diff (-want +got):\n%s", 346 diff) 347 } 348 }) 349 } 350 } 351 352 // TestWebhookSecretDetector_trueNegatives tests WHSEC false negatives. 353 func TestWebhookSecretDetector_trueNegatives(t *testing.T) { 354 engine, err := veles.NewDetectionEngine( 355 []veles.Detector{stripeapikeys.NewWebhookSecretDetector()}, 356 ) 357 if err != nil { 358 t.Fatal(err) 359 } 360 361 cases := []struct { 362 name string 363 input string 364 want []veles.Secret 365 }{{ 366 name: "empty input", 367 input: "", 368 }, { 369 name: "short key should not match", 370 input: detectorWHSEC[:len(detectorWHSEC)-2], 371 }, { 372 name: "invalid_character_in_key_should_not_match", 373 input: "whsec_" + strings.ReplaceAll( 374 detectorWHSEC[6:], "U", "#", 375 ), 376 }, { 377 name: "incorrect prefix should not match", 378 input: "whsec-" + detectorWHSEC[6:], 379 }, { 380 name: "prefix missing underscore should not match", 381 input: "whsec" + detectorWHSEC[6:], // removes the underscore 382 }} 383 384 for _, tc := range cases { 385 t.Run(tc.name, func(t *testing.T) { 386 got, err := engine.Detect(t.Context(), 387 strings.NewReader(tc.input)) 388 if err != nil { 389 t.Errorf("Detect() error: %v, want nil", err) 390 } 391 if diff := cmp.Diff(tc.want, got, 392 cmpopts.EquateEmpty()); diff != "" { 393 t.Errorf("Detect() diff (-want +got):\n%s", 394 diff) 395 } 396 }) 397 } 398 }