diff --git a/policy/lib/tekton/pipeline.rego b/policy/lib/tekton/pipeline.rego index 58ffd8692..703332432 100644 --- a/policy/lib/tekton/pipeline.rego +++ b/policy/lib/tekton/pipeline.rego @@ -2,60 +2,89 @@ package lib.tekton import rego.v1 -import data.lib.sets import data.lib.time as ectime 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 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 { + 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 # 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 - 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 # 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 - 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..023d6b267 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,341 @@ 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 +} + +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 (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") +} + +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(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 +} + +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(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(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(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") }