Replace packages command with scan (#2446)

* replace packages command with scan

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* add tests for packages alias

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* update comments with referenes to the packages command

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* rename valiadte args function

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

---------

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
This commit is contained in:
Alex Goodman 2024-01-04 11:56:57 -05:00 committed by GitHub
parent 7c67df397e
commit 4c20a74d2f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 337 additions and 283 deletions

View File

@ -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 // 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 // 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 // root is currently an alias for the scan command
rootCmd := commands.Root(app, packagesCmd) rootCmd := commands.Root(app, scanCmd)
// add sub-commands // add sub-commands
rootCmd.AddCommand( rootCmd.AddCommand(
packagesCmd, scanCmd,
commands.Packages(app, scanCmd), // this is currently an alias for the scan command
commands.Attest(app), commands.Attest(app),
commands.Convert(app), commands.Convert(app),
clio.VersionCommand(id), clio.VersionCommand(id),
cranecmd.NewCmdAuthLogin(id.Name), // syft login uses the same command as crane 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 // note: we would direct cobra to use our writer explicitly with rootCmd.SetOut(out) , however this causes
rootCmd.SetOut(out) // 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 return app, rootCmd
} }

View File

@ -60,7 +60,7 @@ func Attest(app clio.Application) *cobra.Command {
"appName": id.Name, "appName": id.Name,
"command": "attest", "command": "attest",
}), }),
Args: validatePackagesArgs, Args: validateScanArgs,
PreRunE: applicationUpdateCheck(id, &opts.UpdateCheck), PreRunE: applicationUpdateCheck(id, &opts.UpdateCheck),
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
restoreStdout := ui.CaptureStdoutToTraceLog() restoreStdout := ui.CaptureStdoutToTraceLog()

View File

@ -1,255 +1,33 @@
package commands package commands
import ( import (
"fmt"
"github.com/hashicorp/go-multierror"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/anchore/clio" "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/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 ( func Packages(app clio.Application, scanCmd *cobra.Command) *cobra.Command {
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 {
id := app.ID() id := app.ID()
opts := defaultPackagesOptions() opts := defaultScanOptions()
return app.SetupCommand(&cobra.Command{ cmd := app.SetupCommand(&cobra.Command{
Use: "packages [SOURCE]", Use: "packages [SOURCE]",
Short: "Generate a package SBOM", Short: scanCmd.Short,
Long: "Generate a packaged-based Software Bill Of Materials (SBOM) from container images and filesystems", Long: scanCmd.Long,
Example: internal.Tprintf(packagesHelp, map[string]interface{}{ Args: scanCmd.Args,
"appName": id.Name, Example: scanCmd.Example,
"command": "packages",
}),
Args: validatePackagesArgs,
PreRunE: applicationUpdateCheck(id, &opts.UpdateCheck), PreRunE: applicationUpdateCheck(id, &opts.UpdateCheck),
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
restoreStdout := ui.CaptureStdoutToTraceLog() restoreStdout := ui.CaptureStdoutToTraceLog()
defer restoreStdout() defer restoreStdout()
return runPackages(id, opts, args[0]) return runScan(id, opts, args[0])
}, },
}, opts) }, opts)
}
func validatePackagesArgs(cmd *cobra.Command, args []string) error { cmd.Deprecated = "use `syft scan` instead"
return validateArgs(cmd, args, "an image/directory argument is required")
}
func validateArgs(cmd *cobra.Command, args []string, error string) error { return cmd
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
} }

View File

@ -12,7 +12,7 @@ import (
func Root(app clio.Application, packagesCmd *cobra.Command) *cobra.Command { func Root(app clio.Application, packagesCmd *cobra.Command) *cobra.Command {
id := app.ID() id := app.ID()
opts := defaultPackagesOptions() opts := defaultScanOptions()
return app.SetupRootCommand(&cobra.Command{ return app.SetupRootCommand(&cobra.Command{
Use: fmt.Sprintf("%s [SOURCE]", app.ID().Name), 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() restoreStdout := ui.CaptureStdoutToTraceLog()
defer restoreStdout() defer restoreStdout()
return runPackages(id, opts, args[0]) return runScan(id, opts, args[0])
}, },
}, opts) }, opts)
} }

