Skip to content

Commit

Permalink
feat: add --from flag, refactor source providers (#2610)
Browse files Browse the repository at this point in the history
Signed-off-by: Keith Zantow <kzantow@gmail.com>
  • Loading branch information
kzantow committed Feb 27, 2024
1 parent 928511e commit a978966
Show file tree
Hide file tree
Showing 72 changed files with 1,036 additions and 1,081 deletions.
33 changes: 16 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,8 @@ syft <image> --scope all-layers

### Supported sources

Syft can generate an SBOM from a variety of sources:
Syft can generate an SBOM from a variety of sources including images, files, directories, and archives. Syft will attempt to
determine the type of source based on provided input, for example:

```bash
# catalog a container image archive (from the result of `docker image save ...`, `podman save ...`, or `skopeo copy` commands)
Expand All @@ -135,26 +136,24 @@ syft path/to/image.sif
syft path/to/dir
```

Sources can be explicitly provided with a scheme:
To explicitly specify the source behavior, use the `--from` flag. Allowable options are:

```
docker:yourrepo/yourimage:tag use images from the Docker daemon
podman:yourrepo/yourimage:tag use images from the Podman daemon
containerd:yourrepo/yourimage:tag use images from the Containerd daemon
docker-archive:path/to/yourimage.tar use a tarball from disk for archives created from "docker save"
oci-archive:path/to/yourimage.tar use a tarball from disk for OCI archives (from Skopeo or otherwise)
oci-dir:path/to/yourimage read directly from a path on disk for OCI layout directories (from Skopeo or otherwise)
singularity:path/to/yourimage.sif read directly from a Singularity Image Format (SIF) container on disk
dir:path/to/yourproject read directly from a path on disk (any directory)
file:path/to/yourproject/file read directly from a path on disk (any single file)
registry:yourrepo/yourimage:tag pull image directly from a registry (no container runtime required)
docker use images from the Docker daemon
podman use images from the Podman daemon
containerd use images from the Containerd daemon
docker-archive use a tarball from disk for archives created from "docker save"
oci-archive use a tarball from disk for OCI archives (from Skopeo or otherwise)
oci-dir read directly from a path on disk for OCI layout directories (from Skopeo or otherwise)
singularity read directly from a Singularity Image Format (SIF) container on disk
dir read directly from a path on disk (any directory)
file read directly from a path on disk (any single file)
registry pull image directly from a registry (no container runtime required)
```
If a source is not provided and Syft identifies the input as a potential image reference, Syft will attempt to resolve it using:
the Docker, Podman, and Containerd daemons followed by direct registry access, in that order.

If an image source is not provided and cannot be detected from the given reference it is assumed the image should be pulled from the Docker daemon.
If docker is not present, then the Podman daemon is attempted next, followed by reaching out directly to the image registry last.


This default behavior can be overridden with the `default-image-pull-source` configuration option (See [Configuration](https://github.com/anchore/syft#configuration) for more details).
This default behavior can be overridden with the `default-image-pull-source` configuration option (See [Configuration](#configuration) for more details).


### File selection
Expand Down
15 changes: 6 additions & 9 deletions cmd/syft/internal/commands/attest.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"github.com/wagoodman/go-progress"

"github.com/anchore/clio"
"github.com/anchore/stereoscope"
"github.com/anchore/syft/cmd/syft/internal/options"
"github.com/anchore/syft/cmd/syft/internal/ui"
"github.com/anchore/syft/internal"
Expand All @@ -26,7 +27,6 @@ import (
"github.com/anchore/syft/syft/format/spdxtagvalue"
"github.com/anchore/syft/syft/format/syftjson"
"github.com/anchore/syft/syft/sbom"
"github.com/anchore/syft/syft/source"
)

const (
Expand Down Expand Up @@ -247,7 +247,11 @@ func predicateType(outputName string) string {
}

func generateSBOMForAttestation(ctx context.Context, id clio.Identification, opts *options.Catalog, userInput string) (*sbom.SBOM, error) {
src, err := getSource(opts, userInput, onlyContainerImages)
if len(opts.From) > 1 || (len(opts.From) == 1 && opts.From[0] != stereoscope.RegistryTag) {
return nil, fmt.Errorf("attest requires use of an OCI registry directly, one or more of the specified sources is unsupported: %v", opts.From)
}

src, err := getSource(ctx, opts, userInput, stereoscope.RegistryTag)

if err != nil {
return nil, err
Expand All @@ -273,13 +277,6 @@ func generateSBOMForAttestation(ctx context.Context, id clio.Identification, opt
return s, nil
}

func onlyContainerImages(d *source.Detection) error {
if !d.IsContainerImage() {
return fmt.Errorf("attestations are only supported for oci images at this time")
}
return nil
}

func commandExists(cmd string) bool {
_, err := exec.LookPath(cmd)
return err == nil
Expand Down
83 changes: 45 additions & 38 deletions cmd/syft/internal/commands/scan.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import (
"gopkg.in/yaml.v3"

"github.com/anchore/clio"
"github.com/anchore/go-collections"
"github.com/anchore/stereoscope"
"github.com/anchore/stereoscope/pkg/image"
"github.com/anchore/syft/cmd/syft/internal/options"
"github.com/anchore/syft/cmd/syft/internal/ui"
Expand All @@ -24,6 +26,7 @@ import (
"github.com/anchore/syft/syft"
"github.com/anchore/syft/syft/sbom"
"github.com/anchore/syft/syft/source"
"github.com/anchore/syft/syft/source/sourceproviders"
)

const (
Expand Down Expand Up @@ -162,14 +165,23 @@ func validateArgs(cmd *cobra.Command, args []string, error string) error {
return cobra.MaximumNArgs(1)(cmd, args)
}

// nolint:funlen
func runScan(ctx context.Context, id clio.Identification, opts *scanOptions, userInput string) error {
writer, err := opts.SBOMWriter()
if err != nil {
return err
}

src, err := getSource(&opts.Catalog, userInput)
sources := opts.From
if len(sources) == 0 {
// extract a scheme if it matches any provider tag; this is a holdover for compatibility, using the --from flag is recommended
explicitSource, newUserInput := stereoscope.ExtractSchemeSource(userInput, allSourceProviderTags()...)
if explicitSource != "" {
sources = append(sources, explicitSource)
userInput = newUserInput
}
}

src, err := getSource(ctx, &opts.Catalog, userInput, sources...)

if err != nil {
return err
Expand Down Expand Up @@ -199,52 +211,43 @@ func runScan(ctx context.Context, id clio.Identification, opts *scanOptions, use
return nil
}

func getSource(opts *options.Catalog, userInput string, filters ...func(*source.Detection) error) (source.Source, error) {
detection, err := source.Detect(
userInput,
source.DetectConfig{
DefaultImageSource: opts.Source.Image.DefaultPullSource,
},
)
if err != nil {
return nil, fmt.Errorf("could not deteremine source: %w", err)
}

for _, filter := range filters {
if err := filter(detection); err != nil {
return nil, err
}
}

func getSource(ctx context.Context, opts *options.Catalog, userInput string, sources ...string) (source.Source, error) {
cfg := syft.DefaultGetSourceConfig().
WithRegistryOptions(opts.Registry.ToOptions()).
WithAlias(source.Alias{
Name: opts.Source.Name,
Version: opts.Source.Version,
}).
WithExcludeConfig(source.ExcludeConfig{
Paths: opts.Exclusions,
}).
WithBasePath(opts.Source.BasePath).
WithSources(sources...).
WithDefaultImagePullSource(opts.Source.Image.DefaultPullSource)

var err error
var platform *image.Platform

if opts.Platform != "" {
platform, err = image.NewPlatform(opts.Platform)
if err != nil {
return nil, fmt.Errorf("invalid platform: %w", err)
}
cfg = cfg.WithPlatform(platform)
}

if opts.Source.File.Digests != nil {
hashers, err := file.Hashers(opts.Source.File.Digests...)
if err != nil {
return nil, fmt.Errorf("invalid hash algorithm: %w", err)
}
cfg = cfg.WithDigestAlgorithms(hashers...)
}

hashers, err := file.Hashers(opts.Source.File.Digests...)
src, err := syft.GetSource(ctx, userInput, cfg)
if err != nil {
return nil, fmt.Errorf("invalid hash algorithm: %w", err)
}

src, err := detection.NewSource(
source.DetectionSourceConfig{
Alias: source.Alias{
Name: opts.Source.Name,
Version: opts.Source.Version,
},
RegistryOptions: opts.Registry.ToOptions(),
Platform: platform,
Exclude: source.ExcludeConfig{
Paths: opts.Exclusions,
},
DigestAlgorithms: hashers,
BasePath: opts.Source.BasePath,
},
)
return nil, fmt.Errorf("could not determine source: %w", err)
}

if err != nil {
if userInput == "power-user" {
Expand Down Expand Up @@ -445,3 +448,7 @@ func getHintPhrase(expErr task.ErrInvalidExpression) string {
func trimOperation(x string) string {
return strings.TrimLeft(x, "+-")
}

func allSourceProviderTags() []string {
return collections.TaggedValueSet[source.Provider]{}.Join(sourceproviders.All("", nil)...).Tags()
}
6 changes: 6 additions & 0 deletions cmd/syft/internal/options/catalog.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ type Catalog struct {

// configuration for the source (the subject being analyzed)
Registry registryConfig `yaml:"registry" json:"registry" mapstructure:"registry"`
From []string `yaml:"from" json:"from" mapstructure:"from"`
Platform string `yaml:"platform" json:"platform" mapstructure:"platform"`
Source sourceConfig `yaml:"source" json:"source" mapstructure:"source"`
Exclusions []string `yaml:"exclude" json:"exclude" mapstructure:"exclude"`
Expand Down Expand Up @@ -163,6 +164,9 @@ func (cfg *Catalog) AddFlags(flags clio.FlagSet) {
flags.StringVarP(&cfg.Scope, "scope", "s",
fmt.Sprintf("selection of layers to catalog, options=%v", validScopeValues))

flags.StringArrayVarP(&cfg.From, "from", "",
"specify the source behavior to use (e.g. docker, registry, oci-dir, ...)")

flags.StringVarP(&cfg.Platform, "platform", "",
"an optional platform specifier for container image sources (e.g. 'linux/arm64', 'linux/arm64/v8', 'arm64', 'linux')")

Expand Down Expand Up @@ -220,6 +224,8 @@ func (cfg *Catalog) PostLoad() error {
return out
}

cfg.From = flatten(cfg.From)

cfg.Catalogers = flatten(cfg.Catalogers)
cfg.DefaultCatalogers = flatten(cfg.DefaultCatalogers)
cfg.SelectCatalogers = flatten(cfg.SelectCatalogers)
Expand Down
11 changes: 7 additions & 4 deletions cmd/syft/internal/options/source.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import (
"strings"

"github.com/scylladb/go-set/strset"

"github.com/anchore/syft/syft/source/sourceproviders"
)

type sourceConfig struct {
Expand All @@ -25,12 +27,13 @@ type imageSource struct {
}

func defaultSourceConfig() sourceConfig {
var digests []string
for _, alg := range sourceproviders.DefaultConfig().DigestAlgorithms {
digests = append(digests, alg.String())
}
return sourceConfig{
File: fileSource{
Digests: []string{"sha256"},
},
Image: imageSource{
DefaultPullSource: "",
Digests: digests,
},
}
}
Expand Down
8 changes: 2 additions & 6 deletions cmd/syft/internal/test/integration/catalog_packages_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,15 +22,11 @@ func BenchmarkImagePackageCatalogers(b *testing.B) {
tarPath := imagetest.GetFixtureImageTarPath(b, fixtureImageName)

// get the source object for the image
userInput := "docker-archive:" + tarPath
detection, err := source.Detect(userInput, source.DefaultDetectConfig())
require.NoError(b, err)

theSource, err := detection.NewSource(source.DefaultDetectionSourceConfig())
theSource, err := syft.GetSource(context.Background(), tarPath, syft.DefaultGetSourceConfig().WithSources("docker-archive"))
require.NoError(b, err)

b.Cleanup(func() {
theSource.Close()
require.NoError(b, theSource.Close())
})

// build the SBOM
Expand Down
16 changes: 4 additions & 12 deletions cmd/syft/internal/test/integration/utils_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,17 +34,13 @@ func catalogFixtureImageWithConfig(t *testing.T, fixtureImageName string, cfg *s
// get the fixture image tar file
imagetest.GetFixtureImage(t, "docker-archive", fixtureImageName)
tarPath := imagetest.GetFixtureImageTarPath(t, fixtureImageName)
userInput := "docker-archive:" + tarPath

// get the source to build an SBOM against
detection, err := source.Detect(userInput, source.DefaultDetectConfig())
require.NoError(t, err)

theSource, err := detection.NewSource(source.DefaultDetectionSourceConfig())
theSource, err := syft.GetSource(context.Background(), tarPath, syft.DefaultGetSourceConfig().WithSources("docker-archive"))
require.NoError(t, err)

t.Cleanup(func() {
theSource.Close()
require.NoError(t, theSource.Close())
})

s, err := syft.CreateSBOM(context.Background(), theSource, cfg)
Expand All @@ -71,14 +67,10 @@ func catalogDirectoryWithConfig(t *testing.T, dir string, cfg *syft.CreateSBOMCo
cfg.CatalogerSelection = cfg.CatalogerSelection.WithDefaults(pkgcataloging.DirectoryTag)

// get the source to build an sbom against
userInput := "dir:" + dir
detection, err := source.Detect(userInput, source.DefaultDetectConfig())
require.NoError(t, err)

theSource, err := detection.NewSource(source.DefaultDetectionSourceConfig())
theSource, err := syft.GetSource(context.Background(), dir, syft.DefaultGetSourceConfig().WithSources("dir"))
require.NoError(t, err)
t.Cleanup(func() {
theSource.Close()
require.NoError(t, theSource.Close())
})

// build the SBOM
Expand Down
12 changes: 1 addition & 11 deletions examples/create_custom_sbom/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,17 +44,7 @@ func imageReference() string {
func getSource(input string) source.Source {
fmt.Println("detecting source type for input:", input, "...")

detection, err := source.Detect(input,
source.DetectConfig{
DefaultImageSource: "docker",
},
)

if err != nil {
panic(err)
}

src, err := detection.NewSource(source.DefaultDetectionSourceConfig())
src, err := syft.GetSource(context.Background(), input, nil)

if err != nil {
panic(err)
Expand Down
12 changes: 1 addition & 11 deletions examples/create_simple_sbom/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,17 +37,7 @@ func imageReference() string {
}

func getSource(input string) source.Source {
detection, err := source.Detect(input,
source.DetectConfig{
DefaultImageSource: "docker",
},
)

if err != nil {
panic(err)
}

src, err := detection.NewSource(source.DefaultDetectionSourceConfig())
src, err := syft.GetSource(context.Background(), input, nil)

if err != nil {
panic(err)
Expand Down
12 changes: 1 addition & 11 deletions examples/select_catalogers/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,17 +41,7 @@ func imageReference() string {
}

func getSource(input string) source.Source {
detection, err := source.Detect(input,
source.DetectConfig{
DefaultImageSource: "docker",
},
)

if err != nil {
panic(err)
}

src, err := detection.NewSource(source.DefaultDetectionSourceConfig())
src, err := syft.GetSource(context.Background(), input, nil)

if err != nil {
panic(err)
Expand Down

0 comments on commit a978966

Please sign in to comment.