github.com/google/osv-scalibr@v0.4.1/extractor/filesystem/secrets/secrets_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 secrets_test
    16  
    17  import (
    18  	"strings"
    19  	"testing"
    20  
    21  	"github.com/google/go-cmp/cmp"
    22  	"github.com/google/go-cmp/cmp/cmpopts"
    23  	"github.com/google/osv-scalibr/extractor/filesystem"
    24  	"github.com/google/osv-scalibr/extractor/filesystem/secrets"
    25  	"github.com/google/osv-scalibr/extractor/filesystem/simplefileapi"
    26  	"github.com/google/osv-scalibr/inventory"
    27  	"github.com/google/osv-scalibr/veles"
    28  	"github.com/google/osv-scalibr/veles/secrets/gcpsak"
    29  	"github.com/google/osv-scalibr/veles/velestest"
    30  
    31  	scalibrfs "github.com/google/osv-scalibr/fs"
    32  )
    33  
    34  func TestFileRequired(t *testing.T) {
    35  	cases := []struct {
    36  		name string
    37  		path string
    38  		want bool
    39  	}{
    40  		{
    41  			name: "empty",
    42  			path: "",
    43  			want: false,
    44  		},
    45  		{
    46  			name: "accept_JSON",
    47  			path: "foo.json",
    48  			want: true,
    49  		},
    50  		{
    51  			name: "accept_YAML",
    52  			path: "bar.yaml",
    53  			want: true,
    54  		},
    55  		{
    56  			name: "accept_CFG",
    57  			path: "baz.cfg",
    58  			want: true,
    59  		},
    60  		{
    61  			name: "accept_textproto",
    62  			path: "hello.textproto",
    63  			want: true,
    64  		},
    65  		{
    66  			name: "accepts_full_path",
    67  			path: "/foo/bar/baz/credentials.json",
    68  			want: true,
    69  		},
    70  		{
    71  			name: "accepts_last_of_multiple_extensions",
    72  			path: "credentials.enc.json",
    73  			want: true,
    74  		},
    75  		{
    76  			name: "accepts_uppercase",
    77  			path: "credentials.JSON",
    78  			want: true,
    79  		},
    80  		{
    81  			name: "accepts_mixed_case",
    82  			path: "credentials.Json",
    83  			want: true,
    84  		},
    85  		{
    86  			name: "rejects_e.g._PNG",
    87  			path: "image.png",
    88  			want: false,
    89  		},
    90  		{
    91  			name: "rejects_if_not_last_in_multiple_extensions",
    92  			path: "credentials.json.tar.gz",
    93  			want: false,
    94  		},
    95  		{
    96  			name: "rejects_w/o_extension",
    97  			path: "/foo/bar/baz",
    98  			want: false,
    99  		},
   100  	}
   101  	for _, tc := range cases {
   102  		t.Run(tc.name, func(t *testing.T) {
   103  			engine, err := veles.NewDetectionEngine([]veles.Detector{gcpsak.NewDetector()})
   104  			if err != nil {
   105  				t.Fatalf("veles.NewDetectionEngine(gcpsak): %v", err)
   106  			}
   107  			e := secrets.NewWithEngine(engine)
   108  			if got := e.FileRequired(simplefileapi.New(tc.path, nil)); got != tc.want {
   109  				t.Errorf("FileRequired(%q) = %t, want %t", tc.path, got, tc.want)
   110  			}
   111  		})
   112  	}
   113  }
   114  
   115  // TestExtract tests that the Extractor produces the correct output based on the
   116  // Veles library.
   117  // Dedicated tests for specific credentials exist in the Veles library.
   118  func TestExtract(t *testing.T) {
   119  	path := "/foo/bar/baz.json"
   120  	less := func(a, b *inventory.Secret) bool {
   121  		return velestest.LessFakeSecretT(t)(a.Secret, b.Secret)
   122  	}
   123  	cases := []struct {
   124  		name      string
   125  		detectors []veles.Detector
   126  		input     string
   127  		want      []*inventory.Secret
   128  	}{
   129  		{
   130  			name:      "empty input",
   131  			detectors: velestest.FakeDetectors("FOO"),
   132  			input:     "",
   133  			want:      nil,
   134  		},
   135  		{
   136  			name:      "single match",
   137  			detectors: velestest.FakeDetectors("FOO"),
   138  			input:     "Hello, world! FOO BAR BAZ!",
   139  			want: []*inventory.Secret{
   140  				{
   141  					Secret:   velestest.NewFakeStringSecret("FOO"),
   142  					Location: path,
   143  				},
   144  			},
   145  		},
   146  		{
   147  			name:      "multiple matches",
   148  			detectors: velestest.FakeDetectors("FOO"),
   149  			input:     "Hello FOO! FOO BAR BAZ!",
   150  			want: []*inventory.Secret{
   151  				{
   152  					Secret:   velestest.NewFakeStringSecret("FOO"),
   153  					Location: path,
   154  				},
   155  				{
   156  					Secret:   velestest.NewFakeStringSecret("FOO"),
   157  					Location: path,
   158  				},
   159  			},
   160  		},
   161  		{
   162  			name:      "multiple matches from different detectors",
   163  			detectors: velestest.FakeDetectors("FOO", "BAR"),
   164  			input:     "Hello FOO! FOO BAR BAZ!",
   165  			want: []*inventory.Secret{
   166  				{
   167  					Secret:   velestest.NewFakeStringSecret("FOO"),
   168  					Location: path,
   169  				},
   170  				{
   171  					Secret:   velestest.NewFakeStringSecret("FOO"),
   172  					Location: path,
   173  				},
   174  				{
   175  					Secret:   velestest.NewFakeStringSecret("BAR"),
   176  					Location: path,
   177  				},
   178  			},
   179  		},
   180  	}
   181  	for _, tc := range cases {
   182  		t.Run(tc.name, func(t *testing.T) {
   183  			engine, err := veles.NewDetectionEngine(tc.detectors)
   184  			if err != nil {
   185  				t.Fatalf("veles.NewDetectionEngine() err: %v", err)
   186  			}
   187  			e := secrets.NewWithEngine(engine)
   188  			input := &filesystem.ScanInput{
   189  				FS:     scalibrfs.DirFS("."),
   190  				Path:   path,
   191  				Reader: strings.NewReader(tc.input),
   192  			}
   193  			gotInv, err := e.Extract(t.Context(), input)
   194  			if err != nil {
   195  				t.Errorf("Extract() err=%v, want nil", err)
   196  			}
   197  			if len(gotInv.Packages) > 0 || len(gotInv.GenericFindings) > 0 {
   198  				t.Errorf("Extract() got inventory other than secrets: %v", gotInv)
   199  			}
   200  			got := gotInv.Secrets
   201  			if diff := cmp.Diff(tc.want, got, cmpopts.EquateEmpty(), cmpopts.SortSlices(less)); diff != "" {
   202  				t.Errorf("Extract() diff (-want +got):\n%s", diff)
   203  			}
   204  		})
   205  	}
   206  }