Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 14 additions & 21 deletions app/cli/internal/policydevel/eval.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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 {
Expand Down Expand Up @@ -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
Expand Down
32 changes: 17 additions & 15 deletions app/cli/internal/policydevel/eval_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
Expand All @@ -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) {
Expand Down Expand Up @@ -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")
})
}
Loading