Skip to content

Commit

Permalink
Merge pull request #797 from lcarva/EC-216
Browse files Browse the repository at this point in the history
Add support for unpatched_vulnerabilities
  • Loading branch information
lcarva authored Nov 9, 2023
2 parents c4603b8 + f0ef51a commit fdcdfa4
Show file tree
Hide file tree
Showing 3 changed files with 174 additions and 33 deletions.
2 changes: 2 additions & 0 deletions policy/lib/rule_data.rego
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ rule_data_defaults := {
# Valid levels: "critical", "high", "medium", "low", and "unknown"
"restrict_cve_security_levels": ["critical", "high"],
"warn_cve_security_levels": [],
"restrict_unpatched_cve_security_levels": [],
"warn_unpatched_cve_security_levels": ["critical", "high"],
# 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
94 changes: 78 additions & 16 deletions policy/release/cve.rego
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,11 @@ import data.lib
# METADATA
# title: Blocking CVE check
# description: >-
# The SLSA Provenance attestation for the image is inspected to ensure CVEs
# of certain security levels 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.
# custom:
# short_name: cve_blockers
# failure_msg: Found %d CVE vulnerabilities of %s security level
Expand All @@ -36,19 +35,44 @@ import data.lib
# - cve.cve_results_found
#
deny contains result if {
some level, amount in _non_zero_levels("restrict_cve_security_levels")
some level, amount in _non_zero_vulnerabilities("restrict_cve_security_levels")
result := lib.result_helper_with_term(rego.metadata.chain(), [amount, level], level)
}

# METADATA
# title: Blocking unpatched CVE check
# description: >-
# 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.
# custom:
# short_name: unpatched_cve_blockers
# failure_msg: Found %d unpatched CVE vulnerabilities of %s security 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 `CLAIR_SCAN_RESULT`.
# collections:
# - minimal
# - redhat
# depends_on:
# - 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)
}

