diff --git a/cmd/syft/cli/cli.go b/cmd/syft/cli/cli.go index 691b366f5..15c330679 100644 --- a/cmd/syft/cli/cli.go +++ b/cmd/syft/cli/cli.go @@ -77,22 +77,25 @@ func create(id clio.Identification, out io.Writer) (clio.Application, *cobra.Com // since root is aliased as the packages cmd we need to construct this command first // we also need the command to have information about the `root` options because of this alias - packagesCmd := commands.Packages(app) + scanCmd := commands.Scan(app) - // rootCmd is currently an alias for the packages command - rootCmd := commands.Root(app, packagesCmd) + // root is currently an alias for the scan command + rootCmd := commands.Root(app, scanCmd) // add sub-commands rootCmd.AddCommand( - packagesCmd, + scanCmd, + commands.Packages(app, scanCmd), // this is currently an alias for the scan command commands.Attest(app), commands.Convert(app), clio.VersionCommand(id), cranecmd.NewCmdAuthLogin(id.Name), // syft login uses the same command as crane ) - // explicitly set Cobra output to the real stdout to write things like errors and help - rootCmd.SetOut(out) + // note: we would direct cobra to use our writer explicitly with rootCmd.SetOut(out) , however this causes + // deprecation warnings to be shown to stdout via the writer instead of stderr. This is unfortunate since this + // does not appear to be the correct behavior on cobra's part https://github.com/spf13/cobra/issues/1708 . + // In the future this functionality should be restored. return app, rootCmd } diff --git a/cmd/syft/cli/commands/attest.go b/cmd/syft/cli/commands/attest.go index c3067933e..37bbbe56a 100644 --- a/cmd/syft/cli/commands/attest.go +++ b/cmd/syft/cli/commands/attest.go @@ -60,7 +60,7 @@ func Attest(app clio.Application) *cobra.Command { "appName": id.Name, "command": "attest", }), - Args: validatePackagesArgs, + Args: validateScanArgs, PreRunE: applicationUpdateCheck(id, &opts.UpdateCheck), RunE: func(cmd *cobra.Command, args []string) error { restoreStdout := ui.CaptureStdoutToTraceLog() diff --git a/cmd/syft/cli/commands/packages.go b/cmd/syft/cli/commands/packages.go index 290ab570b..1c81dd3e5 100644 --- a/cmd/syft/cli/commands/packages.go +++ b/cmd/syft/cli/commands/packages.go @@ -1,255 +1,33 @@ package commands import ( - "fmt" - - "github.com/hashicorp/go-multierror" "github.com/spf13/cobra" "github.com/anchore/clio" - "github.com/anchore/stereoscope/pkg/image" - "github.com/anchore/syft/cmd/syft/cli/eventloop" - "github.com/anchore/syft/cmd/syft/cli/options" "github.com/anchore/syft/cmd/syft/internal/ui" - "github.com/anchore/syft/internal" - "github.com/anchore/syft/internal/bus" - "github.com/anchore/syft/internal/file" - "github.com/anchore/syft/internal/log" - "github.com/anchore/syft/syft/artifact" - "github.com/anchore/syft/syft/sbom" - "github.com/anchore/syft/syft/source" ) -const ( - packagesExample = ` {{.appName}} {{.command}} alpine:latest a summary of discovered packages - {{.appName}} {{.command}} alpine:latest -o json show all possible cataloging details - {{.appName}} {{.command}} alpine:latest -o cyclonedx show a CycloneDX formatted SBOM - {{.appName}} {{.command}} alpine:latest -o cyclonedx-json show a CycloneDX JSON formatted SBOM - {{.appName}} {{.command}} alpine:latest -o spdx show a SPDX 2.3 Tag-Value formatted SBOM - {{.appName}} {{.command}} alpine:latest -o spdx@2.2 show a SPDX 2.2 Tag-Value formatted SBOM - {{.appName}} {{.command}} alpine:latest -o spdx-json show a SPDX 2.3 JSON formatted SBOM - {{.appName}} {{.command}} alpine:latest -o spdx-json@2.2 show a SPDX 2.2 JSON formatted SBOM - {{.appName}} {{.command}} alpine:latest -vv show verbose debug information - {{.appName}} {{.command}} alpine:latest -o template -t my_format.tmpl show a SBOM formatted according to given template file - - Supports the following image sources: - {{.appName}} {{.command}} yourrepo/yourimage:tag defaults to using images from a Docker daemon. If Docker is not present, the image is pulled directly from the registry. - {{.appName}} {{.command}} path/to/a/file/or/dir a Docker tar, OCI tar, OCI directory, SIF container, or generic filesystem directory -` - - schemeHelpHeader = "You can also explicitly specify the scheme to use:" - imageSchemeHelp = ` {{.appName}} {{.command}} docker:yourrepo/yourimage:tag explicitly use the Docker daemon - {{.appName}} {{.command}} podman:yourrepo/yourimage:tag explicitly use the Podman daemon - {{.appName}} {{.command}} registry:yourrepo/yourimage:tag pull image directly from a registry (no container runtime required) - {{.appName}} {{.command}} docker-archive:path/to/yourimage.tar use a tarball from disk for archives created from "docker save" - {{.appName}} {{.command}} oci-archive:path/to/yourimage.tar use a tarball from disk for OCI archives (from Skopeo or otherwise) - {{.appName}} {{.command}} oci-dir:path/to/yourimage read directly from a path on disk for OCI layout directories (from Skopeo or otherwise) - {{.appName}} {{.command}} singularity:path/to/yourimage.sif read directly from a Singularity Image Format (SIF) container on disk -` - nonImageSchemeHelp = ` {{.appName}} {{.command}} dir:path/to/yourproject read directly from a path on disk (any directory) - {{.appName}} {{.command}} file:path/to/yourproject/file read directly from a path on disk (any single file) -` - packagesSchemeHelp = "\n " + schemeHelpHeader + "\n" + imageSchemeHelp + nonImageSchemeHelp - - packagesHelp = packagesExample + packagesSchemeHelp -) - -type packagesOptions struct { - options.Config `yaml:",inline" mapstructure:",squash"` - options.Output `yaml:",inline" mapstructure:",squash"` - options.UpdateCheck `yaml:",inline" mapstructure:",squash"` - options.Catalog `yaml:",inline" mapstructure:",squash"` -} - -func defaultPackagesOptions() *packagesOptions { - return &packagesOptions{ - Output: options.DefaultOutput(), - UpdateCheck: options.DefaultUpdateCheck(), - Catalog: options.DefaultCatalog(), - } -} - -//nolint:dupl -func Packages(app clio.Application) *cobra.Command { +func Packages(app clio.Application, scanCmd *cobra.Command) *cobra.Command { id := app.ID() - opts := defaultPackagesOptions() + opts := defaultScanOptions() - return app.SetupCommand(&cobra.Command{ - Use: "packages [SOURCE]", - Short: "Generate a package SBOM", - Long: "Generate a packaged-based Software Bill Of Materials (SBOM) from container images and filesystems", - Example: internal.Tprintf(packagesHelp, map[string]interface{}{ - "appName": id.Name, - "command": "packages", - }), - Args: validatePackagesArgs, + cmd := app.SetupCommand(&cobra.Command{ + Use: "packages [SOURCE]", + Short: scanCmd.Short, + Long: scanCmd.Long, + Args: scanCmd.Args, + Example: scanCmd.Example, PreRunE: applicationUpdateCheck(id, &opts.UpdateCheck), RunE: func(cmd *cobra.Command, args []string) error { restoreStdout := ui.CaptureStdoutToTraceLog() defer restoreStdout() - return runPackages(id, opts, args[0]) + return runScan(id, opts, args[0]) }, }, opts) -} - -func validatePackagesArgs(cmd *cobra.Command, args []string) error { - return validateArgs(cmd, args, "an image/directory argument is required") -} - -func validateArgs(cmd *cobra.Command, args []string, error string) error { - if len(args) == 0 { - // in the case that no arguments are given we want to show the help text and return with a non-0 return code. - if err := cmd.Help(); err != nil { - return fmt.Errorf("unable to display help: %w", err) - } - return fmt.Errorf(error) - } - - return cobra.MaximumNArgs(1)(cmd, args) -} - -// nolint:funlen -func runPackages(id clio.Identification, opts *packagesOptions, userInput string) error { - writer, err := opts.SBOMWriter() - if err != nil { - return err - } - - src, err := getSource(&opts.Catalog, userInput) - - if err != nil { - return err - } - - defer func() { - if src != nil { - if err := src.Close(); err != nil { - log.Tracef("unable to close source: %+v", err) - } - } - }() - - s, err := generateSBOM(id, src, &opts.Catalog) - if err != nil { - return err - } - - if s == nil { - return fmt.Errorf("no SBOM produced for %q", userInput) - } - - if err := writer.Write(*s); err != nil { - return fmt.Errorf("failed to write SBOM: %w", err) - } - - return nil -} - -func getSource(opts *options.Catalog, userInput string, filters ...func(*source.Detection) error) (source.Source, error) { - detection, err := source.Detect( - userInput, - source.DetectConfig{ - DefaultImageSource: opts.DefaultImagePullSource, - }, - ) - if err != nil { - return nil, fmt.Errorf("could not deteremine source: %w", err) - } - - for _, filter := range filters { - if err := filter(detection); err != nil { - return nil, err - } - } - - var platform *image.Platform - - if opts.Platform != "" { - platform, err = image.NewPlatform(opts.Platform) - if err != nil { - return nil, fmt.Errorf("invalid platform: %w", err) - } - } - - hashers, err := file.Hashers(opts.Source.File.Digests...) - if err != nil { - return nil, fmt.Errorf("invalid hash: %w", err) - } - - src, err := detection.NewSource( - source.DetectionSourceConfig{ - Alias: source.Alias{ - Name: opts.Source.Name, - Version: opts.Source.Version, - }, - RegistryOptions: opts.Registry.ToOptions(), - Platform: platform, - Exclude: source.ExcludeConfig{ - Paths: opts.Exclusions, - }, - DigestAlgorithms: hashers, - BasePath: opts.BasePath, - }, - ) - - if err != nil { - if userInput == "power-user" { - bus.Notify("Note: the 'power-user' command has been removed.") - } - return nil, fmt.Errorf("failed to construct source from user input %q: %w", userInput, err) - } - - return src, nil -} - -func generateSBOM(id clio.Identification, src source.Source, opts *options.Catalog) (*sbom.SBOM, error) { - tasks, err := eventloop.Tasks(opts) - if err != nil { - return nil, err - } - - s := sbom.SBOM{ - Source: src.Describe(), - Descriptor: sbom.Descriptor{ - Name: id.Name, - Version: id.Version, - Configuration: opts, - }, - } - - err = buildRelationships(&s, src, tasks) - - return &s, err -} - -func buildRelationships(s *sbom.SBOM, src source.Source, tasks []eventloop.Task) error { - var errs error - - var relationships []<-chan artifact.Relationship - for _, task := range tasks { - c := make(chan artifact.Relationship) - relationships = append(relationships, c) - go func(task eventloop.Task) { - err := eventloop.RunTask(task, &s.Artifacts, src, c) - if err != nil { - errs = multierror.Append(errs, err) - } - }(task) - } - - s.Relationships = append(s.Relationships, mergeRelationships(relationships...)...) - - return errs -} - -func mergeRelationships(cs ...<-chan artifact.Relationship) (relationships []artifact.Relationship) { - for _, c := range cs { - for n := range c { - relationships = append(relationships, n) - } - } - - return relationships + + cmd.Deprecated = "use `syft scan` instead" + + return cmd } diff --git a/cmd/syft/cli/commands/root.go b/cmd/syft/cli/commands/root.go index 6c41fe4a2..51c477d4e 100644 --- a/cmd/syft/cli/commands/root.go +++ b/cmd/syft/cli/commands/root.go @@ -12,7 +12,7 @@ import ( func Root(app clio.Application, packagesCmd *cobra.Command) *cobra.Command { id := app.ID() - opts := defaultPackagesOptions() + opts := defaultScanOptions() return app.SetupRootCommand(&cobra.Command{ Use: fmt.Sprintf("%s [SOURCE]", app.ID().Name), @@ -25,7 +25,7 @@ func Root(app clio.Application, packagesCmd *cobra.Command) *cobra.Command { restoreStdout := ui.CaptureStdoutToTraceLog() defer restoreStdout() - return runPackages(id, opts, args[0]) + return runScan(id, opts, args[0]) }, }, opts) } diff --git a/cmd/syft/cli/commands/scan.go b/cmd/syft/cli/commands/scan.go new file mode 100644 index 000000000..31c78120c --- /dev/null +++ b/cmd/syft/cli/commands/scan.go @@ -0,0 +1,255 @@ +package commands + +import ( + "fmt" + + "github.com/hashicorp/go-multierror" + "github.com/spf13/cobra" + + "github.com/anchore/clio" + "github.com/anchore/stereoscope/pkg/image" + "github.com/anchore/syft/cmd/syft/cli/eventloop" + "github.com/anchore/syft/cmd/syft/cli/options" + "github.com/anchore/syft/cmd/syft/internal/ui" + "github.com/anchore/syft/internal" + "github.com/anchore/syft/internal/bus" + "github.com/anchore/syft/internal/file" + "github.com/anchore/syft/internal/log" + "github.com/anchore/syft/syft/artifact" + "github.com/anchore/syft/syft/sbom" + "github.com/anchore/syft/syft/source" +) + +const ( + scanExample = ` {{.appName}} {{.command}} alpine:latest a summary of discovered packages + {{.appName}} {{.command}} alpine:latest -o json show all possible cataloging details + {{.appName}} {{.command}} alpine:latest -o cyclonedx show a CycloneDX formatted SBOM + {{.appName}} {{.command}} alpine:latest -o cyclonedx-json show a CycloneDX JSON formatted SBOM + {{.appName}} {{.command}} alpine:latest -o spdx show a SPDX 2.3 Tag-Value formatted SBOM + {{.appName}} {{.command}} alpine:latest -o spdx@2.2 show a SPDX 2.2 Tag-Value formatted SBOM + {{.appName}} {{.command}} alpine:latest -o spdx-json show a SPDX 2.3 JSON formatted SBOM + {{.appName}} {{.command}} alpine:latest -o spdx-json@2.2 show a SPDX 2.2 JSON formatted SBOM + {{.appName}} {{.command}} alpine:latest -vv show verbose debug information + {{.appName}} {{.command}} alpine:latest -o template -t my_format.tmpl show a SBOM formatted according to given template file + + Supports the following image sources: + {{.appName}} {{.command}} yourrepo/yourimage:tag defaults to using images from a Docker daemon. If Docker is not present, the image is pulled directly from the registry. + {{.appName}} {{.command}} path/to/a/file/or/dir a Docker tar, OCI tar, OCI directory, SIF container, or generic filesystem directory +` + + schemeHelpHeader = "You can also explicitly specify the scheme to use:" + imageSchemeHelp = ` {{.appName}} {{.command}} docker:yourrepo/yourimage:tag explicitly use the Docker daemon + {{.appName}} {{.command}} podman:yourrepo/yourimage:tag explicitly use the Podman daemon + {{.appName}} {{.command}} registry:yourrepo/yourimage:tag pull image directly from a registry (no container runtime required) + {{.appName}} {{.command}} docker-archive:path/to/yourimage.tar use a tarball from disk for archives created from "docker save" + {{.appName}} {{.command}} oci-archive:path/to/yourimage.tar use a tarball from disk for OCI archives (from Skopeo or otherwise) + {{.appName}} {{.command}} oci-dir:path/to/yourimage read directly from a path on disk for OCI layout directories (from Skopeo or otherwise) + {{.appName}} {{.command}} singularity:path/to/yourimage.sif read directly from a Singularity Image Format (SIF) container on disk +` + nonImageSchemeHelp = ` {{.appName}} {{.command}} dir:path/to/yourproject read directly from a path on disk (any directory) + {{.appName}} {{.command}} file:path/to/yourproject/file read directly from a path on disk (any single file) +` + scanSchemeHelp = "\n " + schemeHelpHeader + "\n" + imageSchemeHelp + nonImageSchemeHelp + + scanHelp = scanExample + scanSchemeHelp +) + +type scanOptions struct { + options.Config `yaml:",inline" mapstructure:",squash"` + options.Output `yaml:",inline" mapstructure:",squash"` + options.UpdateCheck `yaml:",inline" mapstructure:",squash"` + options.Catalog `yaml:",inline" mapstructure:",squash"` +} + +func defaultScanOptions() *scanOptions { + return &scanOptions{ + Output: options.DefaultOutput(), + UpdateCheck: options.DefaultUpdateCheck(), + Catalog: options.DefaultCatalog(), + } +} + +//nolint:dupl +func Scan(app clio.Application) *cobra.Command { + id := app.ID() + + opts := defaultScanOptions() + + return app.SetupCommand(&cobra.Command{ + Use: "scan [SOURCE]", + Short: "Generate an SBOM", + Long: "Generate a packaged-based Software Bill Of Materials (SBOM) from container images and filesystems", + Example: internal.Tprintf(scanHelp, map[string]interface{}{ + "appName": id.Name, + "command": "scan", + }), + Args: validateScanArgs, + PreRunE: applicationUpdateCheck(id, &opts.UpdateCheck), + RunE: func(cmd *cobra.Command, args []string) error { + restoreStdout := ui.CaptureStdoutToTraceLog() + defer restoreStdout() + + return runScan(id, opts, args[0]) + }, + }, opts) +} + +func validateScanArgs(cmd *cobra.Command, args []string) error { + return validateArgs(cmd, args, "an image/directory argument is required") +} + +func validateArgs(cmd *cobra.Command, args []string, error string) error { + if len(args) == 0 { + // in the case that no arguments are given we want to show the help text and return with a non-0 return code. + if err := cmd.Help(); err != nil { + return fmt.Errorf("unable to display help: %w", err) + } + return fmt.Errorf(error) + } + + return cobra.MaximumNArgs(1)(cmd, args) +} + +// nolint:funlen +func runScan(id clio.Identification, opts *scanOptions, userInput string) error { + writer, err := opts.SBOMWriter() + if err != nil { + return err + } + + src, err := getSource(&opts.Catalog, userInput) + + if err != nil { + return err + } + + defer func() { + if src != nil { + if err := src.Close(); err != nil { + log.Tracef("unable to close source: %+v", err) + } + } + }() + + s, err := generateSBOM(id, src, &opts.Catalog) + if err != nil { + return err + } + + if s == nil { + return fmt.Errorf("no SBOM produced for %q", userInput) + } + + if err := writer.Write(*s); err != nil { + return fmt.Errorf("failed to write SBOM: %w", err) + } + + return nil +} + +func getSource(opts *options.Catalog, userInput string, filters ...func(*source.Detection) error) (source.Source, error) { + detection, err := source.Detect( + userInput, + source.DetectConfig{ + DefaultImageSource: opts.DefaultImagePullSource, + }, + ) + if err != nil { + return nil, fmt.Errorf("could not deteremine source: %w", err) + } + + for _, filter := range filters { + if err := filter(detection); err != nil { + return nil, err + } + } + + var platform *image.Platform + + if opts.Platform != "" { + platform, err = image.NewPlatform(opts.Platform) + if err != nil { + return nil, fmt.Errorf("invalid platform: %w", err) + } + } + + hashers, err := file.Hashers(opts.Source.File.Digests...) + if err != nil { + return nil, fmt.Errorf("invalid hash: %w", err) + } + + src, err := detection.NewSource( + source.DetectionSourceConfig{ + Alias: source.Alias{ + Name: opts.Source.Name, + Version: opts.Source.Version, + }, + RegistryOptions: opts.Registry.ToOptions(), + Platform: platform, + Exclude: source.ExcludeConfig{ + Paths: opts.Exclusions, + }, + DigestAlgorithms: hashers, + BasePath: opts.BasePath, + }, + ) + + if err != nil { + if userInput == "power-user" { + bus.Notify("Note: the 'power-user' command has been removed.") + } + return nil, fmt.Errorf("failed to construct source from user input %q: %w", userInput, err) + } + + return src, nil +} + +func generateSBOM(id clio.Identification, src source.Source, opts *options.Catalog) (*sbom.SBOM, error) { + tasks, err := eventloop.Tasks(opts) + if err != nil { + return nil, err + } + + s := sbom.SBOM{ + Source: src.Describe(), + Descriptor: sbom.Descriptor{ + Name: id.Name, + Version: id.Version, + Configuration: opts, + }, + } + + err = buildRelationships(&s, src, tasks) + + return &s, err +} + +func buildRelationships(s *sbom.SBOM, src source.Source, tasks []eventloop.Task) error { + var errs error + + var relationships []<-chan artifact.Relationship + for _, task := range tasks { + c := make(chan artifact.Relationship) + relationships = append(relationships, c) + go func(task eventloop.Task) { + err := eventloop.RunTask(task, &s.Artifacts, src, c) + if err != nil { + errs = multierror.Append(errs, err) + } + }(task) + } + + s.Relationships = append(s.Relationships, mergeRelationships(relationships...)...) + + return errs +} + +func mergeRelationships(cs ...<-chan artifact.Relationship) (relationships []artifact.Relationship) { + for _, c := range cs { + for n := range c { + relationships = append(relationships, n) + } + } + + return relationships +} diff --git a/syft/source/detection.go b/syft/source/detection.go index 84c622a16..4515c43a3 100644 --- a/syft/source/detection.go +++ b/syft/source/detection.go @@ -60,7 +60,7 @@ func Detect(userInput string, cfg DetectConfig) (*Detection, error) { if src == image.UnknownSource { // only run for these two schemes - // only check on packages command, attest we automatically try to pull from userInput + // only check on scan command, attest we automatically try to pull from userInput switch ty { case containerImageType, unknownType: ty = containerImageType diff --git a/test/cli/cyclonedx_valid_test.go b/test/cli/cyclonedx_valid_test.go index c8728f7f3..1feca08d9 100644 --- a/test/cli/cyclonedx_valid_test.go +++ b/test/cli/cyclonedx_valid_test.go @@ -32,7 +32,7 @@ func TestValidCycloneDX(t *testing.T) { }{ { name: "validate cyclonedx output", - subcommand: "packages", + subcommand: "scan", args: []string{"-o", "cyclonedx-json"}, fixture: imageFixture, assertions: []traitAssertion{ diff --git a/test/cli/json_schema_test.go b/test/cli/json_schema_test.go index 84b822458..31b3e4245 100644 --- a/test/cli/json_schema_test.go +++ b/test/cli/json_schema_test.go @@ -31,14 +31,14 @@ func TestJSONSchema(t *testing.T) { fixture func(*testing.T) string }{ { - name: "packages:image:docker-archive:pkg-coverage", - subcommand: "packages", + name: "scan:image:docker-archive:pkg-coverage", + subcommand: "scan", args: []string{"-o", "json"}, fixture: imageFixture, }, { - name: "packages:dir:pkg-coverage", - subcommand: "packages", + name: "scan:dir:pkg-coverage", + subcommand: "scan", args: []string{"-o", "json"}, fixture: func(t *testing.T) string { return "dir:test-fixtures/image-pkg-coverage" diff --git a/test/cli/root_cmd_test.go b/test/cli/root_cmd_test.go index 2270ca539..e772ee196 100644 --- a/test/cli/root_cmd_test.go +++ b/test/cli/root_cmd_test.go @@ -69,8 +69,8 @@ func TestPersistentFlags(t *testing.T) { }{ { name: "quiet-flag", - // note: the root command will always show the deprecation warning, so the packages command is used instead - args: []string{"packages", "-q", request}, + // note: the root command will always show the deprecation warning, so the scan command is used instead + args: []string{"scan", "-q", request}, assertions: []traitAssertion{ func(tb testing.TB, stdout, stderr string, rc int) { // ensure there is no status diff --git a/test/cli/packages_cmd_test.go b/test/cli/scan_cmd_test.go similarity index 83% rename from test/cli/packages_cmd_test.go rename to test/cli/scan_cmd_test.go index 4647309ab..0e967d7a3 100644 --- a/test/cli/packages_cmd_test.go +++ b/test/cli/scan_cmd_test.go @@ -27,7 +27,7 @@ func TestPackagesCmdFlags(t *testing.T) { }{ { name: "no-args-shows-help", - args: []string{"packages"}, + args: []string{"scan"}, assertions: []traitAssertion{ assertInOutput("an image/directory argument is required"), // specific error that should be shown assertInOutput("Generate a packaged-based Software Bill Of Materials"), // excerpt from help description @@ -36,7 +36,7 @@ func TestPackagesCmdFlags(t *testing.T) { }, { name: "json-output-flag", - args: []string{"packages", "-o", "json", coverageImage}, + args: []string{"scan", "-o", "json", coverageImage}, assertions: []traitAssertion{ assertJsonReport, assertInOutput(`"metadataType":"apk-db-entry"`), @@ -46,7 +46,7 @@ func TestPackagesCmdFlags(t *testing.T) { }, { name: "quiet-flag-with-logger", - args: []string{"packages", "-qvv", "-o", "json", coverageImage}, + args: []string{"scan", "-qvv", "-o", "json", coverageImage}, assertions: []traitAssertion{ assertJsonReport, assertNoStderr, @@ -55,7 +55,7 @@ func TestPackagesCmdFlags(t *testing.T) { }, { name: "quiet-flag-with-tui", - args: []string{"packages", "-q", "-o", "json", coverageImage}, + args: []string{"scan", "-q", "-o", "json", coverageImage}, assertions: []traitAssertion{ assertJsonReport, assertNoStderr, @@ -64,7 +64,7 @@ func TestPackagesCmdFlags(t *testing.T) { }, { name: "multiple-output-flags", - args: []string{"packages", "-o", "table", "-o", "json=" + tmp + ".tmp/multiple-output-flag-test.json", coverageImage}, + args: []string{"scan", "-o", "table", "-o", "json=" + tmp + ".tmp/multiple-output-flag-test.json", coverageImage}, assertions: []traitAssertion{ assertTableReport, assertFileExists(tmp + ".tmp/multiple-output-flag-test.json"), @@ -85,7 +85,7 @@ func TestPackagesCmdFlags(t *testing.T) { // // // this is more of an integration test, however, to assert the output we want to see from the application // // a CLI test is much easier. - // args: []string{"packages", "-vv", badBinariesImage}, + // args: []string{"scan", "-vv", badBinariesImage}, // assertions: []traitAssertion{ // assertInOutput("could not parse possible go binary"), // assertSuccessfulReturnCode, @@ -96,7 +96,7 @@ func TestPackagesCmdFlags(t *testing.T) { env: map[string]string{ "SYFT_OUTPUT": "json", }, - args: []string{"packages", coverageImage}, + args: []string{"scan", coverageImage}, assertions: []traitAssertion{ assertJsonReport, assertSuccessfulReturnCode, @@ -104,7 +104,7 @@ func TestPackagesCmdFlags(t *testing.T) { }, { name: "table-output-flag", - args: []string{"packages", "-o", "table", coverageImage}, + args: []string{"scan", "-o", "table", coverageImage}, assertions: []traitAssertion{ assertTableReport, assertSuccessfulReturnCode, @@ -112,7 +112,7 @@ func TestPackagesCmdFlags(t *testing.T) { }, { name: "default-output-flag", - args: []string{"packages", coverageImage}, + args: []string{"scan", coverageImage}, assertions: []traitAssertion{ assertTableReport, assertSuccessfulReturnCode, @@ -120,7 +120,7 @@ func TestPackagesCmdFlags(t *testing.T) { }, { name: "legacy-json-output-flag", - args: []string{"packages", "-o", "json", coverageImage}, + args: []string{"scan", "-o", "json", coverageImage}, env: map[string]string{ "SYFT_FORMAT_JSON_LEGACY": "true", }, @@ -133,7 +133,7 @@ func TestPackagesCmdFlags(t *testing.T) { }, { name: "squashed-scope-flag", - args: []string{"packages", "-o", "json", "-s", "squashed", coverageImage}, + args: []string{"scan", "-o", "json", "-s", "squashed", coverageImage}, assertions: []traitAssertion{ assertPackageCount(coverageImageSquashedPackageCount), assertSuccessfulReturnCode, @@ -141,7 +141,7 @@ func TestPackagesCmdFlags(t *testing.T) { }, { name: "squashed-scope-flag-hidden-packages", - args: []string{"packages", "-o", "json", "-s", "squashed", hiddenPackagesImage}, + args: []string{"scan", "-o", "json", "-s", "squashed", hiddenPackagesImage}, assertions: []traitAssertion{ assertPackageCount(162), assertNotInOutput("vsftpd"), // hidden package @@ -150,7 +150,7 @@ func TestPackagesCmdFlags(t *testing.T) { }, { name: "all-layers-scope-flag", - args: []string{"packages", "-o", "json", "-s", "all-layers", hiddenPackagesImage}, + args: []string{"scan", "-o", "json", "-s", "all-layers", hiddenPackagesImage}, assertions: []traitAssertion{ assertPackageCount(163), // packages are now deduplicated for this case assertInOutput("all-layers"), @@ -160,7 +160,7 @@ func TestPackagesCmdFlags(t *testing.T) { }, { name: "all-layers-scope-flag-by-env", - args: []string{"packages", "-o", "json", hiddenPackagesImage}, + args: []string{"scan", "-o", "json", hiddenPackagesImage}, env: map[string]string{ "SYFT_PACKAGE_CATALOGER_SCOPE": "all-layers", }, @@ -174,7 +174,7 @@ func TestPackagesCmdFlags(t *testing.T) { { // we want to make certain that syft can catalog a single go binary and get a SBOM report that is not empty name: "catalog-single-go-binary", - args: []string{"packages", "-o", "json", getSyftBinaryLocation(t)}, + args: []string{"scan", "-o", "json", getSyftBinaryLocation(t)}, assertions: []traitAssertion{ assertJsonReport, assertStdoutLengthGreaterThan(1000), @@ -183,7 +183,7 @@ func TestPackagesCmdFlags(t *testing.T) { }, { name: "catalog-node-js-binary", - args: []string{"packages", "-o", "json", nodeBinaryImage}, + args: []string{"scan", "-o", "json", nodeBinaryImage}, assertions: []traitAssertion{ assertJsonReport, assertInOutput("node.js"), @@ -207,7 +207,7 @@ func TestPackagesCmdFlags(t *testing.T) { }, { name: "platform-option-wired-up", - args: []string{"packages", "--platform", "arm64", "-o", "json", "registry:busybox:1.31"}, + args: []string{"scan", "--platform", "arm64", "-o", "json", "registry:busybox:1.31"}, assertions: []traitAssertion{ assertInOutput("sha256:1ee006886991ad4689838d3a288e0dd3fd29b70e276622f16b67a8922831a853"), // linux/arm64 image digest assertSuccessfulReturnCode, @@ -215,7 +215,7 @@ func TestPackagesCmdFlags(t *testing.T) { }, { name: "json-file-flag", - args: []string{"packages", "-o", "json", "--file", filepath.Join(tmp, "output-1.json"), coverageImage}, + args: []string{"scan", "-o", "json", "--file", filepath.Join(tmp, "output-1.json"), coverageImage}, assertions: []traitAssertion{ assertSuccessfulReturnCode, assertFileOutput(t, filepath.Join(tmp, "output-1.json"), @@ -225,7 +225,7 @@ func TestPackagesCmdFlags(t *testing.T) { }, { name: "json-output-flag-to-file", - args: []string{"packages", "-o", fmt.Sprintf("json=%s", filepath.Join(tmp, "output-2.json")), coverageImage}, + args: []string{"scan", "-o", fmt.Sprintf("json=%s", filepath.Join(tmp, "output-2.json")), coverageImage}, assertions: []traitAssertion{ assertSuccessfulReturnCode, assertFileOutput(t, filepath.Join(tmp, "output-2.json"), @@ -236,7 +236,7 @@ func TestPackagesCmdFlags(t *testing.T) { { name: "catalogers-option", // This will detect enable python-package-cataloger, python-installed-package-cataloger and ruby-gemspec cataloger - args: []string{"packages", "-o", "json", "--catalogers", "python,ruby-gemspec", coverageImage}, + args: []string{"scan", "-o", "json", "--catalogers", "python,ruby-gemspec", coverageImage}, assertions: []traitAssertion{ assertPackageCount(13), assertSuccessfulReturnCode, @@ -244,7 +244,7 @@ func TestPackagesCmdFlags(t *testing.T) { }, { name: "override-default-parallelism", - args: []string{"packages", "-vvv", "-o", "json", coverageImage}, + args: []string{"scan", "-vvv", "-o", "json", coverageImage}, env: map[string]string{ "SYFT_PARALLELISM": "2", }, @@ -258,7 +258,7 @@ func TestPackagesCmdFlags(t *testing.T) { }, { name: "default-parallelism", - args: []string{"packages", "-vvv", "-o", "json", coverageImage}, + args: []string{"scan", "-vvv", "-o", "json", coverageImage}, assertions: []traitAssertion{ // the application config in the log matches that of what we expect to have been configured. assertInOutput("parallelism: 1"), @@ -269,7 +269,7 @@ func TestPackagesCmdFlags(t *testing.T) { }, { name: "password and key not in config output", - args: []string{"packages", "-vvv", "-o", "json", coverageImage}, + args: []string{"scan", "-vvv", "-o", "json", coverageImage}, env: map[string]string{ "SYFT_ATTEST_PASSWORD": "secret_password", "SYFT_ATTEST_KEY": "secret_key_path", @@ -281,6 +281,24 @@ func TestPackagesCmdFlags(t *testing.T) { assertSuccessfulReturnCode, }, }, + // Testing packages alias ////////////////////////////////////////////// + { + name: "packages-alias-command-works", + args: []string{"packages", coverageImage}, + assertions: []traitAssertion{ + assertTableReport, + assertInOutput("Command \"packages\" is deprecated, use `syft scan` instead"), + assertSuccessfulReturnCode, + }, + }, + { + name: "packages-alias-command--output-flag", + args: []string{"packages", "-o", "json", coverageImage}, + assertions: []traitAssertion{ + assertJsonReport, + assertSuccessfulReturnCode, + }, + }, } for _, test := range tests { @@ -297,7 +315,7 @@ func TestPackagesCmdFlags(t *testing.T) { func TestRegistryAuth(t *testing.T) { host := "localhost:17" image := fmt.Sprintf("%s/something:latest", host) - args := []string{"packages", "-vvv", fmt.Sprintf("registry:%s", image)} + args := []string{"scan", "-vvv", fmt.Sprintf("registry:%s", image)} tests := []struct { name string diff --git a/test/cli/spdx_json_schema_test.go b/test/cli/spdx_json_schema_test.go index c1e78f06c..db518d57f 100644 --- a/test/cli/spdx_json_schema_test.go +++ b/test/cli/spdx_json_schema_test.go @@ -29,14 +29,14 @@ func TestSPDXJSONSchema(t *testing.T) { fixture func(*testing.T) string }{ { - name: "packages:image:docker-archive:pkg-coverage", - subcommand: "packages", + name: "scan:image:docker-archive:pkg-coverage", + subcommand: "scan", args: []string{"-o", "spdx-json"}, fixture: imageFixture, }, { - name: "packages:dir:pkg-coverage", - subcommand: "packages", + name: "scan:dir:pkg-coverage", + subcommand: "scan", args: []string{"-o", "spdx-json"}, fixture: func(t *testing.T) string { return "dir:test-fixtures/image-pkg-coverage" diff --git a/test/cli/spdx_tooling_validation_test.go b/test/cli/spdx_tooling_validation_test.go index 17c81fa4d..cb3d789e7 100644 --- a/test/cli/spdx_tooling_validation_test.go +++ b/test/cli/spdx_tooling_validation_test.go @@ -40,13 +40,13 @@ func TestSpdxValidationTooling(t *testing.T) { }{ { name: "spdx validation tooling tag value", - syftArgs: []string{"packages", "-o", "spdx"}, + syftArgs: []string{"scan", "-o", "spdx"}, images: images, env: env, }, { name: "spdx validation tooling json", - syftArgs: []string{"packages", "-o", "spdx-json"}, + syftArgs: []string{"scan", "-o", "spdx-json"}, images: images, env: env, }, diff --git a/test/cli/symlink_test.go b/test/cli/symlink_test.go index 97a012038..35ae90b52 100644 --- a/test/cli/symlink_test.go +++ b/test/cli/symlink_test.go @@ -9,6 +9,6 @@ import ( func Test_RequestedPathIncludesSymlink(t *testing.T) { // path contains a symlink path := "test-fixtures/image-pkg-coverage/pkgs/java/example-java-app-maven-0.1.0.jar" - _, stdout, _ := runSyft(t, nil, "packages", path) + _, stdout, _ := runSyft(t, nil, "scan", path) assert.Contains(t, stdout, "example-java-app-maven") }