Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

SBOM via packit v2 #296

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 30 additions & 3 deletions build.go
Original file line number Diff line number Diff line change
@@ -1,17 +1,26 @@
package gomodvendor

import (
"github.com/paketo-buildpacks/packit"
"github.com/paketo-buildpacks/packit/scribe"
"time"

"github.com/paketo-buildpacks/packit/v2"
"github.com/paketo-buildpacks/packit/v2/chronos"
"github.com/paketo-buildpacks/packit/v2/sbom"
"github.com/paketo-buildpacks/packit/v2/scribe"
)

//go:generate faux --interface SBOMGenerator --output fakes/sbom_generator.go
type SBOMGenerator interface {
Generate(dir string) (sbom.SBOM, error)
}

//go:generate faux --interface BuildProcess --output fakes/build_process.go
type BuildProcess interface {
ShouldRun(workingDir string) (ok bool, reason string, err error)
Execute(path, workingDir string) error
}

func Build(buildProcess BuildProcess, logs scribe.Emitter) packit.BuildFunc {
func Build(buildProcess BuildProcess, logs scribe.Emitter, clock chronos.Clock, sbomGenerator SBOMGenerator) packit.BuildFunc {
return func(context packit.BuildContext) (packit.BuildResult, error) {
logs.Title("%s %s", context.BuildpackInfo.Name, context.BuildpackInfo.Version)

Expand All @@ -32,13 +41,31 @@ func Build(buildProcess BuildProcess, logs scribe.Emitter) packit.BuildFunc {
return packit.BuildResult{}, err
}

modCacheLayer.Launch = true
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@TisVictress What was the motivation behind making the modules cache layer available at launch time?

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did some more investigating and realized that it's currently not possible to access the SBOMs for layers that aren't marked for launch. See this conversation on a lifecycle issue implementing SBOM support. That means that currently, there's no way for an end user to access the SBOM generated by this buildpack unless the layer is marked for launch. (I'm guessing you already know this and it's why you marked the layer for launch?)

I don't think we should change this buildpack to mark the modules layer for launch – for security/hardening and optimization reasons we don't want the modules layer present in the app image. But unfortunately I think that means there's no way to test or use this feature until the lifecycle and/or pack makes the build SBOM available 😞 .

Copy link
Contributor Author

@TisVictress TisVictress Jan 18, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@fg-j I needed to be able to test that the BOM files exist in the integration test.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So, I pinged CNB folks about this, and it looks like there's now an PR to enable grabbing build-time BOMs: buildpacks/pack#1359

Since this work is blocked anyway, let's wait to see if the build-BOM feature lands soon. That way we can integration test without changing the API of the buildpack.

modCacheLayer.Cache = true

err = buildProcess.Execute(modCacheLayer.Path, context.WorkingDir)
if err != nil {
return packit.BuildResult{}, err
}

logs.Process("Generating SBOM")

var sbomContent sbom.SBOM
duration, err := clock.Measure(func() error {
sbomContent, err = sbomGenerator.Generate(context.WorkingDir)
return err
})
if err != nil {
return packit.BuildResult{}, err
}
logs.Action("Completed in %s", duration.Round(time.Millisecond))

modCacheLayer.SBOM, err = sbomContent.InFormats(context.BuildpackInfo.SBOMFormats...)
if err != nil {
return packit.BuildResult{}, err
}

return packit.BuildResult{
Plan: context.Plan,
Layers: []packit.Layer{modCacheLayer},
Expand Down
100 changes: 77 additions & 23 deletions build_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,17 @@ package gomodvendor_test
import (
"bytes"
"errors"
"github.com/paketo-buildpacks/packit/v2/chronos"
"github.com/paketo-buildpacks/packit/v2/sbom"
"os"
"path/filepath"
"testing"
"time"

gomodvendor "github.com/paketo-buildpacks/go-mod-vendor"
"github.com/paketo-buildpacks/go-mod-vendor/fakes"
"github.com/paketo-buildpacks/packit"
"github.com/paketo-buildpacks/packit/scribe"
"github.com/paketo-buildpacks/packit/v2"
"github.com/paketo-buildpacks/packit/v2/scribe"
"github.com/sclevine/spec"

. "github.com/onsi/gomega"
Expand All @@ -20,10 +23,12 @@ func testBuild(t *testing.T, context spec.G, it spec.S) {
var (
Expect = NewWithT(t).Expect

layersDir string
workingDir string
logs *bytes.Buffer
buildProcess *fakes.BuildProcess
layersDir string
workingDir string
logs *bytes.Buffer
buildProcess *fakes.BuildProcess
sbomGenerator *fakes.SBOMGenerator
clock chronos.Clock

build packit.BuildFunc
)
Expand All @@ -41,9 +46,19 @@ func testBuild(t *testing.T, context spec.G, it spec.S) {
buildProcess = &fakes.BuildProcess{}
buildProcess.ShouldRunCall.Returns.Ok = true

now := time.Now()
clock = chronos.NewClock(func() time.Time {
return now
})

sbomGenerator = &fakes.SBOMGenerator{}
sbomGenerator.GenerateCall.Returns.SBOM = sbom.SBOM{}

build = gomodvendor.Build(
buildProcess,
scribe.NewEmitter(logs),
clock,
sbomGenerator,
)
})

Expand All @@ -57,26 +72,35 @@ func testBuild(t *testing.T, context spec.G, it spec.S) {
Layers: packit.Layers{Path: layersDir},
WorkingDir: workingDir,
BuildpackInfo: packit.BuildpackInfo{
Name: "Some Buildpack",
Version: "some-version",
Name: "Some Buildpack",
Version: "some-version",
SBOMFormats: []string{"application/vnd.cyclonedx+json", "application/spdx+json", "application/vnd.syft+json"},
},
})
Expect(err).NotTo(HaveOccurred())

Expect(result).To(Equal(packit.BuildResult{
Layers: []packit.Layer{
{
Name: "mod-cache",
Path: filepath.Join(layersDir, "mod-cache"),
SharedEnv: packit.Environment{},
BuildEnv: packit.Environment{},
LaunchEnv: packit.Environment{},
ProcessLaunchEnv: map[string]packit.Environment{},
Build: false,
Launch: false,
Cache: true,
Metadata: nil,
},
Expect(err).NotTo(HaveOccurred())
Expect(result.Layers[0].Name).To(Equal("mod-cache"))
Expect(result.Layers[0].Path).To(Equal(filepath.Join(layersDir, "mod-cache")))
Expect(result.Layers[0].SharedEnv).To(Equal(packit.Environment{}))
Expect(result.Layers[0].BuildEnv).To(Equal(packit.Environment{}))
Expect(result.Layers[0].LaunchEnv).To(Equal(packit.Environment{}))
Expect(result.Layers[0].ProcessLaunchEnv).To(Equal(map[string]packit.Environment{}))
Expect(result.Layers[0].Build).To(BeFalse())
Expect(result.Layers[0].Launch).To(BeTrue())
Expect(result.Layers[0].Cache).To(BeTrue())

Expect(result.Layers[0].SBOM.Formats()).To(Equal([]packit.SBOMFormat{
{
Extension: "cdx.json",
Content: sbom.NewFormattedReader(sbom.SBOM{}, sbom.CycloneDXFormat),
},
{
Extension: "spdx.json",
Content: sbom.NewFormattedReader(sbom.SBOM{}, sbom.SPDXFormat),
},
{
Extension: "syft.json",
Content: sbom.NewFormattedReader(sbom.SBOM{}, sbom.SyftFormat),
},
}))

Expand Down Expand Up @@ -155,5 +179,35 @@ func testBuild(t *testing.T, context spec.G, it spec.S) {
Expect(err).To(MatchError(ContainSubstring("build process failed to execute")))
})
})
context("when the BOM cannot be generated", func() {
it.Before(func() {
sbomGenerator.GenerateCall.Returns.Error = errors.New("failed to generate SBOM")
})
it("returns an error", func() {
_, err := build(packit.BuildContext{
BuildpackInfo: packit.BuildpackInfo{
SBOMFormats: []string{"application/vnd.cyclonedx+json", "application/spdx+json", "application/vnd.syft+json"},
},
WorkingDir: workingDir,
Layers: packit.Layers{Path: layersDir},
Plan: packit.BuildpackPlan{
Entries: []packit.BuildpackPlanEntry{{Name: "node_modules"}},
},
Stack: "some-stack",
})
Expect(err).To(MatchError("failed to generate SBOM"))
})
})

context("when the BOM cannot be formatted", func() {
it("returns an error", func() {
_, err := build(packit.BuildContext{
BuildpackInfo: packit.BuildpackInfo{
SBOMFormats: []string{"random-format"},
},
})
Expect(err).To(MatchError("\"random-format\" is not a supported SBOM format"))
})
})
})
}
3 changes: 2 additions & 1 deletion buildpack.toml
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
api = "0.6"
api = "0.7"

[buildpack]
homepage = "https://github.com/paketo-buildpacks/go-mod-vendor"
id = "paketo-buildpacks/go-mod-vendor"
name = "Paketo Go Mod Vendor Buildpack"
sbom-formats = ["application/vnd.cyclonedx+json","application/spdx+json","application/vnd.syft+json"]

[[buildpack.licenses]]
type = "Apache-2.0"
Expand Down
2 changes: 1 addition & 1 deletion detect.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import (
"os"
"path/filepath"

"github.com/paketo-buildpacks/packit"
"github.com/paketo-buildpacks/packit/v2"
)

//go:generate faux --interface VersionParser --output fakes/version_parser.go
Expand Down
2 changes: 1 addition & 1 deletion detect_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import (

gomodvendor "github.com/paketo-buildpacks/go-mod-vendor"
"github.com/paketo-buildpacks/go-mod-vendor/fakes"
"github.com/paketo-buildpacks/packit"
"github.com/paketo-buildpacks/packit/v2"
"github.com/sclevine/spec"

. "github.com/onsi/gomega"
Expand Down
12 changes: 6 additions & 6 deletions fakes/build_process.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import "sync"

type BuildProcess struct {
ExecuteCall struct {
sync.Mutex
mutex sync.Mutex
CallCount int
Receives struct {
Path string
Expand All @@ -16,7 +16,7 @@ type BuildProcess struct {
Stub func(string, string) error
}
ShouldRunCall struct {
sync.Mutex
mutex sync.Mutex
CallCount int
Receives struct {
WorkingDir string
Expand All @@ -31,8 +31,8 @@ type BuildProcess struct {
}

func (f *BuildProcess) Execute(param1 string, param2 string) error {
f.ExecuteCall.Lock()
defer f.ExecuteCall.Unlock()
f.ExecuteCall.mutex.Lock()
defer f.ExecuteCall.mutex.Unlock()
f.ExecuteCall.CallCount++
f.ExecuteCall.Receives.Path = param1
f.ExecuteCall.Receives.WorkingDir = param2
Expand All @@ -42,8 +42,8 @@ func (f *BuildProcess) Execute(param1 string, param2 string) error {
return f.ExecuteCall.Returns.Error
}
func (f *BuildProcess) ShouldRun(param1 string) (bool, string, error) {
f.ShouldRunCall.Lock()
defer f.ShouldRunCall.Unlock()
f.ShouldRunCall.mutex.Lock()
defer f.ShouldRunCall.mutex.Unlock()
f.ShouldRunCall.CallCount++
f.ShouldRunCall.Receives.WorkingDir = param1
if f.ShouldRunCall.Stub != nil {
Expand Down
8 changes: 4 additions & 4 deletions fakes/executable.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@ package fakes
import (
"sync"

"github.com/paketo-buildpacks/packit/pexec"
"github.com/paketo-buildpacks/packit/v2/pexec"
)

type Executable struct {
ExecuteCall struct {
sync.Mutex
mutex sync.Mutex
CallCount int
Receives struct {
Execution pexec.Execution
Expand All @@ -21,8 +21,8 @@ type Executable struct {
}

func (f *Executable) Execute(param1 pexec.Execution) error {
f.ExecuteCall.Lock()
defer f.ExecuteCall.Unlock()
f.ExecuteCall.mutex.Lock()
defer f.ExecuteCall.mutex.Unlock()
f.ExecuteCall.CallCount++
f.ExecuteCall.Receives.Execution = param1
if f.ExecuteCall.Stub != nil {
Expand Down
33 changes: 33 additions & 0 deletions fakes/sbom_generator.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package fakes

import (
"sync"

"github.com/paketo-buildpacks/packit/v2/sbom"
)

type SBOMGenerator struct {
GenerateCall struct {
mutex sync.Mutex
CallCount int
Receives struct {
Dir string
}
Returns struct {
SBOM sbom.SBOM
Error error
}
Stub func(string) (sbom.SBOM, error)
}
}

func (f *SBOMGenerator) Generate(param1 string) (sbom.SBOM, error) {
f.GenerateCall.mutex.Lock()
defer f.GenerateCall.mutex.Unlock()
f.GenerateCall.CallCount++
f.GenerateCall.Receives.Dir = param1
if f.GenerateCall.Stub != nil {
return f.GenerateCall.Stub(param1)
}
return f.GenerateCall.Returns.SBOM, f.GenerateCall.Returns.Error
}
6 changes: 3 additions & 3 deletions fakes/version_parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import "sync"

type VersionParser struct {
ParseVersionCall struct {
sync.Mutex
mutex sync.Mutex
CallCount int
Receives struct {
Path string
Expand All @@ -18,8 +18,8 @@ type VersionParser struct {
}

func (f *VersionParser) ParseVersion(param1 string) (string, error) {
f.ParseVersionCall.Lock()
defer f.ParseVersionCall.Unlock()
f.ParseVersionCall.mutex.Lock()
defer f.ParseVersionCall.mutex.Unlock()
f.ParseVersionCall.CallCount++
f.ParseVersionCall.Receives.Path = param1
if f.ParseVersionCall.Stub != nil {
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,6 @@ require (
github.com/BurntSushi/toml v1.0.0
github.com/onsi/gomega v1.17.0
github.com/paketo-buildpacks/occam v0.3.0
github.com/paketo-buildpacks/packit v1.3.1
github.com/paketo-buildpacks/packit/v2 v2.0.1
github.com/sclevine/spec v1.4.0
)
Loading