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:
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user