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 ProductVulnerabilityAnalysis model implementation #98 #187

Merged
merged 35 commits into from
Dec 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
34f3905
Add ProductVulnerabilityAnalysis model implementation #98
tdruez Oct 24, 2024
af916f9
Add a new Exploitability analysis column #98
tdruez Oct 30, 2024
104e33c
Merge branch 'main' into 98-vulnerability-exploitability
tdruez Oct 31, 2024
3c883d9
Merge main and remake migration #98
tdruez Oct 31, 2024
02e67c5
Move the aliases in the first column #98
tdruez Nov 1, 2024
c14c470
Add prototype to add/edit the Vulnerability analysis #98
tdruez Nov 6, 2024
50e5e7d
Include the VulnerabilityAnalysis data in exported VEX #98
tdruez Nov 7, 2024
b2b6f91
Refactor the VulnerabilityAnalysis for option product context #98
tdruez Nov 7, 2024
d37b450
Add support for VulnerabilityAnalysis update #98
tdruez Nov 7, 2024
e3ec773
Move the Edit action to its own column #98
tdruez Nov 7, 2024
287b97c
Add a filter by vulnerability analyses state #98
tdruez Nov 7, 2024
d60edd1
Merge main and fix conflicts #98
tdruez Nov 20, 2024
3510780
Fix template comment #98
tdruez Nov 20, 2024
b3875a3
Rework and re-arrange the Vulnerability tab #98
tdruez Nov 22, 2024
2c526a3
Make sure get_vulnerability_qs return distinct results #98
tdruez Nov 22, 2024
606fa3a
Refine filters and sorting for the Vulnerabilities tab #98
tdruez Nov 25, 2024
bbd769c
Add a generic dropdown_fields to DataspacedFilterSet #98
tdruez Nov 25, 2024
3135c69
Display VCID in modal title #98
tdruez Nov 25, 2024
860551d
Relate the Vulnerability analysis to a ProductPackage #98
tdruez Nov 25, 2024
05ee85f
Add the ability to filter by null values #98
tdruez Nov 25, 2024
9db7348
Multiple adjustments #98
tdruez Nov 27, 2024
2238cbb
Consolidate migrations #98
tdruez Nov 27, 2024
0f5d05c
Remove help_text duplication #98
tdruez Nov 27, 2024
56e79b7
Properly handle the affected_packages_count on the QS #98
tdruez Nov 27, 2024
4a773d4
Refine the vulnerability_analyses QuerySet in the tab #98
tdruez Nov 29, 2024
b50365f
Fix the issue with the UserWarning csrf token #98
tdruez Nov 29, 2024
1fe4c2c
Add a clean method for form validation #98
tdruez Nov 29, 2024
146c073
Add changelog entry #98
tdruez Nov 29, 2024
88cc98a
Refine the CDX methods implementation and add tests #98
tdruez Dec 2, 2024
a09656d
Add test for the get_cyclonedx_bom_include_vex analysis #98
tdruez Dec 2, 2024
50c652a
Refine the UI for displaying the analysis detail value #98
tdruez Dec 2, 2024
6451aaf
Add unit tests for the Product details view #98
tdruez Dec 2, 2024
eb4f5e0
Add unit tests for analysis instances in tab view #98
tdruez Dec 2, 2024
571866b
Add a unit test for the queries triggerd by the tab_vulnerability #98
tdruez Dec 2, 2024
a43c1e9
Refine the vulnerability_analysis_form_view and add tests #98
tdruez Dec 2, 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
12 changes: 12 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,18 @@ Release notes
vulnerability data is available.
https://github.com/aboutcode-org/dejacode/issues/98

- Introduce a new VulnerabilityAnalysis model based on CycloneDX spec:
https://cyclonedx.org/docs/1.6/json/#vulnerabilities_items_analysis
A VulnerabilityAnalysis is always assigned to a Vulnerability object and a
ProductPackage relation.
The values for a VulnerabilityAnalysis are display in the Product "Vulnerabilities"
tab.
A "Edit" button can be used to open a form in a model to provided analysis data.
Those new VEX related columns can be sorted and filtered.
The VulnerabilityAnalysis data is exported in the VEX (only) and SBOM+VEX (combined)
outputs.
https://github.com/aboutcode-org/dejacode/issues/98

### Version 5.2.1

