diff --git a/internal/formats/common/spdxhelpers/document_name.go b/internal/formats/common/spdxhelpers/document_name.go new file mode 100644 index 000000000..d40a5e606 --- /dev/null +++ b/internal/formats/common/spdxhelpers/document_name.go @@ -0,0 +1,32 @@ +package spdxhelpers + +import ( + "path" + "strings" + + "github.com/anchore/syft/syft/source" + "github.com/google/uuid" +) + +func DocumentName(srcMetadata source.Metadata) string { + switch srcMetadata.Scheme { + case source.ImageScheme: + return cleanSPDXName(srcMetadata.ImageMetadata.UserInput) + case source.DirectoryScheme: + return cleanSPDXName(srcMetadata.Path) + } + + // TODO: is this alright? + return uuid.Must(uuid.NewRandom()).String() +} + +func cleanSPDXName(name string) string { + // remove # according to specification + name = strings.ReplaceAll(name, "#", "-") + + // remove : for url construction + name = strings.ReplaceAll(name, ":", "-") + + // clean relative pathing + return path.Clean(name) +} diff --git a/internal/formats/common/spdxhelpers/document_namespace.go b/internal/formats/common/spdxhelpers/document_namespace.go new file mode 100644 index 000000000..73dbe0b93 --- /dev/null +++ b/internal/formats/common/spdxhelpers/document_namespace.go @@ -0,0 +1,29 @@ +package spdxhelpers + +import ( + "fmt" + "path" + + "github.com/anchore/syft/syft/source" + "github.com/google/uuid" +) + +const SyftDocumentNamespace = "https://anchore.com/syft" + +func DocumentNamespace(name string, srcMetadata source.Metadata) string { + input := "unknown-source-type" + switch srcMetadata.Scheme { + case source.ImageScheme: + input = "image" + case source.DirectoryScheme: + input = "dir" + } + + uniqueID := uuid.Must(uuid.NewRandom()) + identifier := path.Join(input, uniqueID.String()) + if name != "." { + identifier = path.Join(input, fmt.Sprintf("%s-%s", name, uniqueID.String())) + } + + return path.Join(SyftDocumentNamespace, identifier) +} diff --git a/internal/formats/spdx22json/decoder.go b/internal/formats/spdx22json/decoder.go new file mode 100644 index 000000000..fe88b2705 --- /dev/null +++ b/internal/formats/spdx22json/decoder.go @@ -0,0 +1,22 @@ +package spdx22json + +import ( + "encoding/json" + "fmt" + "io" + + "github.com/anchore/syft/internal/formats/spdx22json/model" + "github.com/anchore/syft/syft/sbom" +) + +func decoder(reader io.Reader) (*sbom.SBOM, error) { + dec := json.NewDecoder(reader) + + var doc model.Document + err := dec.Decode(&doc) + if err != nil { + return nil, fmt.Errorf("unable to decode syft-json: %w", err) + } + + return toSyftModel(doc) +} diff --git a/internal/formats/spdx22json/decoder_test.go b/internal/formats/spdx22json/decoder_test.go new file mode 100644 index 000000000..d171da9b5 --- /dev/null +++ b/internal/formats/spdx22json/decoder_test.go @@ -0,0 +1,46 @@ +package spdx22json + +import ( + "bytes" + "strings" + "testing" + + "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" + originalSBOM := testutils.ImageInput(t, testImage) + + var buf bytes.Buffer + assert.NoError(t, encoder(&buf, originalSBOM)) + + actualSBOM, err := decoder(bytes.NewReader(buf.Bytes())) + assert.NoError(t, err) + + for _, d := range deep.Equal(originalSBOM.Source, actualSBOM.Source) { + t.Errorf("metadata difference: %+v", d) + } + + actualPackages := actualSBOM.Artifacts.PackageCatalog.Sorted() + for idx, p := range originalSBOM.Artifacts.PackageCatalog.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 + } + + 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, " != []") { + // semantically the same + continue + } + t.Errorf("package difference (%s): %+v", p.Name, d) + } + } +} diff --git a/internal/formats/spdx22json/encoder.go b/internal/formats/spdx22json/encoder.go index 8159fe130..d9394459a 100644 --- a/internal/formats/spdx22json/encoder.go +++ b/internal/formats/spdx22json/encoder.go @@ -7,8 +7,6 @@ import ( "github.com/anchore/syft/syft/sbom" ) -const anchoreNamespace = "https://anchore.com/syft" - func encoder(output io.Writer, s sbom.SBOM) error { doc := toFormatModel(s) diff --git a/internal/formats/spdx22json/to_format_model.go b/internal/formats/spdx22json/to_format_model.go index d6724ae91..ad100b61a 100644 --- a/internal/formats/spdx22json/to_format_model.go +++ b/internal/formats/spdx22json/to_format_model.go @@ -2,8 +2,6 @@ package spdx22json import ( "fmt" - "path" - "strings" "time" "github.com/anchore/syft/syft/sbom" @@ -14,13 +12,11 @@ import ( "github.com/anchore/syft/internal/spdxlicense" "github.com/anchore/syft/internal/version" "github.com/anchore/syft/syft/pkg" - "github.com/anchore/syft/syft/source" - "github.com/google/uuid" ) // toFormatModel creates and populates a new JSON document struct that follows the SPDX 2.2 spec from the given cataloging results. func toFormatModel(s sbom.SBOM) model.Document { - name := documentName(s.Source) + name := spdxhelpers.DocumentName(s.Source) packages, files, relationships := extractFromCatalog(s.Artifacts.PackageCatalog) return model.Document{ @@ -39,43 +35,13 @@ func toFormatModel(s sbom.SBOM) model.Document { LicenseListVersion: spdxlicense.Version, }, DataLicense: "CC0-1.0", - DocumentNamespace: documentNamespace(name, s.Source), + DocumentNamespace: spdxhelpers.DocumentNamespace(name, s.Source), Packages: packages, Files: files, Relationships: relationships, } } -func documentName(srcMetadata source.Metadata) string { - switch srcMetadata.Scheme { - case source.ImageScheme: - return cleanSPDXName(srcMetadata.ImageMetadata.UserInput) - case source.DirectoryScheme: - return cleanSPDXName(srcMetadata.Path) - } - - // TODO: is this alright? - return uuid.Must(uuid.NewRandom()).String() -} - -func documentNamespace(name string, srcMetadata source.Metadata) string { - input := "unknown-source-type" - switch srcMetadata.Scheme { - case source.ImageScheme: - input = "image" - case source.DirectoryScheme: - input = "dir" - } - - uniqueID := uuid.Must(uuid.NewRandom()) - identifier := path.Join(input, uniqueID.String()) - if name != "." { - identifier = path.Join(input, fmt.Sprintf("%s-%s", name, uniqueID.String())) - } - - return path.Join(anchoreNamespace, identifier) -} - func extractFromCatalog(catalog *pkg.Catalog) ([]model.Package, []model.File, []model.Relationship) { packages := make([]model.Package, 0) relationships := make([]model.Relationship, 0) @@ -115,14 +81,3 @@ func extractFromCatalog(catalog *pkg.Catalog) ([]model.Package, []model.File, [] return packages, files, relationships } - -func cleanSPDXName(name string) string { - // remove # according to specification - name = strings.ReplaceAll(name, "#", "-") - - // remove : for url construction - name = strings.ReplaceAll(name, ":", "-") - - // clean relative pathing - return path.Clean(name) -} diff --git a/internal/formats/spdx22json/to_syft_model.go b/internal/formats/spdx22json/to_syft_model.go new file mode 100644 index 000000000..eed94b7eb --- /dev/null +++ b/internal/formats/spdx22json/to_syft_model.go @@ -0,0 +1,10 @@ +package spdx22json + +import ( + "github.com/anchore/syft/internal/formats/spdx22json/model" + "github.com/anchore/syft/syft/sbom" +) + +func toSyftModel(doc model.Document) (*sbom.SBOM, error) { + +}