From 66f3e9f328598fbf9a5657eb74c4ccf63994e660 Mon Sep 17 00:00:00 2001 From: Daniel Mikusa Date: Fri, 19 Nov 2021 16:02:24 -0500 Subject: [PATCH 1/2] Additional updates for SBOM Support - 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 Co-authored-by: Sambhav Kothari Signed-off-by: Daniel Mikusa --- build.go | 42 ++++++++++++++++++++++++++++++++++++++++++ build_test.go | 26 ++++++++++++++++++++++++++ buildpack.go | 3 +++ layer.go | 38 +++++++++++++++++++++++++++++++++++++- layer_test.go | 20 +++++++++++++++++++- 5 files changed, 127 insertions(+), 2 deletions(-) diff --git a/build.go b/build.go index 223a298..9055bfc 100644 --- a/build.go +++ b/build.go @@ -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 { @@ -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 @@ -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 @@ -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 +} diff --git a/build_test.go b/build_test.go index d079f1a..dec9715 100644 --- a/build_test.go +++ b/build_test.go @@ -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")) + }) + }) } diff --git a/buildpack.go b/buildpack.go index aa16af6..a81ba2c 100644 --- a/buildpack.go +++ b/buildpack.go @@ -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 diff --git a/layer.go b/layer.go index 0ddc71b..dadd26e 100644 --- a/layer.go +++ b/layer.go @@ -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. @@ -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. diff --git a/layer_test.go b/layer_test.go index 0d6d6e2..9bb9054 100644 --- a/layer_test.go +++ b/layer_test.go @@ -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()) @@ -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"), From 6d86013d6d1d91455f6e2dd7e56c531d2bdd3230 Mon Sep 17 00:00:00 2001 From: Sambhav Kothari Date: Tue, 23 Nov 2021 18:51:18 +0000 Subject: [PATCH 2/2] Make validate SBOM private Signed-off-by: Sambhav Kothari --- build.go | 4 +-- build_test.go | 75 +++++++++++++++++++++++++++++++++++++++++++++------ 2 files changed, 69 insertions(+), 10 deletions(-) diff --git a/build.go b/build.go index 9055bfc..0f16d8b 100644 --- a/build.go +++ b/build.go @@ -306,7 +306,7 @@ func Build(builder Builder, options ...Option) { } } - if err := ValidateSBOMFormats(ctx.Layers.Path, ctx.Buildpack.Info.SBOMFormats); err != nil { + 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 } @@ -399,7 +399,7 @@ func contains(candidates []string, s string) bool { return false } -func ValidateSBOMFormats(layersPath string, acceptedSBOMFormats []string) error { +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) diff --git a/build_test.go b/build_test.go index dec9715..19af75e 100644 --- a/build_test.go +++ b/build_test.go @@ -627,28 +627,87 @@ 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() { - Expect(libcnb.ValidateSBOMFormats(layersPath, []string{})).To(BeNil()) + 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()) - Expect(libcnb.ValidateSBOMFormats(layersPath, []string{})).To(MatchError("unable to find actual SBOM Type application/spdx+json in list of supported SBOM types []")) + + 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()) - 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]")) + + 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.spdx.json"), []byte{}, 0600)).To(Succeed()) - Expect(libcnb.ValidateSBOMFormats(layersPath, []string{"application/vnd.cyclonedx+json", "application/spdx+json"})).To(BeNil()) + 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 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")) + 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")) }) }) }