|
23 | 23 | from django.core.validators import MinValueValidator
|
24 | 24 | from django.db import models
|
25 | 25 | from django.db.models import Count
|
| 26 | +from django.db.models import Prefetch |
26 | 27 | from django.db.models import Q
|
27 | 28 | from django.db.models.functions import Length
|
28 | 29 | from django.db.models.functions import Trim
|
|
32 | 33 | from packageurl.contrib.django.models import PackageURLQuerySet
|
33 | 34 | from packageurl.contrib.django.models import without_empty_values
|
34 | 35 | from rest_framework.authtoken.models import Token
|
| 36 | +from univers import versions |
| 37 | +from univers.version_range import RANGE_CLASS_BY_SCHEMES |
35 | 38 |
|
36 | 39 | from vulnerabilities.severity_systems import SCORING_SYSTEMS
|
37 | 40 | from vulnerabilities.utils import build_vcid
|
@@ -67,6 +70,12 @@ def paginated(self, per_page=5000):
|
67 | 70 |
|
68 | 71 |
|
69 | 72 | class VulnerabilityQuerySet(BaseQuerySet):
|
| 73 | + def affecting_vulnerabilities(self): |
| 74 | + """ |
| 75 | + Return a queryset of Vulnerability that affect a package. |
| 76 | + """ |
| 77 | + return self.filter(packagerelatedvulnerability__fix=False) |
| 78 | + |
70 | 79 | def with_cpes(self):
|
71 | 80 | """
|
72 | 81 | Return a queryset of Vulnerability that have one or more NVD CPE references.
|
@@ -404,6 +413,24 @@ def purl_to_dict(purl: PackageURL):
|
404 | 413 |
|
405 | 414 |
|
406 | 415 | class PackageQuerySet(BaseQuerySet, PackageURLQuerySet):
|
| 416 | + def get_fixed_by_package_versions(self, purl: PackageURL, fix=True): |
| 417 | + """ |
| 418 | + Return a queryset of all the package versions of this `package` that fix any vulnerability. |
| 419 | + If `fix` is False, return all package versions whether or not they fix a vulnerability. |
| 420 | + """ |
| 421 | + filter_dict = { |
| 422 | + "name": purl.name, |
| 423 | + "namespace": purl.namespace, |
| 424 | + "type": purl.type, |
| 425 | + "qualifiers": purl.qualifiers, |
| 426 | + "subpath": purl.subpath, |
| 427 | + } |
| 428 | + |
| 429 | + if fix: |
| 430 | + filter_dict["packagerelatedvulnerability__fix"] = True |
| 431 | + |
| 432 | + return Package.objects.filter(**filter_dict).distinct() |
| 433 | + |
407 | 434 | def get_or_create_from_purl(self, purl: PackageURL):
|
408 | 435 | """
|
409 | 436 | Return an existing or new Package (created if neeed) given a
|
@@ -601,7 +628,6 @@ def __str__(self):
|
601 | 628 | return self.package_url
|
602 | 629 |
|
603 | 630 | @property
|
604 |
| - # TODO: consider renaming to "affected_by" |
605 | 631 | def affected_by(self):
|
606 | 632 | """
|
607 | 633 | Return a queryset of vulnerabilities affecting this package.
|
@@ -642,6 +668,144 @@ def get_absolute_url(self):
|
642 | 668 | """
|
643 | 669 | return reverse("package_details", args=[self.purl])
|
644 | 670 |
|
| 671 | + def sort_by_version(self, packages): |
| 672 | + """ |
| 673 | + Return a list of `packages` sorted by version. |
| 674 | + """ |
| 675 | + if not packages: |
| 676 | + return [] |
| 677 | + |
| 678 | + return sorted( |
| 679 | + packages, |
| 680 | + key=lambda x: self.version_class(x.version), |
| 681 | + ) |
| 682 | + |
| 683 | + @property |
| 684 | + def version_class(self): |
| 685 | + return RANGE_CLASS_BY_SCHEMES[self.type].version_class |
| 686 | + |
| 687 | + @property |
| 688 | + def current_version(self): |
| 689 | + return self.version_class(self.version) |
| 690 | + |
| 691 | + @property |
| 692 | + def fixed_package_details(self): |
| 693 | + """ |
| 694 | + Return a mapping of vulnerabilities that affect this package and the next and |
| 695 | + latest non-vulnerable versions. |
| 696 | + """ |
| 697 | + package_details = {} |
| 698 | + package_details["purl"] = PackageURL.from_string(self.purl) |
| 699 | + |
| 700 | + next_non_vulnerable, latest_non_vulnerable = self.get_non_vulnerable_versions() |
| 701 | + package_details["next_non_vulnerable"] = next_non_vulnerable |
| 702 | + package_details["latest_non_vulnerable"] = latest_non_vulnerable |
| 703 | + |
| 704 | + package_details["vulnerabilities"] = self.get_affecting_vulnerabilities() |
| 705 | + |
| 706 | + return package_details |
| 707 | + |
| 708 | + def get_non_vulnerable_versions(self): |
| 709 | + """ |
| 710 | + Return a tuple of the next and latest non-vulnerable versions as PackageURLs. Return a tuple of |
| 711 | + (None, None) if there is no non-vulnerable version. |
| 712 | + """ |
| 713 | + package_versions = Package.objects.get_fixed_by_package_versions(self, fix=False) |
| 714 | + |
| 715 | + non_vulnerable_versions = [] |
| 716 | + for version in package_versions: |
| 717 | + if not version.is_vulnerable: |
| 718 | + non_vulnerable_versions.append(version) |
| 719 | + |
| 720 | + later_non_vulnerable_versions = [] |
| 721 | + for non_vuln_ver in non_vulnerable_versions: |
| 722 | + if self.version_class(non_vuln_ver.version) > self.current_version: |
| 723 | + later_non_vulnerable_versions.append(non_vuln_ver) |
| 724 | + |
| 725 | + if later_non_vulnerable_versions: |
| 726 | + sorted_versions = self.sort_by_version(later_non_vulnerable_versions) |
| 727 | + next_non_vulnerable_version = sorted_versions[0] |
| 728 | + latest_non_vulnerable_version = sorted_versions[-1] |
| 729 | + |
| 730 | + next_non_vulnerable = PackageURL.from_string(next_non_vulnerable_version.purl) |
| 731 | + latest_non_vulnerable = PackageURL.from_string(latest_non_vulnerable_version.purl) |
| 732 | + |
| 733 | + return next_non_vulnerable, latest_non_vulnerable |
| 734 | + |
| 735 | + return None, None |
| 736 | + |
| 737 | + def get_affecting_vulnerabilities(self): |
| 738 | + """ |
| 739 | + Return a list of vulnerabilities that affect this package together with information regarding |
| 740 | + the versions that fix the vulnerabilities. |
| 741 | + """ |
| 742 | + package_details_vulns = [] |
| 743 | + |
| 744 | + fixed_by_packages = Package.objects.get_fixed_by_package_versions(self, fix=True) |
| 745 | + |
| 746 | + package_vulnerabilities = self.vulnerabilities.affecting_vulnerabilities().prefetch_related( |
| 747 | + Prefetch( |
| 748 | + "packages", |
| 749 | + queryset=fixed_by_packages, |
| 750 | + to_attr="fixed_packages", |
| 751 | + ) |
| 752 | + ) |
| 753 | + |
| 754 | + for vuln in package_vulnerabilities: |
| 755 | + package_details_vulns.append({"vulnerability": vuln}) |
| 756 | + later_fixed_packages = [] |
| 757 | + |
| 758 | + for fixed_pkg in vuln.fixed_packages: |
| 759 | + if fixed_pkg not in fixed_by_packages: |
| 760 | + continue |
| 761 | + fixed_version = self.version_class(fixed_pkg.version) |
| 762 | + if fixed_version > self.current_version: |
| 763 | + later_fixed_packages.append(fixed_pkg) |
| 764 | + |
| 765 | + next_fixed_package = None |
| 766 | + next_fixed_package_vulns = [] |
| 767 | + |
| 768 | + sort_fixed_by_packages_by_version = [] |
| 769 | + if later_fixed_packages: |
| 770 | + sort_fixed_by_packages_by_version = self.sort_by_version(later_fixed_packages) |
| 771 | + |
| 772 | + fixed_by_pkgs = [] |
| 773 | + |
| 774 | + for vuln_details in package_details_vulns: |
| 775 | + if vuln_details["vulnerability"] != vuln: |
| 776 | + continue |
| 777 | + vuln_details["fixed_by_purl"] = [] |
| 778 | + vuln_details["fixed_by_purl_vulnerabilities"] = [] |
| 779 | + |
| 780 | + for fixed_by_pkg in sort_fixed_by_packages_by_version: |
| 781 | + fixed_by_package_details = {} |
| 782 | + fixed_by_purl = PackageURL.from_string(fixed_by_pkg.purl) |
| 783 | + next_fixed_package_vulns = list(fixed_by_pkg.affected_by) |
| 784 | + |
| 785 | + fixed_by_package_details["fixed_by_purl"] = fixed_by_purl |
| 786 | + fixed_by_package_details[ |
| 787 | + "fixed_by_purl_vulnerabilities" |
| 788 | + ] = next_fixed_package_vulns |
| 789 | + fixed_by_pkgs.append(fixed_by_package_details) |
| 790 | + |
| 791 | + vuln_details["fixed_by_package_details"] = fixed_by_pkgs |
| 792 | + |
| 793 | + return package_details_vulns |
| 794 | + |
| 795 | + @property |
| 796 | + def fixing_vulnerabilities(self): |
| 797 | + """ |
| 798 | + Return a queryset of Vulnerabilities that are fixed by this `package`. |
| 799 | + """ |
| 800 | + return self.vulnerabilities.filter(packagerelatedvulnerability__fix=True) |
| 801 | + |
| 802 | + @property |
| 803 | + def affecting_vulnerabilities(self): |
| 804 | + """ |
| 805 | + Return a queryset of Vulnerabilities that affect this `package`. |
| 806 | + """ |
| 807 | + return self.vulnerabilities.filter(packagerelatedvulnerability__fix=False) |
| 808 | + |
645 | 809 |
|
646 | 810 | class PackageRelatedVulnerability(models.Model):
|
647 | 811 | """
|
|
0 commit comments