Skip to content
Open
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
25 changes: 25 additions & 0 deletions pkg/build/sbom_builder.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package build

import (
"fmt"
"strings"

"github.com/werf/werf/v2/pkg/image"
)

const containerFactoryRegistry = "registry.deckhouse.io/container-factory"

func isGolangBuilderImage(name string) bool {
return strings.HasPrefix(name, fmt.Sprintf("%s/builder/golang", containerFactoryRegistry))
}

func isAlpineBuilderImage(name string) bool {
return strings.HasPrefix(name, fmt.Sprintf("%s/builder/alpine", containerFactoryRegistry))
}

func isTrustedBuilderImage(labels map[string]string) bool {
if labels == nil {
return false
}
return labels[image.DeckhouseInternalBuilderLabel] == "true"
}
107 changes: 107 additions & 0 deletions pkg/build/sbom_builder_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
package build

import (
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"

werfImage "github.com/werf/werf/v2/pkg/image"
)

var _ = Describe("Builder image classification", func() {
Describe("isGolangBuilderImage()", func() {
DescribeTable("should detect golang builder images from container-factory registry",
func(name string, expected bool) {
Expect(isGolangBuilderImage(name)).To(Equal(expected))
},
// Golang images: must return true
Entry("golang-alpine with tag", "registry.deckhouse.io/container-factory/builder/golang-alpine:1.25", true),
Entry("golang-alt with tag", "registry.deckhouse.io/container-factory/builder/golang-alt:1.25", true),
Entry("golang-debian with tag", "registry.deckhouse.io/container-factory/builder/golang-debian:1.25", true),
Entry("golang-wolfi with tag", "registry.deckhouse.io/container-factory/builder/golang-wolfi:1.25", true),
Entry("golang plain with tag", "registry.deckhouse.io/container-factory/builder/golang:1.26", true),
Entry("golang-artifact with tag", "registry.deckhouse.io/container-factory/builder/golang-artifact:1.26", true),
Entry("golang with digest", "registry.deckhouse.io/container-factory/builder/golang@sha256:abc123", true),
Entry("golang without tag", "registry.deckhouse.io/container-factory/builder/golang", true),

// Alpine images: must return false (not golang)
Entry("alpine (not golang)", "registry.deckhouse.io/container-factory/builder/alpine:3.22", false),
Entry("alpine-svace (not golang)", "registry.deckhouse.io/container-factory/builder/alpine-svace:3.22", false),
Entry("alpine with digest (not golang)", "registry.deckhouse.io/container-factory/builder/alpine@sha256:abc123", false),

// Node images: must return false
Entry("node-alpine (not golang)", "registry.deckhouse.io/container-factory/builder/node-alpine:22.22", false),

// Other builder images from container-factory: must return false
Entry("scratch builder", "registry.deckhouse.io/container-factory/builder/scratch", false),
Entry("wolfi builder", "registry.deckhouse.io/container-factory/builder/wolfi", false),
Entry("distroless builder", "registry.deckhouse.io/container-factory/builder/distroless", false),

// Images from other namespaces: must return false
Entry("werf registry golang image", "registry.werf.io/base/golang:1.12-alpine3.9", false),
Entry("docker.io image", "docker.io/namespace/repo:builder-tag", false),
Entry("docker hub library", "docker.io/library/golang:1.21", false),

// Edge cases
Entry("empty string", "", false),
Entry("just a path without registry", "builder/golang:1.26", false),
Entry("path with golang substring but different registry", "example.com/container-factory/builder/golang:1.0", false),
)

It("should reject image where golang appears as substring in the registry name", func() {
Expect(isGolangBuilderImage("registry.deckhouse.io/container-factory-builder-golang/other:1.0")).To(BeFalse())
})
})

Describe("isAlpineBuilderImage()", func() {
DescribeTable("should detect alpine builder images from container-factory registry",
func(name string, expected bool) {
Expect(isAlpineBuilderImage(name)).To(Equal(expected))
},
// Alpine images: must return true
Entry("alpine with tag", "registry.deckhouse.io/container-factory/builder/alpine:3.22", true),
Entry("alpine-svace with tag", "registry.deckhouse.io/container-factory/builder/alpine-svace:3.22", true),
Entry("alpine with digest", "registry.deckhouse.io/container-factory/builder/alpine@sha256:abc123", true),
Entry("alpine without tag", "registry.deckhouse.io/container-factory/builder/alpine", true),

// Golang-alpine: must return false (starts with builder/golang, not builder/alpine)
Entry("golang-alpine (not alpine builder)", "registry.deckhouse.io/container-factory/builder/golang-alpine:1.25", false),

// Node-alpine: must return false (starts with builder/node, not builder/alpine)
Entry("node-alpine (not alpine builder)", "registry.deckhouse.io/container-factory/builder/node-alpine:22.22", false),

// Other golang images: must return false
Entry("golang (not alpine)", "registry.deckhouse.io/container-factory/builder/golang:1.26", false),
Entry("golang-debian (not alpine)", "registry.deckhouse.io/container-factory/builder/golang-debian:1.26", false),

// Other builder images from container-factory: must return false
Entry("scratch builder", "registry.deckhouse.io/container-factory/builder/scratch", false),
Entry("wolfi builder", "registry.deckhouse.io/container-factory/builder/wolfi", false),
Entry("distroless builder", "registry.deckhouse.io/container-factory/builder/distroless", false),

// Images from other namespaces: must return false
Entry("werf registry alpine image", "registry.werf.io/base/alpine:3.18", false),
Entry("docker.io image", "docker.io/namespace/repo:alpine", false),

// Edge cases
Entry("empty string", "", false),
Entry("just a path without registry", "builder/alpine:3.18", false),
Entry("alpine under different registry", "example.com/container-factory/builder/alpine:1.0", false),
)
})

Describe("isTrustedBuilderImage()", func() {
DescribeTable("should detect trusted builder images by label",
func(labels map[string]string, expected bool) {
Expect(isTrustedBuilderImage(labels)).To(Equal(expected))
},
Entry("nil labels", nil, false),
Entry("empty labels", map[string]string{}, false),
Entry("label set to false", map[string]string{werfImage.DeckhouseInternalBuilderLabel: "false"}, false),
Entry("label set to true", map[string]string{werfImage.DeckhouseInternalBuilderLabel: "true"}, true),
Entry("case-sensitive label mismatch", map[string]string{"io.deckhouse.internal.builder": "True"}, false),
Entry("other labels without builder", map[string]string{"foo": "bar", "baz": "qux"}, false),
Entry("other labels with builder true", map[string]string{"foo": "bar", werfImage.DeckhouseInternalBuilderLabel: "true", "baz": "qux"}, true),
Entry("value is non-empty but not true", map[string]string{werfImage.DeckhouseInternalBuilderLabel: "yes"}, false),
)
})
})
21 changes: 13 additions & 8 deletions pkg/build/sbom_step.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"errors"
"fmt"
"os"
"strings"

