From 1e4079ddb78caa9630e2dff75fe84a0a80f44d58 Mon Sep 17 00:00:00 2001 From: "John M. Horan" Date: Tue, 25 Jul 2023 20:53:57 -0700 Subject: [PATCH 01/53] Add initial fixed-affected-matching work #1228 Reference: https://github.com/nexB/vulnerablecode/issues/1228 Signed-off-by: John M. Horan --- vulnerabilities/models.py | 12 ++++ .../templates/package_details.html | 63 ++++++++++++++++++- vulnerabilities/views.py | 1 - 3 files changed, 74 insertions(+), 2 deletions(-) diff --git a/vulnerabilities/models.py b/vulnerabilities/models.py index 16c93002f..545953316 100644 --- a/vulnerabilities/models.py +++ b/vulnerabilities/models.py @@ -588,6 +588,18 @@ def affected_by(self): # legacy aliases vulnerable_to = affected_by + @property + def test_get_fixing_purls(self): + """ + This is a test -- the goal is to display the closest fixing version for a PURL that is greater + than the affected version and is the same type. We want to filter on type, namespace, + name, qualifiers and subpath for the affected PURL. + """ + return [ + abc.fixed_by_packages + for abc in self.vulnerabilities.filter(packagerelatedvulnerability__fix=False) + ] + @property # TODO: consider renaming to "fixes" or "fixing" ? (TBD) and updating the docstring def fixing(self): diff --git a/vulnerabilities/templates/package_details.html b/vulnerabilities/templates/package_details.html index 6a391d3d3..bb076c930 100644 --- a/vulnerabilities/templates/package_details.html +++ b/vulnerabilities/templates/package_details.html @@ -40,6 +40,58 @@ +
+ Let's try to display fixing packages for this package: {{ package.purl }} +
+ package.purl = {{ package.purl }} +
+
+ package.qualifiers = {{ package.qualifiers }} +
+
+ package.vulnerabilities = {{ package.vulnerabilities }} +
+
+ package.package_url = {{ package.package_url }} +
+
+ package.plain_package_url = {{ package.plain_package_url }} +
+
+ package.purl_object = {{ package.purl_object }} +
+ +
+ package.fixing = {{ package.fixing }} +
+
+ package.fixed_packages = Server Error (500) +
+
+ package.is_vulnerable = {{ package.is_vulnerable }} +
+
+ package.get_absolute_url = {{ package.get_absolute_url }} +
+ +
+ package.affected_by = {{ package.affected_by }} +
+
+ package.test_get_fixing_purls = {{ package.test_get_fixing_purls }} +
+ + {% for abc in package.affected_by %} + +
{{ abc }} -- {{ abc.fixed_by_packages }}
+
{{ abc }} -- {% for pkg in abc.fixed_by_packages %}{{ pkg.purl }}{% endfor %}
+ + {% endfor %} + + + +
+
Affected by vulnerabilities ({{ affected_by_vulnerabilities|length }}) @@ -51,6 +103,7 @@ Vulnerability Summary Aliases + Test fixing PURLs @@ -74,10 +127,18 @@ {% endif %} {% endfor %} + + + + {% for pkg in vulnerability.fixed_by_packages %} + {{ pkg.purl }} + {% endfor %} + {% empty %} - + + This package is not known to be affected by vulnerabilities. diff --git a/vulnerabilities/views.py b/vulnerabilities/views.py index e96f43a6d..fad42eae8 100644 --- a/vulnerabilities/views.py +++ b/vulnerabilities/views.py @@ -176,7 +176,6 @@ class ApiUserCreateView(generic.CreateView): template_name = "api_user_creation_form.html" def form_valid(self, form): - try: response = super().form_valid(form) except ValidationError: From 624f047cb21f04de0fbad927ccdd6c7e7ae8bbd2 Mon Sep 17 00:00:00 2001 From: "John M. Horan" Date: Fri, 28 Jul 2023 09:26:40 -0700 Subject: [PATCH 02/53] Explore context and Package class approaches for affected-fixed package matching #1228 Reference: https://github.com/nexB/vulnerablecode/issues/1228 Note that my updated code is still in testing/dev stage and has not yet been completed or cleaned. Signed-off-by: John M. Horan --- vulnerabilities/models.py | 80 +++++++++-- .../templates/package_details.html | 134 +++++++++++++++--- vulnerabilities/views.py | 35 +++++ 3 files changed, 221 insertions(+), 28 deletions(-) diff --git a/vulnerabilities/models.py b/vulnerabilities/models.py index 545953316..dbdf45ab8 100644 --- a/vulnerabilities/models.py +++ b/vulnerabilities/models.py @@ -22,6 +22,7 @@ from django.core.validators import MinValueValidator from django.db import models from django.db.models import Count +from django.db.models import Prefetch from django.db.models import Q from django.db.models.functions import Length from django.db.models.functions import Trim @@ -588,18 +589,6 @@ def affected_by(self): # legacy aliases vulnerable_to = affected_by - @property - def test_get_fixing_purls(self): - """ - This is a test -- the goal is to display the closest fixing version for a PURL that is greater - than the affected version and is the same type. We want to filter on type, namespace, - name, qualifiers and subpath for the affected PURL. - """ - return [ - abc.fixed_by_packages - for abc in self.vulnerabilities.filter(packagerelatedvulnerability__fix=False) - ] - @property # TODO: consider renaming to "fixes" or "fixing" ? (TBD) and updating the docstring def fixing(self): @@ -631,6 +620,73 @@ def get_absolute_url(self): """ return reverse("package_details", args=[self.purl]) + def get_fixed_packages(self, package): + """ + Return a queryset of all packages that fix a vulnerability with + same type, namespace, name, subpath and qualifiers of the `package` + """ + return Package.objects.filter( + name=package.name, + namespace=package.namespace, + type=package.type, + qualifiers=package.qualifiers, + subpath=package.subpath, + packagerelatedvulnerability__fix=True, + ).distinct() + + @property + def test_get_fixing_purls(self): + """ + This is a test -- the goal is to display the closest fixing version for a PURL that is greater + than the affected version and is the same type. We want to filter on type, namespace, + name, qualifiers and subpath for the affected PURL. + """ + + fixed_packages = self.get_fixed_packages(package=self) + + # Prefetch: + + # Where do we get "fix" from? + # qs = self.vulnerabilities.filter(packagerelatedvulnerability__fix=fix) + + # Not clear to me how we use this: + # qs = qs.prefetch_related( + # Prefetch( + # "packages", + # queryset=fixed_packages, + # to_attr="filtered_fixed_packages", + # ) + # ) + + matching_fixed_packages = [] + + # closest_subsequent_fixed_package = [] + + test_dict = {"affected_purl": self.purl} + test_dict["vulnerabilities"] = [] + + for vuln in self.affected_by: + test_dict["vulnerabilities"].append({"vulnerability": vuln.vulnerability_id}) + + for fixing_pkg in vuln.fixed_by_packages: + # Do not add "backports". TODO: Do we need to use univers to compare versions? + if fixing_pkg in fixed_packages and fixing_pkg.version > self.version: + matching_fixed_packages.append(fixing_pkg) + + # TODO: We also need to check if there is more than one fixing package and if so, keep only the package closest to the affect package's version. + # TODO: Do we want to check whether the fixing version has any vulnerabilities of its own? + + # ======================================================== + print("\ntest_dict = \n{}\n".format(test_dict)) + print(json.dumps(test_dict, indent=4)) + # ======================================================== + + return matching_fixed_packages + + +# 2023-07-27 Thursday 09:50:45. JMH: check this out re related api.py code +# TODO: Not clear how this is involved. + class PackageRelatedVulnerability(models.Model): """ diff --git a/vulnerabilities/templates/package_details.html b/vulnerabilities/templates/package_details.html index bb076c930..63c0fc37e 100644 --- a/vulnerabilities/templates/package_details.html +++ b/vulnerabilities/templates/package_details.html @@ -43,52 +43,131 @@
Let's try to display fixing packages for this package: {{ package.purl }}
- package.purl = {{ package.purl }} + package.purl = {{ package.purl }}
- package.qualifiers = {{ package.qualifiers }} + package.qualifiers = {{ package.qualifiers }}
- package.vulnerabilities = {{ package.vulnerabilities }} + package.vulnerabilities = {{ package.vulnerabilities }}
- package.package_url = {{ package.package_url }} + package.package_url = {{ package.package_url }}
- package.plain_package_url = {{ package.plain_package_url }} + package.plain_package_url = {{ package.plain_package_url }}
- package.purl_object = {{ package.purl_object }} + package.purl_object = {{ package.purl_object }}
- package.fixing = {{ package.fixing }} + package.fixing = {{ package.fixing }}
- package.fixed_packages = Server Error (500) + package.fixed_packages = Server Error (500)
- package.is_vulnerable = {{ package.is_vulnerable }} + package.is_vulnerable = {{ package.is_vulnerable }}
- package.get_absolute_url = {{ package.get_absolute_url }} + package.get_absolute_url = {{ package.get_absolute_url }}
- package.affected_by = {{ package.affected_by }} + package.affected_by = {{ package.affected_by }}
+ +
+ package.test_get_fixing_purls = {{ package.test_get_fixing_purls }} +
+
- package.test_get_fixing_purls = {{ package.test_get_fixing_purls }} + package.test_get_fixing_purls = {{ package.test_get_fixing_purls }}
{% for abc in package.affected_by %} -
{{ abc }} -- {{ abc.fixed_by_packages }}
-
{{ abc }} -- {% for pkg in abc.fixed_by_packages %}{{ pkg.purl }}{% endfor %}
+
{{ abc }} -- {{ abc.fixed_by_packages }}
+
{{ abc }} -- {% for pkg in abc.fixed_by_packages %}{{ pkg.purl }}{% endfor %}
+ + {% endfor %} + +
abc.test_matching_fixed_by_packages
+ {% for abc in package.affected_by %} + + + +
{{ abc }} -- + {% for pkg in abc.test_matching_fixed_by_packages %} +
{{ pkg.purl }}
+ {% endfor %} +
{% endfor %} +
+
test01
+ {% for thing in test01 %} + {{ thing }} +
+ {% endfor %} +
+ +
+
test02
+ {% for thing in test02 %} + - {{ thing }} +
+ {% endfor %} +
+ +
+
test03
+ {% for thing in test03 %} + - {{ thing }} +
+ {% for stuff in thing.test_matching_fixed_by_packages %} + ==> {{ stuff }} +
+ {% endfor %} +
+ {% endfor %} +
+ +
+
test04
+ {% for thing in test04 %} + - {{ thing }} +
+ {% endfor %} +
+ +
+ +
context["test_get_fixing_purls"]
+ {% for thing in test_get_fixing_purls %} + - {{ thing }} +
+ {% endfor %} + {% for vulnerability in affected_by_vulnerabilities %} +
+ {{ vulnerability }} +
+
+ {% for pkg in vulnerability.fixed_by_packages %} + {% if pkg in test_get_fixing_purls %} +
+ {{ pkg.purl }} +
+ {% endif %} + {% endfor %} +
+ {% endfor %} +
@@ -103,7 +182,7 @@ Vulnerability Summary Aliases - Test fixing PURLs + Fixing Packages @@ -131,8 +210,31 @@ {% for pkg in vulnerability.fixed_by_packages %} - {{ pkg.purl }} +
+ {{ pkg.purl }} +
+ {% endfor %} + +
+ + {% for pkg in vulnerability.fixed_by_packages %} + {% if pkg in test04 %} +
+ {{ pkg.purl }} +
+ {% endif %} + {% endfor %} + +
+ + {% for pkg in vulnerability.fixed_by_packages %} + {% if pkg in test_get_fixing_purls %} +
+ {{ pkg.purl }} +
+ {% endif %} {% endfor %} + {% empty %} diff --git a/vulnerabilities/views.py b/vulnerabilities/views.py index fad42eae8..eb181ff93 100644 --- a/vulnerabilities/views.py +++ b/vulnerabilities/views.py @@ -83,6 +83,41 @@ def get_context_data(self, **kwargs): context["affected_by_vulnerabilities"] = package.affected_by.order_by("vulnerability_id") context["fixing_vulnerabilities"] = package.fixing.order_by("vulnerability_id") context["package_search_form"] = PackageSearchForm(self.request.GET) + context["test01"] = [ + "aaa", + "bbb", + "ccc", + ] + context["test02"] = [ + package, + package.type, + package.namespace, + package.name, + package.version, + package.qualifiers, + package.subpath, + ] + context["test03"] = package.affected_by + + # ======================================================== + context["test04"] = [ + fixing_pkg + for vuln in package.affected_by + # for fixing_pkg in vuln.test_matching_fixed_by_packages + for fixing_pkg in vuln.fixed_by_packages + if fixing_pkg.type == package.type + and fixing_pkg.namespace == package.namespace + and fixing_pkg.name == package.name + and fixing_pkg.qualifiers == package.qualifiers + and fixing_pkg.subpath == package.subpath + # I think this version comparison requires the use of univers. + # Plus we just need the closest one, not all that are greater than. + and fixing_pkg.version > package.version + ] + # ======================================================== + context["test_get_fixing_purls"] = package.test_get_fixing_purls + # ======================================================== + return context def get_object(self, queryset=None): From 945b8112bd5a9d7768d336bb6b554800ca1297b4 Mon Sep 17 00:00:00 2001 From: "John M. Horan" Date: Sat, 29 Jul 2023 18:00:26 -0700 Subject: [PATCH 03/53] Add Prefetch and univers-based version comparison #1228 Reference: https://github.com/nexB/vulnerablecode/issues/1228 Signed-off-by: John M. Horan --- vulnerabilities/models.py | 115 +++++++++---- .../templates/package_details.html | 155 +----------------- vulnerabilities/views.py | 35 +--- 3 files changed, 86 insertions(+), 219 deletions(-) diff --git a/vulnerabilities/models.py b/vulnerabilities/models.py index dbdf45ab8..dde000813 100644 --- a/vulnerabilities/models.py +++ b/vulnerabilities/models.py @@ -32,6 +32,7 @@ from packageurl.contrib.django.models import PackageURLQuerySet from packageurl.contrib.django.models import without_empty_values from rest_framework.authtoken.models import Token +from univers import versions from vulnerabilities.importer import AdvisoryData from vulnerabilities.importer import AffectedPackage @@ -634,58 +635,100 @@ def get_fixed_packages(self, package): packagerelatedvulnerability__fix=True, ).distinct() - @property - def test_get_fixing_purls(self): + def assign_univers_version(self, fixing_pkg): """ - This is a test -- the goal is to display the closest fixing version for a PURL that is greater - than the affected version and is the same type. We want to filter on type, namespace, - name, qualifiers and subpath for the affected PURL. + Identify which univers version applies to the two packages to be compared (self and a fixing package), + evaluate whether the fixing_pkg version is > than the target affected package, and + return True or False. """ - fixed_packages = self.get_fixed_packages(package=self) + # TODO: We also need to find which fixing_pkg is closest to the affected version -- we don't do that yet. - # Prefetch: + # Many more to be added. + match_type_to_univers_version = { + "conan": versions.ConanVersion, + "maven": versions.MavenVersion, + } - # Where do we get "fix" from? - # qs = self.vulnerabilities.filter(packagerelatedvulnerability__fix=fix) + command_name = "" - # Not clear to me how we use this: - # qs = qs.prefetch_related( - # Prefetch( - # "packages", - # queryset=fixed_packages, - # to_attr="filtered_fixed_packages", - # ) - # ) + type_to_version = match_type_to_univers_version.get(fixing_pkg.type) + if type_to_version: + print("\t--------------------------") + print("\ttype_to_version = {}\n".format(type_to_version)) + command_name = type_to_version - matching_fixed_packages = [] + else: + print("\ttype_to_version = NO MATCH\n") + command_name = versions.Version - # closest_subsequent_fixed_package = [] + if command_name(fixing_pkg.version) > command_name(self.version): + return True + else: + return False - test_dict = {"affected_purl": self.purl} - test_dict["vulnerabilities"] = [] + @property + def get_fixing_packages(self): + """ + This function identifies the closest fixing version that is greater than the affected version and + is the same type, namespace, name, qualifiers and subpath as the affected package. + """ - for vuln in self.affected_by: - test_dict["vulnerabilities"].append({"vulnerability": vuln.vulnerability_id}) + print("\nself = {}\n".format(self)) - for fixing_pkg in vuln.fixed_by_packages: - # Do not add "backports". TODO: Do we need to use univers to compare versions? - if fixing_pkg in fixed_packages and fixing_pkg.version > self.version: - matching_fixed_packages.append(fixing_pkg) + # This returns all fixing packages that match the target package (type etc.), regardless of fixed vuln. + fixed_packages = self.get_fixed_packages(package=self) - # TODO: We also need to check if there is more than one fixing package and if so, keep only the package closest to the affect package's version. - # TODO: Do we want to check whether the fixing version has any vulnerabilities of its own? + # This returns a list of the vulnerabilities that affect this package (i.e., self). + qs = self.vulnerabilities.filter(packagerelatedvulnerability__fix=False) - # ======================================================== - print("\ntest_dict = \n{}\n".format(test_dict)) - print(json.dumps(test_dict, indent=4)) - # ======================================================== + # This takes the list of vulns affecting the current package, retrieves a list of the fixing packages for each vuln, and assigns the result to a custom attribute, `filtered_fixed_packages`. + # We use this in a for loop below like this -- qs[vuln_count].filtered_fixed_packages -- where `vuln_count` is used to iterate through the list of vulns that affect the current package (i.e., self). + qs = qs.prefetch_related( + Prefetch( + "packages", + queryset=fixed_packages, + to_attr="filtered_fixed_packages", + ) + ) - return matching_fixed_packages + # Ex: qs[0].filtered_fixed_packages gives us the fixed package(s) for the 1st vuln for this affected package (i.e., self). + print("qs = {}\n".format(qs)) + prefetch_fixed_packages = [] -# 2023-07-27 Thursday 09:50:45. JMH: check this out re related api.py code -# TODO: Not clear how this is involved. + vuln_count = 0 + for vuln in qs: + print("vuln = {}\n".format(vuln)) + print( + "\tqs[vuln_count].filtered_fixed_packages = {}".format( + qs[vuln_count].filtered_fixed_packages + ) + ) + print("") + + # Check the Prefetch qs. + # TODO: Do we want to check whether the fixing version has any vulnerabilities of its own? + for fixing_pkg in qs[vuln_count].filtered_fixed_packages: + print("\tfixing_pkg = {}".format(fixing_pkg)) + print("\tfixing_pkg.type = {}".format(fixing_pkg.type)) + print("\tfixing_pkg.version = {}".format(fixing_pkg.version)) + print("\t--------------------------") + print("\tself.type = {}".format(self.type)) + print("\tself.version = {}".format(self.version)) + + # Assign univers version and compare: False = fixing_pkg.version < self.version (affected version). + # TODO: We also need to find which fixing_pkg is closest to the affected version -- we don't do that yet. + immediate_fix = self.assign_univers_version(fixing_pkg) + print("\t--------------------------") + print("\timmediate_fix = {}\n".format(immediate_fix)) + + if fixing_pkg in fixed_packages and immediate_fix: + prefetch_fixed_packages.append(fixing_pkg) + + vuln_count += 1 + + return prefetch_fixed_packages class PackageRelatedVulnerability(models.Model): diff --git a/vulnerabilities/templates/package_details.html b/vulnerabilities/templates/package_details.html index 63c0fc37e..cfbfdd706 100644 --- a/vulnerabilities/templates/package_details.html +++ b/vulnerabilities/templates/package_details.html @@ -40,138 +40,8 @@
-
- Let's try to display fixing packages for this package: {{ package.purl }} -
- package.purl = {{ package.purl }} -
-
- package.qualifiers = {{ package.qualifiers }} -
-
- package.vulnerabilities = {{ package.vulnerabilities }} -
-
- package.package_url = {{ package.package_url }} -
-
- package.plain_package_url = {{ package.plain_package_url }} -
-
- package.purl_object = {{ package.purl_object }} -
- -
- package.fixing = {{ package.fixing }} -
-
- package.fixed_packages = Server Error (500) -
-
- package.is_vulnerable = {{ package.is_vulnerable }} -
-
- package.get_absolute_url = {{ package.get_absolute_url }} -
- -
- package.affected_by = {{ package.affected_by }} -
- -
- package.test_get_fixing_purls = {{ package.test_get_fixing_purls }} -
- -
- package.test_get_fixing_purls = {{ package.test_get_fixing_purls }} -
- - {% for abc in package.affected_by %} - -
{{ abc }} -- {{ abc.fixed_by_packages }}
-
{{ abc }} -- {% for pkg in abc.fixed_by_packages %}{{ pkg.purl }}{% endfor %}
- - {% endfor %} - -
abc.test_matching_fixed_by_packages
- {% for abc in package.affected_by %} - - - -
{{ abc }} -- - {% for pkg in abc.test_matching_fixed_by_packages %} -
{{ pkg.purl }}
- {% endfor %} -
- - {% endfor %} - -
-
test01
- {% for thing in test01 %} - {{ thing }} -
- {% endfor %} -
- -
-
test02
- {% for thing in test02 %} - - {{ thing }} -
- {% endfor %} -
- -
-
test03
- {% for thing in test03 %} - - {{ thing }} -
- {% for stuff in thing.test_matching_fixed_by_packages %} - ==> {{ stuff }} -
- {% endfor %} -
- {% endfor %} -
- -
-
test04
- {% for thing in test04 %} - - {{ thing }} -
- {% endfor %} -
- -
- -
context["test_get_fixing_purls"]
- {% for thing in test_get_fixing_purls %} - - {{ thing }} -
- {% endfor %} - - {% for vulnerability in affected_by_vulnerabilities %} -
- {{ vulnerability }} -
-
- {% for pkg in vulnerability.fixed_by_packages %} - {% if pkg in test_get_fixing_purls %} -
- {{ pkg.purl }} -
- {% endif %} - {% endfor %} -
- {% endfor %} -
- -
-
+
During testing, red = all packages that fix the vulnerability (for pkg in vulnerability.fixed_by_packages) and green = the closest fixing package whose version is greater than the affected package's version (if pkg in get_fixing_packages).
Affected by vulnerabilities ({{ affected_by_vulnerabilities|length }})
@@ -182,7 +52,7 @@ Vulnerability Summary Aliases - Fixing Packages + Fixing Packages @@ -207,39 +77,26 @@ {% endfor %} - - {% for pkg in vulnerability.fixed_by_packages %} -
+
{{ pkg.purl }}
{% endfor %} -
+
{% for pkg in vulnerability.fixed_by_packages %} - {% if pkg in test04 %} -
+ {% if pkg in get_fixing_packages %} +
{{ pkg.purl }}
{% endif %} {% endfor %} -
- - {% for pkg in vulnerability.fixed_by_packages %} - {% if pkg in test_get_fixing_purls %} -
- {{ pkg.purl }} -
- {% endif %} - {% endfor %} - {% empty %} - This package is not known to be affected by vulnerabilities. diff --git a/vulnerabilities/views.py b/vulnerabilities/views.py index eb181ff93..6a41620b3 100644 --- a/vulnerabilities/views.py +++ b/vulnerabilities/views.py @@ -83,40 +83,7 @@ def get_context_data(self, **kwargs): context["affected_by_vulnerabilities"] = package.affected_by.order_by("vulnerability_id") context["fixing_vulnerabilities"] = package.fixing.order_by("vulnerability_id") context["package_search_form"] = PackageSearchForm(self.request.GET) - context["test01"] = [ - "aaa", - "bbb", - "ccc", - ] - context["test02"] = [ - package, - package.type, - package.namespace, - package.name, - package.version, - package.qualifiers, - package.subpath, - ] - context["test03"] = package.affected_by - - # ======================================================== - context["test04"] = [ - fixing_pkg - for vuln in package.affected_by - # for fixing_pkg in vuln.test_matching_fixed_by_packages - for fixing_pkg in vuln.fixed_by_packages - if fixing_pkg.type == package.type - and fixing_pkg.namespace == package.namespace - and fixing_pkg.name == package.name - and fixing_pkg.qualifiers == package.qualifiers - and fixing_pkg.subpath == package.subpath - # I think this version comparison requires the use of univers. - # Plus we just need the closest one, not all that are greater than. - and fixing_pkg.version > package.version - ] - # ======================================================== - context["test_get_fixing_purls"] = package.test_get_fixing_purls - # ======================================================== + context["get_fixing_packages"] = package.get_fixing_packages return context From 6497e90014ab4a5f8370005b827929a38cb180ed Mon Sep 17 00:00:00 2001 From: "John M. Horan" Date: Mon, 7 Aug 2023 19:04:39 -0700 Subject: [PATCH 04/53] Update affected-fixed package matching #1228 Reference: https://github.com/nexB/vulnerablecode/issues/1228 Signed-off-by: John M. Horan --- vulnerabilities/models.py | 663 ++++++++++++++++-- .../templates/package_details.html | 140 +++- vulnerabilities/tests/test_models.py | 65 ++ vulnerabilities/views.py | 5 +- vulnerablecode/settings.py | 4 +- vulnerablecode/static/css/custom.css | 102 ++- 6 files changed, 929 insertions(+), 50 deletions(-) diff --git a/vulnerabilities/models.py b/vulnerabilities/models.py index dde000813..d07a0530f 100644 --- a/vulnerabilities/models.py +++ b/vulnerabilities/models.py @@ -635,100 +635,685 @@ def get_fixed_packages(self, package): packagerelatedvulnerability__fix=True, ).distinct() - def assign_univers_version(self, fixing_pkg): + def get_sibling_packages(self, package): """ - Identify which univers version applies to the two packages to be compared (self and a fixing package), - evaluate whether the fixing_pkg version is > than the target affected package, and + Return a queryset of all packages with the same type, namespace, name, subpath and qualifiers of the `package`, whether or not they fix any vulnerability + """ + return Package.objects.filter( + name=package.name, + namespace=package.namespace, + type=package.type, + qualifiers=package.qualifiers, + subpath=package.subpath, + # packagerelatedvulnerability__fix=True, + ).distinct() + + # def assign_and_compare_univers_versions(self, fixed_pkg): + def assign_univers_version(self, fixed_pkg): + """ + Identify which univers version applies to the two packages to be compared (self and a fixed package), + evaluate whether the fixed_pkg version is > than the target affected package, and return True or False. """ - # TODO: We also need to find which fixing_pkg is closest to the affected version -- we don't do that yet. + # TODO: Instead of return True or False based on evaluating the incoming fixed_pkg type to a univers version and then checking whether the fixed version is greater than the affected (self) version, we'll just use the incoming type to assign and return a univers version -- as command_name -- to be used in get_closest_fixed_package() below to add all greater-than versions to the later_matching_fixed_packages list -- which in turn will be fed to self.sort_by_version(later_matching_fixed_packages) # Many more to be added. match_type_to_univers_version = { "conan": versions.ConanVersion, + "deb": versions.DebianVersion, "maven": versions.MavenVersion, + "openssl": versions.OpensslVersion, + "pypi": versions.PypiVersion, } command_name = "" - type_to_version = match_type_to_univers_version.get(fixing_pkg.type) - if type_to_version: + matched_type_to_version = match_type_to_univers_version.get(fixed_pkg.type) + if matched_type_to_version: print("\t--------------------------") - print("\ttype_to_version = {}\n".format(type_to_version)) - command_name = type_to_version + print("*** matched_type_to_version = {}".format(matched_type_to_version)) + command_name = matched_type_to_version else: - print("\ttype_to_version = NO MATCH\n") - command_name = versions.Version + print("\t--------------------------") + print("*** matched_type_to_version = NO MATCH") + # Using "command_name = versions.Version", the test + # assert versions.Version("0.9") < versions.Version("0.10") + # fails! + # command_name = versions.Version + # Use this as a default fallback instead. + command_name = versions.SemverVersion + + # if command_name(fixed_pkg.version) > command_name(self.version): + # return True + # else: + # return False + + # Instead return command_name for recipient to use as needed for sorting or perhaps other uses + return command_name + + def sort_by_version(self, later_matching_fixed_packages): + # Incoming is a list of + + # ALERT: added this to address server error 500 but related error arose: line 908, in get_closest_fixed_package + # HOT: How is this related to the source of the server error (500)? + if len(later_matching_fixed_packages) == 0: + return + + # Replace find_closest_fixed_by_package()? + print("\nlater_matching_fixed_packages = {}".format(later_matching_fixed_packages)) + print("\nlater_matching_fixed_packages[0] = {}".format(later_matching_fixed_packages[0])) + print( + "\ntype(later_matching_fixed_packages[0]) = {}".format( + type(later_matching_fixed_packages[0]) + ) + ) + # NOTE: This gives us the PURL type but instead we want the PURL itself to pass to assign_univers_version(self, fixed_pkg) and get the command_name in return, which we'll then use in the sort process. + print( + "\nlater_matching_fixed_packages[0].type = {}".format( + later_matching_fixed_packages[0].type + ) + ) + print( + "\ntype(later_matching_fixed_packages[0].type) = {}".format( + type(later_matching_fixed_packages[0].type) + ) + ) - if command_name(fixing_pkg.version) > command_name(self.version): - return True - else: - return False + # Incoming is a list -- later_matching_fixed_packages + + # We'll use assign_univers_version() above to get the univers version as a command_name. + # But what do we pass to it? The [0] index of the incoming list, i.e., later_matching_fixed_packages[0]? + command_name = self.assign_univers_version(later_matching_fixed_packages[0]) + + print("\n>>> command_name = {}\n".format(command_name)) + + # TODO: Maybe we don't need to convert to a PURL, a list of dictionaries etc.?? + print( + "\n+++++++ later_matching_fixed_packages[0].version = {}".format( + later_matching_fixed_packages[0].version + ) + ) + + # sort + test_sort_by_version = [] + test_sort_by_version = sorted( + # later_matching_fixed_packages, key=lambda x: versions.DebianVersion(x["version"]) + later_matching_fixed_packages, + # key=lambda x: versions.MavenVersion(x.version), + key=lambda x: command_name(x.version), + ) + + print("\ntest_sort_by_version = {}\n".format(test_sort_by_version)) + + return test_sort_by_version + + # convert_to_dict_list = [] + + # sorted_later_matching_fixed_packages = [] + + # # TODO: First, convert to a list of dictionaries. + # for pkg in later_matching_fixed_packages: + # # pkg is a + # print("pkg = {}".format(pkg)) + # print("type(pkg) = {}".format(type(pkg))) + + # # pkg_str is a string + # pkg_str = pkg.package_url + # print("pkg_str = {}".format(pkg_str)) + # print("type(pkg_str) = {}".format(type(pkg_str))) + + # # purl is a + # purl = PackageURL.from_string(pkg_str) + # print("purl = {}".format(purl)) + # print("type(purl) = {}".format(type(purl))) + + # purl_dict = purl.to_dict() + # print("purl_dict = {}".format(purl_dict)) + # print("type(purl_dict) = {}".format(type(purl_dict))) + + # convert_to_dict_list.append(purl.to_dict()) + # print("HELLO\n") + + # print("\nconvert_to_dict_list = {}\n".format(convert_to_dict_list)) + + # return convert_to_dict_list + # ========================================================== + # sorted_later_matching_fixed_packages = sorted( + # later_matching_fixed_packages, key=lambda x: versions.MavenVersion(x["version"]) + # ) + # print( + # "\nsorted_later_matching_fixed_packages = {}\n".format( + # sorted_later_matching_fixed_packages + # ) + # ) + # print("\n".join(map(str, sorted_later_matching_fixed_packages))) + + # return what? + + # ========================================================== + # ========================================================== + + # def find_closest_fixed_by_package(self, later_matching_fixed_packages): + # # Maybe use sort_by_version() above instead? + # # take the incoming list later_matching_fixed_packages, convert to list of dictionaries, sort by version using univers.version.[version class], choose the top i.e., index [0] and convert back to PURL and return that PURL. + # print("\nlater_matching_fixed_packages = {}\n".format(later_matching_fixed_packages)) + + # closest_fixed_by_package = "TBD" + + # return closest_fixed_by_package @property - def get_fixing_packages(self): + # def get_fixing_packages(self): + def get_closest_fixed_package(self): """ - This function identifies the closest fixing version that is greater than the affected version and + This function identifies the closest fixed package version that is greater than the affected package version and is the same type, namespace, name, qualifiers and subpath as the affected package. """ print("\nself = {}\n".format(self)) - # This returns all fixing packages that match the target package (type etc.), regardless of fixed vuln. - fixed_packages = self.get_fixed_packages(package=self) + # This returns all fixed packages that match the target package (type etc.), regardless of fixed vuln. + # fixed_packages = self.get_fixed_packages(package=self) + # This is clearer. + matching_fixed_packages = self.get_fixed_packages(package=self) # This returns a list of the vulnerabilities that affect this package (i.e., self). qs = self.vulnerabilities.filter(packagerelatedvulnerability__fix=False) - # This takes the list of vulns affecting the current package, retrieves a list of the fixing packages for each vuln, and assigns the result to a custom attribute, `filtered_fixed_packages`. - # We use this in a for loop below like this -- qs[vuln_count].filtered_fixed_packages -- where `vuln_count` is used to iterate through the list of vulns that affect the current package (i.e., self). + # This takes the list of vulns affecting the current package, retrieves a list of the fixed packages for each vuln, and assigns the result to a custom attribute, `filtered_fixed_packages` (renamed 'matching_fixed_packages'). + # We use this in a for loop below like this -- qs[vuln_count].filtered_fixed_packages (renamed 'matching_fixed_packages') -- where `vuln_count` is used to iterate through the list of vulns that affect the current package (i.e., self). qs = qs.prefetch_related( Prefetch( "packages", - queryset=fixed_packages, - to_attr="filtered_fixed_packages", + # queryset=fixed_packages, + queryset=matching_fixed_packages, + # to_attr="filtered_fixed_packages", + to_attr="matching_fixed_packages", ) ) # Ex: qs[0].filtered_fixed_packages gives us the fixed package(s) for the 1st vuln for this affected package (i.e., self). print("qs = {}\n".format(qs)) - prefetch_fixed_packages = [] + # ************************************************************************ + + later_matching_fixed_packages = [] vuln_count = 0 for vuln in qs: print("vuln = {}\n".format(vuln)) + # print( + # "\tqs[vuln_count].filtered_fixed_packages = {}".format( + # qs[vuln_count].filtered_fixed_packages + # ) + # ) print( - "\tqs[vuln_count].filtered_fixed_packages = {}".format( - qs[vuln_count].filtered_fixed_packages + "\tqs[vuln_count].matching_fixed_packages = {}".format( + qs[vuln_count].matching_fixed_packages ) ) print("") # Check the Prefetch qs. - # TODO: Do we want to check whether the fixing version has any vulnerabilities of its own? - for fixing_pkg in qs[vuln_count].filtered_fixed_packages: - print("\tfixing_pkg = {}".format(fixing_pkg)) - print("\tfixing_pkg.type = {}".format(fixing_pkg.type)) - print("\tfixing_pkg.version = {}".format(fixing_pkg.version)) + # TODO: Do we want to check whether the fixed version has any vulnerabilities of its own? + # for fixed_pkg in qs[vuln_count].filtered_fixed_packages: + for fixed_pkg in qs[vuln_count].matching_fixed_packages: + print("\tfixed_pkg = {}".format(fixed_pkg)) + print("\tfixed_pkg.type = {}".format(fixed_pkg.type)) + print("\tfixed_pkg.version = {}".format(fixed_pkg.version)) print("\t--------------------------") print("\tself.type = {}".format(self.type)) print("\tself.version = {}".format(self.version)) - # Assign univers version and compare: False = fixing_pkg.version < self.version (affected version). - # TODO: We also need to find which fixing_pkg is closest to the affected version -- we don't do that yet. - immediate_fix = self.assign_univers_version(fixing_pkg) - print("\t--------------------------") - print("\timmediate_fix = {}\n".format(immediate_fix)) - - if fixing_pkg in fixed_packages and immediate_fix: - prefetch_fixed_packages.append(fixing_pkg) + # Assign univers version and compare: False = fixed_pkg.version < self.version (affected version). + + # 2023-08-02 Wednesday 16:01:35. atm immediate_fix is True or False. If instead assign_and_compare_univers_versions() returns the univers version, we could get that here and then test with this or similar right here -- enabling use of the univers version function in other places as well, like a sort_by_version function! + # if command_name(fixed_pkg.version) > command_name(self.version): + # return True + # else: + # return False + # ===================================================== + # Replace this with chunk below + # immediate_fix = self.assign_and_compare_univers_versions(fixed_pkg) + # print("\t--------------------------") + # print("\timmediate_fix = {}\n".format(immediate_fix)) + + # if fixed_pkg in fixed_packages and immediate_fix: + # later_matching_fixed_packages.append(fixed_pkg) + # ===================================================== + # command_name = self.assign_and_compare_univers_versions(fixed_pkg) + # renamed + # TODO: Move this up before the for loop -- both for loops if possible -- to reduce calls! + command_name = self.assign_univers_version(fixed_pkg) + print("\nJust requested command_name >>> {}\n".format(command_name)) + # if fixed_pkg in fixed_packages and command_name(fixed_pkg.version) > command_name( + # self.version + # ): + if fixed_pkg in matching_fixed_packages and command_name( + fixed_pkg.version + ) > command_name(self.version): + later_matching_fixed_packages.append(fixed_pkg) vuln_count += 1 - return prefetch_fixed_packages + # find_closest_fixed_by_package -- from the list later_matching_fixed_packages + # closest_fixed_by_package = self.find_closest_fixed_by_package(later_matching_fixed_packages) + + # TODO: or instead use this. This will be a list sorted by univers version class, and here all we need is to grab the [0] index from that list for the closest fixed by package! So we'd return a single closest_fixed_package. + # ALERT: The sort query needs to be done separately for each vulnerability because the list of fixed by packages is likely to be different. As is, we return a single sorted list of all fixed by packages for the affected package and then pass just the [0] package -- not what we want to do! + sort_fixed_by_packages_by_version = self.sort_by_version(later_matching_fixed_packages) + print( + "\nsort_fixed_by_packages_by_version = {}\n".format(sort_fixed_by_packages_by_version) + ) + # ALERT: 2023-08-05 Saturday 23:22:08. Address server error 500? + # ALERT: 2023-08-05 Saturday 23:24:50. This actusally fixed the server error (500) and I can now even see the Packafe details page for pkg:maven/com.fasterxml.jackson.core/jackson-databind@2.14.0-rc1 !!! + # HOT: I need to trace back the root cause of the server error (500). I suspect it's something like a record with no fixed by packages or something else that is an empty list but which i try to measure, e.g., for a print statement, or possibly for a real if condition.. + if sort_fixed_by_packages_by_version is None: + return + + closest_fixed_package = sort_fixed_by_packages_by_version[0] + + # print("\n!!! later_matching_fixed_packages = {}\n".format(later_matching_fixed_packages)) + # print( + # "\n!!! sort_fixed_by_packages_by_version = {}\n".format( + # sort_fixed_by_packages_by_version + # ) + # ) + # print("\n!!! closest_fixed_package = {}\n".format(closest_fixed_package)) + + # rebuilt_purl_from_dict = PackageURL( + # closest_fixed_package["type"], + # closest_fixed_package["namespace"], + # closest_fixed_package["name"], + # closest_fixed_package["version"], + # closest_fixed_package["qualifiers"], + # closest_fixed_package["subpath"], + # ) + # print("\n!!! rebuilt_purl_from_dict = {}\n".format(rebuilt_purl_from_dict)) + + # return later_matching_fixed_packages + return sort_fixed_by_packages_by_version + # return [closest_fixed_package] + + # return [rebuilt_purl_from_dict] + + @property + def fixed_package_details(self): + """ + This is a test that might develop into a model-based equivalent of the loops etc. I was doing/trying to do in the Jinja2 template. I'm going to add this as a context so we can see it in the template. + """ + # return "Hello" + + # vcio_dict = { + # [ + # {"VCID-2nyb-8rwu-aaag": "PURL01"}, + # {"VCID-gqhw-ngh8-aaap": "PURL02"}, + # {"some-other-id": "PURL03"}, + # ] + # } + + print("\n==> This is from the test_property_01() property.\n") + + print("\nself = {}\n".format(self)) + + # This returns all fixed packages that match the target package (type etc.), regardless of fixed vuln. + # fixed_packages = self.get_fixed_packages(package=self) + # This is clearer. + matching_fixed_packages = self.get_fixed_packages(package=self) + + # This returns a list of the vulnerabilities that affect this package (i.e., self). + qs = self.vulnerabilities.filter(packagerelatedvulnerability__fix=False) + + # TODO: Can we get all sibling packages so that we can then determine which have 0 vulnerabilities and of these the closest and maybe the most recent as well? + + all_sibling_packages = self.get_sibling_packages(package=self) + print("\nall_sibling_packages = {}\n".format(all_sibling_packages)) + print("\nlen(all_sibling_packages) = {}\n".format(len(all_sibling_packages))) + + non_vuln_sibs = [] + for sib in all_sibling_packages: + if sib.is_vulnerable is False: + non_vuln_sibs.append(sib) + print("\nnon_vuln_sibs = {}\n".format(non_vuln_sibs)) + print("\nlen(non_vuln_sibs) = {}\n".format(len(non_vuln_sibs))) + + # Add just the greater-than versions to a new list + command_name = self.assign_univers_version(self) + print( + "\nOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOO command_name = {}\n".format( + command_name + ) + ) + later_non_vuln_sibs = [] + for non_vuln_sib in non_vuln_sibs: + if command_name(non_vuln_sib.version) > command_name(self.version): + later_non_vuln_sibs.append(non_vuln_sib) + + print("\nlater_non_vuln_sibs = {}\n".format(later_non_vuln_sibs)) + print("\nlen(later_non_vuln_sibs) = {}\n".format(len(later_non_vuln_sibs))) + + # This takes the list of vulns affecting the current package, retrieves a list of the fixed packages for each vuln, and assigns the result to a custom attribute, `filtered_fixed_packages` (renamed 'matching_fixed_packages'). + # We use this in a for loop below like this -- qs[vuln_count].filtered_fixed_packages (renamed 'matching_fixed_packages') -- where `vuln_count` is used to iterate through the list of vulns that affect the current package (i.e., self). + qs = qs.prefetch_related( + Prefetch( + "packages", + # queryset=fixed_packages, + queryset=matching_fixed_packages, + # to_attr="filtered_fixed_packages", + to_attr="matching_fixed_packages", + ) + ) + + # Ex: qs[0].filtered_fixed_packages gives us the fixed package(s) for the 1st vuln for this affected package (i.e., self). + print("\nzzz qs = {}\n".format(qs)) + + purl_dict = {} + + purl_dict["purl"] = self.purl + + purl_dict.update({"vulnerabilities": []}) + + # purl_dict["vulnerabilities"].append({"fruit": "orange"}) + + for vuln in qs: + print("\nzzz vuln = {}\n".format(vuln)) + print("\nzzz type(vuln) = {}\n".format(type(vuln))) + + later_matching_fixed_packages = [] + # purl_dict[vuln.vulnerability_id] = "aaa" + # purl_dict.update({"vulnerability": vuln.vulnerability_id}) + + purl_dict["vulnerabilities"].append({"vulnerability": vuln.vulnerability_id}) + + # TODO:2023-08-05 Saturday 13:12:28. This returns a list of matching fixed packages for this specific vuln! + vuln_matching_fixed_packages = vuln.matching_fixed_packages + print("\nzzz self.purl = {}\n".format(self.purl)) + print("\nzzz vuln = {}\n".format(vuln)) + print("\nzzz vuln_matching_fixed_packages = {}\n".format(vuln_matching_fixed_packages)) + + # TODO: So we need to sort this list by version using the correct univers version and then return the [0] index in that sorted list + # QUESTION: Do we still need to remove lesser-than fixed packages or did we already do that? + + # ============================================================= + # command_name = self.assign_univers_version(fixed_pkg) + command_name = self.assign_univers_version(self) + print("\nzzz command_name = {}\n".format(command_name)) + + # ALERT: What if there are no fixed by packages? The following thows an error because the list 'vuln_matching_fixed_packages' is empty! + # [I fixed this, right? ;-] + + closest_fixed_package = "" + + if len(vuln_matching_fixed_packages) > 0: + + for fixed_pkg in vuln_matching_fixed_packages: + if fixed_pkg in matching_fixed_packages and command_name( + fixed_pkg.version + ) > command_name(self.version): + later_matching_fixed_packages.append(fixed_pkg) + + # print("\nJust requested command_name >>> {}\n".format(command_name)) + # # if fixed_pkg in fixed_packages and command_name(fixed_pkg.version) > command_name( + # # self.version + # # ): + # if fixed_pkg in matching_fixed_packages and command_name( + # fixed_pkg.version + # ) > command_name(self.version): + # later_matching_fixed_packages.append(fixed_pkg) + # ============================================================= + # later_matching_fixed_packages = vuln.matching_fixed_packages + + print( + "\nzzz later_matching_fixed_packages = {}\n".format( + later_matching_fixed_packages + ) + ) + + sort_fixed_by_packages_by_version = self.sort_by_version( + later_matching_fixed_packages + ) + print( + "\nzzz sort_fixed_by_packages_by_version = {}\n".format( + sort_fixed_by_packages_by_version + ) + ) + closest_fixed_package = sort_fixed_by_packages_by_version[0] + # closest_fixed_package = sort_fixed_by_packages_by_version[0].purl + # 2023-08-06 Sunday 11:15:03. This returns a queryset of vulns affecting this package. + # HOT: How do we get the closest fixed by package vuln count and list of vulns? I keep getting errors. ;-) + closest_fixed_package_vulns = closest_fixed_package.affected_by + # closest_fixed_package_vulns_list = list(closest_fixed_package_vulns) + # ALERT: 2023-08-06 Sunday 14:25:49. This did the trick! + + # closest_fixed_package_vulns_list = [ + # i.vulnerability_id for i in closest_fixed_package_vulns + # ] + + # 2023-08-06 Sunday 16:53:27. Try a named tuple to pass the vuln's vulnerability+id and get_absolute_url. + # FixedPackageVuln = namedtuple("FixedPackageVuln", "vuln_id, vuln_get_absolute_url") + # closest_fixed_package_vulns_list = [ + # FixedPackageVuln( + # vuln_id=fixed_pkg_vuln.vulnerability_id, + # vuln_get_absolute_url=fixed_pkg_vuln.get_absolute_url(), + # ) + # for fixed_pkg_vuln in closest_fixed_package_vulns + # ] + # ALERT: Replace the namedtuple with a dict -- this way it can be added to the purl_dict as a nested dict rather than a list of 2 values. + closest_fixed_package_vulns_dict = [ + { + "vuln_id": fixed_pkg_vuln.vulnerability_id, + "vuln_get_absolute_url": fixed_pkg_vuln.get_absolute_url(), + } + for fixed_pkg_vuln in closest_fixed_package_vulns + ] + + # === + # closest_fixed_package_vulns_list = closest_fixed_package_vulns.objects.values_list() + + # closest_fixed_package_vulns_list = [] + # for closest_vuln in closest_fixed_package_vulns: + # closest_fixed_package_vulns_list.append(closest_vuln) + # print( + # "\t\nQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQ vuln = {}\n".format( + # closest_vuln + # ) + # ) + # print("\t\ntype(closest_vuln) = {}".format(type(closest_vuln))) + + # # closest_fixed_package_vuln_count = len(closest_fixed_package.affected_by) + # print( + # "\t\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ type(closest_fixed_package_vulns) = {}\n".format( + # type(closest_fixed_package_vulns) + # ) + # ) + + else: + closest_fixed_package = "There are no reported fixed packages." + # Is None the value we want? We do not want to display anything but the count = 0. + # closest_fixed_package_vulns = None + # closest_fixed_package_vuln_count = 0 + + # closest_fixed_package_vulns_list = [] + + print("\nzzz closest_fixed_package = {}".format(closest_fixed_package)) + print("zzz type(closest_fixed_package) = {}\n".format(type(closest_fixed_package))) + + # TODO: How do we add 'closest_fixed_by_purl', 'closest_fixed_by_vulns' and 'non_vulnerable_fix'? + + # # for vuln in purl_dict["vulnerabilities"]: + # # # vuln["closest_fixed_by_purl"] = "?????" + # # vuln["closest_fixed_by_purl"] = closest_fixed_package + # # vuln["closest_fixed_by_url"] = "?????" + # # vuln["closest_fixed_by_vulnerabilities"] = "?????" + # # vuln["non_vulnerable_fix"] = "?????" + # # vuln["non_vulnerable_fix_url"] = "?????" + + for dict_vuln in purl_dict["vulnerabilities"]: + print("\n===================================> vuln = {}\n".format(vuln)) + print("\n===================================> type(vuln) = {}\n".format(type(vuln))) + print("\n===================================> vuln.vcid = {}\n".format(vuln.vcid)) + print( + "\n===================================> dict_vuln['vulnerability'] = {}\n".format( + dict_vuln["vulnerability"] + ) + ) + # TODO: Up above we defined 'non_vuln_sibs' but we still need to remove those with less than version + # ALERT: remove less than versions from 'non_vuln_sibs' + # 2023-08-05 Saturday 20:30:47. Hopefully just wrote the code for that up above, with the new list 'later_non_vuln_sibs'. + + closest_non_vulnerable_fix = "" + # if len(non_vuln_sibs) > 0: + # closest_non_vulnerable_fix = self.sort_by_version(non_vuln_sibs)[0] + if len(later_non_vuln_sibs) > 0: + closest_non_vulnerable_fix = self.sort_by_version(later_non_vuln_sibs)[0] + # else: + # # closest_non_vulnerable_fix = ( + # # "There are no reported non-vulnerable fixed packages." + # # ) + # closest_non_vulnerable_fix = None + + most_recent_non_vulnerable_fix = "" + if len(later_non_vuln_sibs) > 0: + most_recent_non_vulnerable_fix = self.sort_by_version(later_non_vuln_sibs)[-1] + else: + # most_recent_non_vulnerable_fix = ( + # "There are no reported non-vulnerable fixed packages." + # ) + most_recent_non_vulnerable_fix = None + + # if dict_vuln["vulnerability"] == vuln.vulnerability_id: + if dict_vuln["vulnerability"] == str(vuln): + # if dict_vuln["vulnerability"] == vuln.vcid: + # dict_vuln["closest_fixed_by_purl"] = "?????" + dict_vuln["closest_fixed_by_purl"] = str(closest_fixed_package) + dict_vuln["closest_fixed_by_url"] = closest_fixed_package.get_absolute_url() + # dict_vuln["closest_fixed_by_vulnerabilities"] = closest_fixed_package_vuln_count + # dict_vuln["closest_fixed_by_vulnerabilities"] = closest_fixed_package_vulns + # dict_vuln["closest_fixed_by_vulnerabilities"] = ["A", "B"] + + # dict_vuln["closest_fixed_by_vulnerabilities"] = closest_fixed_package_vulns_list + # ALERT: Replace the above list created with a namedtuple with the following dictionary: + dict_vuln["closest_fixed_by_vulnerabilities"] = closest_fixed_package_vulns_dict + # ALERT: Moved these up 1 level in the dict. + # dict_vuln["closest_non_vulnerable_fix"] = str(closest_non_vulnerable_fix) + # dict_vuln[ + # "closest_non_vulnerable_fix_url" + # ] = closest_non_vulnerable_fix.get_absolute_url() + # dict_vuln["most_recent_non_vulnerable_fix"] = str( + # most_recent_non_vulnerable_fix + # ) + # dict_vuln[ + # "most_recent_non_vulnerable_fix_url" + # ] = most_recent_non_vulnerable_fix.get_absolute_url() + + # QUESTION: Can we add the non-vuln data as higher-level key-value pairs rather than children of "vulnerabilities"? + + # purl_dict.update({"fruits": []}) + # purl_dict["fruits"].append({"fruit": "apple"}) + # purl_dict["fruits"].append({"fruit": "banana"}) + + # purl_dict.update( + # {"closest_non_vulnerable_fix": str(closest_non_vulnerable_fix)} + # ) + # purl_dict.update( + # { + # "closest_non_vulnerable_fix_url": closest_non_vulnerable_fix.get_absolute_url() + # } + # ) + # purl_dict.update( + # {"most_recent_non_vulnerable_fix": str(most_recent_non_vulnerable_fix)} + # ) + # purl_dict.update( + # { + # "most_recent_non_vulnerable_fix_url": most_recent_non_vulnerable_fix.get_absolute_url() + # } + # ) + + purl_dict["closest_non_vulnerable_fix"] = str(closest_non_vulnerable_fix) + purl_dict[ + "closest_non_vulnerable_fix_url" + ] = closest_non_vulnerable_fix.get_absolute_url() + purl_dict["most_recent_non_vulnerable_fix"] = str( + most_recent_non_vulnerable_fix + ) + purl_dict[ + "most_recent_non_vulnerable_fix_url" + ] = most_recent_non_vulnerable_fix.get_absolute_url() + + print("\npurl_dict = {}\n".format(purl_dict)) + + print(json.dumps(purl_dict, indent=4, sort_keys=False)) + + # # Print to text file + pretty_purl_dict = json.dumps(purl_dict, indent=4, sort_keys=False) + # logger = logging.getLogger(__name__) + # logger.setLevel(logging.INFO) + # # logger.addHandler(logging.FileHandler("2023-08-07-pretty_purl_dict.txt")) + # # will this overwrite prior writes? weird output + # logger.addHandler(logging.FileHandler("2023-08-07-pretty_purl_dict.txt", mode="w")) + # logger.info(pretty_purl_dict) + + with open("/home/jmh/pretty_purl_dict.txt", "w") as f: + f.write(pretty_purl_dict) + + alternate_dict_01 = { + "purl": "pkg:maven/com.fasterxml.jackson.core/jackson-databind@2.13.1", + "vulnerabilities": [ + { + "vulnerability": "VCID-2nyb-8rwu-aaag", + "closest_fixed_by_purl": "pkg:maven/com.fasterxml.jackson.core/jackson-databind@2.13.2", + # "get_absolute_url": reverse("package_details", args=[self.purl]), + "closest_fixed_by_url": reverse( + "package_details", + args=["pkg:maven/com.fasterxml.jackson.core/jackson-databind@2.13.2"], + ), + "closest_fixed_by_vulns": 2, + "non_vulnerable_fix": "pkg:maven/com.fasterxml.jackson.core/jackson-databind@2.14.0-rc1", + "non_vulnerable_fix_url": reverse( + "package_details", + args=["pkg:maven/com.fasterxml.jackson.core/jackson-databind@2.14.0-rc1"], + ), + }, + { + "vulnerability": "VCID-gqhw-ngh8-aaap", + "closest_fixed_by_purl": "pkg:maven/com.fasterxml.jackson.core/jackson-databind@2.13.4", + # "get_absolute_url": reverse("package_details", args=[self.purl]), + "closest_fixed_by_url": reverse( + "package_details", + args=["pkg:maven/com.fasterxml.jackson.core/jackson-databind@2.13.4"], + ), + "closest_fixed_by_vulns": 1, + "non_vulnerable_fix": "pkg:maven/com.fasterxml.jackson.core/jackson-databind@2.14.0-rc1", + "non_vulnerable_fix_url": reverse( + "package_details", + args=["pkg:maven/com.fasterxml.jackson.core/jackson-databind@2.14.0-rc1"], + ), + }, + { + "vulnerability": "VCID-t7e4-g3fr-aaan", + "closest_fixed_by_purl": "pkg:maven/com.fasterxml.jackson.core/jackson-databind@2.14.0-rc1", + # "get_absolute_url": reverse("package_details", args=[self.purl]), + "closest_fixed_by_url": reverse( + "package_details", + args=["pkg:maven/com.fasterxml.jackson.core/jackson-databind@2.14.0-rc1"], + ), + "closest_fixed_by_vulns": 0, + "non_vulnerable_fix": "pkg:maven/com.fasterxml.jackson.core/jackson-databind@2.14.0-rc1", + "non_vulnerable_fix_url": reverse( + "package_details", + args=["pkg:maven/com.fasterxml.jackson.core/jackson-databind@2.14.0-rc1"], + ), + }, + ], + } + + # return vcio_dict + + # return alternate_dict_01 + + return purl_dict class PackageRelatedVulnerability(models.Model): diff --git a/vulnerabilities/templates/package_details.html b/vulnerabilities/templates/package_details.html index cfbfdd706..8d204bccf 100644 --- a/vulnerabilities/templates/package_details.html +++ b/vulnerabilities/templates/package_details.html @@ -41,7 +41,6 @@
-
During testing, red = all packages that fix the vulnerability (for pkg in vulnerability.fixed_by_packages) and green = the closest fixing package whose version is greater than the affected package's version (if pkg in get_fixing_packages).
Affected by vulnerabilities ({{ affected_by_vulnerabilities|length }})
@@ -52,7 +51,7 @@ Vulnerability Summary Aliases - Fixing Packages + Fixed by packages @@ -76,22 +75,147 @@ {% endif %} {% endfor %} - + + + + + + + + {% if package.purl in fixed_package_details.purl %} + {% for key, value in fixed_package_details.items %} + {% if key == "vulnerabilities" %} + {% for abc in value %} + {% if abc.vulnerability == vulnerability.vulnerability_id %} +
    +
  • + PURL: {{ fixed_package_details.purl }} + + + +
  • +
  • + Vulnerability: {{ abc.vulnerability }} +
  • +
  • + Closest fixed-by PURL: + {{ abc.closest_fixed_by_purl }} +
  • +
  • + Closest fixed-by vulnerability count: {{ abc.closest_fixed_by_vulnerabilities|length }} + {% if abc.closest_fixed_by_vulnerabilities|length != 0 %} + + {% endif %} +
  • +
  • + Closest non-vulnerable fix: + + + {{ fixed_package_details.closest_non_vulnerable_fix }} +
  • +
  • + Most recent non-vulnerable fix: + + + {{ fixed_package_details.most_recent_non_vulnerable_fix }} +
  • +
