Skip to content

Commit

Permalink
Adds support for DependencyLayerContributor and HelperLayerContributo…
Browse files Browse the repository at this point in the history
…r to generate SBOMs

- DependencyLayerContributor will write an SBOM with the dependency's metadata
- HelperLayerContributor will write an SBOM entry with the helper's metadata
- Both are written to the layer's SBOM file
- The DependencyLayerContributor support adds two new metadata fields to the BuildpackDependency object: `purl` and `cpes`.
  - purl is a string for the package URL, based on https://github.com/package-url/purl-spec
  - cpes is a list of strings for multiple CPE, or Common Platform Enumeration, identifiers
  - the values for these two fields are loaded from dependencies entries in buildpack.toml
  - if not specified, they default to the Go empty values
  - a single artifact entry is added for each dependency
- The HelpLayerContributor support generates a SBOM file with the following information. It is automatic and no additional metadata is required.
  - Name is `helper`
  - Version is the buildpack version
  - Licenses contains the buildpack's license
  - Locations contains a list of the helper names that are setup for this helper
  - CPEs is a list of `cpe:2.3:a:<buildpack-id>:<helper-name>:<buildpack-version:*:*:*:*:*:*:*`
  - PURL is `pkg:generic/<buildpack-id>@<buildpack-version>`
- ID hashes for all artifacts are calculated using `github.com/mitchellh/hashstructure/v2` and are a hash of the SyftArtifact object before setting the ID.

This functionality requires buildpack API 0.7, for older buildpack versions you may call these methods and they will function properly, however, the lifecycle will not persist any of the information generated.

Other incidental changes in this commit:

- Fixes a bug with `syft packages`. There was a typo in the command and th `-q` argument was missing.
- Moves SBOM functionality from the `sherpa` package into its own package `sbom`.
- Bumps to libcnb 1.25.0, which is required for the buildpack API 0.7 support.

Signed-off-by: Daniel Mikusa <dmikusa@vmware.com>
  • Loading branch information
Daniel Mikusa committed Nov 19, 2021
1 parent 6006d2f commit 625cfd3
Show file tree
Hide file tree
Showing 11 changed files with 432 additions and 25 deletions.
48 changes: 46 additions & 2 deletions buildpack.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import (
"github.com/heroku/color"

"github.com/paketo-buildpacks/libpak/bard"
"github.com/paketo-buildpacks/libpak/sbom"
)

// BuildpackConfiguration represents a build or launch configuration parameter.
Expand Down Expand Up @@ -80,13 +81,19 @@ type BuildpackDependency struct {
// Stacks are the stacks the dependency is compatible with.
Stacks []string `toml:"stacks"`

// Licenses are the stacks the dependency is distributed under.
// Licenses are the licenses the dependency is distributed under.
Licenses []BuildpackDependencyLicense `toml:"licenses"`

// CPEs are the Common Platform Enumeration identifiers for the dependency
CPEs []string `toml:"cpes"`

// PURL is the package URL that identifies the dependency
PURL string `toml:"purl"`
}

// AsBOMEntry renders a bill of materials entry describing the dependency.
//
// Deprecated: as of Buildpacks RFC 95, use `sherpa.SBOMScanner` instead
// Deprecated: as of Buildpacks RFC 95, use `BuildpackDependency.AsSyftArtifact` instead
func (b BuildpackDependency) AsBOMEntry() libcnb.BOMEntry {
return libcnb.BOMEntry{
Name: b.ID,
Expand All @@ -101,6 +108,33 @@ func (b BuildpackDependency) AsBOMEntry() libcnb.BOMEntry {
}
}

// AsSyftArtifact renders a bill of materials entry describing the dependency as Syft.
func (b BuildpackDependency) AsSyftArtifact() (sbom.SyftArtifact, error) {
licenses := []string{}
for _, license := range b.Licenses {
licenses = append(licenses, license.Type)
}

sbomArtifact := sbom.SyftArtifact{
Name: b.Name,
Version: b.Version,
Type: "UnknownPackage",
FoundBy: "libpak",
Licenses: licenses,
Locations: []sbom.SyftLocation{{Path: "buildpack.toml"}},
CPEs: b.CPEs,
PURL: b.PURL,
}

var err error
sbomArtifact.ID, err = sbomArtifact.Hash()
if err != nil {
return sbom.SyftArtifact{}, fmt.Errorf("unable to generate hash\n%w", err)
}

return sbomArtifact, nil
}

// BuildpackMetadata is an extension to libcnb.Buildpack's metadata with opinions.
type BuildpackMetadata struct {

Expand Down Expand Up @@ -196,6 +230,16 @@ func NewBuildpackMetadata(metadata map[string]interface{}) (BuildpackMetadata, e
}
}

if v, ok := v["cpes"].([]interface{}); ok {
for _, v := range v {
d.CPEs = append(d.CPEs, v.(string))
}
}

if v, ok := v["purl"].(string); ok {
d.PURL = v
}

m.Dependencies = append(m.Dependencies, d)
}
}
Expand Down
36 changes: 36 additions & 0 deletions buildpack_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import (
"github.com/sclevine/spec"

"github.com/paketo-buildpacks/libpak"
"github.com/paketo-buildpacks/libpak/sbom"
)

