Skip to content

Commit

Permalink
Merge branch 'main' into not-evaluated
Browse files Browse the repository at this point in the history
  • Loading branch information
nacx authored Dec 12, 2024
2 parents d69f1a3 + d3a8690 commit fbb4448
Show file tree
Hide file tree
Showing 18 changed files with 335 additions and 59 deletions.
8 changes: 4 additions & 4 deletions docs/reference/domains/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@

Domains, generically, are the areas or categories from which data is collected to be evaluated by a `Lula Validation`. Currently supported Domains are:

* Kubernetes
* API
* File
* [Kubernetes](kubernetes-domain.md)
* [API](api-domain.md)
* [File](file-domain.md)

The domain block of a `Lula Validation` is given as follows, where the sample is indicating a Kubernetes domain is in use:
```yaml
Expand All @@ -16,4 +16,4 @@ domain:
# ... Rest of Lula Validation
```

Each domain has a particular specification, given by the respective `<domain>-spec` field of the `domain` property of the `Lula Validation`. The sub-pages describe each of these specifications in greater detail.
Each domain has a particular specification, given by the respective `<domain>-spec` field of the `domain` property of the `Lula Validation`. The sub-pages describe each of these specifications in greater detail.
6 changes: 3 additions & 3 deletions docs/reference/providers/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@

Providers are the policy engines which evaluate the input data from the specified domain. Currently supported Providers are:

* OPA (Open Policy Agent)
* Kyverno
* [OPA (Open Policy Agent)](opa-provider.md)
* [Kyverno](kyverno-provider.md)

The provider block of a `Lula Validation` is given as follows, where the sample is indicating the OPA provider is in use:
```yaml
Expand All @@ -15,4 +15,4 @@ provider:
# ... Rest of Lula Validation
```

Each domain specification retreives a specific dataset, and each will return that data to the selected `Provider` in a domain-specific format. However, this data will always take the form of a JSON object when input to a `Provider`. For that reason, it is important that `Domain` and `Provider`specifications are not built wholly independently in a given Validation.
Each domain specification retreives a specific dataset, and each will return that data to the selected `Provider` in a domain-specific format. However, this data will always take the form of a JSON object when input to a `Provider`. For that reason, it is important that `Domain` and `Provider`specifications are not built wholly independently in a given Validation.
51 changes: 51 additions & 0 deletions docs/reference/providers/opa-provider.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,3 +107,54 @@ rego: |

> [!IMPORTANT]
> `package validate` and `validate` are required package and rule for Lula use currently when an output.validation value has not been set.

## Reusing OPA modules

Custom OPA modules can be imported and referenced in the main rego module. The following example shows how to
import a custom module and use it in the main rego module:

Let's say we have a file that is called `lula.rego` with the following contents that verifies that pods have
the label `lula: "true"`:
```yaml
package lula.labels

import rego.v1

has_lula_label(pod) if {
pod.metadata.labels.lula == "true"
}
```

We can import this module and use it in the main rego module as follows:

```yaml
domain:
type: kubernetes
kubernetes-spec:
resources:
- name: podsvt
resource-rule:
group:
version: v1
resource: pods
namespaces: [validation-test]
provider:
type: opa
opa-spec:
modules:
lula.labels: lula.rego
rego: | # Required - Rego policy used for data validation
package validate # Required - Package name
import future.keywords.every # Optional - Any imported keywords
import data.lula.labels as lula_labels # Optional - Import the custom module
validate { # Required - Rule Name for evaluation - "validate" is the only supported rule
every pod in input.podsvt {
lula_labels.has_lula_label(pod) # Use the rules defined in the custom module
}
}
```
> [!Note]
> The `validate.rego` module name is reserved for the main rego policy and cannot be used as a custom module name.
5 changes: 4 additions & 1 deletion src/pkg/common/schemas/validation.json
Original file line number Diff line number Diff line change
Expand Up @@ -523,6 +523,9 @@
"type": "string",
"pattern": ".*\\S\\s\\n.*"
},
"modules": {
"type": "object"
},
"output": {
"type": "object",
"properties": {
Expand Down Expand Up @@ -782,4 +785,4 @@
"provider"
],
"additionalProperties": false
}
}
12 changes: 4 additions & 8 deletions src/pkg/providers/kyverno/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,11 @@ import (
)

type KyvernoProvider struct {
// Context is the context that the Kyverno policy is being evaluated in
Context context.Context `json:"context" yaml:"context"`

// Spec is the specification of the Kyverno policy
Spec *KyvernoSpec `json:"spec,omitempty" yaml:"spec,omitempty"`
}

