diff --git a/internal/presenter/packages/spdx_json_presenter.go b/internal/presenter/packages/spdx_json_presenter.go index 0e34f4d9c3a..ded9a4a65dc 100644 --- a/internal/presenter/packages/spdx_json_presenter.go +++ b/internal/presenter/packages/spdx_json_presenter.go @@ -4,6 +4,8 @@ import ( "encoding/json" "fmt" "io" + "path" + "strings" "time" "github.com/anchore/syft/internal" @@ -12,8 +14,11 @@ import ( "github.com/anchore/syft/internal/version" "github.com/anchore/syft/syft/pkg" "github.com/anchore/syft/syft/source" + "github.com/google/uuid" ) +const anchoreNamespace = "https://anchore.com/syft" + // SPDXJsonPresenter is a SPDX presentation object for the syft results (see https://github.com/spdx/spdx-spec) type SPDXJsonPresenter struct { catalog *pkg.Catalog @@ -41,14 +46,25 @@ func (pres *SPDXJsonPresenter) Present(output io.Writer) error { // newSPDXJsonDocument creates and populates a new JSON document struct that follows the SPDX 2.2 spec from the given cataloging results. func newSPDXJsonDocument(catalog *pkg.Catalog, srcMetadata source.Metadata) spdx22.Document { - var name string + uniqueID := uuid.Must(uuid.NewRandom()) + + var name, input, identifier string switch srcMetadata.Scheme { case source.ImageScheme: - name = srcMetadata.ImageMetadata.UserInput + name = cleanSPDXName(srcMetadata.ImageMetadata.UserInput) + input = "image" case source.DirectoryScheme: - name = srcMetadata.Path + name = cleanSPDXName(srcMetadata.Path) + input = "dir" + } + + if name != "." { + identifier = path.Join(input, fmt.Sprintf("%s-%s", name, uniqueID.String())) + } else { + identifier = path.Join(input, uniqueID.String()) } + namespace := path.Join(anchoreNamespace, identifier) packages, files, relationships := newSPDXJsonElements(catalog) return spdx22.Document{ @@ -67,7 +83,7 @@ func newSPDXJsonDocument(catalog *pkg.Catalog, srcMetadata source.Metadata) spdx LicenseListVersion: spdxlicense.Version, }, DataLicense: "CC0-1.0", - DocumentNamespace: fmt.Sprintf("https://anchore.com/syft/image/%s", srcMetadata.ImageMetadata.UserInput), + DocumentNamespace: namespace, Packages: packages, Files: files, Relationships: relationships, @@ -113,3 +129,14 @@ func newSPDXJsonElements(catalog *pkg.Catalog) ([]spdx22.Package, []spdx22.File, return packages, files, relationships } + +func cleanSPDXName(name string) string { + // remove # according to specification + name = strings.Replace(name, "#", "-", -1) + + // remove : for url construction + name = strings.Replace(name, ":", "-", -1) + + // clean relative pathing + return path.Clean(name) +} diff --git a/internal/presenter/packages/spdx_json_presenter_test.go b/internal/presenter/packages/spdx_json_presenter_test.go index f0053fd70df..7d6a7f7381d 100644 --- a/internal/presenter/packages/spdx_json_presenter_test.go +++ b/internal/presenter/packages/spdx_json_presenter_test.go @@ -31,6 +31,10 @@ func TestSPDXJSONImagePresenter(t *testing.T) { func spdxJsonRedactor(s []byte) []byte { // each SBOM reports the time it was generated, which is not useful during snapshot testing s = regexp.MustCompile(`"created": .*`).ReplaceAll(s, []byte("redacted")) + + // each SBOM reports a unique documentNamespace when generated, this is not useful for snapshot testing + s = regexp.MustCompile(`"documentNamespace": .*`).ReplaceAll(s, []byte("redacted")) + // the license list will be updated periodically, the value here should not be directly tested in snapshot tests return regexp.MustCompile(`"licenseListVersion": .*`).ReplaceAll(s, []byte("redacted")) }