Skip to content

Commit 5b6c9b7

Browse files
committed
refine rules filtering system
Signed-off-by: tdruez <tdruez@aboutcode.org>
1 parent d46f510 commit 5b6c9b7

3 files changed

Lines changed: 45 additions & 39 deletions

File tree

policy/rules.py

Lines changed: 43 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,13 @@ def get_package_filter(self):
3535
"""Return queryset filter kwargs for ProductPackage to identify violating packages."""
3636
return {}
3737

38+
def filter_queryset(self, queryset, parameters=None):
39+
"""Filter a ProductPackage queryset to packages that violate this rule."""
40+
package_filter = self.get_package_filter()
41+
if package_filter:
42+
return queryset.filter(**package_filter)
43+
return queryset
44+
3845

3946
class PackageBaseRule(BaseRule):
4047
"""Base for rules that count packages matching a fixed filter within a product."""
@@ -112,27 +119,24 @@ class VulnerabilityDetectedRule(BaseRule):
112119
rule_type = "vulnerability_detected"
113120
label = "Vulnerability Detected"
114121
severity = "error"
115-
description = "Detects packages with at least one known vulnerability (non-null risk score)."
122+
description = "Detects packages with at least one known vulnerability."
116123
parameters_schema = {
117124
"min_risk_score": "Minimum risk score (0.0-10.0). Default: any vulnerability.",
118125
}
119126

120-
def get_package_filter(self):
121-
return {"package__risk_score__isnull": False}
122-
123-
def count_violations(self, product, threshold, parameters):
124-
Package = apps.get_model("component_catalog", "package")
125-
126-
packages = Package.objects.filter(
127-
productpackages__product=product,
128-
risk_score__isnull=False,
129-
)
130-
127+
def filter_queryset(self, queryset, parameters=None):
128+
parameters = parameters or {}
131129
min_risk_score = parameters.get("min_risk_score")
132130
if min_risk_score is not None:
133-
packages = packages.filter(risk_score__gte=min_risk_score)
131+
return queryset.filter(
132+
package__affected_by_vulnerabilities__risk_score__gte=min_risk_score
133+
)
134+
return queryset.filter(package__affected_by_vulnerabilities__isnull=False)
134135

135-
count = packages.count()
136+
def count_violations(self, product, threshold, parameters):
137+
ProductPackage = apps.get_model("product_portfolio", "productpackage")
138+
product_packages = ProductPackage.objects.filter(product=product)
139+
count = self.filter_queryset(product_packages, parameters).distinct().count()
136140
return count if count > threshold else 0
137141

138142

@@ -144,28 +148,28 @@ class UnresolvedVulnerabilityCountRule(BaseRule):
144148
"Detects packages with known vulnerabilities that have not been triaged or addressed."
145149
)
146150

147-
def count_violations(self, product, threshold, parameters):
151+
def filter_queryset(self, queryset, parameters=None):
148152
PackageAffectedByVulnerability = apps.get_model(
149153
"component_catalog", "packageaffectedbyvulnerability"
150154
)
151155
VulnerabilityAnalysis = apps.get_model("vulnerabilities", "vulnerabilityanalysis")
152156

153157
terminal_analysis = VulnerabilityAnalysis.objects.filter(
154-
product=product,
158+
product_package=OuterRef(OuterRef("pk")),
155159
state__in=TERMINAL_VULNERABILITY_STATES,
156-
package=OuterRef("package"),
157160
vulnerability=OuterRef("vulnerability"),
158161
)
159-
160-
count = (
161-
PackageAffectedByVulnerability.objects.filter(
162-
package__productpackages__product=product,
163-
)
164-
.annotate(has_terminal_analysis=Exists(terminal_analysis))
165-
.filter(has_terminal_analysis=False)
166-
.distinct()
167-
.count()
162+
unresolved_link = (
163+
PackageAffectedByVulnerability.objects.filter(package=OuterRef("package"))
164+
.annotate(has_terminal=Exists(terminal_analysis))
165+
.filter(has_terminal=False)
168166
)
167+
return queryset.filter(Exists(unresolved_link))
168+
169+
def count_violations(self, product, threshold, parameters):
170+
ProductPackage = apps.get_model("product_portfolio", "productpackage")
171+
product_packages = ProductPackage.objects.filter(product=product)
172+
count = self.filter_queryset(product_packages).distinct().count()
169173
return count if count > threshold else 0
170174

171175

@@ -187,35 +191,37 @@ class StaleVulnerabilityRule(BaseRule):
187191
),
188192
}
189193

190-
def count_violations(self, product, threshold, parameters):
194+
def filter_queryset(self, queryset, parameters=None):
191195
PackageAffectedByVulnerability = apps.get_model(
192196
"component_catalog", "packageaffectedbyvulnerability"
193197
)
194198
VulnerabilityAnalysis = apps.get_model("vulnerabilities", "vulnerabilityanalysis")
195199

200+
parameters = parameters or {}
196201
max_days = parameters.get("max_days", 30)
197202
min_risk_score = parameters.get("min_risk_score", 8.0)
198203
cutoff_date = timezone.now() - timedelta(days=max_days)
199204

200205
terminal_analysis = VulnerabilityAnalysis.objects.filter(
201-
product=product,
206+
product_package=OuterRef(OuterRef("pk")),
202207
state__in=TERMINAL_VULNERABILITY_STATES,
203-
package=OuterRef("package"),
204208
vulnerability=OuterRef("vulnerability"),
205209
)
206-
207-
count = (
210+
stale_link = (
208211
PackageAffectedByVulnerability.objects.filter(
209-
package__productpackages__product=product,
212+
package=OuterRef("package"),
210213
vulnerability__risk_score__gte=min_risk_score,
211214
detected_date__lte=cutoff_date,
212215
)
213-
.annotate(has_terminal_analysis=Exists(terminal_analysis))
214-
.filter(has_terminal_analysis=False)
215-
.values("package_id")
216-
.distinct()
217-
.count()
216+
.annotate(has_terminal=Exists(terminal_analysis))
217+
.filter(has_terminal=False)
218218
)
219+
return queryset.filter(Exists(stale_link))
220+
221+
def count_violations(self, product, threshold, parameters):
222+
ProductPackage = apps.get_model("product_portfolio", "productpackage")
223+
product_packages = ProductPackage.objects.filter(product=product)
224+
count = self.filter_queryset(product_packages, parameters).distinct().count()
219225
return count if count > threshold else 0
220226

221227

policy/tests/test_rules.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,7 @@ def setUp(self):
157157

158158
def test_counts_packages_with_any_vulnerability(self):
159159
package = make_package(self.dataspace)
160-
make_vulnerability(self.dataspace, affecting=package, risk_score=5.0)
160+
make_vulnerability(self.dataspace, affecting=package)
161161
make_product_package(self.product, package=package)
162162
make_product_package(self.product, package=make_package(self.dataspace))
163163
count = VulnerabilityDetectedRule().count_violations(self.product, 0, {})

product_portfolio/filters.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -429,7 +429,7 @@ def filter_by_policy_rule(self, queryset, name, value):
429429
handler = RULE_REGISTRY.get(value)
430430
if not handler:
431431
return queryset
432-
return queryset.filter(**handler.get_package_filter())
432+
return handler.filter_queryset(queryset).distinct()
433433

434434
def __init__(self, *args, **kwargs):
435435
super().__init__(*args, **kwargs)

0 commit comments

Comments
 (0)