From 22d6d4360be3affeac6f3744366b2d6175ca74d4 Mon Sep 17 00:00:00 2001 From: Keith Zantow Date: Thu, 10 Feb 2022 13:07:30 -0500 Subject: [PATCH 01/14] add cyclonedx decoder Signed-off-by: Keith Zantow --- .../formats/common/cyclonedxhelpers/author.go | 2 +- .../common/cyclonedxhelpers/author_test.go | 4 +- .../common/cyclonedxhelpers/component.go | 107 ++++++++++++++++-- .../formats/common/cyclonedxhelpers/cpe.go | 31 ++++- .../common/cyclonedxhelpers/cpe_test.go | 6 +- .../common/cyclonedxhelpers/description.go | 2 +- .../cyclonedxhelpers/description_test.go | 4 +- .../cyclonedxhelpers/external_references.go | 2 +- .../external_references_test.go | 4 +- .../formats/common/cyclonedxhelpers/format.go | 16 ++- .../formats/common/cyclonedxhelpers/group.go | 2 +- .../common/cyclonedxhelpers/group_test.go | 4 +- .../common/cyclonedxhelpers/licenses.go | 11 +- .../common/cyclonedxhelpers/licenses_test.go | 4 +- .../common/cyclonedxhelpers/properties.go | 29 ++++- .../cyclonedxhelpers/properties_test.go | 4 +- .../common/cyclonedxhelpers/publisher.go | 2 +- .../common/cyclonedxhelpers/publisher_test.go | 4 +- internal/formats/cyclonedx13json/format.go | 10 +- internal/formats/cyclonedx13xml/format.go | 10 +- 20 files changed, 212 insertions(+), 46 deletions(-) diff --git a/internal/formats/common/cyclonedxhelpers/author.go b/internal/formats/common/cyclonedxhelpers/author.go index 9f2cff9e6cb..1a6e477b10f 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: 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..f82cdf73faa 100644 --- a/internal/formats/common/cyclonedxhelpers/component.go +++ b/internal/formats/common/cyclonedxhelpers/component.go @@ -5,23 +5,112 @@ import ( "github.com/anchore/syft/syft/pkg" ) -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, error) { + cp, err := decodeCPEs(c) + if err != nil { + return nil, err + } + + typ := pkg.Type(prop(c, "type")) + purl := c.PackageURL + if typ == "" { + if purl != "" { + typ = pkg.TypeFromPURL(purl) + } + } + + p := &pkg.Package{ + Name: c.Name, + Version: c.Version, + FoundBy: prop(c, "foundBy"), + Locations: nil, + Licenses: decodeLicenses(c), + Language: pkg.Language(prop(c, "language")), + Type: pkg.Type(typ), + CPEs: cp, + PURL: purl, + } + + metaType, meta, err := decodePackageMetadata(c) + + if err == nil { + p.MetadataType = metaType + p.Metadata = meta + } + + return p, nil +} + +func decodePackageMetadata(c *cyclonedx.Component) (pkg.MetadataType, interface{}, error) { + if c.Properties != nil { + typ := prop(c, "metadataType") + if typ != "" { + switch typ { + case "ApkMetadata": + return pkg.ApkMetadataType, pkg.ApkMetadata{ + Package: prop(c, "package"), + OriginPackage: prop(c, "originPackage"), + Maintainer: prop(c, "maintainer"), + Version: prop(c, "version"), + License: prop(c, "license"), + Architecture: prop(c, "architecture"), + URL: prop(c, "url"), + Description: prop(c, "description"), + Size: propInt(c, "size"), + InstalledSize: propInt(c, "installedSize"), + PullDependencies: prop(c, "pullDependencies"), + PullChecksum: prop(c, "pullChecksum"), + GitCommitOfAport: prop(c, "gitCommitOfAport"), + Files: []pkg.ApkFileRecord{}, + }, nil + case "DpkgMetadata": + return pkg.DpkgMetadataType, pkg.DpkgMetadata{ + Package: prop(c, "package"), + Source: prop(c, "source"), + Version: prop(c, "version"), + SourceVersion: prop(c, "sourceVersion"), + Architecture: prop(c, "architecture"), + Maintainer: prop(c, "maintainer"), + InstalledSize: propInt(c, "installedSize"), + Files: []pkg.DpkgFileRecord{}, + }, nil + case "RpmdbMetadata": + return pkg.RpmdbMetadataType, pkg.RpmdbMetadata{ + Name: prop(c, "name"), + Version: prop(c, "version"), + Epoch: propIntNil(c, "epoch"), + Arch: prop(c, "arch"), + Release: prop(c, "release"), + SourceRpm: prop(c, "sourceRpm"), + Size: propInt(c, "size"), + License: prop(c, "license"), + Vendor: prop(c, "vendor"), + Files: []pkg.RpmdbFileRecord{}, + }, nil + } + } + } + + return pkg.UnknownMetadataType, nil, nil +} diff --git a/internal/formats/common/cyclonedxhelpers/cpe.go b/internal/formats/common/cyclonedxhelpers/cpe.go index 6b43f847ef8..f4048618869 100644 --- a/internal/formats/common/cyclonedxhelpers/cpe.go +++ b/internal/formats/common/cyclonedxhelpers/cpe.go @@ -1,12 +1,37 @@ package cyclonedxhelpers -import "github.com/anchore/syft/syft/pkg" +import ( + "github.com/CycloneDX/cyclonedx-go" + "github.com/anchore/syft/syft/pkg" + "github.com/anchore/syft/syft/pkg/cataloger/common/cpe" +) -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 + // we can extract the first encodeCPE as the one to output in cyclonedx if len(p.CPEs) > 0 { return pkg.CPEString(p.CPEs[0]) } return "" } + +func decodeCPEs(c *cyclonedx.Component) ([]pkg.CPE, error) { + // FIXME -- why are we not encoding all the CPEs and what is the right behavior to decode them? + cp, err := pkg.NewCPE(c.CPE) + if err != nil { + return nil, err + } + return cpe.Generate(pkg.Package{ + Name: c.Name, + Version: c.Version, + FoundBy: "", + Locations: nil, + Licenses: nil, + Language: "", + Type: "", + CPEs: []pkg.CPE{cp}, + PURL: c.PackageURL, + MetadataType: "", + Metadata: nil, + }), nil +} diff --git a/internal/formats/common/cyclonedxhelpers/cpe_test.go b/internal/formats/common/cyclonedxhelpers/cpe_test.go index 81d6f6f0784..20d39b8cadc 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 { @@ -24,7 +24,7 @@ func Test_CPE(t *testing.T) { expected: "", }, { - name: "single CPE", + name: "single encodeCPE", input: pkg.Package{ CPEs: []pkg.CPE{ testCPE, @@ -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/description.go b/internal/formats/common/cyclonedxhelpers/description.go index 176c019895f..4f7f8a64ccc 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: 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..67ba2970afb 100644 --- a/internal/formats/common/cyclonedxhelpers/external_references.go +++ b/internal/formats/common/cyclonedxhelpers/external_references.go @@ -7,7 +7,7 @@ import ( "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) { 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..2aeba5ef40e 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 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..9d5c958ba81 100644 --- a/internal/formats/common/cyclonedxhelpers/properties.go +++ b/internal/formats/common/cyclonedxhelpers/properties.go @@ -3,12 +3,13 @@ package cyclonedxhelpers import ( "fmt" "reflect" + "strconv" "github.com/CycloneDX/cyclonedx-go" "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 +77,29 @@ func getCycloneDXPropertyValue(field reflect.Value) interface{} { } return "" } + +func prop(c *cyclonedx.Component, name string) string { + for _, p := range *c.Properties { + if p.Name == name { + return p.Value + } + } + return "" +} + +func propInt(c *cyclonedx.Component, name string) int { + v, err := strconv.Atoi(prop(c, name)) + if err != nil { + return 0 + } + return v + +} + +func propIntNil(c *cyclonedx.Component, name string) *int { + v, err := strconv.Atoi(prop(c, name)) + if err != nil { + return nil + } + return &v +} 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..045053c31fd 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: 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/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/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), ) } From e25c2539e5091a29c9993c15148caedaa39d5065 Mon Sep 17 00:00:00 2001 From: Keith Zantow Date: Thu, 10 Feb 2022 13:10:09 -0500 Subject: [PATCH 02/14] oops Signed-off-by: Keith Zantow --- .../common/cyclonedxhelpers/cpe_test.go | 2 +- .../common/cyclonedxhelpers/decoder.go | 246 ++++++++++++++++++ 2 files changed, 247 insertions(+), 1 deletion(-) create mode 100644 internal/formats/common/cyclonedxhelpers/decoder.go diff --git a/internal/formats/common/cyclonedxhelpers/cpe_test.go b/internal/formats/common/cyclonedxhelpers/cpe_test.go index 20d39b8cadc..4678b9a926e 100644 --- a/internal/formats/common/cyclonedxhelpers/cpe_test.go +++ b/internal/formats/common/cyclonedxhelpers/cpe_test.go @@ -24,7 +24,7 @@ func Test_encodeCPE(t *testing.T) { expected: "", }, { - name: "single encodeCPE", + name: "single CPE", input: pkg.Package{ CPEs: []pkg.CPE{ testCPE, diff --git a/internal/formats/common/cyclonedxhelpers/decoder.go b/internal/formats/common/cyclonedxhelpers/decoder.go new file mode 100644 index 00000000000..98b129d99a1 --- /dev/null +++ b/internal/formats/common/cyclonedxhelpers/decoder.go @@ -0,0 +1,246 @@ +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) { + s := &sbom.SBOM{ + Artifacts: sbom.Artifacts{ + PackageCatalog: pkg.NewCatalog(), + LinuxDistribution: linuxReleaseFromComponents(*bom.Components), + }, + Source: decodeMetadata(bom.Metadata.Component), + //Descriptor: sbom.Descriptor{}, + } + + idMap := make(map[string]interface{}) + + if err := collectBomPackages(bom, s, idMap); err != nil { + return nil, err + } + + if err := collectRelationships(bom, s, idMap); err != nil { + return nil, err + } + + 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 _, component := range *bom.Components { + if err := collectPackages(&component, s, idMap); err != nil { + return err + } + } + return nil +} + +func collectPackages(component *cyclonedx.Component, s *sbom.SBOM, idMap map[string]interface{}) error { + switch component.Type { + case cyclonedx.ComponentTypeOS: + case cyclonedx.ComponentTypeContainer: + case cyclonedx.ComponentTypeApplication, cyclonedx.ComponentTypeFramework, cyclonedx.ComponentTypeLibrary: + p, err := decodeComponent(component) + if err != nil { + return err + } + 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 _, c := range *component.Components { + collectPackages(&c, s, idMap) + } + } + + return nil +} + +func linuxReleaseFromComponents(components []cyclonedx.Component) *linux.Release { + for _, component := range components { + 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 "" +} + +// used to indicate that a relationship listed under the syft artifact package can be represented as a cyclonedx dependency. +// NOTE: CycloneDX provides the ability to describe components and their dependency on other components. +// The dependency graph is capable of representing both direct and transitive relationships. +// If a relationship is either direct or transitive it can be included in this function. +// An example of a relationship to not include would be: OwnershipByFileOverlapRelationship. +func _isExpressiblePackageRelationship(ty artifact.RelationshipType) bool { + switch ty { + case artifact.RuntimeDependencyOfRelationship: + return true + case artifact.DevDependencyOfRelationship: + return true + case artifact.BuildDependencyOfRelationship: + return true + case artifact.DependencyOfRelationship: + return true + } + return false +} + +func collectRelationships(bom *cyclonedx.BOM, s *sbom.SBOM, idMap map[string]interface{}) error { + if bom.Dependencies == nil { + return nil + } + 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 + }) + } + } + } + } + return nil +} + +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{} +} From 97e0c589d6e8f3b78ec7ffe38fd24755caec5268 Mon Sep 17 00:00:00 2001 From: Keith Zantow Date: Thu, 10 Feb 2022 13:12:03 -0500 Subject: [PATCH 03/14] oops Signed-off-by: Keith Zantow --- internal/formats/common/cyclonedxhelpers/component.go | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/internal/formats/common/cyclonedxhelpers/component.go b/internal/formats/common/cyclonedxhelpers/component.go index f82cdf73faa..4f695ea6696 100644 --- a/internal/formats/common/cyclonedxhelpers/component.go +++ b/internal/formats/common/cyclonedxhelpers/component.go @@ -34,10 +34,8 @@ func decodeComponent(c *cyclonedx.Component) (*pkg.Package, error) { typ := pkg.Type(prop(c, "type")) purl := c.PackageURL - if typ == "" { - if purl != "" { - typ = pkg.TypeFromPURL(purl) - } + if typ == "" && purl != "" { + typ = pkg.TypeFromPURL(purl) } p := &pkg.Package{ From ddb09e671c86a8adeab109ae07c0657683c99bd2 Mon Sep 17 00:00:00 2001 From: Keith Zantow Date: Thu, 10 Feb 2022 14:34:18 -0500 Subject: [PATCH 04/14] lint Signed-off-by: Keith Zantow --- .../common/cyclonedxhelpers/component.go | 39 +++++++++---------- .../common/cyclonedxhelpers/decoder.go | 34 +++++----------- .../common/cyclonedxhelpers/properties.go | 1 - 3 files changed, 27 insertions(+), 47 deletions(-) diff --git a/internal/formats/common/cyclonedxhelpers/component.go b/internal/formats/common/cyclonedxhelpers/component.go index 4f695ea6696..86c95823c38 100644 --- a/internal/formats/common/cyclonedxhelpers/component.go +++ b/internal/formats/common/cyclonedxhelpers/component.go @@ -38,29 +38,26 @@ func decodeComponent(c *cyclonedx.Component) (*pkg.Package, error) { typ = pkg.TypeFromPURL(purl) } - p := &pkg.Package{ - Name: c.Name, - Version: c.Version, - FoundBy: prop(c, "foundBy"), - Locations: nil, - Licenses: decodeLicenses(c), - Language: pkg.Language(prop(c, "language")), - Type: pkg.Type(typ), - CPEs: cp, - PURL: purl, - } + metaType, meta := decodePackageMetadata(c) - metaType, meta, err := decodePackageMetadata(c) - - if err == nil { - p.MetadataType = metaType - p.Metadata = meta + p := &pkg.Package{ + Name: c.Name, + Version: c.Version, + FoundBy: prop(c, "foundBy"), + Locations: nil, + Licenses: decodeLicenses(c), + Language: pkg.Language(prop(c, "language")), + Type: typ, + CPEs: cp, + PURL: purl, + MetadataType: metaType, + Metadata: meta, } return p, nil } -func decodePackageMetadata(c *cyclonedx.Component) (pkg.MetadataType, interface{}, error) { +func decodePackageMetadata(c *cyclonedx.Component) (pkg.MetadataType, interface{}) { if c.Properties != nil { typ := prop(c, "metadataType") if typ != "" { @@ -81,7 +78,7 @@ func decodePackageMetadata(c *cyclonedx.Component) (pkg.MetadataType, interface{ PullChecksum: prop(c, "pullChecksum"), GitCommitOfAport: prop(c, "gitCommitOfAport"), Files: []pkg.ApkFileRecord{}, - }, nil + } case "DpkgMetadata": return pkg.DpkgMetadataType, pkg.DpkgMetadata{ Package: prop(c, "package"), @@ -92,7 +89,7 @@ func decodePackageMetadata(c *cyclonedx.Component) (pkg.MetadataType, interface{ Maintainer: prop(c, "maintainer"), InstalledSize: propInt(c, "installedSize"), Files: []pkg.DpkgFileRecord{}, - }, nil + } case "RpmdbMetadata": return pkg.RpmdbMetadataType, pkg.RpmdbMetadata{ Name: prop(c, "name"), @@ -105,10 +102,10 @@ func decodePackageMetadata(c *cyclonedx.Component) (pkg.MetadataType, interface{ License: prop(c, "license"), Vendor: prop(c, "vendor"), Files: []pkg.RpmdbFileRecord{}, - }, nil + } } } } - return pkg.UnknownMetadataType, nil, nil + return pkg.UnknownMetadataType, nil } diff --git a/internal/formats/common/cyclonedxhelpers/decoder.go b/internal/formats/common/cyclonedxhelpers/decoder.go index 98b129d99a1..23d44a11b86 100644 --- a/internal/formats/common/cyclonedxhelpers/decoder.go +++ b/internal/formats/common/cyclonedxhelpers/decoder.go @@ -22,7 +22,7 @@ func GetValidator(format cyclonedx.BOMFileFormat) format.Validator { } // random JSON does not necessarily cause an error (e.g. SPDX) if (cyclonedx.BOM{} == *bom) { - return fmt.Errorf("Not a valid CycloneDX document") + return fmt.Errorf("not a valid CycloneDX document") } return nil } @@ -68,7 +68,7 @@ func toSyftModel(bom *cyclonedx.BOM) (*sbom.SBOM, error) { 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") + return fmt.Errorf("no components are defined in the CycloneDX BOM") } for _, component := range *bom.Components { if err := collectPackages(&component, s, idMap); err != nil { @@ -94,8 +94,10 @@ func collectPackages(component *cyclonedx.Component, s *sbom.SBOM, idMap map[str } if component.Components != nil { - for _, c := range *component.Components { - collectPackages(&c, s, idMap) + for i := range *component.Components { + if err := collectPackages(&(*component.Components)[i], s, idMap); err != nil { + return err + } } } @@ -103,9 +105,10 @@ func collectPackages(component *cyclonedx.Component, s *sbom.SBOM, idMap map[str } func linuxReleaseFromComponents(components []cyclonedx.Component) *linux.Release { - for _, component := range components { + for i := range components { + component := &components[i] if component.Type == cyclonedx.ComponentTypeOS { - return linuxReleaseFromOSComponent(&component) + return linuxReleaseFromOSComponent(component) } } return nil @@ -176,25 +179,6 @@ func getPropertyValue(component *cyclonedx.Component, name string) string { return "" } -// used to indicate that a relationship listed under the syft artifact package can be represented as a cyclonedx dependency. -// NOTE: CycloneDX provides the ability to describe components and their dependency on other components. -// The dependency graph is capable of representing both direct and transitive relationships. -// If a relationship is either direct or transitive it can be included in this function. -// An example of a relationship to not include would be: OwnershipByFileOverlapRelationship. -func _isExpressiblePackageRelationship(ty artifact.RelationshipType) bool { - switch ty { - case artifact.RuntimeDependencyOfRelationship: - return true - case artifact.DevDependencyOfRelationship: - return true - case artifact.BuildDependencyOfRelationship: - return true - case artifact.DependencyOfRelationship: - return true - } - return false -} - func collectRelationships(bom *cyclonedx.BOM, s *sbom.SBOM, idMap map[string]interface{}) error { if bom.Dependencies == nil { return nil diff --git a/internal/formats/common/cyclonedxhelpers/properties.go b/internal/formats/common/cyclonedxhelpers/properties.go index 9d5c958ba81..0d36eafcfc2 100644 --- a/internal/formats/common/cyclonedxhelpers/properties.go +++ b/internal/formats/common/cyclonedxhelpers/properties.go @@ -93,7 +93,6 @@ func propInt(c *cyclonedx.Component, name string) int { return 0 } return v - } func propIntNil(c *cyclonedx.Component, name string) *int { From 3d5a8bae7e6704bc0e11da97d6a116d48583c0e4 Mon Sep 17 00:00:00 2001 From: Keith Zantow Date: Thu, 10 Feb 2022 16:16:39 -0500 Subject: [PATCH 05/14] lint Signed-off-by: Keith Zantow --- internal/formats/common/cyclonedxhelpers/decoder.go | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/internal/formats/common/cyclonedxhelpers/decoder.go b/internal/formats/common/cyclonedxhelpers/decoder.go index 23d44a11b86..27e84fd090a 100644 --- a/internal/formats/common/cyclonedxhelpers/decoder.go +++ b/internal/formats/common/cyclonedxhelpers/decoder.go @@ -59,9 +59,7 @@ func toSyftModel(bom *cyclonedx.BOM) (*sbom.SBOM, error) { return nil, err } - if err := collectRelationships(bom, s, idMap); err != nil { - return nil, err - } + collectRelationships(bom, s, idMap) return s, nil } @@ -70,8 +68,8 @@ func collectBomPackages(bom *cyclonedx.BOM, s *sbom.SBOM, idMap map[string]inter if bom.Components == nil { return fmt.Errorf("no components are defined in the CycloneDX BOM") } - for _, component := range *bom.Components { - if err := collectPackages(&component, s, idMap); err != nil { + for i := range *bom.Components { + if err := collectPackages(&(*bom.Components)[i], s, idMap); err != nil { return err } } @@ -179,9 +177,9 @@ func getPropertyValue(component *cyclonedx.Component, name string) string { return "" } -func collectRelationships(bom *cyclonedx.BOM, s *sbom.SBOM, idMap map[string]interface{}) error { +func collectRelationships(bom *cyclonedx.BOM, s *sbom.SBOM, idMap map[string]interface{}) { if bom.Dependencies == nil { - return nil + return } for _, d := range *bom.Dependencies { from, fromOk := idMap[d.Ref].(artifact.Identifiable) @@ -201,7 +199,6 @@ func collectRelationships(bom *cyclonedx.BOM, s *sbom.SBOM, idMap map[string]int } } } - return nil } func decodeMetadata(component *cyclonedx.Component) source.Metadata { From 84517cf3baf8888f289ab196c28cad528a0f2ec8 Mon Sep 17 00:00:00 2001 From: Keith Zantow Date: Thu, 10 Feb 2022 17:24:08 -0500 Subject: [PATCH 06/14] update cyclonedx golden files Signed-off-by: Keith Zantow --- .../TestCycloneDxDirectoryEncoder.golden | 11 +++++++++-- .../snapshot/TestCycloneDxImageEncoder.golden | 15 +++++++++++---- .../stereoscope-fixture-image-simple.golden | Bin 15360 -> 15360 bytes .../TestCycloneDxDirectoryEncoder.golden | 8 +++++--- .../snapshot/TestCycloneDxImageEncoder.golden | 12 +++++++----- .../stereoscope-fixture-image-simple.golden | Bin 15360 -> 15360 bytes 6 files changed, 32 insertions(+), 14 deletions(-) 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/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 From 3dfe6ce0f4556cad0f1356029e46d08ee42747d1 Mon Sep 17 00:00:00 2001 From: Keith Zantow Date: Thu, 10 Feb 2022 20:43:26 -0500 Subject: [PATCH 07/14] add basic testing Signed-off-by: Keith Zantow --- .../formats/common/cyclonedxhelpers/cpe.go | 25 +- .../common/cyclonedxhelpers/decoder.go | 6 +- .../common/cyclonedxhelpers/decoder_test.go | 258 ++++++++++++++++++ .../formats/cyclonedx13json/decoder_test.go | 70 +++++ .../formats/cyclonedx13xml/decoder_test.go | 70 +++++ 5 files changed, 413 insertions(+), 16 deletions(-) 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 diff --git a/internal/formats/common/cyclonedxhelpers/cpe.go b/internal/formats/common/cyclonedxhelpers/cpe.go index f4048618869..2124d454ffe 100644 --- a/internal/formats/common/cyclonedxhelpers/cpe.go +++ b/internal/formats/common/cyclonedxhelpers/cpe.go @@ -17,21 +17,16 @@ func encodeCPE(p pkg.Package) string { func decodeCPEs(c *cyclonedx.Component) ([]pkg.CPE, error) { // FIXME -- why are we not encoding all the CPEs and what is the right behavior to decode them? + cpes := cpe.Generate(pkg.Package{ + Name: c.Name, + Version: c.Version, + PURL: c.PackageURL, + }) + cp, err := pkg.NewCPE(c.CPE) - if err != nil { - return nil, err + if err == nil { + cpes = append(cpes, cp) } - return cpe.Generate(pkg.Package{ - Name: c.Name, - Version: c.Version, - FoundBy: "", - Locations: nil, - Licenses: nil, - Language: "", - Type: "", - CPEs: []pkg.CPE{cp}, - PURL: c.PackageURL, - MetadataType: "", - Metadata: nil, - }), nil + + return cpes, nil } diff --git a/internal/formats/common/cyclonedxhelpers/decoder.go b/internal/formats/common/cyclonedxhelpers/decoder.go index 27e84fd090a..9e4efcb0a5e 100644 --- a/internal/formats/common/cyclonedxhelpers/decoder.go +++ b/internal/formats/common/cyclonedxhelpers/decoder.go @@ -44,12 +44,16 @@ func GetDecoder(format cyclonedx.BOMFileFormat) format.Decoder { } 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: decodeMetadata(bom.Metadata.Component), + Source: meta, //Descriptor: sbom.Descriptor{}, } diff --git a/internal/formats/common/cyclonedxhelpers/decoder_test.go b/internal/formats/common/cyclonedxhelpers/decoder_test.go new file mode 100644 index 00000000000..b67fb3fcd5c --- /dev/null +++ b/internal/formats/common/cyclonedxhelpers/decoder_test.go @@ -0,0 +1,258 @@ +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: "a-purl-2", + 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: "a-purl-2", + 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 { + 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/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/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)) + } + }) + } +} From 24ecf6dab5e26186e1d294c569ad9efe725e3a3d Mon Sep 17 00:00:00 2001 From: Keith Zantow Date: Thu, 10 Feb 2022 20:52:07 -0500 Subject: [PATCH 08/14] add basic testing Signed-off-by: Keith Zantow --- .../formats/common/cyclonedxhelpers/component.go | 7 +------ internal/formats/common/cyclonedxhelpers/cpe.go | 15 ++++++++++----- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/internal/formats/common/cyclonedxhelpers/component.go b/internal/formats/common/cyclonedxhelpers/component.go index 86c95823c38..afef21fe5a6 100644 --- a/internal/formats/common/cyclonedxhelpers/component.go +++ b/internal/formats/common/cyclonedxhelpers/component.go @@ -27,11 +27,6 @@ func hasMetadata(p pkg.Package) bool { } func decodeComponent(c *cyclonedx.Component) (*pkg.Package, error) { - cp, err := decodeCPEs(c) - if err != nil { - return nil, err - } - typ := pkg.Type(prop(c, "type")) purl := c.PackageURL if typ == "" && purl != "" { @@ -48,7 +43,7 @@ func decodeComponent(c *cyclonedx.Component) (*pkg.Package, error) { Licenses: decodeLicenses(c), Language: pkg.Language(prop(c, "language")), Type: typ, - CPEs: cp, + CPEs: decodeCPEs(c), PURL: purl, MetadataType: metaType, Metadata: meta, diff --git a/internal/formats/common/cyclonedxhelpers/cpe.go b/internal/formats/common/cyclonedxhelpers/cpe.go index 2124d454ffe..6d53c5a28bf 100644 --- a/internal/formats/common/cyclonedxhelpers/cpe.go +++ b/internal/formats/common/cyclonedxhelpers/cpe.go @@ -2,6 +2,7 @@ package cyclonedxhelpers import ( "github.com/CycloneDX/cyclonedx-go" + "github.com/anchore/syft/internal/log" "github.com/anchore/syft/syft/pkg" "github.com/anchore/syft/syft/pkg/cataloger/common/cpe" ) @@ -15,7 +16,7 @@ func encodeCPE(p pkg.Package) string { return "" } -func decodeCPEs(c *cyclonedx.Component) ([]pkg.CPE, error) { +func decodeCPEs(c *cyclonedx.Component) []pkg.CPE { // FIXME -- why are we not encoding all the CPEs and what is the right behavior to decode them? cpes := cpe.Generate(pkg.Package{ Name: c.Name, @@ -23,10 +24,14 @@ func decodeCPEs(c *cyclonedx.Component) ([]pkg.CPE, error) { PURL: c.PackageURL, }) - cp, err := pkg.NewCPE(c.CPE) - if err == nil { - cpes = append(cpes, cp) + if c.CPE != "" { + cp, err := pkg.NewCPE(c.CPE) + if err != nil { + log.Warnf("invalid CPE: %s", c.CPE) + } else { + cpes = append(cpes, cp) + } } - return cpes, nil + return cpes } From b7f76c12f683122ca87dba14ea372b6309c75e5a Mon Sep 17 00:00:00 2001 From: Keith Zantow Date: Thu, 10 Feb 2022 20:57:16 -0500 Subject: [PATCH 09/14] lint Signed-off-by: Keith Zantow --- .../common/cyclonedxhelpers/component.go | 4 ++-- .../formats/common/cyclonedxhelpers/decoder.go | 17 ++++------------- 2 files changed, 6 insertions(+), 15 deletions(-) diff --git a/internal/formats/common/cyclonedxhelpers/component.go b/internal/formats/common/cyclonedxhelpers/component.go index afef21fe5a6..00afa89d0c3 100644 --- a/internal/formats/common/cyclonedxhelpers/component.go +++ b/internal/formats/common/cyclonedxhelpers/component.go @@ -26,7 +26,7 @@ func hasMetadata(p pkg.Package) bool { return p.Metadata != nil } -func decodeComponent(c *cyclonedx.Component) (*pkg.Package, error) { +func decodeComponent(c *cyclonedx.Component) *pkg.Package { typ := pkg.Type(prop(c, "type")) purl := c.PackageURL if typ == "" && purl != "" { @@ -49,7 +49,7 @@ func decodeComponent(c *cyclonedx.Component) (*pkg.Package, error) { Metadata: meta, } - return p, nil + return p } func decodePackageMetadata(c *cyclonedx.Component) (pkg.MetadataType, interface{}) { diff --git a/internal/formats/common/cyclonedxhelpers/decoder.go b/internal/formats/common/cyclonedxhelpers/decoder.go index 9e4efcb0a5e..b45187acaaf 100644 --- a/internal/formats/common/cyclonedxhelpers/decoder.go +++ b/internal/formats/common/cyclonedxhelpers/decoder.go @@ -73,22 +73,17 @@ func collectBomPackages(bom *cyclonedx.BOM, s *sbom.SBOM, idMap map[string]inter return fmt.Errorf("no components are defined in the CycloneDX BOM") } for i := range *bom.Components { - if err := collectPackages(&(*bom.Components)[i], s, idMap); err != nil { - return err - } + collectPackages(&(*bom.Components)[i], s, idMap) } return nil } -func collectPackages(component *cyclonedx.Component, s *sbom.SBOM, idMap map[string]interface{}) error { +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, err := decodeComponent(component) - if err != nil { - return err - } + p := decodeComponent(component) idMap[component.BOMRef] = p // TODO there must be a better way than needing to call this manually: p.SetID() @@ -97,13 +92,9 @@ func collectPackages(component *cyclonedx.Component, s *sbom.SBOM, idMap map[str if component.Components != nil { for i := range *component.Components { - if err := collectPackages(&(*component.Components)[i], s, idMap); err != nil { - return err - } + collectPackages(&(*component.Components)[i], s, idMap) } } - - return nil } func linuxReleaseFromComponents(components []cyclonedx.Component) *linux.Release { From ccebb67b2b1b947e925f397c276105d4178e86a3 Mon Sep 17 00:00:00 2001 From: Keith Zantow Date: Thu, 10 Feb 2022 21:09:01 -0500 Subject: [PATCH 10/14] search/replace typo Signed-off-by: Keith Zantow --- internal/formats/common/cyclonedxhelpers/cpe.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/formats/common/cyclonedxhelpers/cpe.go b/internal/formats/common/cyclonedxhelpers/cpe.go index 6d53c5a28bf..dd549fbb401 100644 --- a/internal/formats/common/cyclonedxhelpers/cpe.go +++ b/internal/formats/common/cyclonedxhelpers/cpe.go @@ -9,7 +9,7 @@ import ( func encodeCPE(p pkg.Package) string { // Since the CPEs in a package are sorted by specificity - // we can extract the first encodeCPE as the one to output in cyclonedx + // we can extract the first CPE as the one to output in cyclonedx if len(p.CPEs) > 0 { return pkg.CPEString(p.CPEs[0]) } From 52828fe554c3d0b739515b5577b58d746c4e3072 Mon Sep 17 00:00:00 2001 From: Keith Zantow Date: Fri, 11 Feb 2022 23:29:13 -0500 Subject: [PATCH 11/14] add more cyclonedx testing including encode_decode cycle Signed-off-by: Keith Zantow --- .../formats/common/cyclonedxhelpers/author.go | 15 ++ .../common/cyclonedxhelpers/component.go | 160 +++++++++++++----- .../formats/common/cyclonedxhelpers/cpe.go | 13 +- .../common/cyclonedxhelpers/decoder_test.go | 4 +- .../common/cyclonedxhelpers/description.go | 9 + .../cyclonedxhelpers/external_references.go | 49 ++++++ .../formats/common/cyclonedxhelpers/group.go | 9 + .../common/cyclonedxhelpers/publisher.go | 11 ++ syft/pkg/cataloger/rust/cargo_metadata.go | 10 +- syft/pkg/metadata.go | 19 +++ test/integration/encode_decode_cycle_test.go | 25 ++- 11 files changed, 265 insertions(+), 59 deletions(-) diff --git a/internal/formats/common/cyclonedxhelpers/author.go b/internal/formats/common/cyclonedxhelpers/author.go index 1a6e477b10f..31ebdde8e27 100644 --- a/internal/formats/common/cyclonedxhelpers/author.go +++ b/internal/formats/common/cyclonedxhelpers/author.go @@ -30,3 +30,18 @@ func encodeAuthor(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/component.go b/internal/formats/common/cyclonedxhelpers/component.go index 00afa89d0c3..ec0c5271bff 100644 --- a/internal/formats/common/cyclonedxhelpers/component.go +++ b/internal/formats/common/cyclonedxhelpers/component.go @@ -1,8 +1,16 @@ package cyclonedxhelpers import ( + "encoding/json" + "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 encodeComponent(p pkg.Package) cyclonedx.Component { @@ -39,7 +47,7 @@ func decodeComponent(c *cyclonedx.Component) *pkg.Package { Name: c.Name, Version: c.Version, FoundBy: prop(c, "foundBy"), - Locations: nil, + Locations: decodeLocations(c), Licenses: decodeLicenses(c), Language: pkg.Language(prop(c, "language")), Type: typ, @@ -49,56 +57,118 @@ func decodeComponent(c *cyclonedx.Component) *pkg.Package { Metadata: meta, } + allProps := serializeProps(c) + log.Info(allProps) + 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 serializeProps(c *cyclonedx.Component) string { + data, _ := json.Marshal(c.Properties) + return string(data) +} + +func mapAllProps(c *cyclonedx.Component, obj reflect.Value) { + allProps := serializeProps(c) + log.Info(allProps) + + 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() + } + + incoming := prop(c, name) + switch fieldType.Kind() { + case reflect.String: + if fieldValue.CanSet() { + fieldValue.SetString(incoming) + } 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(incoming); err == nil { + fieldValue.SetBool(b) + } + case reflect.Int: + if i, err := strconv.Atoi(incoming); err == nil { + fieldValue.SetInt(int64(i)) + } + case reflect.Float32, reflect.Float64: + if i, err := strconv.ParseFloat(incoming, 64); err == nil { + fieldValue.SetFloat(float64(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 := prop(c, "metadataType") + typ := pkg.MetadataType(prop(c, "metadataType")) if typ != "" { - switch typ { - case "ApkMetadata": - return pkg.ApkMetadataType, pkg.ApkMetadata{ - Package: prop(c, "package"), - OriginPackage: prop(c, "originPackage"), - Maintainer: prop(c, "maintainer"), - Version: prop(c, "version"), - License: prop(c, "license"), - Architecture: prop(c, "architecture"), - URL: prop(c, "url"), - Description: prop(c, "description"), - Size: propInt(c, "size"), - InstalledSize: propInt(c, "installedSize"), - PullDependencies: prop(c, "pullDependencies"), - PullChecksum: prop(c, "pullChecksum"), - GitCommitOfAport: prop(c, "gitCommitOfAport"), - Files: []pkg.ApkFileRecord{}, - } - case "DpkgMetadata": - return pkg.DpkgMetadataType, pkg.DpkgMetadata{ - Package: prop(c, "package"), - Source: prop(c, "source"), - Version: prop(c, "version"), - SourceVersion: prop(c, "sourceVersion"), - Architecture: prop(c, "architecture"), - Maintainer: prop(c, "maintainer"), - InstalledSize: propInt(c, "installedSize"), - Files: []pkg.DpkgFileRecord{}, - } - case "RpmdbMetadata": - return pkg.RpmdbMetadataType, pkg.RpmdbMetadata{ - Name: prop(c, "name"), - Version: prop(c, "version"), - Epoch: propIntNil(c, "epoch"), - Arch: prop(c, "arch"), - Release: prop(c, "release"), - SourceRpm: prop(c, "sourceRpm"), - Size: propInt(c, "size"), - License: prop(c, "license"), - Vendor: prop(c, "vendor"), - Files: []pkg.RpmdbFileRecord{}, - } - } + 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() } } diff --git a/internal/formats/common/cyclonedxhelpers/cpe.go b/internal/formats/common/cyclonedxhelpers/cpe.go index dd549fbb401..0c241d2a921 100644 --- a/internal/formats/common/cyclonedxhelpers/cpe.go +++ b/internal/formats/common/cyclonedxhelpers/cpe.go @@ -4,7 +4,6 @@ import ( "github.com/CycloneDX/cyclonedx-go" "github.com/anchore/syft/internal/log" "github.com/anchore/syft/syft/pkg" - "github.com/anchore/syft/syft/pkg/cataloger/common/cpe" ) func encodeCPE(p pkg.Package) string { @@ -17,21 +16,15 @@ func encodeCPE(p pkg.Package) string { } func decodeCPEs(c *cyclonedx.Component) []pkg.CPE { - // FIXME -- why are we not encoding all the CPEs and what is the right behavior to decode them? - cpes := cpe.Generate(pkg.Package{ - Name: c.Name, - Version: c.Version, - PURL: c.PackageURL, - }) - + // 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 { - cpes = append(cpes, cp) + return []pkg.CPE{cp} } } - return cpes + return []pkg.CPE{} } diff --git a/internal/formats/common/cyclonedxhelpers/decoder_test.go b/internal/formats/common/cyclonedxhelpers/decoder_test.go index b67fb3fcd5c..0591b3aa70e 100644 --- a/internal/formats/common/cyclonedxhelpers/decoder_test.go +++ b/internal/formats/common/cyclonedxhelpers/decoder_test.go @@ -42,7 +42,7 @@ func Test_decode(t *testing.T) { }, }, CPE: "cpe:2.3:*:some:package:1:*:*:*:*:*:*:*", - PackageURL: "a-purl-2", + PackageURL: "pkg:some/package-1@1.0.1?arch=arm64&upstream=upstream1&distro=alpine-1", ExternalReferences: &[]cyclonedx.ExternalReference{ { URL: "", @@ -189,7 +189,7 @@ func Test_decode(t *testing.T) { pkg: "package-1", ver: "1.0.1", cpe: "cpe:2.3:*:some:package:1:*:*:*:*:*:*:*", - purl: "a-purl-2", + purl: "pkg:some/package-1@1.0.1?arch=arm64&upstream=upstream1&distro=alpine-1", relation: "package-2", }, { diff --git a/internal/formats/common/cyclonedxhelpers/description.go b/internal/formats/common/cyclonedxhelpers/description.go index 4f7f8a64ccc..36add799af1 100644 --- a/internal/formats/common/cyclonedxhelpers/description.go +++ b/internal/formats/common/cyclonedxhelpers/description.go @@ -13,3 +13,12 @@ func encodeDescription(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/external_references.go b/internal/formats/common/cyclonedxhelpers/external_references.go index 67ba2970afb..cfdfbf9166d 100644 --- a/internal/formats/common/cyclonedxhelpers/external_references.go +++ b/internal/formats/common/cyclonedxhelpers/external_references.go @@ -2,6 +2,7 @@ package cyclonedxhelpers import ( "fmt" + "strings" "github.com/CycloneDX/cyclonedx-go" "github.com/anchore/syft/syft/pkg" @@ -63,3 +64,51 @@ func encodeExternalReferences(p pkg.Package) *[]cyclonedx.ExternalReference { } return nil } + +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 "" +} + +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: ") + } +} diff --git a/internal/formats/common/cyclonedxhelpers/group.go b/internal/formats/common/cyclonedxhelpers/group.go index 2aeba5ef40e..6e452aeeef7 100644 --- a/internal/formats/common/cyclonedxhelpers/group.go +++ b/internal/formats/common/cyclonedxhelpers/group.go @@ -10,3 +10,12 @@ func encodeGroup(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/publisher.go b/internal/formats/common/cyclonedxhelpers/publisher.go index 045053c31fd..87e51c3c5f8 100644 --- a/internal/formats/common/cyclonedxhelpers/publisher.go +++ b/internal/formats/common/cyclonedxhelpers/publisher.go @@ -17,3 +17,14 @@ func encodePublisher(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/syft/pkg/cataloger/rust/cargo_metadata.go b/syft/pkg/cataloger/rust/cargo_metadata.go index 7b1563d83aa..451a9b387de 100644 --- a/syft/pkg/cataloger/rust/cargo_metadata.go +++ b/syft/pkg/cataloger/rust/cargo_metadata.go @@ -1,11 +1,19 @@ package rust -import "github.com/anchore/syft/syft/pkg" +import ( + "reflect" + + "github.com/anchore/syft/syft/pkg" +) type CargoMetadata struct { Packages []pkg.CargoPackageMetadata `toml:"package"` } +func init() { + pkg.MetadataTypeByName[pkg.RustCargoPackageMetadataType] = reflect.TypeOf(CargoMetadata{}) +} + // Pkgs returns all of the packages referenced within the Cargo.lock metadata. func (m CargoMetadata) Pkgs() []*pkg.Package { pkgs := make([]*pkg.Package, 0) diff --git a/syft/pkg/metadata.go b/syft/pkg/metadata.go index a7021b4e3cb..d4720aab624 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,18 @@ 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{}), + // This would cause a cycle; it's currently being done in rust/cargo_metadata.go:init() + // RustCargoPackageMetadataType: reflect.TypeOf(rust.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) From c1e7597a183bab7fc852e3c5dc98c83872d13cf0 Mon Sep 17 00:00:00 2001 From: Keith Zantow Date: Sat, 12 Feb 2022 00:15:53 -0500 Subject: [PATCH 12/14] lint Signed-off-by: Keith Zantow --- .../common/cyclonedxhelpers/component.go | 32 ++++--------- .../cyclonedxhelpers/external_references.go | 48 +++++++++---------- .../common/cyclonedxhelpers/properties.go | 19 +------- 3 files changed, 35 insertions(+), 64 deletions(-) diff --git a/internal/formats/common/cyclonedxhelpers/component.go b/internal/formats/common/cyclonedxhelpers/component.go index ec0c5271bff..ef42ab600e5 100644 --- a/internal/formats/common/cyclonedxhelpers/component.go +++ b/internal/formats/common/cyclonedxhelpers/component.go @@ -1,7 +1,6 @@ package cyclonedxhelpers import ( - "encoding/json" "fmt" "reflect" "strconv" @@ -35,7 +34,7 @@ func hasMetadata(p pkg.Package) bool { } func decodeComponent(c *cyclonedx.Component) *pkg.Package { - typ := pkg.Type(prop(c, "type")) + typ := pkg.Type(findPropertyValue(c, "type")) purl := c.PackageURL if typ == "" && purl != "" { typ = pkg.TypeFromPURL(purl) @@ -46,10 +45,10 @@ func decodeComponent(c *cyclonedx.Component) *pkg.Package { p := &pkg.Package{ Name: c.Name, Version: c.Version, - FoundBy: prop(c, "foundBy"), + FoundBy: findPropertyValue(c, "foundBy"), Locations: decodeLocations(c), Licenses: decodeLicenses(c), - Language: pkg.Language(prop(c, "language")), + Language: pkg.Language(findPropertyValue(c, "language")), Type: typ, CPEs: decodeCPEs(c), PURL: purl, @@ -57,9 +56,6 @@ func decodeComponent(c *cyclonedx.Component) *pkg.Package { Metadata: meta, } - allProps := serializeProps(c) - log.Info(allProps) - return p } @@ -81,15 +77,7 @@ func decodeLocations(c *cyclonedx.Component) (out []source.Location) { return } -func serializeProps(c *cyclonedx.Component) string { - data, _ := json.Marshal(c.Properties) - return string(data) -} - func mapAllProps(c *cyclonedx.Component, obj reflect.Value) { - allProps := serializeProps(c) - log.Info(allProps) - value := obj if value.Kind() == reflect.Ptr { value = value.Elem() @@ -118,26 +106,26 @@ func mapAllProps(c *cyclonedx.Component, obj reflect.Value) { fieldValue = fieldValue.Elem() } - incoming := prop(c, name) + propertyValue := findPropertyValue(c, name) switch fieldType.Kind() { case reflect.String: if fieldValue.CanSet() { - fieldValue.SetString(incoming) + 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(incoming); err == nil { + if b, err := strconv.ParseBool(propertyValue); err == nil { fieldValue.SetBool(b) } case reflect.Int: - if i, err := strconv.Atoi(incoming); err == nil { + if i, err := strconv.Atoi(propertyValue); err == nil { fieldValue.SetInt(int64(i)) } case reflect.Float32, reflect.Float64: - if i, err := strconv.ParseFloat(incoming, 64); err == nil { - fieldValue.SetFloat(float64(i)) + if i, err := strconv.ParseFloat(propertyValue, 64); err == nil { + fieldValue.SetFloat(i) } case reflect.Struct: mapAllProps(c, fieldValue) @@ -152,7 +140,7 @@ func mapAllProps(c *cyclonedx.Component, obj reflect.Value) { func decodePackageMetadata(c *cyclonedx.Component) (pkg.MetadataType, interface{}) { if c.Properties != nil { - typ := pkg.MetadataType(prop(c, "metadataType")) + typ := pkg.MetadataType(findPropertyValue(c, "metadataType")) if typ != "" { meta := reflect.New(pkg.MetadataTypeByName[typ]) metaPtr := meta.Interface() diff --git a/internal/formats/common/cyclonedxhelpers/external_references.go b/internal/formats/common/cyclonedxhelpers/external_references.go index cfdfbf9166d..5cc75302166 100644 --- a/internal/formats/common/cyclonedxhelpers/external_references.go +++ b/internal/formats/common/cyclonedxhelpers/external_references.go @@ -65,6 +65,29 @@ func encodeExternalReferences(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 { @@ -76,7 +99,7 @@ func findExternalRef(c *cyclonedx.Component, typ cyclonedx.ExternalReferenceType return nil } -func refUrl(c *cyclonedx.Component, typ cyclonedx.ExternalReferenceType) string { +func refURL(c *cyclonedx.Component, typ cyclonedx.ExternalReferenceType) string { if r := findExternalRef(c, typ); r != nil { return r.URL } @@ -89,26 +112,3 @@ func refComment(c *cyclonedx.Component, typ cyclonedx.ExternalReferenceType) str } return "" } - -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: ") - } -} diff --git a/internal/formats/common/cyclonedxhelpers/properties.go b/internal/formats/common/cyclonedxhelpers/properties.go index 0d36eafcfc2..090f0a37db4 100644 --- a/internal/formats/common/cyclonedxhelpers/properties.go +++ b/internal/formats/common/cyclonedxhelpers/properties.go @@ -3,7 +3,6 @@ package cyclonedxhelpers import ( "fmt" "reflect" - "strconv" "github.com/CycloneDX/cyclonedx-go" "github.com/anchore/syft/syft/pkg" @@ -78,7 +77,7 @@ func getCycloneDXPropertyValue(field reflect.Value) interface{} { return "" } -func prop(c *cyclonedx.Component, name string) string { +func findPropertyValue(c *cyclonedx.Component, name string) string { for _, p := range *c.Properties { if p.Name == name { return p.Value @@ -86,19 +85,3 @@ func prop(c *cyclonedx.Component, name string) string { } return "" } - -func propInt(c *cyclonedx.Component, name string) int { - v, err := strconv.Atoi(prop(c, name)) - if err != nil { - return 0 - } - return v -} - -func propIntNil(c *cyclonedx.Component, name string) *int { - v, err := strconv.Atoi(prop(c, name)) - if err != nil { - return nil - } - return &v -} From eea2d222dfcc0464e3d0b6372ef8698e8315fdc9 Mon Sep 17 00:00:00 2001 From: Keith Zantow Date: Wed, 16 Feb 2022 14:07:10 -0500 Subject: [PATCH 13/14] pr feedback Signed-off-by: Keith Zantow --- .../common/cyclonedxhelpers/decoder_test.go | 58 ++++++++++--------- 1 file changed, 30 insertions(+), 28 deletions(-) diff --git a/internal/formats/common/cyclonedxhelpers/decoder_test.go b/internal/formats/common/cyclonedxhelpers/decoder_test.go index 0591b3aa70e..e18e89f0a33 100644 --- a/internal/formats/common/cyclonedxhelpers/decoder_test.go +++ b/internal/formats/common/cyclonedxhelpers/decoder_test.go @@ -213,42 +213,44 @@ func Test_decode(t *testing.T) { } if e.pkg != "" { for p := range sbom.Artifacts.PackageCatalog.Enumerate() { - if e.pkg == p.Name { - assert.Equal(t, e.ver, p.Version) + if e.pkg != p.Name { + continue + } - 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)) + 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 e.purl != "" { - assert.Equal(t, e.purl, p.PURL) + if !foundCPE { + assert.Fail(t, fmt.Sprintf("CPE not found in package: %s", e.cpe)) } + } - 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)) + 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 } } - continue test + 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)) } From ac654fb364c96e95faa3464276537fca90339a91 Mon Sep 17 00:00:00 2001 From: Keith Zantow Date: Wed, 16 Feb 2022 14:13:43 -0500 Subject: [PATCH 14/14] moved cargo_metadata to syft/pkg Signed-off-by: Keith Zantow --- syft/pkg/cargo_metadata.go | 19 ++++++++++++++ syft/pkg/cataloger/rust/cargo_metadata.go | 29 --------------------- syft/pkg/cataloger/rust/parse_cargo_lock.go | 2 +- syft/pkg/metadata.go | 23 ++++++++-------- 4 files changed, 31 insertions(+), 42 deletions(-) create mode 100644 syft/pkg/cargo_metadata.go delete mode 100644 syft/pkg/cataloger/rust/cargo_metadata.go diff --git a/syft/pkg/cargo_metadata.go b/syft/pkg/cargo_metadata.go new file mode 100644 index 00000000000..f5c4a6694ed --- /dev/null +++ b/syft/pkg/cargo_metadata.go @@ -0,0 +1,19 @@ +package pkg + +type CargoMetadata struct { + Packages []CargoPackageMetadata `toml:"package"` +} + +// Pkgs returns all of the packages referenced within the Cargo.lock metadata. +func (m CargoMetadata) Pkgs() []*Package { + pkgs := make([]*Package, 0) + + for _, p := range m.Packages { + if p.Dependencies == nil { + p.Dependencies = make([]string, 0) + } + pkgs = append(pkgs, p.Pkg()) + } + + return pkgs +} diff --git a/syft/pkg/cataloger/rust/cargo_metadata.go b/syft/pkg/cataloger/rust/cargo_metadata.go deleted file mode 100644 index 451a9b387de..00000000000 --- a/syft/pkg/cataloger/rust/cargo_metadata.go +++ /dev/null @@ -1,29 +0,0 @@ -package rust - -import ( - "reflect" - - "github.com/anchore/syft/syft/pkg" -) - -type CargoMetadata struct { - Packages []pkg.CargoPackageMetadata `toml:"package"` -} - -func init() { - pkg.MetadataTypeByName[pkg.RustCargoPackageMetadataType] = reflect.TypeOf(CargoMetadata{}) -} - -// Pkgs returns all of the packages referenced within the Cargo.lock metadata. -func (m CargoMetadata) Pkgs() []*pkg.Package { - pkgs := make([]*pkg.Package, 0) - - for _, p := range m.Packages { - if p.Dependencies == nil { - p.Dependencies = make([]string, 0) - } - pkgs = append(pkgs, p.Pkg()) - } - - return pkgs -} 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 d4720aab624..10c86bdfe90 100644 --- a/syft/pkg/metadata.go +++ b/syft/pkg/metadata.go @@ -39,16 +39,15 @@ var AllMetadataTypes = []MetadataType{ } 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{}), - // This would cause a cycle; it's currently being done in rust/cargo_metadata.go:init() - // RustCargoPackageMetadataType: reflect.TypeOf(rust.CargoMetadata{}), - KbPackageMetadataType: reflect.TypeOf(KbPackageMetadata{}), - GolangBinMetadataType: reflect.TypeOf(GolangBinMetadata{}), - PhpComposerJSONMetadataType: reflect.TypeOf(PhpComposerJSONMetadata{}), + 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{}), }