diff --git a/pkg/artifacts/signable.go b/pkg/artifacts/signable.go index 5156a76f72..817d7622e6 100644 --- a/pkg/artifacts/signable.go +++ b/pkg/artifacts/signable.go @@ -149,8 +149,10 @@ 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 + SBOMFormat string } func (oa *OCIArtifact) ExtractObjects(ctx context.Context, obj objects.TektonObject) []interface{} { @@ -201,7 +203,7 @@ func (oa *OCIArtifact) ExtractObjects(ctx context.Context, obj objects.TektonObj func ExtractOCIImagesFromResults(ctx context.Context, obj objects.TektonObject) []interface{} { logger := logging.FromContext(ctx) objs := []interface{}{} - ss := extractTargetFromResults(ctx, obj, "IMAGE_URL", "IMAGE_DIGEST") + ss := extractTargetFromResults(ctx, obj, "IMAGE_URL", "IMAGE_DIGEST", "", "") for _, s := range ss { if s == nil || s.Digest == "" || s.URI == "" { continue @@ -242,7 +244,7 @@ func ExtractOCIImagesFromResults(ctx context.Context, obj objects.TektonObject) func ExtractSignableTargetFromResults(ctx context.Context, obj objects.TektonObject) []*StructuredSignable { logger := logging.FromContext(ctx) objs := []*StructuredSignable{} - ss := extractTargetFromResults(ctx, obj, "ARTIFACT_URI", "ARTIFACT_DIGEST") + ss := extractTargetFromResults(ctx, obj, "ARTIFACT_URI", "ARTIFACT_DIGEST", "", "") // Only add it if we got both the signable URI and digest. for _, s := range ss { if s == nil || s.Digest == "" || s.URI == "" { @@ -264,7 +266,7 @@ func (s *StructuredSignable) FullRef() string { return fmt.Sprintf("%s@%s", s.URI, s.Digest) } -func extractTargetFromResults(ctx context.Context, obj objects.TektonObject, identifierSuffix string, digestSuffix string) map[string]*StructuredSignable { +func extractTargetFromResults(ctx context.Context, obj objects.TektonObject, identifierSuffix, digestSuffix, sbomSuffix, sbomFormatSuffix string) map[string]*StructuredSignable { logger := logging.FromContext(ctx) ss := map[string]*StructuredSignable{} @@ -295,6 +297,31 @@ func extractTargetFromResults(ctx context.Context, obj objects.TektonObject, ide 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) + sbomURI := strings.TrimSpace(res.Value.StringVal) + if v, ok := ss[marker]; ok { + v.SBOMURI = sbomURI + } else { + ss[marker] = &StructuredSignable{SBOMURI: sbomURI} + } + } + if sbomFormatSuffix != "" && strings.HasSuffix(res.Name, sbomFormatSuffix) { + if res.Value.StringVal == "" { + logger.Debugf("error getting string value for %s", res.Name) + } + marker := strings.TrimSuffix(res.Name, sbomFormatSuffix) + sbomFormat := strings.TrimSpace(res.Value.StringVal) + if v, ok := ss[marker]; ok { + v.SBOMFormat = sbomFormat + } else { + ss[marker] = &StructuredSignable{SBOMFormat: sbomFormat} + } + } } return ss @@ -435,3 +462,119 @@ func (oa *OCIArtifact) FullKey(obj interface{}) string { func (oa *OCIArtifact) Enabled(cfg config.Config) bool { return cfg.Artifacts.OCI.Enabled() } + +type SBOMArtifact struct{} + +var _ Signable = &SBOMArtifact{} + +func (sa *SBOMArtifact) ExtractObjects(ctx context.Context, tektonObject objects.TektonObject) []interface{} { + var objs []interface{} + for _, obj := range extractSBOMFromResults(ctx, tektonObject) { + objs = append(objs, objects.NewSBOMObject(obj.SBOMURI, obj.SBOMFormat, obj.URI, obj.Digest, tektonObject)) + } + return objs +} + +func (sa *SBOMArtifact) StorageBackend(cfg config.Config) sets.Set[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(ctx context.Context, tektonObject objects.TektonObject) []*StructuredSignable { + logger := logging.FromContext(ctx) + var objs []*StructuredSignable + + ss := extractTargetFromResults(ctx, tektonObject, "IMAGE_URL", "IMAGE_DIGEST", "IMAGE_SBOM_URL", "IMAGE_SBOM_FORMAT") + for _, s := range ss { + if s == nil || s.Digest == "" || s.URI == "" || s.SBOMURI == "" || s.SBOMFormat == "" { + continue + } + if _, err := name.NewDigest(s.SBOMURI); err != nil { + logger.Errorf("error getting digest for SBOM image %s: %v", s.SBOMURI, err) + continue + } + objs = append(objs, s) + } + + var images []name.Digest + var sboms []string + var sbomsFormat string + // look for a comma separated list of images and their SBOMs + 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 + } + if _, err := name.NewDigest(trimmed); err != nil { + logger.Errorf("error getting digest for SBOM image %s: %v", trimmed, err) + continue + } + sboms = append(sboms, trimmed) + } + case "SBOMS_FORMAT": + f := strings.TrimSpace(key.Value.StringVal) + if f != "" { + sbomsFormat = f + } + } + } + + if len(images) != len(sboms) { + logger.Warnf("IMAGES and SBOMS do not contain the same amount of entries") + return objs + } + + if len(sboms) > 0 && sbomsFormat == "" { + logger.Warnf("SBOMS_FORMAT not specified") + return objs + } + + for i, sbom := range sboms { + img := images[i] + objs = append(objs, &StructuredSignable{ + URI: img.Name(), + Digest: img.Identifier(), + SBOMURI: sbom, + SBOMFormat: sbomsFormat, + }) + } + return objs +} diff --git a/pkg/artifacts/signable_test.go b/pkg/artifacts/signable_test.go index 4f78d623e4..3e2c5b43f3 100644 --- a/pkg/artifacts/signable_test.go +++ b/pkg/artifacts/signable_test.go @@ -638,6 +638,438 @@ func TestValidateResults(t *testing.T) { } } +func TestSBOMArtifact_ExtractObjects(t *testing.T) { + tests := []struct { + name string + obj objects.TektonObject + want []interface{} + }{ + { + name: "one SBOM image", + obj: objects.NewTaskRunObject(&v1beta1.TaskRun{ + TypeMeta: metav1.TypeMeta{ + Kind: "TaskRun", + }, + Status: v1beta1.TaskRunStatus{ + TaskRunStatusFields: v1beta1.TaskRunStatusFields{ + TaskRunResults: []v1beta1.TaskRunResult{ + { + Name: "IMAGE_URL", + Value: *v1beta1.NewArrayOrString("gcr.io/foo/bat:latest"), + }, + { + Name: "IMAGE_DIGEST", + Value: *v1beta1.NewArrayOrString(digest1), + }, + { + Name: "IMAGE_SBOM_URL", + Value: *v1beta1.NewArrayOrString("gcr.io/foo/bat:sbom@" + digest2), + }, + { + Name: "IMAGE_SBOM_FORMAT", + Value: *v1beta1.NewArrayOrString("https://cyclonedx.org/schema"), + }, + }, + }, + }, + }), + want: []interface{}{ + objects.NewSBOMObject( + "gcr.io/foo/bat:sbom@"+digest2, + "https://cyclonedx.org/schema", + "gcr.io/foo/bat:latest", + digest1, + nil, + ), + }, + }, + { + name: "multiple SBOM images", + obj: objects.NewTaskRunObject(&v1beta1.TaskRun{ + TypeMeta: metav1.TypeMeta{ + Kind: "TaskRun", + }, + Status: v1beta1.TaskRunStatus{ + TaskRunStatusFields: v1beta1.TaskRunStatusFields{ + TaskRunResults: []v1beta1.TaskRunResult{ + { + Name: "BAT_IMAGE_URL", + Value: *v1beta1.NewArrayOrString("gcr.io/foo/bat:latest"), + }, + { + Name: "BAT_IMAGE_DIGEST", + Value: *v1beta1.NewArrayOrString(digest1), + }, + { + Name: "BAT_IMAGE_SBOM_URL", + Value: *v1beta1.NewArrayOrString("gcr.io/foo/bat:sbom@" + digest2), + }, + { + Name: "BAT_IMAGE_SBOM_FORMAT", + Value: *v1beta1.NewArrayOrString("https://cyclonedx.org/schema"), + }, + { + Name: "BAZ_IMAGE_URL", + Value: *v1beta1.NewArrayOrString("gcr.io/foo/baz:latest"), + }, + { + Name: "BAZ_IMAGE_DIGEST", + Value: *v1beta1.NewArrayOrString(digest3), + }, + { + Name: "BAZ_IMAGE_SBOM_URL", + Value: *v1beta1.NewArrayOrString("gcr.io/foo/baz:sbom@" + digest4), + }, + { + Name: "BAZ_IMAGE_SBOM_FORMAT", + Value: *v1beta1.NewArrayOrString("https://spdx.dev/Document"), + }, + }, + }, + }, + }), + want: []interface{}{ + objects.NewSBOMObject( + "gcr.io/foo/bat:sbom@"+digest2, + "https://cyclonedx.org/schema", + "gcr.io/foo/bat:latest", + digest1, + nil, + ), + objects.NewSBOMObject( + "gcr.io/foo/baz:sbom@"+digest4, + "https://spdx.dev/Document", + "gcr.io/foo/baz:latest", + digest3, + nil, + ), + }, + }, + { + name: "multi-image result", + obj: objects.NewTaskRunObject(&v1beta1.TaskRun{ + TypeMeta: metav1.TypeMeta{ + Kind: "TaskRun", + }, + Status: v1beta1.TaskRunStatus{ + TaskRunStatusFields: v1beta1.TaskRunStatusFields{ + TaskRunResults: []v1beta1.TaskRunResult{ + { + Name: "IMAGES", + Value: *v1beta1.NewArrayOrString("gcr.io/foo/bat@" + digest1 + "\ngcr.io/foo/baz@" + digest2 + "\n\n"), + }, + { + Name: "SBOMS", + Value: *v1beta1.NewArrayOrString("gcr.io/foo/bat:sbom@" + digest3 + "\ngcr.io/foo/baz:sbom@" + digest4 + "\n\n"), + }, + { + Name: "SBOMS_FORMAT", + Value: *v1beta1.NewArrayOrString("https://cyclonedx.org/schema"), + }, + }, + }, + }, + }), + want: []interface{}{ + objects.NewSBOMObject( + "gcr.io/foo/bat:sbom@"+digest3, + "https://cyclonedx.org/schema", + "gcr.io/foo/bat@"+digest1, + digest1, + nil, + ), + objects.NewSBOMObject( + "gcr.io/foo/baz:sbom@"+digest4, + "https://cyclonedx.org/schema", + "gcr.io/foo/baz@"+digest2, + digest2, + nil, + ), + }, + }, + { + name: "missing SBOMS_FORMAT", + obj: objects.NewTaskRunObject(&v1beta1.TaskRun{ + TypeMeta: metav1.TypeMeta{ + Kind: "TaskRun", + }, + Status: v1beta1.TaskRunStatus{ + TaskRunStatusFields: v1beta1.TaskRunStatusFields{ + TaskRunResults: []v1beta1.TaskRunResult{ + { + Name: "IMAGES", + Value: *v1beta1.NewArrayOrString("gcr.io/foo/bat@" + digest1), + }, + { + Name: "SBOMS", + Value: *v1beta1.NewArrayOrString("gcr.io/foo/bat:sbom@" + digest2), + }, + }, + }, + }, + }), + want: nil, + }, + { + name: "missing IMAGES", + obj: objects.NewTaskRunObject(&v1beta1.TaskRun{ + TypeMeta: metav1.TypeMeta{ + Kind: "TaskRun", + }, + Status: v1beta1.TaskRunStatus{ + TaskRunStatusFields: v1beta1.TaskRunStatusFields{ + TaskRunResults: []v1beta1.TaskRunResult{ + { + Name: "SBOMS", + Value: *v1beta1.NewArrayOrString("gcr.io/foo/bat:sbom@" + digest3 + "\ngcr.io/foo/baz:sbom@" + digest4 + "\n\n"), + }, + { + Name: "SBOMS_FORMAT", + Value: *v1beta1.NewArrayOrString("https://cyclonedx.org/schema"), + }, + }, + }, + }, + }), + want: nil, + }, + { + name: "missing SBOMS", + obj: objects.NewTaskRunObject(&v1beta1.TaskRun{ + TypeMeta: metav1.TypeMeta{ + Kind: "TaskRun", + }, + Status: v1beta1.TaskRunStatus{ + TaskRunStatusFields: v1beta1.TaskRunStatusFields{ + TaskRunResults: []v1beta1.TaskRunResult{ + { + Name: "IMAGES", + Value: *v1beta1.NewArrayOrString("gcr.io/foo/bat@" + digest1 + "\ngcr.io/foo/baz@" + digest2 + "\n\n"), + }, + { + Name: "SBOMS_FORMAT", + Value: *v1beta1.NewArrayOrString("https://cyclonedx.org/schema"), + }, + }, + }, + }, + }), + want: nil, + }, + { + name: "missing IMAGE_SBOM_FORMAT", + obj: objects.NewTaskRunObject(&v1beta1.TaskRun{ + TypeMeta: metav1.TypeMeta{ + Kind: "TaskRun", + }, + Status: v1beta1.TaskRunStatus{ + TaskRunStatusFields: v1beta1.TaskRunStatusFields{ + TaskRunResults: []v1beta1.TaskRunResult{{ + Name: "IMAGE_URL", + Value: *v1beta1.NewArrayOrString("gcr.io/foo/bat:latest"), + }, + { + Name: "IMAGE_DIGEST", + Value: *v1beta1.NewArrayOrString(digest1), + }, + { + Name: "IMAGE_SBOM_URL", + Value: *v1beta1.NewArrayOrString("gcr.io/foo/bat:sbom@" + digest2), + }, + }, + }, + }, + }), + want: nil, + }, + { + name: "missing IMAGE_URL", + obj: objects.NewTaskRunObject(&v1beta1.TaskRun{ + TypeMeta: metav1.TypeMeta{ + Kind: "TaskRun", + }, + Status: v1beta1.TaskRunStatus{ + TaskRunStatusFields: v1beta1.TaskRunStatusFields{ + TaskRunResults: []v1beta1.TaskRunResult{ + { + Name: "IMAGE_DIGEST", + Value: *v1beta1.NewArrayOrString(digest1), + }, + { + Name: "IMAGE_SBOM_URL", + Value: *v1beta1.NewArrayOrString("gcr.io/foo/bat:sbom@" + digest2), + }, + { + Name: "IMAGE_SBOM_FORMAT", + Value: *v1beta1.NewArrayOrString("https://cyclonedx.org/schema"), + }, + }, + }, + }, + }), + want: nil, + }, + { + name: "missing IMAGE_DIGEST", + obj: objects.NewTaskRunObject(&v1beta1.TaskRun{ + TypeMeta: metav1.TypeMeta{ + Kind: "TaskRun", + }, + Status: v1beta1.TaskRunStatus{ + TaskRunStatusFields: v1beta1.TaskRunStatusFields{ + TaskRunResults: []v1beta1.TaskRunResult{ + { + Name: "IMAGE_URL", + Value: *v1beta1.NewArrayOrString("gcr.io/foo/bat:latest"), + }, + { + Name: "IMAGE_SBOM_URL", + Value: *v1beta1.NewArrayOrString("gcr.io/foo/bat:sbom@" + digest2), + }, + { + Name: "IMAGE_SBOM_FORMAT", + Value: *v1beta1.NewArrayOrString("https://cyclonedx.org/schema"), + }, + }, + }, + }, + }), + want: nil, + }, + { + name: "missing IMAGE_SBOM_URL", + obj: objects.NewTaskRunObject(&v1beta1.TaskRun{ + TypeMeta: metav1.TypeMeta{ + Kind: "TaskRun", + }, + Status: v1beta1.TaskRunStatus{ + TaskRunStatusFields: v1beta1.TaskRunStatusFields{ + TaskRunResults: []v1beta1.TaskRunResult{ + { + Name: "IMAGE_URL", + Value: *v1beta1.NewArrayOrString("gcr.io/foo/bat:latest"), + }, + { + Name: "IMAGE_DIGEST", + Value: *v1beta1.NewArrayOrString(digest1), + }, + { + Name: "IMAGE_SBOM_FORMAT", + Value: *v1beta1.NewArrayOrString("https://cyclonedx.org/schema"), + }, + }, + }, + }, + }), + want: nil, + }, + { + name: "count mismatch", + obj: objects.NewTaskRunObject(&v1beta1.TaskRun{ + TypeMeta: metav1.TypeMeta{ + Kind: "TaskRun", + }, + Status: v1beta1.TaskRunStatus{ + TaskRunStatusFields: v1beta1.TaskRunStatusFields{ + TaskRunResults: []v1beta1.TaskRunResult{ + { + Name: "IMAGES", + Value: *v1beta1.NewArrayOrString("gcr.io/foo/bat@" + digest1 + "\ngcr.io/foo/baz@" + digest2 + "\n\n"), + }, + { + Name: "SBOMS", + Value: *v1beta1.NewArrayOrString("gcr.io/foo/bat:sbom@" + digest3), + }, + { + Name: "SBOMS_FORMAT", + Value: *v1beta1.NewArrayOrString("https://cyclonedx.org/schema"), + }, + }, + }, + }, + }), + want: nil, + }, + { + name: "non-pinned SBOMs ignored", + obj: objects.NewTaskRunObject(&v1beta1.TaskRun{ + TypeMeta: metav1.TypeMeta{ + Kind: "TaskRun", + }, + Status: v1beta1.TaskRunStatus{ + TaskRunStatusFields: v1beta1.TaskRunStatusFields{ + TaskRunResults: []v1beta1.TaskRunResult{ + { + Name: "IMAGES", + Value: *v1beta1.NewArrayOrString("gcr.io/foo/bat@" + digest1 + "\ngcr.io/foo/baz@" + digest2 + "\n\n"), + }, + { + Name: "SBOMS", + Value: *v1beta1.NewArrayOrString("gcr.io/foo/bat:sbom"), + }, + { + Name: "SBOMS_FORMAT", + Value: *v1beta1.NewArrayOrString("https://cyclonedx.org/schema"), + }, + }, + }, + }, + }), + want: nil, + }, + { + name: "non-pinned IMAGE_SBOM_URL ignored", + obj: objects.NewTaskRunObject(&v1beta1.TaskRun{ + TypeMeta: metav1.TypeMeta{ + Kind: "TaskRun", + }, + Status: v1beta1.TaskRunStatus{ + TaskRunStatusFields: v1beta1.TaskRunStatusFields{ + TaskRunResults: []v1beta1.TaskRunResult{ + { + Name: "IMAGE_URL", + Value: *v1beta1.NewArrayOrString("gcr.io/foo/bat:latest"), + }, + { + Name: "IMAGE_DIGEST", + Value: *v1beta1.NewArrayOrString(digest1), + }, + { + Name: "IMAGE_SBOM_URL", + Value: *v1beta1.NewArrayOrString("gcr.io/foo/bat:sbom"), + }, + { + Name: "IMAGE_SBOM_FORMAT", + Value: *v1beta1.NewArrayOrString("https://cyclonedx.org/schema"), + }, + }, + }, + }, + }), + want: nil, + }, + } + + // Convert an SBOMObject into a string so it can be easily compared and a diff'ed. + transformer := cmp.Transformer("stringify", func(sbomObject *objects.SBOMObject) string { + return fmt.Sprintf( + "ImageDigest=%q\nImageURL=%q\nSBOMFormat=%q\nSBOMURL=%q", + sbomObject.GetImageDigest(), sbomObject.GetImageURL(), sbomObject.GetSBOMFormat(), + sbomObject.GetSBOMURL()) + }) + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctx := logtesting.TestContextWithLogger(t) + sbomArtifact := &SBOMArtifact{} + got := sbomArtifact.ExtractObjects(ctx, tt.obj) + if !cmp.Equal(got, tt.want, transformer) { + t.Errorf("SBOMArtifact.ExtractObjects() = %s", cmp.Diff(got, tt.want, transformer)) + } + }) + } +} + func createDigest(t *testing.T, dgst string) name.Digest { result, err := name.NewDigest(dgst) if err != nil { diff --git a/pkg/chains/formats/format.go b/pkg/chains/formats/format.go index 690baa7b2e..2585b1d869 100644 --- a/pkg/chains/formats/format.go +++ b/pkg/chains/formats/format.go @@ -18,11 +18,12 @@ import ( "fmt" "github.com/tektoncd/chains/pkg/config" + "k8s.io/client-go/kubernetes" ) // Payloader is an interface to generate a chains Payload from a TaskRun type Payloader interface { - CreatePayload(ctx context.Context, obj interface{}) (interface{}, error) + CreatePayload(ctx context.Context, kc kubernetes.Interface, obj interface{}) (interface{}, error) Type() config.PayloadType Wrap() bool } diff --git a/pkg/chains/formats/sbom/sbom.go b/pkg/chains/formats/sbom/sbom.go new file mode 100644 index 0000000000..f9b0c583b0 --- /dev/null +++ b/pkg/chains/formats/sbom/sbom.go @@ -0,0 +1,106 @@ +/* +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" + "context" + "encoding/json" + "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" + "k8s.io/client-go/kubernetes" +) + +const maxSBOMBytes = 5 * 1024 * 1024 // 5 Megabytes + +func GenerateAttestation(ctx context.Context, kc kubernetes.Interface, builderID string, sbom *objects.SBOMObject) (interface{}, error) { + subject := []intoto.Subject{ + {Name: sbom.GetImageURL(), Digest: toDigestSet(sbom.GetImageDigest())}, + } + + data, err := getData(ctx, kc, sbom) + if err != nil { + return nil, err + } + + att := intoto.Statement{ + StatementHeader: intoto.StatementHeader{ + Type: intoto.StatementInTotoV01, + PredicateType: sbom.GetSBOMFormat(), + Subject: subject, + }, + Predicate: data, + } + return att, nil +} + +func getData(ctx context.Context, kc kubernetes.Interface, sbom *objects.SBOMObject) (json.RawMessage, error) { + opt, err := sbom.OCIRemoteOption(ctx, kc) + if err != nil { + return nil, err + } + + uri := sbom.GetSBOMURL() + ref, err := name.ParseReference(uri) + if err != nil { + return nil, err + } + + image, err := remote.Image(ref, opt) + if err != nil { + return nil, err + } + + layers, err := image.Layers() + if err != nil { + return nil, err + } + + if len(layers) != 1 { + return nil, fmt.Errorf("expected exactly 1 layer in sbom image %s, found %d", uri, len(layers)) + } + + layer, err := layers[0].Uncompressed() + if err != nil { + return nil, err + } + defer layer.Close() + + var blob bytes.Buffer + if _, err := io.Copy(&blob, io.LimitReader(layer, maxSBOMBytes)); err != nil { + return nil, err + } + + var data json.RawMessage + if err := json.Unmarshal(blob.Bytes(), &data); err != nil { + return nil, err + } + return data, 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/sbom/sbom_test.go b/pkg/chains/formats/sbom/sbom_test.go new file mode 100644 index 0000000000..5abbd750f7 --- /dev/null +++ b/pkg/chains/formats/sbom/sbom_test.go @@ -0,0 +1,167 @@ +/* +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 ( + "encoding/json" + "fmt" + "net/http/httptest" + "net/url" + "strings" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-containerregistry/pkg/name" + "github.com/google/go-containerregistry/pkg/registry" + "github.com/google/go-containerregistry/pkg/v1/remote" + "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/sigstore/cosign/v2/pkg/oci/static" + "github.com/tektoncd/chains/pkg/chains/objects" + "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1" + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + fakekubeclient "knative.dev/pkg/client/injection/kube/client/fake" + rtesting "knative.dev/pkg/reconciler/testing" +) + +func TestGenerateAttestation(t *testing.T) { + namespace := "test-namespace" + serviceaccount := "test-serviceaccount" + tektonObject := objects.NewTaskRunObject(&v1beta1.TaskRun{ + ObjectMeta: metav1.ObjectMeta{Namespace: namespace}, + Spec: v1beta1.TaskRunSpec{ServiceAccountName: serviceaccount}, + }) + + cases := []struct { + name string + sbomURL string + sbomFormat string + imageURL string + imageDigest string + sbom string + want in_toto.Statement + }{ + { + name: "simple", + sbomURL: testSBOMURL, + sbomFormat: testSBOMFormat, + imageURL: testImageURL, + imageDigest: testImageDigest, + sbom: `{"foo": "bar"}`, + want: in_toto.Statement{ + StatementHeader: in_toto.StatementHeader{ + Type: in_toto.StatementInTotoV01, + PredicateType: testSBOMFormat, + Subject: []in_toto.Subject{ + { + Name: testImageURL, + Digest: slsa.DigestSet{"sha256": testImageDigestNoAlgo}, + }, + }, + }, + Predicate: json.RawMessage(`{"foo": "bar"}`), + }, + }, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + ctx, _ := rtesting.SetupFakeContext(t) + + // Setup kubernetes resources + kc := fakekubeclient.Get(ctx) + if _, err := kc.CoreV1().ServiceAccounts(namespace).Create(ctx, &v1.ServiceAccount{ + ObjectMeta: metav1.ObjectMeta{Name: serviceaccount, Namespace: namespace}, + }, metav1.CreateOptions{}); err != nil { + t.Fatal(err) + } + + // Setup registry + registry := httptest.NewServer(registry.New()) + defer registry.Close() + registryURL, err := url.Parse(registry.URL) + if err != nil { + t.Fatal(err) + } + + // Populate image references with registry host + sbomURL := fmt.Sprintf(c.sbomURL, registryURL.Host) + imageURL := fmt.Sprintf(c.imageURL, registryURL.Host) + + // Create SBOM image. + pinnedSBOM, err := pushSBOMImage(sbomURL, c.sbom) + if err != nil { + t.Fatal(err) + } + sbomURL = pinnedSBOM.String() + + // Setup SBOM Object + sbomObject := objects.NewSBOMObject( + sbomURL, + c.sbomFormat, + imageURL, + c.imageDigest, tektonObject) + + got, err := GenerateAttestation(ctx, kc, testBuilderID, sbomObject) + if err != nil { + t.Fatal(err) + } + + transformer := cmp.Transformer("registry", func(subject in_toto.Subject) in_toto.Subject { + if strings.Contains(subject.Name, "%s") { + subject.Name = fmt.Sprintf(subject.Name, registryURL.Host) + } + return subject + }) + if !cmp.Equal(got, c.want, transformer) { + t.Errorf("GenerateAttestation() = %s", cmp.Diff(got, c.want, transformer)) + } + }) + } + +} + +const ( + testSBOMURL = "%s/foo/bat:sbom" + testSBOMFormat = "https://cyclonedx.org/schema" + testSBOMMediaType = "application/vnd.cyclonedx+json" + testImageURL = "%s/foo/bat:latest" + testImageDigestNoAlgo = "05f95b26ed10668b7183c1e2da98610e91372fa9f510046d4ce5812addad86b6" + testImageDigest = "sha256:" + testImageDigestNoAlgo + testBuilderID = "test-builder-id" +) + +func pushSBOMImage(ref string, data string) (name.Digest, error) { + imgRef, err := name.ParseReference(ref) + if err != nil { + return name.Digest{}, err + } + img, err := static.NewFile([]byte(data), static.WithLayerMediaType(testSBOMMediaType)) + if err != nil { + return name.Digest{}, err + } + if err := remote.Write(imgRef, img); err != nil { + return name.Digest{}, err + } + + // Figure out the digest of the image just pushed + descriptor, err := remote.Head(imgRef) + if err != nil { + return name.Digest{}, err + } + digest := descriptor.Digest.String() + + return imgRef.Context().Digest(digest), nil +} diff --git a/pkg/chains/formats/simple/simple.go b/pkg/chains/formats/simple/simple.go index 10c464f96a..43f46f53fd 100644 --- a/pkg/chains/formats/simple/simple.go +++ b/pkg/chains/formats/simple/simple.go @@ -20,6 +20,7 @@ import ( "github.com/sigstore/sigstore/pkg/signature/payload" "github.com/tektoncd/chains/pkg/chains/formats" "github.com/tektoncd/chains/pkg/config" + "k8s.io/client-go/kubernetes" "github.com/google/go-containerregistry/pkg/name" ) @@ -39,7 +40,7 @@ type SimpleSigning struct{} type SimpleContainerImage payload.SimpleContainerImage // CreatePayload implements the Payloader interface. -func (i *SimpleSigning) CreatePayload(ctx context.Context, obj interface{}) (interface{}, error) { +func (i *SimpleSigning) CreatePayload(ctx context.Context, _ kubernetes.Interface, obj interface{}) (interface{}, error) { switch v := obj.(type) { case name.Digest: format := NewSimpleStruct(v) diff --git a/pkg/chains/formats/simple/simple_test.go b/pkg/chains/formats/simple/simple_test.go index a3be8461a0..fc091210f7 100644 --- a/pkg/chains/formats/simple/simple_test.go +++ b/pkg/chains/formats/simple/simple_test.go @@ -62,7 +62,7 @@ func TestSimpleSigning_CreatePayload(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { i := &SimpleSigning{} - got, err := i.CreatePayload(context.Background(), tt.obj) + got, err := i.CreatePayload(context.Background(), nil, tt.obj) if (err != nil) != tt.wantErr { t.Errorf("SimpleSigning.CreatePayload() error = %v, wantErr %v", err, tt.wantErr) return @@ -82,7 +82,7 @@ func TestImageName(t *testing.T) { obj := makeDigest(t, img) i := &SimpleSigning{} - format, err := i.CreatePayload(context.Background(), obj) + format, err := i.CreatePayload(context.Background(), nil, obj) if err != nil { t.Fatal(err) } diff --git a/pkg/chains/formats/slsa/v1/intotoite6.go b/pkg/chains/formats/slsa/v1/intotoite6.go index e6c7a1fe64..785ea3cba8 100644 --- a/pkg/chains/formats/slsa/v1/intotoite6.go +++ b/pkg/chains/formats/slsa/v1/intotoite6.go @@ -21,10 +21,12 @@ 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" "github.com/tektoncd/chains/pkg/config" + "k8s.io/client-go/kubernetes" ) const ( @@ -51,12 +53,16 @@ func (i *InTotoIte6) Wrap() bool { return true } -func (i *InTotoIte6) CreatePayload(ctx context.Context, obj interface{}) (interface{}, error) { +func (i *InTotoIte6) CreatePayload(ctx context.Context, kc kubernetes.Interface, obj interface{}) (interface{}, error) { switch v := obj.(type) { case *objects.TaskRunObject: return taskrun.GenerateAttestation(ctx, i.builderID, v) case *objects.PipelineRunObject: return pipelinerun.GenerateAttestation(ctx, i.builderID, v) + 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(ctx, kc, i.builderID, v) default: return nil, fmt.Errorf("intoto does not support type: %s", v) } diff --git a/pkg/chains/formats/slsa/v1/intotoite6_test.go b/pkg/chains/formats/slsa/v1/intotoite6_test.go index f9281488b6..014fe5fc1e 100644 --- a/pkg/chains/formats/slsa/v1/intotoite6_test.go +++ b/pkg/chains/formats/slsa/v1/intotoite6_test.go @@ -133,7 +133,7 @@ func TestTaskRunCreatePayload1(t *testing.T) { } i, _ := NewFormatter(cfg) - got, err := i.CreatePayload(ctx, objects.NewTaskRunObject(tr)) + got, err := i.CreatePayload(ctx, nil, objects.NewTaskRunObject(tr)) if err != nil { t.Errorf("unexpected error: %s", err.Error()) @@ -359,7 +359,7 @@ func TestPipelineRunCreatePayload(t *testing.T) { i, _ := NewFormatter(cfg) - got, err := i.CreatePayload(ctx, pro) + got, err := i.CreatePayload(ctx, nil, pro) if err != nil { t.Errorf("unexpected error: %s", err.Error()) } @@ -576,7 +576,7 @@ func TestPipelineRunCreatePayloadChildRefs(t *testing.T) { pro.AppendTaskRun(tr2) i, _ := NewFormatter(cfg) - got, err := i.CreatePayload(ctx, pro) + got, err := i.CreatePayload(ctx, nil, pro) if err != nil { t.Errorf("unexpected error: %s", err.Error()) } @@ -651,7 +651,7 @@ func TestTaskRunCreatePayload2(t *testing.T) { }, } i, _ := NewFormatter(cfg) - got, err := i.CreatePayload(ctx, objects.NewTaskRunObject(tr)) + got, err := i.CreatePayload(ctx, nil, objects.NewTaskRunObject(tr)) if err != nil { t.Errorf("unexpected error: %s", err.Error()) @@ -722,7 +722,7 @@ func TestMultipleSubjects(t *testing.T) { } i, _ := NewFormatter(cfg) - got, err := i.CreatePayload(ctx, objects.NewTaskRunObject(tr)) + got, err := i.CreatePayload(ctx, nil, objects.NewTaskRunObject(tr)) if err != nil { t.Errorf("unexpected error: %s", err.Error()) } @@ -759,7 +759,7 @@ func TestCreatePayloadError(t *testing.T) { f, _ := NewFormatter(cfg) t.Run("Invalid type", func(t *testing.T) { - p, err := f.CreatePayload(ctx, "not a task ref") + p, err := f.CreatePayload(ctx, nil, "not a task ref") if p != nil { t.Errorf("Unexpected payload") diff --git a/pkg/chains/formats/slsa/v2/slsav2.go b/pkg/chains/formats/slsa/v2/slsav2.go index 3242ca3b24..6428b25b3a 100644 --- a/pkg/chains/formats/slsa/v2/slsav2.go +++ b/pkg/chains/formats/slsa/v2/slsav2.go @@ -24,6 +24,7 @@ import ( "github.com/tektoncd/chains/pkg/chains/formats/slsa/v2/taskrun" "github.com/tektoncd/chains/pkg/chains/objects" "github.com/tektoncd/chains/pkg/config" + "k8s.io/client-go/kubernetes" ) const ( @@ -48,7 +49,7 @@ func (s *Slsa) Wrap() bool { return true } -func (s *Slsa) CreatePayload(ctx context.Context, obj interface{}) (interface{}, error) { +func (s *Slsa) CreatePayload(ctx context.Context, kc kubernetes.Interface, obj interface{}) (interface{}, error) { switch v := obj.(type) { case *objects.TaskRunObject: return taskrun.GenerateAttestation(ctx, s.builderID, s.Type(), v) diff --git a/pkg/chains/formats/slsa/v2/slsav2_test.go b/pkg/chains/formats/slsa/v2/slsav2_test.go index 8986ceef49..5c2eed9c2d 100644 --- a/pkg/chains/formats/slsa/v2/slsav2_test.go +++ b/pkg/chains/formats/slsa/v2/slsav2_test.go @@ -161,7 +161,7 @@ func TestTaskRunCreatePayload1(t *testing.T) { } i, _ := NewFormatter(cfg) - got, err := i.CreatePayload(ctx, objects.NewTaskRunObject(tr)) + got, err := i.CreatePayload(ctx, nil, objects.NewTaskRunObject(tr)) if err != nil { t.Errorf("unexpected error: %s", err.Error()) @@ -262,7 +262,7 @@ func TestTaskRunCreatePayload2(t *testing.T) { }, } i, _ := NewFormatter(cfg) - got, err := i.CreatePayload(ctx, objects.NewTaskRunObject(tr)) + got, err := i.CreatePayload(ctx, nil, objects.NewTaskRunObject(tr)) if err != nil { t.Errorf("unexpected error: %s", err.Error()) @@ -360,7 +360,7 @@ func TestMultipleSubjects(t *testing.T) { } i, _ := NewFormatter(cfg) - got, err := i.CreatePayload(ctx, objects.NewTaskRunObject(tr)) + got, err := i.CreatePayload(ctx, nil, objects.NewTaskRunObject(tr)) if err != nil { t.Errorf("unexpected error: %s", err.Error()) } @@ -397,7 +397,7 @@ func TestCreatePayloadError(t *testing.T) { f, _ := NewFormatter(cfg) t.Run("Invalid type", func(t *testing.T) { - p, err := f.CreatePayload(ctx, "not a task ref") + p, err := f.CreatePayload(ctx, nil, "not a task ref") if p != nil { t.Errorf("Unexpected payload") diff --git a/pkg/chains/objects/objects.go b/pkg/chains/objects/objects.go index bb8a73b4f8..e07c4d49c3 100644 --- a/pkg/chains/objects/objects.go +++ b/pkg/chains/objects/objects.go @@ -18,12 +18,15 @@ import ( "errors" "fmt" + "github.com/google/go-containerregistry/pkg/authn/k8schain" + "github.com/google/go-containerregistry/pkg/v1/remote" "github.com/tektoncd/pipeline/pkg/apis/pipeline/pod" "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1" "github.com/tektoncd/pipeline/pkg/client/clientset/versioned" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/kubernetes" "knative.dev/pkg/apis" ) @@ -223,3 +226,50 @@ func getPodPullSecrets(podTemplate *pod.Template) []string { } return imgPullSecrets } + +type SBOMObject struct { + sbomURL string + sbomFormat string + imageURL string + imageDigest string + tektonObject TektonObject +} + +func NewSBOMObject(sbomURL, sbomFormat, imageURL, imageDigest string, tektonObject TektonObject) *SBOMObject { + return &SBOMObject{ + sbomURL: sbomURL, + sbomFormat: sbomFormat, + imageURL: imageURL, + imageDigest: imageDigest, + tektonObject: tektonObject, + } +} + +func (so *SBOMObject) GetSBOMURL() string { + return so.sbomURL +} + +func (so *SBOMObject) GetSBOMFormat() string { + return so.sbomFormat +} + +func (so *SBOMObject) GetImageURL() string { + return so.imageURL +} + +func (so *SBOMObject) GetImageDigest() string { + return so.imageDigest +} + +func (so *SBOMObject) OCIRemoteOption(ctx context.Context, client kubernetes.Interface) (remote.Option, error) { + keychain, err := k8schain.New(ctx, client, + k8schain.Options{ + Namespace: so.tektonObject.GetNamespace(), + ServiceAccountName: so.tektonObject.GetServiceAccountName(), + ImagePullSecrets: so.tektonObject.GetPullSecrets(), + }) + if err != nil { + return nil, err + } + return remote.WithAuthFromKeychain(keychain), nil +} diff --git a/pkg/chains/objects/objects_test.go b/pkg/chains/objects/objects_test.go index e71f7cb92a..cd5a139649 100644 --- a/pkg/chains/objects/objects_test.go +++ b/pkg/chains/objects/objects_test.go @@ -20,7 +20,10 @@ import ( "github.com/tektoncd/pipeline/pkg/apis/pipeline/pod" "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1" corev1 "k8s.io/api/core/v1" + v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + fakekubeclient "knative.dev/pkg/client/injection/kube/client/fake" + rtesting "knative.dev/pkg/reconciler/testing" ) func getPullSecretTemplate(pullSecret string) *pod.PodTemplate { @@ -196,3 +199,33 @@ func TestTaskRun_GetResults(t *testing.T) { }) } + +func TestSBOMObject(t *testing.T) { + namespace := "test-namespace" + serviceaccount := "test-serviceaccount" + + taskRun := getTaskRun() + taskRun.ObjectMeta.Namespace = namespace + taskRun.Spec.ServiceAccountName = serviceaccount + taskRunObject := NewTaskRunObject(taskRun) + + sbomObject := NewSBOMObject("sbomURL", "sbomFormat", "imageURL", "imageDigest", taskRunObject) + + assert.Equal(t, "sbomURL", sbomObject.GetSBOMURL()) + assert.Equal(t, "sbomFormat", sbomObject.GetSBOMFormat()) + assert.Equal(t, "imageURL", sbomObject.GetImageURL()) + assert.Equal(t, "imageDigest", sbomObject.GetImageDigest()) + + ctx, _ := rtesting.SetupFakeContext(t) + kc := fakekubeclient.Get(ctx) + if _, err := kc.CoreV1().ServiceAccounts(namespace).Create(ctx, &v1.ServiceAccount{ + ObjectMeta: metav1.ObjectMeta{Name: serviceaccount, Namespace: namespace}, + }, metav1.CreateOptions{}); err != nil { + t.Fatal(err) + } + + got, err := sbomObject.OCIRemoteOption(ctx, kc) + assert.NoError(t, err) + // TODO: Not sure how to compare the returned remote.Option + assert.NotNil(t, got) +} diff --git a/pkg/chains/signing.go b/pkg/chains/signing.go index 2e3b1a6be6..90051ba269 100644 --- a/pkg/chains/signing.go +++ b/pkg/chains/signing.go @@ -31,6 +31,7 @@ import ( "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1" versioned "github.com/tektoncd/pipeline/pkg/client/clientset/versioned" "k8s.io/apimachinery/pkg/util/sets" + "k8s.io/client-go/kubernetes" "knative.dev/pkg/logging" ) @@ -45,6 +46,7 @@ type ObjectSigner struct { Backends map[string]storage.Backend SecretPath string Pipelineclientset versioned.Interface + KubeClient kubernetes.Interface } func allSigners(ctx context.Context, sp string, cfg config.Config) map[string]signing.Signer { @@ -90,6 +92,7 @@ func getSignableTypes(ctx context.Context, obj objects.TektonObject) ([]artifact return []artifacts.Signable{ &artifacts.TaskRunArtifact{}, &artifacts.OCIArtifact{}, + &artifacts.SBOMArtifact{}, }, nil case *v1beta1.PipelineRun: return []artifacts.Signable{ @@ -133,8 +136,7 @@ func (o *ObjectSigner) Sign(ctx context.Context, tektonObj objects.TektonObject) // Go through each object one at a time. for _, obj := range objects { - - payload, err := payloader.CreatePayload(ctx, obj) + payload, err := payloader.CreatePayload(ctx, o.KubeClient, obj) if err != nil { logger.Error(err) continue diff --git a/pkg/config/config.go b/pkg/config/config.go index 9199cd8647..aff82d3313 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -40,6 +40,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 @@ -158,6 +159,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" @@ -223,6 +228,11 @@ func defaultConfig() *Config { StorageBackend: sets.New[string]("oci"), Signer: "x509", }, + SBOM: Artifact{ + Format: "in-toto", + StorageBackend: sets.New[string]("oci"), + Signer: "x509", + }, }, Transparency: TransparencyConfig{ URL: "https://rekor.sigstore.dev", @@ -266,6 +276,11 @@ func NewConfigFromMap(data map[string]string) (*Config, error) { asStringSet(ociStorageKey, &cfg.Artifacts.OCI.StorageBackend, sets.New[string]("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.New[string]("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 92cf94521a..293e38f717 100644 --- a/pkg/config/store_test.go +++ b/pkg/config/store_test.go @@ -112,6 +112,11 @@ var defaultArtifacts = ArtifactConfigs{ StorageBackend: sets.New[string]("oci"), Signer: "x509", }, + SBOM: Artifact{ + Format: "in-toto", + StorageBackend: sets.New[string]("oci"), + Signer: "x509", + }, } var defaultStorage = StorageConfigs{ @@ -129,6 +134,7 @@ func TestParse(t *testing.T) { name string data map[string]string taskrunEnabled bool + sbomEnabled bool ociEnbaled bool want Config }{ @@ -137,6 +143,7 @@ func TestParse(t *testing.T) { data: map[string]string{}, taskrunEnabled: true, ociEnbaled: true, + sbomEnabled: true, want: Config{ Builder: defaultBuilder, Artifacts: defaultArtifacts, @@ -151,6 +158,7 @@ func TestParse(t *testing.T) { }, taskrunEnabled: true, ociEnbaled: true, + sbomEnabled: true, want: Config{ Builder: BuilderConfig{ "builder-id-test", @@ -167,6 +175,7 @@ func TestParse(t *testing.T) { }, taskrunEnabled: true, ociEnbaled: true, + sbomEnabled: true, want: Config{ Builder: defaultBuilder, Artifacts: defaultArtifacts, @@ -184,6 +193,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{ @@ -202,6 +212,11 @@ func TestParse(t *testing.T) { StorageBackend: sets.New[string]("oci"), Signer: "x509", }, + SBOM: Artifact{ + Format: "in-toto", + StorageBackend: sets.New[string]("oci"), + Signer: "x509", + }, }, Signers: defaultSigners, Storage: defaultStorage, @@ -213,6 +228,7 @@ func TestParse(t *testing.T) { data: map[string]string{taskrunStorageKey: ""}, taskrunEnabled: false, ociEnbaled: true, + sbomEnabled: true, want: Config{ Builder: defaultBuilder, Artifacts: ArtifactConfigs{ @@ -231,6 +247,11 @@ func TestParse(t *testing.T) { StorageBackend: sets.New[string]("oci"), Signer: "x509", }, + SBOM: Artifact{ + Format: "in-toto", + StorageBackend: sets.New[string]("oci"), + Signer: "x509", + }, }, Signers: defaultSigners, Storage: defaultStorage, @@ -242,6 +263,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{ @@ -260,6 +282,11 @@ func TestParse(t *testing.T) { StorageBackend: sets.New[string]("oci", "tekton"), Signer: "x509", }, + SBOM: Artifact{ + Format: "in-toto", + StorageBackend: sets.New[string]("oci"), + Signer: "x509", + }, }, Signers: defaultSigners, Storage: defaultStorage, @@ -271,6 +298,7 @@ func TestParse(t *testing.T) { data: map[string]string{ociStorageKey: ""}, taskrunEnabled: true, ociEnbaled: false, + sbomEnabled: true, want: Config{ Builder: defaultBuilder, Artifacts: ArtifactConfigs{ @@ -289,6 +317,11 @@ func TestParse(t *testing.T) { StorageBackend: sets.New[string](""), Signer: "x509", }, + SBOM: Artifact{ + Format: "in-toto", + StorageBackend: sets.New[string]("oci"), + Signer: "x509", + }, }, Signers: defaultSigners, Storage: defaultStorage, @@ -303,6 +336,7 @@ func TestParse(t *testing.T) { }, taskrunEnabled: true, ociEnbaled: false, + sbomEnabled: true, want: Config{ Builder: defaultBuilder, Artifacts: ArtifactConfigs{ @@ -321,6 +355,11 @@ func TestParse(t *testing.T) { StorageBackend: sets.New[string](""), Signer: "x509", }, + SBOM: Artifact{ + Format: "in-toto", + StorageBackend: sets.New[string]("oci"), + Signer: "x509", + }, }, Signers: defaultSigners, Storage: defaultStorage, @@ -335,6 +374,7 @@ func TestParse(t *testing.T) { }, taskrunEnabled: false, ociEnbaled: true, + sbomEnabled: true, want: Config{ Builder: defaultBuilder, Artifacts: ArtifactConfigs{ @@ -353,6 +393,89 @@ func TestParse(t *testing.T) { StorageBackend: sets.New[string]("oci", "tekton"), Signer: "x509", }, + SBOM: Artifact{ + Format: "in-toto", + StorageBackend: sets.New[string]("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.New[string]("tekton"), + Signer: "x509", + }, + PipelineRuns: Artifact{ + Format: "in-toto", + Signer: "x509", + StorageBackend: sets.New[string]("tekton"), + }, + OCI: Artifact{ + Format: "simplesigning", + StorageBackend: sets.New[string]("oci"), + Signer: "x509", + }, + SBOM: Artifact{ + Format: "in-toto", + StorageBackend: sets.New[string]("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.New[string]("tekton"), + Signer: "x509", + }, + PipelineRuns: Artifact{ + Format: "in-toto", + Signer: "x509", + StorageBackend: sets.New[string]("tekton"), + }, + OCI: Artifact{ + Format: "simplesigning", + StorageBackend: sets.New[string]("oci"), + Signer: "x509", + }, + SBOM: Artifact{ + Format: "in-toto", + StorageBackend: sets.New[string](""), + Signer: "kms", + }, }, Signers: defaultSigners, Storage: defaultStorage, @@ -364,6 +487,7 @@ func TestParse(t *testing.T) { data: map[string]string{taskrunSignerKey: "x509"}, taskrunEnabled: true, ociEnbaled: true, + sbomEnabled: true, want: Config{ Builder: defaultBuilder, Artifacts: ArtifactConfigs{ @@ -382,6 +506,11 @@ func TestParse(t *testing.T) { StorageBackend: sets.New[string]("oci"), Signer: "x509", }, + SBOM: Artifact{ + Format: "in-toto", + StorageBackend: sets.New[string]("oci"), + Signer: "x509", + }, }, Signers: defaultSigners, Storage: defaultStorage, @@ -393,6 +522,7 @@ func TestParse(t *testing.T) { data: map[string]string{transparencyEnabledKey: "manual"}, taskrunEnabled: true, ociEnbaled: true, + sbomEnabled: true, want: Config{ Builder: defaultBuilder, Artifacts: defaultArtifacts, @@ -413,6 +543,7 @@ func TestParse(t *testing.T) { }, taskrunEnabled: true, ociEnbaled: true, + sbomEnabled: true, want: Config{ Builder: defaultBuilder, Artifacts: ArtifactConfigs{ @@ -431,6 +562,11 @@ func TestParse(t *testing.T) { StorageBackend: sets.New[string]("oci"), Signer: "x509", }, + SBOM: Artifact{ + Format: "in-toto", + StorageBackend: sets.New[string]("oci"), + Signer: "x509", + }, }, Signers: defaultSigners, Storage: defaultStorage, @@ -445,6 +581,7 @@ func TestParse(t *testing.T) { }, taskrunEnabled: true, ociEnbaled: true, + sbomEnabled: true, want: Config{ Builder: defaultBuilder, Artifacts: ArtifactConfigs{ @@ -463,6 +600,11 @@ func TestParse(t *testing.T) { StorageBackend: sets.New[string]("oci"), Signer: "x509", }, + SBOM: Artifact{ + Format: "in-toto", + StorageBackend: sets.New[string]("oci"), + Signer: "x509", + }, }, Signers: SignerConfigs{ X509: X509Signer{ @@ -482,6 +624,7 @@ func TestParse(t *testing.T) { }, taskrunEnabled: true, ociEnbaled: true, + sbomEnabled: true, want: Config{ Builder: defaultBuilder, Artifacts: defaultArtifacts, @@ -505,6 +648,7 @@ func TestParse(t *testing.T) { }, taskrunEnabled: true, ociEnbaled: true, + sbomEnabled: true, want: Config{ Builder: defaultBuilder, Artifacts: defaultArtifacts, @@ -536,6 +680,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) } diff --git a/pkg/reconciler/pipelinerun/controller.go b/pkg/reconciler/pipelinerun/controller.go index bc3d7ad47e..397a5528f1 100644 --- a/pkg/reconciler/pipelinerun/controller.go +++ b/pkg/reconciler/pipelinerun/controller.go @@ -44,6 +44,7 @@ func NewController(ctx context.Context, cmw configmap.Watcher) *controller.Impl psSigner := &chains.ObjectSigner{ SecretPath: SecretPath, Pipelineclientset: pipelineClient, + KubeClient: kubeClient, } c := &Reconciler{ diff --git a/pkg/reconciler/taskrun/controller.go b/pkg/reconciler/taskrun/controller.go index dbbb1cdab9..d7d2333121 100644 --- a/pkg/reconciler/taskrun/controller.go +++ b/pkg/reconciler/taskrun/controller.go @@ -40,6 +40,7 @@ func NewController(ctx context.Context, cmw configmap.Watcher) *controller.Impl tsSigner := &chains.ObjectSigner{ SecretPath: SecretPath, Pipelineclientset: pipelineClient, + KubeClient: kubeClient, } c := &Reconciler{