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  }