Skip to content

Commit

Permalink
Merge pull request #1180 from zregvart/issue/EC-838
Browse files Browse the repository at this point in the history
  • Loading branch information
zregvart authored Oct 21, 2024
2 parents c352863 + b00d1c0 commit 2194dba
Show file tree
Hide file tree
Showing 4 changed files with 568 additions and 23 deletions.
18 changes: 9 additions & 9 deletions antora/docs/modules/ROOT/pages/release_policy.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand All @@ -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]
Expand All @@ -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]
Expand All @@ -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]
Expand All @@ -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]
Expand All @@ -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]
Expand Down
7 changes: 7 additions & 0 deletions policy/lib/rule_data.rego
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
200 changes: 186 additions & 14 deletions policy/release/cve/cve.rego
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
}

Expand All @@ -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)
}

Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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(), [])
}

Expand All @@ -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
Expand All @@ -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",
Expand All @@ -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])
}
Loading

0 comments on commit 2194dba

Please sign in to comment.