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 }