Skip to content

Commit

Permalink
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
TEP 0122: complete build instuctions: adding taskspec to buildConfig
Browse files Browse the repository at this point in the history
This PR introduces a new format `slsa/v2` which contains the complete
build instructions as designed in TEP0122.

Note that we are currently surfacing it for the users as `slsa/v2alpha1` since it is WIP and will undergo updates until we make it available for pipelineruns as well.
chitrangpatel committed Feb 15, 2023
1 parent 05232aa commit a808702
Showing 21 changed files with 1,209 additions and 210 deletions.
4 changes: 3 additions & 1 deletion docs/config.md
Original file line number Diff line number Diff line change
@@ -51,11 +51,12 @@ Supported keys include:

| Key | Description | Supported Values | Default |
| :--- | :--- | :--- | :--- |
| `artifacts.taskrun.format` | The format to store `TaskRun` payloads in. | `in-toto`, `slsa/v1`| `in-toto` |
| `artifacts.taskrun.format` | The format to store `TaskRun` payloads in. | `in-toto`, `slsa/v1`, `slsa/v2alpha1`| `in-toto` |
| `artifacts.taskrun.storage` | The storage backend to store `TaskRun` signatures in. Multiple backends can be specified with comma-separated list ("tekton,oci"). To disable the `TaskRun` artifact input an empty string (""). | `tekton`, `oci`, `gcs`, `docdb`, `grafeas` | `tekton` |
| `artifacts.taskrun.signer` | The signature backend to sign `TaskRun` payloads with. | `x509`, `kms` | `x509` |

> NOTE:, `slsa/v1` is an alias of `in-toto` for backwards compatibality.
> NOTE:, `slsa/v2alpha1` is WIP and is likely to undergo multiple changes.
### PipelineRun Configuration

@@ -67,6 +68,7 @@ Supported keys include:

> NOTE: For grafeas storage backend, currently we only support Container Analysis. We will make grafeas server address configurabe within a short time.
> NOTE: `slsa/v1` is an alias of `in-toto` for backwards compatibality.
> NOTE:, `slsa/v2alpha1` is currently not supported for pipelinerun level provenance.
### OCI Configuration

2 changes: 1 addition & 1 deletion examples/taskruns/task-output-image.yaml
Original file line number Diff line number Diff line change
@@ -62,4 +62,4 @@ spec:
type: image
params:
- name: url
value: gcr.io/foo/bar
value: gcr.io/foo/bar
1 change: 1 addition & 0 deletions pkg/chains/formats/all/all.go
Original file line number Diff line number Diff line change
@@ -17,4 +17,5 @@ package all
import (
_ "github.com/tektoncd/chains/pkg/chains/formats/simple"
_ "github.com/tektoncd/chains/pkg/chains/formats/slsa/v1"
_ "github.com/tektoncd/chains/pkg/chains/formats/slsa/v2"
)
1 change: 1 addition & 0 deletions pkg/chains/formats/format.go
Original file line number Diff line number Diff line change
@@ -32,6 +32,7 @@ const (
PayloadTypeSimpleSigning config.PayloadType = "simplesigning"
PayloadTypeInTotoIte6 config.PayloadType = "in-toto"
PayloadTypeSlsav1 config.PayloadType = "slsa/v1"
PayloadTypeSlsav2 config.PayloadType = "slsa/v2alpha1"
)

