1
0
mirror of synced 2026-10-24 12:59:22 +03:00

Rego: add support for custom error/warning messages when evaluating rego rules (#2577)

* support rego custom messages during evaluation

Signed-off-by: Hector Fernandez <hector@chainguard.dev>

* chore: code polishing

Signed-off-by: Hector Fernandez <hector@chainguard.dev>

* chore: code polishing

Signed-off-by: Hector Fernandez <hector@chainguard.dev>

* update rego tests

Signed-off-by: Hector Fernandez <hector@chainguard.dev>

Signed-off-by: Hector Fernandez <hector@chainguard.dev>
This commit is contained in:
Hector Fernandez
2022-12-28 16:34:39 +01:00
committed by GitHub
parent 7bbfea7bf9
commit b2cea0c68a
4 changed files with 162 additions and 23 deletions

View File

@@ -19,6 +19,7 @@ import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"github.com/open-policy-agent/opa/rego"
@@ -35,6 +36,13 @@ const CosignRegoPackageName = "sigstore"
// CosignEvaluationRule defines the expected evaluation role of a provided rego module
const CosignEvaluationRule = "isCompliant"
// CosignRuleResult defines a expected result object when wrapping the custom messages of the result of our cosign rego rule
type CosignRuleResult struct {
Warning string `json:"warning,omitempty"`
Error string `json:"error,omitempty"`
Result bool `json:"result,omitempty"`
}
func ValidateJSON(jsonBody []byte, entrypoints []string) []error {
ctx := context.Background()
@@ -82,7 +90,7 @@ func ValidateJSON(jsonBody []byte, entrypoints []string) []error {
// ValidateJSONWithModuleInput takes the body of the results to evaluate and the defined module
// in a policy to validate against the input data
func ValidateJSONWithModuleInput(jsonBody []byte, moduleInput string) error {
func ValidateJSONWithModuleInput(jsonBody []byte, moduleInput string) (warnings error, errors error) {
ctx := context.Background()
query := fmt.Sprintf("%s = data.%s.%s", CosignEvaluationRule, CosignRegoPackageName, CosignEvaluationRule)
module := fmt.Sprintf("%s.rego", CosignRegoPackageName)
@@ -93,27 +101,56 @@ func ValidateJSONWithModuleInput(jsonBody []byte, moduleInput string) error {
evalQuery, err := r.PrepareForEval(ctx)
if err != nil {
return err
return nil, err
}
var input interface{}
dec := json.NewDecoder(bytes.NewBuffer(jsonBody))
dec.UseNumber()
if err := dec.Decode(&input); err != nil {
return err
return nil, err
}
rs, err := evalQuery.Eval(ctx, rego.EvalInput(input))
if err != nil {
return err
return nil, err
}
for _, result := range rs {
isCompliant, ok := result.Bindings[CosignEvaluationRule].(bool)
if ok && isCompliant {
return nil
switch response := result.Bindings[CosignEvaluationRule].(type) {
case []interface{}:
return evaluateRegoEvalMapResult(query, response)
case bool:
if response {
return nil, nil
}
}
}
return fmt.Errorf("policy is not compliant for query '%s'", query)
return nil, fmt.Errorf("policy is not compliant for query '%s'", query)
}
func evaluateRegoEvalMapResult(query string, response []interface{}) (warning error, error error) {
error = fmt.Errorf("policy is not compliant for query %q", query)
for _, r := range response {
rMap := r.(map[string]interface{})
mapBytes, err := json.Marshal(rMap)
if err != nil {
return nil, fmt.Errorf("policy is not compliant for query '%s' due to parsing errors: %w", query, err)
}
var resultObject CosignRuleResult
err = json.Unmarshal(mapBytes, &resultObject)
if err != nil {
return nil, fmt.Errorf("policy is not compliant for query '%s' due to parsing errors: %w", query, err)
}
fmt.Printf("resultObject %v\n", resultObject)
// Check if it is complaint
if resultObject.Result {
return fmt.Errorf(resultObject.Warning), nil
}
warning = errors.New(resultObject.Warning)
error = fmt.Errorf("policy is not compliant for query '%s' with errors: %s", query, resultObject.Error)
}
return warning, error
}

View File

@@ -151,6 +151,7 @@ func TestValidateJSONWithModuleInput(t *testing.T) {
policy string
pass bool
errorMsg string
warnMsg string
}{
{
name: "passing policy attestations",
@@ -199,17 +200,43 @@ func TestValidateJSONWithModuleInput(t *testing.T) {
pass: false,
errorMsg: "policy is not compliant for query 'isCompliant = data.sigstore.isCompliant'",
},
{
name: "not passing policy attestations",
jsonBody: attestationsJSONBody,
policy: `
package sigstore
isCompliant[response] {
attestationsKeylessATT := input.authorityMatches.keylessatt.attestations
result = (count(attestationsKeylessATT) == 1000)
errorMsg := "attestationsKeylessATT is not equal to 1000"
warnMsg := ""
response :={
"result" : result,
"error" : errorMsg,
"warning": warnMsg,
}
}
`,
pass: false,
errorMsg: "policy is not compliant for query 'isCompliant = data.sigstore.isCompliant' with errors: attestationsKeylessATT is not equal to 1000",
},
}
for _, tt := range cases {
t.Run(tt.name, func(t *testing.T) {
if err := ValidateJSONWithModuleInput([]byte(tt.jsonBody), tt.policy); (err == nil) != tt.pass {
warn, err := ValidateJSONWithModuleInput([]byte(tt.jsonBody), tt.policy)
if (err == nil) != tt.pass {
t.Fatalf("Unexpected result: %v", err)
} else if err != nil {
if fmt.Sprintf("%s", err) != tt.errorMsg {
t.Errorf("Expected error %q, got %q", tt.errorMsg, err)
}
}
if warn != nil && tt.warnMsg != "" && tt.warnMsg != warn.Error() {
t.Errorf("Expected warning %q, got %q", tt.warnMsg, warn)
}
})
}
}

View File

@@ -32,22 +32,24 @@ import (
// policyType - cue|rego
// policyBody - String representing either cue or rego language
// jsonBytes - Bytes to evaluate against the policyBody in the given language
func EvaluatePolicyAgainstJSON(ctx context.Context, name, policyType string, policyBody string, jsonBytes []byte) error {
func EvaluatePolicyAgainstJSON(ctx context.Context, name, policyType string, policyBody string, jsonBytes []byte) (warnings error, errors error) {
switch policyType {
case "cue":
cueValidationErr := evaluateCue(ctx, jsonBytes, policyBody)
if cueValidationErr != nil {
return cosign.NewVerificationError("failed evaluating cue policy for %s: %v", name, cueValidationErr)
return nil, cosign.NewVerificationError("failed evaluating cue policy for %s: %v", name, cueValidationErr)
}
case "rego":
regoValidationErr := evaluateRego(ctx, jsonBytes, policyBody)
regoValidationWarn, regoValidationErr := evaluateRego(ctx, jsonBytes, policyBody)
if regoValidationErr != nil {
return cosign.NewVerificationError("failed evaluating rego policy for type %s: %s", name, regoValidationErr)
return regoValidationWarn, cosign.NewVerificationError("failed evaluating rego policy for type %s: %s", name, regoValidationErr)
}
// It is possible to return warning messages when the policy is compliant
return regoValidationWarn, regoValidationErr
default:
return fmt.Errorf("sorry Type %s is not supported yet", policyType)
return nil, fmt.Errorf("sorry Type %s is not supported yet", policyType)
}
return nil
return nil, nil
}
// evaluateCue evaluates a cue policy `evaluator` against `attestation`
@@ -72,7 +74,7 @@ func evaluateCue(_ context.Context, attestation []byte, evaluator string) error
}
// evaluateRego evaluates a rego policy `evaluator` against `attestation`
func evaluateRego(_ context.Context, attestation []byte, evaluator string) error {
func evaluateRego(_ context.Context, attestation []byte, evaluator string) (warnings error, errors error) {
log.Printf("Evaluating attestation: %s", string(attestation))
log.Printf("Evaluating evaluator: %s", evaluator)

View File

@@ -82,12 +82,13 @@ func TestEvalPolicy(t *testing.T) {
// TODO(vaikas): Consider moving the attestations/cue files into testdata
// directory.
tests := []struct {
name string
json string
policyType string
policyFile string
wantErr bool
wantErrSub string
name string
json string
policyType string
policyFile string
wantErr bool
wantErrSub string
wantWarnSub string
}{{
name: "custom attestation, mismatched predicateType",
json: customAttestation,
@@ -198,20 +199,92 @@ func TestEvalPolicy(t *testing.T) {
keySignature := input.authorityMatches.keysignature.signatures
count(keySignature) == 1
}`,
}, {
name: "Rego cluster image policy main policy succeed with empty error msg",
json: cipAttestation,
policyType: "rego",
policyFile: `package sigstore
isCompliant[response] {
attestationsKeylessATT := input.authorityMatches.keylessatt.attestations
result = (count(attestationsKeylessATT) == 1)
attestationsKeyATT := input.authorityMatches.keyatt.attestations
result = (count(attestationsKeyATT) == 1)
keySignature := input.authorityMatches.keysignature.signatures
result = (count(keySignature) == 1)
errorMsg = ""
warnMsg = ""
response := {
"result" : result,
"error" : errorMsg,
"warning" : warnMsg
}
}`,
}, {
name: "Rego cluster image policy main policy, fails with custom error msg",
json: cipAttestation,
policyType: "rego",
wantErr: true,
wantErrSub: `Not found expected list of attestations`,
policyFile: `package sigstore
isCompliant[response] {
attestationsKeylessATT := input.authorityMatches.keylessatt.attestations
result = (count(attestationsKeylessATT) == 1000)
errorMsg = "Not found expected list of attestations"
warnMsg = ""
response := {
"result" : result,
"error" : errorMsg,
"warning" : warnMsg
}
}`,
}, {
name: "Rego cluster image policy main policy, returns a custom warning msg",
json: cipAttestation,
policyType: "rego",
wantErr: false,
wantWarnSub: `Throw warning error even if succeeded`,
policyFile: `package sigstore
isCompliant[response] {
attestationsKeylessATT := input.authorityMatches.keylessatt.attestations
result = (count(attestationsKeylessATT) == 1)
attestationsKeyATT := input.authorityMatches.keyatt.attestations
result = (count(attestationsKeyATT) == 1)
keySignature := input.authorityMatches.keysignature.signatures
result = (count(keySignature) == 1)
errorMsg = ""
warnMsg = "Throw warning error even if succeeded"
response := {
"result" : result,
"error" : errorMsg,
"warning" : warnMsg
}
}`,
}}
for _, tc := range tests {
ctx := context.Background()
err := EvaluatePolicyAgainstJSON(ctx, tc.name, tc.policyType, tc.policyFile, []byte(tc.json))
warn, err := EvaluatePolicyAgainstJSON(ctx, tc.name, tc.policyType, tc.policyFile, []byte(tc.json))
if tc.wantErr {
if err == nil {
t.Errorf("Did not get an error, wanted %s", tc.wantErrSub)
} else if !strings.Contains(err.Error(), tc.wantErrSub) {
t.Errorf("Unexpected error, want: %s got: %s", tc.wantErrSub, err.Error())
}
if tc.wantWarnSub != "" && !strings.Contains(warn.Error(), tc.wantWarnSub) {
t.Errorf("Unexpected warning, want: %s got: %s", tc.wantErrSub, err.Error())
}
} else {
if !tc.wantErr && err != nil {
t.Errorf("Unexpected error, wanted none, got: %s", err.Error())
}
if tc.wantWarnSub != "" && !strings.Contains(warn.Error(), tc.wantWarnSub) {
t.Errorf("Unexpected warning, want: %s got: %s", tc.wantWarnSub, warn.Error())
}
}
}
}