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