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 }