From 3e9d8c020a1f4ae3aed2e80e1cc31179c21307bc Mon Sep 17 00:00:00 2001 From: Keshav Priyadarshi Date: Wed, 27 May 2026 15:07:22 +0530 Subject: [PATCH 1/6] feat: store todo isssue details in JSONField Signed-off-by: Keshav Priyadarshi --- .../0133_alter_advisorytodov2_issue_detail.py | 20 +++++++++++++ vulnerabilities/models.py | 5 +++- .../test_compute_advisory_todo_v2.py | 28 +++++++------------ 3 files changed, 34 insertions(+), 19 deletions(-) create mode 100644 vulnerabilities/migrations/0133_alter_advisorytodov2_issue_detail.py diff --git a/vulnerabilities/migrations/0133_alter_advisorytodov2_issue_detail.py b/vulnerabilities/migrations/0133_alter_advisorytodov2_issue_detail.py new file mode 100644 index 000000000..f36f42a9b --- /dev/null +++ b/vulnerabilities/migrations/0133_alter_advisorytodov2_issue_detail.py @@ -0,0 +1,20 @@ +# Generated by Django 5.2.11 on 2026-05-27 08:41 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("vulnerabilities", "0132_migrate_advisoryv2_datasource_ids"), + ] + + operations = [ + migrations.AlterField( + model_name="advisorytodov2", + name="issue_detail", + field=models.JSONField( + blank=True, default=dict, help_text="Additional details about the issue." + ), + ), + ] diff --git a/vulnerabilities/models.py b/vulnerabilities/models.py index 69253f54b..511d6dd31 100644 --- a/vulnerabilities/models.py +++ b/vulnerabilities/models.py @@ -2572,8 +2572,9 @@ class AdvisoryToDoV2(models.Model): help_text="Select the issue that needs to be addressed from the available options.", ) - issue_detail = models.TextField( + issue_detail = models.JSONField( blank=True, + default=dict, help_text="Additional details about the issue.", ) @@ -3168,6 +3169,8 @@ class AdvisoryV2(models.Model): choices=AdvisoryStatusType.choices, default=AdvisoryStatusType.PUBLISHED ) + # Note: Fields and relations below are not part of original upstream advisory. + exploitability = models.DecimalField( null=True, blank=True, diff --git a/vulnerabilities/tests/pipelines/v2_improvers/test_compute_advisory_todo_v2.py b/vulnerabilities/tests/pipelines/v2_improvers/test_compute_advisory_todo_v2.py index a1bbe0465..c07dcf9d9 100644 --- a/vulnerabilities/tests/pipelines/v2_improvers/test_compute_advisory_todo_v2.py +++ b/vulnerabilities/tests/pipelines/v2_improvers/test_compute_advisory_todo_v2.py @@ -254,8 +254,8 @@ def test_advisory_todo_conflicting_fixed_affected(self): self.assertEqual(1, AdvisoryToDoV2.objects.count()) self.assertEqual("CONFLICTING_AFFECTED_AND_FIXED_BY_PACKAGES", todo.issue_type) self.assertIn( - '"conflict_checksum": "87d9e2627a8461fc5c068335d822af4aa0a40a8f265a92895c51d275d97ab0d6",', - todo.issue_detail, + "87d9e2627a8461fc5c068335d822af4aa0a40a8f265a92895c51d275d97ab0d6", + todo.issue_detail["conflict_checksum"], ) self.assertEqual(2, todo.advisories.count()) self.assertEqual(todo, adv.advisory_todos.first()) @@ -287,7 +287,7 @@ def test_todo_conflict_details_partial_curation(self): expected_partial_curation_advisory = { "advisory_id": "PLACEHOLDER_PARTIAL_CURATION_AVID", "aliases": ["CVE-000-000"], - "summary": "('test5/test_id_5', 'test6/test_id_6'): Test summary", + "summary": "[test5/test_id_5, test6/test_id_6]: Test summary", "affected_packages": [ { "package": { @@ -377,12 +377,10 @@ def test_todo_conflict_details_partial_curation(self): pipeline.execute() todo = AdvisoryToDoV2.objects.first() - issue_details = json.loads(todo.issue_detail) + issue_details = todo.issue_detail result_partial_curation = issue_details["partial_curation_advisory"] self.assertEqual(1, AdvisoryToDoV2.objects.count()) self.assertEqual("CONFLICTING_FIXED_BY_PACKAGES", todo.issue_type) - print(result_partial_curation) - # breakpoint() self.assertDictEqual(expected_partial_curation_advisory, result_partial_curation) def test_todo_conflict_details_partial_curation_unpaired_purl_and_conflicting_affected_and_fixed( @@ -391,7 +389,7 @@ def test_todo_conflict_details_partial_curation_unpaired_purl_and_conflicting_af expected_partial_curation_advisory = { "advisory_id": "PLACEHOLDER_PARTIAL_CURATION_AVID", "aliases": ["CVE-000-000"], - "summary": "('test1/test_id', 'test5/test_id_5'): Test summary", + "summary": "[test1/test_id, test5/test_id_5]: Test summary", "affected_packages": [ { "package": { @@ -439,19 +437,17 @@ def test_todo_conflict_details_partial_curation_unpaired_purl_and_conflicting_af pipeline.execute() todo = AdvisoryToDoV2.objects.first() - issue_details = json.loads(todo.issue_detail) + issue_details = todo.issue_detail result_partial_curation = issue_details["partial_curation_advisory"] self.assertEqual(1, AdvisoryToDoV2.objects.count()) self.assertEqual("CONFLICTING_AFFECTED_AND_FIXED_BY_PACKAGES", todo.issue_type) - print(result_partial_curation) - # breakpoint() self.assertDictEqual(expected_partial_curation_advisory, result_partial_curation) def test_todo_conflict_details_partial_curation_unpaired_purl_and_conflicting_fixed(self): expected_partial_curation_advisory = { "advisory_id": "PLACEHOLDER_PARTIAL_CURATION_AVID", "aliases": ["CVE-000-000"], - "summary": "('test1/test_id', 'test7/test_id_5'): Test summary", + "summary": "[test1/test_id, test7/test_id_5]: Test summary", "affected_packages": [ { "package": { @@ -513,19 +509,17 @@ def test_todo_conflict_details_partial_curation_unpaired_purl_and_conflicting_fi pipeline.execute() todo = AdvisoryToDoV2.objects.first() - issue_details = json.loads(todo.issue_detail) + issue_details = todo.issue_detail result_partial_curation = issue_details["partial_curation_advisory"] self.assertEqual(1, AdvisoryToDoV2.objects.count()) self.assertEqual("CONFLICTING_FIXED_BY_PACKAGES", todo.issue_type) - print(result_partial_curation) - # breakpoint() self.assertDictEqual(expected_partial_curation_advisory, result_partial_curation) def test_todo_conflict_details_partial_curation_unpaired_purl_and_conflicting_affected(self): expected_partial_curation_advisory = { "advisory_id": "PLACEHOLDER_PARTIAL_CURATION_AVID", "aliases": ["CVE-000-000"], - "summary": "('test1/test_id', 'test8/test_id_5'): Test summary", + "summary": "[test1/test_id, test8/test_id_5]: Test summary", "affected_packages": [ { "package": { @@ -587,10 +581,8 @@ def test_todo_conflict_details_partial_curation_unpaired_purl_and_conflicting_af pipeline.execute() todo = AdvisoryToDoV2.objects.first() - issue_details = json.loads(todo.issue_detail) + issue_details = todo.issue_detail result_partial_curation = issue_details["partial_curation_advisory"] self.assertEqual(1, AdvisoryToDoV2.objects.count()) self.assertEqual("CONFLICTING_AFFECTED_PACKAGES", todo.issue_type) - print(result_partial_curation) - # breakpoint() self.assertDictEqual(expected_partial_curation_advisory, result_partial_curation) From 4dc91d755f75430b0e16c8614541c6292b09244a Mon Sep 17 00:00:00 2001 From: Keshav Priyadarshi Date: Wed, 27 May 2026 15:14:28 +0530 Subject: [PATCH 2/6] feat: precompute curation items for the package curation dashboard Signed-off-by: Keshav Priyadarshi --- .../v2_improvers/compute_advisory_todo.py | 119 ++++++++++++++++-- 1 file changed, 107 insertions(+), 12 deletions(-) diff --git a/vulnerabilities/pipelines/v2_improvers/compute_advisory_todo.py b/vulnerabilities/pipelines/v2_improvers/compute_advisory_todo.py index e146cbe30..ab8e9dfb0 100644 --- a/vulnerabilities/pipelines/v2_improvers/compute_advisory_todo.py +++ b/vulnerabilities/pipelines/v2_improvers/compute_advisory_todo.py @@ -17,6 +17,7 @@ from django.db.models import Prefetch from django.utils import timezone from packageurl import PackageURL +from univers.version_range import RANGE_CLASS_BY_SCHEMES from vulnerabilities.importer import AdvisoryDataV2 from vulnerabilities.models import AdvisoryAlias @@ -281,7 +282,7 @@ def check_missing_summary( todo_to_create, advisory_relation_to_create, ): - alias = advisory.datasource_id.rsplit("/", 1)[-1] + alias = advisory.advisory_id.rsplit("/", 1)[-1] oldest_advisory_date = advisory.date_published or advisory.date_collected if not advisory.summary: todo = AdvisoryToDoV2( @@ -333,7 +334,7 @@ def check_missing_affected_and_fixed_by_packages( elif not has_fixed_package: issue_type = "MISSING_FIXED_BY_PACKAGE" - alias = advisory.datasource_id.rsplit("/", 1)[-1] + alias = advisory.advisory_id.rsplit("/", 1)[-1] oldest_advisory_date = advisory.date_published or advisory.date_collected if issue_type: todo = AdvisoryToDoV2( @@ -360,12 +361,12 @@ def compute_version_range_disagreement(adv_map): fixed_intersection = set.intersection(*fixed_sets) return { - "affected_union": affected_union, - "affected_intersection": affected_intersection, - "affected_disagreement": affected_union - affected_intersection, - "fixed_union": fixed_union, - "fixed_intersection": fixed_intersection, - "fixed_disagreement": fixed_union - fixed_intersection, + "affected_union": list(affected_union), + "affected_intersection": list(affected_intersection), + "affected_disagreement": list(affected_union - affected_intersection), + "fixed_union": list(fixed_union), + "fixed_intersection": list(fixed_intersection), + "fixed_disagreement": list(fixed_union - fixed_intersection), } @@ -417,6 +418,7 @@ def check_conflicting_affected_and_fixed_by_packages_for_alias( """ conflicting_package_details = {} + curation_items = [] has_conflicting_affected_packages = False has_conflicting_fixed_package = False conflicting_advisories = set() @@ -433,6 +435,9 @@ def check_conflicting_affected_and_fixed_by_packages_for_alias( conflicting_package_details[purl] = { "avids": list(adv_map.keys()), } + curation_items.append( + get_grouped_curation_advisories_for_dashboard_ui(purl, adv_map, result, advisories) + ) conflicting_advisories.update([advisories[avid] for avid in adv_map]) conflicting_package_details[purl].update(result) @@ -462,6 +467,7 @@ def check_conflicting_affected_and_fixed_by_packages_for_alias( "conflict_checksum": conflict_checksum, "conflict_details": conflicting_package_details, "partial_curation_advisory": partial_merged_advisory, + "curation_items": curation_items, } todo_id = advisories_checksum(conflicting_advisories) @@ -484,7 +490,7 @@ def check_conflicting_affected_and_fixed_by_packages_for_alias( todo = AdvisoryToDoV2( related_advisories_id=todo_id, issue_type=issue_type, - issue_detail=json.dumps(issue_detail, default=list), + issue_detail=issue_detail, alias=alias, advisories_count=conflicting_advisories_count, oldest_advisory_date=date_published or date_collected, @@ -495,6 +501,94 @@ def check_conflicting_affected_and_fixed_by_packages_for_alias( return conflicting_package_count, conflicting_advisories_count +def get_disagreement_message(fixed_disagreement, affected_disagreement): + messages = [] + + if affected_disagreement: + affected = ", ".join(affected_disagreement) + noun = "version" if len(affected_disagreement) == 1 else "versions" + verb = "is" if len(affected_disagreement) == 1 else "are" + + messages.append(f"Advisories do not agree whether {noun} {affected} {verb} affected.") + + if fixed_disagreement: + fixed = ", ".join(fixed_disagreement) + noun = "version" if len(fixed_disagreement) == 1 else "versions" + verb = "contains" if len(fixed_disagreement) == 1 else "contain" + + messages.append(f"Advisories do not agree whether {noun} {fixed} {verb} the fix.") + + return " ".join(messages) + + +def get_grouped_curation_advisories_for_dashboard_ui(purl, adv_map, conflict_detail, advisories): + """ + Return curation details for the PURL, grouping advisories with similar conflicts based on precedence. + """ + curation_item = { + "purl": purl, + "partial_curation": { + "affected": list(conflict_detail["affected_intersection"]), + "fixing": list(conflict_detail["fixed_intersection"]), + }, + "advisories": [], + } + + all_versions = conflict_detail["affected_union"] + conflict_detail["fixed_union"] + package_url = PackageURL.from_string(purl) + range_class = RANGE_CLASS_BY_SCHEMES[package_url.type] + version_class = range_class.version_class + sorted_versions = sorted([version_class(v) for v in all_versions]) + curation_item["all_versions"] = [str(v) for v in sorted_versions] + curation_item["conflict_reason"] = get_disagreement_message( + fixed_disagreement=conflict_detail["fixed_disagreement"], + affected_disagreement=conflict_detail["affected_disagreement"], + ) + advisory_by_conflict_range = defaultdict(list) + conflict_ranges = {} + for avid, packages in adv_map.items(): + conflict_checksum = sha256_digest( + canonical_value( + { + "affected": packages["affected"], + "fixed": packages["fixed"], + } + ) + ) + if conflict_checksum not in conflict_ranges: + conflict_ranges[conflict_checksum] = { + "affected": list(packages["affected"]), + "fixing": list(packages["fixed"]), + } + + advisory_item = {} + advisory_item["advisory_uid"] = avid + advisory_item["vers_ranges"] = [] + advisory = advisories[avid] + advisory_item["precedence"] = advisory.precedence + advisory_item["advisory_id"] = advisory.advisory_id + advisory_item["datasource_id"] = advisory.datasource_id + for impact in advisory.impacted_packages.all(): + if impact.base_purl != purl: + continue + advisory_item["vers_ranges"].append( + { + "affected_vers": impact.affecting_vers, + "fixing_vers": impact.fixed_vers, + } + ) + + advisory_by_conflict_range[conflict_checksum].append(advisory_item) + + for checksum, adv_items in advisory_by_conflict_range.items(): + primary, *secondaries = sorted(adv_items, key=lambda x: x["precedence"], reverse=True) + conflict_ranges[checksum]["primary"] = primary + conflict_ranges[checksum]["secondaries"] = secondaries + + curation_item["advisories"] = list(conflict_ranges.values()) + return curation_item + + def get_advisory_with_best_impact_for_purls(purl_adv_map, conflicting_avids): """ Return PURL - AVID mapping for packages. @@ -595,9 +689,10 @@ def merged_advisory(advisories, best_purl_avid_impact_map, conflicting_package_d ) for summary, avids in seen_summaries.values(): - merged_summary.append(f"{tuple(sorted(avids))}: {summary}") + avids_str = ", ".join(sorted(avids)) + merged_summary.append(f"[{avids_str}]: {summary}") - merged_adv["summary"] = "\n".join(merged_summary) + merged_adv["summary"] = "\n\n".join(merged_summary) merged_adv["aliases"] = list(merged_adv["aliases"]) merged_adv["weaknesses"] = list(merged_adv["weaknesses"]) @@ -624,7 +719,7 @@ def bulk_create_with_m2m(todos, advisories, logger): try: AdvisoryToDoV2.objects.bulk_create(objs=todos, ignore_conflicts=True) except Exception as e: - logger(f"Error creating AdvisoryToDo: {e}") + logger(f"Error creating AdvisoryToDoV2: {e}") new_todos = AdvisoryToDoV2.objects.filter(created_at__gte=start_time) From 9519be37e2eeadde8bb2c07375ed22911f55d5de Mon Sep 17 00:00:00 2001 From: Keshav Priyadarshi Date: Wed, 27 May 2026 15:20:52 +0530 Subject: [PATCH 3/6] feat: add package curation dashboard view Signed-off-by: Keshav Priyadarshi --- vulnerabilities/views.py | 25 +++++++++++++++++++++++++ vulnerablecode/urls.py | 6 ++++++ 2 files changed, 31 insertions(+) diff --git a/vulnerabilities/views.py b/vulnerabilities/views.py index 6ef3f39a2..477fa9c99 100644 --- a/vulnerabilities/views.py +++ b/vulnerabilities/views.py @@ -7,6 +7,7 @@ # See https://aboutcode.org for more information about nexB OSS projects. # +import json import logging from collections import defaultdict from typing import List @@ -1086,3 +1087,27 @@ def get_context_data(self, **kwargs): context["issue_choices"] = ISSUE_TYPE_CHOICES return context + + +class AdvisoryPackageCurationView(DetailView): + model = AdvisoryToDoV2 + template_name = "package_curation.html" + slug_url_kwarg = "todo_id" + slug_field = "todo_id" + + def get_queryset(self): + package_curation_issue_types = [ + "CONFLICTING_FIXED_BY_PACKAGES", + "CONFLICTING_AFFECTED_PACKAGES", + "CONFLICTING_AFFECTED_AND_FIXED_BY_PACKAGES", + ] + return super().get_queryset().filter(issue_type__in=package_curation_issue_types) + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + todo = self.object + + context["advisory_summaries"] = {adv.avid: adv.summary for adv in todo.advisories.all()} + context["vulnerability_id"] = todo.alias + context["curation_items"] = json.dumps(todo.issue_detail["curation_items"]) + return context diff --git a/vulnerablecode/urls.py b/vulnerablecode/urls.py index c9bee1247..292938b8f 100644 --- a/vulnerablecode/urls.py +++ b/vulnerablecode/urls.py @@ -31,6 +31,7 @@ from vulnerabilities.api_v3 import PackageV3ViewSet from vulnerabilities.views import AdminLoginView from vulnerabilities.views import AdvisoryDetails +from vulnerabilities.views import AdvisoryPackageCurationView from vulnerabilities.views import AdvisoryPackagesDetails from vulnerabilities.views import AdvisoryToDoListView from vulnerabilities.views import AffectedByAdvisoriesListView @@ -105,6 +106,11 @@ def __init__(self, *args, **kwargs): AdvisoryToDoListView.as_view(), name="todo-list", ), + path( + "advisories/todos//package/curate/", + AdvisoryPackageCurationView.as_view(), + name="todo-detail", + ), path( "pipelines//runs/", PipelineRunListView.as_view(), From 574b9c6880dc7552cd9fe8b048cced2c21080829 Mon Sep 17 00:00:00 2001 From: Keshav Priyadarshi Date: Wed, 27 May 2026 15:22:44 +0530 Subject: [PATCH 4/6] feat: link curation queue to package curation dashboard Signed-off-by: Keshav Priyadarshi --- vulnerabilities/templates/advisory_todos.html | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/vulnerabilities/templates/advisory_todos.html b/vulnerabilities/templates/advisory_todos.html index 82dbafd76..0d9c2899c 100644 --- a/vulnerabilities/templates/advisory_todos.html +++ b/vulnerabilities/templates/advisory_todos.html @@ -122,6 +122,10 @@

