github.com/google/osv-scalibr@v0.4.1/veles/secrets/onepasswordkeys/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 onepasswordkeys_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  	"github.com/google/osv-scalibr/veles/secrets/onepasswordkeys"
    40  )
    41  
    42  const (
    43  	testSecretKey       = "A3-XXXXXX-YYYYYY-ZZZZZ-AAAAA-BBBBB-CCCCC"
    44  	testSecretKeyAlt    = "A3-ABC123-DEFGH456789-JKLMN-OPQRS-TUVWX"
    45  	testServiceToken    = "ops_eyJxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
    46  	testServiceTokenAlt = "ops_eyJabcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+/abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+/abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+/abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+/"
    47  	testRecoveryKey     = "1PRK-ABCD-EFGH-IJKL-MNOP-QRST-UVWX-YZ12-3456-789A-BCDE-FGHI-JKLM-NOPQ"
    48  	testRecoveryKeyAlt  = "1PRK-1234-5678-9ABC-DEFG-HIJK-LMNO-PQRS-TUVW-XYZ1-2345-6789-ABCD-EFGH"
    49  )
    50  
    51  // TestSecretKeyDetector_TruePositives tests for cases where we know the SecretKeyDetector
    52  // will find 1Password Secret Key/s.
    53  func TestSecretKeyDetector_TruePositives(t *testing.T) {
    54  	engine, err := veles.NewDetectionEngine([]veles.Detector{onepasswordkeys.NewSecretKeyDetector()})
    55  	if err != nil {
    56  		t.Fatal(err)
    57  	}
    58  	cases := []struct {
    59  		name  string
    60  		input string
    61  		want  []veles.Secret
    62  	}{{
    63  		name:  "simple matching string",
    64  		input: testSecretKey,
    65  		want: []veles.Secret{
    66  			onepasswordkeys.OnePasswordSecretKey{Key: testSecretKey},
    67  		},
    68  	}, {
    69  		name:  "match at end of string",
    70  		input: `OP_SECRET_KEY=` + testSecretKey,
    71  		want: []veles.Secret{
    72  			onepasswordkeys.OnePasswordSecretKey{Key: testSecretKey},
    73  		},
    74  	}, {
    75  		name:  "match in middle of string",
    76  		input: `OP_SECRET_KEY="` + testSecretKey + `"`,
    77  		want: []veles.Secret{
    78  			onepasswordkeys.OnePasswordSecretKey{Key: testSecretKey},
    79  		},
    80  	}, {
    81  		name:  "multiple matches",
    82  		input: testSecretKey + " " + testSecretKey + " " + testSecretKey,
    83  		want: []veles.Secret{
    84  			onepasswordkeys.OnePasswordSecretKey{Key: testSecretKey},
    85  			onepasswordkeys.OnePasswordSecretKey{Key: testSecretKey},
    86  			onepasswordkeys.OnePasswordSecretKey{Key: testSecretKey},
    87  		},
    88  	}, {
    89  		name:  "multiple distinct matches",
    90  		input: testSecretKey + "\n" + testSecretKeyAlt,
    91  		want: []veles.Secret{
    92  			onepasswordkeys.OnePasswordSecretKey{Key: testSecretKey},
    93  			onepasswordkeys.OnePasswordSecretKey{Key: testSecretKeyAlt},
    94  		},
    95  	}, {
    96  		name: "larger_input_containing_key",
    97  		input: fmt.Sprintf(`
    98  :test_secret_key: A3-ABCDE-FGHIJ-KLMNO-PQRST-UVWXY-Z1234
    99  :onepassword_secret_key: %s 
   100  		`, testSecretKey),
   101  		want: []veles.Secret{
   102  			onepasswordkeys.OnePasswordSecretKey{Key: testSecretKey},
   103  		},
   104  	}, {
   105  		name:  "potential match with extra characters",
   106  		input: testSecretKey + `extra`,
   107  		want: []veles.Secret{
   108  			onepasswordkeys.OnePasswordSecretKey{Key: testSecretKey},
   109  		},
   110  	}}
   111  	for _, tc := range cases {
   112  		t.Run(tc.name, func(t *testing.T) {
   113  			got, err := engine.Detect(t.Context(), strings.NewReader(tc.input))
   114  			if err != nil {
   115  				t.Errorf("Detect() error: %v, want nil", err)
   116  			}
   117  			if diff := cmp.Diff(tc.want, got, cmpopts.EquateEmpty()); diff != "" {
   118  				t.Errorf("Detect() diff (-want +got):\n%s", diff)
   119  			}
   120  		})
   121  	}
   122  }
   123  
   124  // TestSecretKeyDetector_TrueNegatives tests for cases where we know the SecretKeyDetector
   125  // will not find a 1Password Secret Key.
   126  func TestSecretKeyDetector_TrueNegatives(t *testing.T) {
   127  	engine, err := veles.NewDetectionEngine([]veles.Detector{onepasswordkeys.NewSecretKeyDetector()})
   128  	if err != nil {
   129  		t.Fatal(err)
   130  	}
   131  	cases := []struct {
   132  		name  string
   133  		input string
   134  		want  []veles.Secret
   135  	}{{
   136  		name:  "empty input",
   137  		input: "",
   138  	}, {
   139  		name:  "wrong prefix",
   140  		input: `A2-XXXXXX-YYYYYY-ZZZZZ-AAAAA-BBBBB-CCCCC`,
   141  	}, {
   142  		name:  "missing prefix dash",
   143  		input: `A3XXXXXX-YYYYYY-ZZZZZ-AAAAA-BBBBB-CCCCC`,
   144  	}, {
   145  		name:  "invalid character in first segment",
   146  		input: `A3-XXXX!X-YYYYYY-ZZZZZ-AAAAA-BBBBB-CCCCC`,
   147  	}, {
   148  		name:  "first segment too short",
   149  		input: `A3-XXXXX-YYYYYY-ZZZZZ-AAAAA-BBBBB-CCCCC`,
   150  	}, {
   151  		name:  "first segment too long",
   152  		input: `A3-XXXXXXX-YYYYYY-ZZZZZ-AAAAA-BBBBB-CCCCC`,
   153  	}, {
   154  		name:  "invalid middle segment length",
   155  		input: `A3-XXXXXX-YYYYY-ZZZZZ-AAAAA-BBBBB-CCCCC`,
   156  	}, {
   157  		name:  "last segment too short",
   158  		input: `A3-XXXXXX-YYYYYY-ZZZZZ-AAAAA-BBBBB-CCCC`,
   159  	}, {
   160  		name:  "lowercase characters",
   161  		input: `a3-xxxxxx-yyyyyy-zzzzz-aaaaa-bbbbb-ccccc`,
   162  	}}
   163  	for _, tc := range cases {
   164  		t.Run(tc.name, func(t *testing.T) {
   165  			got, err := engine.Detect(t.Context(), strings.NewReader(tc.input))
   166  			if err != nil {
   167  				t.Errorf("Detect() error: %v, want nil", err)
   168  			}
   169  			if diff := cmp.Diff(tc.want, got, cmpopts.EquateEmpty()); diff != "" {
   170  				t.Errorf("Detect() diff (-want +got):\n%s", diff)
   171  			}
   172  		})
   173  	}
   174  }
   175  
   176  // TestServiceTokenDetector_TruePositives tests for cases where we know the ServiceTokenDetector
   177  // will find 1Password Service Token/s.
   178  func TestServiceTokenDetector_TruePositives(t *testing.T) {
   179  	engine, err := veles.NewDetectionEngine([]veles.Detector{onepasswordkeys.NewServiceTokenDetector()})
   180  	if err != nil {
   181  		t.Fatal(err)
   182  	}
   183  	cases := []struct {
   184  		name  string
   185  		input string
   186  		want  []veles.Secret
   187  	}{{
   188  		name:  "simple matching string",
   189  		input: testServiceToken,
   190  		want: []veles.Secret{
   191  			onepasswordkeys.OnePasswordServiceToken{Key: testServiceToken},
   192  		},
   193  	}, {
   194  		name:  "match at end of string",
   195  		input: `OP_SERVICE_ACCOUNT_TOKEN=` + testServiceToken,
   196  		want: []veles.Secret{
   197  			onepasswordkeys.OnePasswordServiceToken{Key: testServiceToken},
   198  		},
   199  	}, {
   200  		name:  "match in middle of string",
   201  		input: `OP_SERVICE_ACCOUNT_TOKEN="` + testServiceToken + `"`,
   202  		want: []veles.Secret{
   203  			onepasswordkeys.OnePasswordServiceToken{Key: testServiceToken},
   204  		},
   205  	}, {
   206  		name:  "multiple matches",
   207  		input: testServiceToken + " " + testServiceToken + " " + testServiceToken,
   208  		want: []veles.Secret{
   209  			onepasswordkeys.OnePasswordServiceToken{Key: testServiceToken},
   210  			onepasswordkeys.OnePasswordServiceToken{Key: testServiceToken},
   211  			onepasswordkeys.OnePasswordServiceToken{Key: testServiceToken},
   212  		},
   213  	}, {
   214  		name:  "multiple distinct matches",
   215  		input: testServiceToken + "\n" + testServiceTokenAlt,
   216  		want: []veles.Secret{
   217  			onepasswordkeys.OnePasswordServiceToken{Key: testServiceToken},
   218  			onepasswordkeys.OnePasswordServiceToken{Key: testServiceTokenAlt},
   219  		},
   220  	}, {
   221  		name: "larger_input_containing_token",
   222  		input: fmt.Sprintf(`
   223  :test_service_token: ops_eyJtest
   224  :onepassword_service_token: %s 
   225  		`, testServiceToken),
   226  		want: []veles.Secret{
   227  			onepasswordkeys.OnePasswordServiceToken{Key: testServiceToken},
   228  		},
   229  	}, {
   230  		name:  "token with padding",
   231  		input: testServiceToken + "===",
   232  		want: []veles.Secret{
   233  			onepasswordkeys.OnePasswordServiceToken{Key: testServiceToken + "==="},
   234  		},
   235  	}, {
   236  		name:  "potential match with extra whitespace",
   237  		input: testServiceToken + ` `,
   238  		want: []veles.Secret{
   239  			onepasswordkeys.OnePasswordServiceToken{Key: testServiceToken},
   240  		},
   241  	}}
   242  	for _, tc := range cases {
   243  		t.Run(tc.name, func(t *testing.T) {
   244  			got, err := engine.Detect(t.Context(), strings.NewReader(tc.input))
   245  			if err != nil {
   246  				t.Errorf("Detect() error: %v, want nil", err)
   247  			}
   248  			if diff := cmp.Diff(tc.want, got, cmpopts.EquateEmpty()); diff != "" {
   249  				t.Errorf("Detect() diff (-want +got):\n%s", diff)
   250  			}
   251  		})
   252  	}
   253  }
   254  
   255  // TestServiceTokenDetector_TrueNegatives tests for cases where we know the ServiceTokenDetector
   256  // will not find a 1Password Service Token.
   257  func TestServiceTokenDetector_TrueNegatives(t *testing.T) {
   258  	engine, err := veles.NewDetectionEngine([]veles.Detector{onepasswordkeys.NewServiceTokenDetector()})
   259  	if err != nil {
   260  		t.Fatal(err)
   261  	}
   262  	cases := []struct {
   263  		name  string
   264  		input string
   265  		want  []veles.Secret
   266  	}{{
   267  		name:  "empty input",
   268  		input: "",
   269  	}, {
   270  		name:  "wrong prefix",
   271  		input: `op_eyJxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx`,
   272  	}, {
   273  		name:  "missing underscore in prefix",
   274  		input: `opseyJxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx`,
   275  	}, {
   276  		name:  "token too short",
   277  		input: `ops_eyJabcdefghijklmnopqrstuvwxyz`,
   278  	}, {
   279  		name:  "invalid character in token",
   280  		input: `ops_eyJ` + strings.Repeat("a", 100) + `!` + strings.Repeat("a", 149),
   281  	}, {
   282  		name:  "too much padding",
   283  		input: `ops_eyJ` + strings.Repeat("a", 50) + `====`,
   284  	}}
   285  	for _, tc := range cases {
   286  		t.Run(tc.name, func(t *testing.T) {
   287  			got, err := engine.Detect(t.Context(), strings.NewReader(tc.input))
   288  			if err != nil {
   289  				t.Errorf("Detect() error: %v, want nil", err)
   290  			}
   291  			if diff := cmp.Diff(tc.want, got, cmpopts.EquateEmpty()); diff != "" {
   292  				t.Errorf("Detect() diff (-want +got):\n%s", diff)
   293  			}
   294  		})
   295  	}
   296  }
   297  
   298  // TestRecoveryKeyDetector_TruePositives tests for cases where we know the RecoveryKeyDetector
   299  // will find 1Password Recovery Key/s.
   300  func TestRecoveryKeyDetector_TruePositives(t *testing.T) {
   301  	engine, err := veles.NewDetectionEngine([]veles.Detector{onepasswordkeys.NewRecoveryTokenDetector()})
   302  	if err != nil {
   303  		t.Fatal(err)
   304  	}
   305  	cases := []struct {
   306  		name  string
   307  		input string
   308  		want  []veles.Secret
   309  	}{{
   310  		name:  "simple matching string",
   311  		input: testRecoveryKey,
   312  		want: []veles.Secret{
   313  			onepasswordkeys.OnePasswordRecoveryCode{Key: testRecoveryKey},
   314  		},
   315  	}, {
   316  		name:  "match at end of string",
   317  		input: `OP_RECOVERY_KEY=` + testRecoveryKey,
   318  		want: []veles.Secret{
   319  			onepasswordkeys.OnePasswordRecoveryCode{Key: testRecoveryKey},
   320  		},
   321  	}, {
   322  		name:  "match in middle of string",
   323  		input: `OP_RECOVERY_KEY="` + testRecoveryKey + `"`,
   324  		want: []veles.Secret{
   325  			onepasswordkeys.OnePasswordRecoveryCode{Key: testRecoveryKey},
   326  		},
   327  	}, {
   328  		name:  "multiple matches",
   329  		input: testRecoveryKey + " " + testRecoveryKey + " " + testRecoveryKey,
   330  		want: []veles.Secret{
   331  			onepasswordkeys.OnePasswordRecoveryCode{Key: testRecoveryKey},
   332  			onepasswordkeys.OnePasswordRecoveryCode{Key: testRecoveryKey},
   333  			onepasswordkeys.OnePasswordRecoveryCode{Key: testRecoveryKey},
   334  		},
   335  	}, {
   336  		name:  "multiple distinct matches",
   337  		input: testRecoveryKey + "\n" + testRecoveryKeyAlt,
   338  		want: []veles.Secret{
   339  			onepasswordkeys.OnePasswordRecoveryCode{Key: testRecoveryKey},
   340  			onepasswordkeys.OnePasswordRecoveryCode{Key: testRecoveryKeyAlt},
   341  		},
   342  	}, {
   343  		name: "larger_input_containing_key",
   344  		input: fmt.Sprintf(`
   345  :test_recovery_key: 1PRK-ABCD-EFGH-IJKL
   346  :onepassword_recovery_key: %s 
   347  		`, testRecoveryKey),
   348  		want: []veles.Secret{
   349  			onepasswordkeys.OnePasswordRecoveryCode{Key: testRecoveryKey},
   350  		},
   351  	}, {
   352  		name:  "potential match with extra characters",
   353  		input: testRecoveryKey + `extra`,
   354  		want: []veles.Secret{
   355  			onepasswordkeys.OnePasswordRecoveryCode{Key: testRecoveryKey},
   356  		},
   357  	}}
   358  	for _, tc := range cases {
   359  		t.Run(tc.name, func(t *testing.T) {
   360  			got, err := engine.Detect(t.Context(), strings.NewReader(tc.input))
   361  			if err != nil {
   362  				t.Errorf("Detect() error: %v, want nil", err)
   363  			}
   364  			if diff := cmp.Diff(tc.want, got, cmpopts.EquateEmpty()); diff != "" {
   365  				t.Errorf("Detect() diff (-want +got):\n%s", diff)
   366  			}
   367  		})
   368  	}
   369  }
   370  
   371  // TestRecoveryKeyDetector_TrueNegatives tests for cases where we know the RecoveryKeyDetector
   372  // will not find a 1Password Recovery Key.
   373  func TestRecoveryKeyDetector_TrueNegatives(t *testing.T) {
   374  	engine, err := veles.NewDetectionEngine([]veles.Detector{onepasswordkeys.NewRecoveryTokenDetector()})
   375  	if err != nil {
   376  		t.Fatal(err)
   377  	}
   378  	cases := []struct {
   379  		name  string
   380  		input string
   381  		want  []veles.Secret
   382  	}{{
   383  		name:  "empty input",
   384  		input: "",
   385  	}, {
   386  		name:  "wrong prefix",
   387  		input: `1PRX-ABCD-EFGH-IJKL-MNOP-QRST-UVWX-YZ12-3456-789A-BCDE-FGHI-JKLM-NOPQ`,
   388  	}, {
   389  		name:  "missing prefix dash",
   390  		input: `1PRKABCD-EFGH-IJKL-MNOP-QRST-UVWX-YZ12-3456-789A-BCDE-FGHI-JKLM-NOPQ`,
   391  	}, {
   392  		name:  "too few segments",
   393  		input: `1PRK-ABCD-EFGH-IJKL-MNOP-QRST-UVWX-YZ12-3456-789A-BCDE-FGHI-JKLM`,
   394  	}, {
   395  		name:  "segment too short",
   396  		input: `1PRK-ABC-EFGH-IJKL-MNOP-QRST-UVWX-YZ12-3456-789A-BCDE-FGHI-JKLM-NOPQ`,
   397  	}, {
   398  		name:  "segment too long",
   399  		input: `1PRK-ABCDE-EFGH-IJKL-MNOP-QRST-UVWX-YZ12-3456-789A-BCDE-FGHI-JKLM-NOPQ`,
   400  	}, {
   401  		name:  "invalid character in segment",
   402  		input: `1PRK-AB!D-EFGH-IJKL-MNOP-QRST-UVWX-YZ12-3456-789A-BCDE-FGHI-JKLM-NOPQ`,
   403  	}, {
   404  		name:  "lowercase characters",
   405  		input: `1prk-abcd-efgh-ijkl-mnop-qrst-uvwx-yz12-3456-789a-bcde-fghi-jklm-nopq`,
   406  	}}
   407  	for _, tc := range cases {
   408  		t.Run(tc.name, func(t *testing.T) {
   409  			got, err := engine.Detect(t.Context(), strings.NewReader(tc.input))
   410  			if err != nil {
   411  				t.Errorf("Detect() error: %v, want nil", err)
   412  			}
   413  			if diff := cmp.Diff(tc.want, got, cmpopts.EquateEmpty()); diff != "" {
   414  				t.Errorf("Detect() diff (-want +got):\n%s", diff)
   415  			}
   416  		})
   417  	}
   418  }