Skip to content
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
82 changes: 61 additions & 21 deletions pkg/policies/engine/rego/rego.go
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -27,11 +27,58 @@ 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
}

// 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
Expand All @@ -55,17 +102,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 {
Expand Down Expand Up @@ -107,23 +147,23 @@ 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
}

// If res is nil, it means that the rule hasn't been found
// 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
}

Expand Down Expand Up @@ -230,11 +270,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 (r *Engine) Capabilities() *ast.Capabilities {
capabilities := ast.CapabilitiesForThisVersion()
var enabledBuiltin []*ast.Builtin

switch em {
switch r.operatingMode {
case EnvironmentModeRestrictive:
// Copy all builtins functions
localBuiltIns := make(map[string]*ast.Builtin, len(ast.BuiltinMap))
Expand All @@ -252,7 +292,7 @@ func (em EnvironmentMode) Capabilities() *ast.Capabilities {
}

// Allow specific network domains
capabilities.AllowNet = allowedNetworkDomains
capabilities.AllowNet = r.allowedNetworkDomains

case EnvironmentModePermissive:
enabledBuiltin = capabilities.Builtins
Expand Down
54 changes: 38 additions & 16 deletions pkg/policies/engine/rego/rego_test.go
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand All @@ -262,22 +262,22 @@ 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,
}

_, 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,
Expand All @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 := ""
}
msg := ""
}
15 changes: 3 additions & 12 deletions pkg/policies/policies.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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 &rego.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
Expand Down
Loading