From 4562ae5c32ec99218013ca947b646c7416fd5c70 Mon Sep 17 00:00:00 2001 From: robnester-rh Date: Fri, 1 May 2026 12:41:16 -0400 Subject: [PATCH 1/6] feat: support multiple build task types in pipeline label selector Rename pipeline_label_selector to pipeline_label_selectors returning a set of build types. Add _merged_required_task_list helper that resolves time-based entries independently per build type, then unions the task lists with max(effective_on). Fixes: EC-1763 --- policy/lib/tekton/pipeline.rego | 83 +++++++++++++++++++--------- policy/lib/tekton/pipeline_test.rego | 24 ++++---- 2 files changed, 68 insertions(+), 39 deletions(-) diff --git a/policy/lib/tekton/pipeline.rego b/policy/lib/tekton/pipeline.rego index 58ffd8692..0c07dcb8c 100644 --- a/policy/lib/tekton/pipeline.rego +++ b/policy/lib/tekton/pipeline.rego @@ -9,53 +9,82 @@ pipeline_label := "pipelines.openshift.io/runtime" task_label := "build.appstudio.redhat.com/build_type" -latest_required_pipeline_tasks(pipeline) := pipeline_tasks if { - pipeline_data := required_task_list(pipeline) - pipeline_tasks := ectime.newest(pipeline_data) -} +latest_required_pipeline_tasks(pipeline) := _merged_required_task_list(pipeline, "newest") -current_required_pipeline_tasks(pipeline) := pipeline_tasks if { - pipeline_data := required_task_list(pipeline) - pipeline_tasks := ectime.most_current(pipeline_data) -} +current_required_pipeline_tasks(pipeline) := _merged_required_task_list(pipeline, "most_current") -# get the label from the pipelineRun attestation and return the -# required task list FOR that pipeline +# required_task_list is used by required_pipeline_task_data in tasks.rego +# as a boolean existence check. It concatenates raw time-based arrays +# across all matching build types. required_task_list(pipeline) := pipeline_data if { - pipeline_selector := pipeline_label_selector(pipeline) - pipeline_data := data["pipeline-required-tasks"][pipeline_selector] + selectors := pipeline_label_selectors(pipeline) + pipeline_data := [entry | + some selector in selectors + entries := object.get(data["pipeline-required-tasks"], selector, []) + some entry in entries + ] + count(pipeline_data) > 0 +} + +# _merged_required_task_list resolves time-based entries independently per +# build type, then unions the task lists. Uses max(effective_on) across types. +# This is conservative but imprecise when types have different dates — see +# EC-1798 for per-task effective_on tracking. +_merged_required_task_list(pipeline, mode) := {"effective_on": max_effective_on, "tasks": all_tasks} if { + selectors := pipeline_label_selectors(pipeline) + + resolved := [r | + some selector in selectors + entries := object.get(data["pipeline-required-tasks"], selector, []) + count(entries) > 0 + r := _resolve(entries, mode) + ] + + count(resolved) > 0 + + all_tasks := [task | + some r in resolved + some task in r.tasks + ] + + dates := [r.effective_on | some r in resolved] + ordered_dates := sort(dates) + max_effective_on := ordered_dates[count(ordered_dates) - 1] } -# pipeline_label_selector is a specialized function that returns the name of the -# required tasks list that should be used. -pipeline_label_selector(pipeline) := value if { - not is_fbc # given that the build task is shared between fbc and docker builds we can't rely on the task's label +_resolve(entries, "newest") := ectime.newest(entries) + +_resolve(entries, "most_current") := ectime.most_current(entries) + +# pipeline_label_selectors returns the set of required task list names +# that should be used. When a pipeline has multiple build task types, +# all types are returned and their required tasks are unioned downstream. +pipeline_label_selectors(pipeline) := value if { + not is_fbc # Labels of the build Task from the SLSA Provenance v1.0 of a PipelineRun - values := [l | some build_task in build_tasks(pipeline); l := build_task.metadata.labels[task_label]] - count(sets.to_set(values)) == 1 - value := values[0] + value := {l | some build_task in build_tasks(pipeline); l := build_task.metadata.labels[task_label]} + count(value) > 0 } else := value if { - not is_fbc # given that the build task is shared between fbc and docker builds we can't rely on the task's label + not is_fbc # Labels of the build Task from the SLSA Provenance v0.2 of a PipelineRun - values := [l | some build_task in build_tasks(pipeline); l := build_task.invocation.environment.labels[task_label]] - count(sets.to_set(values)) == 1 - value := values[0] + value := {l | some build_task in build_tasks(pipeline); l := build_task.invocation.environment.labels[task_label]} + count(value) > 0 } else := value if { # PipelineRun labels found in the SLSA Provenance v1.0 - value := pipeline.statement.predicate.buildDefinition.internalParameters.labels[pipeline_label] + value := {pipeline.statement.predicate.buildDefinition.internalParameters.labels[pipeline_label]} } else := value if { # PipelineRun labels found in the SLSA Provenance v0.2 - value := pipeline.statement.predicate.invocation.environment.labels[pipeline_label] + value := {pipeline.statement.predicate.invocation.environment.labels[pipeline_label]} } else := value if { # Labels from a Tekton Pipeline definition - value := pipeline.metadata.labels[pipeline_label] + value := {pipeline.metadata.labels[pipeline_label]} } else := value if { # special handling for fbc pipelines, they're detected via image label is_fbc - value := "fbc" + value := {"fbc"} } pipeline_name := input.metadata.name diff --git a/policy/lib/tekton/pipeline_test.rego b/policy/lib/tekton/pipeline_test.rego index 4d98194a8..3cd864434 100644 --- a/policy/lib/tekton/pipeline_test.rego +++ b/policy/lib/tekton/pipeline_test.rego @@ -5,7 +5,7 @@ import rego.v1 import data.lib.assertions import data.lib.tekton -test_pipeline_label_selector_build_task_slsa_v1_0 if { +test_pipeline_label_selectors_build_task_slsa_v1_0 if { task_base := slsav1_task("build-container") task_w_labels = with_labels(task_base, {tekton.task_label: "generic"}) task_full = with_results( @@ -22,10 +22,10 @@ test_pipeline_label_selector_build_task_slsa_v1_0 if { {}, ) - assertions.assert_equal(tekton.pipeline_label_selector(attestation), "generic") + assertions.assert_equal(tekton.pipeline_label_selectors(attestation), {"generic"}) } -test_pipeline_label_selector_build_task_slsa_v0_2 if { +test_pipeline_label_selectors_build_task_slsa_v0_2 if { task := { "ref": {"name": "build-container", "kind": "Task"}, "results": [ @@ -44,10 +44,10 @@ test_pipeline_label_selector_build_task_slsa_v0_2 if { }, }} - assertions.assert_equal(tekton.pipeline_label_selector(attestation), "generic") + assertions.assert_equal(tekton.pipeline_label_selectors(attestation), {"generic"}) } -test_pipeline_label_selector_pipeline_run_slsa_v1_0 if { +test_pipeline_label_selectors_pipeline_run_slsa_v1_0 if { attestation := json.patch( slsav1_attestation([]), [{ @@ -57,10 +57,10 @@ test_pipeline_label_selector_pipeline_run_slsa_v1_0 if { }], ) - assertions.assert_equal(tekton.pipeline_label_selector(attestation), "generic") + assertions.assert_equal(tekton.pipeline_label_selectors(attestation), {"generic"}) } -test_pipeline_label_selector_pipeline_run_slsa_v0_2 if { +test_pipeline_label_selectors_pipeline_run_slsa_v0_2 if { task := { "ref": {"name": "build-container", "kind": "Task"}, "results": [ @@ -78,15 +78,15 @@ test_pipeline_label_selector_pipeline_run_slsa_v0_2 if { }, }} - assertions.assert_equal(tekton.pipeline_label_selector(attestation), "generic") + assertions.assert_equal(tekton.pipeline_label_selectors(attestation), {"generic"}) } -test_pipeline_label_selector_pipeline_definition if { +test_pipeline_label_selectors_pipeline_definition if { pipeline := {"metadata": {"labels": {tekton.pipeline_label: "generic"}}} - assertions.assert_equal(tekton.pipeline_label_selector(pipeline), "generic") + assertions.assert_equal(tekton.pipeline_label_selectors(pipeline), {"generic"}) } -test_fbc_pipeline_label_selector if { +test_fbc_pipeline_label_selectors if { image := {"config": {"Labels": {"operators.operatorframework.io.index.configs.v1": "/configs"}}} - assertions.assert_equal(tekton.pipeline_label_selector({}), "fbc") with input.image as image + assertions.assert_equal(tekton.pipeline_label_selectors({}), {"fbc"}) with input.image as image } From 9860096552821447e5b7ce9f1a5e28740f1a5ac5 Mon Sep 17 00:00:00 2001 From: robnester-rh Date: Fri, 1 May 2026 12:54:05 -0400 Subject: [PATCH 2/6] test: add multi-build-type pipeline selector tests Covers multi-type selector, required task union, missing type in data (silently skipped), and all types missing. Part of EC-1763. --- policy/lib/tekton/pipeline_test.rego | 144 +++++++++++++++++++++++++++ 1 file changed, 144 insertions(+) diff --git a/policy/lib/tekton/pipeline_test.rego b/policy/lib/tekton/pipeline_test.rego index 3cd864434..c104a0f9b 100644 --- a/policy/lib/tekton/pipeline_test.rego +++ b/policy/lib/tekton/pipeline_test.rego @@ -90,3 +90,147 @@ test_fbc_pipeline_label_selectors if { image := {"config": {"Labels": {"operators.operatorframework.io.index.configs.v1": "/configs"}}} assertions.assert_equal(tekton.pipeline_label_selectors({}), {"fbc"}) with input.image as image } + +test_pipeline_label_selectors_multi_build_type_slsa_v1_0 if { + docker_task_base := slsav1_task("build-container") + docker_task_w_labels := with_labels(docker_task_base, {tekton.task_label: "docker"}) + docker_task := with_results( + docker_task_w_labels, + [ + {"name": "IMAGE_URL", "value": "localhost:5000/repo:latest"}, + {"name": "IMAGE_DIGEST", "value": "sha256:abc0000000000000000000000000000000000000000000000000000000000abc"}, + ], + ) + + bundle_task_base := slsav1_task("build-tekton-bundle") + bundle_task_w_labels := with_labels(bundle_task_base, {tekton.task_label: "tkn-bundle"}) + bundle_task := with_results( + bundle_task_w_labels, + [ + {"name": "IMAGE_URL", "value": "localhost:5000/bundle:latest"}, + {"name": "IMAGE_DIGEST", "value": "sha256:def0000000000000000000000000000000000000000000000000000000000def"}, + ], + ) + + attestation := slsav1_attestation_full( + [docker_task, bundle_task], + {tekton.pipeline_label: "ignored"}, + {}, + ) + + assertions.assert_equal(tekton.pipeline_label_selectors(attestation), {"docker", "tkn-bundle"}) +} + +test_required_task_list_multi_type_union if { + docker_task_base := slsav1_task("build-container") + docker_task_w_labels := with_labels(docker_task_base, {tekton.task_label: "docker"}) + docker_task := with_results( + docker_task_w_labels, + [ + {"name": "IMAGE_URL", "value": "localhost:5000/repo:latest"}, + {"name": "IMAGE_DIGEST", "value": "sha256:abc0000000000000000000000000000000000000000000000000000000000abc"}, + ], + ) + + bundle_task_base := slsav1_task("build-tekton-bundle") + bundle_task_w_labels := with_labels(bundle_task_base, {tekton.task_label: "tkn-bundle"}) + bundle_task := with_results( + bundle_task_w_labels, + [ + {"name": "IMAGE_URL", "value": "localhost:5000/bundle:latest"}, + {"name": "IMAGE_DIGEST", "value": "sha256:def0000000000000000000000000000000000000000000000000000000000def"}, + ], + ) + + attestation := slsav1_attestation_full( + [docker_task, bundle_task], + {}, + {}, + ) + + pipeline_required_tasks := { + "docker": [{ + "effective_on": "2024-01-01T00:00:00Z", + "tasks": ["buildah", "clair-scan", "git-clone"], + }], + "tkn-bundle": [{ + "effective_on": "2024-06-01T00:00:00Z", + "tasks": ["tkn-build", "clair-scan"], + }], + } + + result := tekton.latest_required_pipeline_tasks(attestation) with data["pipeline-required-tasks"] as pipeline_required_tasks + + # Union of tasks from both types + assertions.assert_equal(sort(result.tasks), ["buildah", "clair-scan", "clair-scan", "git-clone", "tkn-build"]) + + # max(effective_on) across types + assertions.assert_equal(result.effective_on, "2024-06-01T00:00:00Z") +} + +test_required_task_list_missing_type_skipped if { + docker_task_base := slsav1_task("build-container") + docker_task_w_labels := with_labels(docker_task_base, {tekton.task_label: "docker"}) + docker_task := with_results( + docker_task_w_labels, + [ + {"name": "IMAGE_URL", "value": "localhost:5000/repo:latest"}, + {"name": "IMAGE_DIGEST", "value": "sha256:abc0000000000000000000000000000000000000000000000000000000000abc"}, + ], + ) + + bundle_task_base := slsav1_task("build-tekton-bundle") + bundle_task_w_labels := with_labels(bundle_task_base, {tekton.task_label: "tkn-bundle"}) + bundle_task := with_results( + bundle_task_w_labels, + [ + {"name": "IMAGE_URL", "value": "localhost:5000/bundle:latest"}, + {"name": "IMAGE_DIGEST", "value": "sha256:def0000000000000000000000000000000000000000000000000000000000def"}, + ], + ) + + attestation := slsav1_attestation_full( + [docker_task, bundle_task], + {}, + {}, + ) + + # Only docker exists in data, tkn-bundle does not + pipeline_required_tasks := {"docker": [{ + "effective_on": "2024-01-01T00:00:00Z", + "tasks": ["buildah", "git-clone"], + }]} + + result := tekton.latest_required_pipeline_tasks(attestation) with data["pipeline-required-tasks"] as pipeline_required_tasks + + # Only docker tasks returned, tkn-bundle silently skipped + assertions.assert_equal(sort(result.tasks), ["buildah", "git-clone"]) + assertions.assert_equal(result.effective_on, "2024-01-01T00:00:00Z") +} + +test_required_task_list_all_types_missing if { + docker_task_base := slsav1_task("build-container") + docker_task_w_labels := with_labels(docker_task_base, {tekton.task_label: "docker"}) + docker_task := with_results( + docker_task_w_labels, + [ + {"name": "IMAGE_URL", "value": "localhost:5000/repo:latest"}, + {"name": "IMAGE_DIGEST", "value": "sha256:abc0000000000000000000000000000000000000000000000000000000000abc"}, + ], + ) + + attestation := slsav1_attestation_full( + [docker_task], + {}, + {}, + ) + + # No matching type in data + pipeline_required_tasks := {"fbc": [{ + "effective_on": "2024-01-01T00:00:00Z", + "tasks": ["fbc-validation"], + }]} + + # required_task_list should be undefined (no matching selectors) + not tekton.required_task_list(attestation) with data["pipeline-required-tasks"] as pipeline_required_tasks +} From 3722ef056be704d903fae2ade4344d9bdd40edc9 Mon Sep 17 00:00:00 2001 From: robnester-rh Date: Fri, 1 May 2026 12:58:30 -0400 Subject: [PATCH 3/6] fix: remove unused sets import from pipeline.rego The refactored pipeline_label_selectors uses set comprehensions directly instead of sets.to_set(), making the import unnecessary. Part of EC-1763. Co-Authored-By: Claude Opus 4.6 (1M context) --- policy/lib/tekton/pipeline.rego | 1 - 1 file changed, 1 deletion(-) diff --git a/policy/lib/tekton/pipeline.rego b/policy/lib/tekton/pipeline.rego index 0c07dcb8c..a5dfb94c0 100644 --- a/policy/lib/tekton/pipeline.rego +++ b/policy/lib/tekton/pipeline.rego @@ -2,7 +2,6 @@ package lib.tekton import rego.v1 -import data.lib.sets import data.lib.time as ectime pipeline_label := "pipelines.openshift.io/runtime" From 248cbd263e376dc426b397d406a9d6c6a1af35e8 Mon Sep 17 00:00:00 2001 From: robnester-rh Date: Fri, 1 May 2026 13:08:22 -0400 Subject: [PATCH 4/6] fix: address review feedback for EC-1763 - Fix inaccurate "unions" comment to "concatenates" - Restore inline FBC comment on not is_fbc guards - Add test for current_required_pipeline_tasks (most_current mode) - Add test for required_task_list multi-type concatenation - Add test for single-type backward compatibility - Add test for multiple time entries per type --- policy/lib/tekton/pipeline.rego | 7 +- policy/lib/tekton/pipeline_test.rego | 184 ++++++++++++++++++++++++++- 2 files changed, 187 insertions(+), 4 deletions(-) diff --git a/policy/lib/tekton/pipeline.rego b/policy/lib/tekton/pipeline.rego index a5dfb94c0..43fec494e 100644 --- a/policy/lib/tekton/pipeline.rego +++ b/policy/lib/tekton/pipeline.rego @@ -26,7 +26,8 @@ required_task_list(pipeline) := pipeline_data if { } # _merged_required_task_list resolves time-based entries independently per -# build type, then unions the task lists. Uses max(effective_on) across types. +# build type, then concatenates the task lists. Uses max(effective_on) across +# types so that no type's tasks are enforced before their designated date. # This is conservative but imprecise when types have different dates — see # EC-1798 for per-task effective_on tracking. _merged_required_task_list(pipeline, mode) := {"effective_on": max_effective_on, "tasks": all_tasks} if { @@ -59,13 +60,13 @@ _resolve(entries, "most_current") := ectime.most_current(entries) # that should be used. When a pipeline has multiple build task types, # all types are returned and their required tasks are unioned downstream. pipeline_label_selectors(pipeline) := value if { - not is_fbc + not is_fbc # FBC builds share the docker build task; its label is unreliable for FBC # Labels of the build Task from the SLSA Provenance v1.0 of a PipelineRun value := {l | some build_task in build_tasks(pipeline); l := build_task.metadata.labels[task_label]} count(value) > 0 } else := value if { - not is_fbc + not is_fbc # FBC builds share the docker build task; its label is unreliable for FBC # Labels of the build Task from the SLSA Provenance v0.2 of a PipelineRun value := {l | some build_task in build_tasks(pipeline); l := build_task.invocation.environment.labels[task_label]} diff --git a/policy/lib/tekton/pipeline_test.rego b/policy/lib/tekton/pipeline_test.rego index c104a0f9b..efc36ded1 100644 --- a/policy/lib/tekton/pipeline_test.rego +++ b/policy/lib/tekton/pipeline_test.rego @@ -161,7 +161,7 @@ test_required_task_list_multi_type_union if { result := tekton.latest_required_pipeline_tasks(attestation) with data["pipeline-required-tasks"] as pipeline_required_tasks - # Union of tasks from both types + # Concatenation of tasks from both types (duplicates preserved) assertions.assert_equal(sort(result.tasks), ["buildah", "clair-scan", "clair-scan", "git-clone", "tkn-build"]) # max(effective_on) across types @@ -234,3 +234,185 @@ test_required_task_list_all_types_missing if { # required_task_list should be undefined (no matching selectors) not tekton.required_task_list(attestation) with data["pipeline-required-tasks"] as pipeline_required_tasks } + +test_current_required_pipeline_tasks_multi_type if { + docker_task_base := slsav1_task("build-container") + docker_task_w_labels := with_labels(docker_task_base, {tekton.task_label: "docker"}) + docker_task := with_results( + docker_task_w_labels, + [ + {"name": "IMAGE_URL", "value": "localhost:5000/repo:latest"}, + {"name": "IMAGE_DIGEST", "value": "sha256:abc0000000000000000000000000000000000000000000000000000000000abc"}, + ], + ) + + bundle_task_base := slsav1_task("build-tekton-bundle") + bundle_task_w_labels := with_labels(bundle_task_base, {tekton.task_label: "tkn-bundle"}) + bundle_task := with_results( + bundle_task_w_labels, + [ + {"name": "IMAGE_URL", "value": "localhost:5000/bundle:latest"}, + {"name": "IMAGE_DIGEST", "value": "sha256:def0000000000000000000000000000000000000000000000000000000000def"}, + ], + ) + + attestation := slsav1_attestation_full( + [docker_task, bundle_task], + {}, + {}, + ) + + pipeline_required_tasks := { + "docker": [{ + "effective_on": "2024-01-01T00:00:00Z", + "tasks": ["buildah", "git-clone"], + }], + "tkn-bundle": [ + { + "effective_on": "2024-06-01T00:00:00Z", + "tasks": ["tkn-build"], + }, + { + "effective_on": "2099-01-01T00:00:00Z", + "tasks": ["tkn-build", "future-task"], + }, + ], + } + + result := tekton.current_required_pipeline_tasks(attestation) with data["pipeline-required-tasks"] as pipeline_required_tasks + + # most_current excludes the future entry (2099), so tkn-bundle resolves to 2024-06-01 + assertions.assert_equal(sort(result.tasks), ["buildah", "git-clone", "tkn-build"]) + assertions.assert_equal(result.effective_on, "2024-06-01T00:00:00Z") +} + +test_required_task_list_multi_type_concatenation if { + docker_task_base := slsav1_task("build-container") + docker_task_w_labels := with_labels(docker_task_base, {tekton.task_label: "docker"}) + docker_task := with_results( + docker_task_w_labels, + [ + {"name": "IMAGE_URL", "value": "localhost:5000/repo:latest"}, + {"name": "IMAGE_DIGEST", "value": "sha256:abc0000000000000000000000000000000000000000000000000000000000abc"}, + ], + ) + + bundle_task_base := slsav1_task("build-tekton-bundle") + bundle_task_w_labels := with_labels(bundle_task_base, {tekton.task_label: "tkn-bundle"}) + bundle_task := with_results( + bundle_task_w_labels, + [ + {"name": "IMAGE_URL", "value": "localhost:5000/bundle:latest"}, + {"name": "IMAGE_DIGEST", "value": "sha256:def0000000000000000000000000000000000000000000000000000000000def"}, + ], + ) + + attestation := slsav1_attestation_full( + [docker_task, bundle_task], + {}, + {}, + ) + + pipeline_required_tasks := { + "docker": [{ + "effective_on": "2024-01-01T00:00:00Z", + "tasks": ["buildah"], + }], + "tkn-bundle": [{ + "effective_on": "2024-06-01T00:00:00Z", + "tasks": ["tkn-build"], + }], + } + + result := tekton.required_task_list(attestation) with data["pipeline-required-tasks"] as pipeline_required_tasks + + # Concatenated raw entries from both types + count(result) == 2 +} + +test_latest_required_pipeline_tasks_single_type if { + task_base := slsav1_task("build-container") + task_w_labels := with_labels(task_base, {tekton.task_label: "docker"}) + task_full := with_results( + task_w_labels, + [ + {"name": "IMAGE_URL", "value": "localhost:5000/repo:latest"}, + {"name": "IMAGE_DIGEST", "value": "sha256:abc0000000000000000000000000000000000000000000000000000000000abc"}, + ], + ) + + attestation := slsav1_attestation_full( + [task_full], + {}, + {}, + ) + + pipeline_required_tasks := {"docker": [{ + "effective_on": "2024-01-01T00:00:00Z", + "tasks": ["buildah", "clair-scan", "git-clone"], + }]} + + result := tekton.latest_required_pipeline_tasks(attestation) with data["pipeline-required-tasks"] as pipeline_required_tasks + + assertions.assert_equal(sort(result.tasks), ["buildah", "clair-scan", "git-clone"]) + assertions.assert_equal(result.effective_on, "2024-01-01T00:00:00Z") +} + +test_latest_required_pipeline_tasks_multi_time_entries if { + docker_task_base := slsav1_task("build-container") + docker_task_w_labels := with_labels(docker_task_base, {tekton.task_label: "docker"}) + docker_task := with_results( + docker_task_w_labels, + [ + {"name": "IMAGE_URL", "value": "localhost:5000/repo:latest"}, + {"name": "IMAGE_DIGEST", "value": "sha256:abc0000000000000000000000000000000000000000000000000000000000abc"}, + ], + ) + + bundle_task_base := slsav1_task("build-tekton-bundle") + bundle_task_w_labels := with_labels(bundle_task_base, {tekton.task_label: "tkn-bundle"}) + bundle_task := with_results( + bundle_task_w_labels, + [ + {"name": "IMAGE_URL", "value": "localhost:5000/bundle:latest"}, + {"name": "IMAGE_DIGEST", "value": "sha256:def0000000000000000000000000000000000000000000000000000000000def"}, + ], + ) + + attestation := slsav1_attestation_full( + [docker_task, bundle_task], + {}, + {}, + ) + + pipeline_required_tasks := { + "docker": [ + { + "effective_on": "2024-01-01T00:00:00Z", + "tasks": ["buildah"], + }, + { + "effective_on": "2024-06-01T00:00:00Z", + "tasks": ["buildah", "clair-scan"], + }, + ], + "tkn-bundle": [ + { + "effective_on": "2024-03-01T00:00:00Z", + "tasks": ["tkn-build"], + }, + { + "effective_on": "2024-09-01T00:00:00Z", + "tasks": ["tkn-build", "tkn-lint"], + }, + ], + } + + result := tekton.latest_required_pipeline_tasks(attestation) with data["pipeline-required-tasks"] as pipeline_required_tasks + + # newest picks the latest entry per type: docker=2024-06-01, tkn-bundle=2024-09-01 + assertions.assert_equal(sort(result.tasks), ["buildah", "clair-scan", "tkn-build", "tkn-lint"]) + + # max(effective_on) across resolved types + assertions.assert_equal(result.effective_on, "2024-09-01T00:00:00Z") +} From e38e1fd7895f22327ef289caf7213636da8416cb Mon Sep 17 00:00:00 2001 From: robnester-rh Date: Fri, 1 May 2026 13:11:51 -0400 Subject: [PATCH 5/6] fix: correct remaining "unioned" comment to "concatenated" Co-Authored-By: Claude Opus 4.6 (1M context) --- policy/lib/tekton/pipeline.rego | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/policy/lib/tekton/pipeline.rego b/policy/lib/tekton/pipeline.rego index 43fec494e..6ce39a00d 100644 --- a/policy/lib/tekton/pipeline.rego +++ b/policy/lib/tekton/pipeline.rego @@ -58,7 +58,7 @@ _resolve(entries, "most_current") := ectime.most_current(entries) # pipeline_label_selectors returns the set of required task list names # that should be used. When a pipeline has multiple build task types, -# all types are returned and their required tasks are unioned downstream. +# all types are returned and their required tasks are concatenated downstream. pipeline_label_selectors(pipeline) := value if { not is_fbc # FBC builds share the docker build task; its label is unreliable for FBC From e8611ae71d17f29442d073d216d02790cb4c5c02 Mon Sep 17 00:00:00 2001 From: robnester-rh Date: Fri, 1 May 2026 13:15:27 -0400 Subject: [PATCH 6/6] fix: use set union instead of concatenation for required tasks The merged task list should deduplicate tasks that appear in multiple build types (e.g. clair-scan required by both docker and tkn-bundle). Changed _merged_required_task_list to use a set comprehension for all_tasks instead of an array comprehension. Co-Authored-By: Claude Opus 4.6 (1M context) --- policy/lib/tekton/pipeline.rego | 8 ++++---- policy/lib/tekton/pipeline_test.rego | 12 ++++++------ 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/policy/lib/tekton/pipeline.rego b/policy/lib/tekton/pipeline.rego index 6ce39a00d..703332432 100644 --- a/policy/lib/tekton/pipeline.rego +++ b/policy/lib/tekton/pipeline.rego @@ -26,7 +26,7 @@ required_task_list(pipeline) := pipeline_data if { } # _merged_required_task_list resolves time-based entries independently per -# build type, then concatenates the task lists. Uses max(effective_on) across +# build type, then unions the task lists. Uses max(effective_on) across # types so that no type's tasks are enforced before their designated date. # This is conservative but imprecise when types have different dates — see # EC-1798 for per-task effective_on tracking. @@ -42,10 +42,10 @@ _merged_required_task_list(pipeline, mode) := {"effective_on": max_effective_on, count(resolved) > 0 - all_tasks := [task | + all_tasks := {task | some r in resolved some task in r.tasks - ] + } dates := [r.effective_on | some r in resolved] ordered_dates := sort(dates) @@ -58,7 +58,7 @@ _resolve(entries, "most_current") := ectime.most_current(entries) # pipeline_label_selectors returns the set of required task list names # that should be used. When a pipeline has multiple build task types, -# all types are returned and their required tasks are concatenated downstream. +# all types are returned and their required tasks are unioned downstream. pipeline_label_selectors(pipeline) := value if { not is_fbc # FBC builds share the docker build task; its label is unreliable for FBC diff --git a/policy/lib/tekton/pipeline_test.rego b/policy/lib/tekton/pipeline_test.rego index efc36ded1..023d6b267 100644 --- a/policy/lib/tekton/pipeline_test.rego +++ b/policy/lib/tekton/pipeline_test.rego @@ -161,8 +161,8 @@ test_required_task_list_multi_type_union if { result := tekton.latest_required_pipeline_tasks(attestation) with data["pipeline-required-tasks"] as pipeline_required_tasks - # Concatenation of tasks from both types (duplicates preserved) - assertions.assert_equal(sort(result.tasks), ["buildah", "clair-scan", "clair-scan", "git-clone", "tkn-build"]) + # Union of tasks from both types (deduplicated) + assertions.assert_equal(result.tasks, {"buildah", "clair-scan", "git-clone", "tkn-build"}) # max(effective_on) across types assertions.assert_equal(result.effective_on, "2024-06-01T00:00:00Z") @@ -204,7 +204,7 @@ test_required_task_list_missing_type_skipped if { result := tekton.latest_required_pipeline_tasks(attestation) with data["pipeline-required-tasks"] as pipeline_required_tasks # Only docker tasks returned, tkn-bundle silently skipped - assertions.assert_equal(sort(result.tasks), ["buildah", "git-clone"]) + assertions.assert_equal(result.tasks, {"buildah", "git-clone"}) assertions.assert_equal(result.effective_on, "2024-01-01T00:00:00Z") } @@ -282,7 +282,7 @@ test_current_required_pipeline_tasks_multi_type if { result := tekton.current_required_pipeline_tasks(attestation) with data["pipeline-required-tasks"] as pipeline_required_tasks # most_current excludes the future entry (2099), so tkn-bundle resolves to 2024-06-01 - assertions.assert_equal(sort(result.tasks), ["buildah", "git-clone", "tkn-build"]) + assertions.assert_equal(result.tasks, {"buildah", "git-clone", "tkn-build"}) assertions.assert_equal(result.effective_on, "2024-06-01T00:00:00Z") } @@ -354,7 +354,7 @@ test_latest_required_pipeline_tasks_single_type if { result := tekton.latest_required_pipeline_tasks(attestation) with data["pipeline-required-tasks"] as pipeline_required_tasks - assertions.assert_equal(sort(result.tasks), ["buildah", "clair-scan", "git-clone"]) + assertions.assert_equal(result.tasks, {"buildah", "clair-scan", "git-clone"}) assertions.assert_equal(result.effective_on, "2024-01-01T00:00:00Z") } @@ -411,7 +411,7 @@ test_latest_required_pipeline_tasks_multi_time_entries if { result := tekton.latest_required_pipeline_tasks(attestation) with data["pipeline-required-tasks"] as pipeline_required_tasks # newest picks the latest entry per type: docker=2024-06-01, tkn-bundle=2024-09-01 - assertions.assert_equal(sort(result.tasks), ["buildah", "clair-scan", "tkn-build", "tkn-lint"]) + assertions.assert_equal(result.tasks, {"buildah", "clair-scan", "tkn-build", "tkn-lint"}) # max(effective_on) across resolved types assertions.assert_equal(result.effective_on, "2024-09-01T00:00:00Z")