Introduce new format pattern + port json processing (#550)

* add new format pattern

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>

* add syftjson format

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>

* add internal formats helper

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>

* add SBOM encode/decode to lib API

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>

* remove json presenter + update presenter tests to use common utils

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>

* remove presenter format enum type + add formats shim in presenter helper

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>

* add MustCPE helper for tests

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>

* update usage of format enum

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>

* add test fixtures for encode/decode tests

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>

* fix integration test

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>

* migrate format detection to use reader

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>

* address review comments

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
This commit is contained in:
Alex Goodman 2021-10-20 17:36:34 -04:00 committed by GitHub
parent 5e315c0f17
commit 560b05c2c9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
57 changed files with 3139 additions and 421 deletions

View File

@ -6,6 +6,8 @@ import (
"io/ioutil" "io/ioutil"
"os" "os"
"github.com/anchore/syft/syft/format"
"github.com/anchore/stereoscope" "github.com/anchore/stereoscope"
"github.com/anchore/syft/internal" "github.com/anchore/syft/internal"
"github.com/anchore/syft/internal/anchore" "github.com/anchore/syft/internal/anchore"
@ -48,7 +50,7 @@ const (
) )
var ( var (
packagesPresenterOpt packages.PresenterOption packagesPresenterOpt format.Option
packagesCmd = &cobra.Command{ packagesCmd = &cobra.Command{
Use: "packages [SOURCE]", Use: "packages [SOURCE]",
Short: "Generate a package SBOM", Short: "Generate a package SBOM",
@ -62,8 +64,8 @@ var (
SilenceErrors: true, SilenceErrors: true,
PreRunE: func(cmd *cobra.Command, args []string) error { PreRunE: func(cmd *cobra.Command, args []string) error {
// set the presenter // set the presenter
presenterOption := packages.ParsePresenterOption(appConfig.Output) presenterOption := format.ParseOption(appConfig.Output)
if presenterOption == packages.UnknownPresenterOption { if presenterOption == format.UnknownFormatOption {
return fmt.Errorf("bad --output value '%s'", appConfig.Output) return fmt.Errorf("bad --output value '%s'", appConfig.Output)
} }
packagesPresenterOpt = presenterOption packagesPresenterOpt = presenterOption
@ -100,8 +102,8 @@ func setPackageFlags(flags *pflag.FlagSet) {
fmt.Sprintf("selection of layers to catalog, options=%v", source.AllScopes)) fmt.Sprintf("selection of layers to catalog, options=%v", source.AllScopes))
flags.StringP( flags.StringP(
"output", "o", string(packages.TablePresenterOption), "output", "o", string(format.TableOption),
fmt.Sprintf("report output formatter, options=%v", packages.AllPresenters), fmt.Sprintf("report output formatter, options=%v", format.AllPresenters),
) )
flags.StringP( flags.StringP(

View File

@ -8,7 +8,7 @@ import (
"fmt" "fmt"
"net/http" "net/http"
"github.com/anchore/syft/internal/presenter/packages" "github.com/anchore/syft/internal/formats/syftjson"
"github.com/wagoodman/go-progress" "github.com/wagoodman/go-progress"
@ -26,8 +26,8 @@ type packageSBOMImportAPI interface {
func packageSbomModel(s source.Metadata, catalog *pkg.Catalog, d *distro.Distro, scope source.Scope) (*external.ImagePackageManifest, error) { func packageSbomModel(s source.Metadata, catalog *pkg.Catalog, d *distro.Distro, scope source.Scope) (*external.ImagePackageManifest, error) {
var buf bytes.Buffer var buf bytes.Buffer
pres := packages.NewJSONPresenter(catalog, s, d, scope)
err := pres.Present(&buf) err := syftjson.Format().Presenter(catalog, &s, d, scope).Present(&buf)
if err != nil { if err != nil {
return nil, fmt.Errorf("unable to serialize results: %w", err) return nil, fmt.Errorf("unable to serialize results: %w", err)
} }

View File

@ -9,18 +9,15 @@ import (
"strings" "strings"
"testing" "testing"
"github.com/anchore/syft/internal/presenter/packages"
"github.com/wagoodman/go-progress"
"github.com/anchore/syft/syft/distro"
"github.com/docker/docker/pkg/ioutils"
"github.com/anchore/client-go/pkg/external" "github.com/anchore/client-go/pkg/external"
"github.com/anchore/syft/internal/formats/syftjson"
syftjsonModel "github.com/anchore/syft/internal/formats/syftjson/model"
"github.com/anchore/syft/syft/distro"
"github.com/anchore/syft/syft/pkg" "github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/source" "github.com/anchore/syft/syft/source"
"github.com/docker/docker/pkg/ioutils"
"github.com/go-test/deep" "github.com/go-test/deep"
"github.com/wagoodman/go-progress"
) )
func must(c pkg.CPE, e error) pkg.CPE { func must(c pkg.CPE, e error) pkg.CPE {
@ -88,19 +85,19 @@ func TestPackageSbomToModel(t *testing.T) {
} }
var buf bytes.Buffer var buf bytes.Buffer
pres := packages.NewJSONPresenter(c, m, &d, source.AllLayersScope) pres := syftjson.Format().Presenter(c, &m, &d, source.AllLayersScope)
if err := pres.Present(&buf); err != nil { if err := pres.Present(&buf); err != nil {
t.Fatalf("unable to get expected json: %+v", err) t.Fatalf("unable to get expected json: %+v", err)
} }
// unmarshal expected result // unmarshal expected result
var expectedDoc packages.JSONDocument var expectedDoc syftjsonModel.Document
if err := json.Unmarshal(buf.Bytes(), &expectedDoc); err != nil { if err := json.Unmarshal(buf.Bytes(), &expectedDoc); err != nil {
t.Fatalf("unable to parse json doc: %+v", err) t.Fatalf("unable to parse json doc: %+v", err)
} }
// unmarshal actual result // unmarshal actual result
var actualDoc packages.JSONDocument var actualDoc syftjsonModel.Document
if err := json.Unmarshal(modelJSON, &actualDoc); err != nil { if err := json.Unmarshal(modelJSON, &actualDoc); err != nil {
t.Fatalf("unable to parse json doc: %+v", err) t.Fatalf("unable to parse json doc: %+v", err)
} }

View File

@ -1,15 +1,16 @@
package packages package testutils
import ( import (
"bytes" "bytes"
"testing" "testing"
"github.com/anchore/syft/syft/presenter"
"github.com/anchore/go-testutils" "github.com/anchore/go-testutils"
"github.com/anchore/stereoscope/pkg/filetree" "github.com/anchore/stereoscope/pkg/filetree"
"github.com/anchore/stereoscope/pkg/imagetest" "github.com/anchore/stereoscope/pkg/imagetest"
"github.com/anchore/syft/syft/distro" "github.com/anchore/syft/syft/distro"
"github.com/anchore/syft/syft/pkg" "github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/presenter"
"github.com/anchore/syft/syft/source" "github.com/anchore/syft/syft/source"
"github.com/sergi/go-diff/diffmatchpatch" "github.com/sergi/go-diff/diffmatchpatch"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
@ -17,7 +18,7 @@ import (
type redactor func(s []byte) []byte type redactor func(s []byte) []byte
func assertPresenterAgainstGoldenImageSnapshot(t *testing.T, pres presenter.Presenter, testImage string, updateSnapshot bool, redactors ...redactor) { func AssertPresenterAgainstGoldenImageSnapshot(t *testing.T, pres presenter.Presenter, testImage string, updateSnapshot bool, redactors ...redactor) {
var buffer bytes.Buffer var buffer bytes.Buffer
// grab the latest image contents and persist // grab the latest image contents and persist
@ -50,7 +51,7 @@ func assertPresenterAgainstGoldenImageSnapshot(t *testing.T, pres presenter.Pres
} }
} }
func assertPresenterAgainstGoldenSnapshot(t *testing.T, pres presenter.Presenter, updateSnapshot bool, redactors ...redactor) { func AssertPresenterAgainstGoldenSnapshot(t *testing.T, pres presenter.Presenter, updateSnapshot bool, redactors ...redactor) {
var buffer bytes.Buffer var buffer bytes.Buffer
err := pres.Present(&buffer) err := pres.Present(&buffer)
@ -77,7 +78,7 @@ func assertPresenterAgainstGoldenSnapshot(t *testing.T, pres presenter.Presenter
} }
} }
func presenterImageInput(t testing.TB, testImage string) (*pkg.Catalog, source.Metadata, *distro.Distro) { func ImageInput(t testing.TB, testImage string) (*pkg.Catalog, source.Metadata, *distro.Distro) {
t.Helper() t.Helper()
catalog := pkg.NewCatalog() catalog := pkg.NewCatalog()
img := imagetest.GetGoldenFixtureImage(t, testImage) img := imagetest.GetGoldenFixtureImage(t, testImage)
@ -104,7 +105,7 @@ func presenterImageInput(t testing.TB, testImage string) (*pkg.Catalog, source.M
}, },
PURL: "a-purl-1", PURL: "a-purl-1",
CPEs: []pkg.CPE{ CPEs: []pkg.CPE{
must(pkg.NewCPE("cpe:2.3:*:some:package:1:*:*:*:*:*:*:*")), pkg.MustCPE("cpe:2.3:*:some:package:1:*:*:*:*:*:*:*"),
}, },
}) })
catalog.Add(pkg.Package{ catalog.Add(pkg.Package{
@ -123,7 +124,7 @@ func presenterImageInput(t testing.TB, testImage string) (*pkg.Catalog, source.M
}, },
PURL: "a-purl-2", PURL: "a-purl-2",
CPEs: []pkg.CPE{ CPEs: []pkg.CPE{
must(pkg.NewCPE("cpe:2.3:*:some:package:2:*:*:*:*:*:*:*")), pkg.MustCPE("cpe:2.3:*:some:package:2:*:*:*:*:*:*:*"),
}, },
}) })
@ -139,7 +140,7 @@ func presenterImageInput(t testing.TB, testImage string) (*pkg.Catalog, source.M
return catalog, src.Metadata, &dist return catalog, src.Metadata, &dist
} }
func presenterDirectoryInput(t testing.TB) (*pkg.Catalog, source.Metadata, *distro.Distro) { func DirectoryInput(t testing.TB) (*pkg.Catalog, source.Metadata, *distro.Distro) {
catalog := pkg.NewCatalog() catalog := pkg.NewCatalog()
// populate catalog with test data // populate catalog with test data
@ -160,13 +161,13 @@ func presenterDirectoryInput(t testing.TB) (*pkg.Catalog, source.Metadata, *dist
Version: "1.0.1", Version: "1.0.1",
Files: []pkg.PythonFileRecord{ Files: []pkg.PythonFileRecord{
{ {
Path: "/some/path/pkg1/depedencies/foo", Path: "/some/path/pkg1/dependencies/foo",
}, },
}, },
}, },
PURL: "a-purl-2", PURL: "a-purl-2",
CPEs: []pkg.CPE{ CPEs: []pkg.CPE{
must(pkg.NewCPE("cpe:2.3:*:some:package:2:*:*:*:*:*:*:*")), pkg.MustCPE("cpe:2.3:*:some:package:2:*:*:*:*:*:*:*"),
}, },
}) })
catalog.Add(pkg.Package{ catalog.Add(pkg.Package{
@ -185,7 +186,7 @@ func presenterDirectoryInput(t testing.TB) (*pkg.Catalog, source.Metadata, *dist
}, },
PURL: "a-purl-2", PURL: "a-purl-2",
CPEs: []pkg.CPE{ CPEs: []pkg.CPE{
must(pkg.NewCPE("cpe:2.3:*:some:package:2:*:*:*:*:*:*:*")), pkg.MustCPE("cpe:2.3:*:some:package:2:*:*:*:*:*:*:*"),
}, },
}) })

View File

@ -0,0 +1,34 @@
package formats
import (
"bytes"
"github.com/anchore/syft/internal/formats/syftjson"
"github.com/anchore/syft/syft/format"
)
// TODO: eventually this is the source of truth for all formatters
func All() []format.Format {
return []format.Format{
syftjson.Format(),
}
}
func Identify(by []byte) (*format.Format, error) {
for _, f := range All() {
if err := f.Validate(bytes.NewReader(by)); err != nil {
continue
}
return &f, nil
}
return nil, nil
}
func ByOption(option format.Option) *format.Format {
for _, f := range All() {
if f.Option == option {
return &f
}
}
return nil
}

View File

@ -0,0 +1,34 @@
package formats
import (
"io"
"os"
"testing"
"github.com/anchore/syft/syft/format"
"github.com/stretchr/testify/assert"
)
func TestIdentify(t *testing.T) {
tests := []struct {
fixture string
expected format.Option
}{
{
fixture: "test-fixtures/alpine-syft.json",
expected: format.JSONOption,
},
}
for _, test := range tests {
t.Run(test.fixture, func(t *testing.T) {
f, err := os.Open(test.fixture)
assert.NoError(t, err)
by, err := io.ReadAll(f)
assert.NoError(t, err)
frmt, err := Identify(by)
assert.NoError(t, err)
assert.NotNil(t, frmt)
assert.Equal(t, test.expected, frmt.Option)
})
}
}

View File

@ -0,0 +1,24 @@
package syftjson
import (
"encoding/json"
"fmt"
"io"
"github.com/anchore/syft/internal/formats/syftjson/model"
"github.com/anchore/syft/syft/distro"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/source"
)
func decoder(reader io.Reader) (*pkg.Catalog, *source.Metadata, *distro.Distro, source.Scope, error) {
dec := json.NewDecoder(reader)
var doc model.Document
err := dec.Decode(&doc)
if err != nil {
return nil, nil, nil, source.UnknownScope, fmt.Errorf("unable to decode syft-json: %w", err)
}
return toSyftModel(doc)
}

View File

@ -0,0 +1,52 @@
package syftjson
import (
"bytes"
"strings"
"testing"
"github.com/anchore/syft/syft/source"
"github.com/anchore/syft/internal/formats/common/testutils"
"github.com/go-test/deep"
"github.com/stretchr/testify/assert"
)
func TestEncodeDecodeCycle(t *testing.T) {
testImage := "image-simple"
originalCatalog, originalMetadata, _ := testutils.ImageInput(t, testImage)
var buf bytes.Buffer
assert.NoError(t, encoder(&buf, originalCatalog, &originalMetadata, nil, source.SquashedScope))
actualCatalog, actualMetadata, _, _, err := decoder(bytes.NewReader(buf.Bytes()))
assert.NoError(t, err)
for _, d := range deep.Equal(originalMetadata, *actualMetadata) {
t.Errorf("metadata difference: %+v", d)
}
actualPackages := actualCatalog.Sorted()
for idx, p := range originalCatalog.Sorted() {
if !assert.Equal(t, p.Name, actualPackages[idx].Name) {
t.Errorf("different package at idx=%d: %s vs %s", idx, p.Name, actualPackages[idx].Name)
continue
}
// ids will never be equal
p.ID = ""
actualPackages[idx].ID = ""
for _, d := range deep.Equal(*p, *actualPackages[idx]) {
if strings.Contains(d, ".VirtualPath: ") {
// location.Virtual path is not exposed in the json output
continue
}
if strings.HasSuffix(d, "<nil slice> != []") {
// semantically the same
continue
}
t.Errorf("package difference (%s): %+v", p.Name, d)
}
}
}

View File

@ -0,0 +1,23 @@
package syftjson
import (
"encoding/json"
"io"
"github.com/anchore/syft/syft/distro"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/source"
)
func encoder(output io.Writer, catalog *pkg.Catalog, srcMetadata *source.Metadata, d *distro.Distro, scope source.Scope) error {
// TODO: application config not available yet
doc := ToFormatModel(catalog, srcMetadata, d, scope, nil)
enc := json.NewEncoder(output)
// prevent > and < from being escaped in the payload
enc.SetEscapeHTML(false)
enc.SetIndent("", " ")
return enc.Encode(&doc)
}

View File

@ -0,0 +1,31 @@
package syftjson
import (
"flag"
"testing"
"github.com/anchore/syft/syft/source"
"github.com/anchore/syft/internal/formats/common/testutils"
"github.com/anchore/syft/syft/format"
)
var updateJson = flag.Bool("update-json", false, "update the *.golden files for json presenters")
func TestDirectoryPresenter(t *testing.T) {
catalog, metadata, distro := testutils.DirectoryInput(t)
testutils.AssertPresenterAgainstGoldenSnapshot(t,
format.NewPresenter(encoder, catalog, &metadata, distro, source.SquashedScope),
*updateJson,
)
}
func TestImagePresenter(t *testing.T) {
testImage := "image-simple"
catalog, metadata, distro := testutils.ImageInput(t, testImage)
testutils.AssertPresenterAgainstGoldenImageSnapshot(t,
format.NewPresenter(encoder, catalog, &metadata, distro, source.SquashedScope),
testImage,
*updateJson,
)
}

View File

@ -0,0 +1,12 @@
package syftjson
import "github.com/anchore/syft/syft/format"
func Format() format.Format {
return format.NewFormat(
format.JSONOption,
encoder,
decoder,
validator,
)
}

View File

@ -0,0 +1,8 @@
package model
// Distro provides information about a detected Linux Distro.
type Distro struct {
Name string `json:"name"` // Name of the Linux distribution
Version string `json:"version"` // Version of the Linux distribution (major or major.minor version)
IDLike string `json:"idLike"` // the ID_LIKE field found within the /etc/os-release file
}

View File

@ -0,0 +1,23 @@
package model
// Document represents the syft cataloging findings as a JSON document
type Document struct {
Artifacts []Package `json:"artifacts"` // Artifacts is the list of packages discovered and placed into the catalog
ArtifactRelationships []Relationship `json:"artifactRelationships"`
Source Source `json:"source"` // Source represents the original object that was cataloged
Distro Distro `json:"distro"` // Distro represents the Linux distribution that was detected from the source
Descriptor Descriptor `json:"descriptor"` // Descriptor is a block containing self-describing information about syft
Schema Schema `json:"schema"` // Schema is a block reserved for defining the version for the shape of this JSON document and where to find the schema document to validate the shape
}
// Descriptor describes what created the document as well as surrounding metadata
type Descriptor struct {
Name string `json:"name"`
Version string `json:"version"`
Configuration interface{} `json:"configuration,omitempty"`
}
type Schema struct {
Version string `json:"version"`
URL string `json:"url"`
}

View File

@ -0,0 +1,123 @@
package model
import (
"encoding/json"
"fmt"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/source"
)
// Package represents a pkg.Package object specialized for JSON marshaling and unmarshaling.
type Package struct {
PackageBasicData
PackageCustomData
}
// PackageBasicData contains non-ambiguous values (type-wise) from pkg.Package.
type PackageBasicData struct {
ID string `json:"id"`
Name string `json:"name"`
Version string `json:"version"`
Type pkg.Type `json:"type"`
FoundBy string `json:"foundBy"`
Locations []source.Location `json:"locations"`
Licenses []string `json:"licenses"`
Language pkg.Language `json:"language"`
CPEs []string `json:"cpes"`
PURL string `json:"purl"`
}
// PackageCustomData contains ambiguous values (type-wise) from pkg.Package.
type PackageCustomData struct {
MetadataType pkg.MetadataType `json:"metadataType"`
Metadata interface{} `json:"metadata"`
}
// packageMetadataUnpacker is all values needed from Package to disambiguate ambiguous fields during json unmarshaling.
type packageMetadataUnpacker struct {
MetadataType pkg.MetadataType `json:"metadataType"`
Metadata json.RawMessage `json:"metadata"`
}
func (p *packageMetadataUnpacker) String() string {
return fmt.Sprintf("metadataType: %s, metadata: %s", p.MetadataType, string(p.Metadata))
}
// UnmarshalJSON is a custom unmarshaller for handling basic values and values with ambiguous types.
// nolint:funlen
func (p *Package) UnmarshalJSON(b []byte) error {
var basic PackageBasicData
if err := json.Unmarshal(b, &basic); err != nil {
return err
}
p.PackageBasicData = basic
var unpacker packageMetadataUnpacker
if err := json.Unmarshal(b, &unpacker); err != nil {
log.Warnf("failed to unmarshall into packageMetadataUnpacker: %v", err)
return err
}
p.MetadataType = unpacker.MetadataType
switch p.MetadataType {
case pkg.ApkMetadataType:
var payload pkg.ApkMetadata
if err := json.Unmarshal(unpacker.Metadata, &payload); err != nil {
return err
}
p.Metadata = payload
case pkg.RpmdbMetadataType:
var payload pkg.RpmdbMetadata
if err := json.Unmarshal(unpacker.Metadata, &payload); err != nil {
return err
}
p.Metadata = payload
case pkg.DpkgMetadataType:
var payload pkg.DpkgMetadata
if err := json.Unmarshal(unpacker.Metadata, &payload); err != nil {
return err
}
p.Metadata = payload
case pkg.JavaMetadataType:
var payload pkg.JavaMetadata
if err := json.Unmarshal(unpacker.Metadata, &payload); err != nil {
return err
}
p.Metadata = payload
case pkg.RustCargoPackageMetadataType:
var payload pkg.CargoPackageMetadata
if err := json.Unmarshal(unpacker.Metadata, &payload); err != nil {
return err
}
p.Metadata = payload
case pkg.GemMetadataType:
var payload pkg.GemMetadata
if err := json.Unmarshal(unpacker.Metadata, &payload); err != nil {
return err
}
p.Metadata = payload
case pkg.KbPackageMetadataType:
var payload pkg.KbPackageMetadata
if err := json.Unmarshal(unpacker.Metadata, &payload); err != nil {
return err
}
p.Metadata = payload
case pkg.PythonPackageMetadataType:
var payload pkg.PythonPackageMetadata
if err := json.Unmarshal(unpacker.Metadata, &payload); err != nil {
return err
}
p.Metadata = payload
case pkg.NpmPackageJSONMetadataType:
var payload pkg.NpmPackageJSONMetadata
if err := json.Unmarshal(unpacker.Metadata, &payload); err != nil {
return err
}
p.Metadata = payload
}
return nil
}

View File

@ -0,0 +1,8 @@
package model
type Relationship struct {
Parent string `json:"parent"`
Child string `json:"child"`
Type string `json:"type"`
Metadata interface{} `json:"metadata"`
}

View File

@ -0,0 +1,57 @@
package model
import (
"encoding/json"
"fmt"
"strconv"
"github.com/anchore/syft/syft/source"
)
// Source object represents the thing that was cataloged
type Source struct {
Type string `json:"type"`
Target interface{} `json:"target"`
}
// sourceUnpacker is used to unmarshal Source objects
type sourceUnpacker struct {
Type string `json:"type"`
Target json.RawMessage `json:"target"`
}
type ImageSource struct {
source.ImageMetadata
Scope source.Scope `json:"scope"`
}
// UnmarshalJSON populates a source object from JSON bytes.
func (s *Source) UnmarshalJSON(b []byte) error {
var unpacker sourceUnpacker
if err := json.Unmarshal(b, &unpacker); err != nil {
return err
}
s.Type = unpacker.Type
switch s.Type {
case "directory":
if target, err := strconv.Unquote(string(unpacker.Target)); err == nil {
s.Target = target
} else {
s.Target = string(unpacker.Target[:])
}
case "image":
var payload ImageSource
if err := json.Unmarshal(unpacker.Target, &payload); err != nil {
return err
}
s.Target = payload
default:
return fmt.Errorf("unsupported package metadata type: %+v", s.Type)
}
return nil
}

View File

@ -0,0 +1,4 @@
# Note: changes to this file will result in updating several test values. Consider making a new image fixture instead of editing this one.
FROM scratch
ADD file-1.txt /somefile-1.txt
ADD file-2.txt /somefile-2.txt

View File

@ -0,0 +1 @@
this file has contents

View File

@ -0,0 +1 @@
file-2 contents!

View File

@ -0,0 +1,86 @@
{
"artifacts": [
{
"id": "package-1-id",
"name": "package-1",
"version": "1.0.1",
"type": "python",
"foundBy": "the-cataloger-1",
"locations": [
{
"path": "/some/path/pkg1"
}
],
"licenses": [
"MIT"
],
"language": "python",
"cpes": [
"cpe:2.3:*:some:package:2:*:*:*:*:*:*:*"
],
"purl": "a-purl-2",
"metadataType": "PythonPackageMetadata",
"metadata": {
"name": "package-1",
"version": "1.0.1",
"license": "",
"author": "",
"authorEmail": "",
"platform": "",
"files": [
{
"path": "/some/path/pkg1/dependencies/foo"
}
],
"sitePackagesRootPath": ""
}
},
{
"id": "package-2-id",
"name": "package-2",
"version": "2.0.1",
"type": "deb",
"foundBy": "the-cataloger-2",
"locations": [
{
"path": "/some/path/pkg1"
}
],
"licenses": [],
"language": "",
"cpes": [
"cpe:2.3:*:some:package:2:*:*:*:*:*:*:*"
],
"purl": "a-purl-2",
"metadataType": "DpkgMetadata",
"metadata": {
"package": "package-2",
"source": "",
"version": "2.0.1",
"sourceVersion": "",
"architecture": "",
"maintainer": "",
"installedSize": 0,
"files": null
}
}
],
"artifactRelationships": [],
"source": {
"type": "directory",
"target": "/some/path"
},
"distro": {
"name": "debian",
"version": "1.2.3",
"idLike": "like!"
},
"descriptor": {
"name": "syft",
"version": "[not provided]"
},
"schema": {
"version": "1.1.0",
"url": "https://raw.githubusercontent.com/anchore/syft/main/schema/json/schema-1.1.0.json"
}
}

View File

@ -0,0 +1,108 @@
{
"artifacts": [
{
"id": "package-1-id",
"name": "package-1",
"version": "1.0.1",
"type": "python",
"foundBy": "the-cataloger-1",
"locations": [
{
"path": "/somefile-1.txt",
"layerID": "sha256:fb6beecb75b39f4bb813dbf177e501edd5ddb3e69bb45cedeb78c676ee1b7a59"
}
],
"licenses": [
"MIT"
],
"language": "python",
"cpes": [
"cpe:2.3:*:some:package:1:*:*:*:*:*:*:*"
],
"purl": "a-purl-1",
"metadataType": "PythonPackageMetadata",
"metadata": {
"name": "package-1",
"version": "1.0.1",
"license": "",
"author": "",
"authorEmail": "",
"platform": "",
"sitePackagesRootPath": ""
}
},
{
"id": "package-2-id",
"name": "package-2",
"version": "2.0.1",
"type": "deb",
"foundBy": "the-cataloger-2",
"locations": [
{
"path": "/somefile-2.txt",
"layerID": "sha256:319b588ce64253a87b533c8ed01cf0025e0eac98e7b516e12532957e1244fdec"
}
],
"licenses": [],
"language": "",
"cpes": [
"cpe:2.3:*:some:package:2:*:*:*:*:*:*:*"
],
"purl": "a-purl-2",
"metadataType": "DpkgMetadata",
"metadata": {
"package": "package-2",
"source": "",
"version": "2.0.1",
"sourceVersion": "",
"architecture": "",
"maintainer": "",
"installedSize": 0,
"files": null
}
}
],
"artifactRelationships": [],
"source": {
"type": "image",
"target": {
"userInput": "user-image-input",
"imageID": "sha256:2480160b55bec40c44d3b145c7b2c1c47160db8575c3dcae086d76b9370ae7ca",
"manifestDigest": "sha256:2731251dc34951c0e50fcc643b4c5f74922dad1a5d98f302b504cf46cd5d9368",
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
"tags": [
"stereoscope-fixture-image-simple:85066c51088bdd274f7a89e99e00490f666c49e72ffc955707cd6e18f0e22c5b"
],
"imageSize": 38,
"layers": [
{
"mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
"digest": "sha256:fb6beecb75b39f4bb813dbf177e501edd5ddb3e69bb45cedeb78c676ee1b7a59",
"size": 22
},
{
"mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
"digest": "sha256:319b588ce64253a87b533c8ed01cf0025e0eac98e7b516e12532957e1244fdec",
"size": 16
}
],
"manifest": "eyJzY2hlbWFWZXJzaW9uIjoyLCJtZWRpYVR5cGUiOiJhcHBsaWNhdGlvbi92bmQuZG9ja2VyLmRpc3RyaWJ1dGlvbi5tYW5pZmVzdC52Mitqc29uIiwiY29uZmlnIjp7Im1lZGlhVHlwZSI6ImFwcGxpY2F0aW9uL3ZuZC5kb2NrZXIuY29udGFpbmVyLmltYWdlLnYxK2pzb24iLCJzaXplIjo2NjcsImRpZ2VzdCI6InNoYTI1NjoyNDgwMTYwYjU1YmVjNDBjNDRkM2IxNDVjN2IyYzFjNDcxNjBkYjg1NzVjM2RjYWUwODZkNzZiOTM3MGFlN2NhIn0sImxheWVycyI6W3sibWVkaWFUeXBlIjoiYXBwbGljYXRpb24vdm5kLmRvY2tlci5pbWFnZS5yb290ZnMuZGlmZi50YXIuZ3ppcCIsInNpemUiOjIwNDgsImRpZ2VzdCI6InNoYTI1NjpmYjZiZWVjYjc1YjM5ZjRiYjgxM2RiZjE3N2U1MDFlZGQ1ZGRiM2U2OWJiNDVjZWRlYjc4YzY3NmVlMWI3YTU5In0seyJtZWRpYVR5cGUiOiJhcHBsaWNhdGlvbi92bmQuZG9ja2VyLmltYWdlLnJvb3Rmcy5kaWZmLnRhci5nemlwIiwic2l6ZSI6MjA0OCwiZGlnZXN0Ijoic2hhMjU2OjMxOWI1ODhjZTY0MjUzYTg3YjUzM2M4ZWQwMWNmMDAyNWUwZWFjOThlN2I1MTZlMTI1MzI5NTdlMTI0NGZkZWMifV19",
"config": "eyJhcmNoaXRlY3R1cmUiOiJhbWQ2NCIsImNvbmZpZyI6eyJFbnYiOlsiUEFUSD0vdXNyL2xvY2FsL3NiaW46L3Vzci9sb2NhbC9iaW46L3Vzci9zYmluOi91c3IvYmluOi9zYmluOi9iaW4iXSwiV29ya2luZ0RpciI6Ii8iLCJPbkJ1aWxkIjpudWxsfSwiY3JlYXRlZCI6IjIwMjEtMTAtMDRUMTE6NDA6MDAuNjM4Mzk0NVoiLCJoaXN0b3J5IjpbeyJjcmVhdGVkIjoiMjAyMS0xMC0wNFQxMTo0MDowMC41OTA3MzE2WiIsImNyZWF0ZWRfYnkiOiJBREQgZmlsZS0xLnR4dCAvc29tZWZpbGUtMS50eHQgIyBidWlsZGtpdCIsImNvbW1lbnQiOiJidWlsZGtpdC5kb2NrZXJmaWxlLnYwIn0seyJjcmVhdGVkIjoiMjAyMS0xMC0wNFQxMTo0MDowMC42MzgzOTQ1WiIsImNyZWF0ZWRfYnkiOiJBREQgZmlsZS0yLnR4dCAvc29tZWZpbGUtMi50eHQgIyBidWlsZGtpdCIsImNvbW1lbnQiOiJidWlsZGtpdC5kb2NrZXJmaWxlLnYwIn1dLCJvcyI6ImxpbnV4Iiwicm9vdGZzIjp7InR5cGUiOiJsYXllcnMiLCJkaWZmX2lkcyI6WyJzaGEyNTY6ZmI2YmVlY2I3NWIzOWY0YmI4MTNkYmYxNzdlNTAxZWRkNWRkYjNlNjliYjQ1Y2VkZWI3OGM2NzZlZTFiN2E1OSIsInNoYTI1NjozMTliNTg4Y2U2NDI1M2E4N2I1MzNjOGVkMDFjZjAwMjVlMGVhYzk4ZTdiNTE2ZTEyNTMyOTU3ZTEyNDRmZGVjIl19fQ==",
"repoDigests": [],
"scope": "Squashed"
}
},
"distro": {
"name": "debian",
"version": "1.2.3",
"idLike": "like!"
},
"descriptor": {
"name": "syft",
"version": "[not provided]"
},
"schema": {
"version": "1.1.0",
"url": "https://raw.githubusercontent.com/anchore/syft/main/schema/json/schema-1.1.0.json"
}
}

View File

@ -0,0 +1,133 @@
package syftjson
import (
"fmt"
"github.com/anchore/syft/internal"
"github.com/anchore/syft/internal/formats/syftjson/model"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/internal/version"
"github.com/anchore/syft/syft/distro"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/source"
)
// TODO: this is export4ed for the use of the power-user command (temp)
func ToFormatModel(catalog *pkg.Catalog, srcMetadata *source.Metadata, d *distro.Distro, scope source.Scope, applicationConfig interface{}) model.Document {
src, err := toSourceModel(srcMetadata, scope)
if err != nil {
log.Warnf("unable to create syft-json source object: %+v", err)
}
return model.Document{
Artifacts: toPackageModels(catalog),
ArtifactRelationships: toRelationshipModel(pkg.NewRelationships(catalog)),
Source: src,
Distro: toDistroModel(d),
Descriptor: model.Descriptor{
Name: internal.ApplicationName,
Version: version.FromBuild().Version,
Configuration: applicationConfig,
},
Schema: model.Schema{
Version: internal.JSONSchemaVersion,
URL: fmt.Sprintf("https://raw.githubusercontent.com/anchore/syft/main/schema/json/schema-%s.json", internal.JSONSchemaVersion),
},
}
}
func toPackageModels(catalog *pkg.Catalog) []model.Package {
artifacts := make([]model.Package, 0)
if catalog == nil {
return artifacts
}
for _, p := range catalog.Sorted() {
artifacts = append(artifacts, toPackageModel(p))
}
return artifacts
}
// toPackageModel crates a new Package from the given pkg.Package.
func toPackageModel(p *pkg.Package) model.Package {
var cpes = make([]string, len(p.CPEs))
for i, c := range p.CPEs {
cpes[i] = c.BindToFmtString()
}
// ensure collections are never nil for presentation reasons
var locations = make([]source.Location, 0)
if p.Locations != nil {
locations = p.Locations
}
var licenses = make([]string, 0)
if p.Licenses != nil {
licenses = p.Licenses
}
return model.Package{
PackageBasicData: model.PackageBasicData{
ID: string(p.ID),
Name: p.Name,
Version: p.Version,
Type: p.Type,
FoundBy: p.FoundBy,
Locations: locations,
Licenses: licenses,
Language: p.Language,
CPEs: cpes,
PURL: p.PURL,
},
PackageCustomData: model.PackageCustomData{
MetadataType: p.MetadataType,
Metadata: p.Metadata,
},
}
}
func toRelationshipModel(relationships []pkg.Relationship) []model.Relationship {
result := make([]model.Relationship, len(relationships))
for i, r := range relationships {
result[i] = model.Relationship{
Parent: string(r.Parent),
Child: string(r.Child),
Type: string(r.Type),
Metadata: r.Metadata,
}
}
return result
}
// toSourceModel creates a new source object to be represented into JSON.
func toSourceModel(src *source.Metadata, scope source.Scope) (model.Source, error) {
switch src.Scheme {
case source.ImageScheme:
return model.Source{
Type: "image",
Target: model.ImageSource{
ImageMetadata: src.ImageMetadata,
Scope: scope,
},
}, nil
case source.DirectoryScheme:
return model.Source{
Type: "directory",
Target: src.Path,
}, nil
default:
return model.Source{}, fmt.Errorf("unsupported source: %q", src.Scheme)
}
}
// toDistroModel creates a struct with the Linux distribution to be represented in JSON.
func toDistroModel(d *distro.Distro) model.Distro {
if d == nil {
return model.Distro{}
}
return model.Distro{
Name: d.Name(),
Version: d.FullVersion(),
IDLike: d.IDLike,
}
}

View File

@ -0,0 +1,73 @@
package syftjson
import (
"github.com/anchore/syft/internal/formats/syftjson/model"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/syft/distro"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/source"
)
func toSyftModel(doc model.Document) (*pkg.Catalog, *source.Metadata, *distro.Distro, source.Scope, error) {
dist, err := distro.NewDistro(distro.Type(doc.Distro.Name), doc.Distro.Version, doc.Distro.IDLike)
if err != nil {
return nil, nil, nil, source.UnknownScope, err
}
srcMetadata, scope := toSyftSourceData(doc.Source)
return toSyftCatalog(doc.Artifacts), srcMetadata, &dist, scope, nil
}
func toSyftSourceData(s model.Source) (*source.Metadata, source.Scope) {
switch s.Type {
case "directory":
return &source.Metadata{
Scheme: source.DirectoryScheme,
Path: s.Target.(string),
}, source.UnknownScope
case "image":
parsedSource := s.Target.(model.ImageSource)
return &source.Metadata{
Scheme: source.ImageScheme,
ImageMetadata: parsedSource.ImageMetadata,
}, parsedSource.Scope
}
return nil, source.UnknownScope
}
func toSyftCatalog(pkgs []model.Package) *pkg.Catalog {
catalog := pkg.NewCatalog()
for _, p := range pkgs {
catalog.Add(toSyftPackage(p))
}
return catalog
}
func toSyftPackage(p model.Package) pkg.Package {
var cpes []pkg.CPE
for _, c := range p.CPEs {
value, err := pkg.NewCPE(c)
if err != nil {
log.Warnf("excluding invalid CPE %q: %v", c, err)
continue
}
cpes = append(cpes, value)
}
return pkg.Package{
ID: pkg.ID(p.ID),
Name: p.Name,
Version: p.Version,
FoundBy: p.FoundBy,
Locations: p.Locations,
Licenses: p.Licenses,
Language: p.Language,
Type: p.Type,
CPEs: cpes,
PURL: p.PURL,
MetadataType: p.MetadataType,
Metadata: p.Metadata,
}
}

View File

@ -0,0 +1,31 @@
package syftjson
import (
"encoding/json"
"fmt"
"io"
"strings"
"github.com/anchore/syft/internal/formats/syftjson/model"
)
func validator(reader io.Reader) error {
type Document struct {
Schema model.Schema `json:"schema"`
}
dec := json.NewDecoder(reader)
var doc Document
err := dec.Decode(&doc)
if err != nil {
return fmt.Errorf("unable to decode: %w", err)
}
// note: we accept all schema versions
// TODO: add per-schema version parsing
if strings.Contains(doc.Schema.URL, "anchore/syft") {
return nil
}
return fmt.Errorf("could not extract syft schema")
}

File diff suppressed because it is too large Load Diff

View File

@ -4,13 +4,15 @@ import (
"flag" "flag"
"regexp" "regexp"
"testing" "testing"
"github.com/anchore/syft/internal/formats/common/testutils"
) )
var updateCycloneDx = flag.Bool("update-cyclonedx", false, "update the *.golden files for cyclone-dx presenters") var updateCycloneDx = flag.Bool("update-cyclonedx", false, "update the *.golden files for cyclone-dx presenters")
func TestCycloneDxDirectoryPresenter(t *testing.T) { func TestCycloneDxDirectoryPresenter(t *testing.T) {
catalog, metadata, _ := presenterDirectoryInput(t) catalog, metadata, _ := testutils.DirectoryInput(t)
assertPresenterAgainstGoldenSnapshot(t, testutils.AssertPresenterAgainstGoldenSnapshot(t,
NewCycloneDxPresenter(catalog, metadata), NewCycloneDxPresenter(catalog, metadata),
*updateCycloneDx, *updateCycloneDx,
cycloneDxRedactor, cycloneDxRedactor,
@ -19,8 +21,8 @@ func TestCycloneDxDirectoryPresenter(t *testing.T) {
func TestCycloneDxImagePresenter(t *testing.T) { func TestCycloneDxImagePresenter(t *testing.T) {
testImage := "image-simple" testImage := "image-simple"
catalog, metadata, _ := presenterImageInput(t, testImage) catalog, metadata, _ := testutils.ImageInput(t, testImage)
assertPresenterAgainstGoldenImageSnapshot(t, testutils.AssertPresenterAgainstGoldenImageSnapshot(t,
NewCycloneDxPresenter(catalog, metadata), NewCycloneDxPresenter(catalog, metadata),
testImage, testImage,
*updateCycloneDx, *updateCycloneDx,

View File

@ -1,23 +0,0 @@
package packages
import "github.com/anchore/syft/syft/distro"
// JSONDistribution provides information about a detected Linux JSONDistribution.
type JSONDistribution struct {
Name string `json:"name"` // Name of the Linux distribution
Version string `json:"version"` // Version of the Linux distribution (major or major.minor version)
IDLike string `json:"idLike"` // the ID_LIKE field found within the /etc/os-release file
}
// NewJSONDistribution creates a struct with the Linux distribution to be represented in JSON.
func NewJSONDistribution(d *distro.Distro) JSONDistribution {
if d == nil {
return JSONDistribution{}
}
return JSONDistribution{
Name: d.Name(),
Version: d.FullVersion(),
IDLike: d.IDLike,
}
}

View File

@ -1,62 +0,0 @@
package packages
import (
"fmt"
"github.com/anchore/syft/internal"
"github.com/anchore/syft/internal/version"
"github.com/anchore/syft/syft/distro"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/source"
)
// JSONDocument represents the syft cataloging findings as a JSON document
type JSONDocument struct {
Artifacts []JSONPackage `json:"artifacts"` // Artifacts is the list of packages discovered and placed into the catalog
ArtifactRelationships []JSONRelationship `json:"artifactRelationships"`
Source JSONSource `json:"source"` // Source represents the original object that was cataloged
Distro JSONDistribution `json:"distro"` // Distro represents the Linux distribution that was detected from the source
Descriptor JSONDescriptor `json:"descriptor"` // Descriptor is a block containing self-describing information about syft
Schema JSONSchema `json:"schema"` // Schema is a block reserved for defining the version for the shape of this JSON document and where to find the schema document to validate the shape
}
// NewJSONDocument creates and populates a new JSON document struct from the given cataloging results.
func NewJSONDocument(catalog *pkg.Catalog, srcMetadata source.Metadata, d *distro.Distro, scope source.Scope, configuration interface{}) (JSONDocument, error) {
src, err := NewJSONSource(srcMetadata, scope)
if err != nil {
return JSONDocument{}, err
}
artifacts, err := NewJSONPackages(catalog)
if err != nil {
return JSONDocument{}, err
}
return JSONDocument{
Artifacts: artifacts,
ArtifactRelationships: newJSONRelationships(pkg.NewRelationships(catalog)),
Source: src,
Distro: NewJSONDistribution(d),
Descriptor: JSONDescriptor{
Name: internal.ApplicationName,
Version: version.FromBuild().Version,
Configuration: configuration,
},
Schema: JSONSchema{
Version: internal.JSONSchemaVersion,
URL: fmt.Sprintf("https://raw.githubusercontent.com/anchore/syft/main/schema/json/schema-%s.json", internal.JSONSchemaVersion),
},
}, nil
}
// JSONDescriptor describes what created the document as well as surrounding metadata
type JSONDescriptor struct {
Name string `json:"name"`
Version string `json:"version"`
Configuration interface{} `json:"configuration,omitempty"`
}
type JSONSchema struct {
Version string `json:"version"`
URL string `json:"url"`
}

View File

@ -1,71 +0,0 @@
package packages
import (
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/source"
)
// JSONPackage represents a pkg.Package object specialized for JSON marshaling and unmarshaling.
type JSONPackage struct {
ID string `json:"id"`
Name string `json:"name"`
Version string `json:"version"`
Type string `json:"type"`
FoundBy string `json:"foundBy"`
Locations []source.Location `json:"locations"`
Licenses []string `json:"licenses"`
Language string `json:"language"`
CPEs []string `json:"cpes"`
PURL string `json:"purl"`
MetadataType string `json:"metadataType"`
Metadata interface{} `json:"metadata"`
}
func NewJSONPackages(catalog *pkg.Catalog) ([]JSONPackage, error) {
artifacts := make([]JSONPackage, 0)
if catalog == nil {
return artifacts, nil
}
for _, p := range catalog.Sorted() {
art, err := NewJSONPackage(p)
if err != nil {
return nil, err
}
artifacts = append(artifacts, art)
}
return artifacts, nil
}
// NewJSONPackage crates a new JSONPackage from the given pkg.Package.
func NewJSONPackage(p *pkg.Package) (JSONPackage, error) {
var cpes = make([]string, len(p.CPEs))
for i, c := range p.CPEs {
cpes[i] = c.BindToFmtString()
}
// ensure collections are never nil for presentation reasons
var locations = make([]source.Location, 0)
if p.Locations != nil {
locations = p.Locations
}
var licenses = make([]string, 0)
if p.Licenses != nil {
licenses = p.Licenses
}
return JSONPackage{
ID: string(p.ID),
Name: p.Name,
Version: p.Version,
Type: string(p.Type),
FoundBy: p.FoundBy,
Locations: locations,
Licenses: licenses,
Language: string(p.Language),
CPEs: cpes,
PURL: p.PURL,
MetadataType: string(p.MetadataType),
Metadata: p.Metadata,
}, nil
}

View File

@ -1,43 +0,0 @@
package packages
import (
"encoding/json"
"io"
"github.com/anchore/syft/syft/distro"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/source"
)
// JSONPresenter is a JSON presentation object for the syft results
type JSONPresenter struct {
catalog *pkg.Catalog
srcMetadata source.Metadata
distro *distro.Distro
scope source.Scope
}
// NewJSONPresenter creates a new JSON presenter object for the given cataloging results.
func NewJSONPresenter(catalog *pkg.Catalog, s source.Metadata, d *distro.Distro, scope source.Scope) *JSONPresenter {
return &JSONPresenter{
catalog: catalog,
srcMetadata: s,
distro: d,
scope: scope,
}
}
// Present the catalog results to the given writer.
func (pres *JSONPresenter) Present(output io.Writer) error {
// we do not pass in configuration for backwards compatibility
doc, err := NewJSONDocument(pres.catalog, pres.srcMetadata, pres.distro, pres.scope, nil)
if err != nil {
return err
}
enc := json.NewEncoder(output)
// prevent > and < from being escaped in the payload
enc.SetEscapeHTML(false)
enc.SetIndent("", " ")
return enc.Encode(&doc)
}

View File

@ -1,37 +0,0 @@
package packages
import (
"flag"
"testing"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/source"
)
var updateJSONGoldenFiles = flag.Bool("update-json", false, "update the *.golden files for json presenters")
func must(c pkg.CPE, e error) pkg.CPE {
if e != nil {
panic(e)
}
return c
}
func TestJSONDirectoryPresenter(t *testing.T) {
catalog, metadata, dist := presenterDirectoryInput(t)
assertPresenterAgainstGoldenSnapshot(t,
NewJSONPresenter(catalog, metadata, dist, source.SquashedScope),
*updateJSONGoldenFiles,
)
}
func TestJSONImagePresenter(t *testing.T) {
testImage := "image-simple"
catalog, metadata, dist := presenterImageInput(t, testImage)
assertPresenterAgainstGoldenImageSnapshot(t,
NewJSONPresenter(catalog, metadata, dist, source.SquashedScope),
testImage,
*updateJSONGoldenFiles,
)
}

View File

@ -1,23 +0,0 @@
package packages
import "github.com/anchore/syft/syft/pkg"
type JSONRelationship struct {
Parent string `json:"parent"`
Child string `json:"child"`
Type string `json:"type"`
Metadata interface{} `json:"metadata"`
}
func newJSONRelationships(relationships []pkg.Relationship) []JSONRelationship {
result := make([]JSONRelationship, len(relationships))
for i, r := range relationships {
result[i] = JSONRelationship{
Parent: string(r.Parent),
Child: string(r.Child),
Type: string(r.Type),
Metadata: r.Metadata,
}
}
return result
}

View File

@ -1,39 +0,0 @@
package packages
import (
"fmt"
"github.com/anchore/syft/syft/source"
)
// JSONSource object represents the thing that was cataloged
type JSONSource struct {
Type string `json:"type"`
Target interface{} `json:"target"`
}
type JSONImageSource struct {
source.ImageMetadata
Scope source.Scope `json:"scope"`
}
// NewJSONSource creates a new source object to be represented into JSON.
func NewJSONSource(src source.Metadata, scope source.Scope) (JSONSource, error) {
switch src.Scheme {
case source.ImageScheme:
return JSONSource{
Type: "image",
Target: JSONImageSource{
Scope: scope,
ImageMetadata: src.ImageMetadata,
},
}, nil
case source.DirectoryScheme:
return JSONSource{
Type: "directory",
Target: src.Path,
}, nil
default:
return JSONSource{}, fmt.Errorf("unsupported source: %q", src.Scheme)
}
}

View File

@ -12,7 +12,7 @@ import (
) )
func Test_getSPDXExternalRefs(t *testing.T) { func Test_getSPDXExternalRefs(t *testing.T) {
testCPE := must(pkg.NewCPE("cpe:2.3:a:name:name:3.2:*:*:*:*:*:*:*")) testCPE := pkg.MustCPE("cpe:2.3:a:name:name:3.2:*:*:*:*:*:*:*")
tests := []struct { tests := []struct {
name string name string
input pkg.Package input pkg.Package

View File

@ -4,13 +4,15 @@ import (
"flag" "flag"
"regexp" "regexp"
"testing" "testing"
"github.com/anchore/syft/internal/formats/common/testutils"
) )
var updateSpdxJson = flag.Bool("update-spdx-json", false, "update the *.golden files for spdx-json presenters") var updateSpdxJson = flag.Bool("update-spdx-json", false, "update the *.golden files for spdx-json presenters")
func TestSPDXJSONDirectoryPresenter(t *testing.T) { func TestSPDXJSONDirectoryPresenter(t *testing.T) {
catalog, metadata, _ := presenterDirectoryInput(t) catalog, metadata, _ := testutils.DirectoryInput(t)
assertPresenterAgainstGoldenSnapshot(t, testutils.AssertPresenterAgainstGoldenSnapshot(t,
NewSPDXJSONPresenter(catalog, metadata), NewSPDXJSONPresenter(catalog, metadata),
*updateSpdxJson, *updateSpdxJson,
spdxJsonRedactor, spdxJsonRedactor,
@ -19,8 +21,8 @@ func TestSPDXJSONDirectoryPresenter(t *testing.T) {
func TestSPDXJSONImagePresenter(t *testing.T) { func TestSPDXJSONImagePresenter(t *testing.T) {
testImage := "image-simple" testImage := "image-simple"
catalog, metadata, _ := presenterImageInput(t, testImage) catalog, metadata, _ := testutils.ImageInput(t, testImage)
assertPresenterAgainstGoldenImageSnapshot(t, testutils.AssertPresenterAgainstGoldenImageSnapshot(t,
NewSPDXJSONPresenter(catalog, metadata), NewSPDXJSONPresenter(catalog, metadata),
testImage, testImage,
*updateSpdxJson, *updateSpdxJson,

View File

@ -4,13 +4,15 @@ import (
"flag" "flag"
"regexp" "regexp"
"testing" "testing"
"github.com/anchore/syft/internal/formats/common/testutils"
) )
var updateSpdxTagValue = flag.Bool("update-spdx-tv", false, "update the *.golden files for spdx-tv presenters") var updateSpdxTagValue = flag.Bool("update-spdx-tv", false, "update the *.golden files for spdx-tv presenters")
func TestSPDXTagValueDirectoryPresenter(t *testing.T) { func TestSPDXTagValueDirectoryPresenter(t *testing.T) {
catalog, metadata, _ := presenterDirectoryInput(t) catalog, metadata, _ := testutils.DirectoryInput(t)
assertPresenterAgainstGoldenSnapshot(t, testutils.AssertPresenterAgainstGoldenSnapshot(t,
NewSPDXTagValuePresenter(catalog, metadata), NewSPDXTagValuePresenter(catalog, metadata),
*updateSpdxTagValue, *updateSpdxTagValue,
spdxTagValueRedactor, spdxTagValueRedactor,
@ -19,8 +21,8 @@ func TestSPDXTagValueDirectoryPresenter(t *testing.T) {
func TestSPDXTagValueImagePresenter(t *testing.T) { func TestSPDXTagValueImagePresenter(t *testing.T) {
testImage := "image-simple" testImage := "image-simple"
catalog, metadata, _ := presenterImageInput(t, testImage) catalog, metadata, _ := testutils.ImageInput(t, testImage)
assertPresenterAgainstGoldenImageSnapshot(t, testutils.AssertPresenterAgainstGoldenImageSnapshot(t,
NewSPDXTagValuePresenter(catalog, metadata), NewSPDXTagValuePresenter(catalog, metadata),
testImage, testImage,
*updateSpdxTagValue, *updateSpdxTagValue,

View File

@ -4,6 +4,8 @@ import (
"flag" "flag"
"testing" "testing"
"github.com/anchore/syft/internal/formats/common/testutils"
"github.com/go-test/deep" "github.com/go-test/deep"
) )
@ -11,8 +13,8 @@ var updateTablePresenterGoldenFiles = flag.Bool("update-table", false, "update t
func TestTablePresenter(t *testing.T) { func TestTablePresenter(t *testing.T) {
testImage := "image-simple" testImage := "image-simple"
catalog, _, _ := presenterImageInput(t, testImage) catalog, _, _ := testutils.ImageInput(t, testImage)
assertPresenterAgainstGoldenImageSnapshot(t, testutils.AssertPresenterAgainstGoldenImageSnapshot(t,
NewTablePresenter(catalog), NewTablePresenter(catalog),
testImage, testImage,
*updateTablePresenterGoldenFiles, *updateTablePresenterGoldenFiles,

View File

@ -3,7 +3,7 @@
"name": "/some/path", "name": "/some/path",
"spdxVersion": "SPDX-2.2", "spdxVersion": "SPDX-2.2",
"creationInfo": { "creationInfo": {
"created": "2021-09-16T20:44:35.198887Z", "created": "2021-10-12T18:40:22.948394Z",
"creators": [ "creators": [
"Organization: Anchore, Inc", "Organization: Anchore, Inc",
"Tool: syft-[not provided]" "Tool: syft-[not provided]"
@ -11,7 +11,7 @@
"licenseListVersion": "3.14" "licenseListVersion": "3.14"
}, },
"dataLicense": "CC0-1.0", "dataLicense": "CC0-1.0",
"documentNamespace": "https://anchore.com/syft/image/", "documentNamespace": "https:/anchore.com/syft/dir/some/path-98ae71fb-f276-4c5c-acf7-25770bf7bca2",
"packages": [ "packages": [
{ {
"SPDXID": "SPDXRef-Package-python-package-1-1.0.1", "SPDXID": "SPDXRef-Package-python-package-1-1.0.1",
@ -32,7 +32,7 @@
], ],
"filesAnalyzed": false, "filesAnalyzed": false,
"hasFiles": [ "hasFiles": [
"SPDXRef-File-package-1-04cd22424378dcd6c77fce08beb52493b5494a60ea5e1f9bdf9b16dc0cacffe9" "SPDXRef-File-package-1-efae7fecc76ca25da40f79d7ef5b8933510434914835832c7976f3e866aa756a"
], ],
"licenseDeclared": "MIT", "licenseDeclared": "MIT",
"sourceInfo": "acquired package info from installed python package manifest file: /some/path/pkg1", "sourceInfo": "acquired package info from installed python package manifest file: /some/path/pkg1",
@ -63,17 +63,17 @@
], ],
"files": [ "files": [
{ {
"SPDXID": "SPDXRef-File-package-1-04cd22424378dcd6c77fce08beb52493b5494a60ea5e1f9bdf9b16dc0cacffe9", "SPDXID": "SPDXRef-File-package-1-efae7fecc76ca25da40f79d7ef5b8933510434914835832c7976f3e866aa756a",
"name": "foo", "name": "foo",
"licenseConcluded": "", "licenseConcluded": "",
"fileName": "/some/path/pkg1/depedencies/foo" "fileName": "/some/path/pkg1/dependencies/foo"
} }
], ],
"relationships": [ "relationships": [
{ {
"spdxElementId": "SPDXRef-Package-python-package-1-1.0.1", "spdxElementId": "SPDXRef-Package-python-package-1-1.0.1",
"relationshipType": "CONTAINS", "relationshipType": "CONTAINS",
"relatedSpdxElement": "SPDXRef-File-package-1-04cd22424378dcd6c77fce08beb52493b5494a60ea5e1f9bdf9b16dc0cacffe9" "relatedSpdxElement": "SPDXRef-File-package-1-efae7fecc76ca25da40f79d7ef5b8933510434914835832c7976f3e866aa756a"
} }
] ]
} }

View File

@ -3,7 +3,7 @@
"name": "user-image-input", "name": "user-image-input",
"spdxVersion": "SPDX-2.2", "spdxVersion": "SPDX-2.2",
"creationInfo": { "creationInfo": {
"created": "2021-09-16T20:44:35.203911Z", "created": "2021-10-12T18:40:22.953633Z",
"creators": [ "creators": [
"Organization: Anchore, Inc", "Organization: Anchore, Inc",
"Tool: syft-[not provided]" "Tool: syft-[not provided]"
@ -11,7 +11,7 @@
"licenseListVersion": "3.14" "licenseListVersion": "3.14"
}, },
"dataLicense": "CC0-1.0", "dataLicense": "CC0-1.0",
"documentNamespace": "https://anchore.com/syft/image/user-image-input", "documentNamespace": "https:/anchore.com/syft/image/user-image-input-149edbad-3c01-4ee0-b3a0-75232312bf51",
"packages": [ "packages": [
{ {
"SPDXID": "SPDXRef-Package-python-package-1-1.0.1", "SPDXID": "SPDXRef-Package-python-package-1-1.0.1",

View File

@ -1,11 +1,11 @@
[Image] [Image]
Layer: 0 Layer: 0
Digest: sha256:ffb5e9eaa453a002110719d12c294960117ca2903953d1faa40f01dc3f77045c Digest: sha256:fb6beecb75b39f4bb813dbf177e501edd5ddb3e69bb45cedeb78c676ee1b7a59
Size: 22 Size: 22
MediaType: application/vnd.docker.image.rootfs.diff.tar.gzip MediaType: application/vnd.docker.image.rootfs.diff.tar.gzip
Layer: 1 Layer: 1
Digest: sha256:8463854829fc53d47b9dcdf7ee79fe7eb4ca7933c910f67f8521412f7a2f5c21 Digest: sha256:319b588ce64253a87b533c8ed01cf0025e0eac98e7b516e12532957e1244fdec
Size: 16 Size: 16
MediaType: application/vnd.docker.image.rootfs.diff.tar.gzip MediaType: application/vnd.docker.image.rootfs.diff.tar.gzip

View File

@ -3,13 +3,15 @@ package packages
import ( import (
"flag" "flag"
"testing" "testing"
"github.com/anchore/syft/internal/formats/common/testutils"
) )
var updateTextPresenterGoldenFiles = flag.Bool("update-text", false, "update the *.golden files for text presenters") var updateTextPresenterGoldenFiles = flag.Bool("update-text", false, "update the *.golden files for text presenters")
func TestTextDirectoryPresenter(t *testing.T) { func TestTextDirectoryPresenter(t *testing.T) {
catalog, metadata, _ := presenterDirectoryInput(t) catalog, metadata, _ := testutils.DirectoryInput(t)
assertPresenterAgainstGoldenSnapshot(t, testutils.AssertPresenterAgainstGoldenSnapshot(t,
NewTextPresenter(catalog, metadata), NewTextPresenter(catalog, metadata),
*updateTextPresenterGoldenFiles, *updateTextPresenterGoldenFiles,
) )
@ -17,8 +19,8 @@ func TestTextDirectoryPresenter(t *testing.T) {
func TestTextImagePresenter(t *testing.T) { func TestTextImagePresenter(t *testing.T) {
testImage := "image-simple" testImage := "image-simple"
catalog, metadata, _ := presenterImageInput(t, testImage) catalog, metadata, _ := testutils.ImageInput(t, testImage)
assertPresenterAgainstGoldenImageSnapshot(t, testutils.AssertPresenterAgainstGoldenImageSnapshot(t,
NewTextPresenter(catalog, metadata), NewTextPresenter(catalog, metadata),
testImage, testImage,
*updateTextPresenterGoldenFiles, *updateTextPresenterGoldenFiles,

View File

@ -1,7 +1,8 @@
package poweruser package poweruser
import ( import (
"github.com/anchore/syft/internal/presenter/packages" "github.com/anchore/syft/internal/formats/syftjson"
"github.com/anchore/syft/internal/formats/syftjson/model"
) )
type JSONDocument struct { type JSONDocument struct {
@ -13,16 +14,11 @@ type JSONDocument struct {
FileContents []JSONFileContents `json:"fileContents,omitempty"` // note: must have omitempty FileContents []JSONFileContents `json:"fileContents,omitempty"` // note: must have omitempty
FileMetadata []JSONFileMetadata `json:"fileMetadata,omitempty"` // note: must have omitempty FileMetadata []JSONFileMetadata `json:"fileMetadata,omitempty"` // note: must have omitempty
Secrets []JSONSecrets `json:"secrets,omitempty"` // note: must have omitempty Secrets []JSONSecrets `json:"secrets,omitempty"` // note: must have omitempty
packages.JSONDocument model.Document
} }
// NewJSONDocument creates and populates a new JSON document struct from the given cataloging results. // NewJSONDocument creates and populates a new JSON document struct from the given cataloging results.
func NewJSONDocument(config JSONDocumentConfig) (JSONDocument, error) { func NewJSONDocument(config JSONDocumentConfig) (JSONDocument, error) {
pkgsDoc, err := packages.NewJSONDocument(config.PackageCatalog, config.SourceMetadata, config.Distro, config.ApplicationConfig.Package.Cataloger.ScopeOpt, config.ApplicationConfig)
if err != nil {
return JSONDocument{}, err
}
fileMetadata, err := NewJSONFileMetadata(config.FileMetadata, config.FileDigests) fileMetadata, err := NewJSONFileMetadata(config.FileMetadata, config.FileDigests)
if err != nil { if err != nil {
return JSONDocument{}, err return JSONDocument{}, err
@ -33,6 +29,6 @@ func NewJSONDocument(config JSONDocumentConfig) (JSONDocument, error) {
FileContents: NewJSONFileContents(config.FileContents), FileContents: NewJSONFileContents(config.FileContents),
FileMetadata: fileMetadata, FileMetadata: fileMetadata,
Secrets: NewJSONSecrets(config.Secrets), Secrets: NewJSONSecrets(config.Secrets),
JSONDocument: pkgsDoc, Document: syftjson.ToFormatModel(config.PackageCatalog, &config.SourceMetadata, config.Distro, config.ApplicationConfig.Package.Cataloger.ScopeOpt, config.ApplicationConfig),
}, nil }, nil
} }

48
syft/encode_decode.go Normal file
View File

@ -0,0 +1,48 @@
package syft
import (
"bytes"
"fmt"
"io"
"github.com/anchore/syft/internal/formats"
"github.com/anchore/syft/syft/distro"
"github.com/anchore/syft/syft/format"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/source"
)
// Encode takes all SBOM elements and a format option and encodes an SBOM document.
// TODO: encapsulate input data into common sbom document object
func Encode(catalog *pkg.Catalog, metadata *source.Metadata, dist *distro.Distro, scope source.Scope, option format.Option) ([]byte, error) {
f := formats.ByOption(option)
if f == nil {
return nil, fmt.Errorf("unsupported format: %+v", option)
}
buff := bytes.Buffer{}
if err := f.Encode(&buff, catalog, dist, metadata, scope); err != nil {
return nil, fmt.Errorf("unable to encode sbom: %w", err)
}
return buff.Bytes(), nil
}
// Decode takes a reader for an SBOM and generates all internal SBOM elements.
// TODO: encapsulate return data into common sbom document object
func Decode(reader io.Reader) (*pkg.Catalog, *source.Metadata, *distro.Distro, source.Scope, format.Option, error) {
by, err := io.ReadAll(reader)
if err != nil {
return nil, nil, nil, source.UnknownScope, format.UnknownFormatOption, fmt.Errorf("unable to read sbom: %w", err)
}
f, err := formats.Identify(by)
if err != nil {
return nil, nil, nil, source.UnknownScope, format.UnknownFormatOption, fmt.Errorf("unable to detect format: %w", err)
}
if f == nil {
return nil, nil, nil, source.UnknownScope, format.UnknownFormatOption, fmt.Errorf("unable to identify format")
}
c, m, d, s, err := f.Decode(bytes.NewReader(by))
return c, m, d, s, f.Option, err
}

View File

@ -0,0 +1,53 @@
package syft
import (
"bytes"
"testing"
"github.com/go-test/deep"
"github.com/anchore/syft/syft/format"
"github.com/anchore/syft/syft/source"
"github.com/stretchr/testify/assert"
)
// TestEncodeDecodeEncodeCycleComparison is testing for differences in how SBOM documents get encoded on multiple cycles.
// By encding and decoding the sbom we can compare the differences between the set of resulting objects. However,
// this requires specific comparisons being done, and select redactions/omissions being made. Additionally, there are
// already unit tests on each format encoder-decoder for properly functioning comparisons in depth, so there is no need
// to do an object-to-object comparison. For this reason this test focuses on a bytes-to-bytes comparison after an
// encode-decode-encode loop which will detect lossy behavior in both directions.
func TestEncodeDecodeEncodeCycleComparison(t *testing.T) {
testImage := "image-simple"
tests := []struct {
format format.Option
}{
{
format: format.JSONOption,
},
}
for _, test := range tests {
t.Run(testImage, func(t *testing.T) {
src, err := source.NewFromDirectory("./test-fixtures/pkgs")
if err != nil {
t.Fatalf("cant get dir")
}
originalCatalog, d, err := CatalogPackages(&src, source.SquashedScope)
by1, err := Encode(originalCatalog, &src.Metadata, d, source.SquashedScope, test.format)
assert.NoError(t, err)
newCatalog, newMetadata, newDistro, newScope, newFormat, err := Decode(bytes.NewReader(by1))
assert.NoError(t, err)
assert.Equal(t, test.format, newFormat)
by2, err := Encode(newCatalog, newMetadata, newDistro, newScope, test.format)
assert.NoError(t, err)
for _, diff := range deep.Equal(by1, by2) {
t.Errorf(diff)
}
assert.True(t, bytes.Equal(by1, by2))
})
}
}

13
syft/format/decoder.go Normal file
View File

@ -0,0 +1,13 @@
package format
import (
"io"
"github.com/anchore/syft/syft/distro"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/source"
)
// Decoder is a function that can convert an SBOM document of a specific format from a reader into Syft native objects.
type Decoder func(reader io.Reader) (*pkg.Catalog, *source.Metadata, *distro.Distro, source.Scope, error)

12
syft/format/encoder.go Normal file
View File

@ -0,0 +1,12 @@
package format
import (
"io"
"github.com/anchore/syft/syft/distro"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/source"
)
// Encoder is a function that can transform Syft native objects into an SBOM document of a specific format written to the given writer.
type Encoder func(io.Writer, *pkg.Catalog, *source.Metadata, *distro.Distro, source.Scope) error

62
syft/format/format.go Normal file
View File

@ -0,0 +1,62 @@
package format
import (
"errors"
"io"
"github.com/anchore/syft/syft/distro"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/source"
)
var (
ErrEncodingNotSupported = errors.New("encoding not supported")
ErrDecodingNotSupported = errors.New("decoding not supported")
ErrValidationNotSupported = errors.New("validation not supported")
)
type Format struct {
Option Option
encoder Encoder
decoder Decoder
validator Validator
}
func NewFormat(option Option, encoder Encoder, decoder Decoder, validator Validator) Format {
return Format{
Option: option,
encoder: encoder,
decoder: decoder,
validator: validator,
}
}
func (f Format) Encode(output io.Writer, catalog *pkg.Catalog, d *distro.Distro, metadata *source.Metadata, scope source.Scope) error {
if f.encoder == nil {
return ErrEncodingNotSupported
}
return f.encoder(output, catalog, metadata, d, scope)
}
func (f Format) Decode(reader io.Reader) (*pkg.Catalog, *source.Metadata, *distro.Distro, source.Scope, error) {
if f.decoder == nil {
return nil, nil, nil, source.UnknownScope, ErrDecodingNotSupported
}
return f.decoder(reader)
}
func (f Format) Validate(reader io.Reader) error {
if f.validator == nil {
return ErrValidationNotSupported
}
return f.validator(reader)
}
func (f Format) Presenter(catalog *pkg.Catalog, metadata *source.Metadata, d *distro.Distro, scope source.Scope) *Presenter {
if f.encoder == nil {
return nil
}
return NewPresenter(f.encoder, catalog, metadata, d, scope)
}

43
syft/format/option.go Normal file
View File

@ -0,0 +1,43 @@
package format
import "strings"
const (
UnknownFormatOption Option = "UnknownFormatOption"
JSONOption Option = "json"
TextOption Option = "text"
TableOption Option = "table"
CycloneDxOption Option = "cyclonedx"
SPDXTagValueOption Option = "spdx-tag-value"
SPDXJSONOption Option = "spdx-json"
)
var AllPresenters = []Option{
JSONOption,
TextOption,
TableOption,
CycloneDxOption,
SPDXTagValueOption,
SPDXJSONOption,
}
type Option string
func ParseOption(userStr string) Option {
switch strings.ToLower(userStr) {
case string(JSONOption):
return JSONOption
case string(TextOption):
return TextOption
case string(TableOption):
return TableOption
case string(CycloneDxOption), "cyclone", "cyclone-dx":
return CycloneDxOption
case string(SPDXTagValueOption), "spdx", "spdx-tagvalue", "spdxtagvalue", "spdx-tv", "spdxtv":
return SPDXTagValueOption
case string(SPDXJSONOption), "spdxjson":
return SPDXJSONOption
default:
return UnknownFormatOption
}
}

32
syft/format/presenter.go Normal file
View File

@ -0,0 +1,32 @@
package format
import (
"io"
"github.com/anchore/syft/syft/distro"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/source"
)
type Presenter struct {
catalog *pkg.Catalog
srcMetadata *source.Metadata
distro *distro.Distro
scope source.Scope
encoder Encoder
}
func NewPresenter(encoder Encoder, catalog *pkg.Catalog, srcMetadata *source.Metadata, d *distro.Distro, scope source.Scope) *Presenter {
return &Presenter{
catalog: catalog,
srcMetadata: srcMetadata,
distro: d,
encoder: encoder,
scope: scope,
}
}
func (pres *Presenter) Present(output io.Writer) error {
return pres.encoder(output, pres.catalog, pres.srcMetadata, pres.distro, pres.scope)
}

12
syft/format/validator.go Normal file
View File

@ -0,0 +1,12 @@
package format
import "io"
// Validator reads the SBOM from the given reader and assesses whether the document conforms to the specific SBOM format.
// The validator should positively confirm if the SBOM is not only the format but also has the minimal set of values
// that the format requires. For example, all syftjson formatted documents have a schema section which should have
// "anchore/syft" within the version --if this isn't found then the validator should raise an error. These active
// assertions protect against "simple" format decoding validations that may lead to false positives (e.g. I decoded
// json successfully therefore this must be the target format, however, all values are their default zero-value and
// really represent a different format that also uses json)
type Validator func(reader io.Reader) error

View File

@ -35,6 +35,14 @@ func NewCPE(cpeStr string) (CPE, error) {
return *value, nil return *value, nil
} }
func MustCPE(cpeStr string) CPE {
c, err := NewCPE(cpeStr)
if err != nil {
panic(err)
}
return c
}
func normalizeCpeField(field string) string { func normalizeCpeField(field string) string {
// keep dashes and forward slashes unescaped // keep dashes and forward slashes unescaped
return strings.ReplaceAll(wfn.StripSlashes(field), `\/`, "/") return strings.ReplaceAll(wfn.StripSlashes(field), `\/`, "/")

View File

@ -5,26 +5,31 @@ a specific Presenter implementation given user configuration.
package packages package packages
import ( import (
"github.com/anchore/syft/internal/formats"
"github.com/anchore/syft/internal/presenter/packages" "github.com/anchore/syft/internal/presenter/packages"
"github.com/anchore/syft/syft/format"
"github.com/anchore/syft/syft/presenter" "github.com/anchore/syft/syft/presenter"
) )
// Presenter returns a presenter for images or directories // Presenter returns a presenter for images or directories
func Presenter(option PresenterOption, config PresenterConfig) presenter.Presenter { func Presenter(option format.Option, config PresenterConfig) presenter.Presenter {
switch option { switch option {
case JSONPresenterOption: case format.TextOption:
return packages.NewJSONPresenter(config.Catalog, config.SourceMetadata, config.Distro, config.Scope)
case TextPresenterOption:
return packages.NewTextPresenter(config.Catalog, config.SourceMetadata) return packages.NewTextPresenter(config.Catalog, config.SourceMetadata)
case TablePresenterOption: case format.TableOption:
return packages.NewTablePresenter(config.Catalog) return packages.NewTablePresenter(config.Catalog)
case CycloneDxPresenterOption: case format.CycloneDxOption:
return packages.NewCycloneDxPresenter(config.Catalog, config.SourceMetadata) return packages.NewCycloneDxPresenter(config.Catalog, config.SourceMetadata)
case SPDXTagValuePresenterOption: case format.SPDXTagValueOption:
return packages.NewSPDXTagValuePresenter(config.Catalog, config.SourceMetadata) return packages.NewSPDXTagValuePresenter(config.Catalog, config.SourceMetadata)
case SPDXJSONPresenterOption: case format.SPDXJSONOption:
return packages.NewSPDXJSONPresenter(config.Catalog, config.SourceMetadata) return packages.NewSPDXJSONPresenter(config.Catalog, config.SourceMetadata)
default: default:
// TODO: the final state is that all other cases would be replaced by formats.ByOption (wed remove this function entirely)
f := formats.ByOption(option)
if f == nil {
return nil return nil
} }
return f.Presenter(config.Catalog, &config.SourceMetadata, config.Distro, config.Scope)
}
} }

View File

@ -1,43 +0,0 @@
package packages
import "strings"
const (
UnknownPresenterOption PresenterOption = "UnknownPresenterOption"
JSONPresenterOption PresenterOption = "json"
TextPresenterOption PresenterOption = "text"
TablePresenterOption PresenterOption = "table"
CycloneDxPresenterOption PresenterOption = "cyclonedx"
SPDXTagValuePresenterOption PresenterOption = "spdx-tag-value"
SPDXJSONPresenterOption PresenterOption = "spdx-json"
)
var AllPresenters = []PresenterOption{
JSONPresenterOption,
TextPresenterOption,
TablePresenterOption,
CycloneDxPresenterOption,
SPDXTagValuePresenterOption,
SPDXJSONPresenterOption,
}
type PresenterOption string
func ParsePresenterOption(userStr string) PresenterOption {
switch strings.ToLower(userStr) {
case string(JSONPresenterOption):
return JSONPresenterOption
case string(TextPresenterOption):
return TextPresenterOption
case string(TablePresenterOption):
return TablePresenterOption
case string(CycloneDxPresenterOption), "cyclone", "cyclone-dx":
return CycloneDxPresenterOption
case string(SPDXTagValuePresenterOption), "spdx", "spdx-tagvalue", "spdxtagvalue", "spdx-tv":
return SPDXTagValuePresenterOption
case string(SPDXJSONPresenterOption), "spdxjson":
return SPDXJSONPresenterOption
default:
return UnknownPresenterOption
}
}

View File

@ -0,0 +1,52 @@
{
"name": "npm-lock",
"version": "1.0.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
"collapse-white-space": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/collapse-white-space/-/collapse-white-space-2.0.0.tgz",
"integrity": "sha512-eh9krktAIMDL0KHuN7WTBJ/0PMv8KUvfQRBkIlGmW61idRM2DJjgd1qXEPr4wyk2PimZZeNww3RVYo6CMvDGlg=="
},
"end-of-stream": {
"version": "1.4.4",
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz",
"integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==",
"dev": true,
"requires": {
"once": "^1.4.0"
}
},
"insert-css": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/insert-css/-/insert-css-2.0.0.tgz",
"integrity": "sha1-610Ql7dUL0x56jBg067gfQU4gPQ="
},
"once": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
"integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
"dev": true,
"requires": {
"wrappy": "1"
}
},
"pump": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz",
"integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==",
"dev": true,
"requires": {
"end-of-stream": "^1.1.0",
"once": "^1.3.1"
}
},
"wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=",
"dev": true
}
}
}

View File

@ -3,10 +3,11 @@ package integration
import ( import (
"bytes" "bytes"
"encoding/json" "encoding/json"
exportedPackages "github.com/anchore/syft/syft/presenter/packages"
"testing" "testing"
internalPackages "github.com/anchore/syft/internal/presenter/packages" syftjsonModel "github.com/anchore/syft/internal/formats/syftjson/model"
"github.com/anchore/syft/syft/format"
exportedPackages "github.com/anchore/syft/syft/presenter/packages"
) )
func TestPackageOwnershipRelationships(t *testing.T) { func TestPackageOwnershipRelationships(t *testing.T) {
@ -24,7 +25,7 @@ func TestPackageOwnershipRelationships(t *testing.T) {
t.Run(test.fixture, func(t *testing.T) { t.Run(test.fixture, func(t *testing.T) {
catalog, d, src := catalogFixtureImage(t, test.fixture) catalog, d, src := catalogFixtureImage(t, test.fixture)
p := exportedPackages.Presenter(exportedPackages.JSONPresenterOption, exportedPackages.PresenterConfig{ p := exportedPackages.Presenter(format.JSONOption, exportedPackages.PresenterConfig{
SourceMetadata: src.Metadata, SourceMetadata: src.Metadata,
Catalog: catalog, Catalog: catalog,
Distro: d, Distro: d,
@ -39,7 +40,7 @@ func TestPackageOwnershipRelationships(t *testing.T) {
t.Fatalf("unable to present: %+v", err) t.Fatalf("unable to present: %+v", err)
} }
var doc internalPackages.JSONDocument var doc syftjsonModel.Document
decoder := json.NewDecoder(output) decoder := json.NewDecoder(output)
if err := decoder.Decode(&doc); err != nil { if err := decoder.Decode(&doc); err != nil {
t.Fatalf("unable to decode json doc: %+v", err) t.Fatalf("unable to decode json doc: %+v", err)