Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
8 changes: 7 additions & 1 deletion CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,13 @@ Version v30.3.2

- We re-enabled support for the PostgreSQL securities advisories importer.
- We fixed the API key request form UI and made it consistent with rest of UI.

- We made bulk search faster by pre-computing
`package_url` and `plain_package_url` in Package model.
And provided two options in package bulk search
``purl_only`` option to get only vulnerable
purls without any extra details, ``plain_purl`` option
to filter purls without qualifiers and
subpath and also return them without qualifiers and subpath.

Version v30.3.1
----------------
Expand Down
68 changes: 45 additions & 23 deletions vulnerabilities/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -238,40 +238,62 @@ def bulk_search(self, request):
"""
Lookup for vulnerable packages using many Package URLs at once.
"""
response = []

purls = request.data.get("purls", []) or []
purl_only = request.data.get("purl_only", False)
plain_purl = request.data.get("plain_purl", False)
if not purls or not isinstance(purls, list):
return Response(
status=400,
data={"Error": "A non-empty 'purls' list of package URLs is required."},
data={"Error": "A non-empty 'purls' list of PURLs is required."},
)

if plain_purl:
purl_objects = [PackageURL.from_string(purl) for purl in purls]
plain_purl_objects = [
PackageURL(
type=purl.type,
namespace=purl.namespace,
name=purl.name,
version=purl.version,
)
for purl in purl_objects
]
plain_purls = [str(purl) for purl in plain_purl_objects]

query = (
Package.objects.filter(plain_package_url__in=plain_purls)
.order_by("plain_package_url")
.distinct("plain_package_url")
)
for purl in request.data["purls"]:
try:
purl_string = purl
purl = PackageURL.from_string(purl)
except ValueError:
return Response(status=400, data={"Error": f"Invalid Package URL: {purl}"})
lookups = get_purl_query_lookups(purl)
purl_data = Package.objects.filter(**lookups)
purl_response = {}
if purl_data:
purl_response = PackageSerializer(purl_data[0], context={"request": request}).data
else:
purl_response = purl.to_dict()
purl_response["unresolved_vulnerabilities"] = []
purl_response["resolved_vulnerabilities"] = []
purl_response["purl"] = purl_string
response.append(purl_response)

return Response(response)

if not purl_only:
return Response(
PackageSerializer(query, many=True, context={"request": request}).data
)

# using order by and distinct because there will be
# many fully qualified purl for a single plain purl
vulnerable_purls = query.vulnerable().only("plain_package_url")
vulnerable_purls = [str(package.plain_package_url) for package in vulnerable_purls]
return Response(data=vulnerable_purls)

query = Package.objects.filter(package_url__in=purls).distinct()

if not purl_only:
return Response(PackageSerializer(query, many=True, context={"request": request}).data)

vulnerable_purls = query.vulnerable().only("package_url")
vulnerable_purls = [str(package.package_url) for package in vulnerable_purls]
return Response(data=vulnerable_purls)

@action(detail=False, methods=["get"], throttle_scope="vulnerable_packages")
def all(self, request):
"""
Return the Package URLs of all packages known to be vulnerable.
"""
vulnerable_packages = Package.objects.vulnerable().only(*PackageURL._fields).distinct()
vulnerable_purls = [str(package.purl) for package in vulnerable_packages]
vulnerable_packages = Package.objects.vulnerable().only("package_url").distinct()
vulnerable_purls = [str(package.package_url) for package in vulnerable_packages]
return Response(vulnerable_purls)


Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Generated by Django 4.0.7 on 2022-11-28 12:58

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('vulnerabilities', '0033_alter_vulnerabilityseverity_scoring_system'),
]

operations = [
migrations.AddField(
model_name='package',
name='package_url',
field=models.CharField(blank=True, db_index=True, help_text='The Package URL for this package.', max_length=1000),
),
migrations.AddField(
model_name='package',
name='plain_package_url',
field=models.CharField(blank=True, db_index=True, help_text='The Package URL for this package without qualifiers and subpath.', max_length=1000),
),
]
41 changes: 41 additions & 0 deletions vulnerabilities/migrations/0035_add_package_url_to_packages.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
from django.db import migrations
from packageurl import PackageURL

class Migration(migrations.Migration):

def save_purls(apps, schema_editor):
Package = apps.get_model("vulnerabilities", "Package")
updatables = []
for package in Package.objects.all():
purl = PackageURL(
type=package.type,
namespace=package.namespace,
name=package.name,
version=package.version,
qualifiers=package.qualifiers,
subpath=package.subpath,
)
plain_purl = PackageURL(
type=package.type,
namespace=package.namespace,
name=package.name,
version=package.version,
)
package.package_url = str(purl)
package.plain_package_url = str(plain_purl)
updatables.append(package)

