diff --git a/postal/buildpack.go b/postal/buildpack.go index b634bf48..1ce68a3a 100644 --- a/postal/buildpack.go +++ b/postal/buildpack.go @@ -10,9 +10,14 @@ import ( // Dependency is a representation of a buildpack dependency. type Dependency struct { - // CPE is the Common Platform Enumerator for the dependency. + // CPE is the Common Platform Enumerator for the dependency. Used in legacy + // image label SBOM (GenerateBillOfMaterials). CPE string `toml:"cpe"` + // CPEs are the Common Platform Enumerators for the dependency. Used in Syft + // and SPDX JSON SBOMs. If unset, falls back to CPE. + CPEs []string `toml:"cpes"` + // DeprecationDate is the data upon which this dependency is considered deprecated. DeprecationDate time.Time `toml:"deprecation_date"` diff --git a/postal/service_test.go b/postal/service_test.go index 6dd56221..5907648b 100644 --- a/postal/service_test.go +++ b/postal/service_test.go @@ -885,6 +885,48 @@ version = "this is super not semver" }, })) }) + context("and there are CPEs", func() { + it("uses CPE, ignores CPEs, for backward compatibility", func() { + entries := service.GenerateBillOfMaterials( + postal.Dependency{ + CPE: "some-cpe", + CPEs: []string{"some-other-cpe"}, + ID: "some-entry", + Name: "Some Entry", + SHA256: "some-sha", + Source: "some-source", + SourceSHA256: "some-source-sha", + Stacks: []string{"some-stack"}, + URI: "some-uri", + Version: "1.2.3", + }, + ) + + Expect(entries).To(Equal([]packit.BOMEntry{ + { + Name: "Some Entry", + Metadata: paketosbom.BOMMetadata{ + CPE: "some-cpe", + Checksum: paketosbom.BOMChecksum{ + Algorithm: paketosbom.SHA256, + Hash: "some-sha", + }, + Source: paketosbom.BOMSource{ + Checksum: paketosbom.BOMChecksum{ + Algorithm: paketosbom.SHA256, + Hash: "some-source-sha", + }, + URI: "some-source", + }, + + URI: "some-uri", + Version: "1.2.3", + }, + }, + })) + }) + + }) }) context("when there is a deprecation date", func() { diff --git a/sbom/sbom.go b/sbom/sbom.go index 79b6b7d7..ed2e9549 100644 --- a/sbom/sbom.go +++ b/sbom/sbom.go @@ -78,17 +78,24 @@ func GenerateFromDependency(dependency postal.Dependency, path string) (SBOM, er if dependency.CPE == "" { dependency.CPE = UnknownCPE } + if len(dependency.CPEs) == 0 { + dependency.CPEs = []string{dependency.CPE} + } - cpe, err := pkg.NewCPE(dependency.CPE) - if err != nil { - return SBOM{}, err + var cpes []pkg.CPE + for _, cpeString := range dependency.CPEs { + cpe, err := pkg.NewCPE(cpeString) + if err != nil { + return SBOM{}, err + } + cpes = append(cpes, cpe) } catalog := pkg.NewCatalog(pkg.Package{ Name: dependency.Name, Version: dependency.Version, Licenses: dependency.Licenses, - CPEs: []pkg.CPE{cpe}, + CPEs: cpes, PURL: dependency.PURL, }) diff --git a/sbom/sbom_test.go b/sbom/sbom_test.go index 5ed76097..10eaf395 100644 --- a/sbom/sbom_test.go +++ b/sbom/sbom_test.go @@ -379,6 +379,55 @@ func testSBOM(t *testing.T, context spec.G, it spec.S) { }}), spdx.String()) }) }) + context("when the input dependency has CPEs and CPE", func() { + it("uses CPEs, not CPE", func() { + bom, err := sbom.GenerateFromDependency(postal.Dependency{ + CPE: "cpe:2.3:a:golang:go:1.16.9:*:*:*:*:*:*:*", + CPEs: []string{"cpe:2.3:a:some:other:cpe:*:*:*:*:*:*:*", "cpe:2.3:a:another:cpe:to:include:*:*:*:*:*:*"}, + ID: "go", + Licenses: []string{"BSD-3-Clause"}, + Name: "Go", + SHA256: "ca9ef23a5db944b116102b87c1ae9344b27e011dae7157d2f1e501abd39e9829", + Source: "https://dl.google.com/go/go1.16.9.src.tar.gz", + SourceSHA256: "0a1cc7fd7bd20448f71ebed64d846138850d5099b18cf5cc10a4fc45160d8c3d", + Stacks: []string{"io.buildpacks.stacks.bionic", "io.paketo.stacks.tiny"}, + URI: "https://deps.paketo.io/go/go_go1.16.9_linux_x64_bionic_ca9ef23a.tgz", + Version: "1.16.9", + }, "some-path") + Expect(err).NotTo(HaveOccurred()) + + formatter, err := bom.InFormats(sbom.SyftFormat, sbom.CycloneDXFormat, sbom.SPDXFormat) + Expect(err).NotTo(HaveOccurred()) + + formats := formatter.Formats() + + syft := bytes.NewBuffer(nil) + for _, format := range formats { + if format.Extension == "syft.json" { + _, err = io.Copy(syft, format.Content) + Expect(err).NotTo(HaveOccurred()) + } + } + + var syftDefaultOutput syftOutput + err = json.NewDecoder(syft).Decode(&syftDefaultOutput) + Expect(err).NotTo(HaveOccurred(), syft.String()) + + Expect(syftDefaultOutput.Schema.Version).To(Equal(`3.0.1`), syft.String()) + + goArtifact := syftDefaultOutput.Artifacts[0] + Expect(goArtifact.Name).To(Equal("Go"), syft.String()) + Expect(goArtifact.Version).To(Equal("1.16.9"), syft.String()) + Expect(goArtifact.Licenses).To(Equal([]string{"BSD-3-Clause"}), syft.String()) + Expect(syftDefaultOutput.Source.Type).To(Equal("directory"), syft.String()) + Expect(syftDefaultOutput.Source.Target).To(Equal("some-path"), syft.String()) + Expect(goArtifact.PURL).To(BeEmpty()) + Expect(goArtifact.CPEs).To(Equal([]string{ + "cpe:2.3:a:some:other:cpe:*:*:*:*:*:*:*", + "cpe:2.3:a:another:cpe:to:include:*:*:*:*:*:*", + })) + }) + }) context("failure cases", func() { context("when the CPE is invalid", func() {