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 }