func CreateKyvernoProvider(ctx context.Context, spec *KyvernoSpec) (types.Provider, error) {
func CreateKyvernoProvider(_ context.Context, spec *KyvernoSpec) (types.Provider, error) {
// Check validity of spec
if spec == nil {
return nil, fmt.Errorf("spec is nil")
Expand All @@ -27,13 +24,12 @@ func CreateKyvernoProvider(ctx context.Context, spec *KyvernoSpec) (types.Provid
}

return KyvernoProvider{
Context: ctx,
Spec: spec,
Spec: spec,
}, nil
}

func (k KyvernoProvider) Evaluate(resources types.DomainResources) (types.Result, error) {
results, err := GetValidatedAssets(k.Context, k.Spec.Policy, resources, k.Spec.Output)
func (k KyvernoProvider) Evaluate(ctx context.Context, resources types.DomainResources) (types.Result, error) {
results, err := GetValidatedAssets(ctx, k.Spec.Policy, resources, k.Spec.Output)
if err != nil {
return types.Result{}, err
}
Expand Down
29 changes: 21 additions & 8 deletions src/pkg/providers/opa/opa.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package opa

import (
"context"
"errors"
"fmt"
"reflect"

Expand All @@ -11,24 +12,36 @@ import (
"github.com/open-policy-agent/opa/rego"
)

var (
ErrCompileRego = errors.New("failed to compile rego policy")
ErrEvaluateRego = errors.New("failed to evaluate rego policy")
)

// mainPolicyModuleName is the name of the OPA module containing the main policy from the spec.rego field.
const mainPolicyModuleName = "validate.rego"

// GetValidatedAssets performs the validation of the dataset against the given rego policy
func GetValidatedAssets(ctx context.Context, regoPolicy string, dataset map[string]interface{}, output *OpaOutput) (types.Result, error) {
func GetValidatedAssets(ctx context.Context, regoPolicy string, regoModules map[string]string, dataset map[string]interface{}, output *OpaOutput) (types.Result, error) {
var matchResult types.Result

if len(dataset) == 0 {
return matchResult, fmt.Errorf("opa validation not performed - no resources to validate")
return matchResult, errors.New("opa validation not performed - no resources to validate")
}

if output == nil {
output = &OpaOutput{}
}

compiler, err := ast.CompileModules(map[string]string{
"validate.rego": regoPolicy,
})
modules := make(map[string]string, len(regoModules)+1)
for k, v := range regoModules {
modules[k] = v
}
modules[mainPolicyModuleName] = regoPolicy

compiler, err := ast.CompileModules(modules)
if err != nil {
message.Debugf("failed to compile rego policy: %s", err.Error())
return matchResult, fmt.Errorf("failed to compile rego policy: %w", err)
return matchResult, fmt.Errorf("%w: %w", ErrCompileRego, err)
}

// Get validation decision
Expand All @@ -45,7 +58,7 @@ func GetValidatedAssets(ctx context.Context, regoPolicy string, dataset map[stri

resultValid, err := regoCalcValid.Eval(ctx)
if err != nil {
return matchResult, fmt.Errorf("failed to evaluate rego policy: %w", err)
return matchResult, fmt.Errorf("%w: %w", ErrEvaluateRego, err)
}
// Checking result length is non-zero: will be zero if validation returns false
if len(resultValid) != 0 {
Expand Down Expand Up @@ -73,7 +86,7 @@ func GetValidatedAssets(ctx context.Context, regoPolicy string, dataset map[stri

resultObv, err := regoCalcObv.Eval(ctx)
if err != nil {
return matchResult, fmt.Errorf("failed to evaluate rego policy: %w", err)
return matchResult, fmt.Errorf("%w: %w", ErrEvaluateRego, err)
}
// To do: check if resultObv is empty - basically some extra error handling if a user defines an output but it's not coming out of the rego
if len(resultObv) != 0 {
Expand Down
97 changes: 97 additions & 0 deletions src/pkg/providers/opa/opa_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
package opa_test

import (
"context"
"errors"
"testing"

"github.com/defenseunicorns/lula/src/pkg/providers/opa"
)

func TestOpaModules(t *testing.T) {
t.Parallel()

tests := []struct {
name string
spec *opa.OpaSpec
wantErr error
wantPassing int
}{
{
name: "no modules",
spec: &opa.OpaSpec{
Rego: "package validate\n\ndefault validate = true",
},
wantPassing: 1,
},
{
name: "empty module",
spec: &opa.OpaSpec{
Rego: "package validate\n\ndefault validate = true",
Modules: map[string]string{"empty": "testdata/empty.rego"},
},
wantErr: opa.ErrCompileRego,
},
{
name: "invalid module",
spec: &opa.OpaSpec{
Rego: "package validate\n\nvalidate = true if true",
Modules: map[string]string{"empty": "testdata/empty.rego"},
},
wantErr: opa.ErrCompileRego,
},
{
name: "invalid module path",
spec: &opa.OpaSpec{
Rego: "package validate\n\ndefault validate = false",
Modules: map[string]string{"test.module": "invalid-path"},
},
wantErr: opa.ErrDownloadModule,
},
{
name: "reserved module validation",
spec: &opa.OpaSpec{
Rego: "package validate\n\nimport data.lula.labels as lula_labels\n\nvalidate { lula_labels.has_lula_label(input.pod) }",
Modules: map[string]string{"lula.labels": "testdata/lula.rego", "validate.rego": "not-used"},
},
wantErr: opa.ErrReservedModuleName,
},
{
name: "module validation",
spec: &opa.OpaSpec{
Rego: "package validate\n\nimport data.lula.labels as lula_labels\n\nvalidate { lula_labels.has_lula_label(input.pod) }",
Modules: map[string]string{"lula.labels": "testdata/lula.rego"},
},
wantPassing: 1,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ctx := context.Background()
provider, err := opa.CreateOpaProvider(ctx, tt.spec)
if err != nil {
t.Errorf("CreateOpaProvider() error: %v", err)
}

result, err := provider.Evaluate(ctx, dummyPod)
if !errors.Is(err, tt.wantErr) {
t.Errorf("Evaluate() error = %v, wantErr %v", err, tt.wantErr)
}

if result.Passing != tt.wantPassing {
t.Errorf("Passing = %d, want %d", result.Passing, tt.wantPassing)
}
})
}
}

var dummyPod = map[string]interface{}{
"pod": map[string]interface{}{
"metadata": map[string]interface{}{
"labels": map[string]string{
"lula": "true",
},
},
},
}
Empty file.
1 change: 1 addition & 0 deletions src/pkg/providers/opa/testdata/invalid.rego
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
][][][]
7 changes: 7 additions & 0 deletions src/pkg/providers/opa/testdata/lula.rego
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package lula.labels

import rego.v1

has_lula_label(pod) if {
pod.metadata.labels.lula == "true"
}
Loading

0 comments on commit fbb4448

Please sign in to comment.