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

Base implementation of a Vulnerability models #94 #148

Merged
merged 64 commits into from
Aug 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
64 commits
Select commit Hold shift + click to select a range
5d5319a
Base implementation of a Vulnerability models #94
tdruez Jul 4, 2024
591181c
Merge branch 'main' into 94-cravex-models
tdruez Jul 12, 2024
45a5cbc
Add management command to fetch all vulnerabilities of a Dataspace #94
tdruez Jul 12, 2024
678bcb9
Refactor the views using the Vulnerability instead of API calls #94
tdruez Jul 12, 2024
2c90ec1
Consolidate migrations #94
tdruez Jul 15, 2024
724998a
Merge branch 'main' into 94-cravex-models
tdruez Aug 5, 2024
a352e00
Update the Package list to use the new Vulnerability model #138
tdruez Aug 5, 2024
b947380
Add support for Component with the new Vulnerability model #138
tdruez Aug 5, 2024
3321237
Refactor the TabVulnerabilityMixin #138
tdruez Aug 5, 2024
1fbc989
Progress on fetchvulnerabilities command #138
tdruez Aug 5, 2024
525d8d3
Remove the user requirement on the Vulnerability model #138
tdruez Aug 5, 2024
5712a50
WIP on the fetch management command #138
tdruez Aug 6, 2024
ebeedad
Consolidate migrations files into one single file #138
tdruez Aug 6, 2024
9859878
Refactor the vulnerability list as a table #138
tdruez Aug 6, 2024
de2f198
Add setup for the RQ scheduler #94
tdruez Aug 12, 2024
c14fa60
Add vulnerability update task to be executed for the RQ scheduler #94
tdruez Aug 12, 2024
8de15c4
Store and display the latest vulnerability update in admin #94
tdruez Aug 12, 2024
9a3a8fc
Add part of support for reporting on Vulnerability #94
tdruez Aug 12, 2024
e8a0b14
Update the ProductTabInventoryView to use the vulnerability fields #94
tdruez Aug 13, 2024
8f32b5a
Move the m2m field at the Component/Package level #94
tdruez Aug 13, 2024
fa401de
Merge branch 'main' into 94-cravex-models
tdruez Aug 13, 2024
a1cb58e
Display the Vulnerability count in tab_hierarchy #94
tdruez Aug 13, 2024
216214a
Display the Vulnerability count in tab_dependencies #94
tdruez Aug 13, 2024
2c5d621
Raise timeout value #94
tdruez Aug 13, 2024
5aa6ff9
Add filter by vulnerable in hierarchy tab #94
tdruez Aug 13, 2024
9cd3dc9
Add filter by vulnerable in inventory tab #94
tdruez Aug 13, 2024
5bb7236
Improve the rendering of is_vulnerable filters in the UI #94
tdruez Aug 14, 2024
b859206
Hide is_vulnerable filters if not enable_vulnerablecodedb_access #94
tdruez Aug 14, 2024
3426230
Fix unit tests #94
tdruez Aug 14, 2024
25658b2
Fix unit tests #94
tdruez Aug 14, 2024
0eba074
Fix unit tests #94
tdruez Aug 14, 2024
e7a12fd
Remove duplication #94
tdruez Aug 14, 2024
fe04c40
Fix more failing tests #94
tdruez Aug 14, 2024
bfecdce
Fix more failing tests #94
tdruez Aug 14, 2024
34b0c63
Fetch vulnerability on Package creation/update #94
tdruez Aug 14, 2024
6dd9be9
Improve the fetchvulnerabilities management command #94
tdruez Aug 14, 2024
0a76e9d
Fix docstring #94
tdruez Aug 14, 2024
1b23f8a
Fix issue #94
tdruez Aug 14, 2024
07f697c
Fix vulnerability filters #94
tdruez Aug 14, 2024
3cbd5fc
Consolidate migrations #94
tdruez Aug 16, 2024
21839c4
Refine and add unit tests for the IsVulnerableFilter #94
tdruez Aug 16, 2024
b7e403d
Fix failing tests #94
tdruez Aug 16, 2024
8aeb586
Refine and add more unit tests #94
tdruez Aug 16, 2024
d14b069
Refine and add more unit tests #94
tdruez Aug 16, 2024
3dda61a
Fix bug in create_vulnerabilities #94
tdruez Aug 16, 2024
c41ca90
Add cron job on app starting #94
tdruez Aug 16, 2024
5978612
Refactor the scheduler into a dedicated service #94
tdruez Aug 19, 2024
d874250
Refine the vulnerability fetching #94
tdruez Aug 19, 2024
46c239d
Implement the update_vulnerabilities task proper #94
tdruez Aug 19, 2024
505d597
Add fetch_vulnerabilities in PackageImporter #94
tdruez Aug 19, 2024
2c1f8a3
Enhance the setupcron command and set proper timeout #94
tdruez Aug 20, 2024
a9a3b02
Start 2 workers using rqworker-pool #94
tdruez Aug 20, 2024
5f936d9
Add unit test for fetch_vulnerabilities triggers #94
tdruez Aug 20, 2024
b8fb7f5
Fetch vulnerabilities post Product imports #94
tdruez Aug 20, 2024
544120a
Display the latest data update in the integration status page #94
tdruez Aug 20, 2024
3de2f31
Add changelog entry #94
tdruez Aug 21, 2024
b65526e
Merge VulnerableObjectMixin into VulnerabilityMixin #94
tdruez Aug 21, 2024
e39dd2c
Make the DEJACODE_VULNERABILITIES_CRON setting #94
tdruez Aug 21, 2024
9a5cc96
Add missing db index on the Package model #94
tdruez Aug 21, 2024
9944ea8
Limit the QS to types supported by VulnerableCode #94
tdruez Aug 21, 2024
251961f
Renaming for consistency and add unit tests #94
tdruez Aug 21, 2024
8b6f449
Add unit tests for VulnerabilityMixin #94
tdruez Aug 21, 2024
056e374
Add unit tests #94
tdruez Aug 21, 2024
39fe7cf
Add unit tests for the fetchvulnerabilities management command #94
tdruez Aug 21, 2024
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
35 changes: 35 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,41 @@ Release notes
Product.
https://github.com/nexB/dejacode/issues/138

