diff --git a/docs/config.md b/docs/config.md index a0383a4472..55b9945587 100644 --- a/docs/config.md +++ b/docs/config.md @@ -35,7 +35,7 @@ Supported keys include: | Key | Description | Supported Values | Default | | :--------------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | :----------------------------------------- | :-------- | -| `artifacts.pipelinerun.format` | The format to store `PipelineRun` payloads in. | `in-toto`, `slsa/v1`, `slsa/v2alpha3` | `in-toto` | +| `artifacts.pipelinerun.format` | The format to store `PipelineRun` payloads in. | `in-toto`, `slsa/v1`, `slsa/v2alpha3`, `slsa/v2alpha4` | `in-toto` | | `artifacts.pipelinerun.storage` | The storage backend to store `PipelineRun` signatures in. Multiple backends can be specified with comma-separated list ("tekton,oci"). To disable the `PipelineRun` artifact input an empty string (""). | `tekton`, `oci`, `gcs`, `docdb`, `grafeas` | `tekton` | | `artifacts.pipelinerun.signer` | The signature backend to sign `PipelineRun` payloads with. | `x509`, `kms` | `x509` | | `artifacts.pipelinerun.enable-deep-inspection` | This boolean option will configure whether Chains should inspect child taskruns in order to capture inputs/outputs within a pipelinerun. `"false"` means that Chains only checks pipeline level results, whereas `"true"` means Chains inspects both pipeline level and task level results. | `"true"`, `"false"` | `"false"` | @@ -45,6 +45,7 @@ Supported keys include: > - For grafeas storage backend, currently we only support Container Analysis. We will make grafeas server address configurabe within a short time. > - `slsa/v1` is an alias of `in-toto` for backwards compatibility. > - `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/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) when `artifacts.pipelinerun.enable-deep-inspection` is set to `true`. Recommended format for new chains users who want the slsav1.0 spec. ### OCI Configuration diff --git a/examples/v2alpha4/pipeline-with-object-type-hinting.yaml b/examples/v2alpha4/pipeline-with-object-type-hinting.yaml new file mode 100644 index 0000000000..1f925bd132 --- /dev/null +++ b/examples/v2alpha4/pipeline-with-object-type-hinting.yaml @@ -0,0 +1,35 @@ +apiVersion: tekton.dev/v1 +kind: PipelineRun +metadata: + name: pipeline-test-run +spec: + pipelineSpec: + results: + - name: output1-ARTIFACT_OUTPUTS + value: $(tasks.t1.results.output1) + - name: output2-ARTIFACT_OUTPUTS + value: $(tasks.t1.results.output2) + tasks: + - name: t1 + taskSpec: + results: + - name: output1 + 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.path) + echo -n "{\"uri\":\"gcr.io/foo/img2\", \"digest\":\"sha256:586789aa031fafc7d78a5393cdc772e0b55107ea54bb8bcf3f2cdac6c6da51ee\"}" > $(results.output2.path) \ No newline at end of file diff --git a/pkg/chains/formats/slsa/internal/build_definition/build_definition.go b/pkg/chains/formats/slsa/internal/build_definition/build_definition.go index 891b3c2dbc..72098d27ed 100644 --- a/pkg/chains/formats/slsa/internal/build_definition/build_definition.go +++ b/pkg/chains/formats/slsa/internal/build_definition/build_definition.go @@ -22,6 +22,7 @@ import ( externalparameters "github.com/tektoncd/chains/pkg/chains/formats/slsa/internal/external_parameters" internalparameters "github.com/tektoncd/chains/pkg/chains/formats/slsa/internal/internal_parameters" resolveddependencies "github.com/tektoncd/chains/pkg/chains/formats/slsa/internal/resolved_dependencies" + "github.com/tektoncd/chains/pkg/chains/formats/slsa/internal/slsaconfig" "github.com/tektoncd/chains/pkg/chains/objects" "google.golang.org/protobuf/encoding/protojson" "google.golang.org/protobuf/types/known/structpb" @@ -62,6 +63,46 @@ func GetTaskRunBuildDefinition(ctx context.Context, tro *objects.TaskRunObjectV1 }, nil } +// GetPipelineRunBuildDefinition returns the buildDefinition for the given PipelineRun based on the configured buildType. This will default to the slsa buildType +func GetPipelineRunBuildDefinition(ctx context.Context, pro *objects.PipelineRunObjectV1, slsaconfig *slsaconfig.SlsaConfig, resolveOpts resolveddependencies.ResolveOptions) (slsa.BuildDefinition, error) { + buildDefinitionType := slsaconfig.BuildType + if slsaconfig.BuildType == "" { + buildDefinitionType = buildtypes.SlsaBuildType + } + + td, err := resolveddependencies.GetTaskDescriptor(buildDefinitionType) + if err != nil { + return slsa.BuildDefinition{}, err + } + + rd, err := resolveddependencies.PipelineRun(ctx, pro, slsaconfig, resolveOpts, td) + if err != nil { + return slsa.BuildDefinition{}, err + } + + externalParams := externalparameters.PipelineRun(pro) + structExternalParams, err := getStruct(externalParams) + if err != nil { + return slsa.BuildDefinition{}, err + } + + internalParams, err := internalparameters.GetInternalParamters(pro, buildDefinitionType) + if err != nil { + return slsa.BuildDefinition{}, err + } + structInternalParams, err := getStruct(internalParams) + if err != nil { + return slsa.BuildDefinition{}, err + } + + return slsa.BuildDefinition{ + BuildType: buildDefinitionType, + ExternalParameters: structExternalParams, + InternalParameters: structInternalParams, + ResolvedDependencies: rd, + }, nil +} + func getStruct(data map[string]any) (*structpb.Struct, error) { bytes, err := json.Marshal(data) if err != 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 index 963d6c07b8..7b825cf213 100644 --- a/pkg/chains/formats/slsa/internal/build_definition/build_definition_test.go +++ b/pkg/chains/formats/slsa/internal/build_definition/build_definition_test.go @@ -19,16 +19,18 @@ import ( "github.com/google/go-cmp/cmp" slsa "github.com/in-toto/attestation/go/predicates/provenance/v1" + intoto "github.com/in-toto/attestation/go/v1" externalparameters "github.com/tektoncd/chains/pkg/chains/formats/slsa/internal/external_parameters" internalparameters "github.com/tektoncd/chains/pkg/chains/formats/slsa/internal/internal_parameters" resolveddependencies "github.com/tektoncd/chains/pkg/chains/formats/slsa/internal/resolved_dependencies" + "github.com/tektoncd/chains/pkg/chains/formats/slsa/internal/slsaconfig" "github.com/tektoncd/chains/pkg/chains/objects" "github.com/tektoncd/chains/pkg/internal/objectloader" "google.golang.org/protobuf/testing/protocmp" "google.golang.org/protobuf/types/known/structpb" ) -func TestGetBuildDefinition(t *testing.T) { +func TestGetTaskRunBuildDefinition(t *testing.T) { tr, err := objectloader.TaskRunFromFile("../../testdata/slsa-v2alpha4/taskrun1.json") if err != nil { t.Fatal(err) @@ -103,7 +105,7 @@ func TestGetBuildDefinition(t *testing.T) { } } -func TestUnsupportedBuildType(t *testing.T) { +func TestTaskRunUnsupportedBuildType(t *testing.T) { tr, err := objectloader.TaskRunFromFile("../../testdata/slsa-v2alpha4/taskrun1.json") if err != nil { t.Fatal(err) @@ -127,3 +129,102 @@ func getProtoStruct(t *testing.T, data map[string]any) *structpb.Struct { return protoStruct } + +func TestGetPipelineRunBuildDefinition(t *testing.T) { + pr := createPro("../../testdata/slsa-v2alpha3/pipelinerun1.json") + pr.Annotations = map[string]string{ + "annotation1": "annotation1", + } + pr.Labels = map[string]string{ + "label1": "label1", + } + tests := []struct { + name string + config *slsaconfig.SlsaConfig + want slsa.BuildDefinition + }{ + { + name: "test slsa build type", + config: &slsaconfig.SlsaConfig{BuildType: "https://tekton.dev/chains/v2/slsa"}, + want: slsa.BuildDefinition{ + BuildType: "https://tekton.dev/chains/v2/slsa", + ExternalParameters: getProtoStruct(t, externalparameters.PipelineRun(pr)), + InternalParameters: getProtoStruct(t, internalparameters.SLSAInternalParameters(pr)), + ResolvedDependencies: getResolvedDependencies(pr, resolveddependencies.AddSLSATaskDescriptor), + }, + }, + { + name: "test tekton build type", + config: &slsaconfig.SlsaConfig{BuildType: "https://tekton.dev/chains/v2/slsa-tekton"}, + want: slsa.BuildDefinition{ + BuildType: "https://tekton.dev/chains/v2/slsa-tekton", + ExternalParameters: getProtoStruct(t, externalparameters.PipelineRun(pr)), + InternalParameters: getProtoStruct(t, internalparameters.TektonInternalParameters(pr)), + ResolvedDependencies: getResolvedDependencies(pr, resolveddependencies.AddTektonTaskDescriptor), + }, + }, + { + name: "test default build type", + config: &slsaconfig.SlsaConfig{BuildType: "https://tekton.dev/chains/v2/slsa"}, + want: slsa.BuildDefinition{ + BuildType: "https://tekton.dev/chains/v2/slsa", + ExternalParameters: getProtoStruct(t, externalparameters.PipelineRun(pr)), + InternalParameters: getProtoStruct(t, internalparameters.SLSAInternalParameters(pr)), + ResolvedDependencies: getResolvedDependencies(pr, resolveddependencies.AddSLSATaskDescriptor), + }, + }, + } + + for i := range tests { + tc := &tests[i] + t.Run(tc.name, func(t *testing.T) { + bd, err := GetPipelineRunBuildDefinition(context.TODO(), pr, tc.config, resolveddependencies.ResolveOptions{}) + if err != nil { + t.Fatalf("Did not expect an error but got %v", err) + } + + if diff := cmp.Diff(&tc.want, &bd, protocmp.Transform()); diff != "" { + t.Errorf("getBuildDefinition(): -want +got: %v", diff) + } + }) + } +} + +func createPro(path string) *objects.PipelineRunObjectV1 { + pr, err := objectloader.PipelineRunFromFile(path) + if err != nil { + panic(err) + } + tr1, err := objectloader.TaskRunFromFile("../../testdata/slsa-v2alpha3/taskrun1.json") + if err != nil { + panic(err) + } + tr2, err := objectloader.TaskRunFromFile("../../testdata/slsa-v2alpha3/taskrun2.json") + if err != nil { + panic(err) + } + p := objects.NewPipelineRunObjectV1(pr) + p.AppendTaskRun(tr1) + p.AppendTaskRun(tr2) + return p +} + +func getResolvedDependencies(pr *objects.PipelineRunObjectV1, addTasks func(*objects.TaskRunObjectV1) (*intoto.ResourceDescriptor, error)) []*intoto.ResourceDescriptor { + rd, err := resolveddependencies.PipelineRun(context.Background(), pr, &slsaconfig.SlsaConfig{}, resolveddependencies.ResolveOptions{}, addTasks) + if err != nil { + return []*intoto.ResourceDescriptor{} + } + return rd +} + +func TestPipelineRunUnsupportedBuildType(t *testing.T) { + pr := createPro("../../testdata/slsa-v2alpha3/pipelinerun1.json") + + got, err := GetPipelineRunBuildDefinition(context.Background(), pr, &slsaconfig.SlsaConfig{BuildType: "bad-buildtype"}, resolveddependencies.ResolveOptions{}) + if err == nil { + t.Error("getBuildDefinition(): expected error got nil") + } + if diff := cmp.Diff(&slsa.BuildDefinition{}, &got, protocmp.Transform()); diff != "" { + t.Errorf("getBuildDefinition(): -want +got: %s", diff) + } +} diff --git a/pkg/chains/formats/slsa/internal/metadata/metadata_test.go b/pkg/chains/formats/slsa/internal/metadata/metadata_test.go index 3f6cb3e7a9..e47ae8031f 100644 --- a/pkg/chains/formats/slsa/internal/metadata/metadata_test.go +++ b/pkg/chains/formats/slsa/internal/metadata/metadata_test.go @@ -27,62 +27,117 @@ import ( ) func TestMetadata(t *testing.T) { - tr := &v1.TaskRun{ - ObjectMeta: metav1.ObjectMeta{ - Name: "my-taskrun", - Namespace: "my-namespace", - Annotations: map[string]string{ - "chains.tekton.dev/reproducible": "true", + tests := []struct { + name string + obj objects.TektonObject + expected slsa.BuildMetadata + }{ + { + name: "taskrun metadata", + obj: objects.NewTaskRunObjectV1(&v1.TaskRun{ + ObjectMeta: metav1.ObjectMeta{ + UID: "abhhf-12354-asjsdbjs23-taskrun", + }, + 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)}, + }, + }, + }), + expected: slsa.BuildMetadata{ + InvocationId: "abhhf-12354-asjsdbjs23-taskrun", + StartedOn: timestamppb.New(time.Date(1995, time.December, 24, 6, 12, 12, 12, time.UTC)), + FinishedOn: timestamppb.New(time.Date(1995, time.December, 24, 6, 12, 12, 24, time.UTC)), }, - 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)}, + { + name: "pipelinerun metadata", + obj: objects.NewPipelineRunObjectV1(&v1.PipelineRun{ + ObjectMeta: metav1.ObjectMeta{ + UID: "abhhf-12354-asjsdbjs23-pipelinerun", + }, + Status: v1.PipelineRunStatus{ + PipelineRunStatusFields: v1.PipelineRunStatusFields{ + 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)}, + }, + }, + }), + expected: slsa.BuildMetadata{ + InvocationId: "abhhf-12354-asjsdbjs23-pipelinerun", + StartedOn: timestamppb.New(time.Date(1995, time.December, 24, 6, 12, 12, 12, time.UTC)), + FinishedOn: timestamppb.New(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: timestamppb.New(start), - FinishedOn: timestamppb.New(end), - } - got := GetBuildMetadata(objects.NewTaskRunObjectV1(tr)) - if d := cmp.Diff(want, got, cmp.Options{protocmp.Transform()}); d != "" { - t.Fatalf("metadata (-want, +got):\n%s", d) + + for i := range tests { + test := &tests[i] + t.Run(test.name, func(t *testing.T) { + got := GetBuildMetadata(test.obj) + if d := cmp.Diff(&test.expected, got, protocmp.Transform()); 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{ - ObjectMeta: metav1.ObjectMeta{ - Name: "my-taskrun", - Namespace: "my-namespace", - Annotations: map[string]string{ - "chains.tekton.dev/reproducible": "true", + + tests := []struct { + name string + obj objects.TektonObject + expected slsa.BuildMetadata + }{ + { + name: "taskrun metadata", + obj: objects.NewTaskRunObjectV1(&v1.TaskRun{ + ObjectMeta: metav1.ObjectMeta{ + UID: "abhhf-12354-asjsdbjs23-taskrun", + }, + 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)}, + }, + }, + }), + expected: slsa.BuildMetadata{ + InvocationId: "abhhf-12354-asjsdbjs23-taskrun", + StartedOn: timestamppb.New(time.Date(1995, time.December, 24, 6, 12, 12, 12, tz).UTC()), + FinishedOn: timestamppb.New(time.Date(1995, time.December, 24, 6, 12, 12, 24, tz).UTC()), }, - 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)}, + { + name: "pipelinerun metadata", + obj: objects.NewPipelineRunObjectV1(&v1.PipelineRun{ + ObjectMeta: metav1.ObjectMeta{ + UID: "abhhf-12354-asjsdbjs23-pipelinerun", + }, + Status: v1.PipelineRunStatus{ + PipelineRunStatusFields: v1.PipelineRunStatusFields{ + 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)}, + }, + }, + }), + expected: slsa.BuildMetadata{ + InvocationId: "abhhf-12354-asjsdbjs23-pipelinerun", + StartedOn: timestamppb.New(time.Date(1995, time.December, 24, 6, 12, 12, 12, tz).UTC()), + FinishedOn: timestamppb.New(time.Date(1995, time.December, 24, 6, 12, 12, 24, tz).UTC()), }, }, } - 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: timestamppb.New(start), - FinishedOn: timestamppb.New(end), - } - got := GetBuildMetadata(objects.NewTaskRunObjectV1(tr)) - if d := cmp.Diff(want, got, cmp.Options{protocmp.Transform()}); d != "" { - t.Fatalf("metadata (-want, +got):\n%s", d) + + for i := range tests { + test := &tests[i] + t.Run(test.name, func(t *testing.T) { + got := GetBuildMetadata(test.obj) + if d := cmp.Diff(&test.expected, got, protocmp.Transform()); d != "" { + t.Fatalf("metadata (-want, +got):\n%s", d) + } + }) } } 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 c76a818196..6bb14a4176 100644 --- a/pkg/chains/formats/slsa/internal/resolved_dependencies/resolved_dependencies.go +++ b/pkg/chains/formats/slsa/internal/resolved_dependencies/resolved_dependencies.go @@ -23,6 +23,7 @@ import ( intoto "github.com/in-toto/attestation/go/v1" "github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/common" + buildtypes "github.com/tektoncd/chains/pkg/chains/formats/slsa/internal/build_types" "github.com/tektoncd/chains/pkg/chains/formats/slsa/internal/material" "github.com/tektoncd/chains/pkg/chains/formats/slsa/internal/slsaconfig" "github.com/tektoncd/chains/pkg/chains/objects" @@ -48,9 +49,8 @@ const ( v1beta1SpecResourceLabel = "tekton.dev/v1beta1-spec-resources" ) -// used to toggle the fields in see AddTektonTaskDescriptor -// and AddSLSATaskDescriptor -type addTaskDescriptorContent func(*objects.TaskRunObjectV1) (*intoto.ResourceDescriptor, error) +// AddTaskDescriptorContent is used to toggle the fields in see AddTektonTaskDescriptor and AddSLSATaskDescriptor +type AddTaskDescriptorContent func(*objects.TaskRunObjectV1) (*intoto.ResourceDescriptor, error) // ResolveOptions represents the configuration to be use to resolve dependencies. type ResolveOptions struct { @@ -143,7 +143,7 @@ func AddSLSATaskDescriptor(tr *objects.TaskRunObjectV1) (*intoto.ResourceDescrip // fromPipelineTask adds the resolved dependencies from pipeline tasks // such as pipeline task uri/digest for remote pipeline tasks and step and sidecar images. -func fromPipelineTask(logger *zap.SugaredLogger, pro *objects.PipelineRunObjectV1, addTasks addTaskDescriptorContent) ([]*intoto.ResourceDescriptor, error) { +func fromPipelineTask(logger *zap.SugaredLogger, pro *objects.PipelineRunObjectV1, addTasks AddTaskDescriptorContent) ([]*intoto.ResourceDescriptor, error) { pSpec := pro.Status.PipelineSpec resolvedDependencies := []*intoto.ResourceDescriptor{} if pSpec != nil { @@ -276,7 +276,7 @@ func TaskRun(ctx context.Context, opts ResolveOptions, tro *objects.TaskRunObjec } // PipelineRun constructs `predicate.resolvedDependencies` section by collecting all the artifacts that influence a pipeline run such as source code repo and step&sidecar base images. -func PipelineRun(ctx context.Context, pro *objects.PipelineRunObjectV1, slsaconfig *slsaconfig.SlsaConfig, addTasks addTaskDescriptorContent) ([]*intoto.ResourceDescriptor, error) { +func PipelineRun(ctx context.Context, pro *objects.PipelineRunObjectV1, slsaconfig *slsaconfig.SlsaConfig, opts ResolveOptions, addTasks AddTaskDescriptorContent) ([]*intoto.ResourceDescriptor, error) { var err error var resolvedDependencies []*intoto.ResourceDescriptor logger := logging.FromContext(ctx) @@ -298,6 +298,14 @@ func PipelineRun(ctx context.Context, pro *objects.PipelineRunObjectV1, slsaconf } resolvedDependencies = append(resolvedDependencies, rds...) + if slsaconfig.DeepInspectionEnabled && opts.WithStepActionsResults { + execTasks := pro.GetExecutedTasks() + for _, task := range execTasks { + stepActionMat := material.FromStepActionsResults(ctx, task) + resolvedDependencies = append(resolvedDependencies, ConvertMaterialsToResolvedDependencies(stepActionMat, InputResultName)...) + } + } + // add resolved dependencies from pipeline results mats := material.FromPipelineParamsAndResults(ctx, pro, slsaconfig) // convert materials to resolved dependencies @@ -310,3 +318,15 @@ func PipelineRun(ctx context.Context, pro *objects.PipelineRunObjectV1, slsaconf } return resolvedDependencies, nil } + +// GetTaskDescriptor returns the conrresponding addTaskDescriptor function according to the given build type. +func GetTaskDescriptor(buildDefinition string) (AddTaskDescriptorContent, error) { + switch buildDefinition { + case buildtypes.SlsaBuildType: + return AddSLSATaskDescriptor, nil + case buildtypes.TektonBuildType: + return AddTektonTaskDescriptor, nil + default: + return nil, fmt.Errorf("unsupported buildType %v", buildDefinition) + } +} 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 e19f2eecfd..45c3659c02 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 @@ -18,6 +18,8 @@ package resolveddependencies import ( "encoding/json" + "reflect" + "runtime" "strings" "testing" @@ -597,7 +599,7 @@ func TestPipelineRun(t *testing.T) { taskRuns := tektonTaskRuns() tests := []struct { name string - taskDescriptor addTaskDescriptorContent + taskDescriptor AddTaskDescriptorContent want []*intoto.ResourceDescriptor }{ { @@ -652,12 +654,55 @@ func TestPipelineRun(t *testing.T) { for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { pro := createPro("../../testdata/slsa-v2alpha3/pipelinerun1.json") - got, err := PipelineRun(ctx, pro, &slsaconfig.SlsaConfig{DeepInspectionEnabled: false}, tc.taskDescriptor) + got, err := PipelineRun(ctx, pro, &slsaconfig.SlsaConfig{DeepInspectionEnabled: false}, ResolveOptions{}, tc.taskDescriptor) if err != nil { t.Error(err) } - if d := cmp.Diff(tc.want, got, cmp.Options{protocmp.Transform()}); d != "" { - t.Errorf("PipelineRunResolvedDependencies(): -want +got: %s", got) + if d := cmp.Diff(tc.want, got, protocmp.Transform()); d != "" { + t.Errorf("PipelineRunResolvedDependencies(): -want +got: %s", d) + } + }) + } +} + +func TestGetTaskDescriptor(t *testing.T) { + tests := []struct { + name string + buildDefinitionType string + expected AddTaskDescriptorContent + shouldErr bool + }{ + { + name: "slsa task descriptor", + buildDefinitionType: "https://tekton.dev/chains/v2/slsa", + expected: AddSLSATaskDescriptor, + }, + { + name: "tekton task descriptor", + buildDefinitionType: "https://tekton.dev/chains/v2/slsa-tekton", + expected: AddTektonTaskDescriptor, + }, + { + name: "bad descriptor", + buildDefinitionType: "https://foo.io/fake", + shouldErr: true, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + td, err := GetTaskDescriptor(test.buildDefinitionType) + didErr := err != nil + + if test.shouldErr != didErr { + t.Fatalf("Unexpected behavior in error, shouldErr: %v, didErr: %v, err: %v", test.shouldErr, didErr, err) + } + + got := runtime.FuncForPC(reflect.ValueOf(td).Pointer()).Name() + expected := runtime.FuncForPC(reflect.ValueOf(test.expected).Pointer()).Name() + + if d := cmp.Diff(expected, got); d != "" { + t.Errorf("GetTaskDescriptor(): -want +got: %v", d) } }) } diff --git a/pkg/chains/formats/slsa/testdata/slsa-v2alpha4/pipelinerun-remote-resolver.json b/pkg/chains/formats/slsa/testdata/slsa-v2alpha4/pipelinerun-remote-resolver.json new file mode 100644 index 0000000000..e4fe52f6f9 --- /dev/null +++ b/pkg/chains/formats/slsa/testdata/slsa-v2alpha4/pipelinerun-remote-resolver.json @@ -0,0 +1,113 @@ +{ + "metadata": { + "name": "build-push-gke-deploy-run-2", + "uid": "5e28bf07-ed8f-4fe2-bfa0-d1f2cb90e5de" + }, + "spec": { + "params": [ + { + "name": "pathToContext", + "value": "gke-deploy/example/app" + }, + { + "name": "pathToKubernetesConfigs", + "value": "gke-deploy/example/app/config" + } + ], + "pipelineRef": { + "params": [ + { + "name": "url", + "value": "https://github.com/tektoncd/catalog" + }, + { + "name": "revision", + "value": "main" + }, + { + "name": "pathInRepo", + "value": "pipeline/build-push-gke-deploy/0.1/build-push-gke-deploy.yaml" + } + ], + "resolver": "git" + }, + "taskRunTemplate": { + "serviceAccountName": "default" + } + }, + "status": { + "childReferences": [ + { + "apiVersion": "tekton.dev/v1", + "kind": "TaskRun", + "name": "build-push-gke-deploy-run-2-kaniko", + "pipelineTaskName": "kaniko" + } + ], + "conditions": [ + { + "lastTransitionTime": "2024-04-29T20:45:15Z", + "message": "Tasks Completed: 0 (Failed: 0, Cancelled 0), Incomplete: 2, Skipped: 0", + "reason": "Running", + "status": "Unknown", + "type": "Succeeded" + } + ], + "pipelineSpec": { + "description": "This Pipeline builds, pushes, and deploys your application to a Google Kubernetes Engine cluster using gke-deploy.", + "params": [ + { + "default": ".", + "description": "The path to the build context relative to your source repo's root. This is used by Kaniko.", + "name": "pathToContext", + "type": "string" + }, + { + "description": "The path to the Kubernetes configs to deploy, relative to your source repo's root.", + "name": "pathToKubernetesConfigs", + "type": "string" + } + ], + "tasks": [ + { + "name": "gke-deploy", + "params": [ + { + "name": "ARGS", + "value": [ + "run", + "--image=gcr.io/foo/gke-deploy-tekton-demo:1.0.0", + "--filename=$(workspaces.source.path)/gke-deploy/example/app/config" + ] + } + ], + "taskRef": { + "kind": "Task", + "name": "gke-deploy" + }, + "workspaces": [ + { + "name": "source", + "workspace": "source" + } + ] + } + ], + "workspaces": [ + { + "description": "The workspace containing the source code which is to be build, pushed and deployed", + "name": "source" + } + ] + }, + "provenance": { + "refSource": { + "digest": { + "sha1": "4df486f198c3c2616ab129186fb30a74f580b5a1" + }, + "entryPoint": "pipeline/build-push-gke-deploy/0.1/build-push-gke-deploy.yaml", + "uri": "git+https://github.com/tektoncd/catalog" + } + } + } +} diff --git a/pkg/chains/formats/slsa/testdata/slsa-v2alpha4/pipelinerun1.json b/pkg/chains/formats/slsa/testdata/slsa-v2alpha4/pipelinerun1.json new file mode 100644 index 0000000000..9148fd450a --- /dev/null +++ b/pkg/chains/formats/slsa/testdata/slsa-v2alpha4/pipelinerun1.json @@ -0,0 +1,320 @@ +{ + "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" + } + } + ], + "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/pipelinerun_structured_results.json b/pkg/chains/formats/slsa/testdata/slsa-v2alpha4/pipelinerun_structured_results.json new file mode 100644 index 0000000000..dbaac82e33 --- /dev/null +++ b/pkg/chains/formats/slsa/testdata/slsa-v2alpha4/pipelinerun_structured_results.json @@ -0,0 +1,270 @@ +{ + "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": "image-ARTIFACT_INPUTS", + "value": { + "uri": "abcd", + "digest": "sha256:827521c857fdcd4374f4da5442fbae2edb01e7fbae285c3ec15673d4c1daecb7" + } + }, + { + "name": "image-ARTIFACT_OUTPUTS", + "value": { + "uri": "hello_world", + "sha256": "827521c857fdcd4374f4da5442fbae2edb01e7fbae285c3ec15673d4c1daecb7" + } + } + ], + "pipelineSpec": { + "params": [ + { + "description": "Image path on registry", + "name": "IMAGE", + "type": "string" + } + ], + "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/v2alpha3/internal/pipelinerun/pipelinerun.go b/pkg/chains/formats/slsa/v2alpha3/internal/pipelinerun/pipelinerun.go index 12f02567e5..0f9b2aa697 100644 --- a/pkg/chains/formats/slsa/v2alpha3/internal/pipelinerun/pipelinerun.go +++ b/pkg/chains/formats/slsa/v2alpha3/internal/pipelinerun/pipelinerun.go @@ -18,18 +18,13 @@ import ( "encoding/json" "fmt" - slsa "github.com/in-toto/attestation/go/predicates/provenance/v1" intoto "github.com/in-toto/attestation/go/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" - "google.golang.org/protobuf/encoding/protojson" - "google.golang.org/protobuf/types/known/structpb" - "google.golang.org/protobuf/types/known/timestamppb" ) const ( @@ -45,55 +40,14 @@ func GenerateAttestation(ctx context.Context, pro *objects.PipelineRunObjectV1, return nil, err } - bd, err := getBuildDefinition(ctx, slsaconfig, pro) + bd, err := builddefinition.GetPipelineRunBuildDefinition(ctx, pro, slsaconfig, resolveddependencies.ResolveOptions{}) if err != nil { return nil, err } - predicate := &slsa.Provenance{ - BuildDefinition: &bd, - RunDetails: &slsa.RunDetails{ - Builder: &slsa.Builder{ - Id: slsaconfig.BuilderID, - }, - Metadata: metadata(pro), - Byproducts: bp, - }, - } - - predicateStruct := &structpb.Struct{} - predicateJSON, err := protojson.Marshal(predicate) - if err != nil { - return nil, err - } - - err = protojson.Unmarshal(predicateJSON, predicateStruct) - if err != nil { - return nil, err - } + sub := extract.SubjectDigests(ctx, pro, slsaconfig) - att := &intoto.Statement{ - Type: intoto.StatementTypeUri, - PredicateType: "https://slsa.dev/provenance/v1", - Subject: extract.SubjectDigests(ctx, pro, slsaconfig), - Predicate: predicateStruct, - } - return att, nil -} - -func metadata(pro *objects.PipelineRunObjectV1) *slsa.BuildMetadata { - m := &slsa.BuildMetadata{ - InvocationId: string(pro.ObjectMeta.UID), - } - if pro.Status.StartTime != nil { - utc := pro.Status.StartTime.Time.UTC() - m.StartedOn = timestamppb.New(utc) - } - if pro.Status.CompletionTime != nil { - utc := pro.Status.CompletionTime.Time.UTC() - m.FinishedOn = timestamppb.New(utc) - } - return m + return provenance.GetSLSA1Statement(pro, sub, &bd, bp, slsaconfig) } // byproducts contains the pipelineRunResults @@ -113,74 +67,3 @@ func byproducts(pro *objects.PipelineRunObjectV1) ([]*intoto.ResourceDescriptor, } return byProd, nil } - -// getBuildDefinition get the buildDefinition based on the configured buildType. This will default to the slsa buildType -func getBuildDefinition(ctx context.Context, slsaconfig *slsaconfig.SlsaConfig, pro *objects.PipelineRunObjectV1) (slsa.BuildDefinition, error) { - // if buildType is not set in the chains-config, default to slsa build type - buildDefinitionType := slsaconfig.BuildType - if slsaconfig.BuildType == "" { - buildDefinitionType = buildtypes.SlsaBuildType - } - - switch buildDefinitionType { - case buildtypes.SlsaBuildType: - rd, err := resolveddependencies.PipelineRun(ctx, pro, slsaconfig, resolveddependencies.AddSLSATaskDescriptor) - if err != nil { - return slsa.BuildDefinition{}, err - } - extParamsStruct, err := getStruct(externalparameters.PipelineRun(pro)) - if err != nil { - return slsa.BuildDefinition{}, err - } - - intParamsStruct, err := getStruct(internalparameters.SLSAInternalParameters(pro)) - if err != nil { - return slsa.BuildDefinition{}, err - } - - return slsa.BuildDefinition{ - BuildType: buildDefinitionType, - ExternalParameters: extParamsStruct, - InternalParameters: intParamsStruct, - ResolvedDependencies: rd, - }, nil - case buildtypes.TektonBuildType: - rd, err := resolveddependencies.PipelineRun(ctx, pro, slsaconfig, resolveddependencies.AddTektonTaskDescriptor) - if err != nil { - return slsa.BuildDefinition{}, err - } - extParamsStruct, err := getStruct(externalparameters.PipelineRun(pro)) - if err != nil { - return slsa.BuildDefinition{}, err - } - - intParamsStruct, err := getStruct(internalparameters.TektonInternalParameters(pro)) - if err != nil { - return slsa.BuildDefinition{}, err - } - - return slsa.BuildDefinition{ - BuildType: buildDefinitionType, - ExternalParameters: extParamsStruct, - InternalParameters: intParamsStruct, - ResolvedDependencies: rd, - }, nil - default: - return slsa.BuildDefinition{}, fmt.Errorf("unsupported buildType %v", buildDefinitionType) - } -} - -func getStruct(data map[string]any) (*structpb.Struct, error) { - bytes, err := json.Marshal(data) - if err != nil { - return nil, err - } - - protoStruct := &structpb.Struct{} - err = protojson.Unmarshal(bytes, protoStruct) - if err != nil { - return nil, err - } - - return protoStruct, nil -} diff --git a/pkg/chains/formats/slsa/v2alpha3/internal/pipelinerun/pipelinerun_test.go b/pkg/chains/formats/slsa/v2alpha3/internal/pipelinerun/pipelinerun_test.go index a363b1b86a..aef7b0f6d6 100644 --- a/pkg/chains/formats/slsa/v2alpha3/internal/pipelinerun/pipelinerun_test.go +++ b/pkg/chains/formats/slsa/v2alpha3/internal/pipelinerun/pipelinerun_test.go @@ -17,7 +17,6 @@ limitations under the License. package pipelinerun import ( - "context" "encoding/json" "testing" "time" @@ -26,9 +25,6 @@ import ( slsa "github.com/in-toto/attestation/go/predicates/provenance/v1" intoto "github.com/in-toto/attestation/go/v1" "github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/common" - externalparameters "github.com/tektoncd/chains/pkg/chains/formats/slsa/internal/external_parameters" - internalparameters "github.com/tektoncd/chains/pkg/chains/formats/slsa/internal/internal_parameters" - resolveddependencies "github.com/tektoncd/chains/pkg/chains/formats/slsa/internal/resolved_dependencies" "github.com/tektoncd/chains/pkg/chains/formats/slsa/internal/slsaconfig" "github.com/tektoncd/chains/pkg/chains/objects" "github.com/tektoncd/chains/pkg/internal/objectloader" @@ -37,71 +33,9 @@ import ( "google.golang.org/protobuf/testing/protocmp" "google.golang.org/protobuf/types/known/structpb" "google.golang.org/protobuf/types/known/timestamppb" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" logtesting "knative.dev/pkg/logging/testing" ) -func TestMetadata(t *testing.T) { - pr := &v1.PipelineRun{ - 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.PipelineRunStatus{ - PipelineRunStatusFields: v1.PipelineRunStatusFields{ - 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: timestamppb.New(start), - FinishedOn: timestamppb.New(end), - } - got := metadata(objects.NewPipelineRunObjectV1(pr)) - if d := cmp.Diff(want, got, protocmp.Transform()); d != "" { - t.Fatalf("metadata (-want, +got):\n%s", d) - } -} - -func TestMetadataInTimeZone(t *testing.T) { - tz := time.FixedZone("Test Time", int((12 * time.Hour).Seconds())) - pr := &v1.PipelineRun{ - 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.PipelineRunStatus{ - PipelineRunStatusFields: v1.PipelineRunStatusFields{ - 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: timestamppb.New(start), - FinishedOn: timestamppb.New(end), - } - got := metadata(objects.NewPipelineRunObjectV1(pr)) - if d := cmp.Diff(want, got, protocmp.Transform()); d != "" { - t.Fatalf("metadata (-want, +got):\n%s", d) - } -} - func TestByProducts(t *testing.T) { resultValue := v1.ResultValue{Type: "string", StringVal: "result-value"} pr := &v1.PipelineRun{ @@ -170,8 +104,8 @@ func TestGenerateAttestation(t *testing.T) { slsaPredicate := slsa.Provenance{ BuildDefinition: &slsa.BuildDefinition{ BuildType: "https://tekton.dev/chains/v2/slsa", - ExternalParameters: getProtoStruct(t, externalParams), - InternalParameters: getProtoStruct(t, map[string]any{}), + ExternalParameters: getStruct(t, externalParams), + InternalParameters: getStruct(t, map[string]any{}), ResolvedDependencies: []*intoto.ResourceDescriptor{ { Uri: "git+https://github.com/test", @@ -295,94 +229,15 @@ func TestGenerateAttestation(t *testing.T) { } } -func getResolvedDependencies(addTasks func(*objects.TaskRunObjectV1) (*intoto.ResourceDescriptor, error)) []*intoto.ResourceDescriptor { - pr := createPro("../../../testdata/slsa-v2alpha3/pipelinerun1.json") - rd, err := resolveddependencies.PipelineRun(context.Background(), pr, &slsaconfig.SlsaConfig{DeepInspectionEnabled: false}, addTasks) +func getStruct(t *testing.T, data map[string]any) *structpb.Struct { + t.Helper() + bytes, err := json.Marshal(data) if err != nil { - return nil - } - return rd -} - -func TestGetBuildDefinition(t *testing.T) { - pr := createPro("../../../testdata/slsa-v2alpha3/pipelinerun1.json") - pr.Annotations = map[string]string{ - "annotation1": "annotation1", - } - pr.Labels = map[string]string{ - "label1": "label1", - } - tests := []struct { - name string - taskContent func(*objects.TaskRunObjectV1) (*intoto.ResourceDescriptor, error) - config *slsaconfig.SlsaConfig - want slsa.BuildDefinition - }{ - { - name: "test slsa build type", - taskContent: resolveddependencies.AddSLSATaskDescriptor, - config: &slsaconfig.SlsaConfig{BuildType: "https://tekton.dev/chains/v2/slsa"}, - want: slsa.BuildDefinition{ - BuildType: "https://tekton.dev/chains/v2/slsa", - ExternalParameters: getProtoStruct(t, externalparameters.PipelineRun(pr)), - InternalParameters: getProtoStruct(t, internalparameters.SLSAInternalParameters(pr)), - ResolvedDependencies: getResolvedDependencies(resolveddependencies.AddSLSATaskDescriptor), - }, - }, - { - name: "test tekton build type", - config: &slsaconfig.SlsaConfig{BuildType: "https://tekton.dev/chains/v2/slsa-tekton"}, - taskContent: resolveddependencies.AddSLSATaskDescriptor, - want: slsa.BuildDefinition{ - BuildType: "https://tekton.dev/chains/v2/slsa-tekton", - ExternalParameters: getProtoStruct(t, externalparameters.PipelineRun(pr)), - InternalParameters: getProtoStruct(t, internalparameters.TektonInternalParameters(pr)), - ResolvedDependencies: getResolvedDependencies(resolveddependencies.AddTektonTaskDescriptor), - }, - }, - { - name: "test default build type", - config: &slsaconfig.SlsaConfig{BuildType: "https://tekton.dev/chains/v2/slsa"}, - taskContent: resolveddependencies.AddSLSATaskDescriptor, - want: slsa.BuildDefinition{ - BuildType: "https://tekton.dev/chains/v2/slsa", - ExternalParameters: getProtoStruct(t, externalparameters.PipelineRun(pr)), - InternalParameters: getProtoStruct(t, internalparameters.SLSAInternalParameters(pr)), - ResolvedDependencies: getResolvedDependencies(resolveddependencies.AddSLSATaskDescriptor), - }, - }, - } - - for i := range tests { - tc := &tests[i] - t.Run(tc.name, func(t *testing.T) { - bd, err := getBuildDefinition(context.Background(), tc.config, pr) - if err != nil { - t.Fatalf("Did not expect an error but got %v", err) - } - - if diff := cmp.Diff(&tc.want, &bd, cmp.Options{protocmp.Transform()}); diff != "" { - t.Errorf("getBuildDefinition(): -want +got: %v", diff) - } - }) - } -} - -func TestUnsupportedBuildType(t *testing.T) { - pr := createPro("../../../testdata/slsa-v2alpha3/pipelinerun1.json") - - got, err := getBuildDefinition(context.Background(), &slsaconfig.SlsaConfig{BuildType: "bad-buildtype"}, pr) - if err == nil { - t.Error("getBuildDefinition(): expected error got nil") - } - if diff := cmp.Diff(&slsa.BuildDefinition{}, &got, cmp.Options{protocmp.Transform()}); diff != "" { - t.Errorf("getBuildDefinition(): -want +got: %s", diff) + t.Fatalf("error getting proto struct: %v", err) } -} -func getProtoStruct(t *testing.T, data map[string]any) *structpb.Struct { - t.Helper() - protoStruct, err := getStruct(data) + protoStruct := &structpb.Struct{} + err = protojson.Unmarshal(bytes, protoStruct) if err != nil { t.Fatalf("error getting proto struct: %v", err) } diff --git a/pkg/chains/formats/slsa/v2alpha4/internal/pipelinerun/pipelinerun.go b/pkg/chains/formats/slsa/v2alpha4/internal/pipelinerun/pipelinerun.go new file mode 100644 index 0000000000..b4130c186c --- /dev/null +++ b/pkg/chains/formats/slsa/v2alpha4/internal/pipelinerun/pipelinerun.go @@ -0,0 +1,89 @@ +/* +Copyright 2023 The Tekton Authors +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package pipelinerun + +import ( + "context" + + intoto "github.com/in-toto/attestation/go/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/formats/slsa/v2alpha4/internal/taskrun" + "github.com/tektoncd/chains/pkg/chains/objects" +) + +const ( + pipelineRunResults = "pipelineRunResults/%s" + // JSONMediaType is the media type of json encoded content used in resource descriptors + JSONMediaType = "application/json" +) + +// GenerateAttestation generates a provenance statement with SLSA v1.0 predicate for a pipeline run. +func GenerateAttestation(ctx context.Context, pro *objects.PipelineRunObjectV1, slsaconfig *slsaconfig.SlsaConfig) (interface{}, error) { + bp, err := byproducts(pro, slsaconfig) + if err != nil { + return nil, err + } + + opts := resolveddependencies.ResolveOptions{WithStepActionsResults: true} + bd, err := builddefinition.GetPipelineRunBuildDefinition(ctx, pro, slsaconfig, opts) + if err != nil { + return nil, err + } + + sub := subjectDigests(ctx, pro, slsaconfig) + + return provenance.GetSLSA1Statement(pro, sub, &bd, bp, slsaconfig) +} + +// byproducts contains the pipelineRunResults +func byproducts(pro *objects.PipelineRunObjectV1, slsaconfig *slsaconfig.SlsaConfig) ([]*intoto.ResourceDescriptor, error) { + byProd := []*intoto.ResourceDescriptor{} + + res, err := results.GetResultsWithoutBuildArtifacts(pro.GetResults(), pipelineRunResults) + if err != nil { + return nil, err + } + byProd = append(byProd, res...) + + if !slsaconfig.DeepInspectionEnabled { + return byProd, nil + } + + tros := pro.GetExecutedTasks() + + for _, tro := range tros { + taskProds, err := taskrun.ByProducts(tro) + if err != nil { + return nil, err + } + byProd = append(byProd, taskProds...) + } + + return byProd, nil +} + +func subjectDigests(ctx context.Context, pro *objects.PipelineRunObjectV1, slsaconfig *slsaconfig.SlsaConfig) []*intoto.ResourceDescriptor { + results := pro.GetResults() + + if slsaconfig.DeepInspectionEnabled { + results = append(results, pro.GetTaskAndStepResults()...) + } + + return extract.SubjectsFromBuildArtifact(ctx, results) +} diff --git a/pkg/chains/formats/slsa/v2alpha4/internal/pipelinerun/pipelinerun_test.go b/pkg/chains/formats/slsa/v2alpha4/internal/pipelinerun/pipelinerun_test.go new file mode 100644 index 0000000000..c37989b095 --- /dev/null +++ b/pkg/chains/formats/slsa/v2alpha4/internal/pipelinerun/pipelinerun_test.go @@ -0,0 +1,249 @@ +/* +Copyright 2021 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 pipelinerun + +import ( + "encoding/json" + "testing" + "time" + + "github.com/google/go-cmp/cmp" + slsa "github.com/in-toto/attestation/go/predicates/provenance/v1" + intoto "github.com/in-toto/attestation/go/v1" + "github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/common" + "github.com/tektoncd/chains/pkg/chains/formats/slsa/internal/compare" + "github.com/tektoncd/chains/pkg/chains/formats/slsa/internal/slsaconfig" + "github.com/tektoncd/chains/pkg/chains/objects" + "github.com/tektoncd/chains/pkg/internal/objectloader" + v1 "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1" + "google.golang.org/protobuf/encoding/protojson" + "google.golang.org/protobuf/testing/protocmp" + "google.golang.org/protobuf/types/known/structpb" + "google.golang.org/protobuf/types/known/timestamppb" + logtesting "knative.dev/pkg/logging/testing" +) + +func TestByProducts(t *testing.T) { + resultValue := v1.ResultValue{Type: "string", StringVal: "result-value"} + pr := &v1.PipelineRun{ + Status: v1.PipelineRunStatus{ + PipelineRunStatusFields: v1.PipelineRunStatusFields{ + Results: []v1.PipelineRunResult{ + { + Name: "result-name", + Value: resultValue, + }, + }, + }, + }, + } + + resultBytes, err := json.Marshal(resultValue) + if err != nil { + t.Fatalf("Could not marshal results: %s", err) + } + want := []*intoto.ResourceDescriptor{ + { + Name: "pipelineRunResults/result-name", + Content: resultBytes, + MediaType: JSONMediaType, + }, + } + got, err := byproducts(objects.NewPipelineRunObjectV1(pr), &slsaconfig.SlsaConfig{}) + if err != nil { + t.Fatalf("Could not extract byproducts: %s", err) + } + if d := cmp.Diff(&want, &got, protocmp.Transform()); d != "" { + t.Fatalf("byproducts (-want, +got):\n%s", d) + } +} + +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) + + predicate := &slsa.Provenance{ + BuildDefinition: &slsa.BuildDefinition{ + BuildType: "https://tekton.dev/chains/v2/slsa", + ExternalParameters: getStruct(t, map[string]any{ + "runSpec": pr.Spec, + }), + InternalParameters: getStruct(t, map[string]any{}), + ResolvedDependencies: []*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"}, + }, + { + Uri: "abc", + Digest: common.DigestSet{"sha256": "827521c857fdcd4374f4da5442fbae2edb01e7fbae285c3ec15673d4c1daecb7"}, + Name: "inputs/result", + }, + { + Name: "inputs/result", + Uri: "git+https://git.test.com.git", + Digest: common.DigestSet{"sha1": "abcd"}, + }, + }, + }, + RunDetails: &slsa.RunDetails{ + Builder: &slsa.Builder{ + Id: "test_builder-1", + }, + Metadata: &slsa.BuildMetadata{ + InvocationId: "abhhf-12354-asjsdbjs23-3435353n", + StartedOn: timestamppb.New(e1BuildStart), + FinishedOn: timestamppb.New(e1BuildFinished), + }, + Byproducts: []*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, + }, + }, + }, + } + + predicateStruct := getPredicateStruct(t, predicate) + + want := &intoto.Statement{ + Type: intoto.StatementTypeUri, + PredicateType: "https://slsa.dev/provenance/v1", + Subject: []*intoto.ResourceDescriptor{ + { + Name: "abc", + Digest: common.DigestSet{ + "sha256": "827521c857fdcd4374f4da5442fbae2edb01e7fbae285c3ec15673d4c1daecb7", + }, + }, + { + Name: "test.io/test/image", + Digest: common.DigestSet{ + "sha256": "827521c857fdcd4374f4da5442fbae2edb01e7fbae285c3ec15673d4c1daecb7", + }, + }, + }, + Predicate: predicateStruct, + } + + got, err := GenerateAttestation(ctx, pr, &slsaconfig.SlsaConfig{ + BuilderID: "test_builder-1", + DeepInspectionEnabled: false, + BuildType: "https://tekton.dev/chains/v2/slsa", + }) + + if err != nil { + t.Errorf("unwant error: %s", err.Error()) + } + + opts := compare.SLSAV1CompareOptions() + opts = append(opts, protocmp.Transform()) + + if diff := cmp.Diff(want, got, opts...); diff != "" { + t.Errorf("GenerateAttestation(): -want +got: %s", diff) + } +} + +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") + if err != nil { + panic(err) + } + p := objects.NewPipelineRunObjectV1(pr) + p.AppendTaskRun(tr1) + p.AppendTaskRun(tr2) + return p +} + +func getStruct(t *testing.T, data map[string]any) *structpb.Struct { + t.Helper() + bytes, err := json.Marshal(data) + if err != nil { + t.Fatalf("error getting proto struct: %v", err) + } + + protoStruct := &structpb.Struct{} + err = protojson.Unmarshal(bytes, protoStruct) + if err != nil { + t.Fatalf("error getting proto struct: %v", err) + } + + return protoStruct +} + +func getPredicateStruct(t *testing.T, slsaPredicate *slsa.Provenance) *structpb.Struct { + t.Helper() + predicateJSON, err := protojson.Marshal(slsaPredicate) + if err != nil { + t.Fatalf("error getting SLSA predicate proto struct: %v", err) + } + + predicateStruct := &structpb.Struct{} + err = protojson.Unmarshal(predicateJSON, predicateStruct) + if err != nil { + t.Fatalf("error getting SLSA predicate proto struct: %v", err) + } + + return predicateStruct +} diff --git a/pkg/chains/formats/slsa/v2alpha4/internal/taskrun/taskrun.go b/pkg/chains/formats/slsa/v2alpha4/internal/taskrun/taskrun.go index 8b41acb0be..10577b347e 100644 --- a/pkg/chains/formats/slsa/v2alpha4/internal/taskrun/taskrun.go +++ b/pkg/chains/formats/slsa/v2alpha4/internal/taskrun/taskrun.go @@ -33,7 +33,7 @@ const ( // 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) + bp, err := ByProducts(tro) if err != nil { return nil, err } @@ -50,7 +50,8 @@ func GenerateAttestation(ctx context.Context, tro *objects.TaskRunObjectV1, slsa return provenance.GetSLSA1Statement(tro, sub, &bd, bp, slsaConfig) } -func byproducts(tro *objects.TaskRunObjectV1) ([]*intoto.ResourceDescriptor, error) { +// ByProducts returns the results categorized as byproduct from the given TaskRun. +func ByProducts(tro *objects.TaskRunObjectV1) ([]*intoto.ResourceDescriptor, error) { byProd := []*intoto.ResourceDescriptor{} res, err := results.GetResultsWithoutBuildArtifacts(tro.GetResults(), taskRunResults) diff --git a/pkg/chains/formats/slsa/v2alpha4/internal/taskrun/taskrun_test.go b/pkg/chains/formats/slsa/v2alpha4/internal/taskrun/taskrun_test.go index 9938287048..1a6aed1fa8 100644 --- a/pkg/chains/formats/slsa/v2alpha4/internal/taskrun/taskrun_test.go +++ b/pkg/chains/formats/slsa/v2alpha4/internal/taskrun/taskrun_test.go @@ -67,7 +67,7 @@ func TestByProducts(t *testing.T) { MediaType: jsonMediaType, }, } - got, err := byproducts(objects.NewTaskRunObjectV1(tr)) + got, err := ByProducts(objects.NewTaskRunObjectV1(tr)) if err != nil { t.Fatalf("Could not extract byproducts: %s", err) } diff --git a/pkg/chains/formats/slsa/v2alpha4/slsav2.go b/pkg/chains/formats/slsa/v2alpha4/slsav2.go index 3db3fbeeff..d756cac477 100644 --- a/pkg/chains/formats/slsa/v2alpha4/slsav2.go +++ b/pkg/chains/formats/slsa/v2alpha4/slsav2.go @@ -22,6 +22,7 @@ import ( "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" "github.com/tektoncd/chains/pkg/chains/formats/slsa/v2alpha4/internal/taskrun" "github.com/tektoncd/chains/pkg/chains/objects" @@ -62,6 +63,8 @@ func (s *Slsa) CreatePayload(ctx context.Context, obj interface{}) (interface{}, switch v := obj.(type) { case *objects.TaskRunObjectV1: return taskrun.GenerateAttestation(ctx, v, s.slsaConfig) + case *objects.PipelineRunObjectV1: + return pipelinerun.GenerateAttestation(ctx, v, s.slsaConfig) default: return nil, fmt.Errorf("intoto does not support type: %s", v) } diff --git a/pkg/chains/objects/objects.go b/pkg/chains/objects/objects.go index 0b6169d2b8..8c24fca546 100644 --- a/pkg/chains/objects/objects.go +++ b/pkg/chains/objects/objects.go @@ -294,6 +294,16 @@ func (pro *PipelineRunObjectV1) GetResults() []Result { return res } +// GetTaskAndStepResults returns the results of the associated TaskRuns of the PipelineRun. +func (pro *PipelineRunObjectV1) GetTaskAndStepResults() (results []Result) { + execTasks := pro.GetExecutedTasks() + for _, task := range execTasks { + results = append(results, task.GetResults()...) + results = append(results, task.GetStepResults()...) + } + return +} + // Get the ServiceAccount declared in the PipelineRun func (pro *PipelineRunObjectV1) GetServiceAccountName() string { return pro.Spec.TaskRunTemplate.ServiceAccountName @@ -379,6 +389,27 @@ func (pro *PipelineRunObjectV1) GetCompletitionTime() *time.Time { return utc } +// GetExecutedTasks returns the tasks that were executed during the pipeline run. +func (pro *PipelineRunObjectV1) GetExecutedTasks() (tro []*TaskRunObjectV1) { + pSpec := pro.Status.PipelineSpec + if pSpec == nil { + return + } + tasks := pSpec.Tasks + tasks = append(tasks, pSpec.Finally...) + for _, task := range tasks { + tr := pro.GetTaskRunFromTask(task.Name) + + if tr == nil || tr.Status.CompletionTime == nil { + continue + } + + tro = append(tro, tr) + } + + return +} + // Get the imgPullSecrets from a pod template, if they exist func getPodPullSecrets(podTemplate *pod.Template) []string { imgPullSecrets := []string{} diff --git a/pkg/config/config.go b/pkg/config/config.go index 5ec0e4233f..c1df774f68 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -271,7 +271,7 @@ func NewConfigFromMap(data map[string]string) (*Config, error) { asString(taskrunSignerKey, &cfg.Artifacts.TaskRuns.Signer, "x509", "kms"), // PipelineRuns - asString(pipelinerunFormatKey, &cfg.Artifacts.PipelineRuns.Format, "in-toto", "slsa/v1", "slsa/v2alpha3"), + asString(pipelinerunFormatKey, &cfg.Artifacts.PipelineRuns.Format, "in-toto", "slsa/v1", "slsa/v2alpha3", "slsa/v2alpha4"), asStringSet(pipelinerunStorageKey, &cfg.Artifacts.PipelineRuns.StorageBackend, sets.New[string]("tekton", "oci", "gcs", "docdb", "grafeas")), asString(pipelinerunSignerKey, &cfg.Artifacts.PipelineRuns.Signer, "x509", "kms"), asBool(pipelinerunEnableDeepInspectionKey, &cfg.Artifacts.PipelineRuns.DeepInspectionEnabled), diff --git a/test/examples_test.go b/test/examples_test.go index 334c07cad9..f317e9e740 100644 --- a/test/examples_test.go +++ b/test/examples_test.go @@ -150,6 +150,33 @@ func TestExamples(t *testing.T) { outputLocation: "slsa/v2alpha4", predicate: "slsav1.0", }, + { + name: "pipelinerun-examples-slsa-v2alpha4", + cm: map[string]string{ + "artifacts.pipelinerun.format": "slsa/v2alpha4", + "artifacts.oci.storage": "tekton", + }, + getExampleObjects: getPipelineRunExamples, + payloadKey: "chains.tekton.dev/payload-pipelinerun-%s", + signatureKey: "chains.tekton.dev/signature-pipelinerun-%s", + outputLocation: "slsa/v2alpha4", + predicate: "slsav1.0", + }, + { + name: "pipelinerun-type-hinted-results-v2alpha4", + cm: map[string]string{ + "artifacts.pipelinerun.format": "slsa/v2alpha4", + "artifacts.oci.storage": "tekton", + }, + pipelinesCm: map[string]string{ + "enable-api-fields": "alpha", + }, + getExampleObjects: getPipelineRunWithTypeHintedResultsExamples, + payloadKey: "chains.tekton.dev/payload-pipelinerun-%s", + signatureKey: "chains.tekton.dev/signature-pipelinerun-%s", + outputLocation: "slsa/v2alpha4", + predicate: "slsav1.0", + }, } for _, test := range tests { @@ -492,6 +519,14 @@ func getTaskRunWithTypeHintedResultsExamples(t *testing.T, ns string) map[string return trs } +func getPipelineRunWithTypeHintedResultsExamples(t *testing.T, ns string) map[string]objects.TektonObject { + t.Helper() + path := "../examples/v2alpha4/pipeline-with-object-type-hinting.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/v2alpha4/pipeline-output-image.json b/test/testdata/slsa/v2alpha4/pipeline-output-image.json new file mode 100644 index 0000000000..03a2cd2639 --- /dev/null +++ b/test/testdata/slsa/v2alpha4/pipeline-output-image.json @@ -0,0 +1,114 @@ +{ + "type": "https://in-toto.io/Statement/v1", + "predicate_type": "https://slsa.dev/provenance/v1", + "subject": [ + { + "name": "gcr.io/foo/bar", + "digest": { + "sha256": "05f95b26ed10668b7183c1e2da98610e91372fa9f510046d4ce5812addad86b5" + } + } + ], + "predicate": { + "buildDefinition": { + "buildType": "https://tekton.dev/chains/v2/slsa", + "externalParameters": { + "runSpec": { + "taskRunTemplate": { + "serviceAccountName": "default" + }, + "params": [ + { + "name": "CHAINS-GIT_COMMIT", + "value": "my-git-commit" + },{ + "name": "CHAINS-GIT_URL", + "value": "https://my-git-url" + } + ], + "pipelineSpec": { + "results": [ + { + "name": "IMAGE_URL", + "description": "", + "value": "$(tasks.buildimage.results.IMAGE_URL)" + }, + { + "name": "IMAGE_DIGEST", + "description": "", + "value": "$(tasks.buildimage.results.IMAGE_DIGEST)" + } + ], + "tasks": [ + { + "name": "buildimage", + "taskSpec": { + "metadata": {}, + "steps": [ + { + "name": "create-dockerfile", + "image": "distroless.dev/busybox@sha256:186312fcf3f381b5fc1dd80b1afc0d316f3ed39fb4add8ff900d1f0c7c49a92c", + "computeResources": {}, + "script": "#!/usr/bin/env sh\necho 'gcr.io/foo/bar' | tee $(results.IMAGE_URL.path)\necho 'sha256:05f95b26ed10668b7183c1e2da98610e91372fa9f510046d4ce5812addad86b5' | tee $(results.IMAGE_DIGEST.path)", + "volumeMounts": [ + { + "mountPath": "/dockerfile", + "name": "dockerfile" + } + ] + } + ], + "spec": null, + "results": [ + { + "name": "IMAGE_URL", + "type": "string" + },{ + "name": "IMAGE_DIGEST", + "type": "string" + } + ], + "volumes": [ + { + "emptyDir": {}, + "name": "dockerfile" + } + ] + + } + } + ] + }, + "timeouts": { + "pipeline": "1h0m0s" + } + } + }, + "resolvedDependencies": [ + {{range .URIDigest}} + { + "uri": "{{.URI}}", + "digest": { + "sha256": "{{.Digest}}" + } + }, + {{end}} + { + "uri": "git+https://my-git-url.git", + "digest": {"sha1": "my-git-commit"}, + "name": "inputs/result" + } + ] + }, + "runDetails": { + "builder": { + "id": "https://tekton.dev/chains/v2" + }, + "metadata": { + "invocationId": "{{.UID}}", + "startedOn": "{{.PipelineStartedOn}}", + "finishedOn": "{{.PipelineFinishedOn}}" + } + } + } +} diff --git a/test/testdata/slsa/v2alpha4/pipeline-with-object-type-hinting.json b/test/testdata/slsa/v2alpha4/pipeline-with-object-type-hinting.json new file mode 100644 index 0000000000..3dbc87d1cf --- /dev/null +++ b/test/testdata/slsa/v2alpha4/pipeline-with-object-type-hinting.json @@ -0,0 +1,139 @@ +{ + "type": "https://in-toto.io/Statement/v1", + "predicate_type": "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": { + "pipelineSpec": { + "tasks": [ + { + "name": "t1", + "taskSpec": { + "spec": null, + "metadata": {}, + "steps": [ + { + "name": "step1", + "image": "busybox:glibc", + "computeResources": {}, + "script": "echo -n \"Hello!\"\necho -n \"{\\\"uri\\\":\\\"gcr.io/foo/img1\\\", \\\"digest\\\":\\\"sha256:586789aa031fafc7d78a5393cdc772e0b55107ea54bb8bcf3f2cdac6c6da51ee\\\", \\\"isBuildArtifact\\\": \\\"true\\\" }\" \u003e $(results.output1.path)\necho -n \"{\\\"uri\\\":\\\"gcr.io/foo/img2\\\", \\\"digest\\\":\\\"sha256:586789aa031fafc7d78a5393cdc772e0b55107ea54bb8bcf3f2cdac6c6da51ee\\\"}\" \u003e $(results.output2.path)" + } + ], + "results": [ + { + "name": "output1", + "type": "object", + "properties": { + "digest": { + "type": "string" + }, + "isBuildArtifact": { + "type": "string" + }, + "uri": { + "type": "string" + } + } + }, + { + "name": "output2", + "type": "object", + "properties": { + "digest": { + "type": "string" + }, + "uri": { + "type": "string" + } + } + } + ] + } + } + ], + "results": [ + { + "name": "output1-ARTIFACT_OUTPUTS", + "description": "", + "value": "$(tasks.t1.results.output1)" + }, + { + "name": "output2-ARTIFACT_OUTPUTS", + "description": "", + "value": "$(tasks.t1.results.output2)" + } + ] + }, + "timeouts": { + "pipeline": "1h0m0s" + }, + "taskRunTemplate": { + "serviceAccountName": "default" + } + } + }, + "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": "{{.PipelineStartedOn}}", + "finishedOn": "{{.PipelineFinishedOn}}" + }, + "byproducts": [ + { + "name": "pipelineRunResults/output2-ARTIFACT_OUTPUTS", + "mediaType": "application/json", + "content": "eyJkaWdlc3QiOiJzaGEyNTY6NTg2Nzg5YWEwMzFmYWZjN2Q3OGE1MzkzY2RjNzcyZTBiNTUxMDdlYTU0YmI4YmNmM2YyY2RhYzZjNmRhNTFlZSIsInVyaSI6Imdjci5pby9mb28vaW1nMiJ9" + } + ] + } + } +} \ No newline at end of file