Skip to content

Commit

Permalink
Merge pull request #98 from buildpacks/polish-pr-97
Browse files Browse the repository at this point in the history
Polish PR: Additional updates for SBOM Support
  • Loading branch information
sambhav authored Nov 24, 2021
2 parents 6aa81e5 + 6d86013 commit a33b367
Show file tree
Hide file tree
Showing 5 changed files with 186 additions and 2 deletions.
42 changes: 42 additions & 0 deletions build.go
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,11 @@ func Build(builder Builder, options ...Option) {
}
}

if err := validateSBOMFormats(ctx.Layers.Path, ctx.Buildpack.Info.SBOMFormats); err != nil {
config.exitHandler.Error(fmt.Errorf("unable to validate SBOM\n%w", err))
return
}

// Deprecated: as of Buildpack API 0.7, to be removed in a future version
var launchBOM, buildBOM []BOMEntry
if result.BOM != nil {
Expand Down Expand Up @@ -338,6 +343,12 @@ func Build(builder Builder, options ...Option) {
}
}

// even if there is data, do not write a BOM if we have buildpack API 0.7, that will cause a lifecycle error
if API == "0.7" {
logger.Info("Warning: this buildpack is including both old and new format SBOM information, which is an invalid state. To prevent the lifecycle from failing, libcnb is discarding the old SBOM information.")
launch.BOM = nil
}

if err = config.tomlWriter.Write(file, launch); err != nil {
config.exitHandler.Error(fmt.Errorf("unable to write application metadata %s\n%w", file, err))
return
Expand All @@ -352,6 +363,13 @@ func Build(builder Builder, options ...Option) {
if !build.isEmpty() {
file = filepath.Join(ctx.Layers.Path, "build.toml")
logger.Debugf("Writing build metadata: %s <= %+v", file, build)

// even if there is data, do not write a BOM if we have buildpack API 0.7, that will cause a lifecycle error
if API == "0.7" {
logger.Info("Warning: this buildpack is including both old and new format SBOM information, which is an invalid state. To prevent the lifecycle from failing, libcnb is discarding the old SBOM information.")
build.BOM = nil
}

if err = config.tomlWriter.Write(file, build); err != nil {
config.exitHandler.Error(fmt.Errorf("unable to write build metadata %s\n%w", file, err))
return
Expand Down Expand Up @@ -380,3 +398,27 @@ func contains(candidates []string, s string) bool {

return false
}

func validateSBOMFormats(layersPath string, acceptedSBOMFormats []string) error {
sbomFiles, err := filepath.Glob(filepath.Join(layersPath, "*.sbom.*"))
if err != nil {
return fmt.Errorf("unable find SBOM files\n%w", err)
}

for _, sbomFile := range sbomFiles {
parts := strings.Split(filepath.Base(sbomFile), ".")
if len(parts) <= 2 {
return fmt.Errorf("invalid format %s", filepath.Base(sbomFile))
}
sbomFormat, err := SBOMFormatFromString(strings.Join(parts[len(parts)-2:], "."))
if err != nil {
return fmt.Errorf("unable to parse SBOM %s\n%w", sbomFormat, err)
}

if !contains(acceptedSBOMFormats, sbomFormat.MediaType()) {
return fmt.Errorf("unable to find actual SBOM Type %s in list of supported SBOM types %s", sbomFormat.MediaType(), acceptedSBOMFormats)
}
}

return nil
}
85 changes: 85 additions & 0 deletions build_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -625,4 +625,89 @@ version = "1.1.1"
},
}))
})

