Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add initial fixed-affected-matching work #1228 #1249

Merged
merged 63 commits into from
Nov 30, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
63 commits
Select commit Hold shift + click to select a range
1e4079d
Add initial fixed-affected-matching work #1228
johnmhoran Jul 26, 2023
624f047
Explore context and Package class approaches for affected-fixed packa…
johnmhoran Jul 28, 2023
945b811
Add Prefetch and univers-based version comparison #1228
johnmhoran Jul 30, 2023
6497e90
Update affected-fixed package matching #1228
johnmhoran Aug 8, 2023
b6dba78
Improve matching and reporting code and UI #1228
johnmhoran Aug 10, 2023
814cd06
Add univers version, revise sort and related code, update and add new…
johnmhoran Aug 14, 2023
eee1d79
Merge branch 'main' into 1228-fixed-affected-version-matching #1228
johnmhoran Aug 14, 2023
f920ded
Move weakness test #1228
johnmhoran Aug 15, 2023
40c1758
Modify UI, update dictionary and tests #1228
johnmhoran Aug 15, 2023
f370671
Begin replacing strings with objects in package details dictionary #1228
johnmhoran Aug 28, 2023
9f73ea4
Merge branch 'main' into 1228-fixed-affected-version-matching #1228
johnmhoran Aug 28, 2023
9ccaed7
Clean current package details template and related model code #1228
johnmhoran Aug 29, 2023
1a26613
Begin work on major-version issue #1228
johnmhoran Aug 30, 2023
b626762
Complete first round of major-version vetting #1228
johnmhoran Sep 6, 2023
005c094
Remove major-version code, clean comments etc. #1228
johnmhoran Sep 8, 2023
ab80c46
Merge branch 'main' into 1228-fixed-affected-version-matching #1228
johnmhoran Sep 8, 2023
55f9678
Begin test refactoring #1228
johnmhoran Sep 11, 2023
cc45e2a
Merge branch 'main' into 1228-fixed-affected-version-matching #1228
johnmhoran Sep 11, 2023
f904f09
Merge branch 'main' into 1228-fixed-affected-version-matching #1228
johnmhoran Sep 11, 2023
b750d65
Finish package details code and template, refactor/create package-rel…
johnmhoran Sep 12, 2023
fe23fec
Commit the initial refactoring changes from last week #1228
johnmhoran Sep 18, 2023
4cdb629
Merge branch 'main' into 1228-fixed-affected-version-matching #1228
johnmhoran Sep 18, 2023
89aab8f
Refactor package details-related code #1228
johnmhoran Sep 21, 2023
04824e8
Merge branch 'main' into 1228-fixed-affected-version-matching #1228
johnmhoran Sep 21, 2023
e2e4e13
Update Package details UI and Package API #1228
johnmhoran Sep 27, 2023
c76a43d
Merge branch 'main' into 1228-fixed-affected-version-matching #1228
johnmhoran Sep 27, 2023
7a8f83b
Save test experiments including commented-out variations #1228
johnmhoran Sep 27, 2023
2fdc594
Fix 1 of 4 failing API tests #1228
johnmhoran Sep 27, 2023
1bb4f20
Add initial fixed-affected-matching work #1228
johnmhoran Jul 26, 2023
dff950f
Explore context and Package class approaches for affected-fixed packa…
johnmhoran Jul 28, 2023
627d117
Add Prefetch and univers-based version comparison #1228
johnmhoran Jul 30, 2023
65d82a7
Update affected-fixed package matching #1228
johnmhoran Aug 8, 2023
3b34e46
Improve matching and reporting code and UI #1228
johnmhoran Aug 10, 2023
eb69a01
Add univers version, revise sort and related code, update and add new…
johnmhoran Aug 14, 2023
9a9401a
Move weakness test #1228
johnmhoran Aug 15, 2023
5c6838c
Modify UI, update dictionary and tests #1228
johnmhoran Aug 15, 2023
3ef542b
Begin replacing strings with objects in package details dictionary #1228
johnmhoran Aug 28, 2023
1045b36
Clean current package details template and related model code #1228
johnmhoran Aug 29, 2023
57007b9
Begin work on major-version issue #1228
johnmhoran Aug 30, 2023
1ffccbe
Complete first round of major-version vetting #1228
johnmhoran Sep 6, 2023
dc701ca
Remove major-version code, clean comments etc. #1228
johnmhoran Sep 8, 2023
f0bdd3b
Begin test refactoring #1228
johnmhoran Sep 11, 2023
4314820
Finish package details code and template, refactor/create package-rel…
johnmhoran Sep 12, 2023
df7f404
Commit the initial refactoring changes from last week #1228
johnmhoran Sep 18, 2023
e475e5d
Refactor package details-related code #1228
johnmhoran Sep 21, 2023
6a72c58
Update Package details UI and Package API #1228
johnmhoran Sep 27, 2023
f5e267c
Save test experiments including commented-out variations #1228
johnmhoran Sep 27, 2023
ce2c6cf
Fix 1 of 4 failing API tests #1228
johnmhoran Sep 27, 2023
7c412ed
Update API including "lesser" fixed by versions, fix and update faili…
johnmhoran Sep 30, 2023
eecd504
Update APITestCasePackage() class #1228
johnmhoran Oct 3, 2023
9978841
Test lack of "vulnerability" property #1228
johnmhoran Oct 18, 2023
6cd41d3
Update get_affected_vulnerabilities() and test #1228
johnmhoran Oct 26, 2023
e111dbe
Update MinimalPackageSerializer() and missing-vulnerability-key test …
johnmhoran Oct 28, 2023
5eaa9ce
Append inside the if condition #1228
johnmhoran Oct 28, 2023
4ce5d51
Update get_vulnerability() method #1228
johnmhoran Nov 1, 2023
f1a0530
Enable test_models.py and fix failing tests #1228
johnmhoran Nov 22, 2023
9ec2a6a
Update per PR comments #1228
johnmhoran Nov 22, 2023
91c1817
Convert Package method to PackageQuerySet method, clean code and test…
johnmhoran Nov 23, 2023
3dac483
Merge branch '1228-fixed-affected-version-matching' of https://github…
TG1999 Nov 23, 2023
2abcba0
Fix failing tests
TG1999 Nov 24, 2023
0ec7d6c
Add property on functions in models
TG1999 Nov 24, 2023
7a512c2
Merge branch 'main' into 1228-fixed-affected-version-matching #1228
johnmhoran Nov 28, 2023
26c0239
Add and fix tests, address other comments #1228
johnmhoran Nov 29, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 54 additions & 6 deletions vulnerabilities/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
johnmhoran marked this conversation as resolved.
Show resolved Hide resolved
fields = ["url", "purl", "is_vulnerable", "affected_by_vulnerabilities"]


class MinimalVulnerabilitySerializer(serializers.HyperlinkedModelSerializer):
Expand Down Expand Up @@ -99,7 +118,6 @@ class Meta:


class VulnerabilitySerializer(serializers.HyperlinkedModelSerializer):

fixed_packages = MinimalPackageSerializer(
many=True, source="filtered_fixed_packages", read_only=True
)
Expand All @@ -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")
Expand All @@ -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(
Expand All @@ -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)
Expand All @@ -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
Expand All @@ -190,6 +236,8 @@ class Meta:
"version",
"qualifiers",
"subpath",
"next_non_vulnerable_version",
"latest_non_vulnerable_version",
"affected_by_vulnerabilities",
"fixing_vulnerabilities",
]
Expand Down
166 changes: 165 additions & 1 deletion vulnerabilities/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -67,6 +70,12 @@ def paginated(self, per_page=5000):


class VulnerabilityQuerySet(BaseQuerySet):
def affecting_vulnerabilities(self):
johnmhoran marked this conversation as resolved.
Show resolved Hide resolved
"""
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.
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing unit test

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test added yesterday.

"""
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):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing unit test

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test added yesterday.

"""
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):
"""
Expand Down
Loading
Loading