Skip to content

Commit

Permalink
Smarter Chains: check taskrun level results for Subjects
Browse files Browse the repository at this point in the history
Related feature 1 in #850

Prior, Chains only looks for pipeline results to understand what
artifacts were generated in a pipeline. That means pipeline authors need
to name pipeline results in the type hinting way and propagate its value
with individual TaskRun results.

Now, Chains is able to dive into individual TaskRun results to understand
what artifacts were generated throughout a pipeline. This way, pipeline
authors no longer need to worry about the rules when writting a pipeline
as long as they pull in right tasks that produce type hinting results.

Signed-off-by: Chuang Wang <[email protected]>
  • Loading branch information
chuangw6 committed Aug 1, 2023
1 parent 25e7a6c commit 6df2fb5
Show file tree
Hide file tree
Showing 24 changed files with 230 additions and 84 deletions.
6 changes: 4 additions & 2 deletions docs/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,9 +64,11 @@ Supported keys include:
| `artifacts.pipelinerun.format` | The format to store `PipelineRun` payloads in. | `in-toto`, `slsa/v1`| `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.observe-mode` | The way that Chains observes inputs & outputs of a PipelineRun. The default option `pr` configures Chains to only inspect Pipeline level params/results, whereas the option `tr` configures Chains to dive into child TaskRuns. | `pr`, `tr` | `pr` |

> 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 compatibility.
> NOTE:
> - 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.
### OCI Configuration

Expand Down
99 changes: 92 additions & 7 deletions pkg/chains/formats/slsa/extract/extract.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,19 +27,106 @@ import (
"github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/common"
"github.com/tektoncd/chains/internal/backport"
"github.com/tektoncd/chains/pkg/artifacts"
"github.com/tektoncd/chains/pkg/chains/formats/slsa/internal/slsaconfig"
"github.com/tektoncd/chains/pkg/chains/objects"
"github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1"
"knative.dev/pkg/logging"
)

// SubjectDigests returns software artifacts produced from the TaskRun/PipelineRun object
// in the form of standard subject field of intoto statement.
// The type hinting fields expected in results help identify the generated software artifacts.
// The type hinting fields expected in TaskRun results help identify the generated software artifacts in a TaskRun/PipelineRun.
// Valid type hinting fields must:
// - have suffix `IMAGE_URL` & `IMAGE_DIGEST` or `ARTIFACT_URI` & `ARTIFACT_DIGEST` pair.
// - the `*_DIGEST` field must be in the format of "<algorithm>:<actual-sha>" where the algorithm must be "sha256" and actual sha must be valid per https://github.com/opencontainers/image-spec/blob/main/descriptor.md#sha-256.
// - the `*_URL` or `*_URI` fields cannot be empty.
func SubjectDigests(ctx context.Context, obj objects.TektonObject) []intoto.Subject {
func SubjectDigests(ctx context.Context, obj objects.TektonObject, slsaconfig *slsaconfig.SlsaConfig) []intoto.Subject {
var subjects []intoto.Subject

switch obj.GetObject().(type) {
case *v1beta1.PipelineRun:

Check failure on line 47 in pkg/chains/formats/slsa/extract/extract.go

View workflow job for this annotation

GitHub Actions / lint

SA1019: v1beta1.PipelineRun is deprecated: Please use v1.PipelineRun instead. (staticcheck)
subjects = subjectsFromPipelineRun(ctx, obj, slsaconfig)
case *v1beta1.TaskRun:

Check failure on line 49 in pkg/chains/formats/slsa/extract/extract.go

View workflow job for this annotation

GitHub Actions / lint

SA1019: v1beta1.TaskRun is deprecated: Please use v1.TaskRun instead. (staticcheck)
subjects = subjectsFromTektonObject(ctx, obj)
}

sort.Slice(subjects, func(i, j int) bool {
return subjects[i].Name <= subjects[j].Name
})

return subjects
}

func subjectsFromPipelineRun(ctx context.Context, obj objects.TektonObject, slsaconfig *slsaconfig.SlsaConfig) []intoto.Subject {
logger := logging.FromContext(ctx)
// If the configured input/output observation mode is pipeline level, then
// call the generic function to parse the subject.
if slsaconfig.PrObserveMode == "pr" || slsaconfig.PrObserveMode == "" {
return subjectsFromTektonObject(ctx, obj)
}

// If the configured input/output observation mode is task level, then dive into
// individual taskruns and collect subjects.

// Create a map to represent the result. Key is the Subject.Name; Value is a slice of Subject.DigestSet
result := []intoto.Subject{}

pro := obj.(*objects.PipelineRunObject)

pSpec := pro.Status.PipelineSpec
if pSpec != nil {
pipelineTasks := append(pSpec.Tasks, pSpec.Finally...)
for _, t := range pipelineTasks {
tr := pro.GetTaskRunFromTask(t.Name)
// Ignore Tasks that did not execute during the PipelineRun.
if tr == nil || tr.Status.CompletionTime == nil {
logger.Infof("taskrun status not found for task %s", t.Name)
continue
}

trSubjects := subjectsFromTektonObject(ctx, objects.NewTaskRunObject(tr))
for _, s := range trSubjects {
result = addSubject(result, s)
}
}
}

return result
}

// addSubject adds a new subject item to the original slice.
func addSubject(original []intoto.Subject, item intoto.Subject) []intoto.Subject {

for i, s := range original {
// if there is an equivalent entry in the original slice, do nothing or replace
// the original entry with the item if the item has more rich digest set.
if subjectEqual(s, item) {
if len(s.Digest) < len(item.Digest) {
original[i] = item
}
return original
}
}

original = append(original, item)
return original
}

// two subjects are equal if and only if they have same name and have at least
// one common algorithm and hex value.
func subjectEqual(x, y intoto.Subject) bool {
if x.Name != y.Name {
return false
}
for algo, hex := range x.Digest {
if v, ok := y.Digest[algo]; ok && v == hex {
return true
}
}
return false
}

func subjectsFromTektonObject(ctx context.Context, obj objects.TektonObject) []intoto.Subject {
logger := logging.FromContext(ctx)
var subjects []intoto.Subject

Expand Down Expand Up @@ -121,19 +208,17 @@ func SubjectDigests(ctx context.Context, obj objects.TektonObject) []intoto.Subj
})
}
}
sort.Slice(subjects, func(i, j int) bool {
return subjects[i].Name <= subjects[j].Name
})

