Skip to content

Commit 0b64eb9

Browse files
committed
test: add volatile config warnings schema contract test
Add acceptance test scenario that validates the schema contract between ec-cli and ec-policies for volatile config warnings. This test: 1. Creates a policy with volatileConfig.include rules containing: - Rule with no expiration (triggers no_expiration warning) - Rule with effectiveUntil in future (triggers expiring_rule warning) - Rule with effectiveOn in future (triggers pending_rule warning) - Rule scoped to component name (tests componentNames field) 2. Uses a Rego policy (package main) that reads from: - input.policy_spec.sources[_].volatileConfig.include[_] - input.component_name - All VolatileCriteria fields (effectiveOn, effectiveUntil, componentNames, etc.) 3. If either the CLI or ec-policies changes the schema in a breaking way, this test will fail, providing early detection of contract violations. Fixed: Use package main and proper result structure with collections/effective_on. Refs: https://issues.redhat.com/browse/EC-1615
1 parent 05e8bf8 commit 0b64eb9

2 files changed

Lines changed: 176 additions & 0 deletions

File tree

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
# Policy to validate volatile config schema contract between ec-cli and ec-policies.
2+
# This serves as an integration test to ensure the input schema is correctly populated.
3+
package main
4+
5+
import rego.v1
6+
7+
# METADATA
8+
# title: Volatile config rule pending activation
9+
# description: Warns when a volatile config rule has effectiveOn in the future
10+
# custom:
11+
# short_name: pending_rule
12+
# failure_msg: "Volatile include rule '%s' is pending activation (effective on: %s)"
13+
# solution: Informational warning about pending volatile config rule
14+
# collections:
15+
# - minimal
16+
warn contains result if {
17+
some source in object.get(input, ["policy_spec", "sources"], [])
18+
volatile_config := object.get(source, "volatileConfig", {})
19+
some config in object.get(volatile_config, "include", [])
20+
_is_applicable(config)
21+
effective_on := object.get(config, "effectiveOn", "")
22+
effective_on != ""
23+
_is_future_date(effective_on)
24+
result := {
25+
"code": "main.pending_rule",
26+
"collections": ["minimal"],
27+
"effective_on": "2022-01-01T00:00:00Z",
28+
"msg": sprintf("Volatile include rule '%s' is pending activation (effective on: %s)", [config.value, effective_on]),
29+
}
30+
}
31+
32+
# METADATA
33+
# title: Volatile config rule expiring soon
34+
# description: Warns when a volatile config rule will expire within threshold
35+
# custom:
36+
# short_name: expiring_rule
37+
# failure_msg: "Volatile include rule '%s' expires soon (effective until: %s)"
38+
# solution: Review the volatile configuration rule and decide whether to extend it
39+
# collections:
40+
# - minimal
41+
warn contains result if {
42+
some source in object.get(input, ["policy_spec", "sources"], [])
43+
volatile_config := object.get(source, "volatileConfig", {})
44+
some config in object.get(volatile_config, "include", [])
45+
_is_applicable(config)
46+
effective_until := object.get(config, "effectiveUntil", "")
47+
effective_until != ""
48+
not _is_past_date(effective_until)
49+
result := {
50+
"code": "main.expiring_rule",
51+
"collections": ["minimal"],
52+
"effective_on": "2022-01-01T00:00:00Z",
53+
"msg": sprintf("Volatile include rule '%s' expires soon (effective until: %s)", [config.value, effective_until]),
54+
}
55+
}
56+
57+
# METADATA
58+
# title: Volatile config rule has no expiration
59+
# description: Warns when a volatile config rule has no effectiveUntil date
60+
# custom:
61+
# short_name: no_expiration
62+
# failure_msg: "Volatile include rule '%s' has no expiration date set"
63+
# solution: Consider adding an effectiveUntil date
64+
# collections:
65+
# - minimal
66+
warn contains result if {
67+
some source in object.get(input, ["policy_spec", "sources"], [])
68+
volatile_config := object.get(source, "volatileConfig", {})
69+
some config in object.get(volatile_config, "include", [])
70+
_is_applicable(config)
71+
object.get(config, "effectiveUntil", "") == ""
72+
effective_on := object.get(config, "effectiveOn", "")
73+
not _is_future_date(effective_on)
74+
result := {
75+
"code": "main.no_expiration",
76+
"collections": ["minimal"],
77+
"effective_on": "2022-01-01T00:00:00Z",
78+
"msg": sprintf("Volatile include rule '%s' has no expiration date set", [config.value]),
79+
}
80+
}
81+
82+
# Check if rule is applicable to current image (global or matching)
83+
_is_applicable(config) if {
84+
# Global rule - no image/component constraints
85+
object.get(config, "imageRef", "") == ""
86+
object.get(config, "imageUrl", "") == ""
87+
object.get(config, "imageDigest", "") == ""
88+
count(object.get(config, "componentNames", [])) == 0
89+
}
90+
91+
_is_applicable(config) if {
92+
# Match by component name
93+
component_names := object.get(config, "componentNames", [])
94+
count(component_names) > 0
95+
input_component := object.get(input, "component_name", "")
96+
some name in component_names
97+
name == input_component
98+
}
99+
100+
_is_applicable(config) if {
101+
# Match by imageDigest
102+
image_digest := object.get(config, "imageDigest", "")
103+
image_digest != ""
104+
_extract_digest(object.get(input, ["image", "ref"], "")) == image_digest
105+
}
106+
107+
# Helper to extract digest from image ref
108+
_extract_digest(ref) := digest if {
109+
contains(ref, "@")
110+
parts := split(ref, "@")
111+
count(parts) == 2
112+
digest := parts[1]
113+
}
114+
115+
_extract_digest(ref) := "" if {
116+
not contains(ref, "@")
117+
}
118+
119+
# Helper to check if date is in the future
120+
_is_future_date(date_str) if {
121+
date_str != ""
122+
date_ns := time.parse_rfc3339_ns(date_str)
123+
now_ns := time.now_ns()
124+
date_ns > now_ns
125+
}
126+
127+
# Helper to check if date is in the past
128+
_is_past_date(date_str) if {
129+
date_str != ""
130+
date_ns := time.parse_rfc3339_ns(date_str)
131+
now_ns := time.now_ns()
132+
date_ns < now_ns
133+
}

