diff --git a/internal/formats/syftjson/decoder.go b/internal/formats/syftjson/decoder.go new file mode 100644 index 00000000000..f1ee7baa90b --- /dev/null +++ b/internal/formats/syftjson/decoder.go @@ -0,0 +1,24 @@ +package syftjson + +import ( + "encoding/json" + "fmt" + "io" + + "github.com/anchore/syft/internal/formats/syftjson/model" + "github.com/anchore/syft/syft/distro" + "github.com/anchore/syft/syft/pkg" + "github.com/anchore/syft/syft/source" +) + +func decoder(reader io.Reader) (*pkg.Catalog, *source.Metadata, *distro.Distro, error) { + dec := json.NewDecoder(reader) + + var doc model.Document + err := dec.Decode(&doc) + if err != nil { + return nil, nil, nil, fmt.Errorf("unable to decode syft-json: %w", err) + } + + return toSyftModel(doc) +} diff --git a/internal/formats/syftjson/decoder_test.go b/internal/formats/syftjson/decoder_test.go new file mode 100644 index 00000000000..f60ccb684cf --- /dev/null +++ b/internal/formats/syftjson/decoder_test.go @@ -0,0 +1,50 @@ +package syftjson + +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" + originalCatalog, originalMetadata, _ := testutils.ImageInput(t, testImage) + + var buf bytes.Buffer + assert.NoError(t, encoder(&buf, originalCatalog, &originalMetadata, nil)) + + actualCatalog, actualMetadata, _, err := decoder(bytes.NewReader(buf.Bytes())) + assert.NoError(t, err) + + for _, d := range deep.Equal(originalMetadata, *actualMetadata) { + t.Errorf("metadata difference: %+v", d) + } + + actualPackages := actualCatalog.Sorted() + for idx, p := range originalCatalog.Sorted() { + if !assert.Equal(t, p.Name, actualPackages[idx].Name) { + t.Errorf("different package at idx=%d: %s vs %s", idx, p.Name, actualPackages[idx].Name) + continue + } + + // ids will never be equal + p.ID = "" + actualPackages[idx].ID = "" + + for _, d := range deep.Equal(*p, *actualPackages[idx]) { + if strings.Contains(d, ".VirtualPath: ") { + // location.Virtual path is not exposed in the json output + continue + } + if strings.HasSuffix(d, " != []") { + // semantically the same + continue + } + t.Errorf("package difference (%s): %+v", p.Name, d) + } + } +} diff --git a/internal/formats/syftjson/encoder.go b/internal/formats/syftjson/encoder.go new file mode 100644 index 00000000000..533961a1e49 --- /dev/null +++ b/internal/formats/syftjson/encoder.go @@ -0,0 +1,23 @@ +package syftjson + +import ( + "encoding/json" + "io" + + "github.com/anchore/syft/syft/distro" + + "github.com/anchore/syft/syft/pkg" + "github.com/anchore/syft/syft/source" +) + +func encoder(output io.Writer, catalog *pkg.Catalog, srcMetadata *source.Metadata, d *distro.Distro) error { + // TODO: application config not available yet + doc := ToFormatModel(catalog, srcMetadata, d, nil) + + enc := json.NewEncoder(output) + // prevent > and < from being escaped in the payload + enc.SetEscapeHTML(false) + enc.SetIndent("", " ") + + return enc.Encode(&doc) +} diff --git a/internal/formats/syftjson/format.go b/internal/formats/syftjson/format.go new file mode 100644 index 00000000000..bc3f5164289 --- /dev/null +++ b/internal/formats/syftjson/format.go @@ -0,0 +1,12 @@ +package syftjson + +import "github.com/anchore/syft/syft/format" + +func Format() format.Format { + return format.NewFormat( + format.JSONOption, + encoder, + decoder, + validator, + ) +} diff --git a/internal/formats/syftjson/model/distro.go b/internal/formats/syftjson/model/distro.go new file mode 100644 index 00000000000..b136f2505a7 --- /dev/null +++ b/internal/formats/syftjson/model/distro.go @@ -0,0 +1,8 @@ +package model + +// Distro provides information about a detected Linux Distro. +type Distro struct { + Name string `json:"name"` // Name of the Linux distribution + Version string `json:"version"` // Version of the Linux distribution (major or major.minor version) + IDLike string `json:"idLike"` // the ID_LIKE field found within the /etc/os-release file +} diff --git a/internal/formats/syftjson/model/document.go b/internal/formats/syftjson/model/document.go new file mode 100644 index 00000000000..71e76f62de1 --- /dev/null +++ b/internal/formats/syftjson/model/document.go @@ -0,0 +1,23 @@ +package model + +// Document represents the syft cataloging findings as a JSON document +type Document struct { + Artifacts []Package `json:"artifacts"` // Artifacts is the list of packages discovered and placed into the catalog + ArtifactRelationships []Relationship `json:"artifactRelationships"` + Source Source `json:"source"` // Source represents the original object that was cataloged + Distro Distro `json:"distro"` // Distro represents the Linux distribution that was detected from the source + Descriptor Descriptor `json:"descriptor"` // Descriptor is a block containing self-describing information about syft + Schema Schema `json:"schema"` // Schema is a block reserved for defining the version for the shape of this JSON document and where to find the schema document to validate the shape +} + +// Descriptor describes what created the document as well as surrounding metadata +type Descriptor struct { + Name string `json:"name"` + Version string `json:"version"` + Configuration interface{} `json:"configuration,omitempty"` +} + +type Schema struct { + Version string `json:"version"` + URL string `json:"url"` +} diff --git a/internal/formats/syftjson/model/package.go b/internal/formats/syftjson/model/package.go new file mode 100644 index 00000000000..9ee46806246 --- /dev/null +++ b/internal/formats/syftjson/model/package.go @@ -0,0 +1,122 @@ +package model + +import ( + "encoding/json" + "fmt" + + "github.com/anchore/syft/internal/log" + "github.com/anchore/syft/syft/pkg" + "github.com/anchore/syft/syft/source" +) + +// Package represents a pkg.Package object specialized for JSON marshaling and unmarshaling. +type Package struct { + PackageBasicData + PackageCustomData +} + +// PackageBasicData contains non-ambiguous values (type-wise) from pkg.Package. +type PackageBasicData struct { + ID string `json:"id"` + Name string `json:"name"` + Version string `json:"version"` + Type pkg.Type `json:"type"` + FoundBy string `json:"foundBy"` + Locations []source.Location `json:"locations"` + Licenses []string `json:"licenses"` + Language pkg.Language `json:"language"` + CPEs []string `json:"cpes"` + PURL string `json:"purl"` +} + +// PackageCustomData contains ambiguous values (type-wise) from pkg.Package. +type PackageCustomData struct { + MetadataType pkg.MetadataType `json:"metadataType"` + Metadata interface{} `json:"metadata"` +} + +// packageMetadataUnpacker is all values needed from Package to disambiguate ambiguous fields during json unmarshaling. +type packageMetadataUnpacker struct { + MetadataType pkg.MetadataType `json:"metadataType"` + Metadata json.RawMessage `json:"metadata"` +} + +func (p *packageMetadataUnpacker) String() string { + return fmt.Sprintf("metadataType: %s, metadata: %s", p.MetadataType, string(p.Metadata)) +} + +// UnmarshalJSON is a custom unmarshaller for handling basic values and values with ambiguous types. +func (p *Package) UnmarshalJSON(b []byte) error { + var basic PackageBasicData + if err := json.Unmarshal(b, &basic); err != nil { + return err + } + p.PackageBasicData = basic + + var unpacker packageMetadataUnpacker + if err := json.Unmarshal(b, &unpacker); err != nil { + log.Warnf("failed to unmarshall into packageMetadataUnpacker: %v", err) + return err + } + + p.MetadataType = unpacker.MetadataType + + switch p.MetadataType { + case pkg.ApkMetadataType: + var payload pkg.ApkMetadata + if err := json.Unmarshal(unpacker.Metadata, &payload); err != nil { + return err + } + p.Metadata = payload + case pkg.RpmdbMetadataType: + var payload pkg.RpmdbMetadata + if err := json.Unmarshal(unpacker.Metadata, &payload); err != nil { + return err + } + p.Metadata = payload + case pkg.DpkgMetadataType: + var payload pkg.DpkgMetadata + if err := json.Unmarshal(unpacker.Metadata, &payload); err != nil { + return err + } + p.Metadata = payload + case pkg.JavaMetadataType: + var payload pkg.JavaMetadata + if err := json.Unmarshal(unpacker.Metadata, &payload); err != nil { + return err + } + p.Metadata = payload + case pkg.RustCargoPackageMetadataType: + var payload pkg.CargoPackageMetadata + if err := json.Unmarshal(unpacker.Metadata, &payload); err != nil { + return err + } + p.Metadata = payload + case pkg.GemMetadataType: + var payload pkg.GemMetadata + if err := json.Unmarshal(unpacker.Metadata, &payload); err != nil { + return err + } + p.Metadata = payload + case pkg.KbPackageMetadataType: + var payload pkg.KbPackageMetadata + if err := json.Unmarshal(unpacker.Metadata, &payload); err != nil { + return err + } + p.Metadata = payload + case pkg.PythonPackageMetadataType: + var payload pkg.PythonPackageMetadata + if err := json.Unmarshal(unpacker.Metadata, &payload); err != nil { + return err + } + p.Metadata = payload + case pkg.NpmPackageJSONMetadataType: + var payload pkg.NpmPackageJSONMetadata + if err := json.Unmarshal(unpacker.Metadata, &payload); err != nil { + return err + } + p.Metadata = payload + } + + return nil +} diff --git a/internal/formats/syftjson/model/relationship.go b/internal/formats/syftjson/model/relationship.go new file mode 100644 index 00000000000..8820156bdc8 --- /dev/null +++ b/internal/formats/syftjson/model/relationship.go @@ -0,0 +1,8 @@ +package model + +type Relationship struct { + Parent string `json:"parent"` + Child string `json:"child"` + Type string `json:"type"` + Metadata interface{} `json:"metadata"` +} diff --git a/internal/formats/syftjson/model/source.go b/internal/formats/syftjson/model/source.go new file mode 100644 index 00000000000..4e6de60dc5d --- /dev/null +++ b/internal/formats/syftjson/model/source.go @@ -0,0 +1,45 @@ +package model + +import ( + "encoding/json" + "fmt" + + "github.com/anchore/syft/syft/source" +) + +// Source object represents the thing that was cataloged +type Source struct { + Type string `json:"type"` + Target interface{} `json:"target"` +} + +// sourceUnpacker is used to unmarshal Source objects +type sourceUnpacker struct { + Type string `json:"type"` + Target json.RawMessage `json:"target"` +} + +// UnmarshalJSON populates a source object from JSON bytes. +func (s *Source) UnmarshalJSON(b []byte) error { + var unpacker sourceUnpacker + if err := json.Unmarshal(b, &unpacker); err != nil { + return err + } + + s.Type = unpacker.Type + + switch s.Type { + case "directory": + s.Target = string(unpacker.Target[:]) + case "image": + var payload source.ImageMetadata + if err := json.Unmarshal(unpacker.Target, &payload); err != nil { + return err + } + s.Target = payload + default: + return fmt.Errorf("unsupported package metadata type: %+v", s.Type) + } + + return nil +} diff --git a/internal/formats/syftjson/test-fixtures/image-simple/Dockerfile b/internal/formats/syftjson/test-fixtures/image-simple/Dockerfile new file mode 100644 index 00000000000..79cfa759e35 --- /dev/null +++ b/internal/formats/syftjson/test-fixtures/image-simple/Dockerfile @@ -0,0 +1,4 @@ +# Note: changes to this file will result in updating several test values. Consider making a new image fixture instead of editing this one. +FROM scratch +ADD file-1.txt /somefile-1.txt +ADD file-2.txt /somefile-2.txt diff --git a/internal/formats/syftjson/test-fixtures/image-simple/file-1.txt b/internal/formats/syftjson/test-fixtures/image-simple/file-1.txt new file mode 100644 index 00000000000..985d3408e98 --- /dev/null +++ b/internal/formats/syftjson/test-fixtures/image-simple/file-1.txt @@ -0,0 +1 @@ +this file has contents \ No newline at end of file diff --git a/internal/formats/syftjson/test-fixtures/image-simple/file-2.txt b/internal/formats/syftjson/test-fixtures/image-simple/file-2.txt new file mode 100644 index 00000000000..396d08bbc72 --- /dev/null +++ b/internal/formats/syftjson/test-fixtures/image-simple/file-2.txt @@ -0,0 +1 @@ +file-2 contents! \ No newline at end of file diff --git a/internal/formats/syftjson/test-fixtures/snapshot/TestSPDXJSONDirectoryPresenter.golden b/internal/formats/syftjson/test-fixtures/snapshot/TestSPDXJSONDirectoryPresenter.golden new file mode 100644 index 00000000000..249517449ad --- /dev/null +++ b/internal/formats/syftjson/test-fixtures/snapshot/TestSPDXJSONDirectoryPresenter.golden @@ -0,0 +1,79 @@ +{ + "SPDXID": "SPDXRef-DOCUMENT", + "name": "/some/path", + "spdxVersion": "SPDX-2.2", + "creationInfo": { + "created": "2021-09-16T20:44:35.198887Z", + "creators": [ + "Organization: Anchore, Inc", + "Tool: syft-[not provided]" + ], + "licenseListVersion": "3.14" + }, + "dataLicense": "CC0-1.0", + "documentNamespace": "https://anchore.com/syft/image/", + "packages": [ + { + "SPDXID": "SPDXRef-Package-python-package-1-1.0.1", + "name": "package-1", + "licenseConcluded": "MIT", + "downloadLocation": "NOASSERTION", + "externalRefs": [ + { + "referenceCategory": "SECURITY", + "referenceLocator": "cpe:2.3:*:some:package:2:*:*:*:*:*:*:*", + "referenceType": "cpe23Type" + }, + { + "referenceCategory": "PACKAGE_MANAGER", + "referenceLocator": "a-purl-2", + "referenceType": "purl" + } + ], + "filesAnalyzed": false, + "hasFiles": [ + "SPDXRef-File-package-1-04cd22424378dcd6c77fce08beb52493b5494a60ea5e1f9bdf9b16dc0cacffe9" + ], + "licenseDeclared": "MIT", + "sourceInfo": "acquired package info from installed python package manifest file: /some/path/pkg1", + "versionInfo": "1.0.1" + }, + { + "SPDXID": "SPDXRef-Package-deb-package-2-2.0.1", + "name": "package-2", + "licenseConcluded": "NONE", + "downloadLocation": "NOASSERTION", + "externalRefs": [ + { + "referenceCategory": "SECURITY", + "referenceLocator": "cpe:2.3:*:some:package:2:*:*:*:*:*:*:*", + "referenceType": "cpe23Type" + }, + { + "referenceCategory": "PACKAGE_MANAGER", + "referenceLocator": "a-purl-2", + "referenceType": "purl" + } + ], + "filesAnalyzed": false, + "licenseDeclared": "NONE", + "sourceInfo": "acquired package info from DPKG DB: /some/path/pkg1", + "versionInfo": "2.0.1" + } + ], + "files": [ + { + "SPDXID": "SPDXRef-File-package-1-04cd22424378dcd6c77fce08beb52493b5494a60ea5e1f9bdf9b16dc0cacffe9", + "name": "foo", + "licenseConcluded": "", + "fileName": "/some/path/pkg1/depedencies/foo" + } + ], + "relationships": [ + { + "spdxElementId": "SPDXRef-Package-python-package-1-1.0.1", + "relationshipType": "CONTAINS", + "relatedSpdxElement": "SPDXRef-File-package-1-04cd22424378dcd6c77fce08beb52493b5494a60ea5e1f9bdf9b16dc0cacffe9" + } + ] +} diff --git a/internal/formats/syftjson/test-fixtures/snapshot/TestSPDXJSONImagePresenter.golden b/internal/formats/syftjson/test-fixtures/snapshot/TestSPDXJSONImagePresenter.golden new file mode 100644 index 00000000000..8906ef161ac --- /dev/null +++ b/internal/formats/syftjson/test-fixtures/snapshot/TestSPDXJSONImagePresenter.golden @@ -0,0 +1,61 @@ +{ + "SPDXID": "SPDXRef-DOCUMENT", + "name": "user-image-input", + "spdxVersion": "SPDX-2.2", + "creationInfo": { + "created": "2021-09-16T20:44:35.203911Z", + "creators": [ + "Organization: Anchore, Inc", + "Tool: syft-[not provided]" + ], + "licenseListVersion": "3.14" + }, + "dataLicense": "CC0-1.0", + "documentNamespace": "https://anchore.com/syft/image/user-image-input", + "packages": [ + { + "SPDXID": "SPDXRef-Package-python-package-1-1.0.1", + "name": "package-1", + "licenseConcluded": "MIT", + "downloadLocation": "NOASSERTION", + "externalRefs": [ + { + "referenceCategory": "SECURITY", + "referenceLocator": "cpe:2.3:*:some:package:1:*:*:*:*:*:*:*", + "referenceType": "cpe23Type" + }, + { + "referenceCategory": "PACKAGE_MANAGER", + "referenceLocator": "a-purl-1", + "referenceType": "purl" + } + ], + "filesAnalyzed": false, + "licenseDeclared": "MIT", + "sourceInfo": "acquired package info from installed python package manifest file: /somefile-1.txt", + "versionInfo": "1.0.1" + }, + { + "SPDXID": "SPDXRef-Package-deb-package-2-2.0.1", + "name": "package-2", + "licenseConcluded": "NONE", + "downloadLocation": "NOASSERTION", + "externalRefs": [ + { + "referenceCategory": "SECURITY", + "referenceLocator": "cpe:2.3:*:some:package:2:*:*:*:*:*:*:*", + "referenceType": "cpe23Type" + }, + { + "referenceCategory": "PACKAGE_MANAGER", + "referenceLocator": "a-purl-2", + "referenceType": "purl" + } + ], + "filesAnalyzed": false, + "licenseDeclared": "NONE", + "sourceInfo": "acquired package info from DPKG DB: /somefile-2.txt", + "versionInfo": "2.0.1" + } + ] +} diff --git a/internal/formats/syftjson/test-fixtures/snapshot/stereoscope-fixture-image-simple.golden b/internal/formats/syftjson/test-fixtures/snapshot/stereoscope-fixture-image-simple.golden new file mode 100644 index 00000000000..c1b1d2b797e Binary files /dev/null and b/internal/formats/syftjson/test-fixtures/snapshot/stereoscope-fixture-image-simple.golden differ diff --git a/internal/formats/syftjson/to_format_model.go b/internal/formats/syftjson/to_format_model.go new file mode 100644 index 00000000000..eacce565ee8 --- /dev/null +++ b/internal/formats/syftjson/to_format_model.go @@ -0,0 +1,144 @@ +package syftjson + +import ( + "fmt" + + "github.com/anchore/syft/internal" + "github.com/anchore/syft/internal/formats/syftjson/model" + "github.com/anchore/syft/internal/log" + "github.com/anchore/syft/internal/version" + "github.com/anchore/syft/syft/distro" + "github.com/anchore/syft/syft/pkg" + "github.com/anchore/syft/syft/source" +) + +//// NewJSONDocument creates and populates a new JSON document struct from the given cataloging results. +//func NewJSONDocument(catalog *pkg.Catalog, srcMetadata source.Metadata, d *distro.Distro, scope source.Scope, configuration interface{}) (Document, error) { +// +//} + +// TODO: this is export4ed for the use of the power-user command (temp) +func ToFormatModel(catalog *pkg.Catalog, srcMetadata *source.Metadata, d *distro.Distro, applicationConfig interface{}) model.Document { + src, err := toSourceModel(srcMetadata) + if err != nil { + log.Warnf("unable to create syft-json source object: %+v", err) + } + + artifacts, err := toPackageModels(catalog) + if err != nil { + return model.Document{} + } + + return model.Document{ + Artifacts: artifacts, + ArtifactRelationships: toRelationshipModel(pkg.NewRelationships(catalog)), + Source: src, + Distro: toDistroModel(d), + Descriptor: model.Descriptor{ + Name: internal.ApplicationName, + Version: version.FromBuild().Version, + Configuration: applicationConfig, + }, + Schema: model.Schema{ + Version: internal.JSONSchemaVersion, + URL: fmt.Sprintf("https://raw.githubusercontent.com/anchore/syft/main/schema/json/schema-%s.json", internal.JSONSchemaVersion), + }, + } +} + +func toPackageModels(catalog *pkg.Catalog) ([]model.Package, error) { + artifacts := make([]model.Package, 0) + if catalog == nil { + return artifacts, nil + } + for _, p := range catalog.Sorted() { + art, err := toPackageModel(p) + if err != nil { + return nil, err + } + artifacts = append(artifacts, art) + } + return artifacts, nil +} + +// toPackageModel crates a new Package from the given pkg.Package. +func toPackageModel(p *pkg.Package) (model.Package, error) { + var cpes = make([]string, len(p.CPEs)) + for i, c := range p.CPEs { + cpes[i] = c.BindToFmtString() + } + + // ensure collections are never nil for presentation reasons + var locations = make([]source.Location, 0) + if p.Locations != nil { + locations = p.Locations + } + + var licenses = make([]string, 0) + if p.Licenses != nil { + licenses = p.Licenses + } + + return model.Package{ + PackageBasicData: model.PackageBasicData{ + ID: string(p.ID), + Name: p.Name, + Version: p.Version, + Type: p.Type, + FoundBy: p.FoundBy, + Locations: locations, + Licenses: licenses, + Language: p.Language, + CPEs: cpes, + PURL: p.PURL, + }, + PackageCustomData: model.PackageCustomData{ + MetadataType: p.MetadataType, + Metadata: p.Metadata, + }, + }, nil +} + +func toRelationshipModel(relationships []pkg.Relationship) []model.Relationship { + result := make([]model.Relationship, len(relationships)) + for i, r := range relationships { + result[i] = model.Relationship{ + Parent: string(r.Parent), + Child: string(r.Child), + Type: string(r.Type), + Metadata: r.Metadata, + } + } + return result +} + +// toSourceModel creates a new source object to be represented into JSON. +func toSourceModel(src *source.Metadata) (model.Source, error) { + switch src.Scheme { + case source.ImageScheme: + return model.Source{ + Type: "image", + Target: src.ImageMetadata, + }, nil + case source.DirectoryScheme: + return model.Source{ + Type: "directory", + Target: src.Path, + }, nil + default: + return model.Source{}, fmt.Errorf("unsupported source: %q", src.Scheme) + } +} + +// toDistroModel creates a struct with the Linux distribution to be represented in JSON. +func toDistroModel(d *distro.Distro) model.Distro { + if d == nil { + return model.Distro{} + } + + return model.Distro{ + Name: d.Name(), + Version: d.FullVersion(), + IDLike: d.IDLike, + } +} diff --git a/internal/formats/syftjson/to_syft_model.go b/internal/formats/syftjson/to_syft_model.go new file mode 100644 index 00000000000..88dd12b1894 --- /dev/null +++ b/internal/formats/syftjson/to_syft_model.go @@ -0,0 +1,70 @@ +package syftjson + +import ( + "github.com/anchore/syft/internal/formats/syftjson/model" + "github.com/anchore/syft/internal/log" + "github.com/anchore/syft/syft/distro" + "github.com/anchore/syft/syft/pkg" + "github.com/anchore/syft/syft/source" +) + +func toSyftModel(doc model.Document) (*pkg.Catalog, *source.Metadata, *distro.Distro, error) { + dist, err := distro.NewDistro(distro.Type(doc.Distro.Name), doc.Distro.Version, doc.Distro.IDLike) + if err != nil { + return nil, nil, nil, err + } + + return toSyftCatalog(doc.Artifacts), toSyftSourceMetadata(doc.Source), &dist, nil +} + +func toSyftSourceMetadata(s model.Source) *source.Metadata { + switch s.Type { + case "directory": + return &source.Metadata{ + Scheme: source.DirectoryScheme, + Path: s.Target.(string), + } + case "image": + return &source.Metadata{ + Scheme: source.ImageScheme, + ImageMetadata: s.Target.(source.ImageMetadata), + } + } + return nil +} + +func toSyftCatalog(pkgs []model.Package) *pkg.Catalog { + catalog := pkg.NewCatalog() + for _, p := range pkgs { + catalog.Add(toSyftPackage(p)) + } + return catalog +} + +func toSyftPackage(p model.Package) pkg.Package { + var cpes []pkg.CPE + for _, c := range p.CPEs { + value, err := pkg.NewCPE(c) + if err != nil { + log.Warnf("excluding invalid CPE %q: %v", c, err) + continue + } + + cpes = append(cpes, value) + } + + return pkg.Package{ + ID: pkg.ID(p.ID), + Name: p.Name, + Version: p.Version, + FoundBy: p.FoundBy, + Locations: p.Locations, + Licenses: p.Licenses, + Language: p.Language, + Type: p.Type, + CPEs: cpes, + PURL: p.PURL, + MetadataType: p.MetadataType, + Metadata: p.Metadata, + } +} diff --git a/internal/formats/syftjson/validator.go b/internal/formats/syftjson/validator.go new file mode 100644 index 00000000000..1adcb90d461 --- /dev/null +++ b/internal/formats/syftjson/validator.go @@ -0,0 +1,31 @@ +package syftjson + +import ( + "encoding/json" + "fmt" + "io" + "strings" + + "github.com/anchore/syft/internal/formats/syftjson/model" +) + +func validator(reader io.Reader) error { + type Document struct { + Schema model.Schema `json:"schema"` + } + + dec := json.NewDecoder(reader) + + var doc Document + err := dec.Decode(&doc) + if err != nil { + return fmt.Errorf("unable to decode: %w", err) + } + + // note: we accept al schema versions + // TODO: add per-schema version parsing + if strings.Contains(doc.Schema.URL, "anchore/syft") { + return nil + } + return fmt.Errorf("could not extract syft schema") +}