Skip to content

Commit

Permalink
Merge pull request #287 from marcelamelara/add-provenance-validators
Browse files Browse the repository at this point in the history
Add Go validator APIs for Provenance v1 predicate
  • Loading branch information
TomHennen authored Oct 13, 2023
2 parents 09a0315 + 6a10854 commit 2713540
Show file tree
Hide file tree
Showing 2 changed files with 277 additions and 0 deletions.
138 changes: 138 additions & 0 deletions go/predicates/provenance/v1/provenance.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
/*
Validator APIs for SLSA Provenance v1 protos.
*/
package v1

import (
"errors"
"fmt"

"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/types/known/structpb"
)

// all of the following errors apply to SLSA Build L1 and above
var (
ErrBuilderRequired = errors.New("runDetails.builder required")
ErrBuilderIdRequired = errors.New("runDetails.builder.id required")
ErrBuildDefinitionRequired = errors.New("buildDefinition required")
ErrBuildTypeRequired = errors.New("buildDefinition.buildType required")
ErrExternalParamsRequired = errors.New("buildDefinition.externalParameters required")
ErrRunDetailsRequired = errors.New("runDetails required")
)

func (m *BuildMetadata) Validate() error {
// check valid timestamps
s := m.GetStartedOn()
if s != nil {
if err := s.CheckValid(); err != nil {
return fmt.Errorf("buildMetadata.startedOn error: %w", err)
}
}

f := m.GetFinishedOn()
if f != nil {
if err := f.CheckValid(); err != nil {
return fmt.Errorf("buildMetadata.finishedOn error: %w", err)
}
}

return nil
}

func (b *Builder) Validate() error {
// the id field is required for SLSA Build L1
if b.GetId() == "" {
return ErrBuilderIdRequired
}

// check that all builderDependencies are valid RDs
builderDeps := b.GetBuilderDependencies()
for i, rd := range builderDeps {
if err := rd.Validate(); err != nil {
return fmt.Errorf("Invalid Builder.BuilderDependencies[%d]: %w", i, err)
}
}

return nil
}

func (b *BuildDefinition) Validate() error {
// the buildType field is required for SLSA Build L1
if b.GetBuildType() == "" {
return ErrBuildTypeRequired
}

// the externalParameters field is required for SLSA Build L1
ext := b.GetExternalParameters()
if ext == nil || proto.Equal(ext, &structpb.Struct{}) {
return ErrExternalParamsRequired
}

// check that all resolvedDependencies are valid RDs
resolvedDeps := b.GetResolvedDependencies()
for i, rd := range resolvedDeps {
if err := rd.Validate(); err != nil {
return fmt.Errorf("Invalid BuildDefinition.ResolvedDependencies[%d]: %w", i, err)
}
}

return nil
}

func (r *RunDetails) Validate() error {
// the builder field is required for SLSA Build L1
builder := r.GetBuilder()
if builder == nil || proto.Equal(builder, &Builder{}) {
return ErrBuilderRequired
}

// check the Builder
if err := builder.Validate(); err != nil {
return fmt.Errorf("runDetails.builder error: %w", err)
}

// check the Metadata, if present
metadata := r.GetMetadata()
if metadata != nil && !proto.Equal(metadata, &BuildMetadata{}) {
if err := metadata.Validate(); err != nil {
return fmt.Errorf("Invalid RunDetails.Metadata: %w", err)
}
}

// check that all byproducts are valid RDs
byproducts := r.GetByproducts()
for i, rd := range byproducts {
if err := rd.Validate(); err != nil {
return fmt.Errorf("Invalid RunDetails.Byproducts[%d]: %w", i, err)
}
}

return nil
}

func (p *Provenance) Validate() error {
// the buildDefinition field is required for SLSA Build L1
buildDef := p.GetBuildDefinition()
if buildDef == nil || proto.Equal(buildDef, &BuildDefinition{}) {
return ErrBuildDefinitionRequired
}

// check the BuildDefinition
if err := buildDef.Validate(); err != nil {
return fmt.Errorf("provenance.buildDefinition error: %w", err)
}

// the runDetails field is required for SLSA Build L1
runDetails := p.GetRunDetails()
if runDetails == nil || proto.Equal(runDetails, &RunDetails{}) {
return ErrRunDetailsRequired
}

// check the RunDetails
if err := runDetails.Validate(); err != nil {
return fmt.Errorf("provenance.runDetails error: %w", err)
}

return nil
}
139 changes: 139 additions & 0 deletions go/predicates/provenance/v1/provenance_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
/*
Tests for SLSA Provenance v1 protos.
*/

package v1

import (
"fmt"
"testing"

ita1 "github.com/in-toto/attestation/go/v1"
"github.com/stretchr/testify/assert"
"google.golang.org/protobuf/encoding/protojson"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/types/known/structpb"
)

func createTestProvenance(t *testing.T) *Provenance {
// Create a Provenance

t.Helper()

rd := &ita1.ResourceDescriptor{
Name: "theResource",
Digest: map[string]string{"alg1": "abc123"},
}

builder := &Builder{
Id: "theId",
Version: map[string]string{"theComponent": "v0.1"},
BuilderDependencies: []*ita1.ResourceDescriptor{rd},
}

buildMeta := &BuildMetadata{
InvocationId: "theInvocationId",
}

runDetails := &RunDetails{
Builder: builder,
Metadata: buildMeta,
Byproducts: []*ita1.ResourceDescriptor{rd},
}

externalParams, err := structpb.NewStruct(map[string]interface{}{
"param1": map[string]interface{}{
"subKey": "subVal"}})
if err != nil {
t.Fatal(err)
}

buildDef := &BuildDefinition{
BuildType: "theBuildType",
ExternalParameters: externalParams,
ResolvedDependencies: []*ita1.ResourceDescriptor{rd},
}

return &Provenance{
BuildDefinition: buildDef,
RunDetails: runDetails,
}
}

