From 9b011779065ec3d043db41492f90fec87b54d5e8 Mon Sep 17 00:00:00 2001 From: jstuart Date: Thu, 23 Jul 2026 11:48:30 -0500 Subject: [PATCH 1/5] Merge pipeline-required-tasks from ruleData configuration The policy now merges pipeline-required-tasks from both data sources and the ruleData configuration (rule_data__configuration__). This allows users to extend pipeline-required-tasks via the ruleData field in their policy config without needing a separate data source file. Uses the same lib_rule_data pattern as trusted_tasks merging. Co-Authored-By: Claude Opus 4.6 --- policy/lib/rule_data/rule_data.rego | 3 ++ policy/lib/tekton/pipeline.rego | 7 ++- policy/lib/tekton/pipeline_test.rego | 75 ++++++++++++++++++++++++++++ policy/release/tasks/tasks.rego | 4 +- 4 files changed, 85 insertions(+), 4 deletions(-) diff --git a/policy/lib/rule_data/rule_data.rego b/policy/lib/rule_data/rule_data.rego index 857d95575..f4c7cd140 100644 --- a/policy/lib/rule_data/rule_data.rego +++ b/policy/lib/rule_data/rule_data.rego @@ -145,6 +145,9 @@ defaults := { # using the ruleData key. Make this default to an empty dict so we can conveniently # merge it with with `data.trusted_tasks` "trusted_tasks": {}, + # Used in lib/tekton/pipeline.rego to merge pipeline-required-tasks from + # ruleData with data sources, matching the trusted_tasks merge pattern. + "pipeline-required-tasks": {}, # Used in lib/tekton/trusted.rego to toggle trusted_task_rules on/off "trusted_task_rules_enabled": false, # Number of days before a version of the Task expires that warnings are reported diff --git a/policy/lib/tekton/pipeline.rego b/policy/lib/tekton/pipeline.rego index 6da72811d..dca24377e 100644 --- a/policy/lib/tekton/pipeline.rego +++ b/policy/lib/tekton/pipeline.rego @@ -2,12 +2,15 @@ package lib.tekton import rego.v1 +import data.lib.rule_data import data.lib.time as ectime pipeline_label := "pipelines.openshift.io/runtime" task_label := "build.appstudio.redhat.com/build_type" +pipeline_required_tasks := object.union(data["pipeline-required-tasks"], rule_data.get("pipeline-required-tasks")) + latest_required_pipeline_tasks(pipeline) := _merged_required_task_list(pipeline, "newest") current_required_pipeline_tasks(pipeline) := _merged_required_task_list(pipeline, "most_current") @@ -19,7 +22,7 @@ required_task_list(pipeline) := pipeline_data if { selectors := pipeline_label_selectors(pipeline) pipeline_data := [entry | some selector in selectors - entries := object.get(data["pipeline-required-tasks"], selector, []) + entries := object.get(pipeline_required_tasks, selector, []) some entry in entries ] count(pipeline_data) > 0 @@ -37,7 +40,7 @@ _merged_required_task_list(pipeline, mode) := { resolved := [r | some selector in selectors - entries := object.get(data["pipeline-required-tasks"], selector, []) + entries := object.get(pipeline_required_tasks, selector, []) count(entries) > 0 r := _resolve(entries, mode) ] diff --git a/policy/lib/tekton/pipeline_test.rego b/policy/lib/tekton/pipeline_test.rego index eac91228e..1e87b5776 100644 --- a/policy/lib/tekton/pipeline_test.rego +++ b/policy/lib/tekton/pipeline_test.rego @@ -535,3 +535,78 @@ test_task_effective_on_with_one_of_tasks if { assertions.assert_equal(tekton.task_effective_on(data_with_map, sorted_task), "2024-06-01T00:00:00Z") assertions.assert_equal(tekton.task_effective_on(data_with_map, "buildah"), "2024-01-01T00:00:00Z") } + +test_pipeline_required_tasks_merges_data_and_rule_data 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"}, + # regal ignore:line-length + {"name": "IMAGE_DIGEST", "value": "sha256:abc0000000000000000000000000000000000000000000000000000000000abc"}, + ], + ) + + attestation := slsav1_attestation_full( + [task_full], + {}, + {}, + ) + + data_source := {"docker": [{ + "effective_on": "2024-01-01T00:00:00Z", + "tasks": ["buildah", "git-clone"], + }]} + + rule_data_config := {"fbc": [{ + "effective_on": "2024-06-01T00:00:00Z", + "tasks": ["fbc-validation"], + }]} + + result := tekton.pipeline_required_tasks + with data["pipeline-required-tasks"] as data_source + with data.rule_data__configuration__["pipeline-required-tasks"] as rule_data_config + + assertions.assert_equal(result, { + "docker": [{"effective_on": "2024-01-01T00:00:00Z", "tasks": ["buildah", "git-clone"]}], + "fbc": [{"effective_on": "2024-06-01T00:00:00Z", "tasks": ["fbc-validation"]}], + }) +} + +test_pipeline_required_tasks_rule_data_overrides_on_key_conflict 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"}, + # regal ignore:line-length + {"name": "IMAGE_DIGEST", "value": "sha256:abc0000000000000000000000000000000000000000000000000000000000abc"}, + ], + ) + + attestation := slsav1_attestation_full( + [task_full], + {}, + {}, + ) + + data_source := {"docker": [{ + "effective_on": "2024-01-01T00:00:00Z", + "tasks": ["buildah", "git-clone"], + }]} + + rule_data_config := {"docker": [{ + "effective_on": "2024-06-01T00:00:00Z", + "tasks": ["buildah", "git-clone", "clair-scan"], + }]} + + result := tekton.latest_required_pipeline_tasks(attestation) + with data["pipeline-required-tasks"] as data_source + with data.rule_data__configuration__["pipeline-required-tasks"] as rule_data_config + + # ruleData config overrides data source for the same key + assertions.assert_equal(result.tasks, {"buildah", "git-clone", "clair-scan"}) + assertions.assert_equal(result.effective_on, "2024-06-01T00:00:00Z") +} diff --git a/policy/release/tasks/tasks.rego b/policy/release/tasks/tasks.rego index ca1f8a5c4..a5b4dd0de 100644 --- a/policy/release/tasks/tasks.rego +++ b/policy/release/tasks/tasks.rego @@ -441,7 +441,7 @@ _expiry_msg_annotation := "build.appstudio.redhat.com/expiry-message" _data_errors contains error if { some e in j.validate_schema( - data["pipeline-required-tasks"], + tekton.pipeline_required_tasks, { "$schema": "http://json-schema.org/draft-07/schema#", "type": "object", @@ -481,7 +481,7 @@ _data_errors contains error if { } _data_errors contains error if { - some key, entries in data["pipeline-required-tasks"] + some key, entries in tekton.pipeline_required_tasks some i, entry in entries effective_on := entry.effective_on not time.parse_rfc3339_ns(effective_on) From ed762e5d09ee488f25dcbcda6804113c979e5c4c Mon Sep 17 00:00:00 2001 From: jstuart Date: Thu, 23 Jul 2026 14:19:47 -0500 Subject: [PATCH 2/5] Add design doc for rule data merge pattern MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Document the non-obvious interaction between rule_data.get() defaults, object.union safety, and override precedence — knowledge needed by anyone adding a new data-source/ruleData merge. Co-Authored-By: Claude Opus 4.6 --- design/rule-data-merge-pattern.md | 56 +++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 design/rule-data-merge-pattern.md diff --git a/design/rule-data-merge-pattern.md b/design/rule-data-merge-pattern.md new file mode 100644 index 000000000..210e2c9d1 --- /dev/null +++ b/design/rule-data-merge-pattern.md @@ -0,0 +1,56 @@ +# Rule Data Merge Pattern + +Merging data-source values with ruleData configuration values using `object.union`. +Used by `pipeline_required_tasks` and `trusted_tasks` in `policy/lib/tekton/`. + +## Why does `rule_data.get()` need an explicit `{}` default for merged keys? + +`rule_data.get(key)` falls back to `[]` (empty array) when a key isn't found in any +data source. But `object.union` requires both arguments to be objects — if either is +an array, OPA returns `undefined` at runtime (see next section). So any key that will +be passed to `object.union` must have an explicit `{}` default registered in the +`defaults` map in `policy/lib/rule_data/rule_data.rego`. Without it, the merge silently +produces `undefined` and all downstream rules stop evaluating with no error output. + +When adding a new `object.union` merge, register the key in rule_data.rego's `defaults`: + +```rego +defaults := { + "pipeline-required-tasks": {}, # must be {} not [] for object.union + "trusted_tasks": {}, # same reason + ... +} +``` + +## Why does `object.union` fail silently instead of erroring? + +OPA has two behaviors depending on when it can determine types: + +- **Static types known** (literals in code): `object.union({"a": 1}, [])` produces a + compile-time `rego_type_error` — policy fails to load. +- **Dynamic types from `data`** (the common case): both arguments are typed `any`, so + OPA skips compile-time checking. At runtime, `object.union` with a non-object argument + returns `undefined` — no error, no diagnostic. The rule becomes undefined and all + consumers silently skip it. + +This means a misconfigured data source that provides an array instead of an object will +cause policy rules to silently stop evaluating, producing zero violations and zero errors. +The `{}` default in rule_data.rego is the primary defense against this. + +## How does the override precedence work in the merge? + +`object.union(A, B)` gives B full override for matching keys. In the merge pattern: + +```rego +pipeline_required_tasks := object.union(data["pipeline-required-tasks"], rule_data.get("pipeline-required-tasks")) +``` + +- A = `data["pipeline-required-tasks"]` (from data sources) +- B = `rule_data.get(...)` which checks, in priority order: + 1. `data.rule_data__configuration__` (ECP policy ruleData) + 2. `data.rule_data_custom` (custom user sources) + 3. `data.rule_data` (default data sources) + 4. `defaults` map (hardcoded `{}`) + +When B has a matching selector key (e.g. `"docker"`), it replaces A's value entirely — +the arrays are not merged. This is intentional and matches the trusted_tasks precedent. From 13a0b939c66a6ca7f896b21444c04b47721d2610 Mon Sep 17 00:00:00 2001 From: jstuart Date: Thu, 23 Jul 2026 14:37:45 -0500 Subject: [PATCH 3/5] Remove unused attestation variable from merge test Fixes opa-check strict mode: assigned var attestation unused. Co-Authored-By: Claude Opus 4.6 --- policy/lib/tekton/pipeline_test.rego | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/policy/lib/tekton/pipeline_test.rego b/policy/lib/tekton/pipeline_test.rego index 1e87b5776..61074ccd2 100644 --- a/policy/lib/tekton/pipeline_test.rego +++ b/policy/lib/tekton/pipeline_test.rego @@ -537,23 +537,6 @@ test_task_effective_on_with_one_of_tasks if { } test_pipeline_required_tasks_merges_data_and_rule_data 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"}, - # regal ignore:line-length - {"name": "IMAGE_DIGEST", "value": "sha256:abc0000000000000000000000000000000000000000000000000000000000abc"}, - ], - ) - - attestation := slsav1_attestation_full( - [task_full], - {}, - {}, - ) - data_source := {"docker": [{ "effective_on": "2024-01-01T00:00:00Z", "tasks": ["buildah", "git-clone"], From 66eb73c0765804392a7caecd95d50ff79d8898a4 Mon Sep 17 00:00:00 2001 From: jstuart Date: Thu, 23 Jul 2026 14:49:26 -0500 Subject: [PATCH 4/5] Fix rego formatting in pipeline_test.rego Co-Authored-By: Claude Opus 4.6 --- policy/lib/tekton/pipeline_test.rego | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/policy/lib/tekton/pipeline_test.rego b/policy/lib/tekton/pipeline_test.rego index 61074ccd2..21f91eb8a 100644 --- a/policy/lib/tekton/pipeline_test.rego +++ b/policy/lib/tekton/pipeline_test.rego @@ -547,8 +547,7 @@ test_pipeline_required_tasks_merges_data_and_rule_data if { "tasks": ["fbc-validation"], }]} - result := tekton.pipeline_required_tasks - with data["pipeline-required-tasks"] as data_source + result := tekton.pipeline_required_tasks with data["pipeline-required-tasks"] as data_source with data.rule_data__configuration__["pipeline-required-tasks"] as rule_data_config assertions.assert_equal(result, { @@ -585,8 +584,7 @@ test_pipeline_required_tasks_rule_data_overrides_on_key_conflict if { "tasks": ["buildah", "git-clone", "clair-scan"], }]} - result := tekton.latest_required_pipeline_tasks(attestation) - with data["pipeline-required-tasks"] as data_source + result := tekton.latest_required_pipeline_tasks(attestation) with data["pipeline-required-tasks"] as data_source with data.rule_data__configuration__["pipeline-required-tasks"] as rule_data_config # ruleData config overrides data source for the same key From 6c0586b5a4d63c850755c9224a96bf3d68798ea4 Mon Sep 17 00:00:00 2001 From: jstuart Date: Thu, 23 Jul 2026 14:52:21 -0500 Subject: [PATCH 5/5] Add design docs pointer to AGENTS.md Co-Authored-By: Claude Opus 4.6 --- AGENTS.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/AGENTS.md b/AGENTS.md index 39a2c4081..4a50ce572 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -57,6 +57,10 @@ attestation formats. Policies consume the normalized form — don't branch on SL **Rule data** lives in `example/data/` (required tasks, trusted task bundles, known RPM repos). These files have `effective_on` dates — rules with future dates are warnings, not failures. +**Design docs** in `design/` capture non-obvious design rationale, cross-repo knowledge, and +architectural constraints that aren't derivable from the code. Check there before reverse-engineering +a subsystem. + ## Common Change Patterns - **Add a release policy rule:** follow the pattern in `policy/release/attestation_type/attestation_type.rego` (rule + `_test.rego` in a subdirectory, declare `collections:` in METADATA)