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

Polish PR: Additional updates for SBOM Support #98

Merged
merged 2 commits into from Nov 24, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
42 changes: 42 additions & 0 deletions build.go
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
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
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
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
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