Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion docs/predicate/slsa/v2.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,8 @@ Name | Description
-----|-------------
**pipeline**|Resolved (immutable) reference to the remote pipeline.
**task**|Resolved (immutable) reference to remote tasks. This name is used for a build performed via a direct `TaskRun`.
**pipelineTask**|Resolved (immutable) reference to remote `PipelineTasks`.
**pipelineTask**|Resolved (immutable) reference to a remote Task used by a PipelineTask in `v2alpha3`.
**pipelineTask/{pipelineTaskName}**|Resolved (immutable) reference to a remote Task used by a PipelineTask in `v2alpha4`. The name comes from the `tekton.dev/pipelineTask` label, or the TaskRun name when that label is absent.
N/A|Resolved reference to `step` and `sidecar container images`.<li>Even if the images are referenced by a tag in the spec it will be resolved and its digest will be included.</li><li>This resolved dependency is not named because it does not add anything of value that is needed right now from step/sidecar images.</li>
**input/results**|Input Artifacts used for the build, e.g. `Source code`. These are type-hinted results emitted by the task.

Expand Down Expand Up @@ -120,6 +121,8 @@ Parameter | Type | Description
-----------|------|------------
results| object | [Optional] Results produced by the pipeline/task. Content will be stored as bytes.

Starting with `v2alpha4`, step result byProduct names use the format `stepResults/{pipelineTaskName}/{stepName}/{resultName}`, where `pipelineTaskName` comes from the `tekton.dev/pipelineTask` label (or the TaskRun name for standalone runs).
Comment thread
ngelman1 marked this conversation as resolved.
Resolved

# Examples
## Provenance Example for referenced build config

Expand Down
4 changes: 4 additions & 0 deletions docs/slsa-provenance.md
Original file line number Diff line number Diff line change
Expand Up @@ -321,10 +321,14 @@ spec:

## `v2alpha4` formatter

Starting with version `v2alpha4`, pipeline task resolvedDependencies use `pipelineTask/{pipelineTaskName}` instead of `pipelineTask`. The name comes from the `tekton.dev/pipelineTask` label, falling back to the TaskRun name when that label is absent.

Starting with version `v2alpha4`, the type-hinted object results value now can include a new boolean flag called `isBuildArtifact`. When set to `true`, this flag indicates the output artifact should be considered as `subject` in the executed TaskRun/PipelineRun.

The `isBuildArtifact` can be set in results whose type-hint uses the `*ARTIFACT_OUTPUTS` format. Results using the `IMAGES` and `*IMAGE_URL` / `*IMAGE_DIGEST` type-hint format will still be considered as `subject` automatically; all other results will be classified as `byProduct`

Starting with version `v2alpha4`, byProduct names for step results include the step name and use the `tekton.dev/pipelineTask` label instead of the Kubernetes-generated TaskRun name: `stepResults/{pipelineTaskName}/{stepName}/{resultName}`.
Comment thread
ngelman1 marked this conversation as resolved.
Resolved

For instance, in the following TaskRun:

