github.com/kastenhq/syft@v0.0.0-20230821225854-0710af25cdbe/cmd/syft/cli/packages/packages.go (about)

     1  package packages
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  
     7  	"github.com/wagoodman/go-partybus"
     8  
     9  	"github.com/anchore/stereoscope"
    10  	"github.com/anchore/stereoscope/pkg/image"
    11  	"github.com/kastenhq/syft/cmd/syft/cli/eventloop"
    12  	"github.com/kastenhq/syft/cmd/syft/cli/options"
    13  	"github.com/kastenhq/syft/cmd/syft/internal/ui"
    14  	"github.com/kastenhq/syft/internal"
    15  	"github.com/kastenhq/syft/internal/bus"
    16  	"github.com/kastenhq/syft/internal/config"
    17  	"github.com/kastenhq/syft/internal/file"
    18  	"github.com/kastenhq/syft/internal/log"
    19  	"github.com/kastenhq/syft/internal/version"
    20  	"github.com/kastenhq/syft/syft"
    21  	"github.com/kastenhq/syft/syft/artifact"
    22  	"github.com/kastenhq/syft/syft/formats/template"
    23  	"github.com/kastenhq/syft/syft/sbom"
    24  	"github.com/kastenhq/syft/syft/source"
    25  )
    26  
    27  func Run(_ context.Context, app *config.Application, args []string) error {
    28  	err := ValidateOutputOptions(app)
    29  	if err != nil {
    30  		return err
    31  	}
    32  
    33  	writer, err := options.MakeSBOMWriter(app.Outputs, app.File, app.OutputTemplatePath)
    34  	if err != nil {
    35  		return err
    36  	}
    37  
    38  	// could be an image or a directory, with or without a scheme
    39  	userInput := args[0]
    40  
    41  	eventBus := partybus.NewBus()
    42  	stereoscope.SetBus(eventBus)
    43  	syft.SetBus(eventBus)
    44  	subscription := eventBus.Subscribe()
    45  
    46  	return eventloop.EventLoop(
    47  		execWorker(app, userInput, writer),
    48  		eventloop.SetupSignals(),
    49  		subscription,
    50  		stereoscope.Cleanup,
    51  		ui.Select(options.IsVerbose(app), app.Quiet)...,
    52  	)
    53  }
    54  
    55  // nolint:funlen
    56  func execWorker(app *config.Application, userInput string, writer sbom.Writer) <-chan error {
    57  	errs := make(chan error)
    58  	go func() {
    59  		defer close(errs)
    60  		defer bus.Exit()
    61  
    62  		detection, err := source.Detect(
    63  			userInput,
    64  			source.DetectConfig{
    65  				DefaultImageSource: app.DefaultImagePullSource,
    66  			},
    67  		)
    68  		if err != nil {
    69  			errs <- fmt.Errorf("could not deteremine source: %w", err)
    70  			return
    71  		}
    72  
    73  		var platform *image.Platform
    74  
    75  		if app.Platform != "" {
    76  			platform, err = image.NewPlatform(app.Platform)
    77  			if err != nil {
    78  				errs <- fmt.Errorf("invalid platform: %w", err)
    79  				return
    80  			}
    81  		}
    82  
    83  		hashers, err := file.Hashers(app.Source.File.Digests...)
    84  		if err != nil {
    85  			errs <- fmt.Errorf("invalid hash: %w", err)
    86  			return
    87  		}
    88  
    89  		src, err := detection.NewSource(
    90  			source.DetectionSourceConfig{
    91  				Alias: source.Alias{
    92  					Name:    app.Source.Name,
    93  					Version: app.Source.Version,
    94  				},
    95  				RegistryOptions: app.Registry.ToOptions(),
    96  				Platform:        platform,
    97  				Exclude: source.ExcludeConfig{
    98  					Paths: app.Exclusions,
    99  				},
   100  				DigestAlgorithms: hashers,
   101  				BasePath:         app.BasePath,
   102  			},
   103  		)
   104  
   105  		if err != nil {
   106  			errs <- fmt.Errorf("failed to construct source from user input %q: %w", userInput, err)
   107  			return
   108  		}
   109  
   110  		defer func() {
   111  			if src != nil {
   112  				if err := src.Close(); err != nil {
   113  					log.Tracef("unable to close source: %+v", err)
   114  				}
   115  			}
   116  		}()
   117  
   118  		s, err := GenerateSBOM(src, errs, app)
   119  		if err != nil {
   120  			errs <- err
   121  			return
   122  		}
   123  
   124  		if s == nil {
   125  			errs <- fmt.Errorf("no SBOM produced for %q", userInput)
   126  			return
   127  		}
   128  
   129  		if err := writer.Write(*s); err != nil {
   130  			errs <- fmt.Errorf("failed to write SBOM: %w", err)
   131  			return
   132  		}
   133  	}()
   134  	return errs
   135  }
   136  
   137  func GenerateSBOM(src source.Source, errs chan error, app *config.Application) (*sbom.SBOM, error) {
   138  	tasks, err := eventloop.Tasks(app)
   139  	if err != nil {
   140  		return nil, err
   141  	}
   142  
   143  	s := sbom.SBOM{
   144  		Source: src.Describe(),
   145  		Descriptor: sbom.Descriptor{
   146  			Name:          internal.ApplicationName,
   147  			Version:       version.FromBuild().Version,
   148  			Configuration: app,
   149  		},
   150  	}
   151  
   152  	buildRelationships(&s, src, tasks, errs)
   153  
   154  	return &s, nil
   155  }
   156  
   157  func buildRelationships(s *sbom.SBOM, src source.Source, tasks []eventloop.Task, errs chan error) {
   158  	var relationships []<-chan artifact.Relationship
   159  	for _, task := range tasks {
   160  		c := make(chan artifact.Relationship)
   161  		relationships = append(relationships, c)
   162  		go eventloop.RunTask(task, &s.Artifacts, src, c, errs)
   163  	}
   164  
   165  	s.Relationships = append(s.Relationships, MergeRelationships(relationships...)...)
   166  }
   167  
   168  func MergeRelationships(cs ...<-chan artifact.Relationship) (relationships []artifact.Relationship) {
   169  	for _, c := range cs {
   170  		for n := range c {
   171  			relationships = append(relationships, n)
   172  		}
   173  	}
   174  
   175  	return relationships
   176  }
   177  
   178  func ValidateOutputOptions(app *config.Application) error {
   179  	var usesTemplateOutput bool
   180  	for _, o := range app.Outputs {
   181  		if o == template.ID.String() {
   182  			usesTemplateOutput = true
   183  			break
   184  		}
   185  	}
   186  
   187  	if usesTemplateOutput && app.OutputTemplatePath == "" {
   188  		return fmt.Errorf(`must specify path to template file when using "template" output format`)
   189  	}
   190  
   191  	return nil
   192  }