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 }