mirror of
https://github.com/anchore/syft
synced 2024-11-10 06:14:16 +00:00
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:
parent
7c67df397e
commit
4c20a74d2f
13 changed files with 337 additions and 283 deletions
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
255
cmd/syft/cli/commands/scan.go
Normal file
255
cmd/syft/cli/commands/scan.go
Normal 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
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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{
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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"
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue