mirror of
https://github.com/anchore/syft
synced 2024-11-10 06:14:16 +00:00
feat: add --from
flag, refactor source providers (#2610)
Signed-off-by: Keith Zantow <kzantow@gmail.com>
This commit is contained in:
parent
928511ea0f
commit
a978966cad
72 changed files with 1035 additions and 1080 deletions
33
README.md
33
README.md
|
@ -122,7 +122,8 @@ syft <image> --scope all-layers
|
||||||
|
|
||||||
### Supported sources
|
### 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
|
```bash
|
||||||
# catalog a container image archive (from the result of `docker image save ...`, `podman save ...`, or `skopeo copy` commands)
|
# catalog a container image archive (from the result of `docker image save ...`, `podman save ...`, or `skopeo copy` commands)
|
||||||
|
@ -135,26 +136,24 @@ syft path/to/image.sif
|
||||||
syft path/to/dir
|
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
|
docker use images from the Docker daemon
|
||||||
podman:yourrepo/yourimage:tag use images from the Podman daemon
|
podman use images from the Podman daemon
|
||||||
containerd:yourrepo/yourimage:tag use images from the Containerd daemon
|
containerd use images from the Containerd daemon
|
||||||
docker-archive:path/to/yourimage.tar use a tarball from disk for archives created from "docker save"
|
docker-archive 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-archive 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)
|
oci-dir 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
|
singularity read directly from a Singularity Image Format (SIF) container on disk
|
||||||
dir:path/to/yourproject read directly from a path on disk (any directory)
|
dir read directly from a path on disk (any directory)
|
||||||
file:path/to/yourproject/file read directly from a path on disk (any single file)
|
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)
|
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.
|
This default behavior can be overridden with the `default-image-pull-source` configuration option (See [Configuration](#configuration) for more details).
|
||||||
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).
|
|
||||||
|
|
||||||
|
|
||||||
### File selection
|
### File selection
|
||||||
|
|
|
@ -13,6 +13,7 @@ import (
|
||||||
"github.com/wagoodman/go-progress"
|
"github.com/wagoodman/go-progress"
|
||||||
|
|
||||||
"github.com/anchore/clio"
|
"github.com/anchore/clio"
|
||||||
|
"github.com/anchore/stereoscope"
|
||||||
"github.com/anchore/syft/cmd/syft/internal/options"
|
"github.com/anchore/syft/cmd/syft/internal/options"
|
||||||
"github.com/anchore/syft/cmd/syft/internal/ui"
|
"github.com/anchore/syft/cmd/syft/internal/ui"
|
||||||
"github.com/anchore/syft/internal"
|
"github.com/anchore/syft/internal"
|
||||||
|
@ -26,7 +27,6 @@ import (
|
||||||
"github.com/anchore/syft/syft/format/spdxtagvalue"
|
"github.com/anchore/syft/syft/format/spdxtagvalue"
|
||||||
"github.com/anchore/syft/syft/format/syftjson"
|
"github.com/anchore/syft/syft/format/syftjson"
|
||||||
"github.com/anchore/syft/syft/sbom"
|
"github.com/anchore/syft/syft/sbom"
|
||||||
"github.com/anchore/syft/syft/source"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -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) {
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -273,13 +277,6 @@ func generateSBOMForAttestation(ctx context.Context, id clio.Identification, opt
|
||||||
return s, nil
|
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 {
|
func commandExists(cmd string) bool {
|
||||||
_, err := exec.LookPath(cmd)
|
_, err := exec.LookPath(cmd)
|
||||||
return err == nil
|
return err == nil
|
||||||
|
|
|
@ -13,6 +13,8 @@ import (
|
||||||
"gopkg.in/yaml.v3"
|
"gopkg.in/yaml.v3"
|
||||||
|
|
||||||
"github.com/anchore/clio"
|
"github.com/anchore/clio"
|
||||||
|
"github.com/anchore/go-collections"
|
||||||
|
"github.com/anchore/stereoscope"
|
||||||
"github.com/anchore/stereoscope/pkg/image"
|
"github.com/anchore/stereoscope/pkg/image"
|
||||||
"github.com/anchore/syft/cmd/syft/internal/options"
|
"github.com/anchore/syft/cmd/syft/internal/options"
|
||||||
"github.com/anchore/syft/cmd/syft/internal/ui"
|
"github.com/anchore/syft/cmd/syft/internal/ui"
|
||||||
|
@ -24,6 +26,7 @@ import (
|
||||||
"github.com/anchore/syft/syft"
|
"github.com/anchore/syft/syft"
|
||||||
"github.com/anchore/syft/syft/sbom"
|
"github.com/anchore/syft/syft/sbom"
|
||||||
"github.com/anchore/syft/syft/source"
|
"github.com/anchore/syft/syft/source"
|
||||||
|
"github.com/anchore/syft/syft/source/sourceproviders"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -162,14 +165,23 @@ func validateArgs(cmd *cobra.Command, args []string, error string) error {
|
||||||
return cobra.MaximumNArgs(1)(cmd, args)
|
return cobra.MaximumNArgs(1)(cmd, args)
|
||||||
}
|
}
|
||||||
|
|
||||||
// nolint:funlen
|
|
||||||
func runScan(ctx context.Context, id clio.Identification, opts *scanOptions, userInput string) error {
|
func runScan(ctx context.Context, id clio.Identification, opts *scanOptions, userInput string) error {
|
||||||
writer, err := opts.SBOMWriter()
|
writer, err := opts.SBOMWriter()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -199,23 +211,21 @@ func runScan(ctx context.Context, id clio.Identification, opts *scanOptions, use
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func getSource(opts *options.Catalog, userInput string, filters ...func(*source.Detection) error) (source.Source, error) {
|
func getSource(ctx context.Context, opts *options.Catalog, userInput string, sources ...string) (source.Source, error) {
|
||||||
detection, err := source.Detect(
|
cfg := syft.DefaultGetSourceConfig().
|
||||||
userInput,
|
WithRegistryOptions(opts.Registry.ToOptions()).
|
||||||
source.DetectConfig{
|
WithAlias(source.Alias{
|
||||||
DefaultImageSource: opts.Source.Image.DefaultPullSource,
|
Name: opts.Source.Name,
|
||||||
},
|
Version: opts.Source.Version,
|
||||||
)
|
}).
|
||||||
if err != nil {
|
WithExcludeConfig(source.ExcludeConfig{
|
||||||
return nil, fmt.Errorf("could not deteremine source: %w", err)
|
Paths: opts.Exclusions,
|
||||||
}
|
}).
|
||||||
|
WithBasePath(opts.Source.BasePath).
|
||||||
for _, filter := range filters {
|
WithSources(sources...).
|
||||||
if err := filter(detection); err != nil {
|
WithDefaultImagePullSource(opts.Source.Image.DefaultPullSource)
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
var err error
|
||||||
var platform *image.Platform
|
var platform *image.Platform
|
||||||
|
|
||||||
if opts.Platform != "" {
|
if opts.Platform != "" {
|
||||||
|
@ -223,28 +233,21 @@ func getSource(opts *options.Catalog, userInput string, filters ...func(*source.
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("invalid platform: %w", err)
|
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...)
|
hashers, err := file.Hashers(opts.Source.File.Digests...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("invalid hash algorithm: %w", err)
|
return nil, fmt.Errorf("invalid hash algorithm: %w", err)
|
||||||
}
|
}
|
||||||
|
cfg = cfg.WithDigestAlgorithms(hashers...)
|
||||||
|
}
|
||||||
|
|
||||||
src, err := detection.NewSource(
|
src, err := syft.GetSource(ctx, userInput, cfg)
|
||||||
source.DetectionSourceConfig{
|
if err != nil {
|
||||||
Alias: source.Alias{
|
return nil, fmt.Errorf("could not determine source: %w", err)
|
||||||
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,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if userInput == "power-user" {
|
if userInput == "power-user" {
|
||||||
|
@ -445,3 +448,7 @@ func getHintPhrase(expErr task.ErrInvalidExpression) string {
|
||||||
func trimOperation(x string) string {
|
func trimOperation(x string) string {
|
||||||
return strings.TrimLeft(x, "+-")
|
return strings.TrimLeft(x, "+-")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func allSourceProviderTags() []string {
|
||||||
|
return collections.TaggedValueSet[source.Provider]{}.Join(sourceproviders.All("", nil)...).Tags()
|
||||||
|
}
|
||||||
|
|
|
@ -46,6 +46,7 @@ type Catalog struct {
|
||||||
|
|
||||||
// configuration for the source (the subject being analyzed)
|
// configuration for the source (the subject being analyzed)
|
||||||
Registry registryConfig `yaml:"registry" json:"registry" mapstructure:"registry"`
|
Registry registryConfig `yaml:"registry" json:"registry" mapstructure:"registry"`
|
||||||
|
From []string `yaml:"from" json:"from" mapstructure:"from"`
|
||||||
Platform string `yaml:"platform" json:"platform" mapstructure:"platform"`
|
Platform string `yaml:"platform" json:"platform" mapstructure:"platform"`
|
||||||
Source sourceConfig `yaml:"source" json:"source" mapstructure:"source"`
|
Source sourceConfig `yaml:"source" json:"source" mapstructure:"source"`
|
||||||
Exclusions []string `yaml:"exclude" json:"exclude" mapstructure:"exclude"`
|
Exclusions []string `yaml:"exclude" json:"exclude" mapstructure:"exclude"`
|
||||||
|
@ -163,6 +164,9 @@ func (cfg *Catalog) AddFlags(flags clio.FlagSet) {
|
||||||
flags.StringVarP(&cfg.Scope, "scope", "s",
|
flags.StringVarP(&cfg.Scope, "scope", "s",
|
||||||
fmt.Sprintf("selection of layers to catalog, options=%v", validScopeValues))
|
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", "",
|
flags.StringVarP(&cfg.Platform, "platform", "",
|
||||||
"an optional platform specifier for container image sources (e.g. 'linux/arm64', 'linux/arm64/v8', 'arm64', 'linux')")
|
"an optional platform specifier for container image sources (e.g. 'linux/arm64', 'linux/arm64/v8', 'arm64', 'linux')")
|
||||||
|
|
||||||
|
@ -220,6 +224,8 @@ func (cfg *Catalog) PostLoad() error {
|
||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cfg.From = flatten(cfg.From)
|
||||||
|
|
||||||
cfg.Catalogers = flatten(cfg.Catalogers)
|
cfg.Catalogers = flatten(cfg.Catalogers)
|
||||||
cfg.DefaultCatalogers = flatten(cfg.DefaultCatalogers)
|
cfg.DefaultCatalogers = flatten(cfg.DefaultCatalogers)
|
||||||
cfg.SelectCatalogers = flatten(cfg.SelectCatalogers)
|
cfg.SelectCatalogers = flatten(cfg.SelectCatalogers)
|
||||||
|
|
|
@ -6,6 +6,8 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/scylladb/go-set/strset"
|
"github.com/scylladb/go-set/strset"
|
||||||
|
|
||||||
|
"github.com/anchore/syft/syft/source/sourceproviders"
|
||||||
)
|
)
|
||||||
|
|
||||||
type sourceConfig struct {
|
type sourceConfig struct {
|
||||||
|
@ -25,12 +27,13 @@ type imageSource struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func defaultSourceConfig() sourceConfig {
|
func defaultSourceConfig() sourceConfig {
|
||||||
|
var digests []string
|
||||||
|
for _, alg := range sourceproviders.DefaultConfig().DigestAlgorithms {
|
||||||
|
digests = append(digests, alg.String())
|
||||||
|
}
|
||||||
return sourceConfig{
|
return sourceConfig{
|
||||||
File: fileSource{
|
File: fileSource{
|
||||||
Digests: []string{"sha256"},
|
Digests: digests,
|
||||||
},
|
|
||||||
Image: imageSource{
|
|
||||||
DefaultPullSource: "",
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,15 +22,11 @@ func BenchmarkImagePackageCatalogers(b *testing.B) {
|
||||||
tarPath := imagetest.GetFixtureImageTarPath(b, fixtureImageName)
|
tarPath := imagetest.GetFixtureImageTarPath(b, fixtureImageName)
|
||||||
|
|
||||||
// get the source object for the image
|
// get the source object for the image
|
||||||
userInput := "docker-archive:" + tarPath
|
theSource, err := syft.GetSource(context.Background(), tarPath, syft.DefaultGetSourceConfig().WithSources("docker-archive"))
|
||||||
detection, err := source.Detect(userInput, source.DefaultDetectConfig())
|
|
||||||
require.NoError(b, err)
|
|
||||||
|
|
||||||
theSource, err := detection.NewSource(source.DefaultDetectionSourceConfig())
|
|
||||||
require.NoError(b, err)
|
require.NoError(b, err)
|
||||||
|
|
||||||
b.Cleanup(func() {
|
b.Cleanup(func() {
|
||||||
theSource.Close()
|
require.NoError(b, theSource.Close())
|
||||||
})
|
})
|
||||||
|
|
||||||
// build the SBOM
|
// build the SBOM
|
||||||
|
|
|
@ -34,17 +34,13 @@ func catalogFixtureImageWithConfig(t *testing.T, fixtureImageName string, cfg *s
|
||||||
// get the fixture image tar file
|
// get the fixture image tar file
|
||||||
imagetest.GetFixtureImage(t, "docker-archive", fixtureImageName)
|
imagetest.GetFixtureImage(t, "docker-archive", fixtureImageName)
|
||||||
tarPath := imagetest.GetFixtureImageTarPath(t, fixtureImageName)
|
tarPath := imagetest.GetFixtureImageTarPath(t, fixtureImageName)
|
||||||
userInput := "docker-archive:" + tarPath
|
|
||||||
|
|
||||||
// get the source to build an SBOM against
|
// get the source to build an SBOM against
|
||||||
detection, err := source.Detect(userInput, source.DefaultDetectConfig())
|
theSource, err := syft.GetSource(context.Background(), tarPath, syft.DefaultGetSourceConfig().WithSources("docker-archive"))
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
theSource, err := detection.NewSource(source.DefaultDetectionSourceConfig())
|
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
t.Cleanup(func() {
|
t.Cleanup(func() {
|
||||||
theSource.Close()
|
require.NoError(t, theSource.Close())
|
||||||
})
|
})
|
||||||
|
|
||||||
s, err := syft.CreateSBOM(context.Background(), theSource, cfg)
|
s, err := syft.CreateSBOM(context.Background(), theSource, cfg)
|
||||||
|
@ -71,14 +67,10 @@ func catalogDirectoryWithConfig(t *testing.T, dir string, cfg *syft.CreateSBOMCo
|
||||||
cfg.CatalogerSelection = cfg.CatalogerSelection.WithDefaults(pkgcataloging.DirectoryTag)
|
cfg.CatalogerSelection = cfg.CatalogerSelection.WithDefaults(pkgcataloging.DirectoryTag)
|
||||||
|
|
||||||
// get the source to build an sbom against
|
// get the source to build an sbom against
|
||||||
userInput := "dir:" + dir
|
theSource, err := syft.GetSource(context.Background(), dir, syft.DefaultGetSourceConfig().WithSources("dir"))
|
||||||
detection, err := source.Detect(userInput, source.DefaultDetectConfig())
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
theSource, err := detection.NewSource(source.DefaultDetectionSourceConfig())
|
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
t.Cleanup(func() {
|
t.Cleanup(func() {
|
||||||
theSource.Close()
|
require.NoError(t, theSource.Close())
|
||||||
})
|
})
|
||||||
|
|
||||||
// build the SBOM
|
// build the SBOM
|
||||||
|
|
|
@ -44,17 +44,7 @@ func imageReference() string {
|
||||||
func getSource(input string) source.Source {
|
func getSource(input string) source.Source {
|
||||||
fmt.Println("detecting source type for input:", input, "...")
|
fmt.Println("detecting source type for input:", input, "...")
|
||||||
|
|
||||||
detection, err := source.Detect(input,
|
src, err := syft.GetSource(context.Background(), input, nil)
|
||||||
source.DetectConfig{
|
|
||||||
DefaultImageSource: "docker",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
src, err := detection.NewSource(source.DefaultDetectionSourceConfig())
|
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
|
|
|
@ -37,17 +37,7 @@ func imageReference() string {
|
||||||
}
|
}
|
||||||
|
|
||||||
func getSource(input string) source.Source {
|
func getSource(input string) source.Source {
|
||||||
detection, err := source.Detect(input,
|
src, err := syft.GetSource(context.Background(), input, nil)
|
||||||
source.DetectConfig{
|
|
||||||
DefaultImageSource: "docker",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
src, err := detection.NewSource(source.DefaultDetectionSourceConfig())
|
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
|
|
|
@ -41,17 +41,7 @@ func imageReference() string {
|
||||||
}
|
}
|
||||||
|
|
||||||
func getSource(input string) source.Source {
|
func getSource(input string) source.Source {
|
||||||
detection, err := source.Detect(input,
|
src, err := syft.GetSource(context.Background(), input, nil)
|
||||||
source.DetectConfig{
|
|
||||||
DefaultImageSource: "docker",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
src, err := detection.NewSource(source.DefaultDetectionSourceConfig())
|
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
|
|
|
@ -1,10 +1,15 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
|
"github.com/anchore/go-collections"
|
||||||
|
"github.com/anchore/stereoscope"
|
||||||
|
"github.com/anchore/syft/syft"
|
||||||
"github.com/anchore/syft/syft/source"
|
"github.com/anchore/syft/syft/source"
|
||||||
|
"github.com/anchore/syft/syft/source/sourceproviders"
|
||||||
)
|
)
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
@ -28,18 +33,18 @@ import (
|
||||||
const defaultImage = "alpine:3.19"
|
const defaultImage = "alpine:3.19"
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
detection, err := source.Detect(
|
userInput := imageReference()
|
||||||
imageReference(),
|
|
||||||
source.DetectConfig{
|
|
||||||
DefaultImageSource: "docker",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
if err != nil {
|
// parse the scheme against the known set of schemes
|
||||||
panic(err)
|
schemeSource, newUserInput := stereoscope.ExtractSchemeSource(userInput, allSourceTags()...)
|
||||||
|
|
||||||
|
// set up the GetSourceConfig
|
||||||
|
getSourceCfg := syft.DefaultGetSourceConfig()
|
||||||
|
if schemeSource != "" {
|
||||||
|
getSourceCfg = getSourceCfg.WithSources(schemeSource)
|
||||||
|
userInput = newUserInput
|
||||||
}
|
}
|
||||||
|
src, err := syft.GetSource(context.Background(), userInput, getSourceCfg)
|
||||||
src, err := detection.NewSource(source.DefaultDetectionSourceConfig())
|
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
|
@ -60,3 +65,7 @@ func imageReference() string {
|
||||||
}
|
}
|
||||||
return defaultImage
|
return defaultImage
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func allSourceTags() []string {
|
||||||
|
return collections.TaggedValueSet[source.Provider]{}.Join(sourceproviders.All("", nil)...).Tags()
|
||||||
|
}
|
||||||
|
|
|
@ -1,11 +1,13 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"github.com/anchore/stereoscope/pkg/image"
|
"github.com/anchore/stereoscope"
|
||||||
"github.com/anchore/syft/syft/source"
|
"github.com/anchore/stereoscope/pkg/image/oci"
|
||||||
|
"github.com/anchore/syft/syft/source/stereoscopesource"
|
||||||
)
|
)
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
@ -16,22 +18,15 @@ import (
|
||||||
const defaultImage = "alpine:3.19"
|
const defaultImage = "alpine:3.19"
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
platform, err := image.NewPlatform("linux/amd64")
|
// using oci.Registry causes the lookup to always use the registry, there are several other "Source" options here
|
||||||
|
img, err := stereoscope.GetImageFromSource(context.Background(), imageReference(), oci.Registry, stereoscope.WithPlatform("linux/amd64"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
src, err := source.NewFromStereoscopeImage(
|
src := stereoscopesource.New(img, stereoscopesource.ImageConfig{
|
||||||
source.StereoscopeImageConfig{
|
|
||||||
Reference: imageReference(),
|
Reference: imageReference(),
|
||||||
From: image.OciRegistrySource, // always use the registry, there are several other "Source" options here
|
})
|
||||||
Platform: platform,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Show a basic description of the source to the screen
|
// Show a basic description of the source to the screen
|
||||||
enc := json.NewEncoder(os.Stdout)
|
enc := json.NewEncoder(os.Stdout)
|
||||||
|
|
4
go.mod
4
go.mod
|
@ -16,7 +16,7 @@ require (
|
||||||
github.com/anchore/go-testutils v0.0.0-20200925183923-d5f45b0d3c04
|
github.com/anchore/go-testutils v0.0.0-20200925183923-d5f45b0d3c04
|
||||||
github.com/anchore/go-version v1.2.2-0.20200701162849-18adb9c92b9b
|
github.com/anchore/go-version v1.2.2-0.20200701162849-18adb9c92b9b
|
||||||
github.com/anchore/packageurl-go v0.1.1-0.20240202171727-877e1747d426
|
github.com/anchore/packageurl-go v0.1.1-0.20240202171727-877e1747d426
|
||||||
github.com/anchore/stereoscope v0.0.2-0.20240216182029-6171ee21e1d5
|
github.com/anchore/stereoscope v0.0.2-0.20240221144950-cf0e754f5b56
|
||||||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be
|
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be
|
||||||
// we are hinting brotli to latest due to warning when installing archiver v3:
|
// we are hinting brotli to latest due to warning when installing archiver v3:
|
||||||
// go: warning: github.com/andybalholm/brotli@v1.0.1: retracted by module author: occasional panics and data corruption
|
// go: warning: github.com/andybalholm/brotli@v1.0.1: retracted by module author: occasional panics and data corruption
|
||||||
|
@ -80,6 +80,8 @@ require (
|
||||||
modernc.org/sqlite v1.29.2
|
modernc.org/sqlite v1.29.2
|
||||||
)
|
)
|
||||||
|
|
||||||
|
require github.com/anchore/go-collections v0.0.0-20240216171411-9321230ce537
|
||||||
|
|
||||||
require (
|
require (
|
||||||
dario.cat/mergo v1.0.0 // indirect
|
dario.cat/mergo v1.0.0 // indirect
|
||||||
github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 // indirect
|
github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 // indirect
|
||||||
|
|
6
go.sum
6
go.sum
|
@ -97,6 +97,8 @@ github.com/anchore/clio v0.0.0-20240209204744-cb94e40a4f65 h1:u9XrEabKlGPsrmRvAE
|
||||||
github.com/anchore/clio v0.0.0-20240209204744-cb94e40a4f65/go.mod h1:8Jr7CjmwFVcBPtkJdTpaAGHimoGJGfbExypjzOu87Og=
|
github.com/anchore/clio v0.0.0-20240209204744-cb94e40a4f65/go.mod h1:8Jr7CjmwFVcBPtkJdTpaAGHimoGJGfbExypjzOu87Og=
|
||||||
github.com/anchore/fangs v0.0.0-20231201140849-5075d28d6d8b h1:L/djgY7ZbZ/38+wUtdkk398W3PIBJLkt1N8nU/7e47A=
|
github.com/anchore/fangs v0.0.0-20231201140849-5075d28d6d8b h1:L/djgY7ZbZ/38+wUtdkk398W3PIBJLkt1N8nU/7e47A=
|
||||||
github.com/anchore/fangs v0.0.0-20231201140849-5075d28d6d8b/go.mod h1:TLcE0RE5+8oIx2/NPWem/dq1DeaMoC+fPEH7hoSzPLo=
|
github.com/anchore/fangs v0.0.0-20231201140849-5075d28d6d8b/go.mod h1:TLcE0RE5+8oIx2/NPWem/dq1DeaMoC+fPEH7hoSzPLo=
|
||||||
|
github.com/anchore/go-collections v0.0.0-20240216171411-9321230ce537 h1:GjNGuwK5jWjJMyVppBjYS54eOiiSNv4Ba869k4wh72Q=
|
||||||
|
github.com/anchore/go-collections v0.0.0-20240216171411-9321230ce537/go.mod h1:1aiktV46ATCkuVg0O573ZrH56BUawTECPETbZyBcqT8=
|
||||||
github.com/anchore/go-logger v0.0.0-20230725134548-c21dafa1ec5a h1:nJ2G8zWKASyVClGVgG7sfM5mwoZlZ2zYpIzN2OhjWkw=
|
github.com/anchore/go-logger v0.0.0-20230725134548-c21dafa1ec5a h1:nJ2G8zWKASyVClGVgG7sfM5mwoZlZ2zYpIzN2OhjWkw=
|
||||||
github.com/anchore/go-logger v0.0.0-20230725134548-c21dafa1ec5a/go.mod h1:ubLFmlsv8/DFUQrZwY5syT5/8Er3ugSr4rDFwHsE3hg=
|
github.com/anchore/go-logger v0.0.0-20230725134548-c21dafa1ec5a/go.mod h1:ubLFmlsv8/DFUQrZwY5syT5/8Er3ugSr4rDFwHsE3hg=
|
||||||
github.com/anchore/go-macholibre v0.0.0-20220308212642-53e6d0aaf6fb h1:iDMnx6LIjtjZ46C0akqveX83WFzhpTD3eqOthawb5vU=
|
github.com/anchore/go-macholibre v0.0.0-20220308212642-53e6d0aaf6fb h1:iDMnx6LIjtjZ46C0akqveX83WFzhpTD3eqOthawb5vU=
|
||||||
|
@ -109,8 +111,8 @@ github.com/anchore/go-version v1.2.2-0.20200701162849-18adb9c92b9b h1:e1bmaoJfZV
|
||||||
github.com/anchore/go-version v1.2.2-0.20200701162849-18adb9c92b9b/go.mod h1:Bkc+JYWjMCF8OyZ340IMSIi2Ebf3uwByOk6ho4wne1E=
|
github.com/anchore/go-version v1.2.2-0.20200701162849-18adb9c92b9b/go.mod h1:Bkc+JYWjMCF8OyZ340IMSIi2Ebf3uwByOk6ho4wne1E=
|
||||||
github.com/anchore/packageurl-go v0.1.1-0.20240202171727-877e1747d426 h1:agoiZchSf1Nnnos1azwIg5hk5Ao9TzZNBD9++AChGEg=
|
github.com/anchore/packageurl-go v0.1.1-0.20240202171727-877e1747d426 h1:agoiZchSf1Nnnos1azwIg5hk5Ao9TzZNBD9++AChGEg=
|
||||||
github.com/anchore/packageurl-go v0.1.1-0.20240202171727-877e1747d426/go.mod h1:Blo6OgJNiYF41ufcgHKkbCKF2MDOMlrqhXv/ij6ocR4=
|
github.com/anchore/packageurl-go v0.1.1-0.20240202171727-877e1747d426/go.mod h1:Blo6OgJNiYF41ufcgHKkbCKF2MDOMlrqhXv/ij6ocR4=
|
||||||
github.com/anchore/stereoscope v0.0.2-0.20240216182029-6171ee21e1d5 h1:o//fhRcSpOYHC/xG/HiI6ddtSMiRgHlB96xJQXZawZM=
|
github.com/anchore/stereoscope v0.0.2-0.20240221144950-cf0e754f5b56 h1:iHvTXZA+qEozPGRRuW1Mv7r7w2fHeJdzWDx+YsSIbyg=
|
||||||
github.com/anchore/stereoscope v0.0.2-0.20240216182029-6171ee21e1d5/go.mod h1:o0TqYkefad6kIPtmbigFKss7P48z4bjd8Vp5Wklbf3Y=
|
github.com/anchore/stereoscope v0.0.2-0.20240221144950-cf0e754f5b56/go.mod h1:evQiJMQG56Z7/L5uhA8kfhhjF6ESJUZzUH9ms6bQ2Co=
|
||||||
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8=
|
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8=
|
||||||
github.com/andybalholm/brotli v1.0.1/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y=
|
github.com/andybalholm/brotli v1.0.1/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y=
|
||||||
github.com/andybalholm/brotli v1.0.4 h1:V7DdXeJtZscaqfNuAdSRuRFzuiKlHSC/Zh3zl9qY3JY=
|
github.com/andybalholm/brotli v1.0.4 h1:V7DdXeJtZscaqfNuAdSRuRFzuiKlHSC/Zh3zl9qY3JY=
|
||||||
|
|
|
@ -345,9 +345,9 @@ func (c *CreateSBOMConfig) Create(ctx context.Context, src source.Source) (*sbom
|
||||||
|
|
||||||
func findDefaultTag(src source.Description) (string, error) {
|
func findDefaultTag(src source.Description) (string, error) {
|
||||||
switch m := src.Metadata.(type) {
|
switch m := src.Metadata.(type) {
|
||||||
case source.StereoscopeImageSourceMetadata:
|
case source.ImageMetadata:
|
||||||
return pkgcataloging.ImageTag, nil
|
return pkgcataloging.ImageTag, nil
|
||||||
case source.FileSourceMetadata, source.DirectorySourceMetadata:
|
case source.FileMetadata, source.DirectoryMetadata:
|
||||||
return pkgcataloging.DirectoryTag, nil
|
return pkgcataloging.DirectoryTag, nil
|
||||||
default:
|
default:
|
||||||
return "", fmt.Errorf("unable to determine default cataloger tag for source type=%T", m)
|
return "", fmt.Errorf("unable to determine default cataloger tag for source type=%T", m)
|
||||||
|
|
|
@ -62,15 +62,15 @@ func TestCreateSBOMConfig_makeTaskGroups(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
imgSrc := source.Description{
|
imgSrc := source.Description{
|
||||||
Metadata: source.StereoscopeImageSourceMetadata{},
|
Metadata: source.ImageMetadata{},
|
||||||
}
|
}
|
||||||
|
|
||||||
dirSrc := source.Description{
|
dirSrc := source.Description{
|
||||||
Metadata: source.DirectorySourceMetadata{},
|
Metadata: source.DirectoryMetadata{},
|
||||||
}
|
}
|
||||||
|
|
||||||
fileSrc := source.Description{
|
fileSrc := source.Description{
|
||||||
Metadata: source.FileSourceMetadata{},
|
Metadata: source.FileMetadata{},
|
||||||
}
|
}
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
|
@ -437,21 +437,21 @@ func Test_findDefaultTag(t *testing.T) {
|
||||||
{
|
{
|
||||||
name: "image",
|
name: "image",
|
||||||
src: source.Description{
|
src: source.Description{
|
||||||
Metadata: source.StereoscopeImageSourceMetadata{},
|
Metadata: source.ImageMetadata{},
|
||||||
},
|
},
|
||||||
want: pkgcataloging.ImageTag,
|
want: pkgcataloging.ImageTag,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "directory",
|
name: "directory",
|
||||||
src: source.Description{
|
src: source.Description{
|
||||||
Metadata: source.DirectorySourceMetadata{},
|
Metadata: source.DirectoryMetadata{},
|
||||||
},
|
},
|
||||||
want: pkgcataloging.DirectoryTag,
|
want: pkgcataloging.DirectoryTag,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "file",
|
name: "file",
|
||||||
src: source.Description{
|
src: source.Description{
|
||||||
Metadata: source.FileSourceMetadata{},
|
Metadata: source.FileMetadata{},
|
||||||
},
|
},
|
||||||
want: pkgcataloging.DirectoryTag, // not a mistake...
|
want: pkgcataloging.DirectoryTag, // not a mistake...
|
||||||
},
|
},
|
||||||
|
|
|
@ -17,6 +17,8 @@ import (
|
||||||
intFile "github.com/anchore/syft/internal/file"
|
intFile "github.com/anchore/syft/internal/file"
|
||||||
"github.com/anchore/syft/syft/file"
|
"github.com/anchore/syft/syft/file"
|
||||||
"github.com/anchore/syft/syft/source"
|
"github.com/anchore/syft/syft/source"
|
||||||
|
"github.com/anchore/syft/syft/source/directorysource"
|
||||||
|
"github.com/anchore/syft/syft/source/stereoscopesource"
|
||||||
)
|
)
|
||||||
|
|
||||||
func testDigests(t testing.TB, root string, files []string, hashes ...crypto.Hash) map[file.Coordinates][]file.Digest {
|
func testDigests(t testing.TB, root string, files []string, hashes ...crypto.Hash) map[file.Coordinates][]file.Digest {
|
||||||
|
@ -77,7 +79,7 @@ func TestDigestsCataloger(t *testing.T) {
|
||||||
t.Run(test.name, func(t *testing.T) {
|
t.Run(test.name, func(t *testing.T) {
|
||||||
c := NewCataloger(test.digests)
|
c := NewCataloger(test.digests)
|
||||||
|
|
||||||
src, err := source.NewFromDirectoryPath("test-fixtures/last/")
|
src, err := directorysource.NewFromPath("test-fixtures/last/")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
resolver, err := src.FileResolver(source.SquashedScope)
|
resolver, err := src.FileResolver(source.SquashedScope)
|
||||||
|
@ -96,10 +98,9 @@ func TestDigestsCataloger_MixFileTypes(t *testing.T) {
|
||||||
|
|
||||||
img := imagetest.GetFixtureImage(t, "docker-archive", testImage)
|
img := imagetest.GetFixtureImage(t, "docker-archive", testImage)
|
||||||
|
|
||||||
src, err := source.NewFromStereoscopeImageObject(img, testImage, nil)
|
src := stereoscopesource.New(img, stereoscopesource.ImageConfig{
|
||||||
if err != nil {
|
Reference: testImage,
|
||||||
t.Fatalf("could not create source: %+v", err)
|
})
|
||||||
}
|
|
||||||
|
|
||||||
resolver, err := src.FileResolver(source.SquashedScope)
|
resolver, err := src.FileResolver(source.SquashedScope)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -169,8 +170,9 @@ func TestFileDigestCataloger_GivenCoordinates(t *testing.T) {
|
||||||
|
|
||||||
c := NewCataloger([]crypto.Hash{crypto.SHA256})
|
c := NewCataloger([]crypto.Hash{crypto.SHA256})
|
||||||
|
|
||||||
src, err := source.NewFromStereoscopeImageObject(img, testImage, nil)
|
src := stereoscopesource.New(img, stereoscopesource.ImageConfig{
|
||||||
require.NoError(t, err)
|
Reference: testImage,
|
||||||
|
})
|
||||||
|
|
||||||
resolver, err := src.FileResolver(source.SquashedScope)
|
resolver, err := src.FileResolver(source.SquashedScope)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
|
@ -12,6 +12,7 @@ import (
|
||||||
"github.com/anchore/stereoscope/pkg/imagetest"
|
"github.com/anchore/stereoscope/pkg/imagetest"
|
||||||
"github.com/anchore/syft/syft/file"
|
"github.com/anchore/syft/syft/file"
|
||||||
"github.com/anchore/syft/syft/source"
|
"github.com/anchore/syft/syft/source"
|
||||||
|
"github.com/anchore/syft/syft/source/stereoscopesource"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestFileMetadataCataloger(t *testing.T) {
|
func TestFileMetadataCataloger(t *testing.T) {
|
||||||
|
@ -21,8 +22,9 @@ func TestFileMetadataCataloger(t *testing.T) {
|
||||||
|
|
||||||
c := NewCataloger()
|
c := NewCataloger()
|
||||||
|
|
||||||
src, err := source.NewFromStereoscopeImageObject(img, testImage, nil)
|
src := stereoscopesource.New(img, stereoscopesource.ImageConfig{
|
||||||
require.NoError(t, err)
|
Reference: testImage,
|
||||||
|
})
|
||||||
|
|
||||||
resolver, err := src.FileResolver(source.SquashedScope)
|
resolver, err := src.FileResolver(source.SquashedScope)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
@ -159,8 +161,9 @@ func TestFileMetadataCataloger_GivenCoordinates(t *testing.T) {
|
||||||
|
|
||||||
c := NewCataloger()
|
c := NewCataloger()
|
||||||
|
|
||||||
src, err := source.NewFromStereoscopeImageObject(img, testImage, nil)
|
src := stereoscopesource.New(img, stereoscopesource.ImageConfig{
|
||||||
require.NoError(t, err)
|
Reference: testImage,
|
||||||
|
})
|
||||||
|
|
||||||
resolver, err := src.FileResolver(source.SquashedScope)
|
resolver, err := src.FileResolver(source.SquashedScope)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
|
@ -12,6 +12,8 @@ import (
|
||||||
"github.com/anchore/stereoscope/pkg/imagetest"
|
"github.com/anchore/stereoscope/pkg/imagetest"
|
||||||
"github.com/anchore/syft/syft/file"
|
"github.com/anchore/syft/syft/file"
|
||||||
"github.com/anchore/syft/syft/source"
|
"github.com/anchore/syft/syft/source"
|
||||||
|
"github.com/anchore/syft/syft/source/directorysource"
|
||||||
|
"github.com/anchore/syft/syft/source/stereoscopesource"
|
||||||
)
|
)
|
||||||
|
|
||||||
func Test_allRegularFiles(t *testing.T) {
|
func Test_allRegularFiles(t *testing.T) {
|
||||||
|
@ -28,8 +30,9 @@ func Test_allRegularFiles(t *testing.T) {
|
||||||
|
|
||||||
img := imagetest.GetFixtureImage(t, "docker-archive", testImage)
|
img := imagetest.GetFixtureImage(t, "docker-archive", testImage)
|
||||||
|
|
||||||
s, err := source.NewFromStereoscopeImageObject(img, testImage, nil)
|
s := stereoscopesource.New(img, stereoscopesource.ImageConfig{
|
||||||
require.NoError(t, err)
|
Reference: testImage,
|
||||||
|
})
|
||||||
|
|
||||||
r, err := s.FileResolver(source.SquashedScope)
|
r, err := s.FileResolver(source.SquashedScope)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
@ -42,7 +45,7 @@ func Test_allRegularFiles(t *testing.T) {
|
||||||
{
|
{
|
||||||
name: "directory",
|
name: "directory",
|
||||||
setup: func() file.Resolver {
|
setup: func() file.Resolver {
|
||||||
s, err := source.NewFromDirectoryPath("test-fixtures/symlinked-root/nested/link-root")
|
s, err := directorysource.NewFromPath("test-fixtures/symlinked-root/nested/link-root")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
r, err := s.FileResolver(source.SquashedScope)
|
r, err := s.FileResolver(source.SquashedScope)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
|
@ -208,7 +208,7 @@ func toDependencies(relationships []artifact.Relationship) []cyclonedx.Dependenc
|
||||||
}
|
}
|
||||||
|
|
||||||
func toBomProperties(srcMetadata source.Description) *[]cyclonedx.Property {
|
func toBomProperties(srcMetadata source.Description) *[]cyclonedx.Property {
|
||||||
metadata, ok := srcMetadata.Metadata.(source.StereoscopeImageSourceMetadata)
|
metadata, ok := srcMetadata.Metadata.(source.ImageMetadata)
|
||||||
if ok {
|
if ok {
|
||||||
props := helpers.EncodeProperties(metadata.Labels, "syft:image:labels")
|
props := helpers.EncodeProperties(metadata.Labels, "syft:image:labels")
|
||||||
return &props
|
return &props
|
||||||
|
@ -220,7 +220,7 @@ func toBomDescriptorComponent(srcMetadata source.Description) *cyclonedx.Compone
|
||||||
name := srcMetadata.Name
|
name := srcMetadata.Name
|
||||||
version := srcMetadata.Version
|
version := srcMetadata.Version
|
||||||
switch metadata := srcMetadata.Metadata.(type) {
|
switch metadata := srcMetadata.Metadata.(type) {
|
||||||
case source.StereoscopeImageSourceMetadata:
|
case source.ImageMetadata:
|
||||||
if name == "" {
|
if name == "" {
|
||||||
name = metadata.UserInput
|
name = metadata.UserInput
|
||||||
}
|
}
|
||||||
|
@ -237,7 +237,7 @@ func toBomDescriptorComponent(srcMetadata source.Description) *cyclonedx.Compone
|
||||||
Name: name,
|
Name: name,
|
||||||
Version: version,
|
Version: version,
|
||||||
}
|
}
|
||||||
case source.DirectorySourceMetadata:
|
case source.DirectoryMetadata:
|
||||||
if name == "" {
|
if name == "" {
|
||||||
name = metadata.Path
|
name = metadata.Path
|
||||||
}
|
}
|
||||||
|
@ -252,7 +252,7 @@ func toBomDescriptorComponent(srcMetadata source.Description) *cyclonedx.Compone
|
||||||
Name: name,
|
Name: name,
|
||||||
Version: version,
|
Version: version,
|
||||||
}
|
}
|
||||||
case source.FileSourceMetadata:
|
case source.FileMetadata:
|
||||||
if name == "" {
|
if name == "" {
|
||||||
name = metadata.Path
|
name = metadata.Path
|
||||||
}
|
}
|
||||||
|
|
|
@ -160,7 +160,7 @@ func Test_toBomDescriptor(t *testing.T) {
|
||||||
name: "test-image",
|
name: "test-image",
|
||||||
version: "1.0.0",
|
version: "1.0.0",
|
||||||
srcMetadata: source.Description{
|
srcMetadata: source.Description{
|
||||||
Metadata: source.StereoscopeImageSourceMetadata{
|
Metadata: source.ImageMetadata{
|
||||||
Labels: map[string]string{
|
Labels: map[string]string{
|
||||||
"key1": "value1",
|
"key1": "value1",
|
||||||
},
|
},
|
||||||
|
|
|
@ -181,7 +181,7 @@ func toRootPackage(s source.Description) *spdx.Package {
|
||||||
purpose := ""
|
purpose := ""
|
||||||
var checksums []spdx.Checksum
|
var checksums []spdx.Checksum
|
||||||
switch m := s.Metadata.(type) {
|
switch m := s.Metadata.(type) {
|
||||||
case source.StereoscopeImageSourceMetadata:
|
case source.ImageMetadata:
|
||||||
prefix = prefixImage
|
prefix = prefixImage
|
||||||
purpose = spdxPrimaryPurposeContainer
|
purpose = spdxPrimaryPurposeContainer
|
||||||
|
|
||||||
|
@ -211,11 +211,11 @@ func toRootPackage(s source.Description) *spdx.Package {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
case source.DirectorySourceMetadata:
|
case source.DirectoryMetadata:
|
||||||
prefix = prefixDirectory
|
prefix = prefixDirectory
|
||||||
purpose = spdxPrimaryPurposeFile
|
purpose = spdxPrimaryPurposeFile
|
||||||
|
|
||||||
case source.FileSourceMetadata:
|
case source.FileMetadata:
|
||||||
prefix = prefixFile
|
prefix = prefixFile
|
||||||
purpose = spdxPrimaryPurposeFile
|
purpose = spdxPrimaryPurposeFile
|
||||||
|
|
||||||
|
|
|
@ -35,7 +35,7 @@ func Test_toFormatModel(t *testing.T) {
|
||||||
Source: source.Description{
|
Source: source.Description{
|
||||||
Name: "alpine",
|
Name: "alpine",
|
||||||
Version: "sha256:d34db33f",
|
Version: "sha256:d34db33f",
|
||||||
Metadata: source.StereoscopeImageSourceMetadata{
|
Metadata: source.ImageMetadata{
|
||||||
UserInput: "alpine:latest",
|
UserInput: "alpine:latest",
|
||||||
ManifestDigest: "sha256:d34db33f",
|
ManifestDigest: "sha256:d34db33f",
|
||||||
},
|
},
|
||||||
|
@ -106,7 +106,7 @@ func Test_toFormatModel(t *testing.T) {
|
||||||
in: sbom.SBOM{
|
in: sbom.SBOM{
|
||||||
Source: source.Description{
|
Source: source.Description{
|
||||||
Name: "some/directory",
|
Name: "some/directory",
|
||||||
Metadata: source.DirectorySourceMetadata{
|
Metadata: source.DirectoryMetadata{
|
||||||
Path: "some/directory",
|
Path: "some/directory",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -170,7 +170,7 @@ func Test_toFormatModel(t *testing.T) {
|
||||||
Source: source.Description{
|
Source: source.Description{
|
||||||
Name: "path/to/some.file",
|
Name: "path/to/some.file",
|
||||||
Version: "sha256:d34db33f",
|
Version: "sha256:d34db33f",
|
||||||
Metadata: source.FileSourceMetadata{
|
Metadata: source.FileMetadata{
|
||||||
Path: "path/to/some.file",
|
Path: "path/to/some.file",
|
||||||
Digests: []file.Digest{
|
Digests: []file.Digest{
|
||||||
{
|
{
|
||||||
|
|
|
@ -146,7 +146,7 @@ func containerSource(p *spdx.Package) source.Description {
|
||||||
ID: id,
|
ID: id,
|
||||||
Name: p.PackageName,
|
Name: p.PackageName,
|
||||||
Version: p.PackageVersion,
|
Version: p.PackageVersion,
|
||||||
Metadata: source.StereoscopeImageSourceMetadata{
|
Metadata: source.ImageMetadata{
|
||||||
UserInput: container,
|
UserInput: container,
|
||||||
ID: id,
|
ID: id,
|
||||||
Layers: nil, // TODO handle formats with nested layer packages like Tern and K8s BOM tool
|
Layers: nil, // TODO handle formats with nested layer packages like Tern and K8s BOM tool
|
||||||
|
@ -187,7 +187,7 @@ func fileSource(p *spdx.Package) source.Description {
|
||||||
func fileSourceMetadata(p *spdx.Package) (any, string) {
|
func fileSourceMetadata(p *spdx.Package) (any, string) {
|
||||||
version := p.PackageVersion
|
version := p.PackageVersion
|
||||||
|
|
||||||
m := source.FileSourceMetadata{
|
m := source.FileMetadata{
|
||||||
Path: p.PackageName,
|
Path: p.PackageName,
|
||||||
}
|
}
|
||||||
// if this is a Syft SBOM, we might have output a digest as the version
|
// if this is a Syft SBOM, we might have output a digest as the version
|
||||||
|
@ -206,7 +206,7 @@ func fileSourceMetadata(p *spdx.Package) (any, string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func directorySourceMetadata(p *spdx.Package) (any, string) {
|
func directorySourceMetadata(p *spdx.Package) (any, string) {
|
||||||
return source.DirectorySourceMetadata{
|
return source.DirectoryMetadata{
|
||||||
Path: p.PackageName,
|
Path: p.PackageName,
|
||||||
Base: "",
|
Base: "",
|
||||||
}, p.PackageVersion
|
}, p.PackageVersion
|
||||||
|
@ -229,15 +229,15 @@ func extractSourceFromNamespace(ns string) source.Description {
|
||||||
switch p {
|
switch p {
|
||||||
case helpers.InputFile:
|
case helpers.InputFile:
|
||||||
return source.Description{
|
return source.Description{
|
||||||
Metadata: source.FileSourceMetadata{},
|
Metadata: source.FileMetadata{},
|
||||||
}
|
}
|
||||||
case helpers.InputImage:
|
case helpers.InputImage:
|
||||||
return source.Description{
|
return source.Description{
|
||||||
Metadata: source.StereoscopeImageSourceMetadata{},
|
Metadata: source.ImageMetadata{},
|
||||||
}
|
}
|
||||||
case helpers.InputDirectory:
|
case helpers.InputDirectory:
|
||||||
return source.Description{
|
return source.Description{
|
||||||
Metadata: source.DirectorySourceMetadata{},
|
Metadata: source.DirectoryMetadata{},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -199,15 +199,15 @@ func TestExtractSourceFromNamespaces(t *testing.T) {
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
namespace: "https://anchore.com/syft/file/d42b01d0-7325-409b-b03f-74082935c4d3",
|
namespace: "https://anchore.com/syft/file/d42b01d0-7325-409b-b03f-74082935c4d3",
|
||||||
expected: source.FileSourceMetadata{},
|
expected: source.FileMetadata{},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
namespace: "https://anchore.com/syft/image/d42b01d0-7325-409b-b03f-74082935c4d3",
|
namespace: "https://anchore.com/syft/image/d42b01d0-7325-409b-b03f-74082935c4d3",
|
||||||
expected: source.StereoscopeImageSourceMetadata{},
|
expected: source.ImageMetadata{},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
namespace: "https://anchore.com/syft/dir/d42b01d0-7325-409b-b03f-74082935c4d3",
|
namespace: "https://anchore.com/syft/dir/d42b01d0-7325-409b-b03f-74082935c4d3",
|
||||||
expected: source.DirectorySourceMetadata{},
|
expected: source.DirectoryMetadata{},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
namespace: "https://another-host/blob/123",
|
namespace: "https://another-host/blob/123",
|
||||||
|
@ -460,7 +460,7 @@ func Test_convertToAndFromFormat(t *testing.T) {
|
||||||
name: "image source",
|
name: "image source",
|
||||||
source: source.Description{
|
source: source.Description{
|
||||||
ID: "DocumentRoot-Image-some-image",
|
ID: "DocumentRoot-Image-some-image",
|
||||||
Metadata: source.StereoscopeImageSourceMetadata{
|
Metadata: source.ImageMetadata{
|
||||||
ID: "DocumentRoot-Image-some-image",
|
ID: "DocumentRoot-Image-some-image",
|
||||||
UserInput: "some-image:some-tag",
|
UserInput: "some-image:some-tag",
|
||||||
ManifestDigest: "sha256:ab8b83234bc28f28d8e",
|
ManifestDigest: "sha256:ab8b83234bc28f28d8e",
|
||||||
|
@ -476,7 +476,7 @@ func Test_convertToAndFromFormat(t *testing.T) {
|
||||||
source: source.Description{
|
source: source.Description{
|
||||||
ID: "DocumentRoot-Directory-.",
|
ID: "DocumentRoot-Directory-.",
|
||||||
Name: ".",
|
Name: ".",
|
||||||
Metadata: source.DirectorySourceMetadata{
|
Metadata: source.DirectoryMetadata{
|
||||||
Path: ".",
|
Path: ".",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -488,7 +488,7 @@ func Test_convertToAndFromFormat(t *testing.T) {
|
||||||
source: source.Description{
|
source: source.Description{
|
||||||
ID: "DocumentRoot-Directory-my-app",
|
ID: "DocumentRoot-Directory-my-app",
|
||||||
Name: "my-app",
|
Name: "my-app",
|
||||||
Metadata: source.DirectorySourceMetadata{
|
Metadata: source.DirectoryMetadata{
|
||||||
Path: "my-app",
|
Path: "my-app",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -499,7 +499,7 @@ func Test_convertToAndFromFormat(t *testing.T) {
|
||||||
name: "file source",
|
name: "file source",
|
||||||
source: source.Description{
|
source: source.Description{
|
||||||
ID: "DocumentRoot-File-my-app.exe",
|
ID: "DocumentRoot-File-my-app.exe",
|
||||||
Metadata: source.FileSourceMetadata{
|
Metadata: source.FileMetadata{
|
||||||
Path: "my-app.exe",
|
Path: "my-app.exe",
|
||||||
Digests: []file.Digest{
|
Digests: []file.Digest{
|
||||||
{
|
{
|
||||||
|
|
|
@ -116,16 +116,16 @@ func toPath(s source.Description, p pkg.Package) string {
|
||||||
}
|
}
|
||||||
packagePath = strings.TrimPrefix(packagePath, "/")
|
packagePath = strings.TrimPrefix(packagePath, "/")
|
||||||
switch metadata := s.Metadata.(type) {
|
switch metadata := s.Metadata.(type) {
|
||||||
case source.StereoscopeImageSourceMetadata:
|
case source.ImageMetadata:
|
||||||
image := strings.ReplaceAll(metadata.UserInput, ":/", "//")
|
image := strings.ReplaceAll(metadata.UserInput, ":/", "//")
|
||||||
return fmt.Sprintf("%s:/%s", image, packagePath)
|
return fmt.Sprintf("%s:/%s", image, packagePath)
|
||||||
case source.FileSourceMetadata:
|
case source.FileMetadata:
|
||||||
path := trimRelative(metadata.Path)
|
path := trimRelative(metadata.Path)
|
||||||
if isArchive(metadata.Path) {
|
if isArchive(metadata.Path) {
|
||||||
return fmt.Sprintf("%s:/%s", path, packagePath)
|
return fmt.Sprintf("%s:/%s", path, packagePath)
|
||||||
}
|
}
|
||||||
return path
|
return path
|
||||||
case source.DirectorySourceMetadata:
|
case source.DirectoryMetadata:
|
||||||
path := trimRelative(metadata.Path)
|
path := trimRelative(metadata.Path)
|
||||||
if path != "" {
|
if path != "" {
|
||||||
return fmt.Sprintf("%s/%s", path, packagePath)
|
return fmt.Sprintf("%s/%s", path, packagePath)
|
||||||
|
|
|
@ -22,7 +22,7 @@ func sbomFixture() sbom.SBOM {
|
||||||
Name: "syft",
|
Name: "syft",
|
||||||
},
|
},
|
||||||
Source: source.Description{
|
Source: source.Description{
|
||||||
Metadata: source.StereoscopeImageSourceMetadata{
|
Metadata: source.ImageMetadata{
|
||||||
UserInput: "ubuntu:18.04",
|
UserInput: "ubuntu:18.04",
|
||||||
Architecture: "amd64",
|
Architecture: "amd64",
|
||||||
},
|
},
|
||||||
|
@ -150,27 +150,27 @@ func Test_toGithubModel(t *testing.T) {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "current directory",
|
name: "current directory",
|
||||||
metadata: source.DirectorySourceMetadata{Path: "."},
|
metadata: source.DirectoryMetadata{Path: "."},
|
||||||
testPath: "etc",
|
testPath: "etc",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "relative directory",
|
name: "relative directory",
|
||||||
metadata: source.DirectorySourceMetadata{Path: "./artifacts"},
|
metadata: source.DirectoryMetadata{Path: "./artifacts"},
|
||||||
testPath: "artifacts/etc",
|
testPath: "artifacts/etc",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "absolute directory",
|
name: "absolute directory",
|
||||||
metadata: source.DirectorySourceMetadata{Path: "/artifacts"},
|
metadata: source.DirectoryMetadata{Path: "/artifacts"},
|
||||||
testPath: "/artifacts/etc",
|
testPath: "/artifacts/etc",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "file",
|
name: "file",
|
||||||
metadata: source.FileSourceMetadata{Path: "./executable"},
|
metadata: source.FileMetadata{Path: "./executable"},
|
||||||
testPath: "executable",
|
testPath: "executable",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "archive",
|
name: "archive",
|
||||||
metadata: source.FileSourceMetadata{Path: "./archive.tar.gz"},
|
metadata: source.FileMetadata{Path: "./archive.tar.gz"},
|
||||||
testPath: "archive.tar.gz:/etc",
|
testPath: "archive.tar.gz:/etc",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
@ -222,7 +222,7 @@ func extractComponents(meta *cyclonedx.Metadata) source.Description {
|
||||||
ID: "",
|
ID: "",
|
||||||
// TODO: can we decode alias name-version somehow? (it isn't be encoded in the first place yet)
|
// TODO: can we decode alias name-version somehow? (it isn't be encoded in the first place yet)
|
||||||
|
|
||||||
Metadata: source.StereoscopeImageSourceMetadata{
|
Metadata: source.ImageMetadata{
|
||||||
UserInput: c.Name,
|
UserInput: c.Name,
|
||||||
ID: c.BOMRef,
|
ID: c.BOMRef,
|
||||||
ManifestDigest: c.Version,
|
ManifestDigest: c.Version,
|
||||||
|
@ -235,7 +235,7 @@ func extractComponents(meta *cyclonedx.Metadata) source.Description {
|
||||||
// TODO: this is lossy... we can't know if this is a file or a directory
|
// TODO: this is lossy... we can't know if this is a file or a directory
|
||||||
return source.Description{
|
return source.Description{
|
||||||
ID: "",
|
ID: "",
|
||||||
Metadata: source.FileSourceMetadata{Path: c.Name},
|
Metadata: source.FileMetadata{Path: c.Name},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return source.Description{}
|
return source.Description{}
|
||||||
|
|
|
@ -10,11 +10,11 @@ func DocumentName(src source.Description) string {
|
||||||
}
|
}
|
||||||
|
|
||||||
switch metadata := src.Metadata.(type) {
|
switch metadata := src.Metadata.(type) {
|
||||||
case source.StereoscopeImageSourceMetadata:
|
case source.ImageMetadata:
|
||||||
return metadata.UserInput
|
return metadata.UserInput
|
||||||
case source.DirectorySourceMetadata:
|
case source.DirectoryMetadata:
|
||||||
return metadata.Path
|
return metadata.Path
|
||||||
case source.FileSourceMetadata:
|
case source.FileMetadata:
|
||||||
return metadata.Path
|
return metadata.Path
|
||||||
default:
|
default:
|
||||||
return "unknown"
|
return "unknown"
|
||||||
|
|
|
@ -23,7 +23,7 @@ func Test_DocumentName(t *testing.T) {
|
||||||
{
|
{
|
||||||
name: "image",
|
name: "image",
|
||||||
srcMetadata: source.Description{
|
srcMetadata: source.Description{
|
||||||
Metadata: source.StereoscopeImageSourceMetadata{
|
Metadata: source.ImageMetadata{
|
||||||
UserInput: "image-repo/name:tag",
|
UserInput: "image-repo/name:tag",
|
||||||
ID: "id",
|
ID: "id",
|
||||||
ManifestDigest: "digest",
|
ManifestDigest: "digest",
|
||||||
|
@ -34,14 +34,14 @@ func Test_DocumentName(t *testing.T) {
|
||||||
{
|
{
|
||||||
name: "directory",
|
name: "directory",
|
||||||
srcMetadata: source.Description{
|
srcMetadata: source.Description{
|
||||||
Metadata: source.DirectorySourceMetadata{Path: "some/path/to/place"},
|
Metadata: source.DirectoryMetadata{Path: "some/path/to/place"},
|
||||||
},
|
},
|
||||||
expected: "some/path/to/place",
|
expected: "some/path/to/place",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "file",
|
name: "file",
|
||||||
srcMetadata: source.Description{
|
srcMetadata: source.Description{
|
||||||
Metadata: source.FileSourceMetadata{Path: "some/path/to/place"},
|
Metadata: source.FileMetadata{Path: "some/path/to/place"},
|
||||||
},
|
},
|
||||||
expected: "some/path/to/place",
|
expected: "some/path/to/place",
|
||||||
},
|
},
|
||||||
|
@ -49,7 +49,7 @@ func Test_DocumentName(t *testing.T) {
|
||||||
name: "named",
|
name: "named",
|
||||||
srcMetadata: source.Description{
|
srcMetadata: source.Description{
|
||||||
Name: "some/name",
|
Name: "some/name",
|
||||||
Metadata: source.FileSourceMetadata{Path: "some/path/to/place"},
|
Metadata: source.FileMetadata{Path: "some/path/to/place"},
|
||||||
},
|
},
|
||||||
expected: "some/name",
|
expected: "some/name",
|
||||||
},
|
},
|
||||||
|
|
|
@ -27,11 +27,11 @@ func DocumentNamespace(name string, src source.Description, desc sbom.Descriptor
|
||||||
name = cleanName(name)
|
name = cleanName(name)
|
||||||
input := "unknown-source-type"
|
input := "unknown-source-type"
|
||||||
switch src.Metadata.(type) {
|
switch src.Metadata.(type) {
|
||||||
case source.StereoscopeImageSourceMetadata:
|
case source.ImageMetadata:
|
||||||
input = InputImage
|
input = InputImage
|
||||||
case source.DirectorySourceMetadata:
|
case source.DirectoryMetadata:
|
||||||
input = InputDirectory
|
input = InputDirectory
|
||||||
case source.FileSourceMetadata:
|
case source.FileMetadata:
|
||||||
input = InputFile
|
input = InputFile
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -25,7 +25,7 @@ func Test_documentNamespace(t *testing.T) {
|
||||||
name: "image",
|
name: "image",
|
||||||
inputName: "my-name",
|
inputName: "my-name",
|
||||||
src: source.Description{
|
src: source.Description{
|
||||||
Metadata: source.StereoscopeImageSourceMetadata{
|
Metadata: source.ImageMetadata{
|
||||||
UserInput: "image-repo/name:tag",
|
UserInput: "image-repo/name:tag",
|
||||||
ID: "id",
|
ID: "id",
|
||||||
ManifestDigest: "digest",
|
ManifestDigest: "digest",
|
||||||
|
@ -37,7 +37,7 @@ func Test_documentNamespace(t *testing.T) {
|
||||||
name: "directory",
|
name: "directory",
|
||||||
inputName: "my-name",
|
inputName: "my-name",
|
||||||
src: source.Description{
|
src: source.Description{
|
||||||
Metadata: source.DirectorySourceMetadata{
|
Metadata: source.DirectoryMetadata{
|
||||||
Path: "some/path/to/place",
|
Path: "some/path/to/place",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -47,7 +47,7 @@ func Test_documentNamespace(t *testing.T) {
|
||||||
name: "file",
|
name: "file",
|
||||||
inputName: "my-name",
|
inputName: "my-name",
|
||||||
src: source.Description{
|
src: source.Description{
|
||||||
Metadata: source.FileSourceMetadata{
|
Metadata: source.FileMetadata{
|
||||||
Path: "some/path/to/place",
|
Path: "some/path/to/place",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
@ -12,7 +12,7 @@ import (
|
||||||
"github.com/anchore/syft/syft/linux"
|
"github.com/anchore/syft/syft/linux"
|
||||||
"github.com/anchore/syft/syft/pkg"
|
"github.com/anchore/syft/syft/pkg"
|
||||||
"github.com/anchore/syft/syft/sbom"
|
"github.com/anchore/syft/syft/sbom"
|
||||||
"github.com/anchore/syft/syft/source"
|
"github.com/anchore/syft/syft/source/directorysource"
|
||||||
)
|
)
|
||||||
|
|
||||||
func DirectoryInput(t testing.TB, dir string) sbom.SBOM {
|
func DirectoryInput(t testing.TB, dir string) sbom.SBOM {
|
||||||
|
@ -22,8 +22,8 @@ func DirectoryInput(t testing.TB, dir string) sbom.SBOM {
|
||||||
|
|
||||||
require.NoError(t, os.MkdirAll(path, 0755))
|
require.NoError(t, os.MkdirAll(path, 0755))
|
||||||
|
|
||||||
src, err := source.NewFromDirectory(
|
src, err := directorysource.New(
|
||||||
source.DirectoryConfig{
|
directorysource.Config{
|
||||||
Path: path,
|
Path: path,
|
||||||
Base: dir,
|
Base: dir,
|
||||||
},
|
},
|
||||||
|
@ -63,8 +63,8 @@ func DirectoryInputWithAuthorField(t testing.TB) sbom.SBOM {
|
||||||
|
|
||||||
require.NoError(t, os.MkdirAll(path, 0755))
|
require.NoError(t, os.MkdirAll(path, 0755))
|
||||||
|
|
||||||
src, err := source.NewFromDirectory(
|
src, err := directorysource.New(
|
||||||
source.DirectoryConfig{
|
directorysource.Config{
|
||||||
Path: path,
|
Path: path,
|
||||||
Base: dir,
|
Base: dir,
|
||||||
},
|
},
|
||||||
|
|
|
@ -5,7 +5,6 @@ import (
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
"github.com/anchore/stereoscope/pkg/filetree"
|
"github.com/anchore/stereoscope/pkg/filetree"
|
||||||
|
@ -16,7 +15,7 @@ import (
|
||||||
"github.com/anchore/syft/syft/linux"
|
"github.com/anchore/syft/syft/linux"
|
||||||
"github.com/anchore/syft/syft/pkg"
|
"github.com/anchore/syft/syft/pkg"
|
||||||
"github.com/anchore/syft/syft/sbom"
|
"github.com/anchore/syft/syft/sbom"
|
||||||
"github.com/anchore/syft/syft/source"
|
"github.com/anchore/syft/syft/source/stereoscopesource"
|
||||||
)
|
)
|
||||||
|
|
||||||
func ImageInput(t testing.TB, testImage string, options ...ImageOption) sbom.SBOM {
|
func ImageInput(t testing.TB, testImage string, options ...ImageOption) sbom.SBOM {
|
||||||
|
@ -42,8 +41,9 @@ func ImageInput(t testing.TB, testImage string, options ...ImageOption) sbom.SBO
|
||||||
// this is a hard coded value that is not given by the fixture helper and must be provided manually
|
// this is a hard coded value that is not given by the fixture helper and must be provided manually
|
||||||
img.Metadata.ManifestDigest = "sha256:2731251dc34951c0e50fcc643b4c5f74922dad1a5d98f302b504cf46cd5d9368"
|
img.Metadata.ManifestDigest = "sha256:2731251dc34951c0e50fcc643b4c5f74922dad1a5d98f302b504cf46cd5d9368"
|
||||||
|
|
||||||
src, err := source.NewFromStereoscopeImageObject(img, "user-image-input", nil)
|
src := stereoscopesource.New(img, stereoscopesource.ImageConfig{
|
||||||
assert.NoError(t, err)
|
Reference: "user-image-input",
|
||||||
|
})
|
||||||
|
|
||||||
return sbom.SBOM{
|
return sbom.SBOM{
|
||||||
Artifacts: sbom.Artifacts{
|
Artifacts: sbom.Artifacts{
|
||||||
|
|
|
@ -72,7 +72,7 @@ func TestSPDXJSONSPDXIDs(t *testing.T) {
|
||||||
Relationships: nil,
|
Relationships: nil,
|
||||||
Source: source.Description{
|
Source: source.Description{
|
||||||
Name: "foobar/baz", // in this case, foobar is used as the spdx document name
|
Name: "foobar/baz", // in this case, foobar is used as the spdx document name
|
||||||
Metadata: source.DirectorySourceMetadata{},
|
Metadata: source.DirectoryMetadata{},
|
||||||
},
|
},
|
||||||
Descriptor: sbom.Descriptor{
|
Descriptor: sbom.Descriptor{
|
||||||
Name: "syft",
|
Name: "syft",
|
||||||
|
|
|
@ -263,7 +263,7 @@ func Test_encodeDecodeFileMetadata(t *testing.T) {
|
||||||
ID: "some-id",
|
ID: "some-id",
|
||||||
Name: "some-name",
|
Name: "some-name",
|
||||||
Version: "some-version",
|
Version: "some-version",
|
||||||
Metadata: source.FileSourceMetadata{
|
Metadata: source.FileMetadata{
|
||||||
Path: "/some-file-source-path",
|
Path: "/some-file-source-path",
|
||||||
Digests: []file.Digest{
|
Digests: []file.Digest{
|
||||||
{
|
{
|
||||||
|
|
|
@ -260,7 +260,7 @@ func TestEncodeFullJSONDocument(t *testing.T) {
|
||||||
},
|
},
|
||||||
Source: source.Description{
|
Source: source.Description{
|
||||||
ID: "c2b46b4eb06296933b7cf0722683964e9ecbd93265b9ef6ae9642e3952afbba0",
|
ID: "c2b46b4eb06296933b7cf0722683964e9ecbd93265b9ef6ae9642e3952afbba0",
|
||||||
Metadata: source.StereoscopeImageSourceMetadata{
|
Metadata: source.ImageMetadata{
|
||||||
UserInput: "user-image-input",
|
UserInput: "user-image-input",
|
||||||
ID: "sha256:c2b46b4eb06296933b7cf0722683964e9ecbd93265b9ef6ae9642e3952afbba0",
|
ID: "sha256:c2b46b4eb06296933b7cf0722683964e9ecbd93265b9ef6ae9642e3952afbba0",
|
||||||
ManifestDigest: "sha256:2731251dc34951c0e50fcc643b4c5f74922dad1a5d98f302b504cf46cd5d9368",
|
ManifestDigest: "sha256:2731251dc34951c0e50fcc643b4c5f74922dad1a5d98f302b504cf46cd5d9368",
|
||||||
|
@ -269,7 +269,7 @@ func TestEncodeFullJSONDocument(t *testing.T) {
|
||||||
"stereoscope-fixture-image-simple:85066c51088bdd274f7a89e99e00490f666c49e72ffc955707cd6e18f0e22c5b",
|
"stereoscope-fixture-image-simple:85066c51088bdd274f7a89e99e00490f666c49e72ffc955707cd6e18f0e22c5b",
|
||||||
},
|
},
|
||||||
Size: 38,
|
Size: 38,
|
||||||
Layers: []source.StereoscopeLayerMetadata{
|
Layers: []source.LayerMetadata{
|
||||||
{
|
{
|
||||||
MediaType: "application/vnd.docker.image.rootfs.diff.tar.gzip",
|
MediaType: "application/vnd.docker.image.rootfs.diff.tar.gzip",
|
||||||
Digest: "sha256:3de16c5b8659a2e8d888b8ded8427be7a5686a3c8c4e4dd30de20f362827285b",
|
Digest: "sha256:3de16c5b8659a2e8d888b8ded8427be7a5686a3c8c4e4dd30de20f362827285b",
|
||||||
|
|
|
@ -89,7 +89,7 @@ func extractPreSchemaV9Metadata(t string, target []byte) (interface{}, error) {
|
||||||
cleanTarget = string(target)
|
cleanTarget = string(target)
|
||||||
}
|
}
|
||||||
|
|
||||||
return source.DirectorySourceMetadata{
|
return source.DirectoryMetadata{
|
||||||
Path: cleanTarget,
|
Path: cleanTarget,
|
||||||
}, nil
|
}, nil
|
||||||
|
|
||||||
|
@ -99,12 +99,12 @@ func extractPreSchemaV9Metadata(t string, target []byte) (interface{}, error) {
|
||||||
cleanTarget = string(target)
|
cleanTarget = string(target)
|
||||||
}
|
}
|
||||||
|
|
||||||
return source.FileSourceMetadata{
|
return source.FileMetadata{
|
||||||
Path: cleanTarget,
|
Path: cleanTarget,
|
||||||
}, nil
|
}, nil
|
||||||
|
|
||||||
case "image":
|
case "image":
|
||||||
var payload source.StereoscopeImageSourceMetadata
|
var payload source.ImageMetadata
|
||||||
if err := json.Unmarshal(target, &payload); err != nil {
|
if err := json.Unmarshal(target, &payload); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,7 +32,7 @@ func TestSource_UnmarshalJSON(t *testing.T) {
|
||||||
expected: &Source{
|
expected: &Source{
|
||||||
ID: "foobar",
|
ID: "foobar",
|
||||||
Type: "directory",
|
Type: "directory",
|
||||||
Metadata: source.DirectorySourceMetadata{
|
Metadata: source.DirectoryMetadata{
|
||||||
Path: "/var/lib/foo",
|
Path: "/var/lib/foo",
|
||||||
//Base: "/nope", // note: should be ignored entirely
|
//Base: "/nope", // note: should be ignored entirely
|
||||||
},
|
},
|
||||||
|
@ -67,14 +67,14 @@ func TestSource_UnmarshalJSON(t *testing.T) {
|
||||||
expected: &Source{
|
expected: &Source{
|
||||||
ID: "foobar",
|
ID: "foobar",
|
||||||
Type: "image",
|
Type: "image",
|
||||||
Metadata: source.StereoscopeImageSourceMetadata{
|
Metadata: source.ImageMetadata{
|
||||||
UserInput: "alpine:3.10",
|
UserInput: "alpine:3.10",
|
||||||
ID: "sha256:e7b300aee9f9bf3433d32bc9305bfdd22183beb59d933b48d77ab56ba53a197a",
|
ID: "sha256:e7b300aee9f9bf3433d32bc9305bfdd22183beb59d933b48d77ab56ba53a197a",
|
||||||
ManifestDigest: "sha256:e515aad2ed234a5072c4d2ef86a1cb77d5bfe4b11aa865d9214875734c4eeb3c",
|
ManifestDigest: "sha256:e515aad2ed234a5072c4d2ef86a1cb77d5bfe4b11aa865d9214875734c4eeb3c",
|
||||||
MediaType: "application/vnd.docker.distribution.manifest.v2+json",
|
MediaType: "application/vnd.docker.distribution.manifest.v2+json",
|
||||||
Tags: []string{},
|
Tags: []string{},
|
||||||
Size: 5576169,
|
Size: 5576169,
|
||||||
Layers: []source.StereoscopeLayerMetadata{
|
Layers: []source.LayerMetadata{
|
||||||
{
|
{
|
||||||
MediaType: "application/vnd.docker.image.rootfs.diff.tar.gzip",
|
MediaType: "application/vnd.docker.image.rootfs.diff.tar.gzip",
|
||||||
Digest: "sha256:9fb3aa2f8b8023a4bebbf92aa567caf88e38e969ada9f0ac12643b2847391635",
|
Digest: "sha256:9fb3aa2f8b8023a4bebbf92aa567caf88e38e969ada9f0ac12643b2847391635",
|
||||||
|
@ -124,7 +124,7 @@ func TestSource_UnmarshalJSON(t *testing.T) {
|
||||||
expected: &Source{
|
expected: &Source{
|
||||||
ID: "foobar",
|
ID: "foobar",
|
||||||
Type: "file",
|
Type: "file",
|
||||||
Metadata: source.FileSourceMetadata{
|
Metadata: source.FileMetadata{
|
||||||
Path: "/var/lib/foo/go.mod",
|
Path: "/var/lib/foo/go.mod",
|
||||||
Digests: []file.Digest{
|
Digests: []file.Digest{
|
||||||
{
|
{
|
||||||
|
@ -188,7 +188,7 @@ func TestSource_UnmarshalJSON_PreSchemaV9(t *testing.T) {
|
||||||
expectedSource: &Source{
|
expectedSource: &Source{
|
||||||
ID: "foobar",
|
ID: "foobar",
|
||||||
Type: "directory",
|
Type: "directory",
|
||||||
Metadata: source.DirectorySourceMetadata{
|
Metadata: source.DirectoryMetadata{
|
||||||
Path: "/var/lib/foo",
|
Path: "/var/lib/foo",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -204,7 +204,7 @@ func TestSource_UnmarshalJSON_PreSchemaV9(t *testing.T) {
|
||||||
expectedSource: &Source{
|
expectedSource: &Source{
|
||||||
ID: "foobar",
|
ID: "foobar",
|
||||||
Type: "directory",
|
Type: "directory",
|
||||||
Metadata: source.DirectorySourceMetadata{
|
Metadata: source.DirectoryMetadata{
|
||||||
Path: "/var/lib/foo",
|
Path: "/var/lib/foo",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -239,14 +239,14 @@ func TestSource_UnmarshalJSON_PreSchemaV9(t *testing.T) {
|
||||||
expectedSource: &Source{
|
expectedSource: &Source{
|
||||||
ID: "foobar",
|
ID: "foobar",
|
||||||
Type: "image",
|
Type: "image",
|
||||||
Metadata: source.StereoscopeImageSourceMetadata{
|
Metadata: source.ImageMetadata{
|
||||||
UserInput: "alpine:3.10",
|
UserInput: "alpine:3.10",
|
||||||
ID: "sha256:e7b300aee9f9bf3433d32bc9305bfdd22183beb59d933b48d77ab56ba53a197a",
|
ID: "sha256:e7b300aee9f9bf3433d32bc9305bfdd22183beb59d933b48d77ab56ba53a197a",
|
||||||
ManifestDigest: "sha256:e515aad2ed234a5072c4d2ef86a1cb77d5bfe4b11aa865d9214875734c4eeb3c",
|
ManifestDigest: "sha256:e515aad2ed234a5072c4d2ef86a1cb77d5bfe4b11aa865d9214875734c4eeb3c",
|
||||||
MediaType: "application/vnd.docker.distribution.manifest.v2+json",
|
MediaType: "application/vnd.docker.distribution.manifest.v2+json",
|
||||||
Tags: []string{},
|
Tags: []string{},
|
||||||
Size: 5576169,
|
Size: 5576169,
|
||||||
Layers: []source.StereoscopeLayerMetadata{
|
Layers: []source.LayerMetadata{
|
||||||
{
|
{
|
||||||
MediaType: "application/vnd.docker.image.rootfs.diff.tar.gzip",
|
MediaType: "application/vnd.docker.image.rootfs.diff.tar.gzip",
|
||||||
Digest: "sha256:9fb3aa2f8b8023a4bebbf92aa567caf88e38e969ada9f0ac12643b2847391635",
|
Digest: "sha256:9fb3aa2f8b8023a4bebbf92aa567caf88e38e969ada9f0ac12643b2847391635",
|
||||||
|
@ -288,7 +288,7 @@ func TestSource_UnmarshalJSON_PreSchemaV9(t *testing.T) {
|
||||||
expectedSource: &Source{
|
expectedSource: &Source{
|
||||||
ID: "foobar",
|
ID: "foobar",
|
||||||
Type: "file",
|
Type: "file",
|
||||||
Metadata: source.FileSourceMetadata{
|
Metadata: source.FileMetadata{
|
||||||
Path: "/var/lib/foo/go.mod",
|
Path: "/var/lib/foo/go.mod",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
@ -303,7 +303,7 @@ func toSourceModel(src source.Description) model.Source {
|
||||||
Metadata: src.Metadata,
|
Metadata: src.Metadata,
|
||||||
}
|
}
|
||||||
|
|
||||||
if metadata, ok := src.Metadata.(source.StereoscopeImageSourceMetadata); ok {
|
if metadata, ok := src.Metadata.(source.ImageMetadata); ok {
|
||||||
// ensure that empty collections are not shown as null
|
// ensure that empty collections are not shown as null
|
||||||
if metadata.RepoDigests == nil {
|
if metadata.RepoDigests == nil {
|
||||||
metadata.RepoDigests = []string{}
|
metadata.RepoDigests = []string{}
|
||||||
|
|
|
@ -26,7 +26,7 @@ func Test_toSourceModel_IgnoreBase(t *testing.T) {
|
||||||
name: "directory",
|
name: "directory",
|
||||||
src: source.Description{
|
src: source.Description{
|
||||||
ID: "test-id",
|
ID: "test-id",
|
||||||
Metadata: source.DirectorySourceMetadata{
|
Metadata: source.DirectoryMetadata{
|
||||||
Path: "some/path",
|
Path: "some/path",
|
||||||
Base: "some/base",
|
Base: "some/base",
|
||||||
},
|
},
|
||||||
|
@ -59,7 +59,7 @@ func Test_toSourceModel(t *testing.T) {
|
||||||
ID: "test-id",
|
ID: "test-id",
|
||||||
Name: "some-name",
|
Name: "some-name",
|
||||||
Version: "some-version",
|
Version: "some-version",
|
||||||
Metadata: source.DirectorySourceMetadata{
|
Metadata: source.DirectoryMetadata{
|
||||||
Path: "some/path",
|
Path: "some/path",
|
||||||
Base: "some/base",
|
Base: "some/base",
|
||||||
},
|
},
|
||||||
|
@ -69,7 +69,7 @@ func Test_toSourceModel(t *testing.T) {
|
||||||
Name: "some-name",
|
Name: "some-name",
|
||||||
Version: "some-version",
|
Version: "some-version",
|
||||||
Type: "directory",
|
Type: "directory",
|
||||||
Metadata: source.DirectorySourceMetadata{
|
Metadata: source.DirectoryMetadata{
|
||||||
Path: "some/path",
|
Path: "some/path",
|
||||||
Base: "some/base",
|
Base: "some/base",
|
||||||
},
|
},
|
||||||
|
@ -81,7 +81,7 @@ func Test_toSourceModel(t *testing.T) {
|
||||||
ID: "test-id",
|
ID: "test-id",
|
||||||
Name: "some-name",
|
Name: "some-name",
|
||||||
Version: "some-version",
|
Version: "some-version",
|
||||||
Metadata: source.FileSourceMetadata{
|
Metadata: source.FileMetadata{
|
||||||
Path: "some/path",
|
Path: "some/path",
|
||||||
Digests: []file.Digest{{Algorithm: "sha256", Value: "some-digest"}},
|
Digests: []file.Digest{{Algorithm: "sha256", Value: "some-digest"}},
|
||||||
MIMEType: "text/plain",
|
MIMEType: "text/plain",
|
||||||
|
@ -92,7 +92,7 @@ func Test_toSourceModel(t *testing.T) {
|
||||||
Name: "some-name",
|
Name: "some-name",
|
||||||
Version: "some-version",
|
Version: "some-version",
|
||||||
Type: "file",
|
Type: "file",
|
||||||
Metadata: source.FileSourceMetadata{
|
Metadata: source.FileMetadata{
|
||||||
Path: "some/path",
|
Path: "some/path",
|
||||||
Digests: []file.Digest{{Algorithm: "sha256", Value: "some-digest"}},
|
Digests: []file.Digest{{Algorithm: "sha256", Value: "some-digest"}},
|
||||||
MIMEType: "text/plain",
|
MIMEType: "text/plain",
|
||||||
|
@ -105,7 +105,7 @@ func Test_toSourceModel(t *testing.T) {
|
||||||
ID: "test-id",
|
ID: "test-id",
|
||||||
Name: "some-name",
|
Name: "some-name",
|
||||||
Version: "some-version",
|
Version: "some-version",
|
||||||
Metadata: source.StereoscopeImageSourceMetadata{
|
Metadata: source.ImageMetadata{
|
||||||
UserInput: "user-input",
|
UserInput: "user-input",
|
||||||
ID: "id...",
|
ID: "id...",
|
||||||
ManifestDigest: "digest...",
|
ManifestDigest: "digest...",
|
||||||
|
@ -117,7 +117,7 @@ func Test_toSourceModel(t *testing.T) {
|
||||||
Name: "some-name",
|
Name: "some-name",
|
||||||
Version: "some-version",
|
Version: "some-version",
|
||||||
Type: "image",
|
Type: "image",
|
||||||
Metadata: source.StereoscopeImageSourceMetadata{
|
Metadata: source.ImageMetadata{
|
||||||
UserInput: "user-input",
|
UserInput: "user-input",
|
||||||
ID: "id...",
|
ID: "id...",
|
||||||
ManifestDigest: "digest...",
|
ManifestDigest: "digest...",
|
||||||
|
@ -133,7 +133,7 @@ func Test_toSourceModel(t *testing.T) {
|
||||||
name: "directory - no name/version",
|
name: "directory - no name/version",
|
||||||
src: source.Description{
|
src: source.Description{
|
||||||
ID: "test-id",
|
ID: "test-id",
|
||||||
Metadata: source.DirectorySourceMetadata{
|
Metadata: source.DirectoryMetadata{
|
||||||
Path: "some/path",
|
Path: "some/path",
|
||||||
Base: "some/base",
|
Base: "some/base",
|
||||||
},
|
},
|
||||||
|
@ -141,7 +141,7 @@ func Test_toSourceModel(t *testing.T) {
|
||||||
expected: model.Source{
|
expected: model.Source{
|
||||||
ID: "test-id",
|
ID: "test-id",
|
||||||
Type: "directory",
|
Type: "directory",
|
||||||
Metadata: source.DirectorySourceMetadata{
|
Metadata: source.DirectoryMetadata{
|
||||||
Path: "some/path",
|
Path: "some/path",
|
||||||
Base: "some/base",
|
Base: "some/base",
|
||||||
},
|
},
|
||||||
|
@ -151,7 +151,7 @@ func Test_toSourceModel(t *testing.T) {
|
||||||
name: "file - no name/version",
|
name: "file - no name/version",
|
||||||
src: source.Description{
|
src: source.Description{
|
||||||
ID: "test-id",
|
ID: "test-id",
|
||||||
Metadata: source.FileSourceMetadata{
|
Metadata: source.FileMetadata{
|
||||||
Path: "some/path",
|
Path: "some/path",
|
||||||
Digests: []file.Digest{{Algorithm: "sha256", Value: "some-digest"}},
|
Digests: []file.Digest{{Algorithm: "sha256", Value: "some-digest"}},
|
||||||
MIMEType: "text/plain",
|
MIMEType: "text/plain",
|
||||||
|
@ -160,7 +160,7 @@ func Test_toSourceModel(t *testing.T) {
|
||||||
expected: model.Source{
|
expected: model.Source{
|
||||||
ID: "test-id",
|
ID: "test-id",
|
||||||
Type: "file",
|
Type: "file",
|
||||||
Metadata: source.FileSourceMetadata{
|
Metadata: source.FileMetadata{
|
||||||
Path: "some/path",
|
Path: "some/path",
|
||||||
Digests: []file.Digest{{Algorithm: "sha256", Value: "some-digest"}},
|
Digests: []file.Digest{{Algorithm: "sha256", Value: "some-digest"}},
|
||||||
MIMEType: "text/plain",
|
MIMEType: "text/plain",
|
||||||
|
@ -171,7 +171,7 @@ func Test_toSourceModel(t *testing.T) {
|
||||||
name: "image - no name/version",
|
name: "image - no name/version",
|
||||||
src: source.Description{
|
src: source.Description{
|
||||||
ID: "test-id",
|
ID: "test-id",
|
||||||
Metadata: source.StereoscopeImageSourceMetadata{
|
Metadata: source.ImageMetadata{
|
||||||
UserInput: "user-input",
|
UserInput: "user-input",
|
||||||
ID: "id...",
|
ID: "id...",
|
||||||
ManifestDigest: "digest...",
|
ManifestDigest: "digest...",
|
||||||
|
@ -181,7 +181,7 @@ func Test_toSourceModel(t *testing.T) {
|
||||||
expected: model.Source{
|
expected: model.Source{
|
||||||
ID: "test-id",
|
ID: "test-id",
|
||||||
Type: "image",
|
Type: "image",
|
||||||
Metadata: source.StereoscopeImageSourceMetadata{
|
Metadata: source.ImageMetadata{
|
||||||
UserInput: "user-input",
|
UserInput: "user-input",
|
||||||
ID: "id...",
|
ID: "id...",
|
||||||
ManifestDigest: "digest...",
|
ManifestDigest: "digest...",
|
||||||
|
|
|
@ -35,7 +35,7 @@ func Test_toSyftSourceData(t *testing.T) {
|
||||||
Name: "some-name",
|
Name: "some-name",
|
||||||
Version: "some-version",
|
Version: "some-version",
|
||||||
Type: "directory",
|
Type: "directory",
|
||||||
Metadata: source.DirectorySourceMetadata{
|
Metadata: source.DirectoryMetadata{
|
||||||
Path: "some/path",
|
Path: "some/path",
|
||||||
Base: "some/base",
|
Base: "some/base",
|
||||||
},
|
},
|
||||||
|
@ -44,7 +44,7 @@ func Test_toSyftSourceData(t *testing.T) {
|
||||||
ID: "the-id",
|
ID: "the-id",
|
||||||
Name: "some-name",
|
Name: "some-name",
|
||||||
Version: "some-version",
|
Version: "some-version",
|
||||||
Metadata: source.DirectorySourceMetadata{
|
Metadata: source.DirectoryMetadata{
|
||||||
Path: "some/path",
|
Path: "some/path",
|
||||||
Base: "some/base",
|
Base: "some/base",
|
||||||
},
|
},
|
||||||
|
@ -57,7 +57,7 @@ func Test_toSyftSourceData(t *testing.T) {
|
||||||
Name: "some-name",
|
Name: "some-name",
|
||||||
Version: "some-version",
|
Version: "some-version",
|
||||||
Type: "file",
|
Type: "file",
|
||||||
Metadata: source.FileSourceMetadata{
|
Metadata: source.FileMetadata{
|
||||||
Path: "some/path",
|
Path: "some/path",
|
||||||
Digests: []file.Digest{{Algorithm: "sha256", Value: "some-digest"}},
|
Digests: []file.Digest{{Algorithm: "sha256", Value: "some-digest"}},
|
||||||
MIMEType: "text/plain",
|
MIMEType: "text/plain",
|
||||||
|
@ -67,7 +67,7 @@ func Test_toSyftSourceData(t *testing.T) {
|
||||||
ID: "the-id",
|
ID: "the-id",
|
||||||
Name: "some-name",
|
Name: "some-name",
|
||||||
Version: "some-version",
|
Version: "some-version",
|
||||||
Metadata: source.FileSourceMetadata{
|
Metadata: source.FileMetadata{
|
||||||
Path: "some/path",
|
Path: "some/path",
|
||||||
Digests: []file.Digest{{Algorithm: "sha256", Value: "some-digest"}},
|
Digests: []file.Digest{{Algorithm: "sha256", Value: "some-digest"}},
|
||||||
MIMEType: "text/plain",
|
MIMEType: "text/plain",
|
||||||
|
@ -81,7 +81,7 @@ func Test_toSyftSourceData(t *testing.T) {
|
||||||
Name: "some-name",
|
Name: "some-name",
|
||||||
Version: "some-version",
|
Version: "some-version",
|
||||||
Type: "image",
|
Type: "image",
|
||||||
Metadata: source.StereoscopeImageSourceMetadata{
|
Metadata: source.ImageMetadata{
|
||||||
UserInput: "user-input",
|
UserInput: "user-input",
|
||||||
ID: "id...",
|
ID: "id...",
|
||||||
ManifestDigest: "digest...",
|
ManifestDigest: "digest...",
|
||||||
|
@ -92,7 +92,7 @@ func Test_toSyftSourceData(t *testing.T) {
|
||||||
ID: "the-id",
|
ID: "the-id",
|
||||||
Name: "some-name",
|
Name: "some-name",
|
||||||
Version: "some-version",
|
Version: "some-version",
|
||||||
Metadata: source.StereoscopeImageSourceMetadata{
|
Metadata: source.ImageMetadata{
|
||||||
UserInput: "user-input",
|
UserInput: "user-input",
|
||||||
ID: "id...",
|
ID: "id...",
|
||||||
ManifestDigest: "digest...",
|
ManifestDigest: "digest...",
|
||||||
|
@ -107,14 +107,14 @@ func Test_toSyftSourceData(t *testing.T) {
|
||||||
src: model.Source{
|
src: model.Source{
|
||||||
ID: "the-id",
|
ID: "the-id",
|
||||||
Type: "directory",
|
Type: "directory",
|
||||||
Metadata: source.DirectorySourceMetadata{
|
Metadata: source.DirectoryMetadata{
|
||||||
Path: "some/path",
|
Path: "some/path",
|
||||||
Base: "some/base",
|
Base: "some/base",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
expected: &source.Description{
|
expected: &source.Description{
|
||||||
ID: "the-id",
|
ID: "the-id",
|
||||||
Metadata: source.DirectorySourceMetadata{
|
Metadata: source.DirectoryMetadata{
|
||||||
Path: "some/path",
|
Path: "some/path",
|
||||||
Base: "some/base",
|
Base: "some/base",
|
||||||
},
|
},
|
||||||
|
@ -125,7 +125,7 @@ func Test_toSyftSourceData(t *testing.T) {
|
||||||
src: model.Source{
|
src: model.Source{
|
||||||
ID: "the-id",
|
ID: "the-id",
|
||||||
Type: "file",
|
Type: "file",
|
||||||
Metadata: source.FileSourceMetadata{
|
Metadata: source.FileMetadata{
|
||||||
Path: "some/path",
|
Path: "some/path",
|
||||||
Digests: []file.Digest{{Algorithm: "sha256", Value: "some-digest"}},
|
Digests: []file.Digest{{Algorithm: "sha256", Value: "some-digest"}},
|
||||||
MIMEType: "text/plain",
|
MIMEType: "text/plain",
|
||||||
|
@ -133,7 +133,7 @@ func Test_toSyftSourceData(t *testing.T) {
|
||||||
},
|
},
|
||||||
expected: &source.Description{
|
expected: &source.Description{
|
||||||
ID: "the-id",
|
ID: "the-id",
|
||||||
Metadata: source.FileSourceMetadata{
|
Metadata: source.FileMetadata{
|
||||||
Path: "some/path",
|
Path: "some/path",
|
||||||
Digests: []file.Digest{{Algorithm: "sha256", Value: "some-digest"}},
|
Digests: []file.Digest{{Algorithm: "sha256", Value: "some-digest"}},
|
||||||
MIMEType: "text/plain",
|
MIMEType: "text/plain",
|
||||||
|
@ -145,7 +145,7 @@ func Test_toSyftSourceData(t *testing.T) {
|
||||||
src: model.Source{
|
src: model.Source{
|
||||||
ID: "the-id",
|
ID: "the-id",
|
||||||
Type: "image",
|
Type: "image",
|
||||||
Metadata: source.StereoscopeImageSourceMetadata{
|
Metadata: source.ImageMetadata{
|
||||||
UserInput: "user-input",
|
UserInput: "user-input",
|
||||||
ID: "id...",
|
ID: "id...",
|
||||||
ManifestDigest: "digest...",
|
ManifestDigest: "digest...",
|
||||||
|
@ -154,7 +154,7 @@ func Test_toSyftSourceData(t *testing.T) {
|
||||||
},
|
},
|
||||||
expected: &source.Description{
|
expected: &source.Description{
|
||||||
ID: "the-id",
|
ID: "the-id",
|
||||||
Metadata: source.StereoscopeImageSourceMetadata{
|
Metadata: source.ImageMetadata{
|
||||||
UserInput: "user-input",
|
UserInput: "user-input",
|
||||||
ID: "id...",
|
ID: "id...",
|
||||||
ManifestDigest: "digest...",
|
ManifestDigest: "digest...",
|
||||||
|
@ -178,7 +178,7 @@ func Test_idsHaveChanged(t *testing.T) {
|
||||||
s := toSyftModel(model.Document{
|
s := toSyftModel(model.Document{
|
||||||
Source: model.Source{
|
Source: model.Source{
|
||||||
Type: "file",
|
Type: "file",
|
||||||
Metadata: source.FileSourceMetadata{Path: "some/path"},
|
Metadata: source.FileMetadata{Path: "some/path"},
|
||||||
},
|
},
|
||||||
Artifacts: []model.Package{
|
Artifacts: []model.Package{
|
||||||
{
|
{
|
||||||
|
|
|
@ -38,11 +38,11 @@ func (e encoder) Encode(writer io.Writer, s sbom.SBOM) error {
|
||||||
w.Init(writer, 0, 8, 0, '\t', tabwriter.AlignRight)
|
w.Init(writer, 0, 8, 0, '\t', tabwriter.AlignRight)
|
||||||
|
|
||||||
switch metadata := s.Source.Metadata.(type) {
|
switch metadata := s.Source.Metadata.(type) {
|
||||||
case source.DirectorySourceMetadata:
|
case source.DirectoryMetadata:
|
||||||
fmt.Fprintf(w, "[Path: %s]\n", metadata.Path)
|
fmt.Fprintf(w, "[Path: %s]\n", metadata.Path)
|
||||||
case source.FileSourceMetadata:
|
case source.FileMetadata:
|
||||||
fmt.Fprintf(w, "[Path: %s]\n", metadata.Path)
|
fmt.Fprintf(w, "[Path: %s]\n", metadata.Path)
|
||||||
case source.StereoscopeImageSourceMetadata:
|
case source.ImageMetadata:
|
||||||
fmt.Fprintln(w, "[Image]")
|
fmt.Fprintln(w, "[Image]")
|
||||||
|
|
||||||
for idx, l := range metadata.Layers {
|
for idx, l := range metadata.Layers {
|
||||||
|
|
101
syft/get_source.go
Normal file
101
syft/get_source.go
Normal file
|
@ -0,0 +1,101 @@
|
||||||
|
package syft
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/anchore/syft/syft/source"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetSource uses all of Syft's known source providers to attempt to resolve the user input to a usable source.Source
|
||||||
|
func GetSource(ctx context.Context, userInput string, cfg *GetSourceConfig) (source.Source, error) {
|
||||||
|
if cfg == nil {
|
||||||
|
cfg = DefaultGetSourceConfig()
|
||||||
|
}
|
||||||
|
|
||||||
|
providers, err := cfg.getProviders(userInput)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var errs []error
|
||||||
|
var fileNotfound error
|
||||||
|
|
||||||
|
// call each source provider until we find a valid source
|
||||||
|
for _, p := range providers {
|
||||||
|
src, err := p.Provide(ctx)
|
||||||
|
if err != nil {
|
||||||
|
err = eachError(err, func(err error) error {
|
||||||
|
if errors.Is(err, os.ErrNotExist) {
|
||||||
|
fileNotfound = err
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
errs = append(errs, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if src != nil {
|
||||||
|
// if we have a non-image type and platform is specified, it's an error
|
||||||
|
if cfg.SourceProviderConfig.Platform != nil {
|
||||||
|
meta := src.Describe().Metadata
|
||||||
|
switch meta.(type) {
|
||||||
|
case *source.ImageMetadata, source.ImageMetadata:
|
||||||
|
default:
|
||||||
|
return src, fmt.Errorf("platform specified with non-image source")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return src, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if fileNotfound != nil {
|
||||||
|
errs = append([]error{fileNotfound}, errs...)
|
||||||
|
}
|
||||||
|
return nil, sourceError(userInput, errs...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func sourceError(userInput string, errs ...error) error {
|
||||||
|
switch len(errs) {
|
||||||
|
case 0:
|
||||||
|
return nil
|
||||||
|
case 1:
|
||||||
|
return fmt.Errorf("an error occurred attempting to resolve '%s': %w", userInput, errs[0])
|
||||||
|
}
|
||||||
|
errorTexts := ""
|
||||||
|
for _, e := range errs {
|
||||||
|
errorTexts += fmt.Sprintf("\n - %s", e)
|
||||||
|
}
|
||||||
|
return fmt.Errorf("errors occurred attempting to resolve '%s':%s", userInput, errorTexts)
|
||||||
|
}
|
||||||
|
|
||||||
|
func eachError(err error, fn func(error) error) error {
|
||||||
|
out := fn(err)
|
||||||
|
// unwrap singly wrapped errors
|
||||||
|
if e, ok := err.(interface {
|
||||||
|
Unwrap() error
|
||||||
|
}); ok {
|
||||||
|
wrapped := e.Unwrap()
|
||||||
|
got := eachError(wrapped, fn)
|
||||||
|
// return the outer error if received the same wrapped error
|
||||||
|
if errors.Is(got, wrapped) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return got
|
||||||
|
}
|
||||||
|
// unwrap errors from errors.Join
|
||||||
|
if errs, ok := err.(interface {
|
||||||
|
Unwrap() []error
|
||||||
|
}); ok {
|
||||||
|
for _, e := range errs.Unwrap() {
|
||||||
|
e = eachError(e, fn)
|
||||||
|
if e != nil {
|
||||||
|
out = errors.Join(out, e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
91
syft/get_source_config.go
Normal file
91
syft/get_source_config.go
Normal file
|
@ -0,0 +1,91 @@
|
||||||
|
package syft
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/anchore/go-collections"
|
||||||
|
"github.com/anchore/stereoscope/pkg/image"
|
||||||
|
"github.com/anchore/syft/syft/source"
|
||||||
|
"github.com/anchore/syft/syft/source/sourceproviders"
|
||||||
|
)
|
||||||
|
|
||||||
|
type GetSourceConfig struct {
|
||||||
|
// SourceProviderConfig may optionally be provided to be used when constructing the default set of source providers, unused if All specified
|
||||||
|
SourceProviderConfig *sourceproviders.Config
|
||||||
|
|
||||||
|
// Sources is an explicit list of source names to use, in order, to attempt to locate a source
|
||||||
|
Sources []string
|
||||||
|
|
||||||
|
// DefaultImagePullSource will cause a particular image pull source to be used as the first pull source, followed by other pull sources
|
||||||
|
DefaultImagePullSource string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *GetSourceConfig) WithAlias(alias source.Alias) *GetSourceConfig {
|
||||||
|
c.SourceProviderConfig = c.SourceProviderConfig.WithAlias(alias)
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *GetSourceConfig) WithRegistryOptions(registryOptions *image.RegistryOptions) *GetSourceConfig {
|
||||||
|
c.SourceProviderConfig = c.SourceProviderConfig.WithRegistryOptions(registryOptions)
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *GetSourceConfig) WithPlatform(platform *image.Platform) *GetSourceConfig {
|
||||||
|
c.SourceProviderConfig = c.SourceProviderConfig.WithPlatform(platform)
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *GetSourceConfig) WithExcludeConfig(excludeConfig source.ExcludeConfig) *GetSourceConfig {
|
||||||
|
c.SourceProviderConfig = c.SourceProviderConfig.WithExcludeConfig(excludeConfig)
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *GetSourceConfig) WithDigestAlgorithms(algorithms ...crypto.Hash) *GetSourceConfig {
|
||||||
|
c.SourceProviderConfig = c.SourceProviderConfig.WithDigestAlgorithms(algorithms...)
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *GetSourceConfig) WithBasePath(basePath string) *GetSourceConfig {
|
||||||
|
c.SourceProviderConfig = c.SourceProviderConfig.WithBasePath(basePath)
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *GetSourceConfig) WithSources(sources ...string) *GetSourceConfig {
|
||||||
|
c.Sources = sources
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *GetSourceConfig) WithDefaultImagePullSource(defaultImagePullSource string) *GetSourceConfig {
|
||||||
|
c.DefaultImagePullSource = defaultImagePullSource
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *GetSourceConfig) getProviders(userInput string) ([]source.Provider, error) {
|
||||||
|
providers := collections.TaggedValueSet[source.Provider]{}.Join(sourceproviders.All(userInput, c.SourceProviderConfig)...)
|
||||||
|
|
||||||
|
// if the "default image pull source" is set, we move this as the first pull source
|
||||||
|
if c.DefaultImagePullSource != "" {
|
||||||
|
base := providers.Remove(sourceproviders.PullTag)
|
||||||
|
pull := providers.Select(sourceproviders.PullTag)
|
||||||
|
def := pull.Select(c.DefaultImagePullSource)
|
||||||
|
if len(def) == 0 {
|
||||||
|
return nil, fmt.Errorf("invalid DefaultImagePullSource: %s; available values are: %v", c.DefaultImagePullSource, pull.Tags())
|
||||||
|
}
|
||||||
|
providers = base.Join(def...).Join(pull...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// narrow the sources to those explicitly requested generally by a user
|
||||||
|
if len(c.Sources) > 0 {
|
||||||
|
// select the explicitly provided sources, in order
|
||||||
|
providers = providers.Select(c.Sources...)
|
||||||
|
}
|
||||||
|
|
||||||
|
return providers.Values(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func DefaultGetSourceConfig() *GetSourceConfig {
|
||||||
|
return &GetSourceConfig{
|
||||||
|
SourceProviderConfig: sourceproviders.DefaultConfig(),
|
||||||
|
}
|
||||||
|
}
|
|
@ -6,5 +6,5 @@ import "github.com/anchore/syft/syft/source"
|
||||||
|
|
||||||
// AllTypes returns a list of all source metadata types that syft supports (that are represented in the source.Description.Metadata field).
|
// AllTypes returns a list of all source metadata types that syft supports (that are represented in the source.Description.Metadata field).
|
||||||
func AllTypes() []any {
|
func AllTypes() []any {
|
||||||
return []any{source.DirectorySourceMetadata{}, source.FileSourceMetadata{}, source.StereoscopeImageSourceMetadata{}}
|
return []any{source.DirectoryMetadata{}, source.FileMetadata{}, source.ImageMetadata{}}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,9 +8,9 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
var jsonNameFromType = map[reflect.Type][]string{
|
var jsonNameFromType = map[reflect.Type][]string{
|
||||||
reflect.TypeOf(source.DirectorySourceMetadata{}): {"directory", "dir"},
|
reflect.TypeOf(source.DirectoryMetadata{}): {"directory", "dir"},
|
||||||
reflect.TypeOf(source.FileSourceMetadata{}): {"file"},
|
reflect.TypeOf(source.FileMetadata{}): {"file"},
|
||||||
reflect.TypeOf(source.StereoscopeImageSourceMetadata{}): {"image"},
|
reflect.TypeOf(source.ImageMetadata{}): {"image"},
|
||||||
}
|
}
|
||||||
|
|
||||||
func AllTypeNames() []string {
|
func AllTypeNames() []string {
|
||||||
|
|
26
syft/internal/testutil/chdir.go
Normal file
26
syft/internal/testutil/chdir.go
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
package testutil
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Chdir(t *testing.T, dir string) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
wd, err := os.Getwd()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to get working directory: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = os.Chdir(dir)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to chdir to '%s': %v", dir, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Cleanup(func() {
|
||||||
|
require.NoError(t, os.Chdir(wd))
|
||||||
|
})
|
||||||
|
}
|
|
@ -9,6 +9,7 @@ import (
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
"github.com/anchore/syft/syft/source"
|
"github.com/anchore/syft/syft/source"
|
||||||
|
"github.com/anchore/syft/syft/source/directorysource"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestIdentifyRelease(t *testing.T) {
|
func TestIdentifyRelease(t *testing.T) {
|
||||||
|
@ -336,7 +337,9 @@ func TestIdentifyRelease(t *testing.T) {
|
||||||
|
|
||||||
for _, test := range tests {
|
for _, test := range tests {
|
||||||
t.Run(test.fixture, func(t *testing.T) {
|
t.Run(test.fixture, func(t *testing.T) {
|
||||||
s, err := source.NewFromDirectoryPath(test.fixture)
|
s, err := directorysource.New(directorysource.Config{
|
||||||
|
Path: test.fixture,
|
||||||
|
})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
resolver, err := s.FileResolver(source.SquashedScope)
|
resolver, err := s.FileResolver(source.SquashedScope)
|
||||||
|
|
|
@ -21,6 +21,8 @@ import (
|
||||||
"github.com/anchore/syft/syft/pkg"
|
"github.com/anchore/syft/syft/pkg"
|
||||||
"github.com/anchore/syft/syft/pkg/cataloger/binary/test-fixtures/manager/testutil"
|
"github.com/anchore/syft/syft/pkg/cataloger/binary/test-fixtures/manager/testutil"
|
||||||
"github.com/anchore/syft/syft/source"
|
"github.com/anchore/syft/syft/source"
|
||||||
|
"github.com/anchore/syft/syft/source/directorysource"
|
||||||
|
"github.com/anchore/syft/syft/source/stereoscopesource"
|
||||||
)
|
)
|
||||||
|
|
||||||
var mustUseOriginalBinaries = flag.Bool("must-use-original-binaries", false, "force the use of binaries for testing (instead of snippets)")
|
var mustUseOriginalBinaries = flag.Bool("must-use-original-binaries", false, "force the use of binaries for testing (instead of snippets)")
|
||||||
|
@ -896,7 +898,7 @@ func Test_Cataloger_PositiveCases(t *testing.T) {
|
||||||
// full binaries are tested (no snippets), and if no binary is found the test will be skipped.
|
// full binaries are tested (no snippets), and if no binary is found the test will be skipped.
|
||||||
path := testutil.SnippetOrBinary(t, test.logicalFixture, *mustUseOriginalBinaries)
|
path := testutil.SnippetOrBinary(t, test.logicalFixture, *mustUseOriginalBinaries)
|
||||||
|
|
||||||
src, err := source.NewFromDirectoryPath(path)
|
src, err := directorysource.NewFromPath(path)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
resolver, err := src.FileResolver(source.SquashedScope)
|
resolver, err := src.FileResolver(source.SquashedScope)
|
||||||
|
@ -936,8 +938,9 @@ func Test_Cataloger_DefaultClassifiers_PositiveCases_Image(t *testing.T) {
|
||||||
c := NewClassifierCataloger(DefaultClassifierCatalogerConfig())
|
c := NewClassifierCataloger(DefaultClassifierCatalogerConfig())
|
||||||
|
|
||||||
img := imagetest.GetFixtureImage(t, "docker-archive", test.fixtureImage)
|
img := imagetest.GetFixtureImage(t, "docker-archive", test.fixtureImage)
|
||||||
src, err := source.NewFromStereoscopeImageObject(img, test.fixtureImage, nil)
|
src := stereoscopesource.New(img, stereoscopesource.ImageConfig{
|
||||||
require.NoError(t, err)
|
Reference: test.fixtureImage,
|
||||||
|
})
|
||||||
|
|
||||||
resolver, err := src.FileResolver(source.SquashedScope)
|
resolver, err := src.FileResolver(source.SquashedScope)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
@ -966,7 +969,7 @@ func Test_Cataloger_DefaultClassifiers_PositiveCases_Image(t *testing.T) {
|
||||||
func TestClassifierCataloger_DefaultClassifiers_NegativeCases(t *testing.T) {
|
func TestClassifierCataloger_DefaultClassifiers_NegativeCases(t *testing.T) {
|
||||||
c := NewClassifierCataloger(DefaultClassifierCatalogerConfig())
|
c := NewClassifierCataloger(DefaultClassifierCatalogerConfig())
|
||||||
|
|
||||||
src, err := source.NewFromDirectoryPath("test-fixtures/classifiers/negative")
|
src, err := directorysource.NewFromPath("test-fixtures/classifiers/negative")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
resolver, err := src.FileResolver(source.SquashedScope)
|
resolver, err := src.FileResolver(source.SquashedScope)
|
||||||
|
@ -1080,7 +1083,7 @@ func Test_Cataloger_CustomClassifiers(t *testing.T) {
|
||||||
t.Run(test.name, func(t *testing.T) {
|
t.Run(test.name, func(t *testing.T) {
|
||||||
c := NewClassifierCataloger(test.config)
|
c := NewClassifierCataloger(test.config)
|
||||||
|
|
||||||
src, err := source.NewFromDirectoryPath(test.fixtureDir)
|
src, err := directorysource.NewFromPath(test.fixtureDir)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
resolver, err := src.FileResolver(source.SquashedScope)
|
resolver, err := src.FileResolver(source.SquashedScope)
|
||||||
|
|
|
@ -22,6 +22,8 @@ import (
|
||||||
"github.com/anchore/syft/syft/pkg"
|
"github.com/anchore/syft/syft/pkg"
|
||||||
"github.com/anchore/syft/syft/pkg/cataloger/generic"
|
"github.com/anchore/syft/syft/pkg/cataloger/generic"
|
||||||
"github.com/anchore/syft/syft/source"
|
"github.com/anchore/syft/syft/source"
|
||||||
|
"github.com/anchore/syft/syft/source/directorysource"
|
||||||
|
"github.com/anchore/syft/syft/source/stereoscopesource"
|
||||||
)
|
)
|
||||||
|
|
||||||
type locationComparer func(x, y file.Location) bool
|
type locationComparer func(x, y file.Location) bool
|
||||||
|
@ -88,7 +90,7 @@ func DefaultLicenseComparer(x, y pkg.License) bool {
|
||||||
func (p *CatalogTester) FromDirectory(t *testing.T, path string) *CatalogTester {
|
func (p *CatalogTester) FromDirectory(t *testing.T, path string) *CatalogTester {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
s, err := source.NewFromDirectoryPath(path)
|
s, err := directorysource.NewFromPath(path)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
resolver, err := s.FileResolver(source.AllLayersScope)
|
resolver, err := s.FileResolver(source.AllLayersScope)
|
||||||
|
@ -152,8 +154,9 @@ func (p *CatalogTester) WithImageResolver(t *testing.T, fixtureName string) *Cat
|
||||||
t.Helper()
|
t.Helper()
|
||||||
img := imagetest.GetFixtureImage(t, "docker-archive", fixtureName)
|
img := imagetest.GetFixtureImage(t, "docker-archive", fixtureName)
|
||||||
|
|
||||||
s, err := source.NewFromStereoscopeImageObject(img, fixtureName, nil)
|
s := stereoscopesource.New(img, stereoscopesource.ImageConfig{
|
||||||
require.NoError(t, err)
|
Reference: fixtureName,
|
||||||
|
})
|
||||||
|
|
||||||
r, err := s.FileResolver(source.SquashedScope)
|
r, err := s.FileResolver(source.SquashedScope)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
|
@ -1,207 +0,0 @@
|
||||||
package source
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto"
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/mitchellh/go-homedir"
|
|
||||||
"github.com/spf13/afero"
|
|
||||||
|
|
||||||
"github.com/anchore/stereoscope/pkg/image"
|
|
||||||
)
|
|
||||||
|
|
||||||
type detectedType string
|
|
||||||
|
|
||||||
const (
|
|
||||||
// unknownType is the default scheme
|
|
||||||
unknownType detectedType = "unknown-type"
|
|
||||||
|
|
||||||
// directoryType indicates the source being cataloged is a directory on the root filesystem
|
|
||||||
directoryType detectedType = "directory-type"
|
|
||||||
|
|
||||||
// containerImageType indicates the source being cataloged is a container image
|
|
||||||
containerImageType detectedType = "container-image-type"
|
|
||||||
|
|
||||||
// fileType indicates the source being cataloged is a single file
|
|
||||||
fileType detectedType = "file-type"
|
|
||||||
)
|
|
||||||
|
|
||||||
type sourceResolver func(string) (image.Source, string, error)
|
|
||||||
|
|
||||||
// Detection is an object that captures the detected user input regarding source location, scheme, and provider type.
|
|
||||||
// It acts as a struct input for some source constructors.
|
|
||||||
type Detection struct {
|
|
||||||
detectedType detectedType
|
|
||||||
imageSource image.Source
|
|
||||||
location string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d Detection) IsContainerImage() bool {
|
|
||||||
return d.detectedType == containerImageType
|
|
||||||
}
|
|
||||||
|
|
||||||
type DetectConfig struct {
|
|
||||||
DefaultImageSource string
|
|
||||||
}
|
|
||||||
|
|
||||||
func DefaultDetectConfig() DetectConfig {
|
|
||||||
return DetectConfig{}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Detect generates a source Detection that can be used as an argument to generate a new source
|
|
||||||
// from specific providers including a registry, with an explicit name.
|
|
||||||
func Detect(userInput string, cfg DetectConfig) (*Detection, error) {
|
|
||||||
fs := afero.NewOsFs()
|
|
||||||
ty, src, location, err := detect(fs, image.DetectSource, userInput)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if src == image.UnknownSource {
|
|
||||||
// only run for these two schemes
|
|
||||||
// only check on scan command, attest we automatically try to pull from userInput
|
|
||||||
switch ty {
|
|
||||||
case containerImageType, unknownType:
|
|
||||||
ty = containerImageType
|
|
||||||
location = userInput
|
|
||||||
if cfg.DefaultImageSource != "" {
|
|
||||||
src = parseDefaultImageSource(cfg.DefaultImageSource)
|
|
||||||
} else {
|
|
||||||
src = image.DetermineDefaultImagePullSource(userInput)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// collect user input for downstream consumption
|
|
||||||
return &Detection{
|
|
||||||
detectedType: ty,
|
|
||||||
imageSource: src,
|
|
||||||
location: location,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type DetectionSourceConfig struct {
|
|
||||||
Alias Alias
|
|
||||||
RegistryOptions *image.RegistryOptions
|
|
||||||
Platform *image.Platform
|
|
||||||
Exclude ExcludeConfig
|
|
||||||
DigestAlgorithms []crypto.Hash
|
|
||||||
BasePath string
|
|
||||||
}
|
|
||||||
|
|
||||||
func DefaultDetectionSourceConfig() DetectionSourceConfig {
|
|
||||||
return DetectionSourceConfig{
|
|
||||||
DigestAlgorithms: []crypto.Hash{
|
|
||||||
crypto.SHA256,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewSource produces a Source based on userInput like dir: or image:tag
|
|
||||||
func (d Detection) NewSource(cfg DetectionSourceConfig) (Source, error) {
|
|
||||||
var err error
|
|
||||||
var src Source
|
|
||||||
|
|
||||||
if d.detectedType != containerImageType && cfg.Platform != nil {
|
|
||||||
return nil, fmt.Errorf("cannot specify a platform for a non-image source")
|
|
||||||
}
|
|
||||||
|
|
||||||
switch d.detectedType {
|
|
||||||
case fileType:
|
|
||||||
src, err = NewFromFile(
|
|
||||||
FileConfig{
|
|
||||||
Path: d.location,
|
|
||||||
Exclude: cfg.Exclude,
|
|
||||||
DigestAlgorithms: cfg.DigestAlgorithms,
|
|
||||||
Alias: cfg.Alias,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
case directoryType:
|
|
||||||
base := cfg.BasePath
|
|
||||||
if base == "" {
|
|
||||||
base = d.location
|
|
||||||
}
|
|
||||||
src, err = NewFromDirectory(
|
|
||||||
DirectoryConfig{
|
|
||||||
Path: d.location,
|
|
||||||
Base: base,
|
|
||||||
Exclude: cfg.Exclude,
|
|
||||||
Alias: cfg.Alias,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
case containerImageType:
|
|
||||||
src, err = NewFromStereoscopeImage(
|
|
||||||
StereoscopeImageConfig{
|
|
||||||
Reference: d.location,
|
|
||||||
From: d.imageSource,
|
|
||||||
Platform: cfg.Platform,
|
|
||||||
RegistryOptions: cfg.RegistryOptions,
|
|
||||||
Exclude: cfg.Exclude,
|
|
||||||
Alias: cfg.Alias,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
default:
|
|
||||||
err = fmt.Errorf("unable to process input for scanning")
|
|
||||||
}
|
|
||||||
|
|
||||||
return src, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func detect(fs afero.Fs, imageSourceResolver sourceResolver, userInput string) (detectedType, image.Source, string, error) {
|
|
||||||
switch {
|
|
||||||
case strings.HasPrefix(userInput, "dir:"):
|
|
||||||
dirLocation, err := homedir.Expand(strings.TrimPrefix(userInput, "dir:"))
|
|
||||||
if err != nil {
|
|
||||||
return unknownType, image.UnknownSource, "", fmt.Errorf("unable to expand directory path: %w", err)
|
|
||||||
}
|
|
||||||
return directoryType, image.UnknownSource, dirLocation, nil
|
|
||||||
|
|
||||||
case strings.HasPrefix(userInput, "file:"):
|
|
||||||
fileLocation, err := homedir.Expand(strings.TrimPrefix(userInput, "file:"))
|
|
||||||
if err != nil {
|
|
||||||
return unknownType, image.UnknownSource, "", fmt.Errorf("unable to expand directory path: %w", err)
|
|
||||||
}
|
|
||||||
return fileType, image.UnknownSource, fileLocation, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// try the most specific sources first and move out towards more generic sources.
|
|
||||||
|
|
||||||
// first: let's try the image detector, which has more scheme parsing internal to stereoscope
|
|
||||||
src, imageSpec, err := imageSourceResolver(userInput)
|
|
||||||
if err == nil && src != image.UnknownSource {
|
|
||||||
return containerImageType, src, imageSpec, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// next: let's try more generic sources (dir, file, etc.)
|
|
||||||
location, err := homedir.Expand(userInput)
|
|
||||||
if err != nil {
|
|
||||||
return unknownType, image.UnknownSource, "", fmt.Errorf("unable to expand potential directory path: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
fileMeta, err := fs.Stat(location)
|
|
||||||
if err != nil {
|
|
||||||
return unknownType, src, "", nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if fileMeta.IsDir() {
|
|
||||||
return directoryType, src, location, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return fileType, src, location, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseDefaultImageSource(defaultImageSource string) image.Source {
|
|
||||||
switch defaultImageSource {
|
|
||||||
case "registry":
|
|
||||||
return image.OciRegistrySource
|
|
||||||
case "docker":
|
|
||||||
return image.DockerDaemonSource
|
|
||||||
case "podman":
|
|
||||||
return image.PodmanDaemonSource
|
|
||||||
case "containerd":
|
|
||||||
return image.ContainerdDaemonSource
|
|
||||||
default:
|
|
||||||
return image.UnknownSource
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,328 +0,0 @@
|
||||||
package source
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/mitchellh/go-homedir"
|
|
||||||
"github.com/spf13/afero"
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
|
|
||||||
"github.com/anchore/stereoscope/pkg/image"
|
|
||||||
)
|
|
||||||
|
|
||||||
func Test_Detect(t *testing.T) {
|
|
||||||
type detectorResult struct {
|
|
||||||
src image.Source
|
|
||||||
ref string
|
|
||||||
err error
|
|
||||||
}
|
|
||||||
|
|
||||||
testCases := []struct {
|
|
||||||
name string
|
|
||||||
userInput string
|
|
||||||
dirs []string
|
|
||||||
files []string
|
|
||||||
detection detectorResult
|
|
||||||
expectedScheme detectedType
|
|
||||||
expectedLocation string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "docker-image-ref",
|
|
||||||
userInput: "wagoodman/dive:latest",
|
|
||||||
detection: detectorResult{
|
|
||||||
src: image.DockerDaemonSource,
|
|
||||||
ref: "wagoodman/dive:latest",
|
|
||||||
},
|
|
||||||
expectedScheme: containerImageType,
|
|
||||||
expectedLocation: "wagoodman/dive:latest",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "docker-image-ref-no-tag",
|
|
||||||
userInput: "wagoodman/dive",
|
|
||||||
detection: detectorResult{
|
|
||||||
src: image.DockerDaemonSource,
|
|
||||||
ref: "wagoodman/dive",
|
|
||||||
},
|
|
||||||
expectedScheme: containerImageType,
|
|
||||||
expectedLocation: "wagoodman/dive",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "registry-image-explicit-scheme",
|
|
||||||
userInput: "registry:wagoodman/dive:latest",
|
|
||||||
detection: detectorResult{
|
|
||||||
src: image.OciRegistrySource,
|
|
||||||
ref: "wagoodman/dive:latest",
|
|
||||||
},
|
|
||||||
expectedScheme: containerImageType,
|
|
||||||
expectedLocation: "wagoodman/dive:latest",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "docker-image-explicit-scheme",
|
|
||||||
userInput: "docker:wagoodman/dive:latest",
|
|
||||||
detection: detectorResult{
|
|
||||||
src: image.DockerDaemonSource,
|
|
||||||
ref: "wagoodman/dive:latest",
|
|
||||||
},
|
|
||||||
expectedScheme: containerImageType,
|
|
||||||
expectedLocation: "wagoodman/dive:latest",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "docker-image-explicit-scheme-no-tag",
|
|
||||||
userInput: "docker:wagoodman/dive",
|
|
||||||
detection: detectorResult{
|
|
||||||
src: image.DockerDaemonSource,
|
|
||||||
ref: "wagoodman/dive",
|
|
||||||
},
|
|
||||||
expectedScheme: containerImageType,
|
|
||||||
expectedLocation: "wagoodman/dive",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "docker-image-edge-case",
|
|
||||||
userInput: "docker:latest",
|
|
||||||
detection: detectorResult{
|
|
||||||
src: image.DockerDaemonSource,
|
|
||||||
ref: "latest",
|
|
||||||
},
|
|
||||||
expectedScheme: containerImageType,
|
|
||||||
// we expected to be able to handle this case better, however, I don't see a way to do this
|
|
||||||
// the user will need to provide more explicit input (docker:docker:latest)
|
|
||||||
expectedLocation: "latest",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "docker-image-edge-case-explicit",
|
|
||||||
userInput: "docker:docker:latest",
|
|
||||||
detection: detectorResult{
|
|
||||||
src: image.DockerDaemonSource,
|
|
||||||
ref: "docker:latest",
|
|
||||||
},
|
|
||||||
expectedScheme: containerImageType,
|
|
||||||
// we expected to be able to handle this case better, however, I don't see a way to do this
|
|
||||||
// the user will need to provide more explicit input (docker:docker:latest)
|
|
||||||
expectedLocation: "docker:latest",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "oci-tar",
|
|
||||||
userInput: "some/path-to-file",
|
|
||||||
detection: detectorResult{
|
|
||||||
src: image.OciTarballSource,
|
|
||||||
ref: "some/path-to-file",
|
|
||||||
},
|
|
||||||
expectedScheme: containerImageType,
|
|
||||||
expectedLocation: "some/path-to-file",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "oci-dir",
|
|
||||||
userInput: "some/path-to-dir",
|
|
||||||
detection: detectorResult{
|
|
||||||
src: image.OciDirectorySource,
|
|
||||||
ref: "some/path-to-dir",
|
|
||||||
},
|
|
||||||
dirs: []string{"some/path-to-dir"},
|
|
||||||
expectedScheme: containerImageType,
|
|
||||||
expectedLocation: "some/path-to-dir",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "guess-dir",
|
|
||||||
userInput: "some/path-to-dir",
|
|
||||||
detection: detectorResult{
|
|
||||||
src: image.UnknownSource,
|
|
||||||
ref: "",
|
|
||||||
},
|
|
||||||
dirs: []string{"some/path-to-dir"},
|
|
||||||
expectedScheme: directoryType,
|
|
||||||
expectedLocation: "some/path-to-dir",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "generic-dir-does-not-exist",
|
|
||||||
userInput: "some/path-to-dir",
|
|
||||||
detection: detectorResult{
|
|
||||||
src: image.DockerDaemonSource,
|
|
||||||
ref: "some/path-to-dir",
|
|
||||||
},
|
|
||||||
expectedScheme: containerImageType,
|
|
||||||
expectedLocation: "some/path-to-dir",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "found-podman-image-scheme",
|
|
||||||
userInput: "podman:something:latest",
|
|
||||||
detection: detectorResult{
|
|
||||||
src: image.PodmanDaemonSource,
|
|
||||||
ref: "something:latest",
|
|
||||||
},
|
|
||||||
expectedScheme: containerImageType,
|
|
||||||
expectedLocation: "something:latest",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "explicit-dir",
|
|
||||||
userInput: "dir:some/path-to-dir",
|
|
||||||
detection: detectorResult{
|
|
||||||
src: image.UnknownSource,
|
|
||||||
ref: "",
|
|
||||||
},
|
|
||||||
dirs: []string{"some/path-to-dir"},
|
|
||||||
expectedScheme: directoryType,
|
|
||||||
expectedLocation: "some/path-to-dir",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "explicit-file",
|
|
||||||
userInput: "file:some/path-to-file",
|
|
||||||
detection: detectorResult{
|
|
||||||
src: image.UnknownSource,
|
|
||||||
ref: "",
|
|
||||||
},
|
|
||||||
files: []string{"some/path-to-file"},
|
|
||||||
expectedScheme: fileType,
|
|
||||||
expectedLocation: "some/path-to-file",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "implicit-file",
|
|
||||||
userInput: "some/path-to-file",
|
|
||||||
detection: detectorResult{
|
|
||||||
src: image.UnknownSource,
|
|
||||||
ref: "",
|
|
||||||
},
|
|
||||||
files: []string{"some/path-to-file"},
|
|
||||||
expectedScheme: fileType,
|
|
||||||
expectedLocation: "some/path-to-file",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "explicit-current-dir",
|
|
||||||
userInput: "dir:.",
|
|
||||||
detection: detectorResult{
|
|
||||||
src: image.UnknownSource,
|
|
||||||
ref: "",
|
|
||||||
},
|
|
||||||
expectedScheme: directoryType,
|
|
||||||
expectedLocation: ".",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "current-dir",
|
|
||||||
userInput: ".",
|
|
||||||
detection: detectorResult{
|
|
||||||
src: image.UnknownSource,
|
|
||||||
ref: "",
|
|
||||||
},
|
|
||||||
expectedScheme: directoryType,
|
|
||||||
expectedLocation: ".",
|
|
||||||
},
|
|
||||||
// we should support tilde expansion
|
|
||||||
{
|
|
||||||
name: "tilde-expansion-image-implicit",
|
|
||||||
userInput: "~/some-path",
|
|
||||||
detection: detectorResult{
|
|
||||||
src: image.OciDirectorySource,
|
|
||||||
ref: "~/some-path",
|
|
||||||
},
|
|
||||||
expectedScheme: containerImageType,
|
|
||||||
expectedLocation: "~/some-path",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "tilde-expansion-dir-implicit",
|
|
||||||
userInput: "~/some-path",
|
|
||||||
detection: detectorResult{
|
|
||||||
src: image.UnknownSource,
|
|
||||||
ref: "",
|
|
||||||
},
|
|
||||||
dirs: []string{"~/some-path"},
|
|
||||||
expectedScheme: directoryType,
|
|
||||||
expectedLocation: "~/some-path",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "tilde-expansion-dir-explicit-exists",
|
|
||||||
userInput: "dir:~/some-path",
|
|
||||||
dirs: []string{"~/some-path"},
|
|
||||||
expectedScheme: directoryType,
|
|
||||||
expectedLocation: "~/some-path",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "tilde-expansion-dir-explicit-dne",
|
|
||||||
userInput: "dir:~/some-path",
|
|
||||||
expectedScheme: directoryType,
|
|
||||||
expectedLocation: "~/some-path",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "tilde-expansion-dir-implicit-dne",
|
|
||||||
userInput: "~/some-path",
|
|
||||||
expectedScheme: unknownType,
|
|
||||||
expectedLocation: "",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "podman-image",
|
|
||||||
userInput: "containerd:anchore/syft",
|
|
||||||
detection: detectorResult{
|
|
||||||
src: image.PodmanDaemonSource,
|
|
||||||
ref: "anchore/syft",
|
|
||||||
},
|
|
||||||
expectedScheme: containerImageType,
|
|
||||||
expectedLocation: "anchore/syft",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "containerd-image",
|
|
||||||
userInput: "containerd:anchore/syft",
|
|
||||||
detection: detectorResult{
|
|
||||||
src: image.ContainerdDaemonSource,
|
|
||||||
ref: "anchore/syft",
|
|
||||||
},
|
|
||||||
expectedScheme: containerImageType,
|
|
||||||
expectedLocation: "anchore/syft",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
for _, test := range testCases {
|
|
||||||
t.Run(test.name, func(t *testing.T) {
|
|
||||||
fs := afero.NewMemMapFs()
|
|
||||||
|
|
||||||
for _, p := range test.dirs {
|
|
||||||
expandedExpectedLocation, err := homedir.Expand(p)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unable to expand path=%q: %+v", p, err)
|
|
||||||
}
|
|
||||||
err = fs.Mkdir(expandedExpectedLocation, os.ModePerm)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("failed to create dummy dir: %+v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, p := range test.files {
|
|
||||||
expandedExpectedLocation, err := homedir.Expand(p)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unable to expand path=%q: %+v", p, err)
|
|
||||||
}
|
|
||||||
_, err = fs.Create(expandedExpectedLocation)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("failed to create dummy file: %+v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
imageDetector := func(string) (image.Source, string, error) {
|
|
||||||
// lean on the users real home directory value
|
|
||||||
switch test.detection.src {
|
|
||||||
case image.OciDirectorySource, image.DockerTarballSource, image.OciTarballSource:
|
|
||||||
expandedExpectedLocation, err := homedir.Expand(test.expectedLocation)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unable to expand path=%q: %+v", test.expectedLocation, err)
|
|
||||||
}
|
|
||||||
return test.detection.src, expandedExpectedLocation, test.detection.err
|
|
||||||
default:
|
|
||||||
return test.detection.src, test.detection.ref, test.detection.err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
actualScheme, actualSource, actualLocation, err := detect(fs, imageDetector, test.userInput)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unexpected err : %+v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
assert.Equal(t, test.detection.src, actualSource, "mismatched source")
|
|
||||||
assert.Equal(t, test.expectedScheme, actualScheme, "mismatched scheme")
|
|
||||||
|
|
||||||
// lean on the users real home directory value
|
|
||||||
expandedExpectedLocation, err := homedir.Expand(test.expectedLocation)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unable to expand path=%q: %+v", test.expectedLocation, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
assert.Equal(t, expandedExpectedLocation, actualLocation, "mismatched location")
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
6
syft/source/directory_metadata.go
Normal file
6
syft/source/directory_metadata.go
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
package source
|
||||||
|
|
||||||
|
type DirectoryMetadata struct {
|
||||||
|
Path string `json:"path" yaml:"path"`
|
||||||
|
Base string `json:"-" yaml:"-"` // though this is important, for display purposes it leaks too much information (abs paths)
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
package source
|
package directorysource
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
@ -14,37 +14,34 @@ import (
|
||||||
"github.com/anchore/syft/syft/artifact"
|
"github.com/anchore/syft/syft/artifact"
|
||||||
"github.com/anchore/syft/syft/file"
|
"github.com/anchore/syft/syft/file"
|
||||||
"github.com/anchore/syft/syft/internal/fileresolver"
|
"github.com/anchore/syft/syft/internal/fileresolver"
|
||||||
|
"github.com/anchore/syft/syft/source"
|
||||||
|
"github.com/anchore/syft/syft/source/internal"
|
||||||
)
|
)
|
||||||
|
|
||||||
var _ Source = (*DirectorySource)(nil)
|
var _ source.Source = (*directorySource)(nil)
|
||||||
|
|
||||||
type DirectoryConfig struct {
|
type Config struct {
|
||||||
Path string
|
Path string
|
||||||
Base string
|
Base string
|
||||||
Exclude ExcludeConfig
|
Exclude source.ExcludeConfig
|
||||||
Alias Alias
|
Alias source.Alias
|
||||||
}
|
}
|
||||||
|
|
||||||
type DirectorySourceMetadata struct {
|
type directorySource struct {
|
||||||
Path string `json:"path" yaml:"path"`
|
|
||||||
Base string `json:"-" yaml:"-"` // though this is important, for display purposes it leaks too much information (abs paths)
|
|
||||||
}
|
|
||||||
|
|
||||||
type DirectorySource struct {
|
|
||||||
id artifact.ID
|
id artifact.ID
|
||||||
config DirectoryConfig
|
config Config
|
||||||
resolver *fileresolver.Directory
|
resolver *fileresolver.Directory
|
||||||
mutex *sync.Mutex
|
mutex *sync.Mutex
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewFromDirectoryPath(path string) (*DirectorySource, error) {
|
func NewFromPath(path string) (source.Source, error) {
|
||||||
cfg := DirectoryConfig{
|
cfg := Config{
|
||||||
Path: path,
|
Path: path,
|
||||||
}
|
}
|
||||||
return NewFromDirectory(cfg)
|
return New(cfg)
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewFromDirectory(cfg DirectoryConfig) (*DirectorySource, error) {
|
func New(cfg Config) (source.Source, error) {
|
||||||
fi, err := os.Stat(cfg.Path)
|
fi, err := os.Stat(cfg.Path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("unable to stat path=%q: %w", cfg.Path, err)
|
return nil, fmt.Errorf("unable to stat path=%q: %w", cfg.Path, err)
|
||||||
|
@ -54,7 +51,7 @@ func NewFromDirectory(cfg DirectoryConfig) (*DirectorySource, error) {
|
||||||
return nil, fmt.Errorf("given path is not a directory: %q", cfg.Path)
|
return nil, fmt.Errorf("given path is not a directory: %q", cfg.Path)
|
||||||
}
|
}
|
||||||
|
|
||||||
return &DirectorySource{
|
return &directorySource{
|
||||||
id: deriveIDFromDirectory(cfg),
|
id: deriveIDFromDirectory(cfg),
|
||||||
config: cfg,
|
config: cfg,
|
||||||
mutex: &sync.Mutex{},
|
mutex: &sync.Mutex{},
|
||||||
|
@ -66,7 +63,7 @@ func NewFromDirectory(cfg DirectoryConfig) (*DirectorySource, error) {
|
||||||
// from the path provided with an attempt to prune a prefix if a base is given. Since the contents of the directory
|
// from the path provided with an attempt to prune a prefix if a base is given. Since the contents of the directory
|
||||||
// are not considered, there is no semantic meaning to the artifact ID -- this is why the alias is preferred without
|
// are not considered, there is no semantic meaning to the artifact ID -- this is why the alias is preferred without
|
||||||
// consideration for the path.
|
// consideration for the path.
|
||||||
func deriveIDFromDirectory(cfg DirectoryConfig) artifact.ID {
|
func deriveIDFromDirectory(cfg Config) artifact.ID {
|
||||||
var info string
|
var info string
|
||||||
if !cfg.Alias.IsEmpty() {
|
if !cfg.Alias.IsEmpty() {
|
||||||
// don't use any of the path information -- instead use the alias name and version as the artifact ID.
|
// don't use any of the path information -- instead use the alias name and version as the artifact ID.
|
||||||
|
@ -78,7 +75,7 @@ func deriveIDFromDirectory(cfg DirectoryConfig) artifact.ID {
|
||||||
info = cleanDirPath(cfg.Path, cfg.Base)
|
info = cleanDirPath(cfg.Path, cfg.Base)
|
||||||
}
|
}
|
||||||
|
|
||||||
return artifactIDFromDigest(digest.SHA256.FromString(filepath.Clean(info)).String())
|
return internal.ArtifactIDFromDigest(digest.SHA256.FromString(filepath.Clean(info)).String())
|
||||||
}
|
}
|
||||||
|
|
||||||
func cleanDirPath(path, base string) string {
|
func cleanDirPath(path, base string) string {
|
||||||
|
@ -108,11 +105,11 @@ func cleanDirPath(path, base string) string {
|
||||||
return path
|
return path
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s DirectorySource) ID() artifact.ID {
|
func (s directorySource) ID() artifact.ID {
|
||||||
return s.id
|
return s.id
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s DirectorySource) Describe() Description {
|
func (s directorySource) Describe() source.Description {
|
||||||
name := cleanDirPath(s.config.Path, s.config.Base)
|
name := cleanDirPath(s.config.Path, s.config.Base)
|
||||||
version := ""
|
version := ""
|
||||||
if !s.config.Alias.IsEmpty() {
|
if !s.config.Alias.IsEmpty() {
|
||||||
|
@ -124,23 +121,23 @@ func (s DirectorySource) Describe() Description {
|
||||||
version = a.Version
|
version = a.Version
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return Description{
|
return source.Description{
|
||||||
ID: string(s.id),
|
ID: string(s.id),
|
||||||
Name: name,
|
Name: name,
|
||||||
Version: version,
|
Version: version,
|
||||||
Metadata: DirectorySourceMetadata{
|
Metadata: source.DirectoryMetadata{
|
||||||
Path: s.config.Path,
|
Path: s.config.Path,
|
||||||
Base: s.config.Base,
|
Base: s.config.Base,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *DirectorySource) FileResolver(_ Scope) (file.Resolver, error) {
|
func (s *directorySource) FileResolver(_ source.Scope) (file.Resolver, error) {
|
||||||
s.mutex.Lock()
|
s.mutex.Lock()
|
||||||
defer s.mutex.Unlock()
|
defer s.mutex.Unlock()
|
||||||
|
|
||||||
if s.resolver == nil {
|
if s.resolver == nil {
|
||||||
exclusionFunctions, err := getDirectoryExclusionFunctions(s.config.Path, s.config.Exclude.Paths)
|
exclusionFunctions, err := GetDirectoryExclusionFunctions(s.config.Path, s.config.Exclude.Paths)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -156,14 +153,14 @@ func (s *DirectorySource) FileResolver(_ Scope) (file.Resolver, error) {
|
||||||
return s.resolver, nil
|
return s.resolver, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *DirectorySource) Close() error {
|
func (s *directorySource) Close() error {
|
||||||
s.mutex.Lock()
|
s.mutex.Lock()
|
||||||
defer s.mutex.Unlock()
|
defer s.mutex.Unlock()
|
||||||
s.resolver = nil
|
s.resolver = nil
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func getDirectoryExclusionFunctions(root string, exclusions []string) ([]fileresolver.PathIndexVisitor, error) {
|
func GetDirectoryExclusionFunctions(root string, exclusions []string) ([]fileresolver.PathIndexVisitor, error) {
|
||||||
if len(exclusions) == 0 {
|
if len(exclusions) == 0 {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
65
syft/source/directorysource/directory_source_provider.go
Normal file
65
syft/source/directorysource/directory_source_provider.go
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
package directorysource
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/mitchellh/go-homedir"
|
||||||
|
"github.com/spf13/afero"
|
||||||
|
|
||||||
|
"github.com/anchore/syft/syft/source"
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewSourceProvider(path string, exclude source.ExcludeConfig, alias source.Alias, basePath string) source.Provider {
|
||||||
|
return &directorySourceProvider{
|
||||||
|
path: path,
|
||||||
|
basePath: basePath,
|
||||||
|
exclude: exclude,
|
||||||
|
alias: alias,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type directorySourceProvider struct {
|
||||||
|
path string
|
||||||
|
basePath string
|
||||||
|
exclude source.ExcludeConfig
|
||||||
|
alias source.Alias
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l directorySourceProvider) Name() string {
|
||||||
|
return "local-directory"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l directorySourceProvider) Provide(_ context.Context) (source.Source, error) {
|
||||||
|
location, err := homedir.Expand(l.path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("unable to expand potential directory path: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fs := afero.NewOsFs()
|
||||||
|
fileMeta, err := fs.Stat(location)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("unable to stat location: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !fileMeta.IsDir() {
|
||||||
|
return nil, fmt.Errorf("not a directory source: %s", l.path)
|
||||||
|
}
|
||||||
|
|
||||||
|
return New(
|
||||||
|
Config{
|
||||||
|
Path: location,
|
||||||
|
Base: basePath(l.basePath, location),
|
||||||
|
Exclude: l.exclude,
|
||||||
|
Alias: l.alias,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// FIXME why is the base always being set instead of left as empty string?
|
||||||
|
func basePath(base, location string) string {
|
||||||
|
if base == "" {
|
||||||
|
base = location
|
||||||
|
}
|
||||||
|
return base
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
package source
|
package directorysource
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"io/fs"
|
"io/fs"
|
||||||
|
@ -13,9 +13,13 @@ import (
|
||||||
"github.com/anchore/stereoscope/pkg/file"
|
"github.com/anchore/stereoscope/pkg/file"
|
||||||
"github.com/anchore/syft/syft/artifact"
|
"github.com/anchore/syft/syft/artifact"
|
||||||
"github.com/anchore/syft/syft/internal/fileresolver"
|
"github.com/anchore/syft/syft/internal/fileresolver"
|
||||||
|
"github.com/anchore/syft/syft/internal/testutil"
|
||||||
|
"github.com/anchore/syft/syft/source"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestNewFromDirectory(t *testing.T) {
|
func TestNewFromDirectory(t *testing.T) {
|
||||||
|
testutil.Chdir(t, "..") // run with source/test-fixtures
|
||||||
|
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
desc string
|
desc string
|
||||||
input string
|
input string
|
||||||
|
@ -54,7 +58,7 @@ func TestNewFromDirectory(t *testing.T) {
|
||||||
if test.cxErr == nil {
|
if test.cxErr == nil {
|
||||||
test.cxErr = require.NoError
|
test.cxErr = require.NoError
|
||||||
}
|
}
|
||||||
src, err := NewFromDirectory(DirectoryConfig{
|
src, err := New(Config{
|
||||||
Path: test.input,
|
Path: test.input,
|
||||||
})
|
})
|
||||||
test.cxErr(t, err)
|
test.cxErr(t, err)
|
||||||
|
@ -65,9 +69,9 @@ func TestNewFromDirectory(t *testing.T) {
|
||||||
t.Cleanup(func() {
|
t.Cleanup(func() {
|
||||||
require.NoError(t, src.Close())
|
require.NoError(t, src.Close())
|
||||||
})
|
})
|
||||||
assert.Equal(t, test.input, src.Describe().Metadata.(DirectorySourceMetadata).Path)
|
assert.Equal(t, test.input, src.Describe().Metadata.(source.DirectoryMetadata).Path)
|
||||||
|
|
||||||
res, err := src.FileResolver(SquashedScope)
|
res, err := src.FileResolver(source.SquashedScope)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
refs, err := res.FilesByPath(test.inputPaths...)
|
refs, err := res.FilesByPath(test.inputPaths...)
|
||||||
|
@ -82,6 +86,8 @@ func TestNewFromDirectory(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func Test_DirectorySource_FilesByGlob(t *testing.T) {
|
func Test_DirectorySource_FilesByGlob(t *testing.T) {
|
||||||
|
testutil.Chdir(t, "..") // run with source/test-fixtures
|
||||||
|
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
desc string
|
desc string
|
||||||
input string
|
input string
|
||||||
|
@ -109,10 +115,10 @@ func Test_DirectorySource_FilesByGlob(t *testing.T) {
|
||||||
}
|
}
|
||||||
for _, test := range testCases {
|
for _, test := range testCases {
|
||||||
t.Run(test.desc, func(t *testing.T) {
|
t.Run(test.desc, func(t *testing.T) {
|
||||||
src, err := NewFromDirectory(DirectoryConfig{Path: test.input})
|
src, err := New(Config{Path: test.input})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
res, err := src.FileResolver(SquashedScope)
|
res, err := src.FileResolver(source.SquashedScope)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
t.Cleanup(func() {
|
t.Cleanup(func() {
|
||||||
require.NoError(t, src.Close())
|
require.NoError(t, src.Close())
|
||||||
|
@ -129,6 +135,8 @@ func Test_DirectorySource_FilesByGlob(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func Test_DirectorySource_Exclusions(t *testing.T) {
|
func Test_DirectorySource_Exclusions(t *testing.T) {
|
||||||
|
testutil.Chdir(t, "..") // run with source/test-fixtures
|
||||||
|
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
desc string
|
desc string
|
||||||
input string
|
input string
|
||||||
|
@ -270,9 +278,9 @@ func Test_DirectorySource_Exclusions(t *testing.T) {
|
||||||
|
|
||||||
for _, test := range testCases {
|
for _, test := range testCases {
|
||||||
t.Run(test.desc, func(t *testing.T) {
|
t.Run(test.desc, func(t *testing.T) {
|
||||||
src, err := NewFromDirectory(DirectoryConfig{
|
src, err := New(Config{
|
||||||
Path: test.input,
|
Path: test.input,
|
||||||
Exclude: ExcludeConfig{
|
Exclude: source.ExcludeConfig{
|
||||||
Paths: test.exclusions,
|
Paths: test.exclusions,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
@ -282,13 +290,13 @@ func Test_DirectorySource_Exclusions(t *testing.T) {
|
||||||
})
|
})
|
||||||
|
|
||||||
if test.err {
|
if test.err {
|
||||||
_, err = src.FileResolver(SquashedScope)
|
_, err = src.FileResolver(source.SquashedScope)
|
||||||
require.Error(t, err)
|
require.Error(t, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
res, err := src.FileResolver(SquashedScope)
|
res, err := src.FileResolver(source.SquashedScope)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
locations, err := res.FilesByGlob(test.glob)
|
locations, err := res.FilesByGlob(test.glob)
|
||||||
|
@ -388,7 +396,7 @@ func Test_getDirectoryExclusionFunctions_crossPlatform(t *testing.T) {
|
||||||
|
|
||||||
for _, test := range testCases {
|
for _, test := range testCases {
|
||||||
t.Run(test.desc, func(t *testing.T) {
|
t.Run(test.desc, func(t *testing.T) {
|
||||||
fns, err := getDirectoryExclusionFunctions(test.root, []string{test.exclude})
|
fns, err := GetDirectoryExclusionFunctions(test.root, []string{test.exclude})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
for _, f := range fns {
|
for _, f := range fns {
|
||||||
|
@ -400,6 +408,8 @@ func Test_getDirectoryExclusionFunctions_crossPlatform(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func Test_DirectorySource_FilesByPathDoesNotExist(t *testing.T) {
|
func Test_DirectorySource_FilesByPathDoesNotExist(t *testing.T) {
|
||||||
|
testutil.Chdir(t, "..") // run with source/test-fixtures
|
||||||
|
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
desc string
|
desc string
|
||||||
input string
|
input string
|
||||||
|
@ -414,13 +424,13 @@ func Test_DirectorySource_FilesByPathDoesNotExist(t *testing.T) {
|
||||||
}
|
}
|
||||||
for _, test := range testCases {
|
for _, test := range testCases {
|
||||||
t.Run(test.desc, func(t *testing.T) {
|
t.Run(test.desc, func(t *testing.T) {
|
||||||
src, err := NewFromDirectory(DirectoryConfig{Path: test.input})
|
src, err := New(Config{Path: test.input})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
t.Cleanup(func() {
|
t.Cleanup(func() {
|
||||||
require.NoError(t, src.Close())
|
require.NoError(t, src.Close())
|
||||||
})
|
})
|
||||||
|
|
||||||
res, err := src.FileResolver(SquashedScope)
|
res, err := src.FileResolver(source.SquashedScope)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
refs, err := res.FilesByPath(test.path)
|
refs, err := res.FilesByPath(test.path)
|
||||||
|
@ -432,41 +442,43 @@ func Test_DirectorySource_FilesByPathDoesNotExist(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func Test_DirectorySource_ID(t *testing.T) {
|
func Test_DirectorySource_ID(t *testing.T) {
|
||||||
|
testutil.Chdir(t, "..") // run with source/test-fixtures
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
cfg DirectoryConfig
|
cfg Config
|
||||||
want artifact.ID
|
want artifact.ID
|
||||||
wantErr require.ErrorAssertionFunc
|
wantErr require.ErrorAssertionFunc
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "empty",
|
name: "empty",
|
||||||
cfg: DirectoryConfig{},
|
cfg: Config{},
|
||||||
wantErr: require.Error,
|
wantErr: require.Error,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "to a non-existent directory",
|
name: "to a non-existent directory",
|
||||||
cfg: DirectoryConfig{
|
cfg: Config{
|
||||||
Path: "./test-fixtures/does-not-exist",
|
Path: "./test-fixtures/does-not-exist",
|
||||||
},
|
},
|
||||||
wantErr: require.Error,
|
wantErr: require.Error,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "with odd unclean path through non-existent directory",
|
name: "with odd unclean path through non-existent directory",
|
||||||
cfg: DirectoryConfig{Path: "test-fixtures/does-not-exist/../"},
|
cfg: Config{Path: "test-fixtures/does-not-exist/../"},
|
||||||
wantErr: require.Error,
|
wantErr: require.Error,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "to a file (not a directory)",
|
name: "to a file (not a directory)",
|
||||||
cfg: DirectoryConfig{
|
cfg: Config{
|
||||||
Path: "./test-fixtures/image-simple/Dockerfile",
|
Path: "./test-fixtures/image-simple/Dockerfile",
|
||||||
},
|
},
|
||||||
wantErr: require.Error,
|
wantErr: require.Error,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "to dir with name and version",
|
name: "to dir with name and version",
|
||||||
cfg: DirectoryConfig{
|
cfg: Config{
|
||||||
Path: "./test-fixtures",
|
Path: "./test-fixtures",
|
||||||
Alias: Alias{
|
Alias: source.Alias{
|
||||||
Name: "name-me-that!",
|
Name: "name-me-that!",
|
||||||
Version: "version-me-this!",
|
Version: "version-me-this!",
|
||||||
},
|
},
|
||||||
|
@ -475,9 +487,9 @@ func Test_DirectorySource_ID(t *testing.T) {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "to different dir with name and version",
|
name: "to different dir with name and version",
|
||||||
cfg: DirectoryConfig{
|
cfg: Config{
|
||||||
Path: "./test-fixtures/image-simple",
|
Path: "./test-fixtures/image-simple",
|
||||||
Alias: Alias{
|
Alias: source.Alias{
|
||||||
Name: "name-me-that!",
|
Name: "name-me-that!",
|
||||||
Version: "version-me-this!",
|
Version: "version-me-this!",
|
||||||
},
|
},
|
||||||
|
@ -487,20 +499,20 @@ func Test_DirectorySource_ID(t *testing.T) {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "with path",
|
name: "with path",
|
||||||
cfg: DirectoryConfig{Path: "./test-fixtures"},
|
cfg: Config{Path: "./test-fixtures"},
|
||||||
want: artifact.ID("c2f936b0054dc6114fc02a3446bf8916bde8fdf87166a23aee22ea011b443522"),
|
want: artifact.ID("c2f936b0054dc6114fc02a3446bf8916bde8fdf87166a23aee22ea011b443522"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "with unclean path",
|
name: "with unclean path",
|
||||||
cfg: DirectoryConfig{Path: "test-fixtures/image-simple/../"},
|
cfg: Config{Path: "test-fixtures/image-simple/../"},
|
||||||
want: artifact.ID("c2f936b0054dc6114fc02a3446bf8916bde8fdf87166a23aee22ea011b443522"),
|
want: artifact.ID("c2f936b0054dc6114fc02a3446bf8916bde8fdf87166a23aee22ea011b443522"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "other fields do not affect ID",
|
name: "other fields do not affect ID",
|
||||||
cfg: DirectoryConfig{
|
cfg: Config{
|
||||||
Path: "test-fixtures",
|
Path: "test-fixtures",
|
||||||
Base: "a-base!",
|
Base: "a-base!",
|
||||||
Exclude: ExcludeConfig{
|
Exclude: source.ExcludeConfig{
|
||||||
Paths: []string{"a", "b"},
|
Paths: []string{"a", "b"},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -512,7 +524,7 @@ func Test_DirectorySource_ID(t *testing.T) {
|
||||||
if tt.wantErr == nil {
|
if tt.wantErr == nil {
|
||||||
tt.wantErr = require.NoError
|
tt.wantErr = require.NoError
|
||||||
}
|
}
|
||||||
s, err := NewFromDirectory(tt.cfg)
|
s, err := New(tt.cfg)
|
||||||
tt.wantErr(t, err)
|
tt.wantErr(t, err)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
|
@ -523,6 +535,7 @@ func Test_DirectorySource_ID(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func Test_cleanDirPath(t *testing.T) {
|
func Test_cleanDirPath(t *testing.T) {
|
||||||
|
testutil.Chdir(t, "..") // run with source/test-fixtures
|
||||||
|
|
||||||
abs, err := filepath.Abs("test-fixtures")
|
abs, err := filepath.Abs("test-fixtures")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
|
@ -12,7 +12,7 @@
|
||||||
// - https://github.com/golang/go/blob/3aea422e2cb8b1ec2e0c2774be97fe96c7299838/src/path/filepath/path_windows.go#L216
|
// - https://github.com/golang/go/blob/3aea422e2cb8b1ec2e0c2774be97fe96c7299838/src/path/filepath/path_windows.go#L216
|
||||||
// ... which means we can't extract this functionality without build tags.
|
// ... which means we can't extract this functionality without build tags.
|
||||||
|
|
||||||
package source
|
package directorysource
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
@ -53,7 +53,7 @@ func Test_DirectorySource_crossPlatformExclusions(t *testing.T) {
|
||||||
|
|
||||||
for _, test := range testCases {
|
for _, test := range testCases {
|
||||||
t.Run(test.desc, func(t *testing.T) {
|
t.Run(test.desc, func(t *testing.T) {
|
||||||
fns, err := getDirectoryExclusionFunctions(test.root, []string{test.exclude})
|
fns, err := GetDirectoryExclusionFunctions(test.root, []string{test.exclude})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
for _, f := range fns {
|
for _, f := range fns {
|
9
syft/source/file_metadata.go
Normal file
9
syft/source/file_metadata.go
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
package source
|
||||||
|
|
||||||
|
import "github.com/anchore/syft/syft/file"
|
||||||
|
|
||||||
|
type FileMetadata struct {
|
||||||
|
Path string `json:"path" yaml:"path"`
|
||||||
|
Digests []file.Digest `json:"digests,omitempty" yaml:"digests,omitempty"`
|
||||||
|
MIMEType string `json:"mimeType" yaml:"mimeType"`
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
package source
|
package filesource
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto"
|
"crypto"
|
||||||
|
@ -18,27 +18,24 @@ import (
|
||||||
"github.com/anchore/syft/syft/artifact"
|
"github.com/anchore/syft/syft/artifact"
|
||||||
"github.com/anchore/syft/syft/file"
|
"github.com/anchore/syft/syft/file"
|
||||||
"github.com/anchore/syft/syft/internal/fileresolver"
|
"github.com/anchore/syft/syft/internal/fileresolver"
|
||||||
|
"github.com/anchore/syft/syft/source"
|
||||||
|
"github.com/anchore/syft/syft/source/directorysource"
|
||||||
|
"github.com/anchore/syft/syft/source/internal"
|
||||||
)
|
)
|
||||||
|
|
||||||
var _ Source = (*FileSource)(nil)
|
var _ source.Source = (*fileSource)(nil)
|
||||||
|
|
||||||
type FileConfig struct {
|
type Config struct {
|
||||||
Path string
|
Path string
|
||||||
Exclude ExcludeConfig
|
Exclude source.ExcludeConfig
|
||||||
DigestAlgorithms []crypto.Hash
|
DigestAlgorithms []crypto.Hash
|
||||||
Alias Alias
|
Alias source.Alias
|
||||||
}
|
}
|
||||||
|
|
||||||
type FileSourceMetadata struct {
|
type fileSource struct {
|
||||||
Path string `json:"path" yaml:"path"`
|
|
||||||
Digests []file.Digest `json:"digests,omitempty" yaml:"digests,omitempty"`
|
|
||||||
MIMEType string `json:"mimeType" yaml:"mimeType"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type FileSource struct {
|
|
||||||
id artifact.ID
|
id artifact.ID
|
||||||
digestForVersion string
|
digestForVersion string
|
||||||
config FileConfig
|
config Config
|
||||||
resolver *fileresolver.Directory
|
resolver *fileresolver.Directory
|
||||||
mutex *sync.Mutex
|
mutex *sync.Mutex
|
||||||
closer func() error
|
closer func() error
|
||||||
|
@ -47,7 +44,11 @@ type FileSource struct {
|
||||||
analysisPath string
|
analysisPath string
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewFromFile(cfg FileConfig) (*FileSource, error) {
|
func NewFromPath(path string) (source.Source, error) {
|
||||||
|
return New(Config{Path: path})
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(cfg Config) (source.Source, error) {
|
||||||
fileMeta, err := os.Stat(cfg.Path)
|
fileMeta, err := os.Stat(cfg.Path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("unable to stat path=%q: %w", cfg.Path, err)
|
return nil, fmt.Errorf("unable to stat path=%q: %w", cfg.Path, err)
|
||||||
|
@ -83,7 +84,7 @@ func NewFromFile(cfg FileConfig) (*FileSource, error) {
|
||||||
|
|
||||||
id, versionDigest := deriveIDFromFile(cfg)
|
id, versionDigest := deriveIDFromFile(cfg)
|
||||||
|
|
||||||
return &FileSource{
|
return &fileSource{
|
||||||
id: id,
|
id: id,
|
||||||
config: cfg,
|
config: cfg,
|
||||||
mutex: &sync.Mutex{},
|
mutex: &sync.Mutex{},
|
||||||
|
@ -98,7 +99,7 @@ func NewFromFile(cfg FileConfig) (*FileSource, error) {
|
||||||
// deriveIDFromFile derives an artifact ID from the contents of a file. If an alias is provided, it will be included
|
// deriveIDFromFile derives an artifact ID from the contents of a file. If an alias is provided, it will be included
|
||||||
// in the ID derivation (along with contents). This way if the user scans the same item but is considered to be
|
// in the ID derivation (along with contents). This way if the user scans the same item but is considered to be
|
||||||
// logically different, then ID will express that.
|
// logically different, then ID will express that.
|
||||||
func deriveIDFromFile(cfg FileConfig) (artifact.ID, string) {
|
func deriveIDFromFile(cfg Config) (artifact.ID, string) {
|
||||||
d := digestOfFileContents(cfg.Path)
|
d := digestOfFileContents(cfg.Path)
|
||||||
info := d
|
info := d
|
||||||
|
|
||||||
|
@ -108,14 +109,14 @@ func deriveIDFromFile(cfg FileConfig) (artifact.ID, string) {
|
||||||
info += fmt.Sprintf(":%s@%s", cfg.Alias.Name, cfg.Alias.Version)
|
info += fmt.Sprintf(":%s@%s", cfg.Alias.Name, cfg.Alias.Version)
|
||||||
}
|
}
|
||||||
|
|
||||||
return artifactIDFromDigest(digest.SHA256.FromString(info).String()), d
|
return internal.ArtifactIDFromDigest(digest.SHA256.FromString(info).String()), d
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s FileSource) ID() artifact.ID {
|
func (s fileSource) ID() artifact.ID {
|
||||||
return s.id
|
return s.id
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s FileSource) Describe() Description {
|
func (s fileSource) Describe() source.Description {
|
||||||
name := path.Base(s.config.Path)
|
name := path.Base(s.config.Path)
|
||||||
version := s.digestForVersion
|
version := s.digestForVersion
|
||||||
if !s.config.Alias.IsEmpty() {
|
if !s.config.Alias.IsEmpty() {
|
||||||
|
@ -128,11 +129,11 @@ func (s FileSource) Describe() Description {
|
||||||
version = a.Version
|
version = a.Version
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return Description{
|
return source.Description{
|
||||||
ID: string(s.id),
|
ID: string(s.id),
|
||||||
Name: name,
|
Name: name,
|
||||||
Version: version,
|
Version: version,
|
||||||
Metadata: FileSourceMetadata{
|
Metadata: source.FileMetadata{
|
||||||
Path: s.config.Path,
|
Path: s.config.Path,
|
||||||
Digests: s.digests,
|
Digests: s.digests,
|
||||||
MIMEType: s.mimeType,
|
MIMEType: s.mimeType,
|
||||||
|
@ -140,7 +141,7 @@ func (s FileSource) Describe() Description {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s FileSource) FileResolver(_ Scope) (file.Resolver, error) {
|
func (s fileSource) FileResolver(_ source.Scope) (file.Resolver, error) {
|
||||||
s.mutex.Lock()
|
s.mutex.Lock()
|
||||||
defer s.mutex.Unlock()
|
defer s.mutex.Unlock()
|
||||||
|
|
||||||
|
@ -148,7 +149,7 @@ func (s FileSource) FileResolver(_ Scope) (file.Resolver, error) {
|
||||||
return s.resolver, nil
|
return s.resolver, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
exclusionFunctions, err := getDirectoryExclusionFunctions(s.analysisPath, s.config.Exclude.Paths)
|
exclusionFunctions, err := directorysource.GetDirectoryExclusionFunctions(s.analysisPath, s.config.Exclude.Paths)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -221,7 +222,7 @@ func absoluteSymlinkFreePathToParent(path string) (string, error) {
|
||||||
return filepath.Dir(dereferencedAbsAnalysisPath), nil
|
return filepath.Dir(dereferencedAbsAnalysisPath), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *FileSource) Close() error {
|
func (s *fileSource) Close() error {
|
||||||
if s.closer == nil {
|
if s.closer == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
58
syft/source/filesource/file_source_provider.go
Normal file
58
syft/source/filesource/file_source_provider.go
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
package filesource
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/mitchellh/go-homedir"
|
||||||
|
"github.com/spf13/afero"
|
||||||
|
|
||||||
|
"github.com/anchore/syft/syft/source"
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewSourceProvider(path string, exclude source.ExcludeConfig, digestAlgorithms []crypto.Hash, alias source.Alias) source.Provider {
|
||||||
|
return &fileSourceProvider{
|
||||||
|
path: path,
|
||||||
|
exclude: exclude,
|
||||||
|
digestAlgorithms: digestAlgorithms,
|
||||||
|
alias: alias,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type fileSourceProvider struct {
|
||||||
|
path string
|
||||||
|
exclude source.ExcludeConfig
|
||||||
|
digestAlgorithms []crypto.Hash
|
||||||
|
alias source.Alias
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p fileSourceProvider) Name() string {
|
||||||
|
return "local-file"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p fileSourceProvider) Provide(_ context.Context) (source.Source, error) {
|
||||||
|
location, err := homedir.Expand(p.path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("unable to expand potential directory path: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fs := afero.NewOsFs()
|
||||||
|
fileMeta, err := fs.Stat(location)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("unable to stat location: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if fileMeta.IsDir() {
|
||||||
|
return nil, fmt.Errorf("not a file source: %s", p.path)
|
||||||
|
}
|
||||||
|
|
||||||
|
return New(
|
||||||
|
Config{
|
||||||
|
Path: location,
|
||||||
|
Exclude: p.exclude,
|
||||||
|
DigestAlgorithms: p.digestAlgorithms,
|
||||||
|
Alias: p.alias,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
package source
|
package filesource
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"io"
|
"io"
|
||||||
|
@ -14,9 +14,13 @@ import (
|
||||||
|
|
||||||
"github.com/anchore/syft/syft/artifact"
|
"github.com/anchore/syft/syft/artifact"
|
||||||
"github.com/anchore/syft/syft/file"
|
"github.com/anchore/syft/syft/file"
|
||||||
|
"github.com/anchore/syft/syft/internal/testutil"
|
||||||
|
"github.com/anchore/syft/syft/source"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestNewFromFile(t *testing.T) {
|
func TestNewFromFile(t *testing.T) {
|
||||||
|
testutil.Chdir(t, "..") // run with source/test-fixtures
|
||||||
|
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
desc string
|
desc string
|
||||||
input string
|
input string
|
||||||
|
@ -67,7 +71,7 @@ func TestNewFromFile(t *testing.T) {
|
||||||
}
|
}
|
||||||
for _, test := range testCases {
|
for _, test := range testCases {
|
||||||
t.Run(test.desc, func(t *testing.T) {
|
t.Run(test.desc, func(t *testing.T) {
|
||||||
src, err := NewFromFile(FileConfig{
|
src, err := New(Config{
|
||||||
Path: test.input,
|
Path: test.input,
|
||||||
})
|
})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
@ -75,9 +79,9 @@ func TestNewFromFile(t *testing.T) {
|
||||||
require.NoError(t, src.Close())
|
require.NoError(t, src.Close())
|
||||||
})
|
})
|
||||||
|
|
||||||
assert.Equal(t, test.input, src.Describe().Metadata.(FileSourceMetadata).Path)
|
assert.Equal(t, test.input, src.Describe().Metadata.(source.FileMetadata).Path)
|
||||||
|
|
||||||
res, err := src.FileResolver(SquashedScope)
|
res, err := src.FileResolver(source.SquashedScope)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
refs, err := test.testPathFn(res)
|
refs, err := test.testPathFn(res)
|
||||||
|
@ -92,6 +96,8 @@ func TestNewFromFile(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestNewFromFile_WithArchive(t *testing.T) {
|
func TestNewFromFile_WithArchive(t *testing.T) {
|
||||||
|
testutil.Chdir(t, "..") // run with source/test-fixtures
|
||||||
|
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
desc string
|
desc string
|
||||||
input string
|
input string
|
||||||
|
@ -120,7 +126,7 @@ func TestNewFromFile_WithArchive(t *testing.T) {
|
||||||
t.Run(test.desc, func(t *testing.T) {
|
t.Run(test.desc, func(t *testing.T) {
|
||||||
archivePath := setupArchiveTest(t, test.input, test.layer2)
|
archivePath := setupArchiveTest(t, test.input, test.layer2)
|
||||||
|
|
||||||
src, err := NewFromFile(FileConfig{
|
src, err := New(Config{
|
||||||
Path: archivePath,
|
Path: archivePath,
|
||||||
})
|
})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
@ -128,9 +134,9 @@ func TestNewFromFile_WithArchive(t *testing.T) {
|
||||||
require.NoError(t, src.Close())
|
require.NoError(t, src.Close())
|
||||||
})
|
})
|
||||||
|
|
||||||
assert.Equal(t, archivePath, src.Describe().Metadata.(FileSourceMetadata).Path)
|
assert.Equal(t, archivePath, src.Describe().Metadata.(source.FileMetadata).Path)
|
||||||
|
|
||||||
res, err := src.FileResolver(SquashedScope)
|
res, err := src.FileResolver(source.SquashedScope)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
refs, err := res.FilesByPath(test.inputPaths...)
|
refs, err := res.FilesByPath(test.inputPaths...)
|
||||||
|
@ -226,43 +232,45 @@ func createArchive(t testing.TB, sourceDirPath, destinationArchivePath string, l
|
||||||
}
|
}
|
||||||
|
|
||||||
func Test_FileSource_ID(t *testing.T) {
|
func Test_FileSource_ID(t *testing.T) {
|
||||||
|
testutil.Chdir(t, "..") // run with source/test-fixtures
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
cfg FileConfig
|
cfg Config
|
||||||
want artifact.ID
|
want artifact.ID
|
||||||
wantDigest string
|
wantDigest string
|
||||||
wantErr require.ErrorAssertionFunc
|
wantErr require.ErrorAssertionFunc
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "empty",
|
name: "empty",
|
||||||
cfg: FileConfig{},
|
cfg: Config{},
|
||||||
wantErr: require.Error,
|
wantErr: require.Error,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "does not exist",
|
name: "does not exist",
|
||||||
cfg: FileConfig{
|
cfg: Config{
|
||||||
Path: "./test-fixtures/does-not-exist",
|
Path: "./test-fixtures/does-not-exist",
|
||||||
},
|
},
|
||||||
wantErr: require.Error,
|
wantErr: require.Error,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "to dir",
|
name: "to dir",
|
||||||
cfg: FileConfig{
|
cfg: Config{
|
||||||
Path: "./test-fixtures/image-simple",
|
Path: "./test-fixtures/image-simple",
|
||||||
},
|
},
|
||||||
wantErr: require.Error,
|
wantErr: require.Error,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "with path",
|
name: "with path",
|
||||||
cfg: FileConfig{Path: "./test-fixtures/image-simple/Dockerfile"},
|
cfg: Config{Path: "./test-fixtures/image-simple/Dockerfile"},
|
||||||
want: artifact.ID("db7146472cf6d49b3ac01b42812fb60020b0b4898b97491b21bb690c808d5159"),
|
want: artifact.ID("db7146472cf6d49b3ac01b42812fb60020b0b4898b97491b21bb690c808d5159"),
|
||||||
wantDigest: "sha256:38601c0bb4269a10ce1d00590ea7689c1117dd9274c758653934ab4f2016f80f",
|
wantDigest: "sha256:38601c0bb4269a10ce1d00590ea7689c1117dd9274c758653934ab4f2016f80f",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "with path and alias",
|
name: "with path and alias",
|
||||||
cfg: FileConfig{
|
cfg: Config{
|
||||||
Path: "./test-fixtures/image-simple/Dockerfile",
|
Path: "./test-fixtures/image-simple/Dockerfile",
|
||||||
Alias: Alias{
|
Alias: source.Alias{
|
||||||
Name: "name-me-that!",
|
Name: "name-me-that!",
|
||||||
Version: "version-me-this!",
|
Version: "version-me-this!",
|
||||||
},
|
},
|
||||||
|
@ -272,9 +280,9 @@ func Test_FileSource_ID(t *testing.T) {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "other fields do not affect ID",
|
name: "other fields do not affect ID",
|
||||||
cfg: FileConfig{
|
cfg: Config{
|
||||||
Path: "test-fixtures/image-simple/Dockerfile",
|
Path: "test-fixtures/image-simple/Dockerfile",
|
||||||
Exclude: ExcludeConfig{
|
Exclude: source.ExcludeConfig{
|
||||||
Paths: []string{"a", "b"},
|
Paths: []string{"a", "b"},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -287,11 +295,12 @@ func Test_FileSource_ID(t *testing.T) {
|
||||||
if tt.wantErr == nil {
|
if tt.wantErr == nil {
|
||||||
tt.wantErr = require.NoError
|
tt.wantErr = require.NoError
|
||||||
}
|
}
|
||||||
s, err := NewFromFile(tt.cfg)
|
newSource, err := New(tt.cfg)
|
||||||
tt.wantErr(t, err)
|
tt.wantErr(t, err)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
s := newSource.(*fileSource)
|
||||||
assert.Equalf(t, tt.want, s.ID(), "ID() mismatch")
|
assert.Equalf(t, tt.want, s.ID(), "ID() mismatch")
|
||||||
assert.Equalf(t, tt.wantDigest, s.digestForVersion, "digestForVersion mismatch")
|
assert.Equalf(t, tt.wantDigest, s.digestForVersion, "digestForVersion mismatch")
|
||||||
})
|
})
|
27
syft/source/image_metadata.go
Normal file
27
syft/source/image_metadata.go
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
package source
|
||||||
|
|
||||||
|
// ImageMetadata represents all static metadata that defines what a container image is. This is useful to later describe
|
||||||
|
// "what" was cataloged without needing the more complicated stereoscope Image objects or FileResolver objects.
|
||||||
|
type ImageMetadata struct {
|
||||||
|
UserInput string `json:"userInput"`
|
||||||
|
ID string `json:"imageID"`
|
||||||
|
ManifestDigest string `json:"manifestDigest"`
|
||||||
|
MediaType string `json:"mediaType"`
|
||||||
|
Tags []string `json:"tags"`
|
||||||
|
Size int64 `json:"imageSize"`
|
||||||
|
Layers []LayerMetadata `json:"layers"`
|
||||||
|
RawManifest []byte `json:"manifest"`
|
||||||
|
RawConfig []byte `json:"config"`
|
||||||
|
RepoDigests []string `json:"repoDigests"`
|
||||||
|
Architecture string `json:"architecture"`
|
||||||
|
Variant string `json:"architectureVariant,omitempty"`
|
||||||
|
OS string `json:"os"`
|
||||||
|
Labels map[string]string `json:"labels,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// LayerMetadata represents all static metadata that defines what a container image layer is.
|
||||||
|
type LayerMetadata struct {
|
||||||
|
MediaType string `json:"mediaType"`
|
||||||
|
Digest string `json:"digest"`
|
||||||
|
Size int64 `json:"size"`
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
package source
|
package internal
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"strings"
|
"strings"
|
||||||
|
@ -6,6 +6,6 @@ import (
|
||||||
"github.com/anchore/syft/syft/artifact"
|
"github.com/anchore/syft/syft/artifact"
|
||||||
)
|
)
|
||||||
|
|
||||||
func artifactIDFromDigest(input string) artifact.ID {
|
func ArtifactIDFromDigest(input string) artifact.ID {
|
||||||
return artifact.ID(strings.TrimPrefix(input, "sha256:"))
|
return artifact.ID(strings.TrimPrefix(input, "sha256:"))
|
||||||
}
|
}
|
11
syft/source/provider.go
Normal file
11
syft/source/provider.go
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
package source
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Provider is able to resolve a source request
|
||||||
|
type Provider interface {
|
||||||
|
Name() string
|
||||||
|
Provide(ctx context.Context) (Source, error)
|
||||||
|
}
|
56
syft/source/sourceproviders/source_provider_config.go
Normal file
56
syft/source/sourceproviders/source_provider_config.go
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
package sourceproviders
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto"
|
||||||
|
|
||||||
|
"github.com/anchore/stereoscope/pkg/image"
|
||||||
|
"github.com/anchore/syft/syft/source"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Config is the uber-configuration for all Syft source providers
|
||||||
|
type Config struct {
|
||||||
|
Platform *image.Platform
|
||||||
|
Alias source.Alias
|
||||||
|
RegistryOptions *image.RegistryOptions
|
||||||
|
Exclude source.ExcludeConfig
|
||||||
|
DigestAlgorithms []crypto.Hash
|
||||||
|
BasePath string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Config) WithAlias(alias source.Alias) *Config {
|
||||||
|
c.Alias = alias
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Config) WithRegistryOptions(registryOptions *image.RegistryOptions) *Config {
|
||||||
|
c.RegistryOptions = registryOptions
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Config) WithPlatform(platform *image.Platform) *Config {
|
||||||
|
c.Platform = platform
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Config) WithExcludeConfig(excludeConfig source.ExcludeConfig) *Config {
|
||||||
|
c.Exclude = excludeConfig
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Config) WithDigestAlgorithms(algorithms ...crypto.Hash) *Config {
|
||||||
|
c.DigestAlgorithms = algorithms
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Config) WithBasePath(basePath string) *Config {
|
||||||
|
c.BasePath = basePath
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
func DefaultConfig() *Config {
|
||||||
|
return &Config{
|
||||||
|
DigestAlgorithms: []crypto.Hash{
|
||||||
|
crypto.SHA256,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
55
syft/source/sourceproviders/source_providers.go
Normal file
55
syft/source/sourceproviders/source_providers.go
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
package sourceproviders
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/anchore/go-collections"
|
||||||
|
"github.com/anchore/stereoscope"
|
||||||
|
"github.com/anchore/stereoscope/pkg/image"
|
||||||
|
"github.com/anchore/syft/syft/source"
|
||||||
|
"github.com/anchore/syft/syft/source/directorysource"
|
||||||
|
"github.com/anchore/syft/syft/source/filesource"
|
||||||
|
"github.com/anchore/syft/syft/source/stereoscopesource"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
FileTag = stereoscope.FileTag
|
||||||
|
DirTag = stereoscope.DirTag
|
||||||
|
PullTag = stereoscope.PullTag
|
||||||
|
)
|
||||||
|
|
||||||
|
// All returns all the configured source providers known to syft
|
||||||
|
func All(userInput string, cfg *Config) []collections.TaggedValue[source.Provider] {
|
||||||
|
if cfg == nil {
|
||||||
|
cfg = DefaultConfig()
|
||||||
|
}
|
||||||
|
stereoscopeProviders := stereoscopeSourceProviders(userInput, cfg)
|
||||||
|
|
||||||
|
return collections.TaggedValueSet[source.Provider]{}.
|
||||||
|
// --from file, dir, oci-archive, etc.
|
||||||
|
Join(stereoscopeProviders.Select(FileTag, DirTag)...).
|
||||||
|
Join(tagProvider(filesource.NewSourceProvider(userInput, cfg.Exclude, cfg.DigestAlgorithms, cfg.Alias), FileTag)).
|
||||||
|
Join(tagProvider(directorysource.NewSourceProvider(userInput, cfg.Exclude, cfg.Alias, cfg.BasePath), DirTag)).
|
||||||
|
|
||||||
|
// --from docker, registry, etc.
|
||||||
|
Join(stereoscopeProviders.Select(PullTag)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func stereoscopeSourceProviders(userInput string, cfg *Config) collections.TaggedValueSet[source.Provider] {
|
||||||
|
var registry image.RegistryOptions
|
||||||
|
if cfg.RegistryOptions != nil {
|
||||||
|
registry = *cfg.RegistryOptions
|
||||||
|
}
|
||||||
|
stereoscopeProviders := stereoscopesource.Providers(stereoscopesource.ProviderConfig{
|
||||||
|
StereoscopeImageProviderConfig: stereoscope.ImageProviderConfig{
|
||||||
|
UserInput: userInput,
|
||||||
|
Platform: cfg.Platform,
|
||||||
|
Registry: registry,
|
||||||
|
},
|
||||||
|
Alias: cfg.Alias,
|
||||||
|
Exclude: cfg.Exclude,
|
||||||
|
})
|
||||||
|
return stereoscopeProviders
|
||||||
|
}
|
||||||
|
|
||||||
|
func tagProvider(provider source.Provider, tags ...string) collections.TaggedValue[source.Provider] {
|
||||||
|
return collections.NewTaggedValue(provider, append([]string{provider.Name()}, tags...)...)
|
||||||
|
}
|
|
@ -1,64 +0,0 @@
|
||||||
package source
|
|
||||||
|
|
||||||
import "github.com/anchore/stereoscope/pkg/image"
|
|
||||||
|
|
||||||
// StereoscopeImageSourceMetadata represents all static metadata that defines what a container image is. This is useful to later describe
|
|
||||||
// "what" was cataloged without needing the more complicated stereoscope Image objects or FileResolver objects.
|
|
||||||
type StereoscopeImageSourceMetadata struct {
|
|
||||||
UserInput string `json:"userInput"`
|
|
||||||
ID string `json:"imageID"`
|
|
||||||
ManifestDigest string `json:"manifestDigest"`
|
|
||||||
MediaType string `json:"mediaType"`
|
|
||||||
Tags []string `json:"tags"`
|
|
||||||
Size int64 `json:"imageSize"`
|
|
||||||
Layers []StereoscopeLayerMetadata `json:"layers"`
|
|
||||||
RawManifest []byte `json:"manifest"`
|
|
||||||
RawConfig []byte `json:"config"`
|
|
||||||
RepoDigests []string `json:"repoDigests"`
|
|
||||||
Architecture string `json:"architecture"`
|
|
||||||
Variant string `json:"architectureVariant,omitempty"`
|
|
||||||
OS string `json:"os"`
|
|
||||||
Labels map[string]string `json:"labels,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// StereoscopeLayerMetadata represents all static metadata that defines what a container image layer is.
|
|
||||||
type StereoscopeLayerMetadata struct {
|
|
||||||
MediaType string `json:"mediaType"`
|
|
||||||
Digest string `json:"digest"`
|
|
||||||
Size int64 `json:"size"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewStereoscopeImageMetadata creates a new ImageMetadata object populated from the given stereoscope Image object and user configuration.
|
|
||||||
func NewStereoscopeImageMetadata(img *image.Image, userInput string) StereoscopeImageSourceMetadata {
|
|
||||||
// populate artifacts...
|
|
||||||
tags := make([]string, len(img.Metadata.Tags))
|
|
||||||
for idx, tag := range img.Metadata.Tags {
|
|
||||||
tags[idx] = tag.String()
|
|
||||||
}
|
|
||||||
theImg := StereoscopeImageSourceMetadata{
|
|
||||||
ID: img.Metadata.ID,
|
|
||||||
UserInput: userInput,
|
|
||||||
ManifestDigest: img.Metadata.ManifestDigest,
|
|
||||||
Size: img.Metadata.Size,
|
|
||||||
MediaType: string(img.Metadata.MediaType),
|
|
||||||
Tags: tags,
|
|
||||||
Layers: make([]StereoscopeLayerMetadata, len(img.Layers)),
|
|
||||||
RawConfig: img.Metadata.RawConfig,
|
|
||||||
RawManifest: img.Metadata.RawManifest,
|
|
||||||
RepoDigests: img.Metadata.RepoDigests,
|
|
||||||
Architecture: img.Metadata.Architecture,
|
|
||||||
Variant: img.Metadata.Variant,
|
|
||||||
OS: img.Metadata.OS,
|
|
||||||
Labels: img.Metadata.Config.Config.Labels,
|
|
||||||
}
|
|
||||||
|
|
||||||
// populate image metadata
|
|
||||||
for idx, l := range img.Layers {
|
|
||||||
theImg.Layers[idx] = StereoscopeLayerMetadata{
|
|
||||||
MediaType: string(l.Metadata.MediaType),
|
|
||||||
Digest: l.Metadata.Digest,
|
|
||||||
Size: l.Metadata.Size,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return theImg
|
|
||||||
}
|
|
|
@ -1,90 +1,53 @@
|
||||||
package source
|
package stereoscopesource
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/bmatcuk/doublestar/v4"
|
"github.com/bmatcuk/doublestar/v4"
|
||||||
"github.com/distribution/reference"
|
"github.com/distribution/reference"
|
||||||
"github.com/opencontainers/go-digest"
|
"github.com/opencontainers/go-digest"
|
||||||
|
|
||||||
"github.com/anchore/stereoscope"
|
|
||||||
"github.com/anchore/stereoscope/pkg/image"
|
"github.com/anchore/stereoscope/pkg/image"
|
||||||
"github.com/anchore/syft/internal/log"
|
"github.com/anchore/syft/internal/log"
|
||||||
"github.com/anchore/syft/syft/artifact"
|
"github.com/anchore/syft/syft/artifact"
|
||||||
"github.com/anchore/syft/syft/file"
|
"github.com/anchore/syft/syft/file"
|
||||||
"github.com/anchore/syft/syft/internal/fileresolver"
|
"github.com/anchore/syft/syft/internal/fileresolver"
|
||||||
|
"github.com/anchore/syft/syft/source"
|
||||||
|
"github.com/anchore/syft/syft/source/internal"
|
||||||
)
|
)
|
||||||
|
|
||||||
var _ Source = (*StereoscopeImageSource)(nil)
|
var _ source.Source = (*stereoscopeImageSource)(nil)
|
||||||
|
|
||||||
type StereoscopeImageConfig struct {
|
type ImageConfig struct {
|
||||||
Reference string
|
Reference string
|
||||||
From image.Source
|
|
||||||
Platform *image.Platform
|
Platform *image.Platform
|
||||||
RegistryOptions *image.RegistryOptions
|
RegistryOptions *image.RegistryOptions
|
||||||
Exclude ExcludeConfig
|
Exclude source.ExcludeConfig
|
||||||
Alias Alias
|
Alias source.Alias
|
||||||
}
|
}
|
||||||
|
|
||||||
type StereoscopeImageSource struct {
|
type stereoscopeImageSource struct {
|
||||||
id artifact.ID
|
id artifact.ID
|
||||||
config StereoscopeImageConfig
|
config ImageConfig
|
||||||
image *image.Image
|
image *image.Image
|
||||||
metadata StereoscopeImageSourceMetadata
|
metadata source.ImageMetadata
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewFromStereoscopeImageObject(img *image.Image, reference string, alias *Alias) (*StereoscopeImageSource, error) {
|
func New(img *image.Image, cfg ImageConfig) source.Source {
|
||||||
var aliasVal Alias
|
|
||||||
if !alias.IsEmpty() {
|
|
||||||
aliasVal = *alias
|
|
||||||
}
|
|
||||||
cfg := StereoscopeImageConfig{
|
|
||||||
Reference: reference,
|
|
||||||
Alias: aliasVal,
|
|
||||||
}
|
|
||||||
metadata := imageMetadataFromStereoscopeImage(img, cfg.Reference)
|
metadata := imageMetadataFromStereoscopeImage(img, cfg.Reference)
|
||||||
|
return &stereoscopeImageSource{
|
||||||
return &StereoscopeImageSource{
|
|
||||||
id: deriveIDFromStereoscopeImage(cfg.Alias, metadata),
|
id: deriveIDFromStereoscopeImage(cfg.Alias, metadata),
|
||||||
config: cfg,
|
config: cfg,
|
||||||
image: img,
|
image: img,
|
||||||
metadata: metadata,
|
metadata: metadata,
|
||||||
}, nil
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewFromStereoscopeImage(cfg StereoscopeImageConfig) (*StereoscopeImageSource, error) {
|
func (s stereoscopeImageSource) ID() artifact.ID {
|
||||||
ctx := context.TODO()
|
|
||||||
|
|
||||||
var opts []stereoscope.Option
|
|
||||||
if cfg.RegistryOptions != nil {
|
|
||||||
opts = append(opts, stereoscope.WithRegistryOptions(*cfg.RegistryOptions))
|
|
||||||
}
|
|
||||||
|
|
||||||
if cfg.Platform != nil {
|
|
||||||
opts = append(opts, stereoscope.WithPlatform(cfg.Platform.String()))
|
|
||||||
}
|
|
||||||
|
|
||||||
img, err := stereoscope.GetImageFromSource(ctx, cfg.Reference, cfg.From, opts...)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("unable to load image: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
metadata := imageMetadataFromStereoscopeImage(img, cfg.Reference)
|
|
||||||
|
|
||||||
return &StereoscopeImageSource{
|
|
||||||
id: deriveIDFromStereoscopeImage(cfg.Alias, metadata),
|
|
||||||
config: cfg,
|
|
||||||
image: img,
|
|
||||||
metadata: metadata,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s StereoscopeImageSource) ID() artifact.ID {
|
|
||||||
return s.id
|
return s.id
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s StereoscopeImageSource) Describe() Description {
|
func (s stereoscopeImageSource) Describe() source.Description {
|
||||||
a := s.config.Alias
|
a := s.config.Alias
|
||||||
|
|
||||||
name := a.Name
|
name := a.Name
|
||||||
|
@ -123,7 +86,7 @@ func (s StereoscopeImageSource) Describe() Description {
|
||||||
nameIfUnset(s.metadata.UserInput)
|
nameIfUnset(s.metadata.UserInput)
|
||||||
versionIfUnset(s.metadata.ManifestDigest)
|
versionIfUnset(s.metadata.ManifestDigest)
|
||||||
|
|
||||||
return Description{
|
return source.Description{
|
||||||
ID: string(s.id),
|
ID: string(s.id),
|
||||||
Name: name,
|
Name: name,
|
||||||
Version: version,
|
Version: version,
|
||||||
|
@ -131,14 +94,14 @@ func (s StereoscopeImageSource) Describe() Description {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s StereoscopeImageSource) FileResolver(scope Scope) (file.Resolver, error) {
|
func (s stereoscopeImageSource) FileResolver(scope source.Scope) (file.Resolver, error) {
|
||||||
var res file.Resolver
|
var res file.Resolver
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
switch scope {
|
switch scope {
|
||||||
case SquashedScope:
|
case source.SquashedScope:
|
||||||
res, err = fileresolver.NewFromContainerImageSquash(s.image)
|
res, err = fileresolver.NewFromContainerImageSquash(s.image)
|
||||||
case AllLayersScope:
|
case source.AllLayersScope:
|
||||||
res, err = fileresolver.NewFromContainerImageAllLayers(s.image)
|
res, err = fileresolver.NewFromContainerImageAllLayers(s.image)
|
||||||
default:
|
default:
|
||||||
return nil, fmt.Errorf("bad image scope provided: %+v", scope)
|
return nil, fmt.Errorf("bad image scope provided: %+v", scope)
|
||||||
|
@ -156,29 +119,29 @@ func (s StereoscopeImageSource) FileResolver(scope Scope) (file.Resolver, error)
|
||||||
return res, nil
|
return res, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s StereoscopeImageSource) Close() error {
|
func (s stereoscopeImageSource) Close() error {
|
||||||
if s.image == nil {
|
if s.image == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return s.image.Cleanup()
|
return s.image.Cleanup()
|
||||||
}
|
}
|
||||||
|
|
||||||
func imageMetadataFromStereoscopeImage(img *image.Image, reference string) StereoscopeImageSourceMetadata {
|
func imageMetadataFromStereoscopeImage(img *image.Image, reference string) source.ImageMetadata {
|
||||||
tags := make([]string, len(img.Metadata.Tags))
|
tags := make([]string, len(img.Metadata.Tags))
|
||||||
for idx, tag := range img.Metadata.Tags {
|
for idx, tag := range img.Metadata.Tags {
|
||||||
tags[idx] = tag.String()
|
tags[idx] = tag.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
layers := make([]StereoscopeLayerMetadata, len(img.Layers))
|
layers := make([]source.LayerMetadata, len(img.Layers))
|
||||||
for idx, l := range img.Layers {
|
for idx, l := range img.Layers {
|
||||||
layers[idx] = StereoscopeLayerMetadata{
|
layers[idx] = source.LayerMetadata{
|
||||||
MediaType: string(l.Metadata.MediaType),
|
MediaType: string(l.Metadata.MediaType),
|
||||||
Digest: l.Metadata.Digest,
|
Digest: l.Metadata.Digest,
|
||||||
Size: l.Metadata.Size,
|
Size: l.Metadata.Size,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return StereoscopeImageSourceMetadata{
|
return source.ImageMetadata{
|
||||||
ID: img.Metadata.ID,
|
ID: img.Metadata.ID,
|
||||||
UserInput: reference,
|
UserInput: reference,
|
||||||
ManifestDigest: img.Metadata.ManifestDigest,
|
ManifestDigest: img.Metadata.ManifestDigest,
|
||||||
|
@ -203,7 +166,7 @@ func imageMetadataFromStereoscopeImage(img *image.Image, reference string) Stere
|
||||||
//
|
//
|
||||||
// in all cases, if an alias is provided, it is additionally considered in the ID calculation. This allows for the
|
// in all cases, if an alias is provided, it is additionally considered in the ID calculation. This allows for the
|
||||||
// same image to be scanned multiple times with different aliases and be considered logically different.
|
// same image to be scanned multiple times with different aliases and be considered logically different.
|
||||||
func deriveIDFromStereoscopeImage(alias Alias, metadata StereoscopeImageSourceMetadata) artifact.ID {
|
func deriveIDFromStereoscopeImage(alias source.Alias, metadata source.ImageMetadata) artifact.ID {
|
||||||
var input string
|
var input string
|
||||||
|
|
||||||
if len(metadata.RawManifest) > 0 {
|
if len(metadata.RawManifest) > 0 {
|
||||||
|
@ -226,10 +189,10 @@ func deriveIDFromStereoscopeImage(alias Alias, metadata StereoscopeImageSourceMe
|
||||||
input = digest.Canonical.FromString(input + aliasStr).String()
|
input = digest.Canonical.FromString(input + aliasStr).String()
|
||||||
}
|
}
|
||||||
|
|
||||||
return artifactIDFromDigest(input)
|
return internal.ArtifactIDFromDigest(input)
|
||||||
}
|
}
|
||||||
|
|
||||||
func calculateChainID(lm []StereoscopeLayerMetadata) string {
|
func calculateChainID(lm []source.LayerMetadata) string {
|
||||||
if len(lm) < 1 {
|
if len(lm) < 1 {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
@ -242,7 +205,7 @@ func calculateChainID(lm []StereoscopeLayerMetadata) string {
|
||||||
return id
|
return id
|
||||||
}
|
}
|
||||||
|
|
||||||
func chain(chainID string, layers []StereoscopeLayerMetadata) string {
|
func chain(chainID string, layers []source.LayerMetadata) string {
|
||||||
if len(layers) < 1 {
|
if len(layers) < 1 {
|
||||||
return chainID
|
return chainID
|
||||||
}
|
}
|
61
syft/source/stereoscopesource/image_source_provider.go
Normal file
61
syft/source/stereoscopesource/image_source_provider.go
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
package stereoscopesource
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/anchore/go-collections"
|
||||||
|
"github.com/anchore/stereoscope"
|
||||||
|
"github.com/anchore/stereoscope/pkg/image"
|
||||||
|
"github.com/anchore/syft/syft/source"
|
||||||
|
)
|
||||||
|
|
||||||
|
const ImageTag = "image"
|
||||||
|
|
||||||
|
type ProviderConfig struct {
|
||||||
|
StereoscopeImageProviderConfig stereoscope.ImageProviderConfig
|
||||||
|
Exclude source.ExcludeConfig
|
||||||
|
Alias source.Alias
|
||||||
|
}
|
||||||
|
|
||||||
|
type stereoscopeImageSourceProvider struct {
|
||||||
|
stereoscopeProvider image.Provider
|
||||||
|
cfg ProviderConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ source.Provider = (*stereoscopeImageSourceProvider)(nil)
|
||||||
|
|
||||||
|
func (l stereoscopeImageSourceProvider) Name() string {
|
||||||
|
return l.stereoscopeProvider.Name()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l stereoscopeImageSourceProvider) Provide(ctx context.Context) (source.Source, error) {
|
||||||
|
img, err := l.stereoscopeProvider.Provide(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
cfg := ImageConfig{
|
||||||
|
Reference: l.cfg.StereoscopeImageProviderConfig.UserInput,
|
||||||
|
Platform: l.cfg.StereoscopeImageProviderConfig.Platform,
|
||||||
|
RegistryOptions: &l.cfg.StereoscopeImageProviderConfig.Registry,
|
||||||
|
Exclude: l.cfg.Exclude,
|
||||||
|
Alias: l.cfg.Alias,
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return New(img, cfg), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func Providers(cfg ProviderConfig) []collections.TaggedValue[source.Provider] {
|
||||||
|
stereoscopeProviders := collections.TaggedValueSet[source.Provider]{}
|
||||||
|
providers := stereoscope.ImageProviders(cfg.StereoscopeImageProviderConfig)
|
||||||
|
for _, provider := range providers {
|
||||||
|
var sourceProvider source.Provider = stereoscopeImageSourceProvider{
|
||||||
|
stereoscopeProvider: provider.Value,
|
||||||
|
cfg: cfg,
|
||||||
|
}
|
||||||
|
stereoscopeProviders = append(stereoscopeProviders,
|
||||||
|
collections.NewTaggedValue(sourceProvider, append([]string{provider.Value.Name(), ImageTag}, provider.Tags...)...))
|
||||||
|
}
|
||||||
|
return stereoscopeProviders
|
||||||
|
}
|
|
@ -1,6 +1,7 @@
|
||||||
package source
|
package stereoscopesource
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
@ -9,12 +10,16 @@ import (
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
"github.com/anchore/stereoscope/pkg/image"
|
"github.com/anchore/stereoscope"
|
||||||
"github.com/anchore/stereoscope/pkg/imagetest"
|
"github.com/anchore/stereoscope/pkg/imagetest"
|
||||||
"github.com/anchore/syft/syft/artifact"
|
"github.com/anchore/syft/syft/artifact"
|
||||||
|
"github.com/anchore/syft/syft/internal/testutil"
|
||||||
|
"github.com/anchore/syft/syft/source"
|
||||||
)
|
)
|
||||||
|
|
||||||
func Test_StereoscopeImage_Exclusions(t *testing.T) {
|
func Test_StereoscopeImage_Exclusions(t *testing.T) {
|
||||||
|
testutil.Chdir(t, "..") // run with source/test-fixtures
|
||||||
|
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
desc string
|
desc string
|
||||||
input string
|
input string
|
||||||
|
@ -76,22 +81,27 @@ func Test_StereoscopeImage_Exclusions(t *testing.T) {
|
||||||
|
|
||||||
for _, test := range testCases {
|
for _, test := range testCases {
|
||||||
t.Run(test.desc, func(t *testing.T) {
|
t.Run(test.desc, func(t *testing.T) {
|
||||||
src, err := NewFromStereoscopeImage(
|
imageName := strings.SplitN(imagetest.PrepareFixtureImage(t, "docker-archive", test.input), ":", 2)[1]
|
||||||
StereoscopeImageConfig{
|
|
||||||
Reference: strings.SplitN(imagetest.PrepareFixtureImage(t, "docker-archive", test.input), ":", 2)[1],
|
img, err := stereoscope.GetImage(context.TODO(), imageName)
|
||||||
From: image.DockerTarballSource,
|
require.NoError(t, err)
|
||||||
Exclude: ExcludeConfig{
|
require.NotNil(t, img)
|
||||||
|
|
||||||
|
src := New(
|
||||||
|
img,
|
||||||
|
ImageConfig{
|
||||||
|
Reference: imageName,
|
||||||
|
Exclude: source.ExcludeConfig{
|
||||||
Paths: test.exclusions,
|
Paths: test.exclusions,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
require.NoError(t, err)
|
|
||||||
t.Cleanup(func() {
|
t.Cleanup(func() {
|
||||||
require.NoError(t, src.Close())
|
require.NoError(t, src.Close())
|
||||||
})
|
})
|
||||||
|
|
||||||
res, err := src.FileResolver(SquashedScope)
|
res, err := src.FileResolver(source.SquashedScope)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
contents, err := res.FilesByGlob(test.glob)
|
contents, err := res.FilesByGlob(test.glob)
|
||||||
|
@ -105,15 +115,15 @@ func Test_StereoscopeImage_Exclusions(t *testing.T) {
|
||||||
func Test_StereoscopeImageSource_ID(t *testing.T) {
|
func Test_StereoscopeImageSource_ID(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
alias Alias
|
alias source.Alias
|
||||||
metadata StereoscopeImageSourceMetadata
|
metadata source.ImageMetadata
|
||||||
want artifact.ID
|
want artifact.ID
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "use raw manifest over chain ID or user input",
|
name: "use raw manifest over chain ID or user input",
|
||||||
metadata: StereoscopeImageSourceMetadata{
|
metadata: source.ImageMetadata{
|
||||||
UserInput: "user-input",
|
UserInput: "user-input",
|
||||||
Layers: []StereoscopeLayerMetadata{
|
Layers: []source.LayerMetadata{
|
||||||
{
|
{
|
||||||
Digest: "a",
|
Digest: "a",
|
||||||
},
|
},
|
||||||
|
@ -134,9 +144,9 @@ func Test_StereoscopeImageSource_ID(t *testing.T) {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "use chain ID over user input",
|
name: "use chain ID over user input",
|
||||||
metadata: StereoscopeImageSourceMetadata{
|
metadata: source.ImageMetadata{
|
||||||
//UserInput: "user-input",
|
//UserInput: "user-input",
|
||||||
Layers: []StereoscopeLayerMetadata{
|
Layers: []source.LayerMetadata{
|
||||||
{
|
{
|
||||||
Digest: "a",
|
Digest: "a",
|
||||||
},
|
},
|
||||||
|
@ -149,7 +159,7 @@ func Test_StereoscopeImageSource_ID(t *testing.T) {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
want: func() artifact.ID {
|
want: func() artifact.ID {
|
||||||
metadata := []StereoscopeLayerMetadata{
|
metadata := []source.LayerMetadata{
|
||||||
{
|
{
|
||||||
Digest: "a",
|
Digest: "a",
|
||||||
},
|
},
|
||||||
|
@ -165,7 +175,7 @@ func Test_StereoscopeImageSource_ID(t *testing.T) {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "use user input last",
|
name: "use user input last",
|
||||||
metadata: StereoscopeImageSourceMetadata{
|
metadata: source.ImageMetadata{
|
||||||
UserInput: "user-input",
|
UserInput: "user-input",
|
||||||
},
|
},
|
||||||
want: func() artifact.ID {
|
want: func() artifact.ID {
|
||||||
|
@ -176,9 +186,9 @@ func Test_StereoscopeImageSource_ID(t *testing.T) {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "without alias (first)",
|
name: "without alias (first)",
|
||||||
metadata: StereoscopeImageSourceMetadata{
|
metadata: source.ImageMetadata{
|
||||||
UserInput: "user-input",
|
UserInput: "user-input",
|
||||||
Layers: []StereoscopeLayerMetadata{
|
Layers: []source.LayerMetadata{
|
||||||
{
|
{
|
||||||
Digest: "a",
|
Digest: "a",
|
||||||
},
|
},
|
||||||
|
@ -195,13 +205,13 @@ func Test_StereoscopeImageSource_ID(t *testing.T) {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "always consider alias (first)",
|
name: "always consider alias (first)",
|
||||||
alias: Alias{
|
alias: source.Alias{
|
||||||
Name: "alias",
|
Name: "alias",
|
||||||
Version: "version",
|
Version: "version",
|
||||||
},
|
},
|
||||||
metadata: StereoscopeImageSourceMetadata{
|
metadata: source.ImageMetadata{
|
||||||
UserInput: "user-input",
|
UserInput: "user-input",
|
||||||
Layers: []StereoscopeLayerMetadata{
|
Layers: []source.LayerMetadata{
|
||||||
{
|
{
|
||||||
Digest: "a",
|
Digest: "a",
|
||||||
},
|
},
|
||||||
|
@ -218,18 +228,18 @@ func Test_StereoscopeImageSource_ID(t *testing.T) {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "without alias (last)",
|
name: "without alias (last)",
|
||||||
metadata: StereoscopeImageSourceMetadata{
|
metadata: source.ImageMetadata{
|
||||||
UserInput: "user-input",
|
UserInput: "user-input",
|
||||||
},
|
},
|
||||||
want: "ab0dff627d80b9753193d7280bec8f45e8ec6b4cb0912c6fffcf7cd782d9739e",
|
want: "ab0dff627d80b9753193d7280bec8f45e8ec6b4cb0912c6fffcf7cd782d9739e",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "always consider alias (last)",
|
name: "always consider alias (last)",
|
||||||
alias: Alias{
|
alias: source.Alias{
|
||||||
Name: "alias",
|
Name: "alias",
|
||||||
Version: "version",
|
Version: "version",
|
||||||
},
|
},
|
||||||
metadata: StereoscopeImageSourceMetadata{
|
metadata: source.ImageMetadata{
|
||||||
UserInput: "user-input",
|
UserInput: "user-input",
|
||||||
},
|
},
|
||||||
want: "fe86c0eecd5654d3c0c0b2176aa394aef6440347c241aa8d9b628dfdde4287cf",
|
want: "fe86c0eecd5654d3c0c0b2176aa394aef6440347c241aa8d9b628dfdde4287cf",
|
||||||
|
@ -245,18 +255,18 @@ func Test_StereoscopeImageSource_ID(t *testing.T) {
|
||||||
func Test_Describe(t *testing.T) {
|
func Test_Describe(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
source StereoscopeImageSource
|
source stereoscopeImageSource
|
||||||
expected Description
|
expected source.Description
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "name from user input",
|
name: "name from user input",
|
||||||
source: StereoscopeImageSource{
|
source: stereoscopeImageSource{
|
||||||
id: "some-id",
|
id: "some-id",
|
||||||
metadata: StereoscopeImageSourceMetadata{
|
metadata: source.ImageMetadata{
|
||||||
UserInput: "user input",
|
UserInput: "user input",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
expected: Description{
|
expected: source.Description{
|
||||||
ID: "some-id",
|
ID: "some-id",
|
||||||
Name: "user input",
|
Name: "user input",
|
||||||
},
|
},
|
|
@ -351,7 +351,7 @@ func TestPackagesCmdFlags(t *testing.T) {
|
||||||
func TestRegistryAuth(t *testing.T) {
|
func TestRegistryAuth(t *testing.T) {
|
||||||
host := "localhost:17"
|
host := "localhost:17"
|
||||||
image := fmt.Sprintf("%s/something:latest", host)
|
image := fmt.Sprintf("%s/something:latest", host)
|
||||||
args := []string{"scan", "-vvv", fmt.Sprintf("registry:%s", image)}
|
args := []string{"scan", "-vvv", image, "--from", "registry"}
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
|
@ -363,7 +363,7 @@ func TestRegistryAuth(t *testing.T) {
|
||||||
name: "fallback to keychain",
|
name: "fallback to keychain",
|
||||||
args: args,
|
args: args,
|
||||||
assertions: []traitAssertion{
|
assertions: []traitAssertion{
|
||||||
assertInOutput("source=OciRegistry"),
|
assertInOutput("from registry"),
|
||||||
assertInOutput(image),
|
assertInOutput(image),
|
||||||
assertInOutput(fmt.Sprintf("no registry credentials configured for %q, using the default keychain", host)),
|
assertInOutput(fmt.Sprintf("no registry credentials configured for %q, using the default keychain", host)),
|
||||||
},
|
},
|
||||||
|
@ -377,7 +377,7 @@ func TestRegistryAuth(t *testing.T) {
|
||||||
"SYFT_REGISTRY_AUTH_PASSWORD": "password",
|
"SYFT_REGISTRY_AUTH_PASSWORD": "password",
|
||||||
},
|
},
|
||||||
assertions: []traitAssertion{
|
assertions: []traitAssertion{
|
||||||
assertInOutput("source=OciRegistry"),
|
assertInOutput("from registry"),
|
||||||
assertInOutput(image),
|
assertInOutput(image),
|
||||||
assertInOutput(fmt.Sprintf(`using basic auth for registry "%s"`, host)),
|
assertInOutput(fmt.Sprintf(`using basic auth for registry "%s"`, host)),
|
||||||
},
|
},
|
||||||
|
@ -390,7 +390,7 @@ func TestRegistryAuth(t *testing.T) {
|
||||||
"SYFT_REGISTRY_AUTH_TOKEN": "my-token",
|
"SYFT_REGISTRY_AUTH_TOKEN": "my-token",
|
||||||
},
|
},
|
||||||
assertions: []traitAssertion{
|
assertions: []traitAssertion{
|
||||||
assertInOutput("source=OciRegistry"),
|
assertInOutput("from registry"),
|
||||||
assertInOutput(image),
|
assertInOutput(image),
|
||||||
assertInOutput(fmt.Sprintf(`using token for registry "%s"`, host)),
|
assertInOutput(fmt.Sprintf(`using token for registry "%s"`, host)),
|
||||||
},
|
},
|
||||||
|
@ -402,7 +402,7 @@ func TestRegistryAuth(t *testing.T) {
|
||||||
"SYFT_REGISTRY_AUTH_AUTHORITY": host,
|
"SYFT_REGISTRY_AUTH_AUTHORITY": host,
|
||||||
},
|
},
|
||||||
assertions: []traitAssertion{
|
assertions: []traitAssertion{
|
||||||
assertInOutput("source=OciRegistry"),
|
assertInOutput("from registry"),
|
||||||
assertInOutput(image),
|
assertInOutput(image),
|
||||||
assertInOutput(fmt.Sprintf(`no registry credentials configured for %q, using the default keychain`, host)),
|
assertInOutput(fmt.Sprintf(`no registry credentials configured for %q, using the default keychain`, host)),
|
||||||
},
|
},
|
||||||
|
|
Loading…
Reference in a new issue