var (
1 change: 1 addition & 0 deletions pkg/chains/formats/slsa/README.md
Original file line number Diff line number Diff line change
@@ -12,3 +12,4 @@ Shown below is the mapping between Tekton chains proveance and SLSA predicate.
|Tekton Chains Provenance Format version | SLSA predicate | Notes |
|:------------------------------------------|---------------:|------:|
|**slsa/v1**| **slsa v0.2** | same as currently supported `in-toto` format|
|**slsa/v2alpha1**| **slsa v0.2** | contains complete build instructions as in [TEP0122](https://github.com/tektoncd/community/pull/820). This is still a WIP and currently only available for taskrun level provenance. |
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
Copyright 2022 The Tekton Authors
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.
@@ -14,16 +14,16 @@ See the License for the specific language governing permissions and
limitations under the License.
*/

package taskrun
package material

import (
"encoding/json"
"fmt"
"strings"

slsa "github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/v0.2"
"github.com/tektoncd/chains/pkg/artifacts"
"github.com/tektoncd/chains/pkg/chains/formats/slsa/attest"
"github.com/tektoncd/chains/pkg/chains/formats/slsa/v1/internal/material"
"github.com/tektoncd/chains/pkg/chains/objects"
"github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1"
"github.com/tektoncd/pipeline/pkg/apis/resource/v1alpha1"
@@ -78,8 +78,8 @@ func AddImageIDToMaterials(imageID string, mats *[]slsa.ProvenanceMaterial) erro
return nil
}

// materials constructs `predicate.materials` section by collecting all the artifacts that influence a taskrun such as source code repo and step&sidecar base images.
func materials(tro *objects.TaskRunObject, logger *zap.SugaredLogger) ([]slsa.ProvenanceMaterial, error) {
// Materials constructs `predicate.materials` section by collecting all the artifacts that influence a taskrun such as source code repo and step&sidecar base images.
func Materials(tro *objects.TaskRunObject, logger *zap.SugaredLogger) ([]slsa.ProvenanceMaterial, error) {
var mats []slsa.ProvenanceMaterial

// add step images
@@ -95,7 +95,7 @@ func materials(tro *objects.TaskRunObject, logger *zap.SugaredLogger) ([]slsa.Pr
// remove duplicate materials
// TODO : move this logic to the very end after removing
// the intermediate return statements below in a followup PR.
mats, err := material.RemoveDuplicateMaterials(mats)
mats, err := RemoveDuplicateMaterials(mats)
if err != nil {
return mats, err
}
@@ -196,3 +196,21 @@ func gitInfo(tro *objects.TaskRunObject) (commit string, url string) {
url = attest.SPDXGit(url, "")
return
}

// RemoveDuplicateMaterials removes duplicate materials from the slice of materials
func RemoveDuplicateMaterials(mats []slsa.ProvenanceMaterial) ([]slsa.ProvenanceMaterial, error) {
// make materialMap to store unique materials
materialMap := map[string]slsa.ProvenanceMaterial{}
for _, mat := range mats {
m, err := json.Marshal(mat)
if err != nil {
return nil, err
}
materialMap[string(m)] = mat
}
distinctMaterials := make([]slsa.ProvenanceMaterial, 0, len(materialMap))
for _, mat := range materialMap {
distinctMaterials = append(distinctMaterials, mat)
}
return distinctMaterials, nil
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
Copyright 2022 The Tekton Authors
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.
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/

package taskrun
package material

import (
"fmt"
@@ -33,6 +33,8 @@ import (
logtesting "knative.dev/pkg/logging/testing"
)

const digest = "sha256:05f95b26ed10668b7183c1e2da98610e91372fa9f510046d4ce5812addad86b7"

func TestMaterialsWithTaskRunResults(t *testing.T) {
// make sure this works with Git resources
taskrun := `apiVersion: tekton.dev/v1beta1
@@ -64,7 +66,7 @@ status:
},
}

got, err := materials(objects.NewTaskRunObject(taskRun), logtesting.TestLogger(t))
got, err := Materials(objects.NewTaskRunObject(taskRun), logtesting.TestLogger(t))
if err != nil {
t.Fatalf("Did not expect an error but got %v", err)
}
@@ -109,7 +111,7 @@ func TestMaterials(t *testing.T) {
Name: "img1_input" + "-" + artifacts.ArtifactsInputsResultName,
Value: *v1beta1.NewObject(map[string]string{
"uri": "gcr.io/foo/bar",
"digest": digest3,
"digest": digest,
}),
},
},
@@ -131,7 +133,7 @@ func TestMaterials(t *testing.T) {
{
URI: "gcr.io/foo/bar",
Digest: slsa.DigestSet{
"sha256": strings.TrimPrefix(digest3, "sha256:"),
"sha256": strings.TrimPrefix(digest, "sha256:"),
},
},
{
@@ -236,7 +238,7 @@ func TestMaterials(t *testing.T) {
},
}}
for _, tc := range tests {
mat, err := materials(objects.NewTaskRunObject(tc.taskRun), logtesting.TestLogger(t))
mat, err := Materials(objects.NewTaskRunObject(tc.taskRun), logtesting.TestLogger(t))
if err != nil {
t.Fatalf("Did not expect an error but got %v", err)
}
@@ -310,7 +312,7 @@ func TestAddStepImagesToMaterials(t *testing.T) {
}
if tc.wantError == nil {
if diff := cmp.Diff(tc.want, mat, test.OptSortMaterial); diff != "" {
t.Errorf("materials(): -want +got: %s", diff)
t.Errorf("Materials(): -want +got: %s", diff)
}
}
}
@@ -380,7 +382,7 @@ func TestAddSidecarImagesToMaterials(t *testing.T) {
}
if tc.wantError == nil {
if diff := cmp.Diff(tc.want, mat, test.OptSortMaterial); diff != "" {
t.Errorf("materials(): -want +got: %s", diff)
t.Errorf("Materials(): -want +got: %s", diff)
}
}
}
@@ -418,8 +420,114 @@ func TestAddImageIDToMaterials(t *testing.T) {
}
if tc.wantError == nil {
if diff := cmp.Diff(tc.want, mat, test.OptSortMaterial); diff != "" {
t.Errorf("materials(): -want +got: %s", diff)
t.Errorf("Materials(): -want +got: %s", diff)
}
}
}
}

func TestRemoveDuplicates(t *testing.T) {
tests := []struct {
name string
mats []slsa.ProvenanceMaterial
want []slsa.ProvenanceMaterial
}{{
name: "no duplicate materials",
mats: []slsa.ProvenanceMaterial{
{
URI: "gcr.io/tekton-releases/github.com/tektoncd/pipeline/cmd/git-init",
Digest: slsa.DigestSet{
"sha256": "b963f6e7a69617db57b685893256f978436277094c21d43b153994acd8a01247",
},
}, {
URI: "gcr.io/cloud-marketplace-containers/google/bazel",
Digest: slsa.DigestSet{
"sha256": "010a1ecd1a8c3610f12039a25b823e3a17bd3e8ae455a53e340dcfdd37a49964",
},
}, {
URI: "gcr.io/tekton-releases/github.com/tektoncd/pipeline/cmd/sidecar-git-init",
Digest: slsa.DigestSet{
"sha256": "a1234f6e7a69617db57b685893256f978436277094c21d43b153994acd8a09567",
},
},
},
want: []slsa.ProvenanceMaterial{
{
URI: "gcr.io/tekton-releases/github.com/tektoncd/pipeline/cmd/git-init",
Digest: slsa.DigestSet{
"sha256": "b963f6e7a69617db57b685893256f978436277094c21d43b153994acd8a01247",
},
}, {
URI: "gcr.io/cloud-marketplace-containers/google/bazel",
Digest: slsa.DigestSet{
"sha256": "010a1ecd1a8c3610f12039a25b823e3a17bd3e8ae455a53e340dcfdd37a49964",
},
}, {
URI: "gcr.io/tekton-releases/github.com/tektoncd/pipeline/cmd/sidecar-git-init",
Digest: slsa.DigestSet{
"sha256": "a1234f6e7a69617db57b685893256f978436277094c21d43b153994acd8a09567",
},
},
},
}, {
name: "same uri and digest",
mats: []slsa.ProvenanceMaterial{
{
URI: "gcr.io/tekton-releases/github.com/tektoncd/pipeline/cmd/git-init",
Digest: slsa.DigestSet{
"sha256": "b963f6e7a69617db57b685893256f978436277094c21d43b153994acd8a01247",
},
}, {
URI: "gcr.io/tekton-releases/github.com/tektoncd/pipeline/cmd/git-init",
Digest: slsa.DigestSet{
"sha256": "b963f6e7a69617db57b685893256f978436277094c21d43b153994acd8a01247",
},
},
},
want: []slsa.ProvenanceMaterial{
{
URI: "gcr.io/tekton-releases/github.com/tektoncd/pipeline/cmd/git-init",
Digest: slsa.DigestSet{
"sha256": "b963f6e7a69617db57b685893256f978436277094c21d43b153994acd8a01247",
},
},
},
}, {
name: "same uri but different digest",
mats: []slsa.ProvenanceMaterial{
{
URI: "gcr.io/tekton-releases/github.com/tektoncd/pipeline/cmd/git-init",
Digest: slsa.DigestSet{
"sha256": "b963f6e7a69617db57b685893256f978436277094c21d43b153994acd8a01247",
},
}, {
URI: "gcr.io/tekton-releases/github.com/tektoncd/pipeline/cmd/git-init",
Digest: slsa.DigestSet{
"sha256": "b963f6e7a69617db57b685893256f978436277094c21d43b153994acd8a01248",
},
},
},
want: []slsa.ProvenanceMaterial{
{
URI: "gcr.io/tekton-releases/github.com/tektoncd/pipeline/cmd/git-init",
Digest: slsa.DigestSet{
"sha256": "b963f6e7a69617db57b685893256f978436277094c21d43b153994acd8a01247",
},
}, {
URI: "gcr.io/tekton-releases/github.com/tektoncd/pipeline/cmd/git-init",
Digest: slsa.DigestSet{
"sha256": "b963f6e7a69617db57b685893256f978436277094c21d43b153994acd8a01248",
},
},
},
}}
for _, tc := range tests {
mat, err := RemoveDuplicateMaterials(tc.mats)
if err != nil {
t.Fatalf("Did not expect an error but got %v", err)
}
if diff := cmp.Diff(tc.want, mat, test.OptSortMaterial); diff != "" {
t.Errorf("materials(): -want +got: %s", diff)
}
}
}
41 changes: 0 additions & 41 deletions pkg/chains/formats/slsa/v1/internal/material/material.go

This file was deleted.

131 changes: 0 additions & 131 deletions pkg/chains/formats/slsa/v1/internal/material/material_test.go

This file was deleted.

7 changes: 3 additions & 4 deletions pkg/chains/formats/slsa/v1/pipelinerun/pipelinerun.go
Original file line number Diff line number Diff line change
@@ -21,8 +21,7 @@ import (
"github.com/tektoncd/chains/pkg/artifacts"
"github.com/tektoncd/chains/pkg/chains/formats/slsa/attest"
"github.com/tektoncd/chains/pkg/chains/formats/slsa/extract"
"github.com/tektoncd/chains/pkg/chains/formats/slsa/v1/internal/material"
"github.com/tektoncd/chains/pkg/chains/formats/slsa/v1/taskrun"
"github.com/tektoncd/chains/pkg/chains/formats/slsa/internal/material"
"github.com/tektoncd/chains/pkg/chains/objects"
"github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1"
"go.uber.org/zap"
@@ -209,12 +208,12 @@ func materials(pro *objects.PipelineRunObject, logger *zap.SugaredLogger) ([]sls
}

// add step images
if err := taskrun.AddStepImagesToMaterials(tr.Status.Steps, &mats); err != nil {
if err := material.AddStepImagesToMaterials(tr.Status.Steps, &mats); err != nil {
return mats, nil
}

// add sidecar images
if err := taskrun.AddSidecarImagesToMaterials(tr.Status.Sidecars, &mats); err != nil {
if err := material.AddSidecarImagesToMaterials(tr.Status.Sidecars, &mats); err != nil {
return mats, nil
}

4 changes: 2 additions & 2 deletions pkg/chains/formats/slsa/v1/taskrun/provenance_test.go
Original file line number Diff line number Diff line change
@@ -66,7 +66,7 @@ func TestMetadata(t *testing.T) {
BuildStartedOn: &start,
BuildFinishedOn: &end,
}
got := metadata(objects.NewTaskRunObject(tr))
got := Metadata(objects.NewTaskRunObject(tr))
if !reflect.DeepEqual(expected, got) {
t.Fatalf("expected %v got %v", expected, got)
}
@@ -95,7 +95,7 @@ func TestMetadataInTimeZone(t *testing.T) {
BuildStartedOn: &start,
BuildFinishedOn: &end,
}
got := metadata(objects.NewTaskRunObject(tr))
got := Metadata(objects.NewTaskRunObject(tr))
if !reflect.DeepEqual(expected, got) {
t.Fatalf("expected %v got %v", expected, got)
}
9 changes: 6 additions & 3 deletions pkg/chains/formats/slsa/v1/taskrun/taskrun.go
Original file line number Diff line number Diff line change
@@ -18,6 +18,7 @@ import (
slsa "github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/v0.2"
"github.com/tektoncd/chains/pkg/chains/formats/slsa/attest"
"github.com/tektoncd/chains/pkg/chains/formats/slsa/extract"
"github.com/tektoncd/chains/pkg/chains/formats/slsa/internal/material"
"github.com/tektoncd/chains/pkg/chains/objects"
"github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1"
"go.uber.org/zap"
@@ -26,7 +27,7 @@ import (
func GenerateAttestation(builderID string, tro *objects.TaskRunObject, logger *zap.SugaredLogger) (interface{}, error) {
subjects := extract.SubjectDigests(tro, logger)

mat, err := materials(tro, logger)
mat, err := material.Materials(tro, logger)
if err != nil {
return nil, err
}
@@ -43,7 +44,7 @@ func GenerateAttestation(builderID string, tro *objects.TaskRunObject, logger *z
BuildType: tro.GetGVK(),
Invocation: invocation(tro),
BuildConfig: buildConfig(tro),
Metadata: metadata(tro),
Metadata: Metadata(tro),
Materials: mat,
},
}
@@ -65,7 +66,9 @@ func invocation(tro *objects.TaskRunObject) slsa.ProvenanceInvocation {
return attest.Invocation(source, tro.Spec.Params, paramSpecs)
}

func metadata(tro *objects.TaskRunObject) *slsa.ProvenanceMetadata {
// Metadata adds taskrun's start time, completion time and reproducibility labels
// to the metadata section of the generated provenance.
func Metadata(tro *objects.TaskRunObject) *slsa.ProvenanceMetadata {
m := &slsa.ProvenanceMetadata{}
if tro.Status.StartTime != nil {
utc := tro.Status.StartTime.Time.UTC()
62 changes: 62 additions & 0 deletions pkg/chains/formats/slsa/v2/slsav2.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/*
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 v2

import (
"context"
"fmt"

"github.com/tektoncd/chains/pkg/chains/formats"
"github.com/tektoncd/chains/pkg/chains/formats/slsa/v2/taskrun"
"github.com/tektoncd/chains/pkg/chains/objects"
"github.com/tektoncd/chains/pkg/config"
)

const (
PayloadTypeSlsav2 = formats.PayloadTypeSlsav2
)

func init() {
formats.RegisterPayloader(PayloadTypeSlsav2, NewFormatter)
}

type SlsaV2 struct {
builderID string
}

func NewFormatter(cfg config.Config) (formats.Payloader, error) {
return &SlsaV2{
builderID: cfg.Builder.ID,
}, nil
}

func (s *SlsaV2) Wrap() bool {
return true
}

func (s *SlsaV2) CreatePayload(ctx context.Context, obj interface{}) (interface{}, error) {
switch v := obj.(type) {
case *objects.TaskRunObject:
return taskrun.GenerateAttestation(s.builderID, s.Type(), v, ctx)
default:
return nil, fmt.Errorf("intoto does not support type: %s", v)
}
}

func (s *SlsaV2) Type() config.PayloadType {
return formats.PayloadTypeSlsav2
}
410 changes: 410 additions & 0 deletions pkg/chains/formats/slsa/v2/slsav2_test.go

Large diffs are not rendered by default.

97 changes: 97 additions & 0 deletions pkg/chains/formats/slsa/v2/taskrun/taskrun.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
/*
Copyright 2022 The Tekton Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package taskrun

import (
"context"
"fmt"
"reflect"

intoto "github.com/in-toto/in-toto-golang/in_toto"
slsa "github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/v0.2"
"github.com/tektoncd/chains/pkg/chains/formats/slsa/extract"
"github.com/tektoncd/chains/pkg/chains/formats/slsa/internal/material"
slsav1 "github.com/tektoncd/chains/pkg/chains/formats/slsa/v1/taskrun"
"github.com/tektoncd/chains/pkg/chains/objects"
"github.com/tektoncd/chains/pkg/config"
"github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1"
"k8s.io/apimachinery/pkg/util/sets"
"knative.dev/pkg/logging"
)

// BuildConfig is the custom Chains format to fill out the
// "buildConfig" section of the slsa-provenance predicate
type BuildConfig struct {
TaskSpec *v1beta1.TaskSpec `json:"taskSpec"`
TaskRunResults []v1beta1.TaskRunResult `json:"taskRunResults"`
}

func GenerateAttestation(builderID string, payloadType config.PayloadType, tro *objects.TaskRunObject, ctx context.Context) (interface{}, error) {
logger := logging.FromContext(ctx)
subjects := extract.SubjectDigests(tro, logger)

mat, err := material.Materials(tro, logger)
if err != nil {
return nil, err
}
att := intoto.ProvenanceStatement{
StatementHeader: intoto.StatementHeader{
Type: intoto.StatementInTotoV01,
PredicateType: slsa.PredicateSLSAProvenance,
Subject: subjects,
},
Predicate: slsa.ProvenancePredicate{
Builder: slsa.ProvenanceBuilder{
ID: builderID,
},
BuildType: fmt.Sprintf("https://chains.tekton.dev/format/%v/type/%s", payloadType, tro.GetGVK()),
Invocation: invocation(tro),
BuildConfig: BuildConfig{TaskSpec: tro.Status.TaskSpec, TaskRunResults: tro.Status.TaskRunResults},
Metadata: slsav1.Metadata(tro),
Materials: mat,
},
}
return att, nil
}

// invocation describes the event that kicked off the build
// we currently don't set ConfigSource because we don't know
// which material the Task definition came from
func invocation(tro *objects.TaskRunObject) slsa.ProvenanceInvocation {
i := slsa.ProvenanceInvocation{}
if p := tro.Status.Provenance; p != nil {
i.ConfigSource = slsa.ConfigSource{
URI: p.ConfigSource.URI,
Digest: p.ConfigSource.Digest,
EntryPoint: p.ConfigSource.EntryPoint,
}
}
i.Parameters = invocationParams(tro)
return i
}

// invocationParams adds all fields from the task run object except
// TaskRef or TaskSpec since they are in the ConfigSource or buildConfig.
func invocationParams(tro *objects.TaskRunObject) map[string]any {
var iParams map[string]any = make(map[string]any)
skipFields := sets.NewString("TaskRef", "TaskSpec")
v := reflect.ValueOf(tro.Spec)
for i := 0; i < v.NumField(); i++ {
fieldName := v.Type().Field(i).Name
if !skipFields.Has(v.Type().Field(i).Name) {
iParams[fieldName] = v.Field(i).Interface()
}
}
return iParams
}
344 changes: 344 additions & 0 deletions pkg/chains/formats/slsa/v2/taskrun/taskrun_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,344 @@
/*
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 taskrun

import (
"reflect"
"strings"
"testing"
"time"

slsa "github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/v0.2"

"github.com/ghodss/yaml"
"github.com/google/go-cmp/cmp"
"github.com/in-toto/in-toto-golang/in_toto"
"github.com/tektoncd/chains/pkg/artifacts"
"github.com/tektoncd/chains/pkg/chains/formats/slsa/extract"
slsav1 "github.com/tektoncd/chains/pkg/chains/formats/slsa/v1/taskrun"
"github.com/tektoncd/chains/pkg/chains/objects"
"github.com/tektoncd/pipeline/pkg/apis/pipeline/pod"
"github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1"
"github.com/tektoncd/pipeline/pkg/apis/resource/v1alpha1"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
logtesting "knative.dev/pkg/logging/testing"
)

const (
digest1 = "sha256:05f95b26ed10668b7183c1e2da98610e91372fa9f510046d4ce5812addad86b5"
digest2 = "sha256:05f95b26ed10668b7183c1e2da98610e91372fa9f510046d4ce5812addad86b6"
digest3 = "sha256:05f95b26ed10668b7183c1e2da98610e91372fa9f510046d4ce5812addad86b7"
digest4 = "sha256:05f95b26ed10668b7183c1e2da98610e91372fa9f510046d4ce5812addad86b8"
digest5 = "sha256:05f95b26ed10668b7183c1e2da98610e91372fa9f510046d4ce5812addad86b9"
)

func TestMetadata(t *testing.T) {
tr := &v1beta1.TaskRun{
ObjectMeta: v1.ObjectMeta{
Name: "my-taskrun",
Namespace: "my-namespace",
Annotations: map[string]string{
"chains.tekton.dev/reproducible": "true",
},
},
Status: v1beta1.TaskRunStatus{
TaskRunStatusFields: v1beta1.TaskRunStatusFields{
StartTime: &v1.Time{Time: time.Date(1995, time.December, 24, 6, 12, 12, 12, time.UTC)},
CompletionTime: &v1.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)
expected := &slsa.ProvenanceMetadata{
BuildStartedOn: &start,
BuildFinishedOn: &end,
}
got := slsav1.Metadata(objects.NewTaskRunObject(tr))
if !reflect.DeepEqual(expected, got) {
t.Fatalf("expected %v got %v", expected, got)
}
}

func TestMetadataInTimeZone(t *testing.T) {
tz := time.FixedZone("Test Time", int((12 * time.Hour).Seconds()))
tr := &v1beta1.TaskRun{
ObjectMeta: v1.ObjectMeta{
Name: "my-taskrun",
Namespace: "my-namespace",
Annotations: map[string]string{
"chains.tekton.dev/reproducible": "true",
},
},
Status: v1beta1.TaskRunStatus{
TaskRunStatusFields: v1beta1.TaskRunStatusFields{
StartTime: &v1.Time{Time: time.Date(1995, time.December, 24, 6, 12, 12, 12, tz)},
CompletionTime: &v1.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()
expected := &slsa.ProvenanceMetadata{
BuildStartedOn: &start,
BuildFinishedOn: &end,
}
got := slsav1.Metadata(objects.NewTaskRunObject(tr))
if !reflect.DeepEqual(expected, got) {
t.Fatalf("expected %v got %v", expected, got)
}
}

func TestInvocation(t *testing.T) {
taskrun := `apiVersion: tekton.dev/v1beta1
kind: TaskRun
metadata:
uid: my-uid
spec:
params:
- name: my-param
value: string-param
- name: my-array-param
value:
- "my"
- "array"
- name: my-empty-string-param
value: ""
- name: my-empty-array-param
value: []
status:
taskSpec:
params:
- name: my-param
default: ignored
- name: my-array-param
type: array
default:
- "also"
- "ignored"
- name: my-default-param
default: string-default-param
- name: my-default-array-param
type: array
default:
- "array"
- "default"
- "param"
- name: my-empty-string-param
default: "ignored"
- name: my-empty-array-param
type: array
default:
- "also"
- "ignored"
- name: my-default-empty-string-param
default: ""
- name: my-default-empty-array-param
type: array
default: []
`

var taskRun *v1beta1.TaskRun
if err := yaml.Unmarshal([]byte(taskrun), &taskRun); err != nil {
t.Fatal(err)
}

expected := slsa.ProvenanceInvocation{
Parameters: map[string]any{
"Params": []v1beta1.Param{
{
Name: "my-param",
Value: v1beta1.ParamValue{Type: "string", StringVal: "string-param"},
},
{
Name: "my-array-param",
Value: v1beta1.ParamValue{Type: "array", ArrayVal: []string{"my", "array"}},
},
{Name: "my-empty-string-param", Value: v1beta1.ParamValue{Type: "string"}},
{
Name: "my-empty-array-param",
Value: v1beta1.ParamValue{Type: "array", ArrayVal: []string{}},
},
},
"ComputeResources": (*corev1.ResourceRequirements)(nil),
"Debug": (*v1beta1.TaskRunDebug)(nil),
"PodTemplate": (*pod.Template)(nil),
"Resources": (*v1beta1.TaskRunResources)(nil),
"Retries": 0,
"ServiceAccountName": "",
"SidecarOverrides": []v1beta1.TaskRunSidecarOverride(nil),
"Status": v1beta1.TaskRunSpecStatus(""),
"StatusMessage": v1beta1.TaskRunSpecStatusMessage(""),
"StepOverrides": []v1beta1.TaskRunStepOverride(nil),
"Timeout": (*metav1.Duration)(nil),
"Workspaces": []v1beta1.WorkspaceBinding(nil),
},
}
got := invocation(objects.NewTaskRunObject(taskRun))
if !reflect.DeepEqual(expected, got) {
if d := cmp.Diff(expected, got); d != "" {
t.Log(d)
}
t.Fatalf("expected \n%v\n got \n%v\n", expected, got)
}
}

func TestGetSubjectDigests(t *testing.T) {
tr := &v1beta1.TaskRun{
Spec: v1beta1.TaskRunSpec{
Resources: &v1beta1.TaskRunResources{
Outputs: []v1beta1.TaskResourceBinding{
{
PipelineResourceBinding: v1beta1.PipelineResourceBinding{
Name: "nil-check",
},
}, {
PipelineResourceBinding: v1beta1.PipelineResourceBinding{
Name: "built-image",
ResourceSpec: &v1alpha1.PipelineResourceSpec{
Type: v1alpha1.PipelineResourceTypeImage,
},
},
},
},
},
},
Status: v1beta1.TaskRunStatus{
TaskRunStatusFields: v1beta1.TaskRunStatusFields{
TaskRunResults: []v1beta1.TaskRunResult{
{
Name: "IMAGE_URL",
Value: *v1beta1.NewArrayOrString("registry/myimage"),
},
{
Name: "IMAGE_DIGEST",
Value: *v1beta1.NewArrayOrString(digest1),
},
{
Name: "mvn1_ARTIFACT_URI",
Value: *v1beta1.NewArrayOrString("maven-test-0.1.1.jar"),
},
{
Name: "mvn1_ARTIFACT_DIGEST",
Value: *v1beta1.NewArrayOrString(digest3),
},
{
Name: "mvn1_pom_ARTIFACT_URI",
Value: *v1beta1.NewArrayOrString("maven-test-0.1.1.pom"),
},
{
Name: "mvn1_pom_ARTIFACT_DIGEST",
Value: *v1beta1.NewArrayOrString(digest4),
},
{
Name: "mvn1_src_ARTIFACT_URI",
Value: *v1beta1.NewArrayOrString("maven-test-0.1.1-sources.jar"),
},
{
Name: "mvn1_src_ARTIFACT_DIGEST",
Value: *v1beta1.NewArrayOrString(digest5),
},
{
Name: "invalid_ARTIFACT_DIGEST",
Value: *v1beta1.NewArrayOrString(digest5),
},
{
Name: "mvn1_pkg" + "-" + artifacts.ArtifactsOutputsResultName,
Value: *v1beta1.NewObject(map[string]string{
"uri": "projects/test-project-1/locations/us-west4/repositories/test-repo/mavenArtifacts/com.google.guava:guava:31.0-jre",
"digest": digest1,
}),
},
{
Name: "mvn1_pom_sha512" + "-" + artifacts.ArtifactsOutputsResultName,
Value: *v1beta1.NewObject(map[string]string{
"uri": "com.google.guava:guava:1.0-jre.pom",
"digest": digest2,
}),
},
{
Name: "img1_input" + "-" + artifacts.ArtifactsInputsResultName,
Value: *v1beta1.NewObject(map[string]string{
"uri": "gcr.io/foo/bar",
"digest": digest3,
}),
},
},
ResourcesResult: []v1beta1.PipelineResourceResult{
{
ResourceName: "built-image",
Key: "url",
Value: "registry/resource-image",
}, {
ResourceName: "built-image",
Key: "digest",
Value: digest2,
},
},
},
},
}

expected := []in_toto.Subject{
{
Name: "com.google.guava:guava:1.0-jre.pom",
Digest: slsa.DigestSet{
"sha256": strings.TrimPrefix(digest2, "sha256:"),
},
}, {
Name: "index.docker.io/registry/myimage",
Digest: slsa.DigestSet{
"sha256": strings.TrimPrefix(digest1, "sha256:"),
},
}, {
Name: "maven-test-0.1.1-sources.jar",
Digest: slsa.DigestSet{
"sha256": strings.TrimPrefix(digest5, "sha256:"),
},
}, {
Name: "maven-test-0.1.1.jar",
Digest: slsa.DigestSet{
"sha256": strings.TrimPrefix(digest3, "sha256:"),
},
}, {
Name: "maven-test-0.1.1.pom",
Digest: slsa.DigestSet{
"sha256": strings.TrimPrefix(digest4, "sha256:"),
},
}, {
Name: "projects/test-project-1/locations/us-west4/repositories/test-repo/mavenArtifacts/com.google.guava:guava:31.0-jre",
Digest: slsa.DigestSet{
"sha256": strings.TrimPrefix(digest1, "sha256:"),
},
}, {
Name: "registry/resource-image",
Digest: slsa.DigestSet{
"sha256": strings.TrimPrefix(digest2, "sha256:"),
},
},
}
tro := objects.NewTaskRunObject(tr)
got := extract.SubjectDigests(tro, logtesting.TestLogger(t))
if !reflect.DeepEqual(expected, got) {
if d := cmp.Diff(expected, got); d != "" {
t.Log(d)
}
t.Fatalf("expected \n%v\n got \n%v\n", expected, got)
}
}
2 changes: 1 addition & 1 deletion pkg/config/config.go
Original file line number Diff line number Diff line change
@@ -250,7 +250,7 @@ func NewConfigFromMap(data map[string]string) (*Config, error) {
if err := cm.Parse(data,
// Artifact-specific configs
// TaskRuns
asString(taskrunFormatKey, &cfg.Artifacts.TaskRuns.Format, "in-toto", "slsa/v1"),
asString(taskrunFormatKey, &cfg.Artifacts.TaskRuns.Format, "in-toto", "slsa/v1", "slsa/v2alpha1"),
asStringSet(taskrunStorageKey, &cfg.Artifacts.TaskRuns.StorageBackend, sets.NewString("tekton", "oci", "gcs", "docdb", "grafeas", "kafka")),
asString(taskrunSignerKey, &cfg.Artifacts.TaskRuns.Signer, "x509", "kms"),

36 changes: 25 additions & 11 deletions test/examples_test.go
Original file line number Diff line number Diff line change
@@ -58,21 +58,34 @@ type TestExample struct {
getExampleObjects func(t *testing.T, ns string) map[string]objects.TektonObject
payloadKey string
signatureKey string
outputLocation string
}

// TestExamples copies the format in the tektoncd/pipelines repo
// https://github.com/tektoncd/pipeline/blob/main/test/examples_test.go
func TestExamples(t *testing.T) {
tests := []TestExample{
{
name: "taskrun-examples",
name: "taskrun-examples-slsa-v1",
cm: map[string]string{
"artifacts.taskrun.format": "slsa/v1",
"artifacts.oci.storage": "tekton",
},
getExampleObjects: getTaskRunExamples,
payloadKey: "chains.tekton.dev/payload-taskrun-%s",
signatureKey: "chains.tekton.dev/signature-taskrun-%s",
outputLocation: "slsa/v1",
},
{
name: "taskrun-examples-slsa-v2",
cm: map[string]string{
"artifacts.taskrun.format": "slsa/v2alpha1",
"artifacts.oci.storage": "tekton",
},
getExampleObjects: getTaskRunExamples,
payloadKey: "chains.tekton.dev/payload-taskrun-%s",
signatureKey: "chains.tekton.dev/signature-taskrun-%s",
outputLocation: "slsa/v2",
},
{
name: "pipelinerun-examples",
@@ -83,6 +96,7 @@ func TestExamples(t *testing.T) {
getExampleObjects: getPipelineRunExamples,
payloadKey: "chains.tekton.dev/payload-pipelinerun-%s",
signatureKey: "chains.tekton.dev/signature-pipelinerun-%s",
outputLocation: "slsa/v1",
},
}

@@ -127,7 +141,7 @@ func runInTotoFormatterTests(ctx context.Context, t *testing.T, ns string, c *cl
if err := json.Unmarshal(payload, &gotProvenance); err != nil {
t.Fatal(err)
}
expected := expectedProvenance(t, path, completed)
expected := expectedProvenance(t, path, completed, test.outputLocation)

if diff := cmp.Diff(expected, gotProvenance, OptSortMaterial); diff != "" {
t.Errorf("provenance dont match: -want +got: %s", diff)
@@ -177,12 +191,12 @@ func (v *verifier) Public() crypto.PublicKey {
return v.pub
}

func expectedProvenance(t *testing.T, example string, obj objects.TektonObject) intoto.ProvenanceStatement {
func expectedProvenance(t *testing.T, example string, obj objects.TektonObject, outputLocation string) intoto.ProvenanceStatement {
switch obj.(type) {
case *objects.TaskRunObject:
return expectedTaskRunProvenance(t, example, obj)
return expectedTaskRunProvenance(t, example, obj, outputLocation)
case *objects.PipelineRunObject:
return expectedPipelineRunProvenance(t, example, obj)
return expectedPipelineRunProvenance(t, example, obj, outputLocation)
default:
t.Error("Unexpected type trying to get provenance")
}
@@ -205,7 +219,7 @@ type Format struct {
URIDigest []URIDigestPair
}

func expectedTaskRunProvenance(t *testing.T, example string, obj objects.TektonObject) intoto.ProvenanceStatement {
func expectedTaskRunProvenance(t *testing.T, example string, obj objects.TektonObject, outputLocation string) intoto.ProvenanceStatement {
tr := obj.GetObject().(*v1beta1.TaskRun)

name := tr.Name
@@ -239,10 +253,10 @@ func expectedTaskRunProvenance(t *testing.T, example string, obj objects.TektonO
URIDigest: uridigest,
}

return readExpectedAttestation(t, example, f)
return readExpectedAttestation(t, example, f, outputLocation)
}

func expectedPipelineRunProvenance(t *testing.T, example string, obj objects.TektonObject) intoto.ProvenanceStatement {
func expectedPipelineRunProvenance(t *testing.T, example string, obj objects.TektonObject, outputLocation string) intoto.ProvenanceStatement {
pr := obj.GetObject().(*v1beta1.PipelineRun)

buildStartTimes := []string{}
@@ -284,11 +298,11 @@ func expectedPipelineRunProvenance(t *testing.T, example string, obj objects.Tek
URIDigest: uridigest,
}

return readExpectedAttestation(t, example, f)
return readExpectedAttestation(t, example, f, outputLocation)
}

func readExpectedAttestation(t *testing.T, example string, f Format) intoto.ProvenanceStatement {
path := filepath.Join("testdata/intoto", strings.Replace(filepath.Base(example), ".yaml", ".json", 1))
func readExpectedAttestation(t *testing.T, example string, f Format, outputLocation string) intoto.ProvenanceStatement {
path := filepath.Join("testdata", outputLocation, strings.Replace(filepath.Base(example), ".yaml", ".json", 1))
t.Logf("Reading expected provenance from %s", path)

contents, err := ioutil.ReadFile(path)
File renamed without changes.
111 changes: 111 additions & 0 deletions test/testdata/slsa/v2/task-output-image.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
{
"_type": "https://in-toto.io/Statement/v0.1",
"predicateType": "https://slsa.dev/provenance/v0.2",
"subject": [
{
"name": "gcr.io/foo/bar",
"digest": {
"sha256": "05f95b26ed10668b7183c1e2da98610e91372fa9f510046d4ce5812addad86b5"
}
}
],
"predicate": {
"builder": {
"id": "https://tekton.dev/chains/v2"
},
"buildType": "https://chains.tekton.dev/format/slsa/v2alpha1/type/tekton.dev/v1beta1/TaskRun",
"invocation": {
"configSource": {},
"parameters": {
"ComputeResources": null,
"Debug": null,
"Params": null,
"PodTemplate": null,
"Resources": {
"inputs": [{
"name": "sourcerepo",
"resourceSpec": {
"params": [
{"name": "revision", "value": "v0.32.0"},
{
"name": "url",
"value": "https://github.com/GoogleContainerTools/skaffold"
}
],
"type": "git"
}
}],
"outputs": [{
"name": "builtImage",
"resourceSpec": {
"params": [{"name": "url", "value": "gcr.io/foo/bar"}],
"type": "image"
}
}]
},
"Retries": 0,
"ServiceAccountName": "default",
"SidecarOverrides": null,
"Status": "",
"StatusMessage": "",
"StepOverrides": null,
"Timeout": "1h0m0s",
"Workspaces": null
}
},
"buildConfig": {
"taskSpec": {
"resources": {
"inputs": [{
"name": "sourcerepo",
"type": "git"
}],
"outputs": [{
"name": "builtImage",
"type": "image",
"targetPath": "/workspace/sourcerepo"
}]
},
"steps": [
{
"name": "build-and-push",
"image": "busybox",
"resources": {},
"script": "set -e\ncat \u003c\u003cEOF > $(inputs.resources.sourcerepo.path)/index.json\n{\n\"schemaVersion\": 2,\n\"manifests\": [\n {\n \"mediaType\": \"application/vnd.oci.image.index.v1+json\",\n \"size\": 314,\n \"digest\": \"sha256:05f95b26ed10668b7183c1e2da98610e91372fa9f510046d4ce5812addad86b5\"\n }\n]\n}\n"
},{
"name": "echo",
"image": "busybox",
"resources": {},
"script": "cat $(inputs.resources.sourcerepo.path)/index.json"
}
]
}
},
"metadata": {
"buildStartedOn": "{{index .BuildStartTimes 0}}",
"buildFinishedOn": "{{index .BuildFinishedTimes 0}}",
"completeness": {
"parameters": false,
"environment": false,
"materials": false
},
"reproducible": false
},
"materials": [
{{range .URIDigest}}
{
"uri": "{{.URI}}",
"digest": {
"sha256": "{{.Digest}}"
}
},
{{end}}
{
"uri": "git+https://github.com/GoogleContainerTools/skaffold@v0.32.0",
"digest": {
"sha1": "6ed7aad5e8a36052ee5f6079fc91368e362121f7"
}
}
]
}
}

0 comments on commit a808702

Please sign in to comment.