context("Validates SBOM entries", func() {
it.Before(func() {
Expect(ioutil.WriteFile(filepath.Join(buildpackPath, "buildpack.toml"),
[]byte(`
api = "0.7"
[buildpack]
id = "test-id"
name = "test-name"
version = "1.1.1"
sbom-formats = ["application/vnd.cyclonedx+json"]
`),
0600),
).To(Succeed())

builder.On("Build", mock.Anything).Return(libcnb.BuildResult{}, nil)
})

it("has no SBOM files", func() {
libcnb.Build(builder,
libcnb.WithArguments([]string{commandPath, layersPath, platformPath, buildpackPlanPath}),
libcnb.WithExitHandler(exitHandler),
)

Expect(exitHandler.Calls).To(BeEmpty())
})

it("has no accepted formats", func() {
Expect(ioutil.WriteFile(filepath.Join(buildpackPath, "buildpack.toml"),
[]byte(`
api = "0.7"
[buildpack]
id = "test-id"
name = "test-name"
version = "1.1.1"
sbom-formats = []
`),
0600),
).To(Succeed())

Expect(ioutil.WriteFile(filepath.Join(layersPath, "launch.sbom.spdx.json"), []byte{}, 0600)).To(Succeed())

libcnb.Build(builder,
libcnb.WithArguments([]string{commandPath, layersPath, platformPath, buildpackPlanPath}),
libcnb.WithExitHandler(exitHandler),
)

Expect(exitHandler.Calls[0].Arguments.Get(0)).To(MatchError("unable to validate SBOM\nunable to find actual SBOM Type application/spdx+json in list of supported SBOM types []"))
})

it("has no matching formats", func() {
Expect(ioutil.WriteFile(filepath.Join(layersPath, "launch.sbom.spdx.json"), []byte{}, 0600)).To(Succeed())

libcnb.Build(builder,
libcnb.WithArguments([]string{commandPath, layersPath, platformPath, buildpackPlanPath}),
libcnb.WithExitHandler(exitHandler),
)

Expect(exitHandler.Calls[0].Arguments.Get(0)).To(MatchError("unable to validate SBOM\nunable to find actual SBOM Type application/spdx+json in list of supported SBOM types [application/vnd.cyclonedx+json]"))
})

it("has a matching format", func() {
Expect(ioutil.WriteFile(filepath.Join(layersPath, "launch.sbom.cdx.json"), []byte{}, 0600)).To(Succeed())
Expect(ioutil.WriteFile(filepath.Join(layersPath, "layer.sbom.cdx.json"), []byte{}, 0600)).To(Succeed())
libcnb.Build(builder,
libcnb.WithArguments([]string{commandPath, layersPath, platformPath, buildpackPlanPath}),
libcnb.WithExitHandler(exitHandler),
)

Expect(exitHandler.Calls).To(BeEmpty())
})

it("has a junk format", func() {
Expect(ioutil.WriteFile(filepath.Join(layersPath, "launch.sbom.random.json"), []byte{}, 0600)).To(Succeed())
Expect(ioutil.WriteFile(filepath.Join(layersPath, "layer.sbom.cdx.json"), []byte{}, 0600)).To(Succeed())
libcnb.Build(builder,
libcnb.WithArguments([]string{commandPath, layersPath, platformPath, buildpackPlanPath}),
libcnb.WithExitHandler(exitHandler),
)

Expect(exitHandler.Calls[0].Arguments.Get(0)).To(MatchError("unable to validate SBOM\nunable to parse SBOM unknown\nunable to translate from random.json to SBOMFormat"))
})
})
}
3 changes: 3 additions & 0 deletions buildpack.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@ type BuildpackInfo struct {

// Licenses a list of buildpack licenses.
Licenses []License `toml:"licenses"`

// SBOM is the list of supported SBOM media types
SBOMFormats []string `toml:"sbom-formats"`
}

// License contains information about a Software License
Expand Down
38 changes: 37 additions & 1 deletion layer.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,16 @@ import (
"github.com/buildpacks/libcnb/internal"
)

const (
BOMFormatCycloneDXExtension = "cdx.json"
BOMFormatSPDXExtension = "spdx.json"
BOMFormatSyftExtension = "syft.json"
BOMMediaTypeCycloneDX = "application/vnd.cyclonedx+json"
BOMMediaTypeSPDX = "application/spdx+json"
BOMMediaTypeSyft = "application/vnd.syft+json"
BOMUnknown = "unknown"
)

// Exec represents the exec.d layer location
type Exec struct {
// Path is the path to the exec.d directory.
Expand Down Expand Up @@ -74,10 +84,36 @@ const (
CycloneDXJSON SBOMFormat = iota
SPDXJSON
SyftJSON
UnknownFormat
)

func (b SBOMFormat) String() string {
return []string{"cdx.json", "spdx.json", "syft.json"}[b]
return []string{
BOMFormatCycloneDXExtension,
BOMFormatSPDXExtension,
BOMFormatSyftExtension,
BOMUnknown}[b]
}

func (b SBOMFormat) MediaType() string {
return []string{
BOMMediaTypeCycloneDX,
BOMMediaTypeSPDX,
BOMMediaTypeSyft,
BOMUnknown}[b]
}

func SBOMFormatFromString(from string) (SBOMFormat, error) {
switch from {
case CycloneDXJSON.String():
return CycloneDXJSON, nil
case SPDXJSON.String():
return SPDXJSON, nil
case SyftJSON.String():
return SyftJSON, nil
}

return UnknownFormat, fmt.Errorf("unable to translate from %s to SBOMFormat", from)
}

// Layer represents a layer managed by the buildpack.
Expand Down
20 changes: 19 additions & 1 deletion layer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ func testLayer(t *testing.T, context spec.G, it spec.S) {
Expect(l.Profile).To(Equal(libcnb.Profile{}))
})

it("generates BOM paths", func() {
it("generates SBOM paths", func() {
l, err := layers.Layer("test-name")
Expect(err).NotTo(HaveOccurred())

Expand All @@ -122,6 +122,24 @@ func testLayer(t *testing.T, context spec.G, it spec.S) {
Expect(l.SBOMPath(libcnb.SyftJSON)).To(Equal(filepath.Join(path, "test-name.sbom.syft.json")))
})

it("maps from string to SBOM Format", func() {
fmt, err := libcnb.SBOMFormatFromString("cdx.json")
Expect(err).ToNot(HaveOccurred())
Expect(fmt).To(Equal(libcnb.CycloneDXJSON))

fmt, err = libcnb.SBOMFormatFromString("spdx.json")
Expect(err).ToNot(HaveOccurred())
Expect(fmt).To(Equal(libcnb.SPDXJSON))

fmt, err = libcnb.SBOMFormatFromString("syft.json")
Expect(err).ToNot(HaveOccurred())
Expect(fmt).To(Equal(libcnb.SyftJSON))

fmt, err = libcnb.SBOMFormatFromString("foobar.json")
Expect(err).To(MatchError("unable to translate from foobar.json to SBOMFormat"))
Expect(fmt).To(Equal(libcnb.UnknownFormat))
})

it("reads existing 0.5 metadata", func() {
Expect(ioutil.WriteFile(
filepath.Join(path, "test-name.toml"),
Expand Down

0 comments on commit a33b367

Please sign in to comment.