From 22aab2d7392a8b64b4a233d5993e54ed9f6e42c6 Mon Sep 17 00:00:00 2001 From: "Cole (Mike) Winberry" Date: Tue, 2 Apr 2024 10:53:10 -0700 Subject: [PATCH] feat(dev/validate): #70. Create initial LintValidation functionality and added to dev validate cmd --- src/cmd/dev/validate.go | 50 ++++++++++ src/cmd/dev/validate_test.go | 176 +++++++++++++++++++++++++++++++++++ 2 files changed, 226 insertions(+) create mode 100644 src/cmd/dev/validate_test.go diff --git a/src/cmd/dev/validate.go b/src/cmd/dev/validate.go index a3dd41ac..b22a7d47 100644 --- a/src/cmd/dev/validate.go +++ b/src/cmd/dev/validate.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "fmt" + "reflect" "strings" "github.com/defenseunicorns/go-oscal/src/pkg/utils" @@ -127,3 +128,52 @@ func writeValidation(result types.Validation, outputFile string) error { return nil } + +// LintValidation checks if a validation has all the required fields. +func LintValidation(validation types.Validation) error { + if validation.Title == "" { + return fmt.Errorf("validation title is required") + } + + // Requires a target + if reflect.DeepEqual(validation.Target, types.Target{}) { + return fmt.Errorf("validation target is required") + } + + // Requires a payload + if reflect.DeepEqual(validation.Target.Payload, types.Payload{}) { + return fmt.Errorf("validation target payload is required") + } + + // Requires resources + if len(validation.Target.Payload.Resources) == 0 { + return fmt.Errorf("validation target resources are required") + } + + // Iterate through each resource and check if the rule has all the required fields + for _, resource := range validation.Target.Payload.Resources { + // get the resource rule + rule := resource.ResourceRule + + // Requires a version + if rule.Version == "" { + return fmt.Errorf("resource %s has no version", rule.Name) + } + + // Requires a resource + if rule.Resource == "" { + return fmt.Errorf("resource %s has no resource", rule.Name) + } + + // Requires a namespace if the resource has a name + if rule.Name != "" && len(rule.Namespaces) == 0 { + return fmt.Errorf("resource %s has no namespaces", rule.Name) + } + + // Requires a name if the resource has a field + if !reflect.DeepEqual(rule.Field, types.Field{}) && rule.Name == "" { + return fmt.Errorf("resource-rule with field must have a name") + } + } + return nil +} diff --git a/src/cmd/dev/validate_test.go b/src/cmd/dev/validate_test.go new file mode 100644 index 00000000..2b214835 --- /dev/null +++ b/src/cmd/dev/validate_test.go @@ -0,0 +1,176 @@ +package dev + +import ( + "testing" + + "github.com/defenseunicorns/lula/src/types" +) + +func Test_lintValidation(t *testing.T) { + + tests := []struct { + name string + validation types.Validation + wantErr bool + }{ + // TODO: Add test cases. + { + name: "success", + validation: types.Validation{ + Title: "Lula Validation", + LulaVersion: ">= v0.1.0", + Target: types.Target{ + Provider: "opa", + Domain: "kubernetes", + Payload: types.Payload{ + Resources: []types.Resource{ + { + Name: "podsvt", + ResourceRule: types.ResourceRule{ + Group: "core", + Version: "v1", + Resource: "pods", + Namespaces: []string{"validation-test"}, + }, + }, + }, + Rego: "package validate\n\nimport future.keywords.every\n\nvalidate {\n every pod in input.podsvt {\n podLabel := pod.metadata.labels.foo\npodlabel == \"bar\"\n }\n}", + }, + }, + }, + wantErr: false, + }, + { + name: "error no validation", + validation: types.Validation{}, + wantErr: true, + }, + { + name: "error no validation.title", + validation: types.Validation{ + Title: "Lula Validation", + }, + wantErr: true, + }, + { + name: "error no target", + validation: types.Validation{ + Title: "Lula Validation", + LulaVersion: ">= v0.1.0", + Target: types.Target{}, + }, + wantErr: true, + }, + { + name: "error resource-rule.name no resource-rule.namespaces", + validation: types.Validation{ + Title: "Lula Validation", + LulaVersion: ">= v0.1.0", + Target: types.Target{ + Provider: "opa", + Domain: "kubernetes", + Payload: types.Payload{ + Resources: []types.Resource{ + { + Name: "podsvt", + ResourceRule: types.ResourceRule{ + Name: "podsvt", + Group: "core", + Version: "v1", + Resource: "pods", + }, + }, + }, + Rego: "package validate\n\nimport future.keywords.every\n\nvalidate {\n every pod in input.podsvt {\n podLabel := pod.metadata.labels.foo\npodlabel == \"bar\"\n }\n}", + }, + }, + }, + wantErr: true, + }, + { + name: "error resource-rule.field no resource-rule.name", + validation: types.Validation{ + Title: "Lula Validation", + LulaVersion: ">= v0.1.0", + Target: types.Target{ + Provider: "opa", + Domain: "kubernetes", + Payload: types.Payload{ + Resources: []types.Resource{ + { + Name: "podsvt", + ResourceRule: types.ResourceRule{ + Group: "core", + Version: "v1", + Resource: "pods", + Field: types.Field{ + Jsonpath: "metadata.labels.foo", + Type: "json", + Base64: false, + }, + }, + }, + }, + Rego: "package validate\n\nimport future.keywords.every\n\nvalidate {\n every pod in input.podsvt {\n podLabel := pod.metadata.labels.foo\npodlabel == \"bar\"\n }\n}", + }, + }, + }, + wantErr: true, + }, + { + name: "error no resource-rule.resource", + validation: types.Validation{ + Title: "Lula Validation", + LulaVersion: ">= v0.1.0", + Target: types.Target{ + Provider: "opa", + Domain: "kubernetes", + Payload: types.Payload{ + Resources: []types.Resource{ + { + Name: "podsvt", + ResourceRule: types.ResourceRule{ + Group: "core", + Version: "v1", + }, + }, + }, + Rego: "package validate\n\nimport future.keywords.every\n\nvalidate {\n every pod in input.podsvt {\n podLabel := pod.metadata.labels.foo\npodlabel == \"bar\"\n }\n}", + }, + }, + }, + wantErr: true, + }, + { + name: "error resource-rule no version", + validation: types.Validation{ + Title: "Lula Validation", + LulaVersion: ">= v0.1.0", + Target: types.Target{ + Provider: "opa", + Domain: "kubernetes", + Payload: types.Payload{ + Resources: []types.Resource{ + { + Name: "podsvt", + ResourceRule: types.ResourceRule{ + Group: "core", + Resource: "pods", + }, + }, + }, + Rego: "package validate\n\nimport future.keywords.every\n\nvalidate {\n every pod in input.podsvt {\n podLabel := pod.metadata.labels.foo\npodlabel == \"bar\"\n }\n}", + }, + }, + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := LintValidation(tt.validation); (err != nil) != tt.wantErr { + t.Errorf("lintValidation() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +}