Skip to content

Commit

Permalink
feat(policies): support new policy output format (chainloop-dev#1400)
Browse files Browse the repository at this point in the history
Signed-off-by: Jose I. Paris <[email protected]>
  • Loading branch information
jiparis authored Oct 16, 2024
1 parent 2129a62 commit 92f6519
Show file tree
Hide file tree
Showing 16 changed files with 574 additions and 227 deletions.
36 changes: 36 additions & 0 deletions app/controlplane/api/gen/frontend/attestation/v1/crafting_state.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

202 changes: 112 additions & 90 deletions pkg/attestation/crafter/api/attestation/v1/crafting_state.pb.go

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,12 @@ message PolicyEvaluation {
// material type, if any, of the evaluated policy
workflowcontract.v1.CraftingSchema.Material.MaterialType type = 8;

// whether this evaluation was skipped or not (because of an invalid input, for example)
bool skipped = 13;

// Evaluation messages, intended to communicate evaluation errors (invalid input)
repeated string skip_reasons = 14;

message Violation {
string subject = 1 [(buf.validate.field).required = true];
string message = 2 [(buf.validate.field).required = true];
Expand Down
2 changes: 1 addition & 1 deletion pkg/attestation/crafter/crafter.go
Original file line number Diff line number Diff line change
Expand Up @@ -569,7 +569,7 @@ func (c *Crafter) addMaterial(ctx context.Context, m *schemaapi.CraftingSchema_M
c.CraftingState.Attestation.PolicyEvaluations = append(c.CraftingState.Attestation.PolicyEvaluations, policyResults...)

// log policy violations
policies.LogPolicyViolations(c.CraftingState.Attestation.PolicyEvaluations, c.Logger)
policies.LogPolicyEvaluations(c.CraftingState.Attestation.PolicyEvaluations, c.Logger)

// 5 - Attach it to state
if c.CraftingState.Attestation.Materials == nil {
Expand Down
6 changes: 5 additions & 1 deletion pkg/attestation/renderer/chainloop/v02.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ type PolicyEvaluation struct {
Violations []*PolicyViolation `json:"violations,omitempty"`
With map[string]string `json:"with,omitempty"`
Type string `json:"type"`
Skipped bool `json:"skipped"`
SkipReasons []string `json:"skip_reasons,omitempty"`
}

type PolicyViolation struct {
Expand Down Expand Up @@ -118,7 +120,7 @@ func (r *RendererV02) Statement(ctx context.Context) (*intoto.Statement, error)
}
evaluations = append(evaluations, policyResults...)
// log policy violations
policies.LogPolicyViolations(evaluations, r.logger)
policies.LogPolicyEvaluations(evaluations, r.logger)

// insert attestation level policy results into statement
if err = addPolicyResults(statement, evaluations); err != nil {
Expand Down Expand Up @@ -313,6 +315,8 @@ func renderEvaluation(ev *v1.PolicyEvaluation) *PolicyEvaluation {
"sha256": ev.ReferenceDigest,
},
},
SkipReasons: ev.SkipReasons,
Skipped: ev.Skipped,
}
}

Expand Down
19 changes: 17 additions & 2 deletions pkg/policies/engine/engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,22 +17,37 @@ package engine

import (
"context"
"fmt"
)

type PolicyEngine interface {
// Verify verifies an input against a policy
Verify(ctx context.Context, policy *Policy, input []byte, args map[string]any) ([]*PolicyViolation, error)
Verify(ctx context.Context, policy *Policy, input []byte, args map[string]any) (*EvaluationResult, error)
}

type EvaluationResult struct {
Violations []*PolicyViolation
Skipped bool
SkipReason string
}

// PolicyViolation represents a policy failure
type PolicyViolation struct {
Subject, Violation string
}

// Policy represents a loaded policy in any of the supported technologies.
// Policy represents a loaded policy in any of the supported engines.
type Policy struct {
// the source code for this policy
Source []byte `json:"module"`
// The unique policy name
Name string `json:"name"`
}

type ResultFormatError struct {
Field string
}

func (e ResultFormatError) Error() string {
return fmt.Sprintf("Policy result format error: %s not found or wrong format", e.Field)
}
70 changes: 62 additions & 8 deletions pkg/policies/engine/rego/rego.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,8 @@ const (
// EnvironmentModePermissive allows all operations on the compiler
EnvironmentModePermissive EnvironmentMode = 1
inputArgs = "args"
mainRule = "violations"
deprecatedMainRule = "deny"
deprecatedRule = "violations"
mainRule = "result"
)

// builtinFuncNotAllowed is a list of builtin functions that are not allowed in the compiler
Expand All @@ -64,7 +64,7 @@ var allowedNetworkDomains = []string{
// Force interface
var _ engine.PolicyEngine = (*Rego)(nil)

func (r *Rego) Verify(ctx context.Context, policy *engine.Policy, input []byte, args map[string]any) ([]*engine.PolicyViolation, error) {
func (r *Rego) Verify(ctx context.Context, policy *engine.Policy, input []byte, args map[string]any) (*engine.EvaluationResult, error) {
policyString := string(policy.Source)
parsedModule, err := ast.ParseModule(policy.Name, policyString)
if err != nil {
Expand Down Expand Up @@ -112,29 +112,38 @@ func (r *Rego) Verify(ctx context.Context, policy *engine.Policy, input []byte,
}

// If res is nil, it means that the rule hasn't been found
// TODO: Remove when this deprecated rule is not used anymore
if res == nil {
// Try with the deprecated main rule
if err := executeQuery(deprecatedMainRule, r.OperatingMode == EnvironmentModeRestrictive); err != nil {
if err := executeQuery(deprecatedRule, r.OperatingMode == EnvironmentModeRestrictive); err != nil {
return nil, err
}

if res == nil {
return nil, fmt.Errorf("failed to evaluate policy: no '%s' nor '%s' rule found", mainRule, deprecatedMainRule)
return nil, fmt.Errorf("failed to evaluate policy: neither '%s' nor '%s' rule found", mainRule, deprecatedRule)
}

return parseViolationsRule(res, policy)
}

return parseResultRule(res, policy)
}

// Parse deprecated list of violations.
// TODO: Remove this path once `result` rule is consolidated
func parseViolationsRule(res rego.ResultSet, policy *engine.Policy) (*engine.EvaluationResult, error) {
violations := make([]*engine.PolicyViolation, 0)
for _, exp := range res {
for _, val := range exp.Expressions {
ruleResults, ok := val.Value.([]interface{})
if !ok {
return nil, fmt.Errorf("failed to evaluate policy expression evaluation result: %s", val.Text)
return nil, engine.ResultFormatError{Field: deprecatedRule}
}

for _, result := range ruleResults {
reasonStr, ok := result.(string)
if !ok {
return nil, fmt.Errorf("failed to evaluate rule result: %s", val.Text)
return nil, engine.ResultFormatError{Field: deprecatedRule}
}

violations = append(violations, &engine.PolicyViolation{
Expand All @@ -145,7 +154,52 @@ func (r *Rego) Verify(ctx context.Context, policy *engine.Policy, input []byte,
}
}

return violations, nil
return &engine.EvaluationResult{
Violations: violations,
Skipped: false, // best effort
SkipReason: "",
}, nil
}

// parse `result` rule
func parseResultRule(res rego.ResultSet, policy *engine.Policy) (*engine.EvaluationResult, error) {
result := &engine.EvaluationResult{Violations: make([]*engine.PolicyViolation, 0)}
for _, exp := range res {
for _, val := range exp.Expressions {
ruleResult, ok := val.Value.(map[string]any)
if !ok {
return nil, engine.ResultFormatError{Field: mainRule}
}

skipped, ok := ruleResult["skipped"].(bool)
if !ok {
return nil, engine.ResultFormatError{Field: "skipped"}
}

reason, ok := ruleResult["skip_reason"].(string)
if !ok {
return nil, engine.ResultFormatError{Field: "skip_reason"}
}

violations, ok := ruleResult["violations"].([]any)
if !ok {
return nil, engine.ResultFormatError{Field: "violations"}
}

result.Skipped = skipped
result.SkipReason = reason

for _, violation := range violations {
vs, ok := violation.(string)
if !ok {
return nil, fmt.Errorf("failed to evaluate violation in policy evaluation result: %s", val.Text)
}
result.Violations = append(result.Violations, &engine.PolicyViolation{Subject: policy.Name, Violation: vs})
}
}
}

return result, nil
}

func queryRego(ctx context.Context, ruleName string, parsedModule *ast.Module, options ...func(r *rego.Rego)) (rego.ResultSet, error) {
Expand Down
Loading

0 comments on commit 92f6519

Please sign in to comment.