diff --git a/app/cli/internal/policydevel/eval.go b/app/cli/internal/policydevel/eval.go index 9a36bb4bc..6cf6ad254 100644 --- a/app/cli/internal/policydevel/eval.go +++ b/app/cli/internal/policydevel/eval.go @@ -26,6 +26,7 @@ import ( "github.com/rs/zerolog" "google.golang.org/grpc" "google.golang.org/protobuf/encoding/protojson" + "google.golang.org/protobuf/proto" v12 "github.com/chainloop-dev/chainloop/pkg/attestation/crafter/api/attestation/v1" "github.com/chainloop-dev/chainloop/pkg/attestation/crafter/materials" @@ -48,10 +49,9 @@ type EvalOptions struct { } type EvalResult struct { - Violations []string `json:"violations"` - StructuredViolations []json.RawMessage `json:"structured_violations,omitempty"` - SkipReasons []string `json:"skip_reasons"` - Skipped bool `json:"skipped"` + Violations []json.RawMessage `json:"violations"` + SkipReasons []string `json:"skip_reasons"` + Skipped bool `json:"skipped"` } type EvalSummary struct { @@ -134,29 +134,22 @@ func verifyMaterial(pol *v1.Policies, material *v12.Attestation_Material, materi Result: &EvalResult{ Skipped: policyEv.GetSkipped(), SkipReasons: policyEv.SkipReasons, - Violations: make([]string, 0, len(policyEv.Violations)), + Violations: make([]json.RawMessage, 0, len(policyEv.Violations)), }, } - hasStructuredFindings := false + // Marshal violations using protojson to match the attestation storage format. + // Subject is cleared since it's redundant in eval context (always the policy name). + marshaler := protojson.MarshalOptions{UseProtoNames: true} for _, v := range policyEv.Violations { - summary.Result.Violations = append(summary.Result.Violations, v.Message) - if v.GetFinding() != nil { - hasStructuredFindings = true - } - } + vc := proto.Clone(v).(*v12.PolicyEvaluation_Violation) + vc.Subject = "" - // Include structured violations when any violation has finding data - if hasStructuredFindings { - marshaler := protojson.MarshalOptions{UseProtoNames: true} - summary.Result.StructuredViolations = make([]json.RawMessage, 0, len(policyEv.Violations)) - for _, v := range policyEv.Violations { - b, err := marshaler.Marshal(v) - if err != nil { - return nil, fmt.Errorf("marshaling structured violation: %w", err) - } - summary.Result.StructuredViolations = append(summary.Result.StructuredViolations, b) + b, err := marshaler.Marshal(vc) + if err != nil { + return nil, fmt.Errorf("marshaling violation: %w", err) } + summary.Result.Violations = append(summary.Result.Violations, b) } // Include raw debug info if requested diff --git a/app/cli/internal/policydevel/eval_test.go b/app/cli/internal/policydevel/eval_test.go index 828ca24b8..c69f7a6f7 100644 --- a/app/cli/internal/policydevel/eval_test.go +++ b/app/cli/internal/policydevel/eval_test.go @@ -146,10 +146,10 @@ func TestEvaluateSimplifiedPolicies(t *testing.T) { require.NotNil(t, result) assert.False(t, result.Result.Skipped) assert.Len(t, result.Result.Violations, 1) - assert.Contains(t, result.Result.Violations[0], "at least 2 components") + assert.Contains(t, string(result.Result.Violations[0]), "at least 2 components") }) - t.Run("structured violations populated for policies with finding_type", func(t *testing.T) { + t.Run("violations with finding_type use unified format matching attestation storage", func(t *testing.T) { opts := &EvalOptions{ PolicyPath: "testdata/sbom-structured-vuln-policy.yaml", MaterialPath: sbomPath, @@ -160,24 +160,23 @@ func TestEvaluateSimplifiedPolicies(t *testing.T) { require.NotNil(t, result) assert.False(t, result.Result.Skipped) - // Both fields populated: violations (messages) and structured_violations (proto JSON) + // Single unified violations field with full violation objects (same as attestation) require.Len(t, result.Result.Violations, 1) - assert.Contains(t, result.Result.Violations[0], "Vulnerability found in test-component@1.0.0") - require.Len(t, result.Result.StructuredViolations, 1) - var sv map[string]any - require.NoError(t, json.Unmarshal(result.Result.StructuredViolations[0], &sv)) - assert.Contains(t, sv["message"], "Vulnerability found in test-component@1.0.0") + var v map[string]any + require.NoError(t, json.Unmarshal(result.Result.Violations[0], &v)) + assert.Nil(t, v["subject"], "subject should be excluded from eval output") + assert.Contains(t, v["message"], "Vulnerability found in test-component@1.0.0") - vuln, ok := sv["vulnerability"].(map[string]any) - require.True(t, ok, "expected vulnerability finding in structured violation") + vuln, ok := v["vulnerability"].(map[string]any) + require.True(t, ok, "expected vulnerability finding in violation object") assert.Equal(t, "CVE-2024-1234", vuln["external_id"]) assert.Equal(t, "pkg:generic/test-component@1.0.0", vuln["package_purl"]) assert.Equal(t, "HIGH", vuln["severity"]) assert.InDelta(t, 7.5, vuln["cvss_v3_score"], 0.001) }) - t.Run("no structured violations for plain string policies", func(t *testing.T) { + t.Run("violations without finding_type use same unified format", func(t *testing.T) { opts := &EvalOptions{ PolicyPath: "testdata/sbom-min-components-policy.yaml", MaterialPath: sbomPath, @@ -187,9 +186,12 @@ func TestEvaluateSimplifiedPolicies(t *testing.T) { require.NoError(t, err) require.NotNil(t, result) require.Len(t, result.Result.Violations, 1) - assert.Contains(t, result.Result.Violations[0], "at least 2 components") - // No structured_violations when policy returns plain strings - assert.Empty(t, result.Result.StructuredViolations) + + // Same structure as attestation: object with message (subject excluded in eval) + var v map[string]any + require.NoError(t, json.Unmarshal(result.Result.Violations[0], &v)) + assert.Nil(t, v["subject"], "subject should be excluded from eval output") + assert.Contains(t, v["message"], "at least 2 components") }) t.Run("sbom metadata component policy", func(t *testing.T) { @@ -229,6 +231,6 @@ func TestEvaluateSimplifiedPolicies(t *testing.T) { require.NotNil(t, result) assert.False(t, result.Result.Skipped) assert.Len(t, result.Result.Violations, 1) - assert.Contains(t, result.Result.Violations[0], "too few components") + assert.Contains(t, string(result.Result.Violations[0]), "too few components") }) }