Skip to content

Commit 07bee74

Browse files
committed
Add tests
Signed-off-by: Tushar Goel <tushar.goel.dav@gmail.com>
1 parent d15aa05 commit 07bee74

File tree

4 files changed

+161
-69
lines changed

4 files changed

+161
-69
lines changed

vulnerabilities/models.py

Lines changed: 95 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,15 @@
1010
import hashlib
1111
import json
1212
import logging
13-
import typing
1413
from contextlib import suppress
1514
from functools import cached_property
16-
from typing import Optional
15+
from itertools import groupby
16+
from operator import attrgetter
1717
from typing import Union
1818

19+
from cvss.exceptions import CVSS2MalformedError
20+
from cvss.exceptions import CVSS3MalformedError
21+
from cvss.exceptions import CVSS4MalformedError
1922
from cwe2.database import Database
2023
from django.contrib.auth import get_user_model
2124
from django.contrib.auth.models import UserManager
@@ -45,6 +48,7 @@
4548

4649
from aboutcode import hashid
4750
from vulnerabilities import utils
51+
from vulnerabilities.severity_systems import EPSS
4852
from vulnerabilities.severity_systems import SCORING_SYSTEMS
4953
from vulnerabilities.utils import normalize_purl
5054
from vulnerabilities.utils import purl_to_dict
@@ -371,6 +375,95 @@ def get_related_purls(self):
371375
"""
372376
return [p.package_url for p in self.packages.distinct().all()]
373377

378+
def aggregate_fixed_and_affected_packages(self):
379+
from vulnerabilities.views import get_purl_version_class
380+
381+
sorted_fixed_by_packages = self.fixed_by_packages.filter(is_ghost=False).order_by(
382+
"type", "namespace", "name", "qualifiers", "subpath"
383+
)
384+
385+
sorted_affected_packages = self.affected_packages.all()
386+
387+
grouped_fixed_by_packages = {
388+
key: list(group)
389+
for key, group in groupby(
390+
sorted_fixed_by_packages,
391+
key=attrgetter("type", "namespace", "name", "qualifiers", "subpath"),
392+
)
393+
}
394+
395+
all_affected_fixed_by_matches = []
396+
397+
for sorted_affected_package in sorted_affected_packages:
398+
affected_fixed_by_matches = {
399+
"affected_package": sorted_affected_package,
400+
"matched_fixed_by_packages": [],
401+
}
402+
403+
# Build the key to find matching group
404+
key = (
405+
sorted_affected_package.type,
406+
sorted_affected_package.namespace,
407+
sorted_affected_package.name,
408+
sorted_affected_package.qualifiers,
409+
sorted_affected_package.subpath,
410+
)
411+
412+
# Get matching group from pre-grouped fixed_by_packages
413+
matching_fixed_packages = grouped_fixed_by_packages.get(key, [])
414+
415+
# Get version classes for comparison
416+
affected_version_class = get_purl_version_class(sorted_affected_package)
417+
affected_version = affected_version_class(sorted_affected_package.version)
418+
419+
# Compare versions and filter valid matches
420+
matched_fixed_by_packages = [
421+
fixed_by_package.purl
422+
for fixed_by_package in matching_fixed_packages
423+
if get_purl_version_class(fixed_by_package)(fixed_by_package.version)
424+
> affected_version
425+
]
426+
427+
affected_fixed_by_matches["matched_fixed_by_packages"] = matched_fixed_by_packages
428+
all_affected_fixed_by_matches.append(affected_fixed_by_matches)
429+
return sorted_fixed_by_packages, sorted_affected_packages, all_affected_fixed_by_matches
430+
431+
def get_severity_vectors_and_values(self):
432+
"""
433+
Collect severity vectors and values, excluding EPSS scoring systems and handling errors gracefully.
434+
"""
435+
severity_vectors = []
436+
severity_values = set()
437+
438+
# Exclude EPSS scoring system
439+
base_severities = self.severities.exclude(scoring_system=EPSS.identifier)
440+
441+
# QuerySet for severities with valid scoring_elements and scoring_system in SCORING_SYSTEMS
442+
valid_scoring_severities = base_severities.filter(
443+
scoring_elements__isnull=False, scoring_system__in=SCORING_SYSTEMS.keys()
444+
)
445+
446+
for severity in valid_scoring_severities:
447+
try:
448+
vector_values = SCORING_SYSTEMS[severity.scoring_system].get(
449+
severity.scoring_elements
450+
)
451+
if vector_values:
452+
severity_vectors.append(vector_values)
453+
except (
454+
CVSS2MalformedError,
455+
CVSS3MalformedError,
456+
CVSS4MalformedError,
457+
NotImplementedError,
458+
) as e:
459+
logging.error(f"CVSSMalformedError for {severity.scoring_elements}: {e}")
460+
461+
valid_value_severities = base_severities.filter(value__isnull=False).exclude(value="")
462+
463+
severity_values.update(valid_value_severities.values_list("value", flat=True))
464+
465+
return severity_vectors, severity_values
466+
374467

375468
class Weakness(models.Model):
376469
"""

