mirror of
https://github.com/anchore/syft.git
synced 2026-11-01 23:16:30 +01:00
Split the sbom.Format interface by encode and decode use cases (#2186)
* split up sbom.Format into encode and decode ops Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * update cmd pkg to inject format configs Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * bump cyclonedx schema to 1.5 Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * redact image metadata from github encoder tests Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * add more testing around format decoder identify Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * add test case for format version options Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * fix cli tests Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * fix CLI test Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * [wip] - review comments Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * keep encoder creation out of post load function Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * keep decider and identify functions Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * add a few more doc comments Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * remove format encoder default function helpers Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * address PR feedback Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * move back to streaming based decode functions Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * with common convention for encoder constructors Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * fix tests and allow for encoders to be created from cli options Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * fix cli tests Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * fix linting Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * buffer reads from stdin to support seeking Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> --------- Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
This commit is contained in:
@@ -1,18 +0,0 @@
|
||||
package syft
|
||||
|
||||
import (
|
||||
"io"
|
||||
|
||||
"github.com/anchore/syft/syft/formats"
|
||||
"github.com/anchore/syft/syft/sbom"
|
||||
)
|
||||
|
||||
// TODO: deprecated, moved to syft/formats/formats.go. will be removed in v1.0.0
|
||||
func Encode(s sbom.SBOM, f sbom.Format) ([]byte, error) {
|
||||
return formats.Encode(s, f)
|
||||
}
|
||||
|
||||
// TODO: deprecated, moved to syft/formats/formats.go. will be removed in v1.0.0
|
||||
func Decode(reader io.Reader) (*sbom.SBOM, sbom.Format, error) {
|
||||
return formats.Decode(reader)
|
||||
}
|
||||
+1
-1
@@ -7,7 +7,7 @@ import (
|
||||
|
||||
"github.com/anchore/packageurl-go"
|
||||
"github.com/anchore/syft/syft/file"
|
||||
"github.com/anchore/syft/syft/formats/common"
|
||||
"github.com/anchore/syft/syft/format/common"
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
)
|
||||
|
||||
+1
-39
@@ -2,56 +2,18 @@ package cyclonedxhelpers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"github.com/CycloneDX/cyclonedx-go"
|
||||
|
||||
"github.com/anchore/packageurl-go"
|
||||
"github.com/anchore/syft/syft/artifact"
|
||||
"github.com/anchore/syft/syft/formats/common"
|
||||
"github.com/anchore/syft/syft/format/common"
|
||||
"github.com/anchore/syft/syft/linux"
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
"github.com/anchore/syft/syft/sbom"
|
||||
"github.com/anchore/syft/syft/source"
|
||||
)
|
||||
|
||||
const cycloneDXXmlSchema = "http://cyclonedx.org/schema/bom"
|
||||
|
||||
func GetValidator(format cyclonedx.BOMFileFormat) sbom.Validator {
|
||||
return func(reader io.Reader) error {
|
||||
bom := &cyclonedx.BOM{}
|
||||
err := cyclonedx.NewBOMDecoder(reader, format).Decode(bom)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
xmlWithoutNS := format == cyclonedx.BOMFileFormatXML && !strings.Contains(bom.XMLNS, cycloneDXXmlSchema)
|
||||
xmlWithoutComponents := format == cyclonedx.BOMFileFormatXML && bom.Components == nil
|
||||
if (cyclonedx.BOM{} == *bom || xmlWithoutComponents || xmlWithoutNS) {
|
||||
return fmt.Errorf("not a valid CycloneDX document")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func GetDecoder(format cyclonedx.BOMFileFormat) sbom.Decoder {
|
||||
return func(reader io.Reader) (*sbom.SBOM, error) {
|
||||
bom := &cyclonedx.BOM{
|
||||
Components: &[]cyclonedx.Component{},
|
||||
}
|
||||
err := cyclonedx.NewBOMDecoder(reader, format).Decode(bom)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
s, err := ToSyftModel(bom)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
}
|
||||
|
||||
func ToSyftModel(bom *cyclonedx.BOM) (*sbom.SBOM, error) {
|
||||
if bom == nil {
|
||||
return nil, fmt.Errorf("no content defined in CycloneDX BOM")
|
||||
-14
@@ -1,8 +1,6 @@
|
||||
package cyclonedxhelpers
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
@@ -327,18 +325,6 @@ func Test_missingDataDecode(t *testing.T) {
|
||||
assert.Equal(t, pkg.Licenses.Empty(), true)
|
||||
}
|
||||
|
||||
func Test_missingComponentsDecode(t *testing.T) {
|
||||
bom := &cyclonedx.BOM{
|
||||
SpecVersion: cyclonedx.SpecVersion1_4,
|
||||
}
|
||||
bomBytes, _ := json.Marshal(&bom)
|
||||
decode := GetDecoder(cyclonedx.BOMFileFormatJSON)
|
||||
|
||||
_, err := decode(bytes.NewReader(bomBytes))
|
||||
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func Test_decodeDependencies(t *testing.T) {
|
||||
c1 := cyclonedx.Component{
|
||||
Name: "c1",
|
||||
+1
-1
@@ -3,7 +3,7 @@ package cyclonedxhelpers
|
||||
import (
|
||||
"github.com/CycloneDX/cyclonedx-go"
|
||||
|
||||
"github.com/anchore/syft/syft/formats/common"
|
||||
"github.com/anchore/syft/syft/format/common"
|
||||
)
|
||||
|
||||
var (
|
||||
+1
-1
@@ -19,7 +19,7 @@ import (
|
||||
"github.com/anchore/syft/internal/spdxlicense"
|
||||
"github.com/anchore/syft/syft/artifact"
|
||||
"github.com/anchore/syft/syft/file"
|
||||
"github.com/anchore/syft/syft/formats/common/util"
|
||||
"github.com/anchore/syft/syft/format/common/util"
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
"github.com/anchore/syft/syft/sbom"
|
||||
"github.com/anchore/syft/syft/source"
|
||||
+1
-1
@@ -18,7 +18,7 @@ import (
|
||||
"github.com/anchore/syft/syft/artifact"
|
||||
"github.com/anchore/syft/syft/cpe"
|
||||
"github.com/anchore/syft/syft/file"
|
||||
"github.com/anchore/syft/syft/formats/common/util"
|
||||
"github.com/anchore/syft/syft/format/common/util"
|
||||
"github.com/anchore/syft/syft/license"
|
||||
"github.com/anchore/syft/syft/linux"
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
@@ -0,0 +1,131 @@
|
||||
package cyclonedxjson
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"github.com/CycloneDX/cyclonedx-go"
|
||||
|
||||
"github.com/anchore/syft/internal/log"
|
||||
"github.com/anchore/syft/syft/format/common/cyclonedxhelpers"
|
||||
"github.com/anchore/syft/syft/format/internal/cyclonedxutil"
|
||||
"github.com/anchore/syft/syft/sbom"
|
||||
)
|
||||
|
||||
var _ sbom.FormatDecoder = (*decoder)(nil)
|
||||
|
||||
type decoder struct {
|
||||
decoder cyclonedxutil.Decoder
|
||||
}
|
||||
|
||||
func NewFormatDecoder() sbom.FormatDecoder {
|
||||
return decoder{
|
||||
decoder: cyclonedxutil.NewDecoder(cyclonedx.BOMFileFormatJSON),
|
||||
}
|
||||
}
|
||||
|
||||
func (d decoder) Decode(reader io.ReadSeeker) (*sbom.SBOM, sbom.FormatID, string, error) {
|
||||
if reader == nil {
|
||||
return nil, "", "", fmt.Errorf("no SBOM bytes provided")
|
||||
}
|
||||
id, version := d.Identify(reader)
|
||||
if id != ID {
|
||||
return nil, "", "", fmt.Errorf("not a cyclonedx json document")
|
||||
}
|
||||
if version == "" {
|
||||
return nil, "", "", fmt.Errorf("unsupported cyclonedx json document version")
|
||||
}
|
||||
|
||||
doc, err := d.decoder.Decode(reader)
|
||||
if err != nil {
|
||||
return nil, id, version, fmt.Errorf("unable to decode cyclonedx json document: %w", err)
|
||||
}
|
||||
|
||||
s, err := cyclonedxhelpers.ToSyftModel(doc)
|
||||
if err != nil {
|
||||
return nil, id, version, err
|
||||
}
|
||||
|
||||
return s, id, version, nil
|
||||
}
|
||||
|
||||
func (d decoder) Identify(reader io.ReadSeeker) (sbom.FormatID, string) {
|
||||
if reader == nil {
|
||||
return "", ""
|
||||
}
|
||||
if _, err := reader.Seek(0, io.SeekStart); err != nil {
|
||||
log.Debugf("unable to seek to start of CycloneDX JSON SBOM: %+v", err)
|
||||
return "", ""
|
||||
}
|
||||
|
||||
type Document struct {
|
||||
JSONSchema string `json:"$schema"`
|
||||
BOMFormat string `json:"bomFormat"`
|
||||
SpecVersion string `json:"specVersion"`
|
||||
}
|
||||
|
||||
dec := json.NewDecoder(reader)
|
||||
|
||||
var doc Document
|
||||
err := dec.Decode(&doc)
|
||||
if err != nil {
|
||||
// maybe not json? maybe not valid? doesn't matter, we won't process it.
|
||||
return "", ""
|
||||
}
|
||||
|
||||
id, version := getFormatInfo(doc.JSONSchema, doc.BOMFormat, doc.SpecVersion)
|
||||
if version == "" || id != ID {
|
||||
// not a cyclonedx json document that we support
|
||||
return "", ""
|
||||
}
|
||||
|
||||
return id, version
|
||||
}
|
||||
|
||||
func getFormatInfo(schemaURI, bomFormat string, specVersion any) (sbom.FormatID, string) {
|
||||
if !strings.Contains(schemaURI, "cyclonedx.org/schema/bom") {
|
||||
// not a cyclonedx json document
|
||||
return "", ""
|
||||
}
|
||||
|
||||
if bomFormat != "CycloneDX" {
|
||||
// not a cyclonedx json document
|
||||
return "", ""
|
||||
}
|
||||
|
||||
// by this point this looks to be valid cyclonedx json, but we need to know the version
|
||||
|
||||
var (
|
||||
version string
|
||||
spec cyclonedx.SpecVersion
|
||||
err error
|
||||
)
|
||||
switch s := specVersion.(type) {
|
||||
case string:
|
||||
version = s
|
||||
spec, err = cyclonedxutil.SpecVersionFromString(version)
|
||||
if err != nil {
|
||||
// not a supported version, but is cyclonedx json
|
||||
return ID, ""
|
||||
}
|
||||
case cyclonedx.SpecVersion:
|
||||
spec = s
|
||||
version = cyclonedxutil.VersionFromSpecVersion(spec)
|
||||
if version == "" {
|
||||
// not a supported version, but is cyclonedx json
|
||||
return ID, ""
|
||||
}
|
||||
default:
|
||||
// bad input provided for version info
|
||||
return ID, ""
|
||||
}
|
||||
|
||||
if spec < 0 {
|
||||
// not a supported version, but is cyclonedx json
|
||||
return ID, ""
|
||||
}
|
||||
|
||||
return ID, version
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
package cyclonedxjson
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/anchore/syft/syft/sbom"
|
||||
)
|
||||
|
||||
func TestDecoder_Decode(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
file string
|
||||
err bool
|
||||
distro string
|
||||
packages []string
|
||||
}{
|
||||
{
|
||||
name: "dir-scan",
|
||||
file: "snapshot/TestCycloneDxDirectoryEncoder.golden",
|
||||
distro: "debian:1.2.3",
|
||||
packages: []string{"package-1:1.0.1", "package-2:2.0.1"},
|
||||
},
|
||||
{
|
||||
name: "image-scan",
|
||||
file: "snapshot/TestCycloneDxImageEncoder.golden",
|
||||
distro: "debian:1.2.3",
|
||||
packages: []string{"package-1:1.0.1", "package-2:2.0.1"},
|
||||
},
|
||||
{
|
||||
name: "not-an-sbom",
|
||||
file: "bad-sbom",
|
||||
err: true,
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.file, func(t *testing.T) {
|
||||
reader, err := os.Open(filepath.Join("test-fixtures", test.file))
|
||||
require.NoError(t, err)
|
||||
|
||||
dec := NewFormatDecoder()
|
||||
|
||||
formatID, formatVersion := dec.Identify(reader)
|
||||
if test.err {
|
||||
assert.Equal(t, sbom.FormatID(""), formatID)
|
||||
assert.Equal(t, "", formatVersion)
|
||||
|
||||
_, decodeID, decodeVersion, err := dec.Decode(reader)
|
||||
require.Error(t, err)
|
||||
assert.Equal(t, sbom.FormatID(""), decodeID)
|
||||
assert.Equal(t, "", decodeVersion)
|
||||
|
||||
return
|
||||
}
|
||||
assert.Equal(t, ID, formatID)
|
||||
assert.NotEmpty(t, formatVersion)
|
||||
|
||||
bom, decodeID, decodeVersion, err := dec.Decode(reader)
|
||||
require.NotNil(t, bom)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, ID, decodeID)
|
||||
assert.Equal(t, formatVersion, decodeVersion)
|
||||
|
||||
split := strings.SplitN(test.distro, ":", 2)
|
||||
distroName := split[0]
|
||||
distroVersion := split[1]
|
||||
assert.Equal(t, bom.Artifacts.LinuxDistribution.ID, distroName)
|
||||
assert.Equal(t, bom.Artifacts.LinuxDistribution.Version, distroVersion)
|
||||
|
||||
var pkgs []string
|
||||
for p := range bom.Artifacts.Packages.Enumerate() {
|
||||
pkgs = append(pkgs, fmt.Sprintf("%s:%s", p.Name, p.Version))
|
||||
}
|
||||
|
||||
assert.ElementsMatch(t, test.packages, pkgs)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecoder_Identify(t *testing.T) {
|
||||
type testCase struct {
|
||||
name string
|
||||
file string
|
||||
id sbom.FormatID
|
||||
version string
|
||||
}
|
||||
|
||||
var cases []testCase
|
||||
|
||||
for _, version := range SupportedVersions() {
|
||||
cases = append(cases, testCase{
|
||||
name: fmt.Sprintf("v%s schema", version),
|
||||
file: fmt.Sprintf("test-fixtures/identify/%s.json", version),
|
||||
id: ID,
|
||||
version: version,
|
||||
})
|
||||
}
|
||||
|
||||
for _, test := range cases {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
reader, err := os.Open(test.file)
|
||||
require.NoError(t, err)
|
||||
|
||||
dec := NewFormatDecoder()
|
||||
|
||||
formatID, formatVersion := dec.Identify(reader)
|
||||
assert.Equal(t, test.id, formatID)
|
||||
assert.Equal(t, test.version, formatVersion)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package cyclonedxjson
|
||||
|
||||
import (
|
||||
"github.com/CycloneDX/cyclonedx-go"
|
||||
|
||||
"github.com/anchore/syft/syft/format/internal/cyclonedxutil"
|
||||
"github.com/anchore/syft/syft/sbom"
|
||||
)
|
||||
|
||||
const ID = cyclonedxutil.JSONFormatID
|
||||
|
||||
func SupportedVersions() []string {
|
||||
return cyclonedxutil.SupportedVersions(ID)
|
||||
}
|
||||
|
||||
type EncoderConfig struct {
|
||||
Version string
|
||||
}
|
||||
|
||||
type encoder struct {
|
||||
cfg EncoderConfig
|
||||
cyclonedxutil.Encoder
|
||||
}
|
||||
|
||||
func NewFormatEncoderWithConfig(cfg EncoderConfig) (sbom.FormatEncoder, error) {
|
||||
enc, err := cyclonedxutil.NewEncoder(cfg.Version, cyclonedx.BOMFileFormatJSON)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return encoder{
|
||||
cfg: cfg,
|
||||
Encoder: enc,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func DefaultEncoderConfig() EncoderConfig {
|
||||
return EncoderConfig{
|
||||
Version: cyclonedxutil.DefaultVersion,
|
||||
}
|
||||
}
|
||||
|
||||
func (e encoder) ID() sbom.FormatID {
|
||||
return ID
|
||||
}
|
||||
|
||||
func (e encoder) Aliases() []string {
|
||||
return []string{}
|
||||
}
|
||||
|
||||
func (e encoder) Version() string {
|
||||
return e.cfg.Version
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
package cyclonedxjson
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"flag"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/anchore/syft/syft/format/internal/testutil"
|
||||
"github.com/anchore/syft/syft/sbom"
|
||||
)
|
||||
|
||||
var updateSnapshot = flag.Bool("update-cyclonedx-json", false, "update the *.golden files for cyclone-dx JSON encoders")
|
||||
var updateImage = flag.Bool("update-image", false, "update the golden image used for image encoder testing")
|
||||
|
||||
func getEncoder(t testing.TB) sbom.FormatEncoder {
|
||||
enc, err := NewFormatEncoderWithConfig(DefaultEncoderConfig())
|
||||
require.NoError(t, err)
|
||||
return enc
|
||||
}
|
||||
|
||||
func TestCycloneDxDirectoryEncoder(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
testutil.AssertEncoderAgainstGoldenSnapshot(t,
|
||||
testutil.EncoderSnapshotTestConfig{
|
||||
Subject: testutil.DirectoryInput(t, dir),
|
||||
Format: getEncoder(t),
|
||||
UpdateSnapshot: *updateSnapshot,
|
||||
PersistRedactionsInSnapshot: true,
|
||||
IsJSON: true,
|
||||
Redactor: redactor(dir),
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
func TestCycloneDxImageEncoder(t *testing.T) {
|
||||
testImage := "image-simple"
|
||||
testutil.AssertEncoderAgainstGoldenImageSnapshot(t,
|
||||
testutil.ImageSnapshotTestConfig{
|
||||
Image: testImage,
|
||||
UpdateImageSnapshot: *updateImage,
|
||||
},
|
||||
testutil.EncoderSnapshotTestConfig{
|
||||
Subject: testutil.ImageInput(t, testImage),
|
||||
Format: getEncoder(t),
|
||||
UpdateSnapshot: *updateSnapshot,
|
||||
PersistRedactionsInSnapshot: true,
|
||||
IsJSON: true,
|
||||
Redactor: redactor(),
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
func redactor(values ...string) testutil.Redactor {
|
||||
return testutil.NewRedactions().
|
||||
WithValuesRedacted(values...).
|
||||
WithPatternRedactors(
|
||||
map[string]string{
|
||||
// UUIDs
|
||||
`urn:uuid:[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}`: `urn:uuid:redacted`,
|
||||
|
||||
// timestamps
|
||||
`([0-9]+)-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])[Tt]([01][0-9]|2[0-3]):([0-5][0-9]):([0-5][0-9]|60)(\.[0-9]+)?(([Zz])|([+|\-]([01][0-9]|2[0-3]):[0-5][0-9]))`: `timestamp:redacted`,
|
||||
|
||||
// image hashes
|
||||
`sha256:[A-Fa-f0-9]{64}`: `sha256:redacted`,
|
||||
|
||||
// BOM refs
|
||||
`"bom-ref":\s*"[^"]+"`: `"bom-ref":"redacted"`,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
func TestSupportedVersions(t *testing.T) {
|
||||
encs := defaultFormatEncoders()
|
||||
require.NotEmpty(t, encs)
|
||||
|
||||
versions := SupportedVersions()
|
||||
require.Equal(t, len(versions), len(encs))
|
||||
|
||||
subject := testutil.DirectoryInput(t, t.TempDir())
|
||||
dec := NewFormatDecoder()
|
||||
|
||||
for _, enc := range encs {
|
||||
t.Run(enc.Version(), func(t *testing.T) {
|
||||
require.Contains(t, versions, enc.Version())
|
||||
|
||||
var buf bytes.Buffer
|
||||
require.NoError(t, enc.Encode(&buf, subject))
|
||||
|
||||
id, version := dec.Identify(bytes.NewReader(buf.Bytes()))
|
||||
require.Equal(t, enc.ID(), id)
|
||||
require.Equal(t, enc.Version(), version)
|
||||
|
||||
var s *sbom.SBOM
|
||||
var err error
|
||||
s, id, version, err = dec.Decode(bytes.NewReader(buf.Bytes()))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, enc.ID(), id)
|
||||
require.Equal(t, enc.Version(), version)
|
||||
|
||||
require.NotEmpty(t, s.Artifacts.Packages.PackageCount())
|
||||
|
||||
assert.Equal(t, len(subject.Relationships), len(s.Relationships), "mismatched relationship count")
|
||||
|
||||
if !assert.Equal(t, subject.Artifacts.Packages.PackageCount(), s.Artifacts.Packages.PackageCount(), "mismatched package count") {
|
||||
t.Logf("expected: %d", subject.Artifacts.Packages.PackageCount())
|
||||
for _, p := range subject.Artifacts.Packages.Sorted() {
|
||||
t.Logf(" - %s", p.String())
|
||||
}
|
||||
t.Logf("actual: %d", s.Artifacts.Packages.PackageCount())
|
||||
for _, p := range s.Artifacts.Packages.Sorted() {
|
||||
t.Logf(" - %s", p.String())
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func defaultFormatEncoders() []sbom.FormatEncoder {
|
||||
var encs []sbom.FormatEncoder
|
||||
for _, version := range SupportedVersions() {
|
||||
enc, err := NewFormatEncoderWithConfig(EncoderConfig{Version: version})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
encs = append(encs, enc)
|
||||
}
|
||||
return encs
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
not an sbom!
|
||||
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"$schema": "http://cyclonedx.org/schema/bom-1.2.schema.json",
|
||||
"bomFormat": "CycloneDX",
|
||||
"specVersion": "1.2",
|
||||
"serialNumber": "urn:uuid:8cfb48be-7ed8-4cea-8c23-8992e98d12be",
|
||||
"version": 1,
|
||||
"metadata": {
|
||||
"timestamp": "2023-09-29T12:02:02-04:00",
|
||||
"tools": [
|
||||
{
|
||||
"vendor": "anchore",
|
||||
"name": "syft",
|
||||
"version": "[not provided]"
|
||||
}
|
||||
],
|
||||
"component": {
|
||||
"bom-ref": "a0ff99a6af10f11f",
|
||||
"type": "file",
|
||||
"name": "go.mod",
|
||||
"version": "sha256:sha256:dc333f342905248a52e424d8dfd061251d01867d01a4f9d7397144a775ff9ebd"
|
||||
}
|
||||
},
|
||||
"components": [
|
||||
{
|
||||
"bom-ref": "pkg:golang/github.com/wagoodman/go-partybus@v0.0.0-20230516145632-8ccac152c651?package-id=2ff71a67fb024c86",
|
||||
"type": "library",
|
||||
"name": "github.com/wagoodman/go-partybus",
|
||||
"version": "v0.0.0-20230516145632-8ccac152c651",
|
||||
"cpe": "cpe:2.3:a:wagoodman:go-partybus:v0.0.0-20230516145632-8ccac152c651:*:*:*:*:*:*:*",
|
||||
"purl": "pkg:golang/github.com/wagoodman/go-partybus@v0.0.0-20230516145632-8ccac152c651"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
{
|
||||
"$schema": "http://cyclonedx.org/schema/bom-1.3.schema.json",
|
||||
"bomFormat": "CycloneDX",
|
||||
"specVersion": "1.3",
|
||||
"serialNumber": "urn:uuid:57ce2636-4ca6-46ba-b389-196af071d6fc",
|
||||
"version": 1,
|
||||
"metadata": {
|
||||
"timestamp": "2023-09-29T12:02:02-04:00",
|
||||
"tools": [
|
||||
{
|
||||
"vendor": "anchore",
|
||||
"name": "syft",
|
||||
"version": "[not provided]"
|
||||
}
|
||||
],
|
||||
"component": {
|
||||
"bom-ref": "a0ff99a6af10f11f",
|
||||
"type": "file",
|
||||
"name": "go.mod",
|
||||
"version": "sha256:sha256:dc333f342905248a52e424d8dfd061251d01867d01a4f9d7397144a775ff9ebd"
|
||||
}
|
||||
},
|
||||
"components": [
|
||||
{
|
||||
"bom-ref": "pkg:golang/github.com/wagoodman/go-partybus@v0.0.0-20230516145632-8ccac152c651?package-id=2ff71a67fb024c86",
|
||||
"type": "library",
|
||||
"name": "github.com/wagoodman/go-partybus",
|
||||
"version": "v0.0.0-20230516145632-8ccac152c651",
|
||||
"cpe": "cpe:2.3:a:wagoodman:go-partybus:v0.0.0-20230516145632-8ccac152c651:*:*:*:*:*:*:*",
|
||||
"purl": "pkg:golang/github.com/wagoodman/go-partybus@v0.0.0-20230516145632-8ccac152c651",
|
||||
"properties": [
|
||||
{
|
||||
"name": "syft:package:foundBy",
|
||||
"value": "go-mod-file-cataloger"
|
||||
},
|
||||
{
|
||||
"name": "syft:package:language",
|
||||
"value": "go"
|
||||
},
|
||||
{
|
||||
"name": "syft:package:metadataType",
|
||||
"value": "GolangModMetadata"
|
||||
},
|
||||
{
|
||||
"name": "syft:package:type",
|
||||
"value": "go-module"
|
||||
},
|
||||
{
|
||||
"name": "syft:cpe23",
|
||||
"value": "cpe:2.3:a:wagoodman:go_partybus:v0.0.0-20230516145632-8ccac152c651:*:*:*:*:*:*:*"
|
||||
},
|
||||
{
|
||||
"name": "syft:location:0:path",
|
||||
"value": "/go.mod"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
{
|
||||
"$schema": "http://cyclonedx.org/schema/bom-1.4.schema.json",
|
||||
"bomFormat": "CycloneDX",
|
||||
"specVersion": "1.4",
|
||||
"serialNumber": "urn:uuid:6be7491f-6a9b-40f0-a60e-f74e2c0e23c3",
|
||||
"version": 1,
|
||||
"metadata": {
|
||||
"timestamp": "2023-09-29T12:02:02-04:00",
|
||||
"tools": [
|
||||
{
|
||||
"vendor": "anchore",
|
||||
"name": "syft",
|
||||
"version": "[not provided]"
|
||||
}
|
||||
],
|
||||
"component": {
|
||||
"bom-ref": "a0ff99a6af10f11f",
|
||||
"type": "file",
|
||||
"name": "go.mod",
|
||||
"version": "sha256:sha256:dc333f342905248a52e424d8dfd061251d01867d01a4f9d7397144a775ff9ebd"
|
||||
}
|
||||
},
|
||||
"components": [
|
||||
{
|
||||
"bom-ref": "pkg:golang/github.com/wagoodman/go-partybus@v0.0.0-20230516145632-8ccac152c651?package-id=2ff71a67fb024c86",
|
||||
"type": "library",
|
||||
"name": "github.com/wagoodman/go-partybus",
|
||||
"version": "v0.0.0-20230516145632-8ccac152c651",
|
||||
"cpe": "cpe:2.3:a:wagoodman:go-partybus:v0.0.0-20230516145632-8ccac152c651:*:*:*:*:*:*:*",
|
||||
"purl": "pkg:golang/github.com/wagoodman/go-partybus@v0.0.0-20230516145632-8ccac152c651",
|
||||
"properties": [
|
||||
{
|
||||
"name": "syft:package:foundBy",
|
||||
"value": "go-mod-file-cataloger"
|
||||
},
|
||||
{
|
||||
"name": "syft:package:language",
|
||||
"value": "go"
|
||||
},
|
||||
{
|
||||
"name": "syft:package:metadataType",
|
||||
"value": "GolangModMetadata"
|
||||
},
|
||||
{
|
||||
"name": "syft:package:type",
|
||||
"value": "go-module"
|
||||
},
|
||||
{
|
||||
"name": "syft:cpe23",
|
||||
"value": "cpe:2.3:a:wagoodman:go_partybus:v0.0.0-20230516145632-8ccac152c651:*:*:*:*:*:*:*"
|
||||
},
|
||||
{
|
||||
"name": "syft:location:0:path",
|
||||
"value": "/go.mod"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
{
|
||||
"$schema": "http://cyclonedx.org/schema/bom-1.5.schema.json",
|
||||
"bomFormat": "CycloneDX",
|
||||
"specVersion": "1.5",
|
||||
"serialNumber": "urn:uuid:22c456c5-c2a0-4285-a8ef-01b4382822c3",
|
||||
"version": 1,
|
||||
"metadata": {
|
||||
"timestamp": "2023-09-29T12:02:02-04:00",
|
||||
"tools": [
|
||||
{
|
||||
"vendor": "anchore",
|
||||
"name": "syft",
|
||||
"version": "[not provided]"
|
||||
}
|
||||
],
|
||||
"component": {
|
||||
"bom-ref": "a0ff99a6af10f11f",
|
||||
"type": "file",
|
||||
"name": "go.mod",
|
||||
"version": "sha256:sha256:dc333f342905248a52e424d8dfd061251d01867d01a4f9d7397144a775ff9ebd"
|
||||
}
|
||||
},
|
||||
"components": [
|
||||
{
|
||||
"bom-ref": "pkg:golang/github.com/wagoodman/go-partybus@v0.0.0-20230516145632-8ccac152c651?package-id=2ff71a67fb024c86",
|
||||
"type": "library",
|
||||
"name": "github.com/wagoodman/go-partybus",
|
||||
"version": "v0.0.0-20230516145632-8ccac152c651",
|
||||
"cpe": "cpe:2.3:a:wagoodman:go-partybus:v0.0.0-20230516145632-8ccac152c651:*:*:*:*:*:*:*",
|
||||
"purl": "pkg:golang/github.com/wagoodman/go-partybus@v0.0.0-20230516145632-8ccac152c651",
|
||||
"properties": [
|
||||
{
|
||||
"name": "syft:package:foundBy",
|
||||
"value": "go-mod-file-cataloger"
|
||||
},
|
||||
{
|
||||
"name": "syft:package:language",
|
||||
"value": "go"
|
||||
},
|
||||
{
|
||||
"name": "syft:package:metadataType",
|
||||
"value": "GolangModMetadata"
|
||||
},
|
||||
{
|
||||
"name": "syft:package:type",
|
||||
"value": "go-module"
|
||||
},
|
||||
{
|
||||
"name": "syft:cpe23",
|
||||
"value": "cpe:2.3:a:wagoodman:go_partybus:v0.0.0-20230516145632-8ccac152c651:*:*:*:*:*:*:*"
|
||||
},
|
||||
{
|
||||
"name": "syft:location:0:path",
|
||||
"value": "/go.mod"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "http://cyclonedx.org/schema/bom-1.4.schema.json",
|
||||
"$schema": "http://cyclonedx.org/schema/bom-1.5.schema.json",
|
||||
"bomFormat": "CycloneDX",
|
||||
"specVersion": "1.4",
|
||||
"specVersion": "1.5",
|
||||
"serialNumber": "urn:uuid:redacted",
|
||||
"version": 1,
|
||||
"metadata": {
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "http://cyclonedx.org/schema/bom-1.4.schema.json",
|
||||
"$schema": "http://cyclonedx.org/schema/bom-1.5.schema.json",
|
||||
"bomFormat": "CycloneDX",
|
||||
"specVersion": "1.4",
|
||||
"specVersion": "1.5",
|
||||
"serialNumber": "urn:uuid:redacted",
|
||||
"version": 1,
|
||||
"metadata": {
|
||||
@@ -0,0 +1,106 @@
|
||||
package cyclonedxxml
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"github.com/CycloneDX/cyclonedx-go"
|
||||
|
||||
"github.com/anchore/syft/internal/log"
|
||||
"github.com/anchore/syft/syft/format/common/cyclonedxhelpers"
|
||||
"github.com/anchore/syft/syft/format/internal/cyclonedxutil"
|
||||
"github.com/anchore/syft/syft/sbom"
|
||||
)
|
||||
|
||||
var _ sbom.FormatDecoder = (*decoder)(nil)
|
||||
|
||||
type decoder struct {
|
||||
decoder cyclonedxutil.Decoder
|
||||
}
|
||||
|
||||
func NewFormatDecoder() sbom.FormatDecoder {
|
||||
return decoder{
|
||||
decoder: cyclonedxutil.NewDecoder(cyclonedx.BOMFileFormatXML),
|
||||
}
|
||||
}
|
||||
|
||||
func (d decoder) Decode(reader io.ReadSeeker) (*sbom.SBOM, sbom.FormatID, string, error) {
|
||||
if reader == nil {
|
||||
return nil, "", "", fmt.Errorf("no SBOM bytes provided")
|
||||
}
|
||||
|
||||
id, version := d.Identify(reader)
|
||||
if id != ID {
|
||||
return nil, "", "", fmt.Errorf("not a cyclonedx xml document")
|
||||
}
|
||||
if version == "" {
|
||||
return nil, "", "", fmt.Errorf("unsupported cyclonedx xml document version")
|
||||
}
|
||||
|
||||
doc, err := d.decoder.Decode(reader)
|
||||
if err != nil {
|
||||
return nil, id, version, fmt.Errorf("unable to decode cyclonedx xml document: %w", err)
|
||||
}
|
||||
|
||||
s, err := cyclonedxhelpers.ToSyftModel(doc)
|
||||
if err != nil {
|
||||
return nil, id, version, err
|
||||
}
|
||||
|
||||
return s, id, version, nil
|
||||
}
|
||||
|
||||
func (d decoder) Identify(reader io.ReadSeeker) (sbom.FormatID, string) {
|
||||
if reader == nil {
|
||||
return "", ""
|
||||
}
|
||||
|
||||
if _, err := reader.Seek(0, io.SeekStart); err != nil {
|
||||
log.Debugf("unable to seek to start of CycloneDX XML SBOM: %+v", err)
|
||||
return "", ""
|
||||
}
|
||||
|
||||
type Document struct {
|
||||
XMLNS string `xml:"xmlns,attr"`
|
||||
}
|
||||
|
||||
dec := xml.NewDecoder(reader)
|
||||
|
||||
var doc Document
|
||||
err := dec.Decode(&doc)
|
||||
if err != nil {
|
||||
// maybe not xml? maybe not valid? doesn't matter, we won't process it.
|
||||
return "", ""
|
||||
}
|
||||
|
||||
id, version := getFormatInfo(doc.XMLNS)
|
||||
if version == "" || id != ID {
|
||||
// not a cyclonedx xml document that we support
|
||||
return "", ""
|
||||
}
|
||||
|
||||
return id, version
|
||||
}
|
||||
|
||||
func getFormatInfo(xmlns string) (sbom.FormatID, string) {
|
||||
version := getVersionFromXMLNS(xmlns)
|
||||
|
||||
if !strings.Contains(xmlns, "cyclonedx.org/schema/bom") {
|
||||
// not a cyclonedx xml document
|
||||
return "", ""
|
||||
}
|
||||
|
||||
spec, err := cyclonedxutil.SpecVersionFromString(version)
|
||||
if spec < 0 || err != nil {
|
||||
// not a supported version, but is cyclonedx xml
|
||||
return ID, ""
|
||||
}
|
||||
return ID, version
|
||||
}
|
||||
|
||||
func getVersionFromXMLNS(xmlns string) string {
|
||||
fields := strings.Split(xmlns, "/")
|
||||
return fields[len(fields)-1]
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
package cyclonedxxml
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/anchore/syft/syft/sbom"
|
||||
)
|
||||
|
||||
func TestDecoder_Decode(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
file string
|
||||
err bool
|
||||
distro string
|
||||
packages []string
|
||||
}{
|
||||
{
|
||||
name: "dir-scan",
|
||||
file: "snapshot/TestCycloneDxDirectoryEncoder.golden",
|
||||
distro: "debian:1.2.3",
|
||||
packages: []string{"package-1:1.0.1", "package-2:2.0.1"},
|
||||
},
|
||||
{
|
||||
name: "image-scan",
|
||||
file: "snapshot/TestCycloneDxImageEncoder.golden",
|
||||
distro: "debian:1.2.3",
|
||||
packages: []string{"package-1:1.0.1", "package-2:2.0.1"},
|
||||
},
|
||||
{
|
||||
name: "not-an-sbom",
|
||||
file: "bad-sbom",
|
||||
err: true,
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.file, func(t *testing.T) {
|
||||
reader, err := os.Open(filepath.Join("test-fixtures", test.file))
|
||||
require.NoError(t, err)
|
||||
|
||||
dec := NewFormatDecoder()
|
||||
|
||||
formatID, formatVersion := dec.Identify(reader)
|
||||
if test.err {
|
||||
assert.Equal(t, sbom.FormatID(""), formatID)
|
||||
assert.Equal(t, "", formatVersion)
|
||||
|
||||
_, decodeID, decodeVersion, err := dec.Decode(reader)
|
||||
require.Error(t, err)
|
||||
assert.Equal(t, sbom.FormatID(""), decodeID)
|
||||
assert.Equal(t, "", decodeVersion)
|
||||
|
||||
return
|
||||
}
|
||||
assert.Equal(t, ID, formatID)
|
||||
assert.NotEmpty(t, formatVersion)
|
||||
|
||||
bom, decodeID, decodeVersion, err := dec.Decode(reader)
|
||||
require.NotNil(t, bom)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, ID, decodeID)
|
||||
assert.Equal(t, formatVersion, decodeVersion)
|
||||
|
||||
split := strings.SplitN(test.distro, ":", 2)
|
||||
distroName := split[0]
|
||||
distroVersion := split[1]
|
||||
assert.Equal(t, bom.Artifacts.LinuxDistribution.ID, distroName)
|
||||
assert.Equal(t, bom.Artifacts.LinuxDistribution.Version, distroVersion)
|
||||
|
||||
var pkgs []string
|
||||
for p := range bom.Artifacts.Packages.Enumerate() {
|
||||
pkgs = append(pkgs, fmt.Sprintf("%s:%s", p.Name, p.Version))
|
||||
}
|
||||
|
||||
assert.ElementsMatch(t, test.packages, pkgs)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecoder_Identify(t *testing.T) {
|
||||
type testCase struct {
|
||||
name string
|
||||
file string
|
||||
id sbom.FormatID
|
||||
version string
|
||||
}
|
||||
|
||||
var cases []testCase
|
||||
|
||||
for _, version := range SupportedVersions() {
|
||||
cases = append(cases, testCase{
|
||||
name: fmt.Sprintf("v%s schema", version),
|
||||
file: fmt.Sprintf("test-fixtures/identify/%s.xml", version),
|
||||
id: ID,
|
||||
version: version,
|
||||
})
|
||||
}
|
||||
|
||||
for _, test := range cases {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
reader, err := os.Open(test.file)
|
||||
require.NoError(t, err)
|
||||
|
||||
dec := NewFormatDecoder()
|
||||
|
||||
formatID, formatVersion := dec.Identify(reader)
|
||||
assert.Equal(t, test.id, formatID)
|
||||
assert.Equal(t, test.version, formatVersion)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package cyclonedxxml
|
||||
|
||||
import (
|
||||
"github.com/CycloneDX/cyclonedx-go"
|
||||
|
||||
"github.com/anchore/syft/syft/format/internal/cyclonedxutil"
|
||||
"github.com/anchore/syft/syft/sbom"
|
||||
)
|
||||
|
||||
var _ sbom.FormatEncoder = (*encoder)(nil)
|
||||
|
||||
const ID = cyclonedxutil.XMLFormatID
|
||||
|
||||
func SupportedVersions() []string {
|
||||
return cyclonedxutil.SupportedVersions(ID)
|
||||
}
|
||||
|
||||
type EncoderConfig struct {
|
||||
Version string
|
||||
}
|
||||
|
||||
type encoder struct {
|
||||
cfg EncoderConfig
|
||||
cyclonedxutil.Encoder
|
||||
}
|
||||
|
||||
func NewFormatEncoderWithConfig(cfg EncoderConfig) (sbom.FormatEncoder, error) {
|
||||
enc, err := cyclonedxutil.NewEncoder(cfg.Version, cyclonedx.BOMFileFormatXML)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return encoder{
|
||||
cfg: cfg,
|
||||
Encoder: enc,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func DefaultEncoderConfig() EncoderConfig {
|
||||
return EncoderConfig{
|
||||
Version: cyclonedxutil.DefaultVersion,
|
||||
}
|
||||
}
|
||||
|
||||
func (e encoder) ID() sbom.FormatID {
|
||||
return ID
|
||||
}
|
||||
|
||||
func (e encoder) Aliases() []string {
|
||||
return []string{
|
||||
"cyclonedx",
|
||||
"cyclone",
|
||||
"cdx",
|
||||
}
|
||||
}
|
||||
|
||||
func (e encoder) Version() string {
|
||||
return e.cfg.Version
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
package cyclonedxxml
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"flag"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/anchore/syft/syft/format/internal/testutil"
|
||||
"github.com/anchore/syft/syft/sbom"
|
||||
)
|
||||
|
||||
var updateSnapshot = flag.Bool("update-cyclonedx-xml", false, "update the *.golden files for cyclone-dx XML encoders")
|
||||
var updateImage = flag.Bool("update-image", false, "update the golden image used for image encoder testing")
|
||||
|
||||
func getEncoder(t testing.TB) sbom.FormatEncoder {
|
||||
enc, err := NewFormatEncoderWithConfig(DefaultEncoderConfig())
|
||||
require.NoError(t, err)
|
||||
return enc
|
||||
}
|
||||
|
||||
func TestCycloneDxDirectoryEncoder(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
testutil.AssertEncoderAgainstGoldenSnapshot(t,
|
||||
testutil.EncoderSnapshotTestConfig{
|
||||
Subject: testutil.DirectoryInput(t, dir),
|
||||
Format: getEncoder(t),
|
||||
UpdateSnapshot: *updateSnapshot,
|
||||
PersistRedactionsInSnapshot: true,
|
||||
IsJSON: false,
|
||||
Redactor: redactor(dir),
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
func TestCycloneDxImageEncoder(t *testing.T) {
|
||||
testImage := "image-simple"
|
||||
testutil.AssertEncoderAgainstGoldenImageSnapshot(t,
|
||||
testutil.ImageSnapshotTestConfig{
|
||||
Image: testImage,
|
||||
UpdateImageSnapshot: *updateImage,
|
||||
},
|
||||
testutil.EncoderSnapshotTestConfig{
|
||||
Subject: testutil.ImageInput(t, testImage),
|
||||
Format: getEncoder(t),
|
||||
UpdateSnapshot: *updateSnapshot,
|
||||
PersistRedactionsInSnapshot: true,
|
||||
IsJSON: false,
|
||||
Redactor: redactor(),
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
func redactor(values ...string) testutil.Redactor {
|
||||
return testutil.NewRedactions().
|
||||
WithValuesRedacted(values...).
|
||||
WithPatternRedactors(
|
||||
map[string]string{
|
||||
// dates
|
||||
`([0-9]+)-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])[Tt]([01][0-9]|2[0-3]):([0-5][0-9]):([0-5][0-9]|60)(\.[0-9]+)?(([Zz])|([+|\-]([01][0-9]|2[0-3]):[0-5][0-9]))`: `redacted`,
|
||||
|
||||
// image hashes and BOM refs
|
||||
`sha256:[A-Za-z0-9]{64}`: `sha256:redacted`,
|
||||
|
||||
// serial numbers and BOM refs
|
||||
`(serialNumber|bom-ref)="[^"]+"`: `$1="redacted"`,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
func TestSupportedVersions(t *testing.T) {
|
||||
encs := defaultFormatEncoders()
|
||||
require.NotEmpty(t, encs)
|
||||
|
||||
versions := SupportedVersions()
|
||||
require.Equal(t, len(versions), len(encs))
|
||||
|
||||
subject := testutil.DirectoryInput(t, t.TempDir())
|
||||
dec := NewFormatDecoder()
|
||||
|
||||
for _, enc := range encs {
|
||||
t.Run(enc.Version(), func(t *testing.T) {
|
||||
require.Contains(t, versions, enc.Version())
|
||||
|
||||
var buf bytes.Buffer
|
||||
require.NoError(t, enc.Encode(&buf, subject))
|
||||
|
||||
id, version := dec.Identify(bytes.NewReader(buf.Bytes()))
|
||||
require.Equal(t, enc.ID(), id)
|
||||
require.Equal(t, enc.Version(), version)
|
||||
|
||||
var s *sbom.SBOM
|
||||
var err error
|
||||
s, id, version, err = dec.Decode(bytes.NewReader(buf.Bytes()))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, enc.ID(), id)
|
||||
require.Equal(t, enc.Version(), version)
|
||||
|
||||
require.NotEmpty(t, s.Artifacts.Packages.PackageCount())
|
||||
|
||||
assert.Equal(t, len(subject.Relationships), len(s.Relationships), "mismatched relationship count")
|
||||
|
||||
if !assert.Equal(t, subject.Artifacts.Packages.PackageCount(), s.Artifacts.Packages.PackageCount(), "mismatched package count") {
|
||||
t.Logf("expected: %d", subject.Artifacts.Packages.PackageCount())
|
||||
for _, p := range subject.Artifacts.Packages.Sorted() {
|
||||
t.Logf(" - %s", p.String())
|
||||
}
|
||||
t.Logf("actual: %d", s.Artifacts.Packages.PackageCount())
|
||||
for _, p := range s.Artifacts.Packages.Sorted() {
|
||||
t.Logf(" - %s", p.String())
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func defaultFormatEncoders() []sbom.FormatEncoder {
|
||||
var encs []sbom.FormatEncoder
|
||||
for _, version := range SupportedVersions() {
|
||||
enc, err := NewFormatEncoderWithConfig(EncoderConfig{Version: version})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
encs = append(encs, enc)
|
||||
}
|
||||
return encs
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
not an sbom!
|
||||
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<bom xmlns="http://cyclonedx.org/schema/bom/1.0" version="1">
|
||||
<components>
|
||||
<component type="library">
|
||||
<name>github.com/wagoodman/go-partybus</name>
|
||||
<version>v0.0.0-20230516145632-8ccac152c651</version>
|
||||
<cpe>cpe:2.3:a:wagoodman:go-partybus:v0.0.0-20230516145632-8ccac152c651:*:*:*:*:*:*:*</cpe>
|
||||
<purl>pkg:golang/github.com/wagoodman/go-partybus@v0.0.0-20230516145632-8ccac152c651</purl>
|
||||
<modified>false</modified>
|
||||
</component>
|
||||
</components>
|
||||
</bom>
|
||||
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<bom xmlns="http://cyclonedx.org/schema/bom/1.1" serialNumber="urn:uuid:32ad4aec-ddc3-4c26-90e7-796f7ca82a1d" version="1">
|
||||
<components>
|
||||
<component bom-ref="pkg:golang/github.com/wagoodman/go-partybus@v0.0.0-20230516145632-8ccac152c651?package-id=2ff71a67fb024c86" type="library">
|
||||
<name>github.com/wagoodman/go-partybus</name>
|
||||
<version>v0.0.0-20230516145632-8ccac152c651</version>
|
||||
<cpe>cpe:2.3:a:wagoodman:go-partybus:v0.0.0-20230516145632-8ccac152c651:*:*:*:*:*:*:*</cpe>
|
||||
<purl>pkg:golang/github.com/wagoodman/go-partybus@v0.0.0-20230516145632-8ccac152c651</purl>
|
||||
</component>
|
||||
</components>
|
||||
</bom>
|
||||
@@ -0,0 +1,25 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<bom xmlns="http://cyclonedx.org/schema/bom/1.2" serialNumber="urn:uuid:214033a9-6fe2-4f0b-88f2-8dbca251d0e0" version="1">
|
||||
<metadata>
|
||||
<timestamp>2023-09-29T11:48:10-04:00</timestamp>
|
||||
<tools>
|
||||
<tool>
|
||||
<vendor>anchore</vendor>
|
||||
<name>syft</name>
|
||||
<version>[not provided]</version>
|
||||
</tool>
|
||||
</tools>
|
||||
<component bom-ref="a0ff99a6af10f11f" type="file">
|
||||
<name>go.mod</name>
|
||||
<version>sha256:sha256:dc333f342905248a52e424d8dfd061251d01867d01a4f9d7397144a775ff9ebd</version>
|
||||
</component>
|
||||
</metadata>
|
||||
<components>
|
||||
<component bom-ref="pkg:golang/github.com/wagoodman/go-partybus@v0.0.0-20230516145632-8ccac152c651?package-id=2ff71a67fb024c86" type="library">
|
||||
<name>github.com/wagoodman/go-partybus</name>
|
||||
<version>v0.0.0-20230516145632-8ccac152c651</version>
|
||||
<cpe>cpe:2.3:a:wagoodman:go-partybus:v0.0.0-20230516145632-8ccac152c651:*:*:*:*:*:*:*</cpe>
|
||||
<purl>pkg:golang/github.com/wagoodman/go-partybus@v0.0.0-20230516145632-8ccac152c651</purl>
|
||||
</component>
|
||||
</components>
|
||||
</bom>
|
||||
@@ -0,0 +1,33 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<bom xmlns="http://cyclonedx.org/schema/bom/1.3" serialNumber="urn:uuid:7ec2b549-3d31-4c7b-85be-fad838c8f312" version="1">
|
||||
<metadata>
|
||||
<timestamp>2023-09-29T11:48:10-04:00</timestamp>
|
||||
<tools>
|
||||
<tool>
|
||||
<vendor>anchore</vendor>
|
||||
<name>syft</name>
|
||||
<version>[not provided]</version>
|
||||
</tool>
|
||||
</tools>
|
||||
<component bom-ref="a0ff99a6af10f11f" type="file">
|
||||
<name>go.mod</name>
|
||||
<version>sha256:sha256:dc333f342905248a52e424d8dfd061251d01867d01a4f9d7397144a775ff9ebd</version>
|
||||
</component>
|
||||
</metadata>
|
||||
<components>
|
||||
<component bom-ref="pkg:golang/github.com/wagoodman/go-partybus@v0.0.0-20230516145632-8ccac152c651?package-id=2ff71a67fb024c86" type="library">
|
||||
<name>github.com/wagoodman/go-partybus</name>
|
||||
<version>v0.0.0-20230516145632-8ccac152c651</version>
|
||||
<cpe>cpe:2.3:a:wagoodman:go-partybus:v0.0.0-20230516145632-8ccac152c651:*:*:*:*:*:*:*</cpe>
|
||||
<purl>pkg:golang/github.com/wagoodman/go-partybus@v0.0.0-20230516145632-8ccac152c651</purl>
|
||||
<properties>
|
||||
<property name="syft:package:foundBy">go-mod-file-cataloger</property>
|
||||
<property name="syft:package:language">go</property>
|
||||
<property name="syft:package:metadataType">GolangModMetadata</property>
|
||||
<property name="syft:package:type">go-module</property>
|
||||
<property name="syft:cpe23">cpe:2.3:a:wagoodman:go_partybus:v0.0.0-20230516145632-8ccac152c651:*:*:*:*:*:*:*</property>
|
||||
<property name="syft:location:0:path">/go.mod</property>
|
||||
</properties>
|
||||
</component>
|
||||
</components>
|
||||
</bom>
|
||||
@@ -0,0 +1,33 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<bom xmlns="http://cyclonedx.org/schema/bom/1.4" serialNumber="urn:uuid:3e5d8b2a-30cd-409f-9519-558c53e542c5" version="1">
|
||||
<metadata>
|
||||
<timestamp>2023-09-29T11:48:10-04:00</timestamp>
|
||||
<tools>
|
||||
<tool>
|
||||
<vendor>anchore</vendor>
|
||||
<name>syft</name>
|
||||
<version>[not provided]</version>
|
||||
</tool>
|
||||
</tools>
|
||||
<component bom-ref="a0ff99a6af10f11f" type="file">
|
||||
<name>go.mod</name>
|
||||
<version>sha256:sha256:dc333f342905248a52e424d8dfd061251d01867d01a4f9d7397144a775ff9ebd</version>
|
||||
</component>
|
||||
</metadata>
|
||||
<components>
|
||||
<component bom-ref="pkg:golang/github.com/wagoodman/go-partybus@v0.0.0-20230516145632-8ccac152c651?package-id=2ff71a67fb024c86" type="library">
|
||||
<name>github.com/wagoodman/go-partybus</name>
|
||||
<version>v0.0.0-20230516145632-8ccac152c651</version>
|
||||
<cpe>cpe:2.3:a:wagoodman:go-partybus:v0.0.0-20230516145632-8ccac152c651:*:*:*:*:*:*:*</cpe>
|
||||
<purl>pkg:golang/github.com/wagoodman/go-partybus@v0.0.0-20230516145632-8ccac152c651</purl>
|
||||
<properties>
|
||||
<property name="syft:package:foundBy">go-mod-file-cataloger</property>
|
||||
<property name="syft:package:language">go</property>
|
||||
<property name="syft:package:metadataType">GolangModMetadata</property>
|
||||
<property name="syft:package:type">go-module</property>
|
||||
<property name="syft:cpe23">cpe:2.3:a:wagoodman:go_partybus:v0.0.0-20230516145632-8ccac152c651:*:*:*:*:*:*:*</property>
|
||||
<property name="syft:location:0:path">/go.mod</property>
|
||||
</properties>
|
||||
</component>
|
||||
</components>
|
||||
</bom>
|
||||
@@ -0,0 +1,33 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<bom xmlns="http://cyclonedx.org/schema/bom/1.5" serialNumber="urn:uuid:b2188bff-93b8-47e7-b86e-41c417e0d75e" version="1">
|
||||
<metadata>
|
||||
<timestamp>2023-09-29T11:48:10-04:00</timestamp>
|
||||
<tools>
|
||||
<tool>
|
||||
<vendor>anchore</vendor>
|
||||
<name>syft</name>
|
||||
<version>[not provided]</version>
|
||||
</tool>
|
||||
</tools>
|
||||
<component bom-ref="a0ff99a6af10f11f" type="file">
|
||||
<name>go.mod</name>
|
||||
<version>sha256:sha256:dc333f342905248a52e424d8dfd061251d01867d01a4f9d7397144a775ff9ebd</version>
|
||||
</component>
|
||||
</metadata>
|
||||
<components>
|
||||
<component bom-ref="pkg:golang/github.com/wagoodman/go-partybus@v0.0.0-20230516145632-8ccac152c651?package-id=2ff71a67fb024c86" type="library">
|
||||
<name>github.com/wagoodman/go-partybus</name>
|
||||
<version>v0.0.0-20230516145632-8ccac152c651</version>
|
||||
<cpe>cpe:2.3:a:wagoodman:go-partybus:v0.0.0-20230516145632-8ccac152c651:*:*:*:*:*:*:*</cpe>
|
||||
<purl>pkg:golang/github.com/wagoodman/go-partybus@v0.0.0-20230516145632-8ccac152c651</purl>
|
||||
<properties>
|
||||
<property name="syft:package:foundBy">go-mod-file-cataloger</property>
|
||||
<property name="syft:package:language">go</property>
|
||||
<property name="syft:package:metadataType">GolangModMetadata</property>
|
||||
<property name="syft:package:type">go-module</property>
|
||||
<property name="syft:cpe23">cpe:2.3:a:wagoodman:go_partybus:v0.0.0-20230516145632-8ccac152c651:*:*:*:*:*:*:*</property>
|
||||
<property name="syft:location:0:path">/go.mod</property>
|
||||
</properties>
|
||||
</component>
|
||||
</components>
|
||||
</bom>
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<bom xmlns="http://cyclonedx.org/schema/bom/1.4" serialNumber="redacted" version="1">
|
||||
<bom xmlns="http://cyclonedx.org/schema/bom/1.5" serialNumber="redacted" version="1">
|
||||
<metadata>
|
||||
<timestamp>redacted</timestamp>
|
||||
<tools>
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<bom xmlns="http://cyclonedx.org/schema/bom/1.4" serialNumber="redacted" version="1">
|
||||
<bom xmlns="http://cyclonedx.org/schema/bom/1.5" serialNumber="redacted" version="1">
|
||||
<metadata>
|
||||
<timestamp>redacted</timestamp>
|
||||
<tools>
|
||||
@@ -0,0 +1,91 @@
|
||||
package format
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"github.com/anchore/syft/syft/format/cyclonedxjson"
|
||||
"github.com/anchore/syft/syft/format/cyclonedxxml"
|
||||
"github.com/anchore/syft/syft/format/spdxjson"
|
||||
"github.com/anchore/syft/syft/format/spdxtagvalue"
|
||||
"github.com/anchore/syft/syft/format/syftjson"
|
||||
"github.com/anchore/syft/syft/sbom"
|
||||
)
|
||||
|
||||
var (
|
||||
staticDecoders sbom.FormatDecoder
|
||||
_ sbom.FormatDecoder = (*DecoderCollection)(nil)
|
||||
)
|
||||
|
||||
func init() {
|
||||
staticDecoders = NewDecoderCollection(Decoders()...)
|
||||
}
|
||||
|
||||
func Decoders() []sbom.FormatDecoder {
|
||||
return []sbom.FormatDecoder{
|
||||
syftjson.NewFormatDecoder(),
|
||||
cyclonedxxml.NewFormatDecoder(),
|
||||
cyclonedxjson.NewFormatDecoder(),
|
||||
spdxtagvalue.NewFormatDecoder(),
|
||||
spdxjson.NewFormatDecoder(),
|
||||
}
|
||||
}
|
||||
|
||||
type DecoderCollection struct {
|
||||
decoders []sbom.FormatDecoder
|
||||
}
|
||||
|
||||
func NewDecoderCollection(decoders ...sbom.FormatDecoder) sbom.FormatDecoder {
|
||||
return &DecoderCollection{
|
||||
decoders: decoders,
|
||||
}
|
||||
}
|
||||
|
||||
// Decode takes a set of bytes and attempts to decode it into an SBOM relative to the decoders in the collection.
|
||||
func (c *DecoderCollection) Decode(reader io.ReadSeeker) (*sbom.SBOM, sbom.FormatID, string, error) {
|
||||
if reader == nil {
|
||||
return nil, "", "", fmt.Errorf("no SBOM bytes provided")
|
||||
}
|
||||
var bestID sbom.FormatID
|
||||
for _, d := range c.decoders {
|
||||
id, version := d.Identify(reader)
|
||||
if id == "" || version == "" {
|
||||
if id != "" {
|
||||
bestID = id
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
return d.Decode(reader)
|
||||
}
|
||||
|
||||
if bestID != "" {
|
||||
return nil, bestID, "", fmt.Errorf("sbom format found to be %q but the version is not supported", bestID)
|
||||
}
|
||||
|
||||
return nil, "", "", fmt.Errorf("sbom format not recognized")
|
||||
}
|
||||
|
||||
// Identify takes a set of bytes and attempts to identify the format of the SBOM relative to the decoders in the collection.
|
||||
func (c *DecoderCollection) Identify(reader io.ReadSeeker) (sbom.FormatID, string) {
|
||||
if reader == nil {
|
||||
return "", ""
|
||||
}
|
||||
for _, d := range c.decoders {
|
||||
id, version := d.Identify(reader)
|
||||
if id != "" && version != "" {
|
||||
return id, version
|
||||
}
|
||||
}
|
||||
return "", ""
|
||||
}
|
||||
|
||||
// Identify takes a set of bytes and attempts to identify the format of the SBOM.
|
||||
func Identify(reader io.ReadSeeker) (sbom.FormatID, string) {
|
||||
return staticDecoders.Identify(reader)
|
||||
}
|
||||
|
||||
// Decode takes a set of bytes and attempts to decode it into an SBOM.
|
||||
func Decode(reader io.ReadSeeker) (*sbom.SBOM, sbom.FormatID, string, error) {
|
||||
return staticDecoders.Decode(reader)
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
package format
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/anchore/syft/syft/format/syftjson"
|
||||
"github.com/anchore/syft/syft/sbom"
|
||||
)
|
||||
|
||||
func TestIdentify(t *testing.T) {
|
||||
tests := []struct {
|
||||
fixture string
|
||||
id sbom.FormatID
|
||||
version string
|
||||
}{
|
||||
{
|
||||
fixture: "test-fixtures/alpine-syft.json",
|
||||
id: syftjson.ID,
|
||||
version: "1.1.0",
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.fixture, func(t *testing.T) {
|
||||
reader, err := os.Open(test.fixture)
|
||||
assert.NoError(t, err)
|
||||
|
||||
id, version := Identify(reader)
|
||||
assert.Equal(t, test.id, id)
|
||||
assert.Equal(t, test.version, version)
|
||||
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFormats_EmptyInput(t *testing.T) {
|
||||
for _, format := range Decoders() {
|
||||
name := strings.Split(fmt.Sprintf("%#v", format), "{")[0]
|
||||
|
||||
t.Run(name, func(t *testing.T) {
|
||||
t.Run("Decode", func(t *testing.T) {
|
||||
assert.NotPanics(t, func() {
|
||||
decodedSBOM, _, _, err := format.Decode(nil)
|
||||
assert.Error(t, err)
|
||||
assert.Nil(t, decodedSBOM)
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("Identify", func(t *testing.T) {
|
||||
assert.NotPanics(t, func() {
|
||||
id, version := format.Identify(nil)
|
||||
assert.Empty(t, id)
|
||||
assert.Empty(t, version)
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
package format
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/scylladb/go-set/strset"
|
||||
|
||||
"github.com/anchore/syft/internal/log"
|
||||
"github.com/anchore/syft/syft/sbom"
|
||||
)
|
||||
|
||||
type EncoderCollection struct {
|
||||
encoders []sbom.FormatEncoder
|
||||
}
|
||||
|
||||
func NewEncoderCollection(encoders ...sbom.FormatEncoder) *EncoderCollection {
|
||||
return &EncoderCollection{
|
||||
encoders: encoders,
|
||||
}
|
||||
}
|
||||
|
||||
// IDs returns all format IDs represented in the collection.
|
||||
func (e EncoderCollection) IDs() []sbom.FormatID {
|
||||
idSet := strset.New()
|
||||
for _, f := range e.encoders {
|
||||
idSet.Add(string(f.ID()))
|
||||
}
|
||||
|
||||
idList := idSet.List()
|
||||
sort.Strings(idList)
|
||||
|
||||
var ids []sbom.FormatID
|
||||
for _, id := range idList {
|
||||
ids = append(ids, sbom.FormatID(id))
|
||||
}
|
||||
|
||||
return ids
|
||||
}
|
||||
|
||||
// NameVersions returns all formats that are supported by the collection as a list of "name@version" strings.
|
||||
func (e EncoderCollection) NameVersions() []string {
|
||||
set := strset.New()
|
||||
for _, f := range e.encoders {
|
||||
if f.Version() == sbom.AnyVersion {
|
||||
set.Add(string(f.ID()))
|
||||
} else {
|
||||
set.Add(fmt.Sprintf("%s@%s", f.ID(), f.Version()))
|
||||
}
|
||||
}
|
||||
|
||||
list := set.List()
|
||||
sort.Strings(list)
|
||||
|
||||
return list
|
||||
}
|
||||
|
||||
// Aliases returns all format aliases represented in the collection (where an ID would be "spdx-tag-value" the alias would be "spdx").
|
||||
func (e EncoderCollection) Aliases() []string {
|
||||
aliases := strset.New()
|
||||
for _, f := range e.encoders {
|
||||
aliases.Add(f.Aliases()...)
|
||||
}
|
||||
lst := aliases.List()
|
||||
sort.Strings(lst)
|
||||
return lst
|
||||
}
|
||||
|
||||
// Get returns the contained encoder for a given format name and version.
|
||||
func (e EncoderCollection) Get(name string, version string) sbom.FormatEncoder {
|
||||
log.WithFields("name", name, "version", version).Trace("looking for matching encoder")
|
||||
|
||||
name = cleanFormatName(name)
|
||||
var mostRecentFormat sbom.FormatEncoder
|
||||
|
||||
for _, f := range e.encoders {
|
||||
log.WithFields("name", f.ID(), "version", f.Version(), "aliases", f.Aliases()).Trace("considering format")
|
||||
names := []string{string(f.ID())}
|
||||
names = append(names, f.Aliases()...)
|
||||
for _, n := range names {
|
||||
if cleanFormatName(n) == name && versionMatches(f.Version(), version) {
|
||||
if mostRecentFormat == nil || f.Version() > mostRecentFormat.Version() {
|
||||
mostRecentFormat = f
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if mostRecentFormat != nil {
|
||||
log.WithFields("name", mostRecentFormat.ID(), "version", mostRecentFormat.Version()).Trace("found matching encoder")
|
||||
} else {
|
||||
log.WithFields("search-name", name, "search-version", version).Trace("no matching encoder found")
|
||||
}
|
||||
|
||||
return mostRecentFormat
|
||||
}
|
||||
|
||||
// GetByString accepts a name@version string, such as:
|
||||
// - json
|
||||
// - spdx-json@2.1
|
||||
// - cdx@1.5
|
||||
func (e EncoderCollection) GetByString(s string) sbom.FormatEncoder {
|
||||
parts := strings.SplitN(s, "@", 2)
|
||||
version := sbom.AnyVersion
|
||||
if len(parts) > 1 {
|
||||
version = parts[1]
|
||||
}
|
||||
return e.Get(parts[0], version)
|
||||
}
|
||||
|
||||
func versionMatches(version string, match string) bool {
|
||||
if version == sbom.AnyVersion || match == sbom.AnyVersion {
|
||||
return true
|
||||
}
|
||||
|
||||
match = strings.ReplaceAll(match, ".", "\\.")
|
||||
match = strings.ReplaceAll(match, "*", ".*")
|
||||
match = fmt.Sprintf("^%s(\\..*)*$", match)
|
||||
matcher, err := regexp.Compile(match)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return matcher.MatchString(version)
|
||||
}
|
||||
|
||||
func cleanFormatName(name string) string {
|
||||
r := strings.NewReplacer("-", "", "_", "")
|
||||
return strings.ToLower(r.Replace(name))
|
||||
}
|
||||
|
||||
// Encode takes all SBOM elements and a format option and encodes an SBOM document.
|
||||
func Encode(s sbom.SBOM, f sbom.FormatEncoder) ([]byte, error) {
|
||||
buff := bytes.Buffer{}
|
||||
|
||||
if err := f.Encode(&buff, s); err != nil {
|
||||
return nil, fmt.Errorf("unable to encode sbom: %w", err)
|
||||
}
|
||||
|
||||
return buff.Bytes(), nil
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
package format
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/anchore/syft/syft/sbom"
|
||||
)
|
||||
|
||||
func Test_versionMatches(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
version string
|
||||
match string
|
||||
matches bool
|
||||
}{
|
||||
{
|
||||
name: "any version matches number",
|
||||
version: string(sbom.AnyVersion),
|
||||
match: "6",
|
||||
matches: true,
|
||||
},
|
||||
{
|
||||
name: "number matches any version",
|
||||
version: "6",
|
||||
match: string(sbom.AnyVersion),
|
||||
matches: true,
|
||||
},
|
||||
{
|
||||
name: "same number matches",
|
||||
version: "3",
|
||||
match: "3",
|
||||
matches: true,
|
||||
},
|
||||
{
|
||||
name: "same major number matches",
|
||||
version: "3.1",
|
||||
match: "3",
|
||||
matches: true,
|
||||
},
|
||||
{
|
||||
name: "same minor number matches",
|
||||
version: "3.1",
|
||||
match: "3.1",
|
||||
matches: true,
|
||||
},
|
||||
{
|
||||
name: "wildcard-version matches minor",
|
||||
version: "7.1.3",
|
||||
match: "7.*",
|
||||
matches: true,
|
||||
},
|
||||
{
|
||||
name: "wildcard-version matches patch",
|
||||
version: "7.4.8",
|
||||
match: "7.4.*",
|
||||
matches: true,
|
||||
},
|
||||
{
|
||||
name: "sub-version matches major",
|
||||
version: "7.19.11",
|
||||
match: "7",
|
||||
matches: true,
|
||||
},
|
||||
{
|
||||
name: "sub-version matches minor",
|
||||
version: "7.55.2",
|
||||
match: "7.55",
|
||||
matches: true,
|
||||
},
|
||||
{
|
||||
name: "sub-version matches patch",
|
||||
version: "7.32.6",
|
||||
match: "7.32.6",
|
||||
matches: true,
|
||||
},
|
||||
// negative tests
|
||||
{
|
||||
name: "different number does not match",
|
||||
version: "3",
|
||||
match: "4",
|
||||
matches: false,
|
||||
},
|
||||
{
|
||||
name: "sub-version doesn't match major",
|
||||
version: "7.2.5",
|
||||
match: "8.2.5",
|
||||
matches: false,
|
||||
},
|
||||
{
|
||||
name: "sub-version doesn't match minor",
|
||||
version: "7.2.9",
|
||||
match: "7.1",
|
||||
matches: false,
|
||||
},
|
||||
{
|
||||
name: "sub-version doesn't match patch",
|
||||
version: "7.32.6",
|
||||
match: "7.32.5",
|
||||
matches: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
matches := versionMatches(test.version, test.match)
|
||||
assert.Equal(t, test.matches, matches)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package github
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
|
||||
"github.com/anchore/syft/syft/format/github/internal/model"
|
||||
"github.com/anchore/syft/syft/sbom"
|
||||
)
|
||||
|
||||
const ID sbom.FormatID = "github-json"
|
||||
|
||||
type encoder struct {
|
||||
}
|
||||
|
||||
func NewFormatEncoder() sbom.FormatEncoder {
|
||||
return encoder{}
|
||||
}
|
||||
|
||||
func (e encoder) ID() sbom.FormatID {
|
||||
return ID
|
||||
}
|
||||
|
||||
func (e encoder) Aliases() []string {
|
||||
return []string{
|
||||
"github",
|
||||
}
|
||||
}
|
||||
|
||||
func (e encoder) Version() string {
|
||||
return sbom.AnyVersion
|
||||
}
|
||||
|
||||
func (e encoder) Encode(writer io.Writer, s sbom.SBOM) error {
|
||||
bom := model.ToGithubModel(&s)
|
||||
|
||||
enc := json.NewEncoder(writer)
|
||||
enc.SetEscapeHTML(false)
|
||||
enc.SetIndent("", " ")
|
||||
|
||||
return enc.Encode(bom)
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package github
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"testing"
|
||||
|
||||
"github.com/anchore/syft/syft/format/internal/testutil"
|
||||
)
|
||||
|
||||
var updateSnapshot = flag.Bool("update-github", false, "update the *.golden files for github encoders")
|
||||
var updateImage = flag.Bool("update-image", false, "update the golden image used for image encoder testing")
|
||||
|
||||
func TestGithubDirectoryEncoder(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
testutil.AssertEncoderAgainstGoldenSnapshot(t,
|
||||
testutil.EncoderSnapshotTestConfig{
|
||||
Subject: testutil.DirectoryInput(t, dir),
|
||||
Format: NewFormatEncoder(),
|
||||
UpdateSnapshot: *updateSnapshot,
|
||||
PersistRedactionsInSnapshot: true,
|
||||
IsJSON: false,
|
||||
Redactor: redactor(dir),
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
func TestGithubImageEncoder(t *testing.T) {
|
||||
testImage := "image-simple"
|
||||
testutil.AssertEncoderAgainstGoldenImageSnapshot(t,
|
||||
testutil.ImageSnapshotTestConfig{
|
||||
Image: testImage,
|
||||
UpdateImageSnapshot: *updateImage,
|
||||
},
|
||||
testutil.EncoderSnapshotTestConfig{
|
||||
Subject: testutil.ImageInput(t, testImage),
|
||||
Format: NewFormatEncoder(),
|
||||
UpdateSnapshot: *updateSnapshot,
|
||||
PersistRedactionsInSnapshot: true,
|
||||
IsJSON: false,
|
||||
Redactor: redactor(),
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
func redactor(values ...string) testutil.Redactor {
|
||||
return testutil.NewRedactions().
|
||||
WithValuesRedacted(values...).
|
||||
WithPatternRedactors(
|
||||
map[string]string{
|
||||
// dates
|
||||
`"scanned":\s*"[^"]+"`: `"scanned":"redacted"`,
|
||||
|
||||
// image metadata
|
||||
`"syft:filesystem":\s*"[^"]+"`: `"syft:filesystem":"redacted"`,
|
||||
},
|
||||
)
|
||||
}
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package github
|
||||
package model
|
||||
|
||||
// Derived from: https://gist.github.com/reiddraper/fdab2883db0f372c146d1a750fc1c43f
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package github
|
||||
package model
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
@@ -14,8 +14,8 @@ import (
|
||||
"github.com/anchore/syft/syft/source"
|
||||
)
|
||||
|
||||
// toGithubModel converts the provided SBOM to a GitHub dependency model
|
||||
func toGithubModel(s *sbom.SBOM) DependencySnapshot {
|
||||
// ToGithubModel converts the provided SBOM to a GitHub dependency model
|
||||
func ToGithubModel(s *sbom.SBOM) DependencySnapshot {
|
||||
scanTime := time.Now().Format(time.RFC3339) // TODO is there a record of this somewhere?
|
||||
v := s.Descriptor.Version
|
||||
if v == "[not provided]" || v == "" {
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
package github
|
||||
package model
|
||||
|
||||
import (
|
||||
"testing"
|
||||
@@ -182,7 +182,7 @@ func Test_toGithubModel(t *testing.T) {
|
||||
if test.metadata != nil {
|
||||
s.Source.Metadata = test.metadata
|
||||
}
|
||||
actual := toGithubModel(&s)
|
||||
actual := ToGithubModel(&s)
|
||||
|
||||
if test.expected != nil {
|
||||
if d := cmp.Diff(*test.expected, actual, cmpopts.IgnoreFields(DependencySnapshot{}, "Scanned")); d != "" {
|
||||
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"version": 0,
|
||||
"job": {},
|
||||
"detector": {
|
||||
"name": "syft",
|
||||
"url": "https://github.com/anchore/syft",
|
||||
"version": "v0.42.0-bogus"
|
||||
},
|
||||
"metadata": {
|
||||
"syft:distro": "pkg:generic/debian@1.2.3?like=like!"
|
||||
},
|
||||
"manifests": {
|
||||
"redacted/some/path/some/path/pkg1": {
|
||||
"name": "redacted/some/path/some/path/pkg1",
|
||||
"file": {
|
||||
"source_location": "redacted/some/path/some/path/pkg1"
|
||||
},
|
||||
"resolved": {
|
||||
"": {
|
||||
"package_url": "a-purl-2",
|
||||
"relationship": "direct",
|
||||
"scope": "runtime"
|
||||
},
|
||||
"pkg:deb/debian/package-2@2.0.1": {
|
||||
"package_url": "pkg:deb/debian/package-2@2.0.1",
|
||||
"relationship": "direct",
|
||||
"scope": "runtime"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"scanned":"redacted"
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
{
|
||||
"version": 0,
|
||||
"job": {},
|
||||
"detector": {
|
||||
"name": "syft",
|
||||
"url": "https://github.com/anchore/syft",
|
||||
"version": "v0.42.0-bogus"
|
||||
},
|
||||
"metadata": {
|
||||
"syft:distro": "pkg:generic/debian@1.2.3?like=like!"
|
||||
},
|
||||
"manifests": {
|
||||
"user-image-input:/somefile-1.txt": {
|
||||
"name": "user-image-input:/somefile-1.txt",
|
||||
"file": {
|
||||
"source_location": "user-image-input:/somefile-1.txt"
|
||||
},
|
||||
"metadata": {
|
||||
"syft:filesystem":"redacted"
|
||||
},
|
||||
"resolved": {
|
||||
"": {
|
||||
"package_url": "a-purl-1",
|
||||
"relationship": "direct",
|
||||
"scope": "runtime"
|
||||
}
|
||||
}
|
||||
},
|
||||
"user-image-input:/somefile-2.txt": {
|
||||
"name": "user-image-input:/somefile-2.txt",
|
||||
"file": {
|
||||
"source_location": "user-image-input:/somefile-2.txt"
|
||||
},
|
||||
"metadata": {
|
||||
"syft:filesystem":"redacted"
|
||||
},
|
||||
"resolved": {
|
||||
"pkg:deb/debian/package-2@2.0.1": {
|
||||
"package_url": "pkg:deb/debian/package-2@2.0.1",
|
||||
"relationship": "direct",
|
||||
"scope": "runtime"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"scanned":"redacted"
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package cyclonedxutil
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"github.com/CycloneDX/cyclonedx-go"
|
||||
)
|
||||
|
||||
type Decoder struct {
|
||||
format cyclonedx.BOMFileFormat
|
||||
}
|
||||
|
||||
func NewDecoder(format cyclonedx.BOMFileFormat) Decoder {
|
||||
return Decoder{
|
||||
format: format,
|
||||
}
|
||||
}
|
||||
|
||||
func (d Decoder) Decode(reader io.ReadSeeker) (*cyclonedx.BOM, error) {
|
||||
doc := &cyclonedx.BOM{
|
||||
Components: &[]cyclonedx.Component{},
|
||||
}
|
||||
if _, err := reader.Seek(0, io.SeekStart); err != nil {
|
||||
return nil, fmt.Errorf("unable to seek to start of CycloneDX SBOM: %w", err)
|
||||
}
|
||||
err := cyclonedx.NewBOMDecoder(reader, d.format).Decode(doc)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return doc, nil
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package cyclonedxutil
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/CycloneDX/cyclonedx-go"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_missingComponentsDecode(t *testing.T) {
|
||||
bom := &cyclonedx.BOM{
|
||||
SpecVersion: cyclonedx.SpecVersion1_4,
|
||||
}
|
||||
bomBytes, _ := json.Marshal(&bom)
|
||||
dec := NewDecoder(cyclonedx.BOMFileFormatJSON)
|
||||
|
||||
_, err := dec.Decode(bytes.NewReader(bomBytes))
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package cyclonedxutil
|
||||
|
||||
import (
|
||||
"io"
|
||||
|
||||
"github.com/CycloneDX/cyclonedx-go"
|
||||
|
||||
"github.com/anchore/syft/syft/format/common/cyclonedxhelpers"
|
||||
"github.com/anchore/syft/syft/sbom"
|
||||
)
|
||||
|
||||
const DefaultVersion = "1.5"
|
||||
|
||||
type Encoder struct {
|
||||
version cyclonedx.SpecVersion
|
||||
format cyclonedx.BOMFileFormat
|
||||
}
|
||||
|
||||
func NewEncoder(version string, format cyclonedx.BOMFileFormat) (Encoder, error) {
|
||||
specVersion, err := SpecVersionFromString(version)
|
||||
if err != nil {
|
||||
return Encoder{}, err
|
||||
}
|
||||
return Encoder{
|
||||
version: specVersion,
|
||||
format: format,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (e Encoder) Encode(writer io.Writer, s sbom.SBOM) error {
|
||||
bom := cyclonedxhelpers.ToFormatModel(s)
|
||||
enc := cyclonedx.NewBOMEncoder(writer, e.format)
|
||||
enc.SetPretty(true)
|
||||
enc.SetEscapeHTML(false)
|
||||
|
||||
return enc.EncodeVersion(bom, e.version)
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package cyclonedxutil
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/CycloneDX/cyclonedx-go"
|
||||
|
||||
"github.com/anchore/syft/syft/sbom"
|
||||
)
|
||||
|
||||
const (
|
||||
XMLFormatID sbom.FormatID = "cyclonedx-xml"
|
||||
JSONFormatID sbom.FormatID = "cyclonedx-json"
|
||||
)
|
||||
|
||||
func SupportedVersions(id sbom.FormatID) []string {
|
||||
versions := []string{
|
||||
"1.2",
|
||||
"1.3",
|
||||
"1.4",
|
||||
"1.5",
|
||||
}
|
||||
|
||||
if id != JSONFormatID {
|
||||
// JSON format not supported for version < 1.2
|
||||
versions = append([]string{"1.0", "1.1"}, versions...)
|
||||
}
|
||||
|
||||
return versions
|
||||
}
|
||||
|
||||
func SpecVersionFromString(v string) (cyclonedx.SpecVersion, error) {
|
||||
switch v {
|
||||
case "1.0":
|
||||
return cyclonedx.SpecVersion1_0, nil
|
||||
case "1.1":
|
||||
return cyclonedx.SpecVersion1_1, nil
|
||||
case "1.2":
|
||||
return cyclonedx.SpecVersion1_2, nil
|
||||
case "1.3":
|
||||
return cyclonedx.SpecVersion1_3, nil
|
||||
case "1.4":
|
||||
return cyclonedx.SpecVersion1_4, nil
|
||||
case "1.5":
|
||||
return cyclonedx.SpecVersion1_5, nil
|
||||
}
|
||||
return -1, fmt.Errorf("unsupported CycloneDX version %q", v)
|
||||
}
|
||||
|
||||
func VersionFromSpecVersion(spec cyclonedx.SpecVersion) string {
|
||||
switch spec {
|
||||
case cyclonedx.SpecVersion1_0:
|
||||
return "1.0"
|
||||
case cyclonedx.SpecVersion1_1:
|
||||
return "1.1"
|
||||
case cyclonedx.SpecVersion1_2:
|
||||
return "1.2"
|
||||
case cyclonedx.SpecVersion1_3:
|
||||
return "1.3"
|
||||
case cyclonedx.SpecVersion1_4:
|
||||
return "1.4"
|
||||
case cyclonedx.SpecVersion1_5:
|
||||
return "1.5"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package spdxutil
|
||||
|
||||
import (
|
||||
"github.com/anchore/syft/syft/sbom"
|
||||
)
|
||||
|
||||
const DefaultVersion = "2.3"
|
||||
|
||||
const (
|
||||
JSONFormatID sbom.FormatID = "spdx-json"
|
||||
TagValueFormatID sbom.FormatID = "spdx-tag-value"
|
||||
)
|
||||
|
||||
func SupportedVersions(id sbom.FormatID) []string {
|
||||
versions := []string{
|
||||
"2.2",
|
||||
"2.3",
|
||||
}
|
||||
|
||||
if id != JSONFormatID {
|
||||
// JSON format is not supported in v2.1
|
||||
return append([]string{"2.1"}, versions...)
|
||||
}
|
||||
|
||||
return versions
|
||||
}
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package testutils
|
||||
package testutil
|
||||
|
||||
import (
|
||||
"os"
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package testutils
|
||||
package testutil
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
+30
-1
@@ -1,9 +1,12 @@
|
||||
package testutils
|
||||
package testutil
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/anchore/stereoscope/pkg/filetree"
|
||||
"github.com/anchore/stereoscope/pkg/image"
|
||||
@@ -25,6 +28,8 @@ func ImageInput(t testing.TB, testImage string, options ...ImageOption) sbom.SBO
|
||||
opt(&cfg)
|
||||
}
|
||||
|
||||
defer changeToDirectoryWithGoldenFixture(t, testImage)()
|
||||
|
||||
switch cfg.fromSnapshot {
|
||||
case true:
|
||||
img = imagetest.GetGoldenFixtureImage(t, testImage)
|
||||
@@ -65,6 +70,30 @@ func ImageInput(t testing.TB, testImage string, options ...ImageOption) sbom.SBO
|
||||
}
|
||||
}
|
||||
|
||||
func changeToDirectoryWithGoldenFixture(t testing.TB, testImage string) func() {
|
||||
// check if test fixture exists... if not, check if there is a shared fixture relative to this dir
|
||||
fn := func() {}
|
||||
|
||||
path := filepath.Join("test-fixtures", testImage)
|
||||
if _, err := os.Stat(path); err != nil {
|
||||
// change dir, restore as defer
|
||||
wd, err := os.Getwd()
|
||||
require.NoError(t, err)
|
||||
fn = func() {
|
||||
require.NoError(t, os.Chdir(wd))
|
||||
}
|
||||
|
||||
// change dir to the testutil dir
|
||||
require.NoError(t, os.Chdir(filepath.Join(wd, "..", "internal", "testutil")))
|
||||
t.Cleanup(fn)
|
||||
|
||||
if _, err := os.Stat(path); err != nil {
|
||||
t.Fatalf("unable to find test fixture: %s", path)
|
||||
}
|
||||
}
|
||||
return fn
|
||||
}
|
||||
|
||||
func populateImageCatalog(catalog *pkg.Collection, img *image.Image) {
|
||||
_, ref1, _ := img.SquashedTree().File("/somefile-1.txt", filetree.FollowBasenameLinks)
|
||||
_, ref2, _ := img.SquashedTree().File("/somefile-2.txt", filetree.FollowBasenameLinks)
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package testutils
|
||||
package testutil
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
+7
-4
@@ -1,4 +1,4 @@
|
||||
package testutils
|
||||
package testutil
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
@@ -27,7 +27,7 @@ func FromSnapshot() ImageOption {
|
||||
|
||||
type EncoderSnapshotTestConfig struct {
|
||||
Subject sbom.SBOM
|
||||
Format sbom.Format
|
||||
Format sbom.FormatEncoder
|
||||
UpdateSnapshot bool
|
||||
PersistRedactionsInSnapshot bool
|
||||
IsJSON bool
|
||||
@@ -71,8 +71,9 @@ func AssertEncoderAgainstGoldenSnapshot(t *testing.T, cfg EncoderSnapshotTestCon
|
||||
|
||||
func requireEqual(t *testing.T, expected any, actual any) {
|
||||
if diff := cmp.Diff(expected, actual); diff != "" {
|
||||
t.Logf("expected: %s", expected)
|
||||
t.Logf("actual: %s", actual)
|
||||
// uncomment to debug
|
||||
// t.Logf("expected: %s", expected)
|
||||
// t.Logf("actual: %s", actual)
|
||||
t.Fatalf("mismatched output: %s", diff)
|
||||
}
|
||||
}
|
||||
@@ -84,6 +85,8 @@ type ImageSnapshotTestConfig struct {
|
||||
|
||||
func AssertEncoderAgainstGoldenImageSnapshot(t *testing.T, imgCfg ImageSnapshotTestConfig, cfg EncoderSnapshotTestConfig) {
|
||||
if imgCfg.UpdateImageSnapshot {
|
||||
defer changeToDirectoryWithGoldenFixture(t, imgCfg.Image)()
|
||||
|
||||
// grab the latest image contents and persist
|
||||
imagetest.UpdateGoldenFixtureImage(t, imgCfg.Image)
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user