+ {% endif %} + {% endfor %} + + + {% endif %} + {% endfor %} + {% else %} + NO-- {{ package.purl }} + {% endif %} + + + + + +
@@ -109,7 +233,7 @@
- Fixing vulnerabilities ({{ fixing_vulnerabilities|length }}) + Fixed by vulnerabilities ({{ fixing_vulnerabilities|length }})
diff --git a/vulnerabilities/tests/test_models.py b/vulnerabilities/tests/test_models.py index 58b95af80..24a66d2d2 100644 --- a/vulnerabilities/tests/test_models.py +++ b/vulnerabilities/tests/test_models.py @@ -7,12 +7,14 @@ # See https://aboutcode.org for more information about nexB OSS projects. # +import urllib.parse from datetime import datetime from unittest import TestCase import pytest from django.db.utils import IntegrityError from freezegun import freeze_time +from univers import versions from vulnerabilities import models @@ -88,3 +90,66 @@ def test_vulnerability_package(self): assert v1.vulnerable_packages.all()[0] == p1 assert v1.patched_packages.all()[0] == p2 + + +@pytest.mark.django_db +class TestPackageModel(TestCase): + def test_univers_version_comparisons(self): + assert versions.PypiVersion("1.2.3") < versions.PypiVersion("1.2.4") + + assert versions.PypiVersion("0.9") < versions.PypiVersion("0.10") + + # pkg:deb/debian/jackson-databind@2.12.1-1%2Bdeb11u1 is a real PURL in the DB + # But I get an error when I try to compare 2 PURLs with the same suffix -- + # univers.versions.InvalidVersion: '2.12.1-1%2Bdeb11u1' is not a valid + # Do we need to replace/delete the "%"? + # assert versions.DebianVersion("2.12.1-1%2Bdeb11u1") < versions.DebianVersion( + # "2.13.1-1%2Bdeb11u1" + # ) + # Test the error + with pytest.raises(versions.InvalidVersion): + assert versions.DebianVersion("2.12.1-1%2Bdeb11u1") < versions.DebianVersion( + "2.13.1-1%2Bdeb11u1" + ) + # Decode the version and test. + assert versions.DebianVersion( + urllib.parse.unquote("2.12.1-1%2Bdeb11u1") + ) < versions.DebianVersion(urllib.parse.unquote("2.13.1-1%2Bdeb11u1")) + + with pytest.raises(TypeError): + assert versions.PypiVersion("0.9") < versions.DebianVersion("0.10") + + # Using versions.Version does not correctly make this comparison! + assert not versions.Version("0.9") < versions.Version("0.10") + # Use SemverVersion instead as a default fallback version for comparisons. + assert versions.SemverVersion("0.9") < versions.SemverVersion("0.10") + + def test_assign_and_compare_univers_versions(self): + deb01 = models.Package.objects.create(type="deb", name="git", version="2.30.1") + deb02 = models.Package.objects.create(type="deb", name="git", version="2.31.1") + + immediate_fix01 = deb01.assign_and_compare_univers_versions(deb02) + print("\nimmediate_fix01 = {}\n".format(immediate_fix01)) + # assert deb01.assign_and_compare_univers_versions(deb02) is True + assert deb01.assign_and_compare_univers_versions(deb02) + + immediate_fix02 = deb02.assign_and_compare_univers_versions(deb01) + print("\nimmediate_fix02 = {}\n".format(immediate_fix02)) + # assert deb02.assign_and_compare_univers_versions(deb01) is False + assert not deb02.assign_and_compare_univers_versions(deb01) + + pypi01 = models.Package.objects.create(type="pypi", name="pyopenssl", version="0.9") + pypi02 = models.Package.objects.create(type="pypi", name="pyopenssl", version="0.10") + + immediate_fix03 = pypi01.assign_and_compare_univers_versions(pypi02) + print("\nimmediate_fix03 = {}\n".format(immediate_fix03)) + # assert pypi01.assign_and_compare_univers_versions(pypi02) is True + assert pypi01.assign_and_compare_univers_versions(pypi02) + + gem01 = models.Package.objects.create(type="gem", name="sidekiq", version="0.9") + gem02 = models.Package.objects.create(type="gem", name="sidekiq", version="0.10") + + immediate_fix04 = gem01.assign_and_compare_univers_versions(gem02) + print("\nimmediate_fix04 = {}\n".format(immediate_fix04)) + # assert gem01.assign_and_compare_univers_versions(gem02) is True + assert gem01.assign_and_compare_univers_versions(gem02) diff --git a/vulnerabilities/views.py b/vulnerabilities/views.py index 6a41620b3..396ac1bc2 100644 --- a/vulnerabilities/views.py +++ b/vulnerabilities/views.py @@ -83,7 +83,10 @@ def get_context_data(self, **kwargs): context["affected_by_vulnerabilities"] = package.affected_by.order_by("vulnerability_id") context["fixing_vulnerabilities"] = package.fixing.order_by("vulnerability_id") context["package_search_form"] = PackageSearchForm(self.request.GET) - context["get_fixing_packages"] = package.get_fixing_packages + # context["get_fixing_packages"] = package.get_fixing_packages + context["get_closest_fixed_package"] = package.get_closest_fixed_package + # context["test_property_01"] = package.test_property_01 + context["fixed_package_details"] = package.fixed_package_details return context diff --git a/vulnerablecode/settings.py b/vulnerablecode/settings.py index 3187b67ec..0071686fb 100644 --- a/vulnerablecode/settings.py +++ b/vulnerablecode/settings.py @@ -39,7 +39,9 @@ CSRF_TRUSTED_ORIGINS = env.list("CSRF_TRUSTED_ORIGINS", default=[]) # SECURITY WARNING: do not run with debug turned on in production -DEBUG = env.bool("VULNERABLECODE_DEBUG", default=False) +# DEBUG = env.bool("VULNERABLECODE_DEBUG", default=False) +# DEBUG = "127.0.0.1" +DEBUG = True # SECURITY WARNING: do not run with debug turned on in production DEBUG_TOOLBAR = env.bool("VULNERABLECODE_DEBUG_TOOLBAR", default=False) diff --git a/vulnerablecode/static/css/custom.css b/vulnerablecode/static/css/custom.css index 6699d8977..96ef5aad3 100644 --- a/vulnerablecode/static/css/custom.css +++ b/vulnerablecode/static/css/custom.css @@ -329,6 +329,14 @@ a.small_page_button { box-shadow: 0px 8px 16px 0px #808080; } +.dropdown-vuln-list-width { + width: 400px; +} + +.dropdown-vuln-dict-width { + max-width: 600px; +} + .width-100-pct { width: 100%; } @@ -368,4 +376,96 @@ a.small_page_button { span.tag.custom { margin: 0px 0px 6px 10px; -} \ No newline at end of file +} + +/* test bulleted list */ + +ul.fixed_by_bullet { + list-style-type: disc; + /*margin-top: 2px; +margin-bottom: 10px;*/ + /*margin-left: -24px;*/ + /*margin-left: -30px;*/ + margin-top: 0.25em; + margin-left: 7px; + margin-bottom: 0.25em; + padding-left: 10px; +} + +ul.fixed_by_bullet ul { + list-style-type: disc; + /*margin-top: 10px;*/ + margin-top: 5px; + margin-top: 0px; + margin-bottom: 0px; + margin-left: 23px; + margin-left: 18px; + padding: 0; + border: none; +} + + + +ul.fixed_by_bullet li { + margin-left: 0px; + font-family: Arial; + font-family: BlinkMacSystemFont, -apple-system, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", "Helvetica", "Arial", sans-serif; + font-size: 13px; + font-weight: normal; + /*margin-bottom: 10px;*/ + margin-bottom: 2px; +} + +ul.fixed_by_bullet li:last-child { + margin-left: 0px; + font-family: Arial; + font-family: BlinkMacSystemFont, -apple-system, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", "Helvetica", "Arial", sans-serif; + font-size: 13px; + font-weight: normal; + /*margin-bottom: 10px;*/ + margin-bottom: 0px; +} + +ul.fixed_by_bullet li li { + margin-left: 0px; + font-family: Arial; + font-family: BlinkMacSystemFont, -apple-system, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", "Helvetica", "Arial", sans-serif; + font-size: 13px; + font-weight: normal; + margin-top: 0px; + color: #000000; +} + +/* 10/10/15 add 3rd-level bullets */ +ul.fixed_by_bullet ul ul { + list-style-type: disc; + margin-top: 0px; + margin-bottom: 0px; + margin-left: 50px; + margin-left: 17px; + padding: 0; + border: none; +} + +ul.fixed_by_bullet li li li { + margin-left: 0px; + font-family: Arial; + font-family: BlinkMacSystemFont, -apple-system, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", "Helvetica", "Arial", sans-serif; + font-size: 13px; + font-weight: normal; + margin-top: 0px; + color: #000000; +} + +/* CSS for dev fixed by headers */ +.dev_fixed_by_headers { + border: solid 1px #cccccc; + border-radius: 3px; + background-color: #f2f2f2; + color: #000000; + font-weight: bold; + font-size: 13px; + padding: 3px; + margin-bottom: 3px; + display: block; +} From b6dba7826706c544bce36dbb4c642d1d5cc46416 Mon Sep 17 00:00:00 2001 From: "John M. Horan" Date: Wed, 9 Aug 2023 18:47:51 -0700 Subject: [PATCH 05/53] Improve matching and reporting code and UI #1228 Reference: https://github.com/nexB/vulnerablecode/issues/1228 Signed-off-by: John M. Horan --- vulnerabilities/models.py | 583 ++---------------- .../templates/package_details.html | 210 +++---- vulnerabilities/tests/test_models.py | 45 +- vulnerabilities/views.py | 3 - vulnerablecode/settings.py | 4 +- vulnerablecode/static/css/custom.css | 109 +--- 6 files changed, 154 insertions(+), 800 deletions(-) diff --git a/vulnerabilities/models.py b/vulnerabilities/models.py index d07a0530f..b8b2bea94 100644 --- a/vulnerabilities/models.py +++ b/vulnerabilities/models.py @@ -637,7 +637,8 @@ def get_fixed_packages(self, package): def get_sibling_packages(self, package): """ - Return a queryset of all packages with the same type, namespace, name, subpath and qualifiers of the `package`, whether or not they fix any vulnerability + Return a queryset of all packages with the same type, namespace, name, subpath and + qualifiers as the target package, whether or not the 'sibling packages' fix any vulnerability. """ return Package.objects.filter( name=package.name, @@ -645,462 +646,126 @@ def get_sibling_packages(self, package): type=package.type, qualifiers=package.qualifiers, subpath=package.subpath, - # packagerelatedvulnerability__fix=True, ).distinct() - # def assign_and_compare_univers_versions(self, fixed_pkg): def assign_univers_version(self, fixed_pkg): """ - Identify which univers version applies to the two packages to be compared (self and a fixed package), - evaluate whether the fixed_pkg version is > than the target affected package, and - return True or False. + Identify which univers version applies to a package and return that version for use, e.g., + in sorting a group of sibling packages (same type etc.). """ - - # TODO: Instead of return True or False based on evaluating the incoming fixed_pkg type to a univers version and then checking whether the fixed version is greater than the affected (self) version, we'll just use the incoming type to assign and return a univers version -- as command_name -- to be used in get_closest_fixed_package() below to add all greater-than versions to the later_matching_fixed_packages list -- which in turn will be fed to self.sort_by_version(later_matching_fixed_packages) - - # Many more to be added. + # TODO: Many more to be added. match_type_to_univers_version = { "conan": versions.ConanVersion, "deb": versions.DebianVersion, + # The following throws an error: AttributeError: module 'univers.versions' has no attribute 'GemVersion' + # "gem": versions.GemVersion, "maven": versions.MavenVersion, + "nginx": versions.NginxVersion, + # "npm": "openssl": versions.OpensslVersion, "pypi": versions.PypiVersion, + # from https://github.com/nexB/univers/blob/205da7ecbf7b0f195662373ea710b2b84a877eb0/tests/test_version_comparison.py + # versions.SemverVersion, + # versions.GolangVersion, + # versions.PypiVersion, + # versions.GenericVersion, + # versions.ComposerVersion, + # versions.NginxVersion, + # versions.ArchLinuxVersion, + # versions.DebianVersion, + # versions.RpmVersion, + # versions.MavenVersion, + # versions.NugetVersion, + # versions.GentooVersion, + # versions.OpensslVersion, + # versions.LegacyOpensslVersion, + # versions.AlpineLinuxVersion, } command_name = "" - matched_type_to_version = match_type_to_univers_version.get(fixed_pkg.type) if matched_type_to_version: - print("\t--------------------------") - print("*** matched_type_to_version = {}".format(matched_type_to_version)) command_name = matched_type_to_version - else: - print("\t--------------------------") - print("*** matched_type_to_version = NO MATCH") - # Using "command_name = versions.Version", the test - # assert versions.Version("0.9") < versions.Version("0.10") - # fails! - # command_name = versions.Version - # Use this as a default fallback instead. command_name = versions.SemverVersion - # if command_name(fixed_pkg.version) > command_name(self.version): - # return True - # else: - # return False - - # Instead return command_name for recipient to use as needed for sorting or perhaps other uses return command_name def sort_by_version(self, later_matching_fixed_packages): # Incoming is a list of - - # ALERT: added this to address server error 500 but related error arose: line 908, in get_closest_fixed_package - # HOT: How is this related to the source of the server error (500)? - if len(later_matching_fixed_packages) == 0: - return - - # Replace find_closest_fixed_by_package()? - print("\nlater_matching_fixed_packages = {}".format(later_matching_fixed_packages)) - print("\nlater_matching_fixed_packages[0] = {}".format(later_matching_fixed_packages[0])) - print( - "\ntype(later_matching_fixed_packages[0]) = {}".format( - type(later_matching_fixed_packages[0]) - ) - ) - # NOTE: This gives us the PURL type but instead we want the PURL itself to pass to assign_univers_version(self, fixed_pkg) and get the command_name in return, which we'll then use in the sort process. - print( - "\nlater_matching_fixed_packages[0].type = {}".format( - later_matching_fixed_packages[0].type - ) - ) - print( - "\ntype(later_matching_fixed_packages[0].type) = {}".format( - type(later_matching_fixed_packages[0].type) - ) - ) - - # Incoming is a list -- later_matching_fixed_packages - # We'll use assign_univers_version() above to get the univers version as a command_name. - # But what do we pass to it? The [0] index of the incoming list, i.e., later_matching_fixed_packages[0]? command_name = self.assign_univers_version(later_matching_fixed_packages[0]) - - print("\n>>> command_name = {}\n".format(command_name)) - - # TODO: Maybe we don't need to convert to a PURL, a list of dictionaries etc.?? - print( - "\n+++++++ later_matching_fixed_packages[0].version = {}".format( - later_matching_fixed_packages[0].version - ) - ) - - # sort test_sort_by_version = [] test_sort_by_version = sorted( - # later_matching_fixed_packages, key=lambda x: versions.DebianVersion(x["version"]) later_matching_fixed_packages, - # key=lambda x: versions.MavenVersion(x.version), key=lambda x: command_name(x.version), ) - print("\ntest_sort_by_version = {}\n".format(test_sort_by_version)) - return test_sort_by_version - # convert_to_dict_list = [] - - # sorted_later_matching_fixed_packages = [] - - # # TODO: First, convert to a list of dictionaries. - # for pkg in later_matching_fixed_packages: - # # pkg is a - # print("pkg = {}".format(pkg)) - # print("type(pkg) = {}".format(type(pkg))) - - # # pkg_str is a string - # pkg_str = pkg.package_url - # print("pkg_str = {}".format(pkg_str)) - # print("type(pkg_str) = {}".format(type(pkg_str))) - - # # purl is a - # purl = PackageURL.from_string(pkg_str) - # print("purl = {}".format(purl)) - # print("type(purl) = {}".format(type(purl))) - - # purl_dict = purl.to_dict() - # print("purl_dict = {}".format(purl_dict)) - # print("type(purl_dict) = {}".format(type(purl_dict))) - - # convert_to_dict_list.append(purl.to_dict()) - # print("HELLO\n") - - # print("\nconvert_to_dict_list = {}\n".format(convert_to_dict_list)) - - # return convert_to_dict_list - # ========================================================== - # sorted_later_matching_fixed_packages = sorted( - # later_matching_fixed_packages, key=lambda x: versions.MavenVersion(x["version"]) - # ) - # print( - # "\nsorted_later_matching_fixed_packages = {}\n".format( - # sorted_later_matching_fixed_packages - # ) - # ) - # print("\n".join(map(str, sorted_later_matching_fixed_packages))) - - # return what? - - # ========================================================== - # ========================================================== - - # def find_closest_fixed_by_package(self, later_matching_fixed_packages): - # # Maybe use sort_by_version() above instead? - # # take the incoming list later_matching_fixed_packages, convert to list of dictionaries, sort by version using univers.version.[version class], choose the top i.e., index [0] and convert back to PURL and return that PURL. - # print("\nlater_matching_fixed_packages = {}\n".format(later_matching_fixed_packages)) - - # closest_fixed_by_package = "TBD" - - # return closest_fixed_by_package - - @property - # def get_fixing_packages(self): - def get_closest_fixed_package(self): - """ - This function identifies the closest fixed package version that is greater than the affected package version and - is the same type, namespace, name, qualifiers and subpath as the affected package. - """ - - print("\nself = {}\n".format(self)) - - # This returns all fixed packages that match the target package (type etc.), regardless of fixed vuln. - # fixed_packages = self.get_fixed_packages(package=self) - # This is clearer. - matching_fixed_packages = self.get_fixed_packages(package=self) - - # This returns a list of the vulnerabilities that affect this package (i.e., self). - qs = self.vulnerabilities.filter(packagerelatedvulnerability__fix=False) - - # This takes the list of vulns affecting the current package, retrieves a list of the fixed packages for each vuln, and assigns the result to a custom attribute, `filtered_fixed_packages` (renamed 'matching_fixed_packages'). - # We use this in a for loop below like this -- qs[vuln_count].filtered_fixed_packages (renamed 'matching_fixed_packages') -- where `vuln_count` is used to iterate through the list of vulns that affect the current package (i.e., self). - qs = qs.prefetch_related( - Prefetch( - "packages", - # queryset=fixed_packages, - queryset=matching_fixed_packages, - # to_attr="filtered_fixed_packages", - to_attr="matching_fixed_packages", - ) - ) - - # Ex: qs[0].filtered_fixed_packages gives us the fixed package(s) for the 1st vuln for this affected package (i.e., self). - print("qs = {}\n".format(qs)) - - # ************************************************************************ - - later_matching_fixed_packages = [] - - vuln_count = 0 - for vuln in qs: - print("vuln = {}\n".format(vuln)) - # print( - # "\tqs[vuln_count].filtered_fixed_packages = {}".format( - # qs[vuln_count].filtered_fixed_packages - # ) - # ) - print( - "\tqs[vuln_count].matching_fixed_packages = {}".format( - qs[vuln_count].matching_fixed_packages - ) - ) - print("") - - # Check the Prefetch qs. - # TODO: Do we want to check whether the fixed version has any vulnerabilities of its own? - # for fixed_pkg in qs[vuln_count].filtered_fixed_packages: - for fixed_pkg in qs[vuln_count].matching_fixed_packages: - print("\tfixed_pkg = {}".format(fixed_pkg)) - print("\tfixed_pkg.type = {}".format(fixed_pkg.type)) - print("\tfixed_pkg.version = {}".format(fixed_pkg.version)) - print("\t--------------------------") - print("\tself.type = {}".format(self.type)) - print("\tself.version = {}".format(self.version)) - - # Assign univers version and compare: False = fixed_pkg.version < self.version (affected version). - - # 2023-08-02 Wednesday 16:01:35. atm immediate_fix is True or False. If instead assign_and_compare_univers_versions() returns the univers version, we could get that here and then test with this or similar right here -- enabling use of the univers version function in other places as well, like a sort_by_version function! - # if command_name(fixed_pkg.version) > command_name(self.version): - # return True - # else: - # return False - # ===================================================== - # Replace this with chunk below - # immediate_fix = self.assign_and_compare_univers_versions(fixed_pkg) - # print("\t--------------------------") - # print("\timmediate_fix = {}\n".format(immediate_fix)) - - # if fixed_pkg in fixed_packages and immediate_fix: - # later_matching_fixed_packages.append(fixed_pkg) - # ===================================================== - # command_name = self.assign_and_compare_univers_versions(fixed_pkg) - # renamed - # TODO: Move this up before the for loop -- both for loops if possible -- to reduce calls! - command_name = self.assign_univers_version(fixed_pkg) - print("\nJust requested command_name >>> {}\n".format(command_name)) - # if fixed_pkg in fixed_packages and command_name(fixed_pkg.version) > command_name( - # self.version - # ): - if fixed_pkg in matching_fixed_packages and command_name( - fixed_pkg.version - ) > command_name(self.version): - later_matching_fixed_packages.append(fixed_pkg) - - vuln_count += 1 - - # find_closest_fixed_by_package -- from the list later_matching_fixed_packages - # closest_fixed_by_package = self.find_closest_fixed_by_package(later_matching_fixed_packages) - - # TODO: or instead use this. This will be a list sorted by univers version class, and here all we need is to grab the [0] index from that list for the closest fixed by package! So we'd return a single closest_fixed_package. - # ALERT: The sort query needs to be done separately for each vulnerability because the list of fixed by packages is likely to be different. As is, we return a single sorted list of all fixed by packages for the affected package and then pass just the [0] package -- not what we want to do! - sort_fixed_by_packages_by_version = self.sort_by_version(later_matching_fixed_packages) - print( - "\nsort_fixed_by_packages_by_version = {}\n".format(sort_fixed_by_packages_by_version) - ) - # ALERT: 2023-08-05 Saturday 23:22:08. Address server error 500? - # ALERT: 2023-08-05 Saturday 23:24:50. This actusally fixed the server error (500) and I can now even see the Packafe details page for pkg:maven/com.fasterxml.jackson.core/jackson-databind@2.14.0-rc1 !!! - # HOT: I need to trace back the root cause of the server error (500). I suspect it's something like a record with no fixed by packages or something else that is an empty list but which i try to measure, e.g., for a print statement, or possibly for a real if condition.. - if sort_fixed_by_packages_by_version is None: - return - - closest_fixed_package = sort_fixed_by_packages_by_version[0] - - # print("\n!!! later_matching_fixed_packages = {}\n".format(later_matching_fixed_packages)) - # print( - # "\n!!! sort_fixed_by_packages_by_version = {}\n".format( - # sort_fixed_by_packages_by_version - # ) - # ) - # print("\n!!! closest_fixed_package = {}\n".format(closest_fixed_package)) - - # rebuilt_purl_from_dict = PackageURL( - # closest_fixed_package["type"], - # closest_fixed_package["namespace"], - # closest_fixed_package["name"], - # closest_fixed_package["version"], - # closest_fixed_package["qualifiers"], - # closest_fixed_package["subpath"], - # ) - # print("\n!!! rebuilt_purl_from_dict = {}\n".format(rebuilt_purl_from_dict)) - - # return later_matching_fixed_packages - return sort_fixed_by_packages_by_version - # return [closest_fixed_package] - - # return [rebuilt_purl_from_dict] - @property def fixed_package_details(self): """ - This is a test that might develop into a model-based equivalent of the loops etc. I was doing/trying to do in the Jinja2 template. I'm going to add this as a context so we can see it in the template. + Find and report all fixed by packages (and certain related versions) for each vulnerability in + each affected package. Report packages with no vulnerabilities as well. """ - # return "Hello" - - # vcio_dict = { - # [ - # {"VCID-2nyb-8rwu-aaag": "PURL01"}, - # {"VCID-gqhw-ngh8-aaap": "PURL02"}, - # {"some-other-id": "PURL03"}, - # ] - # } - - print("\n==> This is from the test_property_01() property.\n") - - print("\nself = {}\n".format(self)) - - # This returns all fixed packages that match the target package (type etc.), regardless of fixed vuln. - # fixed_packages = self.get_fixed_packages(package=self) - # This is clearer. + # Get all fixed packages that match the target package (type etc.), regardless of fixed vuln. matching_fixed_packages = self.get_fixed_packages(package=self) - # This returns a list of the vulnerabilities that affect this package (i.e., self). + # Get a list of the vulnerabilities that affect this package (i.e., self). qs = self.vulnerabilities.filter(packagerelatedvulnerability__fix=False) - # TODO: Can we get all sibling packages so that we can then determine which have 0 vulnerabilities and of these the closest and maybe the most recent as well? - all_sibling_packages = self.get_sibling_packages(package=self) - print("\nall_sibling_packages = {}\n".format(all_sibling_packages)) - print("\nlen(all_sibling_packages) = {}\n".format(len(all_sibling_packages))) non_vuln_sibs = [] for sib in all_sibling_packages: if sib.is_vulnerable is False: non_vuln_sibs.append(sib) - print("\nnon_vuln_sibs = {}\n".format(non_vuln_sibs)) - print("\nlen(non_vuln_sibs) = {}\n".format(len(non_vuln_sibs))) # Add just the greater-than versions to a new list command_name = self.assign_univers_version(self) - print( - "\nOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOO command_name = {}\n".format( - command_name - ) - ) + later_non_vuln_sibs = [] for non_vuln_sib in non_vuln_sibs: if command_name(non_vuln_sib.version) > command_name(self.version): later_non_vuln_sibs.append(non_vuln_sib) - print("\nlater_non_vuln_sibs = {}\n".format(later_non_vuln_sibs)) - print("\nlen(later_non_vuln_sibs) = {}\n".format(len(later_non_vuln_sibs))) - - # This takes the list of vulns affecting the current package, retrieves a list of the fixed packages for each vuln, and assigns the result to a custom attribute, `filtered_fixed_packages` (renamed 'matching_fixed_packages'). - # We use this in a for loop below like this -- qs[vuln_count].filtered_fixed_packages (renamed 'matching_fixed_packages') -- where `vuln_count` is used to iterate through the list of vulns that affect the current package (i.e., self). + # Take the list of vulns affecting the current package, retrieve a list of the fixed packages for each vuln, and assign the result to a custom attribute, 'matching_fixed_packages'. Ex: qs[0].matching_fixed_packages gives us the fixed package(s) for the 1st vuln for this affected package (i.e., self). qs = qs.prefetch_related( Prefetch( "packages", - # queryset=fixed_packages, queryset=matching_fixed_packages, - # to_attr="filtered_fixed_packages", to_attr="matching_fixed_packages", ) ) - # Ex: qs[0].filtered_fixed_packages gives us the fixed package(s) for the 1st vuln for this affected package (i.e., self). - print("\nzzz qs = {}\n".format(qs)) - purl_dict = {} - purl_dict["purl"] = self.purl - purl_dict.update({"vulnerabilities": []}) - # purl_dict["vulnerabilities"].append({"fruit": "orange"}) + # HOT: In constructing the purl_dict, I need to include packages that have no vulnerabilities -- we still want various dict fields to be populated to reflect the relevant structure and content all such packages. atm only a few fields are included in the dict for such a package. "closest_non_vulnerable_fix", "closest_non_vulnerable_fix_url", "most_recent_non_vulnerable_fix" and "most_recent_non_vulnerable_fix_url" are currently omitted. for vuln in qs: - print("\nzzz vuln = {}\n".format(vuln)) - print("\nzzz type(vuln) = {}\n".format(type(vuln))) - later_matching_fixed_packages = [] - # purl_dict[vuln.vulnerability_id] = "aaa" - # purl_dict.update({"vulnerability": vuln.vulnerability_id}) - purl_dict["vulnerabilities"].append({"vulnerability": vuln.vulnerability_id}) - - # TODO:2023-08-05 Saturday 13:12:28. This returns a list of matching fixed packages for this specific vuln! vuln_matching_fixed_packages = vuln.matching_fixed_packages - print("\nzzz self.purl = {}\n".format(self.purl)) - print("\nzzz vuln = {}\n".format(vuln)) - print("\nzzz vuln_matching_fixed_packages = {}\n".format(vuln_matching_fixed_packages)) - - # TODO: So we need to sort this list by version using the correct univers version and then return the [0] index in that sorted list - # QUESTION: Do we still need to remove lesser-than fixed packages or did we already do that? - - # ============================================================= - # command_name = self.assign_univers_version(fixed_pkg) command_name = self.assign_univers_version(self) - print("\nzzz command_name = {}\n".format(command_name)) - - # ALERT: What if there are no fixed by packages? The following thows an error because the list 'vuln_matching_fixed_packages' is empty! - # [I fixed this, right? ;-] - closest_fixed_package = "" if len(vuln_matching_fixed_packages) > 0: - for fixed_pkg in vuln_matching_fixed_packages: if fixed_pkg in matching_fixed_packages and command_name( fixed_pkg.version ) > command_name(self.version): later_matching_fixed_packages.append(fixed_pkg) - # print("\nJust requested command_name >>> {}\n".format(command_name)) - # # if fixed_pkg in fixed_packages and command_name(fixed_pkg.version) > command_name( - # # self.version - # # ): - # if fixed_pkg in matching_fixed_packages and command_name( - # fixed_pkg.version - # ) > command_name(self.version): - # later_matching_fixed_packages.append(fixed_pkg) - # ============================================================= - # later_matching_fixed_packages = vuln.matching_fixed_packages - - print( - "\nzzz later_matching_fixed_packages = {}\n".format( - later_matching_fixed_packages - ) - ) - sort_fixed_by_packages_by_version = self.sort_by_version( later_matching_fixed_packages ) - print( - "\nzzz sort_fixed_by_packages_by_version = {}\n".format( - sort_fixed_by_packages_by_version - ) - ) + closest_fixed_package = sort_fixed_by_packages_by_version[0] - # closest_fixed_package = sort_fixed_by_packages_by_version[0].purl - # 2023-08-06 Sunday 11:15:03. This returns a queryset of vulns affecting this package. - # HOT: How do we get the closest fixed by package vuln count and list of vulns? I keep getting errors. ;-) closest_fixed_package_vulns = closest_fixed_package.affected_by - # closest_fixed_package_vulns_list = list(closest_fixed_package_vulns) - # ALERT: 2023-08-06 Sunday 14:25:49. This did the trick! - - # closest_fixed_package_vulns_list = [ - # i.vulnerability_id for i in closest_fixed_package_vulns - # ] - - # 2023-08-06 Sunday 16:53:27. Try a named tuple to pass the vuln's vulnerability+id and get_absolute_url. - # FixedPackageVuln = namedtuple("FixedPackageVuln", "vuln_id, vuln_get_absolute_url") - # closest_fixed_package_vulns_list = [ - # FixedPackageVuln( - # vuln_id=fixed_pkg_vuln.vulnerability_id, - # vuln_get_absolute_url=fixed_pkg_vuln.get_absolute_url(), - # ) - # for fixed_pkg_vuln in closest_fixed_package_vulns - # ] - # ALERT: Replace the namedtuple with a dict -- this way it can be added to the purl_dict as a nested dict rather than a list of 2 values. + closest_fixed_package_vulns_dict = [ { "vuln_id": fixed_pkg_vuln.vulnerability_id, @@ -1109,127 +774,25 @@ def fixed_package_details(self): for fixed_pkg_vuln in closest_fixed_package_vulns ] - # === - # closest_fixed_package_vulns_list = closest_fixed_package_vulns.objects.values_list() - - # closest_fixed_package_vulns_list = [] - # for closest_vuln in closest_fixed_package_vulns: - # closest_fixed_package_vulns_list.append(closest_vuln) - # print( - # "\t\nQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQ vuln = {}\n".format( - # closest_vuln - # ) - # ) - # print("\t\ntype(closest_vuln) = {}".format(type(closest_vuln))) - - # # closest_fixed_package_vuln_count = len(closest_fixed_package.affected_by) - # print( - # "\t\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ type(closest_fixed_package_vulns) = {}\n".format( - # type(closest_fixed_package_vulns) - # ) - # ) - else: closest_fixed_package = "There are no reported fixed packages." - # Is None the value we want? We do not want to display anything but the count = 0. - # closest_fixed_package_vulns = None - # closest_fixed_package_vuln_count = 0 - - # closest_fixed_package_vulns_list = [] - - print("\nzzz closest_fixed_package = {}".format(closest_fixed_package)) - print("zzz type(closest_fixed_package) = {}\n".format(type(closest_fixed_package))) - - # TODO: How do we add 'closest_fixed_by_purl', 'closest_fixed_by_vulns' and 'non_vulnerable_fix'? - - # # for vuln in purl_dict["vulnerabilities"]: - # # # vuln["closest_fixed_by_purl"] = "?????" - # # vuln["closest_fixed_by_purl"] = closest_fixed_package - # # vuln["closest_fixed_by_url"] = "?????" - # # vuln["closest_fixed_by_vulnerabilities"] = "?????" - # # vuln["non_vulnerable_fix"] = "?????" - # # vuln["non_vulnerable_fix_url"] = "?????" for dict_vuln in purl_dict["vulnerabilities"]: - print("\n===================================> vuln = {}\n".format(vuln)) - print("\n===================================> type(vuln) = {}\n".format(type(vuln))) - print("\n===================================> vuln.vcid = {}\n".format(vuln.vcid)) - print( - "\n===================================> dict_vuln['vulnerability'] = {}\n".format( - dict_vuln["vulnerability"] - ) - ) - # TODO: Up above we defined 'non_vuln_sibs' but we still need to remove those with less than version - # ALERT: remove less than versions from 'non_vuln_sibs' - # 2023-08-05 Saturday 20:30:47. Hopefully just wrote the code for that up above, with the new list 'later_non_vuln_sibs'. - closest_non_vulnerable_fix = "" - # if len(non_vuln_sibs) > 0: - # closest_non_vulnerable_fix = self.sort_by_version(non_vuln_sibs)[0] + if len(later_non_vuln_sibs) > 0: closest_non_vulnerable_fix = self.sort_by_version(later_non_vuln_sibs)[0] - # else: - # # closest_non_vulnerable_fix = ( - # # "There are no reported non-vulnerable fixed packages." - # # ) - # closest_non_vulnerable_fix = None most_recent_non_vulnerable_fix = "" if len(later_non_vuln_sibs) > 0: most_recent_non_vulnerable_fix = self.sort_by_version(later_non_vuln_sibs)[-1] else: - # most_recent_non_vulnerable_fix = ( - # "There are no reported non-vulnerable fixed packages." - # ) most_recent_non_vulnerable_fix = None - # if dict_vuln["vulnerability"] == vuln.vulnerability_id: if dict_vuln["vulnerability"] == str(vuln): - # if dict_vuln["vulnerability"] == vuln.vcid: - # dict_vuln["closest_fixed_by_purl"] = "?????" dict_vuln["closest_fixed_by_purl"] = str(closest_fixed_package) dict_vuln["closest_fixed_by_url"] = closest_fixed_package.get_absolute_url() - # dict_vuln["closest_fixed_by_vulnerabilities"] = closest_fixed_package_vuln_count - # dict_vuln["closest_fixed_by_vulnerabilities"] = closest_fixed_package_vulns - # dict_vuln["closest_fixed_by_vulnerabilities"] = ["A", "B"] - - # dict_vuln["closest_fixed_by_vulnerabilities"] = closest_fixed_package_vulns_list - # ALERT: Replace the above list created with a namedtuple with the following dictionary: dict_vuln["closest_fixed_by_vulnerabilities"] = closest_fixed_package_vulns_dict - # ALERT: Moved these up 1 level in the dict. - # dict_vuln["closest_non_vulnerable_fix"] = str(closest_non_vulnerable_fix) - # dict_vuln[ - # "closest_non_vulnerable_fix_url" - # ] = closest_non_vulnerable_fix.get_absolute_url() - # dict_vuln["most_recent_non_vulnerable_fix"] = str( - # most_recent_non_vulnerable_fix - # ) - # dict_vuln[ - # "most_recent_non_vulnerable_fix_url" - # ] = most_recent_non_vulnerable_fix.get_absolute_url() - - # QUESTION: Can we add the non-vuln data as higher-level key-value pairs rather than children of "vulnerabilities"? - - # purl_dict.update({"fruits": []}) - # purl_dict["fruits"].append({"fruit": "apple"}) - # purl_dict["fruits"].append({"fruit": "banana"}) - - # purl_dict.update( - # {"closest_non_vulnerable_fix": str(closest_non_vulnerable_fix)} - # ) - # purl_dict.update( - # { - # "closest_non_vulnerable_fix_url": closest_non_vulnerable_fix.get_absolute_url() - # } - # ) - # purl_dict.update( - # {"most_recent_non_vulnerable_fix": str(most_recent_non_vulnerable_fix)} - # ) - # purl_dict.update( - # { - # "most_recent_non_vulnerable_fix_url": most_recent_non_vulnerable_fix.get_absolute_url() - # } - # ) purl_dict["closest_non_vulnerable_fix"] = str(closest_non_vulnerable_fix) purl_dict[ @@ -1242,76 +805,14 @@ def fixed_package_details(self): "most_recent_non_vulnerable_fix_url" ] = most_recent_non_vulnerable_fix.get_absolute_url() - print("\npurl_dict = {}\n".format(purl_dict)) - - print(json.dumps(purl_dict, indent=4, sort_keys=False)) - - # # Print to text file - pretty_purl_dict = json.dumps(purl_dict, indent=4, sort_keys=False) - # logger = logging.getLogger(__name__) - # logger.setLevel(logging.INFO) - # # logger.addHandler(logging.FileHandler("2023-08-07-pretty_purl_dict.txt")) - # # will this overwrite prior writes? weird output - # logger.addHandler(logging.FileHandler("2023-08-07-pretty_purl_dict.txt", mode="w")) - # logger.info(pretty_purl_dict) - - with open("/home/jmh/pretty_purl_dict.txt", "w") as f: - f.write(pretty_purl_dict) - - alternate_dict_01 = { - "purl": "pkg:maven/com.fasterxml.jackson.core/jackson-databind@2.13.1", - "vulnerabilities": [ - { - "vulnerability": "VCID-2nyb-8rwu-aaag", - "closest_fixed_by_purl": "pkg:maven/com.fasterxml.jackson.core/jackson-databind@2.13.2", - # "get_absolute_url": reverse("package_details", args=[self.purl]), - "closest_fixed_by_url": reverse( - "package_details", - args=["pkg:maven/com.fasterxml.jackson.core/jackson-databind@2.13.2"], - ), - "closest_fixed_by_vulns": 2, - "non_vulnerable_fix": "pkg:maven/com.fasterxml.jackson.core/jackson-databind@2.14.0-rc1", - "non_vulnerable_fix_url": reverse( - "package_details", - args=["pkg:maven/com.fasterxml.jackson.core/jackson-databind@2.14.0-rc1"], - ), - }, - { - "vulnerability": "VCID-gqhw-ngh8-aaap", - "closest_fixed_by_purl": "pkg:maven/com.fasterxml.jackson.core/jackson-databind@2.13.4", - # "get_absolute_url": reverse("package_details", args=[self.purl]), - "closest_fixed_by_url": reverse( - "package_details", - args=["pkg:maven/com.fasterxml.jackson.core/jackson-databind@2.13.4"], - ), - "closest_fixed_by_vulns": 1, - "non_vulnerable_fix": "pkg:maven/com.fasterxml.jackson.core/jackson-databind@2.14.0-rc1", - "non_vulnerable_fix_url": reverse( - "package_details", - args=["pkg:maven/com.fasterxml.jackson.core/jackson-databind@2.14.0-rc1"], - ), - }, - { - "vulnerability": "VCID-t7e4-g3fr-aaan", - "closest_fixed_by_purl": "pkg:maven/com.fasterxml.jackson.core/jackson-databind@2.14.0-rc1", - # "get_absolute_url": reverse("package_details", args=[self.purl]), - "closest_fixed_by_url": reverse( - "package_details", - args=["pkg:maven/com.fasterxml.jackson.core/jackson-databind@2.14.0-rc1"], - ), - "closest_fixed_by_vulns": 0, - "non_vulnerable_fix": "pkg:maven/com.fasterxml.jackson.core/jackson-databind@2.14.0-rc1", - "non_vulnerable_fix_url": reverse( - "package_details", - args=["pkg:maven/com.fasterxml.jackson.core/jackson-databind@2.14.0-rc1"], - ), - }, - ], - } - - # return vcio_dict + # Temporary print output during dev/testing: + # print("\npurl_dict = {}\n".format(purl_dict)) + # print(json.dumps(purl_dict, indent=4, sort_keys=False)) - # return alternate_dict_01 + # TODO: Consider whether we want to provide the user with an option to output the dictionary to a file. + # pretty_purl_dict = json.dumps(purl_dict, indent=4, sort_keys=False) + # with open("pretty_purl_dict.txt", "w") as f: + # f.write(pretty_purl_dict) return purl_dict diff --git a/vulnerabilities/templates/package_details.html b/vulnerabilities/templates/package_details.html index 8d204bccf..236379ab6 100644 --- a/vulnerabilities/templates/package_details.html +++ b/vulnerabilities/templates/package_details.html @@ -20,7 +20,7 @@ -
+
@@ -40,18 +40,47 @@
+ {% if affected_by_vulnerabilities|length != 0 %} + +
+ + + + + + + + + + + +
+ Closest non-vulnerable purl + + {{ fixed_package_details.closest_non_vulnerable_fix }} +
+ Latest non-vulnerable purl + + {{ fixed_package_details.most_recent_non_vulnerable_fix }} +
+
+ + {% endif %} +
Affected by vulnerabilities ({{ affected_by_vulnerabilities|length }})
+ + - + @@ -75,159 +104,63 @@ {% endif %} {% endfor %} - {% empty %} - + {% endfor %} -
Vulnerability Summary AliasesFixed by packagesClosest fixed by packages
- - - - - - - - {% if package.purl in fixed_package_details.purl %} - {% for key, value in fixed_package_details.items %} - {% if key == "vulnerabilities" %} + + {% if package.purl in fixed_package_details.purl %} + {% for key, value in fixed_package_details.items %} + {% if key == "vulnerabilities" %} {% for abc in value %} {% if abc.vulnerability == vulnerability.vulnerability_id %} -
    -
  • - PURL: {{ fixed_package_details.purl }} - - - -
  • -
  • - Vulnerability: {{ abc.vulnerability }} -
  • -
  • - Closest fixed-by PURL: - {{ abc.closest_fixed_by_purl }} -
  • -
  • - Closest fixed-by vulnerability count: {{ abc.closest_fixed_by_vulnerabilities|length }} - {% if abc.closest_fixed_by_vulnerabilities|length != 0 %} - - {% endif %} -
  • -
  • - Closest non-vulnerable fix: - - - {{ fixed_package_details.closest_non_vulnerable_fix }} -
  • -
  • - Most recent non-vulnerable fix: - - - {{ fixed_package_details.most_recent_non_vulnerable_fix }} -
  • -
{% endif %} {% endfor %} - - - {% endif %} - {% endfor %} - {% else %} - NO-- {{ package.purl }} {% endif %} + {% endfor %} - - - - - - + {% endif %}
This package is not known to be affected by vulnerabilities.
@@ -244,7 +177,6 @@ Aliases - {% for vulnerability in fixing_vulnerabilities %} diff --git a/vulnerabilities/tests/test_models.py b/vulnerabilities/tests/test_models.py index 24a66d2d2..3535173ee 100644 --- a/vulnerabilities/tests/test_models.py +++ b/vulnerabilities/tests/test_models.py @@ -96,60 +96,43 @@ def test_vulnerability_package(self): class TestPackageModel(TestCase): def test_univers_version_comparisons(self): assert versions.PypiVersion("1.2.3") < versions.PypiVersion("1.2.4") - assert versions.PypiVersion("0.9") < versions.PypiVersion("0.10") # pkg:deb/debian/jackson-databind@2.12.1-1%2Bdeb11u1 is a real PURL in the DB - # But I get an error when I try to compare 2 PURLs with the same suffix -- - # univers.versions.InvalidVersion: '2.12.1-1%2Bdeb11u1' is not a valid - # Do we need to replace/delete the "%"? - # assert versions.DebianVersion("2.12.1-1%2Bdeb11u1") < versions.DebianVersion( - # "2.13.1-1%2Bdeb11u1" - # ) - # Test the error + # But we need to replace/delete the "%". Test the error: with pytest.raises(versions.InvalidVersion): assert versions.DebianVersion("2.12.1-1%2Bdeb11u1") < versions.DebianVersion( "2.13.1-1%2Bdeb11u1" ) - # Decode the version and test. + # Decode the version and test: assert versions.DebianVersion( urllib.parse.unquote("2.12.1-1%2Bdeb11u1") ) < versions.DebianVersion(urllib.parse.unquote("2.13.1-1%2Bdeb11u1")) + # Expect an error when comparing different types. with pytest.raises(TypeError): assert versions.PypiVersion("0.9") < versions.DebianVersion("0.10") - # Using versions.Version does not correctly make this comparison! + # This demonstrates that versions.Version does not correctly compare 0.9 vs. 0.10. assert not versions.Version("0.9") < versions.Version("0.10") # Use SemverVersion instead as a default fallback version for comparisons. assert versions.SemverVersion("0.9") < versions.SemverVersion("0.10") - def test_assign_and_compare_univers_versions(self): deb01 = models.Package.objects.create(type="deb", name="git", version="2.30.1") deb02 = models.Package.objects.create(type="deb", name="git", version="2.31.1") + assert versions.DebianVersion(deb01.version) < versions.DebianVersion(deb02.version) - immediate_fix01 = deb01.assign_and_compare_univers_versions(deb02) - print("\nimmediate_fix01 = {}\n".format(immediate_fix01)) - # assert deb01.assign_and_compare_univers_versions(deb02) is True - assert deb01.assign_and_compare_univers_versions(deb02) + def test_assign_univers_version(self): + requesting_package = models.Package.objects.create(type="deb", name="git", version="2.30.1") - immediate_fix02 = deb02.assign_and_compare_univers_versions(deb01) - print("\nimmediate_fix02 = {}\n".format(immediate_fix02)) - # assert deb02.assign_and_compare_univers_versions(deb01) is False - assert not deb02.assign_and_compare_univers_versions(deb01) + deb01 = models.Package.objects.create(type="deb", name="git", version="2.31.1") + command_name_deb01 = requesting_package.assign_univers_version(deb01) + assert command_name_deb01 == versions.DebianVersion pypi01 = models.Package.objects.create(type="pypi", name="pyopenssl", version="0.9") - pypi02 = models.Package.objects.create(type="pypi", name="pyopenssl", version="0.10") - - immediate_fix03 = pypi01.assign_and_compare_univers_versions(pypi02) - print("\nimmediate_fix03 = {}\n".format(immediate_fix03)) - # assert pypi01.assign_and_compare_univers_versions(pypi02) is True - assert pypi01.assign_and_compare_univers_versions(pypi02) + command_name_pypi01 = requesting_package.assign_univers_version(pypi01) + assert command_name_pypi01 == versions.PypiVersion gem01 = models.Package.objects.create(type="gem", name="sidekiq", version="0.9") - gem02 = models.Package.objects.create(type="gem", name="sidekiq", version="0.10") - - immediate_fix04 = gem01.assign_and_compare_univers_versions(gem02) - print("\nimmediate_fix04 = {}\n".format(immediate_fix04)) - # assert gem01.assign_and_compare_univers_versions(gem02) is True - assert gem01.assign_and_compare_univers_versions(gem02) + command_name_gem01 = requesting_package.assign_univers_version(gem01) + assert command_name_gem01 == versions.SemverVersion diff --git a/vulnerabilities/views.py b/vulnerabilities/views.py index 396ac1bc2..41ecb1455 100644 --- a/vulnerabilities/views.py +++ b/vulnerabilities/views.py @@ -83,9 +83,6 @@ def get_context_data(self, **kwargs): context["affected_by_vulnerabilities"] = package.affected_by.order_by("vulnerability_id") context["fixing_vulnerabilities"] = package.fixing.order_by("vulnerability_id") context["package_search_form"] = PackageSearchForm(self.request.GET) - # context["get_fixing_packages"] = package.get_fixing_packages - context["get_closest_fixed_package"] = package.get_closest_fixed_package - # context["test_property_01"] = package.test_property_01 context["fixed_package_details"] = package.fixed_package_details return context diff --git a/vulnerablecode/settings.py b/vulnerablecode/settings.py index 0071686fb..3187b67ec 100644 --- a/vulnerablecode/settings.py +++ b/vulnerablecode/settings.py @@ -39,9 +39,7 @@ CSRF_TRUSTED_ORIGINS = env.list("CSRF_TRUSTED_ORIGINS", default=[]) # SECURITY WARNING: do not run with debug turned on in production -# DEBUG = env.bool("VULNERABLECODE_DEBUG", default=False) -# DEBUG = "127.0.0.1" -DEBUG = True +DEBUG = env.bool("VULNERABLECODE_DEBUG", default=False) # SECURITY WARNING: do not run with debug turned on in production DEBUG_TOOLBAR = env.bool("VULNERABLECODE_DEBUG_TOOLBAR", default=False) diff --git a/vulnerablecode/static/css/custom.css b/vulnerablecode/static/css/custom.css index 96ef5aad3..38da36039 100644 --- a/vulnerablecode/static/css/custom.css +++ b/vulnerablecode/static/css/custom.css @@ -198,16 +198,14 @@ code { } .two-col-left { - width: 160px; + width: 250px; text-align: right !important; font-weight: bold; - padding-right: 20px !important; + padding-right: 15px !important; line-height: 20px; - border: solid 1px #e8e8e8 !important; } .two-col-right { - border: solid 1px #e8e8e8 !important; line-height: 20px; } @@ -378,85 +376,6 @@ span.tag.custom { margin: 0px 0px 6px 10px; } -/* test bulleted list */ - -ul.fixed_by_bullet { - list-style-type: disc; - /*margin-top: 2px; -margin-bottom: 10px;*/ - /*margin-left: -24px;*/ - /*margin-left: -30px;*/ - margin-top: 0.25em; - margin-left: 7px; - margin-bottom: 0.25em; - padding-left: 10px; -} - -ul.fixed_by_bullet ul { - list-style-type: disc; - /*margin-top: 10px;*/ - margin-top: 5px; - margin-top: 0px; - margin-bottom: 0px; - margin-left: 23px; - margin-left: 18px; - padding: 0; - border: none; -} - - - -ul.fixed_by_bullet li { - margin-left: 0px; - font-family: Arial; - font-family: BlinkMacSystemFont, -apple-system, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", "Helvetica", "Arial", sans-serif; - font-size: 13px; - font-weight: normal; - /*margin-bottom: 10px;*/ - margin-bottom: 2px; -} - -ul.fixed_by_bullet li:last-child { - margin-left: 0px; - font-family: Arial; - font-family: BlinkMacSystemFont, -apple-system, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", "Helvetica", "Arial", sans-serif; - font-size: 13px; - font-weight: normal; - /*margin-bottom: 10px;*/ - margin-bottom: 0px; -} - -ul.fixed_by_bullet li li { - margin-left: 0px; - font-family: Arial; - font-family: BlinkMacSystemFont, -apple-system, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", "Helvetica", "Arial", sans-serif; - font-size: 13px; - font-weight: normal; - margin-top: 0px; - color: #000000; -} - -/* 10/10/15 add 3rd-level bullets */ -ul.fixed_by_bullet ul ul { - list-style-type: disc; - margin-top: 0px; - margin-bottom: 0px; - margin-left: 50px; - margin-left: 17px; - padding: 0; - border: none; -} - -ul.fixed_by_bullet li li li { - margin-left: 0px; - font-family: Arial; - font-family: BlinkMacSystemFont, -apple-system, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", "Helvetica", "Arial", sans-serif; - font-size: 13px; - font-weight: normal; - margin-top: 0px; - color: #000000; -} - /* CSS for dev fixed by headers */ .dev_fixed_by_headers { border: solid 1px #cccccc; @@ -469,3 +388,27 @@ ul.fixed_by_bullet li li li { margin-bottom: 3px; display: block; } + +.non-floating-purl { + position: relative; + width: 100%; + z-index: 100; + margin-bottom: 0px; +} + +.non-floating-purl .table td, +.non-floating-purl .table tbody tr:last-child td, +.non-floating-purl .table th { + border: solid 1px #dbdbdb; + background-color: #ffffff; +} + +.non-vuln { + margin-top: -25px; +} + +.non-vuln .table td, +.non-vuln .table tbody tr:last-child td, +.non-vuln .table th { + border: solid 1px #dbdbdb; +} From 814cd063bc10e533de5bb027149ff59ffe1364f3 Mon Sep 17 00:00:00 2001 From: "John M. Horan" Date: Mon, 14 Aug 2023 15:04:38 -0700 Subject: [PATCH 06/53] Add univers version, revise sort and related code, update and add new tests #1228 Reference: https://github.com/nexB/vulnerablecode/issues/1228 Signed-off-by: John M. Horan --- vulnerabilities/models.py | 117 +++++------ vulnerabilities/tests/test_models.py | 292 +++++++++++++++++++++++++-- 2 files changed, 326 insertions(+), 83 deletions(-) diff --git a/vulnerabilities/models.py b/vulnerabilities/models.py index b8b2bea94..9b46cff97 100644 --- a/vulnerabilities/models.py +++ b/vulnerabilities/models.py @@ -33,6 +33,7 @@ from packageurl.contrib.django.models import without_empty_values from rest_framework.authtoken.models import Token from univers import versions +from univers.version_range import RANGE_CLASS_BY_SCHEMES from vulnerabilities.importer import AdvisoryData from vulnerabilities.importer import AffectedPackage @@ -648,60 +649,16 @@ def get_sibling_packages(self, package): subpath=package.subpath, ).distinct() - def assign_univers_version(self, fixed_pkg): - """ - Identify which univers version applies to a package and return that version for use, e.g., - in sorting a group of sibling packages (same type etc.). - """ - # TODO: Many more to be added. - match_type_to_univers_version = { - "conan": versions.ConanVersion, - "deb": versions.DebianVersion, - # The following throws an error: AttributeError: module 'univers.versions' has no attribute 'GemVersion' - # "gem": versions.GemVersion, - "maven": versions.MavenVersion, - "nginx": versions.NginxVersion, - # "npm": - "openssl": versions.OpensslVersion, - "pypi": versions.PypiVersion, - # from https://github.com/nexB/univers/blob/205da7ecbf7b0f195662373ea710b2b84a877eb0/tests/test_version_comparison.py - # versions.SemverVersion, - # versions.GolangVersion, - # versions.PypiVersion, - # versions.GenericVersion, - # versions.ComposerVersion, - # versions.NginxVersion, - # versions.ArchLinuxVersion, - # versions.DebianVersion, - # versions.RpmVersion, - # versions.MavenVersion, - # versions.NugetVersion, - # versions.GentooVersion, - # versions.OpensslVersion, - # versions.LegacyOpensslVersion, - # versions.AlpineLinuxVersion, - } - - command_name = "" - matched_type_to_version = match_type_to_univers_version.get(fixed_pkg.type) - if matched_type_to_version: - command_name = matched_type_to_version - else: - command_name = versions.SemverVersion - - return command_name - - def sort_by_version(self, later_matching_fixed_packages): + def sort_by_version(self, packages_to_sort): # Incoming is a list of - # We'll use assign_univers_version() above to get the univers version as a command_name. - command_name = self.assign_univers_version(later_matching_fixed_packages[0]) - test_sort_by_version = [] - test_sort_by_version = sorted( - later_matching_fixed_packages, - key=lambda x: command_name(x.version), + univers_version = RANGE_CLASS_BY_SCHEMES[packages_to_sort[0].type].version_class + sorted_by_version = [] + sorted_by_version = sorted( + packages_to_sort, + key=lambda x: univers_version(x.version), ) - return test_sort_by_version + return sorted_by_version @property def fixed_package_details(self): @@ -723,11 +680,10 @@ def fixed_package_details(self): non_vuln_sibs.append(sib) # Add just the greater-than versions to a new list - command_name = self.assign_univers_version(self) - + univers_version = RANGE_CLASS_BY_SCHEMES[self.type].version_class later_non_vuln_sibs = [] for non_vuln_sib in non_vuln_sibs: - if command_name(non_vuln_sib.version) > command_name(self.version): + if univers_version(non_vuln_sib.version) > univers_version(self.version): later_non_vuln_sibs.append(non_vuln_sib) # Take the list of vulns affecting the current package, retrieve a list of the fixed packages for each vuln, and assign the result to a custom attribute, 'matching_fixed_packages'. Ex: qs[0].matching_fixed_packages gives us the fixed package(s) for the 1st vuln for this affected package (i.e., self). @@ -743,20 +699,24 @@ def fixed_package_details(self): purl_dict["purl"] = self.purl purl_dict.update({"vulnerabilities": []}) - # HOT: In constructing the purl_dict, I need to include packages that have no vulnerabilities -- we still want various dict fields to be populated to reflect the relevant structure and content all such packages. atm only a few fields are included in the dict for such a package. "closest_non_vulnerable_fix", "closest_non_vulnerable_fix_url", "most_recent_non_vulnerable_fix" and "most_recent_non_vulnerable_fix_url" are currently omitted. + purl_dict["closest_non_vulnerable_fix"] = "" + purl_dict["closest_non_vulnerable_fix_url"] = "" + purl_dict["most_recent_non_vulnerable_fix"] = "" + purl_dict["most_recent_non_vulnerable_fix_url"] = "" for vuln in qs: later_matching_fixed_packages = [] purl_dict["vulnerabilities"].append({"vulnerability": vuln.vulnerability_id}) vuln_matching_fixed_packages = vuln.matching_fixed_packages - command_name = self.assign_univers_version(self) closest_fixed_package = "" + # Need to define here to avoid "" vs. [] for some closest_fixed_by_vulnerabilities values? + closest_fixed_package_vulns_dict = [] if len(vuln_matching_fixed_packages) > 0: for fixed_pkg in vuln_matching_fixed_packages: - if fixed_pkg in matching_fixed_packages and command_name( + if fixed_pkg in matching_fixed_packages and univers_version( fixed_pkg.version - ) > command_name(self.version): + ) > univers_version(self.version): later_matching_fixed_packages.append(fixed_pkg) sort_fixed_by_packages_by_version = self.sort_by_version( @@ -765,7 +725,7 @@ def fixed_package_details(self): closest_fixed_package = sort_fixed_by_packages_by_version[0] closest_fixed_package_vulns = closest_fixed_package.affected_by - + # 2023-08-13 Sunday 16:25:41. Not sure but I think I need to define this initially above just after closest_fixed_package = "" closest_fixed_package_vulns_dict = [ { "vuln_id": fixed_pkg_vuln.vulnerability_id, @@ -780,6 +740,7 @@ def fixed_package_details(self): for dict_vuln in purl_dict["vulnerabilities"]: closest_non_vulnerable_fix = "" + # TODO: 2023-08-13 Sunday 16:01:57. Refactor here and elsewhere! if len(later_non_vuln_sibs) > 0: closest_non_vulnerable_fix = self.sort_by_version(later_non_vuln_sibs)[0] @@ -787,27 +748,45 @@ def fixed_package_details(self): if len(later_non_vuln_sibs) > 0: most_recent_non_vulnerable_fix = self.sort_by_version(later_non_vuln_sibs)[-1] else: - most_recent_non_vulnerable_fix = None + most_recent_non_vulnerable_fix = "" if dict_vuln["vulnerability"] == str(vuln): dict_vuln["closest_fixed_by_purl"] = str(closest_fixed_package) - dict_vuln["closest_fixed_by_url"] = closest_fixed_package.get_absolute_url() - dict_vuln["closest_fixed_by_vulnerabilities"] = closest_fixed_package_vulns_dict + if len(vuln_matching_fixed_packages) > 0: + dict_vuln["closest_fixed_by_url"] = closest_fixed_package.get_absolute_url() + closest_fixed_package_vulns_dict = [ + { + "vuln_id": fixed_pkg_vuln.vulnerability_id, + "vuln_get_absolute_url": fixed_pkg_vuln.get_absolute_url(), + } + for fixed_pkg_vuln in closest_fixed_package_vulns + ] + dict_vuln[ + "closest_fixed_by_vulnerabilities" + ] = closest_fixed_package_vulns_dict + else: + dict_vuln["closest_fixed_by_url"] = "" + dict_vuln["closest_fixed_by_vulnerabilities"] = [] purl_dict["closest_non_vulnerable_fix"] = str(closest_non_vulnerable_fix) - purl_dict[ - "closest_non_vulnerable_fix_url" - ] = closest_non_vulnerable_fix.get_absolute_url() + if len(vuln_matching_fixed_packages) > 0: + purl_dict[ + "closest_non_vulnerable_fix_url" + ] = closest_non_vulnerable_fix.get_absolute_url() + purl_dict[ + "most_recent_non_vulnerable_fix_url" + ] = most_recent_non_vulnerable_fix.get_absolute_url() + else: + purl_dict["closest_non_vulnerable_fix_url"] = "" + purl_dict["most_recent_non_vulnerable_fix_url"] = "" + purl_dict["most_recent_non_vulnerable_fix"] = str( most_recent_non_vulnerable_fix ) - purl_dict[ - "most_recent_non_vulnerable_fix_url" - ] = most_recent_non_vulnerable_fix.get_absolute_url() # Temporary print output during dev/testing: # print("\npurl_dict = {}\n".format(purl_dict)) - # print(json.dumps(purl_dict, indent=4, sort_keys=False)) + print(json.dumps(purl_dict, indent=4, sort_keys=False)) # TODO: Consider whether we want to provide the user with an option to output the dictionary to a file. # pretty_purl_dict = json.dumps(purl_dict, indent=4, sort_keys=False) diff --git a/vulnerabilities/tests/test_models.py b/vulnerabilities/tests/test_models.py index 3535173ee..59b08dd79 100644 --- a/vulnerabilities/tests/test_models.py +++ b/vulnerabilities/tests/test_models.py @@ -10,13 +10,19 @@ import urllib.parse from datetime import datetime from unittest import TestCase +from unittest import mock import pytest +from django.db import transaction +from django.db.models.query import QuerySet from django.db.utils import IntegrityError from freezegun import freeze_time +from packageurl import PackageURL from univers import versions +from univers.version_range import RANGE_CLASS_BY_SCHEMES from vulnerabilities import models +from vulnerabilities.models import Package class TestVulnerabilityModel(TestCase): @@ -94,10 +100,156 @@ def test_vulnerability_package(self): @pytest.mark.django_db class TestPackageModel(TestCase): + def setUp(self): + self.vuln1 = models.Vulnerability.objects.create( + summary="test-vuln1", + vulnerability_id="VCID-123", + ) + self.vuln2 = models.Vulnerability.objects.create( + summary="test-vuln2", + vulnerability_id="VCID-456", + ) + + # Create a vuln of its own for the fixed_by_package + self.vuln3 = models.Vulnerability.objects.create( + summary="test-vuln-not-used-anywhere", + vulnerability_id="VCID-000", + ) + + self.vulnerablecode_package = models.Package.objects.create( + type="maven", + namespace="com.fasterxml.jackson.core", + name="jackson-databind", + version="2.13.1", + qualifiers={}, + subpath="", + ) + + self.fixed_by_package = models.Package.objects.create( + type="maven", + namespace="com.fasterxml.jackson.core", + name="jackson-databind", + version="2.13.2", + qualifiers={}, + subpath="", + ) + + self.backport_fixed_by_package = models.Package.objects.create( + type="maven", + namespace="com.fasterxml.jackson.core", + name="jackson-databind", + version="2.12.6.1", + qualifiers={}, + subpath="", + ) + + self.non_vulnerable_package = models.Package.objects.create( + type="maven", + namespace="com.fasterxml.jackson.core", + name="jackson-databind", + version="2.14.0-rc1", + qualifiers={}, + subpath="", + ) + + models.PackageRelatedVulnerability.objects.create( + package=self.vulnerablecode_package, + vulnerability=self.vuln1, + fix=False, + ) + + models.PackageRelatedVulnerability.objects.create( + package=self.vulnerablecode_package, + vulnerability=self.vuln2, + fix=False, + ) + + # Create a fixed_by package for vuln1 + models.PackageRelatedVulnerability.objects.create( + package=self.fixed_by_package, + vulnerability=self.vuln1, + fix=True, + ) + + # Add backport_fixed_by_package as a fixed_by for vuln1 -- but this should be excluded because its version is less than the affected package's version. + models.PackageRelatedVulnerability.objects.create( + package=self.backport_fixed_by_package, + vulnerability=self.vuln1, + fix=True, + ) + + # Create a vuln of its own for the fixed_by_packagefixed_by package for vuln1 + models.PackageRelatedVulnerability.objects.create( + package=self.fixed_by_package, + vulnerability=self.vuln3, + fix=False, + ) + + def test_get_vulnerable_packages(self): + vuln_packages = Package.objects.vulnerable() + assert vuln_packages.count() == 3 + assert vuln_packages.distinct().count() == 2 + + # matching_fixed_packages = vulnerablecode_package.get_fixed_packages(vulnerablecode_package) + # assert vuln_packages.distinct()[0] + + # matching_fixed_packages = vuln_packages.distinct()[0].get_fixed_packages( + # vuln_packages.distinct()[0] + # ) + + first_vulnerable_package = vuln_packages.distinct()[0] + matching_fixed_packages = first_vulnerable_package.get_fixed_packages( + first_vulnerable_package + ) + first_fixed_by_package = matching_fixed_packages[0] + + assert ( + first_vulnerable_package.purl + == "pkg:maven/com.fasterxml.jackson.core/jackson-databind@2.13.1" + ) + assert len(matching_fixed_packages) == 2 + assert ( + first_fixed_by_package.purl + == "pkg:maven/com.fasterxml.jackson.core/jackson-databind@2.12.6.1" + ) + + purl_dict = { + "purl": "pkg:maven/com.fasterxml.jackson.core/jackson-databind@2.13.1", + "vulnerabilities": [ + { + "vulnerability": "VCID-123", + "closest_fixed_by_purl": "pkg:maven/com.fasterxml.jackson.core/jackson-databind@2.13.2", + "closest_fixed_by_url": "/packages/pkg:maven/com.fasterxml.jackson.core/jackson-databind@2.13.2", + "closest_fixed_by_vulnerabilities": [ + { + "vuln_id": "VCID-000", + "vuln_get_absolute_url": "/vulnerabilities/VCID-000", + } + ], + }, + { + "vulnerability": "VCID-456", + "closest_fixed_by_purl": "There are no reported fixed packages.", + "closest_fixed_by_url": "", + "closest_fixed_by_vulnerabilities": [], + }, + ], + "closest_non_vulnerable_fix": "pkg:maven/com.fasterxml.jackson.core/jackson-databind@2.14.0-rc1", + "closest_non_vulnerable_fix_url": "", + "most_recent_non_vulnerable_fix": "pkg:maven/com.fasterxml.jackson.core/jackson-databind@2.14.0-rc1", + "most_recent_non_vulnerable_fix_url": "", + } + + assert vuln_packages.distinct()[0].fixed_package_details == purl_dict + def test_univers_version_comparisons(self): assert versions.PypiVersion("1.2.3") < versions.PypiVersion("1.2.4") assert versions.PypiVersion("0.9") < versions.PypiVersion("0.10") + deb01 = models.Package.objects.create(type="deb", name="git", version="2.30.1") + deb02 = models.Package.objects.create(type="deb", name="git", version="2.31.1") + assert versions.DebianVersion(deb01.version) < versions.DebianVersion(deb02.version) + # pkg:deb/debian/jackson-databind@2.12.1-1%2Bdeb11u1 is a real PURL in the DB # But we need to replace/delete the "%". Test the error: with pytest.raises(versions.InvalidVersion): @@ -118,21 +270,133 @@ def test_univers_version_comparisons(self): # Use SemverVersion instead as a default fallback version for comparisons. assert versions.SemverVersion("0.9") < versions.SemverVersion("0.10") - deb01 = models.Package.objects.create(type="deb", name="git", version="2.30.1") - deb02 = models.Package.objects.create(type="deb", name="git", version="2.31.1") - assert versions.DebianVersion(deb01.version) < versions.DebianVersion(deb02.version) + def test_univers_version_class(self): + gem_version = RANGE_CLASS_BY_SCHEMES["gem"].version_class + assert gem_version == versions.RubygemsVersion + + gem_package = models.Package.objects.create(type="gem", name="sidekiq", version="0.9") + gem_package_version = RANGE_CLASS_BY_SCHEMES[gem_package.type].version_class + assert gem_package_version == versions.RubygemsVersion + + deb_version = RANGE_CLASS_BY_SCHEMES["deb"].version_class + assert deb_version == versions.DebianVersion + + deb_package = models.Package.objects.create(type="deb", name="git", version="2.31.1") + deb_package_version = RANGE_CLASS_BY_SCHEMES[deb_package.type].version_class + assert deb_package_version == versions.DebianVersion + + pypi_version = RANGE_CLASS_BY_SCHEMES["pypi"].version_class + assert pypi_version == versions.PypiVersion + + pypi_package = models.Package.objects.create(type="pypi", name="pyopenssl", version="0.9") + pypi_package_version = RANGE_CLASS_BY_SCHEMES[pypi_package.type].version_class + assert pypi_package_version == versions.PypiVersion + + def test_sort_by_version(self): + list_to_sort = [ + "pkg:npm/sequelize@3.13.1", + "pkg:npm/sequelize@3.10.1", + "pkg:npm/sequelize@3.40.1", + "pkg:npm/sequelize@3.9.1", + ] + + # Convert list of strings ^ to a list of vulnerablecode Package objects. + vuln_pkg_list = [] + for package in list_to_sort: + purl = PackageURL.from_string(package) + attrs = {k: v for k, v in purl.to_dict().items() if v} + vulnerablecode_package = models.Package.objects.create(**attrs) + vuln_pkg_list.append(vulnerablecode_package) - def test_assign_univers_version(self): - requesting_package = models.Package.objects.create(type="deb", name="git", version="2.30.1") + requesting_package = models.Package.objects.create( + type="npm", + name="sequelize", + version="3.0.0", + ) + + sorted_pkgs = requesting_package.sort_by_version(vuln_pkg_list) + first_sorted_item = sorted_pkgs[0] + + assert sorted_pkgs[0].purl == "pkg:npm/sequelize@3.9.1" + assert sorted_pkgs[-1].purl == "pkg:npm/sequelize@3.40.1" + + def test_string_to_purl_to_dict_to_package(self): + # Convert a PURL string to a PURL to a dictionary to a VulnerableCode Package, i.e., + # a . + + # Convert a PURL string to a PURL. + purl_string = "pkg:maven/org.apache.tomcat.embed/tomcat-embed-core@9.0.31" + purl = PackageURL.from_string(purl_string) + + assert type(purl) == PackageURL + assert purl.type == "maven" + assert purl.qualifiers == {} + assert purl.subpath == None + + # Convert the PURL to a dictionary. + # It appears that this step is where the unwanted None values are created for qualifiers and + # subpath when the PURL does not already contain values for those attributes. + purl_to_dict = purl.to_dict() + + assert purl_to_dict == { + "type": "maven", + "namespace": "org.apache.tomcat.embed", + "name": "tomcat-embed-core", + "version": "9.0.31", + "qualifiers": None, + "subpath": None, + } + assert purl_to_dict.get("qualifiers") == None + assert purl_to_dict.get("subpath") == None - deb01 = models.Package.objects.create(type="deb", name="git", version="2.31.1") - command_name_deb01 = requesting_package.assign_univers_version(deb01) - assert command_name_deb01 == versions.DebianVersion + # Convert the dictionary to a VulnerableCode Package, i.e., + # a - pypi01 = models.Package.objects.create(type="pypi", name="pyopenssl", version="0.9") - command_name_pypi01 = requesting_package.assign_univers_version(pypi01) - assert command_name_pypi01 == versions.PypiVersion + # If subpath is None we get error: django.db.utils.IntegrityError: null value in column + # "subpath" violates not-null constraint -- need to convert value from None to empty string. + # Similar issue with qualifiers, which must be converted from None to {}. - gem01 = models.Package.objects.create(type="gem", name="sidekiq", version="0.9") - command_name_gem01 = requesting_package.assign_univers_version(gem01) - assert command_name_gem01 == versions.SemverVersion + # I've structured the following in this way because trying instead to use + # "with pytest.raises(IntegrityError):" will throw the error + # django.db.transaction.TransactionManagementError: An error occurred in the current + # transaction. You can't execute queries until the end of the 'atomic' block. + + try: + with transaction.atomic(): + vulnerablecode_package = models.Package.objects.create( + type=purl_to_dict.get("type"), + namespace=purl_to_dict.get("namespace"), + name=purl_to_dict.get("name"), + version=purl_to_dict.get("version"), + qualifiers=purl_to_dict.get("qualifiers"), + subpath=purl_to_dict.get("subpath"), + ) + except IntegrityError: + print("\nAs expected, an IntegrityError has occurred.\n") + + # This will avoid the IntegrityError: + if purl_to_dict.get("qualifiers") is None: + purl_to_dict["qualifiers"] = {} + if purl_to_dict.get("subpath") is None: + purl_to_dict["subpath"] = "" + + # Check the qualifiers and subpath values again. + assert purl_to_dict.get("qualifiers") == {} + assert purl_to_dict.get("subpath") == "" + + vulnerablecode_package = models.Package.objects.create( + type=purl_to_dict.get("type"), + namespace=purl_to_dict.get("namespace"), + name=purl_to_dict.get("name"), + version=purl_to_dict.get("version"), + qualifiers=purl_to_dict.get("qualifiers"), + subpath=purl_to_dict.get("subpath"), + ) + + assert type(vulnerablecode_package) == models.Package + assert ( + vulnerablecode_package.purl + == "pkg:maven/org.apache.tomcat.embed/tomcat-embed-core@9.0.31" + ) + assert vulnerablecode_package.qualifiers == {} + assert vulnerablecode_package.subpath == "" From f920deda4961dbbbc5ecce16fcbecfff97618bcb Mon Sep 17 00:00:00 2001 From: "John M. Horan" Date: Mon, 14 Aug 2023 17:42:05 -0700 Subject: [PATCH 07/53] Move weakness test #1228 Reference: https://github.com/nexB/vulnerablecode/issues/1228 Signed-off-by: John M. Horan --- vulnerabilities/tests/test_models.py | 19 ++++++------------- 1 file changed, 6 insertions(+), 13 deletions(-) diff --git a/vulnerabilities/tests/test_models.py b/vulnerabilities/tests/test_models.py index fbd0803ff..58ee7cd79 100644 --- a/vulnerabilities/tests/test_models.py +++ b/vulnerabilities/tests/test_models.py @@ -61,6 +61,12 @@ def test_vulnerability_save_without_vulnerability_id(self): == 1 ) + def test_cwe_not_present_in_weaknesses_db(self): + w1 = models.Weakness.objects.create(name="189") + assert w1.weakness is None + assert w1.name is "" + assert w1.description is "" + # FIXME: The fixture code is duplicated. setUpClass is not working with the pytest mark. @pytest.mark.django_db @@ -190,13 +196,6 @@ def test_get_vulnerable_packages(self): assert vuln_packages.count() == 3 assert vuln_packages.distinct().count() == 2 - # matching_fixed_packages = vulnerablecode_package.get_fixed_packages(vulnerablecode_package) - # assert vuln_packages.distinct()[0] - - # matching_fixed_packages = vuln_packages.distinct()[0].get_fixed_packages( - # vuln_packages.distinct()[0] - # ) - first_vulnerable_package = vuln_packages.distinct()[0] matching_fixed_packages = first_vulnerable_package.get_fixed_packages( first_vulnerable_package @@ -400,9 +399,3 @@ def test_string_to_purl_to_dict_to_package(self): ) assert vulnerablecode_package.qualifiers == {} assert vulnerablecode_package.subpath == "" - - def test_cwe_not_present_in_weaknesses_db(self): - w1 = models.Weakness.objects.create(name="189") - assert w1.weakness is None - assert w1.name is "" - assert w1.description is "" From 40c1758324fff84943d55593098785428bd45681 Mon Sep 17 00:00:00 2001 From: "John M. Horan" Date: Mon, 14 Aug 2023 20:47:05 -0700 Subject: [PATCH 08/53] Modify UI, update dictionary and tests #1228 Reference: https://github.com/nexB/vulnerablecode/issues/1228 Signed-off-by: John M. Horan --- vulnerabilities/models.py | 15 ++++++++ .../templates/package_details.html | 35 ++++++++++++------- vulnerabilities/tests/test_models.py | 4 +++ vulnerablecode/static/css/custom.css | 2 +- 4 files changed, 42 insertions(+), 14 deletions(-) diff --git a/vulnerabilities/models.py b/vulnerabilities/models.py index cfa6e9d8a..1c728c15b 100644 --- a/vulnerabilities/models.py +++ b/vulnerabilities/models.py @@ -709,8 +709,11 @@ def fixed_package_details(self): purl_dict.update({"vulnerabilities": []}) purl_dict["closest_non_vulnerable_fix"] = "" + purl_dict["closest_non_vulnerable_fix_version"] = "" purl_dict["closest_non_vulnerable_fix_url"] = "" + purl_dict["most_recent_non_vulnerable_fix"] = "" + purl_dict["most_recent_non_vulnerable_fix_version"] = "" purl_dict["most_recent_non_vulnerable_fix_url"] = "" for vuln in qs: @@ -761,7 +764,9 @@ def fixed_package_details(self): if dict_vuln["vulnerability"] == str(vuln): dict_vuln["closest_fixed_by_purl"] = str(closest_fixed_package) + if len(vuln_matching_fixed_packages) > 0: + dict_vuln["closest_fixed_by_version"] = str(closest_fixed_package.version) dict_vuln["closest_fixed_by_url"] = closest_fixed_package.get_absolute_url() closest_fixed_package_vulns_dict = [ { @@ -774,19 +779,29 @@ def fixed_package_details(self): "closest_fixed_by_vulnerabilities" ] = closest_fixed_package_vulns_dict else: + dict_vuln["closest_fixed_by_version"] = "" dict_vuln["closest_fixed_by_url"] = "" dict_vuln["closest_fixed_by_vulnerabilities"] = [] purl_dict["closest_non_vulnerable_fix"] = str(closest_non_vulnerable_fix) + if len(vuln_matching_fixed_packages) > 0: + purl_dict["closest_non_vulnerable_fix_version"] = str( + closest_non_vulnerable_fix.version + ) purl_dict[ "closest_non_vulnerable_fix_url" ] = closest_non_vulnerable_fix.get_absolute_url() + purl_dict["most_recent_non_vulnerable_fix_version"] = str( + most_recent_non_vulnerable_fix.version + ) purl_dict[ "most_recent_non_vulnerable_fix_url" ] = most_recent_non_vulnerable_fix.get_absolute_url() else: + purl_dict["closest_non_vulnerable_fix_version"] = "" purl_dict["closest_non_vulnerable_fix_url"] = "" + purl_dict["most_recent_non_vulnerable_fix_version"] = "" purl_dict["most_recent_non_vulnerable_fix_url"] = "" purl_dict["most_recent_non_vulnerable_fix"] = str( diff --git a/vulnerabilities/templates/package_details.html b/vulnerabilities/templates/package_details.html index 236379ab6..1456945e3 100644 --- a/vulnerabilities/templates/package_details.html +++ b/vulnerabilities/templates/package_details.html @@ -47,18 +47,18 @@ - Closest non-vulnerable purl + Non-vulnerable version - {{ fixed_package_details.closest_non_vulnerable_fix }} + {{ fixed_package_details.closest_non_vulnerable_fix_version }} - Latest non-vulnerable purl + Latest non-vulnerable version - {{ fixed_package_details.most_recent_non_vulnerable_fix }} + {{ fixed_package_details.most_recent_non_vulnerable_fix_version }} @@ -80,7 +80,7 @@ Vulnerability Summary Aliases - Closest fixed by packages + Fixed by packages @@ -111,10 +111,24 @@ {% for abc in value %} {% if abc.vulnerability == vulnerability.vulnerability_id %} - {{ abc.closest_fixed_by_purl }} + {% if abc.closest_fixed_by_purl == "There are no reported fixed packages." %} + There are no reported fixed packages. + {% else %} + {{ abc.closest_fixed_by_purl }} + {% endif %}
- Vulnerabilities: {{ abc.closest_fixed_by_vulnerabilities|length }} + {% if abc.closest_fixed_by_purl == "There are no reported fixed packages." %} + + {% else %} + + {% if abc.closest_fixed_by_vulnerabilities|length != 1 %} + Affected by {{ abc.closest_fixed_by_vulnerabilities|length }} other vulnerabilities. + {% else %} + Affected by {{ abc.closest_fixed_by_vulnerabilities|length }} other vulnerability. + {% endif %} + + {% endif %} {% if abc.closest_fixed_by_vulnerabilities|length != 0 %}