- Fix the models documentation navigation.
Expand Down
8 changes: 5 additions & 3 deletions component_catalog/filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,10 @@ class ComponentFilterSet(DataspacedFilterSet):
"primary_language",
"usage_policy",
]
dropdown_fields = [
"type",
"usage_policy",
]
q = MatchOrderedSearchFilter(
label=_("Search"),
match_order_fields=["name"],
Expand Down Expand Up @@ -122,9 +126,7 @@ class Meta:
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.filters["usage_policy"].extra["to_field_name"] = "label"
self.filters["usage_policy"].extra["widget"] = DropDownRightWidget()
self.filters["type"].extra["to_field_name"] = "label"
self.filters["type"].extra["widget"] = DropDownRightWidget()

@cached_property
def sort_value(self):
Expand Down Expand Up @@ -180,6 +182,7 @@ def filter(self, qs, value):


class PackageFilterSet(DataspacedFilterSet):
dropdown_fields = ["usage_policy"]
q = PackageSearchFilter(
label=_("Search"),
match_order_fields=[
Expand Down Expand Up @@ -262,7 +265,6 @@ class Meta:
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.filters["usage_policy"].extra["to_field_name"] = "label"
self.filters["usage_policy"].extra["widget"] = DropDownRightWidget()

@cached_property
def sort_value(self):
Expand Down
20 changes: 17 additions & 3 deletions dejacode/static/css/dejacode_bootstrap.css
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ table.text-break thead {
.bg-warning-orange {
background-color: var(--bs-orange);
}

/* -- Dark there fixes -- */
[data-bs-theme=dark] .btn-outline-dark {
--bs-btn-color: var(--bs-tertiary-color);
Expand Down Expand Up @@ -390,17 +391,26 @@ table.vulnerabilities-table .column-summary {
min-width: 300px;
}
#tab_vulnerabilities .column-exploitability {
width: 150px;
width: 155px;
}
#tab_vulnerabilities .column-weighted_severity {
width: 115px;
width: 120px;
}
#tab_vulnerabilities .column-risk_score {
width: 95px;
width: 90px;
}
#tab_vulnerabilities .column-summary {
width: 300px;
}
#tab_vulnerabilities .column-vulnerability_analyses__state {
min-width: 105px;
}
#tab_vulnerabilities .column-vulnerability_analyses__justification {
min-width: 130px;
}
#tab_vulnerabilities .column-vulnerability_analyses__responses {
min-width: 120px;
}

/* -- Dependency tab -- */
#tab_dependencies .column-for_package {
Expand Down Expand Up @@ -540,6 +550,10 @@ table.purldb-table .column-license_expression {
vertical-align: middle;
}

#vulnerability-analysis-form fieldset legend {
font-size: 1rem;
}