features/validate_image.feature

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -958,6 +958,49 @@ Feature: evaluate enterprise contract
958958
And the output should match the snapshot
959959
And the "${TMPDIR}/input.json" file should match the snapshot
960960

961+
Scenario: volatile config warnings schema contract
962+
Given a key pair named "known"
963+
Given an image named "acceptance/volatile-config-test"
964+
Given a valid image signature of "acceptance/volatile-config-test" image signed by the "known" key
965+
Given a valid attestation of "acceptance/volatile-config-test" signed by the "known" key
966+
Given a git repository named "volatile-config-policy" with
967+
| main.rego | examples/volatile_config_warnings.rego |
968+
Given policy configuration named "ec-policy" with specification
969+
"""
970+
{
971+
"sources": [
972+
{
973+
"name": "volatile-test-source",
974+
"policy": [
975+
"git::https://${GITHOST}/git/volatile-config-policy.git"
976+
],
977+
"volatileConfig": {
978+
"include": [
979+
{
980+
"value": "test.rule_with_no_expiration"
981+
},
982+
{
983+
"value": "test.rule_expiring_soon",
984+
"effectiveUntil": "2099-12-31T23:59:59Z"
985+
},
986+
{
987+
"value": "test.rule_pending_activation",
988+
"effectiveOn": "2099-01-01T00:00:00Z"
989+
},
990+
{
991+
"value": "test.component_scoped_rule",
992+
"componentNames": ["Unnamed"]
993+
}
994+
]
995+
}
996+
}
997+
]
998+
}
999+
"""
1000+
When ec command is run with "validate image --image ${REGISTRY}/acceptance/volatile-config-test --policy acceptance/ec-policy --public-key ${known_PUBLIC_KEY} --ignore-rekor --output json"
1001+
Then the exit status should be 0
1002+
Then the output should match the snapshot
1003+
9611004
Scenario: Unsupported policies
9621005
Given a key pair named "known"
9631006
Given an image named "acceptance/image"

0 commit comments

Comments
 (0)