github.com/lineaje-labs/syft@v0.98.1-0.20231227153149-9e393f60ff1b/test/cli/packages_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 = 25
    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{"packages"},
    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{"packages", "-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{"packages", "-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{"packages", "-q", "-o", "json", coverageImage},
    59  			assertions: []traitAssertion{
    60  				assertJsonReport,
    61  				assertNoStderr,
    62  				assertSuccessfulReturnCode,
    63  			},
    64  		},
    65  		{
    66  			name: "multiple-output-flags",
    67  			args: []string{"packages", "-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{"packages", "-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{"packages", coverageImage},
   100  			assertions: []traitAssertion{
   101  				assertJsonReport,
   102  				assertSuccessfulReturnCode,
   103  			},
   104  		},
   105  		{
   106  			name: "table-output-flag",
   107  			args: []string{"packages", "-o", "table", coverageImage},
   108  			assertions: []traitAssertion{
   109  				assertTableReport,
   110  				assertSuccessfulReturnCode,
   111  			},
   112  		},
   113  		{
   114  			name: "default-output-flag",
   115  			args: []string{"packages", coverageImage},
   116  			assertions: []traitAssertion{
   117  				assertTableReport,
   118  				assertSuccessfulReturnCode,
   119  			},
   120  		},
   121  		{
   122  			name: "legacy-json-output-flag",
   123  			args: []string{"packages", "-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{"packages", "-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{"packages", "-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{"packages", "-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{"packages", "-o", "json", hiddenPackagesImage},
   164  			env: map[string]string{
   165  				"SYFT_PACKAGE_CATALOGER_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{"packages", "-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{"packages", "-o", "json", nodeBinaryImage},
   187  			assertions: []traitAssertion{
   188  				assertJsonReport,
   189  				assertInOutput("node.js"),
   190  				assertSuccessfulReturnCode,
   191  			},
   192  		},
   193  		{
   194  			name: "responds-to-package-cataloger-search-options",
   195  			args: []string{"--help"},
   196  			env: map[string]string{
   197  				"SYFT_PACKAGE_SEARCH_UNINDEXED_ARCHIVES": "true",
   198  				"SYFT_PACKAGE_SEARCH_INDEXED_ARCHIVES":   "false",
   199  			},
   200  			assertions: []traitAssertion{
   201  				// the application config in the log matches that of what we expect to have been configured. Note:
   202  				// we are not testing further wiring of this option, only that the config responds to
   203  				// package-cataloger-level options.
   204  				assertInOutput("search-unindexed-archives: true"),
   205  				assertInOutput("search-indexed-archives: false"),
   206  			},
   207  		},
   208  		{
   209  			name: "platform-option-wired-up",
   210  			args: []string{"packages", "--platform", "arm64", "-o", "json", "registry:busybox:1.31"},
   211  			assertions: []traitAssertion{
   212  				assertInOutput("sha256:1ee006886991ad4689838d3a288e0dd3fd29b70e276622f16b67a8922831a853"), // linux/arm64 image digest
   213  				assertSuccessfulReturnCode,
   214  			},
   215  		},
   216  		{
   217  			name: "json-file-flag",
   218  			args: []string{"packages", "-o", "json", "--file", filepath.Join(tmp, "output-1.json"), coverageImage},
   219  			assertions: []traitAssertion{
   220  				assertSuccessfulReturnCode,
   221  				assertFileOutput(t, filepath.Join(tmp, "output-1.json"),
   222  					assertJsonReport,
   223  				),
   224  			},
   225  		},
   226  		{
   227  			name: "json-output-flag-to-file",
   228  			args: []string{"packages", "-o", fmt.Sprintf("json=%s", filepath.Join(tmp, "output-2.json")), coverageImage},
   229  			assertions: []traitAssertion{
   230  				assertSuccessfulReturnCode,
   231  				assertFileOutput(t, filepath.Join(tmp, "output-2.json"),
   232  					assertJsonReport,
   233  				),
   234  			},
   235  		},
   236  		{
   237  			name: "catalogers-option",
   238  			// This will detect enable python-package-cataloger, python-installed-package-cataloger and ruby-gemspec cataloger
   239  			args: []string{"packages", "-o", "json", "--catalogers", "python,ruby-gemspec", coverageImage},
   240  			assertions: []traitAssertion{
   241  				assertPackageCount(13),
   242  				assertSuccessfulReturnCode,
   243  			},
   244  		},
   245  		{
   246  			name: "override-default-parallelism",
   247  			args: []string{"packages", "-vvv", "-o", "json", coverageImage},
   248  			env: map[string]string{
   249  				"SYFT_PARALLELISM": "2",
   250  			},
   251  			assertions: []traitAssertion{
   252  				// the application config in the log matches that of what we expect to have been configured.
   253  				assertInOutput("parallelism: 2"),
   254  				assertInOutput("parallelism=2"),
   255  				assertPackageCount(coverageImageSquashedPackageCount),
   256  				assertSuccessfulReturnCode,
   257  			},
   258  		},
   259  		{
   260  			name: "default-parallelism",
   261  			args: []string{"packages", "-vvv", "-o", "json", coverageImage},
   262  			assertions: []traitAssertion{
   263  				// the application config in the log matches that of what we expect to have been configured.
   264  				assertInOutput("parallelism: 1"),
   265  				assertInOutput("parallelism=1"),
   266  				assertPackageCount(coverageImageSquashedPackageCount),
   267  				assertSuccessfulReturnCode,
   268  			},
   269  		},
   270  		{
   271  			name: "password and key not in config output",
   272  			args: []string{"packages", "-vvv", "-o", "json", coverageImage},
   273  			env: map[string]string{
   274  				"SYFT_ATTEST_PASSWORD": "secret_password",
   275  				"SYFT_ATTEST_KEY":      "secret_key_path",
   276  			},
   277  			assertions: []traitAssertion{
   278  				assertNotInOutput("secret_password"),
   279  				assertNotInOutput("secret_key_path"),
   280  				assertPackageCount(coverageImageSquashedPackageCount),
   281  				assertSuccessfulReturnCode,
   282  			},
   283  		},
   284  	}
   285  
   286  	for _, test := range tests {
   287  		t.Run(test.name, func(t *testing.T) {
   288  			cmd, stdout, stderr := runSyft(t, test.env, test.args...)
   289  			for _, traitFn := range test.assertions {
   290  				traitFn(t, stdout, stderr, cmd.ProcessState.ExitCode())
   291  			}
   292  			logOutputOnFailure(t, cmd, stdout, stderr)
   293  		})
   294  	}
   295  }
   296  
   297  func TestRegistryAuth(t *testing.T) {
   298  	host := "localhost:17"
   299  	image := fmt.Sprintf("%s/something:latest", host)
   300  	args := []string{"packages", "-vvv", fmt.Sprintf("registry:%s", image)}
   301  
   302  	tests := []struct {
   303  		name       string
   304  		args       []string
   305  		env        map[string]string
   306  		assertions []traitAssertion
   307  	}{
   308  		{
   309  			name: "fallback to keychain",
   310  			args: args,
   311  			assertions: []traitAssertion{
   312  				assertInOutput("source=OciRegistry"),
   313  				assertInOutput(image),
   314  				assertInOutput(fmt.Sprintf("no registry credentials configured for %q, using the default keychain", host)),
   315  			},
   316  		},
   317  		{
   318  			name: "use creds",
   319  			args: args,
   320  			env: map[string]string{
   321  				"SYFT_REGISTRY_AUTH_AUTHORITY": host,
   322  				"SYFT_REGISTRY_AUTH_USERNAME":  "username",
   323  				"SYFT_REGISTRY_AUTH_PASSWORD":  "password",
   324  			},
   325  			assertions: []traitAssertion{
   326  				assertInOutput("source=OciRegistry"),
   327  				assertInOutput(image),
   328  				assertInOutput(fmt.Sprintf(`using basic auth for registry "%s"`, host)),
   329  			},
   330  		},
   331  		{
   332  			name: "use token",
   333  			args: args,
   334  			env: map[string]string{
   335  				"SYFT_REGISTRY_AUTH_AUTHORITY": host,
   336  				"SYFT_REGISTRY_AUTH_TOKEN":     "my-token",
   337  			},
   338  			assertions: []traitAssertion{
   339  				assertInOutput("source=OciRegistry"),
   340  				assertInOutput(image),
   341  				assertInOutput(fmt.Sprintf(`using token for registry "%s"`, host)),
   342  			},
   343  		},
   344  		{
   345  			name: "not enough info fallback to keychain",
   346  			args: args,
   347  			env: map[string]string{
   348  				"SYFT_REGISTRY_AUTH_AUTHORITY": host,
   349  			},
   350  			assertions: []traitAssertion{
   351  				assertInOutput("source=OciRegistry"),
   352  				assertInOutput(image),
   353  				assertInOutput(fmt.Sprintf(`no registry credentials configured for %q, using the default keychain`, host)),
   354  			},
   355  		},
   356  		{
   357  			name: "allows insecure http flag",
   358  			args: args,
   359  			env: map[string]string{
   360  				"SYFT_REGISTRY_INSECURE_USE_HTTP": "true",
   361  			},
   362  			assertions: []traitAssertion{
   363  				assertInOutput("insecure-use-http: true"),
   364  			},
   365  		},
   366  		{
   367  			name: "use tls configuration",
   368  			args: args,
   369  			env: map[string]string{
   370  				"SYFT_REGISTRY_AUTH_TLS_CERT": "place.crt",
   371  				"SYFT_REGISTRY_AUTH_TLS_KEY":  "place.key",
   372  			},
   373  			assertions: []traitAssertion{
   374  				assertInOutput("using custom TLS credentials from"),
   375  			},
   376  		},
   377  	}
   378  
   379  	for _, test := range tests {
   380  		t.Run(test.name, func(t *testing.T) {
   381  			cmd, stdout, stderr := runSyft(t, test.env, test.args...)
   382  			for _, traitAssertionFn := range test.assertions {
   383  				traitAssertionFn(t, stdout, stderr, cmd.ProcessState.ExitCode())
   384  			}
   385  			logOutputOnFailure(t, cmd, stdout, stderr)
   386  		})
   387  	}
   388  }