mirror of
https://github.com/anchore/syft.git
synced 2026-10-30 22:12:28 +01:00
* fix(spdx): write a name for the root package when the source has none
A source read from another SBOM (a purl list, a CycloneDX document
without metadata.component, an SPDX document without a DESCRIBES
relationship) can have no name. The SPDX encoder then wrote the document
root package with an empty PackageName, which SPDX requires. In
tag-value output the PackageName line is omitted, so the root package's
fields land in the previous section and syft cannot read its own output
back ("received unknown tag SPDXID in CreationInfo section").
Fall back to the document name for the root package, and make the
document name fall back to "unknown" for every source type rather than
only for unrecognized ones.
Signed-off-by: Tony <tjagodka@gmail.com>
* fix(spdx): drop syft's root package on decode for every SPDX version
- the root package name now comes from one fallback chain (source name, image repo name, source ID, document name), so the root purl no longer gets an empty name for a source decoded from CycloneDX
- decoding strips the root syft writes for an unknown source, so `syft convert` round trips stop adding an `unknown` package each pass
- for SPDX 2.1 and 2.2, which have no `primaryPackagePurpose`, the purpose is taken from the source type in syft's `DocumentRoot-<type>-` ID
**behavior change:** decoding a syft-written SPDX 2.1/2.2 document no longer returns the root as a package or its `CONTAINS` relationships, matching 2.3.
Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
---------
Signed-off-by: Tony <tjagodka@gmail.com>
Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
Co-authored-by: Alex Goodman <wagoodman@users.noreply.github.com>
287 lines
7.8 KiB
Go
287 lines
7.8 KiB
Go
package spdxjson
|
|
|
|
import (
|
|
"bytes"
|
|
"flag"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/anchore/syft/syft/artifact"
|
|
"github.com/anchore/syft/syft/file"
|
|
"github.com/anchore/syft/syft/format/internal/spdxutil"
|
|
"github.com/anchore/syft/syft/format/internal/testutil"
|
|
"github.com/anchore/syft/syft/pkg"
|
|
"github.com/anchore/syft/syft/sbom"
|
|
)
|
|
|
|
var updateSnapshot = flag.Bool("update-spdx-json", false, "update the *.golden files for spdx-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 {
|
|
cfg := DefaultEncoderConfig()
|
|
cfg.Pretty = true
|
|
|
|
enc, err := NewFormatEncoderWithConfig(cfg)
|
|
require.NoError(t, err)
|
|
return enc
|
|
}
|
|
|
|
func TestPrettyOutput(t *testing.T) {
|
|
run := func(opt bool) string {
|
|
enc, err := NewFormatEncoderWithConfig(EncoderConfig{
|
|
Version: spdxutil.DefaultVersion,
|
|
Pretty: opt,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
dir := t.TempDir()
|
|
s := testutil.DirectoryInput(t, dir)
|
|
|
|
var buffer bytes.Buffer
|
|
err = enc.Encode(&buffer, s)
|
|
require.NoError(t, err)
|
|
|
|
return strings.TrimSpace(buffer.String())
|
|
}
|
|
|
|
t.Run("pretty", func(t *testing.T) {
|
|
actual := run(true)
|
|
assert.Contains(t, actual, "\n")
|
|
})
|
|
|
|
t.Run("compact", func(t *testing.T) {
|
|
actual := run(false)
|
|
assert.NotContains(t, actual, "\n")
|
|
})
|
|
}
|
|
|
|
func TestEscapeHTML(t *testing.T) {
|
|
dir := t.TempDir()
|
|
s := testutil.DirectoryInput(t, dir)
|
|
s.Artifacts.Packages.Add(pkg.Package{
|
|
Name: "<html-package>",
|
|
})
|
|
|
|
// by default we do not escape HTML
|
|
t.Run("default", func(t *testing.T) {
|
|
cfg := DefaultEncoderConfig()
|
|
|
|
enc, err := NewFormatEncoderWithConfig(cfg)
|
|
require.NoError(t, err)
|
|
|
|
var buffer bytes.Buffer
|
|
err = enc.Encode(&buffer, s)
|
|
require.NoError(t, err)
|
|
|
|
actual := buffer.String()
|
|
assert.Contains(t, actual, "<html-package>")
|
|
assert.NotContains(t, actual, "\\u003chtml-package\\u003e")
|
|
})
|
|
|
|
}
|
|
|
|
func TestSPDXJSONDirectoryEncoder(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 TestSPDXJSONImageEncoder(t *testing.T) {
|
|
testImage := "image-simple"
|
|
testutil.AssertEncoderAgainstGoldenImageSnapshot(t,
|
|
testutil.ImageSnapshotTestConfig{
|
|
Image: testImage,
|
|
UpdateImageSnapshot: *updateImage,
|
|
},
|
|
testutil.EncoderSnapshotTestConfig{
|
|
Subject: testutil.ImageInput(t, testImage, testutil.FromSnapshot()),
|
|
Format: getEncoder(t),
|
|
UpdateSnapshot: *updateSnapshot,
|
|
PersistRedactionsInSnapshot: true,
|
|
IsJSON: true,
|
|
Redactor: redactor(),
|
|
},
|
|
)
|
|
}
|
|
|
|
func TestSPDX22JSONRequredProperties(t *testing.T) {
|
|
cfg := DefaultEncoderConfig()
|
|
cfg.Pretty = true
|
|
cfg.Version = "2.2"
|
|
|
|
enc, err := NewFormatEncoderWithConfig(cfg)
|
|
require.NoError(t, err)
|
|
|
|
coords := file.Coordinates{
|
|
RealPath: "/some/file",
|
|
FileSystemID: "ac897d978b6c38749a1",
|
|
}
|
|
|
|
p1 := pkg.Package{
|
|
Name: "files-analyzed-true",
|
|
Version: "v1",
|
|
Locations: file.NewLocationSet(file.NewLocation(coords.RealPath)),
|
|
Licenses: pkg.LicenseSet{},
|
|
Language: pkg.Java,
|
|
Metadata: pkg.JavaArchive{
|
|
ArchiveDigests: []file.Digest{
|
|
{
|
|
Algorithm: "sha256",
|
|
Value: "a9b87321a9879c79d87987987a97c97b9789ce978dffea987",
|
|
},
|
|
},
|
|
Parent: nil,
|
|
},
|
|
}
|
|
p1.SetID()
|
|
|
|
p2 := pkg.Package{
|
|
Name: "files-analyzed-false",
|
|
Version: "v2",
|
|
}
|
|
p2.SetID()
|
|
|
|
testutil.AssertEncoderAgainstGoldenSnapshot(t,
|
|
testutil.EncoderSnapshotTestConfig{
|
|
Subject: sbom.SBOM{
|
|
Artifacts: sbom.Artifacts{
|
|
Packages: pkg.NewCollection(p1, p2),
|
|
FileDigests: map[file.Coordinates][]file.Digest{
|
|
coords: {
|
|
{
|
|
Algorithm: "sha1",
|
|
Value: "3b4ab96c371d913e2a88c269844b6c5fb5cbe761",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
Relationships: []artifact.Relationship{
|
|
{
|
|
From: p1,
|
|
To: coords,
|
|
Type: artifact.ContainsRelationship,
|
|
},
|
|
},
|
|
},
|
|
Format: enc,
|
|
UpdateSnapshot: *updateSnapshot,
|
|
PersistRedactionsInSnapshot: true,
|
|
IsJSON: true,
|
|
Redactor: redactor(),
|
|
},
|
|
)
|
|
}
|
|
|
|
func TestSPDXRelationshipOrder(t *testing.T) {
|
|
testImage := "image-simple"
|
|
|
|
s := testutil.ImageInput(t, testImage, testutil.FromSnapshot())
|
|
testutil.AddSampleFileRelationships(&s)
|
|
|
|
testutil.AssertEncoderAgainstGoldenImageSnapshot(t,
|
|
testutil.ImageSnapshotTestConfig{
|
|
Image: testImage,
|
|
UpdateImageSnapshot: *updateImage,
|
|
},
|
|
testutil.EncoderSnapshotTestConfig{
|
|
Subject: s,
|
|
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{
|
|
// each SBOM reports the time it was generated, which is not useful during snapshot testing
|
|
`"created":\s+"[^"]*"`: `"created":"redacted"`,
|
|
|
|
// each SBOM reports a unique documentNamespace when generated, this is not useful for snapshot testing
|
|
`"documentNamespace":\s+"[^"]*"`: `"documentNamespace":"redacted"`,
|
|
|
|
// spdx3 IDs are URI with the documentnamespace spdxId containing a UID; namespace is the equivalent documentNamespace with the same value
|
|
`"spdxId":\s+"[^"]*"`: `"spdxId":"https://redacted"`,
|
|
`"namespace":\s+"[^"]*"`: `"namespace":"https://redacted/"`,
|
|
|
|
// the license list will be updated periodically, the value here should not be directly tested in snapshot tests
|
|
`"licenseListVersion":\s+"[^"]*"`: `"licenseListVersion":"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()))
|
|
assert.Equal(t, enc.ID(), id)
|
|
assert.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)
|
|
|
|
assert.Equal(t, enc.ID(), id)
|
|
assert.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
|
|
}
|