updated = Package.objects.bulk_update(
objs = updatables,
fields=["package_url", "plain_package_url"],
batch_size=500,
)
print(f"Migrated {updated} packages with package_url")

dependencies = [
("vulnerabilities", "0034_package_package_url_package_plain_package_url"),
]

operations = [
migrations.RunPython(save_purls, reverse_code=migrations.RunPython.noop),
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Generated by Django 4.0.7 on 2022-11-28 13:00

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('vulnerabilities', '0035_add_package_url_to_packages'),
]

operations = [
migrations.AlterField(
model_name='package',
name='package_url',
field=models.CharField(db_index=True, help_text='The Package URL for this package.', max_length=1000),
),
migrations.AlterField(
model_name='package',
name='plain_package_url',
field=models.CharField(db_index=True, help_text='The Package URL for this package without qualifiers and subpath.', max_length=1000),
),
]
33 changes: 33 additions & 0 deletions vulnerabilities/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -531,8 +531,41 @@ class Package(PackageURLMixin):
to="Vulnerability", through="PackageRelatedVulnerability"
)

package_url = models.CharField(
max_length=1000,
null=False,
help_text="The Package URL for this package.",
db_index=True,
)

plain_package_url = models.CharField(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I am not in love with the plain_package_url name but this makes sense for now. We will need to find a better way.
I will merge for now!

max_length=1000,
null=False,
help_text="The Package URL for this package without qualifiers and subpath.",
db_index=True,
)

objects = PackageQuerySet.as_manager()

def save(self, *args, **kwargs):
purl_object = PackageURL(
type=self.type,
namespace=self.namespace,
name=self.name,
version=self.version,
qualifiers=self.qualifiers,
subpath=self.subpath,
)
plain_purl = PackageURL(
type=self.type,
namespace=self.namespace,
name=self.name,
version=self.version,
)
self.package_url = str(purl_object)
self.plain_package_url = str(plain_purl)
super().save(*args, **kwargs)

@property
def purl(self):
return self.package_url
Expand Down
31 changes: 27 additions & 4 deletions vulnerabilities/tests/test_fix_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -358,6 +358,19 @@ def setUp(self):
attrs = {k: v for k, v in purl.to_dict().items() if v}
Package.objects.create(**attrs)

vulnerable_packages = [
"pkg:nginx/nginx@1.0.15?foo=bar",
"pkg:nginx/nginx@1.0.15?foo=baz",
]

vuln = Vulnerability.objects.create(summary="test")

for package in vulnerable_packages:
purl = PackageURL.from_string(package)
attrs = {k: v for k, v in purl.to_dict().items() if v}
pkg = Package.objects.create(**attrs)
PackageRelatedVulnerability.objects.create(package=pkg, vulnerability=vuln)

def test_bulk_api_response(self):
request_body = {
"purls": self.packages,
Expand All @@ -370,9 +383,7 @@ def test_bulk_api_response(self):
assert len(response) == 13

def test_bulk_api_response_with_ignoring_qualifiers(self):
request_body = {
"purls": ["pkg:nginx/nginx@1.0.15?qualifiers=dev"],
}
request_body = {"purls": ["pkg:nginx/nginx@1.0.15?qualifiers=dev"], "plain_purl": True}
response = self.csrf_client.post(
"/api/packages/bulk_search",
data=json.dumps(request_body),
Expand All @@ -382,16 +393,28 @@ def test_bulk_api_response_with_ignoring_qualifiers(self):
assert response[0]["purl"] == "pkg:nginx/nginx@1.0.15"

def test_bulk_api_response_with_ignoring_subpath(self):
request_body = {"purls": ["pkg:nginx/nginx@1.0.15#dev/subpath"], "plain_purl": True}
response = self.csrf_client.post(
"/api/packages/bulk_search",
data=json.dumps(request_body),
content_type="application/json",
).json()
assert len(response) == 1
assert response[0]["purl"] == "pkg:nginx/nginx@1.0.15"

def test_bulk_api_with_purl_only_option(self):
request_body = {
"purls": ["pkg:nginx/nginx@1.0.15#dev/subpath"],
"purl_only": True,
"plain_purl": True,
}
response = self.csrf_client.post(
"/api/packages/bulk_search",
data=json.dumps(request_body),
content_type="application/json",
).json()
assert len(response) == 1
assert response[0]["purl"] == "pkg:nginx/nginx@1.0.15"
assert response[0] == "pkg:nginx/nginx@1.0.15"


class BulkSearchAPICPE(TestCase):
Expand Down