66# See https://aboutcode.org for more information about AboutCode FOSS projects.
77#
88
9+ from datetime import timedelta
910
1011from django .apps import apps
1112from django .db .models import Exists
1213from django .db .models import OuterRef
14+ from django .utils import timezone
1315
1416# VulnerabilityAnalysis states that indicate a vulnerability has been triaged and addressed.
1517TERMINAL_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+
172226RULE_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