diff --git a/vulnerabilities/models.py b/vulnerabilities/models.py
index 2e69be49a..9b3acc9e2 100644
--- a/vulnerabilities/models.py
+++ b/vulnerabilities/models.py
@@ -1136,9 +1136,9 @@ def get_affecting_vulnerabilities(self):
next_fixed_package_vulns = list(fixed_by_pkg.affected_by)
fixed_by_package_details["fixed_by_purl"] = fixed_by_purl
- fixed_by_package_details["fixed_by_purl_vulnerabilities"] = (
- next_fixed_package_vulns
- )
+ fixed_by_package_details[
+ "fixed_by_purl_vulnerabilities"
+ ] = next_fixed_package_vulns
fixed_by_pkgs.append(fixed_by_package_details)
vuln_details["fixed_by_package_details"] = fixed_by_pkgs
@@ -2552,6 +2552,9 @@ class AdvisoryToDoV2(models.Model):
class Meta:
unique_together = ("related_advisories_id", "issue_type")
+ # todo add index!
+ # use uuid for todo identification.
+
class AdvisorySeverity(models.Model):
url = models.URLField(
@@ -2981,6 +2984,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/pipes/openssl.py b/vulnerabilities/pipes/openssl.py
index 1dffdedc1..b240f416c 100644
--- a/vulnerabilities/pipes/openssl.py
+++ b/vulnerabilities/pipes/openssl.py
@@ -89,7 +89,9 @@ def get_reference(reference_name, tag, reference_url):
ref_type = (
AdvisoryReference.COMMIT
if "commit" in name or tag == "patch"
- else AdvisoryReference.ADVISORY if "advisory" in name else AdvisoryReference.OTHER
+ else AdvisoryReference.ADVISORY
+ if "advisory" in name
+ else AdvisoryReference.OTHER
)
return ReferenceV2(
diff --git a/vulnerabilities/templates/advisory_todos.html b/vulnerabilities/templates/advisory_todos.html
new file mode 100644
index 000000000..75b46d3fc
--- /dev/null
+++ b/vulnerabilities/templates/advisory_todos.html
@@ -0,0 +1,165 @@
+{% extends "base.html" %}
+{% load utils %}
+
+{% block title %}
+Advisory To-Dos
+{% endblock %}
+
+{% block extrahead %}
+
+{% endblock %}
+
+
+{% block content %}
+
+
+ To-Dos
+
Pipeline Dashboard
diff --git a/vulnerabilities/templates/selected_page.html b/vulnerabilities/templates/selected_page.html
new file mode 100644
index 000000000..9196e9fd9
--- /dev/null
+++ b/vulnerabilities/templates/selected_page.html
@@ -0,0 +1,11 @@
+
Selected IDs
+
+{% if selected_ids %}
+
+ {% for id in selected_ids %}
+ - {{ id }}
+ {% endfor %}
+
+{% else %}
+
No items selected
+{% endif %}
\ No newline at end of file
diff --git a/vulnerabilities/templates/todo_curation.html b/vulnerabilities/templates/todo_curation.html
new file mode 100644
index 000000000..cf0cac681
--- /dev/null
+++ b/vulnerabilities/templates/todo_curation.html
@@ -0,0 +1,190 @@
+{% extends "base.html" %}
+{% load utils %}
+
+{% block title %}
+Curation Dashboard
+{% endblock %}
+
+{% block extrahead %}
+
+{% endblock %}
+
+
+{% block content %}
+
+
+
+
+
+
+
+
+
+
+
+
+ {% for adv in advisories %}
+
+
+
+
+
+
+
+
+
+
+
+
+
+{{ adv.yml }}
+
+
+
+
+
+
+
+ {% endfor %}
+
+
+
+
+
+
+
+
+
+{% endblock %}
+
+
+{% block scripts %}
+
+{% endblock %}
\ No newline at end of file
diff --git a/vulnerabilities/templatetags/utils.py b/vulnerabilities/templatetags/utils.py
index cea889808..1f8d48a9e 100644
--- a/vulnerabilities/templatetags/utils.py
+++ b/vulnerabilities/templatetags/utils.py
@@ -39,3 +39,16 @@ def active_item(context, url_name):
@register.filter
def get_item(dictionary, key):
return dictionary.get(key)
+
+
+@register.simple_tag
+def querystring(request, **kwargs):
+ query = request.GET.copy()
+
+ for key, value in kwargs.items():
+ if value in [None, ""]:
+ query.pop(key, None)
+ continue
+ query[key] = value
+
+ return query.urlencode()
diff --git a/vulnerabilities/tests/pipelines/v2_importers/test_collect_fix_commit.py b/vulnerabilities/tests/pipelines/v2_importers/test_collect_fix_commit.py
index 9a687a3b7..dac2c7781 100644
--- a/vulnerabilities/tests/pipelines/v2_importers/test_collect_fix_commit.py
+++ b/vulnerabilities/tests/pipelines/v2_importers/test_collect_fix_commit.py
@@ -52,7 +52,9 @@ def test_collect_fix_commits_groups_by_vuln(mock_repo, pipeline):
side_effect=lambda c: (
["CVE-2021-0001"]
if "CVE" in c.message
- else ["GHSA-dead-beef-baad"] if "GHSA" in c.message else []
+ else ["GHSA-dead-beef-baad"]
+ if "GHSA" in c.message
+ else []
)
)
diff --git a/vulnerabilities/tests/test_api.py b/vulnerabilities/tests/test_api.py
index 31f2b7774..9ed647099 100644
--- a/vulnerabilities/tests/test_api.py
+++ b/vulnerabilities/tests/test_api.py
@@ -75,9 +75,9 @@ def cleaned_response(response):
reference["scores"] = sorted(
reference["scores"], key=lambda x: (x["value"], x["scoring_system"])
)
- package_data["resolved_vulnerabilities"][index]["references"][index2]["scores"] = (
- reference["scores"]
- )
+ package_data["resolved_vulnerabilities"][index]["references"][index2][
+ "scores"
+ ] = reference["scores"]
cleaned_response.append(package_data)
diff --git a/vulnerabilities/views.py b/vulnerabilities/views.py
index 860bde8eb..5f7bb6964 100644
--- a/vulnerabilities/views.py
+++ b/vulnerabilities/views.py
@@ -857,3 +857,161 @@ def get_context_data(self, **kwargs):
context["site_title"] = "VulnerableCode site admin"
context["site_header"] = "VulnerableCode Administration"
return context
+
+
+import saneyaml
+from django import forms
+from django.views.generic import DetailView
+
+from vulnerabilities.models import AdvisoryToDoV2
+from vulnerabilities.pipes.export import serialize_advisory
+from vulnerabilities.models import ISSUE_TYPE_CHOICES
+
+# class AdvisorySelectedView(View):
+# def post(self, request):
+# selected_ids = request.POST.getlist("selected_items")
+
+# # redirect with IDs in URL
+# ids_str = ",".join(selected_ids)
+# return redirect(f"/advisory/v2/selected/?ids={ids_str}")
+
+# def get(self, request):
+# ids = request.GET.get("ids", "")
+# selected_ids = ids.split(",") if ids else []
+
+# return render(request, "selected_page.html", {
+# "selected_ids": selected_ids
+# })
+
+
+class AdvisoryCurationDetailView(DetailView):
+ model = AdvisoryToDoV2
+ template_name = "todo_curation.html"
+ context_object_name = "todo"
+
+ # def get_context_data(self, **kwargs):
+ # context = super().get_context_data(**kwargs)
+
+ # advisories = list(
+ # self.object.advisories.all()
+ # .prefetch_related("aliases", "references", "severities")
+ # )
+
+ # # get selected advisory from URL
+ # selected_id = self.request.GET.get("advisory")
+
+ # selected = None
+ # if selected_id:
+ # selected = next((a for a in advisories if str(a.id) == selected_id), None)
+
+ # if not selected:
+ # selected = advisories[0] if advisories else None
+
+ # context["advisories"] = advisories
+ # context["selected_advisory"] = selected
+
+ # # index for navigation
+ # if selected:
+ # idx = advisories.index(selected)
+ # context["prev_advisory"] = advisories[idx - 1] if idx > 0 else None
+ # context["next_advisory"] = (
+ # advisories[idx + 1] if idx < len(advisories) - 1 else None
+ # )
+
+ # return context
+
+ def get_context_data(self, **kwargs):
+ context = super().get_context_data(**kwargs)
+
+ advisories = self.object.advisories.all().prefetch_related(
+ "aliases", "references", "severities", "weaknesses"
+ )
+
+ context["advisories"] = [
+ {"adv": i, "yml": saneyaml.dump(serialize_advisory(i))} for i in advisories
+ ]
+
+ return context
+
+
+class AdvisoryToDoForm(forms.Form):
+ search = forms.CharField(
+ required=False,
+ label=False,
+ widget=forms.TextInput(
+ attrs={
+ "placeholder": "Search ToDos...",
+ "class": "input",
+ },
+ ),
+ )
+
+ resolved = forms.ChoiceField(
+ required=False,
+ choices=[
+ ("", "All"),
+ ("True", "Yes"),
+ ("False", "No"),
+ ],
+ widget=forms.Select(attrs={"class": "select"}),
+ )
+
+ issue_type = forms.ChoiceField(
+ required=False,
+ choices=[("", "All")] + ISSUE_TYPE_CHOICES,
+ widget=forms.Select(attrs={"class": "select"}),
+ )
+
+class AdvisoryToDoView(ListView, FormMixin):
+ model = AdvisoryToDoV2
+ context_object_name = "todo_list"
+ template_name = "advisory_todos.html"
+ paginate_by = 20
+ form_class = AdvisoryToDoForm
+
+ def get_form_kwargs(self):
+ kwargs = super().get_form_kwargs()
+ kwargs["data"] = self.request.GET
+ return kwargs
+
+ def get_queryset(self):
+ form = self.form_class(self.request.GET)
+ resolved = self.request.GET.get("resolved")
+ issue_type = self.request.GET.get("issue_type")
+
+ qs = super().get_queryset().order_by("-created_at")
+ if resolved in ["True", "False"]:
+ qs = qs.filter(is_resolved=(resolved == "True"))
+
+ if issue_type:
+ qs = qs.filter(issue_type=issue_type)
+
+ qs.prefetch_related("advisories__aliases")
+ if form.is_valid() and (search := form.cleaned_data.get("search")):
+ return qs.filter(advisories__aliases__alias__icontains=search)
+
+ return qs
+
+ def get_context_data(self, **kwargs):
+ context = super().get_context_data(**kwargs)
+ page = context["page_obj"]
+ todos = page.object_list
+ identifiers = {}
+ for todo in todos:
+ advisories = list(todo.advisories.all())
+ identifier = advisories[0].advisory_id
+ aliases = [ alias.alias for alias in advisories[0].aliases.all()]
+ if len(advisories)>1:
+ aliases = [ [ alias.alias for alias in adv.aliases.all()] for adv in advisories]
+ commons = list(set(aliases[0]).intersection(*aliases[1:]))
+ all_unique = list(set(aliases[0]).union(*aliases[1:]))
+ cve_item = next((s for s in commons if s.lower().startswith("cve")), None)
+ identifier = cve_item if cve_item else commons[0]
+ all_unique.remove(identifier)
+ aliases = all_unique[:5] if all_unique else None
+ identifiers[todo.id] = {"identifier": identifier, "aliases": aliases}
+
+ context["identifiers"] = identifiers
+ context['issue_choices'] = ISSUE_TYPE_CHOICES
+
+ return context
diff --git a/vulnerablecode/urls.py b/vulnerablecode/urls.py
index 49948a3b9..7a6cb6b36 100644
--- a/vulnerablecode/urls.py
+++ b/vulnerablecode/urls.py
@@ -26,7 +26,11 @@
from vulnerabilities.api_v2 import PackageV3ViewSet
from vulnerabilities.api_v2 import PipelineScheduleV2ViewSet
from vulnerabilities.api_v2 import VulnerabilityV2ViewSet
+
+# from vulnerabilities.views import AdvisorySelectedView
from vulnerabilities.views import AdminLoginView
+from vulnerabilities.views import AdvisoryCurationDetailView
+from vulnerabilities.views import AdvisoryToDoView
from vulnerabilities.views import AdvisoryDetails
from vulnerabilities.views import AdvisoryPackagesDetails
from vulnerabilities.views import ApiUserCreateView
@@ -89,6 +93,17 @@ def __init__(self, *args, **kwargs):
PipelineScheduleListView.as_view(),
name="dashboard",
),
+ path(
+ "advisories/todos/",
+ AdvisoryToDoView.as_view(),
+ name="todo-list",
+ ),
+ # path("advisory/v2/selected/", AdvisorySelectedView.as_view(), name="advisory-curation-selected"),
+ path(
+ "advisories/todos/
/curate/",
+ AdvisoryCurationDetailView.as_view(),
+ name="todo-detail",
+ ),
path(
"pipelines//runs/",
PipelineRunListView.as_view(),