From c4f3e41fb1f350e16bd29139306bbb3edef666f9 Mon Sep 17 00:00:00 2001 From: Renzo Rojas Silva Date: Sun, 28 Apr 2024 11:50:55 -0400 Subject: [PATCH] Add new v2alpha4 version for TaskRuns This new version will now process the information from any associated StepAction from the executed TaskRun. Also, the way chains read results from TaskRun to populate the `subjects` field is changing: now the user has to explicitly mark a result as a subject using an bject type-hinted tag (*ARTIFACT_OUTPUTS) + the new `isBuildArtifact` property in the value --- docs/config.md | 5 +- docs/slsa-provenance.md | 95 +++- examples/stepactions/step-image-builder.yaml | 24 ++ .../stepactions/taskrun-image-builder.yaml | 10 + pkg/artifacts/signable.go | 43 +- pkg/artifacts/signable_test.go | 165 ++++++- pkg/chains/formats/all/all.go | 1 + pkg/chains/formats/format.go | 2 + pkg/chains/formats/slsa/extract/extract.go | 24 +- .../formats/slsa/extract/extract_test.go | 129 ++++++ .../formats/slsa/extract/v1beta1/extract.go | 2 +- .../build_definition/build_definition.go | 57 +++ .../build_definition/build_definition_test.go | 116 +++++ .../internal_parameters.go | 18 + .../internal_parameters_test.go | 73 ++-- .../slsa/internal/material/material.go | 32 +- .../slsa/internal/material/material_test.go | 155 +++++++ .../internal/material/v1beta1/material.go | 4 +- .../slsa/internal/metadata/metadata.go | 27 ++ .../slsa/internal/metadata/metadata_test.go | 86 ++++ .../slsa/internal/provenance/provenance.go | 42 ++ .../resolved_dependencies.go | 17 +- .../resolved_dependencies_test.go | 74 +++- .../formats/slsa/internal/results/results.go | 46 ++ .../slsa/internal/results/results_test.go | 269 ++++++++++++ .../taskrun-multiple-subjects.json | 72 ++++ .../slsa/testdata/slsa-v2alpha4/taskrun1.json | 166 +++++++ .../slsa/testdata/slsa-v2alpha4/taskrun2.json | 115 +++++ .../slsa/v2alpha3/internal/taskrun/taskrun.go | 78 +--- .../v2alpha3/internal/taskrun/taskrun_test.go | 151 +------ .../slsa/v2alpha4/internal/taskrun/taskrun.go | 69 +++ .../v2alpha4/internal/taskrun/taskrun_test.go | 195 +++++++++ pkg/chains/formats/slsa/v2alpha4/slsav2.go | 68 +++ .../formats/slsa/v2alpha4/slsav2_test.go | 405 ++++++++++++++++++ pkg/chains/objects/objects.go | 102 +++++ pkg/config/config.go | 2 +- test/e2e-tests.sh | 2 +- test/examples_test.go | 60 ++- test/test_utils.go | 57 ++- .../slsa/v2alpha4/task-output-image.json | 67 +++ .../slsa/v2alpha4/taskrun-image-builder.json | 91 ++++ 41 files changed, 2929 insertions(+), 287 deletions(-) create mode 100644 examples/stepactions/step-image-builder.yaml create mode 100644 examples/stepactions/taskrun-image-builder.yaml create mode 100644 pkg/chains/formats/slsa/internal/build_definition/build_definition.go create mode 100644 pkg/chains/formats/slsa/internal/build_definition/build_definition_test.go create mode 100644 pkg/chains/formats/slsa/internal/metadata/metadata.go create mode 100644 pkg/chains/formats/slsa/internal/metadata/metadata_test.go create mode 100644 pkg/chains/formats/slsa/internal/provenance/provenance.go create mode 100644 pkg/chains/formats/slsa/internal/results/results.go create mode 100644 pkg/chains/formats/slsa/internal/results/results_test.go create mode 100644 pkg/chains/formats/slsa/testdata/slsa-v2alpha4/taskrun-multiple-subjects.json create mode 100644 pkg/chains/formats/slsa/testdata/slsa-v2alpha4/taskrun1.json create mode 100644 pkg/chains/formats/slsa/testdata/slsa-v2alpha4/taskrun2.json create mode 100644 pkg/chains/formats/slsa/v2alpha4/internal/taskrun/taskrun.go create mode 100644 pkg/chains/formats/slsa/v2alpha4/internal/taskrun/taskrun_test.go create mode 100644 pkg/chains/formats/slsa/v2alpha4/slsav2.go create mode 100644 pkg/chains/formats/slsa/v2alpha4/slsav2_test.go create mode 100644 test/testdata/slsa/v2alpha4/task-output-image.json create mode 100644 test/testdata/slsa/v2alpha4/taskrun-image-builder.json 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..b403b5e895 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 err: %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..51f6f7482e 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,25 @@ 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, results []objects.Result) (subjects []intoto.Subject) { + 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..ffe32c01d6 100644 --- a/pkg/chains/formats/slsa/extract/extract_test.go +++ b/pkg/chains/formats/slsa/extract/extract_test.go @@ -271,6 +271,135 @@ 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) + results := append(test.obj.GetResults(), test.obj.GetNestedResults()...) + got := extract.SubjectsFromBuildArtifact(ctx, results) + 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/build_definition/build_definition.go b/pkg/chains/formats/slsa/internal/build_definition/build_definition.go new file mode 100644 index 0000000000..7e9307c26c --- /dev/null +++ b/pkg/chains/formats/slsa/internal/build_definition/build_definition.go @@ -0,0 +1,57 @@ +/* +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 builddefinition + +import ( + "context" + + slsa "github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/v1" + 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/objects" +) + +// getBuildDefinition returns the SLSA buildDefinition using the given parameters. +func getBuildDefinition(buildDefinitionType string, resolvedDeps []slsa.ResourceDescriptor, extParams, intParams map[string]any) slsa.ProvenanceBuildDefinition { + return slsa.ProvenanceBuildDefinition{ + BuildType: buildDefinitionType, + ExternalParameters: extParams, + InternalParameters: intParams, + ResolvedDependencies: resolvedDeps, + } +} + +// GetBuildDefinition returns the buildDefinition for the given TaskRun based on the configured buildType. This will default to the slsa buildType +func GetTaskRunBuildDefinition(ctx context.Context, tro *objects.TaskRunObjectV1, buildType string, resolveOpts resolveddependencies.ResolveOptions) (slsa.ProvenanceBuildDefinition, error) { + rd, err := resolveddependencies.TaskRun(ctx, resolveOpts, tro) + if err != nil { + return slsa.ProvenanceBuildDefinition{}, err + } + + externalParams := externalparameters.TaskRun(tro) + + buildDefinitionType := buildType + if buildDefinitionType == "" { + buildDefinitionType = buildtypes.SlsaBuildType + } + + internalParams, err := internalparameters.GetInternalParamters(tro, buildDefinitionType) + if err != nil { + return slsa.ProvenanceBuildDefinition{}, err + } + + return getBuildDefinition(buildDefinitionType, rd, externalParams, internalParams), nil +} diff --git a/pkg/chains/formats/slsa/internal/build_definition/build_definition_test.go b/pkg/chains/formats/slsa/internal/build_definition/build_definition_test.go new file mode 100644 index 0000000000..68326622f1 --- /dev/null +++ b/pkg/chains/formats/slsa/internal/build_definition/build_definition_test.go @@ -0,0 +1,116 @@ +/* +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 builddefinition + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + slsa "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/objects" + "github.com/tektoncd/chains/pkg/internal/objectloader" +) + +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", + } + + ctx := context.TODO() + + 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), + }, + 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), + }, + 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), + }, + err: nil, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + rd, err := resolveddependencies.TaskRun(ctx, resolveddependencies.ResolveOptions{}, tro) + if err != nil { + t.Fatalf("Error resolving dependencies: %v", err) + } + tc.want.ResolvedDependencies = rd + + bd, err := GetTaskRunBuildDefinition(context.TODO(), tro, tc.buildType, resolveddependencies.ResolveOptions{}) + 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 := GetTaskRunBuildDefinition(context.TODO(), objects.NewTaskRunObjectV1(tr), "bad-buildType", resolveddependencies.ResolveOptions{}) + 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/internal/internal_parameters/internal_parameters.go b/pkg/chains/formats/slsa/internal/internal_parameters/internal_parameters.go index 80ab28a493..86c09a49b7 100644 --- a/pkg/chains/formats/slsa/internal/internal_parameters/internal_parameters.go +++ b/pkg/chains/formats/slsa/internal/internal_parameters/internal_parameters.go @@ -17,6 +17,9 @@ limitations under the License. package internalparameters import ( + "fmt" + + buildtypes "github.com/tektoncd/chains/pkg/chains/formats/slsa/internal/build_types" "github.com/tektoncd/chains/pkg/chains/objects" v1 "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1" ) @@ -40,3 +43,18 @@ func TektonInternalParameters(tko objects.TektonObject) map[string]any { internalParams["annotations"] = tko.GetAnnotations() return internalParams } + +func GetInternalParamters(obj objects.TektonObject, buildDefinitionType string) (map[string]any, error) { + var internalParameters map[string]any + + switch buildDefinitionType { + case buildtypes.SlsaBuildType: + internalParameters = SLSAInternalParameters(obj) + case buildtypes.TektonBuildType: + internalParameters = TektonInternalParameters(obj) + default: + return nil, fmt.Errorf("unsupported buildType %v", buildDefinitionType) + } + + return internalParameters, nil +} diff --git a/pkg/chains/formats/slsa/internal/internal_parameters/internal_parameters_test.go b/pkg/chains/formats/slsa/internal/internal_parameters/internal_parameters_test.go index 6dd6f2008b..36fd6c887d 100644 --- a/pkg/chains/formats/slsa/internal/internal_parameters/internal_parameters_test.go +++ b/pkg/chains/formats/slsa/internal/internal_parameters/internal_parameters_test.go @@ -20,41 +20,60 @@ import ( "testing" "github.com/google/go-cmp/cmp" + buildtypes "github.com/tektoncd/chains/pkg/chains/formats/slsa/internal/build_types" "github.com/tektoncd/chains/pkg/chains/objects" "github.com/tektoncd/chains/pkg/internal/objectloader" "github.com/tektoncd/pipeline/pkg/apis/config" ) -func TestTektonInternalParameters(t *testing.T) { - tr, err := objectloader.TaskRunV1Beta1FromFile("../../testdata/slsa-v2alpha2/taskrun1.json") - if err != nil { - t.Fatal(err) - } - tro := objects.NewTaskRunObjectV1Beta1(tr) - got := TektonInternalParameters(tro) - want := map[string]any{ - "labels": tro.GetLabels(), - "annotations": tro.GetAnnotations(), - "tekton-pipelines-feature-flags": config.FeatureFlags{EnableAPIFields: "beta", ResultExtractionMethod: "termination-message"}, +func TestGetInternalParamters(t *testing.T) { + tests := []struct { + name string + shouldErr bool + buildDefinitionType string + expected map[string]any + }{ + { + name: "SLSA build type", + buildDefinitionType: buildtypes.SlsaBuildType, + expected: map[string]any{ + "tekton-pipelines-feature-flags": config.FeatureFlags{EnableAPIFields: "beta", ResultExtractionMethod: "termination-message"}, + }, + }, + { + name: "Tekton build type", + buildDefinitionType: buildtypes.TektonBuildType, + expected: map[string]any{ + "labels": map[string]string{"tekton.dev/pipelineTask": "build"}, + "annotations": map[string]string(nil), + "tekton-pipelines-feature-flags": config.FeatureFlags{EnableAPIFields: "beta", ResultExtractionMethod: "termination-message"}, + }, + }, + { + name: "Invalid build type", + buildDefinitionType: "invalid-type", + shouldErr: true, + }, } - if diff := cmp.Diff(want, got); diff != "" { - t.Errorf("TaskRun(): -want +got: %s", diff) - } -} + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + tr, err := objectloader.TaskRunV1Beta1FromFile("../../testdata/slsa-v2alpha2/taskrun1.json") + if err != nil { + t.Fatal(err) + } + tro := objects.NewTaskRunObjectV1Beta1(tr) -func TestSLSAInternalParameters(t *testing.T) { - tr, err := objectloader.TaskRunV1Beta1FromFile("../../testdata/slsa-v2alpha2/taskrun1.json") - if err != nil { - t.Fatal(err) - } - tro := objects.NewTaskRunObjectV1Beta1(tr) - got := SLSAInternalParameters(tro) - want := map[string]any{ - "tekton-pipelines-feature-flags": config.FeatureFlags{EnableAPIFields: "beta", ResultExtractionMethod: "termination-message"}, - } + got, err := GetInternalParamters(tro, test.buildDefinitionType) + + didError := err != nil + if didError != test.shouldErr { + t.Fatalf("Unexpected error behavior, shouldErr: %v, didError: %v, error: %v", test.shouldErr, didError, err) + } - if diff := cmp.Diff(want, got); diff != "" { - t.Errorf("TaskRun(): -want +got: %s", diff) + if diff := cmp.Diff(test.expected, got); diff != "" { + t.Errorf("TaskRun(): -want +got: %s", diff) + } + }) } } 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/metadata/metadata.go b/pkg/chains/formats/slsa/internal/metadata/metadata.go new file mode 100644 index 0000000000..8c832a45d8 --- /dev/null +++ b/pkg/chains/formats/slsa/internal/metadata/metadata.go @@ -0,0 +1,27 @@ +/* +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 metadata + +import ( + slsa "github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/v1" + "github.com/tektoncd/chains/pkg/chains/objects" +) + +func GetBuildMetadata(obj objects.TektonObject) slsa.BuildMetadata { + return slsa.BuildMetadata{ + InvocationID: string(obj.GetUID()), + StartedOn: obj.GetStartTime(), + FinishedOn: obj.GetCompletitionTime(), + } +} diff --git a/pkg/chains/formats/slsa/internal/metadata/metadata_test.go b/pkg/chains/formats/slsa/internal/metadata/metadata_test.go new file mode 100644 index 0000000000..2f0059f249 --- /dev/null +++ b/pkg/chains/formats/slsa/internal/metadata/metadata_test.go @@ -0,0 +1,86 @@ +/* +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 metadata + +import ( + "testing" + "time" + + "github.com/google/go-cmp/cmp" + slsa "github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/v1" + "github.com/tektoncd/chains/pkg/chains/objects" + v1 "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +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 := GetBuildMetadata(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 := GetBuildMetadata(objects.NewTaskRunObjectV1(tr)) + if d := cmp.Diff(want, got); d != "" { + t.Fatalf("metadata (-want, +got):\n%s", d) + } +} diff --git a/pkg/chains/formats/slsa/internal/provenance/provenance.go b/pkg/chains/formats/slsa/internal/provenance/provenance.go new file mode 100644 index 0000000000..f278e3db99 --- /dev/null +++ b/pkg/chains/formats/slsa/internal/provenance/provenance.go @@ -0,0 +1,42 @@ +/* +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 provenance + +import ( + 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/chains/formats/slsa/internal/metadata" + "github.com/tektoncd/chains/pkg/chains/formats/slsa/internal/slsaconfig" + "github.com/tektoncd/chains/pkg/chains/objects" +) + +func GetSLSA1Statement(obj objects.TektonObject, sub []intoto.Subject, bd slsa.ProvenanceBuildDefinition, bp []slsa.ResourceDescriptor, slsaConfig *slsaconfig.SlsaConfig) intoto.ProvenanceStatementSLSA1 { + return intoto.ProvenanceStatementSLSA1{ + StatementHeader: intoto.StatementHeader{ + Type: intoto.StatementInTotoV01, + PredicateType: slsa.PredicateSLSAProvenance, + Subject: sub, + }, + Predicate: slsa.ProvenancePredicate{ + BuildDefinition: bd, + RunDetails: slsa.ProvenanceRunDetails{ + Builder: slsa.Builder{ + ID: slsaConfig.BuilderID, + }, + BuildMetadata: metadata.GetBuildMetadata(obj), + Byproducts: bp, + }, + }, + } +} 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..a619d9d299 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 StepActions type-hinted results should be read to resolve dependecies. + WithStepActionsResults 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.WithStepActionsResults { + 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..a32ab0db9a 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{ + WithStepActionsResults: 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/internal/results/results.go b/pkg/chains/formats/slsa/internal/results/results.go new file mode 100644 index 0000000000..22888a925a --- /dev/null +++ b/pkg/chains/formats/slsa/internal/results/results.go @@ -0,0 +1,46 @@ +/* +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 results + +import ( + "encoding/json" + "fmt" + + "github.com/tektoncd/chains/pkg/artifacts" + "github.com/tektoncd/chains/pkg/chains/objects" + + slsa "github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/v1" +) + +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 && 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 +} diff --git a/pkg/chains/formats/slsa/internal/results/results_test.go b/pkg/chains/formats/slsa/internal/results/results_test.go new file mode 100644 index 0000000000..79051343f4 --- /dev/null +++ b/pkg/chains/formats/slsa/internal/results/results_test.go @@ -0,0 +1,269 @@ +/* +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 results + +import ( + "encoding/json" + "testing" + + "github.com/google/go-cmp/cmp" + slsa "github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/v1" + "github.com/tektoncd/chains/pkg/chains/objects" + v1 "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1" +) + +func TestGetResultsWithoutBuildArtifacts(t *testing.T) { + tests := []struct { + name string + prefix string + results []objects.Result + expected []slsa.ResourceDescriptor + }{ + // { + // name: "no results as input", + // expected: []slsa.ResourceDescriptor{}, + // }, + { + name: "results without build artifacts", + prefix: "taskRunResults/%s", + results: []objects.Result{ + { + Name: "result1", + Type: v1.ResultsTypeString, + Value: v1.ParamValue{ + Type: v1.ParamTypeString, + StringVal: "my-first-result", + }, + }, + { + Name: "IMAGES", + Type: v1.ResultsTypeArray, + Value: v1.ParamValue{ + Type: v1.ParamTypeArray, + ArrayVal: []string{"image1@sha256:123456", "image2@sha256:789032"}, + }, + }, + { + Name: "res1-IMAGE_URL", + Type: v1.ResultsTypeString, + Value: v1.ParamValue{ + Type: v1.ParamTypeString, + StringVal: "gcr.io/foo/bar", + }, + }, + { + Name: "res1-IMAGE_DIGEST", + Type: v1.ResultsTypeString, + Value: v1.ParamValue{ + Type: v1.ParamTypeString, + StringVal: "sha256:586789aa031fafc7d78a5393cdc772e0b55107ea54bb8bcf3f2cdac6c6da51ee", + }, + }, + { + Name: "res2-ARTIFACT_URI", + Type: v1.ResultsTypeString, + Value: v1.ParamValue{ + Type: v1.ParamTypeString, + StringVal: "gcr.io/my/image/fromstep2", + }, + }, + { + Name: "res2-ARTIFACT_DIGEST", + Type: v1.ResultsTypeString, + Value: v1.ParamValue{ + Type: v1.ParamTypeString, + StringVal: "sha256:827521c857fdcd4374f4da5442fbae2edb01e7fbae285c3ec15673d4c1daecb7", + }, + }, + { + Name: "res3-ARTIFACT_OUTPUTS", + Type: v1.ResultsTypeObject, + Value: v1.ParamValue{ + Type: v1.ParamTypeObject, + ObjectVal: map[string]string{ + "uri": "oci://gcr.io/test1/test1", + "digest": "sha256:d4b63d3e24d6eef04a6dc0795cf8a73470688803d97c52cffa3c8d4efd3397b6", + }, + }, + }, + { + Name: "res4-ARTIFACT_OUTPUTS", + Type: v1.ResultsTypeObject, + Value: v1.ParamValue{ + Type: v1.ParamTypeObject, + ObjectVal: map[string]string{ + "uri": "git+https://github.com/test", + "digest": "sha1:ab123", + "isBuildArtifact": "true", + }, + }, + }, + { + Name: "res5-ARTIFACT_OUTPUTS", + Type: v1.ResultsTypeObject, + Value: v1.ParamValue{ + Type: v1.ParamTypeObject, + ObjectVal: map[string]string{ + "uri": "oci://gcr.io/test2/test2", + "digest": "sha256:4d6dd704ef58cb214dd826519929e92a978a57cdee43693006139c0080fd6fac", + "isBuildArtifact": "false", + }, + }, + }, + { + Name: "res6-ARTIFACT_OUTPUTS", + Type: v1.ResultsTypeObject, + Value: v1.ParamValue{ + Type: v1.ParamTypeObject, + ObjectVal: map[string]string{ + "digest": "sha256:4d6dd704ef58cb214dd826519929e92a978a57cdee43693006139c0080fd6fac", + "isBuildArtifact": "true", + }, + }, + }, + { + Name: "res7-ARTIFACT_OUTPUTS", + Type: v1.ResultsTypeObject, + Value: v1.ParamValue{ + Type: v1.ParamTypeObject, + ObjectVal: map[string]string{ + "uri": "oci://gcr.io/test2/test2", + "digest": "sha256:4d6dd704ef58cb214dd826519929e92a978a57cdee43693006139c0080fd6fac", + "isBuildArtifact": "true", + }, + }, + }, + }, + expected: []slsa.ResourceDescriptor{ + { + Name: "taskRunResults/result1", + MediaType: "application/json", + Content: toJsonString(t, v1.ParamValue{ + Type: v1.ParamTypeString, + StringVal: "my-first-result", + }), + }, + { + Name: "taskRunResults/IMAGES", + MediaType: "application/json", + Content: toJsonString(t, v1.ParamValue{ + Type: v1.ParamTypeArray, + ArrayVal: []string{"image1@sha256:123456", "image2@sha256:789032"}, + }), + }, + { + Name: "taskRunResults/res1-IMAGE_URL", + MediaType: "application/json", + Content: toJsonString(t, v1.ParamValue{ + Type: v1.ParamTypeString, + StringVal: "gcr.io/foo/bar", + }), + }, + { + Name: "taskRunResults/res1-IMAGE_DIGEST", + MediaType: "application/json", + Content: toJsonString(t, v1.ParamValue{ + Type: v1.ParamTypeString, + StringVal: "sha256:586789aa031fafc7d78a5393cdc772e0b55107ea54bb8bcf3f2cdac6c6da51ee", + }), + }, + { + Name: "taskRunResults/res2-ARTIFACT_URI", + MediaType: "application/json", + Content: toJsonString(t, v1.ParamValue{ + Type: v1.ParamTypeString, + StringVal: "gcr.io/my/image/fromstep2", + }), + }, + { + Name: "taskRunResults/res2-ARTIFACT_DIGEST", + MediaType: "application/json", + Content: toJsonString(t, v1.ParamValue{ + Type: v1.ParamTypeString, + StringVal: "sha256:827521c857fdcd4374f4da5442fbae2edb01e7fbae285c3ec15673d4c1daecb7", + }), + }, + { + Name: "taskRunResults/res3-ARTIFACT_OUTPUTS", + MediaType: "application/json", + Content: toJsonString(t, v1.ParamValue{ + Type: v1.ParamTypeObject, + ObjectVal: map[string]string{ + "uri": "oci://gcr.io/test1/test1", + "digest": "sha256:d4b63d3e24d6eef04a6dc0795cf8a73470688803d97c52cffa3c8d4efd3397b6", + }, + }), + }, + { + Name: "taskRunResults/res4-ARTIFACT_OUTPUTS", + MediaType: "application/json", + Content: toJsonString(t, v1.ParamValue{ + Type: v1.ParamTypeObject, + ObjectVal: map[string]string{ + "uri": "git+https://github.com/test", + "digest": "sha1:ab123", + "isBuildArtifact": "true", + }, + }), + }, + { + Name: "taskRunResults/res5-ARTIFACT_OUTPUTS", + MediaType: "application/json", + Content: toJsonString(t, v1.ParamValue{ + Type: v1.ParamTypeObject, + ObjectVal: map[string]string{ + "uri": "oci://gcr.io/test2/test2", + "digest": "sha256:4d6dd704ef58cb214dd826519929e92a978a57cdee43693006139c0080fd6fac", + "isBuildArtifact": "false", + }, + }), + }, + { + Name: "taskRunResults/res6-ARTIFACT_OUTPUTS", + MediaType: "application/json", + Content: toJsonString(t, v1.ParamValue{ + Type: v1.ParamTypeObject, + ObjectVal: map[string]string{ + "digest": "sha256:4d6dd704ef58cb214dd826519929e92a978a57cdee43693006139c0080fd6fac", + "isBuildArtifact": "true", + }, + }), + }, + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got, err := GetResultsWithoutBuildArtifacts(test.results, test.prefix) + + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + if d := cmp.Diff(test.expected, got); d != "" { + t.Fatalf("metadata (-want, +got):\n%s", d) + } + }) + } +} + +func toJsonString(t *testing.T, val v1.ParamValue) []byte { + res, err := json.Marshal(val) + if err != nil { + t.Fatalf("error converting to json string: %v", err) + } + + return res +} 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..f3d56f2772 100644 --- a/pkg/chains/formats/slsa/v2alpha3/internal/taskrun/taskrun.go +++ b/pkg/chains/formats/slsa/v2alpha3/internal/taskrun/taskrun.go @@ -18,12 +18,10 @@ import ( "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/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" + builddefinition "github.com/tektoncd/chains/pkg/chains/formats/slsa/internal/build_definition" + "github.com/tektoncd/chains/pkg/chains/formats/slsa/internal/provenance" 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" @@ -38,44 +36,14 @@ func GenerateAttestation(ctx context.Context, tro *objects.TaskRunObjectV1, slsa return nil, err } - bd, err := getBuildDefinition(ctx, slsaConfig.BuildType, tro) + bd, err := builddefinition.GetTaskRunBuildDefinition(ctx, tro, slsaConfig.BuildType, resolveddependencies.ResolveOptions{}) if err != nil { return nil, err } - att := intoto.ProvenanceStatementSLSA1{ - StatementHeader: intoto.StatementHeader{ - Type: intoto.StatementInTotoV01, - PredicateType: slsa.PredicateSLSAProvenance, - Subject: extract.SubjectDigests(ctx, tro, slsaConfig), - }, - Predicate: slsa.ProvenancePredicate{ - BuildDefinition: bd, - RunDetails: slsa.ProvenanceRunDetails{ - Builder: slsa.Builder{ - ID: slsaConfig.BuilderID, - }, - BuildMetadata: metadata(tro), - Byproducts: bp, - }, - }, - } - return att, nil -} + sub := extract.SubjectDigests(ctx, tro, slsaConfig) -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 + return provenance.GetSLSA1Statement(tro, sub, bd, bp, slsaConfig), nil } // byproducts contains the taskRunResults @@ -95,39 +63,3 @@ func byproducts(tro *objects.TaskRunObjectV1) ([]slsa.ResourceDescriptor, error) } return byProd, nil } - -// 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 - } - - switch buildDefinitionType { - case buildtypes.SlsaBuildType: - rd, err := resolveddependencies.TaskRun(ctx, 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, 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/v2alpha3/internal/taskrun/taskrun_test.go b/pkg/chains/formats/slsa/v2alpha3/internal/taskrun/taskrun_test.go index 1606150399..6e3bd7940e 100644 --- a/pkg/chains/formats/slsa/v2alpha3/internal/taskrun/taskrun_test.go +++ b/pkg/chains/formats/slsa/v2alpha3/internal/taskrun/taskrun_test.go @@ -28,8 +28,6 @@ import ( 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/formats/slsa/v2alpha3/internal/pipelinerun" @@ -37,71 +35,9 @@ import ( "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" ) -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 @@ -239,94 +175,9 @@ 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{} } return rd } - -func TestGetBuildDefinition(t *testing.T) { - tr, err := objectloader.TaskRunFromFile("../../../testdata/slsa-v2alpha3/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-v2alpha3/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/internal/taskrun/taskrun.go b/pkg/chains/formats/slsa/v2alpha4/internal/taskrun/taskrun.go new file mode 100644 index 0000000000..d702a52c95 --- /dev/null +++ b/pkg/chains/formats/slsa/v2alpha4/internal/taskrun/taskrun.go @@ -0,0 +1,69 @@ +/* +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" + + slsa "github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/v1" + "github.com/tektoncd/chains/pkg/chains/formats/slsa/extract" + builddefinition "github.com/tektoncd/chains/pkg/chains/formats/slsa/internal/build_definition" + "github.com/tektoncd/chains/pkg/chains/formats/slsa/internal/provenance" + resolveddependencies "github.com/tektoncd/chains/pkg/chains/formats/slsa/internal/resolved_dependencies" + "github.com/tektoncd/chains/pkg/chains/formats/slsa/internal/results" + "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 + } + + resOpts := resolveddependencies.ResolveOptions{WithStepActionsResults: true} + bd, err := builddefinition.GetTaskRunBuildDefinition(ctx, tro, slsaConfig.BuildType, resOpts) + if err != nil { + return nil, err + } + + results := append(tro.GetResults(), tro.GetNestedResults()...) + sub := extract.SubjectsFromBuildArtifact(ctx, results) + + return provenance.GetSLSA1Statement(tro, sub, bd, bp, slsaConfig), nil +} + +func byproducts(tro *objects.TaskRunObjectV1) ([]slsa.ResourceDescriptor, error) { + byProd := []slsa.ResourceDescriptor{} + + res, err := results.GetResultsWithoutBuildArtifacts(tro.GetResults(), taskRunResults) + if err != nil { + return nil, err + } + byProd = append(byProd, res...) + + res, err = results.GetResultsWithoutBuildArtifacts(tro.GetNestedResults(), taskRunStepResults) + if err != nil { + return nil, err + } + byProd = append(byProd, res...) + + return byProd, nil +} 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..134dfad5eb --- /dev/null +++ b/pkg/chains/formats/slsa/v2alpha4/internal/taskrun/taskrun_test.go @@ -0,0 +1,195 @@ +/* +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" + 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" + logtesting "knative.dev/pkg/logging/testing" +) + +const jsonMediaType = "application/json" + +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 +} 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..f959d84d76 100644 --- a/pkg/chains/objects/objects.go +++ b/pkg/chains/objects/objects.go @@ -18,6 +18,7 @@ import ( "errors" "fmt" "strings" + "time" "github.com/tektoncd/pipeline/pkg/apis/config" "github.com/tektoncd/pipeline/pkg/apis/pipeline/pod" @@ -60,6 +61,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 @@ -70,6 +72,8 @@ type TektonObject interface { SupportsOCIArtifact() bool GetRemoteProvenance() *v1.Provenance IsRemote() bool + GetStartTime() *time.Time + GetCompletitionTime() *time.Time } func NewTektonObject(i interface{}) (TektonObject, error) { @@ -143,6 +147,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 { @@ -198,6 +216,24 @@ func (tro *TaskRunObjectV1) IsRemote() bool { return isRemoteTask } +func (tro *TaskRunObjectV1) GetStartTime() *time.Time { + var utc *time.Time = nil + if tro.Status.StartTime != nil { + val := tro.Status.StartTime.Time.UTC() + utc = &val + } + return utc +} + +func (tro *TaskRunObjectV1) GetCompletitionTime() *time.Time { + var utc *time.Time = nil + if tro.Status.CompletionTime != nil { + val := tro.Status.CompletionTime.Time.UTC() + utc = &val + } + return utc +} + // PipelineRunObjectV1 extends v1.PipelineRun with additional functions. type PipelineRunObjectV1 struct { // The base PipelineRun @@ -257,6 +293,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 @@ -322,6 +362,24 @@ func (pro *PipelineRunObjectV1) IsRemote() bool { return isRemotePipeline } +func (pro *PipelineRunObjectV1) GetStartTime() *time.Time { + var utc *time.Time = nil + if pro.Status.StartTime != nil { + val := pro.Status.StartTime.Time.UTC() + utc = &val + } + return utc +} + +func (pro *PipelineRunObjectV1) GetCompletitionTime() *time.Time { + var utc *time.Time = nil + if pro.Status.CompletionTime != nil { + val := pro.Status.CompletionTime.Time.UTC() + utc = &val + } + return utc +} + // Get the imgPullSecrets from a pod template, if they exist func getPodPullSecrets(podTemplate *pod.Template) []string { imgPullSecrets := []string{} @@ -420,6 +478,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 @@ -483,6 +545,24 @@ func (pro *PipelineRunObjectV1Beta1) IsRemote() bool { return isRemotePipeline } +func (pro *PipelineRunObjectV1Beta1) GetStartTime() *time.Time { + var utc *time.Time = nil + if pro.Status.StartTime != nil { + val := pro.Status.StartTime.Time.UTC() + utc = &val + } + return utc +} + +func (pro *PipelineRunObjectV1Beta1) GetCompletitionTime() *time.Time { + var utc *time.Time = nil + if pro.Status.CompletionTime != nil { + val := pro.Status.CompletionTime.Time.UTC() + utc = &val + } + return utc +} + // TaskRunObjectV1Beta1 extends v1beta1.TaskRun with additional functions. type TaskRunObjectV1Beta1 struct { *v1beta1.TaskRun @@ -567,6 +647,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 { @@ -624,3 +708,21 @@ func (tro *TaskRunObjectV1Beta1) IsRemote() bool { } return isRemoteTask } + +func (tro *TaskRunObjectV1Beta1) GetStartTime() *time.Time { + var utc *time.Time = nil + if tro.Status.StartTime != nil { + val := tro.Status.StartTime.Time.UTC() + utc = &val + } + return utc +} + +func (tro *TaskRunObjectV1Beta1) GetCompletitionTime() *time.Time { + var utc *time.Time = nil + if tro.Status.CompletionTime != nil { + val := tro.Status.CompletionTime.Time.UTC() + utc = &val + } + return utc +} 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