- Add a task scheduler service to the Docker Compose stack.
This service runs a dedicated ``setupcron`` management command to create the
application's scheduled cron jobs.
The scheduler is configured to run the daily vulnerabilities update task.
https://github.com/nexB/dejacode/issues/94

- Add a new Vulnerability model and all the code logic to fetch and create
Vulnerability records and assign those to Package/Component through ManyToMany
relationships.
A fetchvulnerabilities management command is available to fetch all the relevant
data from VulnerableCode for a given Dataspace.
The latest vulnerability data refresh date is displayed in the Admin dashboard in a
new "Data updates" section in the bottom right corner.
It is also available in the "Integration Status" page.
The Package/Component views that display vulnerability information (icon or tab)
are now using the data from the Vulnerability model in place of calling the
VulnerableCode API on each request. This results into much better performances as
we do not depend on the VulnerableCode service to render the DejaCode view anymore.
Also, this will make Vulnerability data available in the Reporting system.
The vulnerability icon is displayed next to the Package/Component identifier in the
Product views: "Inventory", "Hierarchy", "Dependencies" tabs.
The vulnerability data is available in Reporting either through the is_vulnerable
property on Package/Component column template or going through the full
affected_by_vulnerabilities m2m field.
This is available in both Query and ColumnTemplate.
The vulnerabilities are fetched each time a Package is created/modified
(note that a purl is required on the package for the lookup).
Also, all the Packages of a Product are updated with latest vulnerabilities from
the VulnerableCode service following importing data in Product using:
- Import data from Scan
- Load Packages from SBOMs
- Import Packages from manifests
- Pull ScanCode.io Project data
https://github.com/nexB/dejacode/issues/94

