Skip to content

Commit fec32fe

Browse files
authored
Merge pull request #1590 from joejstuart/EC-1568
Return the latest pipelineRun attestation
2 parents 5ba8365 + da03c6a commit fec32fe

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)