Skip to content

Commit

Permalink
Add function to extract structured signable targets
Browse files Browse the repository at this point in the history
  • Loading branch information
ywluogg committed Jul 18, 2022
1 parent fe5121d commit 0a1f970
Show file tree
Hide file tree
Showing 4 changed files with 290 additions and 51 deletions.
83 changes: 83 additions & 0 deletions pkg/artifacts/signable.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,15 @@ import (
"github.com/tektoncd/chains/pkg/chains/formats"
"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 {
Expand All @@ -37,6 +44,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
}
Expand Down Expand Up @@ -212,6 +231,70 @@ func extractTargetFromResults(tr *v1beta1.TaskRun, identifierSuffix string, dige
return ss
}

// ExtractStructuredIntotoSignableTargetFromResults extracts structured signable targets aim to generate intoto provenance as materials within TaskRun results and store them as StructuredSignable.
func ExtractStructuredSignableTargetFromResults(tr *v1beta1.TaskRun, categoryMarker string, logger *zap.SugaredLogger) []*StructuredSignable {
objs := []*StructuredSignable{}
artifacts := map[string]string{}
for _, res := range tr.Status.TaskRunResults {
if res.Name == categoryMarker {
for _, i := range res.Value.ArrayVal {
artifacts[i] = i
}
}
}

for _, res := range tr.Status.TaskRunResults {
if artifacts[res.Name] == "" {
continue
}
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
}

// TODO(#490): shared code in Pipeline.
// 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) {
Expand Down
74 changes: 74 additions & 0 deletions pkg/artifacts/signable_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -366,6 +366,80 @@ func TestExtractSignableTargetFromResults(t *testing.T) {
}
}

func TestExtractStructuredSignableTargetFromResults(t *testing.T) {
tr := &v1beta1.TaskRun{
Status: v1beta1.TaskRunStatus{
TaskRunStatusFields: v1beta1.TaskRunStatusFields{
TaskRunResults: []v1beta1.TaskRunResult{
{
Name: "mvn1_pkg",
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",
Value: *v1beta1.NewObject(map[string]string{
"uri": "com.google.guava:guava:31.0-jre.pom",
"digest": digest2,
"signable_type": "",
}),
},
{
Name: "img1_input",
Value: *v1beta1.NewObject(map[string]string{
"uri": "gcr.io/foo/bar",
"digest": digest3,
}),
},
{
Name: "img2_input_no_digest",
Value: *v1beta1.NewObject(map[string]string{
"uri": "gcr.io/foo/foo",
"digest": "",
}),
},
{
Name: ArtifactsInputsResultName,
Value: *v1beta1.NewArrayOrString("img1_input", "img2_input_no_digest", "img2_input_no_digest_alg"),
},
{
Name: ArtifactsOutputsResultName,
Value: *v1beta1.NewArrayOrString("mvn1_pkg", "mvn1_pom_sha512"),
},
},
},
},
}
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 := ExtractStructuredSignableTargetFromResults(tr, ArtifactsInputsResultName, logtesting.TestLogger(t))
gotOutputs := ExtractStructuredSignableTargetFromResults(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 {
Expand Down
29 changes: 27 additions & 2 deletions pkg/chains/formats/intotoite6/intotoite6.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ func (i *InTotoIte6) generateAttestationFromTaskRun(tr *v1beta1.TaskRun) (interf
Invocation: invocation(tr),
BuildConfig: buildConfig(tr),
Metadata: metadata(tr),
Materials: materials(tr),
Materials: materials(tr, i.logger),
},
}
return att, nil
Expand Down Expand Up @@ -166,6 +166,19 @@ func GetSubjectDigests(tr *v1beta1.TaskRun, logger *zap.SugaredLogger) []intoto.
})
}

ssts := artifacts.ExtractStructuredSignableTargetFromResults(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
}
Expand Down Expand Up @@ -205,7 +218,7 @@ func GetSubjectDigests(tr *v1beta1.TaskRun, logger *zap.SugaredLogger) []intoto.
}

// add any Git specification to materials
func materials(tr *v1beta1.TaskRun) []slsa.ProvenanceMaterial {
func materials(tr *v1beta1.TaskRun, logger *zap.SugaredLogger) []slsa.ProvenanceMaterial {
var mats []slsa.ProvenanceMaterial
gitCommit, gitURL := gitInfo(tr)

Expand All @@ -218,6 +231,18 @@ func materials(tr *v1beta1.TaskRun) []slsa.ProvenanceMaterial {
return mats
}

// Retrieve structured provenance for inputs.
ssts := artifacts.ExtractStructuredSignableTargetFromResults(tr, 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 tr.Spec.Resources == nil {
return mats
}
Expand Down
Loading

0 comments on commit 0a1f970

Please sign in to comment.