Skip to content
This repository was archived by the owner on Feb 16, 2026. It is now read-only.
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
83 changes: 65 additions & 18 deletions internal/darnit/condition/cel.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,15 @@ import (

// CELEvaluator handles evaluation of CEL expressions
type CELEvaluator struct {
env *cel.Env
baseEnv *cel.Env
}

// NewCELEvaluator creates a new CEL evaluator
func NewCELEvaluator() (*CELEvaluator, error) {
// Create a new CEL environment with standard env and findings variable
// Create a new CEL environment with standard env and dynamic variable support
// Use standard library which already includes string functions like contains, startsWith
env, err := cel.NewEnv(
cel.StdLib(), // Include standard library of functions
cel.Variable("findings", cel.MapType(cel.StringType, cel.DynType)),
// Only add custom functions that aren't in the standard library
cel.Function("split",
cel.MemberOverload("string_split_string",
Expand All @@ -44,32 +43,41 @@ func NewCELEvaluator() (*CELEvaluator, error) {
return nil, fmt.Errorf("error creating CEL environment: %w", err)
}

return &CELEvaluator{env: env}, nil
return &CELEvaluator{baseEnv: env}, nil
}

// EvaluateExpression evaluates a CEL expression against data
func (e *CELEvaluator) EvaluateExpression(expression string, data map[string]interface{}) (bool, error) {
func (e *CELEvaluator) EvaluateExpression(expression string, data map[string]any) (bool, error) {
// Create a dynamic environment with variables for all data keys
dynamicEnv, err := e.createDynamicEnv(data)
if err != nil {
return false, fmt.Errorf("error creating dynamic environment: %w", err)
}

// Parse the expression
ast, issues := e.env.Parse(expression)
ast, issues := dynamicEnv.Parse(expression)
if issues != nil && issues.Err() != nil {
return false, fmt.Errorf("error parsing expression: %w", issues.Err())
}

// Type-check the expression
checked, issues := e.env.Check(ast)
checked, issues := dynamicEnv.Check(ast)
if issues != nil && issues.Err() != nil {
return false, fmt.Errorf("error type-checking expression: %w", issues.Err())
}

// Compile the expression
program, err := e.env.Program(checked)
program, err := dynamicEnv.Program(checked)
if err != nil {
return false, fmt.Errorf("error compiling expression: %w", err)
}

// Create the variable map from data
vars := map[string]interface{}{
"findings": data["findings"],
// Create the variable map from data (flat access only)
vars := make(map[string]any)

// Add all fields from data directly for flat access
for key, value := range data {
vars[key] = value
}

// Evaluate the expression
Expand All @@ -86,29 +94,68 @@ func (e *CELEvaluator) EvaluateExpression(expression string, data map[string]int
return result.Value().(bool), nil
}

// createDynamicEnv creates a CEL environment with variables for all data keys
func (e *CELEvaluator) createDynamicEnv(data map[string]any) (*cel.Env, error) {
// Start with base environment options
opts := []cel.EnvOption{cel.StdLib()}

// Add custom functions from base environment
opts = append(opts, cel.Function("split",
cel.MemberOverload("string_split_string",
[]*cel.Type{cel.StringType, cel.StringType},
cel.ListType(cel.StringType),
cel.BinaryBinding(func(lhs, rhs ref.Val) ref.Val {
s1, ok1 := lhs.Value().(string)
s2, ok2 := rhs.Value().(string)
if !ok1 || !ok2 {
return types.NewErr("split: unexpected type")
}
parts := strings.Split(s1, s2)
return types.NewStringList(types.DefaultTypeAdapter, parts)
}),
),
))

// Add variables for all data keys
for key := range data {
opts = append(opts, cel.Variable(key, cel.DynType))
}

return cel.NewEnv(opts...)
}

// EvaluateStringArrayExpression evaluates a CEL expression that returns a string array
func (e *CELEvaluator) EvaluateStringArrayExpression(expression string, data map[string]interface{}) ([]string, error) {
func (e *CELEvaluator) EvaluateStringArrayExpression(expression string, data map[string]any) ([]string, error) {
// Create a dynamic environment with variables for all data keys
dynamicEnv, err := e.createDynamicEnv(data)
if err != nil {
return nil, fmt.Errorf("error creating dynamic environment: %w", err)
}

// Parse the expression
ast, issues := e.env.Parse(expression)
ast, issues := dynamicEnv.Parse(expression)
if issues != nil && issues.Err() != nil {
return nil, fmt.Errorf("error parsing expression: %w", issues.Err())
}

// Type-check the expression
checked, issues := e.env.Check(ast)
checked, issues := dynamicEnv.Check(ast)
if issues != nil && issues.Err() != nil {
return nil, fmt.Errorf("error type-checking expression: %w", issues.Err())
}

// Compile the expression
program, err := e.env.Program(checked)
program, err := dynamicEnv.Program(checked)
if err != nil {
return nil, fmt.Errorf("error compiling expression: %w", err)
}

// Create the variable map from data
vars := map[string]interface{}{
"findings": data["findings"],
// Create the variable map from data (flat access only)
vars := make(map[string]any)

// Add all fields from data directly for flat access
for key, value := range data {
vars[key] = value
}

// Evaluate the expression
Expand Down
129 changes: 54 additions & 75 deletions internal/darnit/condition/cel_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,122 +19,102 @@ func TestCELEvaluator(t *testing.T) {
tests := []struct {
name string
expression string
data map[string]interface{}
data map[string]any
expected bool
wantErr bool
}{
{
name: "simple comparison - true",
expression: "findings.security_policy == 'missing'",
data: map[string]interface{}{
"findings": map[string]interface{}{
"security_policy": "missing",
},
expression: "security_policy == 'missing'",
data: map[string]any{
"security_policy": "missing",
},
expected: true,
wantErr: false,
},
{
name: "simple comparison - false",
expression: "findings.security_policy == 'missing'",
data: map[string]interface{}{
"findings": map[string]interface{}{
"security_policy": "present",
},
expression: "security_policy == 'missing'",
data: map[string]any{
"security_policy": "present",
},
expected: false,
wantErr: false,
},
{
name: "logical AND - true",
expression: "findings.security_policy == 'missing' && findings.mfa_status == 'disabled'",
data: map[string]interface{}{
"findings": map[string]interface{}{
"security_policy": "missing",
"mfa_status": "disabled",
},
expression: "security_policy == 'missing' && mfa_status == 'disabled'",
data: map[string]any{
"security_policy": "missing",
"mfa_status": "disabled",
},
expected: true,
wantErr: false,
},
{
name: "logical AND - false",
expression: "findings.security_policy == 'missing' && findings.mfa_status == 'disabled'",
data: map[string]interface{}{
"findings": map[string]interface{}{
"security_policy": "missing",
"mfa_status": "enabled",
},
expression: "security_policy == 'missing' && mfa_status == 'disabled'",
data: map[string]any{
"security_policy": "missing",
"mfa_status": "enabled",
},
expected: false,
wantErr: false,
},
{
name: "logical OR - true",
expression: "findings.security_policy == 'missing' || findings.mfa_status == 'disabled'",
data: map[string]interface{}{
"findings": map[string]interface{}{
"security_policy": "present",
"mfa_status": "disabled",
},
expression: "security_policy == 'missing' || mfa_status == 'disabled'",
data: map[string]any{
"security_policy": "present",
"mfa_status": "disabled",
},
expected: true,
wantErr: false,
},
{
name: "logical OR - false",
expression: "findings.security_policy == 'missing' || findings.mfa_status == 'disabled'",
data: map[string]interface{}{
"findings": map[string]interface{}{
"security_policy": "present",
"mfa_status": "enabled",
},
expression: "security_policy == 'missing' || mfa_status == 'disabled'",
data: map[string]any{
"security_policy": "present",
"mfa_status": "enabled",
},
expected: false,
wantErr: false,
},
{
name: "complex condition - true",
expression: "findings.security_policy == 'missing' || (findings.mfa_status == 'disabled' && findings.branch_protection == 'partial')",
data: map[string]interface{}{
"findings": map[string]interface{}{
"security_policy": "present",
"mfa_status": "disabled",
"branch_protection": "partial",
},
expression: "security_policy == 'missing' || (mfa_status == 'disabled' && branch_protection == 'partial')",
data: map[string]any{
"security_policy": "present",
"mfa_status": "disabled",
"branch_protection": "partial",
},
expected: true,
wantErr: false,
},
{
name: "invalid expression",
expression: "findings.security_policy = 'missing'", // Invalid syntax (= instead of ==)
data: map[string]interface{}{
"findings": map[string]interface{}{
"security_policy": "missing",
},
expression: "security_policy = 'missing'", // Invalid syntax (= instead of ==)
data: map[string]any{
"security_policy": "missing",
},
expected: false,
wantErr: true,
},
{
name: "non-boolean result",
expression: "findings.security_policy", // Doesn't evaluate to boolean
data: map[string]interface{}{
"findings": map[string]interface{}{
"security_policy": "missing",
},
expression: "security_policy", // Doesn't evaluate to boolean
data: map[string]any{
"security_policy": "missing",
},
expected: false,
wantErr: true,
},
{
name: "missing field",
expression: "findings.nonexistent_field == 'value'",
data: map[string]interface{}{
"findings": map[string]interface{}{
"security_policy": "missing",
},
expression: "nonexistent_field == 'value'",
data: map[string]any{
"security_policy": "missing",
},
expected: false,
wantErr: true,
Expand All @@ -161,33 +141,32 @@ func TestCELEvaluatorWithInvalidData(t *testing.T) {
require.NoError(t, err, "Error creating CEL evaluator")

// Test with nil data
_, err = evaluator.EvaluateExpression("findings.security_policy == 'missing'", nil)
_, err = evaluator.EvaluateExpression("security_policy == 'missing'", nil)
assert.Error(t, err, "Expected error for nil data")

// Test with missing findings key
_, err = evaluator.EvaluateExpression("findings.security_policy == 'missing'", map[string]interface{}{})
assert.Error(t, err, "Expected error for missing findings key")
// Test with missing field
_, err = evaluator.EvaluateExpression("security_policy == 'missing'", map[string]any{})
assert.Error(t, err, "Expected error for missing field")

// Test with findings not being a map
_, err = evaluator.EvaluateExpression("findings.security_policy == 'missing'", map[string]interface{}{
"findings": "not a map",
// Test with empty data
_, err = evaluator.EvaluateExpression("nonexistent_field == 'value'", map[string]any{
"security_policy": "missing",
})
assert.Error(t, err, "Expected error for findings not being a map")
assert.Error(t, err, "Expected error for nonexistent field")
}


func TestEvaluateStringArrayExpression(t *testing.T) {
// Create a new evaluator
evaluator, err := condition.NewCELEvaluator()
require.NoError(t, err, "Error creating CEL evaluator")

// Test data
data := map[string]interface{}{
"findings": map[string]interface{}{
"failed_controls": []string{"OSPS-GV-03.01", "OSPS-LE-02.01"},
"has_failed_control": map[string]interface{}{
"OSPS-GV-03.01": true,
"OSPS-LE-02.01": true,
},
data := map[string]any{
"failed_controls": []string{"OSPS-GV-03.01", "OSPS-LE-02.01"},
"has_failed_control": map[string]any{
"OSPS-GV-03.01": true,
"OSPS-LE-02.01": true,
},
}

Expand All @@ -206,13 +185,13 @@ func TestEvaluateStringArrayExpression(t *testing.T) {
},
{
name: "conditional array with concatenation",
expression: "['base'] + (findings.has_failed_control['OSPS-GV-03.01'] ? ['contrib'] : []) + (findings.has_failed_control['OSPS-LE-02.01'] ? ['license'] : [])",
expression: "['base'] + (has_failed_control['OSPS-GV-03.01'] ? ['contrib'] : []) + (has_failed_control['OSPS-LE-02.01'] ? ['license'] : [])",
expected: []string{"base", "contrib", "license"},
wantErr: false,
},
{
name: "invalid expression",
expression: "findings.invalid.property",
expression: "invalid.property",
expected: nil,
wantErr: true,
},
Expand All @@ -230,7 +209,7 @@ func TestEvaluateStringArrayExpression(t *testing.T) {
},
{
name: "array with mixed conditional",
expression: "findings.has_failed_control['OSPS-GV-03.01'] ? ['control-found'] : ['no-control']",
expression: "has_failed_control['OSPS-GV-03.01'] ? ['control-found'] : ['no-control']",
expected: []string{"control-found"},
wantErr: false,
},
Expand Down
Loading
Loading