diff --git a/pkg/artifacts/signable.go b/pkg/artifacts/signable.go index cafbe44223..859d2220b2 100644 --- a/pkg/artifacts/signable.go +++ b/pkg/artifacts/signable.go @@ -24,8 +24,15 @@ import ( "github.com/tektoncd/chains/pkg/chains/objects" "github.com/tektoncd/chains/pkg/config" "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1" + "github.com/tektoncd/pipeline/pkg/list" "go.uber.org/zap" "k8s.io/apimachinery/pkg/util/sets" + "knative.dev/pkg/apis" +) + +const ( + ArtifactsInputsResultName = "ARTIFACT_INPUTS" + ArtifactsOutputsResultName = "ARTIFACT_OUTPUTS" ) type Signable interface { @@ -38,6 +45,18 @@ type Signable interface { Enabled(cfg config.Config) bool } +type ProvenanceSchema struct { + // Properties is the JSON Schema properties to support key-value pairs parameter. + Properties map[string]v1beta1.PropertySpec +} + +var structuredSignableSchema = ProvenanceSchema{ + Properties: map[string]v1beta1.PropertySpec{ + "uri": {Type: v1beta1.ParamTypeString}, + "digest": {Type: v1beta1.ParamTypeString}, + }, +} + type TaskRunArtifact struct { Logger *zap.SugaredLogger } @@ -257,6 +276,59 @@ func extractTargetFromResults(obj objects.TektonObject, identifierSuffix string, return ss } +// ExtractStructuredTargetFromResults extracts structured signable targets aim to generate intoto provenance as materials within TaskRun results and store them as StructuredSignable. +func ExtractStructuredTargetFromResults(obj objects.TektonObject, categoryMarker string, logger *zap.SugaredLogger) []*StructuredSignable { + objs := []*StructuredSignable{} + for _, res := range obj.GetResults() { + if strings.HasSuffix(res.Name, categoryMarker) { + if validateObjectKeys(structuredSignableSchema.Properties, &res.Value) != nil { + logger.Errorf("%s should follow this schema: %v", res.Name, structuredSignableSchema) + continue + } + if err := checkDigest(res.Value.ObjectVal["digest"]); err != nil { + logger.Errorf("error getting digest %s: %v", res.Value.ObjectVal["digest"], err) + continue + } + if res.Value.ObjectVal["uri"] == "" { + logger.Errorf("URI cannot be empty for %s", res.Name) + continue + } + objs = append(objs, &StructuredSignable{URI: res.Value.ObjectVal["uri"], Digest: res.Value.ObjectVal["digest"]}) + } + } + return objs +} + +// validateObjectKeys validates if object keys defined in properties are all provided in its value provider iff the provider is not nil. +func validateObjectKeys(properties map[string]v1beta1.PropertySpec, propertiesProvider *v1beta1.ArrayOrString) (errs *apis.FieldError) { + if propertiesProvider == nil || propertiesProvider.ObjectVal == nil { + return nil + } + + neededKeys := []string{} + providedKeys := []string{} + + // collect all needed keys + for key := range properties { + neededKeys = append(neededKeys, key) + } + + // collect all provided keys + for key := range propertiesProvider.ObjectVal { + providedKeys = append(providedKeys, key) + } + + missings := list.DiffLeft(neededKeys, providedKeys) + if len(missings) != 0 { + return &apis.FieldError{ + Message: fmt.Sprintf("Required key(s) %s are missing in the value provider.", missings), + Paths: []string{"properties", "default"}, + } + } + + return nil +} + func checkDigest(dig string) error { prefix := digest.Canonical.String() + ":" if !strings.HasPrefix(dig, prefix) { diff --git a/pkg/artifacts/signable_test.go b/pkg/artifacts/signable_test.go index 295752246a..877e718249 100644 --- a/pkg/artifacts/signable_test.go +++ b/pkg/artifacts/signable_test.go @@ -381,6 +381,72 @@ func TestExtractSignableTargetFromResults(t *testing.T) { } } +func TestExtractStructuredTargetFromResults(t *testing.T) { + tr := &v1beta1.TaskRun{ + Status: v1beta1.TaskRunStatus{ + TaskRunStatusFields: v1beta1.TaskRunStatusFields{ + TaskRunResults: []v1beta1.TaskRunResult{ + { + Name: "mvn1_pkg" + "_" + ArtifactsOutputsResultName, + Value: *v1beta1.NewObject(map[string]string{ + "uri": "projects/test-project/locations/us-west4/repositories/test-repo/mavenArtifacts/com.google.guava:guava:31.0-jre", + "digest": digest1, + "signable_type": "", + }), + }, + { + Name: "mvn1_pom_sha512" + "_" + ArtifactsOutputsResultName, + Value: *v1beta1.NewObject(map[string]string{ + "uri": "com.google.guava:guava:31.0-jre.pom", + "digest": digest2, + "signable_type": "", + }), + }, + { + Name: "img1_input" + "_" + ArtifactsInputsResultName, + Value: *v1beta1.NewObject(map[string]string{ + "uri": "gcr.io/foo/bar", + "digest": digest3, + }), + }, + { + Name: "img2_input_no_digest" + "_" + ArtifactsInputsResultName, + Value: *v1beta1.NewObject(map[string]string{ + "uri": "gcr.io/foo/foo", + "digest": "", + }), + }, + }, + }, + }, + } + wantInputs := []*StructuredSignable{ + {URI: "gcr.io/foo/bar", Digest: digest3}, + } + + wantOutputs := []*StructuredSignable{ + {URI: "projects/test-project/locations/us-west4/repositories/test-repo/mavenArtifacts/com.google.guava:guava:31.0-jre", Digest: digest1}, + {URI: "com.google.guava:guava:31.0-jre.pom", Digest: digest2}, + } + gotInputs := ExtractStructuredTargetFromResults(objects.NewTaskRunObject(tr), ArtifactsInputsResultName, logtesting.TestLogger(t)) + gotOutputs := ExtractStructuredTargetFromResults(objects.NewTaskRunObject(tr), ArtifactsOutputsResultName, logtesting.TestLogger(t)) + gotInputs = sortArtifacts(gotInputs) + gotOutputs = sortArtifacts(gotOutputs) + if !cmp.Equal(gotInputs, wantInputs, ignore...) { + t.Fatalf("Inputs not the same %s", cmp.Diff(wantInputs, gotInputs, ignore...)) + } + if !cmp.Equal(gotOutputs, wantOutputs, ignore...) { + t.Fatalf("Outputs not the same %s", cmp.Diff(wantOutputs, gotOutputs, ignore...)) + } +} + +func sortArtifacts(artifacts []*StructuredSignable) []*StructuredSignable { + sort.Slice(artifacts, func(i, j int) bool { + return artifacts[i].Digest < artifacts[j].Digest + }) + return artifacts +} + func createDigest(t *testing.T, dgst string) name.Digest { result, err := name.NewDigest(dgst) if err != nil { diff --git a/pkg/chains/formats/intotoite6/extract/extract.go b/pkg/chains/formats/intotoite6/extract/extract.go index 062f5b678b..eda034da47 100644 --- a/pkg/chains/formats/intotoite6/extract/extract.go +++ b/pkg/chains/formats/intotoite6/extract/extract.go @@ -62,6 +62,19 @@ func SubjectDigests(obj objects.TektonObject, logger *zap.SugaredLogger) []intot }) } + ssts := artifacts.ExtractStructuredTargetFromResults(obj, artifacts.ArtifactsOutputsResultName, logger) + for _, s := range ssts { + splits := strings.Split(s.Digest, ":") + alg := splits[0] + digest := splits[1] + subjects = append(subjects, intoto.Subject{ + Name: s.URI, + Digest: slsa.DigestSet{ + alg: digest, + }, + }) + } + // Check if object is a Taskrun, if so search for images used in PipelineResources // Otherwise object is a PipelineRun, where Pipelineresources are not relevant. // PipelineResources have been deprecated so their support has been left out of @@ -105,3 +118,86 @@ func SubjectDigests(obj objects.TektonObject, logger *zap.SugaredLogger) []intot }) return subjects } + +// // GetSubjectDigests extracts OCI images from the TaskRun based on standard hinting set up +// // It also goes through looking for any PipelineResources of Image type +// func GetSubjectDigests(tr *v1beta1.TaskRun, logger *zap.SugaredLogger) []intoto.Subject { +// var subjects []intoto.Subject + +// imgs := artifacts.ExtractOCIImagesFromResults(tr, logger) +// for _, i := range imgs { +// if d, ok := i.(name.Digest); ok { +// subjects = append(subjects, intoto.Subject{ +// Name: d.Repository.Name(), +// Digest: slsa.DigestSet{ +// "sha256": strings.TrimPrefix(d.DigestStr(), "sha256:"), +// }, +// }) +// } +// } + +// sts := artifacts.ExtractSignableTargetFromResults(tr, logger) +// for _, obj := range sts { +// splits := strings.Split(obj.Digest, ":") +// if len(splits) != 2 { +// logger.Errorf("Digest %s should be in the format of: algorthm:abc", obj.Digest) +// continue +// } +// subjects = append(subjects, intoto.Subject{ +// Name: obj.URI, +// Digest: slsa.DigestSet{ +// splits[0]: splits[1], +// }, +// }) +// } + +// ssts := artifacts.ExtractStructuredTargetFromResults(tr, artifacts.ArtifactsOutputsResultName, logger) +// for _, s := range ssts { +// splits := strings.Split(s.Digest, ":") +// alg := splits[0] +// digest := splits[1] +// subjects = append(subjects, intoto.Subject{ +// Name: s.URI, +// Digest: slsa.DigestSet{ +// alg: digest, +// }, +// }) +// } + +// if tr.Spec.Resources == nil { +// return subjects +// } + +// // go through resourcesResult +// for _, output := range tr.Spec.Resources.Outputs { +// name := output.Name +// if output.PipelineResourceBinding.ResourceSpec == nil { +// continue +// } +// // similarly, we could do this for other pipeline resources or whatever thing replaces them +// if output.PipelineResourceBinding.ResourceSpec.Type == v1alpha1.PipelineResourceTypeImage { +// // get the url and digest, and save as a subject +// var url, digest string +// for _, s := range tr.Status.ResourcesResult { +// if s.ResourceName == name { +// if s.Key == "url" { +// url = s.Value +// } +// if s.Key == "digest" { +// digest = s.Value +// } +// } +// } +// subjects = append(subjects, intoto.Subject{ +// Name: url, +// Digest: slsa.DigestSet{ +// "sha256": strings.TrimPrefix(digest, "sha256:"), +// }, +// }) +// } +// } +// sort.Slice(subjects, func(i, j int) bool { +// return subjects[i].Name <= subjects[j].Name +// }) +// return subjects +// } diff --git a/pkg/chains/formats/intotoite6/taskrun/provenance_test.go b/pkg/chains/formats/intotoite6/taskrun/provenance_test.go index 2d87c7c6d2..14f86e3007 100644 --- a/pkg/chains/formats/intotoite6/taskrun/provenance_test.go +++ b/pkg/chains/formats/intotoite6/taskrun/provenance_test.go @@ -27,6 +27,7 @@ import ( "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/intotoite6/extract" "github.com/tektoncd/chains/pkg/chains/objects" "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1" @@ -102,50 +103,68 @@ status: }, } - got := materials(objects.NewTaskRunObject(taskRun)) + got := materials(objects.NewTaskRunObject(taskRun), logtesting.TestLogger(t)) if !reflect.DeepEqual(expected, got) { t.Fatalf("expected %v got %v", expected, got) } } func TestMaterials(t *testing.T) { - // make sure this works with Git resources - taskrun := `apiVersion: tekton.dev/v1beta1 -kind: TaskRun -spec: - resources: - inputs: - - name: nil-resource-spec - - name: repo - resourceSpec: - params: - - name: url - value: https://github.com/GoogleContainerTools/distroless - type: git - taskSpec: - resources: - inputs: - - name: repo - type: git -status: - resourcesResult: - - key: commit - resourceName: repo - resourceRef: - name: repo - value: 50c56a48cfb3a5a80fa36ed91c739bdac8381cbe - - key: url - resourceName: repo - resourceRef: - name: repo - value: https://github.com/GoogleContainerTools/distroless` - - var taskRun *v1beta1.TaskRun - if err := yaml.Unmarshal([]byte(taskrun), &taskRun); err != nil { - t.Fatal(err) + taskrun := &v1beta1.TaskRun{ + Spec: v1beta1.TaskRunSpec{ + Resources: &v1beta1.TaskRunResources{ + Inputs: []v1beta1.TaskResourceBinding{ + { + PipelineResourceBinding: v1beta1.PipelineResourceBinding{ + Name: "nil-resource-spec", + }, + }, { + PipelineResourceBinding: v1beta1.PipelineResourceBinding{ + Name: "repo", + ResourceSpec: &v1alpha1.PipelineResourceSpec{ + Params: []v1alpha1.ResourceParam{ + {Name: "url", Value: "https://github.com/GoogleContainerTools/distroless"}, + }, + Type: v1alpha1.PipelineResourceTypeGit, + }, + }, + }, + }, + }, + }, + Status: v1beta1.TaskRunStatus{ + TaskRunStatusFields: v1beta1.TaskRunStatusFields{ + TaskRunResults: []v1beta1.TaskRunResult{ + { + Name: "img1_input" + "-" + artifacts.ArtifactsInputsResultName, + Value: *v1beta1.NewObject(map[string]string{ + "uri": "gcr.io/foo/bar", + "digest": digest3, + }), + }, + }, + ResourcesResult: []v1beta1.PipelineResourceResult{ + { + ResourceName: "repo", + Key: "commit", + Value: "50c56a48cfb3a5a80fa36ed91c739bdac8381cbe", + }, { + ResourceName: "repo", + Key: "url", + Value: "https://github.com/GoogleContainerTools/distroless", + }, + }, + }, + }, } expected := []slsa.ProvenanceMaterial{ + { + URI: "gcr.io/foo/bar", + Digest: slsa.DigestSet{ + "sha256": strings.TrimPrefix(digest3, "sha256:"), + }, + }, { URI: "git+https://github.com/GoogleContainerTools/distroless.git", Digest: slsa.DigestSet{ @@ -154,25 +173,20 @@ status: }, } - got := materials(objects.NewTaskRunObject(taskRun)) + got := materials(objects.NewTaskRunObject(taskrun), logtesting.TestLogger(t)) if !reflect.DeepEqual(expected, got) { t.Fatalf("expected %v got %v", expected, got) } // make sure this works with GIT* results as well - taskrun = `apiVersion: tekton.dev/v1beta1 -kind: TaskRun -spec: - params: - - name: CHAINS-GIT_COMMIT - value: my-commit - - name: CHAINS-GIT_URL - value: github.com/something` - taskRun = &v1beta1.TaskRun{} - if err := yaml.Unmarshal([]byte(taskrun), &taskRun); err != nil { - t.Fatal(err) + taskRun := &v1beta1.TaskRun{ + Spec: v1beta1.TaskRunSpec{ + Params: []v1beta1.Param{ + {Name: "CHAINS-GIT_COMMIT", Value: v1beta1.ArrayOrString{StringVal: "my-commit"}}, + {Name: "CHAINS-GIT_URL", Value: v1beta1.ArrayOrString{StringVal: "github.com/something"}}, + }, + }, } - expected = []slsa.ProvenanceMaterial{ { URI: "git+github.com/something.git", @@ -182,7 +196,7 @@ spec: }, } - got = materials(objects.NewTaskRunObject(taskRun)) + got = materials(objects.NewTaskRunObject(taskRun), logtesting.TestLogger(t)) if !reflect.DeepEqual(expected, got) { t.Fatalf("expected %v got %v", expected, got) } @@ -323,6 +337,27 @@ func TestGetSubjectDigests(t *testing.T) { 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{ { @@ -341,6 +376,11 @@ func TestGetSubjectDigests(t *testing.T) { 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:"), @@ -360,6 +400,11 @@ func TestGetSubjectDigests(t *testing.T) { 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{ diff --git a/pkg/chains/formats/intotoite6/taskrun/taskrun.go b/pkg/chains/formats/intotoite6/taskrun/taskrun.go index 0374a53708..be16b7c6f4 100644 --- a/pkg/chains/formats/intotoite6/taskrun/taskrun.go +++ b/pkg/chains/formats/intotoite6/taskrun/taskrun.go @@ -15,9 +15,11 @@ package taskrun import ( "fmt" + "strings" 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/artifacts" "github.com/tektoncd/chains/pkg/chains/formats/intotoite6/attest" "github.com/tektoncd/chains/pkg/chains/formats/intotoite6/extract" "github.com/tektoncd/chains/pkg/chains/objects" @@ -45,7 +47,7 @@ func GenerateAttestation(builderID string, tro *objects.TaskRunObject, logger *z Invocation: invocation(tro), BuildConfig: buildConfig(tro), Metadata: metadata(tro), - Materials: materials(tro), + Materials: materials(tro, logger), }, } return att, nil @@ -79,7 +81,7 @@ func metadata(tro *objects.TaskRunObject) *slsa.ProvenanceMetadata { } // add any Git specification to materials -func materials(tro *objects.TaskRunObject) []slsa.ProvenanceMaterial { +func materials(tro *objects.TaskRunObject, logger *zap.SugaredLogger) []slsa.ProvenanceMaterial { var mats []slsa.ProvenanceMaterial gitCommit, gitURL := gitInfo(tro) @@ -92,6 +94,18 @@ func materials(tro *objects.TaskRunObject) []slsa.ProvenanceMaterial { return mats } + // Retrieve structured provenance for inputs. + ssts := artifacts.ExtractStructuredTargetFromResults(tro, artifacts.ArtifactsInputsResultName, logger) + for _, s := range ssts { + splits := strings.Split(s.Digest, ":") + alg := splits[0] + digest := splits[1] + mats = append(mats, slsa.ProvenanceMaterial{ + URI: s.URI, + Digest: map[string]string{alg: digest}, + }) + } + if tro.Spec.Resources == nil { return mats }