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.
- NewDependencyLayerContributor will return an empty BOMEntry if the dependency in question has an empty PURL and an empty CPEs list. This prevents old style and new style BOM entries from both being written, which causes an error under buildpacks API 0.7 and platform API 0.8 but allows for backwards compatibility.
- NewHelperLayerContributor will also return an empty BOMEntry but it is based on the API version for the buildpack that contributes the helpers. If that is 0.7, it will be empty. If it's 0.6 or less, it'll return an entry as before. This prevents old style and new style BOM entries from both being written, which causes an error under buildpacks API 0.7 and platform API 0.8 but allows for backwards compatibility.
- Old style BOM entries are deprecated and will be removed in a future version of libpak.

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 f3f1b0b
Show file tree
Hide file tree
Showing 11 changed files with 484 additions and 41 deletions.
48 changes: 46 additions & 2 deletions buildpack.go
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
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
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
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
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
96 changes: 80 additions & 16 deletions layer.go
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 @@ -139,15 +140,18 @@ func NewDependencyLayer(dependency BuildpackDependency, cache DependencyCache, t
ExpectedTypes: types,
}

entry := dependency.AsBOMEntry()
entry.Metadata["layer"] = c.LayerName()
var entry libcnb.BOMEntry
if dependency.PURL == "" && len(dependency.CPEs) == 0 {
entry = dependency.AsBOMEntry()
entry.Metadata["layer"] = c.LayerName()

if types.Launch {
entry.Launch = true
}
if !(types.Launch && !types.Cache && !types.Build) {
// launch-only layers are the only layers NOT guaranteed to be present in the build environment
entry.Build = true
if types.Launch {
entry.Launch = true
}
if !(types.Launch && !types.Cache && !types.Build) {
// launch-only layers are the only layers NOT guaranteed to be present in the build environment
entry.Build = true
}
}

return c, entry
Expand All @@ -168,6 +172,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 @@ -210,14 +226,17 @@ func NewHelperLayer(buildpack libcnb.Buildpack, names ...string) (HelperLayerCon
BuildpackInfo: buildpack.Info,
}

entry := libcnb.BOMEntry{
Name: "helper",
Metadata: map[string]interface{}{
"layer": c.Name(),
"names": names,
"version": buildpack.Info.Version,
},
Launch: true,
var entry libcnb.BOMEntry
if buildpack.API == "0.6" || buildpack.API == "0.5" || buildpack.API == "0.4" || buildpack.API == "0.3" || buildpack.API == "0.2" || buildpack.API == "0.1" {
entry = libcnb.BOMEntry{
Name: "helper",
Metadata: map[string]interface{}{
"layer": c.Name(),
"names": names,
"version": buildpack.Info.Version,
},
Launch: true,
}
}

return c, entry
Expand Down Expand Up @@ -261,6 +280,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
}

0 comments on commit f3f1b0b

Please sign in to comment.