Skip to content

Commit cde4d4d

Browse files
authored
Add ability to select affected products for analysis data propagation (#201)
Signed-off-by: tdruez <[email protected]>
1 parent bc428e2 commit cde4d4d

File tree

15 files changed

+349
-45
lines changed

15 files changed

+349
-45
lines changed

CHANGELOG.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,12 @@ Release notes
2323
outputs.
2424
https://github.com/aboutcode-org/dejacode/issues/98
2525

26+
- Add the ability to propagate vulnerability analysis data to other affected products.
27+
A new "Propagate analysis to:" section in now displayed the "Vulnerability analysis"
28+
modal. The list of products containing the same package as the one currently being
29+
analysed are listed and can be selected for "analysis propagation".
30+
https://github.com/aboutcode-org/dejacode/issues/105
31+
2632
### Version 5.2.1
2733

2834
- Fix the models documentation navigation.

dejacode/static/css/dejacode_bootstrap.css

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -411,6 +411,14 @@ table.vulnerabilities-table .column-summary {
411411
#tab_vulnerabilities .column-vulnerability_analyses__responses {
412412
min-width: 120px;
413413
}
414+
/* -- Vulnerability analysis modal -- */
415+
#vulnerability-analysis-modal #div_id_responses .form-check {
416+
display: inline-block;
417+
margin-right: 1rem;
418+
}
419+
#vulnerability-analysis-modal .form-label {
420+
font-weight: 600;
421+
}
414422

415423
/* -- Dependency tab -- */
416424
#tab_dependencies .column-for_package {