cdx "github.com/CycloneDX/cyclonedx-go"
Expand All @@ -20,6 +21,7 @@ import (
osPm "github.com/werf/werf/v2/pkg/sbom/packages/os_pm"
"github.com/werf/werf/v2/pkg/sbom/scanner"
"github.com/werf/werf/v2/pkg/storage"
"github.com/werf/werf/v2/pkg/werf/global_warnings"
)

//go:generate mockgen -source sbom_step.go -package mock -destination ../../test/mock/bom_patcher.go -mock_names BOMPatcherInterface=MockBOMPatcher
Expand Down Expand Up @@ -165,7 +167,17 @@ func (step *sbomStep) GetImageBOM(ctx context.Context, imageName string, imageIn
bom, err := step.pullImageSbom(ctx, imageName, imageInfo)
if err != nil {
if isTrustedBuilderImage(imageInfo.Labels) {
return nil, ErrSbomNotRequired
switch {
case isGolangBuilderImage(imageInfo.Name), isAlpineBuilderImage(imageInfo.Name):
global_warnings.GlobalWarningLn(ctx,
fmt.Sprintf("The builder image %q is DEPRECATED and it WILL CAUSE AN ERROR in the future. Plan your migration to an up-to-date builder image.", imageInfo.Name))
return nil, ErrSbomNotRequired
default:
if os.Getenv("WERF_E2E_ALLOW_LOCAL_BUILDER_IMAGES") == "true" {
return nil, ErrSbomNotRequired
}
return nil, fmt.Errorf("the base image %q must have an SBOM artifact attached; the image is a builder image but SBOM is required; %w", imageInfo.Name, err)
}
}
return nil, fmt.Errorf("the base image %q must either have the label %q set to \"true\" or have an SBOM artifact attached; to generate an SBOM for the base image, rebuild it with SBOM generation enabled: %w", imageInfo.Name, image.DeckhouseInternalBuilderLabel, err)
}
Expand All @@ -192,13 +204,6 @@ func (step *sbomStep) pullImageSbom(ctx context.Context, imageName string, image
return bom, nil
}

func isTrustedBuilderImage(labels map[string]string) bool {
if labels == nil {
return false
}
return labels[image.DeckhouseInternalBuilderLabel] == "true"
}

func (step *sbomStep) prepareGostComponents(ctx context.Context, mergeOpts *cyclonedxutil.MergeOpts) error {
if !mergeOpts.Gost.AttackSurface.IsUndefined() || !mergeOpts.Gost.SecurityFunction.IsUndefined() {
logboek.Context(ctx).Default().LogF("Warning: GOST SBOM integration is experimental and its behavior may change in the future\n")
Expand Down
51 changes: 50 additions & 1 deletion pkg/build/sbom_step_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ var _ = Describe("SbomStep", func() {
})

Describe("GetImageBOM() with trusted builder image", func() {
It("should return ErrSbomNotRequired for builder image regardless of digest availability", func(ctx SpecContext) {
It("should return hard error for builder image from different namespace", func(ctx SpecContext) {
step := &sbomStep{}

imageInfo := &werfImage.Info{
Expand All @@ -117,9 +117,58 @@ var _ = Describe("SbomStep", func() {
}

_, err := step.GetImageBOM(ctx, "builder-image", imageInfo)
Expect(err).To(HaveOccurred())
Expect(errors.Is(err, ErrSbomNotRequired)).To(BeFalse())
Expect(err.Error()).To(ContainSubstring("the image is a builder image but SBOM is required"))
})

It("should return ErrSbomNotRequired for golang builder image from container-factory", func(ctx SpecContext) {
step := &sbomStep{}

imageInfo := &werfImage.Info{
Name: "registry.deckhouse.io/container-factory/builder/golang-alpine:1.25",
Repository: "registry.deckhouse.io/container-factory/builder/golang-alpine",
Labels: map[string]string{
werfImage.DeckhouseInternalBuilderLabel: "true",
},
}

_, err := step.GetImageBOM(logging.WithLogger(ctx), "builder-image", imageInfo)
Expect(err).To(MatchError(ErrSbomNotRequired))
})

It("should return ErrSbomNotRequired for alpine builder image from container-factory", func(ctx SpecContext) {
step := &sbomStep{}

imageInfo := &werfImage.Info{
Name: "registry.deckhouse.io/container-factory/builder/alpine:3.22",
Repository: "registry.deckhouse.io/container-factory/builder/alpine",
Labels: map[string]string{
werfImage.DeckhouseInternalBuilderLabel: "true",
},
}

_, err := step.GetImageBOM(ctx, "builder-image", imageInfo)
Expect(err).To(MatchError(ErrSbomNotRequired))
})

It("should return hard error for other builder image from container-factory", func(ctx SpecContext) {
step := &sbomStep{}

imageInfo := &werfImage.Info{
Name: "registry.deckhouse.io/container-factory/builder/scratch",
Repository: "registry.deckhouse.io/container-factory/builder/scratch",
Labels: map[string]string{
werfImage.DeckhouseInternalBuilderLabel: "true",
},
}

_, err := step.GetImageBOM(ctx, "builder-image", imageInfo)
Expect(err).To(HaveOccurred())
Expect(errors.Is(err, ErrSbomNotRequired)).To(BeFalse())
Expect(err.Error()).To(ContainSubstring("the image is a builder image but SBOM is required"))
})

It("should return actionable error for non-builder image when SBOM pull fails", func(ctx SpecContext) {
step := &sbomStep{}

Expand Down
5 changes: 4 additions & 1 deletion test/e2e/sbom/common_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,5 +43,8 @@ func buildTrustedBuilderBase(ctx SpecContext, testRepoPath, refSlug string) []st
builderBaseRef := fmt.Sprintf("%s/%s:test", suite_init.TestRegistry(), refSlug)
utils.RunSucceedCommand(ctx, testRepoPath, "docker", "build", "-t", builderBaseRef, "-f", "Dockerfile.builder-base", ".")
utils.RunSucceedCommand(ctx, testRepoPath, "docker", "push", builderBaseRef)
return []string{fmt.Sprintf("BUILDER_BASE_IMAGE=%s", builderBaseRef)}
return []string{
fmt.Sprintf("BUILDER_BASE_IMAGE=%s", builderBaseRef),
"WERF_E2E_ALLOW_LOCAL_BUILDER_IMAGES=true",
}
}
Loading