From f4a6a11e63182625915f5429a4747d8b805f5761 Mon Sep 17 00:00:00 2001 From: Miguel Martinez Date: Wed, 6 Aug 2025 16:23:11 +0100 Subject: [PATCH 1/3] feat: allow configuring rego engine Signed-off-by: Miguel Martinez --- pkg/policies/engine/rego/rego.go | 88 ++++++++++++++----- pkg/policies/engine/rego/rego_test.go | 54 ++++++++---- .../restrictive_mode_networking.rego | 6 +- pkg/policies/policies.go | 15 +--- 4 files changed, 111 insertions(+), 52 deletions(-) diff --git a/pkg/policies/engine/rego/rego.go b/pkg/policies/engine/rego/rego.go index 7845b229f..1c62405be 100644 --- a/pkg/policies/engine/rego/rego.go +++ b/pkg/policies/engine/rego/rego.go @@ -1,5 +1,5 @@ // -// Copyright 2024 The Chainloop Authors. +// Copyright 2024-2025 The Chainloop Authors. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -27,11 +27,64 @@ import ( "golang.org/x/exp/maps" ) -// Rego policy checker for chainloop attestations and materials -type Rego struct { - // OperatingMode defines the mode of running the policy engine +// Engine policy checker for chainloop attestations and materials +type Engine struct { + // operatingMode defines the mode of running the policy engine // by restricting or not the operations allowed by the compiler - OperatingMode EnvironmentMode + operatingMode EnvironmentMode + // allowedNetworkDomains is a list of network domains that are allowed for the compiler to access + // when using http.send built-in function + allowedNetworkDomains []string +} + +type EngineOption func(*newEngineOptions) + +func WithOperatingMode(mode EnvironmentMode) EngineOption { + return func(e *newEngineOptions) { + e.operatingMode = mode + } +} + +func WithAllowedNetworkDomains(domains ...string) EngineOption { + return func(e *newEngineOptions) { + e.allowedNetworkDomains = domains + } +} + +type newEngineOptions struct { + operatingMode EnvironmentMode + allowedNetworkDomains []string +} + +func WithBaseAllowedNetworkDomains(domains ...string) EngineOption { + return func(e *newEngineOptions) { + e.allowedNetworkDomains = domains + } +} + +// NewEngine creates a new policy engine with the given options +// default operating mode is EnvironmentModeRestrictive +// default allowed network domains are www.chainloop.dev and www.cisa.gov +// user provided allowed network domains are appended to the base ones +func NewEngine(opts ...EngineOption) *Engine { + options := &newEngineOptions{ + operatingMode: EnvironmentModeRestrictive, + } + + for _, opt := range opts { + opt(options) + } + + var baseAllowedNetworkDomains = []string{ + "www.chainloop.dev", + "www.cisa.gov", + } + + return &Engine{ + operatingMode: options.operatingMode, + // append base allowed network domains to the user provided ones + allowedNetworkDomains: append(baseAllowedNetworkDomains, options.allowedNetworkDomains...), + } } // EnvironmentMode defines the mode of running the policy engine @@ -55,17 +108,10 @@ var builtinFuncNotAllowed = []*ast.Builtin{ ast.Trace, } -// allowedNetworkDomains is a list of network domains that are allowed for the compiler to access -// when using http.send built-in function -var allowedNetworkDomains = []string{ - "www.chainloop.dev", - "www.cisa.gov", -} - // Force interface -var _ engine.PolicyEngine = (*Rego)(nil) +var _ engine.PolicyEngine = (*Engine)(nil) -func (r *Rego) Verify(ctx context.Context, policy *engine.Policy, input []byte, args map[string]any) (*engine.EvaluationResult, error) { +func (r *Engine) Verify(ctx context.Context, policy *engine.Policy, input []byte, args map[string]any) (*engine.EvaluationResult, error) { policyString := string(policy.Source) parsedModule, err := ast.ParseModule(policy.Name, policyString) if err != nil { @@ -107,15 +153,15 @@ func (r *Rego) Verify(ctx context.Context, policy *engine.Policy, input []byte, // Function to execute the query with appropriate parameters executeQuery := func(rule string, strict bool) error { if strict { - res, err = queryRego(ctx, rule, parsedModule, regoInput, regoFunc, rego.Capabilities(r.OperatingMode.Capabilities()), rego.StrictBuiltinErrors(true)) + res, err = queryRego(ctx, rule, parsedModule, regoInput, regoFunc, rego.Capabilities(r.Capabilities()), rego.StrictBuiltinErrors(true)) } else { - res, err = queryRego(ctx, rule, parsedModule, regoInput, regoFunc, rego.Capabilities(r.OperatingMode.Capabilities())) + res, err = queryRego(ctx, rule, parsedModule, regoInput, regoFunc, rego.Capabilities(r.Capabilities())) } return err } // Try the main rule first - if err := executeQuery(mainRule, r.OperatingMode == EnvironmentModeRestrictive); err != nil { + if err := executeQuery(mainRule, r.operatingMode == EnvironmentModeRestrictive); err != nil { return nil, err } @@ -123,7 +169,7 @@ func (r *Rego) Verify(ctx context.Context, policy *engine.Policy, input []byte, // TODO: Remove when this deprecated rule is not used anymore if res == nil { // Try with the deprecated main rule - if err := executeQuery(deprecatedRule, r.OperatingMode == EnvironmentModeRestrictive); err != nil { + if err := executeQuery(deprecatedRule, r.operatingMode == EnvironmentModeRestrictive); err != nil { return nil, err } @@ -230,11 +276,11 @@ func queryRego(ctx context.Context, ruleName string, parsedModule *ast.Module, o // Capabilities returns the capabilities of the environment based on the mode of operation // defaulting to EnvironmentModeRestrictive if not provided. -func (em EnvironmentMode) Capabilities() *ast.Capabilities { +func (e *Engine) Capabilities() *ast.Capabilities { capabilities := ast.CapabilitiesForThisVersion() var enabledBuiltin []*ast.Builtin - switch em { + switch e.operatingMode { case EnvironmentModeRestrictive: // Copy all builtins functions localBuiltIns := make(map[string]*ast.Builtin, len(ast.BuiltinMap)) @@ -252,7 +298,7 @@ func (em EnvironmentMode) Capabilities() *ast.Capabilities { } // Allow specific network domains - capabilities.AllowNet = allowedNetworkDomains + capabilities.AllowNet = e.allowedNetworkDomains case EnvironmentModePermissive: enabledBuiltin = capabilities.Builtins diff --git a/pkg/policies/engine/rego/rego_test.go b/pkg/policies/engine/rego/rego_test.go index 948db9037..048681937 100644 --- a/pkg/policies/engine/rego/rego_test.go +++ b/pkg/policies/engine/rego/rego_test.go @@ -1,5 +1,5 @@ // -// Copyright 2024 The Chainloop Authors. +// Copyright 2024-2025 The Chainloop Authors. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -29,7 +29,7 @@ func TestRego_VerifyWithValidPolicy(t *testing.T) { regoContent, err := os.ReadFile("testfiles/check_qa.rego") require.NoError(t, err) - r := &Rego{} + r := NewEngine() policy := &engine.Policy{ Name: "check approval", Source: regoContent, @@ -71,7 +71,7 @@ func TestRego_VerifyWithInputArray(t *testing.T) { regoContent, err := os.ReadFile("testfiles/arrays.rego") require.NoError(t, err) - r := &Rego{} + r := NewEngine() policy := &engine.Policy{ Name: "foobar", Source: regoContent, @@ -88,7 +88,7 @@ func TestRego_VerifyWithArguments(t *testing.T) { regoContent, err := os.ReadFile("testfiles/arguments.rego") require.NoError(t, err) - r := &Rego{} + r := NewEngine() policy := &engine.Policy{ Name: "foobar", Source: regoContent, @@ -123,7 +123,7 @@ func TestRego_VerifyWithComplexArguments(t *testing.T) { regoContent, err := os.ReadFile("testfiles/arguments_array.rego") require.NoError(t, err) - r := &Rego{} + r := NewEngine() policy := &engine.Policy{ Name: "foobar", Source: regoContent, @@ -160,7 +160,7 @@ func TestRego_VerifyInvalidPolicy(t *testing.T) { regoContent, err := os.ReadFile("testfiles/policy_without_violations.rego") require.NoError(t, err) - r := &Rego{} + r := NewEngine() policy := &engine.Policy{ Name: "invalid", Source: regoContent, @@ -177,7 +177,7 @@ func TestRego_ResultFormat(t *testing.T) { regoContent, err := os.ReadFile("testfiles/result_format.rego") require.NoError(t, err) - r := &Rego{} + r := NewEngine() policy := &engine.Policy{ Name: "result-output", Source: regoContent, @@ -227,7 +227,7 @@ func TestRego_ResultFormatWithoutIgnoreValue(t *testing.T) { regoContent, err := os.ReadFile("testfiles/result_format_without_ignore.rego") require.NoError(t, err) - r := &Rego{} + r := NewEngine() policy := &engine.Policy{ Name: "result-output", Source: regoContent, @@ -245,7 +245,7 @@ func TestRego_WithRestrictiveMode(t *testing.T) { regoContent, err := os.ReadFile("testfiles/restrictive_mode.rego") require.NoError(t, err) - r := &Rego{} + r := NewEngine() policy := &engine.Policy{ Name: "policy", Source: regoContent, @@ -262,7 +262,7 @@ func TestRego_WithRestrictiveMode(t *testing.T) { regoContent, err := os.ReadFile("testfiles/restrictive_mode_networking.rego") require.NoError(t, err) - r := &Rego{} + r := NewEngine() policy := &engine.Policy{ Name: "policy", Source: regoContent, @@ -270,14 +270,14 @@ func TestRego_WithRestrictiveMode(t *testing.T) { _, err = r.Verify(context.TODO(), policy, []byte(`{}`), nil) assert.Error(t, err) - assert.Contains(t, err.Error(), "eval_builtin_error: http.send: unallowed host: example.com") + assert.Contains(t, err.Error(), "eval_builtin_error: http.send: unallowed host: github.com") }) - t.Run("allowed network requests", func(t *testing.T) { + t.Run("allowed network requests from defaults", func(t *testing.T) { regoContent, err := os.ReadFile("testfiles/restricted_mode_networking_allowed_host.rego") require.NoError(t, err) - r := &Rego{} + r := NewEngine() policy := &engine.Policy{ Name: "policy", Source: regoContent, @@ -286,15 +286,37 @@ func TestRego_WithRestrictiveMode(t *testing.T) { _, err = r.Verify(context.TODO(), policy, []byte(`{}`), nil) assert.NoError(t, err) }) + + t.Run("allowed network requests from defaults plus custom domains", func(t *testing.T) { + defaultHosts, err := os.ReadFile("testfiles/restricted_mode_networking_allowed_host.rego") + require.NoError(t, err) + customHosts, err := os.ReadFile("testfiles/restrictive_mode_networking.rego") + require.NoError(t, err) + + r := NewEngine(WithAllowedNetworkDomains("github.com")) + policy := &engine.Policy{ + Name: "policy", + Source: defaultHosts, + } + + _, err = r.Verify(context.TODO(), policy, []byte(`{}`), nil) + assert.NoError(t, err) + + policy = &engine.Policy{ + Name: "policy", + Source: customHosts, + } + + _, err = r.Verify(context.TODO(), policy, []byte(`{}`), nil) + assert.NoError(t, err) + }) } func TestRego_WithPermissiveMode(t *testing.T) { regoContent, err := os.ReadFile("testfiles/permissive_mode.rego") require.NoError(t, err) - r := &Rego{ - OperatingMode: EnvironmentModePermissive, - } + r := NewEngine(WithOperatingMode(EnvironmentModePermissive)) policy := &engine.Policy{ Name: "policy", Source: regoContent, diff --git a/pkg/policies/engine/rego/testfiles/restrictive_mode_networking.rego b/pkg/policies/engine/rego/testfiles/restrictive_mode_networking.rego index 656adbb74..516c5a02c 100644 --- a/pkg/policies/engine/rego/testfiles/restrictive_mode_networking.rego +++ b/pkg/policies/engine/rego/testfiles/restrictive_mode_networking.rego @@ -3,7 +3,7 @@ package main import rego.v1 violations contains msg if { - http.send({"method": "GET", "url": "http://example.com"}) + http.send({"method": "GET", "url": "https://github.com"}) - msg := "" -} \ No newline at end of file + msg := "" +} diff --git a/pkg/policies/policies.go b/pkg/policies/policies.go index 393f75749..ee8510fcf 100644 --- a/pkg/policies/policies.go +++ b/pkg/policies/policies.go @@ -150,7 +150,7 @@ func (pv *PolicyVerifier) evaluatePolicyAttachment(ctx context.Context, attachme skipped := true reasons := make([]string, 0) for _, script := range scripts { - r, err := pv.executeScript(ctx, policy, script, material, args) + r, err := pv.executeScript(ctx, script, material, args) if err != nil { return nil, NewPolicyError(err) } @@ -299,9 +299,9 @@ func (pv *PolicyVerifier) VerifyStatement(ctx context.Context, statement *intoto return result, nil } -func (pv *PolicyVerifier) executeScript(ctx context.Context, policy *v1.Policy, script *engine.Policy, material []byte, args map[string]string) (*engine.EvaluationResult, error) { +func (pv *PolicyVerifier) executeScript(ctx context.Context, script *engine.Policy, material []byte, args map[string]string) (*engine.EvaluationResult, error) { // verify the policy - ng := getPolicyEngine(policy) + ng := rego.NewEngine() res, err := ng.Verify(ctx, script, material, getInputArguments(args)) if err != nil { return nil, fmt.Errorf("failed to execute policy : %w", err) @@ -546,15 +546,6 @@ func getPolicyTypes(p *v1.Policy) []v1.CraftingSchema_Material_MaterialType { return policyTypes } -// getPolicyEngine returns a PolicyEngine implementation to evaluate a given policy. -func getPolicyEngine(_ *v1.Policy) engine.PolicyEngine { - // Currently, only Rego is supported - return ®o.Rego{ - // Set the default operating mode to restrictive - OperatingMode: rego.EnvironmentModeRestrictive, - } -} - // LoadPolicyScriptsFromSpec loads all policy script that matches a given material type. It matches if: // * the policy kind is unspecified, meaning that it was forced by name selector // * the policy kind is specified, and it's equal to the material type From 1fb1659446328453f9f9116fc92433f48f189d01 Mon Sep 17 00:00:00 2001 From: Miguel Martinez Date: Wed, 6 Aug 2025 16:31:20 +0100 Subject: [PATCH 2/3] feat: allow configuring rego engine Signed-off-by: Miguel Martinez --- pkg/policies/engine/rego/rego.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/policies/engine/rego/rego.go b/pkg/policies/engine/rego/rego.go index 1c62405be..507d6e483 100644 --- a/pkg/policies/engine/rego/rego.go +++ b/pkg/policies/engine/rego/rego.go @@ -276,11 +276,11 @@ func queryRego(ctx context.Context, ruleName string, parsedModule *ast.Module, o // Capabilities returns the capabilities of the environment based on the mode of operation // defaulting to EnvironmentModeRestrictive if not provided. -func (e *Engine) Capabilities() *ast.Capabilities { +func (r *Engine) Capabilities() *ast.Capabilities { capabilities := ast.CapabilitiesForThisVersion() var enabledBuiltin []*ast.Builtin - switch e.operatingMode { + switch r.operatingMode { case EnvironmentModeRestrictive: // Copy all builtins functions localBuiltIns := make(map[string]*ast.Builtin, len(ast.BuiltinMap)) @@ -298,7 +298,7 @@ func (e *Engine) Capabilities() *ast.Capabilities { } // Allow specific network domains - capabilities.AllowNet = e.allowedNetworkDomains + capabilities.AllowNet = r.allowedNetworkDomains case EnvironmentModePermissive: enabledBuiltin = capabilities.Builtins From d9253ab83383a3a9ad2dd3bbd675f230cc53dacd Mon Sep 17 00:00:00 2001 From: Miguel Martinez Date: Thu, 7 Aug 2025 15:27:56 +0200 Subject: [PATCH 3/3] feat: update from CLI Signed-off-by: Miguel Martinez --- pkg/policies/engine/rego/rego.go | 6 ------ 1 file changed, 6 deletions(-) diff --git a/pkg/policies/engine/rego/rego.go b/pkg/policies/engine/rego/rego.go index 507d6e483..25806eb45 100644 --- a/pkg/policies/engine/rego/rego.go +++ b/pkg/policies/engine/rego/rego.go @@ -56,12 +56,6 @@ type newEngineOptions struct { allowedNetworkDomains []string } -func WithBaseAllowedNetworkDomains(domains ...string) EngineOption { - return func(e *newEngineOptions) { - e.allowedNetworkDomains = domains - } -} - // NewEngine creates a new policy engine with the given options // default operating mode is EnvironmentModeRestrictive // default allowed network domains are www.chainloop.dev and www.cisa.gov