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 }