Skip to content

Commit 0be753f

Browse files
authored
Merge pull request #2347 from joejstuart/volatile-exceptions
Support image ref and digest
2 parents e0c0db5 + a6d9fb5 commit 0be753f

File tree

10 files changed

+266
-47
lines changed

10 files changed

+266
-47
lines changed

acceptance/go.mod

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ require (
77
github.com/cucumber/godog v0.15.0
88
github.com/cyberphone/json-canonicalization v0.0.0-20231217050601-ba74d44ecf5f
99
github.com/doiit/picocolors v1.0.1
10-
github.com/enterprise-contract/enterprise-contract-controller/api v0.1.71
10+
github.com/enterprise-contract/enterprise-contract-controller/api v0.1.79
1111
github.com/evanphx/json-patch/v5 v5.9.0
1212
github.com/gkampitakis/go-snaps v0.5.7
1313
github.com/go-git/go-billy/v5 v5.6.0

acceptance/go.sum

+2
Original file line numberDiff line numberDiff line change
@@ -305,6 +305,8 @@ github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc
305305
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
306306
github.com/enterprise-contract/enterprise-contract-controller/api v0.1.71 h1:vQg7B+fR885wn2eZmRqNI9VoWDIGUAlgtgEtQiap0mc=
307307
github.com/enterprise-contract/enterprise-contract-controller/api v0.1.71/go.mod h1:zkK1IrRezUgKGK4tkN9hJtpu8NVqjKTMh4EshxXOi3g=
308+
github.com/enterprise-contract/enterprise-contract-controller/api v0.1.79 h1:3w1Yjakmg7bD4/EM+xvALQzSaBNcuL4CLjf0EH/0qys=
309+
github.com/enterprise-contract/enterprise-contract-controller/api v0.1.79/go.mod h1:zkK1IrRezUgKGK4tkN9hJtpu8NVqjKTMh4EshxXOi3g=
308310
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
309311
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
310312
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=

features/__snapshots__/validate_image.snap

+104-1
Original file line numberDiff line numberDiff line change
@@ -880,7 +880,8 @@ Error: success criteria not met
880880
"config": {
881881
"include": [
882882
"@stamps",
883-
"filtering.always_pass"
883+
"filtering.always_pass",
884+
"filtering.always_fail"
884885
]
885886
},
886887
"volatileConfig": {
@@ -909,6 +910,108 @@ Error: success criteria not met
909910

910911
---
911912

913+
[policy rule filtering on imageUrl:stdout - 1]
914+
{
915+
"success": true,
916+
"components": [
917+
{
918+
"name": "Unnamed",
919+
"containerImage": "${REGISTRY}/acceptance/ec-happy-day@sha256:${REGISTRY_acceptance/ec-happy-day:latest_DIGEST}",
920+
"source": {},
921+
"successes": [
922+
{
923+
"msg": "Pass",
924+
"metadata": {
925+
"code": "builtin.attestation.signature_check"
926+
}
927+
},
928+
{
929+
"msg": "Pass",
930+
"metadata": {
931+
"code": "builtin.attestation.syntax_check"
932+
}
933+
},
934+
{
935+
"msg": "Pass",
936+
"metadata": {
937+
"code": "builtin.image.signature_check"
938+
}
939+
},
940+
{
941+
"msg": "Pass",
942+
"metadata": {
943+
"code": "filtering.always_pass"
944+
}
945+
},
946+
{
947+
"msg": "Pass",
948+
"metadata": {
949+
"code": "filtering.always_pass_with_collection"
950+
}
951+
}
952+
],
953+
"success": true,
954+
"signatures": [
955+
{
956+
"keyid": "",
957+
"sig": "${IMAGE_SIGNATURE_acceptance/ec-happy-day}"
958+
}
959+
],
960+
"attestations": [
961+
{
962+
"type": "https://in-toto.io/Statement/v0.1",
963+
"predicateType": "https://slsa.dev/provenance/v0.2",
964+
"predicateBuildType": "https://tekton.dev/attestations/chains/pipelinerun@v2",
965+
"signatures": [
966+
{
967+
"keyid": "",
968+
"sig": "${ATTESTATION_SIGNATURE_acceptance/ec-happy-day}"
969+
}
970+
]
971+
}
972+
]
973+
}
974+
],
975+
"key": "${known_PUBLIC_KEY_JSON}",
976+
"policy": {
977+
"sources": [
978+
{
979+
"policy": [
980+
"git::${GITHOST}/git/happy-day-policy.git?ref=${LATEST_COMMIT}"
981+
],
982+
"config": {
983+
"include": [
984+
"@stamps",
985+
"filtering.always_pass",
986+
"filtering.always_fail"
987+
]
988+
},
989+
"volatileConfig": {
990+
"exclude": [
991+
{
992+
"value": "filtering.always_fail",
993+
"imageUrl": "${REGISTRY}/acceptance/ec-happy-day"
994+
},
995+
{
996+
"value": "filtering.always_fail_with_collection",
997+
"imageUrl": "${REGISTRY}/acceptance/ec-happy-day"
998+
}
999+
]
1000+
}
1001+
}
1002+
],
1003+
"rekorUrl": "${REKOR}",
1004+
"publicKey": "${known_PUBLIC_KEY}"
1005+
},
1006+
"ec-version": "${EC_VERSION}",
1007+
"effective-time": "${TIMESTAMP}"
1008+
}
1009+
---
1010+
1011+
[policy rule filtering on imageUrl:stderr - 1]
1012+
1013+
---
1014+
9121015
[application snapshot reference:stdout - 1]
9131016
{
9141017
"success": true,

features/validate_image.feature

+41-1
Original file line numberDiff line numberDiff line change
@@ -452,7 +452,47 @@ Feature: evaluate enterprise contract
452452
]
453453
},
454454
"config": {
455-
"include": ["@stamps", "filtering.always_pass"]
455+
"include": ["@stamps", "filtering.always_pass", "filtering.always_fail"]
456+
},
457+
"policy": [
458+
"git::https://${GITHOST}/git/happy-day-policy.git"
459+
]
460+
}
461+
]
462+
}
463+
"""
464+
When ec command is run with "validate image --image ${REGISTRY}/acceptance/ec-happy-day --policy acceptance/ec-policy --public-key ${known_PUBLIC_KEY} --rekor-url ${REKOR} --show-successes --output json"
465+
Then the exit status should be 0
466+
Then the output should match the snapshot
467+
468+
Scenario: policy rule filtering on imageUrl
469+
Given a key pair named "known"
470+
Given an image named "acceptance/ec-happy-day"
471+
Given a valid image signature of "acceptance/ec-happy-day" image signed by the "known" key
472+
Given a valid Rekor entry for image signature of "acceptance/ec-happy-day"
473+
Given a valid attestation of "acceptance/ec-happy-day" signed by the "known" key
474+
Given a valid Rekor entry for attestation of "acceptance/ec-happy-day"
475+
Given a git repository named "happy-day-policy" with
476+
| filtering.rego | examples/filtering.rego |
477+
Given policy configuration named "ec-policy" with specification
478+
"""
479+
{
480+
"sources": [
481+
{
482+
"volatileConfig": {
483+
"exclude": [
484+
{
485+
"value": "filtering.always_fail",
486+
"imageUrl": "${REGISTRY}/acceptance/ec-happy-day"
487+
},
488+
{
489+
"value": "filtering.always_fail_with_collection",
490+
"imageUrl": "${REGISTRY}/acceptance/ec-happy-day"
491+
}
492+
]
493+
},
494+
"config": {
495+
"include": ["@stamps", "filtering.always_pass", "filtering.always_fail"]
456496
},
457497
"policy": [
458498
"git::https://${GITHOST}/git/happy-day-policy.git"

go.mod

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ require (
77
github.com/MakeNowJust/heredoc v1.0.0
88
github.com/Maldris/go-billy-afero v0.0.0-20200815120323-e9d3de59c99a
99
github.com/conforma/go-gather v1.0.1
10-
github.com/enterprise-contract/enterprise-contract-controller/api v0.1.71
10+
github.com/enterprise-contract/enterprise-contract-controller/api v0.1.79
1111
github.com/evanphx/json-patch v5.9.0+incompatible
1212
github.com/gkampitakis/go-snaps v0.5.7
1313
github.com/go-git/go-git/v5 v5.13.2

go.sum

+2-2
Original file line numberDiff line numberDiff line change
@@ -547,8 +547,8 @@ github.com/emicklei/proto v1.13.2 h1:z/etSFO3uyXeuEsVPzfl56WNgzcvIr42aQazXaQmFZY
547547
github.com/emicklei/proto v1.13.2/go.mod h1:rn1FgRS/FANiZdD2djyH7TMA9jdRDcYQ9IEN9yvjX0A=
548548
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
549549
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
550-
github.com/enterprise-contract/enterprise-contract-controller/api v0.1.71 h1:vQg7B+fR885wn2eZmRqNI9VoWDIGUAlgtgEtQiap0mc=
551-
github.com/enterprise-contract/enterprise-contract-controller/api v0.1.71/go.mod h1:zkK1IrRezUgKGK4tkN9hJtpu8NVqjKTMh4EshxXOi3g=
550+
github.com/enterprise-contract/enterprise-contract-controller/api v0.1.79 h1:3w1Yjakmg7bD4/EM+xvALQzSaBNcuL4CLjf0EH/0qys=
551+
github.com/enterprise-contract/enterprise-contract-controller/api v0.1.79/go.mod h1:zkK1IrRezUgKGK4tkN9hJtpu8NVqjKTMh4EshxXOi3g=
552552
github.com/enterprise-contract/go-containerregistry v0.20.3-0.20241118083807-8c8ea269355e h1:Rti5Q7fdkzIMO+lRLAFaBSReLpNx4yTdq+u6PRJv1Tw=
553553
github.com/enterprise-contract/go-containerregistry v0.20.3-0.20241118083807-8c8ea269355e/go.mod h1:QTzGuUYojyBy1anZvBPMhwXRE0LGZPIcpmn3K8DHYrw=
554554
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=

internal/evaluation_target/application_snapshot_image/application_snapshot_image.go

+4
Original file line numberDiff line numberDiff line change
@@ -277,6 +277,10 @@ func (a *ApplicationSnapshotImage) ResolveDigest(ctx context.Context) (string, e
277277
return digest, nil
278278
}
279279

280+
func (a *ApplicationSnapshotImage) ImageReference(ctx context.Context) string {
281+
return a.reference.String()
282+
}
283+
280284
type attestationData struct {
281285
Statement json.RawMessage `json:"statement"`
282286
Signatures []signature.EntitySignature `json:"signatures,omitempty"`

internal/evaluator/criteria.go

+65-29
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121
"time"
2222

2323
ecc "github.com/enterprise-contract/enterprise-contract-controller/api/v1alpha1"
24+
"github.com/google/go-containerregistry/pkg/name"
2425
log "github.com/sirupsen/logrus"
2526
)
2627

@@ -63,12 +64,38 @@ func (c *Criteria) addArray(key string, values []string) {
6364
}
6465
}
6566

67+
// This accepts an image ref with digest
68+
// and looks up the image url and digest separately.
6669
func (c *Criteria) get(key string) []string {
70+
ref, err := name.ParseReference(key)
71+
if err != nil {
72+
log.Debugf("error parsing target image url: %q", key)
73+
return c.defaultItems
74+
}
75+
76+
// Collect keys to look up: always the repository name,
77+
// and if available, the digest string.
78+
keys := []string{ref.Context().Name()}
79+
if digestRef, ok := ref.(name.Digest); ok {
80+
keys = append(keys, digestRef.DigestStr())
81+
} else {
82+
log.Debugf("no digest found for reference: %q", ref)
83+
}
84+
85+
var items []string
86+
for _, k := range keys {
87+
items = append(items, c.getWithKey(k)...)
88+
}
89+
90+
// Add any exceptions that pertain to all images.
91+
return append(items, c.defaultItems...)
92+
}
93+
94+
func (c *Criteria) getWithKey(key string) []string {
6795
if items, ok := c.digestItems[key]; ok {
68-
items = append(items, c.defaultItems...)
6996
return items
7097
}
71-
return c.defaultItems
98+
return []string{}
7299
}
73100

74101
func computeIncludeExclude(src ecc.Source, p ConfigProvider) (*Criteria, *Criteria) {
@@ -86,33 +113,8 @@ func computeIncludeExclude(src ecc.Source, p ConfigProvider) (*Criteria, *Criter
86113

87114
vc := src.VolatileConfig
88115
if vc != nil {
89-
at := p.EffectiveTime()
90-
filter := func(items *Criteria, volatileCriteria []ecc.VolatileCriteria) *Criteria {
91-
for _, c := range volatileCriteria {
92-
from, err := time.Parse(time.RFC3339, c.EffectiveOn)
93-
if err != nil {
94-
if c.EffectiveOn != "" {
95-
log.Warnf("unable to parse time for criteria %q, was given %q: %v", c.Value, c.EffectiveOn, err)
96-
}
97-
from = at
98-
}
99-
until, err := time.Parse(time.RFC3339, c.EffectiveUntil)
100-
if err != nil {
101-
if c.EffectiveUntil != "" {
102-
log.Warnf("unable to parse time for criteria %q, was given %q: %v", c.Value, c.EffectiveUntil, err)
103-
}
104-
until = at
105-
}
106-
if until.Compare(at) >= 0 && from.Compare(at) <= 0 {
107-
items.addItem(c.ImageRef, c.Value)
108-
}
109-
}
110-
111-
return items
112-
}
113-
114-
include = filter(include, vc.Include)
115-
exclude = filter(exclude, vc.Exclude)
116+
include = collectVolatileConfigItems(include, vc.Include, p)
117+
exclude = collectVolatileConfigItems(exclude, vc.Exclude, p)
116118
}
117119

118120
if policyConfig := p.Spec().Configuration; include.len() == 0 && exclude.len() == 0 && policyConfig != nil {
@@ -130,3 +132,37 @@ func computeIncludeExclude(src ecc.Source, p ConfigProvider) (*Criteria, *Criter
130132

131133
return include, exclude
132134
}
135+
136+
func collectVolatileConfigItems(items *Criteria, volatileCriteria []ecc.VolatileCriteria, p ConfigProvider) *Criteria {
137+
at := p.EffectiveTime()
138+
for _, c := range volatileCriteria {
139+
from, err := time.Parse(time.RFC3339, c.EffectiveOn)
140+
if err != nil {
141+
if c.EffectiveOn != "" {
142+
log.Warnf("unable to parse time for criteria %q, was given %q: %v", c.Value, c.EffectiveOn, err)
143+
}
144+
from = at
145+
}
146+
until, err := time.Parse(time.RFC3339, c.EffectiveUntil)
147+
if err != nil {
148+
if c.EffectiveUntil != "" {
149+
log.Warnf("unable to parse time for criteria %q, was given %q: %v", c.Value, c.EffectiveUntil, err)
150+
}
151+
until = at
152+
}
153+
if until.Compare(at) >= 0 && from.Compare(at) <= 0 {
154+
// DEPRECATED: use c.ImageDigest instead
155+
if c.ImageRef != "" {
156+
items.addItem(c.ImageRef, c.Value)
157+
} else if c.ImageUrl != "" {
158+
items.addItem(c.ImageUrl, c.Value)
159+
} else if c.ImageDigest != "" {
160+
items.addItem(c.ImageDigest, c.Value)
161+
} else {
162+
items.addItem("", c.Value)
163+
}
164+
}
165+
}
166+
167+
return items
168+
}

internal/evaluator/criteria_test.go

+42-9
Original file line numberDiff line numberDiff line change
@@ -209,17 +209,50 @@ func TestAddArray(t *testing.T) {
209209
func TestGet(t *testing.T) {
210210
c := &Criteria{
211211
digestItems: map[string][]string{
212-
"key1": {"item1", "item2"},
212+
"quay.io/test/ec-test": {"item"},
213+
"sha256:2c5e3b2f1e2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c": {"item-digest"},
213214
},
214215
defaultItems: []string{"default1", "default2"},
215216
}
217+
tests := []struct {
218+
name string
219+
key string
220+
expected []string
221+
}{
222+
{
223+
name: "test with image ref",
224+
key: "quay.io/test/ec-test",
225+
expected: []string{"item", "default1", "default2"},
226+
},
227+
{
228+
name: "test with image ref and tag",
229+
key: "quay.io/test/ec-test:latest",
230+
expected: []string{"item", "default1", "default2"},
231+
},
232+
{
233+
name: "test with image digest",
234+
key: "quay.io/test/ec@sha256:2c5e3b2f1e2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c",
235+
expected: []string{"item-digest", "default1", "default2"},
236+
},
237+
{
238+
name: "test key doesn't exist",
239+
key: "unknown",
240+
expected: []string{"default1", "default2"},
241+
},
242+
{
243+
name: "test with image and bad digest",
244+
key: "quay.io/test/ec-test@sha256:2c5e3b2f1e2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d",
245+
expected: []string{"default1", "default2"},
246+
},
247+
{
248+
name: "test with image not set",
249+
expected: []string{"default1", "default2"},
250+
},
251+
}
216252

217-
// Test getting items for a key that exists
218-
expectedItems := []string{"item1", "item2", "default1", "default2"}
219-
assert.ElementsMatch(t, expectedItems, c.get("key1"))
220-
221-
// Test getting items for a key that does not exist
222-
expectedDefaultItems := []string{"default1", "default2"}
223-
assert.ElementsMatch(t, expectedDefaultItems, c.get("key2"))
224-
253+
for _, tt := range tests {
254+
t.Run(tt.name, func(t *testing.T) {
255+
assert.Equal(t, tt.expected, c.get(tt.key))
256+
})
257+
}
225258
}

0 commit comments

Comments
 (0)