github.com/noqcks/syft@v0.0.0-20230920222752-a9e2c4e288e5/cmd/syft/cli/commands/packages.go (about)

     1  package commands
     2  
     3  import (
     4  	"fmt"
     5  
     6  	"github.com/hashicorp/go-multierror"
     7  	"github.com/spf13/cobra"
     8  
     9  	"github.com/anchore/clio"
    10  	"github.com/anchore/stereoscope/pkg/image"
    11  	"github.com/anchore/syft/cmd/syft/cli/eventloop"
    12  	"github.com/anchore/syft/cmd/syft/cli/options"
    13  	"github.com/anchore/syft/internal"
    14  	"github.com/anchore/syft/internal/file"
    15  	"github.com/anchore/syft/internal/log"
    16  	"github.com/anchore/syft/syft/artifact"
    17  	"github.com/anchore/syft/syft/formats/template"
    18  	"github.com/anchore/syft/syft/sbom"
    19  	"github.com/anchore/syft/syft/source"
    20  )
    21  
    22  const (
    23  	packagesExample = `  {{.appName}} {{.command}} alpine:latest                                a summary of discovered packages
    24    {{.appName}} {{.command}} alpine:latest -o json                        show all possible cataloging details
    25    {{.appName}} {{.command}} alpine:latest -o cyclonedx                   show a CycloneDX formatted SBOM
    26    {{.appName}} {{.command}} alpine:latest -o cyclonedx-json              show a CycloneDX JSON formatted SBOM
    27    {{.appName}} {{.command}} alpine:latest -o spdx                        show a SPDX 2.3 Tag-Value formatted SBOM
    28    {{.appName}} {{.command}} alpine:latest -o spdx@2.2                    show a SPDX 2.2 Tag-Value formatted SBOM
    29    {{.appName}} {{.command}} alpine:latest -o spdx-json                   show a SPDX 2.3 JSON formatted SBOM
    30    {{.appName}} {{.command}} alpine:latest -o spdx-json@2.2               show a SPDX 2.2 JSON formatted SBOM
    31    {{.appName}} {{.command}} alpine:latest -vv                            show verbose debug information
    32    {{.appName}} {{.command}} alpine:latest -o template -t my_format.tmpl  show a SBOM formatted according to given template file
    33  
    34    Supports the following image sources:
    35      {{.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.
    36      {{.appName}} {{.command}} path/to/a/file/or/dir      a Docker tar, OCI tar, OCI directory, SIF container, or generic filesystem directory
    37  `
    38  
    39  	schemeHelpHeader = "You can also explicitly specify the scheme to use:"
    40  	imageSchemeHelp  = `    {{.appName}} {{.command}} docker:yourrepo/yourimage:tag            explicitly use the Docker daemon
    41      {{.appName}} {{.command}} podman:yourrepo/yourimage:tag            explicitly use the Podman daemon
    42      {{.appName}} {{.command}} registry:yourrepo/yourimage:tag          pull image directly from a registry (no container runtime required)
    43      {{.appName}} {{.command}} docker-archive:path/to/yourimage.tar     use a tarball from disk for archives created from "docker save"
    44      {{.appName}} {{.command}} oci-archive:path/to/yourimage.tar        use a tarball from disk for OCI archives (from Skopeo or otherwise)
    45      {{.appName}} {{.command}} oci-dir:path/to/yourimage                read directly from a path on disk for OCI layout directories (from Skopeo or otherwise)
    46      {{.appName}} {{.command}} singularity:path/to/yourimage.sif        read directly from a Singularity Image Format (SIF) container on disk
    47  `
    48  	nonImageSchemeHelp = `    {{.appName}} {{.command}} dir:path/to/yourproject                  read directly from a path on disk (any directory)
    49      {{.appName}} {{.command}} file:path/to/yourproject/file            read directly from a path on disk (any single file)
    50  `
    51  	packagesSchemeHelp = "\n  " + schemeHelpHeader + "\n" + imageSchemeHelp + nonImageSchemeHelp
    52  
    53  	packagesHelp = packagesExample + packagesSchemeHelp
    54  )
    55  
    56  type packagesOptions struct {
    57  	options.Config      `yaml:",inline" mapstructure:",squash"`
    58  	options.MultiOutput `yaml:",inline" mapstructure:",squash"`
    59  	options.UpdateCheck `yaml:",inline" mapstructure:",squash"`
    60  	options.Catalog     `yaml:",inline" mapstructure:",squash"`
    61  }
    62  
    63  func defaultPackagesOptions() *packagesOptions {
    64  	return &packagesOptions{
    65  		MultiOutput: options.DefaultOutput(),
    66  		UpdateCheck: options.DefaultUpdateCheck(),
    67  		Catalog:     options.DefaultCatalog(),
    68  	}
    69  }
    70  
    71  //nolint:dupl
    72  func Packages(app clio.Application) *cobra.Command {
    73  	id := app.ID()
    74  
    75  	opts := defaultPackagesOptions()
    76  
    77  	return app.SetupCommand(&cobra.Command{
    78  		Use:   "packages [SOURCE]",
    79  		Short: "Generate a package SBOM",
    80  		Long:  "Generate a packaged-based Software Bill Of Materials (SBOM) from container images and filesystems",
    81  		Example: internal.Tprintf(packagesHelp, map[string]interface{}{
    82  			"appName": id.Name,
    83  			"command": "packages",
    84  		}),
    85  		Args:    validatePackagesArgs,
    86  		PreRunE: applicationUpdateCheck(id, &opts.UpdateCheck),
    87  		RunE: func(cmd *cobra.Command, args []string) error {
    88  			return runPackages(id, opts, args[0])
    89  		},
    90  	}, opts)
    91  }
    92  
    93  func validatePackagesArgs(cmd *cobra.Command, args []string) error {
    94  	return validateArgs(cmd, args, "an image/directory argument is required")
    95  }
    96  
    97  func validateArgs(cmd *cobra.Command, args []string, error string) error {
    98  	if len(args) == 0 {
    99  		// in the case that no arguments are given we want to show the help text and return with a non-0 return code.
   100  		if err := cmd.Help(); err != nil {
   101  			return fmt.Errorf("unable to display help: %w", err)
   102  		}
   103  		return fmt.Errorf(error)
   104  	}
   105  
   106  	return cobra.MaximumNArgs(1)(cmd, args)
   107  }
   108  
   109  // nolint:funlen
   110  func runPackages(id clio.Identification, opts *packagesOptions, userInput string) error {
   111  	err := validatePackageOutputOptions(&opts.MultiOutput)
   112  	if err != nil {
   113  		return err
   114  	}
   115  
   116  	writer, err := opts.SBOMWriter()
   117  	if err != nil {
   118  		return err
   119  	}
   120  
   121  	detection, err := source.Detect(
   122  		userInput,
   123  		source.DetectConfig{
   124  			DefaultImageSource: opts.DefaultImagePullSource,
   125  		},
   126  	)
   127  	if err != nil {
   128  		return fmt.Errorf("could not deteremine source: %w", err)
   129  	}
   130  
   131  	var platform *image.Platform
   132  
   133  	if opts.Platform != "" {
   134  		platform, err = image.NewPlatform(opts.Platform)
   135  		if err != nil {
   136  			return fmt.Errorf("invalid platform: %w", err)
   137  		}
   138  	}
   139  
   140  	hashers, err := file.Hashers(opts.Source.File.Digests...)
   141  	if err != nil {
   142  		return fmt.Errorf("invalid hash: %w", err)
   143  	}
   144  
   145  	src, err := detection.NewSource(
   146  		source.DetectionSourceConfig{
   147  			Alias: source.Alias{
   148  				Name:    opts.Source.Name,
   149  				Version: opts.Source.Version,
   150  			},
   151  			RegistryOptions: opts.Registry.ToOptions(),
   152  			Platform:        platform,
   153  			Exclude: source.ExcludeConfig{
   154  				Paths: opts.Exclusions,
   155  			},
   156  			DigestAlgorithms: hashers,
   157  			BasePath:         opts.BasePath,
   158  		},
   159  	)
   160  
   161  	if err != nil {
   162  		return fmt.Errorf("failed to construct source from user input %q: %w", userInput, err)
   163  	}
   164  
   165  	defer func() {
   166  		if src != nil {
   167  			if err := src.Close(); err != nil {
   168  				log.Tracef("unable to close source: %+v", err)
   169  			}
   170  		}
   171  	}()
   172  
   173  	s, err := generateSBOM(id, src, &opts.Catalog)
   174  	if err != nil {
   175  		return err
   176  	}
   177  
   178  	if s == nil {
   179  		return fmt.Errorf("no SBOM produced for %q", userInput)
   180  	}
   181  
   182  	if err := writer.Write(*s); err != nil {
   183  		return fmt.Errorf("failed to write SBOM: %w", err)
   184  	}
   185  
   186  	return nil
   187  }
   188  
   189  func generateSBOM(id clio.Identification, src source.Source, opts *options.Catalog) (*sbom.SBOM, error) {
   190  	tasks, err := eventloop.Tasks(opts)
   191  	if err != nil {
   192  		return nil, err
   193  	}
   194  
   195  	s := sbom.SBOM{
   196  		Source: src.Describe(),
   197  		Descriptor: sbom.Descriptor{
   198  			Name:          id.Name,
   199  			Version:       id.Version,
   200  			Configuration: opts,
   201  		},
   202  	}
   203  
   204  	err = buildRelationships(&s, src, tasks)
   205  
   206  	return &s, err
   207  }
   208  
   209  func buildRelationships(s *sbom.SBOM, src source.Source, tasks []eventloop.Task) error {
   210  	var errs error
   211  
   212  	var relationships []<-chan artifact.Relationship
   213  	for _, task := range tasks {
   214  		c := make(chan artifact.Relationship)
   215  		relationships = append(relationships, c)
   216  		go func(task eventloop.Task) {
   217  			err := eventloop.RunTask(task, &s.Artifacts, src, c)
   218  			if err != nil {
   219  				errs = multierror.Append(errs, err)
   220  			}
   221  		}(task)
   222  	}
   223  
   224  	s.Relationships = append(s.Relationships, mergeRelationships(relationships...)...)
   225  
   226  	return errs
   227  }
   228  
   229  func mergeRelationships(cs ...<-chan artifact.Relationship) (relationships []artifact.Relationship) {
   230  	for _, c := range cs {
   231  		for n := range c {
   232  			relationships = append(relationships, n)
   233  		}
   234  	}
   235  
   236  	return relationships
   237  }
   238  
   239  func validatePackageOutputOptions(cfg *options.MultiOutput) error {
   240  	var usesTemplateOutput bool
   241  	for _, o := range cfg.Outputs {
   242  		if o == template.ID.String() {
   243  			usesTemplateOutput = true
   244  			break
   245  		}
   246  	}
   247  
   248  	if usesTemplateOutput && cfg.OutputTemplatePath == "" {
   249  		return fmt.Errorf(`must specify path to template file when using "template" output format`)
   250  	}
   251  
   252  	return nil
   253  }