### Version 5.1.0

- Upgrade Python version to 3.12 and Django to 5.0.x
Expand Down
23 changes: 23 additions & 0 deletions component_catalog/filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,21 @@
from license_library.models import License


class IsVulnerableFilter(HasRelationFilter):
def __init__(self, *args, **kwargs):
kwargs["lookup_expr"] = "isnull"
kwargs["empty_label"] = "Any"
kwargs.setdefault("label", _("Is Vulnerable"))
kwargs.setdefault(
"choices",
(
("yes", _("Affected by vulnerabilities")),
("no", _("No vulnerabilities found")),
),
)
super().__init__(*args, **kwargs)


class ComponentFilterSet(DataspacedFilterSet):
related_only = [
"licenses",
Expand Down Expand Up @@ -85,6 +100,10 @@ class ComponentFilterSet(DataspacedFilterSet):
search_placeholder="Search keywords",
),
)
is_vulnerable = IsVulnerableFilter(
field_name="affected_by_vulnerabilities",
widget=DropDownRightWidget(link_content='<i class="fas fa-bug"></i>'),
)

class Meta:
model = Component
Expand Down Expand Up @@ -219,6 +238,10 @@ class PackageFilterSet(DataspacedFilterSet):
empty_label="Last modified (default)",
widget=SortDropDownWidget,
)
is_vulnerable = IsVulnerableFilter(
field_name="affected_by_vulnerabilities",
widget=DropDownRightWidget(link_content='<i class="fas fa-bug"></i>'),
)

class Meta:
model = Package
Expand Down
8 changes: 8 additions & 0 deletions component_catalog/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -446,6 +446,9 @@ def save(self, *args, **kwargs):
)
self.cleaned_data["scan_submitted"] = True

if self.user.dataspace.enable_vulnerablecodedb_access:
instance.fetch_vulnerabilities()

return instance


Expand Down Expand Up @@ -1043,6 +1046,11 @@ def save(self, commit=True):
self._set_purldb_uuid_on_instance()
return super().save(commit)

def _save_m2m(self):
super()._save_m2m()
if self.dataspace.enable_vulnerablecodedb_access:
self.instance.fetch_vulnerabilities()


class ComponentMassUpdateForm(
LicenseExpressionFormMixin,
Expand Down
14 changes: 13 additions & 1 deletion component_catalog/importers.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
from component_catalog.models import Package
from component_catalog.models import Subcomponent
from component_catalog.programming_languages import PROGRAMMING_LANGUAGES
from component_catalog.vulnerabilities import fetch_for_queryset
from dje.fields import SmartFileField
from dje.forms import JSONListField
from dje.importers import BaseImporter
Expand Down Expand Up @@ -182,6 +183,7 @@ class Meta:
"packages",
"completion_level",
"request_count",
"affected_by_vulnerabilities",
# JSONField not supported
"dependencies",
)
Expand Down Expand Up @@ -261,6 +263,7 @@ class Meta:
"file_references",
"request_count",
"parties",
"affected_by_vulnerabilities",
]

def __init__(self, *args, **kwargs):
Expand Down Expand Up @@ -343,7 +346,7 @@ def prepare_data_json(self, data):
input_as_list_of_dict = packages

# Using a dict comprehension to keep the original key order and
# ensure we have all possibile headers.
# ensure we have all possible headers.
header_row = {key: None for package in packages for key in package.keys()}
header_row = list(header_row.keys())

Expand Down Expand Up @@ -423,6 +426,15 @@ def get_form_kwargs(self):
kwargs["is_from_scancode"] = getattr(self, "is_from_scancode", False)
return kwargs

def save_all(self):
"""Fetch vulnerabilities for imported Packages."""
super().save_all()

if self.dataspace.enable_vulnerablecodedb_access:
package_pks = [package.pk for package in self.results["added"]]
package_qs = Package.objects.scope(dataspace=self.dataspace).filter(pk__in=package_pks)
fetch_for_queryset(package_qs, self.dataspace)