/* -- Object form (add/edit) -- */
.datepicker {
width: 130px;
Expand Down
6 changes: 6 additions & 0 deletions dje/filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
from dje.utils import extract_name_version
from dje.utils import get_uuids_list_sorted
from dje.utils import remove_field_from_query_dict
from dje.widgets import DropDownRightWidget

IS_FILTER_LOOKUP_VAR = "_filter_lookup"

Expand Down Expand Up @@ -80,6 +81,7 @@ def get_filters_breadcrumbs(self):

class DataspacedFilterSet(FilterSetUtilsMixin, django_filters.FilterSet):
related_only = []
dropdown_fields = []

def __init__(self, *args, **kwargs):
try:
Expand All @@ -90,6 +92,7 @@ def __init__(self, *args, **kwargs):
self.dynamic_qs = kwargs.pop("dynamic_qs", True)
self.parent_qs_cache = {}
self.anchor = kwargs.pop("anchor", None)
self.dropdown_fields = kwargs.pop("dropdown_fields", []) or self.dropdown_fields

super().__init__(*args, **kwargs)

Expand All @@ -106,6 +109,9 @@ def __init__(self, *args, **kwargs):
model_name = self._meta.model._meta.model_name
usage_policy.queryset = usage_policy.queryset.filter(content_type__model=model_name)

for field_name in self.dropdown_fields:
self.filters[field_name].extra["widget"] = DropDownRightWidget(anchor=self.anchor)

def apply_related_only(self, field_name, filter_):
"""
Limit the filter choices to the values used on the parent queryset.
Expand Down
18 changes: 14 additions & 4 deletions dje/outputs.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,10 +132,20 @@ def get_cyclonedx_bom(instance, user, include_components=True, include_vex=False

if include_vex:
vulnerability_qs = instance.get_vulnerability_qs(prefetch_related_packages=True)
vulnerabilities = [
vulnerability.as_cyclonedx(affected_instances=vulnerability.affected_packages.all())
for vulnerability in vulnerability_qs
]
vulnerabilities = []
for vulnerability in vulnerability_qs:
analysis = None
vulnerability_analyses = vulnerability.vulnerability_analyses.all()
if len(vulnerability_analyses) == 1:
analysis = vulnerability_analyses[0]

vulnerabilities.append(
vulnerability.as_cyclonedx(
affected_instances=vulnerability.affected_packages.all(),
analysis=analysis,
)
)

bom.vulnerabilities = vulnerabilities

return bom
Expand Down
3 changes: 3 additions & 0 deletions dje/templates/includes/object_list_table_header.html
Original file line number Diff line number Diff line change
Expand Up @@ -36,5 +36,8 @@
{% endif %}
</th>
{% endfor %}
{% if include_actions %}
<th class="column-action" scope="col"></th>
{% endif %}
</tr>
</thead>
23 changes: 22 additions & 1 deletion dje/tests/test_outputs.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
# See https://aboutcode.org for more information about AboutCode FOSS projects.
#

import json

from django.test import TestCase

from cyclonedx.model import bom as cyclonedx_bom
Expand All @@ -18,6 +20,7 @@
from dje.tests import create_user
from product_portfolio.models import Product
from product_portfolio.tests import make_product_package
from vulnerabilities.models import VulnerabilityAnalysis
from vulnerabilities.tests import make_vulnerability


Expand Down Expand Up @@ -97,7 +100,7 @@ def test_outputs_get_cyclonedx_bom(self):

def test_outputs_get_cyclonedx_bom_include_vex(self):
package_in_product = make_package(self.dataspace, package_url="pkg:type/name")
make_product_package(self.product1, package_in_product)
product_package1 = make_product_package(self.product1, package_in_product)
package_not_in_product = make_package(self.dataspace)
vulnerability1 = make_vulnerability(
self.dataspace, affecting=[package_in_product, package_not_in_product]
Expand All @@ -112,6 +115,24 @@ def test_outputs_get_cyclonedx_bom_include_vex(self):
self.assertIsInstance(bom, cyclonedx_bom.Bom)
self.assertEqual(1, len(bom.vulnerabilities))
self.assertEqual(vulnerability1.vulnerability_id, bom.vulnerabilities[0].id)
self.assertIsNone(bom.vulnerabilities[0].analysis)

VulnerabilityAnalysis.objects.create(
product_package=product_package1,
vulnerability=vulnerability1,
state=VulnerabilityAnalysis.State.RESOLVED,
justification=VulnerabilityAnalysis.Justification.CODE_NOT_PRESENT,
detail="detail",
dataspace=self.dataspace,
)
bom = outputs.get_cyclonedx_bom(
instance=self.product1,
user=self.super_user,
include_vex=True,
)
analysis = bom.vulnerabilities[0].analysis
expected = {"detail": "detail", "justification": "code_not_present", "state": "resolved"}
self.assertEqual(expected, json.loads(analysis.as_json()))

def test_outputs_get_cyclonedx_bom_json(self):
bom = outputs.get_cyclonedx_bom(instance=self.product1, user=self.super_user)
Expand Down
7 changes: 4 additions & 3 deletions license_library/filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ class LicenseFilterSet(DataspacedFilterSet):
"license_profile",
"usage_policy",
]
dropdown_fields = [
"category__license_type",
"usage_policy",
]
q = MatchOrderedSearchFilter(
label=_("Search"),
match_order_fields=["short_name", "key", "name"],
Expand Down Expand Up @@ -101,6 +105,3 @@ def __init__(self, *args, **kwargs):
self.filters["usage_policy"].extra["to_field_name"] = "label"
self.filters["usage_policy"].label = _("Policy")
self.filters["category__license_type"].label = _("Type")

for filter_name in ["category__license_type", "usage_policy"]:
self.filters[filter_name].extra["widget"] = DropDownRightWidget()
19 changes: 16 additions & 3 deletions product_portfolio/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
from dje.validators import validate_version
from vulnerabilities.fetch import fetch_for_packages
from vulnerabilities.models import Vulnerability
from vulnerabilities.models import VulnerabilityAnalysis

RELATION_LICENSE_EXPRESSION_HELP_TEXT = _(
"The License Expression assigned to a DejaCode Product Package or Product "
Expand Down Expand Up @@ -335,6 +336,10 @@ def all_packages(self):
models.Q(id__in=self.packages.all()) | models.Q(component__in=self.components.all())
).distinct()

@cached_property
def vulnerability_count(self):
return self.get_vulnerability_qs().count()

def get_merged_descendant_ids(self):
"""
Return a list of Component ids collected on the Product descendants:
Expand Down Expand Up @@ -514,13 +519,21 @@ def fetch_vulnerabilities(self):

def get_vulnerability_qs(self, prefetch_related_packages=False):
"""Return a QuerySet of all Vulnerability instances related to this product"""
qs = Vulnerability.objects.filter(affected_packages__in=self.packages.all())
vulnerability_qs = Vulnerability.objects.filter(
affected_packages__in=self.packages.all()
).distinct()

if prefetch_related_packages:
package_qs = Package.objects.filter(product=self).only_rendering_fields()
qs = qs.prefetch_related(models.Prefetch("affected_packages", package_qs))
analysis_qs = VulnerabilityAnalysis.objects.filter(product=self).select_related(
"package"
)
vulnerability_qs = vulnerability_qs.prefetch_related(
models.Prefetch("affected_packages", package_qs),
models.Prefetch("vulnerability_analyses", analysis_qs),
)

return qs
return vulnerability_qs


class ProductRelationStatus(BaseStatusMixin, DataspacedModel):
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
{% load crispy_forms_tags %}
<div id="vulnerability-analysis-modal" class="modal modal-lg" tabindex="-1" role="dialog">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<div class="modal-title">
<h5>
Vulnerability analysis:
<strong id="analysis-vulnerability-id"></strong>
</h5>
<div>
Package: <strong id="analysis-package-identifier"></strong>
</div>
</div>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<form autocomplete="off" method="post" id="vulnerability-analysis-form">
<div class="modal-body bg-body-tertiary" id="vulnerability-analysis-modal-body">
</div>
<div class="modal-footer">
<input type="button" name="close" value="Close" class="btn btn-secondary" data-bs-dismiss="modal">
<input type="submit" id="submit-vulnerability-analysis-form" value="Submit" class="btn btn-primary btn-success">
</div>
</form>
</div>
</div>
</div>
60 changes: 60 additions & 0 deletions product_portfolio/templates/product_portfolio/product_details.html
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,9 @@
{% if tabsets.Imports %}
{% include 'product_portfolio/includes/scancode_project_status_modal.html' %}
{% endif %}
{% if request.user.dataspace.enable_vulnerablecodedb_access and product.vulnerability_count %}
{% include 'product_portfolio/modals/vulnerability_analysis_modal.html' %}
{% endif %}
{% endblock %}

{% block extrastyle %}
Expand Down Expand Up @@ -232,6 +235,63 @@
</script>
{% endif %}

{% if request.user.dataspace.enable_vulnerablecodedb_access and product.vulnerability_count %}
<script>
$(document).ready(function () {
let vulnerability_modal = $('#vulnerability-analysis-modal');
vulnerability_modal.on('show.bs.modal', function (event) {
let modal_body = $('#vulnerability-analysis-modal-body');
modal_body.html(''); // Reset the modal content

let button = $(event.relatedTarget); // Button that triggered the modal
// Extract info from data-* attributes
let edit_url = button.data('edit-url');
let vulnerability_id = button.data('vulnerability-id');
let package_identifier = button.data('package-identifier');

$('#submit-vulnerability-analysis-form').data('edit-url', edit_url);
$('#vulnerability-analysis-modal #analysis-vulnerability-id').text(vulnerability_id);
$('#vulnerability-analysis-modal #analysis-package-identifier').text(package_identifier);

$.ajax({
url: edit_url,
success: function(data) {
modal_body.html(data);
},
error: function() {
modal_body.html('Error.');
}
});
});

$('#submit-vulnerability-analysis-form').on('click', function(event){
event.preventDefault();
let modal_body = $('#vulnerability-analysis-modal-body');
let edit_url = $('#submit-vulnerability-analysis-form').data('edit-url');

$.ajax({
url: edit_url,
type: 'POST',
headers: {'X-CSRFToken': csrftoken},
data: $('#vulnerability-analysis-form').serialize(),
success: function(data) {
if (data['success']) {
location.reload();
return false;
}
modal_body.html(data);
edit_modal.animate({scrollTop: 0});
},
error: function(){
modal_body.html('Error.');
}
});
});

});
</script>
{% endif %}

{% if purldb_enabled %}
<script>
document.addEventListener('DOMContentLoaded', function () {
Expand Down
Loading