func testBuildpack(t *testing.T, context spec.G, it spec.S) {
Expand Down Expand Up @@ -62,6 +63,37 @@ func testBuildpack(t *testing.T, context spec.G, it spec.S) {
}))
})

it("renders dependency as a SyftArtifact", func() {
dependency := libpak.BuildpackDependency{
ID: "test-id",
Name: "test-name",
Version: "1.1.1",
URI: "test-uri",
SHA256: "test-sha256",
Stacks: []string{"test-stack"},
Licenses: []libpak.BuildpackDependencyLicense{
{
Type: "test-type",
URI: "test-uri",
},
},
CPEs: []string{"test-cpe1", "test-cpe2"},
PURL: "test-purl",
}

Expect(dependency.AsSyftArtifact()).To(Equal(sbom.SyftArtifact{
ID: "46713835f08d90b7",
Name: "test-name",
Version: "1.1.1",
Type: "UnknownPackage",
FoundBy: "libpak",
Licenses: []string{"test-type"},
Locations: []sbom.SyftLocation{{Path: "buildpack.toml"}},
CPEs: []string{"test-cpe1", "test-cpe2"},
PURL: "test-purl",
}))
})

context("NewBuildpackMetadata", func() {
it("deserializes metadata", func() {
actual := map[string]interface{}{
Expand All @@ -86,6 +118,8 @@ func testBuildpack(t *testing.T, context spec.G, it spec.S) {
"uri": "test-uri",
},
},
"cpes": []interface{}{"cpe:2.3:a:test-id:1.1.1"},
"purl": "pkg:generic:test-id@1.1.1",
},
},
"include-files": []interface{}{"test-include-file"},
Expand Down Expand Up @@ -114,6 +148,8 @@ func testBuildpack(t *testing.T, context spec.G, it spec.S) {
URI: "test-uri",
},
},
CPEs: []string{"cpe:2.3:a:test-id:1.1.1"},
PURL: "pkg:generic:test-id@1.1.1",
},
},
IncludeFiles: []string{"test-include-file"},
Expand Down
2 changes: 2 additions & 0 deletions dependency_cache_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,8 @@ func testDependencyCache(t *testing.T, context spec.G, it spec.S) {
URI: "test-uri",
},
},
CPEs: []string{"cpe:2.3:a:some:jre:11.0.2:*:*:*:*:*:*:*"},
PURL: "pkg:generic/some-java11@11.0.2?arch=amd64",
}

