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 }