github.com/anchore/syft@v1.38.2/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 = 43
    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  		{
    75  			name: "source flags override bom metadata",
    76  			args: []string{
    77  				"scan",
    78  				"--source-name", "custom-name",
    79  				"--source-version", "custom-version",
    80  				"--source-supplier", "custom-supplier",
    81  				"-o", "json", coverageImage},
    82  			assertions: []traitAssertion{
    83  				assertInOutput("custom-name"),
    84  				assertInOutput("custom-version"),
    85  				assertInOutput("custom-supplier"),
    86  				assertSuccessfulReturnCode,
    87  			},
    88  		},
    89  		// I haven't been able to reproduce locally yet, but in CI this has proven to be unstable:
    90  		// For the same commit:
    91  		//   pass: https://github.com/anchore/syft/runs/4611344142?check_suite_focus=true
    92  		//   fail: https://github.com/anchore/syft/runs/4611343586?check_suite_focus=true
    93  		// For the meantime this test will be commented out, but should be added back in as soon as possible.
    94  		//
    95  		// {
    96  		//	name: "regression-survive-bad-binaries",
    97  		//	// this image has all sorts of rich binaries from the clang-13 test suite that should do pretty bad things
    98  		//	// to the go cataloger binary path. We should NEVER let a panic stop the cataloging process for these
    99  		//	// specific cases.
   100  		//
   101  		//	// this is more of an integration test, however, to assert the output we want to see from the application
   102  		//	// a CLI test is much easier.
   103  		//	args: []string{"scan", "-vv", badBinariesImage},
   104  		//	assertions: []traitAssertion{
   105  		//		assertInOutput("could not parse possible go binary"),
   106  		//		assertSuccessfulReturnCode,
   107  		//	},
   108  		// },
   109  		{
   110  			name: "output-env-binding",
   111  			env: map[string]string{
   112  				"SYFT_OUTPUT": "json",
   113  			},
   114  			args: []string{"scan", coverageImage},
   115  			assertions: []traitAssertion{
   116  				assertJsonReport,
   117  				assertSuccessfulReturnCode,
   118  			},
   119  		},
   120  		{
   121  			name: "table-output-flag",
   122  			args: []string{"scan", "-o", "table", coverageImage},
   123  			assertions: []traitAssertion{
   124  				assertTableReport,
   125  				assertSuccessfulReturnCode,
   126  			},
   127  		},
   128  		{
   129  			name: "default-output-flag",
   130  			args: []string{"scan", coverageImage},
   131  			assertions: []traitAssertion{
   132  				assertTableReport,
   133  				assertSuccessfulReturnCode,
   134  			},
   135  		},
   136  		{
   137  			name: "legacy-json-output-flag",
   138  			args: []string{"scan", "-o", "json", coverageImage},
   139  			env: map[string]string{
   140  				"SYFT_FORMAT_JSON_LEGACY": "true",
   141  			},
   142  			assertions: []traitAssertion{
   143  				assertJsonReport,
   144  				assertNotInOutput(`"metadataType":"apk-db-entry"`),
   145  				assertInOutput(`"metadataType":"ApkMetadata"`),
   146  				assertSuccessfulReturnCode,
   147  			},
   148  		},
   149  		{
   150  			name: "squashed-scope-flag",
   151  			args: []string{"scan", "-o", "json", "-s", "squashed", coverageImage},
   152  			assertions: []traitAssertion{
   153  				assertPackageCount(coverageImageSquashedPackageCount),
   154  				assertSuccessfulReturnCode,
   155  			},
   156  		},
   157  		{
   158  			name: "squashed-scope-flag-hidden-packages",
   159  			args: []string{"scan", "-o", "json", "-s", "squashed", hiddenPackagesImage},
   160  			assertions: []traitAssertion{
   161  				assertPackageCount(14),
   162  				// package 1: alpine-baselayout-data@3.6.5-r0 (apk)
   163  				// package 2: alpine-baselayout@3.6.5-r0 (apk)
   164  				// package 3: alpine-keys@2.4-r1 (apk)
   165  				// package 4: apk-tools@2.14.4-r0 (apk)
   166  				// package 5: busybox-binsh@1.36.1-r29 (apk)
   167  				// package 6: busybox@1.36.1-r29 (apk)
   168  				// package 7: ca-certificates-bundle@20240705-r0 (apk)
   169  				// package 8: libcrypto3@3.3.1-r3 (apk)
   170  				// package 9: libssl3@3.3.1-r3 (apk)
   171  				// package 10: musl-utils@1.2.5-r0 (apk)
   172  				// package 11: musl@1.2.5-r0 (apk)
   173  				// package 12: scanelf@1.3.7-r2 (apk)
   174  				// package 13: ssl_client@1.36.1-r29 (apk)
   175  				// package 14: zlib@1.3.1-r1 (apk)
   176  				assertNotInOutput(`"name":"curl"`), // hidden package
   177  				assertSuccessfulReturnCode,
   178  			},
   179  		},
   180  		{
   181  			name: "all-layers-scope-flag",
   182  			args: []string{"scan", "-o", "json", "-s", "all-layers", hiddenPackagesImage},
   183  			assertions: []traitAssertion{
   184  				assertPackageCount(24),
   185  				// package 1: alpine-baselayout-data@3.6.5-r0 (apk)
   186  				// package 2: alpine-baselayout@3.6.5-r0 (apk)
   187  				// package 3: alpine-keys@2.4-r1 (apk)
   188  				// package 4: apk-tools@2.14.4-r0 (apk)
   189  				// package 5: brotli-libs@1.1.0-r2 (apk)
   190  				// package 6: busybox-binsh@1.36.1-r29 (apk)
   191  				// package 7: busybox@1.36.1-r29 (apk)
   192  				// package 8: c-ares@1.28.1-r0 (apk)
   193  				// package 9: ca-certificates-bundle@20240705-r0 (apk)
   194  				// package 10: ca-certificates@20240705-r0 (apk)
   195  				// package 11: curl@8.9.1-r1 (apk)
   196  				// package 12: libcrypto3@3.3.1-r3 (apk)
   197  				// package 13: libcurl@8.9.1-r1 (apk)
   198  				// package 14: libidn2@2.3.7-r0 (apk)
   199  				// package 15: libpsl@0.21.5-r1 (apk)
   200  				// package 16: libssl3@3.3.1-r3 (apk)
   201  				// package 17: libunistring@1.2-r0 (apk)
   202  				// package 18: musl-utils@1.2.5-r0 (apk)
   203  				// package 19: musl@1.2.5-r0 (apk)
   204  				// package 20: nghttp2-libs@1.62.1-r0 (apk)
   205  				// package 21: scanelf@1.3.7-r2 (apk)
   206  				// package 22: ssl_client@1.36.1-r29 (apk)
   207  				// package 23: zlib@1.3.1-r1 (apk)
   208  				// package 24: zstd-libs@1.5.6-r0 (apk)
   209  				assertInOutput("all-layers"),
   210  				assertInOutput(`"name":"curl"`), // hidden package
   211  				assertSuccessfulReturnCode,
   212  			},
   213  		},
   214  		{
   215  			name: "all-layers-scope-flag-by-env",
   216  			args: []string{"scan", "-o", "json", hiddenPackagesImage},
   217  			env: map[string]string{
   218  				"SYFT_SCOPE": "all-layers",
   219  			},
   220  			assertions: []traitAssertion{
   221  				assertPackageCount(24), // packages are now deduplicated for this case
   222  				assertInOutput("all-layers"),
   223  				assertInOutput(`"name":"curl"`), // hidden package
   224  				assertSuccessfulReturnCode,
   225  			},
   226  		},
   227  		{
   228  			// we want to make certain that syft can catalog a single go binary and get a SBOM report that is not empty
   229  			name: "catalog-single-go-binary",
   230  			args: []string{"scan", "-o", "json", getSyftBinaryLocation(t)},
   231  			assertions: []traitAssertion{
   232  				assertJsonReport,
   233  				assertStdoutLengthGreaterThan(1000),
   234  				assertSuccessfulReturnCode,
   235  			},
   236  		},
   237  		{
   238  			name: "catalog-node-js-binary",
   239  			args: []string{"scan", "-o", "json", nodeBinaryImage},
   240  			assertions: []traitAssertion{
   241  				assertJsonReport,
   242  				assertInOutput("node.js"),
   243  				assertSuccessfulReturnCode,
   244  			},
   245  		},
   246  		// TODO: uncomment this test when we can use `syft config`
   247  		//{
   248  		//	// TODO: this could be a unit test
   249  		//	name: "responds-to-package-cataloger-search-options",
   250  		//	args: []string{"--help"},
   251  		//	env: map[string]string{
   252  		//		"SYFT_PACKAGE_SEARCH_UNINDEXED_ARCHIVES": "true",
   253  		//		"SYFT_PACKAGE_SEARCH_INDEXED_ARCHIVES":   "false",
   254  		//	},
   255  		//	assertions: []traitAssertion{
   256  		//		// the application config in the log matches that of what we expect to have been configured. Note:
   257  		//		// we are not testing further wiring of this option, only that the config responds to
   258  		//		// package-cataloger-level options.
   259  		//		assertInOutput("search-unindexed-archives: true"),
   260  		//		assertInOutput("search-indexed-archives: false"),
   261  		//	},
   262  		//},
   263  		{
   264  			name: "platform-option-wired-up",
   265  			args: []string{"scan", "--platform", "arm64", "-o", "json", "registry:busybox:1.31"},
   266  			assertions: []traitAssertion{
   267  				assertInOutput("sha256:1ee006886991ad4689838d3a288e0dd3fd29b70e276622f16b67a8922831a853"), // linux/arm64 image digest
   268  				assertSuccessfulReturnCode,
   269  			},
   270  		},
   271  		{
   272  			name: "json-file-flag",
   273  			args: []string{"scan", "-o", "json", "--file", filepath.Join(tmp, "output-1.json"), coverageImage},
   274  			assertions: []traitAssertion{
   275  				assertSuccessfulReturnCode,
   276  				assertFileOutput(t, filepath.Join(tmp, "output-1.json"),
   277  					assertJsonReport,
   278  				),
   279  			},
   280  		},
   281  		{
   282  			name: "json-output-flag-to-file",
   283  			args: []string{"scan", "-o", fmt.Sprintf("json=%s", filepath.Join(tmp, "output-2.json")), coverageImage},
   284  			assertions: []traitAssertion{
   285  				assertSuccessfulReturnCode,
   286  				assertFileOutput(t, filepath.Join(tmp, "output-2.json"),
   287  					assertJsonReport,
   288  				),
   289  			},
   290  		},
   291  		{
   292  			name: "legacy-catalogers-option",
   293  			// This will detect enable:
   294  			// - python-installed-package-cataloger
   295  			// - python-package-cataloger
   296  			// - ruby-gemspec-cataloger
   297  			// - ruby-installed-gemspec-cataloger
   298  			args: []string{"packages", "-o", "json", "--catalogers", "python,gemspec", coverageImage},
   299  			assertions: []traitAssertion{
   300  				assertInOutput("Flag --catalogers has been deprecated, use: override-default-catalogers and select-catalogers"),
   301  				assertPackageCount(13),
   302  				assertSuccessfulReturnCode,
   303  			},
   304  		},
   305  		{
   306  			name: "select-catalogers-option",
   307  			// This will detect enable:
   308  			// - python-installed-package-cataloger
   309  			// - ruby-installed-gemspec-cataloger
   310  			args: []string{"scan", "-o", "json", "--select-catalogers", "python,gemspec", coverageImage},
   311  			assertions: []traitAssertion{
   312  				assertPackageCount(6),
   313  				assertSuccessfulReturnCode,
   314  			},
   315  		},
   316  		{
   317  			name: "select-no-package-catalogers",
   318  			args: []string{"scan", "-o", "json", "--select-catalogers", "-package", coverageImage},
   319  			assertions: []traitAssertion{
   320  				assertPackageCount(0),
   321  				assertInOutput(`"used":["file-content-cataloger","file-digest-cataloger","file-executable-cataloger","file-metadata-cataloger"]`),
   322  				assertSuccessfulReturnCode,
   323  			},
   324  		},
   325  		{
   326  			name: "override-default-catalogers-option",
   327  			// This will detect enable:
   328  			// - python-installed-package-cataloger
   329  			// - python-package-cataloger
   330  			// - ruby-gemspec-cataloger
   331  			// - ruby-installed-gemspec-cataloger
   332  			args: []string{"packages", "-o", "json", "--override-default-catalogers", "python,gemspec", coverageImage},
   333  			assertions: []traitAssertion{
   334  				assertPackageCount(13),
   335  				assertSuccessfulReturnCode,
   336  			},
   337  		},
   338  		{
   339  			name: "override-default-catalogers-with-files",
   340  			args: []string{"packages", "-o", "json", "--override-default-catalogers", "file", coverageImage},
   341  			assertions: []traitAssertion{
   342  				assertPackageCount(0),
   343  				assertInOutput(`"used":["file-content-cataloger","file-digest-cataloger","file-executable-cataloger","file-metadata-cataloger"]`),
   344  				assertSuccessfulReturnCode,
   345  			},
   346  		},
   347  		{
   348  			name: "new and old cataloger options are mutually exclusive",
   349  			args: []string{"packages", "-o", "json", "--override-default-catalogers", "python", "--catalogers", "gemspec", coverageImage},
   350  			assertions: []traitAssertion{
   351  				assertFailingReturnCode,
   352  			},
   353  		},
   354  		{
   355  			name: "override-default-parallelism",
   356  			args: []string{"scan", "-vvv", "-o", "json", coverageImage},
   357  			env: map[string]string{
   358  				"SYFT_PARALLELISM": "2",
   359  			},
   360  			assertions: []traitAssertion{
   361  				// the application config in the log matches that of what we expect to have been configured.
   362  				assertInOutput(`parallelism: 2`),
   363  				assertPackageCount(coverageImageSquashedPackageCount),
   364  				assertSuccessfulReturnCode,
   365  			},
   366  		},
   367  		{
   368  			name: "default-parallelism",
   369  			args: []string{"scan", "-vvv", "-o", "json", coverageImage},
   370  			assertions: []traitAssertion{
   371  				// the application config in the log matches that of what we expect to have been configured.
   372  				assertInOutput(`parallelism: 0`),
   373  				assertPackageCount(coverageImageSquashedPackageCount),
   374  				assertSuccessfulReturnCode,
   375  			},
   376  		},
   377  		{
   378  			name: "parallelism-flag",
   379  			args: []string{"scan", "-vvv", "--parallelism", "2", "-o", "json", coverageImage},
   380  			assertions: []traitAssertion{
   381  				// the application config in the log matches that of what we expect to have been configured.
   382  				assertInOutput(`parallelism: 2`),
   383  				assertPackageCount(coverageImageSquashedPackageCount),
   384  				assertSuccessfulReturnCode,
   385  			},
   386  		},
   387  		{
   388  			name: "password and key not in config output",
   389  			args: []string{"scan", "-vvv", "-o", "json", coverageImage},
   390  			env: map[string]string{
   391  				"SYFT_ATTEST_PASSWORD": "secret_password",
   392  				"SYFT_ATTEST_KEY":      "secret_key_path",
   393  			},
   394  			assertions: []traitAssertion{
   395  				assertNotInOutput("secret_password"),
   396  				assertNotInOutput("secret_key_path"),
   397  				assertPackageCount(coverageImageSquashedPackageCount),
   398  				assertSuccessfulReturnCode,
   399  			},
   400  		},
   401  		// Testing packages alias //////////////////////////////////////////////
   402  		{
   403  			name: "packages-alias-command-works",
   404  			args: []string{"packages", coverageImage},
   405  			assertions: []traitAssertion{
   406  				assertTableReport,
   407  				assertInOutput("Command \"packages\" is deprecated, use `syft scan` instead"),
   408  				assertSuccessfulReturnCode,
   409  			},
   410  		},
   411  		{
   412  			name: "packages-alias-command--output-flag",
   413  			args: []string{"packages", "-o", "json", coverageImage},
   414  			assertions: []traitAssertion{
   415  				assertJsonReport,
   416  				assertSuccessfulReturnCode,
   417  			},
   418  		},
   419  	}
   420  
   421  	for _, test := range tests {
   422  		t.Run(test.name, func(t *testing.T) {
   423  			cmd, stdout, stderr := runSyft(t, test.env, test.args...)
   424  			for _, traitFn := range test.assertions {
   425  				traitFn(t, stdout, stderr, cmd.ProcessState.ExitCode())
   426  			}
   427  			logOutputOnFailure(t, cmd, stdout, stderr)
   428  		})
   429  	}
   430  }
   431  
   432  func TestRegistryAuth(t *testing.T) {
   433  	host := "localhost:17"
   434  	image := fmt.Sprintf("%s/something:latest", host)
   435  	args := []string{"scan", "-vvv", image, "--from", "registry"}
   436  
   437  	tests := []struct {
   438  		name       string
   439  		args       []string
   440  		env        map[string]string
   441  		assertions []traitAssertion
   442  	}{
   443  		{
   444  			name: "fallback to keychain",
   445  			args: args,
   446  			assertions: []traitAssertion{
   447  				assertInOutput("from registry"),
   448  				assertInOutput(image),
   449  				assertInOutput(fmt.Sprintf("no registry credentials configured for %q, using the default keychain", host)),
   450  			},
   451  		},
   452  		{
   453  			name: "use creds",
   454  			args: args,
   455  			env: map[string]string{
   456  				"SYFT_REGISTRY_AUTH_AUTHORITY": host,
   457  				"SYFT_REGISTRY_AUTH_USERNAME":  "username",
   458  				"SYFT_REGISTRY_AUTH_PASSWORD":  "password",
   459  			},
   460  			assertions: []traitAssertion{
   461  				assertInOutput("from registry"),
   462  				assertInOutput(image),
   463  				assertInOutput(fmt.Sprintf(`using basic auth for registry "%s"`, host)),
   464  			},
   465  		},
   466  		{
   467  			name: "use token",
   468  			args: args,
   469  			env: map[string]string{
   470  				"SYFT_REGISTRY_AUTH_AUTHORITY": host,
   471  				"SYFT_REGISTRY_AUTH_TOKEN":     "my-token",
   472  			},
   473  			assertions: []traitAssertion{
   474  				assertInOutput("from registry"),
   475  				assertInOutput(image),
   476  				assertInOutput(fmt.Sprintf(`using token for registry "%s"`, host)),
   477  			},
   478  		},
   479  		{
   480  			name: "not enough info fallback to keychain",
   481  			args: args,
   482  			env: map[string]string{
   483  				"SYFT_REGISTRY_AUTH_AUTHORITY": host,
   484  			},
   485  			assertions: []traitAssertion{
   486  				assertInOutput("from registry"),
   487  				assertInOutput(image),
   488  				assertInOutput(fmt.Sprintf(`no registry credentials configured for %q, using the default keychain`, host)),
   489  			},
   490  		},
   491  		{
   492  			name: "allows insecure http flag",
   493  			args: args,
   494  			env: map[string]string{
   495  				"SYFT_REGISTRY_INSECURE_USE_HTTP": "true",
   496  			},
   497  			assertions: []traitAssertion{
   498  				assertInOutput("insecure-use-http: true"),
   499  			},
   500  		},
   501  		{
   502  			name: "use tls configuration",
   503  			args: args,
   504  			env: map[string]string{
   505  				"SYFT_REGISTRY_AUTH_TLS_CERT": "place.crt",
   506  				"SYFT_REGISTRY_AUTH_TLS_KEY":  "place.key",
   507  			},
   508  			assertions: []traitAssertion{
   509  				assertInOutput("using custom TLS credentials from"),
   510  			},
   511  		},
   512  	}
   513  
   514  	for _, test := range tests {
   515  		t.Run(test.name, func(t *testing.T) {
   516  			cmd, stdout, stderr := runSyft(t, test.env, test.args...)
   517  			for _, traitAssertionFn := range test.assertions {
   518  				traitAssertionFn(t, stdout, stderr, cmd.ProcessState.ExitCode())
   519  			}
   520  			logOutputOnFailure(t, cmd, stdout, stderr)
   521  		})
   522  	}
   523  }