```yaml
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,9 @@ type AddTaskDescriptorContent func(*objects.TaskRunObjectV1) (*intoto.ResourceDe
type ResolveOptions struct {
// Indicates if StepActions type-hinted results should be read to resolve dependecies.
WithStepActionsResults bool
// UseQualifiedTaskNames controls whether resolved dependency names for pipeline tasks
// include the task identifier (e.g. "pipelineTask/my-task" instead of "pipelineTask").
UseQualifiedTaskNames bool
}

// ConvertMaterialsToResolvedDependencies converts a SLSAv0.2 Material to a resolved dependency
Expand All @@ -80,14 +83,17 @@ func RemoveDuplicateResolvedDependencies(resolvedDependencies []*intoto.Resource
// make map to store seen resolved dependencies
seen := map[string]bool{}
for _, resolvedDependency := range resolvedDependencies {
// Since resolvedDependencies contain names, we want to ignore those while checking for duplicates.
// Therefore, make a copy of the resolved dependency that only contains the uri and digest fields.
// Ignore names for existing dependency types, but keep qualified pipeline task names
// so distinct tasks using the same resource remain in the provenance.
rDep := intoto.ResourceDescriptor{}
rDep.Uri = resolvedDependency.Uri
rDep.Digest = resolvedDependency.Digest
// pipelinTasks store content with the slsa-tekton buildType
// Pipeline tasks store content with the slsa-tekton buildType.
rDep.Content = resolvedDependency.Content
// This allows us to ignore dependencies that have the same uri and digest.
if strings.HasPrefix(resolvedDependency.Name, PipelineTaskConfigName+"/") {
rDep.Name = resolvedDependency.Name
}
// This allows us to ignore dependencies with the same identifying fields.
rd, err := protojson.Marshal(&rDep)
if err != nil {
return nil, err
Expand Down Expand Up @@ -167,6 +173,13 @@ func fromPipelineTask(logger *zap.SugaredLogger, pro *objects.PipelineRunObjectV
continue
}
if rd != nil {
if opts.UseQualifiedTaskNames && rd.Name == PipelineTaskConfigName {
name := tr.TaskIdentifier()
if len(taskRuns) > 1 {
name = fmt.Sprintf("%s/%s", name, tr.GetName())
}
rd.Name = fmt.Sprintf("%s/%s", PipelineTaskConfigName, name)
}
resolvedDependencies = append(resolvedDependencies, rd)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,60 @@ func TestRemoveDuplicates(t *testing.T) {
}
}

func TestRemoveDuplicatesQualifiedPipelineTasks(t *testing.T) {
dependency := func(name, content string) *intoto.ResourceDescriptor {
var data []byte
if content != "" {
data = []byte(content)
}
return &intoto.ResourceDescriptor{
Name: name,
Uri: "git+https://example.com/task",
Digest: common.DigestSet{"sha256": "abc123"},
Content: data,
}
}

tests := []struct {
name string
rds []*intoto.ResourceDescriptor
want []*intoto.ResourceDescriptor
}{
{
name: "identical qualified tasks collapse",
rds: []*intoto.ResourceDescriptor{dependency("pipelineTask/build", ""), dependency("pipelineTask/build", "")},
want: []*intoto.ResourceDescriptor{dependency("pipelineTask/build", "")},
},
{
name: "different task names remain distinct",
rds: []*intoto.ResourceDescriptor{dependency("pipelineTask/build", ""), dependency("pipelineTask/test", "")},
want: []*intoto.ResourceDescriptor{dependency("pipelineTask/build", ""), dependency("pipelineTask/test", "")},
},
{
name: "different task content remains distinct",
rds: []*intoto.ResourceDescriptor{dependency("pipelineTask/build", "first"), dependency("pipelineTask/build", "second")},
want: []*intoto.ResourceDescriptor{dependency("pipelineTask/build", "first"), dependency("pipelineTask/build", "second")},
},
{
name: "legacy pipeline task names still collapse",
rds: []*intoto.ResourceDescriptor{dependency("pipelineTask", ""), dependency("pipelineTask", "")},
want: []*intoto.ResourceDescriptor{dependency("pipelineTask", "")},
},
}

for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
got, err := RemoveDuplicateResolvedDependencies(tc.rds)
if err != nil {
t.Fatal(err)
}
if diff := cmp.Diff(tc.want, got, protocmp.Transform()); diff != "" {
t.Errorf("RemoveDuplicateResolvedDependencies() mismatch (-want +got): %s", diff)
}
})
}
}

func createPro(path string) *objects.PipelineRunObjectV1 {
var err error
pr, err := objectloader.PipelineRunV1FromFile(path)
Expand Down
28 changes: 28 additions & 0 deletions pkg/chains/formats/slsa/internal/results/results.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,34 @@ func GetResultsWithoutBuildArtifacts(objName string, results []objects.Result, r
return byProd, nil
}

// GetStepResultsWithoutBuildArtifacts returns step results that are not build artifacts,
// using the step name from each Result to produce a 3-segment byProduct name.
func GetStepResultsWithoutBuildArtifacts(taskName string, results []objects.Result, resultTypePrefix string) ([]*slsa.ResourceDescriptor, error) {
byProd := []*slsa.ResourceDescriptor{}
for _, r := range results {
if isBuildArtifact, err := artifacts.IsBuildArtifact(r); err != nil || isBuildArtifact {
continue
}

if isOCIImage(r.Name) {
continue
}

content, err := json.Marshal(r.Value)
if err != nil {
return nil, err
}

byProd = append(byProd, &slsa.ResourceDescriptor{
Name: fmt.Sprintf(resultTypePrefix, taskName, r.StepName, r.Name),
Content: content,
MediaType: "application/json",
})
}

return byProd, nil
}

func isOCIImage(resName string) bool {
for _, suffix := range imageResultsNamesSuffixs {
if strings.HasSuffix(resName, suffix) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ package pipelinerun

import (
"context"
"fmt"

intoto "github.com/in-toto/attestation/go/v1"
"github.com/tektoncd/chains/pkg/chains/formats/slsa/extract"
Expand All @@ -41,7 +42,7 @@ func GenerateAttestation(ctx context.Context, pro *objects.PipelineRunObjectV1,
return nil, err
}

opts := resolveddependencies.ResolveOptions{WithStepActionsResults: true}
opts := resolveddependencies.ResolveOptions{WithStepActionsResults: true, UseQualifiedTaskNames: true}
bd, err := builddefinition.GetPipelineRunBuildDefinition(ctx, pro, slsaconfig, opts)
if err != nil {
return nil, err
Expand All @@ -63,8 +64,13 @@ func byproducts(pro *objects.PipelineRunObjectV1, slsaconfig *slsaconfig.SlsaCon
return byProd, nil
}

trCounts := taskRunCountByPipelineTask(pro)
for _, tro := range pro.GetExecutedTasks() {
taskProds, err := taskrun.ByProducts(tro)
taskName := tro.TaskIdentifier()
if trCounts[taskName] > 1 {
taskName = fmt.Sprintf("%s/%s", taskName, tro.GetName())
}
taskProds, err := taskrun.ByProductsWithTaskName(tro, taskName)
if err != nil {
return nil, err
}
Expand All @@ -74,6 +80,31 @@ func byproducts(pro *objects.PipelineRunObjectV1, slsaconfig *slsaconfig.SlsaCon
return byProd, nil
}

// taskRunCountByPipelineTask returns a map of pipelineTask name to the number of
// TaskRuns associated with it. Used to detect matrix fan-out.
func taskRunCountByPipelineTask(pro *objects.PipelineRunObjectV1) map[string]int {
counts := map[string]int{}
pSpec := pro.Status.PipelineSpec
if pSpec == nil {
return counts
}
tasks := pSpec.Tasks
tasks = append(tasks, pSpec.Finally...)
for _, task := range tasks {
taskRuns := pro.GetTaskRunsFromTask(task.Name)
count := 0
for _, tr := range taskRuns {
if tr != nil && tr.Status.CompletionTime != nil {
count++
}
}
if count > 0 {
counts[task.Name] = count
}
}
return counts
}

// SubjectDigests calculates the subjects associated with the given PipelineRun.
func SubjectDigests(ctx context.Context, pro *objects.PipelineRunObjectV1, slsaconfig *slsaconfig.SlsaConfig) []*intoto.ResourceDescriptor {
subjects := extract.SubjectsFromBuildArtifact(ctx, pro.GetResults())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,11 @@ import (
logtesting "knative.dev/pkg/logging/testing"
)

const (
pipelineTaskGitClone = "pipelineTask/git-clone"
pipelineTaskBuild = "pipelineTask/build"
)

func TestByProducts(t *testing.T) {
resultValue := v1.ResultValue{Type: "string", StringVal: "result-value"}
pr := &v1.PipelineRun{
Expand Down Expand Up @@ -125,7 +130,7 @@ func TestGenerateAttestation(t *testing.T) {
{
Uri: "git+https://github.com/catalog",
Digest: common.DigestSet{"sha1": "x123"},
Name: "pipelineTask",
Name: pipelineTaskGitClone,
},
{
Uri: "oci://gcr.io/test1/test1",
Expand All @@ -134,7 +139,7 @@ func TestGenerateAttestation(t *testing.T) {
{
Uri: "git+https://github.com/test",
Digest: common.DigestSet{"sha1": "ab123"},
Name: "pipelineTask",
Name: pipelineTaskBuild,
},
{
Uri: "oci://gcr.io/test2/test2",
Expand Down Expand Up @@ -220,7 +225,7 @@ func TestGenerateAttestation(t *testing.T) {
{
Uri: "git+https://github.com/catalog",
Digest: common.DigestSet{"sha1": "x123"},
Name: "pipelineTask",
Name: pipelineTaskGitClone,
},
{
Uri: "oci://gcr.io/test1/test1",
Expand All @@ -229,7 +234,7 @@ func TestGenerateAttestation(t *testing.T) {
{
Uri: "git+https://github.com/test",
Digest: common.DigestSet{"sha1": "ab123"},
Name: "pipelineTask",
Name: pipelineTaskBuild,
},
{
Uri: "oci://gcr.io/test2/test2",
Expand Down Expand Up @@ -291,15 +296,15 @@ func TestGenerateAttestation(t *testing.T) {
Content: []uint8(`"pkg:deb/debian/curl@7.50.3-1"`),
MediaType: JSONMediaType,
}, {
Name: "stepResults/git-clone/step1_result1-ARTIFACT_INPUTS",
Name: "stepResults/git-clone/step1/step1_result1-ARTIFACT_INPUTS",
Content: []uint8(`{"digest":"sha1:7f2f46e1b97df36b2b82d1b1d87c81b8b3d21601","uri":"https://github.com/tektoncd/pipeline"}`),
MediaType: JSONMediaType,
}, {
Name: "stepResults/taskrun-build/step1_result1",
Name: "stepResults/build/step1/step1_result1",
Content: []uint8(`"result-value"`),
MediaType: JSONMediaType,
}, {
Name: "stepResults/taskrun-build/step1_result1-ARTIFACT_OUTPUTS",
Name: "stepResults/build/step2/step1_result1-ARTIFACT_OUTPUTS",
Content: []uint8(`{"digest":"sha256:827521c857fdcd4374f4da5442fbae2edb01e7fbae285c3ec15673d4c1daecb7","uri":"gcr.io/my/image/fromstep2"}`),
MediaType: JSONMediaType,
},
Expand Down Expand Up @@ -368,7 +373,7 @@ func TestGenerateAttestation(t *testing.T) {
{
Uri: "git+https://github.com/catalog",
Digest: common.DigestSet{"sha1": "x123"},
Name: "pipelineTask",
Name: pipelineTaskGitClone,
},
{
Uri: "oci://gcr.io/test1/test1",
Expand All @@ -377,7 +382,7 @@ func TestGenerateAttestation(t *testing.T) {
{
Uri: "git+https://github.com/test",
Digest: common.DigestSet{"sha1": "ab123"},
Name: "pipelineTask",
Name: pipelineTaskBuild,
},
{
Uri: "oci://gcr.io/test2/test2",
Expand Down Expand Up @@ -434,11 +439,11 @@ func TestGenerateAttestation(t *testing.T) {
Content: []uint8(`"pkg:deb/debian/curl@7.50.3-1"`),
MediaType: JSONMediaType,
}, {
Name: "stepResults/git-clone/step1_result1-ARTIFACT_INPUTS",
Name: "stepResults/git-clone/step1/step1_result1-ARTIFACT_INPUTS",
Content: []uint8(`{"digest":"sha1:7f2f46e1b97df36b2b82d1b1d87c81b8b3d21601","uri":"https://github.com/tektoncd/pipeline"}`),
MediaType: JSONMediaType,
}, {
Name: "stepResults/taskrun-build/step1_result1",
Name: "stepResults/build/step1/step1_result1",
Content: []uint8(`"result-value"`),
MediaType: JSONMediaType,
},
Expand Down
14 changes: 10 additions & 4 deletions pkg/chains/formats/slsa/v2alpha4/internal/taskrun/taskrun.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ import (

const (
taskRunResults = "taskRunResults/%s/%s"
taskRunStepResults = "stepResults/%s/%s"
taskRunStepResults = "stepResults/%s/%s/%s" // 3 segments" stepResults/{taskIdentifier}/{stepName}/{resultName}
)

// GenerateAttestation returns the provenance for the given taskrun in SALSA 1.0 format.
Expand All @@ -51,17 +51,23 @@ func GenerateAttestation(ctx context.Context, tro *objects.TaskRunObjectV1, slsa
return provenance.GetSLSA1Statement(tro, sub, &bd, bp, slsaConfig)
}

// ByProducts returns the results categorized as byproduct from the given TaskRun.
func ByProducts(tro *objects.TaskRunObjectV1) ([]*intoto.ResourceDescriptor, error) {
return ByProductsWithTaskName(tro, tro.TaskIdentifier())
}

// ByProductsWithTaskName is like ByProducts but uses the given taskName instead
// of TaskIdentifier(). This is needed for matrix fan-out where multiple TaskRuns
// share the same pipelineTask label and need a disambiguated name.
func ByProductsWithTaskName(tro *objects.TaskRunObjectV1, taskName string) ([]*intoto.ResourceDescriptor, error) {
byProd := []*intoto.ResourceDescriptor{}

res, err := results.GetResultsWithoutBuildArtifacts(tro.GetName(), tro.GetResults(), taskRunResults)
res, err := results.GetResultsWithoutBuildArtifacts(taskName, tro.GetResults(), taskRunResults)
if err != nil {
return nil, err
}
byProd = append(byProd, res...)

res, err = results.GetResultsWithoutBuildArtifacts(tro.GetName(), tro.GetStepResults(), taskRunStepResults)
res, err = results.GetStepResultsWithoutBuildArtifacts(taskName, tro.GetStepResults(), taskRunStepResults)
if err != nil {
return nil, err
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -138,12 +138,12 @@ func TestTaskRunGenerateAttestation(t *testing.T) {
},
Byproducts: []*intoto.ResourceDescriptor{
{
Name: "stepResults/taskrun-build/step1_result1",
Name: "stepResults/build/step1/step1_result1",
MediaType: "application/json",
Content: []uint8(`"result-value"`),
},
{
Name: "stepResults/taskrun-build/step1_result1-ARTIFACT_OUTPUTS",
Name: "stepResults/build/step2/step1_result1-ARTIFACT_OUTPUTS",
MediaType: "application/json",
Content: []uint8(`{"digest":"sha256:827521c857fdcd4374f4da5442fbae2edb01e7fbae285c3ec15673d4c1daecb7","uri":"gcr.io/my/image/fromstep2"}`),
},
Expand Down
Loading
Loading