github.com/google/osv-scalibr@v0.4.1/veles/secrets/hashicorpvault/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  package hashicorpvault
    16  
    17  import (
    18  	"strings"
    19  	"testing"
    20  
    21  	"github.com/google/go-cmp/cmp"
    22  	"github.com/google/osv-scalibr/veles"
    23  )
    24  
    25  func TestNewTokenDetector_Detect(t *testing.T) {
    26  	tests := []struct {
    27  		name     string
    28  		input    string
    29  		expected []veles.Secret
    30  	}{
    31  		{
    32  			name:  "hvs token",
    33  			input: "VAULT_TOKEN=hvs.CAESIB8KI2QJk0ePUYdOQXaxl0",
    34  			expected: []veles.Secret{
    35  				Token{Token: "hvs.CAESIB8KI2QJk0ePUYdOQXaxl0"},
    36  			},
    37  		},
    38  		{
    39  			name:  "hvb token",
    40  			input: "export VAULT_TOKEN=hvb.AAAAAQJz0zBvWUNOIKTkDhYX",
    41  			expected: []veles.Secret{
    42  				Token{Token: "hvb.AAAAAQJz0zBvWUNOIKTkDhYX"},
    43  			},
    44  		},
    45  		{
    46  			name:  "multiple tokens",
    47  			input: "hvs.CAESIB8KI2QJk0ePUYdOQXaxl0 and hvb.AAAAAQJz0zBvWUNOIKTkDhYX",
    48  			expected: []veles.Secret{
    49  				Token{Token: "hvs.CAESIB8KI2QJk0ePUYdOQXaxl0"},
    50  				Token{Token: "hvb.AAAAAQJz0zBvWUNOIKTkDhYX"},
    51  			},
    52  		},
    53  		{
    54  			name:  "long token",
    55  			input: "hvs.CAESIDOKRphWTHBGVCFxpRF0iKHJsQUF6aMSCxdGH6_7n8MbGicKImh2cy5raWJjQjdrcmpYOEoxanUza2ljZGJhYwkEGPDt4a4",
    56  			expected: []veles.Secret{
    57  				Token{Token: "hvs.CAESIDOKRphWTHBGVCFxpRF0iKHJsQUF6aMSCxdGH6_7n8MbGicKImh2cy5raWJjQjdrcmpYOEoxanUza2ljZGJhYwkEGPDt4a4"},
    58  			},
    59  		},
    60  		{
    61  			name:     "no token",
    62  			input:    "some random text without vault tokens",
    63  			expected: nil,
    64  		},
    65  		{
    66  			name:     "invalid prefix",
    67  			input:    "hvx.CAESIB8KI2QJk0ePUYdOQXaxl0",
    68  			expected: nil,
    69  		},
    70  		{
    71  			name:     "too short",
    72  			input:    "hvs.short",
    73  			expected: nil,
    74  		},
    75  	}
    76  
    77  	engine, err := veles.NewDetectionEngine([]veles.Detector{NewTokenDetector()})
    78  	if err != nil {
    79  		t.Fatalf("Failed to create detection engine: %v", err)
    80  	}
    81  
    82  	for _, test := range tests {
    83  		t.Run(test.name, func(t *testing.T) {
    84  			reader := strings.NewReader(test.input)
    85  			secrets, err := engine.Detect(t.Context(), reader)
    86  			if err != nil {
    87  				t.Fatalf("Detect() returned error: %v", err)
    88  			}
    89  			if diff := cmp.Diff(test.expected, secrets); diff != "" {
    90  				t.Errorf("Detect() returned unexpected result (-expected +got):\n%s", diff)
    91  			}
    92  		})
    93  	}
    94  }
    95  
    96  func TestNewAppRoleDetector_Detect(t *testing.T) {
    97  	tests := []struct {
    98  		name     string
    99  		input    string
   100  		expected []veles.Secret
   101  	}{
   102  		{
   103  			name:  "single UUID with ROLE_ID context",
   104  			input: "ROLE_ID=12345678-1234-1234-1234-123456789012",
   105  			expected: []veles.Secret{
   106  				AppRoleCredentials{RoleID: "12345678-1234-1234-1234-123456789012"},
   107  			},
   108  		},
   109  		{
   110  			name:  "context-aware credential pair",
   111  			input: "role_id: 87654321-4321-4321-4321-210987654321\nsecret_id: 11111111-2222-3333-4444-555555555555",
   112  			expected: []veles.Secret{
   113  				AppRoleCredentials{
   114  					RoleID:   "87654321-4321-4321-4321-210987654321",
   115  					SecretID: "11111111-2222-3333-4444-555555555555",
   116  				},
   117  			},
   118  		},
   119  		{
   120  			name:  "standalone role_id with context",
   121  			input: "role_id: 87654321-4321-4321-4321-210987654321",
   122  			expected: []veles.Secret{
   123  				AppRoleCredentials{RoleID: "87654321-4321-4321-4321-210987654321"},
   124  			},
   125  		},
   126  		{
   127  			name:  "standalone secret_id with context",
   128  			input: "secret_id: 11111111-2222-3333-4444-555555555555",
   129  			expected: []veles.Secret{
   130  				AppRoleCredentials{SecretID: "11111111-2222-3333-4444-555555555555"},
   131  			},
   132  		},
   133  		{
   134  			name:  "UUID without context",
   135  			input: "some random UUID: 12345678-1234-1234-1234-123456789012 in text",
   136  			expected: []veles.Secret{
   137  				AppRoleCredentials{ID: "12345678-1234-1234-1234-123456789012"},
   138  			},
   139  		},
   140  		{
   141  			name:     "mixed case UUID with invalid hex chars",
   142  			input:    "ROLE_ID=12345678-ABCD-1234-EFGH-123456789012",
   143  			expected: nil, // G and H are not valid hex characters
   144  		},
   145  		{
   146  			name:     "no UUID",
   147  			input:    "some random text without UUIDs",
   148  			expected: nil,
   149  		},
   150  		{
   151  			name:     "invalid UUID format",
   152  			input:    "12345678-1234-1234-1234-12345678901", // too short
   153  			expected: nil,
   154  		},
   155  		{
   156  			name:  "invalid UUID format with extra chars",
   157  			input: "12345678-1234-1234-1234-1234567890123", // too long
   158  			expected: []veles.Secret{
   159  				AppRoleCredentials{ID: "12345678-1234-1234-1234-123456789012"},
   160  			}, // The regex will match the valid portion
   161  		},
   162  		{
   163  			name:     "invalid UUID format missing hyphens",
   164  			input:    "1234567812341234123412345678901",
   165  			expected: nil,
   166  		},
   167  	}
   168  
   169  	engine, err := veles.NewDetectionEngine([]veles.Detector{NewAppRoleDetector()})
   170  	if err != nil {
   171  		t.Fatalf("Failed to create detection engine: %v", err)
   172  	}
   173  
   174  	for _, test := range tests {
   175  		t.Run(test.name, func(t *testing.T) {
   176  			reader := strings.NewReader(test.input)
   177  			secrets, err := engine.Detect(t.Context(), reader)
   178  			if err != nil {
   179  				t.Fatalf("Detect() returned error: %v", err)
   180  			}
   181  			if diff := cmp.Diff(test.expected, secrets); diff != "" {
   182  				t.Errorf("Detect() returned unexpected result (-expected +got):\n%s", diff)
   183  			}
   184  		})
   185  	}
   186  }
   187  
   188  func TestDetector_LargeInput(t *testing.T) {
   189  	// Test that detector can handle large inputs without issues
   190  	largeInput := strings.Repeat("some random text ", 10000) + "hvs.CAESIB8KI2QJk0ePUYdOQXaxl0"
   191  
   192  	engine, err := veles.NewDetectionEngine([]veles.Detector{NewTokenDetector()})
   193  	if err != nil {
   194  		t.Fatalf("Failed to create detection engine: %v", err)
   195  	}
   196  
   197  	reader := strings.NewReader(largeInput)
   198  	secrets, err := engine.Detect(t.Context(), reader)
   199  	if err != nil {
   200  		t.Fatalf("Detect() returned error: %v", err)
   201  	}
   202  
   203  	expected := []veles.Secret{
   204  		Token{Token: "hvs.CAESIB8KI2QJk0ePUYdOQXaxl0"},
   205  	}
   206  
   207  	if diff := cmp.Diff(expected, secrets); diff != "" {
   208  		t.Errorf("Detect() returned unexpected result (-expected +got):\n%s", diff)
   209  	}
   210  }
   211  
   212  func TestDetector_EmptyInput(t *testing.T) {
   213  	engine, err := veles.NewDetectionEngine([]veles.Detector{NewTokenDetector()})
   214  	if err != nil {
   215  		t.Fatalf("Failed to create detection engine: %v", err)
   216  	}
   217  
   218  	reader := strings.NewReader("")
   219  	secrets, err := engine.Detect(t.Context(), reader)
   220  	if err != nil {
   221  		t.Fatalf("Detect() returned error: %v", err)
   222  	}
   223  
   224  	if len(secrets) != 0 {
   225  		t.Errorf("Expected no secrets from empty input, got %d secrets", len(secrets))
   226  	}
   227  }
   228  
   229  // Note: TestDetector_ErrorReading was removed because the DetectionEngine
   230  // handles reader errors at a different level than individual detectors.
   231  
   232  func TestDetect_IncorrectFormat(t *testing.T) {
   233  	tests := []struct {
   234  		name  string
   235  		input string
   236  	}{
   237  		{
   238  			name:  "invalid token prefix",
   239  			input: "hvx.CAESIB8KI2QJk0ePUYdOQXaxl0",
   240  		},
   241  		{
   242  			name:  "token too short",
   243  			input: "hvs.ABC",
   244  		},
   245  		{
   246  			name:  "wrong separator",
   247  			input: "hvs-CAESIB8KI2QJk0ePUYdOQXaxl0",
   248  		},
   249  		{
   250  			name:  "invalid UUID format - missing hyphens",
   251  			input: "role_id: 12345678123412341234123456789012",
   252  		},
   253  		{
   254  			name:  "invalid UUID format - wrong length",
   255  			input: "secret_id: 12345678-1234-1234-1234-12345678901",
   256  		},
   257  		{
   258  			name:  "invalid UUID format - invalid hex characters",
   259  			input: "role_id: 12345678-1234-1234-1234-12345678901G",
   260  		},
   261  		{
   262  			name:  "malformed token with valid prefix",
   263  			input: "hvs.",
   264  		},
   265  		{
   266  			name:  "legacy token too short",
   267  			input: "s.ABC",
   268  		},
   269  		{
   270  			name:  "mixed valid and invalid",
   271  			input: "hvx.invalid and role_id: invalid-uuid-format",
   272  		},
   273  	}
   274  
   275  	// Test both token and AppRole detectors
   276  	detectors := []struct {
   277  		name     string
   278  		detector veles.Detector
   279  	}{
   280  		{"TokenDetector", NewTokenDetector()},
   281  		{"AppRoleDetector", NewAppRoleDetector()},
   282  	}
   283  
   284  	for _, detectorTest := range detectors {
   285  		t.Run(detectorTest.name, func(t *testing.T) {
   286  			engine, err := veles.NewDetectionEngine([]veles.Detector{detectorTest.detector})
   287  			if err != nil {
   288  				t.Fatalf("Failed to create detection engine: %v", err)
   289  			}
   290  
   291  			for _, test := range tests {
   292  				t.Run(test.name, func(t *testing.T) {
   293  					reader := strings.NewReader(test.input)
   294  					secrets, err := engine.Detect(t.Context(), reader)
   295  					if err != nil {
   296  						t.Fatalf("Detect() returned error: %v", err)
   297  					}
   298  
   299  					// All these inputs should produce no valid secrets
   300  					if len(secrets) != 0 {
   301  						t.Errorf("Expected no secrets for invalid format '%s', but got %d secrets: %v",
   302  							test.input, len(secrets), secrets)
   303  					}
   304  				})
   305  			}
   306  		})
   307  	}
   308  }