diff --git a/antora/docs/modules/ROOT/pages/release_policy.adoc b/antora/docs/modules/ROOT/pages/release_policy.adoc index e32ee4a9..f6cf392e 100644 --- a/antora/docs/modules/ROOT/pages/release_policy.adoc +++ b/antora/docs/modules/ROOT/pages/release_policy.adoc @@ -395,26 +395,26 @@ This package is responsible for verifying a CVE scan was performed during the bu [#cve__cve_blockers] === link:#cve__cve_blockers[Blocking CVE check] -The SLSA Provenance attestation for the image is inspected to ensure CVEs that have a known fix and meet a certain security level have not been detected. If detected, this policy rule will fail. By default, only CVEs of critical and high security level cause a failure. This is configurable by the rule data key `restrict_cve_security_levels`. The available levels are critical, high, medium, low, and unknown. +The SLSA Provenance attestation for the image is inspected to ensure CVEs that have a known fix and meet a certain security level have not been detected. If detected, this policy rule will fail. By default, only CVEs of critical and high security level cause a failure. This is configurable by the rule data key `restrict_cve_security_levels`. The available levels are critical, high, medium, low, and unknown. In addition to that leeway can be granted per severity using the `cve_leeway` rule data key containing days of allowed leeway, measured as time between found vulnerability's public disclosure date and current effective time, per severity level. *Solution*: Make sure to address any CVE's related to the image. The CVEs are detected by the task that runs a Clair scan and emits a result named `SCAN_OUTPUT`. * Rule type: [rule-type-indicator failure]#FAILURE# * FAILURE message: `Found %d CVE vulnerabilities of %s security level` * Code: `cve.cve_blockers` -* https://github.com/enterprise-contract/ec-policies/blob/{page-origin-refhash}/policy/release/cve/cve.rego#L88[Source, window="_blank"] +* https://github.com/enterprise-contract/ec-policies/blob/{page-origin-refhash}/policy/release/cve/cve.rego#L90[Source, window="_blank"] [#cve__unpatched_cve_blockers] === link:#cve__unpatched_cve_blockers[Blocking unpatched CVE check] -The SLSA Provenance attestation for the image is inspected to ensure CVEs that do NOT have a known fix and meet a certain security level have not been detected. If detected, this policy rule will fail. By default, the list of security levels used by this policy is empty. This is configurable by the rule data key `restrict_unpatched_cve_security_levels`. The available levels are critical, high, medium, low, and unknown. +The SLSA Provenance attestation for the image is inspected to ensure CVEs that do NOT have a known fix and meet a certain security level have not been detected. If detected, this policy rule will fail. By default, the list of security levels used by this policy is empty. This is configurable by the rule data key `restrict_unpatched_cve_security_levels`. The available levels are critical, high, medium, low, and unknown. In addition to that leeway can be granted per severity using the `cve_leeway` rule data key containing days of allowed leeway, measured as time between found vulnerability's public disclosure date and current effective time, per severity level. *Solution*: CVEs without a known fix can only be remediated by either removing the impacted dependency, or by waiting for a fix to be available. The CVEs are detected by the task that emits a result named `SCAN_OUTPUT`. * Rule type: [rule-type-indicator failure]#FAILURE# * FAILURE message: `Found %d unpatched CVE vulnerabilities of %s security level` * Code: `cve.unpatched_cve_blockers` -* https://github.com/enterprise-contract/ec-policies/blob/{page-origin-refhash}/policy/release/cve/cve.rego#L113[Source, window="_blank"] +* https://github.com/enterprise-contract/ec-policies/blob/{page-origin-refhash}/policy/release/cve/cve.rego#L130[Source, window="_blank"] [#cve__cve_results_found] === link:#cve__cve_results_found[CVE scan results found] @@ -426,7 +426,7 @@ Confirm that clair-scan task results are present in the SLSA Provenance attestat * Rule type: [rule-type-indicator failure]#FAILURE# * FAILURE message: `Clair CVE scan results were not found` * Code: `cve.cve_results_found` -* https://github.com/enterprise-contract/ec-policies/blob/{page-origin-refhash}/policy/release/cve/cve.rego#L139[Source, window="_blank"] +* https://github.com/enterprise-contract/ec-policies/blob/{page-origin-refhash}/policy/release/cve/cve.rego#L172[Source, window="_blank"] [#cve__deprecated_cve_result_name] === link:#cve__deprecated_cve_result_name[Deprecated CVE result name] @@ -438,7 +438,7 @@ The `CLAIR_SCAN_RESULT` result name has been deprecated, and has been replaced w * Rule type: [rule-type-indicator warning]#WARNING# * WARNING message: `CVE scan uses deprecated result name` * Code: `cve.deprecated_cve_result_name` -* https://github.com/enterprise-contract/ec-policies/blob/{page-origin-refhash}/policy/release/cve/cve.rego#L66[Source, window="_blank"] +* https://github.com/enterprise-contract/ec-policies/blob/{page-origin-refhash}/policy/release/cve/cve.rego#L68[Source, window="_blank"] [#cve__cve_warnings] === link:#cve__cve_warnings[Non-blocking CVE check] @@ -450,7 +450,7 @@ The SLSA Provenance attestation for the image is inspected to ensure CVEs that h * Rule type: [rule-type-indicator warning]#WARNING# * WARNING message: `Found %d non-blocking CVE vulnerabilities of %s security level` * Code: `cve.cve_warnings` -* https://github.com/enterprise-contract/ec-policies/blob/{page-origin-refhash}/policy/release/cve/cve.rego#L15[Source, window="_blank"] +* https://github.com/enterprise-contract/ec-policies/blob/{page-origin-refhash}/policy/release/cve/cve.rego#L17[Source, window="_blank"] [#cve__unpatched_cve_warnings] === link:#cve__unpatched_cve_warnings[Non-blocking unpatched CVE check] @@ -462,7 +462,7 @@ The SLSA Provenance attestation for the image is inspected to ensure CVEs that d * Rule type: [rule-type-indicator warning]#WARNING# * WARNING message: `Found %d non-blocking unpatched CVE vulnerabilities of %s security level` * Code: `cve.unpatched_cve_warnings` -* https://github.com/enterprise-contract/ec-policies/blob/{page-origin-refhash}/policy/release/cve/cve.rego#L40[Source, window="_blank"] +* https://github.com/enterprise-contract/ec-policies/blob/{page-origin-refhash}/policy/release/cve/cve.rego#L42[Source, window="_blank"] [#cve__rule_data_provided] === link:#cve__rule_data_provided[Rule data provided] @@ -474,7 +474,7 @@ Confirm the expected rule data keys have been provided in the expected format. T * Rule type: [rule-type-indicator failure]#FAILURE# * FAILURE message: `%s` * Code: `cve.rule_data_provided` -* https://github.com/enterprise-contract/ec-policies/blob/{page-origin-refhash}/policy/release/cve/cve.rego#L164[Source, window="_blank"] +* https://github.com/enterprise-contract/ec-policies/blob/{page-origin-refhash}/policy/release/cve/cve.rego#L197[Source, window="_blank"] [#external_parameters_package] == link:#external_parameters_package[External parameters] diff --git a/policy/lib/rule_data.rego b/policy/lib/rule_data.rego index 6657f996..1315cf1f 100644 --- a/policy/lib/rule_data.rego +++ b/policy/lib/rule_data.rego @@ -35,6 +35,13 @@ rule_data_defaults := { "warn_cve_security_levels": [], "restrict_unpatched_cve_security_levels": [], "warn_unpatched_cve_security_levels": ["critical", "high"], + "cve_leeway": { + "critical": 0, + "high": 0, + "medium": 0, + "low": 0, + "unknown": 0, + }, # Used in policy/release/slsa_source_correlated.rego # According to https://pip.pypa.io/en/latest/topics/vcs-support/#vcs-support # and https://spdx.dev/spdx-specification-20-web-version/#h.49x2ik5 diff --git a/policy/release/cve/cve.rego b/policy/release/cve/cve.rego index 3fd7654f..652238dc 100644 --- a/policy/release/cve/cve.rego +++ b/policy/release/cve/cve.rego @@ -11,6 +11,8 @@ package release.cve import rego.v1 import data.lib +import data.lib.image +import data.lib.time as lib_time # METADATA # title: Non-blocking CVE check @@ -33,7 +35,7 @@ import data.lib # - cve.cve_results_found # warn contains result if { - some level, amount in _non_zero_vulnerabilities("warn_cve_security_levels") + some level, amount in _non_zero_vulnerabilities("warn_cve_security_levels", _zero_period) result := lib.result_helper_with_term(rego.metadata.chain(), [amount, level], level) } @@ -59,7 +61,7 @@ warn contains result if { # - cve.cve_results_found # warn contains result if { - some level, amount in _non_zero_unpatched("warn_unpatched_cve_security_levels") + some level, amount in _non_zero_unpatched("warn_unpatched_cve_security_levels", _zero_period) result := lib.result_helper_with_term(rego.metadata.chain(), [amount, level], level) } @@ -92,7 +94,9 @@ warn contains result if { # and meet a certain security level have not been detected. If detected, this policy rule will # fail. By default, only CVEs of critical and high security level cause a failure. This is # configurable by the rule data key `restrict_cve_security_levels`. The available levels are -# critical, high, medium, low, and unknown. +# critical, high, medium, low, and unknown. In addition to that leeway can be granted per severity +# using the `cve_leeway` rule data key containing days of allowed leeway, measured as time between +# found vulnerability's public disclosure date and current effective time, per severity level. # custom: # short_name: cve_blockers # failure_msg: Found %d CVE vulnerabilities of %s security level @@ -106,8 +110,21 @@ warn contains result if { # - cve.cve_results_found # deny contains result if { - some level, amount in _non_zero_vulnerabilities("restrict_cve_security_levels") - result := lib.result_helper_with_term(rego.metadata.chain(), [amount, level], level) + enforced_results := {result | + some level, amount in _non_zero_vulnerabilities("restrict_cve_security_levels", _configured_period) + result := lib.result_helper_with_term(rego.metadata.chain(), [amount, level], level) + } + + leewayed_results := {result | + some level, amount in _leewayed_vulnerabilities("restrict_cve_security_levels") + + result := _with_effective_on( + lib.result_helper_with_term(rego.metadata.chain(), [amount, level], level), + _configured_period[level].end, + ) + } + + some result in (enforced_results | leewayed_results) } # METADATA @@ -117,7 +134,10 @@ deny contains result if { # known fix and meet a certain security level have not been detected. If detected, this policy # rule will fail. By default, the list of security levels used by this policy is empty. This is # configurable by the rule data key `restrict_unpatched_cve_security_levels`. The available levels -# are critical, high, medium, low, and unknown. +# are critical, high, medium, low, and unknown. In addition to that leeway can be granted per +# severity using the `cve_leeway` rule data key containing days of allowed leeway, measured as +# time between found vulnerability's public disclosure date and current effective time, per +# severity level. # custom: # short_name: unpatched_cve_blockers # failure_msg: Found %d unpatched CVE vulnerabilities of %s security level @@ -132,8 +152,21 @@ deny contains result if { # - cve.cve_results_found # deny contains result if { - some level, amount in _non_zero_unpatched("restrict_unpatched_cve_security_levels") - result := lib.result_helper_with_term(rego.metadata.chain(), [amount, level], level) + enforced_results := {result | + some level, amount in _non_zero_unpatched("restrict_unpatched_cve_security_levels", _configured_period) + result := lib.result_helper_with_term(rego.metadata.chain(), [amount, level], level) + } + + leewayed_results := {result | + some level, amount in _leewayed_unpatched_vulnerabilities("restrict_unpatched_cve_security_levels") + + result := _with_effective_on( + lib.result_helper_with_term(rego.metadata.chain(), [amount, level], level), + _configured_period[level].end, + ) + } + + some result in (enforced_results | leewayed_results) } # METADATA @@ -157,7 +190,7 @@ deny contains result if { # NOTE: unpatched vulnerabilities are defined as an optional attribute. The lack of them should # not be considered a violation nor a warning. See details in: # https://github.com/konflux-ci/architecture/blob/main/ADR/0030-tekton-results-naming-convention.md - not _vulnerabilities + not _vulnerabilities(_configured_period) result := lib.result_helper(rego.metadata.chain(), []) } @@ -181,12 +214,76 @@ deny contains result if { result := lib.result_helper(rego.metadata.chain(), [error]) } -_vulnerabilities := vulnerabilities if { +# extracts the clair report attached to the image +_clair_report := report if { + input_image := image.parse(input.image.ref) + + some reports in lib.results_named(_reports_result_name) + report_image := object.union(input_image, {"digest": reports.value[input_image.digest]}) + report_ref := image.str(report_image) + report_manifest := ec.oci.image_manifest(report_ref) + + some layer in report_manifest.layers + layer.mediaType == _report_oci_mime_type + report_blob := object.union(input_image, {"digest": layer.digest}) + report_blob_ref := image.str(report_blob) + + report := json.unmarshal(ec.oci.blob(report_blob_ref)) +} + +# maps vulnerabilities and reports the counts by category (patched/unpatched) +# and severity +_clair_vulnerabilities(period) := vulns if { + reported_vulnerabilities := _clair_report.vulnerabilities + + patched_vulnerabilities := [v | + some v in reported_vulnerabilities + v.fixed_in_version != "" + ] + + unpatched_vulnerabilities := [v | + some v in reported_vulnerabilities + v.fixed_in_version == "" + ] + + vulns := { + "vulnerabilities": { + "critical": _count_by_severity_with_period(patched_vulnerabilities, "critical", period), + "high": _count_by_severity_with_period(patched_vulnerabilities, "high", period), + "medium": _count_by_severity_with_period(patched_vulnerabilities, "medium", period), + "low": _count_by_severity_with_period(patched_vulnerabilities, "low", period), + "unknown": _count_by_severity_with_period(patched_vulnerabilities, "unknown", period), + }, + "unpatched_vulnerabilities": { + "critical": _count_by_severity_with_period(unpatched_vulnerabilities, "critical", period), + "high": _count_by_severity_with_period(unpatched_vulnerabilities, "high", period), + "medium": _count_by_severity_with_period(unpatched_vulnerabilities, "medium", period), + "low": _count_by_severity_with_period(unpatched_vulnerabilities, "low", period), + "unknown": _count_by_severity_with_period(unpatched_vulnerabilities, "unknown", period), + }, + } +} + +# counts the vulnerabilities with the given severity excluding vulnerabilities +# within the given period +_count_by_severity_with_period(vulnerabilities, severity, period) := count([v | + some v in vulnerabilities + lower(v.normalized_severity) == severity + p := period[severity] + time.parse_rfc3339_ns(v.issued) >= p.start + time.parse_rfc3339_ns(v.issued) < p.end +]) + +_vulnerabilities(period) := vulnerabilities if { + vulnerabilities := _clair_vulnerabilities(period).vulnerabilities +} else := vulnerabilities if { some result in lib.results_named(_result_name) vulnerabilities := result.value.vulnerabilities } else := _vulnerabilities_deprecated -_unpatched_vulnerabilities := vulnerabilities if { +_unpatched_vulnerabilities(period) := vulnerabilities if { + vulnerabilities := _clair_vulnerabilities(period).unpatched_vulnerabilities +} else := vulnerabilities if { some result in lib.results_named(_result_name) vulnerabilities := result.value.unpatched_vulnerabilities } else := _unpatched_vulnerabilities_deprecated @@ -205,16 +302,67 @@ _result_name := "SCAN_OUTPUT" _deprecated_result_name := "CLAIR_SCAN_RESULT" -_non_zero_vulnerabilities(key) := _non_zero_levels(key, _vulnerabilities) +_reports_result_name := "REPORTS" -_non_zero_unpatched(key) := _non_zero_levels(key, _unpatched_vulnerabilities) +_report_oci_mime_type := "application/vnd.redhat.clair-report+json" -_non_zero_levels(key, vulnerabilities) := {level: amount | +_non_zero_vulnerabilities(key, period) := _non_zero_levels(key, _vulnerabilities(period)) + +_leewayed_vulnerabilities(key) := {l1: amount | + some l1, amount_with_leeway in _count_vulnerabilities(key, _configured_period) + some l2, amount_without_leeway in _count_vulnerabilities(key, _zero_period) + l1 == l2 + amount := amount_without_leeway - amount_with_leeway + amount > 0 +} + +_non_zero_unpatched(key, period) := _non_zero_levels(key, _unpatched_vulnerabilities(period)) + +_leewayed_unpatched_vulnerabilities(key) := {l1: amount | + some l1, amount_with_leeway in _count_unpatched_vulnerabilities(key, _configured_period) + some l2, amount_without_leeway in _count_unpatched_vulnerabilities(key, _zero_period) + l1 == l2 + amount := amount_without_leeway - amount_with_leeway + amount > 0 +} + +_count_vulnerabilities(key, period) := _count_levels(key, _vulnerabilities(period)) + +_count_unpatched_vulnerabilities(key, period) := _count_levels(key, _unpatched_vulnerabilities(period)) + +_count_levels(key, vulnerabilities) := {level: amount | some level in {a | some a in lib.rule_data(key)} amount := vulnerabilities[level] +} + +_non_zero_levels(key, vulnerabilities) := {level: amount | + some level, amount in _count_levels(key, vulnerabilities) amount > 0 } +_configured_period[severity] := period if { + leeway := lib.rule_data("cve_leeway") + + some severity in {"critical", "high", "medium", "low", "unknown"} + period := { + "start": 0, + "end": time.add_date(lib_time.effective_current_time_ns, 0, 0, leeway[severity] * -1), + } +} + +_zero_period[severity] := period if { + some severity in {"critical", "high", "medium", "low", "unknown"} + period := { + "start": 0, + "end": lib_time.effective_current_time_ns, + } +} + +_with_effective_on(result, effective_on) := object.union( + result, + {"effective_on": time.format([effective_on, "UTC", "2006-01-02T15:04:05Z07:00"])}, +) + _rule_data_errors contains msg if { keys := [ "restrict_cve_security_levels", @@ -238,3 +386,27 @@ _rule_data_errors contains msg if { )[1] msg := sprintf("Rule data %s has unexpected format: %s", [key, violation.error]) } + +_rule_data_errors contains msg if { + value := lib.rule_data("cve_leeway") + leeway_days := { + "type": "integer", + "minimum": 0, + } + some violation in json.match_schema( + value, + { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "properties": { + "critical": leeway_days, + "high": leeway_days, + "medium": leeway_days, + "low": leeway_days, + "unknown": leeway_days, + }, + "additionalProperties": false, + }, + )[1] + msg := sprintf("Rule data cve_leeway has unexpected format: %s", [violation.error]) +} diff --git a/policy/release/cve/cve_test.rego b/policy/release/cve/cve_test.rego index f2d26356..facf2f67 100644 --- a/policy/release/cve/cve_test.rego +++ b/policy/release/cve/cve_test.rego @@ -4,6 +4,7 @@ import rego.v1 import data.lib import data.lib.tekton_test +import data.lib.time as lib_time import data.lib_test import data.release.cve @@ -503,6 +504,371 @@ test_rule_data_provided if { with data.rule_data as d } +test_clair_report if { + report := {"sha256:image_digest": "sha256:report_digest"} + + attestations := [lib_test.att_mock_helper_ref( + cve._reports_result_name, + report, + "clair-scan", + _bundle, + )] + + got := cve._clair_report with input.image.ref as "registry.io/repository/image@sha256:image_digest" + with input.attestations as attestations + with ec.oci.image_manifest as _mock_image_manifest + with ec.oci.blob as _mock_blob + + lib.assert_equal(_clair_report, got) +} + +test_clair_report_fetch_manifest_failure if { + report := {"sha256:image_digest": "sha256:report_digest"} + + attestations := [lib_test.att_mock_helper_ref( + cve._reports_result_name, + report, + "clair-scan", + _bundle, + )] + + not cve._clair_report with input.image.ref as "registry.io/repository/image@sha256:image_digest" + with input.attestations as attestations + with ec.oci.image_manifest as null + with ec.oci.blob as _mock_blob +} + +test_clair_report_fetch_blob_failure if { + report := {"sha256:image_digest": "sha256:report_digest"} + + attestations := [lib_test.att_mock_helper_ref( + cve._reports_result_name, + report, + "clair-scan", + _bundle, + )] + + not cve._clair_report with input.image.ref as "registry.io/repository/image@sha256:image_digest" + with input.attestations as attestations + with ec.oci.image_manifest as _mock_image_manifest + with ec.oci.blob as null +} + +test_clair_vulnerabilities if { + expected := { + "vulnerabilities": { + "critical": 1, + "high": 2, + "medium": 3, + "low": 4, + "unknown": 5, + }, + "unpatched_vulnerabilities": { + "critical": 6, + "high": 7, + "medium": 8, + "low": 9, + "unknown": 10, + }, + } + + p := { + "start": 0, + "end": lib_time.effective_current_time_ns, + } + + period := { + "critical": p, + "high": p, + "medium": p, + "low": p, + "unknown": p, + } + + got := cve._clair_vulnerabilities(period) with cve._clair_report as _clair_report + + lib.assert_equal(expected, got) +} + +test_success_with_full_report if { + reports := {"sha256:image_digest": "sha256:no_vulnerabilities_report_digest"} + + slsav1_task_with_result := tekton_test.slsav1_task_result_ref( + "clair-scan", + [{ + "name": cve._reports_result_name, + "type": "string", + "value": reports, + }], + ) + attestations := [ + lib_test.att_mock_helper_ref( + cve._reports_result_name, + reports, + "clair-scan", + _bundle, + ), + lib_test.mock_slsav1_attestation_with_tasks([tekton_test.slsav1_task_bundle(slsav1_task_with_result, _bundle)]), + ] + lib.assert_empty(cve.deny | cve.warn) with input.image.ref as "registry.io/repository/image@sha256:image_digest" + with input.attestations as attestations + with ec.oci.image_manifest as _mock_image_manifest + with ec.oci.blob as _mock_blob +} + +test_failure_with_full_report if { + reports := {"sha256:image_digest": "sha256:report_digest"} + + slsav1_task_with_result := tekton_test.slsav1_task_result_ref( + "clair-scan", + [{ + "name": cve._reports_result_name, + "type": "string", + "value": reports, + }], + ) + attestations := [ + lib_test.att_mock_helper_ref( + cve._reports_result_name, + reports, + "clair-scan", + _bundle, + ), + lib_test.mock_slsav1_attestation_with_tasks([tekton_test.slsav1_task_bundle(slsav1_task_with_result, _bundle)]), + ] + + expected_deny := { + { + "code": "cve.cve_blockers", + "term": "critical", + "msg": "Found 1 CVE vulnerabilities of critical security level", + }, + { + "code": "cve.cve_blockers", + "term": "high", + "msg": "Found 2 CVE vulnerabilities of high security level", + }, + } + + # regal ignore:line-length + lib.assert_equal_results(cve.deny, expected_deny) with input.image.ref as "registry.io/repository/image@sha256:image_digest" + with input.attestations as attestations + with ec.oci.image_manifest as _mock_image_manifest + with ec.oci.blob as _mock_blob + + expected_warn := { + { + "code": "cve.unpatched_cve_warnings", + "term": "critical", + "msg": "Found 6 non-blocking unpatched CVE vulnerabilities of critical security level", + }, + { + "code": "cve.unpatched_cve_warnings", + "term": "high", + "msg": "Found 7 non-blocking unpatched CVE vulnerabilities of high security level", + }, + } + + # regal ignore:line-length + lib.assert_equal_results(cve.warn, expected_warn) with input.image.ref as "registry.io/repository/image@sha256:image_digest" + with input.attestations as attestations + with ec.oci.image_manifest as _mock_image_manifest + with ec.oci.blob as _mock_blob +} + +test_full_report_fetch_issue if { + reports := {"sha256:image_digest": "sha256:no_vulnerabilities_report_digest"} + + slsav1_task_with_result := tekton_test.slsav1_task_result_ref( + "clair-scan", + [{ + "name": cve._reports_result_name, + "type": "string", + "value": reports, + }], + ) + attestations := [ + lib_test.att_mock_helper_ref( + cve._reports_result_name, + reports, + "clair-scan", + _bundle, + ), + lib_test.mock_slsav1_attestation_with_tasks([tekton_test.slsav1_task_bundle(slsav1_task_with_result, _bundle)]), + ] + + expected := {{ + "code": "cve.cve_results_found", + "msg": "Clair CVE scan results were not found", + }} + + lib.assert_equal_results(cve.deny, expected) with input.image.ref as "registry.io/repository/image@sha256:image_digest" + with input.attestations as attestations + with ec.oci.image_manifest as null + lib.assert_equal_results(cve.deny, expected) with input.image.ref as "registry.io/repository/image@sha256:image_digest" + with input.attestations as attestations + with ec.oci.image_manifest as _mock_image_manifest + with ec.oci.blob as null + + lib.assert_empty(cve.warn) with input.image.ref as "registry.io/repository/image@sha256:image_digest" + with input.attestations as attestations + with ec.oci.image_manifest as null + lib.assert_empty(cve.warn) with input.image.ref as "registry.io/repository/image@sha256:image_digest" + with input.attestations as attestations + with ec.oci.image_manifest as _mock_image_manifest + with ec.oci.blob as null +} + +test_warning_leeway_with_full_report if { + reports := {"sha256:image_digest": "sha256:report_digest"} + + slsav1_task_with_result := tekton_test.slsav1_task_result_ref( + "clair-scan", + [{ + "name": cve._reports_result_name, + "type": "string", + "value": reports, + }], + ) + attestations := [ + lib_test.att_mock_helper_ref( + cve._reports_result_name, + reports, + "clair-scan", + _bundle, + ), + lib_test.mock_slsav1_attestation_with_tasks([tekton_test.slsav1_task_bundle(slsav1_task_with_result, _bundle)]), + ] + expected_deny := { + { + "code": "cve.cve_blockers", + "effective_on": lib_time.default_effective_on, + "msg": "Found 1 CVE vulnerabilities of critical security level", + "term": "critical", + }, + { + "code": "cve.cve_blockers", + "effective_on": "2022-03-26T00:00:00Z", # 2022-03-26 + 10 days = 2022-04-05 + "msg": "Found 2 CVE vulnerabilities of high security level", + "term": "high", + }, + { + "code": "cve.unpatched_cve_blockers", + "effective_on": lib_time.default_effective_on, + "msg": "Found 6 unpatched CVE vulnerabilities of critical security level", + "term": "critical", + }, + { + "code": "cve.unpatched_cve_blockers", + "effective_on": "2022-03-26T00:00:00Z", # 2022-03-26 + 10 days = 2022-04-05 + "msg": "Found 7 unpatched CVE vulnerabilities of high security level", + "term": "high", + }, + } + + # regal ignore:line-length + lib.assert_equal_results_no_collections(cve.deny, expected_deny) with input.image.ref as "registry.io/repository/image@sha256:image_digest" + with input.attestations as attestations + with ec.oci.image_manifest as _mock_image_manifest + with ec.oci.blob as _mock_blob + with data.rule_data.cve_leeway as {"critical": 9, "high": 10} + with data.rule_data.restrict_unpatched_cve_security_levels as ["critical", "high"] + with data.rule_data.warn_unpatched_cve_security_levels as [] + with lib_time.effective_current_time_ns as time.parse_rfc3339_ns("2022-04-05T00:00:00Z") + + lib.assert_empty(cve.warn) with input.image.ref as "registry.io/repository/image@sha256:image_digest" + with input.attestations as attestations + with ec.oci.image_manifest as _mock_image_manifest + with ec.oci.blob as _mock_blob + with data.rule_data.cve_leeway as {"critical": 9, "high": 10} + with data.rule_data.restrict_unpatched_cve_security_levels as ["critical", "high"] + with data.rule_data.warn_unpatched_cve_security_levels as [] + with lib_time.effective_current_time_ns as time.parse_rfc3339_ns("2022-04-05T00:00:00Z") +} + +test_leeway_rule_data_check if { + d := {"cve_leeway": { + # wrong key + "blooper": 1, + # wrong type + "critical": "one", + # negative number + "high": -10, + # all good + "medium": 10, + }} + + expected := { + { + "code": "cve.rule_data_provided", + "msg": "Rule data cve_leeway has unexpected format: (Root): Additional property blooper is not allowed", + }, + { + "code": "cve.rule_data_provided", + "msg": "Rule data cve_leeway has unexpected format: critical: Invalid type. Expected: integer, given: string", + }, + { + "code": "cve.rule_data_provided", + "msg": "Rule data cve_leeway has unexpected format: high: Must be greater than or equal to 0", + }, + } + + attestations := [lib_test.att_mock_helper_ref( + cve._result_name, + { + "vulnerabilities": _dummy_counts_zero_high, + "unpatched_vulnerabilities": _dummy_counts_zero_high, + }, + "clair-scan", + _bundle, + )] + lib.assert_equal_results(cve.deny, expected) with input.attestations as attestations + with data.rule_data as d +} + +_fingerprints(a, b) := [v | some n in numbers.range(a, b); v := sprintf("%d", [n])] + +_vulns(fingerprits, template) := {v | + some fingerprint in fingerprits + v := {fingerprint: template} +} + +_vuln(severity, fixed_in, issued) := { + "fixed_in_version": fixed_in, + "normalized_severity": severity, + "issued": issued, +} + +# `opa fmt` is causing this +# regal ignore:line-length +vulnerabilities := object.union_n(lib.to_array(((((_vulns(_fingerprints(1, 1), _vuln("Critical", "1.0", "2022-03-26T00:00:00Z")) | _vulns(_fingerprints(2, 3), _vuln("High", "1.0", "2022-03-26T00:00:00Z"))) | _vulns(_fingerprints(4, 6), _vuln("Medium", "1.0", "2022-03-26T00:00:00Z"))) | _vulns(_fingerprints(7, 10), _vuln("Low", "1.0", "2022-03-26T00:00:00Z"))) | _vulns(_fingerprints(11, 15), _vuln("Unknown", "1.0", "2022-03-26T00:00:00Z"))))) + +# `opa fmt` is causing this +# regal ignore:line-length +unpatched_vulnerabilities := object.union_n(lib.to_array(((((_vulns(_fingerprints(16, 21), _vuln("Critical", "", "2022-03-26T00:00:00Z")) | _vulns(_fingerprints(22, 28), _vuln("High", "", "2022-03-26T00:00:00Z"))) | _vulns(_fingerprints(29, 36), _vuln("Medium", "", "2022-03-26T00:00:00Z"))) | _vulns(_fingerprints(37, 45), _vuln("Low", "", "2022-03-26T00:00:00Z"))) | _vulns(_fingerprints(46, 55), _vuln("Unknown", "", "2022-03-26T00:00:00Z"))))) + +_clair_report := {"vulnerabilities": object.union(vulnerabilities, unpatched_vulnerabilities)} + +_manifests := { + "registry.io/repository/image@sha256:report_digest": {"layers": [{ + "mediaType": cve._report_oci_mime_type, + "digest": "sha256:report_blob_digest", + }]}, + "registry.io/repository/image@sha256:no_vulnerabilities_report_digest": {"layers": [{ + "mediaType": cve._report_oci_mime_type, + "digest": "sha256:no_vulnerabilities_report_blob_digest", + }]}, +} + +_blobs := { + "registry.io/repository/image@sha256:report_blob_digest": json.marshal(_clair_report), + "registry.io/repository/image@sha256:no_vulnerabilities_report_blob_digest": json.marshal({"vulnerabilities": {}}), +} + +_mock_image_manifest(ref) := _manifests[ref] + +_mock_blob(ref) := _blobs[ref] + _bundle := "registry.img/spam@sha256:4e388ab32b10dc8dbc7e28144f552830adc74787c1e2c0824032078a79f227fb" _dummy_counts := {"critical": 1, "high": 10, "medium": 20, "low": 300, "unknown": 2}