github.com/google/osv-scalibr@v0.4.1/binary/cli/cli_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 cli_test
    16  
    17  import (
    18  	"io/fs"
    19  	"os"
    20  	"path/filepath"
    21  	"runtime"
    22  	"strings"
    23  	"testing"
    24  
    25  	"github.com/google/go-cmp/cmp"
    26  	"github.com/google/go-cmp/cmp/cmpopts"
    27  	scalibr "github.com/google/osv-scalibr"
    28  	"github.com/google/osv-scalibr/binary/cli"
    29  	cpb "github.com/google/osv-scalibr/binary/proto/config_go_proto"
    30  	"github.com/google/osv-scalibr/extractor/filesystem/language/golang/gobinary"
    31  	"github.com/google/osv-scalibr/plugin"
    32  	"google.golang.org/protobuf/testing/protocmp"
    33  )
    34  
    35  func TestValidateFlags(t *testing.T) {
    36  	for _, tc := range []struct {
    37  		desc    string
    38  		flags   *cli.Flags
    39  		wantErr error
    40  	}{
    41  		{
    42  			desc: "Valid config",
    43  			flags: &cli.Flags{
    44  				Root:            "/",
    45  				ResultFile:      "result.textproto",
    46  				Output:          []string{"textproto=result2.textproto", "spdx23-yaml=result.spdx.yaml"},
    47  				ExtractorsToRun: []string{"java,python", "javascript"},
    48  				DetectorsToRun:  []string{"weakcredentials,cis"},
    49  				PluginsToRun:    []string{"vex"},
    50  				DirsToSkip:      []string{"path1,path2", "path3"},
    51  				SPDXCreators:    "Tool:SCALIBR,Organization:Google",
    52  			},
    53  			wantErr: nil,
    54  		},
    55  		{
    56  			desc:    "Only --version set",
    57  			flags:   &cli.Flags{PrintVersion: true},
    58  			wantErr: nil,
    59  		},
    60  		{
    61  			desc:    "Either output flag missing",
    62  			flags:   &cli.Flags{Root: "/"},
    63  			wantErr: cmpopts.AnyError,
    64  		}, {
    65  			desc: "Result flag present",
    66  			flags: &cli.Flags{
    67  				Root:       "/",
    68  				ResultFile: "result.textproto",
    69  			},
    70  			wantErr: nil,
    71  		}, {
    72  			desc: "Output flag present",
    73  			flags: &cli.Flags{
    74  				Root:   "/",
    75  				Output: []string{"textproto=result.textproto"},
    76  			},
    77  			wantErr: nil,
    78  		}, {
    79  			desc: "Wrong result extension",
    80  			flags: &cli.Flags{
    81  				Root:       "/",
    82  				ResultFile: "result.png",
    83  			},
    84  			wantErr: cmpopts.AnyError,
    85  		}, {
    86  			desc: "Invalid output format",
    87  			flags: &cli.Flags{
    88  				Root:   "/",
    89  				Output: []string{"invalid"},
    90  			},
    91  			wantErr: cmpopts.AnyError,
    92  		}, {
    93  			desc: "Unknown output format",
    94  			flags: &cli.Flags{
    95  				Root:   "/",
    96  				Output: []string{"unknown=foo.bar"},
    97  			},
    98  			wantErr: cmpopts.AnyError,
    99  		}, {
   100  			desc: "Wrong output extension",
   101  			flags: &cli.Flags{
   102  				Root:   "/",
   103  				Output: []string{"proto=result.png"},
   104  			},
   105  			wantErr: cmpopts.AnyError,
   106  		}, {
   107  			desc: "Invalid extractors",
   108  			flags: &cli.Flags{
   109  				Root:            "/",
   110  				ResultFile:      "result.textproto",
   111  				ExtractorsToRun: []string{",python"},
   112  			},
   113  			wantErr: cmpopts.AnyError,
   114  		},
   115  		{
   116  			desc: "Nonexistent extractors",
   117  			flags: &cli.Flags{
   118  				Root:            "/",
   119  				ResultFile:      "result.textproto",
   120  				ExtractorsToRun: []string{"asdf"},
   121  			},
   122  			wantErr: cmpopts.AnyError,
   123  		},
   124  		{
   125  			desc: "Invalid detectors",
   126  			flags: &cli.Flags{
   127  				Root:           "/",
   128  				ResultFile:     "result.textproto",
   129  				DetectorsToRun: []string{"cve,"},
   130  			},
   131  			wantErr: cmpopts.AnyError,
   132  		},
   133  		{
   134  			desc: "Nonexistent detectors",
   135  			flags: &cli.Flags{
   136  				Root:           "/",
   137  				ResultFile:     "result.textproto",
   138  				DetectorsToRun: []string{"asdf"},
   139  			},
   140  			wantErr: cmpopts.AnyError,
   141  		},
   142  		{
   143  			desc: "Detector with missing extractor dependency (enabled automatically)",
   144  			flags: &cli.Flags{
   145  				Root:            "/",
   146  				ResultFile:      "result.textproto",
   147  				ExtractorsToRun: []string{"python,javascript"},
   148  				DetectorsToRun:  []string{"govulncheck"}, // Needs the Go binary extractor.
   149  			},
   150  			wantErr: nil,
   151  		},
   152  		{
   153  			desc: "Invalid paths to skip",
   154  			flags: &cli.Flags{
   155  				Root:       "/",
   156  				ResultFile: "result.textproto",
   157  				DirsToSkip: []string{"path1,,path3"},
   158  			},
   159  			wantErr: cmpopts.AnyError,
   160  		},
   161  		{
   162  			desc: "Invalid glob for skipping directories",
   163  			flags: &cli.Flags{
   164  				Root:        "/",
   165  				ResultFile:  "result.textproto",
   166  				SkipDirGlob: "[",
   167  			},
   168  			wantErr: cmpopts.AnyError,
   169  		},
   170  		{
   171  			desc: "Invalid SPDX creator format",
   172  			flags: &cli.Flags{
   173  				Root:         "/",
   174  				SPDXCreators: "invalid:creator:format",
   175  			},
   176  			wantErr: cmpopts.AnyError,
   177  		},
   178  		{
   179  			desc: "Image Platform without Remote Image",
   180  			flags: &cli.Flags{
   181  				ImagePlatform: "linux/amd64",
   182  			},
   183  			wantErr: cmpopts.AnyError,
   184  		},
   185  		{
   186  			desc: "Image Platform with Remote Image",
   187  			flags: &cli.Flags{
   188  				RemoteImage:   "docker",
   189  				ImagePlatform: "linux/amd64",
   190  				ResultFile:    "result.textproto",
   191  			},
   192  			wantErr: nil,
   193  		},
   194  		{
   195  			desc: "Remote Image with Image Tarball",
   196  			flags: &cli.Flags{
   197  				RemoteImage:  "docker",
   198  				ImageTarball: "image.tar",
   199  				ResultFile:   "result.textproto",
   200  			},
   201  			wantErr: cmpopts.AnyError,
   202  		},
   203  		{
   204  			desc: "Local Docker Image",
   205  			flags: &cli.Flags{
   206  				ImageLocal: "nginx:latest",
   207  				ResultFile: "result.textproto",
   208  			},
   209  			wantErr: nil,
   210  		},
   211  		{
   212  			desc: "Local Image with Image Tarball",
   213  			flags: &cli.Flags{
   214  				ImageLocal:   "nginx:latest",
   215  				ImageTarball: "image.tar",
   216  				ResultFile:   "result.textproto",
   217  			},
   218  			wantErr: cmpopts.AnyError,
   219  		},
   220  		{
   221  			desc: "valid extractor override",
   222  			flags: &cli.Flags{
   223  				Root:              "/",
   224  				ResultFile:        "result.textproto",
   225  				ExtractorOverride: []string{"python/wheelegg:*.py"},
   226  			},
   227  			wantErr: nil,
   228  		},
   229  		{
   230  			desc: "extractor override invalid format",
   231  			flags: &cli.Flags{
   232  				Root:              "/",
   233  				ResultFile:        "result.textproto",
   234  				ExtractorOverride: []string{"python/wheelegg"},
   235  			},
   236  			wantErr: cmpopts.AnyError,
   237  		},
   238  		{
   239  			desc: "extractor override invalid glob",
   240  			flags: &cli.Flags{
   241  				Root:              "/",
   242  				ResultFile:        "result.textproto",
   243  				ExtractorOverride: []string{"python/wheelegg:["},
   244  			},
   245  			wantErr: cmpopts.AnyError,
   246  		},
   247  	} {
   248  		t.Run(tc.desc, func(t *testing.T) {
   249  			err := cli.ValidateFlags(tc.flags)
   250  			if diff := cmp.Diff(tc.wantErr, err, cmpopts.EquateErrors()); diff != "" {
   251  				t.Errorf("cli.ValidateFlags(%v) error got diff (-want +got):\n%s", tc.flags, diff)
   252  			}
   253  		})
   254  	}
   255  }
   256  
   257  func TestGetScanConfig_ScanRoots(t *testing.T) {
   258  	for _, tc := range []struct {
   259  		desc          string
   260  		flags         map[string]*cli.Flags
   261  		wantScanRoots map[string][]string
   262  	}{
   263  		{
   264  			desc: "Default scan roots",
   265  			flags: map[string]*cli.Flags{
   266  				"darwin":  {},
   267  				"linux":   {},
   268  				"windows": {},
   269  			},
   270  			wantScanRoots: map[string][]string{
   271  				"darwin":  {"/"},
   272  				"linux":   {"/"},
   273  				"windows": {"C:\\"},
   274  			},
   275  		},
   276  		{
   277  			desc: "Scan root are provided and used",
   278  			flags: map[string]*cli.Flags{
   279  				"darwin":  {Root: "/root"},
   280  				"linux":   {Root: "/root"},
   281  				"windows": {Root: "C:\\myroot"},
   282  			},
   283  			wantScanRoots: map[string][]string{
   284  				"darwin":  {"/root"},
   285  				"linux":   {"/root"},
   286  				"windows": {"C:\\myroot"},
   287  			},
   288  		},
   289  		{
   290  			desc: "Scan root is null if image tarball is provided",
   291  			flags: map[string]*cli.Flags{
   292  				"darwin":  {ImageTarball: "image.tar"},
   293  				"linux":   {ImageTarball: "image.tar"},
   294  				"windows": {ImageTarball: "image.tar"},
   295  			},
   296  			wantScanRoots: map[string][]string{
   297  				"darwin":  nil,
   298  				"linux":   nil,
   299  				"windows": nil,
   300  			},
   301  		},
   302  		{
   303  			desc: "Scan root is null if local image is provided",
   304  			flags: map[string]*cli.Flags{
   305  				"darwin":  {ImageLocal: "nginx:latest"},
   306  				"linux":   {ImageLocal: "nginx:latest"},
   307  				"windows": {ImageLocal: "nginx:latest"},
   308  			},
   309  			wantScanRoots: map[string][]string{
   310  				"darwin":  nil,
   311  				"linux":   nil,
   312  				"windows": nil,
   313  			},
   314  		},
   315  	} {
   316  		t.Run(tc.desc, func(t *testing.T) {
   317  			wantScanRoots, ok := tc.wantScanRoots[runtime.GOOS]
   318  			if !ok {
   319  				t.Fatalf("Current system %q not supported, please add test cases", runtime.GOOS)
   320  			}
   321  
   322  			flags, ok := tc.flags[runtime.GOOS]
   323  			if !ok {
   324  				t.Fatalf("Current system %q not supported, please add test cases", runtime.GOOS)
   325  			}
   326  
   327  			cfg, err := flags.GetScanConfig()
   328  			if err != nil {
   329  				t.Errorf("%v.GetScanConfig(): %v", flags, err)
   330  			}
   331  			var gotScanRoots []string
   332  			for _, r := range cfg.ScanRoots {
   333  				gotScanRoots = append(gotScanRoots, r.Path)
   334  			}
   335  			if diff := cmp.Diff(wantScanRoots, gotScanRoots); diff != "" {
   336  				t.Errorf("%v.GetScanConfig() ScanRoots got diff (-want +got):\n%s", flags, diff)
   337  			}
   338  		})
   339  	}
   340  }
   341  
   342  func TestGetScanConfig_NetworkCapabilities(t *testing.T) {
   343  	for _, tc := range []struct {
   344  		desc        string
   345  		flags       cli.Flags
   346  		wantNetwork plugin.Network
   347  	}{
   348  		{
   349  			desc:        "online_if_nothing_set",
   350  			flags:       cli.Flags{},
   351  			wantNetwork: plugin.NetworkOnline,
   352  		},
   353  		{
   354  			desc:        "offline_if_offline_flag_set",
   355  			flags:       cli.Flags{Offline: true},
   356  			wantNetwork: plugin.NetworkOffline,
   357  		},
   358  	} {
   359  		t.Run(tc.desc, func(t *testing.T) {
   360  			cfg, err := tc.flags.GetScanConfig()
   361  			if err != nil {
   362  				t.Errorf("%v.GetScanConfig(): %v", tc.flags, err)
   363  			}
   364  			if tc.wantNetwork != cfg.Capabilities.Network {
   365  				t.Errorf("%v.GetScanConfig(): want %v, got %v", tc.flags, tc.wantNetwork, cfg.Capabilities.Network)
   366  			}
   367  		})
   368  	}
   369  }
   370  
   371  func TestGetScanConfig_DirsToSkip(t *testing.T) {
   372  	for _, tc := range []struct {
   373  		desc           string
   374  		flags          map[string]*cli.Flags
   375  		wantDirsToSkip map[string][]string
   376  	}{
   377  		{
   378  			desc: "Skip default dirs",
   379  			flags: map[string]*cli.Flags{
   380  				"darwin":  {Root: "/"},
   381  				"linux":   {Root: "/"},
   382  				"windows": {Root: "C:\\"},
   383  			},
   384  			wantDirsToSkip: map[string][]string{
   385  				"darwin":  {"/dev", "/proc", "/sys"},
   386  				"linux":   {"/dev", "/proc", "/sys"},
   387  				"windows": {"C:\\Windows"},
   388  			},
   389  		},
   390  		{
   391  			desc: "Skip additional dirs",
   392  			flags: map[string]*cli.Flags{
   393  				"darwin": {
   394  					Root:       "/",
   395  					DirsToSkip: []string{"/boot,/mnt,C:\\boot", "C:\\mnt"},
   396  				},
   397  				"linux": {
   398  					Root:       "/",
   399  					DirsToSkip: []string{"/boot,/mnt", "C:\\boot,C:\\mnt"},
   400  				},
   401  				"windows": {
   402  					Root:       "C:\\",
   403  					DirsToSkip: []string{"C:\\boot,C:\\mnt"},
   404  				},
   405  			},
   406  			wantDirsToSkip: map[string][]string{
   407  				"darwin":  {"/dev", "/proc", "/sys", "/boot", "/mnt"},
   408  				"linux":   {"/dev", "/proc", "/sys", "/boot", "/mnt"},
   409  				"windows": {"C:\\Windows", "C:\\boot", "C:\\mnt"},
   410  			},
   411  		},
   412  		{
   413  			desc: "Ignore paths outside root",
   414  			flags: map[string]*cli.Flags{
   415  				"darwin": {
   416  					Root:       "/root",
   417  					DirsToSkip: []string{"/root/dir1,/dir2"},
   418  				},
   419  				"linux": {
   420  					Root:       "/root",
   421  					DirsToSkip: []string{"/root/dir1,/dir2"},
   422  				},
   423  				"windows": {
   424  					Root:       "C:\\root",
   425  					DirsToSkip: []string{"C:\\root\\dir1,c:\\dir2"},
   426  				},
   427  			},
   428  			wantDirsToSkip: map[string][]string{
   429  				"darwin":  {"/root/dir1"},
   430  				"linux":   {"/root/dir1"},
   431  				"windows": {"C:\\root\\dir1"},
   432  			},
   433  		},
   434  	} {
   435  		t.Run(tc.desc, func(t *testing.T) {
   436  			wantDirsToSkip, ok := tc.wantDirsToSkip[runtime.GOOS]
   437  			if !ok {
   438  				t.Fatalf("Current system %q not supported, please add test cases", runtime.GOOS)
   439  			}
   440  
   441  			flags, ok := tc.flags[runtime.GOOS]
   442  			if !ok {
   443  				t.Fatalf("Current system %q not supported, please add test cases", runtime.GOOS)
   444  			}
   445  
   446  			cfg, err := flags.GetScanConfig()
   447  			if err != nil {
   448  				t.Errorf("%v.GetScanConfig(): %v", flags, err)
   449  			}
   450  			if diff := cmp.Diff(wantDirsToSkip, cfg.DirsToSkip); diff != "" {
   451  				t.Errorf("%v.GetScanConfig() dirsToSkip got diff (-want +got):\n%s", flags, diff)
   452  			}
   453  		})
   454  	}
   455  }
   456  
   457  func TestGetScanConfig_SkipDirRegex(t *testing.T) {
   458  	for _, tc := range []struct {
   459  		desc             string
   460  		flags            *cli.Flags
   461  		wantSkipDirRegex string
   462  		wantNil          bool
   463  	}{
   464  		{
   465  			desc: "simple regex",
   466  			flags: &cli.Flags{
   467  				Root:         "/",
   468  				SkipDirRegex: "asdf.*foo",
   469  			},
   470  			wantSkipDirRegex: "asdf.*foo",
   471  		},
   472  		{
   473  			desc: "no regex",
   474  			flags: &cli.Flags{
   475  				Root: "/",
   476  			},
   477  			wantNil: true,
   478  		},
   479  	} {
   480  		t.Run(tc.desc, func(t *testing.T) {
   481  			cfg, err := tc.flags.GetScanConfig()
   482  			if err != nil {
   483  				t.Errorf("%v.GetScanConfig(): %v", tc.flags, err)
   484  			}
   485  			if tc.wantNil && cfg.SkipDirRegex != nil {
   486  				t.Errorf("%v.GetScanConfig() SkipDirRegex got %q, want nil", tc.flags, cfg.SkipDirRegex)
   487  			}
   488  			if !tc.wantNil && tc.wantSkipDirRegex != cfg.SkipDirRegex.String() {
   489  				t.Errorf("%v.GetScanConfig() SkipDirRegex got %q, want %q", tc.flags, cfg.SkipDirRegex.String(), tc.wantSkipDirRegex)
   490  			}
   491  		})
   492  	}
   493  }
   494  
   495  func TestGetScanConfig_CreatePlugins(t *testing.T) {
   496  	for _, tc := range []struct {
   497  		desc            string
   498  		flags           *cli.Flags
   499  		wantPluginCount int
   500  	}{
   501  		{
   502  			desc: "Create an extractor",
   503  			flags: &cli.Flags{
   504  				PluginsToRun: []string{"python/wheelegg"},
   505  			},
   506  			wantPluginCount: 1,
   507  		},
   508  		{
   509  			desc: "Create an extractor - legacy field",
   510  			flags: &cli.Flags{
   511  				ExtractorsToRun: []string{"python/wheelegg"},
   512  			},
   513  			wantPluginCount: 1,
   514  		},
   515  		{
   516  			desc: "Create a detector - legacy field",
   517  			flags: &cli.Flags{
   518  				PluginsToRun: []string{"cis"},
   519  			},
   520  			wantPluginCount: 1,
   521  		},
   522  		{
   523  			desc: "Create a detector - legacy field",
   524  			flags: &cli.Flags{
   525  				DetectorsToRun: []string{"cis"},
   526  			},
   527  			wantPluginCount: 1,
   528  		},
   529  		{
   530  			desc: "Create an annotator",
   531  			flags: &cli.Flags{
   532  				PluginsToRun: []string{"vex/cachedir"},
   533  			},
   534  			wantPluginCount: 1,
   535  		},
   536  	} {
   537  		t.Run(tc.desc, func(t *testing.T) {
   538  			cfg, err := tc.flags.GetScanConfig()
   539  			if err != nil {
   540  				t.Errorf("%v.GetScanConfig(): %v", tc.flags, err)
   541  			}
   542  			if len(cfg.Plugins) != tc.wantPluginCount {
   543  				t.Errorf("%v.GetScanConfig() want plugin count %d got %d", tc.flags, tc.wantPluginCount, len(cfg.Plugins))
   544  			}
   545  		})
   546  	}
   547  }
   548  
   549  func TestGetScanConfig_PluginConfig(t *testing.T) {
   550  	for _, tc := range []struct {
   551  		desc                   string
   552  		cfgFlags               []string
   553  		wantCFG                *cpb.PluginConfig
   554  		wantMaxFileSizeBytes   int64
   555  		wantVersionFromContent bool
   556  	}{
   557  		{
   558  			desc:     "single_setting_in_one_flag",
   559  			cfgFlags: []string{"max_file_size_bytes:1234"},
   560  			wantCFG: &cpb.PluginConfig{
   561  				MaxFileSizeBytes: 1234,
   562  			},
   563  			wantMaxFileSizeBytes: 1234,
   564  		},
   565  		{
   566  			desc:     "multiple_settings_in_one_flag",
   567  			cfgFlags: []string{"max_file_size_bytes:1234 plugin_specific:{go_binary:{version_from_content:true}}"},
   568  			wantCFG: &cpb.PluginConfig{
   569  				MaxFileSizeBytes: 1234,
   570  				PluginSpecific: []*cpb.PluginSpecificConfig{
   571  					{Config: &cpb.PluginSpecificConfig_GoBinary{GoBinary: &cpb.GoBinaryConfig{VersionFromContent: true}}},
   572  				},
   573  			},
   574  			wantMaxFileSizeBytes:   1234,
   575  			wantVersionFromContent: true,
   576  		},
   577  		{
   578  			desc: "multiple_settings_in_multiple_flags",
   579  			cfgFlags: []string{
   580  				"max_file_size_bytes:1234",
   581  				"plugin_specific:{go_binary:{version_from_content:true}}",
   582  			},
   583  			wantCFG: &cpb.PluginConfig{
   584  				MaxFileSizeBytes: 1234,
   585  				PluginSpecific: []*cpb.PluginSpecificConfig{
   586  					{Config: &cpb.PluginSpecificConfig_GoBinary{GoBinary: &cpb.GoBinaryConfig{VersionFromContent: true}}},
   587  				},
   588  			},
   589  			wantMaxFileSizeBytes:   1234,
   590  			wantVersionFromContent: true,
   591  		},
   592  		{
   593  			desc:     "plugin_specific_config_short_version",
   594  			cfgFlags: []string{"go_binary:{version_from_content:true}"},
   595  			wantCFG: &cpb.PluginConfig{
   596  				PluginSpecific: []*cpb.PluginSpecificConfig{
   597  					{Config: &cpb.PluginSpecificConfig_GoBinary{GoBinary: &cpb.GoBinaryConfig{VersionFromContent: true}}},
   598  				},
   599  			},
   600  			wantVersionFromContent: true,
   601  		},
   602  	} {
   603  		t.Run(tc.desc, func(t *testing.T) {
   604  			flags := &cli.Flags{
   605  				ExtractorsToRun: []string{gobinary.Name},
   606  				PluginCFG:       tc.cfgFlags,
   607  			}
   608  
   609  			scanConfig, err := flags.GetScanConfig()
   610  			if err != nil {
   611  				t.Errorf("%v.GetScanConfig(): %v", flags, err)
   612  			}
   613  
   614  			if diff := cmp.Diff(tc.wantCFG, scanConfig.RequiredPluginConfig, protocmp.Transform()); diff != "" {
   615  				t.Errorf("%v.GetScanConfig() ScanRoots got diff (-want +got):\n%s", flags, diff)
   616  			}
   617  			if len(scanConfig.Plugins) != 1 {
   618  				t.Fatalf("%v.GetScanConfig(): Got %d plugins, want 1", flags, len(scanConfig.Plugins))
   619  			}
   620  
   621  			ext, ok := scanConfig.Plugins[0].(*gobinary.Extractor)
   622  			if !ok {
   623  				t.Fatalf("%v.GetScanConfig(): Got wrong plugin type", flags)
   624  			}
   625  
   626  			maxFileSizeBytes := ext.MaxFileSizeBytes()
   627  			if tc.wantMaxFileSizeBytes != maxFileSizeBytes {
   628  				t.Errorf("%v.GetScanConfig(): Want maxFileSizeBytes %d, got %d", flags, tc.wantMaxFileSizeBytes, maxFileSizeBytes)
   629  			}
   630  
   631  			versionFromContent := ext.VersionFromContent()
   632  			if tc.wantVersionFromContent != versionFromContent {
   633  				t.Errorf("%v.GetScanConfig(): Want versionFromContent %t, got %t", flags, tc.wantVersionFromContent, versionFromContent)
   634  			}
   635  		})
   636  	}
   637  }
   638  
   639  func TestGetScanConfig_MaxFileSize(t *testing.T) {
   640  	for _, tc := range []struct {
   641  		desc            string
   642  		flags           *cli.Flags
   643  		wantMaxFileSize int
   644  	}{
   645  		{
   646  			desc: "max file size unset",
   647  			flags: &cli.Flags{
   648  				MaxFileSize: 0,
   649  			},
   650  			wantMaxFileSize: 0,
   651  		},
   652  		{
   653  			desc: "max file size set",
   654  			flags: &cli.Flags{
   655  				MaxFileSize: 100,
   656  			},
   657  			wantMaxFileSize: 100,
   658  		},
   659  	} {
   660  		t.Run(tc.desc, func(t *testing.T) {
   661  			cfg, err := tc.flags.GetScanConfig()
   662  			if err != nil {
   663  				t.Errorf("%+v.GetScanConfig(): %v", tc.flags, err)
   664  			}
   665  			if cfg.MaxFileSize != tc.wantMaxFileSize {
   666  				t.Errorf("%+v.GetScanConfig() got max file size %d, want %d", tc.flags, cfg.MaxFileSize, tc.wantMaxFileSize)
   667  			}
   668  		})
   669  	}
   670  }
   671  
   672  func TestGetScanConfig_PluginGroups(t *testing.T) {
   673  	for _, tc := range []struct {
   674  		desc            string
   675  		flags           *cli.Flags
   676  		wantPlugins     []string
   677  		dontWantPlugins []string
   678  	}{
   679  		{
   680  			desc:  "default_plugins_if_nothing_is_specified",
   681  			flags: &cli.Flags{},
   682  			wantPlugins: []string{
   683  				"python/wheelegg",
   684  				"windows/dismpatch",
   685  				"vex/cachedir",
   686  			},
   687  			dontWantPlugins: []string{
   688  				// Not default plugins
   689  				"govulncheck/binary",
   690  				"vscode/extensions",
   691  				"baseimage",
   692  			},
   693  		},
   694  		{
   695  			desc: "default_extractors_legacy",
   696  			flags: &cli.Flags{
   697  				ExtractorsToRun: []string{"default"},
   698  			},
   699  			wantPlugins: []string{
   700  				// Filesystem Extractor
   701  				"python/wheelegg",
   702  				// Standalone Extractor
   703  				"windows/dismpatch",
   704  			},
   705  			dontWantPlugins: []string{
   706  				// Not a default Extractor
   707  				"vscode/extensions",
   708  				// Not an Extractor
   709  				"govulncheck/binary",
   710  			},
   711  		},
   712  		{
   713  			desc: "all_extractors_legacy",
   714  			flags: &cli.Flags{
   715  				ExtractorsToRun: []string{"all"},
   716  			},
   717  			wantPlugins: []string{
   718  				// Filesystem Extractor
   719  				"vscode/extensions",
   720  				// Standalone Extractor
   721  				"windows/dismpatch",
   722  			},
   723  			dontWantPlugins: []string{
   724  				// Not an Extractor
   725  				"govulncheck/binary",
   726  			},
   727  		},
   728  		{
   729  			desc: "default_detectors_legacy",
   730  			flags: &cli.Flags{
   731  				DetectorsToRun: []string{"default"},
   732  			},
   733  			// There are no default Detectors at the moment.
   734  			dontWantPlugins: []string{
   735  				// Not a default Detector
   736  				"govulncheck/binary",
   737  				// Not a Detector
   738  				"python/wheelegg",
   739  			},
   740  		},
   741  		{
   742  			desc: "all_detectors_legacy",
   743  			flags: &cli.Flags{
   744  				DetectorsToRun: []string{"all"},
   745  			},
   746  			wantPlugins: []string{
   747  				"govulncheck/binary",
   748  			},
   749  			dontWantPlugins: []string{
   750  				// Not Detectors
   751  				"python/wheelegg",
   752  				"vex/cachedir",
   753  			},
   754  		},
   755  		{
   756  			desc: "all_extractors",
   757  			flags: &cli.Flags{
   758  				PluginsToRun: []string{"extractors/all"},
   759  			},
   760  			wantPlugins: []string{
   761  				// Filesystem Extractor
   762  				"vscode/extensions",
   763  				// Standalone Extractor
   764  				"windows/dismpatch",
   765  			},
   766  			dontWantPlugins: []string{
   767  				// Not an Extractor
   768  				"govulncheck/binary",
   769  			},
   770  		},
   771  		{
   772  			desc: "all_detectors",
   773  			flags: &cli.Flags{
   774  				PluginsToRun: []string{"detectors/all"},
   775  			},
   776  			wantPlugins: []string{
   777  				"govulncheck/binary",
   778  			},
   779  			dontWantPlugins: []string{
   780  				// Not Detectors
   781  				"python/wheelegg",
   782  				"vex/cachedir",
   783  			},
   784  		},
   785  		{
   786  			desc: "all_annotators",
   787  			flags: &cli.Flags{
   788  				PluginsToRun: []string{"annotators/all"},
   789  			},
   790  			wantPlugins: []string{
   791  				"vex/cachedir",
   792  			},
   793  			dontWantPlugins: []string{
   794  				// Not Annotators
   795  				"python/wheelegg",
   796  				"govulncheck/binary",
   797  			},
   798  		},
   799  		{
   800  			desc: "all_enrichers",
   801  			flags: &cli.Flags{
   802  				PluginsToRun: []string{"enrichers/all"},
   803  			},
   804  			wantPlugins: []string{
   805  				"baseimage",
   806  			},
   807  			dontWantPlugins: []string{
   808  				// Not Enrichers
   809  				"python/wheelegg",
   810  				"govulncheck/binary",
   811  				"vex/cachedir",
   812  			},
   813  		},
   814  		{
   815  			desc: "default_plugins",
   816  			flags: &cli.Flags{
   817  				PluginsToRun: []string{"default"},
   818  			},
   819  			wantPlugins: []string{
   820  				"python/wheelegg",
   821  				"windows/dismpatch",
   822  				"vex/cachedir",
   823  			},
   824  			dontWantPlugins: []string{
   825  				// Not default plugins
   826  				"govulncheck/binary",
   827  				"vscode/extensions",
   828  				"baseimage",
   829  			},
   830  		},
   831  		{
   832  			desc: "all_plugins",
   833  			flags: &cli.Flags{
   834  				PluginsToRun: []string{"all"},
   835  			},
   836  			wantPlugins: []string{
   837  				"python/wheelegg",
   838  				"windows/dismpatch",
   839  				"govulncheck/binary",
   840  				"vex/cachedir",
   841  				"baseimage",
   842  			},
   843  		},
   844  	} {
   845  		t.Run(tc.desc, func(t *testing.T) {
   846  			cfg, err := tc.flags.GetScanConfig()
   847  			if err != nil {
   848  				t.Errorf("%+v.GetScanConfig(): %v", tc.flags, err)
   849  			}
   850  			for _, name := range tc.wantPlugins {
   851  				found := false
   852  				for _, p := range cfg.Plugins {
   853  					if p.Name() == name {
   854  						found = true
   855  						break
   856  					}
   857  				}
   858  				if !found {
   859  					t.Errorf("%+v.GetScanConfig() didn't find wanted plugin %q in config", tc.flags, name)
   860  				}
   861  			}
   862  			for _, name := range tc.dontWantPlugins {
   863  				for _, p := range cfg.Plugins {
   864  					if p.Name() == name {
   865  						t.Errorf("%+v.GetScanConfig() found unwanted plugin %q in config", tc.flags, name)
   866  						break
   867  					}
   868  				}
   869  			}
   870  		})
   871  	}
   872  }
   873  
   874  func TestWriteScanResults(t *testing.T) {
   875  	testDirPath := t.TempDir()
   876  	result := &scalibr.ScanResult{
   877  		Version: "1.2.3",
   878  		Status:  &plugin.ScanStatus{Status: plugin.ScanStatusSucceeded},
   879  	}
   880  	for _, tc := range []struct {
   881  		desc              string
   882  		flags             *cli.Flags
   883  		wantFilename      string
   884  		wantContentPrefix string
   885  	}{
   886  		{
   887  			desc: "Create proto using --result flag",
   888  			flags: &cli.Flags{
   889  				ResultFile: filepath.Join(testDirPath, "result.textproto"),
   890  			},
   891  			wantFilename:      "result.textproto",
   892  			wantContentPrefix: "version:",
   893  		},
   894  		{
   895  			desc: "Create proto using --output flag",
   896  			flags: &cli.Flags{
   897  				Output: []string{"textproto=" + filepath.Join(testDirPath, "result2.textproto")},
   898  			},
   899  			wantFilename:      "result2.textproto",
   900  			wantContentPrefix: "version:",
   901  		},
   902  		{
   903  			desc: "Create SPDX 2.3",
   904  			flags: &cli.Flags{
   905  				Output: []string{"spdx23-tag-value=" + filepath.Join(testDirPath, "result.spdx")},
   906  			},
   907  			wantFilename:      "result.spdx",
   908  			wantContentPrefix: "SPDXVersion: SPDX-2.3",
   909  		},
   910  		{
   911  			desc: "Create CDX",
   912  			flags: &cli.Flags{
   913  				Output: []string{"cdx-json=" + filepath.Join(testDirPath, "result.cyclonedx.json")},
   914  			},
   915  			wantFilename:      "result.cyclonedx.json",
   916  			wantContentPrefix: "{\n  \"$schema\": \"http://cyclonedx.org/schema/bom-1.6.schema.json\"",
   917  		},
   918  	} {
   919  		t.Run(tc.desc, func(t *testing.T) {
   920  			if err := tc.flags.WriteScanResults(result); err != nil {
   921  				t.Fatalf("%v.WriteScanResults(%v): %v", tc.flags, result, err)
   922  			}
   923  
   924  			fullPath := filepath.Join(testDirPath, tc.wantFilename)
   925  			got, err := os.ReadFile(fullPath)
   926  			if err != nil {
   927  				t.Fatalf("error while reading %s: %v", fullPath, err)
   928  			}
   929  			gotStr := string(got)
   930  
   931  			if !strings.HasPrefix(gotStr, tc.wantContentPrefix) {
   932  				t.Errorf("%v.WriteScanResults(%v) want file with content prefix %q, got %q", tc.flags, result, tc.wantContentPrefix, gotStr)
   933  			}
   934  		})
   935  	}
   936  }
   937  
   938  type fakeFileAPI struct {
   939  	path string
   940  }
   941  
   942  func (f *fakeFileAPI) Path() string {
   943  	return f.path
   944  }
   945  
   946  func (f *fakeFileAPI) Stat() (fs.FileInfo, error) {
   947  	return nil, nil
   948  }
   949  
   950  func TestGetScanConfig_ExtractorOverride(t *testing.T) {
   951  	tests := []struct {
   952  		name              string
   953  		flags             *cli.Flags
   954  		fileAPI           *fakeFileAPI
   955  		wantExtractorName string
   956  		wantNumExtractors int
   957  		wantErr           error
   958  	}{
   959  		{
   960  			name: "no_override",
   961  			flags: &cli.Flags{
   962  				Root:       "/",
   963  				ResultFile: "result.textproto",
   964  			},
   965  			fileAPI:           &fakeFileAPI{path: "foo.py"},
   966  			wantNumExtractors: 0,
   967  			wantErr:           nil,
   968  		},
   969  		{
   970  			name: "extractor_override_plugin_not_found",
   971  			flags: &cli.Flags{
   972  				Root:              "/",
   973  				ResultFile:        "result.textproto",
   974  				ExtractorOverride: []string{"nonexistent/plugin:*.py"},
   975  				PluginsToRun:      []string{"python/wheelegg"},
   976  			},
   977  			fileAPI:           &fakeFileAPI{path: "foo.py"},
   978  			wantNumExtractors: 0,
   979  			wantErr:           cmpopts.AnyError,
   980  		},
   981  		{
   982  			name: "override_matches",
   983  			flags: &cli.Flags{
   984  				Root:              "/",
   985  				ResultFile:        "result.textproto",
   986  				ExtractorOverride: []string{"python/wheelegg:*.py"},
   987  				PluginsToRun:      []string{"python/wheelegg"},
   988  			},
   989  			fileAPI:           &fakeFileAPI{path: "foo.py"},
   990  			wantExtractorName: "python/wheelegg",
   991  			wantNumExtractors: 1,
   992  			wantErr:           nil,
   993  		},
   994  		{
   995  			name: "override_does_not_match",
   996  			flags: &cli.Flags{
   997  				Root:              "/",
   998  				ResultFile:        "result.textproto",
   999  				ExtractorOverride: []string{"python/wheelegg:*.py"},
  1000  				PluginsToRun:      []string{"python/wheelegg"},
  1001  			},
  1002  			fileAPI:           &fakeFileAPI{path: "abc/efg/foo.go"},
  1003  			wantNumExtractors: 0,
  1004  			wantErr:           nil,
  1005  		},
  1006  	}
  1007  
  1008  	for _, tt := range tests {
  1009  		t.Run(tt.name, func(t *testing.T) {
  1010  			cfg, err := tt.flags.GetScanConfig()
  1011  			if diff := cmp.Diff(tt.wantErr, err, cmpopts.EquateErrors()); diff != "" {
  1012  				t.Fatalf("GetScanConfig() error got diff (-want +got):\n%s", diff)
  1013  			}
  1014  
  1015  			// If an error was expected, the rest of the checks are not necessary.
  1016  			if tt.wantErr != nil {
  1017  				return
  1018  			}
  1019  
  1020  			if cfg.ExtractorOverride == nil && tt.wantNumExtractors > 0 {
  1021  				t.Fatalf("ExtractorOverride is nil, want non-nil")
  1022  			}
  1023  			if cfg.ExtractorOverride != nil {
  1024  				extractors := cfg.ExtractorOverride(tt.fileAPI)
  1025  				if len(extractors) != tt.wantNumExtractors {
  1026  					t.Fatalf("ExtractorOverride() returned %d extractors, want %d", len(extractors), tt.wantNumExtractors)
  1027  				}
  1028  				if tt.wantNumExtractors == 1 && extractors[0].Name() != tt.wantExtractorName {
  1029  					t.Errorf("ExtractorOverride() returned extractor %q, want %q", extractors[0].Name(), tt.wantExtractorName)
  1030  				}
  1031  			}
  1032  		})
  1033  	}
  1034  }