From 602281362c5c73da7af33c2f05b3c80e3821382b Mon Sep 17 00:00:00 2001 From: Keith Zantow Date: Fri, 18 Feb 2022 11:19:02 -0500 Subject: [PATCH] Add CycloneDX decoder (#811) --- .../formats/common/cyclonedxhelpers/author.go | 17 +- .../common/cyclonedxhelpers/author_test.go | 4 +- .../common/cyclonedxhelpers/component.go | 155 ++++++++++- .../formats/common/cyclonedxhelpers/cpe.go | 22 +- .../common/cyclonedxhelpers/cpe_test.go | 4 +- .../common/cyclonedxhelpers/decoder.go | 222 +++++++++++++++ .../common/cyclonedxhelpers/decoder_test.go | 260 ++++++++++++++++++ .../common/cyclonedxhelpers/description.go | 11 +- .../cyclonedxhelpers/description_test.go | 4 +- .../cyclonedxhelpers/external_references.go | 51 +++- .../external_references_test.go | 4 +- .../formats/common/cyclonedxhelpers/format.go | 16 +- .../formats/common/cyclonedxhelpers/group.go | 11 +- .../common/cyclonedxhelpers/group_test.go | 4 +- .../common/cyclonedxhelpers/licenses.go | 11 +- .../common/cyclonedxhelpers/licenses_test.go | 4 +- .../common/cyclonedxhelpers/properties.go | 11 +- .../cyclonedxhelpers/properties_test.go | 4 +- .../common/cyclonedxhelpers/publisher.go | 13 +- .../common/cyclonedxhelpers/publisher_test.go | 4 +- .../formats/cyclonedx13json/decoder_test.go | 70 +++++ internal/formats/cyclonedx13json/format.go | 10 +- .../TestCycloneDxDirectoryEncoder.golden | 11 +- .../snapshot/TestCycloneDxImageEncoder.golden | 15 +- .../stereoscope-fixture-image-simple.golden | Bin 15360 -> 15360 bytes .../formats/cyclonedx13xml/decoder_test.go | 70 +++++ internal/formats/cyclonedx13xml/format.go | 10 +- .../TestCycloneDxDirectoryEncoder.golden | 8 +- .../snapshot/TestCycloneDxImageEncoder.golden | 12 +- .../stereoscope-fixture-image-simple.golden | Bin 15360 -> 15360 bytes .../{cataloger/rust => }/cargo_metadata.go | 10 +- syft/pkg/cataloger/rust/parse_cargo_lock.go | 2 +- syft/pkg/metadata.go | 18 ++ test/integration/encode_decode_cycle_test.go | 25 +- 34 files changed, 1027 insertions(+), 66 deletions(-) create mode 100644 internal/formats/common/cyclonedxhelpers/decoder.go create mode 100644 internal/formats/common/cyclonedxhelpers/decoder_test.go create mode 100644 internal/formats/cyclonedx13json/decoder_test.go create mode 100644 internal/formats/cyclonedx13xml/decoder_test.go rename syft/pkg/{cataloger/rust => }/cargo_metadata.go (58%) diff --git a/internal/formats/common/cyclonedxhelpers/author.go b/internal/formats/common/cyclonedxhelpers/author.go index 9f2cff9e6cb..31ebdde8e27 100644 --- a/internal/formats/common/cyclonedxhelpers/author.go +++ b/internal/formats/common/cyclonedxhelpers/author.go @@ -7,7 +7,7 @@ import ( "github.com/anchore/syft/syft/pkg" ) -func Author(p pkg.Package) string { +func encodeAuthor(p pkg.Package) string { if hasMetadata(p) { switch metadata := p.Metadata.(type) { case pkg.NpmPackageJSONMetadata: @@ -30,3 +30,18 @@ func Author(p pkg.Package) string { } return "" } + +func decodeAuthor(author string, metadata interface{}) { + switch meta := metadata.(type) { + case *pkg.NpmPackageJSONMetadata: + meta.Author = author + case *pkg.PythonPackageMetadata: + parts := strings.SplitN(author, " <", 2) + meta.Author = parts[0] + if len(parts) > 1 { + meta.AuthorEmail = strings.TrimSuffix(parts[1], ">") + } + case *pkg.GemMetadata: + meta.Authors = strings.Split(author, ",") + } +} diff --git a/internal/formats/common/cyclonedxhelpers/author_test.go b/internal/formats/common/cyclonedxhelpers/author_test.go index 6da4e77f641..11cd95d667a 100644 --- a/internal/formats/common/cyclonedxhelpers/author_test.go +++ b/internal/formats/common/cyclonedxhelpers/author_test.go @@ -7,7 +7,7 @@ import ( "github.com/stretchr/testify/assert" ) -func Test_Author(t *testing.T) { +func Test_encodeAuthor(t *testing.T) { tests := []struct { name string input pkg.Package @@ -81,7 +81,7 @@ func Test_Author(t *testing.T) { } for _, test := range tests { t.Run(test.name, func(t *testing.T) { - assert.Equal(t, test.expected, Author(test.input)) + assert.Equal(t, test.expected, encodeAuthor(test.input)) }) } } diff --git a/internal/formats/common/cyclonedxhelpers/component.go b/internal/formats/common/cyclonedxhelpers/component.go index ed493a55299..ef42ab600e5 100644 --- a/internal/formats/common/cyclonedxhelpers/component.go +++ b/internal/formats/common/cyclonedxhelpers/component.go @@ -1,27 +1,164 @@ package cyclonedxhelpers import ( + "fmt" + "reflect" + "strconv" + "github.com/CycloneDX/cyclonedx-go" + + "github.com/anchore/syft/internal/log" "github.com/anchore/syft/syft/pkg" + "github.com/anchore/syft/syft/source" ) -func Component(p pkg.Package) cyclonedx.Component { +func encodeComponent(p pkg.Package) cyclonedx.Component { return cyclonedx.Component{ Type: cyclonedx.ComponentTypeLibrary, Name: p.Name, - Group: Group(p), + Group: encodeGroup(p), Version: p.Version, PackageURL: p.PURL, - Licenses: Licenses(p), - CPE: CPE(p), - Author: Author(p), - Publisher: Publisher(p), - Description: Description(p), - ExternalReferences: ExternalReferences(p), - Properties: Properties(p), + Licenses: encodeLicenses(p), + CPE: encodeCPE(p), + Author: encodeAuthor(p), + Publisher: encodePublisher(p), + Description: encodeDescription(p), + ExternalReferences: encodeExternalReferences(p), + Properties: encodeProperties(p), } } func hasMetadata(p pkg.Package) bool { return p.Metadata != nil } + +func decodeComponent(c *cyclonedx.Component) *pkg.Package { + typ := pkg.Type(findPropertyValue(c, "type")) + purl := c.PackageURL + if typ == "" && purl != "" { + typ = pkg.TypeFromPURL(purl) + } + + metaType, meta := decodePackageMetadata(c) + + p := &pkg.Package{ + Name: c.Name, + Version: c.Version, + FoundBy: findPropertyValue(c, "foundBy"), + Locations: decodeLocations(c), + Licenses: decodeLicenses(c), + Language: pkg.Language(findPropertyValue(c, "language")), + Type: typ, + CPEs: decodeCPEs(c), + PURL: purl, + MetadataType: metaType, + Metadata: meta, + } + + return p +} + +func decodeLocations(c *cyclonedx.Component) (out []source.Location) { + if c.Properties != nil { + props := *c.Properties + for i := 0; i < len(props)-1; i++ { + if props[i].Name == "path" && props[i+1].Name == "layerID" { + out = append(out, source.Location{ + Coordinates: source.Coordinates{ + RealPath: props[i].Value, + FileSystemID: props[i+1].Value, + }, + }) + i++ + } + } + } + return +} + +func mapAllProps(c *cyclonedx.Component, obj reflect.Value) { + value := obj + if value.Kind() == reflect.Ptr { + value = value.Elem() + } + + structType := value.Type() + if structType.Kind() != reflect.Struct { + return + } + for i := 0; i < value.NumField(); i++ { + field := structType.Field(i) + fieldType := field.Type + fieldValue := value.Field(i) + + name, mapped := field.Tag.Lookup("cyclonedx") + if !mapped { + continue + } + + if fieldType.Kind() == reflect.Ptr { + fieldType = fieldType.Elem() + if fieldValue.IsNil() { + newValue := reflect.New(fieldType) + fieldValue.Set(newValue) + } + fieldValue = fieldValue.Elem() + } + + propertyValue := findPropertyValue(c, name) + switch fieldType.Kind() { + case reflect.String: + if fieldValue.CanSet() { + fieldValue.SetString(propertyValue) + } else { + msg := fmt.Sprintf("unable to set field: %s.%s", structType.Name(), field.Name) + log.Info(msg) + } + case reflect.Bool: + if b, err := strconv.ParseBool(propertyValue); err == nil { + fieldValue.SetBool(b) + } + case reflect.Int: + if i, err := strconv.Atoi(propertyValue); err == nil { + fieldValue.SetInt(int64(i)) + } + case reflect.Float32, reflect.Float64: + if i, err := strconv.ParseFloat(propertyValue, 64); err == nil { + fieldValue.SetFloat(i) + } + case reflect.Struct: + mapAllProps(c, fieldValue) + case reflect.Complex128, reflect.Complex64: + fallthrough + case reflect.Ptr: + msg := fmt.Sprintf("decoding CycloneDX properties to a pointer is not supported: %s.%s", field.Type.Name(), field.Name) + log.Warnf(msg) + } + } +} + +func decodePackageMetadata(c *cyclonedx.Component) (pkg.MetadataType, interface{}) { + if c.Properties != nil { + typ := pkg.MetadataType(findPropertyValue(c, "metadataType")) + if typ != "" { + meta := reflect.New(pkg.MetadataTypeByName[typ]) + metaPtr := meta.Interface() + + // Map all dynamic properties + mapAllProps(c, meta.Elem()) + + // Map all explicit metadata properties + decodeAuthor(c.Author, metaPtr) + decodeGroup(c.Group, metaPtr) + decodePublisher(c.Publisher, metaPtr) + decodeDescription(c.Description, metaPtr) + decodeExternalReferences(c, metaPtr) + + // return the actual interface{} | struct ( not interface{} | *struct ) + return typ, meta.Elem().Interface() + } + } + + return pkg.UnknownMetadataType, nil +} diff --git a/internal/formats/common/cyclonedxhelpers/cpe.go b/internal/formats/common/cyclonedxhelpers/cpe.go index 6b43f847ef8..0c241d2a921 100644 --- a/internal/formats/common/cyclonedxhelpers/cpe.go +++ b/internal/formats/common/cyclonedxhelpers/cpe.go @@ -1,8 +1,12 @@ package cyclonedxhelpers -import "github.com/anchore/syft/syft/pkg" +import ( + "github.com/CycloneDX/cyclonedx-go" + "github.com/anchore/syft/internal/log" + "github.com/anchore/syft/syft/pkg" +) -func CPE(p pkg.Package) string { +func encodeCPE(p pkg.Package) string { // Since the CPEs in a package are sorted by specificity // we can extract the first CPE as the one to output in cyclonedx if len(p.CPEs) > 0 { @@ -10,3 +14,17 @@ func CPE(p pkg.Package) string { } return "" } + +func decodeCPEs(c *cyclonedx.Component) []pkg.CPE { + // FIXME we not encoding all the CPEs (see above), so here we just use the single provided one + if c.CPE != "" { + cp, err := pkg.NewCPE(c.CPE) + if err != nil { + log.Warnf("invalid CPE: %s", c.CPE) + } else { + return []pkg.CPE{cp} + } + } + + return []pkg.CPE{} +} diff --git a/internal/formats/common/cyclonedxhelpers/cpe_test.go b/internal/formats/common/cyclonedxhelpers/cpe_test.go index 81d6f6f0784..4678b9a926e 100644 --- a/internal/formats/common/cyclonedxhelpers/cpe_test.go +++ b/internal/formats/common/cyclonedxhelpers/cpe_test.go @@ -7,7 +7,7 @@ import ( "github.com/stretchr/testify/assert" ) -func Test_CPE(t *testing.T) { +func Test_encodeCPE(t *testing.T) { testCPE := pkg.MustCPE("cpe:2.3:a:name:name:3.2:*:*:*:*:*:*:*") testCPE2 := pkg.MustCPE("cpe:2.3:a:name:name2:3.2:*:*:*:*:*:*:*") tests := []struct { @@ -51,7 +51,7 @@ func Test_CPE(t *testing.T) { } for _, test := range tests { t.Run(test.name, func(t *testing.T) { - assert.Equal(t, test.expected, CPE(test.input)) + assert.Equal(t, test.expected, encodeCPE(test.input)) }) } } diff --git a/internal/formats/common/cyclonedxhelpers/decoder.go b/internal/formats/common/cyclonedxhelpers/decoder.go new file mode 100644 index 00000000000..b45187acaaf --- /dev/null +++ b/internal/formats/common/cyclonedxhelpers/decoder.go @@ -0,0 +1,222 @@ +package cyclonedxhelpers + +import ( + "fmt" + "io" + + "github.com/CycloneDX/cyclonedx-go" + "github.com/anchore/syft/syft/artifact" + "github.com/anchore/syft/syft/format" + "github.com/anchore/syft/syft/linux" + "github.com/anchore/syft/syft/pkg" + "github.com/anchore/syft/syft/sbom" + "github.com/anchore/syft/syft/source" +) + +func GetValidator(format cyclonedx.BOMFileFormat) format.Validator { + return func(reader io.Reader) error { + bom := &cyclonedx.BOM{} + err := cyclonedx.NewBOMDecoder(reader, format).Decode(bom) + if err != nil { + return err + } + // random JSON does not necessarily cause an error (e.g. SPDX) + if (cyclonedx.BOM{} == *bom) { + return fmt.Errorf("not a valid CycloneDX document") + } + return nil + } +} + +func GetDecoder(format cyclonedx.BOMFileFormat) format.Decoder { + return func(reader io.Reader) (*sbom.SBOM, error) { + bom := &cyclonedx.BOM{} + 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) { + meta := source.Metadata{} + if bom.Metadata != nil { + meta = decodeMetadata(bom.Metadata.Component) + } + s := &sbom.SBOM{ + Artifacts: sbom.Artifacts{ + PackageCatalog: pkg.NewCatalog(), + LinuxDistribution: linuxReleaseFromComponents(*bom.Components), + }, + Source: meta, + //Descriptor: sbom.Descriptor{}, + } + + idMap := make(map[string]interface{}) + + if err := collectBomPackages(bom, s, idMap); err != nil { + return nil, err + } + + collectRelationships(bom, s, idMap) + + return s, nil +} + +func collectBomPackages(bom *cyclonedx.BOM, s *sbom.SBOM, idMap map[string]interface{}) error { + if bom.Components == nil { + return fmt.Errorf("no components are defined in the CycloneDX BOM") + } + for i := range *bom.Components { + collectPackages(&(*bom.Components)[i], s, idMap) + } + return nil +} + +func collectPackages(component *cyclonedx.Component, s *sbom.SBOM, idMap map[string]interface{}) { + switch component.Type { + case cyclonedx.ComponentTypeOS: + case cyclonedx.ComponentTypeContainer: + case cyclonedx.ComponentTypeApplication, cyclonedx.ComponentTypeFramework, cyclonedx.ComponentTypeLibrary: + p := decodeComponent(component) + idMap[component.BOMRef] = p + // TODO there must be a better way than needing to call this manually: + p.SetID() + s.Artifacts.PackageCatalog.Add(*p) + } + + if component.Components != nil { + for i := range *component.Components { + collectPackages(&(*component.Components)[i], s, idMap) + } + } +} + +func linuxReleaseFromComponents(components []cyclonedx.Component) *linux.Release { + for i := range components { + component := &components[i] + if component.Type == cyclonedx.ComponentTypeOS { + return linuxReleaseFromOSComponent(component) + } + } + return nil +} + +func linuxReleaseFromOSComponent(component *cyclonedx.Component) *linux.Release { + if component == nil { + return nil + } + + var name string + var version string + if component.SWID != nil { + name = component.SWID.Name + version = component.SWID.Version + } + if name == "" { + name = component.Name + } + if name == "" { + name = getPropertyValue(component, "id") + } + if version == "" { + version = component.Version + } + if version == "" { + version = getPropertyValue(component, "versionID") + } + + rel := &linux.Release{ + CPEName: component.CPE, + PrettyName: name, + Name: name, + ID: name, + IDLike: []string{name}, + Version: version, + VersionID: version, + } + if component.ExternalReferences != nil { + for _, ref := range *component.ExternalReferences { + switch ref.Type { + case cyclonedx.ERTypeIssueTracker: + rel.BugReportURL = ref.URL + case cyclonedx.ERTypeWebsite: + rel.HomeURL = ref.URL + case cyclonedx.ERTypeOther: + switch ref.Comment { + case "support": + rel.SupportURL = ref.URL + case "privacyPolicy": + rel.PrivacyPolicyURL = ref.URL + } + } + } + } + + return rel +} + +func getPropertyValue(component *cyclonedx.Component, name string) string { + if component.Properties != nil { + for _, p := range *component.Properties { + if p.Name == name { + return p.Value + } + } + } + return "" +} + +func collectRelationships(bom *cyclonedx.BOM, s *sbom.SBOM, idMap map[string]interface{}) { + if bom.Dependencies == nil { + return + } + for _, d := range *bom.Dependencies { + from, fromOk := idMap[d.Ref].(artifact.Identifiable) + if fromOk { + if d.Dependencies == nil { + continue + } + for _, t := range *d.Dependencies { + to, toOk := idMap[t.Ref].(artifact.Identifiable) + if toOk { + s.Relationships = append(s.Relationships, artifact.Relationship{ + From: from, + To: to, + Type: artifact.DependencyOfRelationship, // FIXME this information is lost + }) + } + } + } + } +} + +func decodeMetadata(component *cyclonedx.Component) source.Metadata { + switch component.Type { + case cyclonedx.ComponentTypeContainer: + return source.Metadata{ + Scheme: source.ImageScheme, + ImageMetadata: source.ImageMetadata{ + UserInput: component.Name, + ID: component.BOMRef, + ManifestDigest: component.Version, + }, + } + case cyclonedx.ComponentTypeFile: + return source.Metadata{ + Scheme: source.FileScheme, // or source.DirectoryScheme + Path: component.Name, + ImageMetadata: source.ImageMetadata{ + UserInput: component.Name, + ID: component.BOMRef, + ManifestDigest: component.Version, + }, + } + } + return source.Metadata{} +} diff --git a/internal/formats/common/cyclonedxhelpers/decoder_test.go b/internal/formats/common/cyclonedxhelpers/decoder_test.go new file mode 100644 index 00000000000..e18e89f0a33 --- /dev/null +++ b/internal/formats/common/cyclonedxhelpers/decoder_test.go @@ -0,0 +1,260 @@ +package cyclonedxhelpers + +import ( + "fmt" + "testing" + + "github.com/CycloneDX/cyclonedx-go" + "github.com/stretchr/testify/assert" +) + +func Test_decode(t *testing.T) { + type expected struct { + os string + pkg string + ver string + relation string + purl string + cpe string + } + tests := []struct { + name string + input cyclonedx.BOM + expected []expected + }{ + { + name: "basic mapping from cyclonedx", + input: cyclonedx.BOM{ + Metadata: nil, + Components: &[]cyclonedx.Component{ + { + BOMRef: "p1", + Type: cyclonedx.ComponentTypeLibrary, + Name: "package-1", + Version: "1.0.1", + Description: "", + Hashes: nil, + Licenses: &cyclonedx.Licenses{ + { + License: &cyclonedx.License{ + ID: "MIT", + }, + }, + }, + CPE: "cpe:2.3:*:some:package:1:*:*:*:*:*:*:*", + PackageURL: "pkg:some/package-1@1.0.1?arch=arm64&upstream=upstream1&distro=alpine-1", + ExternalReferences: &[]cyclonedx.ExternalReference{ + { + URL: "", + Comment: "", + Hashes: nil, + Type: "", + }, + }, + Properties: &[]cyclonedx.Property{ + { + Name: "foundBy", + Value: "the-cataloger-1", + }, + { + Name: "language", + Value: "python", + }, + { + Name: "type", + Value: "python", + }, + { + Name: "metadataType", + Value: "PythonPackageMetadata", + }, + { + Name: "path", + Value: "/some/path/pkg1", + }, + }, + Components: nil, + Evidence: nil, + }, + { + BOMRef: "p2", + Type: cyclonedx.ComponentTypeLibrary, + Name: "package-2", + Version: "2.0.2", + Hashes: nil, + Licenses: &cyclonedx.Licenses{ + { + License: &cyclonedx.License{ + ID: "MIT", + }, + }, + }, + CPE: "cpe:2.3:*:another:package:2:*:*:*:*:*:*:*", + PackageURL: "pkg:alpine/alpine-baselayout@3.2.0-r16?arch=x86_64&upstream=alpine-baselayout&distro=alpine-3.14.2", + Properties: &[]cyclonedx.Property{ + + { + Name: "foundBy", + Value: "apkdb-cataloger", + }, + { + Name: "type", + Value: "apk", + }, + { + Name: "metadataType", + Value: "ApkMetadata", + }, + { + Name: "path", + Value: "/lib/apk/db/installed", + }, + { + Name: "layerID", + Value: "sha256:9fb3aa2f8b8023a4bebbf92aa567caf88e38e969ada9f0ac12643b2847391635", + }, + { + Name: "originPackage", + Value: "zlib", + }, + { + Name: "size", + Value: "51213", + }, + { + Name: "installedSize", + Value: "110592", + }, + { + Name: "pullDependencies", + Value: "so:libc.musl-x86_64.so.1", + }, + { + Name: "pullChecksum", + Value: "Q1uss4DfpvL16Nw2YUTwmzGBABz3Y=", + }, + { + Name: "gitCommitOfApkPort", + Value: "d2bfb22c8e8f67ad7d8d02704f35ec4d2a19f9b9", + }, + }, + }, + { + Type: cyclonedx.ComponentTypeOS, + Name: "debian", + Version: "1.2.3", + Hashes: nil, + Licenses: &cyclonedx.Licenses{ + { + License: &cyclonedx.License{ + ID: "MIT", + }, + }, + }, + Properties: &[]cyclonedx.Property{ + { + Name: "prettyName", + Value: "debian", + }, + { + Name: "id", + Value: "debian", + }, + { + Name: "versionID", + Value: "1.2.3", + }, + }, + Components: nil, + Evidence: nil, + }, + }, + Dependencies: &[]cyclonedx.Dependency{ + { + Ref: "p1", + Dependencies: &[]cyclonedx.Dependency{ + { + Ref: "p2", + }, + }, + }, + }, + }, + expected: []expected{ + { + os: "debian", + ver: "1.2.3", + }, + { + pkg: "package-1", + ver: "1.0.1", + cpe: "cpe:2.3:*:some:package:1:*:*:*:*:*:*:*", + purl: "pkg:some/package-1@1.0.1?arch=arm64&upstream=upstream1&distro=alpine-1", + relation: "package-2", + }, + { + pkg: "package-2", + ver: "2.0.2", + purl: "pkg:alpine/alpine-baselayout@3.2.0-r16?arch=x86_64&upstream=alpine-baselayout&distro=alpine-3.14.2", + }, + }, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + sbom, err := toSyftModel(&test.input) + assert.NoError(t, err) + + test: + for _, e := range test.expected { + if e.os != "" { + assert.Equal(t, e.os, sbom.Artifacts.LinuxDistribution.ID) + assert.Equal(t, e.ver, sbom.Artifacts.LinuxDistribution.VersionID) + } + if e.pkg != "" { + for p := range sbom.Artifacts.PackageCatalog.Enumerate() { + if e.pkg != p.Name { + continue + } + + assert.Equal(t, e.ver, p.Version) + + if e.cpe != "" { + foundCPE := false + for _, c := range p.CPEs { + cstr := c.BindToFmtString() + if e.cpe == cstr { + foundCPE = true + break + } + } + if !foundCPE { + assert.Fail(t, fmt.Sprintf("CPE not found in package: %s", e.cpe)) + } + } + + if e.purl != "" { + assert.Equal(t, e.purl, p.PURL) + } + + if e.relation != "" { + foundRelation := false + for _, r := range sbom.Relationships { + p := sbom.Artifacts.PackageCatalog.Package(r.To.ID()) + if e.relation == p.Name { + foundRelation = true + break + } + } + if !foundRelation { + assert.Fail(t, fmt.Sprintf("relation not found: %s", e.relation)) + } + } + continue test + } + assert.Fail(t, fmt.Sprintf("package should be present: %s", e.pkg)) + } + } + }) + } +} diff --git a/internal/formats/common/cyclonedxhelpers/description.go b/internal/formats/common/cyclonedxhelpers/description.go index 176c019895f..36add799af1 100644 --- a/internal/formats/common/cyclonedxhelpers/description.go +++ b/internal/formats/common/cyclonedxhelpers/description.go @@ -2,7 +2,7 @@ package cyclonedxhelpers import "github.com/anchore/syft/syft/pkg" -func Description(p pkg.Package) string { +func encodeDescription(p pkg.Package) string { if hasMetadata(p) { switch metadata := p.Metadata.(type) { case pkg.ApkMetadata: @@ -13,3 +13,12 @@ func Description(p pkg.Package) string { } return "" } + +func decodeDescription(description string, metadata interface{}) { + switch meta := metadata.(type) { + case *pkg.ApkMetadata: + meta.Description = description + case *pkg.NpmPackageJSONMetadata: + meta.Description = description + } +} diff --git a/internal/formats/common/cyclonedxhelpers/description_test.go b/internal/formats/common/cyclonedxhelpers/description_test.go index 0b8dec8748c..21f72270204 100644 --- a/internal/formats/common/cyclonedxhelpers/description_test.go +++ b/internal/formats/common/cyclonedxhelpers/description_test.go @@ -7,7 +7,7 @@ import ( "github.com/stretchr/testify/assert" ) -func Test_Description(t *testing.T) { +func Test_encodeDescription(t *testing.T) { tests := []struct { name string input pkg.Package @@ -50,7 +50,7 @@ func Test_Description(t *testing.T) { } for _, test := range tests { t.Run(test.name, func(t *testing.T) { - assert.Equal(t, test.expected, Description(test.input)) + assert.Equal(t, test.expected, encodeDescription(test.input)) }) } } diff --git a/internal/formats/common/cyclonedxhelpers/external_references.go b/internal/formats/common/cyclonedxhelpers/external_references.go index f109ad1993e..5cc75302166 100644 --- a/internal/formats/common/cyclonedxhelpers/external_references.go +++ b/internal/formats/common/cyclonedxhelpers/external_references.go @@ -2,12 +2,13 @@ package cyclonedxhelpers import ( "fmt" + "strings" "github.com/CycloneDX/cyclonedx-go" "github.com/anchore/syft/syft/pkg" ) -func ExternalReferences(p pkg.Package) *[]cyclonedx.ExternalReference { +func encodeExternalReferences(p pkg.Package) *[]cyclonedx.ExternalReference { refs := []cyclonedx.ExternalReference{} if hasMetadata(p) { switch metadata := p.Metadata.(type) { @@ -63,3 +64,51 @@ func ExternalReferences(p pkg.Package) *[]cyclonedx.ExternalReference { } return nil } + +func decodeExternalReferences(c *cyclonedx.Component, metadata interface{}) { + if c.ExternalReferences == nil { + return + } + switch meta := metadata.(type) { + case *pkg.ApkMetadata: + meta.URL = refURL(c, cyclonedx.ERTypeDistribution) + case *pkg.CargoPackageMetadata: + meta.Source = refURL(c, cyclonedx.ERTypeDistribution) + case *pkg.NpmPackageJSONMetadata: + meta.URL = refURL(c, cyclonedx.ERTypeDistribution) + meta.Homepage = refURL(c, cyclonedx.ERTypeWebsite) + case *pkg.GemMetadata: + meta.Homepage = refURL(c, cyclonedx.ERTypeWebsite) + case *pkg.PythonPackageMetadata: + if meta.DirectURLOrigin == nil { + meta.DirectURLOrigin = &pkg.PythonDirectURLOriginInfo{} + } + meta.DirectURLOrigin.URL = refURL(c, cyclonedx.ERTypeVCS) + meta.DirectURLOrigin.CommitID = strings.TrimPrefix(refComment(c, cyclonedx.ERTypeVCS), "commit: ") + } +} + +func findExternalRef(c *cyclonedx.Component, typ cyclonedx.ExternalReferenceType) *cyclonedx.ExternalReference { + if c.ExternalReferences != nil { + for _, r := range *c.ExternalReferences { + if r.Type == typ { + return &r + } + } + } + return nil +} + +func refURL(c *cyclonedx.Component, typ cyclonedx.ExternalReferenceType) string { + if r := findExternalRef(c, typ); r != nil { + return r.URL + } + return "" +} + +func refComment(c *cyclonedx.Component, typ cyclonedx.ExternalReferenceType) string { + if r := findExternalRef(c, typ); r != nil { + return r.Comment + } + return "" +} diff --git a/internal/formats/common/cyclonedxhelpers/external_references_test.go b/internal/formats/common/cyclonedxhelpers/external_references_test.go index 9f22f75cdd5..e5a91daa667 100644 --- a/internal/formats/common/cyclonedxhelpers/external_references_test.go +++ b/internal/formats/common/cyclonedxhelpers/external_references_test.go @@ -8,7 +8,7 @@ import ( "github.com/stretchr/testify/assert" ) -func Test_ExternalReferences(t *testing.T) { +func Test_encodeExternalReferences(t *testing.T) { tests := []struct { name string input pkg.Package @@ -127,7 +127,7 @@ func Test_ExternalReferences(t *testing.T) { } for _, test := range tests { t.Run(test.name, func(t *testing.T) { - assert.Equal(t, test.expected, ExternalReferences(test.input)) + assert.Equal(t, test.expected, encodeExternalReferences(test.input)) }) } } diff --git a/internal/formats/common/cyclonedxhelpers/format.go b/internal/formats/common/cyclonedxhelpers/format.go index 132dcd530e2..997a5f85ebf 100644 --- a/internal/formats/common/cyclonedxhelpers/format.go +++ b/internal/formats/common/cyclonedxhelpers/format.go @@ -27,7 +27,7 @@ func ToFormatModel(s sbom.SBOM) *cyclonedx.BOM { packages := s.Artifacts.PackageCatalog.Sorted() components := make([]cyclonedx.Component, len(packages)) for i, p := range packages { - components[i] = Component(p) + components[i] = encodeComponent(p) } components = append(components, toOSComponent(s.Artifacts.LinuxDistribution)...) cdxBOM.Components = &components @@ -80,9 +80,17 @@ func toOSComponent(distro *linux.Release) []cyclonedx.Component { } return []cyclonedx.Component{ { - Type: cyclonedx.ComponentTypeOS, - Name: distro.Name, - Version: distro.Version, + Type: cyclonedx.ComponentTypeOS, + // FIXME is it idiomatic to be using SWID here for specific name and version information? + SWID: &cyclonedx.SWID{ + TagID: distro.ID, + Name: distro.ID, + Version: distro.VersionID, + }, + Description: distro.PrettyName, + Name: distro.ID, + Version: distro.VersionID, + // TODO should we add a PURL? CPE: distro.CPEName, ExternalReferences: eRefs, Properties: props, diff --git a/internal/formats/common/cyclonedxhelpers/group.go b/internal/formats/common/cyclonedxhelpers/group.go index 0a2a8b34c50..6e452aeeef7 100644 --- a/internal/formats/common/cyclonedxhelpers/group.go +++ b/internal/formats/common/cyclonedxhelpers/group.go @@ -2,7 +2,7 @@ package cyclonedxhelpers import "github.com/anchore/syft/syft/pkg" -func Group(p pkg.Package) string { +func encodeGroup(p pkg.Package) string { if hasMetadata(p) { if metadata, ok := p.Metadata.(pkg.JavaMetadata); ok && metadata.PomProperties != nil { return metadata.PomProperties.GroupID @@ -10,3 +10,12 @@ func Group(p pkg.Package) string { } return "" } + +func decodeGroup(group string, metadata interface{}) { + if meta, ok := metadata.(*pkg.JavaMetadata); ok { + if meta.PomProperties == nil { + meta.PomProperties = &pkg.PomProperties{} + } + meta.PomProperties.GroupID = group + } +} diff --git a/internal/formats/common/cyclonedxhelpers/group_test.go b/internal/formats/common/cyclonedxhelpers/group_test.go index 908fc3b876c..38352131fad 100644 --- a/internal/formats/common/cyclonedxhelpers/group_test.go +++ b/internal/formats/common/cyclonedxhelpers/group_test.go @@ -7,7 +7,7 @@ import ( "github.com/stretchr/testify/assert" ) -func TestGroup(t *testing.T) { +func Test_encodeGroup(t *testing.T) { tests := []struct { name string input pkg.Package @@ -46,7 +46,7 @@ func TestGroup(t *testing.T) { } for _, test := range tests { t.Run(test.name, func(t *testing.T) { - assert.Equal(t, test.expected, Group(test.input)) + assert.Equal(t, test.expected, encodeGroup(test.input)) }) } } diff --git a/internal/formats/common/cyclonedxhelpers/licenses.go b/internal/formats/common/cyclonedxhelpers/licenses.go index c1e2c44a2f4..68da1967b60 100644 --- a/internal/formats/common/cyclonedxhelpers/licenses.go +++ b/internal/formats/common/cyclonedxhelpers/licenses.go @@ -6,7 +6,7 @@ import ( "github.com/anchore/syft/syft/pkg" ) -func Licenses(p pkg.Package) *cyclonedx.Licenses { +func encodeLicenses(p pkg.Package) *cyclonedx.Licenses { lc := cyclonedx.Licenses{} for _, licenseName := range p.Licenses { if value, exists := spdxlicense.ID(licenseName); exists { @@ -22,3 +22,12 @@ func Licenses(p pkg.Package) *cyclonedx.Licenses { } return nil } + +func decodeLicenses(c *cyclonedx.Component) (out []string) { + if c.Licenses != nil { + for _, l := range *c.Licenses { + out = append(out, l.License.ID) + } + } + return +} diff --git a/internal/formats/common/cyclonedxhelpers/licenses_test.go b/internal/formats/common/cyclonedxhelpers/licenses_test.go index 41839a0d1d0..e72697559c5 100644 --- a/internal/formats/common/cyclonedxhelpers/licenses_test.go +++ b/internal/formats/common/cyclonedxhelpers/licenses_test.go @@ -8,7 +8,7 @@ import ( "github.com/stretchr/testify/assert" ) -func Test_License(t *testing.T) { +func Test_encodeLicense(t *testing.T) { tests := []struct { name string input pkg.Package @@ -77,7 +77,7 @@ func Test_License(t *testing.T) { } for _, test := range tests { t.Run(test.name, func(t *testing.T) { - assert.Equal(t, test.expected, Licenses(test.input)) + assert.Equal(t, test.expected, encodeLicenses(test.input)) }) } } diff --git a/internal/formats/common/cyclonedxhelpers/properties.go b/internal/formats/common/cyclonedxhelpers/properties.go index 17c71e225c2..090f0a37db4 100644 --- a/internal/formats/common/cyclonedxhelpers/properties.go +++ b/internal/formats/common/cyclonedxhelpers/properties.go @@ -8,7 +8,7 @@ import ( "github.com/anchore/syft/syft/pkg" ) -func Properties(p pkg.Package) *[]cyclonedx.Property { +func encodeProperties(p pkg.Package) *[]cyclonedx.Property { props := []cyclonedx.Property{} props = append(props, *getCycloneDXProperties(p)...) if len(p.Locations) > 0 { @@ -76,3 +76,12 @@ func getCycloneDXPropertyValue(field reflect.Value) interface{} { } return "" } + +func findPropertyValue(c *cyclonedx.Component, name string) string { + for _, p := range *c.Properties { + if p.Name == name { + return p.Value + } + } + return "" +} diff --git a/internal/formats/common/cyclonedxhelpers/properties_test.go b/internal/formats/common/cyclonedxhelpers/properties_test.go index 96a5a4c47a7..a6cf701c40a 100644 --- a/internal/formats/common/cyclonedxhelpers/properties_test.go +++ b/internal/formats/common/cyclonedxhelpers/properties_test.go @@ -9,7 +9,7 @@ import ( "github.com/stretchr/testify/assert" ) -func Test_Properties(t *testing.T) { +func Test_encodeProperties(t *testing.T) { epoch := 2 tests := []struct { name string @@ -132,7 +132,7 @@ func Test_Properties(t *testing.T) { } for _, test := range tests { t.Run(test.name, func(t *testing.T) { - assert.Equal(t, test.expected, Properties(test.input)) + assert.Equal(t, test.expected, encodeProperties(test.input)) }) } } diff --git a/internal/formats/common/cyclonedxhelpers/publisher.go b/internal/formats/common/cyclonedxhelpers/publisher.go index 71e64d5c4ed..87e51c3c5f8 100644 --- a/internal/formats/common/cyclonedxhelpers/publisher.go +++ b/internal/formats/common/cyclonedxhelpers/publisher.go @@ -4,7 +4,7 @@ import ( "github.com/anchore/syft/syft/pkg" ) -func Publisher(p pkg.Package) string { +func encodePublisher(p pkg.Package) string { if hasMetadata(p) { switch metadata := p.Metadata.(type) { case pkg.ApkMetadata: @@ -17,3 +17,14 @@ func Publisher(p pkg.Package) string { } return "" } + +func decodePublisher(publisher string, metadata interface{}) { + switch meta := metadata.(type) { + case *pkg.ApkMetadata: + meta.Maintainer = publisher + case *pkg.RpmdbMetadata: + meta.Vendor = publisher + case *pkg.DpkgMetadata: + meta.Maintainer = publisher + } +} diff --git a/internal/formats/common/cyclonedxhelpers/publisher_test.go b/internal/formats/common/cyclonedxhelpers/publisher_test.go index d1f68c154f2..03e69db2c6c 100644 --- a/internal/formats/common/cyclonedxhelpers/publisher_test.go +++ b/internal/formats/common/cyclonedxhelpers/publisher_test.go @@ -7,7 +7,7 @@ import ( "github.com/stretchr/testify/assert" ) -func Test_Publisher(t *testing.T) { +func Test_encodePublisher(t *testing.T) { tests := []struct { name string input pkg.Package @@ -59,7 +59,7 @@ func Test_Publisher(t *testing.T) { } for _, test := range tests { t.Run(test.name, func(t *testing.T) { - assert.Equal(t, test.expected, Publisher(test.input)) + assert.Equal(t, test.expected, encodePublisher(test.input)) }) } } diff --git a/internal/formats/cyclonedx13json/decoder_test.go b/internal/formats/cyclonedx13json/decoder_test.go new file mode 100644 index 00000000000..b1d038913d9 --- /dev/null +++ b/internal/formats/cyclonedx13json/decoder_test.go @@ -0,0 +1,70 @@ +package cyclonedx13json + +import ( + "fmt" + "os" + "strings" + "testing" + + "github.com/stretchr/testify/assert" +) + +func Test_decodeJSON(t *testing.T) { + tests := []struct { + file string + err bool + distro string + packages []string + }{ + { + file: "snapshot/TestCycloneDxDirectoryEncoder.golden", + distro: "debian:1.2.3", + packages: []string{"package-1:1.0.1", "package-2:2.0.1"}, + }, + { + file: "snapshot/TestCycloneDxImageEncoder.golden", + distro: "debian:1.2.3", + packages: []string{"package-1:1.0.1", "package-2:2.0.1"}, + }, + { + file: "image-simple/Dockerfile", + err: true, + }, + } + for _, test := range tests { + t.Run(test.file, func(t *testing.T) { + reader, err := os.Open("test-fixtures/" + test.file) + assert.NoError(t, err) + + if test.err { + err = Format().Validate(reader) + assert.Error(t, err) + return + } + + bom, err := Format().Decode(reader) + + assert.NoError(t, err) + + split := strings.SplitN(test.distro, ":", 2) + name := split[0] + version := split[1] + assert.Equal(t, bom.Artifacts.LinuxDistribution.ID, name) + assert.Equal(t, bom.Artifacts.LinuxDistribution.Version, version) + + pkgs: + for _, pkg := range test.packages { + split = strings.SplitN(pkg, ":", 2) + name = split[0] + version = split[1] + for p := range bom.Artifacts.PackageCatalog.Enumerate() { + if p.Name == name { + assert.Equal(t, version, p.Version) + continue pkgs + } + } + assert.Fail(t, fmt.Sprintf("package should be present: %s", pkg)) + } + }) + } +} diff --git a/internal/formats/cyclonedx13json/format.go b/internal/formats/cyclonedx13json/format.go index afbe4336f35..0f3aa494c37 100644 --- a/internal/formats/cyclonedx13json/format.go +++ b/internal/formats/cyclonedx13json/format.go @@ -1,12 +1,16 @@ package cyclonedx13json -import "github.com/anchore/syft/syft/format" +import ( + "github.com/CycloneDX/cyclonedx-go" + "github.com/anchore/syft/internal/formats/common/cyclonedxhelpers" + "github.com/anchore/syft/syft/format" +) func Format() format.Format { return format.NewFormat( format.CycloneDxJSONOption, encoder, - nil, - nil, + cyclonedxhelpers.GetDecoder(cyclonedx.BOMFileFormatJSON), + cyclonedxhelpers.GetValidator(cyclonedx.BOMFileFormatJSON), ) } diff --git a/internal/formats/cyclonedx13json/test-fixtures/snapshot/TestCycloneDxDirectoryEncoder.golden b/internal/formats/cyclonedx13json/test-fixtures/snapshot/TestCycloneDxDirectoryEncoder.golden index e76154bc9a0..d9edf73bfb0 100644 --- a/internal/formats/cyclonedx13json/test-fixtures/snapshot/TestCycloneDxDirectoryEncoder.golden +++ b/internal/formats/cyclonedx13json/test-fixtures/snapshot/TestCycloneDxDirectoryEncoder.golden @@ -1,10 +1,10 @@ { "bomFormat": "CycloneDX", "specVersion": "1.3", - "serialNumber": "urn:uuid:258d2616-5b1f-48cd-82a3-d6c95e262950", + "serialNumber": "urn:uuid:326afa86-5620-4a80-8f2b-7f283b954b9b", "version": 1, "metadata": { - "timestamp": "2022-01-14T22:47:00Z", + "timestamp": "2022-02-10T17:19:38-05:00", "tools": [ { "vendor": "anchore", @@ -13,6 +13,7 @@ } ], "component": { + "bom-ref": "163686ac6e30c752", "type": "file", "name": "/some/path", "version": "" @@ -84,6 +85,12 @@ "type": "operating-system", "name": "debian", "version": "1.2.3", + "description": "debian", + "swid": { + "tagId": "debian", + "name": "debian", + "version": "1.2.3" + }, "properties": [ { "name": "prettyName", diff --git a/internal/formats/cyclonedx13json/test-fixtures/snapshot/TestCycloneDxImageEncoder.golden b/internal/formats/cyclonedx13json/test-fixtures/snapshot/TestCycloneDxImageEncoder.golden index 9505eb86599..9358fa51b6e 100644 --- a/internal/formats/cyclonedx13json/test-fixtures/snapshot/TestCycloneDxImageEncoder.golden +++ b/internal/formats/cyclonedx13json/test-fixtures/snapshot/TestCycloneDxImageEncoder.golden @@ -1,10 +1,10 @@ { "bomFormat": "CycloneDX", "specVersion": "1.3", - "serialNumber": "urn:uuid:8a84b1cf-e918-4842-a6a8-c7fdafc55bc0", + "serialNumber": "urn:uuid:761a2036-0f25-4787-bf28-f5e9a7d9a0bf", "version": 1, "metadata": { - "timestamp": "2022-01-14T22:47:00Z", + "timestamp": "2022-02-10T17:19:38-05:00", "tools": [ { "vendor": "anchore", @@ -13,6 +13,7 @@ } ], "component": { + "bom-ref": "4f9453fd20e0cf80", "type": "container", "name": "user-image-input", "version": "sha256:2731251dc34951c0e50fcc643b4c5f74922dad1a5d98f302b504cf46cd5d9368" @@ -55,7 +56,7 @@ }, { "name": "layerID", - "value": "sha256:16e64541f2ddf59a90391ce7bb8af90313f7d373f2105d88f3d3267b72e0ebab" + "value": "sha256:41e7295da66c405eb3a4df29188dcf80f622f9304d487033a86d4a22e3f01abe" } ] }, @@ -84,7 +85,7 @@ }, { "name": "layerID", - "value": "sha256:de6c235f76ea24c8503ec08891445b5d6a8bdf8249117ed8d8b0b6fb3ebe4f67" + "value": "sha256:68a2c166dcb3acf6b7303e995ca1fe7d794bd3b5852a0b4048f9c96b796086aa" } ] }, @@ -92,6 +93,12 @@ "type": "operating-system", "name": "debian", "version": "1.2.3", + "description": "debian", + "swid": { + "tagId": "debian", + "name": "debian", + "version": "1.2.3" + }, "properties": [ { "name": "prettyName", diff --git a/internal/formats/cyclonedx13json/test-fixtures/snapshot/stereoscope-fixture-image-simple.golden b/internal/formats/cyclonedx13json/test-fixtures/snapshot/stereoscope-fixture-image-simple.golden index c483fa49b75941b2cfb6a6819ff94a681f25ee41..afbd18d5a138193f379091613e3f16239e26ca8a 100644 GIT binary patch literal 15360 zcmeHOZBN@s5YFfO6*=EmlFYu^8|gl91*w&)qiVUHbO@pKdKY{oc4Rvh2=U)zZH~k(Dwgt z41w16|8JRRE3RpU{S!f#{XYT5z5TC%buG;w~C*sQ8$#W6R+g`fo( zp2U{RL~#SoM%1xTOUj&5nhR$LKIg$|&M^4UCNUDj4u-N!VRdDjg#jf!LVKVNNjL;G z6wv29Vj(4z{8M+$vr4C#El%*vrKiF0DtMolRi?)l7lS?a@-cR}EB{s6V%D--Roa|= zsgg7chE9)4yA#~jx4&*y(Pf2Xw8|7P;P9EgwqH?RFp$LD{yf%4m( zqqY6F&;Np?FwAb1=C44cDeYM;u~zG4Egwf%1-|3`_C z{`tQR2XLGJBd?kNlMnNM$p6^No9UnY{eN|qmfboD*gezbZ5nVJcb=caBj6G62zUfu z69FYyq!pElktm8oDHSlVU?VN!SVUpOVhO^5AtKgD&rDoQRBRNBj5ZOde*66&p?rD& z2gn8W&;M=Q7gxa}q=q{-O~YGR6HjA-(^69~7APz=Y32KwKNz`Gd4U|9qpMZJDC3e) zp_*;zY=)B9f? z2(EWlE@XkZ^uG&f-oxKp$gKR#g&-s5-kx$NDDu2=$Wplqs>?}DU1@f0C0@>I+DO;HFu%Tjn-;VHr!RN}JOUnp I9|nQ{0PKm6EdT%j literal 15360 zcmeHOZExE)5ccQ&3Xl7m*nFpCU>~xkKnoO0(Pka6pa>|wBwB6BkmRC4kpI4u>^N?+ zMoXlq8A`!`M3Fq5?szos;UhwsawN7!<0N9G_Y)S?;cIte-{TyY9lK0QOTOY|x)t4uE(r~oYv^93}xDgRNrVqJ4kRoa}tuWXv3 znb-5u?V;_xd{y1O=?dt&X?a{-Y_E@&F8f+eQ##5FD6wh(GW(2X*=j!DLoXKaM(fMh z3w^dx^g}*hEnInB^JiXsO0%=WbffLdtST;+d76P<>%K?XZ>w~^74=3ZZocV$wTE)J zMDsLTeL+_(vD9~USI1B1`oa~HdJXl>qb2_N-Sr<)M1}bOra^nGDD#E$>D=v;N%f`b zQSgqa7XM>R<#ztZoQyCbip${t9TPs;Wp$pG<9Yy$&viMjr)ZZ|<&!-w2o4qq76=vy z7I-EWkXR;>QJOjuMLGc>kW#?ll#rO}NXN`L>ZPLE8N%UAW9PMHF&D;L?a342f1pFa z|ArVMo&Enb_#X}V{}A3EjYz=%hB5RreM;x}?~eb~8$a9pZ@vFR2n+cCAR}gj|8*S( z0NyQi0jXo(SR#eZ$9+O;ERoO|VKdGq91|839x%oGFvb4`#(IL@9vr{=b-F5x={z@j zJ}r|po9&dh#hZ%e*18Cn=;LER+9?d!hoZE)Fz0FIOtmU%7+5c?qS?n;^+;|qdNe}ryWq0_0 zkoJZ3-w>nc5`81Bxyc_JHcU^M+E+}OItlxV7z0$88x~PZ<`4VURbBwE=j5`b6XQ%W zP8+yj%~3i{pdUIoJZyHp*VKpcbh{L8s1V~{#z{@t`IJ_mrpXtJHM((qb7FJz$rbhb zY4RDPtGzZgc2SFeRijyf@2IgZKTr+GhIbOuJyhg*<$+#wiK>fbowhc7of2Njrrw{X z7V1t=d9JCDGa{Yjf)fuEZ!cmUW58LAQ%MqO4+X-!vP>~g2^KbrJhO~Sl_=`4OLPKK zH?k!Nsqw!zH3zYITz_;%c#rJBV1ZzP JV1eE(@DIZvjQs!r diff --git a/internal/formats/cyclonedx13xml/decoder_test.go b/internal/formats/cyclonedx13xml/decoder_test.go new file mode 100644 index 00000000000..30f5c1bf166 --- /dev/null +++ b/internal/formats/cyclonedx13xml/decoder_test.go @@ -0,0 +1,70 @@ +package cyclonedx13xml + +import ( + "fmt" + "os" + "strings" + "testing" + + "github.com/stretchr/testify/assert" +) + +func Test_decodeXML(t *testing.T) { + tests := []struct { + file string + err bool + distro string + packages []string + }{ + { + file: "snapshot/TestCycloneDxDirectoryEncoder.golden", + distro: "debian:1.2.3", + packages: []string{"package-1:1.0.1", "package-2:2.0.1"}, + }, + { + file: "snapshot/TestCycloneDxImageEncoder.golden", + distro: "debian:1.2.3", + packages: []string{"package-1:1.0.1", "package-2:2.0.1"}, + }, + { + file: "image-simple/Dockerfile", + err: true, + }, + } + for _, test := range tests { + t.Run(test.file, func(t *testing.T) { + reader, err := os.Open("test-fixtures/" + test.file) + assert.NoError(t, err) + + if test.err { + err = Format().Validate(reader) + assert.Error(t, err) + return + } + + bom, err := Format().Decode(reader) + + assert.NoError(t, err) + + split := strings.SplitN(test.distro, ":", 2) + name := split[0] + version := split[1] + assert.Equal(t, bom.Artifacts.LinuxDistribution.ID, name) + assert.Equal(t, bom.Artifacts.LinuxDistribution.Version, version) + + pkgs: + for _, pkg := range test.packages { + split = strings.SplitN(pkg, ":", 2) + name = split[0] + version = split[1] + for p := range bom.Artifacts.PackageCatalog.Enumerate() { + if p.Name == name { + assert.Equal(t, version, p.Version) + continue pkgs + } + } + assert.Fail(t, fmt.Sprintf("package should be present: %s", pkg)) + } + }) + } +} diff --git a/internal/formats/cyclonedx13xml/format.go b/internal/formats/cyclonedx13xml/format.go index 173aa36afd5..9ba8d940112 100644 --- a/internal/formats/cyclonedx13xml/format.go +++ b/internal/formats/cyclonedx13xml/format.go @@ -1,12 +1,16 @@ package cyclonedx13xml -import "github.com/anchore/syft/syft/format" +import ( + "github.com/CycloneDX/cyclonedx-go" + "github.com/anchore/syft/internal/formats/common/cyclonedxhelpers" + "github.com/anchore/syft/syft/format" +) func Format() format.Format { return format.NewFormat( format.CycloneDxXMLOption, encoder, - nil, - nil, + cyclonedxhelpers.GetDecoder(cyclonedx.BOMFileFormatXML), + cyclonedxhelpers.GetValidator(cyclonedx.BOMFileFormatXML), ) } diff --git a/internal/formats/cyclonedx13xml/test-fixtures/snapshot/TestCycloneDxDirectoryEncoder.golden b/internal/formats/cyclonedx13xml/test-fixtures/snapshot/TestCycloneDxDirectoryEncoder.golden index 4b4ad555233..d5bb2a495ac 100644 --- a/internal/formats/cyclonedx13xml/test-fixtures/snapshot/TestCycloneDxDirectoryEncoder.golden +++ b/internal/formats/cyclonedx13xml/test-fixtures/snapshot/TestCycloneDxDirectoryEncoder.golden @@ -1,7 +1,7 @@ - + - 2022-01-14T22:46:49Z + 2022-02-10T17:18:31-05:00 anchore @@ -9,7 +9,7 @@ [not provided] - + /some/path @@ -48,6 +48,8 @@ debian 1.2.3 + debian + debian debian diff --git a/internal/formats/cyclonedx13xml/test-fixtures/snapshot/TestCycloneDxImageEncoder.golden b/internal/formats/cyclonedx13xml/test-fixtures/snapshot/TestCycloneDxImageEncoder.golden index 58421601882..eb6b8634b45 100644 --- a/internal/formats/cyclonedx13xml/test-fixtures/snapshot/TestCycloneDxImageEncoder.golden +++ b/internal/formats/cyclonedx13xml/test-fixtures/snapshot/TestCycloneDxImageEncoder.golden @@ -1,7 +1,7 @@ - + - 2022-01-14T22:46:49Z + 2022-02-10T17:18:31-05:00 anchore @@ -9,7 +9,7 @@ [not provided] - + user-image-input sha256:2731251dc34951c0e50fcc643b4c5f74922dad1a5d98f302b504cf46cd5d9368 @@ -31,7 +31,7 @@ python PythonPackageMetadata /somefile-1.txt - sha256:16e64541f2ddf59a90391ce7bb8af90313f7d373f2105d88f3d3267b72e0ebab + sha256:41e7295da66c405eb3a4df29188dcf80f622f9304d487033a86d4a22e3f01abe @@ -44,12 +44,14 @@ deb DpkgMetadata /somefile-2.txt - sha256:de6c235f76ea24c8503ec08891445b5d6a8bdf8249117ed8d8b0b6fb3ebe4f67 + sha256:68a2c166dcb3acf6b7303e995ca1fe7d794bd3b5852a0b4048f9c96b796086aa debian 1.2.3 + debian + debian debian diff --git a/internal/formats/cyclonedx13xml/test-fixtures/snapshot/stereoscope-fixture-image-simple.golden b/internal/formats/cyclonedx13xml/test-fixtures/snapshot/stereoscope-fixture-image-simple.golden index c483fa49b75941b2cfb6a6819ff94a681f25ee41..afbd18d5a138193f379091613e3f16239e26ca8a 100644 GIT binary patch literal 15360 zcmeHOZBN@s5YFfO6*=EmlFYu^8|gl91*w&)qiVUHbO@pKdKY{oc4Rvh2=U)zZH~k(Dwgt z41w16|8JRRE3RpU{S!f#{XYT5z5TC%buG;w~C*sQ8$#W6R+g`fo( zp2U{RL~#SoM%1xTOUj&5nhR$LKIg$|&M^4UCNUDj4u-N!VRdDjg#jf!LVKVNNjL;G z6wv29Vj(4z{8M+$vr4C#El%*vrKiF0DtMolRi?)l7lS?a@-cR}EB{s6V%D--Roa|= zsgg7chE9)4yA#~jx4&*y(Pf2Xw8|7P;P9EgwqH?RFp$LD{yf%4m( zqqY6F&;Np?FwAb1=C44cDeYM;u~zG4Egwf%1-|3`_C z{`tQR2XLGJBd?kNlMnNM$p6^No9UnY{eN|qmfboD*gezbZ5nVJcb=caBj6G62zUfu z69FYyq!pElktm8oDHSlVU?VN!SVUpOVhO^5AtKgD&rDoQRBRNBj5ZOde*66&p?rD& z2gn8W&;M=Q7gxa}q=q{-O~YGR6HjA-(^69~7APz=Y32KwKNz`Gd4U|9qpMZJDC3e) zp_*;zY=)B9f? z2(EWlE@XkZ^uG&f-oxKp$gKR#g&-s5-kx$NDDu2=$Wplqs>?}DU1@f0C0@>I+DO;HFu%Tjn-;VHr!RN}JOUnp I9|nQ{0PKm6EdT%j literal 15360 zcmeHOZExE)5ccQ&3Xl7m*nFpCU>~xkKnoO0(Pka6pa>|wBwB6BkmRC4kpI4u>^N?+ zMoXlq8A`!`M3Fq5?szos;UhwsawN7!<0N9G_Y)S?;cIte-{TyY9lK0QOTOY|x)t4uE(r~oYv^93}xDgRNrVqJ4kRoa}tuWXv3 znb-5u?V;_xd{y1O=?dt&X?a{-Y_E@&F8f+eQ##5FD6wh(GW(2X*=j!DLoXKaM(fMh z3w^dx^g}*hEnInB^JiXsO0%=WbffLdtST;+d76P<>%K?XZ>w~^74=3ZZocV$wTE)J zMDsLTeL+_(vD9~USI1B1`oa~HdJXl>qb2_N-Sr<)M1}bOra^nGDD#E$>D=v;N%f`b zQSgqa7XM>R<#ztZoQyCbip${t9TPs;Wp$pG<9Yy$&viMjr)ZZ|<&!-w2o4qq76=vy z7I-EWkXR;>QJOjuMLGc>kW#?ll#rO}NXN`L>ZPLE8N%UAW9PMHF&D;L?a342f1pFa z|ArVMo&Enb_#X}V{}A3EjYz=%hB5RreM;x}?~eb~8$a9pZ@vFR2n+cCAR}gj|8*S( z0NyQi0jXo(SR#eZ$9+O;ERoO|VKdGq91|839x%oGFvb4`#(IL@9vr{=b-F5x={z@j zJ}r|po9&dh#hZ%e*18Cn=;LER+9?d!hoZE)Fz0FIOtmU%7+5c?qS?n;^+;|qdNe}ryWq0_0 zkoJZ3-w>nc5`81Bxyc_JHcU^M+E+}OItlxV7z0$88x~PZ<`4VURbBwE=j5`b6XQ%W zP8+yj%~3i{pdUIoJZyHp*VKpcbh{L8s1V~{#z{@t`IJ_mrpXtJHM((qb7FJz$rbhb zY4RDPtGzZgc2SFeRijyf@2IgZKTr+GhIbOuJyhg*<$+#wiK>fbowhc7of2Njrrw{X z7V1t=d9JCDGa{Yjf)fuEZ!cmUW58LAQ%MqO4+X-!vP>~g2^KbrJhO~Sl_=`4OLPKK zH?k!Nsqw!zH3zYITz_;%c#rJBV1ZzP JV1eE(@DIZvjQs!r diff --git a/syft/pkg/cataloger/rust/cargo_metadata.go b/syft/pkg/cargo_metadata.go similarity index 58% rename from syft/pkg/cataloger/rust/cargo_metadata.go rename to syft/pkg/cargo_metadata.go index 7b1563d83aa..f5c4a6694ed 100644 --- a/syft/pkg/cataloger/rust/cargo_metadata.go +++ b/syft/pkg/cargo_metadata.go @@ -1,14 +1,12 @@ -package rust - -import "github.com/anchore/syft/syft/pkg" +package pkg type CargoMetadata struct { - Packages []pkg.CargoPackageMetadata `toml:"package"` + Packages []CargoPackageMetadata `toml:"package"` } // Pkgs returns all of the packages referenced within the Cargo.lock metadata. -func (m CargoMetadata) Pkgs() []*pkg.Package { - pkgs := make([]*pkg.Package, 0) +func (m CargoMetadata) Pkgs() []*Package { + pkgs := make([]*Package, 0) for _, p := range m.Packages { if p.Dependencies == nil { diff --git a/syft/pkg/cataloger/rust/parse_cargo_lock.go b/syft/pkg/cataloger/rust/parse_cargo_lock.go index 87859e98c81..8910a70dd1a 100644 --- a/syft/pkg/cataloger/rust/parse_cargo_lock.go +++ b/syft/pkg/cataloger/rust/parse_cargo_lock.go @@ -20,7 +20,7 @@ func parseCargoLock(_ string, reader io.Reader) ([]*pkg.Package, []artifact.Rela return nil, nil, fmt.Errorf("unable to load Cargo.lock for parsing: %v", err) } - metadata := CargoMetadata{} + metadata := pkg.CargoMetadata{} err = tree.Unmarshal(&metadata) if err != nil { return nil, nil, fmt.Errorf("unable to parse Cargo.lock: %v", err) diff --git a/syft/pkg/metadata.go b/syft/pkg/metadata.go index a7021b4e3cb..10c86bdfe90 100644 --- a/syft/pkg/metadata.go +++ b/syft/pkg/metadata.go @@ -1,5 +1,9 @@ package pkg +import ( + "reflect" +) + // MetadataType represents the data shape stored within pkg.Package.Metadata. type MetadataType string @@ -33,3 +37,17 @@ var AllMetadataTypes = []MetadataType{ GolangBinMetadataType, PhpComposerJSONMetadataType, } + +var MetadataTypeByName = map[MetadataType]reflect.Type{ + ApkMetadataType: reflect.TypeOf(ApkMetadata{}), + DpkgMetadataType: reflect.TypeOf(DpkgMetadata{}), + GemMetadataType: reflect.TypeOf(GemMetadata{}), + JavaMetadataType: reflect.TypeOf(JavaMetadata{}), + NpmPackageJSONMetadataType: reflect.TypeOf(NpmPackageJSONMetadata{}), + RpmdbMetadataType: reflect.TypeOf(RpmdbMetadata{}), + PythonPackageMetadataType: reflect.TypeOf(PythonPackageMetadata{}), + RustCargoPackageMetadataType: reflect.TypeOf(CargoMetadata{}), + KbPackageMetadataType: reflect.TypeOf(KbPackageMetadata{}), + GolangBinMetadataType: reflect.TypeOf(GolangBinMetadata{}), + PhpComposerJSONMetadataType: reflect.TypeOf(PhpComposerJSONMetadata{}), +} diff --git a/test/integration/encode_decode_cycle_test.go b/test/integration/encode_decode_cycle_test.go index e06d2609fed..9d222a4d1a4 100644 --- a/test/integration/encode_decode_cycle_test.go +++ b/test/integration/encode_decode_cycle_test.go @@ -2,6 +2,7 @@ package integration import ( "bytes" + "regexp" "testing" "github.com/anchore/syft/syft" @@ -20,11 +21,27 @@ import ( // encode-decode-encode loop which will detect lossy behavior in both directions. func TestEncodeDecodeEncodeCycleComparison(t *testing.T) { tests := []struct { - format format.Option + format format.Option + redactor func(in []byte) []byte }{ { format: format.JSONOption, }, + { + format: format.CycloneDxJSONOption, + redactor: func(in []byte) []byte { + in = regexp.MustCompile("\"(timestamp|serialNumber|bom-ref)\": \"[^\"]+").ReplaceAll(in, []byte{}) + return in + }, + }, + { + format: format.CycloneDxXMLOption, + redactor: func(in []byte) []byte { + in = regexp.MustCompile("(serialNumber|bom-ref)=\"[^\"]+").ReplaceAll(in, []byte{}) + in = regexp.MustCompile("[^<]+").ReplaceAll(in, []byte{}) + return in + }, + }, } for _, test := range tests { t.Run(string(test.format), func(t *testing.T) { @@ -33,6 +50,7 @@ func TestEncodeDecodeEncodeCycleComparison(t *testing.T) { by1, err := syft.Encode(originalSBOM, test.format) assert.NoError(t, err) + newSBOM, newFormat, err := syft.Decode(bytes.NewReader(by1)) assert.NoError(t, err) assert.Equal(t, test.format, newFormat) @@ -40,6 +58,11 @@ func TestEncodeDecodeEncodeCycleComparison(t *testing.T) { by2, err := syft.Encode(*newSBOM, test.format) assert.NoError(t, err) + if test.redactor != nil { + by1 = test.redactor(by1) + by2 = test.redactor(by2) + } + if !assert.True(t, bytes.Equal(by1, by2)) { dmp := diffmatchpatch.New() diffs := dmp.DiffMain(string(by1), string(by2), true)