Skip to content

Commit

Permalink
Add support for Component with the new Vulnerability model #138
Browse files Browse the repository at this point in the history
Signed-off-by: tdruez <[email protected]>
  • Loading branch information
tdruez committed Aug 5, 2024
1 parent a352e00 commit b947380
Show file tree
Hide file tree
Showing 5 changed files with 68 additions and 23 deletions.
10 changes: 10 additions & 0 deletions component_catalog/filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,16 @@ class ComponentFilterSet(DataspacedFilterSet):
search_placeholder="Search keywords",
),
)
# TODO: Remove duplication with Package
is_vulnerable = HasRelationFilter(
label=_("Is Vulnerable"),
field_name="affected_by_vulnerabilities",
choices=(
("yes", _("Affected by vulnerabilities")),
("no", _("No vulnerabilities found")),
),
widget=DropDownRightWidget,
)

class Meta:
model = Component
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Generated by Django 5.0.6 on 2024-08-05 07:34

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('component_catalog', '0006_vulnerability'),
]

operations = [
migrations.AddField(
model_name='vulnerability',
name='affected_components',
field=models.ManyToManyField(help_text='Components affected by this vulnerability.', related_name='affected_by_vulnerabilities', to='component_catalog.component'),
),
migrations.AlterField(
model_name='vulnerability',
name='affected_packages',
field=models.ManyToManyField(help_text='Packages affected by this vulnerability.', related_name='affected_by_vulnerabilities', to='component_catalog.package'),
),
]
49 changes: 30 additions & 19 deletions component_catalog/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,18 @@ def validate_filename(value):
)


class VulnerabilityQuerySetMixin:
def with_vulnerability_count(self):
"""Annotate the QuerySet with the vulnerability_count."""
return self.annotate(
vulnerability_count=models.Count("affected_by_vulnerabilities", distinct=True)
)

def with_vulnerabilties(self):
"""Return vulnerable Packages."""
return self.with_vulnerability_count().filter(vulnerability_count__gt=0)


class LicenseExpressionMixin:
"""Model mixin for models that store license expressions."""

Expand Down Expand Up @@ -852,7 +864,7 @@ def as_cyclonedx(self, license_expression_spdx=None):
BaseComponentMixin = component_mixin_factory("component")


class ComponentQuerySet(DataspacedQuerySet):
class ComponentQuerySet(VulnerabilityQuerySetMixin, DataspacedQuerySet):
def with_has_hierarchy(self):
subcomponents = Subcomponent.objects.filter(
models.Q(child_id=OuterRef("pk")) | models.Q(parent_id=OuterRef("pk"))
Expand Down Expand Up @@ -1622,21 +1634,11 @@ def __str__(self):
PACKAGE_URL_FIELDS = ["type", "namespace", "name", "version", "qualifiers", "subpath"]


class PackageQuerySet(PackageURLQuerySetMixin, DataspacedQuerySet):
class PackageQuerySet(PackageURLQuerySetMixin, VulnerabilityQuerySetMixin, DataspacedQuerySet):
def has_package_url(self):
"""Return objects with Package URL defined."""
return self.filter(~models.Q(type="") & ~models.Q(name=""))

def with_vulnerability_count(self):
"""Annotate the QuerySet with the vulnerability_count."""
return self.annotate(
vulnerability_count=models.Count("affected_by_vulnerabilities", distinct=True)
)

def with_vulnerabilties(self):
"""Return vulnerable Packages."""
return self.with_vulnerability_count().filter(vulnerability_count__gt=0)

def annotate_sortable_identifier(self):
"""
Annotate the QuerySet with a `sortable_identifier` value that combines
Expand Down Expand Up @@ -2528,37 +2530,37 @@ class Vulnerability(HistoryFieldsMixin, DataspacedModel):
"For example, 'VCID-2024-0001'."
),
)

summary = models.TextField(
help_text=_("A brief summary of the vulnerability, outlining its nature and impact."),
blank=True,
)

aliases = JSONListField(
blank=True,
help_text=_(
"A list of aliases for this vulnerability, such as CVE identifiers "
"(e.g., 'CVE-2017-1000136')."
),
)

references = JSONListField(
blank=True,
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 = JSONListField(
blank=True,
help_text=_("A list of packages that are not affected by this vulnerability."),
)

affected_packages = models.ManyToManyField(
to="component_catalog.Package",
related_name="affected_by_vulnerabilities",
help_text=_("Packages that are affected by this vulnerability."),
help_text=_("Packages affected by this vulnerability."),
)
affected_components = models.ManyToManyField(
to="component_catalog.Component",
related_name="affected_by_vulnerabilities",
help_text=_("Components affected by this vulnerability."),
)

class Meta:
Expand All @@ -2576,11 +2578,20 @@ def add_affected_packages(self, packages):
"""Assign the ``packages`` as affected to this vulnerability."""
self.affected_packages.add(*packages)

def add_affected_components(self, components):
"""Assign the ``components`` as affected to this vulnerability."""
self.affected_components.add(*components)

@classmethod
def create_from_data(cls, user, data, validate=False, affected_packages=None):
def create_from_data(
cls, user, data, validate=False, affected_packages=None, affected_components=None
):
vulnerability = super().create_from_data(user, data, validate=validate)

if affected_packages:
vulnerability.add_affected_packages(affected_packages)

if affected_components:
vulnerability.add_affected_component(affected_components)

return vulnerability
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
</td>
{% endif %}
<td>
{% if object.has_hierarchy or object.request_count or object.cpe %}
{% if object.has_hierarchy or object.request_count or object.vulnerability_count %}
<ul class="list-inline float-end mb-0">
{% if object.has_hierarchy %}
<li class="list-inline-item">
Expand All @@ -37,9 +37,9 @@
<a href="{% inject_preserved_filters object.get_absolute_url %}#activity" class="r-link"><span class="badge text-bg-request">R</span></a>
</li>
{% endif %}
{% if object.cpe in vulnerable_cpes %}
{% if object.vulnerability_count %}
<li class="list-inline-item">
{% include 'component_catalog/includes/vulnerability_icon_link.html' %}
{% include 'component_catalog/includes/vulnerability_icon_link.html' with count=object.vulnerability_count %}
</li>
{% endif %}
</ul>
Expand Down
3 changes: 2 additions & 1 deletion component_catalog/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -408,7 +408,7 @@ class ComponentListView(
group_name_version = True

table_headers = (
Header("name", _("Component name")),
Header("name", _("Component name"), filter="is_vulnerable"),
Header("version", _("Version")),
Header("usage_policy", _("Policy"), filter="usage_policy", condition=include_policy),
Header("license_expression", _("Concluded license"), filter="licenses"),
Expand Down Expand Up @@ -448,6 +448,7 @@ def get_queryset(self):
"licenses__usage_policy",
)
.with_has_hierarchy()
.with_vulnerability_count()
.order_by(
"-last_modified_date",
)
Expand Down

0 comments on commit b947380

Please sign in to comment.