Skip to content

Commit

Permalink
Additional updates for SBOM Support
Browse files Browse the repository at this point in the history
- Adds SBOMFormats (maps to `sbom-formats` in buildpack.toml) to the BuildpackInfo struct. This makes the information accessible to buildpacks.
- Modifies build such that it only writes the old-style build and launch BOM information if the buildpack API is less than 0.7. If it's 0.7+, it should not write the old style format as that can conflict with the new SBOM format and cause the lifecycle to fail. Omits a warning message if this occurs.
- Adds validation of the new SBOM files that are written by a buildpack. We check that the extension matches up with a valid MIME type that is listed in buildpack.toml's `sbom-formats` field. If it does not match up, then it fails. This should not generally happen with published buildpacks. This check can be helpful while authoring buildpacks, to ensure everything is correctly setup.

Signed-off-by: Daniel Mikusa <dmikusa@vmware.com>
Co-authored-by: Sambhav Kothari <sambhavs.email@gmail.com>
Signed-off-by: Daniel Mikusa <dmikusa@vmware.com>
  • Loading branch information
Daniel Mikusa and samj1912 committed Nov 21, 2021
1 parent 6aa81e5 commit 66f3e9f
Show file tree
Hide file tree
Showing 5 changed files with 127 additions and 2 deletions.
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
}
26 changes: 26 additions & 0 deletions build_test.go
Expand Up @@ -625,4 +625,30 @@ version = "1.1.1"
},
}))
})

context("Validates SBOM entries", func() {
it("has no SBOM files", func() {
Expect(libcnb.ValidateSBOMFormats(layersPath, []string{})).To(BeNil())
})

it("has no accepted formats", func() {
Expect(ioutil.WriteFile(filepath.Join(layersPath, "launch.sbom.spdx.json"), []byte{}, 0600)).To(Succeed())
Expect(libcnb.ValidateSBOMFormats(layersPath, []string{})).To(MatchError("unable 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())
Expect(libcnb.ValidateSBOMFormats(layersPath, []string{"application/vnd.cyclonedx+json"})).To(MatchError("unable 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.spdx.json"), []byte{}, 0600)).To(Succeed())
Expect(libcnb.ValidateSBOMFormats(layersPath, []string{"application/vnd.cyclonedx+json", "application/spdx+json"})).To(BeNil())
})

it("has an invalid format", func() {
Expect(ioutil.WriteFile(filepath.Join(layersPath, "launch.sbom.junk.json"), []byte{}, 0600)).To(Succeed())
Expect(libcnb.ValidateSBOMFormats(layersPath, []string{})).To(MatchError("unable to parse SBOM unknown\nunable to translate from junk.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

0 comments on commit 66f3e9f

Please sign in to comment.