Skip to content

Commit 2cc25f3

Browse files
authored
feat(policy-devel): extend eval command to show raw evaluation of messages (#2357)
Signed-off-by: Sylwester Piskozub <[email protected]>
1 parent af1b0d4 commit 2cc25f3

File tree

16 files changed

+612
-229
lines changed

16 files changed

+612
-229
lines changed

app/cli/cmd/output.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,6 @@ type tabulatedData interface {
5555
[]*action.APITokenItem |
5656
*action.AttestationStatusMaterial |
5757
*action.ListMembershipResult |
58-
*action.PolicyEvalResult |
5958
*action.PolicyLintResult
6059
}
6160

app/cli/cmd/policy_develop_eval.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ func newPolicyDevelopEvalCmd() *cobra.Command {
3232
policyPath string
3333
inputs []string
3434
allowedHostnames []string
35+
debug bool
3536
)
3637

3738
cmd := &cobra.Command{
@@ -51,6 +52,7 @@ evaluates the policy against the provided material or attestation.`,
5152
PolicyPath: policyPath,
5253
Inputs: parseKeyValue(inputs),
5354
AllowedHostnames: allowedHostnames,
55+
Debug: debug,
5456
}
5557

5658
policyEval, err := action.NewPolicyEval(opts, actionOpts)
@@ -74,6 +76,7 @@ evaluates the policy against the provided material or attestation.`,
7476
cmd.Flags().StringVarP(&policyPath, "policy", "p", "policy.yaml", "Path to custom policy file")
7577
cmd.Flags().StringArrayVar(&inputs, "input", []string{}, "Key-value pairs of policy inputs (key=value)")
7678
cmd.Flags().StringSliceVar(&allowedHostnames, "allowed-hostnames", []string{}, "Additional hostnames allowed for http.send requests in policies")
79+
cmd.Flags().BoolVarP(&debug, "debug", "", false, "Include detailed evaluation inputs/outputs in JSON output and enable verbose logging")
7780

7881
return cmd
7982
}

app/cli/documentation/cli-reference.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2847,6 +2847,7 @@ Options
28472847
```
28482848
--allowed-hostnames strings Additional hostnames allowed for http.send requests in policies
28492849
--annotation strings Key-value pairs of material annotations (key=value)
2850+
--debug Include detailed evaluation inputs/outputs in JSON output and enable verbose logging
28502851
-h, --help help for eval
28512852
--input stringArray Key-value pairs of policy inputs (key=value)
28522853
--kind string Kind of the material: ["ARTIFACT" "ATTESTATION" "BLACKDUCK_SCA_JSON" "CHAINLOOP_RUNNER_CONTEXT" "CONTAINER_IMAGE" "CSAF_INFORMATIONAL_ADVISORY" "CSAF_SECURITY_ADVISORY" "CSAF_SECURITY_INCIDENT_RESPONSE" "CSAF_VEX" "EVIDENCE" "GHAS_CODE_SCAN" "GHAS_DEPENDENCY_SCAN" "GHAS_SECRET_SCAN" "GITLAB_SECURITY_REPORT" "HELM_CHART" "JACOCO_XML" "JUNIT_XML" "OPENVEX" "SARIF" "SBOM_CYCLONEDX_JSON" "SBOM_SPDX_JSON" "SLSA_PROVENANCE" "STRING" "TWISTCLI_SCAN_JSON" "ZAP_DAST_ZIP"]
@@ -2862,7 +2863,6 @@ Options inherited from parent commands
28622863
-c, --config string Path to an existing config file (default is $HOME/.config/chainloop/config.toml)
28632864
--control-plane string URL for the Control Plane API ($CHAINLOOP_CONTROL_PLANE_API) (default "api.cp.chainloop.dev:443")
28642865
--control-plane-ca string CUSTOM CA file for the Control Plane API (optional) ($CHAINLOOP_CONTROL_PLANE_API_CA)
2865-
--debug Enable debug/verbose logging mode
28662866
-i, --insecure Skip TLS transport during connection to the control plane ($CHAINLOOP_API_INSECURE)
28672867
-n, --org string organization name
28682868
-o, --output string Output format, valid options are json and table (default "table")

app/cli/internal/action/policy_develop_eval.go

Lines changed: 4 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -26,13 +26,7 @@ type PolicyEvalOpts struct {
2626
PolicyPath string
2727
Inputs map[string]string
2828
AllowedHostnames []string
29-
}
30-
31-
type PolicyEvalResult struct {
32-
Violations []string `json:"violations"`
33-
SkipReasons []string `json:"skip_reasons"`
34-
Skipped bool `json:"skipped"`
35-
Ignored bool `json:"ignored,omitempty"`
29+
Debug bool
3630
}
3731

3832
type PolicyEval struct {
@@ -47,14 +41,15 @@ func NewPolicyEval(opts *PolicyEvalOpts, actionOpts *ActionsOpts) (*PolicyEval,
4741
}, nil
4842
}
4943

50-
func (action *PolicyEval) Run() ([]*PolicyEvalResult, error) {
44+
func (action *PolicyEval) Run() (*policydevel.EvalSummary, error) {
5145
evalOpts := &policydevel.EvalOptions{
5246
PolicyPath: action.opts.PolicyPath,
5347
MaterialKind: action.opts.Kind,
5448
Annotations: action.opts.Annotations,
5549
MaterialPath: action.opts.MaterialPath,
5650
Inputs: action.opts.Inputs,
5751
AllowedHostnames: action.opts.AllowedHostnames,
52+
Debug: action.opts.Debug,
5853
}
5954

6055
// Evaluate policy
@@ -63,15 +58,5 @@ func (action *PolicyEval) Run() ([]*PolicyEvalResult, error) {
6358
return nil, err
6459
}
6560

66-
results := make([]*PolicyEvalResult, 0, len(resp))
67-
for _, r := range resp {
68-
results = append(results, &PolicyEvalResult{
69-
Violations: r.Violations,
70-
SkipReasons: r.SkipReasons,
71-
Skipped: r.Skipped,
72-
Ignored: r.Ignored,
73-
})
74-
}
75-
76-
return results, nil
61+
return resp, nil
7762
}

app/cli/internal/policydevel/eval.go

Lines changed: 55 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ package policydevel
1616

1717
import (
1818
"context"
19+
"encoding/json"
1920
"fmt"
2021
"os"
2122

@@ -35,16 +36,26 @@ type EvalOptions struct {
3536
MaterialPath string
3637
Inputs map[string]string
3738
AllowedHostnames []string
39+
Debug bool
3840
}
3941

4042
type EvalResult struct {
41-
Skipped bool
42-
SkipReasons []string
43-
Violations []string
44-
Ignored bool
43+
Violations []string `json:"violations"`
44+
SkipReasons []string `json:"skip_reasons"`
45+
Skipped bool `json:"skipped"`
4546
}
4647

47-
func Evaluate(opts *EvalOptions, logger zerolog.Logger) ([]*EvalResult, error) {
48+
type EvalSummary struct {
49+
Result *EvalResult `json:"result"`
50+
DebugInfo *EvalSummaryDebugInfo `json:"debug_info,omitempty"`
51+
}
52+
53+
type EvalSummaryDebugInfo struct {
54+
Inputs []json.RawMessage `json:"inputs"`
55+
RawResults []json.RawMessage `json:"raw_results"`
56+
}
57+
58+
func Evaluate(opts *EvalOptions, logger zerolog.Logger) (*EvalSummary, error) {
4859
// 1. Create crafting schema
4960
schema, err := createCraftingSchema(opts.PolicyPath, opts.Inputs)
5061
if err != nil {
@@ -59,12 +70,12 @@ func Evaluate(opts *EvalOptions, logger zerolog.Logger) ([]*EvalResult, error) {
5970
material.Annotations = opts.Annotations
6071

6172
// 3. Verify material against policy
62-
result, err := verifyMaterial(schema, material, opts.MaterialPath, opts.AllowedHostnames, &logger)
73+
summary, err := verifyMaterial(schema, material, opts.MaterialPath, opts.Debug, opts.AllowedHostnames, &logger)
6374
if err != nil {
6475
return nil, err
6576
}
6677

67-
return result, nil
78+
return summary, nil
6879
}
6980

7081
func createCraftingSchema(policyPath string, inputs map[string]string) (*v1.CraftingSchema, error) {
@@ -82,41 +93,63 @@ func createCraftingSchema(policyPath string, inputs map[string]string) (*v1.Craf
8293
}, nil
8394
}
8495

85-
func verifyMaterial(schema *v1.CraftingSchema, material *v12.Attestation_Material, materialPath string, allowedHostnames []string, logger *zerolog.Logger) ([]*EvalResult, error) {
96+
func verifyMaterial(schema *v1.CraftingSchema, material *v12.Attestation_Material, materialPath string, debug bool, allowedHostnames []string, logger *zerolog.Logger) (*EvalSummary, error) {
8697
var opts []policies.PolicyVerifierOption
8798
if len(allowedHostnames) > 0 {
8899
opts = append(opts, policies.WithAllowedHostnames(allowedHostnames...))
89100
}
101+
102+
opts = append(opts, policies.WithIncludeRawData(debug))
103+
90104
v := policies.NewPolicyVerifier(schema, nil, logger, opts...)
91105
policyEvs, err := v.VerifyMaterial(context.Background(), material, materialPath)
92106
if err != nil {
93107
return nil, err
94108
}
95109

96-
// no evaluations were returned
97-
if len(policyEvs) == 0 {
110+
if len(policyEvs) == 0 || policyEvs[0] == nil {
98111
return nil, fmt.Errorf("no execution branch matched for kind %s", material.MaterialType.String())
99112
}
100113

101-
results := make([]*EvalResult, 0, len(policyEvs))
102-
for _, policyEv := range policyEvs {
103-
result := &EvalResult{
114+
// Only one evaluation expected for a single policy attachment
115+
policyEv := policyEvs[0]
116+
117+
summary := &EvalSummary{
118+
Result: &EvalResult{
104119
Skipped: policyEv.GetSkipped(),
105120
SkipReasons: policyEv.SkipReasons,
106-
Ignored: false,
107-
}
121+
Violations: make([]string, 0, len(policyEv.Violations)),
122+
},
123+
}
108124

109-
// Collect all violation messages
110-
violations := make([]string, 0, len(policyEv.Violations))
111-
for _, v := range policyEv.Violations {
112-
violations = append(violations, v.Message)
125+
// Collect violation messages
126+
for _, v := range policyEv.Violations {
127+
summary.Result.Violations = append(summary.Result.Violations, v.Message)
128+
}
129+
130+
// Include raw debug info if requested
131+
if debug {
132+
summary.DebugInfo = &EvalSummaryDebugInfo{
133+
Inputs: []json.RawMessage{},
134+
RawResults: []json.RawMessage{},
113135
}
114-
result.Violations = violations
115136

116-
results = append(results, result)
137+
for _, rr := range policyEv.RawResults {
138+
if rr == nil {
139+
continue
140+
}
141+
// Take the first input found, as we only allow one material input
142+
if len(summary.DebugInfo.Inputs) == 0 && rr.Input != nil {
143+
summary.DebugInfo.Inputs = append(summary.DebugInfo.Inputs, json.RawMessage(rr.Input))
144+
}
145+
// Collect all output raw results
146+
if rr.Output != nil {
147+
summary.DebugInfo.RawResults = append(summary.DebugInfo.RawResults, json.RawMessage(rr.Output))
148+
}
149+
}
117150
}
118151

119-
return results, nil
152+
return summary, nil
120153
}
121154

122155
func craftMaterial(materialPath, materialKind string, logger *zerolog.Logger) (*v12.Attestation_Material, error) {

app/cli/internal/policydevel/eval_test.go

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -55,14 +55,14 @@ func TestEvaluate(t *testing.T) {
5555
Annotations: map[string]string{"key": "value"},
5656
}
5757

58-
results, err := Evaluate(opts, logger)
58+
result, err := Evaluate(opts, logger)
5959
require.NoError(t, err)
60-
require.NotEmpty(t, results)
60+
require.NotNil(t, result)
6161

62-
if len(results[0].Violations) == 0 {
62+
if len(result.Result.Violations) == 0 {
6363
t.Log("Policy evaluation passed (no violations)")
6464
} else {
65-
for _, violation := range results[0].Violations {
65+
for _, violation := range result.Result.Violations {
6666
t.Logf("Violation: %s", violation)
6767
}
6868
}
@@ -78,14 +78,14 @@ func TestEvaluate(t *testing.T) {
7878
Annotations: map[string]string{"key": "value"},
7979
}
8080

81-
results, err := Evaluate(opts, logger)
81+
result, err := Evaluate(opts, logger)
8282
require.NoError(t, err)
83-
require.NotEmpty(t, results)
83+
require.NotNil(t, result)
8484

85-
if len(results[0].Violations) == 0 {
85+
if len(result.Result.Violations) == 0 {
8686
t.Log("Policy evaluation passed (no violations)")
8787
} else {
88-
for _, violation := range results[0].Violations {
88+
for _, violation := range result.Result.Violations {
8989
t.Logf("Violation: %s", violation)
9090
}
9191
}

0 commit comments

Comments
 (0)