View File

@ -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
}

View File

@ -60,7 +60,7 @@ func Detect(userInput string, cfg DetectConfig) (*Detection, error) {
if src == image.UnknownSource { if src == image.UnknownSource {
// only run for these two schemes // 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 { switch ty {
case containerImageType, unknownType: case containerImageType, unknownType:
ty = containerImageType ty = containerImageType

View File

@ -32,7 +32,7 @@ func TestValidCycloneDX(t *testing.T) {
}{ }{
{ {
name: "validate cyclonedx output", name: "validate cyclonedx output",
subcommand: "packages", subcommand: "scan",
args: []string{"-o", "cyclonedx-json"}, args: []string{"-o", "cyclonedx-json"},
fixture: imageFixture, fixture: imageFixture,
assertions: []traitAssertion{ assertions: []traitAssertion{

View File

@ -31,14 +31,14 @@ func TestJSONSchema(t *testing.T) {
fixture func(*testing.T) string fixture func(*testing.T) string
}{ }{
{ {
name: "packages:image:docker-archive:pkg-coverage", name: "scan:image:docker-archive:pkg-coverage",
subcommand: "packages", subcommand: "scan",
args: []string{"-o", "json"}, args: []string{"-o", "json"},
fixture: imageFixture, fixture: imageFixture,
}, },
{ {
name: "packages:dir:pkg-coverage", name: "scan:dir:pkg-coverage",
subcommand: "packages", subcommand: "scan",
args: []string{"-o", "json"}, args: []string{"-o", "json"},
fixture: func(t *testing.T) string { fixture: func(t *testing.T) string {
return "dir:test-fixtures/image-pkg-coverage" return "dir:test-fixtures/image-pkg-coverage"

View File

@ -69,8 +69,8 @@ func TestPersistentFlags(t *testing.T) {
}{ }{
{ {
name: "quiet-flag", name: "quiet-flag",
// note: the root command will always show the deprecation warning, so the packages command is used instead // note: the root command will always show the deprecation warning, so the scan command is used instead
args: []string{"packages", "-q", request}, args: []string{"scan", "-q", request},
assertions: []traitAssertion{ assertions: []traitAssertion{
func(tb testing.TB, stdout, stderr string, rc int) { func(tb testing.TB, stdout, stderr string, rc int) {
// ensure there is no status // ensure there is no status

View File

@ -27,7 +27,7 @@ func TestPackagesCmdFlags(t *testing.T) {
}{ }{
{ {
name: "no-args-shows-help", name: "no-args-shows-help",
args: []string{"packages"}, args: []string{"scan"},
assertions: []traitAssertion{ assertions: []traitAssertion{
assertInOutput("an image/directory argument is required"), // specific error that should be shown 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 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", name: "json-output-flag",
args: []string{"packages", "-o", "json", coverageImage}, args: []string{"scan", "-o", "json", coverageImage},
assertions: []traitAssertion{ assertions: []traitAssertion{
assertJsonReport, assertJsonReport,
assertInOutput(`"metadataType":"apk-db-entry"`), assertInOutput(`"metadataType":"apk-db-entry"`),
@ -46,7 +46,7 @@ func TestPackagesCmdFlags(t *testing.T) {
}, },
{ {
name: "quiet-flag-with-logger", name: "quiet-flag-with-logger",
args: []string{"packages", "-qvv", "-o", "json", coverageImage}, args: []string{"scan", "-qvv", "-o", "json", coverageImage},
assertions: []traitAssertion{ assertions: []traitAssertion{
assertJsonReport, assertJsonReport,
assertNoStderr, assertNoStderr,
@ -55,7 +55,7 @@ func TestPackagesCmdFlags(t *testing.T) {
}, },
{ {
name: "quiet-flag-with-tui", name: "quiet-flag-with-tui",
args: []string{"packages", "-q", "-o", "json", coverageImage}, args: []string{"scan", "-q", "-o", "json", coverageImage},
assertions: []traitAssertion{ assertions: []traitAssertion{
assertJsonReport, assertJsonReport,
assertNoStderr, assertNoStderr,
@ -64,7 +64,7 @@ func TestPackagesCmdFlags(t *testing.T) {
}, },
{ {
name: "multiple-output-flags", 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{ assertions: []traitAssertion{
assertTableReport, assertTableReport,
assertFileExists(tmp + ".tmp/multiple-output-flag-test.json"), 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 // // 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. // // a CLI test is much easier.
// args: []string{"packages", "-vv", badBinariesImage}, // args: []string{"scan", "-vv", badBinariesImage},
// assertions: []traitAssertion{ // assertions: []traitAssertion{
// assertInOutput("could not parse possible go binary"), // assertInOutput("could not parse possible go binary"),
// assertSuccessfulReturnCode, // assertSuccessfulReturnCode,
@ -96,7 +96,7 @@ func TestPackagesCmdFlags(t *testing.T) {
env: map[string]string{ env: map[string]string{
"SYFT_OUTPUT": "json", "SYFT_OUTPUT": "json",
}, },
args: []string{"packages", coverageImage}, args: []string{"scan", coverageImage},
assertions: []traitAssertion{ assertions: []traitAssertion{
assertJsonReport, assertJsonReport,
assertSuccessfulReturnCode, assertSuccessfulReturnCode,
@ -104,7 +104,7 @@ func TestPackagesCmdFlags(t *testing.T) {
}, },
{ {
name: "table-output-flag", name: "table-output-flag",
args: []string{"packages", "-o", "table", coverageImage}, args: []string{"scan", "-o", "table", coverageImage},
assertions: []traitAssertion{ assertions: []traitAssertion{
assertTableReport, assertTableReport,
assertSuccessfulReturnCode, assertSuccessfulReturnCode,
@ -112,7 +112,7 @@ func TestPackagesCmdFlags(t *testing.T) {
}, },
{ {
name: "default-output-flag", name: "default-output-flag",
args: []string{"packages", coverageImage}, args: []string{"scan", coverageImage},
assertions: []traitAssertion{ assertions: []traitAssertion{
assertTableReport, assertTableReport,
assertSuccessfulReturnCode, assertSuccessfulReturnCode,
@ -120,7 +120,7 @@ func TestPackagesCmdFlags(t *testing.T) {
}, },
{ {
name: "legacy-json-output-flag", name: "legacy-json-output-flag",
args: []string{"packages", "-o", "json", coverageImage}, args: []string{"scan", "-o", "json", coverageImage},
env: map[string]string{ env: map[string]string{
"SYFT_FORMAT_JSON_LEGACY": "true", "SYFT_FORMAT_JSON_LEGACY": "true",
}, },
@ -133,7 +133,7 @@ func TestPackagesCmdFlags(t *testing.T) {
}, },
{ {
name: "squashed-scope-flag", name: "squashed-scope-flag",
args: []string{"packages", "-o", "json", "-s", "squashed", coverageImage}, args: []string{"scan", "-o", "json", "-s", "squashed", coverageImage},
assertions: []traitAssertion{ assertions: []traitAssertion{
assertPackageCount(coverageImageSquashedPackageCount), assertPackageCount(coverageImageSquashedPackageCount),
assertSuccessfulReturnCode, assertSuccessfulReturnCode,
@ -141,7 +141,7 @@ func TestPackagesCmdFlags(t *testing.T) {
}, },
{ {
name: "squashed-scope-flag-hidden-packages", name: "squashed-scope-flag-hidden-packages",
args: []string{"packages", "-o", "json", "-s", "squashed", hiddenPackagesImage}, args: []string{"scan", "-o", "json", "-s", "squashed", hiddenPackagesImage},
assertions: []traitAssertion{ assertions: []traitAssertion{
assertPackageCount(162), assertPackageCount(162),
assertNotInOutput("vsftpd"), // hidden package assertNotInOutput("vsftpd"), // hidden package
@ -150,7 +150,7 @@ func TestPackagesCmdFlags(t *testing.T) {
}, },
{ {
name: "all-layers-scope-flag", 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{ assertions: []traitAssertion{
assertPackageCount(163), // packages are now deduplicated for this case assertPackageCount(163), // packages are now deduplicated for this case
assertInOutput("all-layers"), assertInOutput("all-layers"),
@ -160,7 +160,7 @@ func TestPackagesCmdFlags(t *testing.T) {
}, },
{ {
name: "all-layers-scope-flag-by-env", name: "all-layers-scope-flag-by-env",
args: []string{"packages", "-o", "json", hiddenPackagesImage}, args: []string{"scan", "-o", "json", hiddenPackagesImage},
env: map[string]string{ env: map[string]string{
"SYFT_PACKAGE_CATALOGER_SCOPE": "all-layers", "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 // 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", name: "catalog-single-go-binary",
args: []string{"packages", "-o", "json", getSyftBinaryLocation(t)}, args: []string{"scan", "-o", "json", getSyftBinaryLocation(t)},
assertions: []traitAssertion{ assertions: []traitAssertion{
assertJsonReport, assertJsonReport,
assertStdoutLengthGreaterThan(1000), assertStdoutLengthGreaterThan(1000),
@ -183,7 +183,7 @@ func TestPackagesCmdFlags(t *testing.T) {
}, },
{ {
name: "catalog-node-js-binary", name: "catalog-node-js-binary",
args: []string{"packages", "-o", "json", nodeBinaryImage}, args: []string{"scan", "-o", "json", nodeBinaryImage},
assertions: []traitAssertion{ assertions: []traitAssertion{
assertJsonReport, assertJsonReport,
assertInOutput("node.js"), assertInOutput("node.js"),
@ -207,7 +207,7 @@ func TestPackagesCmdFlags(t *testing.T) {
}, },
{ {
name: "platform-option-wired-up", 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{ assertions: []traitAssertion{
assertInOutput("sha256:1ee006886991ad4689838d3a288e0dd3fd29b70e276622f16b67a8922831a853"), // linux/arm64 image digest assertInOutput("sha256:1ee006886991ad4689838d3a288e0dd3fd29b70e276622f16b67a8922831a853"), // linux/arm64 image digest
assertSuccessfulReturnCode, assertSuccessfulReturnCode,
@ -215,7 +215,7 @@ func TestPackagesCmdFlags(t *testing.T) {
}, },
{ {
name: "json-file-flag", 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{ assertions: []traitAssertion{
assertSuccessfulReturnCode, assertSuccessfulReturnCode,
assertFileOutput(t, filepath.Join(tmp, "output-1.json"), assertFileOutput(t, filepath.Join(tmp, "output-1.json"),
@ -225,7 +225,7 @@ func TestPackagesCmdFlags(t *testing.T) {
}, },
{ {
name: "json-output-flag-to-file", 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{ assertions: []traitAssertion{
assertSuccessfulReturnCode, assertSuccessfulReturnCode,
assertFileOutput(t, filepath.Join(tmp, "output-2.json"), assertFileOutput(t, filepath.Join(tmp, "output-2.json"),
@ -236,7 +236,7 @@ func TestPackagesCmdFlags(t *testing.T) {
{ {
name: "catalogers-option", name: "catalogers-option",
// This will detect enable python-package-cataloger, python-installed-package-cataloger and ruby-gemspec cataloger // 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{ assertions: []traitAssertion{
assertPackageCount(13), assertPackageCount(13),
assertSuccessfulReturnCode, assertSuccessfulReturnCode,
@ -244,7 +244,7 @@ func TestPackagesCmdFlags(t *testing.T) {
}, },
{ {
name: "override-default-parallelism", name: "override-default-parallelism",
args: []string{"packages", "-vvv", "-o", "json", coverageImage}, args: []string{"scan", "-vvv", "-o", "json", coverageImage},
env: map[string]string{ env: map[string]string{
"SYFT_PARALLELISM": "2", "SYFT_PARALLELISM": "2",
}, },
@ -258,7 +258,7 @@ func TestPackagesCmdFlags(t *testing.T) {
}, },
{ {
name: "default-parallelism", name: "default-parallelism",
args: []string{"packages", "-vvv", "-o", "json", coverageImage}, args: []string{"scan", "-vvv", "-o", "json", coverageImage},
assertions: []traitAssertion{ assertions: []traitAssertion{
// the application config in the log matches that of what we expect to have been configured. // the application config in the log matches that of what we expect to have been configured.
assertInOutput("parallelism: 1"), assertInOutput("parallelism: 1"),
@ -269,7 +269,7 @@ func TestPackagesCmdFlags(t *testing.T) {
}, },
{ {
name: "password and key not in config output", 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{ env: map[string]string{
"SYFT_ATTEST_PASSWORD": "secret_password", "SYFT_ATTEST_PASSWORD": "secret_password",
"SYFT_ATTEST_KEY": "secret_key_path", "SYFT_ATTEST_KEY": "secret_key_path",
@ -281,6 +281,24 @@ func TestPackagesCmdFlags(t *testing.T) {
assertSuccessfulReturnCode, 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 { for _, test := range tests {
@ -297,7 +315,7 @@ func TestPackagesCmdFlags(t *testing.T) {
func TestRegistryAuth(t *testing.T) { func TestRegistryAuth(t *testing.T) {
host := "localhost:17" host := "localhost:17"
image := fmt.Sprintf("%s/something:latest", host) 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 { tests := []struct {
name string name string

View File

@ -29,14 +29,14 @@ func TestSPDXJSONSchema(t *testing.T) {
fixture func(*testing.T) string fixture func(*testing.T) string
}{ }{
{ {
name: "packages:image:docker-archive:pkg-coverage", name: "scan:image:docker-archive:pkg-coverage",
subcommand: "packages", subcommand: "scan",
args: []string{"-o", "spdx-json"}, args: []string{"-o", "spdx-json"},
fixture: imageFixture, fixture: imageFixture,
}, },
{ {
name: "packages:dir:pkg-coverage", name: "scan:dir:pkg-coverage",
subcommand: "packages", subcommand: "scan",
args: []string{"-o", "spdx-json"}, args: []string{"-o", "spdx-json"},
fixture: func(t *testing.T) string { fixture: func(t *testing.T) string {
return "dir:test-fixtures/image-pkg-coverage" return "dir:test-fixtures/image-pkg-coverage"

View File

@ -40,13 +40,13 @@ func TestSpdxValidationTooling(t *testing.T) {
}{ }{
{ {
name: "spdx validation tooling tag value", name: "spdx validation tooling tag value",
syftArgs: []string{"packages", "-o", "spdx"}, syftArgs: []string{"scan", "-o", "spdx"},
images: images, images: images,
env: env, env: env,
}, },
{ {
name: "spdx validation tooling json", name: "spdx validation tooling json",
syftArgs: []string{"packages", "-o", "spdx-json"}, syftArgs: []string{"scan", "-o", "spdx-json"},
images: images, images: images,
env: env, env: env,
}, },

View File

@ -9,6 +9,6 @@ import (
func Test_RequestedPathIncludesSymlink(t *testing.T) { func Test_RequestedPathIncludesSymlink(t *testing.T) {
// path contains a symlink // path contains a symlink
path := "test-fixtures/image-pkg-coverage/pkgs/java/example-java-app-maven-0.1.0.jar" 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") assert.Contains(t, stdout, "example-java-app-maven")
} }