From 69c24a92bbdbda42dc8e3676710a4129cac82f6e Mon Sep 17 00:00:00 2001 From: fullsend-fix <278716306+fullsend-ai-coder[bot]@users.noreply.github.com> Date: Wed, 8 Jul 2026 14:12:24 +0000 Subject: [PATCH] feat(#3345): improve error messages for Rego compilation failures Intercept rego_type_error, rego_parse_error, and rego_compile_error from OPA/conftest compilation and wrap them with a user-friendly message that includes the bundled OPA version and remediation guidance. Changes: - Add internal/evaluator/rego_errors.go with isRegoCompilationError, bundledOPAVersion, and wrapRegoError helper functions - Apply wrapRegoError in conftest_evaluator.go error paths from TestRunner.Run() and LoadWithData() - Apply wrapRegoError in opa_evaluator.go error path from LoadWithData() - Add unit tests for all new functions covering rego and non-rego error cases, OPA version lookup, and error preservation The original low-level error is preserved via fmt.Errorf %w wrapping so it remains available for debugging. Closes #3345 --- features/__snapshots__/validate_image.snap | 12 +- internal/evaluator/conftest_evaluator.go | 12 +- internal/evaluator/opa_evaluator.go | 2 +- internal/evaluator/rego_errors.go | 97 ++++++++ internal/evaluator/rego_errors_test.go | 253 +++++++++++++++++++++ 5 files changed, 367 insertions(+), 9 deletions(-) create mode 100644 internal/evaluator/rego_errors.go create mode 100644 internal/evaluator/rego_errors_test.go diff --git a/features/__snapshots__/validate_image.snap b/features/__snapshots__/validate_image.snap index 2c19f55d3..ef941cc58 100644 --- a/features/__snapshots__/validate_image.snap +++ b/features/__snapshots__/validate_image.snap @@ -2164,7 +2164,17 @@ Error: success criteria not met --- [TestFeatures/Dropping rego capabilities:stderr - 1] -Error: error validating image ${REGISTRY}/acceptance/ec-happy-day of component Unnamed: load: loading policies: get compiler: 3 errors occurred: +Error: error validating image ${REGISTRY}/acceptance/ec-happy-day of component Unnamed: policy compilation error: the policy references Rego built-in functions not available in this version of Conforma CLI (v1.15.2). + +Upgrade Conforma CLI to a newer version that includes the required functions. + +If upgrading does not help, check for less common causes: + - Conforma CLI disables certain built-in functions for security (opa.runtime, http.send, net.lookup_ip_addr). Policies using these will not compile regardless of the CLI version + - The policy may use Rego syntax requiring a newer OPA version. + Adjust the policy to target OPA v1.15.2 or earlier + +Details: + load: loading policies: get compiler: 3 errors occurred: ${TEMP}/ec-work-${RANDOM}/policy/${RANDOM}/main.rego:14: rego_type_error: undefined function opa.runtime ${TEMP}/ec-work-${RANDOM}/policy/${RANDOM}/main.rego:22: rego_type_error: undefined function http.send ${TEMP}/ec-work-${RANDOM}/policy/${RANDOM}/main.rego:33: rego_type_error: undefined function net.lookup_ip_addr diff --git a/internal/evaluator/conftest_evaluator.go b/internal/evaluator/conftest_evaluator.go index 29a57ffe3..8d7cc633d 100644 --- a/internal/evaluator/conftest_evaluator.go +++ b/internal/evaluator/conftest_evaluator.go @@ -221,6 +221,7 @@ func (r conftestRunner) Run(ctx context.Context, fileList []string) (result []Ou var conftestResult []output.CheckResult conftestResult, err = r.TestRunner.Run(ctx, fileList) if err != nil { + err = wrapRegoError(err) return } @@ -268,6 +269,7 @@ func (r conftestRunner) Run(ctx context.Context, fileList []string) (result []Ou } engine, err = conftest.LoadWithData(r.Policy, r.Data, compilerOptions) if err != nil { + err = wrapRegoError(err) return } @@ -1196,13 +1198,9 @@ func strictCapabilities(ctx context.Context) (string, error) { log.Debug("Network access from rego policies disabled") builtins := make([]*ast.Builtin, 0, len(capabilities.Builtins)) - disallowed := sets.NewString( - // disallow access to environment variables - "opa.runtime", - // disallow external connections. This is a second layer of defense since - // AllowNet should prevent external connections in the first place. - "http.send", "net.lookup_ip_addr", - ) + // disallowedBuiltins is defined in rego_errors.go as the single source of + // truth for security-restricted built-in functions. + disallowed := sets.NewString(disallowedBuiltins...) for _, b := range capabilities.Builtins { if !disallowed.Has(b.Name) { builtins = append(builtins, b) diff --git a/internal/evaluator/opa_evaluator.go b/internal/evaluator/opa_evaluator.go index dbda12661..83f39f0bb 100644 --- a/internal/evaluator/opa_evaluator.go +++ b/internal/evaluator/opa_evaluator.go @@ -94,7 +94,7 @@ func (o *opaEvaluator) compileEngine(ctx context.Context) error { engine, err := conftest.LoadWithData([]string{o.policyDir}, dataDirs, opts) if err != nil { - return fmt.Errorf("load: %w", err) + return wrapRegoError(fmt.Errorf("load: %w", err)) } engine.EnableInterQueryCache() diff --git a/internal/evaluator/rego_errors.go b/internal/evaluator/rego_errors.go new file mode 100644 index 000000000..4890fc36d --- /dev/null +++ b/internal/evaluator/rego_errors.go @@ -0,0 +1,97 @@ +// Copyright The Conforma Contributors +// +// Licensed 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. +// +// SPDX-License-Identifier: Apache-2.0 + +package evaluator + +import ( + "fmt" + dbg "runtime/debug" + "strings" +) + +// opaModulePath is the Go module path used to look up the bundled OPA version +// from build info. +const opaModulePath = "github.com/open-policy-agent/opa" + +// disallowedBuiltins lists the OPA built-in functions that are disabled by +// strictCapabilities for security reasons. This is the single source of truth +// used by both strictCapabilities (conftest_evaluator.go) and wrapRegoError. +var disallowedBuiltins = []string{ + "opa.runtime", + "http.send", + "net.lookup_ip_addr", +} + +// readBuildInfo is a variable to allow overriding in tests. + +var readBuildInfo = dbg.ReadBuildInfo + +// isRegoCompilationError checks whether the error message contains OPA/Rego +// compilation error patterns (rego_type_error, rego_parse_error, +// rego_compile_error) that may indicate a version incompatibility or a +// capability restriction. +func isRegoCompilationError(err error) bool { + if err == nil { + return false + } + msg := err.Error() + return strings.Contains(msg, "rego_type_error") || + strings.Contains(msg, "rego_parse_error") || + strings.Contains(msg, "rego_compile_error") +} + +// bundledOPAVersion returns the version of the OPA module bundled in this +// binary, or "unknown" if it cannot be determined. +func bundledOPAVersion() string { + info, ok := readBuildInfo() + if !ok { + return "unknown" + } + for _, dep := range info.Deps { + if dep.Path == opaModulePath { + return dep.Version + } + } + return "unknown" +} + +// wrapRegoError inspects err for OPA/Rego compilation error patterns +// (rego_type_error, rego_parse_error, rego_compile_error). If found, it wraps +// the error with a user-friendly message that includes the bundled OPA version +// and remediation guidance. Non-matching errors are returned as-is. +func wrapRegoError(err error) error { + if !isRegoCompilationError(err) { + return err + } + + version := bundledOPAVersion() + + versionSuffix := "" + adjustLine := "" + if version != "unknown" { + versionSuffix = " (" + version + ")" + adjustLine = fmt.Sprintf(".\n Adjust the policy to target OPA %s or earlier", version) + } + + return fmt.Errorf("policy compilation error: the policy references Rego built-in "+ + "functions not available in this version of Conforma CLI%s.\n\n"+ + "Upgrade Conforma CLI to a newer version that includes the required functions.\n\n"+ + "If upgrading does not help, check for less common causes:\n"+ + " - Conforma CLI disables certain built-in functions for security "+ + "(%s). Policies using these will not compile regardless of the CLI version\n"+ + " - The policy may use Rego syntax requiring a newer OPA version%s\n\n"+ + "Details:\n %w", versionSuffix, strings.Join(disallowedBuiltins, ", "), adjustLine, err) +} diff --git a/internal/evaluator/rego_errors_test.go b/internal/evaluator/rego_errors_test.go new file mode 100644 index 000000000..38db5f5b8 --- /dev/null +++ b/internal/evaluator/rego_errors_test.go @@ -0,0 +1,253 @@ +// Copyright The Conforma Contributors +// +// Licensed 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. +// +// SPDX-License-Identifier: Apache-2.0 + +//go:build unit + +package evaluator + +import ( + "errors" + "fmt" + dbg "runtime/debug" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestIsRegoCompilationError(t *testing.T) { + tests := []struct { + name string + err error + expected bool + }{ + { + name: "nil error", + err: nil, + expected: false, + }, + { + name: "rego_type_error", + err: errors.New("rego_type_error: undefined function opa.runtime"), + expected: true, + }, + { + name: "rego_parse_error", + err: errors.New("rego_parse_error: unexpected token"), + expected: true, + }, + { + name: "rego_type_error in longer message", + err: fmt.Errorf("3 errors occurred: /tmp/main.rego:14: rego_type_error: undefined function opa.runtime"), + expected: true, + }, + { + name: "rego_compile_error", + err: errors.New("rego_compile_error: some compile error"), + expected: true, + }, + { + name: "non-rego error", + err: errors.New("file not found"), + expected: false, + }, + { + name: "empty error", + err: errors.New(""), + expected: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.expected, isRegoCompilationError(tt.err)) + }) + } +} + +func TestBundledOPAVersion(t *testing.T) { + t.Run("returns version from build info", func(t *testing.T) { + original := readBuildInfo + t.Cleanup(func() { readBuildInfo = original }) + + readBuildInfo = func() (*dbg.BuildInfo, bool) { + return &dbg.BuildInfo{ + Deps: []*dbg.Module{ + {Path: "github.com/open-policy-agent/opa", Version: "v1.15.2"}, + {Path: "github.com/other/dep", Version: "v2.0.0"}, + }, + }, true + } + + assert.Equal(t, "v1.15.2", bundledOPAVersion()) + }) + + t.Run("returns unknown when build info unavailable", func(t *testing.T) { + original := readBuildInfo + t.Cleanup(func() { readBuildInfo = original }) + + readBuildInfo = func() (*dbg.BuildInfo, bool) { + return nil, false + } + + assert.Equal(t, "unknown", bundledOPAVersion()) + }) + + t.Run("returns unknown when OPA not in deps", func(t *testing.T) { + original := readBuildInfo + t.Cleanup(func() { readBuildInfo = original }) + + readBuildInfo = func() (*dbg.BuildInfo, bool) { + return &dbg.BuildInfo{ + Deps: []*dbg.Module{ + {Path: "github.com/other/dep", Version: "v1.0.0"}, + }, + }, true + } + + assert.Equal(t, "unknown", bundledOPAVersion()) + }) +} + +func TestWrapRegoError(t *testing.T) { + original := readBuildInfo + t.Cleanup(func() { readBuildInfo = original }) + + readBuildInfo = func() (*dbg.BuildInfo, bool) { + return &dbg.BuildInfo{ + Deps: []*dbg.Module{ + {Path: "github.com/open-policy-agent/opa", Version: "v1.15.2"}, + }, + }, true + } + + t.Run("wraps rego_type_error with version and guidance", func(t *testing.T) { + origErr := errors.New("rego_type_error: undefined function opa.runtime") + wrapped := wrapRegoError(origErr) + + require.NotEqual(t, origErr, wrapped, "error should be wrapped") + msg := wrapped.Error() + assert.Contains(t, msg, "v1.15.2", "should contain OPA version") + assert.Contains(t, msg, "policy compilation error", "should contain user-friendly prefix") + assert.Contains(t, msg, "Conforma CLI (v1.15.2)", "should show version in context") + assert.Contains(t, msg, "Upgrade Conforma CLI", "should lead with upgrade suggestion") + assert.Contains(t, msg, "less common causes", "should mention less common causes section") + assert.Contains(t, msg, "disables certain built-in functions for security", "should mention security restrictions") + assert.Contains(t, msg, "opa.runtime", "should list restricted built-in functions") + assert.Contains(t, msg, "http.send", "should list restricted built-in functions") + assert.Contains(t, msg, "net.lookup_ip_addr", "should list restricted built-in functions") + assert.Contains(t, msg, "Adjust the policy to target OPA v1.15.2 or earlier", "should contain version-specific adjust suggestion") + assert.Contains(t, msg, "Details:", "should use Details label for original error") + assert.Contains(t, msg, "rego_type_error: undefined function opa.runtime", "should preserve original error text") + + // Verify the original error is preserved via errors.Unwrap + assert.ErrorIs(t, wrapped, origErr, "should preserve original error via wrapping") + }) + + t.Run("wraps rego_parse_error with version and guidance", func(t *testing.T) { + origErr := errors.New("rego_parse_error: unexpected token") + wrapped := wrapRegoError(origErr) + + require.NotEqual(t, origErr, wrapped) + msg := wrapped.Error() + assert.Contains(t, msg, "v1.15.2") + assert.Contains(t, msg, "policy compilation error") + assert.Contains(t, msg, "rego_parse_error: unexpected token") + assert.ErrorIs(t, wrapped, origErr) + }) + + t.Run("passes through non-rego errors unchanged", func(t *testing.T) { + origErr := errors.New("file not found") + result := wrapRegoError(origErr) + + assert.Equal(t, origErr, result, "non-rego errors should pass through unchanged") + }) + + t.Run("passes through nil error", func(t *testing.T) { + assert.Nil(t, wrapRegoError(nil)) + }) + + t.Run("wraps rego_compile_error with version and guidance", func(t *testing.T) { + origErr := errors.New("rego_compile_error: some compile error") + wrapped := wrapRegoError(origErr) + + require.NotEqual(t, origErr, wrapped) + msg := wrapped.Error() + assert.Contains(t, msg, "v1.15.2") + assert.Contains(t, msg, "policy compilation error") + assert.Contains(t, msg, "rego_compile_error: some compile error") + assert.ErrorIs(t, wrapped, origErr) + }) + + t.Run("handles multi-error with rego_type_error", func(t *testing.T) { + origErr := fmt.Errorf("load: loading policies: get compiler: 3 errors occurred: rego_type_error: undefined function opa.runtime") + wrapped := wrapRegoError(origErr) + + msg := wrapped.Error() + assert.Contains(t, msg, "v1.15.2") + assert.Contains(t, msg, "policy compilation error") + assert.Contains(t, msg, "rego_type_error") + assert.ErrorIs(t, wrapped, origErr, "should preserve original error via wrapping") + }) + + t.Run("adjusts message when version is unknown (build info unavailable)", func(t *testing.T) { + original := readBuildInfo + t.Cleanup(func() { readBuildInfo = original }) + + readBuildInfo = func() (*dbg.BuildInfo, bool) { + return nil, false + } + + origErr := errors.New("rego_type_error: undefined function opa.runtime") + wrapped := wrapRegoError(origErr) + + require.NotEqual(t, origErr, wrapped, "error should be wrapped") + msg := wrapped.Error() + assert.Contains(t, msg, "policy compilation error", "should contain user-friendly prefix") + assert.Contains(t, msg, "Upgrade Conforma CLI", "should still suggest upgrading") + assert.Contains(t, msg, "less common causes", "should mention less common causes") + assert.Contains(t, msg, "disables certain built-in functions for security", "should mention security restrictions") + assert.NotContains(t, msg, "(unknown)", "should not show unknown in parentheses") + assert.NotContains(t, msg, "OPA unknown", "should not reference OPA unknown") + assert.NotContains(t, msg, "Adjust the policy", "should omit version-specific suggestion when version is unknown") + assert.Contains(t, msg, "Details:", "should use Details label for original error") + assert.ErrorIs(t, wrapped, origErr, "should preserve original error via wrapping") + }) + + t.Run("adjusts message when version is unknown (OPA not in deps)", func(t *testing.T) { + original := readBuildInfo + t.Cleanup(func() { readBuildInfo = original }) + + readBuildInfo = func() (*dbg.BuildInfo, bool) { + return &dbg.BuildInfo{ + Deps: []*dbg.Module{ + {Path: "github.com/other/dep", Version: "v1.0.0"}, + }, + }, true + } + + origErr := errors.New("rego_parse_error: unexpected token") + wrapped := wrapRegoError(origErr) + + require.NotEqual(t, origErr, wrapped, "error should be wrapped") + msg := wrapped.Error() + assert.Contains(t, msg, "policy compilation error", "should contain user-friendly prefix") + assert.NotContains(t, msg, "(unknown)", "should not show unknown in parentheses") + assert.NotContains(t, msg, "OPA unknown", "should not reference OPA unknown") + assert.NotContains(t, msg, "Adjust the policy", "should omit version-specific suggestion when version is unknown") + assert.ErrorIs(t, wrapped, origErr, "should preserve original error via wrapping") + }) +}