Skip to content

Commit da03c6a

Browse files
committed
Return the latest pipelineRun attestation
If an image is rebuilt two pipelineRun attestations can be attached to it with one the older one being invalid. This adds a rule that returns the latest v0.2 and v1 pipelineRun attestations based on the on the buildFinishedOn timestamp. https://issues.redhat.com/browse/EC-1568 Assited by Claude
1 parent 508082c commit da03c6a

File tree

6 files changed

+644
-201
lines changed

6 files changed

+644
-201
lines changed

policy/release/buildah_build_task/buildah_build_task_test.rego

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -213,18 +213,21 @@ test_add_capabilities_param if {
213213
lib.assert_empty(buildah_build_task.deny) with input.attestations as [attestation_spaces]
214214
}
215215

216-
test_platform_param if {
216+
test_platform_param_disallowed if {
217+
# Test v1.0 attestation with disallowed platform pattern
217218
expected := {{
218219
"code": "buildah_build_task.platform_param",
219220
"msg": "PLATFORM parameter value \"linux-root/arm64\" is disallowed by regex \".*root.*\"",
220221
}}
222+
attestation := _slsav1_attestation("buildah", [{"name": "PLATFORM", "value": "linux-root/arm64"}], _results)
223+
lib.assert_equal_results(expected, buildah_build_task.deny) with input.attestations as [attestation]
224+
with data.rule_data.disallowed_platform_patterns as [".*root.*"]
225+
}
221226

222-
attestations := [
223-
_slsav1_attestation("buildah", [{"name": "PLATFORM", "value": "linux-root/arm64"}], _results),
224-
_slsav1_attestation("buildah", [{"name": "PLATFORM", "value": "linux/arm64"}], _results),
225-
]
226-
227-
lib.assert_equal_results(expected, buildah_build_task.deny) with input.attestations as attestations
227+
test_platform_param_allowed if {
228+
# Test v1.0 attestation with allowed platform pattern
229+
attestation := _slsav1_attestation("buildah", [{"name": "PLATFORM", "value": "linux/arm64"}], _results)
230+
lib.assert_empty(buildah_build_task.deny) with input.attestations as [attestation]
228231
with data.rule_data.disallowed_platform_patterns as [".*root.*"]
229232
}
230233

policy/release/lib/attestations.rego

Lines changed: 55 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -43,16 +43,64 @@ slsa_provenance_attestations := [att |
4343
]
4444

4545
# These are the ones we're interested in
46-
pipelinerun_attestations := att if {
47-
v1_0 := [a |
48-
some a in pipelinerun_slsa_provenance_v1
49-
]
50-
v0_2 := [a |
46+
pipelinerun_attestations := array.concat(latest_v02_pipelinerun_attestation, latest_v1_pipelinerun_attestation)
47+
48+
# Helper function to extract buildFinishedOn timestamp from an attestation
49+
# Handles both SLSA v0.2 and v1.0 formats
50+
_build_finished_on(att) := timestamp if {
51+
# Try SLSA v0.2 path first
52+
timestamp := att.statement.predicate.metadata.buildFinishedOn
53+
} else := timestamp if {
54+
# Fallback to SLSA v1.0 path if v0.2 doesn't exist
55+
timestamp := att.statement.predicate.runDetails.metadata.buildFinishedOn
56+
}
57+
58+
# Returns the latest PipelineRun attestation per type (SLSA v0.2 and v1.0)
59+
# based on the buildFinishedOn timestamp. If there's only one attestation of a type,
60+
# return it regardless of timestamp. Returns a list (empty if none exist).
61+
latest_v02_pipelinerun_attestation := [pipelinerun_slsa_provenance02[0]] if {
62+
# If there's only one v0.2 attestation, return it regardless of timestamp
63+
count(pipelinerun_slsa_provenance02) == 1
64+
} else := [att |
65+
# Multiple v0.2 attestations - filter by timestamp and return latest
66+
v02_with_timestamp := [a |
5167
some a in pipelinerun_slsa_provenance02
68+
_build_finished_on(a)
5269
]
5370

54-
att := array.concat(v1_0, v0_2)
55-
}
71+
# make sure all v0.2 attestations have a timestamp
72+
count(v02_with_timestamp) == count(pipelinerun_slsa_provenance02)
73+
74+
# Find the latest v0.2 attestation
75+
max_v02_timestamp := max({ts |
76+
some a in v02_with_timestamp
77+
ts := _build_finished_on(a)
78+
})
79+
some att in v02_with_timestamp
80+
_build_finished_on(att) == max_v02_timestamp
81+
]
82+
83+
latest_v1_pipelinerun_attestation := [pipelinerun_slsa_provenance_v1[0]] if {
84+
# If there's only one v1.0 attestation, return it regardless of timestamp
85+
count(pipelinerun_slsa_provenance_v1) == 1
86+
} else := [att |
87+
# Multiple v1.0 attestations - filter by timestamp and return latest
88+
v1_with_timestamp := [a |
89+
some a in pipelinerun_slsa_provenance_v1
90+
_build_finished_on(a)
91+
]
92+
93+
# make sure all v1.0 attestations have a timestamp
94+
count(v1_with_timestamp) == count(pipelinerun_slsa_provenance_v1)
95+
96+
# Find the latest v1.0 attestation
97+
max_v1_timestamp := max({ts |
98+
some a in v1_with_timestamp
99+
ts := _build_finished_on(a)
100+
})
101+
some att in v1_with_timestamp
102+
_build_finished_on(att) == max_v1_timestamp
103+
]
56104

57105
pipelinerun_slsa_provenance02 := [att |
58106
some att in input.attestations

policy/release/lib/attestations_test.rego

Lines changed: 232 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -149,18 +149,48 @@ test_slsa_provenance_attestations if {
149149
lib.assert_equal(lib.slsa_provenance_attestations, expected) with input.attestations as attestations
150150
}
151151

152-
test_pr_attestations if {
152+
test_pr_attestations_v1 if {
153+
# Test v1.0 PipelineRun attestation
154+
lib.assert_equal([mock_pr_att], lib.pipelinerun_attestations) with input.attestations as [
155+
mock_tr_att,
156+
mock_pr_att,
157+
garbage_att,
158+
]
159+
}
160+
161+
test_pr_attestations_v02 if {
162+
# Test v0.2 PipelineRun attestation
163+
lib.assert_equal([mock_pr_att_legacy], lib.pipelinerun_attestations) with input.attestations as [
164+
mock_tr_att_legacy,
165+
mock_pr_att_legacy,
166+
garbage_att,
167+
]
168+
}
169+
170+
test_pr_attestations_both if {
171+
# Test both v0.2 and v1.0 PipelineRun attestations together
172+
# Use properly structured v1.0 attestation
173+
v1_att := {"statement": {
174+
"predicateType": "https://slsa.dev/provenance/v1",
175+
"predicate": {"buildDefinition": {
176+
"buildType": "https://tekton.dev/chains/v2/slsa-tekton",
177+
"externalParameters": {"runSpec": {"pipelineSpec": {}}},
178+
}},
179+
}}
153180
lib.assert_equal(
154-
[mock_pr_att, mock_pr_att_legacy],
181+
[mock_pr_att_legacy, v1_att],
155182
lib.pipelinerun_attestations,
156183
) with input.attestations as [
157184
mock_tr_att,
158185
mock_tr_att_legacy,
159-
mock_pr_att,
186+
v1_att,
160187
mock_pr_att_legacy,
161188
garbage_att,
162189
]
190+
}
163191

192+
test_pr_attestations_empty if {
193+
# Test that no PipelineRun attestations returns empty list
164194
lib.assert_equal([], lib.pipelinerun_attestations) with input.attestations as [
165195
mock_tr_att,
166196
mock_tr_att_legacy,
@@ -390,3 +420,202 @@ test_result_values if {
390420

391421
not lib.result_values(123)
392422
}
423+
424+
# Helper to create a build task (has IMAGE_URL and IMAGE_DIGEST)
425+
_build_task := {
426+
"name": "buildah",
427+
"ref": {"kind": "Task", "name": "buildah", "bundle": trusted_bundle_ref},
428+
"results": [
429+
{"name": "IMAGE_URL", "value": "quay.io/test/image:tag"},
430+
{"name": "IMAGE_DIGEST", "value": "sha256:abc123"},
431+
],
432+
}
433+
434+
# Helper to create a non-build task (no IMAGE_URL/IMAGE_DIGEST)
435+
_non_build_task := {
436+
"name": "git-clone",
437+
"ref": {"kind": "Task", "name": "git-clone", "bundle": trusted_bundle_ref},
438+
"results": [
439+
{"name": "url", "value": "https://github.com/test/repo"},
440+
{"name": "commit", "value": "abc123"},
441+
],
442+
}
443+
444+
# Helper to create SLSA v0.2 attestation with metadata
445+
_attestation_v02_with_metadata(build_finished_on, tasks) := {"statement": {
446+
"predicateType": "https://slsa.dev/provenance/v0.2",
447+
"predicate": {
448+
"buildType": lib.tekton_pipeline_run,
449+
"buildConfig": {"tasks": tasks},
450+
"metadata": {
451+
"buildFinishedOn": build_finished_on,
452+
"buildStartedOn": "2025-01-01T00:00:00Z",
453+
},
454+
},
455+
}}
456+
457+
# Helper to create SLSA v1.0 attestation with metadata
458+
_attestation_v1_with_metadata(build_finished_on, tasks) := {"statement": {
459+
"predicateType": "https://slsa.dev/provenance/v1",
460+
"predicate": {
461+
"buildDefinition": {
462+
"buildType": lib.tekton_slsav1_pipeline_run,
463+
"externalParameters": {"runSpec": {"pipelineSpec": {}}},
464+
"resolvedDependencies": tekton_test.resolved_dependencies(tasks),
465+
},
466+
"runDetails": {"metadata": {
467+
"buildFinishedOn": build_finished_on,
468+
"buildStartedOn": "2025-01-01T00:00:00Z",
469+
}},
470+
},
471+
}}
472+
473+
test_pipelinerun_attestations_single_v02 if {
474+
# Test single v0.2 attestation
475+
att := _attestation_v02_with_metadata("2025-01-15T10:30:00Z", [_build_task])
476+
expected := [att]
477+
lib.assert_equal(expected, lib.pipelinerun_attestations) with input.attestations as [att]
478+
}
479+
480+
test_pipelinerun_attestations_multiple_v02_latest_first if {
481+
# Multiple v0.2 attestations, latest is first in list
482+
att1 := _attestation_v02_with_metadata("2025-01-20T15:45:00Z", [_build_task])
483+
att2 := _attestation_v02_with_metadata("2025-01-15T10:30:00Z", [_build_task])
484+
attestations := [att1, att2]
485+
expected := [att1]
486+
lib.assert_equal(expected, lib.pipelinerun_attestations) with input.attestations as attestations
487+
}
488+
489+
test_pipelinerun_attestations_multiple_v02_latest_last if {
490+
# Multiple v0.2 attestations, latest is last in list
491+
att1 := _attestation_v02_with_metadata("2025-01-15T10:30:00Z", [_build_task])
492+
att2 := _attestation_v02_with_metadata("2025-01-20T15:45:00Z", [_build_task])
493+
attestations := [att1, att2]
494+
expected := [att2]
495+
lib.assert_equal(expected, lib.pipelinerun_attestations) with input.attestations as attestations
496+
}
497+
498+
test_pipelinerun_attestations_multiple_v02_middle if {
499+
# Multiple v0.2 attestations, latest is in the middle
500+
att1 := _attestation_v02_with_metadata("2025-01-15T10:30:00Z", [_build_task])
501+
att2 := _attestation_v02_with_metadata("2025-01-25T20:00:00Z", [_build_task])
502+
att3 := _attestation_v02_with_metadata("2025-01-20T15:45:00Z", [_build_task])
503+
attestations := [att1, att2, att3]
504+
expected := [att2]
505+
lib.assert_equal(expected, lib.pipelinerun_attestations) with input.attestations as attestations
506+
}
507+
508+
test_pipelinerun_attestations_multiple_v02_missing_timestamp if {
509+
# Multiple v0.2 attestations where at least one doesn't have a timestamp - should return empty
510+
att_with_metadata := _attestation_v02_with_metadata("2025-01-20T15:45:00Z", [_build_task])
511+
att_without_metadata := json.patch(
512+
_attestation_v02_with_metadata("2025-01-25T20:00:00Z", [_build_task]),
513+
[{"op": "remove", "path": "/statement/predicate/metadata"}],
514+
)
515+
attestations := [att_with_metadata, att_without_metadata]
516+
expected := []
517+
lib.assert_equal(expected, lib.pipelinerun_attestations) with input.attestations as attestations
518+
}
519+
520+
test_pipelinerun_attestations_multiple_v1_missing_timestamp if {
521+
# Multiple v1.0 attestations where at least one doesn't have a timestamp - should return empty
522+
v1_task := tekton_test.slsav1_task_bundle(
523+
tekton_test.slsav1_task_result(
524+
"buildah",
525+
[
526+
{"name": "IMAGE_URL", "type": "string", "value": "quay.io/test/image:tag"},
527+
{"name": "IMAGE_DIGEST", "type": "string", "value": "sha256:abc123"},
528+
],
529+
),
530+
trusted_bundle_ref,
531+
)
532+
att_with_metadata := _attestation_v1_with_metadata("2025-01-20T15:45:00Z", [v1_task])
533+
att_without_metadata := json.patch(
534+
_attestation_v1_with_metadata("2025-01-25T20:00:00Z", [v1_task]),
535+
[{"op": "remove", "path": "/statement/predicate/runDetails"}],
536+
)
537+
attestations := [att_with_metadata, att_without_metadata]
538+
expected := []
539+
lib.assert_equal(expected, lib.pipelinerun_attestations) with input.attestations as attestations
540+
}
541+
542+
test_pipelinerun_attestations_mixed_formats if {
543+
# Test with both v0.2 and v1.0 attestations - should return both (one per type)
544+
v02_task := _build_task
545+
v1_task := tekton_test.slsav1_task_bundle(
546+
tekton_test.slsav1_task_result(
547+
"buildah",
548+
[
549+
{"name": "IMAGE_URL", "type": "string", "value": "quay.io/test/image:tag"},
550+
{"name": "IMAGE_DIGEST", "type": "string", "value": "sha256:abc123"},
551+
],
552+
),
553+
trusted_bundle_ref,
554+
)
555+
att_v02 := _attestation_v02_with_metadata("2025-01-15T10:30:00Z", [v02_task])
556+
att_v1 := _attestation_v1_with_metadata("2025-01-20T15:45:00Z", [v1_task])
557+
attestations := [att_v02, att_v1]
558+
expected := [att_v02, att_v1]
559+
lib.assert_equal(expected, lib.pipelinerun_attestations) with input.attestations as attestations
560+
}
561+
562+
test_pipelinerun_attestations_empty if {
563+
# No attestations should return empty list
564+
expected := []
565+
lib.assert_equal(expected, lib.pipelinerun_attestations) with input.attestations as []
566+
}
567+
568+
test_pipelinerun_attestations_single_no_timestamp if {
569+
# Single attestation without timestamp should still be returned
570+
att := _attestation_v02_with_metadata("2025-01-15T10:30:00Z", [_non_build_task])
571+
expected := [att]
572+
lib.assert_equal(expected, lib.pipelinerun_attestations) with input.attestations as [att]
573+
}
574+
575+
test_pipelinerun_attestations_multiple_per_type if {
576+
# Test scenario: 3 attestations where 2 are v0.2 and 1 is v1.0
577+
# Should return the latest v0.2 and the v1.0
578+
v02_att1 := _attestation_v02_with_metadata("2025-01-15T10:30:00Z", [_build_task])
579+
v02_att2 := _attestation_v02_with_metadata("2025-01-20T15:45:00Z", [_build_task])
580+
v1_att := _attestation_v1_with_metadata("2025-01-18T12:00:00Z", [_build_task])
581+
attestations := [v02_att1, v02_att2, v1_att]
582+
583+
# Should return latest v0.2 (v02_att2) and the v1.0 (v1_att)
584+
expected := [v02_att2, v1_att]
585+
lib.assert_equal(expected, lib.pipelinerun_attestations) with input.attestations as attestations
586+
}
587+
588+
test_pipelinerun_attestations_v1_multiple if {
589+
# Test multiple v1.0 attestations - should return the latest
590+
v1_att1 := _attestation_v1_with_metadata("2025-01-15T10:30:00Z", [_build_task])
591+
v1_att2 := _attestation_v1_with_metadata("2025-01-20T15:45:00Z", [_build_task])
592+
attestations := [v1_att1, v1_att2]
593+
expected := [v1_att2]
594+
lib.assert_equal(expected, lib.pipelinerun_attestations) with input.attestations as attestations
595+
}
596+
597+
test_pipelinerun_attestations_v1_single_no_timestamp if {
598+
# Test single v1.0 attestation without timestamp - should still return it
599+
v1_task := tekton_test.slsav1_task_bundle(
600+
tekton_test.slsav1_task_result(
601+
"buildah",
602+
[
603+
{"name": "IMAGE_URL", "type": "string", "value": "quay.io/test/image:tag"},
604+
{"name": "IMAGE_DIGEST", "type": "string", "value": "sha256:abc123"},
605+
],
606+
),
607+
trusted_bundle_ref,
608+
)
609+
610+
# Create v1.0 attestation without runDetails.metadata.buildFinishedOn
611+
v1_att := {"statement": {
612+
"predicateType": "https://slsa.dev/provenance/v1",
613+
"predicate": {"buildDefinition": {
614+
"buildType": lib.tekton_slsav1_pipeline_run,
615+
"externalParameters": {"runSpec": {"pipelineSpec": {}}},
616+
"resolvedDependencies": tekton_test.resolved_dependencies([v1_task]),
617+
}},
618+
}}
619+
expected := [v1_att]
620+
lib.assert_equal(expected, lib.pipelinerun_attestations) with input.attestations as [v1_att]
621+
}

0 commit comments

Comments
 (0)