vulnerabilities/tests/test_view.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
from packageurl import PackageURL
1616
from univers import versions
1717

18+
from vulnerabilities import models
1819
from vulnerabilities.models import Alias
1920
from vulnerabilities.models import Package
2021
from vulnerabilities.models import Vulnerability
@@ -273,3 +274,56 @@ class TestCustomFilters:
273274
def test_url_quote_filter(self, input_value, expected_output):
274275
filtered = url_quote_filter(input_value)
275276
assert filtered == expected_output
277+
278+
279+
class VulnerabilitySearchTestCaseWithPackages(TestCase):
280+
def setUp(self):
281+
self.vuln1 = models.Vulnerability.objects.create(
282+
vulnerability_id="VCID-1", summary="Vuln 1"
283+
)
284+
self.vuln2 = models.Vulnerability.objects.create(
285+
vulnerability_id="VCID-2", summary="Vuln 2"
286+
)
287+
self.vuln3 = models.Vulnerability.objects.create(
288+
vulnerability_id="VCID-3", summary="Vuln 3"
289+
)
290+
self.vuln4 = models.Vulnerability.objects.create(
291+
vulnerability_id="VCID-4", summary="Vuln 4"
292+
)
293+
self.vuln5 = models.Vulnerability.objects.create(
294+
vulnerability_id="VCID-5", summary="Vuln 5"
295+
)
296+
297+
self.package1 = models.Package.objects.create(type="pypi", name="django", version="1.0.0")
298+
self.package2 = models.Package.objects.create(type="pypi", name="django", version="2.0.0")
299+
self.package3 = models.Package.objects.create(type="pypi", name="django", version="3.0.0")
300+
301+
models.AffectedByPackageRelatedVulnerability.objects.create(
302+
package=self.package1, vulnerability=self.vuln1
303+
)
304+
305+
models.AffectedByPackageRelatedVulnerability.objects.create(
306+
package=self.package1, vulnerability=self.vuln2
307+
)
308+
309+
models.AffectedByPackageRelatedVulnerability.objects.create(
310+
package=self.package2, vulnerability=self.vuln3
311+
)
312+
313+
models.AffectedByPackageRelatedVulnerability.objects.create(
314+
package=self.package2, vulnerability=self.vuln4
315+
)
316+
317+
# Associate fixed_by package with vuln5
318+
319+
models.FixingPackageRelatedVulnerability.objects.create(
320+
package=self.package3, vulnerability=self.vuln5
321+
)
322+
323+
def test_aggregate_fixed_and_affected_packages(self):
324+
with self.assertNumQueries(11):
325+
response = self.client.get(f"/vulnerabilities/{self.vuln1.vulnerability_id}")
326+
self.assertEqual(response.status_code, 200)
327+
328+
with self.assertNumQueries(2):
329+
self.vuln1.aggregate_fixed_and_affected_packages()

vulnerabilities/utils.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
from packageurl import PackageURL
3333
from packageurl.contrib.django.utils import without_empty_values
3434
from univers.version_range import RANGE_CLASS_BY_SCHEMES
35+
from univers.version_range import AlpineLinuxVersionRange
3536
from univers.version_range import NginxVersionRange
3637
from univers.version_range import VersionRange
3738

@@ -536,3 +537,12 @@ def normalize_purl(purl: Union[PackageURL, str]):
536537
if isinstance(purl, PackageURL):
537538
purl = str(purl)
538539
return PackageURL.from_string(purl)
540+
541+
542+
def get_purl_version_class(purl):
543+
RANGE_CLASS_BY_SCHEMES["alpine"] = AlpineLinuxVersionRange
544+
purl_version_class = None
545+
check_version_class = RANGE_CLASS_BY_SCHEMES.get(purl.type, None)
546+
if check_version_class:
547+
purl_version_class = check_version_class.version_class
548+
return purl_version_class

vulnerabilities/views.py

Lines changed: 2 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,6 @@
77
# See https://aboutcode.org for more information about nexB OSS projects.
88
#
99
import logging
10-
from itertools import groupby
11-
from operator import attrgetter
1210