# METADATA
# title: Non-blocking CVE check
# description: >-
# The SLSA Provenance attestation for the image is inspected to ensure CVEs
# of certain security levels have not been detected. If detected, this policy
# rule will raise a warning. By default, the list of CVE security levels used
# by this policy is empty. However, this is configurable by the rule data key
# `warn_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
# raise a warning. By default, the list of CVE security levels used by this policy is empty.
# However, this is configurable by the rule data key `warn_cve_security_levels`. The available
# levels are critical, high, medium, low, and unknown.
# custom:
# short_name: cve_warnings
# failure_msg: Found %d non-blocking CVE vulnerabilities of %s security level
Expand All @@ -62,7 +86,33 @@ deny contains result if {
# - cve.cve_results_found
#
warn contains result if {
some level, amount in _non_zero_levels("warn_cve_security_levels")
some level, amount in _non_zero_vulnerabilities("warn_cve_security_levels")
result := lib.result_helper_with_term(rego.metadata.chain(), [amount, level], level)
}

# METADATA
# title: Non-blocking unpatched CVE check
# description: >-
# 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 raise a warning. By default, only CVEs of critical and high security level cause a
# warning. This is configurable by the rule data key `warn_unpatched_cve_security_levels`. The
# available levels are critical, high, medium, low, and unknown.
# custom:
# short_name: unpatched_cve_warnings
# failure_msg: Found %d non-blocking unpatched CVE vulnerabilities of %s security 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 `CLAIR_SCAN_RESULT`.
# collections:
# - minimal
# - redhat
# depends_on:
# - cve.cve_results_found
#
warn contains result if {
some level, amount in _non_zero_unpatched("warn_unpatched_cve_security_levels")
result := lib.result_helper_with_term(rego.metadata.chain(), [amount, level], level)
}

Expand All @@ -84,6 +134,9 @@ warn contains result if {
# - attestation_type.known_attestation_type
#
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://redhat-appstudio.github.io/book/ADR/0030-tekton-results-naming-convention.html
not _vulnerabilities
result := lib.result_helper(rego.metadata.chain(), [])
}
Expand All @@ -93,10 +146,19 @@ _vulnerabilities := vulnerabilities if {
vulnerabilities := result.value.vulnerabilities
}

_unpatched_vulnerabilities := vulnerabilities if {
some result in lib.results_named(_result_name)
vulnerabilities := result.value.unpatched_vulnerabilities
}

_result_name := "CLAIR_SCAN_RESULT"

_non_zero_levels(key) := {level: amount |
_non_zero_vulnerabilities(key) := _non_zero_levels(key, _vulnerabilities)

_non_zero_unpatched(key) := _non_zero_levels(key, _unpatched_vulnerabilities)

_non_zero_levels(key, vulnerabilities) := {level: amount |
some level in {a | some a in lib.rule_data(key)}
amount := _vulnerabilities[level]
amount := vulnerabilities[level]
amount > 0
}
111 changes: 94 additions & 17 deletions policy/release/cve_test.rego
Original file line number Diff line number Diff line change
Expand Up @@ -15,19 +15,25 @@ test_success if {
[{
"name": cve._result_name,
"type": "string",
"value": {"vulnerabilities": _dummy_counts_zero_high},
"value": {
"vulnerabilities": _dummy_counts_zero_high,
"unpatched_vulnerabilities": _dummy_counts_zero_high,
},
}],
)
attestations := [
lib_test.att_mock_helper_ref(
cve._result_name,
{"vulnerabilities": _dummy_counts_zero_high},
{
"vulnerabilities": _dummy_counts_zero_high,
"unpatched_vulnerabilities": _dummy_counts_zero_high,
},
"clair-scan",
_bundle,
),
lib_test.mock_slsav1_attestation_with_tasks([tkn_test.slsav1_task_bundle(slsav1_task_with_result, _bundle)]),
]
lib.assert_empty(cve.deny) with input.attestations as attestations
lib.assert_empty(cve.deny | cve.warn) with input.attestations as attestations
}

test_success_with_rule_data if {
Expand All @@ -36,20 +42,27 @@ test_success_with_rule_data if {
[{
"name": cve._result_name,
"type": "string",
"value": {"vulnerabilities": _dummy_counts},
"value": {
"vulnerabilities": _dummy_counts,
"unpatched_vulnerabilities": _dummy_counts,
},
}],
)
attestations := [
lib_test.att_mock_helper_ref(
cve._result_name,
{"vulnerabilities": _dummy_counts},
{
"vulnerabilities": _dummy_counts,
"unpatched_vulnerabilities": _dummy_counts,
},
"clair-scan",
_bundle,
),
lib_test.mock_slsav1_attestation_with_tasks([tkn_test.slsav1_task_bundle(slsav1_task_with_result, _bundle)]),
]
lib.assert_empty(cve.deny) with input.attestations as attestations
lib.assert_empty(cve.deny | cve.warn) with input.attestations as attestations
with data.rule_data.restrict_cve_security_levels as ["spam"]
with data.rule_data.warn_unpatched_cve_security_levels as ["spam"]
}

test_failure if {
Expand All @@ -58,19 +71,25 @@ test_failure if {
[{
"name": cve._result_name,
"type": "string",
"value": {"vulnerabilities": _dummy_counts},
"value": {
"vulnerabilities": _dummy_counts,
"unpatched_vulnerabilities": _dummy_counts,
},
}],
)
attestations := [
lib_test.att_mock_helper_ref(
cve._result_name,
{"vulnerabilities": _dummy_counts},
{
"vulnerabilities": _dummy_counts,
"unpatched_vulnerabilities": _dummy_counts,
},
"clair-scan",
_bundle,
),
lib_test.mock_slsav1_attestation_with_tasks([tkn_test.slsav1_task_bundle(slsav1_task_with_result, _bundle)]),
]
expected := {
expected_deny := {
{
"code": "cve.cve_blockers",
"term": "critical",
Expand All @@ -82,22 +101,29 @@ test_failure if {
"msg": "Found 10 CVE vulnerabilities of high security level",
},
}
lib.assert_equal_results(cve.deny, expected) with input.attestations as attestations
lib.assert_equal_results(cve.deny, expected_deny) with input.attestations as attestations
}

test_failure_with_rule_data if {
_custom_counts := {"spam": 1, "bacon": 2, "eggs": 3}
slsav1_task_with_result := tkn_test.slsav1_task_result_ref(
"clair-scan",
[{
"name": cve._result_name,
"type": "string",
"value": {"vulnerabilities": {"spam": 1, "bacon": 2, "eggs": 3}},
"value": {
"vulnerabilities": _custom_counts,
"unpatched_vulnerabilities": _custom_counts,
},
}],
)
attestations := [
lib_test.att_mock_helper_ref(
cve._result_name,
{"vulnerabilities": {"spam": 1, "bacon": 2, "eggs": 3}},
{
"vulnerabilities": _custom_counts,
"unpatched_vulnerabilities": _custom_counts,
},
"clair-scan",
_bundle,
),
Expand All @@ -114,9 +140,20 @@ test_failure_with_rule_data if {
"term": "bacon",
"msg": "Found 2 CVE vulnerabilities of bacon security level",
},
{
"code": "cve.unpatched_cve_blockers",
"term": "spam",
"msg": "Found 1 unpatched CVE vulnerabilities of spam security level",
},
{
"code": "cve.unpatched_cve_blockers",
"term": "bacon",
"msg": "Found 2 unpatched CVE vulnerabilities of bacon security level",
},
}
lib.assert_equal_results(cve.deny, expected) with input.attestations as attestations
with data.rule_data.restrict_cve_security_levels as ["spam", "bacon"]
with data.rule_data.restrict_unpatched_cve_security_levels as ["spam", "bacon"]
}

test_warn if {
Expand All @@ -125,19 +162,37 @@ test_warn if {
[{
"name": cve._result_name,
"type": "string",
"value": {"vulnerabilities": _dummy_counts},
"value": {
"vulnerabilities": _dummy_counts,
"unpatched_vulnerabilities": _dummy_counts,
},
}],
)
attestations := [
lib_test.att_mock_helper_ref(
cve._result_name,
{"vulnerabilities": _dummy_counts},
{
"vulnerabilities": _dummy_counts,
"unpatched_vulnerabilities": _dummy_counts,
},
"clair-scan",
_bundle,
),
lib_test.mock_slsav1_attestation_with_tasks([tkn_test.slsav1_task_bundle(slsav1_task_with_result, _bundle)]),
]
lib.assert_empty(cve.warn) with input.attestations as attestations
expected := {
{
"code": "cve.unpatched_cve_warnings",
"term": "critical",
"msg": "Found 1 non-blocking unpatched CVE vulnerabilities of critical security level",
},
{
"code": "cve.unpatched_cve_warnings",
"term": "high",
"msg": "Found 10 non-blocking unpatched CVE vulnerabilities of high security level",
},
}
lib.assert_equal_results(cve.warn, expected) with input.attestations as attestations
}

test_warn_with_rule_data if {
Expand All @@ -146,13 +201,19 @@ test_warn_with_rule_data if {
[{
"name": cve._result_name,
"type": "string",
"value": {"vulnerabilities": _dummy_counts},
"value": {
"vulnerabilities": _dummy_counts,
"unpatched_vulnerabilities": _dummy_counts,
},
}],
)
attestations := [
lib_test.att_mock_helper_ref(
cve._result_name,
{"vulnerabilities": _dummy_counts},
{
"vulnerabilities": _dummy_counts,
"unpatched_vulnerabilities": _dummy_counts,
},
"clair-scan",
_bundle,
),
Expand All @@ -174,9 +235,25 @@ test_warn_with_rule_data if {
"term": "unknown",
"msg": "Found 2 non-blocking CVE vulnerabilities of unknown security level",
},
{
"code": "cve.unpatched_cve_warnings",
"term": "medium",
"msg": "Found 20 non-blocking unpatched CVE vulnerabilities of medium security level",
},
{
"code": "cve.unpatched_cve_warnings",
"term": "low",
"msg": "Found 300 non-blocking unpatched CVE vulnerabilities of low security level",
},
{
"code": "cve.unpatched_cve_warnings",
"term": "unknown",
"msg": "Found 2 non-blocking unpatched CVE vulnerabilities of unknown security level",
},
}
lib.assert_equal_results(cve.warn, expected) with input.attestations as attestations
with data.rule_data.warn_cve_security_levels as ["medium", "low", "unknown"]
with data.rule_data.warn_unpatched_cve_security_levels as ["medium", "low", "unknown"]
}

test_missing_cve_scan_result if {
Expand Down

0 comments on commit fdcdfa4

Please sign in to comment.