class SubcomponentImportForm(
ComponentRelatedFieldImportMixin,
Expand Down
46 changes: 46 additions & 0 deletions component_catalog/management/commands/fetchvulnerabilities.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
#
# Copyright (c) nexB Inc. and others. All rights reserved.
# DejaCode is a trademark of nexB Inc.
# SPDX-License-Identifier: AGPL-3.0-only
# See https://github.com/nexB/dejacode for support or download.
# See https://aboutcode.org for more information about AboutCode FOSS projects.
#

from django.core.management.base import CommandError

from component_catalog.vulnerabilities import fetch_from_vulnerablecode
from dejacode_toolkit.vulnerablecode import VulnerableCode
from dje.management.commands import DataspacedCommand


class Command(DataspacedCommand):
help = "Fetch vulnerabilities for the provided Dataspace"

def add_arguments(self, parser):
super().add_arguments(parser)
parser.add_argument(
"--batch-size",
type=int,
default=50,
help="Specifies the number of objects per requests to the VulnerableCode service",
)
parser.add_argument(
"--timeout",
type=int,
default=30,
help="Request timeout in seconds",
)

def handle(self, *args, **options):
super().handle(*args, **options)
batch_size = options["batch_size"]
timeout = options["timeout"]

if not self.dataspace.enable_vulnerablecodedb_access:
raise CommandError("VulnerableCode is not enabled on this Dataspace.")

vulnerablecode = VulnerableCode(self.dataspace)
if not vulnerablecode.is_configured():
raise CommandError("VulnerableCode is not configured.")

fetch_from_vulnerablecode(self.dataspace, batch_size, timeout, log_func=self.stdout.write)
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
# Generated by Django 5.0.6 on 2024-08-21 11:20

import component_catalog.models
import django.db.models.deletion
import dje.fields
import uuid
from django.conf import settings
from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('component_catalog', '0005_remove_component_concluded_license_and_more'),
('dje', '0004_dataspace_vulnerabilities_updated_at'),
('license_library', '0002_initial'),
('policy', '0002_initial'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]

operations = [
migrations.AlterField(
model_name='package',
name='filename',
field=models.CharField(blank=True, help_text='The exact file name (typically an archive of some type) of the package. This is usually the name of the file as downloaded from a website.', max_length=255, validators=[component_catalog.models.validate_filename], verbose_name='Filename'),
),
migrations.AlterField(
model_name='package',
name='license_expression',
field=models.CharField(blank=True, help_text='The License Expression assigned to a DejaCode Package or Component is an editable value equivalent to a "concluded license" as determined by a curator who has performed analysis to clarify or correct the declared license expression, which may have been assigned automatically (from a scan or an associated package definition) when the Package or Component was originally created. A license expression defines the relationship of one or more licenses to a software object. More than one applicable license can be expressed as "license-key-a AND license-key-b". A choice of applicable licenses can be expressed as "license-key-a OR license-key-b", and you can indicate the primary (preferred) license by placing it first, on the left-hand side of the OR relationship. The relationship words (OR, AND) can be combined as needed, and the use of parentheses can be applied to clarify the meaning; for example "((license-key-a AND license-key-b) OR (license-key-c))". An exception to a license can be expressed as "license-key WITH license-exception-key".', max_length=1024, verbose_name='Concluded license expression'),
),
migrations.AlterField(
model_name='package',
name='md5',
field=models.CharField(blank=True, help_text='MD5 checksum hex-encoded, as in md5sum.', max_length=32, verbose_name='MD5'),
),
migrations.AlterField(
model_name='package',
name='primary_language',
field=models.CharField(blank=True, help_text='The primary programming language associated with the package.', max_length=50),
),
migrations.AlterField(
model_name='package',
name='project',
field=models.CharField(blank=True, help_text='Project is a free-form label that you can use to group and find packages and components that interest you; for example, you may be starting a new development project, evaluating them for use in a product or you may want to get approval to use them.', max_length=50),
),
migrations.AlterField(
model_name='package',
name='sha1',
field=models.CharField(blank=True, help_text='SHA1 checksum hex-encoded, as in sha1sum.', max_length=40, verbose_name='SHA1'),
),
migrations.AlterField(
model_name='package',
name='size',
field=models.BigIntegerField(blank=True, help_text='The size of the package file in bytes.', null=True),
),
migrations.CreateModel(
name='Vulnerability',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('uuid', models.UUIDField(default=uuid.uuid4, editable=False, verbose_name='UUID')),
('created_date', models.DateTimeField(auto_now_add=True, db_index=True, help_text='The date and time the object was created.')),
('last_modified_date', models.DateTimeField(auto_now=True, db_index=True, help_text='The date and time the object was last modified.')),
('vulnerability_id', models.CharField(help_text="A unique identifier for the vulnerability, prefixed with 'VCID-'. For example, 'VCID-2024-0001'.", max_length=20)),
('summary', models.TextField(blank=True, help_text='A brief summary of the vulnerability, outlining its nature and impact.')),
('aliases', dje.fields.JSONListField(blank=True, default=list, help_text="A list of aliases for this vulnerability, such as CVE identifiers (e.g., 'CVE-2017-1000136').")),
('references', dje.fields.JSONListField(blank=True, default=list, help_text='A list of references for this vulnerability. Each reference includes a URL, an optional reference ID, scores, and the URL for further details. ')),
('fixed_packages', dje.fields.JSONListField(blank=True, default=list, help_text='A list of packages that are not affected by this vulnerability.')),
('dataspace', models.ForeignKey(editable=False, help_text='A Dataspace is an independent, exclusive set of DejaCode data, which can be either nexB master reference data or installation-specific data.', on_delete=django.db.models.deletion.PROTECT, to='dje.dataspace')),
],
options={
'verbose_name_plural': 'Vulnerabilities',
},
),
migrations.AddField(
model_name='component',
name='affected_by_vulnerabilities',
field=models.ManyToManyField(help_text='Vulnerabilities affecting this object.', related_name='affected_%(class)ss', to='component_catalog.vulnerability'),
),
migrations.AddField(
model_name='package',
name='affected_by_vulnerabilities',
field=models.ManyToManyField(help_text='Vulnerabilities affecting this object.', related_name='affected_%(class)ss', to='component_catalog.vulnerability'),
),
migrations.AddIndex(
model_name='package',
index=models.Index(fields=['type'], name='component_c_type_4af44a_idx'),
),
migrations.AddIndex(
model_name='package',
index=models.Index(fields=['namespace'], name='component_c_namespa_d10c6b_idx'),
),
migrations.AddIndex(
model_name='package',
index=models.Index(fields=['name'], name='component_c_name_c82010_idx'),
),
migrations.AddIndex(
model_name='package',
index=models.Index(fields=['version'], name='component_c_version_ad217d_idx'),
),
migrations.AddIndex(
model_name='package',
index=models.Index(fields=['filename'], name='component_c_filenam_b60780_idx'),
),
migrations.AddIndex(
model_name='package',
index=models.Index(fields=['size'], name='component_c_size_22e128_idx'),
),
migrations.AddIndex(
model_name='package',
index=models.Index(fields=['primary_language'], name='component_c_primary_2e8211_idx'),
),
migrations.AddIndex(
model_name='package',
index=models.Index(fields=['project'], name='component_c_project_74b1c0_idx'),
),
migrations.AddIndex(
model_name='package',
index=models.Index(fields=['license_expression'], name='component_c_license_20719b_idx'),
),
migrations.AddIndex(
model_name='vulnerability',
index=models.Index(fields=['vulnerability_id'], name='component_c_vulnera_e18d50_idx'),
),
migrations.AlterUniqueTogether(
name='vulnerability',
unique_together={('dataspace', 'uuid'), ('dataspace', 'vulnerability_id')},
),
]
Loading