From 85ce20bdb055e7effe3635cd3db93d4ff0cafd03 Mon Sep 17 00:00:00 2001 From: Yawen Luo Date: Mon, 4 Jul 2022 17:32:02 -0400 Subject: [PATCH] TEP 109 implementation: add function to extract structured signable targets --- docs/intoto.md | 57 ++++- go.mod | 2 +- pkg/artifacts/signable.go | 88 ++++++++ pkg/artifacts/signable_test.go | 200 ++++++++++++++++++ .../formats/intotoite6/extract/extract.go | 15 +- .../formats/intotoite6/intotoite6_test.go | 1 + .../intotoite6/pipelinerun/pipelinerun.go | 8 +- .../intotoite6/pipelinerun/provenance_test.go | 25 ++- .../intotoite6/taskrun/provenance_test.go | 143 ++++++++----- .../formats/intotoite6/taskrun/taskrun.go | 8 +- .../intotoite6/testdata/pipelinerun1.json | 18 ++ 11 files changed, 506 insertions(+), 59 deletions(-) diff --git a/docs/intoto.md b/docs/intoto.md index b716280eac..b9ae2ebcb3 100644 --- a/docs/intoto.md +++ b/docs/intoto.md @@ -84,14 +84,14 @@ subject `pkg:/docker/test/foo@sha256:abcd?repository_url=gcr.io` is created. Note that image references are represented using [Package URL](https://github.com/package-url/purl-spec) format. -## Limitations +### Limitations This is an MVP implementation of the in-toto attestation format. More work would be required to properly capture the `Entrypoint` field in the provenance predicate, now the `TaskRef`'s name is used. Also metadata related to hermeticity/reproducibility are currently not populated. -## Examples +### Examples Example `TaskRun` attestation: @@ -401,3 +401,56 @@ Example `PipelineRun` attestation: } ``` + +### Structured Result Type Hinting + +**_A new feature will be implemented to have better support for artifact provenance retrieval. More details can be found in [Tekton Pipelines](https://github.com/tektoncd/pipeline/issues/5455)._** + +The feature requires **Tekton Pipeline v0.38** or later. + +To capture artifacts created by a task in a structured manner, Tekton Chains integrated with structured results and retrieve artifacts' provenances. +The result should be in the following format in a Task: +``` yaml +results: + - name: {artifact_name}-ARTIFACT_INPUTS + description: Digest of the image just built. + type: object + properties: + uri: + type: string + digest: + type:string + - name: {artifact_name}-ARTIFACT_OUTPUTS + description: Digest of the image just built. + type: object + properties: + uri: + type: string + digest: + type:string +``` +Suffix `-ARTIFACT_INPUTS` will retrieve the artifact provenance and put them in [Intoto Materials](https://github.com/in-toto/attestation/blob/v0.1.0/spec/predicates/provenance.md#fields), and `-ARTIFACT_OUTPUTS` will retrieve the artifact provenance and put them in [Intoto Subjects](https://github.com/in-toto/attestation/tree/v0.1.0/spec#statement). + +`uri` is the unique identifier for this artifact, and `digest` needs to be a string on the format `alg:digest`. + +An example structured result in a TaskRun: +``` yaml +results: + - name: img_1-ARTIFACT_INPUTS + value: + uri: gcr.io/foo/bar + digest: sha123@89dedecaca1b85346600c7db9939a4fe090a42ef + - name: mvn1_pkg-ARTIFACT_OUTPUTS + value: + uri: maven-test-0.0.1.jar + digest: sha256@89dedecaca1b85346600c7db9939a4fe090a42ee + - name: mvn1_pom-ARTIFACT_OUTPUTS + value: + uri: maven-test-0.0.1.pom + digest: sha256@89dedecaca1b85346600c7db9939a4fe090a42eg + - name: mvn1_src-ARTIFACT_OUTPUTS + value: + uri: maven-test-0.0.1-sources.jar + digest: sha256@89dedecaca1b85346600c7db9939a4fe090a42ez +``` + diff --git a/go.mod b/go.mod index 8c60ed384c..a3b3ccf417 100644 --- a/go.mod +++ b/go.mod @@ -42,6 +42,7 @@ require ( github.com/sigstore/rekor v0.12.1-0.20220915152154-4bb6f441c1b2 github.com/sigstore/sigstore v1.4.2 github.com/spiffe/go-spiffe/v2 v2.1.1 + github.com/stretchr/testify v1.8.0 github.com/tektoncd/pipeline v0.40.1 github.com/tektoncd/plumbing v0.0.0-20220817140952-3da8ce01aeeb github.com/titanous/rocacheck v0.0.0-20171023193734-afe73141d399 @@ -368,7 +369,6 @@ require ( github.com/ssgreg/nlreturn/v2 v2.2.1 // indirect github.com/stbenjam/no-sprintf-host-port v0.1.1 // indirect github.com/stretchr/objx v0.4.0 // indirect - github.com/stretchr/testify v1.8.0 // indirect github.com/subosito/gotenv v1.4.1 // indirect github.com/sylvia7788/contextcheck v1.0.6 // indirect github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d // indirect diff --git a/pkg/artifacts/signable.go b/pkg/artifacts/signable.go index cafbe44223..6d7e879316 100644 --- a/pkg/artifacts/signable.go +++ b/pkg/artifacts/signable.go @@ -19,6 +19,7 @@ import ( "strings" "github.com/google/go-containerregistry/pkg/name" + slsa "github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/v0.2" "github.com/opencontainers/go-digest" "github.com/tektoncd/chains/pkg/chains/formats" "github.com/tektoncd/chains/pkg/chains/objects" @@ -28,6 +29,11 @@ import ( "k8s.io/apimachinery/pkg/util/sets" ) +const ( + ArtifactsInputsResultName = "ARTIFACT_INPUTS" + ArtifactsOutputsResultName = "ARTIFACT_OUTPUTS" +) + type Signable interface { ExtractObjects(obj objects.TektonObject) []interface{} StorageBackend(cfg config.Config) sets.String @@ -257,6 +263,88 @@ func extractTargetFromResults(obj objects.TektonObject, identifierSuffix string, return ss } +// RetrieveMaterialsFromStructuredResults retrieves structured results from Tekton Object, and convert them into materials. +func RetrieveMaterialsFromStructuredResults(obj objects.TektonObject, categoryMarker string, logger *zap.SugaredLogger) []slsa.ProvenanceMaterial { + // Retrieve structured provenance for inputs. + mats := []slsa.ProvenanceMaterial{} + ssts := ExtractStructuredTargetFromResults(obj, ArtifactsInputsResultName, logger) + for _, s := range ssts { + if err := checkDigest(s.Digest); err != nil { + logger.Debugf("Digest for %s not in the right format: %s, %v", s.URI, s.Digest, err) + continue + } + splits := strings.Split(s.Digest, ":") + alg := splits[0] + digest := splits[1] + mats = append(mats, slsa.ProvenanceMaterial{ + URI: s.URI, + Digest: map[string]string{alg: digest}, + }) + } + return mats +} + +// ExtractStructuredTargetFromResults extracts structured signable targets aim to generate intoto provenance as materials within TaskRun results and store them as StructuredSignable. +// categoryMarker categorizes signable targets into inputs and outputs. +func ExtractStructuredTargetFromResults(obj objects.TektonObject, categoryMarker string, logger *zap.SugaredLogger) []*StructuredSignable { + objs := []*StructuredSignable{} + if categoryMarker != ArtifactsInputsResultName && categoryMarker != ArtifactsOutputsResultName { + return objs + } + + // TODO(#592): support structured results using Run + results := []objects.Result{} + tr, isTaskRun := obj.GetObject().(*v1beta1.TaskRun) + if isTaskRun { + for _, res := range tr.Status.TaskRunResults { + results = append(results, objects.Result{ + Name: res.Name, + Value: res.Value, + }) + } + + } else { + pr := obj.GetObject().(*v1beta1.PipelineRun) + for _, res := range pr.Status.PipelineResults { + results = append(results, objects.Result{ + Name: res.Name, + Value: res.Value, + }) + } + } + for _, res := range results { + if strings.HasSuffix(res.Name, categoryMarker) { + valid, err := isStructuredResult(res, categoryMarker) + if err != nil { + logger.Debugf("ExtractStructuredTargetFromResults: %v", err) + } + if valid { + objs = append(objs, &StructuredSignable{URI: res.Value.ObjectVal["uri"], Digest: res.Value.ObjectVal["digest"]}) + } + } + } + return objs +} + +func isStructuredResult(res objects.Result, categoryMarker string) (bool, error) { + if !strings.HasSuffix(res.Name, categoryMarker) { + return false, nil + } + if res.Value.ObjectVal == nil { + return false, fmt.Errorf("%s should be an object: %v", res.Name, res.Value.ObjectVal) + } + if res.Value.ObjectVal["uri"] == "" { + return false, fmt.Errorf("%s should have uri field: %v", res.Name, res.Value.ObjectVal) + } + if res.Value.ObjectVal["digest"] == "" { + return false, fmt.Errorf("%s should have digest field: %v", res.Name, res.Value.ObjectVal) + } + if err := checkDigest(res.Value.ObjectVal["digest"]); err != nil { + return false, fmt.Errorf("error getting digest %s: %v", res.Value.ObjectVal["digest"], err) + } + return true, nil +} + func checkDigest(dig string) error { prefix := digest.Canonical.String() + ":" if !strings.HasPrefix(dig, prefix) { diff --git a/pkg/artifacts/signable_test.go b/pkg/artifacts/signable_test.go index 295752246a..9af25f6c53 100644 --- a/pkg/artifacts/signable_test.go +++ b/pkg/artifacts/signable_test.go @@ -21,6 +21,7 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/go-containerregistry/pkg/name" + slsa "github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/v0.2" "github.com/tektoncd/chains/pkg/chains/objects" "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -381,6 +382,205 @@ func TestExtractSignableTargetFromResults(t *testing.T) { } } +func TestExtractStructuredTargetFromResults(t *testing.T) { + tr := &v1beta1.TaskRun{ + Status: v1beta1.TaskRunStatus{ + TaskRunStatusFields: v1beta1.TaskRunStatusFields{ + TaskRunResults: []v1beta1.TaskRunResult{ + { + Name: "mvn1_pkg" + "_" + ArtifactsOutputsResultName, + Value: *v1beta1.NewObject(map[string]string{ + "uri": "projects/test-project/locations/us-west4/repositories/test-repo/mavenArtifacts/com.google.guava:guava:31.0-jre", + "digest": digest1, + "signable_type": "", + }), + }, + { + Name: "mvn1_pom_sha512" + "_" + ArtifactsOutputsResultName, + Value: *v1beta1.NewObject(map[string]string{ + "uri": "com.google.guava:guava:31.0-jre.pom", + "digest": digest2, + "signable_type": "", + }), + }, + { + Name: "img1_input" + "_" + ArtifactsInputsResultName, + Value: *v1beta1.NewObject(map[string]string{ + "uri": "gcr.io/foo/bar", + "digest": digest3, + }), + }, + { + Name: "img2_input_no_digest" + "_" + ArtifactsInputsResultName, + Value: *v1beta1.NewObject(map[string]string{ + "uri": "gcr.io/foo/foo", + "digest": "", + }), + }, + }, + }, + }, + } + + wantInputs := []*StructuredSignable{ + {URI: "gcr.io/foo/bar", Digest: digest3}, + } + gotInputs := ExtractStructuredTargetFromResults(objects.NewTaskRunObject(tr), ArtifactsInputsResultName, logtesting.TestLogger(t)) + if diff := cmp.Diff(gotInputs, wantInputs, cmpopts.SortSlices(func(x, y *StructuredSignable) bool { return x.Digest < y.Digest })); diff != "" { + t.Errorf("Inputs are not as expected: %v", diff) + } + + wantOutputs := []*StructuredSignable{ + {URI: "projects/test-project/locations/us-west4/repositories/test-repo/mavenArtifacts/com.google.guava:guava:31.0-jre", Digest: digest1}, + {URI: "com.google.guava:guava:31.0-jre.pom", Digest: digest2}, + } + gotOutputs := ExtractStructuredTargetFromResults(objects.NewTaskRunObject(tr), ArtifactsOutputsResultName, logtesting.TestLogger(t)) + opts := append(ignore, cmpopts.SortSlices(func(x, y *StructuredSignable) bool { return x.Digest < y.Digest })) + if diff := cmp.Diff(gotOutputs, wantOutputs, opts...); diff != "" { + t.Error(diff) + } +} + +func TestRetrieveMaterialsFromStructuredResults(t *testing.T) { + tr := &v1beta1.TaskRun{ + Status: v1beta1.TaskRunStatus{ + TaskRunStatusFields: v1beta1.TaskRunStatusFields{ + TaskRunResults: []v1beta1.TaskRunResult{ + { + Name: "img1_input" + "_" + ArtifactsInputsResultName, + Value: *v1beta1.NewObject(map[string]string{ + "uri": "gcr.io/foo/bar", + "digest": "sha256:05f95b26ed10668b7183c1e2da98610e91372fa9f510046d4ce5812addad86b7", + }), + }, + { + Name: "img2_input_no_digest" + "_" + ArtifactsInputsResultName, + Value: *v1beta1.NewObject(map[string]string{ + "uri": "gcr.io/foo/foo", + "digest": "", + }), + }, + { + Name: "img2_input_invalid_digest" + "_" + ArtifactsInputsResultName, + Value: *v1beta1.NewObject(map[string]string{ + "uri": "gcr.io/foo/foo", + "digest": "sha:123", + }), + }, + }, + }, + }, + } + wantMaterials := []slsa.ProvenanceMaterial{ + { + URI: "gcr.io/foo/bar", + Digest: map[string]string{"sha256": "05f95b26ed10668b7183c1e2da98610e91372fa9f510046d4ce5812addad86b7"}, + }, + } + + gotMaterials := RetrieveMaterialsFromStructuredResults(objects.NewTaskRunObject(tr), ArtifactsInputsResultName, logtesting.TestLogger(t)) + + if diff := cmp.Diff(gotMaterials, wantMaterials, ignore...); diff != "" { + t.Fatalf("Materials not the same %s", diff) + } +} + +func TestValidateResults(t *testing.T) { + tests := []struct { + name string + obj objects.Result + categoryMarker string + wantResult bool + wantErr error + }{ + { + name: "valid result", + categoryMarker: ArtifactsOutputsResultName, + obj: objects.Result{ + Name: "valid_result-ARTIFACT_OUTPUTS", + Value: v1beta1.ParamValue{ + ObjectVal: map[string]string{ + "uri": "gcr.io/foo/bar", + "digest": digest3, + }, + }, + }, + wantResult: true, + wantErr: nil, + }, + { + name: "invalid result without digest field", + categoryMarker: ArtifactsOutputsResultName, + obj: objects.Result{ + Name: "missing_digest-ARTIFACT_OUTPUTS", + Value: v1beta1.ParamValue{ + ObjectVal: map[string]string{ + "uri": "gcr.io/foo/bar", + }, + }, + }, + wantResult: false, + wantErr: fmt.Errorf("%s should follow digest key", "missing_digest-ARTIFACTS_OUTPUTS"), + }, + { + name: "invalid result without uri field", + categoryMarker: ArtifactsOutputsResultName, + obj: objects.Result{ + Name: "missing_digest-ARTIFACT_OUTPUTS", + Value: v1beta1.ParamValue{ + ObjectVal: map[string]string{ + "digest": digest3, + }, + }, + }, + wantResult: false, + wantErr: fmt.Errorf("%s should follow digest key", "missing_digest-ARTIFACTS_OUTPUTS"), + }, + { + name: "invalid result wrong digest format", + categoryMarker: ArtifactsOutputsResultName, + obj: objects.Result{ + Name: "missing_digest-ARTIFACT_OUTPUTS", + Value: v1beta1.ParamValue{ + ObjectVal: map[string]string{ + "uri": "gcr.io/foo/bar", + "digest": "", + }, + }, + }, + wantResult: false, + wantErr: fmt.Errorf("error getting digest %s: ", ""), + }, + { + name: "invalid result wrong type hinting", + categoryMarker: ArtifactsOutputsResultName, + obj: objects.Result{ + Name: "missing_digest-ARTIFACTs_OUTPUTS", + Value: v1beta1.ParamValue{ + ObjectVal: map[string]string{ + "uri": "gcr.io/foo/bar", + "digest": digest3, + }, + }, + }, + wantResult: false, + wantErr: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := isStructuredResult(tt.obj, tt.categoryMarker) + if got != tt.wantResult { + t.Errorf("Validation result is not as the expected: got %v and wanted %v", got, tt.wantResult) + } + if diff := cmp.Diff(err, tt.wantErr, ignore...); diff != "" { + t.Errorf("Validation error is not as the expected: %s", diff) + } + }) + } +} + func createDigest(t *testing.T, dgst string) name.Digest { result, err := name.NewDigest(dgst) if err != nil { diff --git a/pkg/chains/formats/intotoite6/extract/extract.go b/pkg/chains/formats/intotoite6/extract/extract.go index 062f5b678b..bdf8e1901c 100644 --- a/pkg/chains/formats/intotoite6/extract/extract.go +++ b/pkg/chains/formats/intotoite6/extract/extract.go @@ -30,7 +30,7 @@ import ( "go.uber.org/zap" ) -// GetSubjectDigests extracts OCI images from the TaskRun based on standard hinting set up +// SubjectDigests extracts OCI images and other structured results from the TaskRun and PipelineRun based on standard hinting set up // It also goes through looking for any PipelineResources of Image type func SubjectDigests(obj objects.TektonObject, logger *zap.SugaredLogger) []intoto.Subject { var subjects []intoto.Subject @@ -62,6 +62,19 @@ func SubjectDigests(obj objects.TektonObject, logger *zap.SugaredLogger) []intot }) } + ssts := artifacts.ExtractStructuredTargetFromResults(obj, artifacts.ArtifactsOutputsResultName, logger) + for _, s := range ssts { + splits := strings.Split(s.Digest, ":") + alg := splits[0] + digest := splits[1] + subjects = append(subjects, intoto.Subject{ + Name: s.URI, + Digest: slsa.DigestSet{ + alg: digest, + }, + }) + } + // Check if object is a Taskrun, if so search for images used in PipelineResources // Otherwise object is a PipelineRun, where Pipelineresources are not relevant. // PipelineResources have been deprecated so their support has been left out of diff --git a/pkg/chains/formats/intotoite6/intotoite6_test.go b/pkg/chains/formats/intotoite6/intotoite6_test.go index 1127664413..67bb872ed1 100644 --- a/pkg/chains/formats/intotoite6/intotoite6_test.go +++ b/pkg/chains/formats/intotoite6/intotoite6_test.go @@ -156,6 +156,7 @@ func TestPipelineRunCreatePayload(t *testing.T) { Reproducible: false, }, Materials: []slsa.ProvenanceMaterial{ + {URI: "abc", Digest: slsa.DigestSet{"sha256": "827521c857fdcd4374f4da5442fbae2edb01e7fbae285c3ec15673d4c1daecb7"}}, {URI: "git+https://git.test.com.git", Digest: slsa.DigestSet{"sha1": "abcd"}}, }, Invocation: slsa.ProvenanceInvocation{ diff --git a/pkg/chains/formats/intotoite6/pipelinerun/pipelinerun.go b/pkg/chains/formats/intotoite6/pipelinerun/pipelinerun.go index 2b9901280f..f9f97207f9 100644 --- a/pkg/chains/formats/intotoite6/pipelinerun/pipelinerun.go +++ b/pkg/chains/formats/intotoite6/pipelinerun/pipelinerun.go @@ -19,6 +19,7 @@ import ( 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/artifacts" "github.com/tektoncd/chains/pkg/chains/formats/intotoite6/attest" "github.com/tektoncd/chains/pkg/chains/formats/intotoite6/extract" "github.com/tektoncd/chains/pkg/chains/objects" @@ -61,7 +62,7 @@ func GenerateAttestation(builderID string, pro *objects.PipelineRunObject, logge Invocation: invocation(pro), BuildConfig: buildConfig(pro, logger), Metadata: metadata(pro), - Materials: materials(pro), + Materials: materials(pro, logger), }, } return att, nil @@ -169,7 +170,7 @@ func metadata(pro *objects.PipelineRunObject) *slsa.ProvenanceMetadata { } // add any Git specification to materials -func materials(pro *objects.PipelineRunObject) []slsa.ProvenanceMaterial { +func materials(pro *objects.PipelineRunObject, logger *zap.SugaredLogger) []slsa.ProvenanceMaterial { var mats []slsa.ProvenanceMaterial var commit, url string // search spec.params @@ -183,6 +184,9 @@ func materials(pro *objects.PipelineRunObject) []slsa.ProvenanceMaterial { } } + sms := artifacts.RetrieveMaterialsFromStructuredResults(pro, artifacts.ArtifactsInputsResultName, logger) + mats = append(mats, sms...) + // search status.PipelineSpec.params if pro.Status.PipelineSpec != nil { for _, p := range pro.Status.PipelineSpec.Params { diff --git a/pkg/chains/formats/intotoite6/pipelinerun/provenance_test.go b/pkg/chains/formats/intotoite6/pipelinerun/provenance_test.go index 099ecf91fd..704949a4e0 100644 --- a/pkg/chains/formats/intotoite6/pipelinerun/provenance_test.go +++ b/pkg/chains/formats/intotoite6/pipelinerun/provenance_test.go @@ -18,8 +18,12 @@ import ( "time" "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/go-containerregistry/pkg/name" + 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/formats/intotoite6/attest" + "github.com/tektoncd/chains/pkg/chains/formats/intotoite6/extract" "github.com/tektoncd/chains/pkg/chains/objects" "github.com/tektoncd/chains/pkg/internal/objectloader" "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1" @@ -160,7 +164,6 @@ func TestBuildConfig(t *testing.T) { Invocation: slsa.ProvenanceInvocation{ ConfigSource: slsa.ConfigSource{}, Parameters: map[string]v1beta1.ArrayOrString{ - // "CHAINS-GIT_COMMIT": {Type: "string", StringVal: "abcd"}, "CHAINS-GIT_COMMIT": {Type: "string", StringVal: "sha:taskrun"}, "CHAINS-GIT_URL": {Type: "string", StringVal: "https://git.test.com"}, "IMAGE": {Type: "string", StringVal: "test.io/test/image"}, @@ -403,10 +406,28 @@ func TestMetadata(t *testing.T) { func TestMaterials(t *testing.T) { expected := []slsa.ProvenanceMaterial{ + {URI: "abc", Digest: slsa.DigestSet{"sha256": "827521c857fdcd4374f4da5442fbae2edb01e7fbae285c3ec15673d4c1daecb7"}}, {URI: "git+https://git.test.com.git", Digest: slsa.DigestSet{"sha1": "abcd"}}, } - got := materials(pro) + got := materials(pro, logtesting.TestLogger(t)) if diff := cmp.Diff(expected, got); diff != "" { t.Errorf("materials(): -want +got: %s", diff) } } + +var ignore = []cmp.Option{cmpopts.IgnoreUnexported(name.Registry{}, name.Repository{}, name.Digest{})} + +func TestSubjectDigests(t *testing.T) { + wantSubjects := []intoto.Subject{ + { + Name: "test.io/test/image", + Digest: slsa.DigestSet{"sha256": "827521c857fdcd4374f4da5442fbae2edb01e7fbae285c3ec15673d4c1daecb7"}, + }, + } + + gotSubjects := extract.SubjectDigests(pro, logtesting.TestLogger(t)) + opts := append(ignore, cmpopts.SortSlices(func(x, y intoto.Subject) bool { return x.Name < y.Name }) + if diff := cmp.Equal(gotSubjects, wantSubjects, opts...); diff != "" { + t.Errorf("Differences in subjects: -want +got: %s", diff) + } +} diff --git a/pkg/chains/formats/intotoite6/taskrun/provenance_test.go b/pkg/chains/formats/intotoite6/taskrun/provenance_test.go index 2d87c7c6d2..14f86e3007 100644 --- a/pkg/chains/formats/intotoite6/taskrun/provenance_test.go +++ b/pkg/chains/formats/intotoite6/taskrun/provenance_test.go @@ -27,6 +27,7 @@ import ( "github.com/ghodss/yaml" "github.com/google/go-cmp/cmp" "github.com/in-toto/in-toto-golang/in_toto" + "github.com/tektoncd/chains/pkg/artifacts" "github.com/tektoncd/chains/pkg/chains/formats/intotoite6/extract" "github.com/tektoncd/chains/pkg/chains/objects" "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1" @@ -102,50 +103,68 @@ status: }, } - got := materials(objects.NewTaskRunObject(taskRun)) + got := materials(objects.NewTaskRunObject(taskRun), logtesting.TestLogger(t)) if !reflect.DeepEqual(expected, got) { t.Fatalf("expected %v got %v", expected, got) } } func TestMaterials(t *testing.T) { - // make sure this works with Git resources - taskrun := `apiVersion: tekton.dev/v1beta1 -kind: TaskRun -spec: - resources: - inputs: - - name: nil-resource-spec - - name: repo - resourceSpec: - params: - - name: url - value: https://github.com/GoogleContainerTools/distroless - type: git - taskSpec: - resources: - inputs: - - name: repo - type: git -status: - resourcesResult: - - key: commit - resourceName: repo - resourceRef: - name: repo - value: 50c56a48cfb3a5a80fa36ed91c739bdac8381cbe - - key: url - resourceName: repo - resourceRef: - name: repo - value: https://github.com/GoogleContainerTools/distroless` - - var taskRun *v1beta1.TaskRun - if err := yaml.Unmarshal([]byte(taskrun), &taskRun); err != nil { - t.Fatal(err) + taskrun := &v1beta1.TaskRun{ + Spec: v1beta1.TaskRunSpec{ + Resources: &v1beta1.TaskRunResources{ + Inputs: []v1beta1.TaskResourceBinding{ + { + PipelineResourceBinding: v1beta1.PipelineResourceBinding{ + Name: "nil-resource-spec", + }, + }, { + PipelineResourceBinding: v1beta1.PipelineResourceBinding{ + Name: "repo", + ResourceSpec: &v1alpha1.PipelineResourceSpec{ + Params: []v1alpha1.ResourceParam{ + {Name: "url", Value: "https://github.com/GoogleContainerTools/distroless"}, + }, + Type: v1alpha1.PipelineResourceTypeGit, + }, + }, + }, + }, + }, + }, + Status: v1beta1.TaskRunStatus{ + TaskRunStatusFields: v1beta1.TaskRunStatusFields{ + TaskRunResults: []v1beta1.TaskRunResult{ + { + Name: "img1_input" + "-" + artifacts.ArtifactsInputsResultName, + Value: *v1beta1.NewObject(map[string]string{ + "uri": "gcr.io/foo/bar", + "digest": digest3, + }), + }, + }, + ResourcesResult: []v1beta1.PipelineResourceResult{ + { + ResourceName: "repo", + Key: "commit", + Value: "50c56a48cfb3a5a80fa36ed91c739bdac8381cbe", + }, { + ResourceName: "repo", + Key: "url", + Value: "https://github.com/GoogleContainerTools/distroless", + }, + }, + }, + }, } expected := []slsa.ProvenanceMaterial{ + { + URI: "gcr.io/foo/bar", + Digest: slsa.DigestSet{ + "sha256": strings.TrimPrefix(digest3, "sha256:"), + }, + }, { URI: "git+https://github.com/GoogleContainerTools/distroless.git", Digest: slsa.DigestSet{ @@ -154,25 +173,20 @@ status: }, } - got := materials(objects.NewTaskRunObject(taskRun)) + got := materials(objects.NewTaskRunObject(taskrun), logtesting.TestLogger(t)) if !reflect.DeepEqual(expected, got) { t.Fatalf("expected %v got %v", expected, got) } // make sure this works with GIT* results as well - taskrun = `apiVersion: tekton.dev/v1beta1 -kind: TaskRun -spec: - params: - - name: CHAINS-GIT_COMMIT - value: my-commit - - name: CHAINS-GIT_URL - value: github.com/something` - taskRun = &v1beta1.TaskRun{} - if err := yaml.Unmarshal([]byte(taskrun), &taskRun); err != nil { - t.Fatal(err) + taskRun := &v1beta1.TaskRun{ + Spec: v1beta1.TaskRunSpec{ + Params: []v1beta1.Param{ + {Name: "CHAINS-GIT_COMMIT", Value: v1beta1.ArrayOrString{StringVal: "my-commit"}}, + {Name: "CHAINS-GIT_URL", Value: v1beta1.ArrayOrString{StringVal: "github.com/something"}}, + }, + }, } - expected = []slsa.ProvenanceMaterial{ { URI: "git+github.com/something.git", @@ -182,7 +196,7 @@ spec: }, } - got = materials(objects.NewTaskRunObject(taskRun)) + got = materials(objects.NewTaskRunObject(taskRun), logtesting.TestLogger(t)) if !reflect.DeepEqual(expected, got) { t.Fatalf("expected %v got %v", expected, got) } @@ -323,6 +337,27 @@ func TestGetSubjectDigests(t *testing.T) { Name: "invalid_ARTIFACT_DIGEST", Value: *v1beta1.NewArrayOrString(digest5), }, + { + Name: "mvn1_pkg" + "-" + artifacts.ArtifactsOutputsResultName, + Value: *v1beta1.NewObject(map[string]string{ + "uri": "projects/test-project-1/locations/us-west4/repositories/test-repo/mavenArtifacts/com.google.guava:guava:31.0-jre", + "digest": digest1, + }), + }, + { + Name: "mvn1_pom_sha512" + "-" + artifacts.ArtifactsOutputsResultName, + Value: *v1beta1.NewObject(map[string]string{ + "uri": "com.google.guava:guava:1.0-jre.pom", + "digest": digest2, + }), + }, + { + Name: "img1_input" + "-" + artifacts.ArtifactsInputsResultName, + Value: *v1beta1.NewObject(map[string]string{ + "uri": "gcr.io/foo/bar", + "digest": digest3, + }), + }, }, ResourcesResult: []v1beta1.PipelineResourceResult{ { @@ -341,6 +376,11 @@ func TestGetSubjectDigests(t *testing.T) { expected := []in_toto.Subject{ { + Name: "com.google.guava:guava:1.0-jre.pom", + Digest: slsa.DigestSet{ + "sha256": strings.TrimPrefix(digest2, "sha256:"), + }, + }, { Name: "index.docker.io/registry/myimage", Digest: slsa.DigestSet{ "sha256": strings.TrimPrefix(digest1, "sha256:"), @@ -360,6 +400,11 @@ func TestGetSubjectDigests(t *testing.T) { Digest: slsa.DigestSet{ "sha256": strings.TrimPrefix(digest4, "sha256:"), }, + }, { + Name: "projects/test-project-1/locations/us-west4/repositories/test-repo/mavenArtifacts/com.google.guava:guava:31.0-jre", + Digest: slsa.DigestSet{ + "sha256": strings.TrimPrefix(digest1, "sha256:"), + }, }, { Name: "registry/resource-image", Digest: slsa.DigestSet{ diff --git a/pkg/chains/formats/intotoite6/taskrun/taskrun.go b/pkg/chains/formats/intotoite6/taskrun/taskrun.go index 0374a53708..1a52749c95 100644 --- a/pkg/chains/formats/intotoite6/taskrun/taskrun.go +++ b/pkg/chains/formats/intotoite6/taskrun/taskrun.go @@ -18,6 +18,7 @@ import ( 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/artifacts" "github.com/tektoncd/chains/pkg/chains/formats/intotoite6/attest" "github.com/tektoncd/chains/pkg/chains/formats/intotoite6/extract" "github.com/tektoncd/chains/pkg/chains/objects" @@ -45,7 +46,7 @@ func GenerateAttestation(builderID string, tro *objects.TaskRunObject, logger *z Invocation: invocation(tro), BuildConfig: buildConfig(tro), Metadata: metadata(tro), - Materials: materials(tro), + Materials: materials(tro, logger), }, } return att, nil @@ -79,7 +80,7 @@ func metadata(tro *objects.TaskRunObject) *slsa.ProvenanceMetadata { } // add any Git specification to materials -func materials(tro *objects.TaskRunObject) []slsa.ProvenanceMaterial { +func materials(tro *objects.TaskRunObject, logger *zap.SugaredLogger) []slsa.ProvenanceMaterial { var mats []slsa.ProvenanceMaterial gitCommit, gitURL := gitInfo(tro) @@ -92,6 +93,9 @@ func materials(tro *objects.TaskRunObject) []slsa.ProvenanceMaterial { return mats } + sms := artifacts.RetrieveMaterialsFromStructuredResults(tro, artifacts.ArtifactsInputsResultName, logger) + mats = append(mats, sms...) + if tro.Spec.Resources == nil { return mats } diff --git a/pkg/chains/formats/intotoite6/testdata/pipelinerun1.json b/pkg/chains/formats/intotoite6/testdata/pipelinerun1.json index 5e244e0fcf..c66edd7f32 100644 --- a/pkg/chains/formats/intotoite6/testdata/pipelinerun1.json +++ b/pkg/chains/formats/intotoite6/testdata/pipelinerun1.json @@ -39,6 +39,24 @@ { "name": "IMAGE_DIGEST", "value": "sha256:827521c857fdcd4374f4da5442fbae2edb01e7fbae285c3ec15673d4c1daecb7" + }, + { + "name": "img-ARTIFACT_INPUTS", + "value": { + "uri": "abc","digest": "sha256:827521c857fdcd4374f4da5442fbae2edb01e7fbae285c3ec15673d4c1daecb7" + } + }, + { + "name": "img2-ARTIFACT_OUTPUTS", + "value": { + "uri": "def","digest": "sha256:" + } + }, + { + "name": "img_no_uri-ARTIFACT_OUTPUTS", + "value": { + "digest": "sha256:827521c857fdcd4374f4da5442fbae2edb01e7fbae285c3ec15673d4c1daecb7" + } } ], "pipelineSpec": {