diff --git a/policy/lib/bundles.rego b/policy/lib/bundles.rego index 3b5ded92a..edf44b1ee 100644 --- a/policy/lib/bundles.rego +++ b/policy/lib/bundles.rego @@ -6,6 +6,9 @@ import data.lib.image import data.lib.refs import data.lib.time as time_lib +# Return the bundle reference as is +bundle(task) := refs.task_ref(task).bundle + # Returns a subset of tasks that do not use a bundle reference. disallowed_task_reference(tasks) := {task | some task in tasks @@ -15,83 +18,165 @@ disallowed_task_reference(tasks) := {task | # Returns a subset of tasks that use an empty bundle reference. empty_task_bundle_reference(tasks) := {task | some task in tasks - bundle(task) == "" + refs.task_ref(task).bundle == "" } # Returns a subset of tasks that use bundle references not pinned to a digest. unpinned_task_bundle(tasks) := {task | some task in tasks - ref := image.parse(bundle(task)) + ref := image.parse(_bundle_ref(task, data["task-bundles"])) ref.digest == "" } +# Returns if the required task-bundles data is missing +default missing_task_bundles_data := false + +missing_task_bundles_data { + count(data["task-bundles"]) == 0 +} + # Returns a subset of tasks that use an acceptable bundle reference, but # an updated bundle reference exists. out_of_date_task_bundle(tasks) := {task | some task in tasks - ref := image.parse(bundle(task)) - collection := _collection(ref) - some match_index, out_of_date in collection - is_equal(out_of_date, ref) - match_index > 0 + ref := image.parse(_bundle_ref(task, data["task-bundles"])) + + _is_out_of_date(ref) } # Returns a subset of tasks that do not use an acceptable bundle reference. unacceptable_task_bundle(tasks) := {task | some task in tasks - ref := image.parse(bundle(task)) - collection := _collection(ref) - matches := [record | - some record in collection - is_equal(record, ref) - ] + ref := image.parse(_bundle_ref(task, data["task-bundles"])) - count(matches) == 0 + _is_unacceptable(ref) } -# Returns if the required task-bundles data is missing -default missing_task_bundles_data := false +_is_unacceptable(ref) { + not _is_acceptable(ref) +} -missing_task_bundles_data { - count(data["task-bundles"]) == 0 +_is_unacceptable(ref) { + _newer_version_exists(ref, data["task-bundles"]) + _is_expired(ref, data["task-bundles"]) } # Returns true if the provided bundle reference is acceptable -is_acceptable(bundle_ref) { - ref := image.parse(bundle_ref) - collection := _collection(ref) - matches := [r | - some r in collection - is_equal(r, ref) - ] +_is_acceptable(ref) { + # all records in acceptable task bundles for the given repository + records := data["task-bundles"][ref.repo] + + some record in records - count(matches) > 0 + # an acceptable task bundle reference is one that is recorded in the + # acceptable task bundles, this is done by matching it's digest; note no + # care is given to the expiry or freshness + record.digest == ref.digest } -# Returns whether or not the ref matches the digest of the record. -is_equal(record, ref) := match { - ref.digest != "" - match := record.digest == ref.digest +# Out of date references are those that are acceptable, meaning that their +# reference is recorded in acceptable task bundles data and no newer version +# exists or the reference has expired +_is_out_of_date(ref) { + _is_acceptable(ref) + _newer_version_exists(ref, data["task-bundles"]) + not _is_expired(ref, data["task-bundles"]) } -# Returns whether or not the ref matches the tag of the record as a fallback -# in case the digest is blank for the ref. This is a weaker comparison as, -# unlike digests, tags are not immutable entities. It is expected that a -# missing digest results in a warning whenever possible. -is_equal(record, ref) := match { - ref.digest == "" - match := record.tag == ref.tag +_is_out_of_date(ref) { + _is_acceptable(ref) + not _newer_version_exists(ref, data["task-bundles"]) + _is_expired(ref, data["task-bundles"]) } -bundle(task) := refs.task_ref(task).bundle +# Evaluates to true if the tasks bundle reference is found in the acceptable +# task bundles data, matched by digest, and it is not in effect, meaning that +# it's effective_on is in the future, i.e. it has expired +_is_expired(ref, acceptable) { + # all records in acceptable task bundles for the given repository + records := acceptable[ref.repo] + + some record in records + + # consider all records, if a match is found via exact digest and it's + # effective_on is greater than current time in effect (remember it is + # configurable) we deem the bundle reference out of date + record.digest == ref.digest + + time_lib.effective_current_time_ns > time.parse_rfc3339_ns(record.effective_on) +} -# _collection returns an array representing the full list of records to -# be taken into consideration when evaluating policy rules for bundle -# references. Any irrelevant records are filtered out from the array. -# (The else condition is for when data["task-bundles"][ref.repo] doesn't exist.) -_collection(ref) := items { - full_collection := data["task-bundles"][ref.repo] - items := time_lib.acceptable_items(full_collection) -} else := [] +# Evaluates to true if the tasks bundle reference is found in the acceptable +# task bundles data, but also in the data there is a newer version of the task, +# i.e. has a effective_on that is newer than the provided reference's +# effective_on; two references are considered belonging to the same version if +# they have the same tag +_newer_version_exists(ref, acceptable) { + # all records in acceptable task bundles for the given repository + records := acceptable[ref.repo] + + some record in records + + # consider all records, if a match is found via exact digest and there + # exists a newer record for the same tag but it is newer, i.e. has greater + # effective_on value + record.digest == ref.digest + + some other in records + + record.tag == other.tag + + time.parse_rfc3339_ns(other.effective_on) > time.parse_rfc3339_ns(record.effective_on) +} + +# Determine the image reference of the task bundle, if the provided task bundle +# image reference doesn't have the tag within it try to lookup the tag from the +# acceptable task bundles data +_bundle_ref(task, _) := ref { + ref := refs.task_ref(task).bundle + img := image.parse(ref) + img.tag != "" +} + +_bundle_ref(task, acceptable) := ref { + ref_no_tag := refs.task_ref(task).bundle + img := image.parse(ref_no_tag) + img.tag == "" + + # try to find the tag for the reference based on it's digest + records := acceptable[img.repo] + + some record in records + record.digest == img.digest + record.tag + + ref := image.str({ + "digest": img.digest, + "repo": img.repo, + "tag": record.tag, + }) +} + +_bundle_ref(task, acceptable) := ref { + ref_no_tag := refs.task_ref(task).bundle + img := image.parse(ref_no_tag) + img.tag == "" + + records := acceptable[img.repo] + + count([r | some r in records; r.digest == img.digest]) == 0 + + ref := ref_no_tag +} + +_bundle_ref(task, acceptable) := ref { + ref_no_tag := refs.task_ref(task).bundle + img := image.parse(ref_no_tag) + img.tag == "" + + not acceptable[img.repo] + + ref := ref_no_tag +} diff --git a/policy/lib/bundles_test.rego b/policy/lib/bundles_test.rego index 8c2830bc3..01fe1aed9 100644 --- a/policy/lib/bundles_test.rego +++ b/policy/lib/bundles_test.rego @@ -4,6 +4,7 @@ import future.keywords.in import data.lib import data.lib.bundles +import data.lib.image # used as reference bundle data in tests bundle_data := {"registry.img/acceptable": [{ @@ -21,7 +22,7 @@ test_disallowed_task_reference { {"name": "my-task-2", "ref": {}}, ] - expected := {task | some task in tasks} + expected := lib.to_set(tasks) lib.assert_equal(bundles.disallowed_task_reference(tasks), expected) } @@ -31,7 +32,7 @@ test_empty_task_bundle_reference { {"name": "my-task-2", "ref": {"bundle": ""}}, ] - expected := {task | some task in tasks} + expected := lib.to_set(tasks) lib.assert_equal(bundles.empty_task_bundle_reference(tasks), expected) } @@ -47,8 +48,8 @@ test_unpinned_task_bundle { }, ] - expected := {task | some task in tasks} - lib.assert_equal(bundles.unpinned_task_bundle(tasks), expected) + expected := lib.to_set(tasks) + lib.assert_equal(bundles.unpinned_task_bundle(tasks), expected) with data["task-bundles"] as [] } # All good when the most recent bundle is used. @@ -68,76 +69,54 @@ test_acceptable_bundle { test_out_of_date_task_bundle { tasks := [ {"name": "my-task-1", "taskRef": {"bundle": "reg.com/repo@sha256:bcd"}}, - {"name": "my-task-2", "taskRef": {"bundle": "reg.com/repo@sha256:cde"}}, {"name": "my-task-3", "ref": {"bundle": "reg.com/repo@sha256:bcd"}}, - {"name": "my-task-4", "ref": {"bundle": "reg.com/repo@sha256:cde"}}, ] - expected := {task | some task in tasks} + expected := lib.to_set(tasks) lib.assert_equal(bundles.out_of_date_task_bundle(tasks), expected) with data["task-bundles"] as task_bundles } test_unacceptable_task_bundles { tasks := [ - {"name": "my-task-1", "taskRef": {"bundle": "reg.com/repo@sha256:def"}}, - {"name": "my-task-2", "ref": {"bundle": "reg.com/repo@sha256:def"}}, + {"name": "my-task-1", "taskRef": {"bundle": "reg.com/repo@sha256:blah"}}, + {"name": "my-task-2", "ref": {"bundle": "reg.com/repo@sha256:blah"}}, + {"name": "my-task-3", "ref": {"bundle": "wat.com/repo@sha256:blah"}}, ] - expected := {task | some task in tasks} + expected := lib.to_set(tasks) lib.assert_equal(bundles.unacceptable_task_bundle(tasks), expected) with data["task-bundles"] as task_bundles } -test_is_equal { - record := {"digest": "sha256:abc", "tag": "spam"} - - # Exact match - lib.assert_equal(bundles.is_equal(record, {"digest": "sha256:abc", "tag": "spam"}), true) - - # Tag is ignored if digest matches - lib.assert_equal(bundles.is_equal(record, {"digest": "sha256:abc", "tag": "not-spam"}), true) - - # Tag is not required - lib.assert_equal(bundles.is_equal(record, {"digest": "sha256:abc", "tag": ""}), true) - - # When digest is missing on ref, compare tag - lib.assert_equal(bundles.is_equal(record, {"digest": "", "tag": "spam"}), true) - - # If digest does not match, tag is still ignored - lib.assert_equal(bundles.is_equal(record, {"digest": "sha256:bcd", "tag": "spam"}), false) - - # No match is honored when digest is missing - lib.assert_equal(bundles.is_equal(record, {"digest": "", "tag": "not-spam"}), false) -} - task_bundles := {"reg.com/repo": [ { "digest": "sha256:abc", # Allow - "tag": "903d49a833d22f359bce3d67b15b006e1197bae5", + "tag": "v1", "effective_on": "2262-04-11T00:00:00Z", }, { "digest": "sha256:bcd", # Warn - "tag": "b7d8f6ae908641f5f2309ee6a9d6b2b83a56e1af", + "tag": "v1", "effective_on": "2262-03-11T00:00:00Z", }, { "digest": "sha256:cde", # Warn - "tag": "120dda49a6cc3b89516b491e19fe1f3a07f1427f", + "tag": "v1", "effective_on": "2022-02-01T00:00:00Z", }, { "digest": "sha256:def", # Warn - "tag": "903d49a833d22f359bce3d67b15b006e1197bae5", + "tag": "v1", "effective_on": "2021-01-01T00:00:00Z", }, ]} test_acceptable_bundle_is_acceptable { - bundles.is_acceptable(acceptable_bundle_ref) with data["task-bundles"] as bundle_data + bundles._is_acceptable(image.parse(acceptable_bundle_ref)) with data["task-bundles"] as bundle_data } test_unacceptable_bundle_is_unacceptable { - not bundles.is_acceptable("registry.img/unacceptable@sha256:digest") with data["task-bundles"] as bundle_data + ref := "registry.img/unacceptable@sha256:digest" + not bundles._is_acceptable(image.parse(ref)) with data["task-bundles"] as bundle_data } test_missing_required_data { diff --git a/policy/pipeline/task_bundle_test.rego b/policy/pipeline/task_bundle_test.rego index 20893268a..47caa89a2 100644 --- a/policy/pipeline/task_bundle_test.rego +++ b/policy/pipeline/task_bundle_test.rego @@ -36,7 +36,7 @@ test_bundle_unpinned { lib.assert_equal_results(task_bundle.warn, {{ "code": "task_bundle.unpinned_task_bundle", "msg": "Pipeline task 'my-task' uses an unpinned task bundle reference 'reg.com/repo:latest'", - }}) with input.spec.tasks as tasks + }}) with input.spec.tasks as tasks with data["task-bundles"] as [] } test_bundle_reference_valid { @@ -60,6 +60,17 @@ test_acceptable_bundle_up_to_date { with data["task-bundles"] as task_bundles } +# All good when the most recent bundle is used for a version that is still maintained +test_acceptable_bundle_up_to_date_maintained_version { + tasks := [{"name": "my-task", "taskRef": {"bundle": "reg.com/repo@sha256:ghi"}}] + + lib.assert_empty(task_bundle.warn) with input.spec.tasks as tasks + with data["task-bundles"] as task_bundles + + lib.assert_empty(task_bundle.deny) with input.spec.tasks as tasks + with data["task-bundles"] as task_bundles +} + # Warn about out of date bundles that are still acceptable. test_acceptable_bundle_out_of_date_past { tasks := [ @@ -109,25 +120,31 @@ task_bundles := {"reg.com/repo": [ { # Latest bundle, allowed "digest": "sha256:abc", - "tag": "", + "tag": "v2", + "effective_on": "2262-04-11T00:00:00Z", + }, + { + # Latest bundle, diferent tag + "digest": "sha256:ghi", + "tag": "v3", "effective_on": "2262-04-11T00:00:00Z", }, { # Recent bundle effective in the future, allowed but warn to upgrade "digest": "sha256:bcd", - "tag": "", + "tag": "v2", "effective_on": "2262-03-11T00:00:00Z", }, { # Recent bundle effective in the past, allowed but warn to upgrade "digest": "sha256:cde", - "tag": "", + "tag": "v1", "effective_on": "2022-02-01T00:00:00Z", }, { # Old bundle, denied "digest": "sha256:def", - "tag": "", + "tag": "v1", "effective_on": "2021-01-01T00:00:00Z", }, ]} diff --git a/policy/release/attestation_task_bundle_test.rego b/policy/release/attestation_task_bundle_test.rego index a3241fbe4..ba0d78169 100644 --- a/policy/release/attestation_task_bundle_test.rego +++ b/policy/release/attestation_task_bundle_test.rego @@ -70,7 +70,7 @@ test_bundle_unpinned { lib.assert_equal_results(attestation_task_bundle.warn, {{ "code": "attestation_task_bundle.task_ref_bundles_pinned", "msg": expected_msg, - }}) with input.attestations as attestations + }}) with input.attestations as attestations with data["task-bundles"] as [] } test_bundle_reference_valid { @@ -210,25 +210,25 @@ task_bundles := {"reg.com/repo": [ { # Latest bundle, allowed "digest": "sha256:abc", - "tag": "", + "tag": "v2", "effective_on": "2262-04-11T00:00:00Z", }, { # Recent bundle effective in the future, allowed but attestation_task_bundle.warn to upgrade "digest": "sha256:bcd", - "tag": "", + "tag": "v2", "effective_on": "2262-03-11T00:00:00Z", }, { # Recent bundle effective in the past, allowed but attestation_task_bundle.warn to upgrade "digest": "sha256:cde", - "tag": "", + "tag": "v1", "effective_on": "2022-02-01T00:00:00Z", }, { # Old bundle, denied "digest": "sha256:def", - "tag": "", + "tag": "v1", "effective_on": "2021-01-01T00:00:00Z", }, ]}