Advisory To-Dos

{% for todo in todo_list %} + {% with supported_curation="CONFLICTING_FIXED_BY_PACKAGES CONFLICTING_AFFECTED_PACKAGES CONFLICTING_AFFECTED_AND_FIXED_BY_PACKAGES" %} + {% if todo.issue_type in supported_curation.split %} + + {% endif %}
{{ todo.alias }} @@ -139,6 +143,10 @@

Advisory To-Dos

{{ todo.get_issue_type_display }}
+ {% if todo.issue_type in supported_curation.split %} +
+ {% endif %} + {% endwith %} {% empty %} From 28046dd3aa9b24311aed49149cbd7c8ec013c03b Mon Sep 17 00:00:00 2001 From: Keshav Priyadarshi Date: Wed, 27 May 2026 15:25:16 +0530 Subject: [PATCH 5/6] feat(ui): add style and template for curation dashboard Signed-off-by: Keshav Priyadarshi --- .../templates/advisory_detail.html | 3 +- .../templates/package_curation.html | 101 ++++ vulnerabilities/templatetags/utils.py | 12 + .../static/css/package_curation.css | 174 +++++++ vulnerablecode/static/js/package_curation.js | 441 ++++++++++++++++++ .../static/js/package_curation.min.js | 48 ++ 6 files changed, 778 insertions(+), 1 deletion(-) create mode 100644 vulnerabilities/templates/package_curation.html create mode 100644 vulnerablecode/static/css/package_curation.css create mode 100644 vulnerablecode/static/js/package_curation.js create mode 100644 vulnerablecode/static/js/package_curation.min.js diff --git a/vulnerabilities/templates/advisory_detail.html b/vulnerabilities/templates/advisory_detail.html index 0b850c006..fa687bd99 100644 --- a/vulnerabilities/templates/advisory_detail.html +++ b/vulnerabilities/templates/advisory_detail.html @@ -4,6 +4,7 @@ {% load static %} {% load show_cvss %} {% load url_filters %} +{% load utils %} {% block title %} VulnerableCode Advisory Details - {{ advisory.advisory_id }} @@ -115,7 +116,7 @@ Summary - {{ advisory.summary }} + {{ advisory.summary|linebreaksbr }} {% if severity_score_range %} diff --git a/vulnerabilities/templates/package_curation.html b/vulnerabilities/templates/package_curation.html new file mode 100644 index 000000000..02a442635 --- /dev/null +++ b/vulnerabilities/templates/package_curation.html @@ -0,0 +1,101 @@ +{% extends "base.html" %} +{% load static %} +{% load utils %} + +{% block extrahead %} + +{% endblock %} + +{% block content %} +
+
+
+
+

