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 searching and sorting to Findings page #3804

Merged
merged 13 commits into from
Nov 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
16 changes: 10 additions & 6 deletions rocky/rocky/locale/django.pot
Original file line number Diff line number Diff line change
Expand Up @@ -5321,8 +5321,12 @@ msgid "findings"
msgstr ""

#: rocky/templates/findings/finding_list.html
#: rocky/templates/organizations/organization_crisis_room.html
msgid "Findings table"
msgid "Findings table "
msgstr ""

#: rocky/templates/findings/finding_list.html
#: rocky/templates/oois/ooi_list.html
msgid "column headers with buttons are sortable"
msgstr ""

#: rocky/templates/findings/finding_list.html
Expand Down Expand Up @@ -5813,10 +5817,6 @@ msgstr ""
msgid "Objects "
msgstr ""

#: rocky/templates/oois/ooi_list.html
msgid "column headers with buttons are sortable"
msgstr ""

#: rocky/templates/oois/ooi_list.html
msgid "Delete object(s)"
msgstr ""
Expand Down Expand Up @@ -5931,6 +5931,10 @@ msgstr ""
msgid "Top 10 most severe Findings"
msgstr ""

#: rocky/templates/organizations/organization_crisis_room.html
msgid "Findings table"
msgstr ""

#: rocky/templates/organizations/organization_crisis_room.html
msgid "Finding type:"
msgstr ""
Expand Down
34 changes: 27 additions & 7 deletions rocky/rocky/templates/findings/finding_list.html
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ <h1>{% translate "Findings @ " %}{{ observed_at|date }}</h1>
{% translate "Object list" as filter_title %}
{% if object_list|length > 1 and perms.tools.can_mute_findings %}
<form method="post"
id="finding-list-form"
class="inline layout-wide"
action="{% url 'finding_mute_bulk' organization.code %}">
{% csrf_token %}
Expand All @@ -33,18 +34,37 @@ <h1>{% translate "Findings @ " %}{{ observed_at|date }}</h1>
</p>
<div class="horizontal-scroll sticky-column">
<table class="action-buttons nowrap">
<caption class="visually-hidden">{% translate "Findings table" %}</caption>
<caption class="visually-hidden">
{% translate "Findings table " %}
<span class="visually-hidden">, {% translate "column headers with buttons are sortable" %}</span>
</caption>
<thead>
<tr>
{% if object_list|length > 1 and perms.tools.can_mute_findings %}
<th>
<input class="toggle-all" data-toggle-target="finding" type="checkbox">
</th>
{% endif %}
<th>{% translate "Severity" %}</th>
<th>{% translate "Finding" %}</th>
<th class="actions">{% translate "Tree" %}</th>
<th class="actions">{% translate "Graph" %}</th>
<th scope="col"
class="sortable"
{% if order_by == "score" %}aria-sort="{{ sorting_order_class }}"{% endif %}>
<button form="finding-list" name="order_by" value="score" class="sort">
{% translate "Severity" %}
<span aria-hidden="true"
class="icon ti-{% if order_by == "score" and sorting_order == "asc" %}chevron-up{% elif order_by == "score" and sorting_order == "desc" %}chevron-down{% else %}direction{% endif %}"></span>
</button>
</th>
<th scope="col"
class="sortable"
{% if order_by == "finding_type" %}aria-sort="{{ sorting_order_class }}"{% endif %}>
<button form="finding-list" name="order_by" value="finding_type" class="sort">
{% translate "Finding" %}
<span aria-hidden="true"
class="icon ti-{% if order_by == "finding_type" and sorting_order == "asc" %}chevron-up{% elif order_by == "finding_type" and sorting_order == "desc" %}chevron-down{% else %}direction{% endif %}"></span>
</button>
</th>
<th>{% translate "Tree" %}</th>
<th>{% translate "Graph" %}</th>
<th class="sticky-cell visually-hidden actions">{% translate "Details" %}</th>
</tr>
</thead>
Expand Down Expand Up @@ -122,10 +142,10 @@ <h2 class="heading-normal">{% translate "Risk score" %}</h2>
{% endif %}
<div class="button-container">
{% if only_muted %}
<button type="submit">{% translate "Unmute Findings" %}</button>
<button type="submit" form="finding-list-form">{% translate "Unmute Findings" %}</button>
<input type="checkbox" class="hidden" value="true" name="unmute" checked>
{% else %}
<button type="submit">{% translate "Mute Findings" %}</button>
<button type="submit" form="finding-list-form">{% translate "Mute Findings" %}</button>
{% endif %}
</div>
</form>
Expand Down
7 changes: 6 additions & 1 deletion rocky/rocky/templates/findings/findings_filter.html
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,17 @@
{% endif %}
</button>
</div>
<form method="get" class="help">
<form id="finding-list" method="get" class="help">
{% include "partials/form/fieldset.html" with fields=observed_at_form %}
{% include "partials/form/fieldset.html" with fields=severity_filter fieldset_class="filter-fields-direction column" %}
{% include "partials/form/fieldset.html" with fields=muted_findings_filter fieldset_class="filter-fields-direction column" %}
{% include "partials/form/fieldset.html" with fields=finding_search_form %}

