github.com/anchore/syft@v1.4.2-0.20240516191711-1bec1fc5d397/test/cli/scan_cmd_test.go (about)

     1  package cli
     2  
     3  import (
     4  	"fmt"
     5  	"path/filepath"
     6  	"testing"
     7  )
     8  
     9  const (
    10  	// this is the number of packages that should be found in the image-pkg-coverage fixture image
    11  	// when analyzed with the squashed scope.
    12  	coverageImageSquashedPackageCount = 29
    13  )
    14  
    15  func TestPackagesCmdFlags(t *testing.T) {
    16  	hiddenPackagesImage := "docker-archive:" + getFixtureImage(t, "image-hidden-packages")
    17  	coverageImage := "docker-archive:" + getFixtureImage(t, "image-pkg-coverage")
    18  	nodeBinaryImage := "docker-archive:" + getFixtureImage(t, "image-node-binary")
    19  	// badBinariesImage := "docker-archive:" + getFixtureImage(t, "image-bad-binaries")
    20  	tmp := t.TempDir() + "/"
    21  
    22  	tests := []struct {
    23  		name       string
    24  		args       []string
    25  		env        map[string]string
    26  		assertions []traitAssertion
    27  	}{
    28  		{
    29  			name: "no-args-shows-help",
    30  			args: []string{"scan"},
    31  			assertions: []traitAssertion{
    32  				assertInOutput("an image/directory argument is required"),              // specific error that should be shown
    33  				assertInOutput("Generate a packaged-based Software Bill Of Materials"), // excerpt from help description
    34  				assertFailingReturnCode,
    35  			},
    36  		},
    37  		{
    38  			name: "json-output-flag",
    39  			args: []string{"scan", "-o", "json", coverageImage},
    40  			assertions: []traitAssertion{
    41  				assertJsonReport,
    42  				assertInOutput(`"metadataType":"apk-db-entry"`),
    43  				assertNotInOutput(`"metadataType":"ApkMetadata"`),
    44  				assertSuccessfulReturnCode,
    45  			},
    46  		},
    47  		{
    48  			name: "quiet-flag-with-logger",
    49  			args: []string{"scan", "-qvv", "-o", "json", coverageImage},
    50  			assertions: []traitAssertion{
    51  				assertJsonReport,
    52  				assertNoStderr,
    53  				assertSuccessfulReturnCode,
    54  			},
    55  		},
    56  		{
    57  			name: "quiet-flag-with-tui",
    58  			args: []string{"scan", "-q", "-o", "json", coverageImage},
    59  			assertions: []traitAssertion{
    60  				assertJsonReport,
    61  				assertNoStderr,
    62  				assertSuccessfulReturnCode,
    63  			},
    64  		},
    65  		{
    66  			name: "multiple-output-flags",
    67  			args: []string{"scan", "-o", "table", "-o", "json=" + tmp + ".tmp/multiple-output-flag-test.json", coverageImage},
    68  			assertions: []traitAssertion{
    69  				assertTableReport,
    70  				assertFileExists(tmp + ".tmp/multiple-output-flag-test.json"),
    71  				assertSuccessfulReturnCode,
    72  			},
    73  		},
    74  		// I haven't been able to reproduce locally yet, but in CI this has proven to be unstable:
    75  		// For the same commit:
    76  		//   pass: https://github.com/anchore/syft/runs/4611344142?check_suite_focus=true
    77  		//   fail: https://github.com/anchore/syft/runs/4611343586?check_suite_focus=true
    78  		// For the meantime this test will be commented out, but should be added back in as soon as possible.
    79  		//
    80  		// {
    81  		//	name: "regression-survive-bad-binaries",
    82  		//	// this image has all sorts of rich binaries from the clang-13 test suite that should do pretty bad things
    83  		//	// to the go cataloger binary path. We should NEVER let a panic stop the cataloging process for these
    84  		//	// specific cases.
    85  		//
    86  		//	// this is more of an integration test, however, to assert the output we want to see from the application
    87  		//	// a CLI test is much easier.
    88  		//	args: []string{"scan", "-vv", badBinariesImage},
    89  		//	assertions: []traitAssertion{
    90  		//		assertInOutput("could not parse possible go binary"),
    91  		//		assertSuccessfulReturnCode,
    92  		//	},
    93  		// },
    94  		{
    95  			name: "output-env-binding",
    96  			env: map[string]string{
    97  				"SYFT_OUTPUT": "json",
    98  			},
    99  			args: []string{"scan", coverageImage},
   100  			assertions: []traitAssertion{
   101  				assertJsonReport,
   102  				assertSuccessfulReturnCode,
   103  			},
   104  		},
   105  		{
   106  			name: "table-output-flag",
   107  			args: []string{"scan", "-o", "table", coverageImage},
   108  			assertions: []traitAssertion{
   109  				assertTableReport,
   110  				assertSuccessfulReturnCode,
   111  			},
   112  		},
   113  		{
   114  			name: "default-output-flag",
   115  			args: []string{"scan", coverageImage},
   116  			assertions: []traitAssertion{
   117  				assertTableReport,
   118  				assertSuccessfulReturnCode,
   119  			},
   120  		},
   121  		{
   122  			name: "legacy-json-output-flag",
   123  			args: []string{"scan", "-o", "json", coverageImage},
   124  			env: map[string]string{
   125  				"SYFT_FORMAT_JSON_LEGACY": "true",
   126  			},
   127  			assertions: []traitAssertion{
   128  				assertJsonReport,
   129  				assertNotInOutput(`"metadataType":"apk-db-entry"`),
   130  				assertInOutput(`"metadataType":"ApkMetadata"`),
   131  				assertSuccessfulReturnCode,
   132  			},
   133  		},
   134  		{
   135  			name: "squashed-scope-flag",
   136  			args: []string{"scan", "-o", "json", "-s", "squashed", coverageImage},
   137  			assertions: []traitAssertion{
   138  				assertPackageCount(coverageImageSquashedPackageCount),
   139  				assertSuccessfulReturnCode,
   140  			},
   141  		},
   142  		{
   143  			name: "squashed-scope-flag-hidden-packages",
   144  			args: []string{"scan", "-o", "json", "-s", "squashed", hiddenPackagesImage},
   145  			assertions: []traitAssertion{
   146  				assertPackageCount(162),
   147  				assertNotInOutput("vsftpd"), // hidden package
   148  				assertSuccessfulReturnCode,
   149  			},
   150  		},
   151  		{
   152  			name: "all-layers-scope-flag",
   153  			args: []string{"scan", "-o", "json", "-s", "all-layers", hiddenPackagesImage},
   154  			assertions: []traitAssertion{
   155  				assertPackageCount(163), // packages are now deduplicated for this case
   156  				assertInOutput("all-layers"),
   157  				assertInOutput("vsftpd"), // hidden package
   158  				assertSuccessfulReturnCode,
   159  			},
   160  		},
   161  		{
   162  			name: "all-layers-scope-flag-by-env",
   163  			args: []string{"scan", "-o", "json", hiddenPackagesImage},
   164  			env: map[string]string{
   165  				"SYFT_SCOPE": "all-layers",
   166  			},
   167  			assertions: []traitAssertion{
   168  				assertPackageCount(163), // packages are now deduplicated for this case
   169  				assertInOutput("all-layers"),
   170  				assertInOutput("vsftpd"), // hidden package
   171  				assertSuccessfulReturnCode,
   172  			},
   173  		},
   174  		{
   175  			// we want to make certain that syft can catalog a single go binary and get a SBOM report that is not empty
   176  			name: "catalog-single-go-binary",
   177  			args: []string{"scan", "-o", "json", getSyftBinaryLocation(t)},
   178  			assertions: []traitAssertion{
   179  				assertJsonReport,
   180  				assertStdoutLengthGreaterThan(1000),
   181  				assertSuccessfulReturnCode,
   182  			},
   183  		},
   184  		{
   185  			name: "catalog-node-js-binary",
   186  			args: []string{"scan", "-o", "json", nodeBinaryImage},
   187  			assertions: []traitAssertion{
   188  				assertJsonReport,
   189  				assertInOutput("node.js"),
   190  				assertSuccessfulReturnCode,
   191  			},
   192  		},
   193  		// TODO: uncomment this test when we can use `syft config`
   194  		//{
   195  		//	// TODO: this could be a unit test
   196  		//	name: "responds-to-package-cataloger-search-options",
   197  		//	args: []string{"--help"},
   198  		//	env: map[string]string{
   199  		//		"SYFT_PACKAGE_SEARCH_UNINDEXED_ARCHIVES": "true",
   200  		//		"SYFT_PACKAGE_SEARCH_INDEXED_ARCHIVES":   "false",
   201  		//	},
   202  		//	assertions: []traitAssertion{
   203  		//		// the application config in the log matches that of what we expect to have been configured. Note:
   204  		//		// we are not testing further wiring of this option, only that the config responds to
   205  		//		// package-cataloger-level options.
   206  		//		assertInOutput("search-unindexed-archives: true"),
   207  		//		assertInOutput("search-indexed-archives: false"),
   208  		//	},
   209  		//},
   210  		{
   211  			name: "platform-option-wired-up",
   212  			args: []string{"scan", "--platform", "arm64", "-o", "json", "registry:busybox:1.31"},
   213  			assertions: []traitAssertion{
   214  				assertInOutput("sha256:1ee006886991ad4689838d3a288e0dd3fd29b70e276622f16b67a8922831a853"), // linux/arm64 image digest
   215  				assertSuccessfulReturnCode,
   216  			},
   217  		},
   218  		{
   219  			name: "json-file-flag",
   220  			args: []string{"scan", "-o", "json", "--file", filepath.Join(tmp, "output-1.json"), coverageImage},
   221  			assertions: []traitAssertion{
   222  				assertSuccessfulReturnCode,
   223  				assertFileOutput(t, filepath.Join(tmp, "output-1.json"),
   224  					assertJsonReport,
   225  				),
   226  			},
   227  		},
   228  		{
   229  			name: "json-output-flag-to-file",
   230  			args: []string{"scan", "-o", fmt.Sprintf("json=%s", filepath.Join(tmp, "output-2.json")), coverageImage},
   231  			assertions: []traitAssertion{
   232  				assertSuccessfulReturnCode,
   233  				assertFileOutput(t, filepath.Join(tmp, "output-2.json"),
   234  					assertJsonReport,
   235  				),
   236  			},
   237  		},
   238  		{
   239  			name: "legacy-catalogers-option",
   240  			// This will detect enable:
   241  			// - python-installed-package-cataloger
   242  			// - python-package-cataloger
   243  			// - ruby-gemspec-cataloger
   244  			// - ruby-installed-gemspec-cataloger
   245  			args: []string{"packages", "-o", "json", "--catalogers", "python,gemspec", coverageImage},
   246  			assertions: []traitAssertion{
   247  				assertInOutput("Flag --catalogers has been deprecated, use: override-default-catalogers and select-catalogers"),
   248  				assertPackageCount(13),
   249  				assertSuccessfulReturnCode,
   250  			},
   251  		},
   252  		{
   253  			name: "select-catalogers-option",
   254  			// This will detect enable:
   255  			// - python-installed-package-cataloger
   256  			// - ruby-installed-gemspec-cataloger
   257  			args: []string{"scan", "-o", "json", "--select-catalogers", "python,gemspec", coverageImage},
   258  			assertions: []traitAssertion{
   259  				assertPackageCount(6),
   260  				assertSuccessfulReturnCode,
   261  			},
   262  		},
   263  		{
   264  			name: "override-default-catalogers-option",
   265  			// This will detect enable:
   266  			// - python-installed-package-cataloger
   267  			// - python-package-cataloger
   268  			// - ruby-gemspec-cataloger
   269  			// - ruby-installed-gemspec-cataloger
   270  			args: []string{"packages", "-o", "json", "--override-default-catalogers", "python,gemspec", coverageImage},
   271  			assertions: []traitAssertion{
   272  				assertPackageCount(13),
   273  				assertSuccessfulReturnCode,
   274  			},
   275  		},
   276  		{
   277  			name: "new and old cataloger options are mutually exclusive",
   278  			args: []string{"packages", "-o", "json", "--override-default-catalogers", "python", "--catalogers", "gemspec", coverageImage},
   279  			assertions: []traitAssertion{
   280  				assertFailingReturnCode,
   281  			},
   282  		},
   283  		{
   284  			name: "override-default-parallelism",
   285  			args: []string{"scan", "-vvv", "-o", "json", coverageImage},
   286  			env: map[string]string{
   287  				"SYFT_PARALLELISM": "2",
   288  			},
   289  			assertions: []traitAssertion{
   290  				// the application config in the log matches that of what we expect to have been configured.
   291  				assertInOutput(`parallelism: 2`),
   292  				assertPackageCount(coverageImageSquashedPackageCount),
   293  				assertSuccessfulReturnCode,
   294  			},
   295  		},
   296  		{
   297  			name: "default-parallelism",
   298  			args: []string{"scan", "-vvv", "-o", "json", coverageImage},
   299  			assertions: []traitAssertion{
   300  				// the application config in the log matches that of what we expect to have been configured.
   301  				assertInOutput(`parallelism: 1`),
   302  				assertPackageCount(coverageImageSquashedPackageCount),
   303  				assertSuccessfulReturnCode,
   304  			},
   305  		},
   306  		{
   307  			name: "password and key not in config output",
   308  			args: []string{"scan", "-vvv", "-o", "json", coverageImage},
   309  			env: map[string]string{
   310  				"SYFT_ATTEST_PASSWORD": "secret_password",
   311  				"SYFT_ATTEST_KEY":      "secret_key_path",
   312  			},
   313  			assertions: []traitAssertion{
   314  				assertNotInOutput("secret_password"),
   315  				assertNotInOutput("secret_key_path"),
   316  				assertPackageCount(coverageImageSquashedPackageCount),
   317  				assertSuccessfulReturnCode,
   318  			},
   319  		},
   320  		// Testing packages alias //////////////////////////////////////////////
   321  		{
   322  			name: "packages-alias-command-works",
   323  			args: []string{"packages", coverageImage},
   324  			assertions: []traitAssertion{
   325  				assertTableReport,
   326  				assertInOutput("Command \"packages\" is deprecated, use `syft scan` instead"),
   327  				assertSuccessfulReturnCode,
   328  			},
   329  		},
   330  		{
   331  			name: "packages-alias-command--output-flag",
   332  			args: []string{"packages", "-o", "json", coverageImage},
   333  			assertions: []traitAssertion{
   334  				assertJsonReport,
   335  				assertSuccessfulReturnCode,
   336  			},
   337  		},
   338  	}
   339  
   340  	for _, test := range tests {
   341  		t.Run(test.name, func(t *testing.T) {
   342  			cmd, stdout, stderr := runSyft(t, test.env, test.args...)
   343  			for _, traitFn := range test.assertions {
   344  				traitFn(t, stdout, stderr, cmd.ProcessState.ExitCode())
   345  			}
   346  			logOutputOnFailure(t, cmd, stdout, stderr)
   347  		})
   348  	}
   349  }
   350  
   351  func TestRegistryAuth(t *testing.T) {
   352  	host := "localhost:17"
   353  	image := fmt.Sprintf("%s/something:latest", host)
   354  	args := []string{"scan", "-vvv", image, "--from", "registry"}
   355  
   356  	tests := []struct {
   357  		name       string
   358  		args       []string
   359  		env        map[string]string
   360  		assertions []traitAssertion
   361  	}{
   362  		{
   363  			name: "fallback to keychain",
   364  			args: args,
   365  			assertions: []traitAssertion{
   366  				assertInOutput("from registry"),
   367  				assertInOutput(image),
   368  				assertInOutput(fmt.Sprintf("no registry credentials configured for %q, using the default keychain", host)),
   369  			},
   370  		},
   371  		{
   372  			name: "use creds",
   373  			args: args,
   374  			env: map[string]string{
   375  				"SYFT_REGISTRY_AUTH_AUTHORITY": host,
   376  				"SYFT_REGISTRY_AUTH_USERNAME":  "username",
   377  				"SYFT_REGISTRY_AUTH_PASSWORD":  "password",
   378  			},
   379  			assertions: []traitAssertion{
   380  				assertInOutput("from registry"),
   381  				assertInOutput(image),
   382  				assertInOutput(fmt.Sprintf(`using basic auth for registry "%s"`, host)),
   383  			},
   384  		},
   385  		{
   386  			name: "use token",
   387  			args: args,
   388  			env: map[string]string{
   389  				"SYFT_REGISTRY_AUTH_AUTHORITY": host,
   390  				"SYFT_REGISTRY_AUTH_TOKEN":     "my-token",
   391  			},
   392  			assertions: []traitAssertion{
   393  				assertInOutput("from registry"),
   394  				assertInOutput(image),
   395  				assertInOutput(fmt.Sprintf(`using token for registry "%s"`, host)),
   396  			},
   397  		},
   398  		{
   399  			name: "not enough info fallback to keychain",
   400  			args: args,
   401  			env: map[string]string{
   402  				"SYFT_REGISTRY_AUTH_AUTHORITY": host,
   403  			},
   404  			assertions: []traitAssertion{
   405  				assertInOutput("from registry"),
   406  				assertInOutput(image),
   407  				assertInOutput(fmt.Sprintf(`no registry credentials configured for %q, using the default keychain`, host)),
   408  			},
   409  		},
   410  		{
   411  			name: "allows insecure http flag",
   412  			args: args,
   413  			env: map[string]string{
   414  				"SYFT_REGISTRY_INSECURE_USE_HTTP": "true",
   415  			},
   416  			assertions: []traitAssertion{
   417  				assertInOutput("insecure-use-http: true"),
   418  			},
   419  		},
   420  		{
   421  			name: "use tls configuration",
   422  			args: args,
   423  			env: map[string]string{
   424  				"SYFT_REGISTRY_AUTH_TLS_CERT": "place.crt",
   425  				"SYFT_REGISTRY_AUTH_TLS_KEY":  "place.key",
   426  			},
   427  			assertions: []traitAssertion{
   428  				assertInOutput("using custom TLS credentials from"),
   429  			},
   430  		},
   431  	}
   432  
   433  	for _, test := range tests {
   434  		t.Run(test.name, func(t *testing.T) {
   435  			cmd, stdout, stderr := runSyft(t, test.env, test.args...)
   436  			for _, traitAssertionFn := range test.assertions {
   437  				traitAssertionFn(t, stdout, stderr, cmd.ProcessState.ExitCode())
   438  			}
   439  			logOutputOnFailure(t, cmd, stdout, stderr)
   440  		})
   441  	}
   442  }