diff --git a/docs/config.md b/docs/config.md index 03b0099df9..efeebece26 100644 --- a/docs/config.md +++ b/docs/config.md @@ -21,7 +21,7 @@ Supported keys include: | Key | Description | Supported Values | Default | | :-------------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :----------------------------------------- | :-------- | -| `artifacts.taskrun.format` | The format to store `TaskRun` payloads in. | `in-toto`, `slsa/v1`, `slsa/v2alpha2`, `slsa/v2alpha3` | `in-toto` | +| `artifacts.taskrun.format` | The format to store `TaskRun` payloads in. | `in-toto`, `slsa/v1`, `slsa/v2alpha2`, `slsa/v2alpha3`, `slsa/v2alpha4` | `in-toto` | | `artifacts.taskrun.storage` | The storage backend to store `TaskRun` signatures in. Multiple backends can be specified with comma-separated list ("tekton,oci"). To disable the `TaskRun` artifact input an empty string (""). | `tekton`, `oci`, `gcs`, `docdb`, `grafeas` | `tekton` | | `artifacts.taskrun.signer` | The signature backend to sign `TaskRun` payloads with. | `x509`, `kms` | `x509` | @@ -29,7 +29,8 @@ Supported keys include: > > - `slsa/v1` is an alias of `in-toto` for backwards compatibility. > - `slsa/v2alpha2` corresponds to the slsav1.0 spec. and uses now deprecated [`v1beta1` Tekton Objects](https://tekton.dev/docs/pipelines/pipeline-api/#tekton.dev/v1beta1). -> - `slsa/v2alpha3` corresponds to the slsav1.0 spec. and uses latest [`v1` Tekton Objects](https://tekton.dev/docs/pipelines/pipeline-api/#tekton.dev/v1). Recommended format for new chains users who want the slsav1.0 spec. +> - `slsa/v2alpha3` corresponds to the slsav1.0 spec. and uses latest [`v1` Tekton Objects](https://tekton.dev/docs/pipelines/pipeline-api/#tekton.dev/v1). +> - `slsa/v2alpha4` corresponds to the slsav1.0 spec. and uses latest [`v1` Tekton Objects](https://tekton.dev/docs/pipelines/pipeline-api/#tekton.dev/v1). It reads type-hinted results from [StepActions](https://tekton.dev/docs/pipelines/pipeline-api/#tekton.dev/v1alpha1.StepAction). Recommended format for new chains users who want the slsav1.0 spec. ### PipelineRun Configuration diff --git a/docs/slsa-provenance.md b/docs/slsa-provenance.md index 12c67512c1..38cd0a9ad5 100644 --- a/docs/slsa-provenance.md +++ b/docs/slsa-provenance.md @@ -38,7 +38,7 @@ The following shows the mapping between slsa version and formatter name. | SLSA Version | Formatter Name | | ------------ | ---------------------- | -| v1.0 | `slsa/v2alpha3` | +| v1.0 | `slsa/v2alpha3` and `slsa/v2alpha4` | | v0.2 | `slsa/v1` or `in-toto` | To configure Task-level provenance version @@ -319,6 +319,99 @@ spec: +## `v2alpha4` formatter + +Starting with version `v2alpha4`, the type-hinted object results value now can include a new boolean flag called `isBuildArtifact`. When set to `true`, this flag indicates the output artifact should be considered as `subject` in the executed TaskRun/PipelineRun. + +The `isBuildArtifact` can be set in results whose type-hint uses the `*ARTIFACT_OUTPUTS` format. For results not using this format, the associated result will be automatically classified as a `byProduct`. + +For instance, in the following TaskRun: + +```yaml +apiVersion: tekton.dev/v1 +kind: TaskRun +metadata: + name: image-build +spec: + taskSpec: + results: + - name: first-ARTIFACT_OUTPUTS + description: The first artifact built + type: object + properties: + uri: {} + digest: {} + + - name: second-ARTIFACT_OUTPUTS + description: The second artifact built + type: object + properties: + uri: {} + digest: {} + isBuildArtifact: {} + + - name: third-IMAGE_URL + type: string + - name: third-IMAGE_DIGEST + type: string + steps: + - name: dummy-build + image: bash:latest + script: | + echo -n "{\"uri\":\"gcr.io/foo/img1\", \"digest\":\"sha256:586789aa031fafc7d78a5393cdc772e0b55107ea54bb8bcf3f2cdac6c6da51ee\"}" > $(results.first-ARTIFACT_OUTPUTS.path) + + echo -n "{\"uri\":\"gcr.io/foo/img2\", \"digest\":\"sha256:05f95b26ed10668b7183c1e2da98610e91372fa9f510046d4ce5812addad86b5\", \"isBuildArtifact\":\"true\"}" > $(results.second-ARTIFACT_OUTPUTS.path) + + echo -n "gcr.io/foo/bar" | tee $(results.third-IMAGE_URL.path) + echo -n "sha256:05f95b26ed10668b7183c1e2da98610e91372fa9f510046d4ce5812addad86b6" | tee $(results.third-IMAGE_DIGEST.path) +``` + +Only the result `second-ARTIFACT_OUTPUTS` will be considered as a `subject` due to it uses the `*ARTIFACT_OUTPUTS` type hint format, and has `isBuildArtifact` set to `true`. + +Chains' `v2alpha4` formatter now automatically reads type-hinted results from StepActions associated to the executed TaskRun; users no longer need to manually surface these results from the StepActions when the appropriate type hints are in place. For instance, the following TaskRun: + +```yaml +apiVersion: tekton.dev/v1alpha1 +kind: StepAction +metadata: + name: img-builder +spec: + image: busybox:glibc + + results: + - name: first-ARTIFACT_OUTPUTS + description: The first artifact built + type: object + properties: + uri: {} + digest: {} + + - name: second-IMAGE_URL + type: string + - name: second-IMAGE_DIGEST + type: string + + script: | + echo -n "{\"uri\":\"gcr.io/foo/img1\", \"digest\":\"sha256:586789aa031fafc7d78a5393cdc772e0b55107ea54bb8bcf3f2cdac6c6da51ee\", \"isBuildArtifact\": \"true\" }" > $(step.results.first-ARTIFACT_OUTPUTS.path) + + echo -n "gcr.io/foo/bar" > $(step.results.second-IMAGE_URL.path) + echo -n "sha256:05f95b26ed10668b7183c1e2da98610e91372fa9f510046d4ce5812addad86b6" > $(step.results.second-IMAGE_DIGEST.path) +--- +apiVersion: tekton.dev/v1 +kind: TaskRun +metadata: + name: taskrun +spec: + taskSpec: + steps: + - name: action-runner + ref: + name: img-builder +``` + +Will read `first-ARTIFACT_OUTPUTS` from the StepAction and clasify it as a `subject`. + + ## Besides inputs/outputs Tekton Chains is also able to capture the feature flags being used for Tekton Pipelines controller and the origin of the build configuration file with immutable references such as task.yaml and pipeline.yaml. However, those fields in Tekton Pipelines are gated by a dedicated feature flag. Therefore, the feature flag needs to be enabled to let Tekton Pipelines controller to populate these fields. diff --git a/examples/stepactions/step-image-builder.yaml b/examples/stepactions/step-image-builder.yaml new file mode 100644 index 0000000000..0c259d2996 --- /dev/null +++ b/examples/stepactions/step-image-builder.yaml @@ -0,0 +1,24 @@ +apiVersion: tekton.dev/v1alpha1 +kind: StepAction +metadata: + name: img-builder +spec: + image: busybox:glibc + + results: + - name: first-ARTIFACT_OUTPUTS + description: The first artifact built + type: object + properties: + uri: {} + digest: {} + + - name: second-IMAGE_URL + type: string + - name: second-IMAGE_DIGEST + type: string + + script: | + echo -n "{\"uri\":\"gcr.io/foo/img1\", \"digest\":\"sha256:586789aa031fafc7d78a5393cdc772e0b55107ea54bb8bcf3f2cdac6c6da51ee\", \"isBuildArtifact\": \"true\" }" > $(step.results.first-ARTIFACT_OUTPUTS.path) + echo -n "gcr.io/foo/bar" > $(step.results.second-IMAGE_URL.path) + echo -n "sha256:05f95b26ed10668b7183c1e2da98610e91372fa9f510046d4ce5812addad86b6" > $(step.results.second-IMAGE_DIGEST.path) \ No newline at end of file diff --git a/examples/stepactions/taskrun-image-builder.yaml b/examples/stepactions/taskrun-image-builder.yaml new file mode 100644 index 0000000000..72202527a2 --- /dev/null +++ b/examples/stepactions/taskrun-image-builder.yaml @@ -0,0 +1,10 @@ +apiVersion: tekton.dev/v1 +kind: TaskRun +metadata: + name: taskrun +spec: + taskSpec: + steps: + - name: action-runner + ref: + name: img-builder \ No newline at end of file diff --git a/pkg/artifacts/signable.go b/pkg/artifacts/signable.go index d3fbf885d3..be77838ec7 100644 --- a/pkg/artifacts/signable.go +++ b/pkg/artifacts/signable.go @@ -38,6 +38,7 @@ const ( ArtifactsOutputsResultName = "ARTIFACT_OUTPUTS" OCIScheme = "oci://" GitSchemePrefix = "git+" + isBuildArtifactField = "isBuildArtifact" ) var ( @@ -264,12 +265,12 @@ func (s *StructuredSignable) FullRef() string { return fmt.Sprintf("%s@%s", s.URI, s.Digest) } -// RetrieveMaterialsFromStructuredResults retrieves structured results from Tekton Object, and convert them into materials. -func RetrieveMaterialsFromStructuredResults(ctx context.Context, obj objects.TektonObject, categoryMarker string) []common.ProvenanceMaterial { +// RetrieveMaterialsFromStructuredResults retrieves structured results from Object Results, and convert them into materials. +func RetrieveMaterialsFromStructuredResults(ctx context.Context, objResults []objects.Result) []common.ProvenanceMaterial { logger := logging.FromContext(ctx) // Retrieve structured provenance for inputs. mats := []common.ProvenanceMaterial{} - ssts := ExtractStructuredTargetFromResults(ctx, obj, ArtifactsInputsResultName) + ssts := ExtractStructuredTargetFromResults(ctx, objResults, ArtifactsInputsResultName) for _, s := range ssts { alg, digest, err := ParseDigest(s.Digest) if err != nil { @@ -286,7 +287,7 @@ func RetrieveMaterialsFromStructuredResults(ctx context.Context, obj objects.Tek // 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(ctx context.Context, obj objects.TektonObject, categoryMarker string) []*StructuredSignable { +func ExtractStructuredTargetFromResults(ctx context.Context, objResults []objects.Result, categoryMarker string) []*StructuredSignable { logger := logging.FromContext(ctx) objs := []*StructuredSignable{} if categoryMarker != ArtifactsInputsResultName && categoryMarker != ArtifactsOutputsResultName { @@ -295,7 +296,7 @@ func ExtractStructuredTargetFromResults(ctx context.Context, obj objects.TektonO // TODO(#592): support structured results using Run results := []objects.Result{} - for _, res := range obj.GetResults() { + for _, res := range objResults { results = append(results, objects.Result{ Name: res.Name, Value: res.Value, @@ -316,6 +317,38 @@ func ExtractStructuredTargetFromResults(ctx context.Context, obj objects.TektonO return objs } +// ExtractBuildArtifactsFromResults extracts all the structured signable targets from the given results, only processing the ones marked as build artifacts. +func ExtractBuildArtifactsFromResults(ctx context.Context, results []objects.Result) (objs []*StructuredSignable) { + logger := logging.FromContext(ctx) + + for _, res := range results { + valid, err := IsBuildArtifact(res) + if err != nil { + logger.Debugf("ExtractBuildArtifactsFromResults: %v", err) + } + if valid { + logger.Debugf("Extracted Build artifact data from Result %s, %s", res.Value.ObjectVal["uri"], res.Value.ObjectVal["digest"]) + objs = append(objs, &StructuredSignable{URI: res.Value.ObjectVal["uri"], Digest: res.Value.ObjectVal["digest"]}) + } + } + return +} + +// IsBuildArtifact indicates if a given result was marked as a Build Artifact. +func IsBuildArtifact(res objects.Result) (bool, error) { + isObjResult, err := isStructuredResult(res, ArtifactsOutputsResultName) + if err != nil { + return false, err + } + + if !isObjResult { + return false, nil + } + + isBuildArtifact := res.Value.ObjectVal[isBuildArtifactField] + return isBuildArtifact == "true", nil +} + func isStructuredResult(res objects.Result, categoryMarker string) (bool, error) { if !strings.HasSuffix(res.Name, categoryMarker) { return false, nil diff --git a/pkg/artifacts/signable_test.go b/pkg/artifacts/signable_test.go index 5cbdab0847..90e9de62b3 100644 --- a/pkg/artifacts/signable_test.go +++ b/pkg/artifacts/signable_test.go @@ -495,7 +495,7 @@ func TestExtractStructuredTargetFromResults(t *testing.T) { {URI: "gcr.io/foo/bar", Digest: digest_sha512}, } ctx := logtesting.TestContextWithLogger(t) - gotInputs := ExtractStructuredTargetFromResults(ctx, objects.NewTaskRunObjectV1(tr), ArtifactsInputsResultName) + gotInputs := ExtractStructuredTargetFromResults(ctx, objects.NewTaskRunObjectV1(tr).GetResults(), ArtifactsInputsResultName) 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) } @@ -504,7 +504,7 @@ func TestExtractStructuredTargetFromResults(t *testing.T) { {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(ctx, objects.NewTaskRunObjectV1(tr), ArtifactsOutputsResultName) + gotOutputs := ExtractStructuredTargetFromResults(ctx, objects.NewTaskRunObjectV1(tr).GetResults(), ArtifactsOutputsResultName) 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) @@ -548,7 +548,7 @@ func TestRetrieveMaterialsFromStructuredResults(t *testing.T) { }, } ctx := logtesting.TestContextWithLogger(t) - gotMaterials := RetrieveMaterialsFromStructuredResults(ctx, objects.NewTaskRunObjectV1(tr), ArtifactsInputsResultName) + gotMaterials := RetrieveMaterialsFromStructuredResults(ctx, objects.NewTaskRunObjectV1(tr).GetResults()) if diff := cmp.Diff(gotMaterials, wantMaterials, ignore...); diff != "" { t.Fatalf("Materials not the same %s", diff) @@ -654,6 +654,165 @@ func TestValidateResults(t *testing.T) { } } +func TestExtractBuildArtifactsFromResults(t *testing.T) { + tests := []struct { + name string + results []objects.Result + expectedBuildArtifacts []*StructuredSignable + }{ + { + name: "structured result without isBuildArtifact property", + results: []objects.Result{ + { + Name: "result-ARTIFACT_OUTPUTS", + + Value: v1.ParamValue{ + ObjectVal: map[string]string{ + "uri": "gcr.io/foo/bar", + "digest": digest1, + }, + }, + }, + }, + }, + { + name: "structured result without expected schema", + results: []objects.Result{ + { + Name: "bad-type-ARTIFACT_OUTPUTS", + Value: v1.ParamValue{ + StringVal: "not-expected-type-value", + }, + }, + { + Name: "bad-url-field-ARTIFACT_OUTPUTS", + Value: v1.ParamValue{ + ObjectVal: map[string]string{ + "url": "foo.com", + isBuildArtifactField: "true", + }, + }, + }, + { + Name: "bad-commit-field-ARTIFACT_OUTPUTS", + Value: v1.ParamValue{ + ObjectVal: map[string]string{ + "uri": "gcr.io/foo/bar", + "commit": digest1, + isBuildArtifactField: "true", + }, + }, + }, + { + Name: "bad-digest-value-ARTIFACT_OUTPUTS", + Value: v1.ParamValue{ + ObjectVal: map[string]string{ + "uri": "gcr.io/foo/bar", + "digest": "sha512:baddigestvalue", + isBuildArtifactField: "true", + }, + }, + }, + }, + }, + { + name: "structured result without expected type-hint", + results: []objects.Result{ + { + Name: "result-ARTIFACT_OBJ", + Value: v1.ParamValue{ + ObjectVal: map[string]string{ + "uri": "gcr.io/foo/bar", + "digest": digest1, + isBuildArtifactField: "true", + }, + }, + }, + { + Name: "result-ARTIFACT_URI", + Value: v1.ParamValue{ + StringVal: "gcr.io/foo/bar", + }, + }, + { + Name: "result-ARTIFACT_DIGEST", + Value: v1.ParamValue{ + StringVal: digest1, + }, + }, + { + Name: "result-IMAGE_URL", + Value: v1.ParamValue{ + StringVal: "gcr.io/foo/bar", + }, + }, + { + Name: "result-IMAGE_DIGEST", + Value: v1.ParamValue{ + StringVal: digest1, + }, + }, + { + Name: "IMAGES", + Value: v1.ParamValue{ + ArrayVal: []string{ + fmt.Sprintf("img1@sha256:%v", digest1), + fmt.Sprintf("img2@sha256:%v", digest2), + }, + }, + }, + }, + }, + { + name: "structured result mark as build artifact", + results: []objects.Result{ + { + Name: "result-1-ARTIFACT_OUTPUTS", + Value: v1.ParamValue{ + ObjectVal: map[string]string{ + "uri": "gcr.io/foo/bar", + "digest": digest1, + isBuildArtifactField: "true", + }, + }, + }, + { + Name: "result-2-ARTIFACT_OUTPUTS", + Value: v1.ParamValue{ + ObjectVal: map[string]string{ + "uri": "gcr.io/bar/foo", + "digest": digest2, + isBuildArtifactField: "false", + }, + }, + }, + { + Name: "result-3-ARTIFACT_OUTPUTS", + Value: v1.ParamValue{ + ObjectVal: map[string]string{ + "uri": "gcr.io/repo/test", + "digest": digest3, + }, + }, + }, + }, + expectedBuildArtifacts: []*StructuredSignable{ + {URI: "gcr.io/foo/bar", Digest: digest1}, + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + ctx := logtesting.TestContextWithLogger(t) + gotBuildArtifacts := ExtractBuildArtifactsFromResults(ctx, test.results) + if diff := cmp.Diff(gotBuildArtifacts, test.expectedBuildArtifacts); diff != "" { + t.Fatalf("Materials not the same %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/all/all.go b/pkg/chains/formats/all/all.go index 2d15efc893..393c946b83 100644 --- a/pkg/chains/formats/all/all.go +++ b/pkg/chains/formats/all/all.go @@ -20,4 +20,5 @@ import ( _ "github.com/tektoncd/chains/pkg/chains/formats/slsa/v2alpha1" _ "github.com/tektoncd/chains/pkg/chains/formats/slsa/v2alpha2" _ "github.com/tektoncd/chains/pkg/chains/formats/slsa/v2alpha3" + _ "github.com/tektoncd/chains/pkg/chains/formats/slsa/v2alpha4" ) diff --git a/pkg/chains/formats/format.go b/pkg/chains/formats/format.go index fccb396853..08f37e96d6 100644 --- a/pkg/chains/formats/format.go +++ b/pkg/chains/formats/format.go @@ -35,6 +35,7 @@ const ( PayloadTypeSlsav2alpha1 config.PayloadType = "slsa/v2alpha1" PayloadTypeSlsav2alpha2 config.PayloadType = "slsa/v2alpha2" PayloadTypeSlsav2alpha3 config.PayloadType = "slsa/v2alpha3" + PayloadTypeSlsav2alpha4 config.PayloadType = "slsa/v2alpha4" ) var ( @@ -44,6 +45,7 @@ var ( PayloadTypeSlsav2alpha1: {}, PayloadTypeSlsav2alpha2: {}, PayloadTypeSlsav2alpha3: {}, + PayloadTypeSlsav2alpha4: {}, } payloaderMap = map[config.PayloadType]PayloaderInit{} ) diff --git a/pkg/chains/formats/slsa/extract/extract.go b/pkg/chains/formats/slsa/extract/extract.go index 2cc4f4861b..308f67be6c 100644 --- a/pkg/chains/formats/slsa/extract/extract.go +++ b/pkg/chains/formats/slsa/extract/extract.go @@ -128,7 +128,7 @@ func subjectsFromTektonObject(ctx context.Context, obj objects.TektonObject) []i }) } - ssts := artifacts.ExtractStructuredTargetFromResults(ctx, obj, artifacts.ArtifactsOutputsResultName) + ssts := artifacts.ExtractStructuredTargetFromResults(ctx, obj.GetResults(), artifacts.ArtifactsOutputsResultName) for _, s := range ssts { splits := strings.Split(s.Digest, ":") alg := splits[0] @@ -202,3 +202,27 @@ func RetrieveAllArtifactURIs(ctx context.Context, obj objects.TektonObject, deep } return result } + +// SubjectsFromBuildArtifact returns the software artifacts produced by the TaskRun/PipelineRun in the form of standard +// subject field of intoto statement. The detection is based on type hinting. To be read as a software artifact the +// type hintint should: +// - Use the object type-hinting suffix, *ARTIFACT_OUTPUTS +// - The value associated with the result should be an object with the fields `uri`, `digest`, and `isBuildArtifact` set to true. +func SubjectsFromBuildArtifact(ctx context.Context, obj objects.TektonObject) (subjects []intoto.Subject) { + results := append(obj.GetResults(), obj.GetNestedResults()...) + + buildArtifacts := artifacts.ExtractBuildArtifactsFromResults(ctx, results) + for _, ba := range buildArtifacts { + splits := strings.Split(ba.Digest, ":") + alg := splits[0] + digest := splits[1] + subjects = artifact.AppendSubjects(subjects, intoto.Subject{ + Name: ba.URI, + Digest: common.DigestSet{ + alg: digest, + }, + }) + } + + return +} diff --git a/pkg/chains/formats/slsa/extract/extract_test.go b/pkg/chains/formats/slsa/extract/extract_test.go index 583a727170..2af83a0020 100644 --- a/pkg/chains/formats/slsa/extract/extract_test.go +++ b/pkg/chains/formats/slsa/extract/extract_test.go @@ -271,6 +271,134 @@ func TestPipelineRunObserveModeForSubjects(t *testing.T) { } } +func TestSubjectsFromBuildArtifact(t *testing.T) { + tests := []struct { + name string + obj objects.TektonObject + expectedSubjects []intoto.Subject + }{ + { + name: "no type-hinted build artifacts", + obj: objects.NewTaskRunObjectV1( + &v1.TaskRun{ + Status: v1.TaskRunStatus{ + TaskRunStatusFields: v1.TaskRunStatusFields{ + Results: []v1.TaskRunResult{ + {Name: "result1_IMAGE_DIGEST", Value: *v1.NewStructuredValues("sha256:" + artifactDigest1)}, + {Name: "result1_IMAGE_URL", Value: *v1.NewStructuredValues(artifactURL1)}, + {Name: "IMAGES", Value: *v1.NewStructuredValues( + fmt.Sprintf("%v@sha256:%v", artifactURL1, artifactDigest1), + fmt.Sprintf("%vsha256:%v", artifactURL2, artifactDigest2))}, + }, + Steps: []v1.StepState{{ + Results: []v1.TaskRunStepResult{ + {Name: "result2_ARTIFACT_DIGEST", Value: *v1.NewStructuredValues("sha256:" + artifactDigest1)}, + {Name: "result2_IMAGE_URL", Value: *v1.NewStructuredValues(artifactURL1)}, + {Name: "result3_ARTIFACT_OUTPUTS", Value: *v1.NewObject(map[string]string{ + "uri": artifactURL1, + "digest": "sha256:" + artifactDigest1, + })}, + }, + }}, + }, + }, + }, + ), + }, + { + name: "type-hinted build artifacts", + obj: objects.NewTaskRunObjectV1( + &v1.TaskRun{ + Status: v1.TaskRunStatus{ + TaskRunStatusFields: v1.TaskRunStatusFields{ + Results: []v1.TaskRunResult{ + {Name: "result1_IMAGE_DIGEST", Value: *v1.NewStructuredValues("sha256:" + artifactDigest1)}, + {Name: "result1_IMAGE_URL", Value: *v1.NewStructuredValues(artifactURL1)}, + {Name: "IMAGES", Value: *v1.NewStructuredValues( + fmt.Sprintf("%v@sha256:%v", artifactURL1, artifactDigest1), + fmt.Sprintf("%vsha256:%v", artifactURL2, artifactDigest2))}, + {Name: "result2_ARTIFACT_OUTPUTS", Value: *v1.NewObject(map[string]string{ + "uri": artifactURL1, + "digest": "sha256:" + artifactDigest1, + "isBuildArtifact": "true", + })}, + }, + Steps: []v1.StepState{{ + Results: []v1.TaskRunStepResult{ + {Name: "result3_ARTIFACT_OUTPUTS", Value: *v1.NewObject(map[string]string{ + "uri": artifactURL2, + "digest": "sha256:" + artifactDigest2, + "isBuildArtifact": "true", + })}, + }, + }}, + }, + }, + }, + ), + expectedSubjects: []intoto.Subject{ + { + Name: artifactURL1, + Digest: map[string]string{ + "sha256": artifactDigest1, + }, + }, + { + Name: artifactURL2, + Digest: map[string]string{ + "sha256": artifactDigest2, + }, + }, + }, + }, + { + name: "no repetead type-hinted build artifacts", + obj: objects.NewTaskRunObjectV1( + &v1.TaskRun{ + Status: v1.TaskRunStatus{ + TaskRunStatusFields: v1.TaskRunStatusFields{ + Results: []v1.TaskRunResult{ + {Name: "result2_ARTIFACT_OUTPUTS", Value: *v1.NewObject(map[string]string{ + "uri": artifactURL1, + "digest": "sha256:" + artifactDigest1, + "isBuildArtifact": "true", + })}, + }, + Steps: []v1.StepState{{ + Results: []v1.TaskRunStepResult{ + {Name: "result3_ARTIFACT_OUTPUTS", Value: *v1.NewObject(map[string]string{ + "uri": artifactURL1, + "digest": "sha256:" + artifactDigest1, + "isBuildArtifact": "true", + })}, + }, + }}, + }, + }, + }, + ), + expectedSubjects: []intoto.Subject{ + { + Name: artifactURL1, + Digest: map[string]string{ + "sha256": artifactDigest1, + }, + }, + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + ctx := logtesting.TestContextWithLogger(t) + got := extract.SubjectsFromBuildArtifact(ctx, test.obj) + if diff := cmp.Diff(test.expectedSubjects, got); diff != "" { + t.Errorf("Wrong subjects from build artifacts, diff=%s", diff) + } + }) + } +} + func createTaskRunObjectWithResults(results map[string]string) objects.TektonObject { trResults := []v1.TaskRunResult{} prefix := 0 diff --git a/pkg/chains/formats/slsa/extract/v1beta1/extract.go b/pkg/chains/formats/slsa/extract/v1beta1/extract.go index cb630ba26f..52cfbe444b 100644 --- a/pkg/chains/formats/slsa/extract/v1beta1/extract.go +++ b/pkg/chains/formats/slsa/extract/v1beta1/extract.go @@ -121,7 +121,7 @@ func SubjectsFromTektonObjectV1Beta1(ctx context.Context, obj objects.TektonObje }) } - ssts := artifacts.ExtractStructuredTargetFromResults(ctx, obj, artifacts.ArtifactsOutputsResultName) + ssts := artifacts.ExtractStructuredTargetFromResults(ctx, obj.GetResults(), artifacts.ArtifactsOutputsResultName) for _, s := range ssts { splits := strings.Split(s.Digest, ":") alg := splits[0] diff --git a/pkg/chains/formats/slsa/internal/material/material.go b/pkg/chains/formats/slsa/internal/material/material.go index 6c29f4fe93..43593c9e05 100644 --- a/pkg/chains/formats/slsa/internal/material/material.go +++ b/pkg/chains/formats/slsa/internal/material/material.go @@ -255,16 +255,44 @@ func FromTaskParamsAndResults(ctx context.Context, tro *objects.TaskRunObjectV1) }) } - sms := artifacts.RetrieveMaterialsFromStructuredResults(ctx, tro, artifacts.ArtifactsInputsResultName) + sms := artifacts.RetrieveMaterialsFromStructuredResults(ctx, tro.GetResults()) mats = artifact.AppendMaterials(mats, sms...) return mats } +// FromStepActionsResults extracts type hinted results from StepActions associated with the TaskRun and adds the url and digest to materials. +func FromStepActionsResults(ctx context.Context, tro *objects.TaskRunObjectV1) (mats []common.ProvenanceMaterial) { + for _, s := range tro.Status.Steps { + var sCommit, sURL string + for _, r := range s.Results { + if r.Name == attest.CommitParam { + sCommit = r.Value.StringVal + continue + } + + if r.Name == attest.URLParam { + sURL = r.Value.StringVal + } + } + + sURL = attest.SPDXGit(sURL, "") + if sCommit != "" && sURL != "" { + mats = artifact.AppendMaterials(mats, common.ProvenanceMaterial{ + URI: sURL, + Digest: map[string]string{"sha1": sCommit}, + }) + } + } + sms := artifacts.RetrieveMaterialsFromStructuredResults(ctx, tro.GetNestedResults()) + mats = artifact.AppendMaterials(mats, sms...) + return +} + // FromPipelineParamsAndResults extracts type hinted params and results and adds the url and digest to materials. func FromPipelineParamsAndResults(ctx context.Context, pro *objects.PipelineRunObjectV1, slsaconfig *slsaconfig.SlsaConfig) []common.ProvenanceMaterial { mats := []common.ProvenanceMaterial{} - sms := artifacts.RetrieveMaterialsFromStructuredResults(ctx, pro, artifacts.ArtifactsInputsResultName) + sms := artifacts.RetrieveMaterialsFromStructuredResults(ctx, pro.GetResults()) mats = artifact.AppendMaterials(mats, sms...) var commit, url string diff --git a/pkg/chains/formats/slsa/internal/material/material_test.go b/pkg/chains/formats/slsa/internal/material/material_test.go index 9bd827aa8e..27ede6a389 100644 --- a/pkg/chains/formats/slsa/internal/material/material_test.go +++ b/pkg/chains/formats/slsa/internal/material/material_test.go @@ -598,6 +598,161 @@ func TestFromPipelineParamsAndResults(t *testing.T) { } } +func TestFromStepActionsResults(t *testing.T) { + tests := []struct { + name string + steps []v1.StepState + expected []common.ProvenanceMaterial + }{ + { + name: "no type-hint input", + steps: []v1.StepState{ + { + Results: []v1.TaskRunStepResult{ + {Name: "result1_ARTIFACT_URI", Value: *v1.NewStructuredValues("gcr.io/foo/bar1")}, + {Name: "result1_ARTIFACT_DIGEST", Value: *v1.NewStructuredValues(digest)}, + {Name: "result2_IMAGE_URL", Value: *v1.NewStructuredValues("gcr.io/foo/bar2")}, + {Name: "result2_IMAGE_DIGEST", Value: *v1.NewStructuredValues(digest)}, + }, + }, + { + Results: []v1.TaskRunStepResult{ + {Name: "result3_ARTIFACT_OUTPUTS", Value: *v1.NewObject(map[string]string{ + "uri": "gcr.io/foo/bar1", + "digest": digest, + })}, + }, + }, + }, + }, + { + name: "git result type-hint input", + steps: []v1.StepState{ + { + Results: []v1.TaskRunStepResult{ + {Name: "CHAINS-GIT_URL", Value: *v1.NewStructuredValues("https://github.com/org/repo1")}, + {Name: "CHAINS-GIT_COMMIT", Value: *v1.NewStructuredValues("a3efeffe520230f3608b8fc41f7807cbf19a472d")}, + }, + }, + { + Results: []v1.TaskRunStepResult{ + {Name: "CHAINS-GIT_URL", Value: *v1.NewStructuredValues("https://github.com/org/repo2")}, + {Name: "CHAINS-GIT_COMMIT", Value: *v1.NewStructuredValues("05669ed367ed21569f68edee8b93c64eda91e910")}, + }, + }, + }, + expected: []common.ProvenanceMaterial{ + { + URI: artifacts.GitSchemePrefix + "https://github.com/org/repo1.git", + Digest: common.DigestSet{ + "sha1": "a3efeffe520230f3608b8fc41f7807cbf19a472d", + }, + }, + { + URI: artifacts.GitSchemePrefix + "https://github.com/org/repo2.git", + Digest: common.DigestSet{ + "sha1": "05669ed367ed21569f68edee8b93c64eda91e910", + }, + }, + }, + }, + { + name: "object result type-hint input", + steps: []v1.StepState{ + { + Results: []v1.TaskRunStepResult{ + {Name: "res1_ARTIFACT_INPUTS", Value: *v1.NewObject(map[string]string{ + "uri": "https://github.com/tektoncd/pipeline", + "digest": "sha1:7f2f46e1b97df36b2b82d1b1d87c81b8b3d21601", + })}, + }, + }, + { + Results: []v1.TaskRunStepResult{ + {Name: "res2_ARTIFACT_INPUTS", Value: *v1.NewObject(map[string]string{ + "uri": "https://github.com/org/repo2", + "digest": "sha1:05669ed367ed21569f68edee8b93c64eda91e910", + })}, + }, + }, + }, + expected: []common.ProvenanceMaterial{ + { + URI: "https://github.com/tektoncd/pipeline", + Digest: common.DigestSet{ + "sha1": "7f2f46e1b97df36b2b82d1b1d87c81b8b3d21601", + }, + }, + { + URI: "https://github.com/org/repo2", + Digest: common.DigestSet{ + "sha1": "05669ed367ed21569f68edee8b93c64eda91e910", + }, + }, + }, + }, + { + name: "no repeated inputs", + steps: []v1.StepState{ + { + Results: []v1.TaskRunStepResult{ + {Name: "CHAINS-GIT_URL", Value: *v1.NewStructuredValues("https://github.com/tektoncd/pipeline")}, + {Name: "CHAINS-GIT_COMMIT", Value: *v1.NewStructuredValues("7f2f46e1b97df36b2b82d1b1d87c81b8b3d21601")}, + {Name: "res1_ARTIFACT_INPUTS", Value: *v1.NewObject(map[string]string{ + "uri": "https://github.com/tektoncd/pipeline", + "digest": "sha1:7f2f46e1b97df36b2b82d1b1d87c81b8b3d21601", + })}, + }, + }, + { + Results: []v1.TaskRunStepResult{ + {Name: "CHAINS-GIT_URL", Value: *v1.NewStructuredValues("https://github.com/tektoncd/pipeline")}, + {Name: "CHAINS-GIT_COMMIT", Value: *v1.NewStructuredValues("7f2f46e1b97df36b2b82d1b1d87c81b8b3d21601")}, + {Name: "res1_ARTIFACT_INPUTS", Value: *v1.NewObject(map[string]string{ + "uri": "https://github.com/tektoncd/pipeline", + "digest": "sha1:7f2f46e1b97df36b2b82d1b1d87c81b8b3d21601", + })}, + }, + }, + }, + expected: []common.ProvenanceMaterial{ + { + URI: "https://github.com/tektoncd/pipeline", + Digest: common.DigestSet{ + "sha1": "7f2f46e1b97df36b2b82d1b1d87c81b8b3d21601", + }, + }, + { + URI: artifacts.GitSchemePrefix + "https://github.com/tektoncd/pipeline.git", + Digest: common.DigestSet{ + "sha1": "7f2f46e1b97df36b2b82d1b1d87c81b8b3d21601", + }, + }, + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + ctx := logtesting.TestContextWithLogger(t) + tr := objects.NewTaskRunObjectV1( + &v1.TaskRun{ + Status: v1.TaskRunStatus{ + TaskRunStatusFields: v1.TaskRunStatusFields{ + Steps: test.steps, + }, + }, + }, + ) + + got := FromStepActionsResults(ctx, tr) + if diff := cmp.Diff(test.expected, got, compare.MaterialsCompareOption()); diff != "" { + t.Errorf("FromStepActionsResults(): -want +got: %s", diff) + } + }) + } +} + //nolint:all func createProWithPipelineParamAndTaskResult() *objects.PipelineRunObjectV1 { pro := objects.NewPipelineRunObjectV1(&v1.PipelineRun{ diff --git a/pkg/chains/formats/slsa/internal/material/v1beta1/material.go b/pkg/chains/formats/slsa/internal/material/v1beta1/material.go index 3bcec5480b..ef49f2b73c 100644 --- a/pkg/chains/formats/slsa/internal/material/v1beta1/material.go +++ b/pkg/chains/formats/slsa/internal/material/v1beta1/material.go @@ -250,7 +250,7 @@ func FromTaskParamsAndResults(ctx context.Context, tro *objects.TaskRunObjectV1B }) } - sms := artifacts.RetrieveMaterialsFromStructuredResults(ctx, tro, artifacts.ArtifactsInputsResultName) + sms := artifacts.RetrieveMaterialsFromStructuredResults(ctx, tro.GetResults()) mats = artifact.AppendMaterials(mats, sms...) return mats @@ -259,7 +259,7 @@ func FromTaskParamsAndResults(ctx context.Context, tro *objects.TaskRunObjectV1B // FromPipelineParamsAndResults extracts type hinted params and results and adds the url and digest to materials. func FromPipelineParamsAndResults(ctx context.Context, pro *objects.PipelineRunObjectV1Beta1, slsaconfig *slsaconfig.SlsaConfig) []common.ProvenanceMaterial { mats := []common.ProvenanceMaterial{} - sms := artifacts.RetrieveMaterialsFromStructuredResults(ctx, pro, artifacts.ArtifactsInputsResultName) + sms := artifacts.RetrieveMaterialsFromStructuredResults(ctx, pro.GetResults()) mats = artifact.AppendMaterials(mats, sms...) var commit, url string diff --git a/pkg/chains/formats/slsa/internal/resolved_dependencies/resolved_dependencies.go b/pkg/chains/formats/slsa/internal/resolved_dependencies/resolved_dependencies.go index 2cc1a8b60c..d1b8a2e724 100644 --- a/pkg/chains/formats/slsa/internal/resolved_dependencies/resolved_dependencies.go +++ b/pkg/chains/formats/slsa/internal/resolved_dependencies/resolved_dependencies.go @@ -51,6 +51,12 @@ const ( // and AddSLSATaskDescriptor type addTaskDescriptorContent func(*objects.TaskRunObjectV1) (*slsa.ResourceDescriptor, error) //nolint:staticcheck +// ResolveOptions represents the configuration to be use to resolve dependencies. +type ResolveOptions struct { + // Indicates if nested type-hinted results (e.g, from step actions) should be read to resolve dependecies. + WithNestedResults bool +} + // ConvertMaterialToResolvedDependency converts a SLSAv0.2 Material to a resolved dependency func ConvertMaterialsToResolvedDependencies(mats []common.ProvenanceMaterial, name string) []slsa.ResourceDescriptor { rds := []slsa.ResourceDescriptor{} @@ -183,7 +189,7 @@ func fromPipelineTask(logger *zap.SugaredLogger, pro *objects.PipelineRunObjectV } // taskDependencies gather all dependencies in a task and adds them to resolvedDependencies -func taskDependencies(ctx context.Context, tro *objects.TaskRunObjectV1) ([]slsa.ResourceDescriptor, error) { +func taskDependencies(ctx context.Context, opts ResolveOptions, tro *objects.TaskRunObjectV1) ([]slsa.ResourceDescriptor, error) { var resolvedDependencies []slsa.ResourceDescriptor var err error mats := []common.ProvenanceMaterial{} @@ -202,6 +208,11 @@ func taskDependencies(ctx context.Context, tro *objects.TaskRunObjectV1) ([]slsa mats = append(mats, sidecarMaterials...) resolvedDependencies = append(resolvedDependencies, ConvertMaterialsToResolvedDependencies(mats, "")...) + if opts.WithNestedResults { + mats = material.FromStepActionsResults(ctx, tro) + resolvedDependencies = append(resolvedDependencies, ConvertMaterialsToResolvedDependencies(mats, InputResultName)...) + } + mats = material.FromTaskParamsAndResults(ctx, tro) // convert materials to resolved dependencies resolvedDependencies = append(resolvedDependencies, ConvertMaterialsToResolvedDependencies(mats, InputResultName)...) @@ -240,7 +251,7 @@ func taskDependencies(ctx context.Context, tro *objects.TaskRunObjectV1) ([]slsa } // TaskRun constructs `predicate.resolvedDependencies` section by collecting all the artifacts that influence a taskrun such as source code repo and step&sidecar base images. -func TaskRun(ctx context.Context, tro *objects.TaskRunObjectV1) ([]slsa.ResourceDescriptor, error) { +func TaskRun(ctx context.Context, opts ResolveOptions, tro *objects.TaskRunObjectV1) ([]slsa.ResourceDescriptor, error) { var resolvedDependencies []slsa.ResourceDescriptor var err error @@ -254,7 +265,7 @@ func TaskRun(ctx context.Context, tro *objects.TaskRunObjectV1) ([]slsa.Resource resolvedDependencies = append(resolvedDependencies, rd) } - rds, err := taskDependencies(ctx, tro) + rds, err := taskDependencies(ctx, opts, tro) if err != nil { return nil, err } diff --git a/pkg/chains/formats/slsa/internal/resolved_dependencies/resolved_dependencies_test.go b/pkg/chains/formats/slsa/internal/resolved_dependencies/resolved_dependencies_test.go index 8e9d0f88ab..f61e09b6a6 100644 --- a/pkg/chains/formats/slsa/internal/resolved_dependencies/resolved_dependencies_test.go +++ b/pkg/chains/formats/slsa/internal/resolved_dependencies/resolved_dependencies_test.go @@ -301,9 +301,10 @@ func tektonTaskRuns() map[string][]byte { func TestTaskRun(t *testing.T) { tests := []struct { - name string - obj objects.TektonObject //nolint:staticcheck - want []v1slsa.ResourceDescriptor + name string + obj objects.TektonObject //nolint:staticcheck + resolveOpts ResolveOptions + want []v1slsa.ResourceDescriptor }{ { name: "resolvedDependencies from pipeline resources", @@ -459,6 +460,12 @@ func TestTaskRun(t *testing.T) { Status: v1.TaskRunStatus{ TaskRunStatusFields: v1.TaskRunStatusFields{ Steps: []v1.StepState{{ + Results: []v1.TaskRunStepResult{ + {Name: "res1_ARTIFACT_INPUTS", Value: *v1.NewObject(map[string]string{ + "uri": "https://github.com/tektoncd/pipeline", + "digest": "sha1:7f2f46e1b97df36b2b82d1b1d87c81b8b3d21601", + })}, + }, Name: "git-source-repo-jwqcl", ImageID: "gcr.io/tekton-releases/github.com/tektoncd/pipeline/cmd/git-init@sha256:b963f6e7a69617db57b685893256f978436277094c21d43b153994acd8a01247", }, { @@ -493,7 +500,64 @@ func TestTaskRun(t *testing.T) { }, }, }, - }} + }, + { + name: "resolvedDependencies with nested results", + resolveOpts: ResolveOptions{ + WithNestedResults: true, + }, + obj: objects.NewTaskRunObjectV1(&v1.TaskRun{ //nolint:staticcheck + Status: v1.TaskRunStatus{ + TaskRunStatusFields: v1.TaskRunStatusFields{ + Steps: []v1.StepState{{ + Results: []v1.TaskRunStepResult{ + {Name: "res1_ARTIFACT_INPUTS", Value: *v1.NewObject(map[string]string{ + "uri": "https://github.com/tektoncd/pipeline", + "digest": "sha1:7f2f46e1b97df36b2b82d1b1d87c81b8b3d21601", + })}, + }, + Name: "git-source-repo-jwqcl", + ImageID: "gcr.io/tekton-releases/github.com/tektoncd/pipeline/cmd/git-init@sha256:b963f6e7a69617db57b685893256f978436277094c21d43b153994acd8a01247", + }, { + Name: "git-source-repo-repeat-again-jwqcl", + ImageID: "gcr.io/tekton-releases/github.com/tektoncd/pipeline/cmd/git-init@sha256:b963f6e7a69617db57b685893256f978436277094c21d43b153994acd8a01247", + }, { + Name: "build", + ImageID: "gcr.io/cloud-marketplace-containers/google/bazel@sha256:010a1ecd1a8c3610f12039a25b823e3a17bd3e8ae455a53e340dcfdd37a49964", + }}, + Sidecars: []v1.SidecarState{{ + Name: "sidecar-jwqcl", + ImageID: "gcr.io/tekton-releases/github.com/tektoncd/pipeline/cmd/sidecar-git-init@sha256:a1234f6e7a69617db57b685893256f978436277094c21d43b153994acd8a09567", + }}, + }, + }, + }), + want: []v1slsa.ResourceDescriptor{ + { + URI: "oci://gcr.io/tekton-releases/github.com/tektoncd/pipeline/cmd/git-init", + Digest: common.DigestSet{ + "sha256": "b963f6e7a69617db57b685893256f978436277094c21d43b153994acd8a01247", + }, + }, { + URI: "oci://gcr.io/cloud-marketplace-containers/google/bazel", + Digest: common.DigestSet{ + "sha256": "010a1ecd1a8c3610f12039a25b823e3a17bd3e8ae455a53e340dcfdd37a49964", + }, + }, { + URI: "oci://gcr.io/tekton-releases/github.com/tektoncd/pipeline/cmd/sidecar-git-init", + Digest: common.DigestSet{ + "sha256": "a1234f6e7a69617db57b685893256f978436277094c21d43b153994acd8a09567", + }, + }, { + Name: "inputs/result", + URI: "https://github.com/tektoncd/pipeline", + Digest: common.DigestSet{ + "sha1": "7f2f46e1b97df36b2b82d1b1d87c81b8b3d21601", + }, + }, + }, + }, + } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { ctx := logtesting.TestContextWithLogger(t) @@ -517,7 +581,7 @@ func TestTaskRun(t *testing.T) { } } - rd, err := TaskRun(ctx, input) + rd, err := TaskRun(ctx, tc.resolveOpts, input) if err != nil { t.Fatalf("Did not expect an error but got %v", err) } diff --git a/pkg/chains/formats/slsa/testdata/slsa-v2alpha4/taskrun-multiple-subjects.json b/pkg/chains/formats/slsa/testdata/slsa-v2alpha4/taskrun-multiple-subjects.json new file mode 100644 index 0000000000..5175226f71 --- /dev/null +++ b/pkg/chains/formats/slsa/testdata/slsa-v2alpha4/taskrun-multiple-subjects.json @@ -0,0 +1,72 @@ +{ + "spec": { + "params": [], + "taskRef": { + "name": "test-task", + "kind": "Task" + }, + "serviceAccountName": "default" + }, + "status": { + "conditions": [ + { + "type": "Succeeded", + "status": "True", + "lastTransitionTime": "2021-03-29T09:50:15Z", + "reason": "Succeeded", + "message": "All Steps have completed executing" + } + ], + "podName": "test-pod-name", + "steps": [ + { + "name": "step1", + "container": "step-step1", + "imageID": "docker-pullable://gcr.io/test1/test1@sha256:d4b63d3e24d6eef04a6dc0795cf8a73470688803d97c52cffa3c8d4efd3397b6" + } + ], + "results": [ + { + "name": "IMAGES", + "value": "gcr.io/myimage1@sha256:d4b63d3e24d6eef04a6dc0795cf8a73470688803d97c52cffa3c8d4efd3397b6,gcr.io/myimage2@sha256:daa1a56e13c85cf164e7d9e595006649e3a04c47fe4a8261320e18a0bf3b0367" + }, + { + "name": "result1_ARTIFACT_OUTPUTS", + "value": { + "uri": "gcr.io/foo/bar", + "digest": "sha256:d4b63d3e24d6eef04a6dc0795cf8a73470688803d97c52cffa3c8d4efd3397b6", + "isBuildArtifact": "true" + } + }, + { + "name": "result2_ARTIFACT_OUTPUTS", + "value": { + "uri": "gcr.io/myimage2", + "digest": "sha256:daa1a56e13c85cf164e7d9e595006649e3a04c47fe4a8261320e18a0bf3b0367", + "isBuildArtifact": "true" + } + } + ], + "taskSpec": { + "params": [], + "results": [ + { + "name": "file1_DIGEST", + "description": "Digest of a file to push." + }, + { + "name": "file1", + "description": "some assembled file" + }, + { + "name": "file2_DIGEST", + "description": "Digest of a file to push." + }, + { + "name": "file2", + "description": "some assembled file" + } + ] + } + } +} diff --git a/pkg/chains/formats/slsa/testdata/slsa-v2alpha4/taskrun1.json b/pkg/chains/formats/slsa/testdata/slsa-v2alpha4/taskrun1.json new file mode 100644 index 0000000000..200f89ebd0 --- /dev/null +++ b/pkg/chains/formats/slsa/testdata/slsa-v2alpha4/taskrun1.json @@ -0,0 +1,166 @@ +{ + "metadata": { + "name": "taskrun-build", + "labels": { + "tekton.dev/pipelineTask": "build" + }, + "uid": "abhhf-12354-asjsdbjs23-3435353n" + }, + "spec": { + "params": [ + { + "name": "IMAGE", + "value": "test.io/test/image" + }, + { + "name": "CHAINS-GIT_COMMIT", + "value": "taskrun" + }, + { + "name": "CHAINS-GIT_URL", + "value": "https://git.test.com" + } + ], + "taskRef": { + "name": "build", + "kind": "Task" + }, + "serviceAccountName": "default" + }, + "status": { + "startTime": "2021-03-29T09:50:00Z", + "completionTime": "2021-03-29T09:50:15Z", + "conditions": [ + { + "type": "Succeeded", + "status": "True", + "lastTransitionTime": "2021-03-29T09:50:15Z", + "reason": "Succeeded", + "message": "All Steps have completed executing" + } + ], + "podName": "test-pod-name", + "steps": [ + { + "name": "step1", + "container": "step-step1", + "imageID": "docker-pullable://gcr.io/test1/test1@sha256:d4b63d3e24d6eef04a6dc0795cf8a73470688803d97c52cffa3c8d4efd3397b6", + "results": [ + { + "name": "step1_result1", + "value": "result-value" + } + ] + }, + { + "name": "step2", + "container": "step-step2", + "imageID": "docker-pullable://gcr.io/test2/test2@sha256:4d6dd704ef58cb214dd826519929e92a978a57cdee43693006139c0080fd6fac", + "results": [ + { + "name": "step1_result1-ARTIFACT_OUTPUTS", + "value": { + "uri": "gcr.io/my/image/fromstep2", + "digest": "sha256:827521c857fdcd4374f4da5442fbae2edb01e7fbae285c3ec15673d4c1daecb7" + } + } + ] + }, + { + "name": "step3", + "container": "step-step3", + "imageID": "docker-pullable://gcr.io/test3/test3@sha256:f1a8b8549c179f41e27ff3db0fe1a1793e4b109da46586501a8343637b1d0478", + "results": [ + { + "name": "step3_result1-ARTIFACT_OUTPUTS", + "value": { + "uri": "gcr.io/my/image/fromstep3", + "digest": "sha256:827521c857fdcd4374f4da5442fbae2edb01e7fbae285c3ec15673d4c1daecb7", + "isBuildArtifact": "true" + } + } + ] + } + ], + "results": [ + { + "name": "IMAGE_DIGEST", + "value": "sha256:827521c857fdcd4374f4da5442fbae2edb01e7fbae285c3ec15673d4c1daecb7" + }, + { + "name": "IMAGE_URL", + "value": "gcr.io/my/image" + } + ], + "taskSpec": { + "params": [ + { + "name": "IMAGE", + "type": "string" + }, + { + "name": "filename", + "type": "string" + }, + { + "name": "DOCKERFILE", + "type": "string" + }, + { + "name": "CONTEXT", + "type": "string" + }, + { + "name": "EXTRA_ARGS", + "type": "string" + }, + { + "name": "BUILDER_IMAGE", + "type": "string" + }, { + "name": "CHAINS-GIT_COMMIT", + "type": "string", + "default": "task" + }, { + "name": "CHAINS-GIT_URL", + "type": "string", + "default": "https://defaultgit.test.com" + } + ], + "steps": [ + { + "name": "step1" + }, + { + "name": "step2" + }, + { + "name": "step3" + } + ], + "results": [ + { + "name": "IMAGE_DIGEST", + "description": "Digest of the image just built." + }, + { + "name": "filename_DIGEST", + "description": "Digest of the file just built." + } + ] + }, + "provenance": { + "refSource": { + "uri": "git+https://github.com/test", + "digest": { + "sha1": "ab123" + }, + "entryPoint": "build.yaml" + }, + "featureFlags": { + "EnableAPIFields": "beta", + "ResultExtractionMethod": "termination-message" + } + } + } +} diff --git a/pkg/chains/formats/slsa/testdata/slsa-v2alpha4/taskrun2.json b/pkg/chains/formats/slsa/testdata/slsa-v2alpha4/taskrun2.json new file mode 100644 index 0000000000..2c162a703e --- /dev/null +++ b/pkg/chains/formats/slsa/testdata/slsa-v2alpha4/taskrun2.json @@ -0,0 +1,115 @@ +{ + "metadata": { + "name": "git-clone", + "labels": { + "tekton.dev/pipelineTask": "git-clone" + }, + "uid": "abhhf-12354-asjsdbjs23-3435353n" + }, + "spec": { + "params": [ + { + "name": "url", + "value": "https://git.test.com" + }, + { + "name": "revision", + "value": "" + } + ], + "taskRef": { + "name": "git-clone", + "kind": "Task" + }, + "serviceAccountName": "default" + }, + "status": { + "startTime": "2021-03-29T09:50:00Z", + "completionTime": "2021-03-29T09:50:15Z", + "conditions": [ + { + "type": "Succeeded", + "status": "True", + "lastTransitionTime": "2021-03-29T09:50:15Z", + "reason": "Succeeded", + "message": "All Steps have completed executing" + } + ], + "podName": "test-pod-name", + "steps": [ + { + "name": "step1", + "container": "step-step1", + "imageID": "docker-pullable://gcr.io/test1/test1@sha256:d4b63d3e24d6eef04a6dc0795cf8a73470688803d97c52cffa3c8d4efd3397b6", + "results": [ + { + "name": "step1_result1-ARTIFACT_INPUTS", + "value": { + "uri": "https://github.com/tektoncd/pipeline", + "digest": "sha1:7f2f46e1b97df36b2b82d1b1d87c81b8b3d21601" + } + } + ] + } + ], + "results": [ + { + "name": "some-uri_DIGEST", + "value": "sha256:d4b63d3e24d6eef04a6dc0795cf8a73470688803d97c52cffa3c8d4efd3397b6" + }, + { + "name": "some-uri", + "value": "pkg:deb/debian/curl@7.50.3-1" + } + ], + "taskSpec": { + "steps": [ + { + "env": [ + { + "name": "HOME", + "value": "$(params.userHome)" + }, + { + "name": "PARAM_URL", + "value": "$(params.url)" + } + ], + "name": "step1", + "script": "git clone" + } + ], + "params": [ + { + "name": "CHAINS-GIT_COMMIT", + "type": "string", + "default": "sha:taskdefault" + }, + { + "name": "CHAINS-GIT_URL", + "type": "string", + "default": "https://git.test.com" + } + ], + "results": [ + { + "name": "some-uri_DIGEST", + "description": "Digest of a file to push." + }, + { + "name": "some-uri", + "description": "some calculated uri" + } + ] + }, + "provenance": { + "refSource": { + "uri": "git+https://github.com/catalog", + "digest": { + "sha1": "x123" + }, + "entryPoint": "git-clone.yaml" + } + } + } +} diff --git a/pkg/chains/formats/slsa/v2alpha3/internal/taskrun/taskrun.go b/pkg/chains/formats/slsa/v2alpha3/internal/taskrun/taskrun.go index 793a732db7..ca1c5b91a1 100644 --- a/pkg/chains/formats/slsa/v2alpha3/internal/taskrun/taskrun.go +++ b/pkg/chains/formats/slsa/v2alpha3/internal/taskrun/taskrun.go @@ -106,7 +106,7 @@ func getBuildDefinition(ctx context.Context, buildType string, tro *objects.Task switch buildDefinitionType { case buildtypes.SlsaBuildType: - rd, err := resolveddependencies.TaskRun(ctx, tro) + rd, err := resolveddependencies.TaskRun(ctx, resolveddependencies.ResolveOptions{}, tro) if err != nil { return slsa.ProvenanceBuildDefinition{}, err } @@ -117,7 +117,7 @@ func getBuildDefinition(ctx context.Context, buildType string, tro *objects.Task ResolvedDependencies: rd, }, nil case buildtypes.TektonBuildType: - rd, err := resolveddependencies.TaskRun(ctx, tro) + rd, err := resolveddependencies.TaskRun(ctx, resolveddependencies.ResolveOptions{}, tro) if err != nil { return slsa.ProvenanceBuildDefinition{}, err } diff --git a/pkg/chains/formats/slsa/v2alpha3/internal/taskrun/taskrun_test.go b/pkg/chains/formats/slsa/v2alpha3/internal/taskrun/taskrun_test.go index 1606150399..63a92f2e7a 100644 --- a/pkg/chains/formats/slsa/v2alpha3/internal/taskrun/taskrun_test.go +++ b/pkg/chains/formats/slsa/v2alpha3/internal/taskrun/taskrun_test.go @@ -239,7 +239,7 @@ func TestTaskRunGenerateAttestation(t *testing.T) { } func getResolvedDependencies(tro *objects.TaskRunObjectV1) []v1resourcedescriptor.ResourceDescriptor { - rd, err := resolveddependencies.TaskRun(context.Background(), tro) + rd, err := resolveddependencies.TaskRun(context.Background(), resolveddependencies.ResolveOptions{}, tro) if err != nil { return []v1resourcedescriptor.ResourceDescriptor{} } diff --git a/pkg/chains/formats/slsa/v2alpha4/internal/taskrun/taskrun.go b/pkg/chains/formats/slsa/v2alpha4/internal/taskrun/taskrun.go new file mode 100644 index 0000000000..028773827b --- /dev/null +++ b/pkg/chains/formats/slsa/v2alpha4/internal/taskrun/taskrun.go @@ -0,0 +1,166 @@ +/* +Copyright 2024 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 taskrun + +import ( + "context" + "encoding/json" + "fmt" + + intoto "github.com/in-toto/in-toto-golang/in_toto" + slsa "github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/v1" + "github.com/tektoncd/chains/pkg/artifacts" + "github.com/tektoncd/chains/pkg/chains/formats/slsa/extract" + buildtypes "github.com/tektoncd/chains/pkg/chains/formats/slsa/internal/build_types" + externalparameters "github.com/tektoncd/chains/pkg/chains/formats/slsa/internal/external_parameters" + internalparameters "github.com/tektoncd/chains/pkg/chains/formats/slsa/internal/internal_parameters" + resolveddependencies "github.com/tektoncd/chains/pkg/chains/formats/slsa/internal/resolved_dependencies" + "github.com/tektoncd/chains/pkg/chains/formats/slsa/internal/slsaconfig" + "github.com/tektoncd/chains/pkg/chains/objects" +) + +const ( + taskRunResults = "taskRunResults/%s" + taskRunStepResults = "stepResults/%s" +) + +// GenerateAttestation returns the provenance for the given taskrun in SALSA 1.0 format. +func GenerateAttestation(ctx context.Context, tro *objects.TaskRunObjectV1, slsaConfig *slsaconfig.SlsaConfig) (interface{}, error) { + bp, err := byproducts(tro) + if err != nil { + return nil, err + } + + bd, err := getBuildDefinition(ctx, slsaConfig.BuildType, tro) + if err != nil { + return nil, err + } + + att := intoto.ProvenanceStatementSLSA1{ + StatementHeader: intoto.StatementHeader{ + Type: intoto.StatementInTotoV01, + PredicateType: slsa.PredicateSLSAProvenance, + Subject: extract.SubjectsFromBuildArtifact(ctx, tro), + }, + Predicate: slsa.ProvenancePredicate{ + BuildDefinition: bd, + RunDetails: slsa.ProvenanceRunDetails{ + Builder: slsa.Builder{ + ID: slsaConfig.BuilderID, + }, + BuildMetadata: metadata(tro), + Byproducts: bp, + }, + }, + } + + return att, nil +} + +func byproducts(tro *objects.TaskRunObjectV1) ([]slsa.ResourceDescriptor, error) { + byProd := []slsa.ResourceDescriptor{} + + results, err := getResultsWithoutBuildArtifacts(tro.GetResults(), taskRunResults) + if err != nil { + return nil, err + } + byProd = append(byProd, results...) + + results, err = getResultsWithoutBuildArtifacts(tro.GetNestedResults(), taskRunStepResults) + if err != nil { + return nil, err + } + byProd = append(byProd, results...) + + return byProd, nil +} + +func getResultsWithoutBuildArtifacts(results []objects.Result, resultTypePrefix string) ([]slsa.ResourceDescriptor, error) { + byProd := []slsa.ResourceDescriptor{} + for _, r := range results { + if isBuildArt, err := artifacts.IsBuildArtifact(r); err != nil { + return nil, err + } else if isBuildArt { + continue + } + + content, err := json.Marshal(r.Value) + if err != nil { + return nil, err + } + + byProd = append(byProd, slsa.ResourceDescriptor{ + Name: fmt.Sprintf(resultTypePrefix, r.Name), + Content: content, + MediaType: "application/json", + }) + } + + return byProd, nil +} + +func metadata(tro *objects.TaskRunObjectV1) slsa.BuildMetadata { + m := slsa.BuildMetadata{ + InvocationID: string(tro.ObjectMeta.UID), + } + if tro.Status.StartTime != nil { + utc := tro.Status.StartTime.Time.UTC() + m.StartedOn = &utc + } + if tro.Status.CompletionTime != nil { + utc := tro.Status.CompletionTime.Time.UTC() + m.FinishedOn = &utc + } + return m +} + +// getBuildDefinition get the buildDefinition based on the configured buildType. This will default to the slsa buildType +func getBuildDefinition(ctx context.Context, buildType string, tro *objects.TaskRunObjectV1) (slsa.ProvenanceBuildDefinition, error) { + // if buildType is not set in the chains-config, default to slsa build type + buildDefinitionType := buildType + if buildType == "" { + buildDefinitionType = buildtypes.SlsaBuildType + } + + resolveDependenciesOpts := resolveddependencies.ResolveOptions{ + WithNestedResults: true, + } + + switch buildDefinitionType { + case buildtypes.SlsaBuildType: + rd, err := resolveddependencies.TaskRun(ctx, resolveDependenciesOpts, tro) + if err != nil { + return slsa.ProvenanceBuildDefinition{}, err + } + return slsa.ProvenanceBuildDefinition{ + BuildType: buildDefinitionType, + ExternalParameters: externalparameters.TaskRun(tro), + InternalParameters: internalparameters.SLSAInternalParameters(tro), + ResolvedDependencies: rd, + }, nil + case buildtypes.TektonBuildType: + rd, err := resolveddependencies.TaskRun(ctx, resolveDependenciesOpts, tro) + if err != nil { + return slsa.ProvenanceBuildDefinition{}, err + } + return slsa.ProvenanceBuildDefinition{ + BuildType: buildDefinitionType, + ExternalParameters: externalparameters.TaskRun(tro), + InternalParameters: internalparameters.TektonInternalParameters(tro), + ResolvedDependencies: rd, + }, nil + default: + return slsa.ProvenanceBuildDefinition{}, fmt.Errorf("unsupported buildType %v", buildType) + } +} diff --git a/pkg/chains/formats/slsa/v2alpha4/internal/taskrun/taskrun_test.go b/pkg/chains/formats/slsa/v2alpha4/internal/taskrun/taskrun_test.go new file mode 100644 index 0000000000..28ef79fb38 --- /dev/null +++ b/pkg/chains/formats/slsa/v2alpha4/internal/taskrun/taskrun_test.go @@ -0,0 +1,343 @@ +/* +Copyright 2024 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 taskrun + +import ( + "context" + "encoding/json" + "testing" + "time" + + "github.com/google/go-cmp/cmp" + "github.com/in-toto/in-toto-golang/in_toto" + "github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/common" + slsa "github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/v1" + + v1resourcedescriptor "github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/v1" + externalparameters "github.com/tektoncd/chains/pkg/chains/formats/slsa/internal/external_parameters" + internalparameters "github.com/tektoncd/chains/pkg/chains/formats/slsa/internal/internal_parameters" + resolveddependencies "github.com/tektoncd/chains/pkg/chains/formats/slsa/internal/resolved_dependencies" + "github.com/tektoncd/chains/pkg/chains/formats/slsa/internal/slsaconfig" + + "github.com/tektoncd/chains/pkg/chains/objects" + "github.com/tektoncd/chains/pkg/internal/objectloader" + "github.com/tektoncd/pipeline/pkg/apis/config" + v1 "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + logtesting "knative.dev/pkg/logging/testing" +) + +const jsonMediaType = "application/json" + +func TestMetadata(t *testing.T) { + tr := &v1.TaskRun{ //nolint:staticcheck + ObjectMeta: metav1.ObjectMeta{ + Name: "my-taskrun", + Namespace: "my-namespace", + Annotations: map[string]string{ + "chains.tekton.dev/reproducible": "true", + }, + UID: "abhhf-12354-asjsdbjs23-3435353n", + }, + Status: v1.TaskRunStatus{ + TaskRunStatusFields: v1.TaskRunStatusFields{ + StartTime: &metav1.Time{Time: time.Date(1995, time.December, 24, 6, 12, 12, 12, time.UTC)}, + CompletionTime: &metav1.Time{Time: time.Date(1995, time.December, 24, 6, 12, 12, 24, time.UTC)}, + }, + }, + } + start := time.Date(1995, time.December, 24, 6, 12, 12, 12, time.UTC) + end := time.Date(1995, time.December, 24, 6, 12, 12, 24, time.UTC) + want := slsa.BuildMetadata{ + InvocationID: "abhhf-12354-asjsdbjs23-3435353n", + StartedOn: &start, + FinishedOn: &end, + } + got := metadata(objects.NewTaskRunObjectV1(tr)) + if d := cmp.Diff(want, got); d != "" { + t.Fatalf("metadata (-want, +got):\n%s", d) + } +} + +func TestMetadataInTimeZone(t *testing.T) { + tz := time.FixedZone("Test Time", int((12 * time.Hour).Seconds())) + tr := &v1.TaskRun{ //nolint:staticcheck + ObjectMeta: metav1.ObjectMeta{ + Name: "my-taskrun", + Namespace: "my-namespace", + Annotations: map[string]string{ + "chains.tekton.dev/reproducible": "true", + }, + UID: "abhhf-12354-asjsdbjs23-3435353n", + }, + Status: v1.TaskRunStatus{ + TaskRunStatusFields: v1.TaskRunStatusFields{ + StartTime: &metav1.Time{Time: time.Date(1995, time.December, 24, 6, 12, 12, 12, tz)}, + CompletionTime: &metav1.Time{Time: time.Date(1995, time.December, 24, 6, 12, 12, 24, tz)}, + }, + }, + } + start := time.Date(1995, time.December, 24, 6, 12, 12, 12, tz).UTC() + end := time.Date(1995, time.December, 24, 6, 12, 12, 24, tz).UTC() + want := slsa.BuildMetadata{ + InvocationID: "abhhf-12354-asjsdbjs23-3435353n", + StartedOn: &start, + FinishedOn: &end, + } + got := metadata(objects.NewTaskRunObjectV1(tr)) + if d := cmp.Diff(want, got); d != "" { + t.Fatalf("metadata (-want, +got):\n%s", d) + } +} + +func TestByProducts(t *testing.T) { + resultValue := v1.ResultValue{Type: "string", StringVal: "result-value"} + tr := &v1.TaskRun{ //nolint:staticcheck + Status: v1.TaskRunStatus{ + TaskRunStatusFields: v1.TaskRunStatusFields{ + Results: []v1.TaskRunResult{ + { + Name: "result-name", + Value: resultValue, + }, + }, + }, + }, + } + + resultBytes, err := json.Marshal(resultValue) + if err != nil { + t.Fatalf("Could not marshal results: %s", err) + } + want := []slsa.ResourceDescriptor{ + { + Name: "taskRunResults/result-name", + Content: resultBytes, + MediaType: jsonMediaType, + }, + } + got, err := byproducts(objects.NewTaskRunObjectV1(tr)) + if err != nil { + t.Fatalf("Could not extract byproducts: %s", err) + } + if d := cmp.Diff(want, got); d != "" { + t.Fatalf("byproducts (-want, +got):\n%s", d) + } +} + +func TestTaskRunGenerateAttestation(t *testing.T) { + ctx := logtesting.TestContextWithLogger(t) + tr, err := objectloader.TaskRunFromFile("../../../testdata/slsa-v2alpha4/taskrun1.json") + if err != nil { + t.Fatal(err) + } + e1BuildStart := time.Unix(1617011400, 0) + e1BuildFinished := time.Unix(1617011415, 0) + + resultValue := v1.ResultValue{Type: "string", StringVal: "sha256:827521c857fdcd4374f4da5442fbae2edb01e7fbae285c3ec15673d4c1daecb7"} + resultBytesDigest, err := json.Marshal(resultValue) + if err != nil { + t.Fatalf("Could not marshal results: %s", err) + } + resultValue = v1.ResultValue{Type: "string", StringVal: "gcr.io/my/image"} + resultBytesURI, err := json.Marshal(resultValue) + if err != nil { + t.Fatalf("Could not marshal results: %s", err) + } + + want := in_toto.ProvenanceStatementSLSA1{ + StatementHeader: in_toto.StatementHeader{ + Type: in_toto.StatementInTotoV01, + PredicateType: slsa.PredicateSLSAProvenance, + Subject: []in_toto.Subject{ + { + Name: "gcr.io/my/image/fromstep3", + Digest: common.DigestSet{ + "sha256": "827521c857fdcd4374f4da5442fbae2edb01e7fbae285c3ec15673d4c1daecb7", + }, + }, + }, + }, + Predicate: slsa.ProvenancePredicate{ + BuildDefinition: slsa.ProvenanceBuildDefinition{ + BuildType: "https://tekton.dev/chains/v2/slsa", + ExternalParameters: map[string]any{ + "runSpec": tr.Spec, + }, + InternalParameters: map[string]any{ + "tekton-pipelines-feature-flags": config.FeatureFlags{EnableAPIFields: "beta", ResultExtractionMethod: "termination-message"}, + }, + ResolvedDependencies: []slsa.ResourceDescriptor{ + { + URI: "git+https://github.com/test", + Digest: common.DigestSet{"sha1": "ab123"}, + Name: "task", + }, + { + URI: "oci://gcr.io/test1/test1", + Digest: common.DigestSet{"sha256": "d4b63d3e24d6eef04a6dc0795cf8a73470688803d97c52cffa3c8d4efd3397b6"}, + }, + { + URI: "oci://gcr.io/test2/test2", + Digest: common.DigestSet{"sha256": "4d6dd704ef58cb214dd826519929e92a978a57cdee43693006139c0080fd6fac"}, + }, + { + URI: "oci://gcr.io/test3/test3", + Digest: common.DigestSet{"sha256": "f1a8b8549c179f41e27ff3db0fe1a1793e4b109da46586501a8343637b1d0478"}, + }, + {Name: "inputs/result", URI: "git+https://git.test.com.git", Digest: common.DigestSet{"sha1": "taskrun"}}, + }, + }, + RunDetails: slsa.ProvenanceRunDetails{ + Builder: slsa.Builder{ + ID: "test_builder-1", + }, + BuildMetadata: slsa.BuildMetadata{ + InvocationID: "abhhf-12354-asjsdbjs23-3435353n", + StartedOn: &e1BuildStart, + FinishedOn: &e1BuildFinished, + }, + Byproducts: []slsa.ResourceDescriptor{ + { + Name: "taskRunResults/IMAGE_DIGEST", + Content: resultBytesDigest, + MediaType: jsonMediaType, + }, + { + Name: "taskRunResults/IMAGE_URL", + Content: resultBytesURI, + MediaType: jsonMediaType, + }, + { + Name: "stepResults/step1_result1", + MediaType: "application/json", + Content: []uint8(`"result-value"`), + }, + { + Name: "stepResults/step1_result1-ARTIFACT_OUTPUTS", + MediaType: "application/json", + Content: []uint8(`{"digest":"sha256:827521c857fdcd4374f4da5442fbae2edb01e7fbae285c3ec15673d4c1daecb7","uri":"gcr.io/my/image/fromstep2"}`), + }, + }, + }, + }, + } + + got, err := GenerateAttestation(ctx, objects.NewTaskRunObjectV1(tr), &slsaconfig.SlsaConfig{ + BuilderID: "test_builder-1", + BuildType: "https://tekton.dev/chains/v2/slsa", + }) + + if err != nil { + t.Errorf("unwant error: %s", err.Error()) + } + if diff := cmp.Diff(want, got); diff != "" { + t.Errorf("GenerateAttestation(): -want +got: %s", diff) + } +} + +func getResolvedDependencies(tro *objects.TaskRunObjectV1) []v1resourcedescriptor.ResourceDescriptor { + rd, err := resolveddependencies.TaskRun(context.Background(), resolveddependencies.ResolveOptions{}, tro) + if err != nil { + return []v1resourcedescriptor.ResourceDescriptor{} + } + return rd +} + +func TestGetBuildDefinition(t *testing.T) { + tr, err := objectloader.TaskRunFromFile("../../../testdata/slsa-v2alpha4/taskrun1.json") + if err != nil { + t.Fatal(err) + } + + tr.Annotations = map[string]string{ + "annotation1": "annotation1", + } + tr.Labels = map[string]string{ + "label1": "label1", + } + + tro := objects.NewTaskRunObjectV1(tr) + tests := []struct { + name string + buildType string + want slsa.ProvenanceBuildDefinition + err error + }{ + { + name: "test slsa build type", + buildType: "https://tekton.dev/chains/v2/slsa", + want: slsa.ProvenanceBuildDefinition{ + BuildType: "https://tekton.dev/chains/v2/slsa", + ExternalParameters: externalparameters.TaskRun(tro), + InternalParameters: internalparameters.SLSAInternalParameters(tro), + ResolvedDependencies: getResolvedDependencies(tro), + }, + err: nil, + }, + { + name: "test default build type", + buildType: "", + want: slsa.ProvenanceBuildDefinition{ + BuildType: "https://tekton.dev/chains/v2/slsa", + ExternalParameters: externalparameters.TaskRun(tro), + InternalParameters: internalparameters.SLSAInternalParameters(tro), + ResolvedDependencies: getResolvedDependencies(tro), + }, + err: nil, + }, + { + name: "test tekton build type", + buildType: "https://tekton.dev/chains/v2/slsa-tekton", + want: slsa.ProvenanceBuildDefinition{ + BuildType: "https://tekton.dev/chains/v2/slsa-tekton", + ExternalParameters: externalparameters.TaskRun(tro), + InternalParameters: internalparameters.TektonInternalParameters(tro), + ResolvedDependencies: getResolvedDependencies(tro), + }, + err: nil, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + bd, err := getBuildDefinition(context.Background(), tc.buildType, tro) + if err != nil { + t.Fatalf("Did not expect an error but got %v", err) + } + + if diff := cmp.Diff(tc.want, bd); diff != "" { + t.Errorf("getBuildDefinition(): -want +got: %v", diff) + } + }) + } +} + +func TestUnsupportedBuildType(t *testing.T) { + tr, err := objectloader.TaskRunFromFile("../../../testdata/slsa-v2alpha4/taskrun1.json") + if err != nil { + t.Fatal(err) + } + + got, err := getBuildDefinition(context.Background(), "bad-buildType", objects.NewTaskRunObjectV1(tr)) + if err == nil { + t.Error("getBuildDefinition(): expected error got nil") + } + if diff := cmp.Diff(slsa.ProvenanceBuildDefinition{}, got); diff != "" { + t.Errorf("getBuildDefinition(): -want +got: %s", diff) + } +} diff --git a/pkg/chains/formats/slsa/v2alpha4/slsav2.go b/pkg/chains/formats/slsa/v2alpha4/slsav2.go new file mode 100644 index 0000000000..59312139d7 --- /dev/null +++ b/pkg/chains/formats/slsa/v2alpha4/slsav2.go @@ -0,0 +1,68 @@ +/* +Copyright 2024 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 v2alpha4 + +import ( + "context" + "fmt" + + "github.com/tektoncd/chains/pkg/chains/formats" + "github.com/tektoncd/chains/pkg/chains/formats/slsa/internal/slsaconfig" + "github.com/tektoncd/chains/pkg/chains/formats/slsa/v2alpha4/internal/taskrun" + + "github.com/tektoncd/chains/pkg/chains/objects" + "github.com/tektoncd/chains/pkg/config" +) + +const ( + payloadTypeSlsav2alpha4 = formats.PayloadTypeSlsav2alpha4 +) + +func init() { + formats.RegisterPayloader(payloadTypeSlsav2alpha4, NewFormatter) +} + +type Slsa struct { + slsaConfig *slsaconfig.SlsaConfig +} + +func NewFormatter(cfg config.Config) (formats.Payloader, error) { + return &Slsa{ + slsaConfig: &slsaconfig.SlsaConfig{ + BuilderID: cfg.Builder.ID, + DeepInspectionEnabled: cfg.Artifacts.PipelineRuns.DeepInspectionEnabled, + BuildType: cfg.BuildDefinition.BuildType, + }, + }, nil +} + +func (s *Slsa) Wrap() bool { + return true +} + +func (s *Slsa) CreatePayload(ctx context.Context, obj interface{}) (interface{}, error) { + switch v := obj.(type) { + case *objects.TaskRunObjectV1: + return taskrun.GenerateAttestation(ctx, v, s.slsaConfig) + default: + return nil, fmt.Errorf("intoto does not support type: %s", v) + } +} + +func (s *Slsa) Type() config.PayloadType { + return payloadTypeSlsav2alpha4 +} diff --git a/pkg/chains/formats/slsa/v2alpha4/slsav2_test.go b/pkg/chains/formats/slsa/v2alpha4/slsav2_test.go new file mode 100644 index 0000000000..4bdc712592 --- /dev/null +++ b/pkg/chains/formats/slsa/v2alpha4/slsav2_test.go @@ -0,0 +1,405 @@ +/* +Copyright 2024 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 v2alpha4 + +import ( + "encoding/json" + "testing" + "time" + + "github.com/tektoncd/chains/pkg/chains/formats" + "github.com/tektoncd/chains/pkg/chains/objects" + "github.com/tektoncd/chains/pkg/config" + "github.com/tektoncd/chains/pkg/internal/objectloader" + + "github.com/google/go-cmp/cmp" + "github.com/in-toto/in-toto-golang/in_toto" + "github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/common" + slsa "github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/v1" + pipelineConfig "github.com/tektoncd/pipeline/pkg/apis/config" + v1 "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1" + logtesting "knative.dev/pkg/logging/testing" +) + +var ( + e1BuildStart = time.Unix(1617011400, 0) + e1BuildFinished = time.Unix(1617011415, 0) +) + +const jsonMediaType = "application/json" + +func TestNewFormatter(t *testing.T) { + t.Run("Ok", func(t *testing.T) { + cfg := config.Config{ + Builder: config.BuilderConfig{ + ID: "testid", + }, + } + f, err := NewFormatter(cfg) + if err != nil { + t.Errorf("Error creating formatter: %s", err) + } + if f == nil { + t.Error("Failed to create formatter") + } + }) +} + +func TestCreatePayloadError(t *testing.T) { + ctx := logtesting.TestContextWithLogger(t) + + cfg := config.Config{ + Builder: config.BuilderConfig{ + ID: "testid", + }, + } + f, _ := NewFormatter(cfg) + + t.Run("Invalid type", func(t *testing.T) { + p, err := f.CreatePayload(ctx, "not a task ref") + + if p != nil { + t.Errorf("Unexpected payload") + } + if err == nil { + t.Errorf("Expected error") + } else if err.Error() != "intoto does not support type: not a task ref" { + t.Errorf("wrong error returned: '%s'", err.Error()) + } + }) +} + +func TestCorrectPayloadType(t *testing.T) { + var i Slsa + if i.Type() != formats.PayloadTypeSlsav2alpha4 { + t.Errorf("Invalid type returned: %s", i.Type()) + } +} + +func TestTaskRunCreatePayload1(t *testing.T) { + ctx := logtesting.TestContextWithLogger(t) + + tr, err := objectloader.TaskRunFromFile("../testdata/slsa-v2alpha4/taskrun1.json") + if err != nil { + t.Fatal(err) + } + + resultValue := v1.ParamValue{Type: "string", StringVal: "sha256:827521c857fdcd4374f4da5442fbae2edb01e7fbae285c3ec15673d4c1daecb7"} + resultBytesDigest, err := json.Marshal(resultValue) + if err != nil { + t.Fatalf("Could not marshal results: %s", err) + } + resultValue = v1.ParamValue{Type: "string", StringVal: "gcr.io/my/image"} + resultBytesURI, err := json.Marshal(resultValue) + if err != nil { + t.Fatalf("Could not marshal results: %s", err) + } + + resultValue = v1.ParamValue{Type: "string", StringVal: "result-value"} + resultBytesStepResult, err := json.Marshal(resultValue) + if err != nil { + t.Fatalf("Could not marshal results: %s", err) + } + + resultValue = v1.ParamValue{Type: "object", ObjectVal: map[string]string{ + "uri": "gcr.io/my/image/fromstep2", + "digest": "sha256:827521c857fdcd4374f4da5442fbae2edb01e7fbae285c3ec15673d4c1daecb7", + }} + resultBytesStepResultObj, err := json.Marshal(resultValue) + if err != nil { + t.Fatalf("Could not marshal results: %s", err) + } + + cfg := config.Config{ + Builder: config.BuilderConfig{ + ID: "test_builder-1", + }, + } + expected := in_toto.ProvenanceStatementSLSA1{ + StatementHeader: in_toto.StatementHeader{ + Type: in_toto.StatementInTotoV01, + PredicateType: slsa.PredicateSLSAProvenance, + Subject: []in_toto.Subject{ + { + Name: "gcr.io/my/image/fromstep3", + Digest: common.DigestSet{"sha256": "827521c857fdcd4374f4da5442fbae2edb01e7fbae285c3ec15673d4c1daecb7"}, + }, + }, + }, + Predicate: slsa.ProvenancePredicate{ + BuildDefinition: slsa.ProvenanceBuildDefinition{ + BuildType: "https://tekton.dev/chains/v2/slsa", + ExternalParameters: map[string]any{ + "runSpec": tr.Spec, + }, + InternalParameters: map[string]any{ + "tekton-pipelines-feature-flags": pipelineConfig.FeatureFlags{EnableAPIFields: "beta", ResultExtractionMethod: "termination-message"}, + }, + ResolvedDependencies: []slsa.ResourceDescriptor{ + { + URI: "git+https://github.com/test", + Digest: common.DigestSet{"sha1": "ab123"}, + Name: "task", + }, + { + URI: "oci://gcr.io/test1/test1", + Digest: common.DigestSet{"sha256": "d4b63d3e24d6eef04a6dc0795cf8a73470688803d97c52cffa3c8d4efd3397b6"}, + }, + { + URI: "oci://gcr.io/test2/test2", + Digest: common.DigestSet{"sha256": "4d6dd704ef58cb214dd826519929e92a978a57cdee43693006139c0080fd6fac"}, + }, + { + URI: "oci://gcr.io/test3/test3", + Digest: common.DigestSet{"sha256": "f1a8b8549c179f41e27ff3db0fe1a1793e4b109da46586501a8343637b1d0478"}, + }, + {Name: "inputs/result", URI: "git+https://git.test.com.git", Digest: common.DigestSet{"sha1": "taskrun"}}, + }, + }, + RunDetails: slsa.ProvenanceRunDetails{ + Builder: slsa.Builder{ + ID: "test_builder-1", + }, + BuildMetadata: slsa.BuildMetadata{ + InvocationID: "abhhf-12354-asjsdbjs23-3435353n", + StartedOn: &e1BuildStart, + FinishedOn: &e1BuildFinished, + }, + Byproducts: []slsa.ResourceDescriptor{ + { + Name: "taskRunResults/IMAGE_DIGEST", + Content: resultBytesDigest, + MediaType: jsonMediaType, + }, + { + Name: "taskRunResults/IMAGE_URL", + Content: resultBytesURI, + MediaType: jsonMediaType, + }, + { + Name: "stepResults/step1_result1", + Content: resultBytesStepResult, + MediaType: jsonMediaType, + }, + { + Name: "stepResults/step1_result1-ARTIFACT_OUTPUTS", + Content: resultBytesStepResultObj, + MediaType: jsonMediaType, + }, + }, + }, + }, + } + + i, _ := NewFormatter(cfg) + + got, err := i.CreatePayload(ctx, objects.NewTaskRunObjectV1(tr)) + + if err != nil { + t.Errorf("unexpected error: %s", err.Error()) + } + if diff := cmp.Diff(expected, got); diff != "" { + t.Errorf("Slsa.CreatePayload(): -want +got: %s", diff) + } +} + +func TestTaskRunCreatePayload2(t *testing.T) { + ctx := logtesting.TestContextWithLogger(t) + tr, err := objectloader.TaskRunFromFile("../testdata/slsa-v2alpha4/taskrun2.json") + if err != nil { + t.Fatal(err) + } + + resultValue := v1.ParamValue{Type: "string", StringVal: "sha256:d4b63d3e24d6eef04a6dc0795cf8a73470688803d97c52cffa3c8d4efd3397b6"} + resultBytesDigest, err := json.Marshal(resultValue) + if err != nil { + t.Fatalf("Could not marshal results: %s", err) + } + resultValue = v1.ParamValue{Type: "string", StringVal: "pkg:deb/debian/curl@7.50.3-1"} + resultBytesURI, err := json.Marshal(resultValue) + if err != nil { + t.Fatalf("Could not marshal results: %s", err) + } + resultValue = v1.ParamValue{Type: "object", ObjectVal: map[string]string{ + "uri": "https://github.com/tektoncd/pipeline", + "digest": "sha1:7f2f46e1b97df36b2b82d1b1d87c81b8b3d21601", + }} + resultBytesObj, err := json.Marshal(resultValue) + if err != nil { + t.Fatalf("Could not marshal results: %s", err) + } + + cfg := config.Config{ + Builder: config.BuilderConfig{ + ID: "test_builder-2", + }, + } + expected := in_toto.ProvenanceStatementSLSA1{ + StatementHeader: in_toto.StatementHeader{ + Type: in_toto.StatementInTotoV01, + PredicateType: slsa.PredicateSLSAProvenance, + Subject: nil, + }, + Predicate: slsa.ProvenancePredicate{ + BuildDefinition: slsa.ProvenanceBuildDefinition{ + BuildType: "https://tekton.dev/chains/v2/slsa", + ExternalParameters: map[string]any{ + "runSpec": tr.Spec, + }, + InternalParameters: map[string]any{}, + ResolvedDependencies: []slsa.ResourceDescriptor{ + { + URI: "git+https://github.com/catalog", + Digest: common.DigestSet{"sha1": "x123"}, + Name: "task", + }, + { + URI: "oci://gcr.io/test1/test1", + Digest: common.DigestSet{"sha256": "d4b63d3e24d6eef04a6dc0795cf8a73470688803d97c52cffa3c8d4efd3397b6"}, + }, + { + Name: "inputs/result", + URI: "https://github.com/tektoncd/pipeline", + Digest: common.DigestSet{"sha1": "7f2f46e1b97df36b2b82d1b1d87c81b8b3d21601"}, + }, + { + Name: "inputs/result", + URI: "git+https://git.test.com.git", + Digest: common.DigestSet{"sha1": "sha:taskdefault"}, + }, + }, + }, + RunDetails: slsa.ProvenanceRunDetails{ + Builder: slsa.Builder{ + ID: "test_builder-2", + }, + BuildMetadata: slsa.BuildMetadata{ + InvocationID: "abhhf-12354-asjsdbjs23-3435353n", + StartedOn: &e1BuildStart, + FinishedOn: &e1BuildFinished, + }, + Byproducts: []slsa.ResourceDescriptor{ + { + Name: "taskRunResults/some-uri_DIGEST", + Content: resultBytesDigest, + MediaType: jsonMediaType, + }, + { + Name: "taskRunResults/some-uri", + Content: resultBytesURI, + MediaType: jsonMediaType, + }, + { + Name: "stepResults/step1_result1-ARTIFACT_INPUTS", + Content: resultBytesObj, + MediaType: jsonMediaType, + }, + }, + }, + }, + } + + i, _ := NewFormatter(cfg) + got, err := i.CreatePayload(ctx, objects.NewTaskRunObjectV1(tr)) + + if err != nil { + t.Errorf("unexpected error: %s", err.Error()) + } + if diff := cmp.Diff(expected, got); diff != "" { + t.Errorf("Slsa.CreatePayload(): -want +got: %s", diff) + } +} + +func TestMultipleSubjects(t *testing.T) { + ctx := logtesting.TestContextWithLogger(t) + + tr, err := objectloader.TaskRunFromFile("../testdata/slsa-v2alpha4/taskrun-multiple-subjects.json") + if err != nil { + t.Fatal(err) + } + + resultValue := v1.ParamValue{ + Type: "string", + StringVal: "gcr.io/myimage1@sha256:d4b63d3e24d6eef04a6dc0795cf8a73470688803d97c52cffa3c8d4efd3397b6,gcr.io/myimage2@sha256:daa1a56e13c85cf164e7d9e595006649e3a04c47fe4a8261320e18a0bf3b0367", + } + resultBytes, err := json.Marshal(resultValue) + if err != nil { + t.Fatalf("Could not marshal results: %s", err) + } + cfg := config.Config{ + Builder: config.BuilderConfig{ + ID: "test_builder-multiple", + }, + } + expected := in_toto.ProvenanceStatementSLSA1{ + StatementHeader: in_toto.StatementHeader{ + Type: in_toto.StatementInTotoV01, + PredicateType: slsa.PredicateSLSAProvenance, + Subject: []in_toto.Subject{ + { + Name: "gcr.io/foo/bar", + Digest: common.DigestSet{ + "sha256": "d4b63d3e24d6eef04a6dc0795cf8a73470688803d97c52cffa3c8d4efd3397b6", + }, + }, + { + Name: "gcr.io/myimage2", + Digest: common.DigestSet{ + "sha256": "daa1a56e13c85cf164e7d9e595006649e3a04c47fe4a8261320e18a0bf3b0367", + }, + }, + }, + }, + Predicate: slsa.ProvenancePredicate{ + BuildDefinition: slsa.ProvenanceBuildDefinition{ + BuildType: "https://tekton.dev/chains/v2/slsa", + ExternalParameters: map[string]any{ + "runSpec": tr.Spec, + }, + InternalParameters: map[string]any{}, + ResolvedDependencies: []slsa.ResourceDescriptor{ + { + URI: "oci://gcr.io/test1/test1", + Digest: common.DigestSet{"sha256": "d4b63d3e24d6eef04a6dc0795cf8a73470688803d97c52cffa3c8d4efd3397b6"}, + }, + }, + }, + RunDetails: slsa.ProvenanceRunDetails{ + Builder: slsa.Builder{ + ID: "test_builder-multiple", + }, + BuildMetadata: slsa.BuildMetadata{}, + Byproducts: []slsa.ResourceDescriptor{ + { + Name: "taskRunResults/IMAGES", + Content: resultBytes, + MediaType: jsonMediaType, + }, + }, + }, + }, + } + + i, _ := NewFormatter(cfg) + got, err := i.CreatePayload(ctx, objects.NewTaskRunObjectV1(tr)) + if err != nil { + t.Errorf("unexpected error: %s", err.Error()) + } + if diff := cmp.Diff(expected, got); diff != "" { + t.Errorf("Slsa.CreatePayload(): -want +got: %s", diff) + } +} diff --git a/pkg/chains/objects/objects.go b/pkg/chains/objects/objects.go index 95319be9ae..87ba4e3d98 100644 --- a/pkg/chains/objects/objects.go +++ b/pkg/chains/objects/objects.go @@ -60,6 +60,7 @@ type TektonObject interface { GetLatestAnnotations(ctx context.Context, clientSet versioned.Interface) (map[string]string, error) Patch(ctx context.Context, clientSet versioned.Interface, patchBytes []byte) error GetResults() []Result + GetNestedResults() []Result GetProvenance() *v1.Provenance GetServiceAccountName() string GetPullSecrets() []string @@ -143,6 +144,20 @@ func (tro *TaskRunObjectV1) GetResults() []Result { return res } +// GetNestedResults returns all the results from associated StepActions. +func (tro *TaskRunObjectV1) GetNestedResults() []Result { + res := []Result{} + for _, s := range tro.Status.Steps { + for _, r := range s.Results { + res = append(res, Result{ + Name: r.Name, + Value: r.Value, + }) + } + } + return res +} + func (tro *TaskRunObjectV1) GetStepImages() []string { images := []string{} for _, stepState := range tro.Status.Steps { @@ -257,6 +272,10 @@ func (pro *PipelineRunObjectV1) GetResults() []Result { return res } +func (pro *PipelineRunObjectV1) GetNestedResults() []Result { + return []Result{} +} + // Get the ServiceAccount declared in the PipelineRun func (pro *PipelineRunObjectV1) GetServiceAccountName() string { return pro.Spec.TaskRunTemplate.ServiceAccountName @@ -420,6 +439,10 @@ func (pro *PipelineRunObjectV1Beta1) GetResults() []Result { return res } +func (pro *PipelineRunObjectV1Beta1) GetNestedResults() []Result { + return []Result{} +} + // Get the ServiceAccount declared in the PipelineRun func (pro *PipelineRunObjectV1Beta1) GetServiceAccountName() string { return pro.Spec.ServiceAccountName @@ -567,6 +590,10 @@ func (tro *TaskRunObjectV1Beta1) GetResults() []Result { return res } +func (tro *TaskRunObjectV1Beta1) GetNestedResults() []Result { + return []Result{} +} + func (tro *TaskRunObjectV1Beta1) GetStepImages() []string { images := []string{} for _, stepState := range tro.Status.Steps { diff --git a/pkg/config/config.go b/pkg/config/config.go index 1d3cb3fbc9..c18cc9af2c 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -266,7 +266,7 @@ func NewConfigFromMap(data map[string]string) (*Config, error) { if err := cm.Parse(data, // Artifact-specific configs // TaskRuns - asString(taskrunFormatKey, &cfg.Artifacts.TaskRuns.Format, "in-toto", "slsa/v1", "slsa/v2alpha1", "slsa/v2alpha2", "slsa/v2alpha3"), + asString(taskrunFormatKey, &cfg.Artifacts.TaskRuns.Format, "in-toto", "slsa/v1", "slsa/v2alpha1", "slsa/v2alpha2", "slsa/v2alpha3", "slsa/v2alpha4"), asStringSet(taskrunStorageKey, &cfg.Artifacts.TaskRuns.StorageBackend, sets.New[string]("tekton", "oci", "gcs", "docdb", "grafeas", "kafka")), asString(taskrunSignerKey, &cfg.Artifacts.TaskRuns.Signer, "x509", "kms"), diff --git a/test/e2e-tests.sh b/test/e2e-tests.sh index d91c60846d..6b6b434d91 100755 --- a/test/e2e-tests.sh +++ b/test/e2e-tests.sh @@ -31,7 +31,7 @@ header "Setting up environment" # Test against nightly instead of latest. install_tkn -export RELEASE_YAML="https://storage.googleapis.com/tekton-releases/pipeline/previous/v0.45.0/release.yaml" +export RELEASE_YAML="https://storage.googleapis.com/tekton-releases/pipeline/previous/v0.59.0/release.yaml" install_pipeline_crd install_chains diff --git a/test/examples_test.go b/test/examples_test.go index e7384c2bb0..3ad0b98d6a 100644 --- a/test/examples_test.go +++ b/test/examples_test.go @@ -47,6 +47,7 @@ import ( "github.com/tektoncd/chains/pkg/chains/objects" "github.com/tektoncd/chains/pkg/test/tekton" v1 "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1" + objv1alpha1 "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1alpha1" "sigs.k8s.io/yaml" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -65,6 +66,8 @@ type TestExample struct { signatureKey string outputLocation string predicate string + stepactions map[string]string + pipelinesCm map[string]string } // TestExamples copies the format in the tektoncd/pipelines repo @@ -155,6 +158,34 @@ func TestExamples(t *testing.T) { outputLocation: "slsa/v2alpha3", predicate: "slsav1.0", }, + { + name: "taskrun-examples-slsa-v2alpha4", + cm: map[string]string{ + "artifacts.taskrun.format": "slsa/v2alpha4", + "artifacts.oci.storage": "tekton", + }, + getExampleObjects: getTaskRunExamples, + payloadKey: "chains.tekton.dev/payload-taskrun-%s", + signatureKey: "chains.tekton.dev/signature-taskrun-%s", + outputLocation: "slsa/v2alpha4", + predicate: "slsav1.0", + }, + { + name: "taskrun-with-step-actions-v2alpha4", + cm: map[string]string{ + "artifacts.taskrun.format": "slsa/v2alpha4", + "artifacts.oci.storage": "tekton", + }, + getExampleObjects: getTaskRunWithStepAction, + payloadKey: "chains.tekton.dev/payload-taskrun-%s", + signatureKey: "chains.tekton.dev/signature-taskrun-%s", + outputLocation: "slsa/v2alpha4", + predicate: "slsav1.0", + stepactions: map[string]string{ + "img-builder": "../examples/stepactions/step-image-builder.yaml", + }, + pipelinesCm: map[string]string{"enable-step-actions": "true"}, + }, } for _, test := range tests { @@ -162,13 +193,33 @@ func TestExamples(t *testing.T) { ctx := context.Background() c, ns, cleanup := setup(ctx, t, setupOpts{}) t.Cleanup(cleanup) + cleanUpInTotoFormatter := setConfigMap(ctx, t, c, test.cm) + t.Cleanup(cleanUpInTotoFormatter) + + if len(test.pipelinesCm) > 0 { + resetPipelinesConfig := setupPipelinesFeatureFlags(ctx, t, c, test.pipelinesCm) + t.Cleanup(resetPipelinesConfig) + } + + createSteps(ctx, t, test.stepactions, c, ns) + runInTotoFormatterTests(ctx, t, ns, c, test) - cleanUpInTotoFormatter() }) } } +func createSteps(ctx context.Context, t *testing.T, steps map[string]string, c *clients, ns string) { + for name, path := range steps { + sa := getObjectFromFile[objv1alpha1.StepAction](t, path) + sa.Name = name + _, err := c.PipelineClient.TektonV1alpha1().StepActions(ns).Create(ctx, sa, metav1.CreateOptions{}) + if err != nil { + t.Fatal(err) + } + } +} + func runInTotoFormatterTests(ctx context.Context, t *testing.T, ns string, c *clients, test TestExample) { // TODO: Commenting this out for now. Causes race condition where tests write and revert the chains-config // and signing-secrets out of order @@ -454,6 +505,13 @@ func getTaskRunExamples(t *testing.T, ns string) map[string]objects.TektonObject return examples } +func getTaskRunWithStepAction(t *testing.T, ns string) map[string]objects.TektonObject { + path := "../examples/stepactions/taskrun-image-builder.yaml" + trs := make(map[string]objects.TektonObject) + trs[path] = taskRunFromExample(t, ns, path) + return trs +} + func getPipelineRunExamples(t *testing.T, ns string) map[string]objects.TektonObject { examples := make(map[string]objects.TektonObject) for _, example := range getExamplePaths(t, pipelineRunExamplesPath) { diff --git a/test/test_utils.go b/test/test_utils.go index 77a14b1fb2..e19ddbdcf1 100644 --- a/test/test_utils.go +++ b/test/test_utils.go @@ -42,6 +42,7 @@ import ( "k8s.io/apimachinery/pkg/util/sets" "k8s.io/apimachinery/pkg/util/wait" "k8s.io/client-go/kubernetes" + yaml "sigs.k8s.io/yaml/goyaml.v2" ) func getTr(ctx context.Context, t *testing.T, c pipelineclientset.Interface, name, ns string) (tr *v1.TaskRun) { @@ -172,7 +173,31 @@ func readObj(t *testing.T, bucket, name string, client *storage.Client) io.Reade func setConfigMap(ctx context.Context, t *testing.T, c *clients, data map[string]string) func() { // Change the config to be GCS storage with this bucket. // Note(rgreinho): This comment does not look right... - cm, err := c.KubeClient.CoreV1().ConfigMaps(namespace).Get(ctx, "chains-config", metav1.GetOptions{}) + clean := updateConfigMap(ctx, t, c, data, namespace, "chains-config") + + err := restartChainsControllerPod(ctx, c.KubeClient, 300*time.Second) + if err != nil { + t.Fatalf("Failed to restart the pod: %v", err) + } + + return clean +} + +func setupPipelinesFeatureFlags(ctx context.Context, t *testing.T, c *clients, data map[string]string) func() { + pipelinesNs := "tekton-pipelines" + + clean := updateConfigMap(ctx, t, c, data, pipelinesNs, "feature-flags") + + err := restartControllerPod(ctx, c.KubeClient, 300*time.Second, pipelinesNs, "app.kubernetes.io/component=controller") + if err != nil { + t.Fatalf("Failed to restart the pod: %v", err) + } + + return clean +} + +func updateConfigMap(ctx context.Context, t *testing.T, c *clients, data map[string]string, ns, configMapName string) func() { + cm, err := c.KubeClient.CoreV1().ConfigMaps(ns).Get(ctx, configMapName, metav1.GetOptions{}) if err != nil { t.Fatal(err) } @@ -190,14 +215,10 @@ func setConfigMap(ctx context.Context, t *testing.T, c *clients, data map[string for k, v := range data { cm.Data[k] = v } - cm, err = c.KubeClient.CoreV1().ConfigMaps(namespace).Update(ctx, cm, metav1.UpdateOptions{}) + cm, err = c.KubeClient.CoreV1().ConfigMaps(ns).Update(ctx, cm, metav1.UpdateOptions{}) if err != nil { t.Fatal(err) } - err = restartChainsControllerPod(ctx, c.KubeClient, 300*time.Second) - if err != nil { - t.Fatalf("Failed to restart the pod: %v", err) - } return func() { for k := range data { @@ -206,7 +227,7 @@ func setConfigMap(ctx context.Context, t *testing.T, c *clients, data map[string for k, v := range oldData { cm.Data[k] = v } - if _, err := c.KubeClient.CoreV1().ConfigMaps(namespace).Update(ctx, cm, metav1.UpdateOptions{}); err != nil { + if _, err := c.KubeClient.CoreV1().ConfigMaps(ns).Update(ctx, cm, metav1.UpdateOptions{}); err != nil { t.Log(err) } } @@ -297,7 +318,11 @@ func verifySignature(ctx context.Context, t *testing.T, c *clients, obj objects. // restartChainsControllerPod restarts the pod running Chains // it then waits for a given timeout for the pod to resume running state func restartChainsControllerPod(ctx context.Context, c kubernetes.Interface, timeout time.Duration) error { - pods, err := c.CoreV1().Pods(namespace).List(ctx, metav1.ListOptions{LabelSelector: "app.kubernetes.io/component=controller"}) + return restartControllerPod(ctx, c, timeout, namespace, "app.kubernetes.io/component=controller") +} + +func restartControllerPod(ctx context.Context, c kubernetes.Interface, timeout time.Duration, ns, labelSelector string) error { + pods, err := c.CoreV1().Pods(ns).List(ctx, metav1.ListOptions{LabelSelector: labelSelector}) if err != nil { return err } @@ -310,7 +335,7 @@ func restartChainsControllerPod(ctx context.Context, c kubernetes.Interface, tim } return wait.PollUntilContextTimeout(ctx, 2*time.Second, timeout, true, func(context.Context) (done bool, err error) { - pods, err := c.CoreV1().Pods(namespace).List(context.Background(), metav1.ListOptions{LabelSelector: "app.kubernetes.io/component=controller"}) + pods, err := c.CoreV1().Pods(ns).List(context.Background(), metav1.ListOptions{LabelSelector: labelSelector}) if err != nil { return false, err } @@ -323,3 +348,17 @@ func restartChainsControllerPod(ctx context.Context, c kubernetes.Interface, tim return false, nil }) } + +func getObjectFromFile[T any](t *testing.T, path string) *T { + bytes, err := os.ReadFile(path) + if err != nil { + t.Fatal(err) + } + var obj *T + err = yaml.Unmarshal(bytes, &obj) + if err != nil { + t.Fatal(err) + } + + return obj +} diff --git a/test/testdata/slsa/v2alpha4/task-output-image.json b/test/testdata/slsa/v2alpha4/task-output-image.json new file mode 100644 index 0000000000..f373361c96 --- /dev/null +++ b/test/testdata/slsa/v2alpha4/task-output-image.json @@ -0,0 +1,67 @@ +{ + "_type": "https://in-toto.io/Statement/v0.1", + "predicateType": "https://slsa.dev/provenance/v1", + "subject": null, + "predicate": { + "buildDefinition": { + "buildType": "https://tekton.dev/chains/v2/slsa", + "externalParameters": { + "runSpec": { + "serviceAccountName": "default", + "taskSpec": { + "steps": [ + { + "name": "create-image", + "image": "busybox", + "computeResources": {}, + "script": "#!/usr/bin/env sh\necho 'gcr.io/foo/bar' | tee $(results.IMAGE_URL.path)\necho 'sha256:05f95b26ed10668b7183c1e2da98610e91372fa9f510046d4ce5812addad86b5' | tee $(results.IMAGE_DIGEST.path)" + } + ], + "results": [ + { + "name": "IMAGE_URL", + "type": "string" + },{ + "name": "IMAGE_DIGEST", + "type": "string" + } + ] + }, + "timeout": "1h0m0s" + } + }, + "resolvedDependencies": [ + {{range .URIDigest}} + { + "uri": "{{.URI}}", + "digest": { + "sha256": "{{.Digest}}" + } + } + {{end}} + ] + }, + "runDetails": { + "builder": { + "id": "https://tekton.dev/chains/v2" + }, + "metadata": { + "invocationID": "{{.UID}}", + "startedOn": "{{index .BuildStartTimes 0}}", + "finishedOn": "{{index .BuildFinishedTimes 0}}" + }, + "byproducts": [ + { + "name": "taskRunResults/IMAGE_DIGEST", + "mediaType": "application/json", + "content": "InNoYTI1NjowNWY5NWIyNmVkMTA2NjhiNzE4M2MxZTJkYTk4NjEwZTkxMzcyZmE5ZjUxMDA0NmQ0Y2U1ODEyYWRkYWQ4NmI1XG4i" + }, + { + "name": "taskRunResults/IMAGE_URL", + "mediaType": "application/json", + "content": "Imdjci5pby9mb28vYmFyXG4i" + } + ] + } + } +} diff --git a/test/testdata/slsa/v2alpha4/taskrun-image-builder.json b/test/testdata/slsa/v2alpha4/taskrun-image-builder.json new file mode 100644 index 0000000000..5e78a62378 --- /dev/null +++ b/test/testdata/slsa/v2alpha4/taskrun-image-builder.json @@ -0,0 +1,91 @@ +{ + "_type": "https://in-toto.io/Statement/v0.1", + "predicateType": "https://slsa.dev/provenance/v1", + "subject": [ + { + "name": "gcr.io/foo/img1", + "digest": { + "sha256": "586789aa031fafc7d78a5393cdc772e0b55107ea54bb8bcf3f2cdac6c6da51ee" + } + } + ], + "predicate": { + "buildDefinition": { + "buildType": "https://tekton.dev/chains/v2/slsa", + "externalParameters": { + "runSpec": { + "serviceAccountName": "default", + "taskSpec": { + "steps": [ + { + "name": "action-runner", + "computeResources": {}, + "ref": { + "name": "img-builder" + } + } + ] + }, + "timeout": "1h0m0s" + } + }, + "internalParameters": { + "tekton-pipelines-feature-flags": { + "DisableAffinityAssistant": false, + "DisableCredsInit": false, + "RunningInEnvWithInjectedSidecars": true, + "RequireGitSSHSecretKnownHosts": false, + "EnableTektonOCIBundles": false, + "ScopeWhenExpressionsToTask": false, + "EnableAPIFields": "beta", + "SendCloudEventsForRuns": false, + "AwaitSidecarReadiness": true, + "EnforceNonfalsifiability": "none", + "EnableKeepPodOnCancel": false, + "VerificationNoMatchPolicy": "ignore", + "EnableProvenanceInStatus": true, + "ResultExtractionMethod": "termination-message", + "MaxResultSize": 4096, + "SetSecurityContext": false, + "Coschedule": "workspaces", + "EnableCELInWhenExpression": false, + "EnableStepActions": true, + "EnableParamEnum": false, + "EnableArtifacts": false + } + }, + "resolvedDependencies": [ + {{range .URIDigest}} + { + "uri": "{{.URI}}", + "digest": { + "sha256": "{{.Digest}}" + } + } + {{end}} + ] + }, + "runDetails": { + "builder": { + "id": "https://tekton.dev/chains/v2" + }, + "metadata": { + "invocationID": "{{.UID}}", + "startedOn": "{{index .BuildStartTimes 0}}", + "finishedOn": "{{index .BuildFinishedTimes 0}}" + }, + "byproducts": [ + { + "name": "stepResults/second-IMAGE_DIGEST", + "mediaType": "application/json", + "content": "InNoYTI1NjowNWY5NWIyNmVkMTA2NjhiNzE4M2MxZTJkYTk4NjEwZTkxMzcyZmE5ZjUxMDA0NmQ0Y2U1ODEyYWRkYWQ4NmI2Ig==" + }, + { + "name": "stepResults/second-IMAGE_URL", + "mediaType": "application/json", + "content": "Imdjci5pby9mb28vYmFyIg==" + } + ] + } + } +} \ No newline at end of file