{{ order_by_severity_form }}
{{ order_by_finding_type_form }}
<input type="hidden"
name="sorting_order"
value="{% if sorting_order == "asc" %}desc{% else %}asc{% endif %}">
<div class="button-container">
<input type="submit"
value="{% if submit_text %}{{ submit_text }}{% else %}{% translate "Set filters" %}{% endif %}" />
Expand Down
43 changes: 33 additions & 10 deletions rocky/rocky/views/finding_list.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
from collections.abc import Iterable
from datetime import datetime, timezone
from typing import Any
from typing import Any, Literal

import structlog
from django.urls.base import reverse_lazy
from django.utils.translation import gettext_lazy as _
from django.views.generic import ListView
from tools.forms.base import ObservedAtForm
from tools.forms.findings import FindingSearchForm, FindingSeverityMultiSelectForm, MutedFindingSelectionForm
from tools.forms.findings import (
FindingSearchForm,
FindingSeverityMultiSelectForm,
MutedFindingSelectionForm,
OrderByFindingTypeForm,
OrderBySeverityForm,
)
from tools.view_helpers import BreadcrumbsMixin

from octopoes.models.ooi.findings import RiskLevelSeverity
Expand Down Expand Up @@ -69,14 +75,26 @@ def count_active_filters(self):
return len(self.severities) + 1 if self.muted_findings else 0 + self.count_observed_at_filter()

def get_queryset(self) -> FindingList:
return FindingList(
octopoes_connector=self.octopoes_api_connector,
valid_time=self.observed_at,
severities=self.severities,
exclude_muted=self.exclude_muted,
only_muted=self.only_muted,
search_string=self.search_string,
)
return FindingList(self.octopoes_api_connector, **self.get_queryset_params())

def get_queryset_params(self):
return {
"valid_time": self.observed_at,
"severities": self.severities,
"exclude_muted": self.exclude_muted,
"only_muted": self.only_muted,
"search_string": self.search_string,
"order_by": self.order_by,
"asc_desc": self.sorting_order,
}

@property
def order_by(self) -> Literal["score", "finding_type"]:
return "finding_type" if self.request.GET.get("order_by", "") == "finding_type" else "score"

@property
def sorting_order(self) -> Literal["asc", "desc"]:
return "asc" if self.request.GET.get("sorting_order", "") == "asc" else "desc"

def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
Expand All @@ -87,6 +105,11 @@ def get_context_data(self, **kwargs):
context["finding_search_form"] = FindingSearchForm(self.request.GET)
context["only_muted"] = self.only_muted
context["active_filters_counter"] = self.count_active_filters()
context["order_by"] = self.order_by
context["order_by_severity_form"] = OrderBySeverityForm(self.request.GET)
context["order_by_finding_type_form"] = OrderByFindingTypeForm(self.request.GET)
context["sorting_order"] = self.sorting_order
context["sorting_order_class"] = "ascending" if self.sorting_order == "asc" else "descending"
return context


Expand Down
4 changes: 2 additions & 2 deletions rocky/tests/objects/test_objects_findings.py
Original file line number Diff line number Diff line change
Expand Up @@ -240,7 +240,7 @@ def test_muted_finding_button_presence_more_findings_and_post(
assert response.status_code == 200
assertContains(response, '<input class="toggle-all" data-toggle-target="finding" type="checkbox">', html=True)
assertContains(response, '<input type="checkbox" name="finding" value="' + finding_1.primary_key + '">', html=True)
assertContains(response, '<button type="submit">Mute Findings</button>')
assertContains(response, '<button type="submit" form="finding-list-form">Mute Findings</button>')

request = setup_request(
rf.post("finding_mute_bulk", {"finding": [finding_1, finding_2], "reason": "testing"}), member.user
Expand Down Expand Up @@ -285,7 +285,7 @@ def test_can_mute_findings_perms(rf, request, member, mock_organization_view_oct
)

assert response.status_code == 200
assertNotContains(response, '<button type="submit">Mute Findings</button>')
assertNotContains(response, '<button type="submit" form="finding-list-form">Mute Findings</button>')


@pytest.mark.parametrize("member", ["superuser_member", "admin_member", "redteam_member", "client_member"])
Expand Down
8 changes: 8 additions & 0 deletions rocky/tools/forms/findings.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,11 @@ class FindingSearchForm(BaseRockyForm):
search = forms.CharField(
label=_("Search"), required=False, max_length=256, help_text=_("Object ID contains (case sensitive)")
)


class OrderByFindingTypeForm(BaseRockyForm):
order_by = forms.CharField(widget=forms.HiddenInput(attrs={"value": "finding_type"}), required=False)


class OrderBySeverityForm(BaseRockyForm):
order_by = forms.CharField(widget=forms.HiddenInput(attrs={"value": "score"}), required=False)