dependencyCache = libpak.DependencyCache{
Expand Down
3 changes: 2 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,11 @@ go 1.15
require (
github.com/CycloneDX/cyclonedx-go v0.4.0
github.com/Masterminds/semver/v3 v3.1.1
github.com/buildpacks/libcnb v1.24.1-0.20211118031525-6aa81e50810d
github.com/buildpacks/libcnb v1.25.0
github.com/creack/pty v1.1.17
github.com/heroku/color v0.0.6
github.com/imdario/mergo v0.3.12
github.com/mitchellh/hashstructure/v2 v2.0.2
github.com/onsi/gomega v1.17.0
github.com/pelletier/go-toml v1.9.4
github.com/sclevine/spec v1.4.0
Expand Down
6 changes: 4 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ github.com/Masterminds/semver/v3 v3.1.1 h1:hLg3sBzpNErnxhQtUy/mmLR2I9foDujNK030I
github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs=
github.com/bradleyjkemp/cupaloy/v2 v2.6.0 h1:knToPYa2xtfg42U3I6punFEjaGFKWQRXJwj0JTv4mTs=
github.com/bradleyjkemp/cupaloy/v2 v2.6.0/go.mod h1:bm7JXdkRd4BHJk9HpwqAI8BoAY1lps46Enkdqw6aRX0=
github.com/buildpacks/libcnb v1.24.1-0.20211118031525-6aa81e50810d h1:rAJsgF0p6rtUPGSTOCnCt/ofXKQM34wN+XKMXH3bNBE=
github.com/buildpacks/libcnb v1.24.1-0.20211118031525-6aa81e50810d/go.mod h1:XX0+zHW8CNLNwiiwowgydAgWWfyDt8Lj1NcuWtkkBJQ=
github.com/buildpacks/libcnb v1.25.0 h1:f0UWYUbXQ/vTX6SztGn+sP/F6cVSAbBQO4B5/R1LEP8=
github.com/buildpacks/libcnb v1.25.0/go.mod h1:XX0+zHW8CNLNwiiwowgydAgWWfyDt8Lj1NcuWtkkBJQ=
github.com/creack/pty v1.1.17 h1:QeVUsEDNrLBW4tMgZHvxy18sKtr6VI492kBhUfhDJNI=
github.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
Expand Down Expand Up @@ -41,6 +41,8 @@ github.com/mattn/go-colorable v0.1.2 h1:/bC9yWikZXAL9uJdulbSfyVNIR3n3trXl+v8+1sx
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-isatty v0.0.8 h1:HLtExJ+uU2HOZ+wI0Tt5DtUDrx8yhUqDcp7fYERX4CE=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4=
github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE=
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
Expand Down
58 changes: 58 additions & 0 deletions layer.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import (

"github.com/buildpacks/libcnb"

"github.com/paketo-buildpacks/libpak/sbom"
"github.com/paketo-buildpacks/libpak/sherpa"

"github.com/paketo-buildpacks/libpak/bard"
Expand Down Expand Up @@ -168,6 +169,18 @@ func (d *DependencyLayerContributor) Contribute(layer libcnb.Layer, f Dependency
}
defer artifact.Close()

sbomArtifact, err := d.Dependency.AsSyftArtifact()
if err != nil {
return libcnb.Layer{}, fmt.Errorf("unable to get SBOM artifact %s\n%w", d.Dependency.ID, err)
}

sbomPath := layer.SBOMPath(libcnb.SyftJSON)
dep := sbom.NewSyftDependency(layer.Path, []sbom.SyftArtifact{sbomArtifact})
d.Logger.Debugf("Writing Syft SBOM at %s: %+v", sbomPath, dep)
if err := dep.WriteTo(sbomPath); err != nil {
return libcnb.Layer{}, fmt.Errorf("unable to write SBOM\n%w", err)
}

return f(artifact)
})
}
Expand Down Expand Up @@ -261,6 +274,51 @@ func (h HelperLayerContributor) Contribute(layer libcnb.Layer) (libcnb.Layer, er
}
}

sbomArtifact, err := h.AsSyftArtifact()
if err != nil {
return libcnb.Layer{}, fmt.Errorf("unable to get SBOM artifact for helper\n%w", err)
}

sbomPath := layer.SBOMPath(libcnb.SyftJSON)
dep := sbom.NewSyftDependency(layer.Path, []sbom.SyftArtifact{sbomArtifact})
h.Logger.Debugf("Writing Syft SBOM at %s: %+v", sbomPath, dep)
if err := dep.WriteTo(sbomPath); err != nil {
return libcnb.Layer{}, fmt.Errorf("unable to write SBOM\n%w", err)
}

return layer, nil
})
}

func (h HelperLayerContributor) AsSyftArtifact() (sbom.SyftArtifact, error) {
licenses := []string{}
for _, license := range h.BuildpackInfo.Licenses {
licenses = append(licenses, license.Type)
}

locations := []sbom.SyftLocation{}
cpes := []string{}
for _, name := range h.Names {
locations = append(locations, sbom.SyftLocation{Path: name})
cpes = append(cpes, fmt.Sprintf("cpe:2.3:a:%s:%s:%s:*:*:*:*:*:*:*",
h.BuildpackInfo.ID, name, h.BuildpackInfo.Version))
}

artifact := sbom.SyftArtifact{
Name: "helper",
Version: h.BuildpackInfo.Version,
Type: "UnknownPackage",
FoundBy: "libpak",
Licenses: licenses,
Locations: locations,
CPEs: cpes,
PURL: fmt.Sprintf("pkg:generic/%s@%s", h.BuildpackInfo.ID, h.BuildpackInfo.Version),
}
var err error
artifact.ID, err = artifact.Hash()
if err != nil {
return sbom.SyftArtifact{}, fmt.Errorf("unable to generate hash\n%w", err)
}

return artifact, nil
}
61 changes: 61 additions & 0 deletions layer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,8 @@ func testLayer(t *testing.T, context spec.G, it spec.S) {
URI: "test-uri",
},
},
CPEs: []string{"cpe:2.3:a:some:jre:11.0.2:*:*:*:*:*:*:*"},
PURL: "pkg:generic/some-java11@11.0.2?arch=amd64",
}

layer.Metadata = map[string]interface{}{}
Expand Down Expand Up @@ -394,6 +396,8 @@ func testLayer(t *testing.T, context spec.G, it spec.S) {
"uri": dependency.Licenses[0].URI,
},
},
"cpes": []interface{}{"cpe:2.3:a:some:jre:11.0.2:*:*:*:*:*:*:*"},
"purl": "pkg:generic/some-java11@11.0.2?arch=amd64",
}

