pkg/bundle/ruleset/engine/cel/engine.go (68 lines of code) (raw):
// Licensed to Elasticsearch B.V. under one or more contributor
// license agreements. See the NOTICE file distributed with
// this work for additional information regarding copyright
// ownership. Elasticsearch B.V. licenses this file to you under
// the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package cel
import (
"context"
"errors"
"fmt"
"github.com/google/cel-go/cel"
"github.com/google/cel-go/checker/decls"
celext "github.com/google/cel-go/ext"
"google.golang.org/protobuf/proto"
bundlev1 "github.com/elastic/harp/api/gen/go/harp/bundle/v1"
"github.com/elastic/harp/pkg/bundle/ruleset/engine"
"github.com/elastic/harp/pkg/bundle/ruleset/engine/cel/ext"
)
// -----------------------------------------------------------------------------
// New returns a Google CEL based linter engine.
func New(expressions []string) (engine.PackageLinter, error) {
// Prepare CEL Environment
env, err := cel.NewEnv(
cel.Types(&bundlev1.Bundle{}, &bundlev1.Package{}, &bundlev1.SecretChain{}, &bundlev1.KV{}),
ext.Packages(),
ext.Secrets(),
celext.Strings(),
)
if err != nil {
return nil, fmt.Errorf("unable to prepare CEL engine environment: %w", err)
}
// Assemble the complete ruleset
ruleset := make([]cel.Program, 0, len(expressions))
for _, exp := range expressions {
// Parse expression
parsed, issues := env.Parse(exp)
if issues != nil && issues.Err() != nil {
return nil, fmt.Errorf("unable to parse '%s', go error: %w", exp, issues.Err())
}
// Extract AST
ast, cerr := env.Check(parsed)
if cerr != nil && cerr.Err() != nil {
return nil, fmt.Errorf("invalid CEL expression: %w", cerr.Err())
}
// request matching is a boolean operation, so we don't really know
// what to do if the expression returns a non-boolean type
//nolint:staticcheck // TODO: refactor for deprecations
if !proto.Equal(ast.ResultType(), decls.Bool) {
return nil, fmt.Errorf("CEL rule engine expects return type of bool, not %s", ast.ResultType())
}
// Compile the program
p, err := env.Program(ast)
if err != nil {
return nil, fmt.Errorf("error while creating CEL program: %w", err)
}
// Add to context
ruleset = append(ruleset, p)
}
// Return rule engine
return &ruleEngine{
cel: env,
ruleset: ruleset,
}, nil
}
// -----------------------------------------------------------------------------
type ruleEngine struct {
cel *cel.Env
ruleset []cel.Program
}
//nolint:revive // refactor use of ctx
func (re *ruleEngine) EvaluatePackage(ctx context.Context, p *bundlev1.Package) error {
// Check arguments
if p == nil {
return errors.New("unable to evaluate nil package")
}
// Apply evaluation (implicit AND between rules)
for _, exp := range re.ruleset {
// Evaluate using the bundle context
out, _, err := exp.Eval(map[string]interface{}{
"p": p,
})
if err != nil {
return fmt.Errorf("an error occurred during the rule evaluation: %w", err)
}
// Boolean rule returned false
if out.Value() == false {
return engine.ErrRuleNotValid
}
}
// No error
return nil
}