Skip to content

Commit 052fefa

Browse files
authored
feat: allow configuring rego engine (#2314)
Signed-off-by: Miguel Martinez <miguel@chainloop.dev>
1 parent e1f537c commit 052fefa

4 files changed

Lines changed: 105 additions & 52 deletions

File tree

pkg/policies/engine/rego/rego.go

Lines changed: 61 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
//
2-
// Copyright 2024 The Chainloop Authors.
2+
// Copyright 2024-2025 The Chainloop Authors.
33
//
44
// Licensed under the Apache License, Version 2.0 (the "License");
55
// you may not use this file except in compliance with the License.
@@ -27,11 +27,58 @@ import (
2727
"golang.org/x/exp/maps"
2828
)
2929

30-
// Rego policy checker for chainloop attestations and materials
31-
type Rego struct {
32-
// OperatingMode defines the mode of running the policy engine
30+
// Engine policy checker for chainloop attestations and materials
31+
type Engine struct {
32+
// operatingMode defines the mode of running the policy engine
3333
// by restricting or not the operations allowed by the compiler
34-
OperatingMode EnvironmentMode
34+
operatingMode EnvironmentMode
35+
// allowedNetworkDomains is a list of network domains that are allowed for the compiler to access
36+
// when using http.send built-in function
37+
allowedNetworkDomains []string
38+
}
39+
40+
type EngineOption func(*newEngineOptions)
41+
42+
func WithOperatingMode(mode EnvironmentMode) EngineOption {
43+
return func(e *newEngineOptions) {
44+
e.operatingMode = mode
45+
}
46+
}
47+
48+
func WithAllowedNetworkDomains(domains ...string) EngineOption {
49+
return func(e *newEngineOptions) {
50+
e.allowedNetworkDomains = domains
51+
}
52+
}
53+
54+
type newEngineOptions struct {
55+
operatingMode EnvironmentMode
56+
allowedNetworkDomains []string
57+
}
58+
59+
// NewEngine creates a new policy engine with the given options
60+
// default operating mode is EnvironmentModeRestrictive
61+
// default allowed network domains are www.chainloop.dev and www.cisa.gov
62+
// user provided allowed network domains are appended to the base ones
63+
func NewEngine(opts ...EngineOption) *Engine {
64+
options := &newEngineOptions{
65+
operatingMode: EnvironmentModeRestrictive,
66+
}
67+
68+
for _, opt := range opts {
69+
opt(options)
70+
}
71+
72+
var baseAllowedNetworkDomains = []string{
73+
"www.chainloop.dev",
74+
"www.cisa.gov",
75+
}
76+
77+
return &Engine{
78+
operatingMode: options.operatingMode,
79+
// append base allowed network domains to the user provided ones
80+
allowedNetworkDomains: append(baseAllowedNetworkDomains, options.allowedNetworkDomains...),
81+
}
3582
}
3683

3784
// EnvironmentMode defines the mode of running the policy engine
@@ -55,17 +102,10 @@ var builtinFuncNotAllowed = []*ast.Builtin{
55102
ast.Trace,
56103
}
57104

58-
// allowedNetworkDomains is a list of network domains that are allowed for the compiler to access
59-
// when using http.send built-in function
60-
var allowedNetworkDomains = []string{
61-
"www.chainloop.dev",
62-
"www.cisa.gov",
63-
}
64-
65105
// Force interface
66-
var _ engine.PolicyEngine = (*Rego)(nil)
106+
var _ engine.PolicyEngine = (*Engine)(nil)
67107

68-
func (r *Rego) Verify(ctx context.Context, policy *engine.Policy, input []byte, args map[string]any) (*engine.EvaluationResult, error) {
108+
func (r *Engine) Verify(ctx context.Context, policy *engine.Policy, input []byte, args map[string]any) (*engine.EvaluationResult, error) {
69109
policyString := string(policy.Source)
70110
parsedModule, err := ast.ParseModule(policy.Name, policyString)
71111
if err != nil {
@@ -107,23 +147,23 @@ func (r *Rego) Verify(ctx context.Context, policy *engine.Policy, input []byte,
107147
// Function to execute the query with appropriate parameters
108148
executeQuery := func(rule string, strict bool) error {
109149
if strict {
110-
res, err = queryRego(ctx, rule, parsedModule, regoInput, regoFunc, rego.Capabilities(r.OperatingMode.Capabilities()), rego.StrictBuiltinErrors(true))
150+
res, err = queryRego(ctx, rule, parsedModule, regoInput, regoFunc, rego.Capabilities(r.Capabilities()), rego.StrictBuiltinErrors(true))
111151
} else {
112-
res, err = queryRego(ctx, rule, parsedModule, regoInput, regoFunc, rego.Capabilities(r.OperatingMode.Capabilities()))
152+
res, err = queryRego(ctx, rule, parsedModule, regoInput, regoFunc, rego.Capabilities(r.Capabilities()))
113153
}
114154
return err
115155
}
116156

117157
// Try the main rule first
118-
if err := executeQuery(mainRule, r.OperatingMode == EnvironmentModeRestrictive); err != nil {
158+
if err := executeQuery(mainRule, r.operatingMode == EnvironmentModeRestrictive); err != nil {
119159
return nil, err
120160
}
121161

122162
// If res is nil, it means that the rule hasn't been found
123163
// TODO: Remove when this deprecated rule is not used anymore
124164
if res == nil {
125165
// Try with the deprecated main rule
126-
if err := executeQuery(deprecatedRule, r.OperatingMode == EnvironmentModeRestrictive); err != nil {
166+
if err := executeQuery(deprecatedRule, r.operatingMode == EnvironmentModeRestrictive); err != nil {
127167
return nil, err
128168
}
129169

@@ -230,11 +270,11 @@ func queryRego(ctx context.Context, ruleName string, parsedModule *ast.Module, o
230270

231271
// Capabilities returns the capabilities of the environment based on the mode of operation
232272
// defaulting to EnvironmentModeRestrictive if not provided.
233-
func (em EnvironmentMode) Capabilities() *ast.Capabilities {
273+
func (r *Engine) Capabilities() *ast.Capabilities {
234274
capabilities := ast.CapabilitiesForThisVersion()
235275
var enabledBuiltin []*ast.Builtin
236276

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

254294
// Allow specific network domains
255-
capabilities.AllowNet = allowedNetworkDomains
295+
capabilities.AllowNet = r.allowedNetworkDomains
256296

257297
case EnvironmentModePermissive:
258298
enabledBuiltin = capabilities.Builtins

pkg/policies/engine/rego/rego_test.go

Lines changed: 38 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
//
2-
// Copyright 2024 The Chainloop Authors.
2+
// Copyright 2024-2025 The Chainloop Authors.
33
//
44
// Licensed under the Apache License, Version 2.0 (the "License");
55
// you may not use this file except in compliance with the License.
@@ -29,7 +29,7 @@ func TestRego_VerifyWithValidPolicy(t *testing.T) {
2929
regoContent, err := os.ReadFile("testfiles/check_qa.rego")
3030
require.NoError(t, err)
3131

32-
r := &Rego{}
32+
r := NewEngine()
3333
policy := &engine.Policy{
3434
Name: "check approval",
3535
Source: regoContent,
@@ -71,7 +71,7 @@ func TestRego_VerifyWithInputArray(t *testing.T) {
7171
regoContent, err := os.ReadFile("testfiles/arrays.rego")
7272
require.NoError(t, err)
7373

74-
r := &Rego{}
74+
r := NewEngine()
7575
policy := &engine.Policy{
7676
Name: "foobar",
7777
Source: regoContent,
@@ -88,7 +88,7 @@ func TestRego_VerifyWithArguments(t *testing.T) {
8888
regoContent, err := os.ReadFile("testfiles/arguments.rego")
8989
require.NoError(t, err)
9090

91-
r := &Rego{}
91+
r := NewEngine()
9292
policy := &engine.Policy{
9393
Name: "foobar",
9494
Source: regoContent,
@@ -123,7 +123,7 @@ func TestRego_VerifyWithComplexArguments(t *testing.T) {
123123
regoContent, err := os.ReadFile("testfiles/arguments_array.rego")
124124
require.NoError(t, err)
125125

126-
r := &Rego{}
126+
r := NewEngine()
127127
policy := &engine.Policy{
128128
Name: "foobar",
129129
Source: regoContent,
@@ -160,7 +160,7 @@ func TestRego_VerifyInvalidPolicy(t *testing.T) {
160160
regoContent, err := os.ReadFile("testfiles/policy_without_violations.rego")
161161
require.NoError(t, err)
162162

163-
r := &Rego{}
163+
r := NewEngine()
164164
policy := &engine.Policy{
165165
Name: "invalid",
166166
Source: regoContent,
@@ -177,7 +177,7 @@ func TestRego_ResultFormat(t *testing.T) {
177177
regoContent, err := os.ReadFile("testfiles/result_format.rego")
178178
require.NoError(t, err)
179179

180-
r := &Rego{}
180+
r := NewEngine()
181181
policy := &engine.Policy{
182182
Name: "result-output",
183183
Source: regoContent,
@@ -227,7 +227,7 @@ func TestRego_ResultFormatWithoutIgnoreValue(t *testing.T) {
227227
regoContent, err := os.ReadFile("testfiles/result_format_without_ignore.rego")
228228
require.NoError(t, err)
229229

230-
r := &Rego{}
230+
r := NewEngine()
231231
policy := &engine.Policy{
232232
Name: "result-output",
233233
Source: regoContent,
@@ -245,7 +245,7 @@ func TestRego_WithRestrictiveMode(t *testing.T) {
245245
regoContent, err := os.ReadFile("testfiles/restrictive_mode.rego")
246246
require.NoError(t, err)
247247

248-
r := &Rego{}
248+
r := NewEngine()
249249
policy := &engine.Policy{
250250
Name: "policy",
251251
Source: regoContent,
@@ -262,22 +262,22 @@ func TestRego_WithRestrictiveMode(t *testing.T) {
262262
regoContent, err := os.ReadFile("testfiles/restrictive_mode_networking.rego")
263263
require.NoError(t, err)
264264

265-
r := &Rego{}
265+
r := NewEngine()
266266
policy := &engine.Policy{
267267
Name: "policy",
268268
Source: regoContent,
269269
}
270270

271271
_, err = r.Verify(context.TODO(), policy, []byte(`{}`), nil)
272272
assert.Error(t, err)
273-
assert.Contains(t, err.Error(), "eval_builtin_error: http.send: unallowed host: example.com")
273+
assert.Contains(t, err.Error(), "eval_builtin_error: http.send: unallowed host: github.com")
274274
})
275275

276-
t.Run("allowed network requests", func(t *testing.T) {
276+
t.Run("allowed network requests from defaults", func(t *testing.T) {
277277
regoContent, err := os.ReadFile("testfiles/restricted_mode_networking_allowed_host.rego")
278278
require.NoError(t, err)
279279

280-
r := &Rego{}
280+
r := NewEngine()
281281
policy := &engine.Policy{
282282
Name: "policy",
283283
Source: regoContent,
@@ -286,15 +286,37 @@ func TestRego_WithRestrictiveMode(t *testing.T) {
286286
_, err = r.Verify(context.TODO(), policy, []byte(`{}`), nil)
287287
assert.NoError(t, err)
288288
})
289+
290+
t.Run("allowed network requests from defaults plus custom domains", func(t *testing.T) {
291+
defaultHosts, err := os.ReadFile("testfiles/restricted_mode_networking_allowed_host.rego")
292+
require.NoError(t, err)
293+
customHosts, err := os.ReadFile("testfiles/restrictive_mode_networking.rego")
294+
require.NoError(t, err)
295+
296+
r := NewEngine(WithAllowedNetworkDomains("github.com"))
297+
policy := &engine.Policy{
298+
Name: "policy",
299+
Source: defaultHosts,
300+
}
301+
302+
_, err = r.Verify(context.TODO(), policy, []byte(`{}`), nil)
303+
assert.NoError(t, err)
304+
305+
policy = &engine.Policy{
306+
Name: "policy",
307+
Source: customHosts,
308+
}
309+
310+
_, err = r.Verify(context.TODO(), policy, []byte(`{}`), nil)
311+
assert.NoError(t, err)
312+
})
289313
}
290314

291315
func TestRego_WithPermissiveMode(t *testing.T) {
292316
regoContent, err := os.ReadFile("testfiles/permissive_mode.rego")
293317
require.NoError(t, err)
294318

295-
r := &Rego{
296-
OperatingMode: EnvironmentModePermissive,
297-
}
319+
r := NewEngine(WithOperatingMode(EnvironmentModePermissive))
298320
policy := &engine.Policy{
299321
Name: "policy",
300322
Source: regoContent,

pkg/policies/engine/rego/testfiles/restrictive_mode_networking.rego

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ package main
33
import rego.v1
44

55
violations contains msg if {
6-
http.send({"method": "GET", "url": "http://example.com"})
6+
http.send({"method": "GET", "url": "https://github.com"})
77

8-
msg := ""
9-
}
8+
msg := ""
9+
}

pkg/policies/policies.go

Lines changed: 3 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,7 @@ func (pv *PolicyVerifier) evaluatePolicyAttachment(ctx context.Context, attachme
150150
skipped := true
151151
reasons := make([]string, 0)
152152
for _, script := range scripts {
153-
r, err := pv.executeScript(ctx, policy, script, material, args)
153+
r, err := pv.executeScript(ctx, script, material, args)
154154
if err != nil {
155155
return nil, NewPolicyError(err)
156156
}
@@ -299,9 +299,9 @@ func (pv *PolicyVerifier) VerifyStatement(ctx context.Context, statement *intoto
299299
return result, nil
300300
}
301301

302-
func (pv *PolicyVerifier) executeScript(ctx context.Context, policy *v1.Policy, script *engine.Policy, material []byte, args map[string]string) (*engine.EvaluationResult, error) {
302+
func (pv *PolicyVerifier) executeScript(ctx context.Context, script *engine.Policy, material []byte, args map[string]string) (*engine.EvaluationResult, error) {
303303
// verify the policy
304-
ng := getPolicyEngine(policy)
304+
ng := rego.NewEngine()
305305
res, err := ng.Verify(ctx, script, material, getInputArguments(args))
306306
if err != nil {
307307
return nil, fmt.Errorf("failed to execute policy : %w", err)
@@ -546,15 +546,6 @@ func getPolicyTypes(p *v1.Policy) []v1.CraftingSchema_Material_MaterialType {
546546
return policyTypes
547547
}
548548

549-
// getPolicyEngine returns a PolicyEngine implementation to evaluate a given policy.
550-
func getPolicyEngine(_ *v1.Policy) engine.PolicyEngine {
551-
// Currently, only Rego is supported
552-
return &rego.Rego{
553-
// Set the default operating mode to restrictive
554-
OperatingMode: rego.EnvironmentModeRestrictive,
555-
}
556-
}
557-
558549
// LoadPolicyScriptsFromSpec loads all policy script that matches a given material type. It matches if:
559550
// * the policy kind is unspecified, meaning that it was forced by name selector
560551
// * the policy kind is specified, and it's equal to the material type

0 commit comments

Comments
 (0)