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

View file

@ -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()

View file

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

View file

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

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 {
// 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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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