diff --git a/vulnerabilities/api.py b/vulnerabilities/api.py index 6045fc8ca..86f800e71 100644 --- a/vulnerabilities/api.py +++ b/vulnerabilities/api.py @@ -48,11 +48,30 @@ class MinimalPackageSerializer(serializers.HyperlinkedModelSerializer): Used for nesting inside vulnerability focused APIs. """ + def get_affected_vulnerabilities(self, package): + parent_affected_vulnerabilities = package.fixed_package_details.get("vulnerabilities") or [] + + affected_vulnerabilities = [ + self.get_vulnerability(vuln) for vuln in parent_affected_vulnerabilities + ] + + return affected_vulnerabilities + + def get_vulnerability(self, vuln): + affected_vulnerability = {} + + vulnerability = vuln.get("vulnerability") + if vulnerability: + affected_vulnerability["vulnerability"] = vulnerability.vulnerability_id + return affected_vulnerability + + affected_by_vulnerabilities = serializers.SerializerMethodField("get_affected_vulnerabilities") + purl = serializers.CharField(source="package_url") class Meta: model = Package - fields = ["url", "purl", "is_vulnerable"] + fields = ["url", "purl", "is_vulnerable", "affected_by_vulnerabilities"] class MinimalVulnerabilitySerializer(serializers.HyperlinkedModelSerializer): @@ -99,7 +118,6 @@ class Meta: class VulnerabilitySerializer(serializers.HyperlinkedModelSerializer): - fixed_packages = MinimalPackageSerializer( many=True, source="filtered_fixed_packages", read_only=True ) @@ -126,6 +144,20 @@ class PackageSerializer(serializers.HyperlinkedModelSerializer): Lookup software package using Package URLs """ + next_non_vulnerable_version = serializers.SerializerMethodField("get_next_non_vulnerable") + + def get_next_non_vulnerable(self, package): + next_non_vulnerable = package.fixed_package_details.get("next_non_vulnerable", None) + if next_non_vulnerable: + return next_non_vulnerable.version + + latest_non_vulnerable_version = serializers.SerializerMethodField("get_latest_non_vulnerable") + + def get_latest_non_vulnerable(self, package): + latest_non_vulnerable = package.fixed_package_details.get("latest_non_vulnerable", None) + if latest_non_vulnerable: + return latest_non_vulnerable.version + purl = serializers.CharField(source="package_url") affected_by_vulnerabilities = serializers.SerializerMethodField("get_affected_vulnerabilities") @@ -134,7 +166,7 @@ class PackageSerializer(serializers.HyperlinkedModelSerializer): def get_fixed_packages(self, package): """ - Return a queryset of all packages that fixes a vulnerability with + 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( @@ -149,7 +181,7 @@ def get_fixed_packages(self, package): def get_vulnerabilities_for_a_package(self, package, fix) -> dict: """ Return a mapping of vulnerabilities data related to the given `package`. - Return vulnerabilities that affects the `package` if given `fix` flag is False, + Return vulnerabilities that affect the `package` if given `fix` flag is False, otherwise return vulnerabilities fixed by the `package`. """ fixed_packages = self.get_fixed_packages(package=package) @@ -175,9 +207,23 @@ def get_fixed_vulnerabilities(self, package) -> dict: def get_affected_vulnerabilities(self, package) -> dict: """ - Return a mapping of vulnerabilities that affects the given `package`. + Return a mapping of vulnerabilities that affect the given `package` (including packages that + fix each vulnerability and whose version is greater than the `package` version). """ - return self.get_vulnerabilities_for_a_package(package=package, fix=False) + excluded_purls = [] + package_vulnerabilities = self.get_vulnerabilities_for_a_package(package=package, fix=False) + + for vuln in package_vulnerabilities: + for pkg in vuln["fixed_packages"]: + real_purl = PackageURL.from_string(pkg["purl"]) + if package.version_class(real_purl.version) <= package.current_version: + excluded_purls.append(pkg) + + vuln["fixed_packages"] = [ + pkg for pkg in vuln["fixed_packages"] if pkg not in excluded_purls + ] + + return package_vulnerabilities class Meta: model = Package @@ -190,6 +236,8 @@ class Meta: "version", "qualifiers", "subpath", + "next_non_vulnerable_version", + "latest_non_vulnerable_version", "affected_by_vulnerabilities", "fixing_vulnerabilities", ] diff --git a/vulnerabilities/models.py b/vulnerabilities/models.py index 1078a4677..0a724031f 100644 --- a/vulnerabilities/models.py +++ b/vulnerabilities/models.py @@ -23,6 +23,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 @@ -32,6 +33,8 @@ 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 univers.version_range import RANGE_CLASS_BY_SCHEMES from vulnerabilities.severity_systems import SCORING_SYSTEMS from vulnerabilities.utils import build_vcid @@ -67,6 +70,12 @@ def paginated(self, per_page=5000): class VulnerabilityQuerySet(BaseQuerySet): + def affecting_vulnerabilities(self): + """ + Return a queryset of Vulnerability that affect a package. + """ + return self.filter(packagerelatedvulnerability__fix=False) + def with_cpes(self): """ Return a queryset of Vulnerability that have one or more NVD CPE references. @@ -404,6 +413,24 @@ def purl_to_dict(purl: PackageURL): class PackageQuerySet(BaseQuerySet, PackageURLQuerySet): + def get_fixed_by_package_versions(self, purl: PackageURL, fix=True): + """ + Return a queryset of all the package versions of this `package` that fix any vulnerability. + If `fix` is False, return all package versions whether or not they fix a vulnerability. + """ + filter_dict = { + "name": purl.name, + "namespace": purl.namespace, + "type": purl.type, + "qualifiers": purl.qualifiers, + "subpath": purl.subpath, + } + + if fix: + filter_dict["packagerelatedvulnerability__fix"] = True + + return Package.objects.filter(**filter_dict).distinct() + def get_or_create_from_purl(self, purl: PackageURL): """ Return an existing or new Package (created if neeed) given a @@ -601,7 +628,6 @@ def __str__(self): return self.package_url @property - # TODO: consider renaming to "affected_by" def affected_by(self): """ Return a queryset of vulnerabilities affecting this package. @@ -642,6 +668,144 @@ def get_absolute_url(self): """ return reverse("package_details", args=[self.purl]) + def sort_by_version(self, packages): + """ + Return a list of `packages` sorted by version. + """ + if not packages: + return [] + + return sorted( + packages, + key=lambda x: self.version_class(x.version), + ) + + @property + def version_class(self): + return RANGE_CLASS_BY_SCHEMES[self.type].version_class + + @property + def current_version(self): + return self.version_class(self.version) + + @property + def fixed_package_details(self): + """ + Return a mapping of vulnerabilities that affect this package and the next and + latest non-vulnerable versions. + """ + package_details = {} + package_details["purl"] = PackageURL.from_string(self.purl) + + next_non_vulnerable, latest_non_vulnerable = self.get_non_vulnerable_versions() + package_details["next_non_vulnerable"] = next_non_vulnerable + package_details["latest_non_vulnerable"] = latest_non_vulnerable + + package_details["vulnerabilities"] = self.get_affecting_vulnerabilities() + + return package_details + + def get_non_vulnerable_versions(self): + """ + Return a tuple of the next and latest non-vulnerable versions as PackageURLs. Return a tuple of + (None, None) if there is no non-vulnerable version. + """ + package_versions = Package.objects.get_fixed_by_package_versions(self, fix=False) + + non_vulnerable_versions = [] + for version in package_versions: + if not version.is_vulnerable: + non_vulnerable_versions.append(version) + + later_non_vulnerable_versions = [] + for non_vuln_ver in non_vulnerable_versions: + if self.version_class(non_vuln_ver.version) > self.current_version: + later_non_vulnerable_versions.append(non_vuln_ver) + + if later_non_vulnerable_versions: + sorted_versions = self.sort_by_version(later_non_vulnerable_versions) + next_non_vulnerable_version = sorted_versions[0] + latest_non_vulnerable_version = sorted_versions[-1] + + next_non_vulnerable = PackageURL.from_string(next_non_vulnerable_version.purl) + latest_non_vulnerable = PackageURL.from_string(latest_non_vulnerable_version.purl) + + return next_non_vulnerable, latest_non_vulnerable + + return None, None + + def get_affecting_vulnerabilities(self): + """ + Return a list of vulnerabilities that affect this package together with information regarding + the versions that fix the vulnerabilities. + """ + package_details_vulns = [] + + fixed_by_packages = Package.objects.get_fixed_by_package_versions(self, fix=True) + + package_vulnerabilities = self.vulnerabilities.affecting_vulnerabilities().prefetch_related( + Prefetch( + "packages", + queryset=fixed_by_packages, + to_attr="fixed_packages", + ) + ) + + for vuln in package_vulnerabilities: + package_details_vulns.append({"vulnerability": vuln}) + later_fixed_packages = [] + + for fixed_pkg in vuln.fixed_packages: + if fixed_pkg not in fixed_by_packages: + continue + fixed_version = self.version_class(fixed_pkg.version) + if fixed_version > self.current_version: + later_fixed_packages.append(fixed_pkg) + + next_fixed_package = None + next_fixed_package_vulns = [] + + sort_fixed_by_packages_by_version = [] + if later_fixed_packages: + sort_fixed_by_packages_by_version = self.sort_by_version(later_fixed_packages) + + fixed_by_pkgs = [] + + for vuln_details in package_details_vulns: + if vuln_details["vulnerability"] != vuln: + continue + vuln_details["fixed_by_purl"] = [] + vuln_details["fixed_by_purl_vulnerabilities"] = [] + + for fixed_by_pkg in sort_fixed_by_packages_by_version: + fixed_by_package_details = {} + fixed_by_purl = PackageURL.from_string(fixed_by_pkg.purl) + next_fixed_package_vulns = list(fixed_by_pkg.affected_by) + + fixed_by_package_details["fixed_by_purl"] = fixed_by_purl + fixed_by_package_details[ + "fixed_by_purl_vulnerabilities" + ] = next_fixed_package_vulns + fixed_by_pkgs.append(fixed_by_package_details) + + vuln_details["fixed_by_package_details"] = fixed_by_pkgs + + return package_details_vulns + + @property + def fixing_vulnerabilities(self): + """ + Return a queryset of Vulnerabilities that are fixed by this `package`. + """ + return self.vulnerabilities.filter(packagerelatedvulnerability__fix=True) + + @property + def affecting_vulnerabilities(self): + """ + Return a queryset of Vulnerabilities that affect this `package`. + """ + return self.vulnerabilities.filter(packagerelatedvulnerability__fix=False) + class PackageRelatedVulnerability(models.Model): """ diff --git a/vulnerabilities/templates/package_details.html b/vulnerabilities/templates/package_details.html index 3956af2ce..c70adaee0 100644 --- a/vulnerabilities/templates/package_details.html +++ b/vulnerabilities/templates/package_details.html @@ -24,7 +24,7 @@ -
+ - {{ package.purl }} + {{ fixed_package_details.purl.to_string }}
+ {% if affected_by_vulnerabilities|length != 0 %} + +
+ + + + + + + + + + + +
+ Next non-vulnerable version + + {% if fixed_package_details.next_non_vulnerable.version %} + {{ fixed_package_details.next_non_vulnerable.version }} + {% else %} + None. + {% endif %} +
+ Latest non-vulnerable version + + {% if fixed_package_details.latest_non_vulnerable.version %} + {{ fixed_package_details.latest_non_vulnerable.version }} + {% else %} + None. + {% endif %} +
+
+ + {% endif %} +
- Affected by vulnerabilities ({{ affected_by_vulnerabilities|length }}) + Vulnerabilities affecting this package ({{ affected_by_vulnerabilities|length }})
- + - + @@ -59,11 +94,9 @@ - - + + {% empty %} {% endfor %} -
VulnerabilityVulnerability SummaryAliasesFixed by
{{ vulnerability.vulnerability_id }} - - {{ vulnerability.summary }} - +
+ Aliases: +
{% for alias in vulnerability.alias %} {% if alias.url %} {{ alias }} @@ -74,33 +107,90 @@ {% endif %} {% endfor %}
+ {{ vulnerability.summary }} + + {% if package.purl == fixed_package_details.purl.to_string %} + {% for key, value in fixed_package_details.items %} + {% if key == "vulnerabilities" %} + {% for vuln in value %} + {% if vuln.vulnerability.vulnerability_id == vulnerability.vulnerability_id %} + {% if vuln.fixed_by_package_details is None %} + There are no reported fixed by versions. + {% else %} + {% for fixed_pkg in vuln.fixed_by_package_details %} +
+ {% if fixed_pkg.fixed_by_purl_vulnerabilities|length == 0 %} + {{ fixed_pkg.fixed_by_purl.version }} +
+ Affected by 0 other vulnerabilities. + {% else %} + {{ fixed_pkg.fixed_by_purl.version }} + {% if fixed_pkg.fixed_by_purl_vulnerabilities|length != 1 %} +
+ Affected by {{ fixed_pkg.fixed_by_purl_vulnerabilities|length }} other vulnerabilities. + {% else %} +
+ Affected by {{ fixed_pkg.fixed_by_purl_vulnerabilities|length }} other vulnerability. + {% endif %} + + + {% endif %} +
+ {% endfor %} + {% endif %} + {% endif %} + {% endfor %} + {% endif %} + {% endfor %} + {% endif %} +
- This package is not known to be affected by vulnerabilities. + This package is not known to be affected by vulnerabilities.
- Fixing vulnerabilities ({{ fixing_vulnerabilities|length }}) + Vulnerabilities fixed by this package ({{ fixing_vulnerabilities|length }})
- + - {% for vulnerability in fixing_vulnerabilities %} @@ -125,7 +215,7 @@ {% empty %} {% endfor %} @@ -137,5 +227,4 @@ {% endif %} - {% endblock %} diff --git a/vulnerabilities/templates/packages.html b/vulnerabilities/templates/packages.html index 000fe45be..357d60ce7 100644 --- a/vulnerabilities/templates/packages.html +++ b/vulnerabilities/templates/packages.html @@ -24,7 +24,7 @@ - +
VulnerabilityVulnerability Summary Aliases
- This package is not known to fix vulnerabilities. + This package is not known to fix vulnerabilities.
@@ -41,14 +41,14 @@ - Affected by vulnerabilities + Affected by vulnerabilities diff --git a/vulnerabilities/templates/vulnerabilities.html b/vulnerabilities/templates/vulnerabilities.html index 023d3f97f..bdada6ee1 100644 --- a/vulnerabilities/templates/vulnerabilities.html +++ b/vulnerabilities/templates/vulnerabilities.html @@ -32,8 +32,8 @@ - - + + diff --git a/vulnerabilities/templates/vulnerability_details.html b/vulnerabilities/templates/vulnerability_details.html index 4d1af2e5c..c42f6d885 100644 --- a/vulnerabilities/templates/vulnerability_details.html +++ b/vulnerabilities/templates/vulnerability_details.html @@ -34,14 +34,14 @@
  • - Fixed by packages ({{ fixed_by_packages|length }}) + Fixed by packages ({{ fixed_by_packages|length }})
  • - Affected packages ({{ affected_packages|length }}) + Affected packages ({{ affected_packages|length }})
  • @@ -128,7 +128,7 @@
    - Fixed by packages ({{ fixed_by_packages|length }}) + Fixed by packages ({{ fixed_by_packages|length }})
    - Fixing vulnerabilities + Fixed by vulnerabilities
    Vulnerability id AliasesAffected packagesFixed by packagesAffected packagesFixed by packages
    @@ -142,14 +142,14 @@ {% empty %} {% endfor %} {% if fixed_by_packages|length > 3 %} {% endif %} @@ -157,7 +157,7 @@
    - Affected packages ({{ affected_packages|length }}) + Affected packages ({{ affected_packages|length }})
    - There are no known fixed packages. + There are no known fixed by packages.
    - See Fixed by packages tab for more + See Fixed by packages tab for more
    @@ -171,14 +171,14 @@ {% empty %} {% endfor %} {% if affected_packages|length > 3 %} {% endif %} diff --git a/vulnerabilities/tests/conftest.py b/vulnerabilities/tests/conftest.py index de7bb560a..bfd59e045 100644 --- a/vulnerabilities/tests/conftest.py +++ b/vulnerabilities/tests/conftest.py @@ -25,7 +25,6 @@ def no_rmtree(monkeypatch): # Step 2: Run test for importer only if it is activated (pytestmark = pytest.mark.skipif(...)) # Step 3: Migrate all the tests collect_ignore = [ - "test_models.py", "test_package_managers.py", "test_ruby.py", "test_rust.py", diff --git a/vulnerabilities/tests/test_api.py b/vulnerabilities/tests/test_api.py index 5dd9a2319..57c0ce2da 100644 --- a/vulnerabilities/tests/test_api.py +++ b/vulnerabilities/tests/test_api.py @@ -18,6 +18,7 @@ from rest_framework import status from rest_framework.test import APIClient +from vulnerabilities.api import MinimalPackageSerializer from vulnerabilities.api import PackageSerializer from vulnerabilities.models import Alias from vulnerabilities.models import ApiUser @@ -88,7 +89,6 @@ def setUp(self): self.client.credentials(HTTP_AUTHORIZATION=self.auth) def test_query_qualifier_filtering(self): - # packages to check filtering with single/multiple and unordered qualifier filtering pk_multi_qf = Package.objects.create( name="vlc", version="1.50-1.1", type="deb", qualifiers={"foo": "bar", "tar": "ball"} @@ -192,7 +192,7 @@ def setUp(self): ) self.vulnerability = Vulnerability.objects.create(summary="test") self.pkg1 = Package.objects.create(name="flask", type="pypi", version="0.1.2") - self.pkg2 = Package.objects.create(name="flask", type="debian", version="0.1.2") + self.pkg2 = Package.objects.create(name="flask", type="deb", version="0.1.2") for pkg in [self.pkg1, self.pkg2]: PackageRelatedVulnerability.objects.create( package=pkg, vulnerability=self.vulnerability, fix=True @@ -210,6 +210,7 @@ def test_api_with_single_vulnerability(self): response = self.csrf_client.get( f"/api/vulnerabilities/{self.vulnerability.id}", format="json" ).data + assert response == { "url": f"http://testserver/api/vulnerabilities/{self.vulnerability.id}", "vulnerability_id": self.vulnerability.vulnerability_id, @@ -218,13 +219,15 @@ def test_api_with_single_vulnerability(self): "fixed_packages": [ { "url": f"http://testserver/api/packages/{self.pkg2.id}", - "purl": "pkg:debian/flask@0.1.2", + "purl": "pkg:deb/flask@0.1.2", "is_vulnerable": False, + "affected_by_vulnerabilities": [], }, { "url": f"http://testserver/api/packages/{self.pkg1.id}", "purl": "pkg:pypi/flask@0.1.2", "is_vulnerable": False, + "affected_by_vulnerabilities": [], }, ], "affected_packages": [], @@ -245,6 +248,7 @@ def test_api_with_single_vulnerability_with_filters(self): "url": f"http://testserver/api/packages/{self.pkg1.id}", "purl": "pkg:pypi/flask@0.1.2", "is_vulnerable": False, + "affected_by_vulnerabilities": [], }, ], "affected_packages": [], @@ -258,142 +262,234 @@ def setUp(self): self.auth = f"Token {self.user.auth_token.key}" self.csrf_client = APIClient(enforce_csrf_checks=True) self.csrf_client.credentials(HTTP_AUTHORIZATION=self.auth) - vuln = Vulnerability.objects.create( - summary="test-vuln", + + # searched-for pkg + self.package_maven_jackson_databind_2_13_1 = Package.objects.create( + type="maven", + namespace="com.fasterxml.jackson.core", + name="jackson-databind", + version="2.13.1", + qualifiers={}, + subpath="", ) - self.vuln = vuln - self.vulnerable_packages = [] - for i in range(0, 10): - query_kwargs = dict( - type="generic", - namespace="nginx", - name="test", - version=str(i), - qualifiers={}, - subpath="", - ) - vuln_package = Package.objects.create(**query_kwargs) - PackageRelatedVulnerability.objects.create( - package=vuln_package, - vulnerability=vuln, - fix=False, - ) - self.vuln_package = vuln_package - query_kwargs = dict( - type="generic", - namespace="nginx", - name="test", - version="11", + + # searched-for pkg's vuln + self.vuln_VCID_2nyb_8rwu_aaag = Vulnerability.objects.create( + summary="This is VCID-2nyb-8rwu-aaag", + vulnerability_id="VCID-2nyb-8rwu-aaag", + ) + + # pkg-vuln affect relationship + PackageRelatedVulnerability.objects.create( + package=self.package_maven_jackson_databind_2_13_1, + vulnerability=self.vuln_VCID_2nyb_8rwu_aaag, + fix=False, + ) + + # vuln aliases + Alias.objects.create(alias="CVE-2020-36518", vulnerability=self.vuln_VCID_2nyb_8rwu_aaag) + Alias.objects.create( + alias="GHSA-57j2-w4cx-62h2", vulnerability=self.vuln_VCID_2nyb_8rwu_aaag + ) + + # pkg (1 of 2 -- this is a lesser version and will be omitted from the API) that fixes searched-for pkg's vuln + self.package_maven_jackson_databind_2_12_6_1 = Package.objects.create( + type="maven", + namespace="com.fasterxml.jackson.core", + name="jackson-databind", + version="2.12.6.1", qualifiers={}, subpath="", ) - self.package = Package.objects.create(**query_kwargs) + + # pkg-vuln fix relationship PackageRelatedVulnerability.objects.create( - package=self.package, - vulnerability=vuln, + package=self.package_maven_jackson_databind_2_12_6_1, + vulnerability=self.vuln_VCID_2nyb_8rwu_aaag, fix=True, ) - vuln1 = Vulnerability.objects.create( - summary="test-vuln1", + + # fixed by pkg own vuln + self.vuln_VCID_gqhw_ngh8_aaap = Vulnerability.objects.create( + summary="This is VCID-gqhw-ngh8-aaap", + vulnerability_id="VCID-gqhw-ngh8-aaap", ) - Alias.objects.create(alias="CVE-2019-1234", vulnerability=vuln1) - Alias.objects.create(alias="GMS-1234-4321", vulnerability=vuln1) - Alias.objects.create(alias="CVE-2029-1234", vulnerability=vuln) - self.vuln1 = vuln1 + + # pkg-vuln affect relationship PackageRelatedVulnerability.objects.create( - package=self.package, - vulnerability=vuln1, + package=self.package_maven_jackson_databind_2_12_6_1, + vulnerability=self.vuln_VCID_gqhw_ngh8_aaap, fix=False, ) - def test_is_vulnerable_attribute(self): - self.assertTrue(self.package.is_vulnerable) + # pkg (2 of 2 -- this is a greater version) that fixes searched-for pkg's vuln + self.package_maven_jackson_databind_2_13_2 = Package.objects.create( + type="maven", + namespace="com.fasterxml.jackson.core", + name="jackson-databind", + version="2.13.2", + qualifiers={}, + subpath="", + ) - def test_api_status(self): - response = self.csrf_client.get("/api/packages/", format="json") - self.assertEqual(status.HTTP_200_OK, response.status_code) + # pkg-vuln fix relationship + PackageRelatedVulnerability.objects.create( + package=self.package_maven_jackson_databind_2_13_2, + vulnerability=self.vuln_VCID_2nyb_8rwu_aaag, + fix=True, + ) - def test_api_response(self): - response = self.csrf_client.get("/api/packages/", format="json").data - self.assertEqual(response["count"], 11) + # pkg-vuln affect relationship + PackageRelatedVulnerability.objects.create( + package=self.package_maven_jackson_databind_2_13_2, + vulnerability=self.vuln_VCID_gqhw_ngh8_aaap, + fix=False, + ) - def test_api_with_namespace_filter(self): - response = self.csrf_client.get("/api/packages/?namespace=nginx", format="json").data - self.assertEqual(response["count"], 11) + # This is the vuln fixed by the searched-for pkg -- and by a lesser version (created below), which WILL be included in the API + self.vuln_VCID_ftmk_wbwx_aaar = Vulnerability.objects.create( + summary="This is VCID-ftmk-wbwx-aaar", + vulnerability_id="VCID-ftmk-wbwx-aaar", + ) - def test_api_with_wrong_namespace_filter(self): - response = self.csrf_client.get("/api/packages/?namespace=foo-bar", format="json").data - self.assertEqual(response["count"], 0) + # searched-for pkg-vuln fix relationship + PackageRelatedVulnerability.objects.create( + package=self.package_maven_jackson_databind_2_13_1, + vulnerability=self.vuln_VCID_ftmk_wbwx_aaar, + fix=True, + ) - def test_api_with_single_vulnerability_and_fixed_package(self): - response = self.csrf_client.get(f"/api/packages/{self.package.id}", format="json").data - assert response == { - "url": f"http://testserver/api/packages/{self.package.id}", - "purl": "pkg:generic/nginx/test@11", - "type": "generic", - "namespace": "nginx", - "name": "test", - "version": "11", + # lesser-version pkg that also fixes the vuln fixed by the searched-for pkg + self.package_maven_jackson_databind_2_12_6 = Package.objects.create( + type="maven", + namespace="com.fasterxml.jackson.core", + name="jackson-databind", + version="2.12.6", + qualifiers={}, + subpath="", + ) + + # lesser-version pkg-vuln fix relationship + PackageRelatedVulnerability.objects.create( + package=self.package_maven_jackson_databind_2_12_6, + vulnerability=self.vuln_VCID_ftmk_wbwx_aaar, + fix=True, + ) + + # aliases for vuln fixed by searched-for pkg + Alias.objects.create(alias="CVE-2021-46877", vulnerability=self.vuln_VCID_ftmk_wbwx_aaar) + Alias.objects.create( + alias="GHSA-3x8x-79m2-3w2w", vulnerability=self.vuln_VCID_ftmk_wbwx_aaar + ) + + # This addresses both next and latest non-vulnerable pkg + self.package_maven_jackson_databind_2_14_0_rc1 = Package.objects.create( + type="maven", + namespace="com.fasterxml.jackson.core", + name="jackson-databind", + version="2.14.0-rc1", + qualifiers={}, + subpath="", + ) + + def test_api_with_package_with_no_vulnerabilities(self): + affected_vulnerabilities = [] + vuln = { + "foo": "bar", + } + + package_with_no_vulnerabilities = MinimalPackageSerializer.get_vulnerability( + self, + vuln, + ) + + assert package_with_no_vulnerabilities is None + + def test_api_with_lesser_and_greater_fixed_by_packages(self): + response = self.csrf_client.get( + f"/api/packages/{self.package_maven_jackson_databind_2_13_1.id}", format="json" + ).data + + expected_response = { + "url": f"http://testserver/api/packages/{self.package_maven_jackson_databind_2_13_1.id}", + "purl": "pkg:maven/com.fasterxml.jackson.core/jackson-databind@2.13.1", + "type": "maven", + "namespace": "com.fasterxml.jackson.core", + "name": "jackson-databind", + "version": "2.13.1", "qualifiers": {}, "subpath": "", + "next_non_vulnerable_version": "2.14.0-rc1", + "latest_non_vulnerable_version": "2.14.0-rc1", "affected_by_vulnerabilities": [ { - "url": f"http://testserver/api/vulnerabilities/{self.vuln1.id}", - "vulnerability_id": self.vuln1.vulnerability_id, - "summary": "test-vuln1", - "references": [], - "fixed_packages": [], - "aliases": ["CVE-2019-1234", "GMS-1234-4321"], - } - ], - "fixing_vulnerabilities": [ - { - "url": f"http://testserver/api/vulnerabilities/{self.vuln.id}", - "vulnerability_id": self.vuln.vulnerability_id, - "summary": "test-vuln", + "url": f"http://testserver/api/vulnerabilities/{self.vuln_VCID_2nyb_8rwu_aaag.id}", + "vulnerability_id": "VCID-2nyb-8rwu-aaag", + "summary": "This is VCID-2nyb-8rwu-aaag", "references": [], "fixed_packages": [ { - "url": f"http://testserver/api/packages/{self.package.id}", - "purl": "pkg:generic/nginx/test@11", + "url": f"http://testserver/api/packages/{self.package_maven_jackson_databind_2_13_2.id}", + "purl": "pkg:maven/com.fasterxml.jackson.core/jackson-databind@2.13.2", "is_vulnerable": True, + "affected_by_vulnerabilities": [ + {"vulnerability": "VCID-gqhw-ngh8-aaap"} + ], } ], - "aliases": ["CVE-2029-1234"], - }, + "aliases": ["CVE-2020-36518", "GHSA-57j2-w4cx-62h2"], + } ], - } - - def test_api_with_single_vulnerability_and_vulnerable_package(self): - response = self.csrf_client.get(f"/api/packages/{self.vuln_package.id}", format="json").data - assert response == { - "url": f"http://testserver/api/packages/{self.vuln_package.id}", - "purl": "pkg:generic/nginx/test@9", - "type": "generic", - "namespace": "nginx", - "name": "test", - "version": "9", - "qualifiers": {}, - "subpath": "", - "affected_by_vulnerabilities": [ + "fixing_vulnerabilities": [ { - "url": f"http://testserver/api/vulnerabilities/{self.vuln.id}", - "vulnerability_id": self.vuln.vulnerability_id, - "summary": "test-vuln", + "url": f"http://testserver/api/vulnerabilities/{self.vuln_VCID_ftmk_wbwx_aaar.id}", + "vulnerability_id": "VCID-ftmk-wbwx-aaar", + "summary": "This is VCID-ftmk-wbwx-aaar", "references": [], "fixed_packages": [ { - "url": f"http://testserver/api/packages/{self.package.id}", - "purl": "pkg:generic/nginx/test@11", + "url": f"http://testserver/api/packages/{self.package_maven_jackson_databind_2_12_6.id}", + "purl": "pkg:maven/com.fasterxml.jackson.core/jackson-databind@2.12.6", + "is_vulnerable": False, + "affected_by_vulnerabilities": [], + }, + { + "url": f"http://testserver/api/packages/{self.package_maven_jackson_databind_2_13_1.id}", + "purl": "pkg:maven/com.fasterxml.jackson.core/jackson-databind@2.13.1", "is_vulnerable": True, - } + "affected_by_vulnerabilities": [ + {"vulnerability": "VCID-2nyb-8rwu-aaag"} + ], + }, ], - "aliases": ["CVE-2029-1234"], - } + "aliases": ["CVE-2021-46877", "GHSA-3x8x-79m2-3w2w"], + }, ], - "fixing_vulnerabilities": [], } + assert response == expected_response + + def test_is_vulnerable_attribute(self): + self.assertTrue(self.package_maven_jackson_databind_2_13_1.is_vulnerable) + + def test_api_status(self): + response = self.csrf_client.get("/api/packages/", format="json") + self.assertEqual(status.HTTP_200_OK, response.status_code) + + def test_api_response(self): + response = self.csrf_client.get("/api/packages/", format="json").data + self.assertEqual(response["count"], 5) + + def test_api_with_namespace_filter(self): + response = self.csrf_client.get( + "/api/packages/?namespace=com.fasterxml.jackson.core", format="json" + ).data + self.assertEqual(response["count"], 5) + + def test_api_with_wrong_namespace_filter(self): + response = self.csrf_client.get("/api/packages/?namespace=foo-bar", format="json").data + self.assertEqual(response["count"], 0) + def test_api_with_all_vulnerable_packages(self): with self.assertNumQueries(4): # There are 4 queries: @@ -402,27 +498,24 @@ def test_api_with_all_vulnerable_packages(self): # 3. Get all vulnerable packages # 4. RELEASE SAVEPOINT response = self.csrf_client.get(f"/api/packages/all", format="json").data - assert len(response) == 11 + + assert len(response) == 3 assert response == [ - "pkg:generic/nginx/test@0", - "pkg:generic/nginx/test@1", - "pkg:generic/nginx/test@11", - "pkg:generic/nginx/test@2", - "pkg:generic/nginx/test@3", - "pkg:generic/nginx/test@4", - "pkg:generic/nginx/test@5", - "pkg:generic/nginx/test@6", - "pkg:generic/nginx/test@7", - "pkg:generic/nginx/test@8", - "pkg:generic/nginx/test@9", + "pkg:maven/com.fasterxml.jackson.core/jackson-databind@2.12.6.1", + "pkg:maven/com.fasterxml.jackson.core/jackson-databind@2.13.1", + "pkg:maven/com.fasterxml.jackson.core/jackson-databind@2.13.2", ] def test_api_with_ignorning_qualifiers(self): response = self.csrf_client.get( - f"/api/packages/?purl=pkg:generic/nginx/test@9?foo=bar", format="json" + f"/api/packages/?purl=pkg:maven/com.fasterxml.jackson.core/jackson-databind@2.14.0-rc1?foo=bar", + format="json", ).data assert response["count"] == 1 - assert response["results"][0]["purl"] == "pkg:generic/nginx/test@9" + assert ( + response["results"][0]["purl"] + == "pkg:maven/com.fasterxml.jackson.core/jackson-databind@2.14.0-rc1" + ) class CPEApi(TestCase): diff --git a/vulnerabilities/tests/test_models.py b/vulnerabilities/tests/test_models.py index 3daeca99a..ed8eb29e6 100644 --- a/vulnerabilities/tests/test_models.py +++ b/vulnerabilities/tests/test_models.py @@ -7,51 +7,40 @@ # See https://aboutcode.org for more information about nexB OSS projects. # +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 univers.version_range import AlpineLinuxVersionRange from vulnerabilities import models +from vulnerabilities.models import Alias +from vulnerabilities.models import Package +from vulnerabilities.models import Vulnerability +from vulnerabilities.models import VulnerabilityQuerySet class TestVulnerabilityModel(TestCase): - def test_generate_vulcoid_given_timestamp_object(self): - timestamp_object = datetime(2021, 1, 1, 11, 12, 13, 2000) - expected_vulcoid = "VULCOID-20210101-1112-13002000" - found_vulcoid = models.Vulnerability.generate_vulcoid(timestamp_object) - assert expected_vulcoid == found_vulcoid - - def test_generate_vulcoid(self): - expected_vulcoid = "VULCOID-20210101-1112-13000000" - with freeze_time("2021-01-01 11:12:13.000000"): - found_vulcoid = models.Vulnerability.generate_vulcoid() - assert expected_vulcoid == found_vulcoid - @pytest.mark.django_db def test_vulnerability_save_with_vulnerability_id(self): models.Vulnerability(vulnerability_id="CVE-2020-7965").save() assert models.Vulnerability.objects.filter(vulnerability_id="CVE-2020-7965").count() == 1 @pytest.mark.django_db - def test_vulnerability_save_without_vulnerability_id(self): - assert ( - models.Vulnerability.objects.filter( - vulnerability_id="VULCOID-20210101-1112-13000000" - ).count() - == 0 - ) - - with freeze_time("2021-01-01 11:12:13.000000"): - models.Vulnerability(vulnerability_id="").save() - assert ( - models.Vulnerability.objects.filter( - vulnerability_id="VULCOID-20210101-1112-13000000" - ).count() - == 1 - ) + def test_cwe_not_present_in_weaknesses_db(self): + w1 = models.Weakness.objects.create(cwe_id=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. @@ -63,16 +52,16 @@ def test_package_to_vulnerability(self): v1 = models.Vulnerability.objects.create(vulnerability_id="CVE-123-2002") prv1 = models.PackageRelatedVulnerability.objects.create( - patched_package=p2, package=p1, vulnerability=v1 + package=p1, vulnerability=v1, fix=False + ) + prv2 = models.PackageRelatedVulnerability.objects.create( + package=p2, vulnerability=v1, fix=True ) - assert p1.vulnerabilities.all().count() == 1 - assert p1.resolved_vulnerabilities.all().count() == 0 - assert p1.vulnerabilities.all()[0] == v1 + assert p1.fixing_vulnerabilities.count() == 0 - assert p2.vulnerabilities.all().count() == 0 - assert p2.resolved_vulnerabilities.all().count() == 1 - assert p2.resolved_vulnerabilities.all()[0] == v1 + assert p2.fixing_vulnerabilities.count() == 1 + assert p2.fixing_vulnerabilities[0] == v1 def test_vulnerability_package(self): p1 = models.Package.objects.create(type="deb", name="git", version="2.30.1") @@ -80,17 +69,574 @@ def test_vulnerability_package(self): v1 = models.Vulnerability.objects.create(vulnerability_id="CVE-123-2002") prv1 = models.PackageRelatedVulnerability.objects.create( - patched_package=p2, package=p1, vulnerability=v1 + package=p1, vulnerability=v1, fix=False + ) + prv2 = models.PackageRelatedVulnerability.objects.create( + package=p2, vulnerability=v1, fix=True ) - assert v1.vulnerable_packages.all().count() == 1 - assert v1.patched_packages.all().count() == 1 + assert v1.vulnerable_packages.count() == 1 + assert v1.fixed_by_packages.count() == 1 - assert v1.vulnerable_packages.all()[0] == p1 - assert v1.patched_packages.all()[0] == p2 + assert v1.vulnerable_packages[0] == p1 + assert v1.fixed_by_packages[0] == p2 - 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 "" + +@pytest.mark.django_db +class TestPackageModel(TestCase): + def setUp(self): + """ + This uses a package/vuln/fix group we know from the DB/UI testing: pkg:pypi/redis@4.1.1. + It has 2 non-vuln versions, both the same: 5.0.0b1. The first of its two vulns is + VCID-g2fu-45jw-aaan (aliases: CVE-2023-28858 and GHSA-24wv-mv5m-xv4h), fixed by + 4.3.6 w/1 vuln of its own. The second is VCID-rqe1-dkmg-aaad (aliases: CVE-2023-28859 + and GHSA-8fww-64cx-x8p5), fixed by 5.0.0b1 w/ 0 vulns of its own. + """ + + # pkg + self.package_pypi_redis_4_1_1 = models.Package.objects.create( + type="pypi", + namespace="", + name="redis", + version="4.1.1", + qualifiers={}, + subpath="", + ) + + # vuln #1 for affected pkg + self.vuln_VCID_g2fu_45jw_aaan = models.Vulnerability.objects.create( + summary="This is VCID-g2fu-45jw-aaan", + vulnerability_id="VCID-g2fu-45jw-aaan", + ) + + # relationship + models.PackageRelatedVulnerability.objects.create( + package=self.package_pypi_redis_4_1_1, + vulnerability=self.vuln_VCID_g2fu_45jw_aaan, + fix=False, + ) + + # aliases + Alias.objects.create(alias="CVE-2023-28858", vulnerability=self.vuln_VCID_g2fu_45jw_aaan) + Alias.objects.create( + alias="GHSA-24wv-mv5m-xv4h", vulnerability=self.vuln_VCID_g2fu_45jw_aaan + ) + + # fixed pkg for vuln #1 for affected pkg + self.package_pypi_redis_4_3_6 = models.Package.objects.create( + type="pypi", + namespace="", + name="redis", + version="4.3.6", + qualifiers={}, + subpath="", + ) + + # relationship + models.PackageRelatedVulnerability.objects.create( + package=self.package_pypi_redis_4_3_6, + vulnerability=self.vuln_VCID_g2fu_45jw_aaan, + fix=True, + ) + + # vuln for fixed pkg -- and also vuln # 2 for affected pkg + self.vuln_VCID_rqe1_dkmg_aaad = models.Vulnerability.objects.create( + summary="This is VCID-rqe1-dkmg-aaad", + vulnerability_id="VCID-rqe1-dkmg-aaad", + ) + + # relationship + models.PackageRelatedVulnerability.objects.create( + package=self.package_pypi_redis_4_3_6, + vulnerability=self.vuln_VCID_rqe1_dkmg_aaad, + fix=False, + ) + + # aliases + Alias.objects.create(alias="CVE-2023-28859", vulnerability=self.vuln_VCID_rqe1_dkmg_aaad) + Alias.objects.create( + alias="GHSA-8fww-64cx-x8p5", vulnerability=self.vuln_VCID_rqe1_dkmg_aaad + ) + + # vuln # 2 for affected pkg -- already defined above bc also vuln for fixed pkg above! + + # relationship + models.PackageRelatedVulnerability.objects.create( + package=self.package_pypi_redis_4_1_1, + vulnerability=self.vuln_VCID_rqe1_dkmg_aaad, + fix=False, + ) + + # aliases -- already defined above + + # fixed pkg -- 0 vulns of its own + self.package_pypi_redis_5_0_0b1 = models.Package.objects.create( + type="pypi", + namespace="", + name="redis", + version="5.0.0b1", + qualifiers={}, + subpath="", + ) + + # relationship + models.PackageRelatedVulnerability.objects.create( + package=self.package_pypi_redis_5_0_0b1, + vulnerability=self.vuln_VCID_rqe1_dkmg_aaad, + fix=True, + ) + + # This vulnerability does not affect any redis packages in this set of tests but does affect a made-up package, self.package_pypi_bogus_1_2_3. + self.vuln_VCID_abcd_efgh_1234 = models.Vulnerability.objects.create( + summary="This is VCID-abcd-efgh-1234", + vulnerability_id="VCID-abcd-efgh-1234", + ) + + # pkg + self.package_pypi_bogus_1_2_3 = models.Package.objects.create( + type="pypi", + namespace="", + name="bogus", + version="1.2.3", + qualifiers={}, + subpath="", + ) + + # relationship + models.PackageRelatedVulnerability.objects.create( + package=self.package_pypi_bogus_1_2_3, + vulnerability=self.vuln_VCID_abcd_efgh_1234, + fix=False, + ) + + # This vulnerability does not affect any packages in this set of tests included to test .all(). + self.vuln_VCID_wxyz_0000_0000 = models.Vulnerability.objects.create( + summary="This is VCID-wxyz-0000-0000", + vulnerability_id="VCID-wxyz-0000-0000", + ) + + def test_fixed_package_details(self): + searched_for_package = self.package_pypi_redis_4_1_1 + + assert searched_for_package.package_url == "pkg:pypi/redis@4.1.1" + assert searched_for_package.plain_package_url == "pkg:pypi/redis@4.1.1" + assert searched_for_package.get_absolute_url() == "/packages/pkg:pypi/redis@4.1.1" + assert searched_for_package.purl == "pkg:pypi/redis@4.1.1" + + assert len(searched_for_package.affected_by) == 2 + + assert self.vuln_VCID_g2fu_45jw_aaan in searched_for_package.affected_by + assert self.package_pypi_redis_4_3_6 in self.vuln_VCID_g2fu_45jw_aaan.fixed_by_packages + + assert self.vuln_VCID_rqe1_dkmg_aaad in searched_for_package.affected_by + assert self.package_pypi_redis_5_0_0b1 in self.vuln_VCID_rqe1_dkmg_aaad.fixed_by_packages + + searched_for_package_details = searched_for_package.fixed_package_details + + package_details = { + "purl": PackageURL( + type="pypi", + name="redis", + version="4.1.1", + ), + "next_non_vulnerable": PackageURL( + type="pypi", + name="redis", + version="5.0.0b1", + ), + "latest_non_vulnerable": PackageURL( + type="pypi", + namespace=None, + name="redis", + version="5.0.0b1", + qualifiers={}, + subpath=None, + ), + "vulnerabilities": [ + { + "vulnerability": self.vuln_VCID_g2fu_45jw_aaan, + "fixed_by_package_details": [ + { + "fixed_by_purl": PackageURL( + type="pypi", + namespace=None, + name="redis", + version="4.3.6", + qualifiers={}, + subpath=None, + ), + "fixed_by_purl_vulnerabilities": [self.vuln_VCID_rqe1_dkmg_aaad], + } + ], + "fixed_by_purl": [], + "fixed_by_purl_vulnerabilities": [], + }, + { + "vulnerability": self.vuln_VCID_rqe1_dkmg_aaad, + "fixed_by_package_details": [ + { + "fixed_by_purl": PackageURL( + type="pypi", + namespace=None, + name="redis", + version="5.0.0b1", + qualifiers={}, + subpath=None, + ), + "fixed_by_purl_vulnerabilities": [], + } + ], + "fixed_by_purl": [], + "fixed_by_purl_vulnerabilities": [], + }, + ], + } + + assert searched_for_package_details == package_details + + assert searched_for_package_details.get("latest_non_vulnerable") == PackageURL( + type="pypi", + namespace=None, + name="redis", + version="5.0.0b1", + qualifiers={}, + subpath=None, + ) + + searched_for_package_fixing = searched_for_package.fixing + assert type(searched_for_package_fixing) == models.VulnerabilityQuerySet + assert searched_for_package_fixing.count() == 0 + assert len(searched_for_package_fixing) == 0 + assert list(searched_for_package_fixing) == [] + + def test_get_vulnerable_packages(self): + vuln_packages = Package.objects.vulnerable() + assert vuln_packages.count() == 4 + assert vuln_packages.distinct().count() == 3 + + first_vulnerable_package = vuln_packages.distinct()[0] + assert first_vulnerable_package.purl == "pkg:pypi/bogus@1.2.3" + + second_vulnerable_package = vuln_packages.distinct()[1] + assert second_vulnerable_package.purl == "pkg:pypi/redis@4.1.1" + + second_vulnerable_package_matching_fixed_packages = ( + Package.objects.get_fixed_by_package_versions(second_vulnerable_package, fix=True) + ) + first_fixed_by_package = second_vulnerable_package_matching_fixed_packages[0] + + assert len(second_vulnerable_package_matching_fixed_packages) == 2 + assert first_fixed_by_package.purl == "pkg:pypi/redis@4.3.6" + + def test_string_to_package(self): + purl_string = "pkg:maven/org.apache.tomcat/tomcat@10.0.0-M4" + purl = PackageURL.from_string(purl_string) + purl_to_dict = purl.to_dict() + + # For namespace, version, qualifiers and subpath, we need to add the or * to avoid an + # IntegrityError, e.g., django.db.utils.IntegrityError: null value in column "subpath" violates + # not-null constraint + vulnerablecode_package = models.Package.objects.create( + type=purl_to_dict.get("type"), + namespace=purl_to_dict.get("namespace") or "", + name=purl_to_dict.get("name"), + version=purl_to_dict.get("version") or "", + qualifiers=purl_to_dict.get("qualifiers") or {}, + subpath=purl_to_dict.get("subpath") or "", + ) + + assert type(vulnerablecode_package) == models.Package + assert vulnerablecode_package.purl == "pkg:maven/org.apache.tomcat/tomcat@10.0.0-M4" + assert vulnerablecode_package.package_url == "pkg:maven/org.apache.tomcat/tomcat@10.0.0-M4" + assert ( + vulnerablecode_package.plain_package_url + == "pkg:maven/org.apache.tomcat/tomcat@10.0.0-M4" + ) + assert ( + vulnerablecode_package.get_absolute_url() + == "/packages/pkg:maven/org.apache.tomcat/tomcat@10.0.0-M4" + ) + + 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): + 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")) + + # Expect an error when comparing different types. + with pytest.raises(TypeError): + assert versions.PypiVersion("0.9") < versions.DebianVersion("0.10") + + # 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_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 + + RANGE_CLASS_BY_SCHEMES["alpine"] = AlpineLinuxVersionRange + alpine_version = RANGE_CLASS_BY_SCHEMES["alpine"].version_class + assert alpine_version == versions.AlpineLinuxVersion + + 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) + + 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_affecting_vulnerabilities_vulnerabilityqueryset_method(self): + """ + Return querysets of Vulnerabilities using the VulnerabilityQuerySet affecting_vulnerabilities() method. + """ + searched_for_package = self.package_pypi_redis_4_1_1 + + # Return a queryset of Vulnerabilities that affect this Package. + this_package_vulnerabilities = ( + searched_for_package.vulnerabilities.affecting_vulnerabilities() + ) + + assert this_package_vulnerabilities[0] == self.vuln_VCID_g2fu_45jw_aaan + assert this_package_vulnerabilities[1] == self.vuln_VCID_rqe1_dkmg_aaad + + # Return a queryset of Vulnerabilities that affect any Package. + any_package_vulnerabilities = Vulnerability.objects.affecting_vulnerabilities() + + assert any_package_vulnerabilities[0] == self.vuln_VCID_abcd_efgh_1234 + assert any_package_vulnerabilities[1] == self.vuln_VCID_g2fu_45jw_aaan + assert any_package_vulnerabilities[2] == self.vuln_VCID_rqe1_dkmg_aaad + assert any_package_vulnerabilities[3] == self.vuln_VCID_rqe1_dkmg_aaad + + # Return a queryset of distinct Vulnerabilities that affect any Package. + any_package_vulnerabilities_distinct = ( + Vulnerability.objects.affecting_vulnerabilities().distinct() + ) + + assert any_package_vulnerabilities_distinct[0] == self.vuln_VCID_abcd_efgh_1234 + assert any_package_vulnerabilities_distinct[1] == self.vuln_VCID_g2fu_45jw_aaan + assert any_package_vulnerabilities_distinct[2] == self.vuln_VCID_rqe1_dkmg_aaad + + # Return a count of the queryset of distinct Vulnerabilities that affect any Package. + any_package_vulnerabilities_distinct_count = ( + Vulnerability.objects.affecting_vulnerabilities().distinct().count() + ) + + assert any_package_vulnerabilities_distinct_count == 3 + + # Return a queryset of all Vulnerabilities, regardless of whether they affect a package. + all_vulnerabilities = Vulnerability.objects.all() + + assert all_vulnerabilities[0] == self.vuln_VCID_abcd_efgh_1234 + assert all_vulnerabilities[1] == self.vuln_VCID_g2fu_45jw_aaan + assert all_vulnerabilities[2] == self.vuln_VCID_rqe1_dkmg_aaad + assert all_vulnerabilities[3] == self.vuln_VCID_wxyz_0000_0000 + + # Return a count of the queryset of all Vulnerabilities, regardless of whether they affect a package. + all_vulnerabilities_count = Vulnerability.objects.all().count() + + assert all_vulnerabilities_count == 4 + + def test_affecting_vulnerabilities_package_property_method(self): + """ + Return a queryset of Vulnerabilities using the Package affecting_vulnerabilities() property + method. + """ + searched_for_package = self.package_pypi_redis_4_1_1 + + # Return a queryset of Vulnerabilities that affect a specific Package. + this_package_vulnerabilities = searched_for_package.affecting_vulnerabilities + + assert this_package_vulnerabilities[0] == self.vuln_VCID_g2fu_45jw_aaan + assert this_package_vulnerabilities[1] == self.vuln_VCID_rqe1_dkmg_aaad + + def test_fixing_vulnerabilities_package_property_method(self): + """ + Return a queryset of Vulnerabilities using the Package fixing_vulnerabilities() property + method. + """ + # Return a queryset of Vulnerabilities that are fixed by a specific Package. + searched_for_package_redis_4_1_1 = self.package_pypi_redis_4_1_1 + redis_4_1_1_fixing_vulnerabilities = searched_for_package_redis_4_1_1.fixing_vulnerabilities + + assert redis_4_1_1_fixing_vulnerabilities.count() == 0 + + searched_for_package_redis_4_3_6 = self.package_pypi_redis_4_3_6 + redis_4_3_6_fixing_vulnerabilities = searched_for_package_redis_4_3_6.fixing_vulnerabilities + + assert redis_4_3_6_fixing_vulnerabilities.count() == 1 + assert redis_4_3_6_fixing_vulnerabilities[0] == self.vuln_VCID_g2fu_45jw_aaan + + searched_for_package_redis_5_0_0b1 = self.package_pypi_redis_5_0_0b1 + redis_5_0_0b1_fixing_vulnerabilities = ( + searched_for_package_redis_5_0_0b1.fixing_vulnerabilities + ) + + assert redis_5_0_0b1_fixing_vulnerabilities.count() == 1 + assert redis_5_0_0b1_fixing_vulnerabilities[0] == self.vuln_VCID_rqe1_dkmg_aaad + + def test_get_affecting_vulnerabilities_package_method(self): + """ + Return a list of vulnerabilities that affect this package. + """ + searched_for_package_redis_4_1_1 = self.package_pypi_redis_4_1_1 + redis_4_1_1_affecting_vulnerabilities = ( + searched_for_package_redis_4_1_1.get_affecting_vulnerabilities() + ) + + affecting_vulnerabilities = [ + { + "vulnerability": self.vuln_VCID_g2fu_45jw_aaan, + "fixed_by_purl": [], + "fixed_by_purl_vulnerabilities": [], + "fixed_by_package_details": [ + { + "fixed_by_purl": PackageURL( + type="pypi", + namespace=None, + name="redis", + version="4.3.6", + qualifiers={}, + subpath=None, + ), + "fixed_by_purl_vulnerabilities": [self.vuln_VCID_rqe1_dkmg_aaad], + } + ], + }, + { + "vulnerability": self.vuln_VCID_rqe1_dkmg_aaad, + "fixed_by_purl": [], + "fixed_by_purl_vulnerabilities": [], + "fixed_by_package_details": [ + { + "fixed_by_purl": PackageURL( + type="pypi", + namespace=None, + name="redis", + version="5.0.0b1", + qualifiers={}, + subpath=None, + ), + "fixed_by_purl_vulnerabilities": [], + } + ], + }, + ] + + assert redis_4_1_1_affecting_vulnerabilities == affecting_vulnerabilities + + def test_get_non_vulnerable_versions(self): + """ + Return a tuple of the next and latest non-vulnerable versions of this package as PackageURLs. + """ + searched_for_package_redis_4_1_1 = self.package_pypi_redis_4_1_1 + redis_4_1_1_non_vulnerable_versions = ( + searched_for_package_redis_4_1_1.get_non_vulnerable_versions() + ) + + non_vulnerable_versions = ( + PackageURL( + type="pypi", + namespace=None, + name="redis", + version="5.0.0b1", + qualifiers={}, + subpath=None, + ), + PackageURL( + type="pypi", + namespace=None, + name="redis", + version="5.0.0b1", + qualifiers={}, + subpath=None, + ), + ) + + assert redis_4_1_1_non_vulnerable_versions == non_vulnerable_versions + + def test_version_class_and_current_version(self): + searched_for_package_redis_4_1_1 = self.package_pypi_redis_4_1_1 + + package_version_class = RANGE_CLASS_BY_SCHEMES[ + searched_for_package_redis_4_1_1.type + ].version_class + + assert package_version_class == versions.PypiVersion + assert searched_for_package_redis_4_1_1.current_version == package_version_class( + string="4.1.1" + ) + assert str(searched_for_package_redis_4_1_1.current_version) == "4.1.1" + + def test_get_fixed_by_package_versions(self): + searched_for_package_redis_4_1_1 = self.package_pypi_redis_4_1_1 + + fixed_by_package_versions = Package.objects.get_fixed_by_package_versions( + searched_for_package_redis_4_1_1, fix=True + ) + + all_package_versions = Package.objects.get_fixed_by_package_versions( + searched_for_package_redis_4_1_1, fix=False + ) + + assert fixed_by_package_versions[0] == self.package_pypi_redis_4_3_6 + assert fixed_by_package_versions[1] == self.package_pypi_redis_5_0_0b1 + assert fixed_by_package_versions.count() == 2 + + assert all_package_versions[0] == self.package_pypi_redis_4_1_1 + assert all_package_versions[1] == self.package_pypi_redis_4_3_6 + assert all_package_versions[2] == self.package_pypi_redis_5_0_0b1 + assert all_package_versions.count() == 3 diff --git a/vulnerabilities/views.py b/vulnerabilities/views.py index fe406a0a8..7618369e2 100644 --- a/vulnerabilities/views.py +++ b/vulnerabilities/views.py @@ -84,6 +84,8 @@ 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["fixed_package_details"] = package.fixed_package_details + return context def get_object(self, queryset=None): @@ -186,7 +188,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: diff --git a/vulnerablecode/static/css/custom.css b/vulnerablecode/static/css/custom.css index b80ba3257..09fc632f2 100644 --- a/vulnerablecode/static/css/custom.css +++ b/vulnerablecode/static/css/custom.css @@ -196,16 +196,14 @@ code { } .two-col-left { - width: 160px; + width: 255px; 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; } @@ -327,6 +325,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,6 +374,174 @@ span.tag.custom { margin: 0px 0px 6px 10px; } +/* 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; +} + +.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; +} + +/* Floating container to display the PURL on the Package details page as the user scrolls down. */ +.floating-purl { + position: sticky; + top: 0; + width: 100%; + z-index: 100; + margin-bottom: 0px; +} + +.floating-purl .table td, +.floating-purl .table tbody tr:last-child td, +.floating-purl .table th { + border: solid 1px #dbdbdb; + background-color: #ffffff; +} +/* 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; +} + +.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; +} + /* Floating container to display the PURL on the Package details page as the user scrolls down. */ .floating-purl { position: sticky; @@ -383,3 +557,20 @@ span.tag.custom { border: solid 1px #dbdbdb; background-color: #ffffff; } + +/* Emphasis for affected/fixed headings and related references. */ +.affected-fixed { + color: #000000; + font-weight: 800; +} + +/* Emphasis for not vulnerable. */ +.emphasis-not-vulnerable { + background-color: #e6ffe6; + /* background-color: #e6ffff; */ +} + +/* Emphasis for vulnerable. */ +.emphasis-vulnerable { + background-color: #ffe6e6; +}
    - There are no known affected packages. + There are no known affected packages.
    - See Affected packages tab for more + See Affected packages tab for more