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
21 changes: 19 additions & 2 deletions pkg/policies/engine/rego/boilerplate.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
//
// Copyright 2025 The Chainloop Authors.
// Copyright 2025-2026 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 All @@ -18,6 +18,7 @@ package rego
import (
"bytes"
_ "embed"
"errors"
"fmt"
"strings"
"text/template"
Expand Down Expand Up @@ -48,6 +49,22 @@ type boilerplateData struct {
NeedsDefaultViolations bool
}

// sanitizeParseError reduces a Rego parse error to the code and location of its first
// problem, since OPA parse errors can quote the policy source.
func sanitizeParseError(err error) error {
var astErrs ast.Errors
if !errors.As(err, &astErrs) || len(astErrs) == 0 {
return errors.New("invalid policy")
}

first := astErrs[0]
if first.Location == nil {
return errors.New(first.Code)
}

return fmt.Errorf("%s at line %d, column %d", first.Code, first.Location.Row, first.Location.Col)
}

// InjectBoilerplate automatically injects common policy boilerplate if it doesn't exist.
// This allows users to write simplified policies with only the violations rules.
// Requirements: Policy must have package declaration and import rego.v1
Expand All @@ -65,7 +82,7 @@ func InjectBoilerplate(policySource []byte, policyName string) ([]byte, error) {
// Parse the policy
module, err := ast.ParseModule(policyName, originalPolicy)
if err != nil {
return nil, fmt.Errorf("failed to parse policy (must have 'package' and 'import rego.v1'): %w", err)
return nil, fmt.Errorf("failed to parse policy (must have 'package' and 'import rego.v1'): %w", sanitizeParseError(err))
}

// Detect which rules already exist using AST
Expand Down
33 changes: 32 additions & 1 deletion pkg/policies/engine/rego/boilerplate_test.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
//
// Copyright 2025 The Chainloop Authors.
// Copyright 2025-2026 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 @@ -91,6 +91,37 @@ func TestInjectBoilerplate(t *testing.T) {
}
}

func TestInjectBoilerplateParseErrorOmitsSource(t *testing.T) {
const marker = "unique_marker_value_42"

testCases := []struct {
name string
input string
wantErr string
}{
{
name: "non rego content",
input: "KEY=" + marker + "\n",
wantErr: "line 1, column 1",
},
{
name: "illegal string literal",
input: "package main\n\nimport rego.v1\n\nx := \"" + marker + "\n",
wantErr: "line 5",
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
_, err := InjectBoilerplate([]byte(tc.input), "test-policy")
require.Error(t, err)
assert.Contains(t, err.Error(), "failed to parse policy")
assert.Contains(t, err.Error(), tc.wantErr)
assert.NotContains(t, err.Error(), marker)
})
}
}

// matchesOutput compares result against expected output file
func matchesOutput(t *testing.T, result []byte, outputName string) {
t.Helper()
Expand Down
6 changes: 3 additions & 3 deletions pkg/policies/engine/rego/rego.go
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ func (r *Engine) Verify(ctx context.Context, policy *engine.Policy, input []byte
policyString := string(policy.Source)
parsedModule, err := ast.ParseModule(policy.Name, policyString)
if err != nil {
return nil, fmt.Errorf("failed to parse rego policy: %w", err)
return nil, fmt.Errorf("failed to parse rego policy: %w", sanitizeParseError(err))
}

// Decode input as json
Expand Down Expand Up @@ -386,7 +386,7 @@ func (r *Engine) MatchesParameters(ctx context.Context, policy *engine.Policy, e
policyString := string(policy.Source)
parsedModule, err := ast.ParseModule(policy.Name, policyString)
if err != nil {
return false, fmt.Errorf("failed to parse rego policy: %w", err)
return false, fmt.Errorf("failed to parse rego policy: %w", sanitizeParseError(err))
}

// Create input with policy and expected parameters
Expand Down Expand Up @@ -422,7 +422,7 @@ func (r *Engine) MatchesEvaluation(ctx context.Context, policy *engine.Policy, v
policyString := string(policy.Source)
parsedModule, err := ast.ParseModule(policy.Name, policyString)
if err != nil {
return false, fmt.Errorf("failed to parse rego policy: %w", err)
return false, fmt.Errorf("failed to parse rego policy: %w", sanitizeParseError(err))
}

// Create input expected parameters and policy violations
Expand Down
47 changes: 47 additions & 0 deletions pkg/policies/engine/rego/rego_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,53 @@ import (
"github.com/stretchr/testify/require"
)

func TestRego_ParseErrorOmitsSource(t *testing.T) {
const marker = "unique_marker_value_42"

r := NewEngine()
policy := &engine.Policy{
Name: "invalid",
Source: []byte("package main\n\nx := \"" + marker + "\n"),
}

testCases := []struct {
name string
run func() error
}{
{
name: "verify",
run: func() error {
_, err := r.Verify(context.TODO(), policy, []byte("{}"), nil)
return err
},
},
{
name: "matches parameters",
run: func() error {
_, err := r.MatchesParameters(context.TODO(), policy, nil, nil)
return err
},
},
{
name: "matches evaluation",
run: func() error {
_, err := r.MatchesEvaluation(context.TODO(), policy, nil, nil)
return err
},
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
err := tc.run()
require.Error(t, err)
assert.Contains(t, err.Error(), "failed to parse rego policy")
assert.Contains(t, err.Error(), "line 3")
assert.NotContains(t, err.Error(), marker)
})
}
}

func TestRego_VerifyWithValidPolicy(t *testing.T) {
regoContent, err := os.ReadFile("testfiles/check_qa.rego")
require.NoError(t, err)
Expand Down
Loading
Loading