dejacode/static/js/dejacode_main.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,10 @@ function setupPopovers() {
3131

3232
function setupSelectionCheckboxes() {
3333
const selectAllCheckbox = document.getElementById("checkbox-select-all");
34-
const rowCheckboxes = document.querySelectorAll("table#object-list-table tbody input[type='checkbox']");
34+
if (!selectAllCheckbox) return;
35+
36+
const parentTable = selectAllCheckbox.closest("table");
37+
const rowCheckboxes = parentTable.querySelectorAll("tbody input[type='checkbox']");
3538
let lastChecked; // Store the last checked checkbox
3639

3740
if (!rowCheckboxes) return;

dje/templates/bootstrap_base_js.html

Lines changed: 0 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -2,29 +2,4 @@
22
<script>
33
NEXB = {};
44
NEXB.client_data = JSON.parse(document.getElementById("client_data").textContent);
5-
6-
NEXB.displayOverlay = function(text) {
7-
const overlay = document.createElement('div');
8-
overlay.id = 'overlay';
9-
overlay.textContent = text;
10-
11-
Object.assign(overlay.style, {
12-
position: 'fixed',
13-
top: 0,
14-
left: 0,
15-
width: '100%',
16-
height: '100%',
17-
backgroundColor: 'rgba(0, 0, 0, .5)',
18-
zIndex: 10000,
19-
verticalAlign: 'middle',
20-
paddingTop: '300px',
21-
textAlign: 'center',
22-
color: '#fff',
23-
fontSize: '30px',
24-
fontWeight: 'bold',
25-
cursor: 'wait'
26-
});
27-
28-
document.body.appendChild(overlay);
29-
}
305
</script>

product_portfolio/templates/product_portfolio/modals/vulnerability_analysis_modal.html

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,19 @@
55
<div class="modal-header">
66
<div class="modal-title">
77
<h5>
8+
<i class="fas fa-bug vulnerability"></i>
89
Vulnerability analysis:
910
<strong id="analysis-vulnerability-id"></strong>
1011
</h5>
1112
<div>
12-
Package: <strong id="analysis-package-identifier"></strong>
13+
<i class="fa fa-archive"></i>
14+
<strong id="analysis-package-identifier"></strong>
1315
</div>
1416
</div>
1517
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
1618
</div>
1719
<form autocomplete="off" method="post" id="vulnerability-analysis-form">
18-
<div class="modal-body bg-body-tertiary" id="vulnerability-analysis-modal-body">
20+
<div class="modal-body bg-body-tertiary pb-0" id="vulnerability-analysis-modal-body">
1921
</div>
2022
<div class="modal-footer">
2123
<input type="button" name="close" value="Close" class="btn btn-secondary" data-bs-dismiss="modal">

product_portfolio/templates/product_portfolio/product_details.html

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,9 @@
257257
url: edit_url,
258258
success: function(data) {
259259
modal_body.html(data);
260+
setupTooltips();
261+
setupPopovers();
262+
setupSelectionCheckboxes();
260263
},
261264
error: function() {
262265
modal_body.html('Error.');
@@ -280,7 +283,7 @@
280283
return false;
281284
}
282285
modal_body.html(data);
283-
edit_modal.animate({scrollTop: 0});
286+
vulnerability_modal.animate({scrollTop: 0});
284287
},
285288
error: function(){
286289
modal_body.html('Error.');

product_portfolio/templates/product_portfolio/tabs/tab_vulnerabilities.html

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,11 @@
6565
</td>
6666
<td>
6767
{% if package.vulnerability_analysis.responses %}
68-
{{ package.vulnerability_analysis.responses|join:"<br>" }}
68+
<ul class="ps-3 m-0">
69+
{% for response in package.vulnerability_analysis.responses %}
70+
<li>{{ response }}</li>
71+
{% endfor %}
72+
</ul>
6973
{% endif %}
7074
</td>
7175
<td class="p-1">

product_portfolio/tests/test_views.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -416,7 +416,12 @@ def test_product_portfolio_tab_vulnerability_view_analysis_rendering(self):
416416
</span>
417417
</td>
418418
<td>Code Not Present</td>
419-
<td>can_not_fix<br>rollback</td>
419+
<td>
420+
<ul class="ps-3 m-0">
421+
<li>can_not_fix</li>
422+
<li>rollback</li>
423+
</ul>
424+
</td>
420425
"""
421426
self.assertContains(response, expected, html=True)
422427

product_portfolio/views.py

Lines changed: 42 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,9 @@
2525
from django.core.paginator import Paginator
2626
from django.db import transaction
2727
from django.db.models import Count
28-
from django.db.models import ObjectDoesNotExist
28+
from django.db.models import OuterRef
2929
from django.db.models import Prefetch
30+
from django.db.models import Subquery
3031
from django.db.models.functions import Lower
3132
from django.forms import modelformset_factory
3233
from django.http import Http404
@@ -2464,35 +2465,64 @@ def improve_packages_from_purldb_view(request, dataspace, name, version=""):
24642465
@login_required
24652466
def vulnerability_analysis_form_view(request, product_uuid, vulnerability_id, package_uuid):
24662467
user = request.user
2468+
dataspace = user.dataspace
24672469
form_class = VulnerabilityAnalysisForm
24682470
perms = "change_product"
24692471

2470-
qs = Product.objects.get_queryset(user, perms=perms)
2471-
product = get_object_or_404(qs, uuid=product_uuid)
2472-
vulnerability_qs = Vulnerability.objects.scope(user.dataspace)
2472+
product_qs = Product.objects.get_queryset(user, perms=perms)
2473+
product = get_object_or_404(product_qs, uuid=product_uuid)
2474+
vulnerability_qs = Vulnerability.objects.scope(dataspace)
24732475
vulnerability = get_object_or_404(vulnerability_qs, vulnerability_id=vulnerability_id)
24742476
product_package_qs = ProductPackage.objects.product_secured(user, perms=perms)
24752477
product_package = get_object_or_404(
24762478
product_package_qs, product=product, package__uuid=package_uuid
24772479
)
2480+
vulnerability_analysis_qs = VulnerabilityAnalysis.objects.scope(dataspace)
24782481

2479-
try:
2480-
vulnerability_analysis = VulnerabilityAnalysis.objects.scope(user.dataspace).get(
2481-
product_package=product_package,
2482-
vulnerability=vulnerability,
2482+
# Fetch the existing Analysis values for each affected products
2483+
product_analysis = vulnerability_analysis_qs.filter(
2484+
product=OuterRef("pk"),
2485+
package=OuterRef("packages__pk"),
2486+
vulnerability=OuterRef("packages__affected_by_vulnerabilities__pk"),
2487+
)
2488+
affected_products = (
2489+
product_qs.exclude(pk=product.pk)
2490+
.filter(
2491+
packages__uuid=package_uuid,
2492+
packages__affected_by_vulnerabilities=vulnerability,
2493+
)
2494+
.annotate(
2495+
analysis_state=Subquery(product_analysis.values("state")[:1]),
2496+
analysis_justification=Subquery(product_analysis.values("justification")[:1]),
2497+
analysis_responses=Subquery(product_analysis.values("responses")[:1]),
2498+
analysis_detail=Subquery(product_analysis.values("detail")[:1]),
24832499
)
2484-
except ObjectDoesNotExist:
2485-
vulnerability_analysis = None # Addition
2500+
)
2501+
2502+
vulnerability_analysis = vulnerability_analysis_qs.get_or_none(
2503+
product_package=product_package,
2504+
vulnerability=vulnerability,
2505+
)
24862506

24872507
if request.method == "POST":
2488-
form = form_class(user, instance=vulnerability_analysis, data=request.POST)
2508+
form = form_class(
2509+
user,
2510+
instance=vulnerability_analysis,
2511+
data=request.POST,
2512+
affected_products=affected_products,
2513+
)
24892514
if form.is_valid():
24902515
form.save()
24912516
messages.success(request, "Vulnerability analysis successfully updated.")
24922517
return JsonResponse({"success": "updated"}, status=200)
24932518
else:
24942519
initial = {"product_package": product_package, "vulnerability": vulnerability}
2495-
form = form_class(user, instance=vulnerability_analysis, initial=initial)
2520+
form = form_class(
2521+
user,
2522+
instance=vulnerability_analysis,
2523+
initial=initial,
2524+
affected_products=affected_products,
2525+
)
24962526

24972527
rendered_form = render_crispy_form(form, context=csrf(request))
24982528

vulnerabilities/forms.py

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,12 @@
1010
from django.core.exceptions import ValidationError
1111

1212
from crispy_forms.helper import FormHelper
13+
from crispy_forms.layout import Field
14+
from crispy_forms.layout import Fieldset
15+
from crispy_forms.layout import Layout
1316

1417
from dje.forms import DataspacedModelForm
18+
from dje.forms import Group
1519
from product_portfolio.models import ProductPackage
1620
from vulnerabilities.models import VulnerabilityAnalysis
1721

@@ -22,6 +26,15 @@ class VulnerabilityAnalysisForm(DataspacedModelForm):
2226
widget=forms.CheckboxSelectMultiple,
2327
required=False,
2428
)
29+
propagate_to_products = forms.MultipleChoiceField(
30+
label="Propagate analysis to:",
31+
widget=forms.CheckboxSelectMultiple,
32+
required=False,
33+
help_text=(
34+
"The listed products share the same vulnerable package. "
35+
"The analysis values will be applied to all selected products."
36+
),
37+
)
2538

2639
class Meta:
2740
model = VulnerabilityAnalysis
@@ -32,16 +45,26 @@ class Meta:
3245
"justification",
3346
"responses",
3447
"detail",
48+
"propagate_to_products",
3549
]
3650
widgets = {
3751
"product_package": forms.widgets.HiddenInput,
3852
"vulnerability": forms.widgets.HiddenInput,
39-
"detail": forms.Textarea(attrs={"rows": 3}),
53+
"detail": forms.Textarea(attrs={"rows": 2}),
4054
}
4155

4256
def __init__(self, user, *args, **kwargs):
57+
affected_products = kwargs.pop("affected_products", [])
4358
super().__init__(user, *args, **kwargs)
4459

60+
propagate_to_field = self.fields["propagate_to_products"]
61+
# Note: the whole `product` object is set as the label to be fully used in the
62+
# template rendering.
63+
propagate_to_products_choices = [(product.uuid, product) for product in affected_products]
64+
propagate_to_field.choices = propagate_to_products_choices
65+
if not propagate_to_products_choices:
66+
propagate_to_field.widget = forms.widgets.HiddenInput()
67+
4568
responses_model_field = self._meta.model._meta.get_field("responses")
4669
self.fields["responses"].help_text = responses_model_field.help_text
4770

@@ -53,12 +76,34 @@ def __init__(self, user, *args, **kwargs):
5376
def helper(self):
5477
helper = FormHelper()
5578
helper.form_method = "post"
56-
helper.form_id = "product-vulnerability-analysis-form"
5779
helper.form_tag = False
5880
helper.modal_title = "Vulnerability analysis"
5981
helper.modal_id = "vulnerability-analysis-modal"
82+
helper.layout = Layout(
83+
Fieldset(
84+
"",
85+
"product_package",
86+
"vulnerability",
87+
Group("state", "justification"),
88+
"responses",
89+
"detail",
90+
Field(
91+
"propagate_to_products",
92+
template="vulnerabilities/forms/widgets/propagate_to_table.html",
93+
),
94+
),
95+
)
6096
return helper
6197

98+
def save(self, *args, **kwargs):
99+
instance = super().save(*args, **kwargs)
100+
101+
if products := self.cleaned_data.get("propagate_to_products"):
102+
for product_uuid in products:
103+
instance.propagate(product_uuid, self.user)
104+
105+
return instance
106+
62107
def clean(self):
63108
main_fields = ["state", "justification", "responses", "detail"]
64109
if not any(self.cleaned_data.get(field_name) for field_name in main_fields):

0 commit comments

Comments
 (0)