func TestJsonUnmarshalProvenance(t *testing.T) {
var wantSt = `{"buildDefinition":{"buildType":"theBuildType","externalParameters":{"param1":{"subKey":"subVal"}},"resolvedDependencies":[{"name":"theResource","digest":{"alg1":"abc123"}}]},"runDetails":{"builder":{"id":"theId","version":{"theComponent":"v0.1"},"builderDependencies":[{"name":"theResource","digest":{"alg1":"abc123"}}]},"metadata":{"invocationId":"theInvocationId"},"byproducts":[{"name":"theResource","digest":{"alg1":"abc123"}}]}}`

got := &Provenance{}
err := protojson.Unmarshal([]byte(wantSt), got)
assert.NoError(t, err, "error during JSON unmarshalling")

want := createTestProvenance(t)
assert.NoError(t, err, "unexpected error during test Statement creation")
assert.True(t, proto.Equal(got, want), "protos do not match")
}

func TestBadProvenanceBuildDefinition(t *testing.T) {
tests := map[string]struct {
input string
err error
noErrMessage string
}{
"no buildDefinition": {
input: `{"buildDefinition":{},"runDetails":{"builder":{"id":"theId","version":{"theComponent":"v0.1"},"builderDependencies":[{"name":"theResource","digest":{"alg1":"abc123"}}]},"metadata":{"invocationId":"theInvocationId"},"byproducts":[{"name":"theResource","digest":{"alg1":"abc123"}}]}}`,
err: ErrBuildDefinitionRequired,
noErrMessage: "created malformed Provenance (empty buildDefinition)",
},
"buildDefinition missing buildType required field": {
input: `{"buildDefinition":{"externalParameters":{"param1":{"subKey":"subVal"}},"resolvedDependencies":[{"name":"theResource","digest":{"alg1":"abc123"}}]},"runDetails":{"builder":{"id":"theId","version":{"theComponent":"v0.1"},"builderDependencies":[{"name":"theResource","digest":{"alg1":"abc123"}}]},"metadata":{"invocationId":"theInvocationId"},"byproducts":[{"name":"theResource","digest":{"alg1":"abc123"}}]}}`,
err: ErrBuildTypeRequired,
noErrMessage: "created malformed Provenance (buildDefinition missing required buildType field)",
},
"buildDefinition missing externalParameters required field": {
input: `{"buildDefinition":{"buildType":"theBuildType","resolvedDependencies":[{"name":"theResource","digest":{"alg1":"abc123"}}]},"runDetails":{"builder":{"id":"theId","version":{"theComponent":"v0.1"},"builderDependencies":[{"name":"theResource","digest":{"alg1":"abc123"}}]},"metadata":{"invocationId":"theInvocationId"},"byproducts":[{"name":"theResource","digest":{"alg1":"abc123"}}]}}`,
err: ErrExternalParamsRequired,
noErrMessage: "created malformed Provenance (buildDefinition missing requried externalParameters field)",
},
}

for name, test := range tests {
got := &Provenance{}
err := protojson.Unmarshal([]byte(test.input), got)
assert.NoError(t, err, fmt.Sprintf("error during JSON unmarshalling in test '%s'", name))

err = got.Validate()
assert.ErrorIs(t, err, test.err, fmt.Sprintf("%s in test '%s'", test.noErrMessage, name))
}
}

func TestBadProvenanceRunDetails(t *testing.T) {
tests := map[string]struct {
input string
err error
noErrMessage string
}{
"no runDetails": {
input: `{"buildDefinition":{"buildType":"theBuildType","externalParameters":{"param1":{"subKey":"subVal"}},"resolvedDependencies":[{"name":"theResource","digest":{"alg1":"abc123"}}]},"runDetails":{}}`,
err: ErrRunDetailsRequired,
noErrMessage: "created malformed Provenance (empty runDetails)",
},
"runDetails missing builder required field": {
input: `{"buildDefinition":{"buildType":"theBuildType","externalParameters":{"param1":{"subKey":"subVal"}},"resolvedDependencies":[{"name":"theResource","digest":{"alg1":"abc123"}}]},"runDetails":{"builder":{},"metadata":{"invocationId":"theInvocationId"},"byproducts":[{"name":"theResource","digest":{"alg1":"abc123"}}]}}`,
err: ErrBuilderRequired,
noErrMessage: "created malformed Provenance (runDetails missing required builder field)",
},
"runDetails.builder missing id required field": {
input: `{"buildDefinition":{"buildType":"theBuildType","externalParameters":{"param1":{"subKey":"subVal"}},"resolvedDependencies":[{"name":"theResource","digest":{"alg1":"abc123"}}]},"runDetails":{"builder":{"id":"","version":{"theComponent":"v0.1"},"builderDependencies":[{"name":"theResource","digest":{"alg1":"abc123"}}]},"metadata":{"invocationId":"theInvocationId"},"byproducts":[{"name":"theResource","digest":{"alg1":"abc123"}}]}}`,
err: ErrBuilderIdRequired,
noErrMessage: "created malformed Provenance (runDetails.builder missing requried id field)",
},
}

for name, test := range tests {
got := &Provenance{}
err := protojson.Unmarshal([]byte(test.input), got)
assert.NoError(t, err, fmt.Sprintf("error during JSON unmarshalling in test '%s'", name))

err = got.Validate()
assert.ErrorIs(t, err, test.err, fmt.Sprintf("%s in test '%s'", test.noErrMessage, name))
}
}

0 comments on commit 2713540

Please sign in to comment.