return subjects
}

// RetrieveAllArtifactURIs returns all the URIs of the software artifacts produced from the run object.
// - It first extracts intoto subjects from run object results and converts the subjects
// to a slice of string URIs in the format of "NAME" + "@" + "ALGORITHM" + ":" + "DIGEST".
// - If no subjects could be extracted from results, then an empty slice is returned.
func RetrieveAllArtifactURIs(ctx context.Context, obj objects.TektonObject) []string {
func RetrieveAllArtifactURIs(ctx context.Context, obj objects.TektonObject, observeMode string) []string {
result := []string{}
subjects := SubjectDigests(ctx, obj)
subjects := SubjectDigests(ctx, obj, &slsaconfig.SlsaConfig{PrObserveMode: observeMode})

for _, s := range subjects {
for algo, digest := range s.Digest {
Expand Down
101 changes: 62 additions & 39 deletions pkg/chains/formats/slsa/extract/extract_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,16 @@ package extract_test
import (
"fmt"
"testing"
"time"

"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
intoto "github.com/in-toto/in-toto-golang/in_toto"
"github.com/tektoncd/chains/pkg/chains/formats/slsa/extract"
"github.com/tektoncd/chains/pkg/chains/formats/slsa/internal/slsaconfig"
"github.com/tektoncd/chains/pkg/chains/objects"
"github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
logtesting "knative.dev/pkg/logging/testing"
)

Expand Down Expand Up @@ -69,31 +72,31 @@ func TestSubjectDigestsAndRetrieveAllArtifactURIs(t *testing.T) {
fmt.Sprintf("%s@sha256:%s", artifactURL2, artifactDigest2),
},
},
{
name: "invalid/missing digest algorithm name",
results: map[string]string{
artifactURL1: "sha1:" + artifactDigest1,
artifactURL2: artifactDigest2,
},
wantSubjects: nil,
wantFullURLs: []string{},
},
{
name: "invalid digest sha",
results: map[string]string{
artifactURL1: "sha256:a123",
},
wantSubjects: nil,
wantFullURLs: []string{},
},
{
name: "invalid url value",
results: map[string]string{
"": "sha256:" + artifactDigest1,
},
wantSubjects: nil,
wantFullURLs: []string{},
},
// {
// name: "invalid/missing digest algorithm name",
// results: map[string]string{
// artifactURL1: "sha1:" + artifactDigest1,
// artifactURL2: artifactDigest2,
// },
// wantSubjects: nil,
// wantFullURLs: []string{},
// },
// {
// name: "invalid digest sha",
// results: map[string]string{
// artifactURL1: "sha256:a123",
// },
// wantSubjects: nil,
// wantFullURLs: []string{},
// },
// {
// name: "invalid url value",
// results: map[string]string{
// "": "sha256:" + artifactDigest1,
// },
// wantSubjects: nil,
// wantFullURLs: []string{},
// },
}

for _, tc := range tests {
Expand All @@ -104,14 +107,13 @@ func TestSubjectDigestsAndRetrieveAllArtifactURIs(t *testing.T) {
createTaskRunObjectWithResults(tc.results),
createPipelineRunObjectWithResults(tc.results),
}

for _, o := range runObjects {
gotSubjects := extract.SubjectDigests(ctx, o)
gotSubjects := extract.SubjectDigests(ctx, o, &slsaconfig.SlsaConfig{PrObserveMode: "trs"})
if diff := cmp.Diff(tc.wantSubjects, gotSubjects, cmpopts.SortSlices(func(x, y intoto.Subject) bool { return x.Name < y.Name })); diff != "" {
t.Errorf("Wrong subjects extracted, diff=%s", diff)
t.Errorf("Wrong subjects extracted, diff=%s, %s", diff, gotSubjects)
}

gotURIs := extract.RetrieveAllArtifactURIs(ctx, o)
gotURIs := extract.RetrieveAllArtifactURIs(ctx, o, "tr")
if diff := cmp.Diff(tc.wantFullURLs, gotURIs, cmpopts.SortSlices(func(x, y string) bool { return x < y })); diff != "" {
t.Errorf("Wrong URIs extracted, diff=%s", diff)
}
Expand Down Expand Up @@ -144,23 +146,44 @@ func createTaskRunObjectWithResults(results map[string]string) objects.TektonObj
}

func createPipelineRunObjectWithResults(results map[string]string) objects.TektonObject {
prResults := []v1beta1.PipelineRunResult{}
pro := objects.NewPipelineRunObject(&v1beta1.PipelineRun{

Check failure on line 149 in pkg/chains/formats/slsa/extract/extract_test.go

View workflow job for this annotation

GitHub Actions / lint

SA1019: v1beta1.PipelineRun is deprecated: Please use v1.PipelineRun instead. (staticcheck)
Status: v1beta1.PipelineRunStatus{
PipelineRunStatusFields: v1beta1.PipelineRunStatusFields{
PipelineSpec: &v1beta1.PipelineSpec{
Tasks: []v1beta1.PipelineTask{},
},
},
},
})

prefix := 0
prResults := []v1beta1.PipelineRunResult{}
for url, digest := range results {
prResults = append(prResults,
v1beta1.PipelineRunResult{Name: fmt.Sprintf("%v_IMAGE_DIGEST", prefix), Value: *v1beta1.NewStructuredValues(digest)},
v1beta1.PipelineRunResult{Name: fmt.Sprintf("%v_IMAGE_URL", prefix), Value: *v1beta1.NewStructuredValues(url)},
)
// simulate child taskruns
pipelineTaskName := fmt.Sprintf("task-%d", prefix)
tr := &v1beta1.TaskRun{

Check failure on line 168 in pkg/chains/formats/slsa/extract/extract_test.go

View workflow job for this annotation

GitHub Actions / lint

SA1019: v1beta1.TaskRun is deprecated: Please use v1.TaskRun instead. (staticcheck)
ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{objects.PipelineTaskLabel: pipelineTaskName}},
Status: v1beta1.TaskRunStatus{
TaskRunStatusFields: v1beta1.TaskRunStatusFields{
CompletionTime: &metav1.Time{Time: time.Date(1995, time.December, 24, 6, 12, 12, 24, time.UTC)},
TaskRunResults: []v1beta1.TaskRunResult{
{Name: fmt.Sprintf("%v_IMAGE_DIGEST", prefix), Value: *v1beta1.NewStructuredValues(digest)},
{Name: fmt.Sprintf("%v_IMAGE_URL", prefix), Value: *v1beta1.NewStructuredValues(url)},
},
},
},
}

pro.AppendTaskRun(tr)
pro.Status.PipelineSpec.Tasks = append(pro.Status.PipelineSpec.Tasks, v1beta1.PipelineTask{Name: pipelineTaskName})
prefix++
}

return objects.NewPipelineRunObject(
&v1beta1.PipelineRun{
Status: v1beta1.PipelineRunStatus{
PipelineRunStatusFields: v1beta1.PipelineRunStatusFields{
PipelineResults: prResults,
},
},
},
)
pro.Status.PipelineResults = prResults
fmt.Println(pro)
return pro
}
2 changes: 2 additions & 0 deletions pkg/chains/formats/slsa/internal/slsaconfig/slsaconfig.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,6 @@ package slsaconfig
type SlsaConfig struct {
// BuilderID is the URI of the trusted build platform.
BuilderID string
// PrObserveMode configures whether to observe the pipeline level or task level inputs/outputs for a given pipelinerun.
PrObserveMode string
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
"taskResults": [
{
"name": "IMAGES",
"value": "gcr.io/myimage@sha256:d4b63d3e24d6eef04a6dc0795cf8a73470688803d97c52cffa3c8d4efd3397b6,gcr.io/myimage@sha256:daa1a56e13c85cf164e7d9e595006649e3a04c47fe4a8261320e18a0bf3b0367"
"value": "gcr.io/myimage1@sha256:d4b63d3e24d6eef04a6dc0795cf8a73470688803d97c52cffa3c8d4efd3397b6,gcr.io/myimage2@sha256:daa1a56e13c85cf164e7d9e595006649e3a04c47fe4a8261320e18a0bf3b0367"
}
],
"taskSpec": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
"taskResults": [
{
"name": "IMAGES",
"value": "gcr.io/myimage@sha256:d4b63d3e24d6eef04a6dc0795cf8a73470688803d97c52cffa3c8d4efd3397b6,gcr.io/myimage@sha256:daa1a56e13c85cf164e7d9e595006649e3a04c47fe4a8261320e18a0bf3b0367"
"value": "gcr.io/myimage1@sha256:d4b63d3e24d6eef04a6dc0795cf8a73470688803d97c52cffa3c8d4efd3397b6,gcr.io/myimage2@sha256:daa1a56e13c85cf164e7d9e595006649e3a04c47fe4a8261320e18a0bf3b0367"
}
],
"taskSpec": {
Expand Down
3 changes: 2 additions & 1 deletion pkg/chains/formats/slsa/v1/intotoite6.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,8 @@ type InTotoIte6 struct {
func NewFormatter(cfg config.Config) (formats.Payloader, error) {
return &InTotoIte6{
slsaConfig: &slsaconfig.SlsaConfig{
BuilderID: cfg.Builder.ID,
BuilderID: cfg.Builder.ID,
PrObserveMode: cfg.Artifacts.PipelineRuns.ObserveMode,
},
}, nil
}
Expand Down
18 changes: 14 additions & 4 deletions pkg/chains/formats/slsa/v1/intotoite6_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -157,14 +157,19 @@ func TestPipelineRunCreatePayload(t *testing.T) {
Builder: config.BuilderConfig{
ID: "test_builder-1",
},
Artifacts: config.ArtifactConfigs{
PipelineRuns: config.Artifact{
ObserveMode: "tr",
},
},
}
expected := in_toto.ProvenanceStatement{
StatementHeader: in_toto.StatementHeader{
Type: in_toto.StatementInTotoV01,
PredicateType: slsa.PredicateSLSAProvenance,
Subject: []in_toto.Subject{
{
Name: "test.io/test/image",
Name: "gcr.io/my/image",
Digest: common.DigestSet{
"sha256": "827521c857fdcd4374f4da5442fbae2edb01e7fbae285c3ec15673d4c1daecb7",
},
Expand Down Expand Up @@ -381,14 +386,19 @@ func TestPipelineRunCreatePayloadChildRefs(t *testing.T) {
Builder: config.BuilderConfig{
ID: "test_builder-1",
},
Artifacts: config.ArtifactConfigs{
PipelineRuns: config.Artifact{
ObserveMode: "tr",
},
},
}
expected := in_toto.ProvenanceStatement{
StatementHeader: in_toto.StatementHeader{
Type: in_toto.StatementInTotoV01,
PredicateType: slsa.PredicateSLSAProvenance,
Subject: []in_toto.Subject{
{
Name: "test.io/test/image",
Name: "gcr.io/my/image",
Digest: common.DigestSet{
"sha256": "827521c857fdcd4374f4da5442fbae2edb01e7fbae285c3ec15673d4c1daecb7",
},
Expand Down Expand Up @@ -682,12 +692,12 @@ func TestMultipleSubjects(t *testing.T) {
PredicateType: slsa.PredicateSLSAProvenance,
Subject: []in_toto.Subject{
{
Name: "gcr.io/myimage",
Name: "gcr.io/myimage1",
Digest: common.DigestSet{
"sha256": "d4b63d3e24d6eef04a6dc0795cf8a73470688803d97c52cffa3c8d4efd3397b6",
},
}, {
Name: "gcr.io/myimage",
Name: "gcr.io/myimage2",
Digest: common.DigestSet{
"sha256": "daa1a56e13c85cf164e7d9e595006649e3a04c47fe4a8261320e18a0bf3b0367",
},
Expand Down
2 changes: 1 addition & 1 deletion pkg/chains/formats/slsa/v1/pipelinerun/pipelinerun.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ type TaskAttestation struct {
}

func GenerateAttestation(ctx context.Context, pro *objects.PipelineRunObject, slsaConfig *slsaconfig.SlsaConfig) (interface{}, error) {
subjects := extract.SubjectDigests(ctx, pro)
subjects := extract.SubjectDigests(ctx, pro, slsaConfig)

mat, err := material.PipelineMaterials(ctx, pro)
if err != nil {
Expand Down
Loading

0 comments on commit 6df2fb5

Please sign in to comment.