Curation Dashboard

+

{{ vulnerability_id }}

+
+ +
+
+
+ + Progress + + 0 / 0 +
+ +
+
+ +
+ +
+
+

Advisory Summaries

+
+ {% for avid, text in advisory_summaries.items %} +
+

{{ avid }}

+
{{ text|normalize_links|urlize }}
+
+ {% empty %} +
+ No summaries available. +
+ {% endfor %} +
+
+ +
+
+
+

+

+
+ +
+ + + + + + + + + +
+
+ +
+
+ +
+
+ + +
+
+
+
+
+
+
+{% endblock %} + + +{% block scripts %} + + +{% endblock %} \ No newline at end of file diff --git a/vulnerabilities/templatetags/utils.py b/vulnerabilities/templatetags/utils.py index 1f8d48a9e..543191283 100644 --- a/vulnerabilities/templatetags/utils.py +++ b/vulnerabilities/templatetags/utils.py @@ -8,6 +8,8 @@ # +import re + from aboutcode.pipeline import humanize_time from django import template @@ -52,3 +54,13 @@ def querystring(request, **kwargs): query[key] = value return query.urlencode() + + +@register.filter +def normalize_links(value): + """Normalize Markdown URLs.""" + if not value: + return "" + + markdown_links = re.compile(r"\[([^\]]+)\]\((https?://[^\s)]+)\s*\)") + return markdown_links.sub(r"\1 \2", value) diff --git a/vulnerablecode/static/css/package_curation.css b/vulnerablecode/static/css/package_curation.css new file mode 100644 index 000000000..19d7deed9 --- /dev/null +++ b/vulnerablecode/static/css/package_curation.css @@ -0,0 +1,174 @@ + .state-affected { + background-color: #f14668 !important; + color: #fff !important; + } + + .state-fixed { + background-color: #48c78e !important; + color: #fff !important; + } + + .table { + table-layout: fixed !important; + width: 100% !important; + } + + .table th { + border-width: 0 1px 1px 1px !important; + border-color: #dbdbdb !important; + background-color: #fff !important; + } + + #table-header th { + height: 140px; + vertical-align: top !important; + padding: 0.75rem 0.5rem !important; + } + + #table-header th>div { + display: inline-flex !important; + flex-direction: column !important; + justify-content: space-between !important; + height: 100% !important; + width: 100%; + } + + #table-header th:first-child { + width: 100px !important; + } + + #table-header th:nth-child(2) { + width: 100px !important; + } + + #table-header th:nth-child(n+3) { + width: 100px !important; + } + + + .curation-cell, + .advisory-cell, + #curation-body td, + .folded-row-marker, + .range-data-cell, + .help-instructions { + font-size: 0.8rem !important; + } + + .curation-cell { + cursor: pointer; + text-align: center !important; + font-weight: 700; + user-select: none; + transition: background-color 0.1s ease; + border: 1px solid #dbdbdb !important; + } + + .curation-cell:hover { + filter: brightness(0.95); + box-shadow: inset 0 0 0 1px #3273dc; + } + + .advisory-cell, + .advisory-cell * { + cursor: not-allowed; + } + + .advisory-cell { + text-align: center !important; + vertical-align: middle !important; + border: 1px solid #dbdbdb !important; + } + + .folded-row-marker, + .range-row-marker { + cursor: pointer; + text-align: center; + color: #4a4a4a; + font-weight: 600; + background-color: #f5f5f5; + } + + .folded-row-marker { + height: 30px; + } + + .folded-row-marker:hover, + .range-row-marker:hover { + background-color: #ededed; + color: #0a0a0a; + } + + .folded-row-marker .icon { + transition: transform 0.2s ease; + } + + .folded-row-marker.is-expanded .icon { + transform: rotate(180deg); + } + + .range-row-marker { + font-size: 0.8rem; + padding: 6px 0; + border: 1px solid #dbdbdb !important; + } + + .range-data-cell { + background-color: #fafafa; + padding: 6px !important; + vertical-align: top !important; + border: 1px solid #dbdbdb !important; + + white-space: normal !important; + word-break: break-all !important; + overflow-wrap: break-word !important; + } + + #summaries-container p strong { + display: block; + word-break: break-all; + overflow-wrap: break-word; + line-height: 1.3; + margin-bottom: 4px; + } + + .summary-text { + font-size: 0.85rem; + line-height: 1.4; + white-space: pre-wrap; + word-break: break-word; + } + + div.summary-text a:link, + div.summary-text a { + color: #3273dc !important; + } + + div.summary-text a:visited { + color: #8a4de8 !important; + } + + div.summary-text a:hover { + color: #0a0a0a !important; + } + + .advisory-wrapper { + width: 100%; + word-break: break-all; + white-space: normal; +} + +.advisory-link { + display: inline-flex; + align-items: center; + gap: 4px; + text-align: left; + text-decoration: underline; + white-space: normal; + flex-wrap: wrap; +} + +.advisory-link .icon { + font-size: 0.75rem; + flex-shrink: 0; +} \ No newline at end of file diff --git a/vulnerablecode/static/js/package_curation.js b/vulnerablecode/static/js/package_curation.js new file mode 100644 index 000000000..b2b34b775 --- /dev/null +++ b/vulnerablecode/static/js/package_curation.js @@ -0,0 +1,441 @@ +const app = { + currentIndex: 0, + userStates: {}, + expandedFolds: new Set(), + showRanges: false, + foldAgreementBlocks: true, + init() { + this.renderPackageCuration(); + + document.querySelector('.summary-text')?.addEventListener('click', (e) => { + if (e.target.closest('a')) { + e.target.target = '_blank'; + } + }); + }, + + renderPackageCuration() { + const item = curationItems[this.currentIndex]; + const versions = item.all_versions || item.all_version; + + const total = curationItems.length; + const progPercentage = ((this.currentIndex + 1) / total) * 100; + document.getElementById('main-progress').value = progPercentage; + document.getElementById('progress-text').innerText = `${this.currentIndex + 1} / ${total}`; + document.getElementById('current-purl').innerText = item.purl; + document.getElementById('conflict-reason').innerText = item.conflict_reason; + if (!this.userStates[this.currentIndex]) { + this.userStates[this.currentIndex] = {}; + versions.forEach(v => { + if (item.partial_curation.affected.includes(v)) this.userStates[this.currentIndex][v] = 'affected'; + else if (item.partial_curation.fixing.includes(v)) this.userStates[this.currentIndex][v] = 'fixed'; + else this.userStates[this.currentIndex][v] = '?'; + }); + } + this.renderHeader(item); + this.renderBody(item, versions); + this.updateNavButtons(); + }, + + renderHeader(item) { + const header = document.getElementById('table-header'); + header.innerHTML = ` + Version + +
+
+
Curation
+
+ +
+ `; + item.advisories.forEach((adv, idx) => { + const th = document.createElement('th'); + th.className = "has-text-centered"; + const targetUrl = baseAdvisoryUrl.replace('0', adv.advisory_uid); + th.innerHTML = ` +
+ + +
+ `; + header.appendChild(th); + }); + }, + + renderBody(item, versions) { + const body = document.getElementById('curation-body'); + body.innerHTML = ''; + const totalColumns = item.advisories.length + 2; + const rangeToggleRow = document.createElement('tr'); + rangeToggleRow.innerHTML = ` + + + ${this.showRanges ? 'Hide' : 'Show'} Version Ranges + `; + body.appendChild(rangeToggleRow); + if (this.showRanges) { + const rangeDataRow = document.createElement('tr'); + let rowHtml = ``; + item.advisories.forEach(adv => { + const rangeHtml = adv.vers_ranges.map(r => { + let htmlLines = []; + if (r.affected_vers && r.affected_vers.trim() !== "") { + htmlLines.push(`
Affected: ${r.affected_vers}
`); + } + if (r.fixing_vers && r.fixing_vers.trim() !== "") { + htmlLines.push(`
Fixing: ${r.fixing_vers}
`); + } + return htmlLines.length > 0 ? htmlLines.join('') : '
No range specified
'; + }).join('
'); + rowHtml += `${rangeHtml}`; + }); + rangeDataRow.innerHTML = rowHtml; + body.appendChild(rangeDataRow); + } + const foldable = this.getFoldableRanges(item, versions); + for (let i = 0; i < versions.length; i++) { + const range = foldable.find(r => i >= r.start && i <= r.end); + if (range) { + const foldKey = `${this.currentIndex}-${range.start}`; + let isExpanded = this.expandedFolds.has(foldKey); + if (!this.foldAgreementBlocks) { + isExpanded = !this.expandedFolds.has(`${this.currentIndex}-${range.start}-collapsed`); + } + if (i === range.start) { + const marker = document.createElement('tr'); + marker.innerHTML = ` + + ${isExpanded ? 'Hide' : 'Show'} Consensus Range (${range.end - range.start + 1} versions) + `; + body.appendChild(marker); + } + if (!isExpanded) { + if (i === range.end) continue; + i = range.end; + continue; + } + } + body.appendChild(this.createRow(versions[i], item)); + } + }, + + resetCurrentCuration() { + const item = curationItems[this.currentIndex]; + const versions = item.all_versions || item.all_version; + versions.forEach(v => { + if (item.partial_curation.affected.includes(v)) { + this.userStates[this.currentIndex][v] = 'affected'; + } else if (item.partial_curation.fixing.includes(v)) { + this.userStates[this.currentIndex][v] = 'fixed'; + } else { + this.userStates[this.currentIndex][v] = '?'; + } + }); + this.renderBody(item, versions); + }, + + renderHeader(item) { + const header = document.getElementById('table-header'); + header.innerHTML = ` + Version + +
+
+
Curation
+
+ +
+ `; + + item.advisories.forEach((advGroup, groupIdx) => { + const secondaries = advGroup.secondaries || []; + const hasSecondaries = secondaries.length > 0; + + const colKey = `${this.currentIndex}-col-${groupIdx}`; + const isExpanded = this.expandedFolds.has(colKey); + + const primaryTh = document.createElement('th'); + primaryTh.className = "has-text-centered"; + + const primaryUrl = baseAdvisoryUrl.replace('0', advGroup.primary.advisory_uid); + let primaryUidHtml = ` + + `; + + let toggleHtml = ""; + if (hasSecondaries) { + toggleHtml = ` + + `; + } + + primaryTh.innerHTML = ` +
+
+ ${toggleHtml} + ${primaryUidHtml} +
+ +
+ `; + header.appendChild(primaryTh); + + if (hasSecondaries && isExpanded) { + secondaries.forEach((sec, secIdx) => { + const secTh = document.createElement('th'); + secTh.className = "has-text-centered"; + secTh.style.backgroundColor = "#fafafa"; + + const secUrl = baseAdvisoryUrl.replace('0', sec.advisory_uid); + secTh.innerHTML = ` +
+ + +
+ `; + header.appendChild(secTh); + }); + } + }); + }, + + renderBody(item, versions) { + const body = document.getElementById('curation-body'); + body.innerHTML = ''; + + let totalColumns = 2; + item.advisories.forEach((advGroup, groupIdx) => { + totalColumns += 1; + const colKey = `${this.currentIndex}-col-${groupIdx}`; + if (this.expandedFolds.has(colKey)) { + totalColumns += (advGroup.secondaries || []).length; + } + }); + + const rangeToggleRow = document.createElement('tr'); + rangeToggleRow.innerHTML = ` + + + ${this.showRanges ? 'Hide' : 'Show'} Version Ranges + `; + body.appendChild(rangeToggleRow); + + if (this.showRanges) { + const rangeDataRow = document.createElement('tr'); + let rowHtml = ``; + + item.advisories.forEach((advGroup, groupIdx) => { + const colKey = `${this.currentIndex}-col-${groupIdx}`; + const isExpanded = this.expandedFolds.has(colKey); + + const renderRangeHtml = (ranges) => ranges.map(r => { + let htmlLines = []; + if (r.affected_vers && r.affected_vers.trim() !== "") { + htmlLines.push(`
Affected: ${r.affected_vers}
`); + } + if (r.fixing_vers && r.fixing_vers.trim() !== "") { + htmlLines.push(`
Fixing: ${r.fixing_vers}
`); + } + return htmlLines.length > 0 ? htmlLines.join('') : '
No range specified
'; + }).join('
'); + + rowHtml += `${renderRangeHtml(advGroup.primary.vers_ranges)}`; + + if (isExpanded && advGroup.secondaries) { + advGroup.secondaries.forEach(sec => { + rowHtml += `${renderRangeHtml(sec.vers_ranges)}`; + }); + } + }); + + rangeDataRow.innerHTML = rowHtml; + body.appendChild(rangeDataRow); + } + + const foldable = this.getFoldableRanges(item, versions); + for (let i = 0; i < versions.length; i++) { + const range = foldable.find(r => i >= r.start && i <= r.end); + if (range) { + const foldKey = `${this.currentIndex}-${range.start}`; + let isExpanded = this.expandedFolds.has(foldKey); + if (!this.foldAgreementBlocks) { + isExpanded = !this.expandedFolds.has(`${this.currentIndex}-${range.start}-collapsed`); + } + if (i === range.start) { + const marker = document.createElement('tr'); + marker.innerHTML = ` + + ${isExpanded ? 'Hide' : 'Show'} Consensus Range (${range.end - range.start + 1} versions) + `; + body.appendChild(marker); + } + if (!isExpanded) { + if (i === range.end) continue; + i = range.end; + continue; + } + } + body.appendChild(this.createRow(versions[i], item)); + } + }, + + createRow(v, item) { + const tr = document.createElement('tr'); + const state = this.userStates[this.currentIndex][v]; + tr.innerHTML = `${v}`; + const userTd = document.createElement('td'); + userTd.className = `curation-cell state-${state} `; + userTd.innerText = state.toUpperCase(); + userTd.onclick = () => this.cycleState(v); + tr.appendChild(userTd); + + item.advisories.forEach((advGroup, groupIdx) => { + const colKey = `${this.currentIndex}-col-${groupIdx}`; + const isExpanded = this.expandedFolds.has(colKey); + + const primaryState = advGroup.affected.includes(v) ? 'affected' : (advGroup.fixing.includes(v) ? 'fixed' : 'unaffected'); + const td = document.createElement('td'); + td.className = `advisory-cell state-${primaryState}`; + td.innerText = primaryState.toUpperCase(); + tr.appendChild(td); + + if (isExpanded && advGroup.secondaries) { + advGroup.secondaries.forEach(sec => { + const secAffected = sec.affected || advGroup.affected; + const secFixing = sec.fixing || advGroup.fixing; + + const secState = secAffected.includes(v) ? 'affected' : (secFixing.includes(v) ? 'fixed' : 'unaffected'); + const secTd = document.createElement('td'); + secTd.className = `advisory-cell state-${secState}`; + secTd.style.borderLeft = "1px dashed #dbdbdb"; + secTd.innerText = secState.toUpperCase(); + tr.appendChild(secTd); + }); + } + }); + return tr; + }, + + getFoldableRanges(item, versions) { + const ranges = []; + let start = -1; + for (let i = 0; i < versions.length; i++) { + const v = versions[i]; + const states = item.advisories.map(a => a.affected.includes(v) ? 'affected' : (a.fixing.includes(v) ? 'fixed' : 'unaffected')); + const allMatch = states.every(s => s === states[0]); + if (allMatch) { + if (start === -1) start = i; + } else { + if (start !== -1 && (i - start) >= 3) ranges.push({ + start, + end: i - 1 + }); + start = -1; + } + } + if (start !== -1 && (versions.length - start) >= 3) ranges.push({ + start, + end: versions.length - 1 + }); + return ranges; + }, + + toggleFold(startIdx) { + const foldKey = `${this.currentIndex}-${startIdx}`; + const collapseKey = `${this.currentIndex}-${startIdx}-collapsed`; + + if (this.foldAgreementBlocks) { + if (this.expandedFolds.has(foldKey)) { + this.expandedFolds.delete(foldKey); + } else { + this.expandedFolds.add(foldKey); + } + } else { + if (this.expandedFolds.has(collapseKey)) { + this.expandedFolds.delete(collapseKey); + } else { + this.expandedFolds.add(collapseKey); + } + } + const item = curationItems[this.currentIndex]; + const versions = item.all_versions || item.all_version; + this.renderBody(item, versions); + }, + + toggleColumnFold(groupIdx) { + const colKey = `${this.currentIndex}-col-${groupIdx}`; + if (this.expandedFolds.has(colKey)) { + this.expandedFolds.delete(colKey); + } else { + this.expandedFolds.add(colKey); + } + const item = curationItems[this.currentIndex]; + const versions = item.all_versions || item.all_version; + + this.renderHeader(item); + this.renderBody(item, versions); + }, + + toggleRanges() { + this.showRanges = !this.showRanges; + const item = curationItems[this.currentIndex]; + const versions = item.all_versions || item.all_version; + this.renderBody(item, versions); + }, + + cycleState(v) { + const seq = ['unaffected', 'affected', 'fixed']; + const current = this.userStates[this.currentIndex][v]; + this.userStates[this.currentIndex][v] = seq[(seq.indexOf(current) + 1) % 3]; + const item = curationItems[this.currentIndex]; + const versions = item.all_versions || item.all_version; + this.renderBody(item, versions); + }, + + pickAdvisory(advIdx, type, secondaryIdx) { + const item = curationItems[this.currentIndex]; + const advGroup = item.advisories[advIdx]; + const versions = item.all_versions || item.all_version; + + versions.forEach(v => { + if (advGroup.affected.includes(v)) this.userStates[this.currentIndex][v] = 'affected'; + else if (advGroup.fixing.includes(v)) this.userStates[this.currentIndex][v] = 'fixed'; + else this.userStates[this.currentIndex][v] = 'unaffected'; + }); + this.renderBody(item, versions); + }, + + navigate(dir) { + this.currentIndex += dir; + this.renderPackageCuration(); + }, + + updateNavButtons() { + document.getElementById('prev-btn').disabled = this.currentIndex === 0; + const isLast = this.currentIndex === curationItems.length - 1; + document.getElementById('next-btn').classList.toggle('is-hidden', isLast); + document.getElementById('finish-btn').classList.toggle('is-hidden', !isLast); + }, + +}; +document.addEventListener('DOMContentLoaded', () => app.init()); \ No newline at end of file diff --git a/vulnerablecode/static/js/package_curation.min.js b/vulnerablecode/static/js/package_curation.min.js new file mode 100644 index 000000000..eeb5af880 --- /dev/null +++ b/vulnerablecode/static/js/package_curation.min.js @@ -0,0 +1,48 @@ +const app={currentIndex:0,userStates:{},expandedFolds:new Set,showRanges:!1,foldAgreementBlocks:!0,init(){this.renderPackageCuration(),document.querySelector(".summary-text")?.addEventListener("click",e=>{e.target.closest("a")&&(e.target.target="_blank")})},renderPackageCuration(){const t=curationItems[this.currentIndex];var e=t.all_versions||t.all_version,s=curationItems.length,n=(this.currentIndex+1)/s*100;document.getElementById("main-progress").value=n,document.getElementById("progress-text").innerText=this.currentIndex+1+" / "+s,document.getElementById("current-purl").innerText=t.purl,document.getElementById("conflict-reason").innerText=t.conflict_reason,this.userStates[this.currentIndex]||(this.userStates[this.currentIndex]={},e.forEach(e=>{t.partial_curation.affected.includes(e)?this.userStates[this.currentIndex][e]="affected":t.partial_curation.fixing.includes(e)?this.userStates[this.currentIndex][e]="fixed":this.userStates[this.currentIndex][e]="?"})),this.renderHeader(t),this.renderBody(t,e),this.updateNavButtons()},renderHeader:function(e){const d=document.getElementById("table-header");d.innerHTML=` + Version + +
+
+
Curation
+
+ +
+ `,e.advisories.forEach((e,a)=>{var t=e.secondaries||[],s=0 + + ${e.primary.advisory_uid} + + + + `;let r="";s&&(r=` + + `),i.innerHTML=` +
+
+ ${r} + ${e} +
+ +
+ `,d.appendChild(i),s&&n&&t.forEach((e,t)=>{var s=document.createElement("th"),n=(s.className="has-text-centered",s.style.backgroundColor="#fafafa",baseAdvisoryUrl.replace("0",e.advisory_uid));s.innerHTML=` +
+ + +
+ `,d.appendChild(s)})})},renderBody:function(e,s){var a=document.getElementById("curation-body");a.innerHTML="";let n=2;e.advisories.forEach((e,t)=>{n+=1,t=this.currentIndex+"-col-"+t,this.expandedFolds.has(t)&&(n+=(e.secondaries||[]).length)});var t=document.createElement("tr");if(t.innerHTML=` + + + ${this.showRanges?"Hide":"Show"} Version Ranges + `,a.appendChild(t),this.showRanges){t=document.createElement("tr");let n="";e.advisories.forEach((e,t)=>{t=this.currentIndex+"-col-"+t,t=this.expandedFolds.has(t);const s=e=>e.map(e=>{var t=[];return e.affected_vers&&""!==e.affected_vers.trim()&&t.push(`
Affected: ${e.affected_vers}
`),e.fixing_vers&&""!==e.fixing_vers.trim()&&t.push(`
Fixing: ${e.fixing_vers}
`),0No range specified"}).join('
');n+=`${s(e.primary.vers_ranges)}`,t&&e.secondaries&&e.secondaries.forEach(e=>{n+=`${s(e.vers_ranges)}`})}),t.innerHTML=n,a.appendChild(t)}var i=this.getFoldableRanges(e,s);for(let t=0;tt>=e.start&&t<=e.end);if(r){var d=this.currentIndex+"-"+r.start;let e=this.expandedFolds.has(d);if(this.foldAgreementBlocks||(e=!this.expandedFolds.has(`${this.currentIndex}-${r.start}-collapsed`)),t===r.start&&((d=document.createElement("tr")).innerHTML=` + + ${e?"Hide":"Show"} Consensus Range (${r.end-r.start+1} versions) + `,a.appendChild(d)),!e){if(t===r.end)continue;t=r.end;continue}}a.appendChild(this.createRow(s[t],e))}},resetCurrentCuration(){const t=curationItems[this.currentIndex];var e=t.all_versions||t.all_version;e.forEach(e=>{t.partial_curation.affected.includes(e)?this.userStates[this.currentIndex][e]="affected":t.partial_curation.fixing.includes(e)?this.userStates[this.currentIndex][e]="fixed":this.userStates[this.currentIndex][e]="?"}),this.renderBody(t,e)},createRow(a,e){const i=document.createElement("tr");var t=this.userStates[this.currentIndex][a],s=(i.innerHTML=`${a}`,document.createElement("td"));return s.className=`curation-cell state-${t} `,s.innerText=t.toUpperCase(),s.onclick=()=>this.cycleState(a),i.appendChild(s),e.advisories.forEach((s,e)=>{var e=this.currentIndex+"-col-"+e,e=this.expandedFolds.has(e),t=s.affected.includes(a)?"affected":s.fixing.includes(a)?"fixed":"unaffected",n=document.createElement("td");n.className="advisory-cell state-"+t,n.innerText=t.toUpperCase(),i.appendChild(n),e&&s.secondaries&&s.secondaries.forEach(e=>{var t=e.affected||s.affected,e=e.fixing||s.fixing,t=t.includes(a)?"affected":e.includes(a)?"fixed":"unaffected";(e=document.createElement("td")).className="advisory-cell state-"+t,e.style.borderLeft="1px dashed #dbdbdb",e.innerText=t.toUpperCase(),i.appendChild(e)})}),i},getFoldableRanges(t,s){var n=[];let a=-1;for(let e=0;ee.affected.includes(i)?"affected":e.fixing.includes(i)?"fixed":"unaffected");r.every(e=>e===r[0])?-1===a&&(a=e):(-1!==a&&3<=e-a&&n.push({start:a,end:e-1}),a=-1)}return-1!==a&&3<=s.length-a&&n.push({start:a,end:s.length-1}),n},toggleFold(e){var t=this.currentIndex+"-"+e,e=this.currentIndex+`-${e}-collapsed`;this.foldAgreementBlocks?this.expandedFolds.has(t)?this.expandedFolds.delete(t):this.expandedFolds.add(t):this.expandedFolds.has(e)?this.expandedFolds.delete(e):this.expandedFolds.add(e);e=(t=curationItems[this.currentIndex]).all_versions||t.all_version;this.renderBody(t,e)},toggleColumnFold(e){e=this.currentIndex+"-col-"+e;this.expandedFolds.has(e)?this.expandedFolds.delete(e):this.expandedFolds.add(e);var t=(e=curationItems[this.currentIndex]).all_versions||e.all_version;this.renderHeader(e),this.renderBody(e,t)},toggleRanges(){this.showRanges=!this.showRanges;var e=curationItems[this.currentIndex],t=e.all_versions||e.all_version;this.renderBody(e,t)},cycleState(e){var t=["unaffected","affected","fixed"],s=this.userStates[this.currentIndex][e];this.userStates[this.currentIndex][e]=t[(t.indexOf(s)+1)%3];t=(e=curationItems[this.currentIndex]).all_versions||e.all_version;this.renderBody(e,t)},pickAdvisory(e,t,s){var n=curationItems[this.currentIndex];const a=n.advisories[e];(e=n.all_versions||n.all_version).forEach(e=>{a.affected.includes(e)?this.userStates[this.currentIndex][e]="affected":a.fixing.includes(e)?this.userStates[this.currentIndex][e]="fixed":this.userStates[this.currentIndex][e]="unaffected"}),this.renderBody(n,e)},navigate(e){this.currentIndex+=e,this.renderPackageCuration()},updateNavButtons(){document.getElementById("prev-btn").disabled=0===this.currentIndex;var e=this.currentIndex===curationItems.length-1;document.getElementById("next-btn").classList.toggle("is-hidden",e),document.getElementById("finish-btn").classList.toggle("is-hidden",!e)}};document.addEventListener("DOMContentLoaded",()=>app.init()); \ No newline at end of file From b886d87b45711f2bb3552e28a02e995bc9f7edb7 Mon Sep 17 00:00:00 2001 From: Keshav Priyadarshi Date: Wed, 27 May 2026 18:20:08 +0530 Subject: [PATCH 6/6] fix: use pipeline_id field to exclude advisories from todo computation Signed-off-by: Keshav Priyadarshi --- vulnerabilities/models.py | 2 +- vulnerabilities/templates/advisory_detail.html | 3 +-- vulnerabilities/views.py | 4 +++- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/vulnerabilities/models.py b/vulnerabilities/models.py index 511d6dd31..d05529638 100644 --- a/vulnerabilities/models.py +++ b/vulnerabilities/models.py @@ -3011,7 +3011,7 @@ def todo_excluded(self): """Exclude advisory ineligible for ToDo computation.""" from vulnerabilities.importers import TODO_EXCLUDED_PIPELINES - return self.exclude(datasource_id__in=TODO_EXCLUDED_PIPELINES) + return self.exclude(pipeline_id__in=TODO_EXCLUDED_PIPELINES) class AdvisorySet(models.Model): diff --git a/vulnerabilities/templates/advisory_detail.html b/vulnerabilities/templates/advisory_detail.html index fa687bd99..0b850c006 100644 --- a/vulnerabilities/templates/advisory_detail.html +++ b/vulnerabilities/templates/advisory_detail.html @@ -4,7 +4,6 @@ {% load static %} {% load show_cvss %} {% load url_filters %} -{% load utils %} {% block title %} VulnerableCode Advisory Details - {{ advisory.advisory_id }} @@ -116,7 +115,7 @@ Summary - {{ advisory.summary|linebreaksbr }} + {{ advisory.summary }} {% if severity_score_range %} diff --git a/vulnerabilities/views.py b/vulnerabilities/views.py index 477fa9c99..04ac8a787 100644 --- a/vulnerabilities/views.py +++ b/vulnerabilities/views.py @@ -1107,7 +1107,9 @@ def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) todo = self.object - context["advisory_summaries"] = {adv.avid: adv.summary for adv in todo.advisories.all()} + context["advisory_summaries"] = { + adv.avid: adv.summary for adv in todo.advisories.all() if adv.summary.strip() + } context["vulnerability_id"] = todo.alias context["curation_items"] = json.dumps(todo.issue_detail["curation_items"]) return context