From 2924f10cd93f86c1504d5014c5ffd36224b58a0b Mon Sep 17 00:00:00 2001 From: Luiz Carvalho Date: Mon, 27 Feb 2023 16:40:25 -0500 Subject: [PATCH] Add support for signing SBOMs The idea is a TaskRun produces an unsigned SBOM and pushes it to the OCI registry. The reference to the SBOM is then added to the result IMAGE_SBOM_URL. When Chains sees this result, it downloads the SBOM from the OCI registry, signs it, then adds to the image attestations with an SBOM-specific predicate. This is also possible when using the IMAGES result, and the corresponding SBOMS result. Both must have the exact number of items. Signed-off-by: Luiz Carvalho --- pkg/artifacts/signable.go | 121 ++++++++++++++++++- pkg/chains/formats/sbom/sbom.go | 103 ++++++++++++++++ pkg/chains/formats/slsa/v1/intotoite6.go | 5 + pkg/chains/objects/objects.go | 26 ++++ pkg/chains/signing.go | 1 + pkg/config/config.go | 15 +++ pkg/config/store_test.go | 147 +++++++++++++++++++++++ 7 files changed, 413 insertions(+), 5 deletions(-) create mode 100644 pkg/chains/formats/sbom/sbom.go diff --git a/pkg/artifacts/signable.go b/pkg/artifacts/signable.go index 3beccb123b..fc0dca3bf9 100644 --- a/pkg/artifacts/signable.go +++ b/pkg/artifacts/signable.go @@ -154,8 +154,9 @@ type image struct { // URI is the resource uri for the target needed iff the target is a material. // Digest is the target's SHA digest. type StructuredSignable struct { - URI string - Digest string + URI string + Digest string + SBOMURI string } func (oa *OCIArtifact) ExtractObjects(obj objects.TektonObject) []interface{} { @@ -204,7 +205,7 @@ func (oa *OCIArtifact) ExtractObjects(obj objects.TektonObject) []interface{} { func ExtractOCIImagesFromResults(obj objects.TektonObject, logger *zap.SugaredLogger) []interface{} { objs := []interface{}{} - ss := extractTargetFromResults(obj, "IMAGE_URL", "IMAGE_DIGEST", logger) + ss := extractTargetFromResults(obj, "IMAGE_URL", "IMAGE_DIGEST", "", logger) for _, s := range ss { if s == nil || s.Digest == "" || s.URI == "" { continue @@ -244,7 +245,7 @@ func ExtractOCIImagesFromResults(obj objects.TektonObject, logger *zap.SugaredLo // ExtractSignableTargetFromResults extracts signable targets that aim to generate intoto provenance as materials within TaskRun results and store them as StructuredSignable. func ExtractSignableTargetFromResults(obj objects.TektonObject, logger *zap.SugaredLogger) []*StructuredSignable { objs := []*StructuredSignable{} - ss := extractTargetFromResults(obj, "ARTIFACT_URI", "ARTIFACT_DIGEST", logger) + ss := extractTargetFromResults(obj, "ARTIFACT_URI", "ARTIFACT_DIGEST", "", logger) // Only add it if we got both the signable URI and digest. for _, s := range ss { if s == nil || s.Digest == "" || s.URI == "" { @@ -266,7 +267,7 @@ func (s *StructuredSignable) FullRef() string { return fmt.Sprintf("%s@%s", s.URI, s.Digest) } -func extractTargetFromResults(obj objects.TektonObject, identifierSuffix string, digestSuffix string, logger *zap.SugaredLogger) map[string]*StructuredSignable { +func extractTargetFromResults(obj objects.TektonObject, identifierSuffix, digestSuffix, sbomSuffix string, logger *zap.SugaredLogger) map[string]*StructuredSignable { ss := map[string]*StructuredSignable{} for _, res := range obj.GetResults() { @@ -296,6 +297,18 @@ func extractTargetFromResults(obj objects.TektonObject, identifierSuffix string, ss[marker] = &StructuredSignable{Digest: strings.TrimSpace(res.Value.StringVal)} } } + if sbomSuffix != "" && strings.HasSuffix(res.Name, sbomSuffix) { + if res.Value.StringVal == "" { + logger.Debugf("error getting string value for %s", res.Name) + continue + } + marker := strings.TrimSuffix(res.Name, sbomSuffix) + if v, ok := ss[marker]; ok { + v.SBOMURI = strings.TrimSpace(res.Value.StringVal) + } else { + ss[marker] = &StructuredSignable{SBOMURI: strings.TrimSpace(res.Value.StringVal)} + } + } } return ss @@ -434,3 +447,101 @@ func (oa *OCIArtifact) FullKey(obj interface{}) string { func (oa *OCIArtifact) Enabled(cfg config.Config) bool { return cfg.Artifacts.OCI.Enabled() } + +type SBOMArtifact struct { + Logger *zap.SugaredLogger +} + +var _ Signable = &SBOMArtifact{} + +func (sa *SBOMArtifact) ExtractObjects(tektonObject objects.TektonObject) []interface{} { + var objs []interface{} + for _, obj := range extractSBOMFromResults(tektonObject, sa.Logger) { + objs = append(objs, objects.NewSBOMObject(obj.SBOMURI, obj.URI, obj.Digest)) + } + return objs +} + +func (sa *SBOMArtifact) StorageBackend(cfg config.Config) sets.String { + return cfg.Artifacts.SBOM.StorageBackend +} +func (sa *SBOMArtifact) Signer(cfg config.Config) string { + return cfg.Artifacts.SBOM.Signer +} +func (sa *SBOMArtifact) PayloadFormat(cfg config.Config) config.PayloadType { + return config.PayloadType(cfg.Artifacts.SBOM.Format) +} + +func (sa *SBOMArtifact) FullKey(obj interface{}) string { + v := obj.(*objects.SBOMObject) + return v.GetSBOMURL() +} + +func (sa *SBOMArtifact) ShortKey(obj interface{}) string { + v := obj.(*objects.SBOMObject) + return v.GetSBOMURL() +} +func (sa *SBOMArtifact) Type() string { + return "sbom" +} +func (sa *SBOMArtifact) Enabled(cfg config.Config) bool { + return cfg.Artifacts.SBOM.Enabled() +} + +func extractSBOMFromResults(tektonObject objects.TektonObject, logger *zap.SugaredLogger) []*StructuredSignable { + var objs []*StructuredSignable + + // TODO: Maybe run each SBOM through name.NewDigest(trimmed) to ensure it is a digest ref? + ss := extractTargetFromResults(tektonObject, "IMAGE_URL", "IMAGE_DIGEST", "IMAGE_SBOM_URL", logger) + for _, s := range ss { + if s == nil || s.Digest == "" || s.URI == "" || s.SBOMURI == "" { + continue + } + objs = append(objs, s) + } + + var images []name.Digest + var sboms []string + // look for a comma separated list of images + for _, key := range tektonObject.GetResults() { + switch key.Name { + case "IMAGES": + for _, img := range strings.FieldsFunc(key.Value.StringVal, split) { + trimmed := strings.TrimSpace(img) + if trimmed == "" { + continue + } + + dgst, err := name.NewDigest(trimmed) + if err != nil { + logger.Errorf("error getting digest for img %s: %v", trimmed, err) + continue + } + images = append(images, dgst) + } + case "SBOMS": + for _, img := range strings.FieldsFunc(key.Value.StringVal, split) { + trimmed := strings.TrimSpace(img) + if trimmed == "" { + continue + } + // TODO: Maybe run each SBOM through name.NewDigest(trimmed) to ensure it is a digest ref? + sboms = append(sboms, trimmed) + } + } + } + + if len(images) != len(sboms) { + logger.Warnf("IMAGES and SBOMS do not contain the same amount of entries") + } else { + for i, sbom := range sboms { + img := images[i] + objs = append(objs, &StructuredSignable{ + URI: img.Name(), + Digest: img.Identifier(), + SBOMURI: sbom, + }) + } + } + return objs +} diff --git a/pkg/chains/formats/sbom/sbom.go b/pkg/chains/formats/sbom/sbom.go new file mode 100644 index 0000000000..eb77b13959 --- /dev/null +++ b/pkg/chains/formats/sbom/sbom.go @@ -0,0 +1,103 @@ +/* +Copyright 2023 The Tekton Authors +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package sbom + +import ( + "bytes" + "fmt" + "io" + "strings" + + "github.com/google/go-containerregistry/pkg/name" + "github.com/google/go-containerregistry/pkg/v1/remote" + intoto "github.com/in-toto/in-toto-golang/in_toto" + slsa "github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/v0.2" + "github.com/tektoncd/chains/pkg/chains/objects" + "go.uber.org/zap" +) + +const maxSBOMBytes = 5 * 1024 * 1024 // 5 Megabytes + +func GenerateAttestation(builderID string, sbom *objects.SBOMObject, logger *zap.SugaredLogger) (interface{}, error) { + subject := []intoto.Subject{ + {Name: sbom.GetImageURL(), Digest: toDigestSet(sbom.GetImageDigest())}, + } + + data, err := getData(sbom.GetSBOMURL()) + if err != nil { + return nil, err + } + + // TODO: This needs to be configurable, obviously. Maybe it can + // be auto-detected from the SBOM data? + att := intoto.CycloneDXStatement{ + StatementHeader: intoto.StatementHeader{ + Type: intoto.StatementInTotoV01, + PredicateType: intoto.PredicateCycloneDX, + Subject: subject, + }, + Predicate: SBOMPredicate{ + Data: data, + }, + } + return att, nil +} + +type SBOMPredicate struct { + Data string `json:"Data"` +} + +func getData(uri string) (string, error) { + // TODO: remote options so it works against a private repository? + ref, err := name.ParseReference(uri) + if err != nil { + return "", err + } + + image, err := remote.Image(ref) + if err != nil { + return "", err + } + + layers, err := image.Layers() + if err != nil { + return "", err + } + + if len(layers) != 1 { + return "", fmt.Errorf("expected exactly 1 layer in sbom image %s, found %d", uri, len(layers)) + } + + layer, err := layers[0].Uncompressed() + if err != nil { + return "", err + } + defer layer.Close() + + var data bytes.Buffer + if _, err := io.Copy(&data, io.LimitReader(layer, maxSBOMBytes)); err != nil { + return "", err + } + + return data.String(), nil +} + +func toDigestSet(digest string) slsa.DigestSet { + algo, value, found := strings.Cut(digest, ":") + if !found { + value = algo + algo = "sha256" + } + return slsa.DigestSet{algo: value} +} diff --git a/pkg/chains/formats/slsa/v1/intotoite6.go b/pkg/chains/formats/slsa/v1/intotoite6.go index 1e10de7316..002aea0055 100644 --- a/pkg/chains/formats/slsa/v1/intotoite6.go +++ b/pkg/chains/formats/slsa/v1/intotoite6.go @@ -21,6 +21,7 @@ import ( "fmt" "github.com/tektoncd/chains/pkg/chains/formats" + "github.com/tektoncd/chains/pkg/chains/formats/sbom" "github.com/tektoncd/chains/pkg/chains/formats/slsa/v1/pipelinerun" "github.com/tektoncd/chains/pkg/chains/formats/slsa/v1/taskrun" "github.com/tektoncd/chains/pkg/chains/objects" @@ -59,6 +60,10 @@ func (i *InTotoIte6) CreatePayload(ctx context.Context, obj interface{}) (interf return taskrun.GenerateAttestation(i.builderID, v, logger) case *objects.PipelineRunObject: return pipelinerun.GenerateAttestation(i.builderID, v, logger) + case *objects.SBOMObject: + // TODO: It is odd that the slsa package has a dependency on the sbom package. But, + // this is required for now since intotoite6 is currently a part of the slsa package. + return sbom.GenerateAttestation(i.builderID, v, logger) default: return nil, fmt.Errorf("intoto does not support type: %s", v) } diff --git a/pkg/chains/objects/objects.go b/pkg/chains/objects/objects.go index dc96b620d4..b12154abac 100644 --- a/pkg/chains/objects/objects.go +++ b/pkg/chains/objects/objects.go @@ -224,3 +224,29 @@ func getPodPullSecrets(podTemplate *pod.Template) []string { } return imgPullSecrets } + +type SBOMObject struct { + sbomURL string + imageURL string + imageDigest string +} + +func NewSBOMObject(sbomURL, imageURL, imageDigest string) *SBOMObject { + return &SBOMObject{ + sbomURL: sbomURL, + imageURL: imageURL, + imageDigest: imageDigest, + } +} + +func (so *SBOMObject) GetSBOMURL() string { + return so.sbomURL +} + +func (so *SBOMObject) GetImageURL() string { + return so.imageURL +} + +func (so *SBOMObject) GetImageDigest() string { + return so.imageDigest +} diff --git a/pkg/chains/signing.go b/pkg/chains/signing.go index d369422e55..7368c7a7cc 100644 --- a/pkg/chains/signing.go +++ b/pkg/chains/signing.go @@ -89,6 +89,7 @@ func getSignableTypes(obj objects.TektonObject, logger *zap.SugaredLogger) ([]ar return []artifacts.Signable{ &artifacts.TaskRunArtifact{Logger: logger}, &artifacts.OCIArtifact{Logger: logger}, + &artifacts.SBOMArtifact{Logger: logger}, }, nil case *v1beta1.PipelineRun: return []artifacts.Signable{ diff --git a/pkg/config/config.go b/pkg/config/config.go index 3f02b70eef..9bf312a7c7 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -39,6 +39,7 @@ type ArtifactConfigs struct { OCI Artifact PipelineRuns Artifact TaskRuns Artifact + SBOM Artifact } // Artifact contains the configuration for how to sign/store/format the signatures for a single artifact @@ -155,6 +156,10 @@ const ( ociStorageKey = "artifacts.oci.storage" ociSignerKey = "artifacts.oci.signer" + sbomFormatKey = "artifacts.sbom.format" + sbomStorageKey = "artifacts.sbom.storage" + sbomSignerKey = "artifacts.sbom.signer" + gcsBucketKey = "storage.gcs.bucket" ociRepositoryKey = "storage.oci.repository" ociRepositoryInsecureKey = "storage.oci.repository.insecure" @@ -222,6 +227,11 @@ func defaultConfig() *Config { StorageBackend: sets.NewString("oci"), Signer: "x509", }, + SBOM: Artifact{ + Format: "in-toto", + StorageBackend: sets.NewString("oci"), + Signer: "x509", + }, }, Transparency: TransparencyConfig{ URL: "https://rekor.sigstore.dev", @@ -264,6 +274,11 @@ func NewConfigFromMap(data map[string]string) (*Config, error) { asStringSet(ociStorageKey, &cfg.Artifacts.OCI.StorageBackend, sets.NewString("tekton", "oci", "gcs", "docdb", "grafeas", "kafka")), asString(ociSignerKey, &cfg.Artifacts.OCI.Signer, "x509", "kms"), + // SBOM + asString(sbomFormatKey, &cfg.Artifacts.SBOM.Format, "in-toto"), + asStringSet(sbomStorageKey, &cfg.Artifacts.SBOM.StorageBackend, sets.NewString("tekton", "oci", "gcs", "docdb", "grafeas", "kafka")), + asString(sbomSignerKey, &cfg.Artifacts.SBOM.Signer, "x509", "kms"), + // PubSub - General asString(pubsubProvider, &cfg.Storage.PubSub.Provider, "inmemory", "kafka"), asString(pubsubTopic, &cfg.Storage.PubSub.Topic), diff --git a/pkg/config/store_test.go b/pkg/config/store_test.go index 2e5a0866aa..7d282b9798 100644 --- a/pkg/config/store_test.go +++ b/pkg/config/store_test.go @@ -111,6 +111,11 @@ var defaultArtifacts = ArtifactConfigs{ StorageBackend: sets.NewString("oci"), Signer: "x509", }, + SBOM: Artifact{ + Format: "in-toto", + StorageBackend: sets.NewString("oci"), + Signer: "x509", + }, } var defaultStorage = StorageConfigs{ @@ -128,6 +133,7 @@ func TestParse(t *testing.T) { name string data map[string]string taskrunEnabled bool + sbomEnabled bool ociEnbaled bool want Config }{ @@ -136,6 +142,7 @@ func TestParse(t *testing.T) { data: map[string]string{}, taskrunEnabled: true, ociEnbaled: true, + sbomEnabled: true, want: Config{ Builder: defaultBuilder, Artifacts: defaultArtifacts, @@ -150,6 +157,7 @@ func TestParse(t *testing.T) { }, taskrunEnabled: true, ociEnbaled: true, + sbomEnabled: true, want: Config{ Builder: BuilderConfig{ "builder-id-test", @@ -166,6 +174,7 @@ func TestParse(t *testing.T) { }, taskrunEnabled: true, ociEnbaled: true, + sbomEnabled: true, want: Config{ Builder: defaultBuilder, Artifacts: defaultArtifacts, @@ -183,6 +192,7 @@ func TestParse(t *testing.T) { data: map[string]string{taskrunStorageKey: "tekton,oci"}, taskrunEnabled: true, ociEnbaled: true, + sbomEnabled: true, want: Config{ Builder: defaultBuilder, Artifacts: ArtifactConfigs{ @@ -201,6 +211,11 @@ func TestParse(t *testing.T) { StorageBackend: sets.NewString("oci"), Signer: "x509", }, + SBOM: Artifact{ + Format: "in-toto", + StorageBackend: sets.NewString("oci"), + Signer: "x509", + }, }, Signers: defaultSigners, Storage: defaultStorage, @@ -212,6 +227,7 @@ func TestParse(t *testing.T) { data: map[string]string{taskrunStorageKey: ""}, taskrunEnabled: false, ociEnbaled: true, + sbomEnabled: true, want: Config{ Builder: defaultBuilder, Artifacts: ArtifactConfigs{ @@ -230,6 +246,11 @@ func TestParse(t *testing.T) { StorageBackend: sets.NewString("oci"), Signer: "x509", }, + SBOM: Artifact{ + Format: "in-toto", + StorageBackend: sets.NewString("oci"), + Signer: "x509", + }, }, Signers: defaultSigners, Storage: defaultStorage, @@ -241,6 +262,7 @@ func TestParse(t *testing.T) { data: map[string]string{ociStorageKey: "oci,tekton"}, taskrunEnabled: true, ociEnbaled: true, + sbomEnabled: true, want: Config{ Builder: defaultBuilder, Artifacts: ArtifactConfigs{ @@ -259,6 +281,11 @@ func TestParse(t *testing.T) { StorageBackend: sets.NewString("oci", "tekton"), Signer: "x509", }, + SBOM: Artifact{ + Format: "in-toto", + StorageBackend: sets.NewString("oci"), + Signer: "x509", + }, }, Signers: defaultSigners, Storage: defaultStorage, @@ -270,6 +297,7 @@ func TestParse(t *testing.T) { data: map[string]string{ociStorageKey: ""}, taskrunEnabled: true, ociEnbaled: false, + sbomEnabled: true, want: Config{ Builder: defaultBuilder, Artifacts: ArtifactConfigs{ @@ -288,6 +316,11 @@ func TestParse(t *testing.T) { StorageBackend: sets.NewString(""), Signer: "x509", }, + SBOM: Artifact{ + Format: "in-toto", + StorageBackend: sets.NewString("oci"), + Signer: "x509", + }, }, Signers: defaultSigners, Storage: defaultStorage, @@ -302,6 +335,7 @@ func TestParse(t *testing.T) { }, taskrunEnabled: true, ociEnbaled: false, + sbomEnabled: true, want: Config{ Builder: defaultBuilder, Artifacts: ArtifactConfigs{ @@ -320,6 +354,11 @@ func TestParse(t *testing.T) { StorageBackend: sets.NewString(""), Signer: "x509", }, + SBOM: Artifact{ + Format: "in-toto", + StorageBackend: sets.NewString("oci"), + Signer: "x509", + }, }, Signers: defaultSigners, Storage: defaultStorage, @@ -334,6 +373,7 @@ func TestParse(t *testing.T) { }, taskrunEnabled: false, ociEnbaled: true, + sbomEnabled: true, want: Config{ Builder: defaultBuilder, Artifacts: ArtifactConfigs{ @@ -352,6 +392,89 @@ func TestParse(t *testing.T) { StorageBackend: sets.NewString("oci", "tekton"), Signer: "x509", }, + SBOM: Artifact{ + Format: "in-toto", + StorageBackend: sets.NewString("oci"), + Signer: "x509", + }, + }, + Signers: defaultSigners, + Storage: defaultStorage, + Transparency: defaultTransparency, + }, + }, + { + name: "sbom", + data: map[string]string{ + sbomStorageKey: "oci,tekton", + sbomFormatKey: "in-toto", + sbomSignerKey: "kms", + }, + taskrunEnabled: true, + ociEnbaled: true, + sbomEnabled: true, + want: Config{ + Builder: defaultBuilder, + Artifacts: ArtifactConfigs{ + TaskRuns: Artifact{ + Format: "in-toto", + StorageBackend: sets.NewString("tekton"), + Signer: "x509", + }, + PipelineRuns: Artifact{ + Format: "in-toto", + Signer: "x509", + StorageBackend: sets.NewString("tekton"), + }, + OCI: Artifact{ + Format: "simplesigning", + StorageBackend: sets.NewString("oci"), + Signer: "x509", + }, + SBOM: Artifact{ + Format: "in-toto", + StorageBackend: sets.NewString("oci", "tekton"), + Signer: "kms", + }, + }, + Signers: defaultSigners, + Storage: defaultStorage, + Transparency: defaultTransparency, + }, + }, + { + name: "sbom disabled", + data: map[string]string{ + sbomStorageKey: "", + sbomFormatKey: "in-toto", + sbomSignerKey: "kms", + }, + taskrunEnabled: true, + ociEnbaled: true, + sbomEnabled: false, + want: Config{ + Builder: defaultBuilder, + Artifacts: ArtifactConfigs{ + TaskRuns: Artifact{ + Format: "in-toto", + StorageBackend: sets.NewString("tekton"), + Signer: "x509", + }, + PipelineRuns: Artifact{ + Format: "in-toto", + Signer: "x509", + StorageBackend: sets.NewString("tekton"), + }, + OCI: Artifact{ + Format: "simplesigning", + StorageBackend: sets.NewString("oci"), + Signer: "x509", + }, + SBOM: Artifact{ + Format: "in-toto", + StorageBackend: sets.NewString(""), + Signer: "kms", + }, }, Signers: defaultSigners, Storage: defaultStorage, @@ -363,6 +486,7 @@ func TestParse(t *testing.T) { data: map[string]string{taskrunSignerKey: "x509"}, taskrunEnabled: true, ociEnbaled: true, + sbomEnabled: true, want: Config{ Builder: defaultBuilder, Artifacts: ArtifactConfigs{ @@ -381,6 +505,11 @@ func TestParse(t *testing.T) { StorageBackend: sets.NewString("oci"), Signer: "x509", }, + SBOM: Artifact{ + Format: "in-toto", + StorageBackend: sets.NewString("oci"), + Signer: "x509", + }, }, Signers: defaultSigners, Storage: defaultStorage, @@ -392,6 +521,7 @@ func TestParse(t *testing.T) { data: map[string]string{transparencyEnabledKey: "manual"}, taskrunEnabled: true, ociEnbaled: true, + sbomEnabled: true, want: Config{ Builder: defaultBuilder, Artifacts: defaultArtifacts, @@ -412,6 +542,7 @@ func TestParse(t *testing.T) { }, taskrunEnabled: true, ociEnbaled: true, + sbomEnabled: true, want: Config{ Builder: defaultBuilder, Artifacts: ArtifactConfigs{ @@ -430,6 +561,11 @@ func TestParse(t *testing.T) { StorageBackend: sets.NewString("oci"), Signer: "x509", }, + SBOM: Artifact{ + Format: "in-toto", + StorageBackend: sets.NewString("oci"), + Signer: "x509", + }, }, Signers: defaultSigners, Storage: defaultStorage, @@ -444,6 +580,7 @@ func TestParse(t *testing.T) { }, taskrunEnabled: true, ociEnbaled: true, + sbomEnabled: true, want: Config{ Builder: defaultBuilder, Artifacts: ArtifactConfigs{ @@ -462,6 +599,11 @@ func TestParse(t *testing.T) { StorageBackend: sets.NewString("oci"), Signer: "x509", }, + SBOM: Artifact{ + Format: "in-toto", + StorageBackend: sets.NewString("oci"), + Signer: "x509", + }, }, Signers: SignerConfigs{ X509: X509Signer{ @@ -480,6 +622,7 @@ func TestParse(t *testing.T) { }, taskrunEnabled: true, ociEnbaled: true, + sbomEnabled: true, want: Config{ Builder: defaultBuilder, Artifacts: defaultArtifacts, @@ -502,6 +645,7 @@ func TestParse(t *testing.T) { }, taskrunEnabled: true, ociEnbaled: true, + sbomEnabled: true, want: Config{ Builder: defaultBuilder, Artifacts: defaultArtifacts, @@ -532,6 +676,9 @@ func TestParse(t *testing.T) { if got.Artifacts.TaskRuns.Enabled() != tt.taskrunEnabled { t.Errorf("Taskrun artifact enable mismatch") } + if got.Artifacts.SBOM.Enabled() != tt.sbomEnabled { + t.Errorf("SBOM artifact enable mismatch") + } if diff := cmp.Diff(*got, tt.want); diff != "" { t.Errorf("parse() = %v", diff) }