-
Notifications
You must be signed in to change notification settings - Fork 74
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #287 from marcelamelara/add-provenance-validators
Add Go validator APIs for Provenance v1 predicate
- Loading branch information
Showing
2 changed files
with
277 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)) | ||
} | ||
} |