Skip to content

Commit 5a2a09c

Browse files
committed
add ability to set a analysis preset
Signed-off-by: tdruez <tdruez@aboutcode.org>
1 parent 97bb661 commit 5a2a09c

4 files changed

Lines changed: 82 additions & 1 deletion

File tree

product_portfolio/templates/product_portfolio/product_details.html

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -384,6 +384,21 @@
384384
});
385385
});
386386

387+
document.addEventListener('click', function(event) {
388+
const btn = event.target.closest('.quick-preset-btn');
389+
if (!btn) return;
390+
fetch(btn.dataset.applyUrl, {
391+
method: 'POST',
392+
headers: {'X-CSRFToken': csrftoken},
393+
})
394+
.then(response => response.json())
395+
.then(data => {
396+
if (data.success) location.reload();
397+
else alert(data.error || 'Error applying preset.');
398+
})
399+
.catch(() => alert('Error applying preset.'));
400+
});
401+
387402
});
388403
</script>
389404
{% endif %}

product_portfolio/templates/product_portfolio/tabs/tab_packages_vulnerabilities.html

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@
134134
<i class="fa-solid fa-bug-slash" data-bs-toggle="tooltip" title="Vulnerability is NOT reachable"></i>
135135
{% endif %}
136136
</td>
137-
<td class="p-1">
137+
<td class="p-1 text-nowrap">
138138
<span data-bs-toggle="modal"
139139
data-bs-target="#vulnerability-analysis-modal"
140140
data-advisory-uid="{{ vulnerability.advisory_uid }}"
@@ -145,6 +145,24 @@
145145
<i class="far fa-edit fa-sm"></i>
146146
</button>
147147
</span>
148+
{% if analysis_presets and not vulnerability.vulnerability_analysis %}
149+
<div class="dropdown d-inline">
150+
<button type="button" class="btn btn-link p-0" data-bs-toggle="dropdown" aria-label="Quick action" title="Apply preset">
151+
<i class="fas fa-bolt fa-sm text-warning"></i>
152+
</button>
153+
<ul class="dropdown-menu dropdown-menu-end">
154+
<li><h6 class="dropdown-header small">Apply preset</h6></li>
155+
{% for preset in analysis_presets %}
156+
<li>
157+
<button type="button" class="dropdown-item small quick-preset-btn"
158+
data-apply-url="{% url 'product_portfolio:apply_analysis_preset' product_package.uuid preset.pk vulnerability.advisory_uid %}">
159+
{{ preset.name }}
160+
</button>
161+
</li>
162+
{% endfor %}
163+
</ul>
164+
</div>
165+
{% endif %}
148166
</td>
149167
{% if not forloop.first %}</tr>{% endif %}
150168
{% endfor %}

product_portfolio/urls.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
from product_portfolio.views import ProductUpdateView
4040
from product_portfolio.views import PullProjectDataFromScanCodeIOView
4141
from product_portfolio.views import add_customcomponent_ajax_view
42+
from product_portfolio.views import apply_analysis_preset_view
4243
from product_portfolio.views import check_package_version_ajax_view
4344
from product_portfolio.views import delete_scan_htmx_view
4445
from product_portfolio.views import edit_productrelation_ajax_view
@@ -118,6 +119,11 @@ def product_path(path_segment, view):
118119
edit_productrelation_ajax_view,
119120
name="edit_productrelation_ajax",
120121
),
122+
path(
123+
"vulnerability_analysis/<uuid:productpackage_uuid>/preset/<int:preset_id>/<path:advisory_uid>/",
124+
apply_analysis_preset_view,
125+
name="apply_analysis_preset",
126+
),
121127
path(
122128
"vulnerability_analysis/<uuid:productpackage_uuid>/<path:advisory_uid>/",
123129
vulnerability_analysis_form_view,

product_portfolio/views.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,7 @@
151151
from vulnerabilities.models import VulnerabilityAnalysis
152152
from vulnerabilities.models import get_risk_level
153153
from vulnerabilities.triage.engine import evaluate_ruleset
154+
from vulnerabilities.triage.models import AnalysisPreset
154155
from vulnerabilities.triage.models import ProductTriageRuleset
155156
from vulnerabilities.triage.models import TriageAction
156157
from vulnerabilities.triage.models import TriageRecord
@@ -1426,6 +1427,7 @@ def get_context_data(self, **kwargs):
14261427
self.attach_vulnerability_analyses(page_obj)
14271428
self.attach_triage_data(product, page_obj)
14281429

1430+
analysis_presets = list(AnalysisPreset.objects.scope(product.dataspace))
14291431
context_data.update(
14301432
{
14311433
"filterset": self.filterset,
@@ -1434,6 +1436,7 @@ def get_context_data(self, **kwargs):
14341436
"search_query": self.request.GET.get("vulnerabilities-q", ""),
14351437
"risk_threshold": risk_threshold,
14361438
"has_triage_rulesets": self.has_triage_rulesets,
1439+
"analysis_presets": analysis_presets,
14371440
}
14381441
)
14391442

@@ -2934,6 +2937,45 @@ def vulnerability_analysis_form_view(request, productpackage_uuid, advisory_uid)
29342937
return HttpResponse(rendered_form)
29352938

29362939

2940+
@login_required
2941+
@require_POST
2942+
def apply_analysis_preset_view(request, productpackage_uuid, advisory_uid, preset_id):
2943+
user = request.user
2944+
dataspace = user.dataspace
2945+
2946+
product_package_qs = ProductPackage.objects.product_secured(user, perms="change_product")
2947+
product_package = get_object_or_404(product_package_qs, uuid=productpackage_uuid)
2948+
vulnerability = get_object_or_404(
2949+
Vulnerability.objects.scope(dataspace), advisory_uid=advisory_uid
2950+
)
2951+
preset = get_object_or_404(AnalysisPreset.objects.scope(dataspace), pk=preset_id)
2952+
2953+
existing = VulnerabilityAnalysis.objects.scope(dataspace).get_or_none(
2954+
product_package=product_package,
2955+
vulnerability=vulnerability,
2956+
)
2957+
if existing:
2958+
return JsonResponse(
2959+
{"error": "An analysis already exists for this vulnerability."}, status=400
2960+
)
2961+
2962+
analysis = VulnerabilityAnalysis(
2963+
product_package=product_package,
2964+
vulnerability=vulnerability,
2965+
dataspace=dataspace,
2966+
)
2967+
preset.apply_to_analysis(analysis)
2968+
2969+
content_fields = [analysis.state, analysis.justification, analysis.responses, analysis.detail]
2970+
if not any(content_fields):
2971+
return JsonResponse({"error": "This preset has no content fields to apply."}, status=400)
2972+
2973+
analysis.applied_by_preset = preset
2974+
analysis.save()
2975+
2976+
return JsonResponse({"success": "applied"}, status=200)
2977+
2978+
29372979
@login_required
29382980
@csrf_exempt
29392981
@require_http_methods(["DELETE"])

0 commit comments

Comments
 (0)