Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Go validator APIs for Provenance v1 predicate #287

Merged
merged 4 commits into from
Oct 13, 2023
Merged
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
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))
}
}