1311
from cvss.exceptions import CVSS2MalformedError
1412
from cvss.exceptions import CVSS3MalformedError
@@ -24,17 +22,14 @@
2422
from django.views import generic
2523
from django.views.generic.detail import DetailView
2624
from django.views.generic.list import ListView
27-
from univers.version_range import RANGE_CLASS_BY_SCHEMES
28-
from univers.version_range import AlpineLinuxVersionRange
2925

3026
from vulnerabilities import models
3127
from vulnerabilities.forms import ApiUserCreationForm
3228
from vulnerabilities.forms import PackageSearchForm
3329
from vulnerabilities.forms import VulnerabilitySearchForm
34-
from vulnerabilities.models import VulnerabilityStatusType
3530
from vulnerabilities.severity_systems import EPSS
3631
from vulnerabilities.severity_systems import SCORING_SYSTEMS
37-
from vulnerabilities.utils import get_severity_range
32+
from vulnerabilities.utils import get_purl_version_class
3833
from vulnerablecode.settings import env
3934

4035
PAGE_SIZE = 20
@@ -54,15 +49,6 @@ def purl_sort_key(purl: models.Package):
5449
return (purl.type, purl.namespace, purl.name, purl_sort_version, purl.qualifiers, purl.subpath)
5550

5651

57-
def get_purl_version_class(purl: models.Package):
58-
RANGE_CLASS_BY_SCHEMES["alpine"] = AlpineLinuxVersionRange
59-
purl_version_class = None
60-
check_version_class = RANGE_CLASS_BY_SCHEMES.get(purl.type, None)
61-
if check_version_class:
62-
purl_version_class = check_version_class.version_class
63-
return purl_version_class
64-
65-
6652
class PackageSearch(ListView):
6753
model = models.Package
6854
template_name = "packages.html"
@@ -183,7 +169,7 @@ def get_context_data(self, **kwargs):
183169
sorted_fixed_by_packages,
184170
sorted_affected_packages,
185171
all_affected_fixed_by_matches,
186-
) = self.aggregate_fixed_and_affected_packages()
172+
) = self.object.aggregate_fixed_and_affected_packages()
187173

188174
context.update(
189175
{
@@ -204,57 +190,6 @@ def get_context_data(self, **kwargs):
204190
)
205191
return context
206192

207-
def aggregate_fixed_and_affected_packages(self):
208-
sorted_fixed_by_packages = self.object.fixed_by_packages.filter(is_ghost=False).order_by(
209-
"type", "namespace", "name", "qualifiers", "subpath"
210-
)
211-
212-
sorted_affected_packages = self.object.affected_packages.all()
213-
214-
grouped_fixed_by_packages = {
215-
key: list(group)
216-
for key, group in groupby(
217-
sorted_fixed_by_packages,
218-
key=attrgetter("type", "namespace", "name", "qualifiers", "subpath"),
219-
)
220-
}
221-
222-
all_affected_fixed_by_matches = []
223-
224-
for sorted_affected_package in sorted_affected_packages:
225-
affected_fixed_by_matches = {
226-
"affected_package": sorted_affected_package,
227-
"matched_fixed_by_packages": [],
228-
}
229-
230-
# Build the key to find matching group
231-
key = (
232-
sorted_affected_package.type,
233-
sorted_affected_package.namespace,
234-
sorted_affected_package.name,
235-
sorted_affected_package.qualifiers,
236-
sorted_affected_package.subpath,
237-
)
238-
239-
# Get matching group from pre-grouped fixed_by_packages
240-
matching_fixed_packages = grouped_fixed_by_packages.get(key, [])
241-
242-
# Get version classes for comparison
243-
affected_version_class = get_purl_version_class(sorted_affected_package)
244-
affected_version = affected_version_class(sorted_affected_package.version)
245-
246-
# Compare versions and filter valid matches
247-
matched_fixed_by_packages = [
248-
fixed_by_package.purl
249-
for fixed_by_package in matching_fixed_packages
250-
if get_purl_version_class(fixed_by_package)(fixed_by_package.version)
251-
> affected_version
252-
]
253-
254-
affected_fixed_by_matches["matched_fixed_by_packages"] = matched_fixed_by_packages
255-
all_affected_fixed_by_matches.append(affected_fixed_by_matches)
256-
return sorted_fixed_by_packages, sorted_affected_packages, all_affected_fixed_by_matches
257-
258193
def get_severity_vectors_and_values(self):
259194
"""
260195
Collect severity vectors and values, excluding EPSS scoring systems and handling errors gracefully.

0 commit comments

Comments
 (0)