diff --git a/examples/v2alpha4/pipeline-with-repeated-results.yaml b/examples/v2alpha4/pipeline-with-repeated-results.yaml new file mode 100644 index 0000000000..e259c1d9e7 --- /dev/null +++ b/examples/v2alpha4/pipeline-with-repeated-results.yaml @@ -0,0 +1,64 @@ +apiVersion: tekton.dev/v1 +kind: PipelineRun +metadata: + name: pipeline-test-run +spec: + pipelineSpec: + results: + - name: output1-ARTIFACT_OUTPUTS + value: $(tasks.t1.results.output1-ARTIFACT_OUTPUTS) + - name: output2-ARTIFACT_OUTPUTS + value: $(tasks.t1.results.output2) + - name: output3-ARTIFACT_OUTPUTS + value: $(tasks.t2.results.output3-ARTIFACT_OUTPUTS) + tasks: + - name: t1 + taskSpec: + results: + - name: output1-ARTIFACT_OUTPUTS + type: object + properties: + uri: {} + digest: {} + isBuildArtifact: {} + + - name: output2 + type: object + properties: + uri: {} + digest: {} + + steps: + - name: step1 + image: busybox:glibc + script: | + echo -n "Hello!" + echo -n "{\"uri\":\"gcr.io/foo/img1\", \"digest\":\"sha256:586789aa031fafc7d78a5393cdc772e0b55107ea54bb8bcf3f2cdac6c6da51ee\", \"isBuildArtifact\": \"true\" }" > $(results.output1-ARTIFACT_OUTPUTS.path) + echo -n "{\"uri\":\"gcr.io/foo/img2\", \"digest\":\"sha256:586789aa031fafc7d78a5393cdc772e0b55107ea54bb8bcf3f2cdac6c6da51ee\"}" > $(results.output2.path) + + - name: t2 + taskSpec: + results: + - name: output3-ARTIFACT_OUTPUTS + type: object + properties: + uri: {} + digest: {} + isBuildArtifact: {} + steps: + - name: step1 + image: busybox:glibc + script: | + echo -n "Hello!" + echo -n "{\"uri\":\"gcr.io/foo/img1\", \"digest\":\"sha256:586789aa031fafc7d78a5393cdc772e0b55107ea54bb8bcf3f2cdac6c6da51ee\", \"isBuildArtifact\": \"true\" }" > $(results.output3-ARTIFACT_OUTPUTS.path) + + - name: t3 + taskSpec: + results: + - name: IMAGES + type: string + steps: + - name: step1 + image: busybox:glibc + script: | + echo -n "gcr.io/foo/img1@sha256:586789aa031fafc7d78a5393cdc772e0b55107ea54bb8bcf3f2cdac6c6da51ee" > $(results.IMAGES.path) \ No newline at end of file diff --git a/pkg/chains/formats/format.go b/pkg/chains/formats/format.go index 797240f999..4caabbc88b 100644 --- a/pkg/chains/formats/format.go +++ b/pkg/chains/formats/format.go @@ -25,6 +25,7 @@ type Payloader interface { CreatePayload(ctx context.Context, obj interface{}) (interface{}, error) Type() config.PayloadType Wrap() bool + RetrieveAllArtifactURIs(ctx context.Context, obj interface{}) ([]string, error) } const ( diff --git a/pkg/chains/formats/simple/simple.go b/pkg/chains/formats/simple/simple.go index 10c464f96a..1089574774 100644 --- a/pkg/chains/formats/simple/simple.go +++ b/pkg/chains/formats/simple/simple.go @@ -69,3 +69,8 @@ func (i SimpleContainerImage) ImageName() string { func (i *SimpleSigning) Type() config.PayloadType { return formats.PayloadTypeSimpleSigning } + +// RetrieveAllArtifactURIs returns always an error, feature not available for simplesigning formatter. +func (i *SimpleSigning) RetrieveAllArtifactURIs(_ context.Context, _ interface{}) ([]string, error) { + return nil, fmt.Errorf("RetrieveAllArtifactURIs not supported for simeplesining formatter") +} diff --git a/pkg/chains/formats/slsa/testdata/slsa-v2alpha4/pipelinerun2.json b/pkg/chains/formats/slsa/testdata/slsa-v2alpha4/pipelinerun2.json new file mode 100644 index 0000000000..46af7d9952 --- /dev/null +++ b/pkg/chains/formats/slsa/testdata/slsa-v2alpha4/pipelinerun2.json @@ -0,0 +1,328 @@ +{ + "metadata": { + "name": "pipelinerun-build", + "uid": "abhhf-12354-asjsdbjs23-3435353n" + }, + "spec": { + "params": [ + { + "name": "IMAGE", + "value": "test.io/test/image" + } + ], + "pipelineRef": { + "name": "test-pipeline" + }, + "taskRunTemplate": { + "serviceAccountName": "pipeline" + } + }, + "status": { + "startTime": "2021-03-29T09:50:00Z", + "completionTime": "2021-03-29T09:50:15Z", + "conditions": [ + { + "lastTransitionTime": "2021-03-29T09:50:15Z", + "message": "Tasks Completed: 2 (Failed: 0, Cancelled 0), Skipped: 0", + "reason": "Succeeded", + "status": "True", + "type": "Succeeded" + } + ], + "results": [ + { + "name": "CHAINS-GIT_COMMIT", + "value": "abcd" + }, + { + "name": "CHAINS-GIT_URL", + "value": "https://git.test.com" + }, + { + "name": "IMAGE_URL", + "value": "test.io/test/image" + }, + { + "name": "IMAGE_DIGEST", + "value": "sha256:827521c857fdcd4374f4da5442fbae2edb01e7fbae285c3ec15673d4c1daecb7" + }, + { + "name": "build-artifact-ARTIFACT_OUTPUTS", + "value": { + "uri": "abc", + "digest": "sha256:827521c857fdcd4374f4da5442fbae2edb01e7fbae285c3ec15673d4c1daecb7", + "isBuildArtifact": "true" + } + }, + { + "name": "img-ARTIFACT_INPUTS", + "value": { + "uri": "abc","digest": "sha256:827521c857fdcd4374f4da5442fbae2edb01e7fbae285c3ec15673d4c1daecb7" + } + }, + { + "name": "img2-ARTIFACT_OUTPUTS", + "value": { + "uri": "def","digest": "sha256:","isBuildArtifact": "true" + } + }, + { + "name": "img_no_uri-ARTIFACT_OUTPUTS", + "value": { + "digest": "sha256:827521c857fdcd4374f4da5442fbae2edb01e7fbae285c3ec15673d4c1daecb7" + } + }, + { + "name": "common-ARTIFACT_OUTPUTS", + "value": { + "uri": "gcr.io/common/image", + "digest": "sha256:33e7e52645f4859622e282167d9200da9861b3d0a6e9c93b85e9cae5526ffc0a", + "isBuildArtifact": "true" + } + } + ], + "pipelineSpec": { + "params": [ + { + "description": "Image path on registry", + "name": "IMAGE", + "type": "string" + } + ], + "results": [ + { + "description": "", + "name": "CHAINS-GIT_COMMIT", + "value": "$(tasks.git-clone.results.commit)" + }, + { + "description": "", + "name": "CHAINS-GIT_URL", + "value": "$(tasks.git-clone.results.url)" + }, + { + "description": "", + "name": "IMAGE_URL", + "value": "$(tasks.build.results.IMAGE_URL)" + }, + { + "description": "", + "name": "IMAGE_DIGEST", + "value": "$(tasks.build.results.IMAGE_DIGEST)" + } + ], + "tasks": [ + { + "name": "git-clone", + "params": [ + { + "name": "url", + "value": "https://git.test.com" + }, + { + "name": "revision", + "value": "" + } + ], + "taskRef": { + "kind": "ClusterTask", + "name": "git-clone" + } + }, + { + "name": "build", + "params": [ + { + "name": "CHAINS-GIT_COMMIT", + "value": "$(tasks.git-clone.results.commit)" + }, + { + "name": "CHAINS-GIT_URL", + "value": "$(tasks.git-clone.results.url)" + } + ], + "taskRef": { + "kind": "ClusterTask", + "name": "build" + } + } + ] + }, + "taskRuns": { + "git-clone": { + "pipelineTaskName": "git-clone", + "status": { + "completionTime": "2021-03-29T09:50:15Z", + "conditions": [ + { + "lastTransitionTime": "2021-03-29T09:50:15Z", + "message": "All Steps have completed executing", + "reason": "Succeeded", + "status": "True", + "type": "Succeeded" + } + ], + "podName": "git-clone-pod", + "startTime": "2021-03-29T09:50:00Z", + "steps": [ + { + "container": "step-clone", + "imageID": "test.io/test/clone-image", + "name": "clone", + "terminated": { + "exitCode": 0, + "finishedAt": "2021-03-29T09:50:15Z", + "reason": "Completed", + "startedAt": "2022-05-31T19:13:27Z" + } + } + ], + "results": [ + { + "name": "commit", + "value": "abcd" + }, + { + "name": "url", + "value": "https://git.test.com" + } + ], + "taskSpec": { + "params": [ + { + "description": "Repository URL to clone from.", + "name": "url", + "type": "string" + }, + { + "default": "", + "description": "Revision to checkout. (branch, tag, sha, ref, etc...)", + "name": "revision", + "type": "string" + } + ], + "results": [ + { + "description": "The precise commit SHA that was fetched by this Task.", + "name": "commit" + }, + { + "description": "The precise URL that was fetched by this Task.", + "name": "url" + } + ], + "steps": [ + { + "env": [ + { + "name": "HOME", + "value": "$(params.userHome)" + }, + { + "name": "PARAM_URL", + "value": "$(params.url)" + } + ], + "image": "$(params.gitInitImage)", + "name": "clone", + "resources": {}, + "script": "git clone" + } + ] + } + } + }, + "taskrun-build": { + "pipelineTaskName": "build", + "status": { + "completionTime": "2021-03-29T09:50:15Z", + "conditions": [ + { + "lastTransitionTime": "2021-03-29T09:50:15Z", + "message": "All Steps have completed executing", + "reason": "Succeeded", + "status": "True", + "type": "Succeeded" + } + ], + "podName": "build-pod", + "startTime": "2021-03-29T09:50:00Z", + "steps": [ + { + "container": "step-build", + "imageID": "test.io/test/build-image", + "name": "build", + "terminated": { + "exitCode": 0, + "finishedAt": "2022-05-31T19:17:30Z", + "reason": "Completed", + "startedAt": "2021-03-29T09:50:00Z" + } + } + ], + "results": [ + { + "name": "IMAGE_DIGEST", + "value": "sha256:827521c857fdcd4374f4da5442fbae2edb01e7fbae285c3ec15673d4c1daecb7" + }, + { + "name": "IMAGE_URL", + "value": "test.io/test/image\n" + } + ], + "taskSpec": { + "params": [ + { + "description": "Git CHAINS URL", + "name": "CHAINS-GIT_URL", + "type": "string" + }, + { + "description": "Git CHAINS Commit", + "name": "CHAINS-GIT_COMMIT", + "type": "string" + } + ], + "results": [ + { + "description": "Digest of the image just built.", + "name": "IMAGE_DIGEST" + }, + { + "description": "URL of the image just built.", + "name": "IMAGE_URL" + } + ], + "steps": [ + { + "command": [ + "buildah", + "build" + ], + "image": "test.io/test/build-image", + "name": "generate" + }, + { + "command": [ + "buildah", + "push" + ], + "image": "test.io/test/build-image", + "name": "push" + } + ] + } + } + } + }, + "provenance": { + "refSource": { + "uri": "git+https://github.com/test", + "digest": { + "sha1": "28b123" + }, + "entryPoint": "pipeline.yaml" + } + } + } +} diff --git a/pkg/chains/formats/slsa/testdata/slsa-v2alpha4/taskrun3.json b/pkg/chains/formats/slsa/testdata/slsa-v2alpha4/taskrun3.json new file mode 100644 index 0000000000..0c2ea16856 --- /dev/null +++ b/pkg/chains/formats/slsa/testdata/slsa-v2alpha4/taskrun3.json @@ -0,0 +1,175 @@ +{ + "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/common/image", + "digest": "sha256:33e7e52645f4859622e282167d9200da9861b3d0a6e9c93b85e9cae5526ffc0a", + "isBuildArtifact": "true" + } + } + ] + }, + { + "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:d31cc8328054de2bd93735f9cbf0ccfb6e0ee8f4c4225da7d8f8cb3900eaf466" + }, + { + "name": "IMAGE_URL", + "value": "gcr.io/my/image" + }, + { + "name": "IMAGES", + "value": "gcr.io/common/image@sha256:33e7e52645f4859622e282167d9200da9861b3d0a6e9c93b85e9cae5526ffc0a, gcr.io/task1/result@sha256:c6262181543796435ae52eb233d7337ec570ff0448e333460122f4a65a59a96a" + } + ], + "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": "IMAGE_URL", + "description": "URL of the file just built." + }, + { + "name": "IMAGE_URL", + "description": "Images 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/taskrun4.json b/pkg/chains/formats/slsa/testdata/slsa-v2alpha4/taskrun4.json new file mode 100644 index 0000000000..5694120e72 --- /dev/null +++ b/pkg/chains/formats/slsa/testdata/slsa-v2alpha4/taskrun4.json @@ -0,0 +1,133 @@ +{ + "metadata": { + "name": "git-clone", + "labels": { + "tekton.dev/pipelineTask": "git-clone" + }, + "uid": "abhhf-12354-asjsdbjs23-3435353n" + }, + "spec": { + "params": [], + "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" + } + } + ] + }, + { + "name": "step3", + "container": "step-step3", + "imageID": "docker-pullable://gcr.io/test1/test1@sha256:d4b63d3e24d6eef04a6dc0795cf8a73470688803d97c52cffa3c8d4efd3397b6", + "results": [ + { + "name": "step3_result1-ARTIFACT_OUTPUTS", + "value": { + "uri": "gcr.io/task2/step/artifact", + "digest": "sha256:cb06e289303c9529cd980657a5b1a2c8a146c1b13ca08a2bbedb72ec4b7573b9", + "isBuildArtifact": "true" + } + } + ] + }, + { + "name": "step4", + "container": "step-step4", + "imageID": "docker-pullable://gcr.io/test1/test1@sha256:d4b63d3e24d6eef04a6dc0795cf8a73470688803d97c52cffa3c8d4efd3397b6", + "results": [ + { + "name": "step4_result1-ARTIFACT_OUTPUTS", + "value": { + "uri": "gcr.io/common/image", + "digest": "sha256:33e7e52645f4859622e282167d9200da9861b3d0a6e9c93b85e9cae5526ffc0a", + "isBuildArtifact": "true" + } + } + ] + } + ], + "results": [ + { + "name": "some-uri_DIGEST", + "value": "sha256:d4b63d3e24d6eef04a6dc0795cf8a73470688803d97c52cffa3c8d4efd3397b6" + }, + { + "name": "some-uri", + "value": "pkg:deb/debian/curl@7.50.3-1" + }, + { + "name": "task2-ARTIFACT_OUTPUTS", + "value": { + "uri": "gcr.io/common/image", + "digest": "sha256:33e7e52645f4859622e282167d9200da9861b3d0a6e9c93b85e9cae5526ffc0a", + "isBuildArtifact": "true" + } + } + ], + "taskSpec": { + "steps": [ + { + "env": [ + { + "name": "HOME", + "value": "$(params.userHome)" + }, + { + "name": "PARAM_URL", + "value": "$(params.url)" + } + ], + "name": "step1", + "script": "git clone" + } + ], + "params": [], + "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/v1/intotoite6.go b/pkg/chains/formats/slsa/v1/intotoite6.go index f4c3bc8d8f..d6a9c8abf9 100644 --- a/pkg/chains/formats/slsa/v1/intotoite6.go +++ b/pkg/chains/formats/slsa/v1/intotoite6.go @@ -21,6 +21,7 @@ import ( "fmt" "github.com/tektoncd/chains/pkg/chains/formats" + "github.com/tektoncd/chains/pkg/chains/formats/slsa/extract" "github.com/tektoncd/chains/pkg/chains/formats/slsa/internal/slsaconfig" "github.com/tektoncd/chains/pkg/chains/formats/slsa/v1/pipelinerun" "github.com/tektoncd/chains/pkg/chains/formats/slsa/v1/taskrun" @@ -94,3 +95,12 @@ func (i *InTotoIte6) CreatePayload(ctx context.Context, obj interface{}) (interf func (i *InTotoIte6) Type() config.PayloadType { return formats.PayloadTypeSlsav1 } + +// RetrieveAllArtifactURIs returns the full URI of all artifacts detected as subjects. +func (i *InTotoIte6) RetrieveAllArtifactURIs(ctx context.Context, obj interface{}) ([]string, error) { + tkObj, ok := obj.(objects.TektonObject) + if !ok { + return nil, fmt.Errorf("intoto does not support type") + } + return extract.RetrieveAllArtifactURIs(ctx, tkObj, i.slsaConfig.DeepInspectionEnabled), nil +} diff --git a/pkg/chains/formats/slsa/v2alpha3/slsav2.go b/pkg/chains/formats/slsa/v2alpha3/slsav2.go index d86dd01802..3b8c241462 100644 --- a/pkg/chains/formats/slsa/v2alpha3/slsav2.go +++ b/pkg/chains/formats/slsa/v2alpha3/slsav2.go @@ -21,6 +21,7 @@ import ( "fmt" "github.com/tektoncd/chains/pkg/chains/formats" + "github.com/tektoncd/chains/pkg/chains/formats/slsa/extract" "github.com/tektoncd/chains/pkg/chains/formats/slsa/internal/slsaconfig" "github.com/tektoncd/chains/pkg/chains/formats/slsa/v2alpha3/internal/pipelinerun" "github.com/tektoncd/chains/pkg/chains/formats/slsa/v2alpha3/internal/taskrun" @@ -68,3 +69,12 @@ func (s *Slsa) CreatePayload(ctx context.Context, obj interface{}) (interface{}, func (s *Slsa) Type() config.PayloadType { return formats.PayloadTypeSlsav2alpha3 } + +// RetrieveAllArtifactURIs returns the full URI of all artifacts detected as subjects. +func (s *Slsa) RetrieveAllArtifactURIs(ctx context.Context, obj interface{}) ([]string, error) { + tkObj, ok := obj.(objects.TektonObject) + if !ok { + return nil, fmt.Errorf("intoto does not support type") + } + return extract.RetrieveAllArtifactURIs(ctx, tkObj, s.slsaConfig.DeepInspectionEnabled), nil +} diff --git a/pkg/chains/formats/slsa/v2alpha4/internal/pipelinerun/pipelinerun.go b/pkg/chains/formats/slsa/v2alpha4/internal/pipelinerun/pipelinerun.go index ca504a4006..4ccf941a89 100644 --- a/pkg/chains/formats/slsa/v2alpha4/internal/pipelinerun/pipelinerun.go +++ b/pkg/chains/formats/slsa/v2alpha4/internal/pipelinerun/pipelinerun.go @@ -18,6 +18,7 @@ import ( intoto "github.com/in-toto/attestation/go/v1" "github.com/tektoncd/chains/pkg/chains/formats/slsa/extract" + "github.com/tektoncd/chains/pkg/chains/formats/slsa/internal/artifact" 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" @@ -46,7 +47,7 @@ func GenerateAttestation(ctx context.Context, pro *objects.PipelineRunObjectV1, return nil, err } - sub := subjectDigests(ctx, pro, slsaconfig) + sub := SubjectDigests(ctx, pro, slsaconfig) return provenance.GetSLSA1Statement(pro, sub, &bd, bp, slsaconfig) } @@ -73,7 +74,8 @@ func byproducts(pro *objects.PipelineRunObjectV1, slsaconfig *slsaconfig.SlsaCon return byProd, nil } -func subjectDigests(ctx context.Context, pro *objects.PipelineRunObjectV1, slsaconfig *slsaconfig.SlsaConfig) []*intoto.ResourceDescriptor { +// SubjectDigests calculates the subjects associated with the given PipelineRun. +func SubjectDigests(ctx context.Context, pro *objects.PipelineRunObjectV1, slsaconfig *slsaconfig.SlsaConfig) []*intoto.ResourceDescriptor { subjects := extract.SubjectsFromBuildArtifact(ctx, pro.GetResults()) if !slsaconfig.DeepInspectionEnabled { @@ -81,7 +83,7 @@ func subjectDigests(ctx context.Context, pro *objects.PipelineRunObjectV1, slsac } for _, task := range pro.GetExecutedTasks() { - subjects = append(subjects, taskrun.SubjectDigests(ctx, task)...) + subjects = artifact.AppendSubjects(subjects, taskrun.SubjectDigests(ctx, task)...) } return subjects diff --git a/pkg/chains/formats/slsa/v2alpha4/internal/pipelinerun/pipelinerun_test.go b/pkg/chains/formats/slsa/v2alpha4/internal/pipelinerun/pipelinerun_test.go index e848d2c46a..deb2141a37 100644 --- a/pkg/chains/formats/slsa/v2alpha4/internal/pipelinerun/pipelinerun_test.go +++ b/pkg/chains/formats/slsa/v2alpha4/internal/pipelinerun/pipelinerun_test.go @@ -75,13 +75,13 @@ func TestByProducts(t *testing.T) { func TestGenerateAttestation(t *testing.T) { ctx := logtesting.TestContextWithLogger(t) - pr := createPro("../../../testdata/slsa-v2alpha4/pipelinerun1.json") e1BuildStart := time.Unix(1617011400, 0) e1BuildFinished := time.Unix(1617011415, 0) tests := []struct { name string + pr *objects.PipelineRunObjectV1 expectedStatement *intoto.Statement expectedPredicate *slsa.Provenance expectedSubjects []*intoto.ResourceDescriptor @@ -91,6 +91,7 @@ func TestGenerateAttestation(t *testing.T) { }{ { name: "attestation without deepinspection", + pr: createPro("../../../testdata/slsa-v2alpha4/pipelinerun1.json", "../../../testdata/slsa-v2alpha4/taskrun1.json", "../../../testdata/slsa-v2alpha4/taskrun2.json"), expectedSubjects: []*intoto.ResourceDescriptor{ { Name: "abc", @@ -166,6 +167,7 @@ func TestGenerateAttestation(t *testing.T) { }, { name: "attestation with deepinspection", + pr: createPro("../../../testdata/slsa-v2alpha4/pipelinerun1.json", "../../../testdata/slsa-v2alpha4/taskrun1.json", "../../../testdata/slsa-v2alpha4/taskrun2.json"), withDeepInspection: true, expectedSubjects: []*intoto.ResourceDescriptor{ { @@ -287,11 +289,144 @@ func TestGenerateAttestation(t *testing.T) { }, }, }, + { + name: "attestation with no repetead subjects", + pr: createPro("../../../testdata/slsa-v2alpha4/pipelinerun2.json", "../../../testdata/slsa-v2alpha4/taskrun3.json", "../../../testdata/slsa-v2alpha4/taskrun4.json"), + withDeepInspection: true, + expectedSubjects: []*intoto.ResourceDescriptor{ + { + Name: "abc", + Digest: common.DigestSet{ + "sha256": "827521c857fdcd4374f4da5442fbae2edb01e7fbae285c3ec15673d4c1daecb7", + }, + }, + { + Name: "gcr.io/common/image", + Digest: common.DigestSet{ + "sha256": "33e7e52645f4859622e282167d9200da9861b3d0a6e9c93b85e9cae5526ffc0a", + }, + }, + { + Name: "test.io/test/image", + Digest: common.DigestSet{ + "sha256": "827521c857fdcd4374f4da5442fbae2edb01e7fbae285c3ec15673d4c1daecb7", + }, + }, + { + Name: "gcr.io/task2/step/artifact", + Digest: common.DigestSet{ + "sha256": "cb06e289303c9529cd980657a5b1a2c8a146c1b13ca08a2bbedb72ec4b7573b9", + }, + }, + { + Name: "gcr.io/my/image/fromstep3", + Digest: common.DigestSet{ + "sha256": "827521c857fdcd4374f4da5442fbae2edb01e7fbae285c3ec15673d4c1daecb7", + }, + }, + { + Name: "gcr.io/my/image", + Digest: common.DigestSet{ + "sha256": "d31cc8328054de2bd93735f9cbf0ccfb6e0ee8f4c4225da7d8f8cb3900eaf466", + }, + }, + { + Name: "gcr.io/task1/result", + Digest: common.DigestSet{ + "sha256": "c6262181543796435ae52eb233d7337ec570ff0448e333460122f4a65a59a96a", + }, + }, + }, + expectedResolvedDependencies: []*intoto.ResourceDescriptor{ + { + Uri: "git+https://github.com/test", + Digest: common.DigestSet{"sha1": "28b123"}, + Name: "pipeline", + }, + { + Uri: "git+https://github.com/catalog", + Digest: common.DigestSet{"sha1": "x123"}, + Name: "pipelineTask", + }, + { + Uri: "oci://gcr.io/test1/test1", + Digest: common.DigestSet{"sha256": "d4b63d3e24d6eef04a6dc0795cf8a73470688803d97c52cffa3c8d4efd3397b6"}, + }, + { + Uri: "git+https://github.com/test", + Digest: common.DigestSet{"sha1": "ab123"}, + Name: "pipelineTask", + }, + { + 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: "https://github.com/tektoncd/pipeline", + Digest: common.DigestSet{"sha1": "7f2f46e1b97df36b2b82d1b1d87c81b8b3d21601"}, + }, + { + Uri: "abc", + Digest: common.DigestSet{"sha256": "827521c857fdcd4374f4da5442fbae2edb01e7fbae285c3ec15673d4c1daecb7"}, + Name: "inputs/result", + }, + { + Name: "inputs/result", + Uri: "git+https://git.test.com.git", + Digest: common.DigestSet{"sha1": "taskrun"}, + }, + { + Name: "inputs/result", + Uri: "git+https://git.test.com.git", + Digest: common.DigestSet{"sha1": "abcd"}, + }, + }, + expectedByProducts: []*intoto.ResourceDescriptor{ + { + Name: "pipelineRunResults/CHAINS-GIT_COMMIT", + Content: []uint8(`"abcd"`), + MediaType: JSONMediaType, + }, { + Name: "pipelineRunResults/CHAINS-GIT_URL", + Content: []uint8(`"https://git.test.com"`), + MediaType: JSONMediaType, + }, { + Name: "pipelineRunResults/img-ARTIFACT_INPUTS", + Content: []uint8(`{"digest":"sha256:827521c857fdcd4374f4da5442fbae2edb01e7fbae285c3ec15673d4c1daecb7","uri":"abc"}`), + MediaType: JSONMediaType, + }, { + Name: "pipelineRunResults/img_no_uri-ARTIFACT_OUTPUTS", + Content: []uint8(`{"digest":"sha256:827521c857fdcd4374f4da5442fbae2edb01e7fbae285c3ec15673d4c1daecb7"}`), + MediaType: JSONMediaType, + }, { + Name: "taskRunResults/some-uri_DIGEST", + Content: []uint8(`"sha256:d4b63d3e24d6eef04a6dc0795cf8a73470688803d97c52cffa3c8d4efd3397b6"`), + MediaType: JSONMediaType, + }, { + Name: "taskRunResults/some-uri", + Content: []uint8(`"pkg:deb/debian/curl@7.50.3-1"`), + MediaType: JSONMediaType, + }, { + Name: "stepResults/step1_result1-ARTIFACT_INPUTS", + Content: []uint8(`{"digest":"sha1:7f2f46e1b97df36b2b82d1b1d87c81b8b3d21601","uri":"https://github.com/tektoncd/pipeline"}`), + MediaType: JSONMediaType, + }, { + Name: "stepResults/step1_result1", + Content: []uint8(`"result-value"`), + MediaType: JSONMediaType, + }, + }, + }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { - got, err := GenerateAttestation(ctx, pr, &slsaconfig.SlsaConfig{ + got, err := GenerateAttestation(ctx, test.pr, &slsaconfig.SlsaConfig{ BuilderID: "test_builder-1", DeepInspectionEnabled: test.withDeepInspection, BuildType: "https://tekton.dev/chains/v2/slsa", @@ -305,7 +440,7 @@ func TestGenerateAttestation(t *testing.T) { BuildDefinition: &slsa.BuildDefinition{ BuildType: "https://tekton.dev/chains/v2/slsa", ExternalParameters: getStruct(t, map[string]any{ - "runSpec": pr.Spec, + "runSpec": test.pr.Spec, }), InternalParameters: getStruct(t, map[string]any{}), ResolvedDependencies: test.expectedResolvedDependencies, @@ -339,22 +474,21 @@ func TestGenerateAttestation(t *testing.T) { } } -func createPro(path string) *objects.PipelineRunObjectV1 { - pr, err := objectloader.PipelineRunFromFile(path) - if err != nil { - panic(err) - } - tr1, err := objectloader.TaskRunFromFile("../../../testdata/slsa-v2alpha4/taskrun1.json") - if err != nil { - panic(err) - } - tr2, err := objectloader.TaskRunFromFile("../../../testdata/slsa-v2alpha4/taskrun2.json") +func createPro(prPath string, trPaths ...string) *objects.PipelineRunObjectV1 { + pr, err := objectloader.PipelineRunFromFile(prPath) if err != nil { panic(err) } p := objects.NewPipelineRunObjectV1(pr) - p.AppendTaskRun(tr1) - p.AppendTaskRun(tr2) + + for _, trPath := range trPaths { + tr, err := objectloader.TaskRunFromFile(trPath) + if err != nil { + panic(err) + } + p.AppendTaskRun(tr) + } + return p } diff --git a/pkg/chains/formats/slsa/v2alpha4/slsav2.go b/pkg/chains/formats/slsa/v2alpha4/slsav2.go index d756cac477..c5e6cb5273 100644 --- a/pkg/chains/formats/slsa/v2alpha4/slsav2.go +++ b/pkg/chains/formats/slsa/v2alpha4/slsav2.go @@ -20,6 +20,7 @@ import ( "context" "fmt" + intoto "github.com/in-toto/attestation/go/v1" "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/pipelinerun" @@ -74,3 +75,25 @@ func (s *Slsa) CreatePayload(ctx context.Context, obj interface{}) (interface{}, func (s *Slsa) Type() config.PayloadType { return payloadTypeSlsav2alpha4 } + +// RetrieveAllArtifactURIs returns the full URI of all artifacts detected as subjects. +func (s *Slsa) RetrieveAllArtifactURIs(ctx context.Context, obj interface{}) ([]string, error) { + var subjects []*intoto.ResourceDescriptor + var fullURIs []string + + switch v := obj.(type) { + case *objects.TaskRunObjectV1: + subjects = taskrun.SubjectDigests(ctx, v) + case *objects.PipelineRunObjectV1: + subjects = pipelinerun.SubjectDigests(ctx, v, s.slsaConfig) + default: + return nil, fmt.Errorf("intoto does not support type: %s", v) + } + + for _, s := range subjects { + for algo, digest := range s.Digest { + fullURIs = append(fullURIs, fmt.Sprintf("%s@%s:%s", s.Name, algo, digest)) + } + } + return fullURIs, nil +} diff --git a/pkg/chains/signing.go b/pkg/chains/signing.go index 453bad9820..ce0bb380af 100644 --- a/pkg/chains/signing.go +++ b/pkg/chains/signing.go @@ -20,6 +20,7 @@ import ( "fmt" "github.com/hashicorp/go-multierror" + intoto "github.com/in-toto/attestation/go/v1" "github.com/tektoncd/chains/pkg/artifacts" "github.com/tektoncd/chains/pkg/chains/formats" "github.com/tektoncd/chains/pkg/chains/objects" @@ -29,6 +30,7 @@ import ( "github.com/tektoncd/chains/pkg/chains/storage" "github.com/tektoncd/chains/pkg/config" versioned "github.com/tektoncd/pipeline/pkg/client/clientset/versioned" + "google.golang.org/protobuf/encoding/protojson" "k8s.io/apimachinery/pkg/util/sets" "knative.dev/pkg/logging" ) @@ -169,7 +171,7 @@ func (o *ObjectSigner) Sign(ctx context.Context, tektonObj objects.TektonObject) } logger.Infof("Signing object with %s", signerType) - rawPayload, err := json.Marshal(payload) + rawPayload, err := getRawPayload(payload) if err != nil { logger.Warnf("Unable to marshal payload: %v", signerType, obj) continue @@ -248,3 +250,19 @@ func HandleRetry(ctx context.Context, obj objects.TektonObject, ps versioned.Int } return MarkFailed(ctx, obj, ps, annotations) } + +// getRawPayload returns the payload as a json string. If the given payload is a intoto.Statement type, protojson.Marshal +// is used to get the proper labels/field names in the resulting json. +func getRawPayload(payload interface{}) ([]byte, error) { + switch payloadObj := payload.(type) { + case intoto.Statement: + return protojson.Marshal(&payloadObj) + case *intoto.Statement: + if payloadObj == nil { + return json.Marshal(payload) + } + return protojson.Marshal(payloadObj) + default: + return json.Marshal(payload) + } +} diff --git a/pkg/chains/signing_test.go b/pkg/chains/signing_test.go index 0a8cba2f09..687953afe2 100644 --- a/pkg/chains/signing_test.go +++ b/pkg/chains/signing_test.go @@ -14,12 +14,16 @@ limitations under the License. package chains import ( + "bytes" "context" + "encoding/json" "errors" "fmt" "reflect" "testing" + "github.com/google/go-cmp/cmp" + intoto "github.com/in-toto/attestation/go/v1" "github.com/sigstore/rekor/pkg/generated/models" "github.com/tektoncd/chains/pkg/chains/objects" "github.com/tektoncd/chains/pkg/chains/signing" @@ -405,6 +409,84 @@ func TestSigningObjects(t *testing.T) { } } +func TestGetRawPayload(t *testing.T) { + tests := []struct { + name string + payload interface{} + expected string + }{ + { + name: "intoto.Statement object", + payload: intoto.Statement{ + Type: "type1", + PredicateType: "predicate-type1", + }, + expected: compactJSON(t, []byte(`{"_type":"type1","predicateType":"predicate-type1"}`)), + }, + { + name: "*intoto.Statement object", + payload: &intoto.Statement{ + Type: "type1", + PredicateType: "predicate-type1", + }, + expected: compactJSON(t, []byte(`{"_type":"type1","predicateType":"predicate-type1"}`)), + }, + { + name: "*intoto.Statement object - nil", + payload: (func() *intoto.Statement { return nil })(), + expected: "null", + }, + { + name: "other object - nil", + payload: nil, + expected: "null", + }, + { + name: "other object with value", + payload: struct { + Name string + ID int + Inner any + }{ + Name: "wrapper", + ID: 1, + Inner: struct { + InnerID int + Description string + IsArtifact bool + }{ + InnerID: 2, + Description: "some description", + IsArtifact: true, + }, + }, + expected: compactJSON(t, []byte(`{"Name":"wrapper","ID":1,"Inner": {"InnerID":2,"Description":"some description","IsArtifact":true}}`)), + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got, err := getRawPayload(test.payload) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + compactExpected := compactJSON(t, got) + if diff := cmp.Diff(test.expected, compactExpected); diff != "" { + t.Errorf("getRawPayload(), -want +got, diff = %s", diff) + } + }) + } +} + +func compactJSON(t *testing.T, jsonString []byte) string { + t.Helper() + dst := &bytes.Buffer{} + if err := json.Compact(dst, jsonString); err != nil { + t.Fatalf("error getting compact JSON: %v", err) + } + return dst.String() +} + func fakeAllBackends(backends []*mockBackend) map[string]storage.Backend { newBackends := map[string]storage.Backend{} for _, m := range backends { diff --git a/pkg/chains/storage/grafeas/grafeas.go b/pkg/chains/storage/grafeas/grafeas.go index 7f04f3e86e..ef86b87acc 100644 --- a/pkg/chains/storage/grafeas/grafeas.go +++ b/pkg/chains/storage/grafeas/grafeas.go @@ -253,7 +253,7 @@ func (b *Backend) createOccurrence(ctx context.Context, obj objects.TektonObject } // create Occurrence_Build for TaskRun - allURIs := extract.RetrieveAllArtifactURIs(ctx, obj, b.cfg.Artifacts.PipelineRuns.DeepInspectionEnabled) + allURIs := b.getAllArtifactURIs(ctx, opts.PayloadFormat, obj) for _, uri := range allURIs { occ, err := b.createBuildOccurrence(ctx, obj, payload, signature, uri) if err != nil { @@ -264,6 +264,22 @@ func (b *Backend) createOccurrence(ctx context.Context, obj objects.TektonObject return occs, nil } +func (b *Backend) getAllArtifactURIs(ctx context.Context, payloadFormat config.PayloadType, obj objects.TektonObject) []string { + logger := logging.FromContext(ctx) + payloader, err := formats.GetPayloader(payloadFormat, b.cfg) + if err != nil { + logger.Infof("couldn't get payloader for %v format, will use extract.RetrieveAllArtifactURIs method instead", payloadFormat) + return extract.RetrieveAllArtifactURIs(ctx, obj, b.cfg.Artifacts.PipelineRuns.DeepInspectionEnabled) + } + + if uris, err := payloader.RetrieveAllArtifactURIs(ctx, obj); err == nil { + return uris + } + + logger.Infof("couldn't get URIs from payloader %v, will use extract.RetrieveAllArtifactURIs method instead", payloadFormat) + return extract.RetrieveAllArtifactURIs(ctx, obj, b.cfg.Artifacts.PipelineRuns.DeepInspectionEnabled) +} + func (b *Backend) createAttestationOccurrence(ctx context.Context, payload []byte, signature string, uri string) (*pb.Occurrence, error) { occurrenceDetails := &pb.Occurrence_Attestation{ Attestation: &pb.AttestationOccurrence{ @@ -364,7 +380,7 @@ func (b *Backend) getBuildNotePath(obj objects.TektonObject) string { func (b *Backend) getAllOccurrences(ctx context.Context, obj objects.TektonObject, opts config.StorageOpts) ([]*pb.Occurrence, error) { result := []*pb.Occurrence{} // step 1: get all resource URIs created under the taskrun - uriFilters := extract.RetrieveAllArtifactURIs(ctx, obj, b.cfg.Artifacts.PipelineRuns.DeepInspectionEnabled) + uriFilters := b.getAllArtifactURIs(ctx, opts.PayloadFormat, obj) // step 2: find all build occurrences if _, ok := formats.IntotoAttestationSet[opts.PayloadFormat]; ok { diff --git a/pkg/chains/storage/grafeas/grafeas_test.go b/pkg/chains/storage/grafeas/grafeas_test.go index 57809aee31..398c7a8eee 100644 --- a/pkg/chains/storage/grafeas/grafeas_test.go +++ b/pkg/chains/storage/grafeas/grafeas_test.go @@ -21,14 +21,15 @@ import ( "sort" "strings" "testing" + "time" "github.com/google/go-cmp/cmp" intoto "github.com/in-toto/attestation/go/v1" "github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/common" slsa "github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/v0.2" "github.com/tektoncd/chains/pkg/chains/formats" - "github.com/tektoncd/chains/pkg/chains/formats/slsa/extract" "github.com/tektoncd/chains/pkg/chains/objects" + v1 "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1" "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1" "google.golang.org/grpc" "google.golang.org/grpc/codes" @@ -46,6 +47,8 @@ import ( "knative.dev/pkg/logging" logtesting "knative.dev/pkg/logging/testing" rtesting "knative.dev/pkg/reconciler/testing" + + _ "github.com/tektoncd/chains/pkg/chains/formats/all" ) const ( @@ -159,6 +162,125 @@ var ( }, } + // TaskRun with step results. + taskRunWithStepResults = &v1.TaskRun{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "default", + Name: "taskrun-with-steps", + UID: types.UID("uid-pipeline"), + Labels: map[string]string{ + "tekton.dev/pipelineTask": "taskrun-with-steps", + }, + }, + Status: v1.TaskRunStatus{ + TaskRunStatusFields: v1.TaskRunStatusFields{ + CompletionTime: &metav1.Time{Time: time.Date(1995, time.December, 24, 6, 12, 12, 12, time.UTC)}, + Results: []v1.TaskRunResult{ + { + Name: "art1-ARTIFACT_OUTPUTS", + Value: *v1.NewObject(map[string]string{ + "uri": "gcr.io/img1", + "digest": "sha256:52e18b100a8da6e191a1955913ba127b75a8b38146cd9b0f573ec1d8e8ecd135", + "isBuildArtifact": "true", + }), + }, + { + Name: "art2-ARTIFACT_OUTPUTS", + Value: *v1.NewObject(map[string]string{ + "uri": "gcr.io/img2", + "digest": "sha256:2996854378975c2f8011ddf0526975d1aaf1790b404da7aad4bf25293055bc8b", + "isBuildArtifact": "false", + }), + }, + {Name: "IMAGE_URL", Value: *v1.NewStructuredValues("gcr.io/img3")}, + {Name: "IMAGE_DIGEST", Value: *v1.NewStructuredValues("sha256:ef334b5d9704da9b325ed6d4e3e5327863847e2da6d43f81831fd1decbdb2213")}, + }, + Steps: []v1.StepState{ + { + Name: "step1", + Results: []v1.TaskRunStepResult{ + { + Name: "art3-repeated-ARTIFACT_OUTPUTS", + Value: *v1.NewObject(map[string]string{ + "uri": "gcr.io/img1", + "digest": "sha256:52e18b100a8da6e191a1955913ba127b75a8b38146cd9b0f573ec1d8e8ecd135", + "isBuildArtifact": "true", + }), + }, + { + Name: "art4-ARTIFACT_OUTPUTS", + Value: *v1.NewObject(map[string]string{ + "uri": "gcr.io/img4", + "digest": "sha256:910700c5ace59f70588c4e2a38ed131146c9f65c94379dfe12376075fc2f338f", + "isBuildArtifact": "true", + }), + }, + { + Name: "art5-ARTIFACT_OUTPUTS", + Value: *v1.NewObject(map[string]string{ + "uri": "gcr.io/img5", + "digest": "sha256:7492314e32aa75ff1f2cfea35b7dda85d8831929d076aab52420c3400c8c65d8", + }), + }, + }, + }, + }, + }, + }, + } + + taskRunWithStepResultsProvenancev2alpha4 = intoto.Statement{ + Subject: []*intoto.ResourceDescriptor{ + { + Name: "gcr.io/img1", + Digest: common.DigestSet{ + "sha256": "52e18b100a8da6e191a1955913ba127b75a8b38146cd9b0f573ec1d8e8ecd135", + }, + }, + { + Name: "gcr.io/img4", + Digest: common.DigestSet{ + "sha256": "910700c5ace59f70588c4e2a38ed131146c9f65c94379dfe12376075fc2f338f", + }, + }, + { + Name: "gcr.io/img3", + Digest: common.DigestSet{ + "sha256": "ef334b5d9704da9b325ed6d4e3e5327863847e2da6d43f81831fd1decbdb2213", + }, + }, + }, + } + + pipelineRunWithStepResultsProvenancev2alpha4 = intoto.Statement{ + Subject: []*intoto.ResourceDescriptor{ + { + Name: "gcr.io/img1", + Digest: common.DigestSet{ + "sha256": "52e18b100a8da6e191a1955913ba127b75a8b38146cd9b0f573ec1d8e8ecd135", + }, + }, + { + Name: "gcr.io/img0", + Digest: common.DigestSet{ + "sha256": "e82e58757ff417de62d35689a6ff58f4a3975c331595ceda979d900d4f5de465", + }, + }, + { + Name: "gcr.io/img4", + Digest: common.DigestSet{ + "sha256": "910700c5ace59f70588c4e2a38ed131146c9f65c94379dfe12376075fc2f338f", + }, + }, + { + Name: "gcr.io/img3", + Digest: common.DigestSet{ + "sha256": "ef334b5d9704da9b325ed6d4e3e5327863847e2da6d43f81831fd1decbdb2213", + }, + }, + }, + } + // ci pipelinerun provenance ciPipelineRunPredicate = slsa.ProvenancePredicate{ Materials: cloneTaskRunPredicate.Materials, @@ -173,10 +295,11 @@ type args struct { } type testConfig struct { - name string - args args - wantOccurrences []*pb.Occurrence - wantErr bool + name string + args args + withDeepInspection bool + wantOccurrences []*pb.Occurrence + wantErr bool } // This function is to test the implementation of the fake server's ListOccurrences function. @@ -327,6 +450,29 @@ func TestGrafeasBackend_StoreAndRetrieve(t *testing.T) { wantOccurrences: nil, wantErr: true, }, + { + name: "slsav1/v2alpha4 taskrun format, no error", + args: args{ + runObject: &objects.TaskRunObjectV1{ + TaskRun: taskRunWithStepResults, + }, + payload: getRawFromProto(t, &taskRunWithStepResultsProvenancev2alpha4), + signature: "bar", + opts: config.StorageOpts{PayloadFormat: formats.PayloadTypeSlsav2alpha4}, + }, + wantOccurrences: getOccFromProvenance(t, &taskRunWithStepResultsProvenancev2alpha4, "taskrun"), + }, + { + name: "slsav1/v2alpha4 pipelinerun format, no error", + withDeepInspection: true, + args: args{ + runObject: getPipelineRunWithSteps(t), + payload: getRawFromProto(t, &pipelineRunWithStepResultsProvenancev2alpha4), + signature: "bar", + opts: config.StorageOpts{PayloadFormat: formats.PayloadTypeSlsav2alpha4}, + }, + wantOccurrences: getOccFromProvenance(t, &pipelineRunWithStepResultsProvenancev2alpha4, "pipelinerun"), + }, } // setup connection @@ -354,7 +500,7 @@ func TestGrafeasBackend_StoreAndRetrieve(t *testing.T) { }, Artifacts: config.ArtifactConfigs{ PipelineRuns: config.Artifact{ - DeepInspectionEnabled: false, + DeepInspectionEnabled: test.withDeepInspection, }, }, }, @@ -388,6 +534,112 @@ func TestGrafeasBackend_StoreAndRetrieve(t *testing.T) { } } +func getPipelineRunWithSteps(t *testing.T) *objects.PipelineRunObjectV1 { + t.Helper() + pr := &v1.PipelineRun{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "default", + Name: "pipeline-with-steps", + UID: types.UID("uid-pipeline"), + }, + Status: v1.PipelineRunStatus{ + PipelineRunStatusFields: v1.PipelineRunStatusFields{ + Results: []v1.PipelineRunResult{ + { + Name: "art1-ARTIFACT_OUTPUTS", + Value: *v1.NewObject(map[string]string{ + "uri": "gcr.io/img1", + "digest": "sha256:52e18b100a8da6e191a1955913ba127b75a8b38146cd9b0f573ec1d8e8ecd135", + "isBuildArtifact": "true", + }), + }, + { + Name: "art0-ARTIFACT_OUTPUTS", + Value: *v1.NewObject(map[string]string{ + "uri": "gcr.io/img0", + "digest": "sha256:e82e58757ff417de62d35689a6ff58f4a3975c331595ceda979d900d4f5de465", + "isBuildArtifact": "true", + }), + }, + { + Name: "art0-ARTIFACT_OUTPUTS", + Value: *v1.NewObject(map[string]string{ + "uri": "gcr.io/img7", + "digest": "sha256:dc9c5a6b73b21dbc5c49254907fafead52ce59604e073c200c2c38002a9102e7", + "isBuildArtifact": "false", + }), + }, + }, + PipelineSpec: &v1.PipelineSpec{ + Tasks: []v1.PipelineTask{ + { + Name: "taskrun-with-steps", + }, + }, + }, + }, + }, + } + + prObj := &objects.PipelineRunObjectV1{ + PipelineRun: pr, + } + + prObj.AppendTaskRun(taskRunWithStepResults) + + return prObj +} + +func getOccFromProvenance(t *testing.T, provenance *intoto.Statement, noteType string) []*pb.Occurrence { + t.Helper() + var intotoSubjects []*pb.Subject + var occurs []*pb.Occurrence + subjects := provenance.Subject + noteName := fmt.Sprintf("projects/%s/notes/%s-%s-intoto", ProjectID, NoteID, noteType) + + for _, subject := range subjects { + intotoSubjects = append(intotoSubjects, &pb.Subject{ + Name: subject.Name, + Digest: subject.Digest, + }) + } + + for _, subject := range subjects { + identifier := fmt.Sprintf("%v@sha256:%v", subject.Name, subject.Digest["sha256"]) + occ := &pb.Occurrence{ + Name: identifier, + ResourceUri: identifier, + NoteName: noteName, + Details: &pb.Occurrence_Build{ + Build: &pb.BuildOccurrence{ + IntotoStatement: &pb.InTotoStatement{ + Subject: intotoSubjects, + Predicate: &pb.InTotoStatement_SlsaProvenanceZeroTwo{ + SlsaProvenanceZeroTwo: &pb.SlsaProvenanceZeroTwo{ + Builder: &pb.SlsaProvenanceZeroTwo_SlsaBuilder{}, + Invocation: &pb.SlsaProvenanceZeroTwo_SlsaInvocation{ + ConfigSource: &pb.SlsaProvenanceZeroTwo_SlsaConfigSource{}, + }, + }, + }, + }, + }, + }, + Envelope: &pb.Envelope{ + Payload: getRawFromProto(t, provenance), + PayloadType: "application/vnd.in-toto+json", + Signatures: []*pb.EnvelopeSignature{ + {Sig: []byte("bar")}, + }, + }, + } + + occurs = append(occurs, occ) + } + + return occurs +} + // test attestation storage and retrieval func testStoreAndRetrieveHelper(ctx context.Context, t *testing.T, test testConfig, backend Backend) { t.Helper() @@ -407,7 +659,7 @@ func testStoreAndRetrieveHelper(ctx context.Context, t *testing.T, test testConf expectSignature[test.args.opts.FullKey] = []string{test.args.signature} } if _, ok := formats.IntotoAttestationSet[test.args.opts.PayloadFormat]; ok { - allURIs := extract.RetrieveAllArtifactURIs(ctx, test.args.runObject, false) + allURIs := retrieveAllArtifactURIs(ctx, t, test, backend) for _, u := range allURIs { expectSignature[u] = []string{test.args.signature} } @@ -429,7 +681,7 @@ func testStoreAndRetrieveHelper(ctx context.Context, t *testing.T, test testConf expectPayload[test.args.opts.FullKey] = string(test.args.payload) } if _, ok := formats.IntotoAttestationSet[test.args.opts.PayloadFormat]; ok { - allURIs := extract.RetrieveAllArtifactURIs(ctx, test.args.runObject, false) + allURIs := retrieveAllArtifactURIs(ctx, t, test, backend) for _, u := range allURIs { expectPayload[u] = string(test.args.payload) } @@ -445,6 +697,20 @@ func testStoreAndRetrieveHelper(ctx context.Context, t *testing.T, test testConf } } +func retrieveAllArtifactURIs(ctx context.Context, t *testing.T, test testConfig, backend Backend) []string { + t.Helper() + payloader, err := formats.GetPayloader(test.args.opts.PayloadFormat, backend.cfg) + if err != nil { + t.Fatalf("error getting payloader: %v", err) + } + allURIs, err := payloader.RetrieveAllArtifactURIs(ctx, test.args.runObject) + if err != nil { + t.Fatalf("error getting artifacts URIs: %v", err) + } + + return allURIs +} + // ------------------ occurrences for taskruns and pipelineruns -------------- // BUILD Occurrence for the build taskrun that stores the slsa provenance func getTaskRunBuildOcc(t *testing.T, identifier string) *pb.Occurrence { @@ -570,6 +836,15 @@ func getRawPayload(t *testing.T, in interface{}) []byte { return rawPayload } +func getRawFromProto(t *testing.T, in *intoto.Statement) []byte { + t.Helper() + raw, err := protojson.Marshal(in) + if err != nil { + t.Errorf("Unable to marshal the proto provenance: %v", in) + } + return raw +} + // set up the connection between grafeas server and client // and return the client object to the caller func setupConnection() (*grpc.ClientConn, pb.GrafeasClient, error) { //nolint:ireturn diff --git a/test/examples_test.go b/test/examples_test.go index f317e9e740..e2efa2a766 100644 --- a/test/examples_test.go +++ b/test/examples_test.go @@ -177,6 +177,22 @@ func TestExamples(t *testing.T) { outputLocation: "slsa/v2alpha4", predicate: "slsav1.0", }, + { + name: "pipelinerun-no-repeated-subjects-v2alpha4", + cm: map[string]string{ + "artifacts.pipelinerun.format": "slsa/v2alpha4", + "artifacts.oci.storage": "tekton", + "artifacts.pipelinerun.enable-deep-inspection": "true", + }, + pipelinesCm: map[string]string{ + "enable-api-fields": "alpha", + }, + getExampleObjects: getPipelineRunWithRepeatedBuildArtifacts, + payloadKey: "chains.tekton.dev/payload-pipelinerun-%s", + signatureKey: "chains.tekton.dev/signature-pipelinerun-%s", + outputLocation: "slsa/v2alpha4", + predicate: "slsav1.0", + }, } for _, test := range tests { @@ -527,6 +543,14 @@ func getPipelineRunWithTypeHintedResultsExamples(t *testing.T, ns string) map[st return prs } +func getPipelineRunWithRepeatedBuildArtifacts(t *testing.T, ns string) map[string]objects.TektonObject { + t.Helper() + path := "../examples/v2alpha4/pipeline-with-repeated-results.yaml" + prs := make(map[string]objects.TektonObject) + prs[path] = pipelineRunFromExample(t, ns, path) + return prs +} + func getPipelineRunExamples(t *testing.T, ns string) map[string]objects.TektonObject { t.Helper() examples := make(map[string]objects.TektonObject) diff --git a/test/testdata/slsa/v1/pipeline-output-image.json b/test/testdata/slsa/v1/pipeline-output-image.json index 5a303c06c5..2c57b0090b 100644 --- a/test/testdata/slsa/v1/pipeline-output-image.json +++ b/test/testdata/slsa/v1/pipeline-output-image.json @@ -1,6 +1,6 @@ { - "type": "https://in-toto.io/Statement/v0.1", - "predicate_type": "https://slsa.dev/provenance/v0.2", + "_type": "https://in-toto.io/Statement/v0.1", + "predicateType": "https://slsa.dev/provenance/v0.2", "subject": [ { "name": "gcr.io/foo/bar", diff --git a/test/testdata/slsa/v1/task-output-image.json b/test/testdata/slsa/v1/task-output-image.json index c816782574..6f50533549 100644 --- a/test/testdata/slsa/v1/task-output-image.json +++ b/test/testdata/slsa/v1/task-output-image.json @@ -1,6 +1,6 @@ { - "type": "https://in-toto.io/Statement/v0.1", - "predicate_type": "https://slsa.dev/provenance/v0.2", + "_type": "https://in-toto.io/Statement/v0.1", + "predicateType": "https://slsa.dev/provenance/v0.2", "subject": [ { "name": "gcr.io/foo/bar", diff --git a/test/testdata/slsa/v2/task-output-image.json b/test/testdata/slsa/v2/task-output-image.json index aa4ebf9575..46bcf76a57 100644 --- a/test/testdata/slsa/v2/task-output-image.json +++ b/test/testdata/slsa/v2/task-output-image.json @@ -1,6 +1,6 @@ { - "type": "https://in-toto.io/Statement/v0.1", - "predicate_type": "https://slsa.dev/provenance/v0.2", + "_type": "https://in-toto.io/Statement/v0.1", + "predicateType": "https://slsa.dev/provenance/v0.2", "subject": [ { "name": "gcr.io/foo/bar", diff --git a/test/testdata/slsa/v2alpha3/pipeline-output-image.json b/test/testdata/slsa/v2alpha3/pipeline-output-image.json index 74ffc938d4..569c44a509 100644 --- a/test/testdata/slsa/v2alpha3/pipeline-output-image.json +++ b/test/testdata/slsa/v2alpha3/pipeline-output-image.json @@ -1,6 +1,6 @@ { - "type": "https://in-toto.io/Statement/v1", - "predicate_type": "https://slsa.dev/provenance/v1", + "_type": "https://in-toto.io/Statement/v1", + "predicateType": "https://slsa.dev/provenance/v1", "subject": [ { "name": "gcr.io/foo/bar", diff --git a/test/testdata/slsa/v2alpha3/task-output-image.json b/test/testdata/slsa/v2alpha3/task-output-image.json index c8e8956401..bb092330d4 100644 --- a/test/testdata/slsa/v2alpha3/task-output-image.json +++ b/test/testdata/slsa/v2alpha3/task-output-image.json @@ -1,6 +1,6 @@ { - "type": "https://in-toto.io/Statement/v1", - "predicate_type": "https://slsa.dev/provenance/v1", + "_type": "https://in-toto.io/Statement/v1", + "predicateType": "https://slsa.dev/provenance/v1", "subject": [ { "name": "gcr.io/foo/bar", diff --git a/test/testdata/slsa/v2alpha4/pipeline-output-image.json b/test/testdata/slsa/v2alpha4/pipeline-output-image.json index 03a2cd2639..d7ae8848de 100644 --- a/test/testdata/slsa/v2alpha4/pipeline-output-image.json +++ b/test/testdata/slsa/v2alpha4/pipeline-output-image.json @@ -1,6 +1,6 @@ { - "type": "https://in-toto.io/Statement/v1", - "predicate_type": "https://slsa.dev/provenance/v1", + "_type": "https://in-toto.io/Statement/v1", + "predicateType": "https://slsa.dev/provenance/v1", "subject": [ { "name": "gcr.io/foo/bar", diff --git a/test/testdata/slsa/v2alpha4/pipeline-with-object-type-hinting.json b/test/testdata/slsa/v2alpha4/pipeline-with-object-type-hinting.json index 3dbc87d1cf..704f896f34 100644 --- a/test/testdata/slsa/v2alpha4/pipeline-with-object-type-hinting.json +++ b/test/testdata/slsa/v2alpha4/pipeline-with-object-type-hinting.json @@ -1,6 +1,6 @@ { - "type": "https://in-toto.io/Statement/v1", - "predicate_type": "https://slsa.dev/provenance/v1", + "_type": "https://in-toto.io/Statement/v1", + "predicateType": "https://slsa.dev/provenance/v1", "subject": [ { "name": "gcr.io/foo/img1", diff --git a/test/testdata/slsa/v2alpha4/pipeline-with-repeated-results.json b/test/testdata/slsa/v2alpha4/pipeline-with-repeated-results.json new file mode 100644 index 0000000000..3f7651843e --- /dev/null +++ b/test/testdata/slsa/v2alpha4/pipeline-with-repeated-results.json @@ -0,0 +1,181 @@ +{ + "_type": "https://in-toto.io/Statement/v1", + "subject": [ + { + "name": "gcr.io/foo/img1", + "digest": { + "sha256": "586789aa031fafc7d78a5393cdc772e0b55107ea54bb8bcf3f2cdac6c6da51ee" + } + } + ], + "predicateType": "https://slsa.dev/provenance/v1", + "predicate": { + "buildDefinition": { + "buildType": "https://tekton.dev/chains/v2/slsa", + "externalParameters": { + "runSpec": { + "pipelineSpec": { + "results": [ + { + "description": "", + "name": "output1-ARTIFACT_OUTPUTS", + "value": "$(tasks.t1.results.output1-ARTIFACT_OUTPUTS)" + }, + { + "description": "", + "name": "output2-ARTIFACT_OUTPUTS", + "value": "$(tasks.t1.results.output2)" + }, + { + "description": "", + "name": "output3-ARTIFACT_OUTPUTS", + "value": "$(tasks.t2.results.output3-ARTIFACT_OUTPUTS)" + } + ], + "tasks": [ + { + "name": "t1", + "taskSpec": { + "metadata": {}, + "results": [ + { + "name": "output1-ARTIFACT_OUTPUTS", + "properties": { + "digest": { + "type": "string" + }, + "isBuildArtifact": { + "type": "string" + }, + "uri": { + "type": "string" + } + }, + "type": "object" + }, + { + "name": "output2", + "properties": { + "digest": { + "type": "string" + }, + "uri": { + "type": "string" + } + }, + "type": "object" + } + ], + "spec": null, + "steps": [ + { + "computeResources": {}, + "image": "busybox:glibc", + "name": "step1", + "script": "echo -n \"Hello!\"\necho -n \"{\\\"uri\\\":\\\"gcr.io/foo/img1\\\", \\\"digest\\\":\\\"sha256:586789aa031fafc7d78a5393cdc772e0b55107ea54bb8bcf3f2cdac6c6da51ee\\\", \\\"isBuildArtifact\\\": \\\"true\\\" }\" > $(results.output1-ARTIFACT_OUTPUTS.path)\necho -n \"{\\\"uri\\\":\\\"gcr.io/foo/img2\\\", \\\"digest\\\":\\\"sha256:586789aa031fafc7d78a5393cdc772e0b55107ea54bb8bcf3f2cdac6c6da51ee\\\"}\" > $(results.output2.path)\n" + } + ] + } + }, + { + "name": "t2", + "taskSpec": { + "metadata": {}, + "results": [ + { + "name": "output3-ARTIFACT_OUTPUTS", + "properties": { + "digest": { + "type": "string" + }, + "isBuildArtifact": { + "type": "string" + }, + "uri": { + "type": "string" + } + }, + "type": "object" + } + ], + "spec": null, + "steps": [ + { + "computeResources": {}, + "image": "busybox:glibc", + "name": "step1", + "script": "echo -n \"Hello!\"\necho -n \"{\\\"uri\\\":\\\"gcr.io/foo/img1\\\", \\\"digest\\\":\\\"sha256:586789aa031fafc7d78a5393cdc772e0b55107ea54bb8bcf3f2cdac6c6da51ee\\\", \\\"isBuildArtifact\\\": \\\"true\\\" }\" > $(results.output3-ARTIFACT_OUTPUTS.path)\n" + } + ] + } + } + ] + }, + "taskRunTemplate": { + "serviceAccountName": "default" + }, + "timeouts": { + "pipeline": "1h0m0s" + } + } + }, + "internalParameters": { + "tekton-pipelines-feature-flags": { + "AwaitSidecarReadiness": true, + "Coschedule": "workspaces", + "DisableAffinityAssistant": false, + "DisableCredsInit": false, + "EnableAPIFields": "beta", + "EnableArtifacts": false, + "EnableCELInWhenExpression": false, + "EnableKeepPodOnCancel": false, + "EnableParamEnum": false, + "EnableProvenanceInStatus": true, + "EnableStepActions": true, + "EnableTektonOCIBundles": false, + "EnforceNonfalsifiability": "none", + "MaxResultSize": 4096, + "RequireGitSSHSecretKnownHosts": false, + "ResultExtractionMethod": "termination-message", + "RunningInEnvWithInjectedSidecars": true, + "ScopeWhenExpressionsToTask": false, + "SendCloudEventsForRuns": false, + "SetSecurityContext": false, + "VerificationNoMatchPolicy": "ignore" + } + }, + "resolvedDependencies": [ + {{range .URIDigest}} + { + "uri": "{{.URI}}", + "digest": { + "sha256": "{{.Digest}}" + } + } + {{end}} + ] + }, + "runDetails": { + "builder": { + "id": "https://tekton.dev/chains/v2" + }, + "byproducts": [ + { + "content": "eyJkaWdlc3QiOiJzaGEyNTY6NTg2Nzg5YWEwMzFmYWZjN2Q3OGE1MzkzY2RjNzcyZTBiNTUxMDdlYTU0YmI4YmNmM2YyY2RhYzZjNmRhNTFlZSIsInVyaSI6Imdjci5pby9mb28vaW1nMiJ9", + "mediaType": "application/json", + "name": "pipelineRunResults/output2-ARTIFACT_OUTPUTS" + }, + { + "content": "eyJkaWdlc3QiOiJzaGEyNTY6NTg2Nzg5YWEwMzFmYWZjN2Q3OGE1MzkzY2RjNzcyZTBiNTUxMDdlYTU0YmI4YmNmM2YyY2RhYzZjNmRhNTFlZSIsInVyaSI6Imdjci5pby9mb28vaW1nMiJ9", + "mediaType": "application/json", + "name": "taskRunResults/output2" + } + ], + "metadata": { + "invocationId": "{{.UID}}", + "startedOn": "{{.PipelineStartedOn}}", + "finishedOn": "{{.PipelineFinishedOn}}" + } + } + } +} \ No newline at end of file diff --git a/test/testdata/slsa/v2alpha4/task-output-image.json b/test/testdata/slsa/v2alpha4/task-output-image.json index 440ea595f3..d16164077c 100644 --- a/test/testdata/slsa/v2alpha4/task-output-image.json +++ b/test/testdata/slsa/v2alpha4/task-output-image.json @@ -1,6 +1,6 @@ { - "type": "https://in-toto.io/Statement/v1", - "predicate_type": "https://slsa.dev/provenance/v1", + "_type": "https://in-toto.io/Statement/v1", + "predicateType": "https://slsa.dev/provenance/v1", "subject": [ { "name": "gcr.io/foo/bar", diff --git a/test/testdata/slsa/v2alpha4/task-with-object-type-hinting.json b/test/testdata/slsa/v2alpha4/task-with-object-type-hinting.json index 9f835644a3..a8c156330d 100644 --- a/test/testdata/slsa/v2alpha4/task-with-object-type-hinting.json +++ b/test/testdata/slsa/v2alpha4/task-with-object-type-hinting.json @@ -1,6 +1,6 @@ { - "type": "https://in-toto.io/Statement/v1", - "predicate_type": "https://slsa.dev/provenance/v1", + "_type": "https://in-toto.io/Statement/v1", + "predicateType": "https://slsa.dev/provenance/v1", "subject": [ { "name": "gcr.io/foo/img2",