var called bool
Expand Down Expand Up @@ -442,6 +446,8 @@ func testLayer(t *testing.T, context spec.G, it spec.S) {
"uri": dependency.Licenses[0].URI,
},
},
"cpes": []interface{}{"cpe:2.3:a:some:jre:11.0.2:*:*:*:*:*:*:*"},
"purl": "pkg:generic/some-java11@11.0.2?arch=amd64",
}))
})

Expand All @@ -459,6 +465,8 @@ func testLayer(t *testing.T, context spec.G, it spec.S) {
"uri": dependency.Licenses[0].URI,
},
},
"cpes": []interface{}{"cpe:2.3:a:some:jre:11.0.2:*:*:*:*:*:*:*"},
"purl": "pkg:generic/some-java11@11.0.2?arch=amd64",
}
dlc.ExpectedTypes.Launch = true
dlc.ExpectedTypes.Cache = true
Expand All @@ -480,6 +488,28 @@ func testLayer(t *testing.T, context spec.G, it spec.S) {
Expect(layer.LayerTypes.Cache).To(BeTrue())
Expect(layer.LayerTypes.Build).To(BeTrue())
})

it("adds expected Syft SBOM file", func() {
server.AppendHandlers(ghttp.RespondWith(http.StatusOK, "test-fixture"))

layer, err := dlc.Contribute(layer, func(artifact *os.File) (libcnb.Layer, error) {
defer artifact.Close()
return layer, nil
})
Expect(err).NotTo(HaveOccurred())

outputFile := layer.SBOMPath(libcnb.SyftJSON)
Expect(outputFile).To(BeARegularFile())

data, err := ioutil.ReadFile(outputFile)
Expect(err).ToNot(HaveOccurred())
Expect(string(data)).To(ContainSubstring(`"Artifacts":[`))
Expect(string(data)).To(ContainSubstring(`"FoundBy":"libpak",`))
Expect(string(data)).To(ContainSubstring(`"PURL":"pkg:generic/some-java11@11.0.2?arch=amd64"`))
Expect(string(data)).To(ContainSubstring(`"Schema":{`))
Expect(string(data)).To(ContainSubstring(`"Descriptor":{`))
Expect(string(data)).To(ContainSubstring(`"Source":{`))
})
})

context("NewHelperLayer", func() {
Expand Down Expand Up @@ -618,5 +648,36 @@ func testLayer(t *testing.T, context spec.G, it spec.S) {
Expect(layer.LayerTypes.Cache).To(BeFalse())
Expect(layer.LayerTypes.Build).To(BeFalse())
})

it("adds expected Syft SBOM file", func() {
layer.Metadata = map[string]interface{}{
"id": buildpack.Info.ID,
"name": buildpack.Info.Name,
"version": buildpack.Info.Version,
"homepage": buildpack.Info.Homepage,
"clear-env": buildpack.Info.ClearEnvironment,
"description": "",
"keywords": []interface{}{},
}

_, err := hlc.Contribute(layer)
Expect(err).NotTo(HaveOccurred())

Expect(filepath.Join(layer.Exec.FilePath("test-name-1"))).NotTo(BeAnExistingFile())
Expect(filepath.Join(layer.Exec.FilePath("test-name-2"))).NotTo(BeAnExistingFile())

outputFile := layer.SBOMPath(libcnb.SyftJSON)
Expect(outputFile).To(BeARegularFile())

data, err := ioutil.ReadFile(outputFile)
Expect(err).ToNot(HaveOccurred())
Expect(string(data)).To(ContainSubstring(`"Artifacts":[`))
Expect(string(data)).To(ContainSubstring(`"FoundBy":"libpak",`))
Expect(string(data)).To(ContainSubstring(`"PURL":"pkg:generic/test-id@test-version"`))
Expect(string(data)).To(ContainSubstring(`"CPEs":["cpe:2.3:a:test-id:test-name-1:test-version:*:*:*:*:*:*:*","cpe:2.3:a:test-id:test-name-2:test-version:*:*:*:*:*:*:*"]`))
Expect(string(data)).To(ContainSubstring(`"Schema":{`))
Expect(string(data)).To(ContainSubstring(`"Descriptor":{`))
Expect(string(data)).To(ContainSubstring(`"Source":{`))
})
})
}
30 changes: 30 additions & 0 deletions sbom/init_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/*
* Copyright 2018-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package sbom_test

import (
"testing"

"github.com/sclevine/spec"
"github.com/sclevine/spec/report"
)

func TestUnit(t *testing.T) {
suite := spec.New("libpak/sbom", spec.Report(report.Terminal{}))
suite("SBOM", testSBOM)
suite.Run(t)
}
Loading

0 comments on commit 625cfd3

Please sign in to comment.