github.com/zntrio/harp/v2@v2.0.9/pkg/bundle/ruleset/engine/cel/engine.go (about) 1 // Licensed to Elasticsearch B.V. under one or more contributor 2 // license agreements. See the NOTICE file distributed with 3 // this work for additional information regarding copyright 4 // ownership. Elasticsearch B.V. licenses this file to you under 5 // the Apache License, Version 2.0 (the "License"); you may 6 // not use this file except in compliance with the License. 7 // You may obtain a copy of the License at 8 // 9 // http://www.apache.org/licenses/LICENSE-2.0 10 // 11 // Unless required by applicable law or agreed to in writing, 12 // software distributed under the License is distributed on an 13 // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 // KIND, either express or implied. See the License for the 15 // specific language governing permissions and limitations 16 // under the License. 17 18 package cel 19 20 import ( 21 "context" 22 "errors" 23 "fmt" 24 25 "github.com/google/cel-go/cel" 26 celext "github.com/google/cel-go/ext" 27 28 bundlev1 "github.com/zntrio/harp/v2/api/gen/go/harp/bundle/v1" 29 "github.com/zntrio/harp/v2/pkg/bundle/ruleset/engine" 30 "github.com/zntrio/harp/v2/pkg/bundle/ruleset/engine/cel/ext" 31 ) 32 33 // ----------------------------------------------------------------------------- 34 35 // New returns a Google CEL based linter engine. 36 func New(expressions []string) (engine.PackageLinter, error) { 37 // Prepare CEL Environment 38 env, err := cel.NewEnv( 39 cel.Types(&bundlev1.Bundle{}, &bundlev1.Package{}, &bundlev1.SecretChain{}, &bundlev1.KV{}), 40 ext.Packages(), 41 ext.Secrets(), 42 celext.Strings(), 43 ) 44 if err != nil { 45 return nil, fmt.Errorf("unable to prepare CEL engine environment: %w", err) 46 } 47 48 // Assemble the complete ruleset 49 ruleset := make([]cel.Program, 0, len(expressions)) 50 for _, exp := range expressions { 51 // Parse expression 52 parsed, issues := env.Parse(exp) 53 if issues != nil && issues.Err() != nil { 54 return nil, fmt.Errorf("unable to parse %q, go error: %w", exp, issues.Err()) 55 } 56 57 // Extract AST 58 ast, cerr := env.Check(parsed) 59 if cerr != nil && cerr.Err() != nil { 60 return nil, fmt.Errorf("invalid CEL expression: %w", cerr.Err()) 61 } 62 63 // request matching is a boolean operation, so we don't really know 64 // what to do if the expression returns a non-boolean type 65 if ast.OutputType() != cel.BoolType { 66 return nil, fmt.Errorf("CEL rule engine expects return type of bool, not %s", ast.OutputType()) 67 } 68 69 // Compile the program 70 p, err := env.Program(ast) 71 if err != nil { 72 return nil, fmt.Errorf("error while creating CEL program: %w", err) 73 } 74 75 // Add to context 76 ruleset = append(ruleset, p) 77 } 78 79 // Return rule engine 80 return &ruleEngine{ 81 cel: env, 82 ruleset: ruleset, 83 }, nil 84 } 85 86 // ----------------------------------------------------------------------------- 87 88 type ruleEngine struct { 89 cel *cel.Env 90 ruleset []cel.Program 91 } 92 93 func (re *ruleEngine) EvaluatePackage(ctx context.Context, p *bundlev1.Package) error { 94 // Check arguments 95 if p == nil { 96 return errors.New("unable to evaluate nil package") 97 } 98 99 // Apply evaluation (implicit AND between rules) 100 for _, exp := range re.ruleset { 101 // Evaluate using the bundle context 102 out, _, err := exp.Eval(map[string]interface{}{ 103 "p": p, 104 }) 105 if err != nil { 106 return fmt.Errorf("an error occurred during the rule evaluation: %w", err) 107 } 108 109 // Boolean rule returned false 110 if out.Value() == false { 111 return engine.ErrRuleNotValid 112 } 113 } 114 115 // No error 116 return nil 117 }