Skip to content

Commit c7e9765

Browse files
committed
add Stale Vulnerability rule
Signed-off-by: tdruez <tdruez@aboutcode.org>
1 parent e0018cc commit c7e9765

1 file changed

Lines changed: 55 additions & 0 deletions

File tree

policy/rules.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,12 @@
66
# See https://aboutcode.org for more information about AboutCode FOSS projects.
77
#
88

9+
from datetime import timedelta
910

1011
from django.apps import apps
1112
from django.db.models import Exists
1213
from django.db.models import OuterRef
14+
from django.utils import timezone
1315

1416
# VulnerabilityAnalysis states that indicate a vulnerability has been triaged and addressed.
1517
TERMINAL_VULNERABILITY_STATES = ["resolved", "resolved_with_pedigree", "not_affected"]
@@ -169,6 +171,58 @@ def count_violations(self, product, threshold, parameters):
169171
return count if count > threshold else 0
170172

171173

174+
class StaleVulnerabilityRule(BaseRule):
175+
rule_type = "stale_vulnerability"
176+
label = "Stale Vulnerability"
177+
severity = "error"
178+
description = (
179+
"Detects packages with a high-risk vulnerability that has remained unaddressed "
180+
"beyond a configured delay. The delay is measured from the detected_date on the "
181+
"package-vulnerability link, i.e. when the vulnerability was first imported for "
182+
"that specific package."
183+
)
184+
parameters_schema = {
185+
"max_days": (
186+
"Maximum number of days a vulnerability may remain unaddressed before flagging. "
187+
"Default: 30."
188+
),
189+
"min_risk_score": (
190+
"Only consider vulnerabilities with at least this risk score. Default: 8.0."
191+
),
192+
}
193+
194+
def count_violations(self, product, threshold, parameters):
195+
PackageAffectedByVulnerability = apps.get_model(
196+
"component_catalog", "packageaffectedbyvulnerability"
197+
)
198+
VulnerabilityAnalysis = apps.get_model("vulnerabilities", "vulnerabilityanalysis")
199+
200+
max_days = parameters.get("max_days", 30)
201+
min_risk_score = parameters.get("min_risk_score", 8.0)
202+
cutoff_date = timezone.now() - timedelta(days=max_days)
203+
204+
terminal_analysis = VulnerabilityAnalysis.objects.filter(
205+
product=product,
206+
state__in=TERMINAL_VULNERABILITY_STATES,
207+
package=OuterRef("package"),
208+
vulnerability=OuterRef("vulnerability"),
209+
)
210+
211+
count = (
212+
PackageAffectedByVulnerability.objects.filter(
213+
package__productpackages__product=product,
214+
vulnerability__risk_score__gte=min_risk_score,
215+
detected_date__lte=cutoff_date,
216+
)
217+
.annotate(has_terminal_analysis=Exists(terminal_analysis))
218+
.filter(has_terminal_analysis=False)
219+
.values("package_id")
220+
.distinct()
221+
.count()
222+
)
223+
return count if count > threshold else 0
224+
225+
172226
RULE_REGISTRY = {
173227
UsagePolicyErrorRule.rule_type: UsagePolicyErrorRule(),
174228
UsagePolicyWarningRule.rule_type: UsagePolicyWarningRule(),
@@ -177,4 +231,5 @@ def count_violations(self, product, threshold, parameters):
177231
LicenseCoverageGapRule.rule_type: LicenseCoverageGapRule(),
178232
VulnerabilityDetectedRule.rule_type: VulnerabilityDetectedRule(),
179233
UnresolvedVulnerabilityCountRule.rule_type: UnresolvedVulnerabilityCountRule(),
234+
StaleVulnerabilityRule.rule_type: StaleVulnerabilityRule(),
180235
}

0 commit comments

Comments
 (0)