Skip to content

Commit 16ad43e

Browse files
authored
fix(attestation): fix deduplication of policy evaluations (#3016)
Signed-off-by: Jose I. Paris <jiparis@chainloop.dev>
1 parent baf34b5 commit 16ad43e

2 files changed

Lines changed: 114 additions & 6 deletions

File tree

pkg/attestation/crafter/crafter.go

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,9 @@ import (
1919
"context"
2020
"errors"
2121
"fmt"
22+
"maps"
2223
"net/url"
2324
"os"
24-
"reflect"
2525
"slices"
2626
"strings"
2727
"time"
@@ -752,6 +752,13 @@ func (c *Crafter) addMaterial(ctx context.Context, m *schemaapi.CraftingSchema_M
752752
return mt, nil
753753
}
754754

755+
// policyEvalMatches returns true if two policy evaluations refer to the same policy
756+
// with the same arguments. It treats nil and empty maps as equivalent to handle
757+
// protojson round-trip serialization where empty maps are omitted.
758+
func policyEvalMatches(a, b *api.PolicyEvaluation) bool {
759+
return proto.Equal(a.GetPolicyReference(), b.GetPolicyReference()) && maps.Equal(a.GetWith(), b.GetWith())
760+
}
761+
755762
// EvaluateAttestationPolicies evaluates the attestation-level policies and stores them in the attestation state.
756763
// The phase parameter controls which policies are evaluated based on their attestation_phases spec field.
757764
func (c *Crafter) EvaluateAttestationPolicies(ctx context.Context, attestationID string, statement *intoto.Statement, phase policies.EvalPhase) error {
@@ -783,7 +790,7 @@ func (c *Crafter) EvaluateAttestationPolicies(ctx context.Context, attestationID
783790
for _, ev := range policyEvaluations {
784791
var duplicated bool
785792
for _, existing := range filteredPolicyEvaluations {
786-
if proto.Equal(existing.PolicyReference, ev.PolicyReference) && reflect.DeepEqual(existing.With, ev.With) {
793+
if policyEvalMatches(existing, ev) {
787794
duplicated = true
788795
break
789796
}
@@ -808,7 +815,7 @@ func (c *Crafter) EvaluateAttestationPolicies(ctx context.Context, attestationID
808815
// Check if this attestation-level evaluation was re-evaluated in the current phase
809816
var reEvaluated bool
810817
for _, newEv := range policyEvaluations {
811-
if proto.Equal(newEv.PolicyReference, ev.PolicyReference) && reflect.DeepEqual(newEv.With, ev.With) {
818+
if policyEvalMatches(newEv, ev) {
812819
reEvaluated = true
813820
break
814821
}

pkg/attestation/crafter/crafter_unit_test.go

Lines changed: 104 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
//
2-
// Copyright 2023 The Chainloop Authors.
2+
// Copyright 2023-2026 The Chainloop Authors.
33
//
44
// Licensed under the Apache License, Version 2.0 (the "License");
55
// you may not use this file except in compliance with the License.
@@ -18,15 +18,16 @@ package crafter
1818
import (
1919
"os"
2020
"path/filepath"
21+
"slices"
2122
"testing"
2223
"time"
2324

25+
api "github.com/chainloop-dev/chainloop/pkg/attestation/crafter/api/attestation/v1"
2426
"github.com/go-git/go-git/v5"
27+
"github.com/go-git/go-git/v5/config"
2528
"github.com/go-git/go-git/v5/plumbing/object"
2629
"github.com/stretchr/testify/assert"
2730
"github.com/stretchr/testify/require"
28-
29-
"github.com/go-git/go-git/v5/config"
3031
"github.com/stretchr/testify/suite"
3132
)
3233

@@ -214,6 +215,106 @@ func (s *crafterUnitSuite) TestGitRepoHead() {
214215
}
215216
}
216217

218+
func (s *crafterUnitSuite) TestPolicyEvaluationDedup() {
219+
// Simulate the protojson round-trip issue:
220+
// - Init phase sets With = map[string]string{} (empty map)
221+
// - protojson.Marshal omits empty maps
222+
// - protojson.Unmarshal sets With = nil (absent field)
223+
// - Push phase produces With = map[string]string{} again
224+
// The dedup comparison must treat nil and empty map as equal.
225+
226+
policyRef := &api.PolicyEvaluation_Reference{
227+
Name: "source-commit",
228+
Digest: "sha256:abc123",
229+
}
230+
231+
testCases := []struct {
232+
name string
233+
existing []*api.PolicyEvaluation
234+
newEvals []*api.PolicyEvaluation
235+
wantCount int
236+
description string
237+
}{
238+
{
239+
name: "nil vs empty map With are deduplicated",
240+
existing: []*api.PolicyEvaluation{
241+
{Name: "source-commit", PolicyReference: policyRef, With: nil},
242+
},
243+
newEvals: []*api.PolicyEvaluation{
244+
{Name: "source-commit", PolicyReference: policyRef, With: map[string]string{}},
245+
},
246+
wantCount: 1,
247+
description: "after protojson round-trip, nil With should match empty map With",
248+
},
249+
{
250+
name: "empty map vs empty map With are deduplicated",
251+
existing: []*api.PolicyEvaluation{
252+
{Name: "source-commit", PolicyReference: policyRef, With: map[string]string{}},
253+
},
254+
newEvals: []*api.PolicyEvaluation{
255+
{Name: "source-commit", PolicyReference: policyRef, With: map[string]string{}},
256+
},
257+
wantCount: 1,
258+
description: "identical empty maps should deduplicate",
259+
},
260+
{
261+
name: "nil vs nil With are deduplicated",
262+
existing: []*api.PolicyEvaluation{
263+
{Name: "source-commit", PolicyReference: policyRef, With: nil},
264+
},
265+
newEvals: []*api.PolicyEvaluation{
266+
{Name: "source-commit", PolicyReference: policyRef, With: nil},
267+
},
268+
wantCount: 1,
269+
description: "both nil should deduplicate",
270+
},
271+
{
272+
name: "different With args are not deduplicated",
273+
existing: []*api.PolicyEvaluation{
274+
{Name: "source-commit", PolicyReference: policyRef, With: map[string]string{"key": "val1"}},
275+
},
276+
newEvals: []*api.PolicyEvaluation{
277+
{Name: "source-commit", PolicyReference: policyRef, With: map[string]string{"key": "val2"}},
278+
},
279+
wantCount: 2,
280+
description: "different With values should not deduplicate",
281+
},
282+
{
283+
name: "different policy references are not deduplicated",
284+
existing: []*api.PolicyEvaluation{
285+
{Name: "policy-a", PolicyReference: &api.PolicyEvaluation_Reference{Name: "policy-a"}, With: nil},
286+
},
287+
newEvals: []*api.PolicyEvaluation{
288+
{Name: "policy-b", PolicyReference: &api.PolicyEvaluation_Reference{Name: "policy-b"}, With: nil},
289+
},
290+
wantCount: 2,
291+
description: "different policies should both be kept",
292+
},
293+
}
294+
295+
for _, tc := range testCases {
296+
s.Run(tc.name, func() {
297+
all := slices.Concat(tc.existing, tc.newEvals)
298+
299+
var filtered []*api.PolicyEvaluation
300+
for _, ev := range all {
301+
var duplicated bool
302+
for _, existing := range filtered {
303+
if policyEvalMatches(existing, ev) {
304+
duplicated = true
305+
break
306+
}
307+
}
308+
if !duplicated {
309+
filtered = append(filtered, ev)
310+
}
311+
}
312+
313+
s.Len(filtered, tc.wantCount, tc.description)
314+
})
315+
}
316+
}
317+
217318
func TestSuite(t *testing.T) {
218319
suite.Run(t, new(crafterUnitSuite))
219320
}

0 commit comments

Comments
 (0)