diff --git a/.dryrunsecurity.yaml b/.dryrunsecurity.yaml
index 21d26b11375..da92963ddac 100644
--- a/.dryrunsecurity.yaml
+++ b/.dryrunsecurity.yaml
@@ -66,7 +66,7 @@ allowedAuthors:
- kiblik
- dsever
- dogboat
- - FelixHernandez
+ - hblankenship
notificationList:
- '@mtesauro'
- '@grendel513'
diff --git a/components/package.json b/components/package.json
index 336f73dfe74..8b076e0bbbc 100644
--- a/components/package.json
+++ b/components/package.json
@@ -1,6 +1,6 @@
{
"name": "defectdojo",
- "version": "2.33.5",
+ "version": "2.33.7",
"license" : "BSD-3-Clause",
"private": true,
"dependencies": {
diff --git a/docs/content/en/usage/performance.md b/docs/content/en/usage/performance.md
index d7957ddb724..70dfa44b421 100644
--- a/docs/content/en/usage/performance.md
+++ b/docs/content/en/usage/performance.md
@@ -5,6 +5,16 @@ draft: false
weight: 4
---
+## Filter String Matching Optimization
+
+IN the UI, many of the filters for a given object will also query related objects
+for an easy visual match of an item to filter on. For instances with many objects,
+this could lead to a considerable performance hit. To alleviate this constriction,
+enable the "Filter String Matching Optimization" setting in the System Settings to
+change many filters to only search on names, rather than the objects themselves.
+This change will save many large queries, and will improve the performance of UI
+based interactions.
+
## Asynchronous Import
DefectDojo offers an experimental feature to aynschronously import security reports.
diff --git a/dojo/__init__.py b/dojo/__init__.py
index c2f8acb4004..24987ce2acc 100644
--- a/dojo/__init__.py
+++ b/dojo/__init__.py
@@ -4,6 +4,6 @@
# Django starts so that shared_task will use this app.
from .celery import app as celery_app # noqa: F401
-__version__ = '2.33.5'
+__version__ = '2.33.7'
__url__ = 'https://github.com/DefectDojo/django-DefectDojo'
__docs__ = 'https://documentation.defectdojo.com'
diff --git a/dojo/components/views.py b/dojo/components/views.py
index 717f79d86a1..82898f6fa8e 100644
--- a/dojo/components/views.py
+++ b/dojo/components/views.py
@@ -1,8 +1,8 @@
from django.shortcuts import render
from django.db.models import Count, Q
from django.db.models.expressions import Value
-from dojo.utils import add_breadcrumb, get_page_items
-from dojo.filters import ComponentFilter
+from dojo.utils import add_breadcrumb, get_page_items, get_system_setting
+from dojo.filters import ComponentFilter, ComponentFilterWithoutObjectLookups
from dojo.components.sql_group_concat import Sql_GroupConcat
from django.db import connection
from django.contrib.postgres.aggregates import StringAgg
@@ -52,7 +52,9 @@ def components(request):
"-total"
) # Default sort by total descending
- comp_filter = ComponentFilter(request.GET, queryset=component_query)
+ filter_string_matching = get_system_setting("filter_string_matching", False)
+ filter_class = ComponentFilterWithoutObjectLookups if filter_string_matching else ComponentFilter
+ comp_filter = filter_class(request.GET, queryset=component_query)
result = get_page_items(request, comp_filter.qs, 25)
# Filter out None values for auto-complete
diff --git a/dojo/db_migrations/0210_system_settings_filter_string_matching.py b/dojo/db_migrations/0210_system_settings_filter_string_matching.py
new file mode 100644
index 00000000000..de1f617c4d0
--- /dev/null
+++ b/dojo/db_migrations/0210_system_settings_filter_string_matching.py
@@ -0,0 +1,18 @@
+# Generated by Django 4.1.13 on 2024-04-25 18:22
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('dojo', '0209_alter_finding_severity'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='system_settings',
+ name='filter_string_matching',
+ field=models.BooleanField(default=False, help_text='When turned on, all filter operations in the UI will require string matches rather than ID. This is a performance enhancement to avoid fetching objects unnecessarily.', verbose_name='Filter String Matching Optimization'),
+ ),
+ ]
diff --git a/dojo/db_migrations/0211_system_settings_enable_similar_findings.py b/dojo/db_migrations/0211_system_settings_enable_similar_findings.py
new file mode 100644
index 00000000000..014977be7d0
--- /dev/null
+++ b/dojo/db_migrations/0211_system_settings_enable_similar_findings.py
@@ -0,0 +1,18 @@
+# Generated by Django 4.1.13 on 2024-04-26 21:52
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('dojo', '0210_system_settings_filter_string_matching'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='system_settings',
+ name='enable_similar_findings',
+ field=models.BooleanField(default=True, help_text='Enable the query of similar findings on the view finding page. This feature can involve potentially large queries and negatively impact performance', verbose_name='Enable Similar Findings'),
+ ),
+ ]
diff --git a/dojo/endpoint/views.py b/dojo/endpoint/views.py
index d35a1390988..bd1a85534e4 100644
--- a/dojo/endpoint/views.py
+++ b/dojo/endpoint/views.py
@@ -12,12 +12,12 @@
from django.db.models import Q, QuerySet, Count
from dojo.endpoint.utils import clean_hosts_run, endpoint_meta_import
-from dojo.filters import EndpointFilter
+from dojo.filters import EndpointFilter, EndpointFilterWithoutObjectLookups
from dojo.forms import EditEndpointForm, \
DeleteEndpointForm, AddEndpointForm, DojoMetaDataForm, ImportEndpointMetaForm
from dojo.models import Product, Endpoint, Finding, DojoMeta, Endpoint_Status
from dojo.utils import get_page_items, add_breadcrumb, get_period_counts, Product_Tab, calculate_grade, redirect, \
- add_error_message_to_response, is_scan_file_too_large
+ add_error_message_to_response, is_scan_file_too_large, get_system_setting
from dojo.notifications.helper import create_notification
from dojo.authorization.authorization_decorators import user_is_authorized
from dojo.authorization.roles_permissions import Permissions
@@ -42,12 +42,13 @@ def process_endpoints_view(request, host_view=False, vulnerable=False):
endpoints = endpoints.prefetch_related('product', 'product__tags', 'tags').distinct()
endpoints = get_authorized_endpoints(Permissions.Endpoint_View, endpoints, request.user)
-
+ filter_string_matching = get_system_setting("filter_string_matching", False)
+ filter_class = EndpointFilterWithoutObjectLookups if filter_string_matching else EndpointFilter
if host_view:
- ids = get_endpoint_ids(EndpointFilter(request.GET, queryset=endpoints, user=request.user).qs)
- endpoints = EndpointFilter(request.GET, queryset=endpoints.filter(id__in=ids), user=request.user)
+ ids = get_endpoint_ids(filter_class(request.GET, queryset=endpoints, user=request.user).qs)
+ endpoints = filter_class(request.GET, queryset=endpoints.filter(id__in=ids), user=request.user)
else:
- endpoints = EndpointFilter(request.GET, queryset=endpoints, user=request.user)
+ endpoints = filter_class(request.GET, queryset=endpoints, user=request.user)
paged_endpoints = get_page_items(request, endpoints.qs, 25)
diff --git a/dojo/engagement/views.py b/dojo/engagement/views.py
index 230c18cc3a0..bdf4c2cb143 100644
--- a/dojo/engagement/views.py
+++ b/dojo/engagement/views.py
@@ -1,6 +1,7 @@
import logging
import csv
import re
+from typing import List
from django.views import View
from openpyxl import Workbook
from openpyxl.styles import Font
@@ -14,7 +15,7 @@
from django.core.exceptions import ValidationError, PermissionDenied
from django.urls import reverse, Resolver404
from django.db.models import Q, Count
-from django.http import HttpResponseRedirect, StreamingHttpResponse, HttpResponse, FileResponse, QueryDict
+from django.http import HttpResponseRedirect, StreamingHttpResponse, HttpResponse, FileResponse, QueryDict, HttpRequest
from django.shortcuts import render, get_object_or_404
from django.views.decorators.cache import cache_page
from django.utils import timezone
@@ -23,7 +24,14 @@
from django.db import DEFAULT_DB_ALIAS
from dojo.engagement.services import close_engagement, reopen_engagement
-from dojo.filters import EngagementFilter, EngagementDirectFilter, EngagementTestFilter
+from dojo.filters import (
+ EngagementFilter,
+ EngagementFilterWithoutObjectLookups,
+ EngagementDirectFilter,
+ EngagementDirectFilterWithoutObjectLookups,
+ EngagementTestFilter,
+ EngagementTestFilterWithoutObjectLookups
+)
from dojo.forms import CheckForm, \
UploadThreatForm, RiskAcceptanceForm, NoteForm, DoneForm, \
EngForm, TestForm, ReplaceRiskAcceptanceProofForm, AddFindingsRiskAcceptanceForm, DeleteEngagementForm, ImportScanForm, \
@@ -112,7 +120,9 @@ def get_filtered_engagements(request, view):
'product__jira_project_set__jira_instance'
)
- engagements = EngagementDirectFilter(request.GET, queryset=engagements)
+ filter_string_matching = get_system_setting("filter_string_matching", False)
+ filter_class = EngagementDirectFilterWithoutObjectLookups if filter_string_matching else EngagementDirectFilter
+ engagements = filter_class(request.GET, queryset=engagements)
return engagements
@@ -181,8 +191,9 @@ def engagements_all(request):
'engagement_set__jira_project__jira_instance',
'jira_project_set__jira_instance'
)
-
- filtered = EngagementFilter(
+ filter_string_matching = get_system_setting("filter_string_matching", False)
+ filter_class = EngagementFilterWithoutObjectLookups if filter_string_matching else EngagementFilter
+ filtered = filter_class(
request.GET,
queryset=filter_qs
)
@@ -384,11 +395,21 @@ def get_risks_accepted(self, eng):
risks_accepted = eng.risk_acceptance.all().select_related('owner').annotate(accepted_findings_count=Count('accepted_findings__id'))
return risks_accepted
+ def get_filtered_tests(
+ self,
+ request: HttpRequest,
+ queryset: List[Test],
+ engagement: Engagement,
+ ):
+ filter_string_matching = get_system_setting("filter_string_matching", False)
+ filter_class = EngagementTestFilterWithoutObjectLookups if filter_string_matching else EngagementTestFilter
+ return filter_class(request.GET, queryset=queryset, engagement=engagement)
+
def get(self, request, eid, *args, **kwargs):
eng = get_object_or_404(Engagement, id=eid)
tests = eng.test_set.all().order_by('test_type__name', '-updated')
default_page_num = 10
- tests_filter = EngagementTestFilter(request.GET, queryset=tests, engagement=eng)
+ tests_filter = self.get_filtered_tests(request, tests, eng)
paged_tests = get_page_items(request, tests_filter.qs, default_page_num)
paged_tests.object_list = prefetch_for_view_tests(paged_tests.object_list)
prod = eng.product
@@ -458,7 +479,7 @@ def post(self, request, eid, *args, **kwargs):
default_page_num = 10
- tests_filter = EngagementTestFilter(request.GET, queryset=tests, engagement=eng)
+ tests_filter = self.get_filtered_tests(request, tests, eng)
paged_tests = get_page_items(request, tests_filter.qs, default_page_num)
# prefetch only after creating the filters to avoid https://code.djangoproject.com/ticket/23771 and https://code.djangoproject.com/ticket/25375
paged_tests.object_list = prefetch_for_view_tests(paged_tests.object_list)
diff --git a/dojo/filters.py b/dojo/filters.py
index 4f1f3c539ad..8dc61379104 100644
--- a/dojo/filters.py
+++ b/dojo/filters.py
@@ -289,7 +289,7 @@ def get_tags_label_from_model(model):
return 'Tags (Unknown)'
-def get_finding_filterset_fields(metrics=False, similar=False):
+def get_finding_filterset_fields(metrics=False, similar=False, filter_string_matching=False):
fields = []
if similar:
@@ -315,10 +315,28 @@ def get_finding_filterset_fields(metrics=False, similar=False):
'mitigated',
'reporter',
'reviewers',
- 'test__engagement__product__prod_type',
- 'test__engagement__product',
- 'test__engagement',
- 'test',
+ ])
+
+ if filter_string_matching:
+ fields.extend([
+ 'reporter',
+ 'reviewers',
+ 'test__engagement__product__prod_type__name',
+ 'test__engagement__product__name',
+ 'test__engagement__name',
+ 'test__title',
+ ])
+ else:
+ fields.extend([
+ 'reporter',
+ 'reviewers',
+ 'test__engagement__product__prod_type',
+ 'test__engagement__product',
+ 'test__engagement',
+ 'test',
+ ])
+
+ fields.extend([
'test__test_type',
'test__engagement__version',
'test__version',
@@ -348,9 +366,9 @@ def get_finding_filterset_fields(metrics=False, similar=False):
])
fields.extend([
- 'param',
- 'payload',
- 'risk_acceptance',
+ 'param',
+ 'payload',
+ 'risk_acceptance',
])
if get_system_setting('enable_jira'):
@@ -362,10 +380,16 @@ def get_finding_filterset_fields(metrics=False, similar=False):
])
if is_finding_groups_enabled():
- fields.extend([
- 'has_finding_group',
- 'finding_group',
- ])
+ if filter_string_matching:
+ fields.extend([
+ 'has_finding_group',
+ 'finding_group__name',
+ ])
+ else:
+ fields.extend([
+ 'has_finding_group',
+ 'finding_group',
+ ])
if get_system_setting('enable_jira'):
fields.extend([
@@ -375,73 +399,155 @@ def get_finding_filterset_fields(metrics=False, similar=False):
return fields
-class FindingFilterWithTags(DojoFilter):
+class FindingTagFilter(DojoFilter):
+ tag = CharFilter(
+ field_name="tags__name",
+ lookup_expr="icontains",
+ label="Tag name contains",
+ help_text="Search for tags on a Finding that contain a given pattern")
tags = ModelMultipleChoiceFilter(
- field_name='tags__name',
- to_field_name='name',
- queryset=Finding.tags.tag_model.objects.all().order_by('name'),
- # label='tags', # doesn't work with tagulous, need to set in __init__ below
- )
-
+ field_name="tags__name",
+ to_field_name="name",
+ queryset=Finding.tags.tag_model.objects.all().order_by("name"),
+ help_text="Filter Findings by the selected tags")
test__tags = ModelMultipleChoiceFilter(
- field_name='test__tags__name',
- to_field_name='name',
- queryset=Test.tags.tag_model.objects.all().order_by('name'),
- # label='tags', # doesn't work with tagulous, need to set in __init__ below
- )
-
+ field_name="test__tags__name",
+ to_field_name="name",
+ queryset=Test.tags.tag_model.objects.all().order_by("name"),
+ help_text="Filter Tests by the selected tags")
test__engagement__tags = ModelMultipleChoiceFilter(
- field_name='test__engagement__tags__name',
- to_field_name='name',
- queryset=Engagement.tags.tag_model.objects.all().order_by('name'),
- # label='tags', # doesn't work with tagulous, need to set in __init__ below
- )
-
+ field_name="test__engagement__tags__name",
+ to_field_name="name",
+ queryset=Engagement.tags.tag_model.objects.all().order_by("name"),
+ help_text="Filter Engagements by the selected tags")
test__engagement__product__tags = ModelMultipleChoiceFilter(
- field_name='test__engagement__product__tags__name',
- to_field_name='name',
- queryset=Product.tags.tag_model.objects.all().order_by('name'),
- # label='tags', # doesn't work with tagulous, need to set in __init__ below
- )
-
- tag = CharFilter(field_name='tags__name', lookup_expr='icontains', label='Tag name contains')
+ field_name="test__engagement__product__tags__name",
+ to_field_name="name",
+ queryset=Product.tags.tag_model.objects.all().order_by("name"),
+ help_text="Filter Products by the selected tags")
not_tags = ModelMultipleChoiceFilter(
- field_name='tags__name',
- to_field_name='name',
- exclude=True,
- queryset=Finding.tags.tag_model.objects.all().order_by('name'),
- # label='tags', # doesn't work with tagulous, need to set in __init__ below
- )
-
+ field_name="tags__name",
+ to_field_name="name",
+ queryset=Finding.tags.tag_model.objects.all().order_by("name"),
+ help_text="Search for tags on a Finding that contain a given pattern, and exclude them",
+ exclude=True)
not_test__tags = ModelMultipleChoiceFilter(
- field_name='test__tags__name',
- to_field_name='name',
- exclude=True,
- label='Test without tags',
- queryset=Test.tags.tag_model.objects.all().order_by('name'),
- # label='tags', # doesn't work with tagulous, need to set in __init__ below
- )
-
+ field_name="test__tags__name",
+ to_field_name="name",
+ label="Test without tags",
+ queryset=Test.tags.tag_model.objects.all().order_by("name"),
+ help_text="Search for tags on a Test that contain a given pattern, and exclude them",
+ exclude=True)
not_test__engagement__tags = ModelMultipleChoiceFilter(
- field_name='test__engagement__tags__name',
- to_field_name='name',
- exclude=True,
- label='Engagement without tags',
- queryset=Engagement.tags.tag_model.objects.all().order_by('name'),
- # label='tags', # doesn't work with tagulous, need to set in __init__ below
- )
-
+ field_name="test__engagement__tags__name",
+ to_field_name="name",
+ label="Engagement without tags",
+ queryset=Engagement.tags.tag_model.objects.all().order_by("name"),
+ help_text="Search for tags on a Engagement that contain a given pattern, and exclude them",
+ exclude=True)
not_test__engagement__product__tags = ModelMultipleChoiceFilter(
- field_name='test__engagement__product__tags__name',
- to_field_name='name',
- exclude=True,
- label='Product without tags',
- queryset=Product.tags.tag_model.objects.all().order_by('name'),
- # label='tags', # doesn't work with tagulous, need to set in __init__ below
- )
+ field_name="test__engagement__product__tags__name",
+ to_field_name="name",
+ label="Product without tags",
+ queryset=Product.tags.tag_model.objects.all().order_by("name"),
+ help_text="Search for tags on a Product that contain a given pattern, and exclude them",
+ exclude=True)
- not_tag = CharFilter(field_name='tags__name', lookup_expr='icontains', label='Not tag name contains', exclude=True)
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+
+
+class FindingTagStringFilter(FilterSet):
+ tags_contains = CharFilter(
+ label="Finding Tag Contains",
+ field_name="tags__name",
+ lookup_expr="icontains",
+ help_text="Search for tags on a Finding that contain a given pattern")
+ tags = CharFilter(
+ label="Finding Tag",
+ field_name="tags__name",
+ lookup_expr="iexact",
+ help_text="Search for tags on a Finding that are an exact match")
+ test__tags_contains = CharFilter(
+ label="Test Tag Contains",
+ field_name="test__tags__name",
+ lookup_expr="icontains",
+ help_text="Search for tags on a Finding that contain a given pattern")
+ test__tags = CharFilter(
+ label="Test Tag",
+ field_name="test__tags__name",
+ lookup_expr="iexact",
+ help_text="Search for tags on a Finding that are an exact match")
+ test__engagement__tags_contains = CharFilter(
+ label="Engagement Tag Contains",
+ field_name="test__engagement__tags__name",
+ lookup_expr="icontains",
+ help_text="Search for tags on a Finding that contain a given pattern")
+ test__engagement__tags = CharFilter(
+ label="Engagement Tag",
+ field_name="test__engagement__tags__name",
+ lookup_expr="iexact",
+ help_text="Search for tags on a Finding that are an exact match")
+ test__engagement__product__tags_contains = CharFilter(
+ label="Product Tag Contains",
+ field_name="test__engagement__product__tags__name",
+ lookup_expr="icontains",
+ help_text="Search for tags on a Finding that contain a given pattern")
+ test__engagement__product__tags = CharFilter(
+ label="Product Tag",
+ field_name="test__engagement__product__tags__name",
+ lookup_expr="iexact",
+ help_text="Search for tags on a Finding that are an exact match")
+
+ not_tags_contains = CharFilter(
+ label="Finding Tag Does Not Contain",
+ field_name="tags__name",
+ lookup_expr="icontains",
+ help_text="Search for tags on a Finding that contain a given pattern, and exclude them",
+ exclude=True)
+ not_tags = CharFilter(
+ label="Not Finding Tag",
+ field_name="tags__name",
+ lookup_expr="iexact",
+ help_text="Search for tags on a Finding that are an exact match, and exclude them",
+ exclude=True)
+ not_test__tags_contains = CharFilter(
+ label="Test Tag Does Not Contain",
+ field_name="test__tags__name",
+ lookup_expr="icontains",
+ help_text="Search for tags on a Test that contain a given pattern, and exclude them",
+ exclude=True)
+ not_test__tags = CharFilter(
+ label="Not Test Tag",
+ field_name="test__tags__name",
+ lookup_expr="iexact",
+ help_text="Search for tags on a Test that are an exact match, and exclude them",
+ exclude=True)
+ not_test__engagement__tags_contains = CharFilter(
+ label="Engagement Tag Does Not Contain",
+ field_name="test__engagement__tags__name",
+ lookup_expr="icontains",
+ help_text="Search for tags on a Engagement that contain a given pattern, and exclude them",
+ exclude=True)
+ not_test__engagement__tags = CharFilter(
+ label="Not Engagement Tag",
+ field_name="test__engagement__tags__name",
+ lookup_expr="iexact",
+ help_text="Search for tags on a Engagement that are an exact match, and exclude them",
+ exclude=True)
+ not_test__engagement__product__tags_contains = CharFilter(
+ label="Product Tag Does Not Contain",
+ field_name="test__engagement__product__tags__name",
+ lookup_expr="icontains",
+ help_text="Search for tags on a Product that contain a given pattern, and exclude them",
+ exclude=True)
+ not_test__engagement__product__tags = CharFilter(
+ label="Not Product Tag",
+ field_name="test__engagement__product__tags__name",
+ lookup_expr="iexact",
+ help_text="Search for tags on a Product that are an exact match, and exclude them",
+ exclude=True)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
@@ -720,6 +826,29 @@ class ProductComponentFilter(DojoFilter):
)
+class ComponentFilterWithoutObjectLookups(ProductComponentFilter):
+ test__engagement__product__prod_type__name = CharFilter(
+ field_name="test__engagement__product__prod_type__name",
+ lookup_expr="iexact",
+ label="Product Type Name",
+ help_text="Search for Product Type names that are an exact match")
+ test__engagement__product__prod_type__name_contains = CharFilter(
+ field_name="test__engagement__product__prod_type__name",
+ lookup_expr="icontains",
+ label="Product Type Name Contains",
+ help_text="Search for Product Type names that contain a given pattern")
+ test__engagement__product__name = CharFilter(
+ field_name="test__engagement__product__name",
+ lookup_expr="iexact",
+ label="Product Name",
+ help_text="Search for Product names that are an exact match")
+ test__engagement__product__name_contains = CharFilter(
+ field_name="test__engagement__product__name",
+ lookup_expr="icontains",
+ label="Product Name Contains",
+ help_text="Search for Product names that contain a given pattern")
+
+
class ComponentFilter(ProductComponentFilter):
test__engagement__product__prod_type = ModelMultipleChoiceFilter(
queryset=Product_Type.objects.none(),
@@ -736,125 +865,140 @@ def __init__(self, *args, **kwargs):
'test__engagement__product'].queryset = get_authorized_products(Permissions.Product_View)
-class EngagementDirectFilter(DojoFilter):
- name = CharFilter(lookup_expr='icontains', label='Engagement name contains')
- lead = ModelChoiceFilter(queryset=Dojo_User.objects.none(), label="Lead")
- version = CharFilter(field_name='version', lookup_expr='icontains', label='Engagement version')
- test__version = CharFilter(field_name='test__version', lookup_expr='icontains', label='Test version')
-
- product__name = CharFilter(lookup_expr='icontains', label='Product name contains')
- product__prod_type = ModelMultipleChoiceFilter(
- queryset=Product_Type.objects.none(),
- label="Product Type")
+class EngagementDirectFilterHelper(FilterSet):
+ name = CharFilter(lookup_expr="icontains", label="Engagement name contains")
+ version = CharFilter(field_name="version", lookup_expr="icontains", label="Engagement version")
+ test__version = CharFilter(field_name="test__version", lookup_expr="icontains", label="Test version")
+ product__name = CharFilter(lookup_expr="icontains", label="Product name contains")
+ status = MultipleChoiceFilter(choices=ENGAGEMENT_STATUS_CHOICES, label="Status")
+ tag = CharFilter(field_name="tags__name", lookup_expr="icontains", label="Tag name contains")
+ not_tag = CharFilter(field_name="tags__name", lookup_expr="icontains", label="Not tag name contains", exclude=True)
+ has_tags = BooleanFilter(field_name="tags", lookup_expr="isnull", exclude=True, label="Has tags")
test__engagement__product__lifecycle = MultipleChoiceFilter(
- choices=Product.LIFECYCLE_CHOICES, label='Product lifecycle', null_label='Empty')
- status = MultipleChoiceFilter(choices=ENGAGEMENT_STATUS_CHOICES,
- label="Status")
-
- tags = ModelMultipleChoiceFilter(
- field_name='tags__name',
- to_field_name='name',
- queryset=Engagement.tags.tag_model.objects.all().order_by('name'),
- # label='tags', # doesn't work with tagulous, need to set in __init__ below
- )
-
- tag = CharFilter(field_name='tags__name', lookup_expr='icontains', label='Tag name contains')
-
- not_tags = ModelMultipleChoiceFilter(
- field_name='tags__name',
- to_field_name='name',
- exclude=True,
- queryset=Engagement.tags.tag_model.objects.all().order_by('name'),
- # label='tags', # doesn't work with tagulous, need to set in __init__ below
- )
-
- not_tag = CharFilter(field_name='tags__name', lookup_expr='icontains', label='Not tag name contains', exclude=True)
-
- has_tags = BooleanFilter(field_name='tags', lookup_expr='isnull', exclude=True, label='Has tags')
-
+ choices=Product.LIFECYCLE_CHOICES,
+ label="Product lifecycle",
+ null_label="Empty")
o = OrderingFilter(
# tuple-mapping retains order
fields=(
- ('target_start', 'target_start'),
- ('name', 'name'),
- ('product__name', 'product__name'),
- ('product__prod_type__name', 'product__prod_type__name'),
- ('lead__first_name', 'lead__first_name'),
+ ("target_start", "target_start"),
+ ("name", "name"),
+ ("product__name", "product__name"),
+ ("product__prod_type__name", "product__prod_type__name"),
+ ("lead__first_name", "lead__first_name"),
),
field_labels={
- 'target_start': 'Start date',
- 'name': 'Engagement',
- 'product__name': 'Product Name',
- 'product__prod_type__name': 'Product Type',
- 'lead__first_name': 'Lead',
+ "target_start": "Start date",
+ "name": "Engagement",
+ "product__name": "Product Name",
+ "product__prod_type__name": "Product Type",
+ "lead__first_name": "Lead",
}
-
)
+
+class EngagementDirectFilter(EngagementDirectFilterHelper, DojoFilter):
+ lead = ModelChoiceFilter(queryset=Dojo_User.objects.none(), label="Lead")
+ product__prod_type = ModelMultipleChoiceFilter(
+ queryset=Product_Type.objects.none(),
+ label="Product Type")
+ tags = ModelMultipleChoiceFilter(
+ field_name="tags__name",
+ to_field_name="name",
+ queryset=Engagement.tags.tag_model.objects.all().order_by("name"))
+ not_tags = ModelMultipleChoiceFilter(
+ field_name="tags__name",
+ to_field_name="name",
+ exclude=True,
+ queryset=Engagement.tags.tag_model.objects.all().order_by("name"))
+
def __init__(self, *args, **kwargs):
super(EngagementDirectFilter, self).__init__(*args, **kwargs)
- self.form.fields['product__prod_type'].queryset = get_authorized_product_types(Permissions.Product_Type_View)
- self.form.fields['lead'].queryset = get_authorized_users(Permissions.Product_Type_View) \
+ self.form.fields["product__prod_type"].queryset = get_authorized_product_types(Permissions.Product_Type_View)
+ self.form.fields["lead"].queryset = get_authorized_users(Permissions.Product_Type_View) \
.filter(engagement__lead__isnull=False).distinct()
class Meta:
model = Engagement
- fields = ['product__name', 'product__prod_type']
+ fields = ["product__name", "product__prod_type"]
+
+
+class EngagementDirectFilterWithoutObjectLookups(EngagementDirectFilterHelper):
+ lead = CharFilter(
+ field_name="lead__username",
+ lookup_expr="iexact",
+ label="Lead Username",
+ help_text="Search for Lead username that are an exact match")
+ lead_contains = CharFilter(
+ field_name="lead__username",
+ lookup_expr="icontains",
+ label="Lead Username Contains",
+ help_text="Search for Lead username that contain a given pattern")
+ product__prod_type__name = CharFilter(
+ field_name="product__prod_type__name",
+ lookup_expr="iexact",
+ label="Product Type Name",
+ help_text="Search for Product Type names that are an exact match")
+ product__prod_type__name_contains = CharFilter(
+ field_name="product__prod_type__name",
+ lookup_expr="icontains",
+ label="Product Type Name Contains",
+ help_text="Search for Product Type names that contain a given pattern")
+ class Meta:
+ model = Engagement
+ fields = ["product__name"]
-class EngagementFilter(DojoFilter):
- engagement__name = CharFilter(lookup_expr='icontains', label='Engagement name contains')
- engagement__lead = ModelChoiceFilter(queryset=Dojo_User.objects.none(), label="Lead")
- engagement__version = CharFilter(field_name='engagement__version', lookup_expr='icontains', label='Engagement version')
- engagement__test__version = CharFilter(field_name='engagement__test__version', lookup_expr='icontains', label='Test version')
- name = CharFilter(lookup_expr='icontains', label='Product name contains')
- prod_type = ModelMultipleChoiceFilter(
- queryset=Product_Type.objects.none(),
- label="Product Type")
+class EngagementFilterHelper(FilterSet):
+ name = CharFilter(lookup_expr="icontains", label="Product name contains")
+ tag = CharFilter(field_name="tags__name", lookup_expr="icontains", label="Tag name contains")
+ not_tag = CharFilter(field_name="tags__name", lookup_expr="icontains", label="Not tag name contains", exclude=True)
+ has_tags = BooleanFilter(field_name="tags", lookup_expr="isnull", exclude=True, label="Has tags")
+ engagement__name = CharFilter(lookup_expr="icontains", label="Engagement name contains")
+ engagement__version = CharFilter(field_name="engagement__version", lookup_expr="icontains", label="Engagement version")
+ engagement__test__version = CharFilter(field_name="engagement__test__version", lookup_expr="icontains", label="Test version")
engagement__product__lifecycle = MultipleChoiceFilter(
- choices=Product.LIFECYCLE_CHOICES, label='Product lifecycle', null_label='Empty')
- engagement__status = MultipleChoiceFilter(choices=ENGAGEMENT_STATUS_CHOICES,
- label="Status")
-
- tags = ModelMultipleChoiceFilter(
- field_name='tags__name',
- to_field_name='name',
- queryset=Engagement.tags.tag_model.objects.all().order_by('name'),
- # label='tags', # doesn't work with tagulous, need to set in __init__ below
- )
-
- tag = CharFilter(field_name='tags__name', lookup_expr='icontains', label='Tag name contains')
-
- not_tags = ModelMultipleChoiceFilter(
- field_name='tags__name',
- to_field_name='name',
- exclude=True,
- queryset=Engagement.tags.tag_model.objects.all().order_by('name'),
- # label='tags', # doesn't work with tagulous, need to set in __init__ below
- )
-
- not_tag = CharFilter(field_name='tags__name', lookup_expr='icontains', label='Not tag name contains', exclude=True)
-
- has_tags = BooleanFilter(field_name='tags', lookup_expr='isnull', exclude=True, label='Has tags')
-
+ choices=Product.LIFECYCLE_CHOICES,
+ label="Product lifecycle",
+ null_label="Empty")
+ engagement__status = MultipleChoiceFilter(
+ choices=ENGAGEMENT_STATUS_CHOICES,
+ label="Status")
o = OrderingFilter(
# tuple-mapping retains order
fields=(
- ('name', 'name'),
- ('prod_type__name', 'prod_type__name'),
+ ("name", "name"),
+ ("prod_type__name", "prod_type__name"),
),
field_labels={
- 'name': 'Product Name',
- 'prod_type__name': 'Product Type',
+ "name": "Product Name",
+ "prod_type__name": "Product Type",
}
-
)
+
+class EngagementFilter(EngagementFilterHelper, DojoFilter):
+ engagement__lead = ModelChoiceFilter(
+ queryset=Dojo_User.objects.none(),
+ label="Lead")
+ prod_type = ModelMultipleChoiceFilter(
+ queryset=Product_Type.objects.none(),
+ label="Product Type")
+ tags = ModelMultipleChoiceFilter(
+ field_name="tags__name",
+ to_field_name="name",
+ queryset=Engagement.tags.tag_model.objects.all().order_by("name"))
+ not_tags = ModelMultipleChoiceFilter(
+ field_name="tags__name",
+ to_field_name="name",
+ exclude=True,
+ queryset=Engagement.tags.tag_model.objects.all().order_by("name"))
+
def __init__(self, *args, **kwargs):
super(EngagementFilter, self).__init__(*args, **kwargs)
- self.form.fields['prod_type'].queryset = get_authorized_product_types(Permissions.Product_Type_View)
- self.form.fields['engagement__lead'].queryset = get_authorized_users(Permissions.Product_Type_View) \
+ self.form.fields["prod_type"].queryset = get_authorized_product_types(Permissions.Product_Type_View)
+ self.form.fields["engagement__lead"].queryset = get_authorized_users(Permissions.Product_Type_View) \
.filter(engagement__lead__isnull=False).distinct()
class Meta:
@@ -862,37 +1006,42 @@ class Meta:
fields = ['name', 'prod_type']
-class ProductEngagementFilter(DojoFilter):
- lead = ModelChoiceFilter(queryset=Dojo_User.objects.none(), label="Lead")
+class EngagementFilterWithoutObjectLookups(EngagementFilterHelper):
+ engagement__lead = CharFilter(
+ field_name="engagement__lead__username",
+ lookup_expr="iexact",
+ label="Lead Username",
+ help_text="Search for Lead username that are an exact match")
+ engagement__lead_contains = CharFilter(
+ field_name="engagement__lead__username",
+ lookup_expr="icontains",
+ label="Lead Username Contains",
+ help_text="Search for Lead username that contain a given pattern")
+ prod_type__name = CharFilter(
+ field_name="prod_type__name",
+ lookup_expr="iexact",
+ label="Product Type Name",
+ help_text="Search for Product Type names that are an exact match")
+ prod_type__name_contains = CharFilter(
+ field_name="prod_type__name",
+ lookup_expr="icontains",
+ label="Product Type Name Contains",
+ help_text="Search for Product Type names that contain a given pattern")
+
+ class Meta:
+ model = Product
+ fields = ['name']
+
+
+class ProductEngagementFilterHelper(FilterSet):
version = CharFilter(lookup_expr='icontains', label='Engagement version')
test__version = CharFilter(field_name='test__version', lookup_expr='icontains', label='Test version')
-
name = CharFilter(lookup_expr='icontains')
- status = MultipleChoiceFilter(choices=ENGAGEMENT_STATUS_CHOICES,
- label="Status")
-
+ status = MultipleChoiceFilter(choices=ENGAGEMENT_STATUS_CHOICES, label="Status")
target_start = DateRangeFilter()
target_end = DateRangeFilter()
-
- tags = ModelMultipleChoiceFilter(
- field_name='tags__name',
- to_field_name='name',
- queryset=Engagement.tags.tag_model.objects.all().order_by('name'),
- # label='tags', # doesn't work with tagulous, need to set in __init__ below
- )
-
tag = CharFilter(field_name='tags__name', lookup_expr='icontains', label='Tag name contains')
-
- not_tags = ModelMultipleChoiceFilter(
- field_name='tags__name',
- to_field_name='name',
- exclude=True,
- queryset=Engagement.tags.tag_model.objects.all().order_by('name'),
- # label='tags', # doesn't work with tagulous, need to set in __init__ below
- )
-
not_tag = CharFilter(field_name='tags__name', lookup_expr='icontains', label='Not tag name contains', exclude=True)
-
o = OrderingFilter(
# tuple-mapping retains order
fields=(
@@ -906,19 +1055,44 @@ class ProductEngagementFilter(DojoFilter):
field_labels={
'name': 'Engagement Name',
}
-
)
- def __init__(self, *args, **kwargs):
- super(ProductEngagementFilter, self).__init__(*args, **kwargs)
- self.form.fields['lead'].queryset = get_authorized_users(Permissions.Product_Type_View) \
- .filter(engagement__lead__isnull=False).distinct()
-
class Meta:
model = Product
fields = ['name']
+class ProductEngagementFilter(ProductEngagementFilterHelper, DojoFilter):
+ lead = ModelChoiceFilter(queryset=Dojo_User.objects.none(), label="Lead")
+ tags = ModelMultipleChoiceFilter(
+ field_name="tags__name",
+ to_field_name="name",
+ queryset=Engagement.tags.tag_model.objects.all().order_by("name"))
+ not_tags = ModelMultipleChoiceFilter(
+ field_name="tags__name",
+ to_field_name="name",
+ exclude=True,
+ queryset=Engagement.tags.tag_model.objects.all().order_by("name"))
+
+ def __init__(self, *args, **kwargs):
+ super(ProductEngagementFilter, self).__init__(*args, **kwargs)
+ self.form.fields["lead"].queryset = get_authorized_users(
+ Permissions.Product_Type_View).filter(engagement__lead__isnull=False).distinct()
+
+
+class ProductEngagementFilterWithoutObjectLookups(ProductEngagementFilterHelper, DojoFilter):
+ lead = CharFilter(
+ field_name="lead__username",
+ lookup_expr="iexact",
+ label="Lead Username",
+ help_text="Search for Lead username that are an exact match")
+ lead_contains = CharFilter(
+ field_name="lead__username",
+ lookup_expr="icontains",
+ label="Lead Username Contains",
+ help_text="Search for Lead username that contain a given pattern")
+
+
class ApiEngagementFilter(DojoFilter):
product__prod_type = NumberInFilter(field_name='product__prod_type', lookup_expr='in')
tag = CharFilter(field_name='tags__name', lookup_expr='icontains', help_text='Tag name contains')
@@ -963,107 +1137,19 @@ class Meta:
'pen_test', 'status', 'product', 'name', 'version', 'tags']
-class ProductFilter(DojoFilter):
+class ProductFilterHelper(FilterSet):
name = CharFilter(lookup_expr='icontains', label="Product Name")
name_exact = CharFilter(field_name='name', lookup_expr='iexact', label="Exact Product Name")
- prod_type = ModelMultipleChoiceFilter(
- queryset=Product_Type.objects.none(),
- label="Product Type")
business_criticality = MultipleChoiceFilter(choices=Product.BUSINESS_CRITICALITY_CHOICES, null_label="Empty")
platform = MultipleChoiceFilter(choices=Product.PLATFORM_CHOICES, null_label="Empty")
lifecycle = MultipleChoiceFilter(choices=Product.LIFECYCLE_CHOICES, null_label="Empty")
origin = MultipleChoiceFilter(choices=Product.ORIGIN_CHOICES, null_label="Empty")
external_audience = BooleanFilter(field_name='external_audience')
internet_accessible = BooleanFilter(field_name='internet_accessible')
-
- # not specifying anything for tags will render a multiselect input functioning as OR
-
- tags = ModelMultipleChoiceFilter(
- field_name='tags__name',
- to_field_name='name',
- queryset=Product.tags.tag_model.objects.all().order_by('name'),
- # label='tags', # doesn't work with tagulous, need to set in __init__ below
- )
-
- # tags_and = ModelMultipleChoiceFilter(
- # field_name='tags__name',
- # to_field_name='name',
- # queryset=Product.tags.tag_model.objects.all().order_by('name'),
- # label='tags (AND)',
- # conjoined=True,
- # )
-
- # tags__name = ModelMultipleChoiceFilter(
- # queryset=Product.tags.tag_model.objects.all().order_by('name'),
- # label="tags (AND)"
- # )
-
tag = CharFilter(field_name='tags__name', lookup_expr='icontains', label="Tag contains")
-
- # tags__name = CharFilter(
- # lookup_expr='icontains',
- # label="Tag contains",
- # )
-
- # tags__all = ModelMultipleChoiceFilter(
- # queryset=Product.tags.tag_model.objects.all().order_by('name'),
- # field_name='tags__name',
- # label="tags (AND)"
- # )
-
- # not working below
-
- # tags = ModelMultipleChoiceFilter(
- # queryset=Product.tags.tag_model.objects.all().order_by('name'),
- # label="tags_widget", widget=TagWidget, tag_options=tagulous.models.TagOptions(
- # force_lowercase=True,)
- # ,)
-
- # tags__name = CharFilter(lookup_expr='icontains')
-
- # tags__and = ModelMultipleChoiceFilter(
- # field_name='tags__name',
- # to_field_name='name',
- # lookup_expr='in',
- # queryset=Product.tags.tag_model.objects.all().order_by('name'),
- # label="tags (AND)"
- # )
-
- # tags = ModelMultipleChoiceFilter(
- # queryset=Product.tags.tag_model.objects.all().order_by('name'),
- # label="tags (OR)"
- # )
-
- # tags = ModelMultipleChoiceFilter(
- # field_name='tags__name',
- # to_field_name='name',
- # queryset=Product.tags.tag_model.objects.all().order_by('name'),
- # label="tags (OR2)",
- # )
-
- # tags = ModelMultipleChoiceFilter(
- # field_name='tags',
- # to_field_name='name',
- # # lookup_expr='icontains', # nor working
- # # without lookup_expr we get an error: ValueError: invalid literal for int() with base 10: 'magento'
- # queryset=Product.tags.tag_model.objects.all().order_by('name'),
- # label="tags (OR3)",
- # )
-
- not_tags = ModelMultipleChoiceFilter(
- field_name='tags__name',
- to_field_name='name',
- exclude=True,
- queryset=Product.tags.tag_model.objects.all().order_by('name'),
- # label='tags', # doesn't work with tagulous, need to set in __init__ below
- )
-
not_tag = CharFilter(field_name='tags__name', lookup_expr='icontains', label='Not tag name contains', exclude=True)
-
outside_of_sla = ProductSLAFilter(label="Outside of SLA")
-
has_tags = BooleanFilter(field_name='tags', lookup_expr='isnull', exclude=True, label='Has tags')
-
o = OrderingFilter(
# tuple-mapping retains order
fields=(
@@ -1088,24 +1174,61 @@ class ProductFilter(DojoFilter):
'external_audience': 'External Audience ',
'internet_accessible': 'Internet Accessible ',
}
-
)
- # tags = CharFilter(lookup_expr='icontains', label="Tags")
+
+class ProductFilter(ProductFilterHelper, DojoFilter):
+ prod_type = ModelMultipleChoiceFilter(
+ queryset=Product_Type.objects.none(),
+ label="Product Type")
+ tags = ModelMultipleChoiceFilter(
+ field_name="tags__name",
+ to_field_name="name",
+ queryset=Product.tags.tag_model.objects.all().order_by("name"))
+ not_tags = ModelMultipleChoiceFilter(
+ field_name="tags__name",
+ to_field_name="name",
+ exclude=True,
+ queryset=Product.tags.tag_model.objects.all().order_by("name"))
def __init__(self, *args, **kwargs):
self.user = None
- if 'user' in kwargs:
- self.user = kwargs.pop('user')
-
+ if "user" in kwargs:
+ self.user = kwargs.pop("user")
super(ProductFilter, self).__init__(*args, **kwargs)
+ self.form.fields["prod_type"].queryset = get_authorized_product_types(Permissions.Product_Type_View)
+
+ class Meta:
+ model = Product
+ fields = [
+ "name", "name_exact", "prod_type", "business_criticality",
+ "platform", "lifecycle", "origin", "external_audience",
+ "internet_accessible", "tags"
+ ]
+
+
+class ProductFilterWithoutObjectLookups(ProductFilterHelper):
+ prod_type__name = CharFilter(
+ field_name="prod_type__name",
+ lookup_expr="iexact",
+ label="Product Type Name",
+ help_text="Search for Product Type names that are an exact match")
+ prod_type__name_contains = CharFilter(
+ field_name="prod_type__name",
+ lookup_expr="icontains",
+ label="Product Type Name Contains",
+ help_text="Search for Product Type names that contain a given pattern")
- self.form.fields['prod_type'].queryset = get_authorized_product_types(Permissions.Product_Type_View)
+ def __init__(self, *args, **kwargs):
+ kwargs.pop("user", None)
+ super(ProductFilterWithoutObjectLookups, self).__init__(*args, **kwargs)
class Meta:
model = Product
- fields = ['name', 'name_exact', 'prod_type', 'business_criticality', 'platform', 'lifecycle', 'origin', 'external_audience',
- 'internet_accessible', 'tags']
+ fields = [
+ "name", "name_exact", "business_criticality", "platform",
+ "lifecycle", "origin", "external_audience", "internet_accessible",
+ ]
class ApiProductFilter(DojoFilter):
@@ -1338,156 +1461,91 @@ def filter(self, qs, value):
return super().filter(qs, value)
-class FindingFilter(FindingFilterWithTags):
- # tag = CharFilter(field_name='tags__name', lookup_expr='icontains', label='Tag name contains')
- title = CharFilter(lookup_expr='icontains')
+class FindingFilterHelper(FilterSet):
+ title = CharFilter(lookup_expr="icontains")
date = DateRangeFilter()
- on = DateFilter(field_name='date', lookup_expr='exact', label='On')
- before = DateFilter(field_name='date', lookup_expr='lt', label='Before')
- after = DateFilter(field_name='date', lookup_expr='gt', label='After')
+ on = DateFilter(field_name="date", lookup_expr="exact", label="On")
+ before = DateFilter(field_name="date", lookup_expr="lt", label="Before")
+ after = DateFilter(field_name="date", lookup_expr="gt", label="After")
last_reviewed = DateRangeFilter()
last_status_update = DateRangeFilter()
cwe = MultipleChoiceFilter(choices=[])
- vulnerability_id = CharFilter(method=vulnerability_id_filter, label='Vulnerability Id')
+ vulnerability_id = CharFilter(method=vulnerability_id_filter, label="Vulnerability Id")
severity = MultipleChoiceFilter(choices=SEVERITY_CHOICES)
- test__test_type = ModelMultipleChoiceFilter(
- queryset=Test_Type.objects.all(), label='Test Type')
-
duplicate = ReportBooleanFilter()
is_mitigated = ReportBooleanFilter()
mitigated = DateRangeFilter(label="Mitigated Date")
-
planned_remediation_date = DateRangeOmniFilter()
- planned_remediation_version = CharFilter(lookup_expr='icontains', label=_('Planned remediation version'))
-
- file_path = CharFilter(lookup_expr='icontains')
- param = CharFilter(lookup_expr='icontains')
- payload = CharFilter(lookup_expr='icontains')
-
- reporter = ModelMultipleChoiceFilter(
- queryset=Dojo_User.objects.none())
-
- reviewers = ModelMultipleChoiceFilter(
- queryset=Dojo_User.objects.none())
-
- test__engagement__product__prod_type = ModelMultipleChoiceFilter(
- queryset=Product_Type.objects.none(),
- label="Product Type")
-
- test__engagement__product__lifecycle = MultipleChoiceFilter(
- choices=Product.LIFECYCLE_CHOICES, label='Product lifecycle')
-
- test__engagement__product = ModelMultipleChoiceFilter(
- queryset=Product.objects.none(),
- label="Product")
- test__engagement = ModelMultipleChoiceFilter(
- queryset=Engagement.objects.none(),
- label="Engagement")
-
- endpoints__host = CharFilter(lookup_expr='icontains', label="Endpoint Host")
-
- service = CharFilter(lookup_expr='icontains')
-
- test = ModelMultipleChoiceFilter(
- queryset=Test.objects.none(),
- label="Test")
-
- test__engagement__version = CharFilter(lookup_expr='icontains', label="Engagement Version")
- test__version = CharFilter(lookup_expr='icontains', label="Test Version")
-
- status = FindingStatusFilter(label='Status')
-
- if is_finding_groups_enabled():
- finding_group = ModelMultipleChoiceFilter(
- queryset=Finding_Group.objects.none(),
- label="Finding Group")
-
- has_finding_group = BooleanFilter(field_name='finding_group',
- lookup_expr='isnull',
- exclude=True,
- label='Is Grouped')
-
- risk_acceptance = ReportRiskAcceptanceFilter(
- label="Risk Accepted")
-
+ planned_remediation_version = CharFilter(lookup_expr="icontains", label=_("Planned remediation version"))
+ file_path = CharFilter(lookup_expr="icontains")
+ param = CharFilter(lookup_expr="icontains")
+ payload = CharFilter(lookup_expr="icontains")
+ test__test_type = ModelMultipleChoiceFilter(queryset=Test_Type.objects.all(), label='Test Type')
+ endpoints__host = CharFilter(lookup_expr="icontains", label="Endpoint Host")
+ service = CharFilter(lookup_expr="icontains")
+ test__engagement__version = CharFilter(lookup_expr="icontains", label="Engagement Version")
+ test__version = CharFilter(lookup_expr="icontains", label="Test Version")
+ risk_acceptance = ReportRiskAcceptanceFilter(label="Risk Accepted")
effort_for_fixing = MultipleChoiceFilter(choices=EFFORT_FOR_FIXING_CHOICES)
-
test_import_finding_action__test_import = NumberFilter(widget=HiddenInput())
endpoints = NumberFilter(widget=HiddenInput())
-
- if get_system_setting('enable_jira'):
- has_jira_issue = BooleanFilter(field_name='jira_issue',
- lookup_expr='isnull',
- exclude=True,
- label='Has JIRA')
- jira_creation = DateRangeFilter(field_name='jira_issue__jira_creation', label='JIRA Creation')
- jira_change = DateRangeFilter(field_name='jira_issue__jira_change', label='JIRA Updated')
- jira_issue__jira_key = CharFilter(field_name='jira_issue__jira_key', lookup_expr='icontains', label="JIRA issue")
-
- if is_finding_groups_enabled():
- has_jira_group_issue = BooleanFilter(field_name='finding_group__jira_issue',
- lookup_expr='isnull',
- exclude=True,
- label='Has Group JIRA')
-
- has_component = BooleanFilter(field_name='component_name',
- lookup_expr='isnull',
- exclude=True,
- label='Has Component')
-
- has_notes = BooleanFilter(field_name='notes',
- lookup_expr='isnull',
- exclude=True,
- label='Has notes')
-
- not_tags = ModelMultipleChoiceFilter(
- field_name='tags__name',
- to_field_name='name',
- exclude=True,
- queryset=Finding.tags.tag_model.objects.all().order_by('name'),
- # label='tags', # doesn't work with tagulous, need to set in __init__ below
- )
-
- not_test__tags = ModelMultipleChoiceFilter(
- field_name='test__tags__name',
- to_field_name='name',
+ status = FindingStatusFilter(label="Status")
+ has_component = BooleanFilter(
+ field_name="component_name",
+ lookup_expr="isnull",
exclude=True,
- label='Test without tags',
- queryset=Test.tags.tag_model.objects.all().order_by('name'),
- # label='tags', # doesn't work with tagulous, need to set in __init__ below
- )
-
- not_test__engagement__tags = ModelMultipleChoiceFilter(
- field_name='test__engagement__tags__name',
- to_field_name='name',
+ label="Has Component")
+ has_notes = BooleanFilter(
+ field_name="notes",
+ lookup_expr="isnull",
exclude=True,
- label='Engagement without tags',
- queryset=Engagement.tags.tag_model.objects.all().order_by('name'),
- # label='tags', # doesn't work with tagulous, need to set in __init__ below
- )
+ label="Has notes")
- not_test__engagement__product__tags = ModelMultipleChoiceFilter(
- field_name='test__engagement__product__tags__name',
- to_field_name='name',
- exclude=True,
- label='Product without tags',
- queryset=Product.tags.tag_model.objects.all().order_by('name'),
- # label='tags', # doesn't work with tagulous, need to set in __init__ below
- )
+ if is_finding_groups_enabled():
+ has_finding_group = BooleanFilter(
+ field_name='finding_group',
+ lookup_expr='isnull',
+ exclude=True,
+ label='Is Grouped')
+
+ if get_system_setting("enable_jira"):
+ has_jira_issue = BooleanFilter(
+ field_name="jira_issue",
+ lookup_expr="isnull",
+ exclude=True,
+ label="Has JIRA")
+ jira_creation = DateRangeFilter(field_name="jira_issue__jira_creation", label="JIRA Creation")
+ jira_change = DateRangeFilter(field_name="jira_issue__jira_change", label="JIRA Updated")
+ jira_issue__jira_key = CharFilter(field_name="jira_issue__jira_key", lookup_expr="icontains", label="JIRA issue")
- not_tag = CharFilter(field_name='tags__name', lookup_expr='icontains', label='Not tag name contains', exclude=True)
+ if is_finding_groups_enabled():
+ has_jira_group_issue = BooleanFilter(
+ field_name="finding_group__jira_issue",
+ lookup_expr="isnull",
+ exclude=True,
+ label="Has Group JIRA")
outside_of_sla = FindingSLAFilter(label="Outside of SLA")
-
- has_tags = BooleanFilter(field_name='tags', lookup_expr='isnull', exclude=True, label='Has tags')
-
- epss_score = PercentageFilter(field_name='epss_score', label='EPSS score')
- epss_score_range = PercentageRangeFilter(field_name='epss_score', label='EPSS score range',
- help_text='The range of EPSS score percentages to filter on; the left input is a lower bound, the right is an upper bound. Leaving one empty will skip that bound (e.g., leaving the lower bound input empty will filter only on the upper bound -- filtering on "less than or equal").')
-
- epss_percentile = PercentageFilter(field_name='epss_percentile', label='EPSS percentile')
- epss_percentile_range = PercentageRangeFilter(field_name='epss_percentile', label='EPSS percentile range',
- help_text='The range of EPSS percentiles to filter on; the left input is a lower bound, the right is an upper bound. Leaving one empty will skip that bound (e.g., leaving the lower bound input empty will filter only on the upper bound -- filtering on "less than or equal").')
+ has_tags = BooleanFilter(field_name="tags", lookup_expr="isnull", exclude=True, label="Has tags")
+ epss_score = PercentageFilter(field_name="epss_score", label="EPSS score")
+ epss_score_range = PercentageRangeFilter(
+ field_name="epss_score",
+ label="EPSS score range",
+ help_text=(
+ "The range of EPSS score percentages to filter on; the left input is a lower bound, "
+ "the right is an upper bound. Leaving one empty will skip that bound (e.g., leaving "
+ "the lower bound input empty will filter only on the upper bound -- filtering on "
+ "\"less than or equal\")."
+ ))
+ epss_percentile = PercentageFilter(field_name="epss_percentile", label="EPSS percentile")
+ epss_percentile_range = PercentageRangeFilter(
+ field_name="epss_percentile",
+ label="EPSS percentile range",
+ help_text=(
+ "The range of EPSS percentiles to filter on; the left input is a lower bound, the right "
+ "is an upper bound. Leaving one empty will skip that bound (e.g., leaving the lower bound "
+ "input empty will filter only on the upper bound -- filtering on \"less than or equal\")."
+ ))
o = OrderingFilter(
# tuple-mapping retains order
@@ -1517,9 +1575,94 @@ class FindingFilter(FindingFilterWithTags):
}
)
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+
+ def set_date_fields(self, *args: list, **kwargs: dict):
+ date_input_widget = forms.DateInput(attrs={'class': 'datepicker', 'placeholder': 'YYYY-MM-DD'}, format="%Y-%m-%d")
+ self.form.fields['on'].widget = date_input_widget
+ self.form.fields['before'].widget = date_input_widget
+ self.form.fields['after'].widget = date_input_widget
+ self.form.fields['cwe'].choices = cwe_options(self.queryset)
+
+
+class FindingFilterWithoutObjectLookups(FindingFilterHelper, FindingTagStringFilter):
+ reporter = CharFilter(
+ field_name="reporter__username",
+ lookup_expr="iexact",
+ label="Reporter Username",
+ help_text="Search for Reporter names that are an exact match")
+ reporter_contains = CharFilter(
+ field_name="reporter__username",
+ lookup_expr="icontains",
+ label="Reporter Username Contains",
+ help_text="Search for Reporter names that contain a given pattern")
+ reviewer = CharFilter(
+ field_name="reviewer__username",
+ lookup_expr="iexact",
+ label="Reviewer Username",
+ help_text="Search for Reviewer names that are an exact match")
+ reviewer_contains = CharFilter(
+ field_name="reviewer__username",
+ lookup_expr="icontains",
+ label="Reviewer Username Contains",
+ help_text="Search for Reviewer usernames that contain a given pattern")
+ test__engagement__product__prod_type__name = CharFilter(
+ field_name="test__engagement__product__prod_type__name",
+ lookup_expr="iexact",
+ label="Product Type Name",
+ help_text="Search for Product Type names that are an exact match")
+ test__engagement__product__prod_type__name_contains = CharFilter(
+ field_name="test__engagement__product__prod_type__name",
+ lookup_expr="icontains",
+ label="Product Type Name Contains",
+ help_text="Search for Product Type names that contain a given pattern")
+ test__engagement__product__name = CharFilter(
+ field_name="test__engagement__product__name",
+ lookup_expr="iexact",
+ label="Product Name",
+ help_text="Search for Product names that are an exact match")
+ test__engagement__product__name_contains = CharFilter(
+ field_name="test__engagement__product__name",
+ lookup_expr="icontains",
+ label="Product name Contains",
+ help_text="Search for Product Typ names that contain a given pattern")
+ test__engagement__name = CharFilter(
+ field_name="test__engagement__name",
+ lookup_expr="iexact",
+ label="Engagement Name",
+ help_text="Search for Engagement names that are an exact match")
+ test__engagement__name_contains = CharFilter(
+ field_name="test__engagement__name",
+ lookup_expr="icontains",
+ label="Engagement name Contains",
+ help_text="Search for Engagement names that contain a given pattern")
+ test__name = CharFilter(
+ field_name="test__engagement__name",
+ lookup_expr="iexact",
+ label="Test Name",
+ help_text="Search for Test names that are an exact match")
+ test__name_contains = CharFilter(
+ field_name="test__engagement__name",
+ lookup_expr="icontains",
+ label="Test name Contains",
+ help_text="Search for Test names that contain a given pattern")
+
+ if is_finding_groups_enabled():
+ finding_group__name = CharFilter(
+ field_name="finding_group__name",
+ lookup_expr="iexact",
+ label="Finding Group Name",
+ help_text="Search for Finding Group names that are an exact match")
+ finding_group__name_contains = CharFilter(
+ field_name="finding_group__name",
+ lookup_expr="icontains",
+ label="Finding Group Name Contains",
+ help_text="Search for Finding Group names that contain a given pattern")
+
class Meta:
model = Finding
- fields = get_finding_filterset_fields()
+ fields = get_finding_filterset_fields(filter_string_matching=True)
exclude = ['url', 'description', 'mitigation', 'impact',
'endpoints', 'references',
@@ -1538,15 +1681,71 @@ def __init__(self, *args, **kwargs):
if 'pid' in kwargs:
self.pid = kwargs.pop('pid')
super().__init__(*args, **kwargs)
+ # Set some date fields
+ self.set_date_fields(*args, **kwargs)
+ # Don't show the product filter on the product finding view
+ if self.pid:
+ del self.form.fields['test__engagement__product__name']
+ del self.form.fields['test__engagement__product__name_contains']
+ del self.form.fields['test__engagement__product__prod_type__name']
+ del self.form.fields['test__engagement__product__prod_type__name_contains']
+ else:
+ del self.form.fields['test__name']
+ del self.form.fields['test__name_contains']
- self.form.fields['cwe'].choices = cwe_options(self.queryset)
- date_input_widget = forms.DateInput(attrs={'class': 'datepicker', 'placeholder': 'YYYY-MM-DD'}, format="%Y-%m-%d")
- self.form.fields['on'].widget = date_input_widget
- self.form.fields['before'].widget = date_input_widget
- self.form.fields['after'].widget = date_input_widget
+class FindingFilter(FindingFilterHelper, FindingTagFilter):
+ reporter = ModelMultipleChoiceFilter(queryset=Dojo_User.objects.none())
+ reviewers = ModelMultipleChoiceFilter(queryset=Dojo_User.objects.none())
+ test__engagement__product__prod_type = ModelMultipleChoiceFilter(
+ queryset=Product_Type.objects.none(),
+ label="Product Type")
+ test__engagement__product__lifecycle = MultipleChoiceFilter(
+ choices=Product.LIFECYCLE_CHOICES,
+ label='Product lifecycle')
+ test__engagement__product = ModelMultipleChoiceFilter(
+ queryset=Product.objects.none(),
+ label="Product")
+ test__engagement = ModelMultipleChoiceFilter(
+ queryset=Engagement.objects.none(),
+ label="Engagement")
+ test = ModelMultipleChoiceFilter(
+ queryset=Test.objects.none(),
+ label="Test")
+
+ if is_finding_groups_enabled():
+ finding_group = ModelMultipleChoiceFilter(
+ queryset=Finding_Group.objects.none(),
+ label="Finding Group")
+
+ class Meta:
+ model = Finding
+ fields = get_finding_filterset_fields()
+
+ exclude = ['url', 'description', 'mitigation', 'impact',
+ 'endpoints', 'references',
+ 'thread_id', 'notes', 'scanner_confidence',
+ 'numerical_severity', 'line', 'duplicate_finding',
+ 'hash_code', 'reviewers', 'created', 'files',
+ 'sla_start_date', 'sla_expiration_date', 'cvssv3',
+ 'severity_justification', 'steps_to_reproduce',]
+
+ def __init__(self, *args, **kwargs):
+ self.user = None
+ self.pid = None
+ if 'user' in kwargs:
+ self.user = kwargs.pop('user')
+
+ if 'pid' in kwargs:
+ self.pid = kwargs.pop('pid')
+ super().__init__(*args, **kwargs)
+ # Set some date fields
+ self.set_date_fields(*args, **kwargs)
# Don't show the product filter on the product finding view
- if self.pid:
+ self.set_related_object_fields(*args, **kwargs)
+
+ def set_related_object_fields(self, *args: list, **kwargs: dict):
+ if self.pid is not None:
del self.form.fields['test__engagement__product']
del self.form.fields['test__engagement__product__prod_type']
# TODO add authorized check to be sure
@@ -1569,18 +1768,13 @@ def __init__(self, *args, **kwargs):
class AcceptedFindingFilter(FindingFilter):
- risk_acceptance__created__date = \
- DateRangeFilter(label="Acceptance Date")
-
- risk_acceptance__owner = \
- ModelMultipleChoiceFilter(
+ risk_acceptance__created__date = DateRangeFilter(label="Acceptance Date")
+ risk_acceptance__owner = ModelMultipleChoiceFilter(
queryset=Dojo_User.objects.none(),
label="Risk Acceptance Owner")
-
risk_acceptance = ModelMultipleChoiceFilter(
queryset=Risk_Acceptance.objects.none(),
- label="Accepted By"
- )
+ label="Accepted By")
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
@@ -1588,24 +1782,35 @@ def __init__(self, *args, **kwargs):
self.form.fields['risk_acceptance'].queryset = get_authorized_risk_acceptances(Permissions.Risk_Acceptance)
-class SimilarFindingFilter(FindingFilter):
+class AcceptedFindingFilterWithoutObjectLookups(FindingFilterWithoutObjectLookups):
+ risk_acceptance__created__date = DateRangeFilter(label="Acceptance Date")
+ risk_acceptance__owner = CharFilter(
+ field_name="risk_acceptance__owner__username",
+ lookup_expr="iexact",
+ label="Risk Acceptance Owner Username",
+ help_text="Search for Risk Acceptance Owners username that are an exact match")
+ risk_acceptance__owner_contains = CharFilter(
+ field_name="risk_acceptance__owner__username",
+ lookup_expr="icontains",
+ label="Risk Acceptance Owner Username Contains",
+ help_text="Search for Risk Acceptance Owners username that contain a given pattern")
+ risk_acceptance__name = CharFilter(
+ field_name="risk_acceptance__name",
+ lookup_expr="iexact",
+ label="Risk Acceptance Name",
+ help_text="Search for Risk Acceptance name that are an exact match")
+ risk_acceptance__name_contains = CharFilter(
+ field_name="risk_acceptance__name",
+ lookup_expr="icontains",
+ label="Risk Acceptance Name",
+ help_text="Search for Risk Acceptance name contain a given pattern")
+
+
+class SimilarFindingHelper(FilterSet):
hash_code = MultipleChoiceFilter()
vulnerability_ids = CharFilter(method=custom_vulnerability_id_filter, label='Vulnerability Ids')
- class Meta(FindingFilter.Meta):
- model = Finding
- # slightly different fields from FindingFilter, but keep the same ordering for UI consistency
- fields = get_finding_filterset_fields(similar=True)
-
- def __init__(self, data=None, *args, **kwargs):
- self.user = None
- if 'user' in kwargs:
- self.user = kwargs.pop('user')
-
- self.finding = None
- if 'finding' in kwargs:
- self.finding = kwargs.pop('finding')
-
+ def update_data(self, data: dict, *args: list, **kwargs: dict):
# if filterset is bound, use initial values as defaults
# because of this, we can't rely on the self.form.has_changed
self.has_changed = True
@@ -1624,18 +1829,53 @@ def __init__(self, data=None, *args, **kwargs):
self.has_changed = False
- super().__init__(data, *args, **kwargs)
-
+ def set_hash_codes(self, *args: list, **kwargs: dict):
if self.finding and self.finding.hash_code:
self.form.fields['hash_code'] = forms.MultipleChoiceField(choices=[(self.finding.hash_code, self.finding.hash_code[:24] + '...')], required=False, initial=[])
- def filter_queryset(self, *args, **kwargs):
+ def filter_queryset(self, *args: list, **kwargs: dict):
queryset = super().filter_queryset(*args, **kwargs)
queryset = get_authorized_findings(Permissions.Finding_View, queryset, self.user)
queryset = queryset.exclude(pk=self.finding.pk)
return queryset
+class SimilarFindingFilter(FindingFilter, SimilarFindingHelper):
+ class Meta(FindingFilter.Meta):
+ model = Finding
+ # slightly different fields from FindingFilter, but keep the same ordering for UI consistency
+ fields = get_finding_filterset_fields(similar=True)
+
+ def __init__(self, data=None, *args, **kwargs):
+ self.user = None
+ if 'user' in kwargs:
+ self.user = kwargs.pop('user')
+ self.finding = None
+ if 'finding' in kwargs:
+ self.finding = kwargs.pop('finding')
+ self.update_data(data, *args, **kwargs)
+ super().__init__(data, *args, **kwargs)
+ self.set_hash_codes(*args, **kwargs)
+
+
+class SimilarFindingFilterWithoutObjectLookups(FindingFilterWithoutObjectLookups, SimilarFindingHelper):
+ class Meta(FindingFilterWithoutObjectLookups.Meta):
+ model = Finding
+ # slightly different fields from FindingFilter, but keep the same ordering for UI consistency
+ fields = get_finding_filterset_fields(similar=True, filter_string_matching=True)
+
+ def __init__(self, data=None, *args, **kwargs):
+ self.user = None
+ if 'user' in kwargs:
+ self.user = kwargs.pop('user')
+ self.finding = None
+ if 'finding' in kwargs:
+ self.finding = kwargs.pop('finding')
+ self.update_data(data, *args, **kwargs)
+ super().__init__(data, *args, **kwargs)
+ self.set_hash_codes(*args, **kwargs)
+
+
class TemplateFindingFilter(DojoFilter):
title = CharFilter(lookup_expr='icontains')
cwe = MultipleChoiceFilter(choices=[])
@@ -1772,30 +2012,11 @@ class Meta(FindingFilter.Meta):
fields = get_finding_filterset_fields(metrics=True)
-class MetricsEndpointFilter(FilterSet):
+class MetricsFindingFilterWithoutObjectLookups(FindingFilterWithoutObjectLookups):
start_date = DateFilter(field_name='date', label='Start Date', lookup_expr=('gt'))
end_date = DateFilter(field_name='date', label='End Date', lookup_expr=('lt'))
date = MetricsDateRangeFilter()
- finding__test__engagement__product__prod_type = ModelMultipleChoiceFilter(
- queryset=Product_Type.objects.none(),
- label="Product Type")
- finding__test__engagement = ModelMultipleChoiceFilter(
- queryset=Engagement.objects.none(),
- label="Engagement")
- finding__test__engagement__version = CharFilter(lookup_expr='icontains', label="Engagement Version")
- finding__severity = MultipleChoiceFilter(choices=SEVERITY_CHOICES, label="Severity")
-
- endpoint__host = CharFilter(lookup_expr='icontains', label="Endpoint Host")
- finding_title = CharFilter(lookup_expr='icontains', label="Finding Title")
-
- tags = ModelMultipleChoiceFilter(
- field_name='tags__name',
- to_field_name='name',
- queryset=Endpoint.tags.tag_model.objects.all().order_by('name'),
- # label='tags', # doesn't work with tagulous, need to set in __init__ below
- )
-
- tag = CharFilter(field_name='tags__name', lookup_expr='icontains', label='Tag name contains')
+ vulnerability_id = CharFilter(method=vulnerability_id_filter, label='Vulnerability Id')
not_tags = ModelMultipleChoiceFilter(
field_name='tags__name',
@@ -1807,78 +2028,276 @@ class MetricsEndpointFilter(FilterSet):
not_tag = CharFilter(field_name='tags__name', lookup_expr='icontains', label='Not tag name contains', exclude=True)
- not_tags = ModelMultipleChoiceFilter(
- field_name='tags__name',
+ def __init__(self, *args, **kwargs):
+ if args[0]:
+ if args[0].get('start_date', '') != '' or args[0].get('end_date', '') != '':
+ args[0]._mutable = True
+ args[0]['date'] = 8
+ args[0]._mutable = False
+
+ super().__init__(*args, **kwargs)
+
+ class Meta(FindingFilterWithoutObjectLookups.Meta):
+ model = Finding
+ fields = get_finding_filterset_fields(metrics=True, filter_string_matching=True)
+
+
+class MetricsEndpointFilterHelper(FilterSet):
+ start_date = DateFilter(field_name="date", label="Start Date", lookup_expr=("gt"))
+ end_date = DateFilter(field_name="date", label="End Date", lookup_expr=("lt"))
+ date = MetricsDateRangeFilter()
+ finding__test__engagement__version = CharFilter(lookup_expr="icontains", label="Engagement Version")
+ finding__severity = MultipleChoiceFilter(choices=SEVERITY_CHOICES, label="Severity")
+ endpoint__host = CharFilter(lookup_expr="icontains", label="Endpoint Host")
+ finding_title = CharFilter(lookup_expr="icontains", label="Finding Title")
+ tag = CharFilter(field_name="tags__name", lookup_expr="icontains", label="Tag name contains")
+ not_tag = CharFilter(field_name="tags__name", lookup_expr="icontains", label="Not tag name contains", exclude=True)
+
+
+class MetricsEndpointFilter(MetricsEndpointFilterHelper):
+ finding__test__engagement__product__prod_type = ModelMultipleChoiceFilter(
+ queryset=Product_Type.objects.none(),
+ label="Product Type")
+ finding__test__engagement = ModelMultipleChoiceFilter(
+ queryset=Engagement.objects.none(),
+ label="Engagement")
+ endpoint__tags = ModelMultipleChoiceFilter(
+ field_name='endpoint__tags__name',
+ to_field_name='name',
+ label='Endpoint tags',
+ queryset=Endpoint.tags.tag_model.objects.all().order_by('name'))
+ finding__tags = ModelMultipleChoiceFilter(
+ field_name='finding__tags__name',
+ to_field_name='name',
+ label='Finding tags',
+ queryset=Finding.tags.tag_model.objects.all().order_by('name'))
+ finding__test__tags = ModelMultipleChoiceFilter(
+ field_name='finding__test__tags__name',
+ to_field_name='name',
+ label='Test tags',
+ queryset=Test.tags.tag_model.objects.all().order_by('name'))
+ finding__test__engagement__tags = ModelMultipleChoiceFilter(
+ field_name='finding__test__engagement__tags__name',
+ to_field_name='name',
+ label='Engagement tags',
+ queryset=Engagement.tags.tag_model.objects.all().order_by('name'))
+ finding__test__engagement__product__tags = ModelMultipleChoiceFilter(
+ field_name='finding__test__engagement__product__tags__name',
+ to_field_name='name',
+ label='Product tags',
+ queryset=Product.tags.tag_model.objects.all().order_by('name'))
+ not_endpoint__tags = ModelMultipleChoiceFilter(
+ field_name='endpoint__tags__name',
to_field_name='name',
exclude=True,
- queryset=Finding.tags.tag_model.objects.all().order_by('name'),
- # label='tags', # doesn't work with tagulous, need to set in __init__ below
- )
-
- not_test__tags = ModelMultipleChoiceFilter(
- field_name='test__tags__name',
+ label='Endpoint without tags',
+ queryset=Endpoint.tags.tag_model.objects.all().order_by('name'))
+ not_finding__tags = ModelMultipleChoiceFilter(
+ field_name='finding__tags__name',
+ to_field_name='name',
+ exclude=True,
+ label='Finding without tags',
+ queryset=Finding.tags.tag_model.objects.all().order_by('name'))
+ not_finding__test__tags = ModelMultipleChoiceFilter(
+ field_name='finding__test__tags__name',
to_field_name='name',
exclude=True,
label='Test without tags',
- queryset=Test.tags.tag_model.objects.all().order_by('name'),
- # label='tags', # doesn't work with tagulous, need to set in __init__ below
- )
-
- not_test__engagement__tags = ModelMultipleChoiceFilter(
- field_name='test__engagement__tags__name',
+ queryset=Test.tags.tag_model.objects.all().order_by('name'))
+ not_finding__test__engagement__tags = ModelMultipleChoiceFilter(
+ field_name='finding__test__engagement__tags__name',
to_field_name='name',
exclude=True,
label='Engagement without tags',
- queryset=Engagement.tags.tag_model.objects.all().order_by('name'),
- # label='tags', # doesn't work with tagulous, need to set in __init__ below
- )
-
- not_test__engagement__product__tags = ModelMultipleChoiceFilter(
- field_name='test__engagement__product__tags__name',
+ queryset=Engagement.tags.tag_model.objects.all().order_by('name'))
+ not_finding__test__engagement__product__tags = ModelMultipleChoiceFilter(
+ field_name='finding__test__engagement__product__tags__name',
to_field_name='name',
exclude=True,
label='Product without tags',
- queryset=Product.tags.tag_model.objects.all().order_by('name'),
- # label='tags', # doesn't work with tagulous, need to set in __init__ below
- )
-
- not_tag = CharFilter(field_name='tags__name', lookup_expr='icontains', label='Not tag name contains', exclude=True)
-
- tag = CharFilter(field_name='tags__name', lookup_expr='icontains', label='Tag name contains')
+ queryset=Product.tags.tag_model.objects.all().order_by('name'))
def __init__(self, *args, **kwargs):
if args[0]:
- if args[0].get('start_date', '') != '' or args[0].get('end_date', '') != '':
+ if args[0].get("start_date", "") != "" or args[0].get("end_date", "") != "":
args[0]._mutable = True
- args[0]['date'] = 8
+ args[0]["date"] = 8
args[0]._mutable = False
self.pid = None
- if 'pid' in kwargs:
- self.pid = kwargs.pop('pid')
+ if "pid" in kwargs:
+ self.pid = kwargs.pop("pid")
super().__init__(*args, **kwargs)
if self.pid:
- del self.form.fields['finding__test__engagement__product__prod_type']
- self.form.fields['finding__test__engagement'].queryset = Engagement.objects.filter(
+ del self.form.fields["finding__test__engagement__product__prod_type"]
+ self.form.fields["finding__test__engagement"].queryset = Engagement.objects.filter(
product_id=self.pid
).all()
else:
- self.form.fields['finding__test__engagement'].queryset = get_authorized_engagements(Permissions.Engagement_View).order_by('name')
+ self.form.fields["finding__test__engagement"].queryset = get_authorized_engagements(Permissions.Engagement_View).order_by("name")
- if 'finding__test__engagement__product__prod_type' in self.form.fields:
+ if "finding__test__engagement__product__prod_type" in self.form.fields:
self.form.fields[
- 'finding__test__engagement__product__prod_type'].queryset = get_authorized_product_types(Permissions.Product_Type_View)
+ "finding__test__engagement__product__prod_type"].queryset = get_authorized_product_types(Permissions.Product_Type_View)
class Meta:
model = Endpoint_Status
- exclude = ['last_modified', 'endpoint', 'finding']
+ exclude = ["last_modified", "endpoint", "finding"]
+
+
+class MetricsEndpointFilterWithoutObjectLookups(MetricsEndpointFilterHelper, FindingTagStringFilter):
+ finding__test__engagement__product__prod_type = CharFilter(
+ field_name="finding__test__engagement__product__prod_type",
+ lookup_expr="iexact",
+ label="Product Type Name",
+ help_text="Search for Product Type names that are an exact match")
+ finding__test__engagement__product__prod_type_contains = CharFilter(
+ field_name="finding__test__engagement__product__prod_type",
+ lookup_expr="icontains",
+ label="Product Type Name Contains",
+ help_text="Search for Product Type names that contain a given pattern")
+ finding__test__engagement = CharFilter(
+ field_name="finding__test__engagement",
+ lookup_expr="iexact",
+ label="Engagement Name",
+ help_text="Search for Engagement names that are an exact match")
+ finding__test__engagement_contains = CharFilter(
+ field_name="finding__test__engagement",
+ lookup_expr="icontains",
+ label="Engagement Name Contains",
+ help_text="Search for Engagement names that contain a given pattern")
+ endpoint__tags_contains = CharFilter(
+ label="Endpoint Tag Contains",
+ field_name="endpoint__tags__name",
+ lookup_expr="icontains",
+ help_text="Search for tags on a Endpoint that contain a given pattern")
+ endpoint__tags = CharFilter(
+ label="Endpoint Tag",
+ field_name="endpoint__tags__name",
+ lookup_expr="iexact",
+ help_text="Search for tags on a Endpoint that are an exact match")
+ finding__tags_contains = CharFilter(
+ label="Finding Tag Contains",
+ field_name="finding__tags__name",
+ lookup_expr="icontains",
+ help_text="Search for tags on a Finding that contain a given pattern")
+ finding__tags = CharFilter(
+ label="Finding Tag",
+ field_name="finding__tags__name",
+ lookup_expr="iexact",
+ help_text="Search for tags on a Finding that are an exact match")
+ finding__test__tags_contains = CharFilter(
+ label="Test Tag Contains",
+ field_name="finding__test__tags__name",
+ lookup_expr="icontains",
+ help_text="Search for tags on a Finding that contain a given pattern")
+ finding__test__tags = CharFilter(
+ label="Test Tag",
+ field_name="finding__test__tags__name",
+ lookup_expr="iexact",
+ help_text="Search for tags on a Finding that are an exact match")
+ finding__test__engagement__tags_contains = CharFilter(
+ label="Engagement Tag Contains",
+ field_name="finding__test__engagement__tags__name",
+ lookup_expr="icontains",
+ help_text="Search for tags on a Finding that contain a given pattern")
+ finding__test__engagement__tags = CharFilter(
+ label="Engagement Tag",
+ field_name="finding__test__engagement__tags__name",
+ lookup_expr="iexact",
+ help_text="Search for tags on a Finding that are an exact match")
+ finding__test__engagement__product__tags_contains = CharFilter(
+ label="Product Tag Contains",
+ field_name="finding__test__engagement__product__tags__name",
+ lookup_expr="icontains",
+ help_text="Search for tags on a Finding that contain a given pattern")
+ finding__test__engagement__product__tags = CharFilter(
+ label="Product Tag",
+ field_name="finding__test__engagement__product__tags__name",
+ lookup_expr="iexact",
+ help_text="Search for tags on a Finding that are an exact match")
+
+ not_endpoint__tags_contains = CharFilter(
+ label="Endpoint Tag Does Not Contain",
+ field_name="endpoint__tags__name",
+ lookup_expr="icontains",
+ help_text="Search for tags on a Endpoint that contain a given pattern, and exclude them",
+ exclude=True)
+ not_endpoint__tags = CharFilter(
+ label="Not Endpoint Tag",
+ field_name="endpoint__tags__name",
+ lookup_expr="iexact",
+ help_text="Search for tags on a Endpoint that are an exact match, and exclude them",
+ exclude=True)
+ not_finding__tags_contains = CharFilter(
+ label="Finding Tag Does Not Contain",
+ field_name="finding__tags__name",
+ lookup_expr="icontains",
+ help_text="Search for tags on a Finding that contain a given pattern, and exclude them",
+ exclude=True)
+ not_finding__tags = CharFilter(
+ label="Not Finding Tag",
+ field_name="finding__tags__name",
+ lookup_expr="iexact",
+ help_text="Search for tags on a Finding that are an exact match, and exclude them",
+ exclude=True)
+ not_finding__test__tags_contains = CharFilter(
+ label="Test Tag Does Not Contain",
+ field_name="finding__test__tags__name",
+ lookup_expr="icontains",
+ help_text="Search for tags on a Test that contain a given pattern, and exclude them",
+ exclude=True)
+ not_finding__test__tags = CharFilter(
+ label="Not Test Tag",
+ field_name="finding__test__tags__name",
+ lookup_expr="iexact",
+ help_text="Search for tags on a Test that are an exact match, and exclude them",
+ exclude=True)
+ not_finding__test__engagement__tags_contains = CharFilter(
+ label="Engagement Tag Does Not Contain",
+ field_name="finding__test__engagement__tags__name",
+ lookup_expr="icontains",
+ help_text="Search for tags on a Engagement that contain a given pattern, and exclude them",
+ exclude=True)
+ not_finding__test__engagement__tags = CharFilter(
+ label="Not Engagement Tag",
+ field_name="finding__test__engagement__tags__name",
+ lookup_expr="iexact",
+ help_text="Search for tags on a Engagement that are an exact match, and exclude them",
+ exclude=True)
+ not_finding__test__engagement__product__tags_contains = CharFilter(
+ label="Product Tag Does Not Contain",
+ field_name="finding__test__engagement__product__tags__name",
+ lookup_expr="icontains",
+ help_text="Search for tags on a Product that contain a given pattern, and exclude them",
+ exclude=True)
+ not_finding__test__engagement__product__tags = CharFilter(
+ label="Not Product Tag",
+ field_name="finding__test__engagement__product__tags__name",
+ lookup_expr="iexact",
+ help_text="Search for tags on a Product that are an exact match, and exclude them",
+ exclude=True)
+ def __init__(self, *args, **kwargs):
+ if args[0]:
+ if args[0].get("start_date", "") != "" or args[0].get("end_date", "") != "":
+ args[0]._mutable = True
+ args[0]["date"] = 8
+ args[0]._mutable = False
+ self.pid = None
+ if "pid" in kwargs:
+ self.pid = kwargs.pop("pid")
+ super().__init__(*args, **kwargs)
+ if self.pid:
+ del self.form.fields["finding__test__engagement__product__prod_type"]
-class EndpointFilter(DojoFilter):
- product = ModelMultipleChoiceFilter(
- queryset=Product.objects.none(),
- label="Product")
+ class Meta:
+ model = Endpoint_Status
+ exclude = ["last_modified", "endpoint", "finding"]
+
+
+class EndpointFilterHelper(FilterSet):
protocol = CharFilter(lookup_expr='icontains')
userinfo = CharFilter(lookup_expr='icontains')
host = CharFilter(lookup_expr='icontains')
@@ -1886,65 +2305,77 @@ class EndpointFilter(DojoFilter):
path = CharFilter(lookup_expr='icontains')
query = CharFilter(lookup_expr='icontains')
fragment = CharFilter(lookup_expr='icontains')
-
- tags = ModelMultipleChoiceFilter(
- field_name='tags__name',
- to_field_name='name',
- queryset=Endpoint.tags.tag_model.objects.all().order_by('name'),
- # label='tags', # doesn't work with tagulous, need to set in __init__ below
+ tag = CharFilter(field_name='tags__name', lookup_expr='icontains', label='Tag name contains')
+ not_tag = CharFilter(field_name='tags__name', lookup_expr='icontains', label='Not tag name contains', exclude=True)
+ has_tags = BooleanFilter(field_name='tags', lookup_expr='isnull', exclude=True, label='Has tags')
+ o = OrderingFilter(
+ # tuple-mapping retains order
+ fields=(
+ ('product', 'product'),
+ ('host', 'host'),
+ ),
)
- tag = CharFilter(field_name='tags__name', lookup_expr='icontains', label='Tag name contains')
+class EndpointFilter(EndpointFilterHelper, DojoFilter):
+ product = ModelMultipleChoiceFilter(
+ queryset=Product.objects.none(),
+ label="Product")
tags = ModelMultipleChoiceFilter(
field_name='tags__name',
to_field_name='name',
- queryset=Finding.tags.tag_model.objects.all().order_by('name'),
- # label='tags', # doesn't work with tagulous, need to set in __init__ below
- )
-
- test__tags = ModelMultipleChoiceFilter(
- field_name='test__tags__name',
+ label="Endpoint Tags",
+ queryset=Endpoint.tags.tag_model.objects.all().order_by('name'))
+ findings__tags = ModelMultipleChoiceFilter(
+ field_name='findings__tags__name',
to_field_name='name',
- queryset=Test.tags.tag_model.objects.all().order_by('name'),
- # label='tags', # doesn't work with tagulous, need to set in __init__ below
- )
-
- test__engagement__tags = ModelMultipleChoiceFilter(
- field_name='test__engagement__tags__name',
+ label="Finding Tags",
+ queryset=Finding.tags.tag_model.objects.all().order_by('name'))
+ findings__test__tags = ModelMultipleChoiceFilter(
+ field_name='findings__test__tags__name',
to_field_name='name',
- queryset=Engagement.tags.tag_model.objects.all().order_by('name'),
- # label='tags', # doesn't work with tagulous, need to set in __init__ below
- )
-
- test__engagement__product__tags = ModelMultipleChoiceFilter(
- field_name='test__engagement__product__tags__name',
+ label="Test Tags",
+ queryset=Test.tags.tag_model.objects.all().order_by('name'))
+ findings__test__engagement__tags = ModelMultipleChoiceFilter(
+ field_name='findings__test__engagement__tags__name',
to_field_name='name',
- queryset=Product.tags.tag_model.objects.all().order_by('name'),
- # label='tags', # doesn't work with tagulous, need to set in __init__ below
- )
-
- tag = CharFilter(field_name='tags__name', lookup_expr='icontains', label='Tag name contains')
-
+ label="Engagement Tags",
+ queryset=Engagement.tags.tag_model.objects.all().order_by('name'))
+ findings__test__engagement__product__tags = ModelMultipleChoiceFilter(
+ field_name='findings__test__engagement__product__tags__name',
+ to_field_name='name',
+ label="Product Tags",
+ queryset=Product.tags.tag_model.objects.all().order_by('name'))
not_tags = ModelMultipleChoiceFilter(
field_name='tags__name',
to_field_name='name',
+ label="Not Endpoint Tags",
exclude=True,
- queryset=Finding.tags.tag_model.objects.all().order_by('name'),
- # label='tags', # doesn't work with tagulous, need to set in __init__ below
- )
-
- not_tag = CharFilter(field_name='tags__name', lookup_expr='icontains', label='Not tag name contains', exclude=True)
-
- has_tags = BooleanFilter(field_name='tags', lookup_expr='isnull', exclude=True, label='Has tags')
-
- o = OrderingFilter(
- # tuple-mapping retains order
- fields=(
- ('product', 'product'),
- ('host', 'host'),
- ),
- )
+ queryset=Endpoint.tags.tag_model.objects.all().order_by('name'))
+ not_findings__tags = ModelMultipleChoiceFilter(
+ field_name='findings__tags__name',
+ to_field_name='name',
+ label="Not Finding Tags",
+ exclude=True,
+ queryset=Finding.tags.tag_model.objects.all().order_by('name'))
+ not_findings__test__tags = ModelMultipleChoiceFilter(
+ field_name='findings__test__tags__name',
+ to_field_name='name',
+ label="Not Test Tags",
+ exclude=True,
+ queryset=Test.tags.tag_model.objects.all().order_by('name'))
+ not_findings__test__engagement__tags = ModelMultipleChoiceFilter(
+ field_name='findings__test__engagement__tags__name',
+ to_field_name='name',
+ label="Not Engagement Tags",
+ exclude=True,
+ queryset=Engagement.tags.tag_model.objects.all().order_by('name'))
+ not_findings__test__engagement__product__tags = ModelMultipleChoiceFilter(
+ field_name='findings__test__engagement__product__tags__name',
+ to_field_name='name',
+ label="Not Product Tags",
+ exclude=True,
+ queryset=Product.tags.tag_model.objects.all().order_by('name'))
def __init__(self, *args, **kwargs):
self.user = None
@@ -1960,7 +2391,147 @@ def qs(self):
class Meta:
model = Endpoint
- exclude = ['findings']
+ exclude = ["findings", "inherited_tags"]
+
+
+class EndpointFilterWithoutObjectLookups(EndpointFilterHelper):
+ product__name = CharFilter(
+ field_name="product__name",
+ lookup_expr="iexact",
+ label="Product Name",
+ help_text="Search for Product names that are an exact match")
+ product__name_contains = CharFilter(
+ field_name="product__name",
+ lookup_expr="icontains",
+ label="Product Name Contains",
+ help_text="Search for Product names that contain a given pattern")
+
+ tags_contains = CharFilter(
+ label="Endpoint Tag Contains",
+ field_name="tags__name",
+ lookup_expr="icontains",
+ help_text="Search for tags on a Endpoint that contain a given pattern")
+ tags = CharFilter(
+ label="Endpoint Tag",
+ field_name="tags__name",
+ lookup_expr="iexact",
+ help_text="Search for tags on a Endpoint that are an exact match")
+ findings__tags_contains = CharFilter(
+ label="Finding Tag Contains",
+ field_name="findings__tags__name",
+ lookup_expr="icontains",
+ help_text="Search for tags on a Finding that contain a given pattern")
+ findings__tags = CharFilter(
+ label="Finding Tag",
+ field_name="findings__tags__name",
+ lookup_expr="iexact",
+ help_text="Search for tags on a Finding that are an exact match")
+ findings__test__tags_contains = CharFilter(
+ label="Test Tag Contains",
+ field_name="findings__test__tags__name",
+ lookup_expr="icontains",
+ help_text="Search for tags on a Finding that contain a given pattern")
+ findings__test__tags = CharFilter(
+ label="Test Tag",
+ field_name="findings__test__tags__name",
+ lookup_expr="iexact",
+ help_text="Search for tags on a Finding that are an exact match")
+ findings__test__engagement__tags_contains = CharFilter(
+ label="Engagement Tag Contains",
+ field_name="findings__test__engagement__tags__name",
+ lookup_expr="icontains",
+ help_text="Search for tags on a Finding that contain a given pattern")
+ findings__test__engagement__tags = CharFilter(
+ label="Engagement Tag",
+ field_name="findings__test__engagement__tags__name",
+ lookup_expr="iexact",
+ help_text="Search for tags on a Finding that are an exact match")
+ findings__test__engagement__product__tags_contains = CharFilter(
+ label="Product Tag Contains",
+ field_name="findings__test__engagement__product__tags__name",
+ lookup_expr="icontains",
+ help_text="Search for tags on a Finding that contain a given pattern")
+ findings__test__engagement__product__tags = CharFilter(
+ label="Product Tag",
+ field_name="findings__test__engagement__product__tags__name",
+ lookup_expr="iexact",
+ help_text="Search for tags on a Finding that are an exact match")
+
+ not_tags_contains = CharFilter(
+ label="Endpoint Tag Does Not Contain",
+ field_name="tags__name",
+ lookup_expr="icontains",
+ help_text="Search for tags on a Endpoint that contain a given pattern, and exclude them",
+ exclude=True)
+ not_tags = CharFilter(
+ label="Not Endpoint Tag",
+ field_name="tags__name",
+ lookup_expr="iexact",
+ help_text="Search for tags on a Endpoint that are an exact match, and exclude them",
+ exclude=True)
+ not_findings__tags_contains = CharFilter(
+ label="Finding Tag Does Not Contain",
+ field_name="findings__tags__name",
+ lookup_expr="icontains",
+ help_text="Search for tags on a Finding that contain a given pattern, and exclude them",
+ exclude=True)
+ not_findings__tags = CharFilter(
+ label="Not Finding Tag",
+ field_name="findings__tags__name",
+ lookup_expr="iexact",
+ help_text="Search for tags on a Finding that are an exact match, and exclude them",
+ exclude=True)
+ not_findings__test__tags_contains = CharFilter(
+ label="Test Tag Does Not Contain",
+ field_name="findings__test__tags__name",
+ lookup_expr="icontains",
+ help_text="Search for tags on a Test that contain a given pattern, and exclude them",
+ exclude=True)
+ not_findings__test__tags = CharFilter(
+ label="Not Test Tag",
+ field_name="findings__test__tags__name",
+ lookup_expr="iexact",
+ help_text="Search for tags on a Test that are an exact match, and exclude them",
+ exclude=True)
+ not_findings__test__engagement__tags_contains = CharFilter(
+ label="Engagement Tag Does Not Contain",
+ field_name="findings__test__engagement__tags__name",
+ lookup_expr="icontains",
+ help_text="Search for tags on a Engagement that contain a given pattern, and exclude them",
+ exclude=True)
+ not_findings__test__engagement__tags = CharFilter(
+ label="Not Engagement Tag",
+ field_name="findings__test__engagement__tags__name",
+ lookup_expr="iexact",
+ help_text="Search for tags on a Engagement that are an exact match, and exclude them",
+ exclude=True)
+ not_findings__test__engagement__product__tags_contains = CharFilter(
+ label="Product Tag Does Not Contain",
+ field_name="findings__test__engagement__product__tags__name",
+ lookup_expr="icontains",
+ help_text="Search for tags on a Product that contain a given pattern, and exclude them",
+ exclude=True)
+ not_findings__test__engagement__product__tags = CharFilter(
+ label="Not Product Tag",
+ field_name="findings__test__engagement__product__tags__name",
+ lookup_expr="iexact",
+ help_text="Search for tags on a Product that are an exact match, and exclude them",
+ exclude=True)
+
+ def __init__(self, *args, **kwargs):
+ self.user = None
+ if 'user' in kwargs:
+ self.user = kwargs.pop('user')
+ super(EndpointFilterWithoutObjectLookups, self).__init__(*args, **kwargs)
+
+ @property
+ def qs(self):
+ parent = super(EndpointFilterWithoutObjectLookups, self).qs
+ return get_authorized_endpoints(Permissions.Endpoint_View, parent)
+
+ class Meta:
+ model = Endpoint
+ exclude = ["findings", "inherited_tags", "product"]
class ApiEndpointFilter(DojoFilter):
@@ -2004,37 +2575,15 @@ class Meta:
]
-class EngagementTestFilter(DojoFilter):
- lead = ModelChoiceFilter(queryset=Dojo_User.objects.none(), label="Lead")
+class EngagementTestFilterHelper(FilterSet):
version = CharFilter(lookup_expr='icontains', label='Version')
-
if settings.TRACK_IMPORT_HISTORY:
test_import__version = CharFilter(field_name='test_import__version', lookup_expr='icontains', label='Reimported Version')
-
target_start = DateRangeFilter()
target_end = DateRangeFilter()
-
- tags = ModelMultipleChoiceFilter(
- field_name='tags__name',
- to_field_name='name',
- queryset=Test.tags.tag_model.objects.all().order_by('name'),
- # label='tags', # doesn't work with tagulous, need to set in __init__ below
- )
-
tag = CharFilter(field_name='tags__name', lookup_expr='icontains', label='Tag name contains')
-
- not_tags = ModelMultipleChoiceFilter(
- field_name='tags__name',
- to_field_name='name',
- exclude=True,
- queryset=Test.tags.tag_model.objects.all().order_by('name'),
- # label='tags', # doesn't work with tagulous, need to set in __init__ below
- )
-
not_tag = CharFilter(field_name='tags__name', lookup_expr='icontains', label='Not tag name contains', exclude=True)
-
has_tags = BooleanFilter(field_name='tags', lookup_expr='isnull', exclude=True, label='Has tags')
-
o = OrderingFilter(
# tuple-mapping retains order
fields=(
@@ -2048,14 +2597,31 @@ class EngagementTestFilter(DojoFilter):
field_labels={
'name': 'Test Name',
}
-
)
+
+class EngagementTestFilter(EngagementTestFilterHelper, DojoFilter):
+ lead = ModelChoiceFilter(queryset=Dojo_User.objects.none(), label="Lead")
+ api_scan_configuration = ModelChoiceFilter(
+ queryset=Product_API_Scan_Configuration.objects.none(),
+ label="API Scan Configuration")
+ tags = ModelMultipleChoiceFilter(
+ field_name="tags__name",
+ to_field_name="name",
+ queryset=Test.tags.tag_model.objects.all().order_by("name"))
+ not_tags = ModelMultipleChoiceFilter(
+ field_name="tags__name",
+ to_field_name="name",
+ exclude=True,
+ queryset=Test.tags.tag_model.objects.all().order_by("name"))
+
class Meta:
model = Test
- fields = ['id', 'title', 'test_type', 'target_start',
- 'target_end', 'percent_complete',
- 'version', 'api_scan_configuration']
+ fields = [
+ "title", "test_type", "target_start",
+ "target_end", "percent_complete",
+ "version", "api_scan_configuration",
+ ]
def __init__(self, *args, **kwargs):
self.engagement = kwargs.pop('engagement')
@@ -2066,6 +2632,63 @@ def __init__(self, *args, **kwargs):
.filter(test__lead__isnull=False).distinct()
+class EngagementTestFilterWithoutObjectLookups(EngagementTestFilterHelper):
+ lead = CharFilter(
+ field_name="lead__username",
+ lookup_expr="iexact",
+ label="Lead Username",
+ help_text="Search for Lead username that are an exact match")
+ lead_contains = CharFilter(
+ field_name="lead__username",
+ lookup_expr="icontains",
+ label="Lead Username Contains",
+ help_text="Search for Lead username that contain a given pattern")
+ api_scan_configuration__tool_configuration__name = CharFilter(
+ field_name="api_scan_configuration__tool_configuration__name",
+ lookup_expr="iexact",
+ label="API Scan Configuration Name",
+ help_text="Search for Lead username that are an exact match")
+ api_scan_configuration__tool_configuration__name_contains = CharFilter(
+ field_name="api_scan_configuration__tool_configuration__name",
+ lookup_expr="icontains",
+ label="API Scan Configuration Name Contains",
+ help_text="Search for Lead username that contain a given pattern")
+ tags_contains = CharFilter(
+ label="Test Tag Contains",
+ field_name="tags__name",
+ lookup_expr="icontains",
+ help_text="Search for tags on a Test that contain a given pattern")
+ tags = CharFilter(
+ label="Test Tag",
+ field_name="tags__name",
+ lookup_expr="iexact",
+ help_text="Search for tags on a Test that are an exact match")
+ not_tags_contains = CharFilter(
+ label="Test Tag Does Not Contain",
+ field_name="tags__name",
+ lookup_expr="icontains",
+ help_text="Search for tags on a Test that contain a given pattern, and exclude them",
+ exclude=True)
+ not_tags = CharFilter(
+ label="Not Test Tag",
+ field_name="tags__name",
+ lookup_expr="iexact",
+ help_text="Search for tags on a Test that are an exact match, and exclude them",
+ exclude=True)
+
+ class Meta:
+ model = Test
+ fields = [
+ "title", "test_type", "target_start",
+ "target_end", "percent_complete", "version",
+ ]
+
+ def __init__(self, *args, **kwargs):
+ self.engagement = kwargs.pop('engagement')
+ super(EngagementTestFilterWithoutObjectLookups, self).__init__(*args, **kwargs)
+ self.form.fields['test_type'].queryset = Test_Type.objects.filter(test__engagement=self.engagement).distinct().order_by('name')
+
+
class ApiTestFilter(DojoFilter):
tag = CharFilter(field_name='tags__name', lookup_expr='icontains', help_text='Tag name contains')
tags = CharFieldInFilter(field_name='tags__name', lookup_expr='in',
@@ -2175,7 +2798,7 @@ class Meta:
exclude = ['product']
-class ReportFindingFilter(FindingFilterWithTags):
+class ReportFindingFilter(FindingTagFilter):
title = CharFilter(lookup_expr='icontains', label='Name')
test__engagement__product = ModelMultipleChoiceFilter(
queryset=Product.objects.none(), label="Product")
diff --git a/dojo/finding/views.py b/dojo/finding/views.py
index 08ce201491d..9faef2cffc5 100644
--- a/dojo/finding/views.py
+++ b/dojo/finding/views.py
@@ -39,8 +39,11 @@
from dojo.filters import (
TemplateFindingFilter,
SimilarFindingFilter,
+ SimilarFindingFilterWithoutObjectLookups,
FindingFilter,
+ FindingFilterWithoutObjectLookups,
AcceptedFindingFilter,
+ AcceptedFindingFilterWithoutObjectLookups,
TestImportFindingActionFilter,
TestImportFilter,
)
@@ -345,10 +348,13 @@ def filter_findings_by_form(self, request: HttpRequest, findings: QuerySet[Findi
"pid": self.get_product_id(),
}
+ filter_string_matching = get_system_setting("filter_string_matching", False)
+ finding_filter_class = FindingFilterWithoutObjectLookups if filter_string_matching else FindingFilter
+ accepted_finding_filter_class = AcceptedFindingFilterWithoutObjectLookups if filter_string_matching else AcceptedFindingFilter
return (
- AcceptedFindingFilter(*args, **kwargs)
+ accepted_finding_filter_class(*args, **kwargs)
if self.get_filter_name() == "Accepted"
- else FindingFilter(*args, **kwargs)
+ else finding_filter_class(*args, **kwargs)
)
def get_filtered_findings(self):
@@ -596,6 +602,14 @@ def get_test_import_data(self, request: HttpRequest, finding: Finding):
}
def get_similar_findings(self, request: HttpRequest, finding: Finding):
+ similar_findings_enabled = get_system_setting("enable_similar_findings", True)
+ if similar_findings_enabled is False:
+ return {
+ "similar_findings_enabled": similar_findings_enabled,
+ "duplicate_cluster": duplicate_cluster(request, finding),
+ "similar_findings": None,
+ "similar_findings_filter": None,
+ }
# add related actions for non-similar and non-duplicate cluster members
finding.related_actions = calculate_possible_related_actions_for_similar_finding(
request, finding, finding
@@ -606,7 +620,9 @@ def get_similar_findings(self, request: HttpRequest, finding: Finding):
request, finding, finding.duplicate_finding
)
)
- similar_findings_filter = SimilarFindingFilter(
+ filter_string_matching = get_system_setting("filter_string_matching", False)
+ finding_filter_class = SimilarFindingFilterWithoutObjectLookups if filter_string_matching else SimilarFindingFilter
+ similar_findings_filter = finding_filter_class(
request.GET,
queryset=get_authorized_findings(Permissions.Finding_View),
user=request.user,
@@ -630,6 +646,7 @@ def get_similar_findings(self, request: HttpRequest, finding: Finding):
)
return {
+ "similar_findings_enabled": similar_findings_enabled,
"duplicate_cluster": duplicate_cluster(request, finding),
"similar_findings": similar_findings,
"similar_findings_filter": similar_findings_filter,
diff --git a/dojo/finding_group/views.py b/dojo/finding_group/views.py
index e6f92a71d27..2a5df5a775b 100644
--- a/dojo/finding_group/views.py
+++ b/dojo/finding_group/views.py
@@ -1,8 +1,8 @@
-from dojo.utils import Product_Tab, add_breadcrumb, get_words_for_field, get_page_items
+from dojo.utils import Product_Tab, add_breadcrumb, get_words_for_field, get_page_items, get_system_setting
from dojo.forms import DeleteFindingGroupForm, EditFindingGroupForm, FindingBulkUpdateForm
from dojo.notifications.helper import create_notification
from dojo.finding.views import prefetch_for_findings
-from dojo.filters import FindingFilter
+from dojo.filters import FindingFilter, FindingFilterWithoutObjectLookups
from django.contrib import messages
from django.contrib.admin.utils import NestedObjects
from django.db.utils import DEFAULT_DB_ALIAS
@@ -25,13 +25,14 @@ def view_finding_group(request, fgid):
finding_group = get_object_or_404(Finding_Group, pk=fgid)
findings = finding_group.findings.all()
edit_finding_group_form = EditFindingGroupForm(instance=finding_group)
+ filter_string_matching = get_system_setting("filter_string_matching", False)
+ finding_filter_class = FindingFilterWithoutObjectLookups if filter_string_matching else FindingFilter
show_product_column = True
custom_breadcrumb = None
product_tab = None
jira_project = None
github_config = None
-
if finding_group.test.engagement.product.id:
pid = finding_group.test.engagement.product.id
product = get_object_or_404(Product, id=pid)
@@ -39,7 +40,7 @@ def view_finding_group(request, fgid):
product_tab = Product_Tab(product, title="Findings", tab="findings")
jira_project = jira_helper.get_jira_project(product)
github_config = GITHUB_PKey.objects.filter(product=pid).first()
- findings_filter = FindingFilter(request.GET, findings, user=request.user, pid=pid)
+ findings_filter = finding_filter_class(request.GET, findings, user=request.user, pid=pid)
elif finding_group.test.engagement.id:
eid = finding_group.test.engagement.id
engagement = get_object_or_404(Engagement, id=eid)
@@ -47,7 +48,7 @@ def view_finding_group(request, fgid):
product_tab = Product_Tab(engagement.product, title=engagement.name, tab="engagements")
jira_project = jira_helper.get_jira_project(engagement)
github_config = GITHUB_PKey.objects.filter(product__engagement=eid).first()
- findings_filter = FindingFilter(request.GET, findings, user=request.user, eid=eid)
+ findings_filter = finding_filter_class(request.GET, findings, user=request.user, eid=eid)
title_words = get_words_for_field(Finding, 'title')
component_words = get_words_for_field(Finding, 'component_name')
diff --git a/dojo/jira_link/views.py b/dojo/jira_link/views.py
index 2f3ff1d3c30..e652b8703e4 100644
--- a/dojo/jira_link/views.py
+++ b/dojo/jira_link/views.py
@@ -7,7 +7,7 @@
from django.contrib.admin.utils import NestedObjects
from django.urls import reverse
from django.db import DEFAULT_DB_ALIAS
-from django.http import HttpResponseRedirect, HttpResponse, Http404, HttpResponseBadRequest
+from django.http import HttpResponseRedirect, HttpResponse, Http404
from django.shortcuts import render, get_object_or_404
from django.utils import timezone
from django.utils.dateparse import parse_datetime
@@ -15,8 +15,8 @@
from django.core.exceptions import PermissionDenied
# Local application/library imports
from dojo.forms import JIRAForm, DeleteJIRAInstanceForm, ExpressJIRAForm
-from dojo.models import User, JIRA_Instance, JIRA_Issue, Notes
-from dojo.utils import add_breadcrumb, add_error_message_to_response, get_system_setting
+from dojo.models import System_Settings, User, JIRA_Instance, JIRA_Issue, Notes
+from dojo.utils import add_breadcrumb, add_error_message_to_response
from dojo.notifications.helper import create_notification
from django.views.decorators.http import require_POST
import dojo.jira_link.helper as jira_helper
@@ -26,114 +26,140 @@
logger = logging.getLogger(__name__)
-# for examples of incoming json, see the unit tests for the webhook: https://github.com/DefectDojo/django-DefectDojo/blob/master/unittests/test_jira_webhook.py
-# or the officials docs (which are not always clear): https://developer.atlassian.com/server/jira/platform/webhooks/
+def webhook_responser_handler(
+ log_level: str,
+ message: str,
+) -> HttpResponse:
+ # These represent an error and will be sent to the debugger
+ # for development purposes
+ if log_level == "info":
+ logger.info(message)
+ # These are more common in misconfigurations and have a better
+ # chance of being seen by a user
+ elif log_level == "debug":
+ logger.debug(message)
+ # Return the response with the code
+ return HttpResponse(message, status=200)
+
+
@csrf_exempt
@require_POST
def webhook(request, secret=None):
- if not get_system_setting('enable_jira'):
- logger.debug('ignoring incoming webhook as JIRA is disabled.')
- raise Http404('JIRA disabled')
- elif not get_system_setting('enable_jira_web_hook'):
- logger.debug('ignoring incoming webhook as JIRA Webhook is disabled.')
- raise Http404('JIRA Webhook disabled')
- elif not get_system_setting('disable_jira_webhook_secret'):
- if not get_system_setting('jira_webhook_secret'):
- logger.warning('ignoring incoming webhook as JIRA Webhook secret is empty in Defect Dojo system settings.')
- raise PermissionDenied('JIRA Webhook secret cannot be empty')
- if secret != get_system_setting('jira_webhook_secret'):
- logger.warning('invalid secret provided to JIRA Webhook')
- raise PermissionDenied('invalid or no secret provided to JIRA Webhook')
+ """
+ for examples of incoming json, see the unit tests for the webhook:
+ https://github.com/DefectDojo/django-DefectDojo/blob/master/unittests/test_jira_webhook.py
+ or the officials docs (which are not always clear):
+ https://developer.atlassian.com/server/jira/platform/webhooks/
+ All responses here will return a 201 so that we may have control over the
+ logging level
+ """
+ # Make sure the request is a POST, otherwise, we reject
+ if request.method != "POST":
+ return webhook_responser_handler("debug", "Only POST requests are supported")
+ # Determine if th webhook is in use or not
+ system_settings = System_Settings.objects.get()
+ # If the jira integration is not enabled, then return a 404
+ if not system_settings.enable_jira:
+ return webhook_responser_handler("info", "Ignoring incoming webhook as JIRA is disabled.")
+ # If the webhook is not enabled, then return a 404
+ elif not system_settings.enable_jira_web_hook:
+ return webhook_responser_handler("info", "Ignoring incoming webhook as JIRA Webhook is disabled.")
+ # Determine if the request should be "authenticated"
+ elif not system_settings.disable_jira_webhook_secret:
+ # Make sure there is a value for the webhook secret before making a comparison
+ if not system_settings.jira_webhook_secret:
+ return webhook_responser_handler("info", "Ignoring incoming webhook as JIRA Webhook secret is empty in Defect Dojo system settings.")
+ # Make sure the secret supplied in the path of the webhook request matches the
+ # secret supplied in the system settings
+ if secret != system_settings.jira_webhook_secret:
+ return webhook_responser_handler("info", "Invalid or no secret provided to JIRA Webhook")
# if webhook secret is disabled in system_settings, we ignore the incoming secret, even if it doesn't match
-
# example json bodies at the end of this file
-
- if request.content_type != 'application/json':
- return HttpResponseBadRequest("only application/json supported")
-
- if request.method == 'POST':
- try:
- parsed = json.loads(request.body.decode('utf-8'))
- if parsed.get('webhookEvent') == 'jira:issue_updated':
- # xml examples at the end of file
- jid = parsed['issue']['id']
- jissue = get_object_or_404(JIRA_Issue, jira_id=jid)
-
- findings = None
- if jissue.finding:
- logging.info(f"Received issue update for {jissue.jira_key} for finding {jissue.finding.id}")
- findings = [jissue.finding]
- elif jissue.finding_group:
- logging.info(f"Received issue update for {jissue.jira_key} for finding group {jissue.finding_group}")
- findings = jissue.finding_group.findings.all()
- elif jissue.engagement:
- # if parsed['issue']['fields']['resolution'] != None:
- # eng.active = False
- # eng.status = 'Completed'
- # eng.save()
- return HttpResponse('Update for engagement ignored')
- else:
- logging.info(f"Received issue update for {jissue.jira_key} for unknown object")
- raise Http404(f'No finding, finding_group or engagement found for JIRA issue {jissue.jira_key}')
-
- assignee = parsed['issue']['fields'].get('assignee')
- assignee_name = 'Jira User'
- if assignee is not None:
- # First look for the 'name' field. If not present, try 'displayName'. Else put None
- assignee_name = assignee.get('name', assignee.get('displayName'))
-
- resolution = parsed['issue']['fields']['resolution']
-
- # "resolution":{
- # "self":"http://www.testjira.com/rest/api/2/resolution/11",
- # "id":"11",
- # "description":"Cancelled by the customer.",
- # "name":"Cancelled"
- # },
-
- # or
- # "resolution": null
-
- # or
- # "resolution": "None"
-
- resolution = resolution if resolution and resolution != "None" else None
- resolution_id = resolution['id'] if resolution else None
- resolution_name = resolution['name'] if resolution else None
- jira_now = parse_datetime(parsed['issue']['fields']['updated'])
-
- if findings:
- for finding in findings:
- jira_helper.process_resolution_from_jira(finding, resolution_id, resolution_name, assignee_name, jira_now, jissue)
- # Check for any comment that could have come along with the resolution
- if (error_response := check_for_and_create_comment(parsed)) is not None:
- return error_response
-
- if parsed.get('webhookEvent') == 'comment_created':
- if (error_response := check_for_and_create_comment(parsed)) is not None:
- return error_response
-
- if parsed.get('webhookEvent') not in ['comment_created', 'jira:issue_updated']:
- logger.info(f"Unrecognized JIRA webhook event received: {parsed.get('webhookEvent')}")
-
- except Exception as e:
- if isinstance(e, Http404):
- logger.warning('404 error processing JIRA webhook')
- logger.warning(str(e))
- else:
- logger.exception(e)
-
+ if request.content_type != "application/json":
+ return webhook_responser_handler("debug", "only application/json supported")
+ # Time to process the request
+ try:
+ parsed = json.loads(request.body.decode("utf-8"))
+ # Check if the events supplied are supported
+ if parsed.get('webhookEvent') not in ['comment_created', 'jira:issue_updated']:
+ return webhook_responser_handler("info", f"Unrecognized JIRA webhook event received: {parsed.get('webhookEvent')}")
+
+ if parsed.get('webhookEvent') == 'jira:issue_updated':
+ # xml examples at the end of file
+ jid = parsed['issue']['id']
+ # This may raise a 404, but it will be handled in the exception response
try:
- logger.debug('jira_webhook_body_parsed:')
- logger.debug(json.dumps(parsed, indent=4))
- except Exception:
- logger.debug('jira_webhook_body:')
- logger.debug(request.body.decode('utf-8'))
+ jissue = JIRA_Issue.objects.get(jira_id=jid)
+ except JIRA_Instance.DoesNotExist:
+ return webhook_responser_handler("info", f"JIRA issue {jid} is not linked to a DefectDojo Finding")
+ findings = None
+ # Determine what type of object we will be working with
+ if jissue.finding:
+ logging.debug(f"Received issue update for {jissue.jira_key} for finding {jissue.finding.id}")
+ findings = [jissue.finding]
+ elif jissue.finding_group:
+ logging.debug(f"Received issue update for {jissue.jira_key} for finding group {jissue.finding_group}")
+ findings = jissue.finding_group.findings.all()
+ elif jissue.engagement:
+ return webhook_responser_handler("debug", "Update for engagement ignored")
+ else:
+ return webhook_responser_handler("info", f"Received issue update for {jissue.jira_key} for unknown object")
+ # Process the assignee if present
+ assignee = parsed['issue']['fields'].get('assignee')
+ assignee_name = 'Jira User'
+ if assignee is not None:
+ # First look for the 'name' field. If not present, try 'displayName'. Else put None
+ assignee_name = assignee.get('name', assignee.get('displayName'))
+
+ # "resolution":{
+ # "self":"http://www.testjira.com/rest/api/2/resolution/11",
+ # "id":"11",
+ # "description":"Cancelled by the customer.",
+ # "name":"Cancelled"
+ # },
+
+ # or
+ # "resolution": null
+
+ # or
+ # "resolution": "None"
+
+ resolution = parsed['issue']['fields']['resolution']
+ resolution = resolution if resolution and resolution != "None" else None
+ resolution_id = resolution['id'] if resolution else None
+ resolution_name = resolution['name'] if resolution else None
+ jira_now = parse_datetime(parsed['issue']['fields']['updated'])
+
+ if findings:
+ for finding in findings:
+ jira_helper.process_resolution_from_jira(finding, resolution_id, resolution_name, assignee_name, jira_now, jissue)
+ # Check for any comment that could have come along with the resolution
+ if (error_response := check_for_and_create_comment(parsed)) is not None:
+ return error_response
+
+ if parsed.get('webhookEvent') == 'comment_created':
+ if (error_response := check_for_and_create_comment(parsed)) is not None:
+ return error_response
+
+ except Exception as e:
+ # Check if the issue is originally a 404
+ if isinstance(e, Http404):
+ return webhook_responser_handler("debug", str(e))
+ # Try to get a little more information on the exact exception
+ try:
+ message = (
+ f"Original Exception: {e}\n"
+ f"jira webhook body parsed:\n{json.dumps(parsed, indent=4)}"
+ )
+ except Exception:
+ message = (
+ f"Original Exception: {e}\n"
+ f"jira webhook body :\n{request.body.decode('utf-8')}"
+ )
+ return webhook_responser_handler("debug", message)
- # reraise to make sure we don't silently swallow things
- raise
- return HttpResponse('')
+ return webhook_responser_handler("No logging here", "Success!")
def check_for_and_create_comment(parsed_json):
@@ -194,31 +220,30 @@ def check_for_and_create_comment(parsed_json):
commenter_display_name = comment.get('updateAuthor', {}).get('displayName')
# example: body['comment']['self'] = "http://www.testjira.com/jira_under_a_path/rest/api/2/issue/666/comment/456843"
jid = comment.get('self', '').split('/')[-3]
- jissue = get_object_or_404(JIRA_Issue, jira_id=jid)
- logging.info(f"Received issue comment for {jissue.jira_key}")
+ try:
+ jissue = JIRA_Issue.objects.get(jira_id=jid)
+ except JIRA_Instance.DoesNotExist:
+ return webhook_responser_handler("info", f"JIRA issue {jid} is not linked to a DefectDojo Finding")
+ logging.debug(f"Received issue comment for {jissue.jira_key}")
logger.debug('jissue: %s', vars(jissue))
jira_usernames = JIRA_Instance.objects.values_list('username', flat=True)
for jira_user_id in jira_usernames:
# logger.debug('incoming username: %s jira config username: %s', commenter.lower(), jira_user_id.lower())
if jira_user_id.lower() == commenter.lower():
- logger.debug('skipping incoming JIRA comment as the user id of the comment in JIRA (%s) matches the JIRA username in DefectDojo (%s)', commenter.lower(), jira_user_id.lower())
- return HttpResponse('')
+ return webhook_responser_handler("debug", f"skipping incoming JIRA comment as the user id of the comment in JIRA {commenter.lower()} matches the JIRA username in DefectDojo {jira_user_id.lower()}")
findings = None
if jissue.finding:
findings = [jissue.finding]
create_notification(event='other', title=f'JIRA incoming comment - {jissue.finding}', finding=jissue.finding, url=reverse("view_finding", args=(jissue.finding.id,)), icon='check')
-
elif jissue.finding_group:
findings = [jissue.finding_group.findings.all()]
create_notification(event='other', title=f'JIRA incoming comment - {jissue.finding}', finding=jissue.finding, url=reverse("view_finding_group", args=(jissue.finding_group.id,)), icon='check')
-
elif jissue.engagement:
- return HttpResponse('Comment for engagement ignored')
+ return webhook_responser_handler("debug", "Comment for engagement ignored")
else:
- raise Http404(f'No finding or engagement found for JIRA issue {jissue.jira_key}')
-
+ return webhook_responser_handler("info", f"Received issue update for {jissue.jira_key} for unknown object")
# Set the fields for the notes
author, _ = User.objects.get_or_create(username='JIRA')
entry = f'({commenter_display_name} ({commenter})): {comment_text}'
diff --git a/dojo/metrics/views.py b/dojo/metrics/views.py
index 5f34dd7717c..865fa3a0e7b 100644
--- a/dojo/metrics/views.py
+++ b/dojo/metrics/views.py
@@ -20,7 +20,13 @@
from django.views.decorators.cache import cache_page
from django.utils import timezone
-from dojo.filters import MetricsFindingFilter, UserFilter, MetricsEndpointFilter
+from dojo.filters import (
+ MetricsEndpointFilter,
+ MetricsEndpointFilterWithoutObjectLookups,
+ MetricsFindingFilter,
+ MetricsFindingFilterWithoutObjectLookups,
+ UserFilter,
+)
from dojo.forms import SimpleMetricsForm, ProductTypeCountsForm, ProductTagCountsForm
from dojo.models import Product_Type, Finding, Product, Engagement, Test, \
Risk_Acceptance, Dojo_User, Endpoint_Status
@@ -141,7 +147,10 @@ def finding_querys(prod_type, request):
'test__engagement__risk_acceptance',
'test__test_type',
)
- findings = MetricsFindingFilter(request.GET, queryset=findings_query)
+ filter_string_matching = get_system_setting("filter_string_matching", False)
+ finding_filter_class = MetricsFindingFilterWithoutObjectLookups if filter_string_matching else MetricsFindingFilter
+ findings = finding_filter_class(request.GET, queryset=findings_query)
+ form = findings.form
findings_qs = queryset_check(findings)
# Quick check to determine if the filters were too tight and filtered everything away
if not findings_qs and not findings_query:
@@ -205,6 +214,7 @@ def finding_querys(prod_type, request):
'weeks_between': weeks_between,
'start_date': start_date,
'end_date': end_date,
+ 'form': form,
}
@@ -218,8 +228,10 @@ def endpoint_querys(prod_type, request):
'finding__reporter')
endpoints_query = get_authorized_endpoint_status(Permissions.Endpoint_View, endpoints_query, request.user)
- endpoints = MetricsEndpointFilter(request.GET, queryset=endpoints_query)
-
+ filter_string_matching = get_system_setting("filter_string_matching", False)
+ filter_class = MetricsEndpointFilterWithoutObjectLookups if filter_string_matching else MetricsEndpointFilter
+ endpoints = filter_class(request.GET, queryset=endpoints_query)
+ form = endpoints.form
endpoints_qs = queryset_check(endpoints)
if not endpoints_qs:
@@ -295,6 +307,7 @@ def endpoint_querys(prod_type, request):
'weeks_between': weeks_between,
'start_date': start_date,
'end_date': end_date,
+ 'form': form,
}
@@ -445,6 +458,7 @@ def metrics(request, mtype):
'closed_in_period_details': closed_in_period_details,
'punchcard': punchcard,
'ticks': ticks,
+ 'form': filters.get('form', None),
'show_pt_filter': show_pt_filter,
})
diff --git a/dojo/models.py b/dojo/models.py
index ab456ae4830..74b8da26888 100755
--- a/dojo/models.py
+++ b/dojo/models.py
@@ -421,6 +421,12 @@ class System_Settings(models.Model):
verbose_name=_('Enable Remediation Advice'),
help_text=_("Enables global remediation advice and matching on CWE and Title. The text will be replaced for mitigation, impact and references on a finding. Useful for providing consistent impact and remediation advice regardless of the scanner."))
+ enable_similar_findings = models.BooleanField(
+ default=True,
+ blank=False,
+ verbose_name=_("Enable Similar Findings"),
+ help_text=_("Enable the query of similar findings on the view finding page. This feature can involve potentially large queries and negatively impact performance"))
+
engagement_auto_close = models.BooleanField(
default=False,
blank=False,
@@ -571,6 +577,14 @@ class System_Settings(models.Model):
blank=False,
verbose_name=_("API expose error details"),
help_text=_("When turned on, the API will expose error details in the response."))
+ filter_string_matching = models.BooleanField(
+ default=False,
+ blank=False,
+ verbose_name=_("Filter String Matching Optimization"),
+ help_text=_(
+ "When turned on, all filter operations in the UI will require string matches rather than ID. "
+ "This is a performance enhancement to avoid fetching objects unnecessarily."
+ ))
from dojo.middleware import System_Settings_Manager
objects = System_Settings_Manager()
diff --git a/dojo/product/views.py b/dojo/product/views.py
index c4b45ae329d..0724f7d4f35 100755
--- a/dojo/product/views.py
+++ b/dojo/product/views.py
@@ -25,8 +25,19 @@
from django.views import View
from dojo.templatetags.display_tags import asvs_calc_level
-from dojo.filters import ProductEngagementFilter, ProductFilter, EngagementFilter, MetricsEndpointFilter, \
- MetricsFindingFilter, ProductComponentFilter
+from dojo.filters import (
+ ProductEngagementFilter,
+ ProductEngagementFilterWithoutObjectLookups,
+ ProductFilter,
+ ProductFilterWithoutObjectLookups,
+ EngagementFilter,
+ EngagementFilterWithoutObjectLookups,
+ MetricsEndpointFilter,
+ MetricsEndpointFilterWithoutObjectLookups,
+ MetricsFindingFilter,
+ MetricsFindingFilterWithoutObjectLookups,
+ ProductComponentFilter,
+)
from dojo.forms import ProductForm, EngForm, DeleteProductForm, DojoMetaDataForm, JIRAProjectForm, JIRAFindingForm, \
AdHocFindingForm, \
EngagementPresetsForm, DeleteEngagementPresetsForm, ProductNotificationsForm, \
@@ -39,7 +50,7 @@
Endpoint, Engagement_Presets, DojoMeta, Notifications, BurpRawRequestResponse, Product_Member, \
Product_Group, Product_API_Scan_Configuration
from dojo.utils import add_external_issue, add_error_message_to_response, add_field_errors_to_response, get_page_items, \
- add_breadcrumb, async_delete, \
+ add_breadcrumb, async_delete, calculate_finding_age, \
get_system_setting, get_setting, Product_Tab, get_punchcard_data, queryset_check, is_title_in_breadcrumbs, \
get_enabled_notifications_list, get_zero_severity_level, sum_by_severity_level, get_open_findings_burndown
@@ -67,8 +78,9 @@ def product(request):
# otherwise the paginator will perform all the annotations/prefetching already only to count the total number of records
# see https://code.djangoproject.com/ticket/23771 and https://code.djangoproject.com/ticket/25375
name_words = prods.values_list('name', flat=True)
-
- prod_filter = ProductFilter(request.GET, queryset=prods, user=request.user)
+ filter_string_matching = get_system_setting("filter_string_matching", False)
+ filter_class = ProductFilterWithoutObjectLookups if filter_string_matching else ProductFilter
+ prod_filter = filter_class(request.GET, queryset=prods, user=request.user)
prod_list = get_page_items(request, prod_filter.qs, 25)
@@ -304,7 +316,9 @@ def finding_querys(request, prod):
# 'test__test_type',
# 'risk_acceptance_set',
'reporter')
- findings = MetricsFindingFilter(request.GET, queryset=findings_query, pid=prod)
+ filter_string_matching = get_system_setting("filter_string_matching", False)
+ finding_filter_class = MetricsFindingFilterWithoutObjectLookups if filter_string_matching else MetricsFindingFilter
+ findings = finding_filter_class(request.GET, queryset=findings_query, pid=prod)
findings_qs = queryset_check(findings)
filters['form'] = findings.form
@@ -368,7 +382,9 @@ def endpoint_querys(request, prod):
'finding__test__engagement__risk_acceptance',
'finding__risk_acceptance_set',
'finding__reporter').annotate(severity=F('finding__severity'))
- endpoints = MetricsEndpointFilter(request.GET, queryset=endpoints_query)
+ filter_string_matching = get_system_setting("filter_string_matching", False)
+ filter_class = MetricsEndpointFilterWithoutObjectLookups if filter_string_matching else MetricsEndpointFilter
+ endpoints = filter_class(request.GET, queryset=endpoints_query)
endpoints_qs = queryset_check(endpoints)
filters['form'] = endpoints.form
@@ -448,7 +464,9 @@ def view_product_metrics(request, pid):
engs = Engagement.objects.filter(product=prod, active=True)
view = identify_view(request)
- result = EngagementFilter(
+ filter_string_matching = get_system_setting("filter_string_matching", False)
+ filter_class = EngagementFilterWithoutObjectLookups if filter_string_matching else EngagementFilter
+ result = filter_class(
request.GET,
queryset=Engagement.objects.filter(product=prod, active=False).order_by('-target_end'))
@@ -460,16 +478,9 @@ def view_product_metrics(request, pid):
elif view == 'Endpoint':
filters = endpoint_querys(request, prod)
- start_date = filters['start_date']
+ start_date = timezone.make_aware(datetime.combine(filters['start_date'], datetime.min.time()))
end_date = filters['end_date']
- tests = Test.objects.filter(engagement__product=prod).prefetch_related('finding_set', 'test_type')
- tests = tests.annotate(verified_finding_count=Count('finding__id', filter=Q(finding__verified=True)))
-
- open_vulnerabilities = filters['open_vulns']
- all_vulnerabilities = filters['all_vulns']
-
- start_date = timezone.make_aware(datetime.combine(start_date, datetime.min.time()))
r = relativedelta(end_date, start_date)
weeks_between = int(ceil((((r.years * 12) + r.months) * 4.33) + (r.days / 7)))
if weeks_between <= 0:
@@ -485,19 +496,45 @@ def view_product_metrics(request, pid):
critical_weekly = OrderedDict()
high_weekly = OrderedDict()
medium_weekly = OrderedDict()
+ open_objs_by_age = {}
open_objs_by_severity = get_zero_severity_level()
closed_objs_by_severity = get_zero_severity_level()
accepted_objs_by_severity = get_zero_severity_level()
- for finding in filters.get("all", []):
- iso_cal = finding.date.isocalendar()
+ # Optimization: Make all queries lists, and only pull values of fields for metrics based calculations
+ open_vulnerabilities = list(filters['open_vulns'].values('cwe', 'count'))
+ all_vulnerabilities = list(filters['all_vulns'].values('cwe', 'count'))
+
+ verified_objs_by_severity = list(filters.get('verified').values('severity'))
+ inactive_objs_by_severity = list(filters.get('inactive').values('severity'))
+ false_positive_objs_by_severity = list(filters.get('false_positive').values('severity'))
+ out_of_scope_objs_by_severity = list(filters.get('out_of_scope').values('severity'))
+ new_objs_by_severity = list(filters.get('new_verified').values('severity'))
+ all_objs_by_severity = list(filters.get('all').values('severity'))
+
+ all_findings = list(filters.get("all", []).values('id', 'date', 'severity'))
+ open_findings = list(filters.get("open", []).values('id', 'date', 'mitigated', 'severity'))
+ closed_findings = list(filters.get("closed", []).values('id', 'date', 'severity'))
+ accepted_findings = list(filters.get("accepted", []).values('id', 'date', 'severity'))
+
+ '''
+ Optimization: Create dictionaries in the structure of { finding_id: True } for index based search
+ Previously the for-loop below used "if finding in open_findings" -- an average O(n^2) time complexity
+ This allows for "if open_findings.get(finding_id, None)" -- an average O(n) time complexity
+ '''
+ open_findings_dict = {f.get('id'): True for f in open_findings}
+ closed_findings_dict = {f.get('id'): True for f in closed_findings}
+ accepted_findings_dict = {f.get('id'): True for f in accepted_findings}
+
+ for finding in all_findings:
+ iso_cal = finding.get('date').isocalendar()
date = iso_to_gregorian(iso_cal[0], iso_cal[1], 1)
html_date = date.strftime("%m/%d
%Y")
unix_timestamp = (tcalendar.timegm(date.timetuple()) * 1000)
# Open findings
- if finding in filters.get("open", []):
+ if open_findings_dict.get(finding.get('id', None), None):
if unix_timestamp not in critical_weekly:
critical_weekly[unix_timestamp] = {'count': 0, 'week': html_date}
if unix_timestamp not in high_weekly:
@@ -512,9 +549,15 @@ def view_product_metrics(request, pid):
open_close_weekly[unix_timestamp]['week'] = html_date
if view == 'Finding':
- severity = finding.severity
+ severity = finding.get('severity')
elif view == 'Endpoint':
- severity = finding.finding.severity
+ severity = finding.finding.get('severity')
+
+ finding_age = calculate_finding_age(finding)
+ if open_objs_by_age.get(finding_age, None):
+ open_objs_by_age[finding_age] += 1
+ else:
+ open_objs_by_age[finding_age] = 1
if unix_timestamp in severity_weekly:
if severity in severity_weekly[unix_timestamp]:
@@ -542,28 +585,33 @@ def view_product_metrics(request, pid):
else:
medium_weekly[unix_timestamp] = {'count': 1, 'week': html_date}
# Optimization: count severity level on server side
- if open_objs_by_severity.get(finding.severity) is not None:
- open_objs_by_severity[finding.severity] += 1
+ if open_objs_by_severity.get(finding.get('severity')) is not None:
+ open_objs_by_severity[finding.get('severity')] += 1
+
# Close findings
- if finding in filters.get("closed", []):
+ elif closed_findings_dict.get(finding.get('id', None), None):
if unix_timestamp in open_close_weekly:
open_close_weekly[unix_timestamp]['closed'] += 1
else:
open_close_weekly[unix_timestamp] = {'closed': 1, 'open': 0, 'accepted': 0}
open_close_weekly[unix_timestamp]['week'] = html_date
# Optimization: count severity level on server side
- if closed_objs_by_severity.get(finding.severity) is not None:
- closed_objs_by_severity[finding.severity] += 1
+ if closed_objs_by_severity.get(finding.get('severity')) is not None:
+ closed_objs_by_severity[finding.get('severity')] += 1
+
# Risk Accepted findings
- if finding in filters.get("accepted", []):
+ if accepted_findings_dict.get(finding.get('id', None), None):
if unix_timestamp in open_close_weekly:
open_close_weekly[unix_timestamp]['accepted'] += 1
else:
open_close_weekly[unix_timestamp] = {'closed': 0, 'open': 0, 'accepted': 1}
open_close_weekly[unix_timestamp]['week'] = html_date
# Optimization: count severity level on server side
- if accepted_objs_by_severity.get(finding.severity) is not None:
- accepted_objs_by_severity[finding.severity] += 1
+ if accepted_objs_by_severity.get(finding.get('severity')) is not None:
+ accepted_objs_by_severity[finding.get('severity')] += 1
+
+ tests = Test.objects.filter(engagement__product=prod).prefetch_related('finding_set', 'test_type')
+ tests = tests.annotate(verified_finding_count=Count('finding__id', filter=Q(finding__verified=True)))
test_data = {}
for t in tests:
@@ -572,9 +620,11 @@ def view_product_metrics(request, pid):
else:
test_data[t.test_type.name] = t.verified_finding_count
- product_tab = Product_Tab(prod, title=_("Product"), tab="metrics")
+ # Optimization: Format Open/Total CWE vulnerabilities graph data here, instead of template
+ open_vulnerabilities = [['CWE-' + str(f.get('cwe')), f.get('count')] for f in open_vulnerabilities]
+ all_vulnerabilities = [['CWE-' + str(f.get('cwe')), f.get('count')] for f in all_vulnerabilities]
- open_objs_by_age = {x: len([_ for _ in filters.get('open') if _.age == x]) for x in set([_.age for _ in filters.get('open')])}
+ product_tab = Product_Tab(prod, title=_("Product"), tab="metrics")
return render(request, 'dojo/product_metrics.html', {
'prod': prod,
@@ -582,28 +632,30 @@ def view_product_metrics(request, pid):
'engs': engs,
'inactive_engs': inactive_engs_page,
'view': view,
- 'verified_objs': filters.get('verified', None),
- 'verified_objs_by_severity': sum_by_severity_level(filters.get('verified')),
- 'open_objs': filters.get('open', None),
+ 'verified_objs': len(verified_objs_by_severity),
+ 'verified_objs_by_severity': sum_by_severity_level(verified_objs_by_severity),
+ 'open_objs': len(open_findings),
'open_objs_by_severity': open_objs_by_severity,
'open_objs_by_age': open_objs_by_age,
- 'inactive_objs': filters.get('inactive', None),
- 'inactive_objs_by_severity': sum_by_severity_level(filters.get('inactive')),
- 'closed_objs': filters.get('closed', None),
+ 'inactive_objs': len(inactive_objs_by_severity),
+ 'inactive_objs_by_severity': sum_by_severity_level(inactive_objs_by_severity),
+ 'closed_objs': len(closed_findings),
'closed_objs_by_severity': closed_objs_by_severity,
- 'false_positive_objs': filters.get('false_positive', None),
- 'false_positive_objs_by_severity': sum_by_severity_level(filters.get('false_positive')),
- 'out_of_scope_objs': filters.get('out_of_scope', None),
- 'out_of_scope_objs_by_severity': sum_by_severity_level(filters.get('out_of_scope')),
- 'accepted_objs': filters.get('accepted', None),
+ 'false_positive_objs': len(false_positive_objs_by_severity),
+ 'false_positive_objs_by_severity': sum_by_severity_level(false_positive_objs_by_severity),
+ 'out_of_scope_objs': len(out_of_scope_objs_by_severity),
+ 'out_of_scope_objs_by_severity': sum_by_severity_level(out_of_scope_objs_by_severity),
+ 'accepted_objs': len(accepted_findings),
'accepted_objs_by_severity': accepted_objs_by_severity,
- 'new_objs': filters.get('new_verified', None),
- 'new_objs_by_severity': sum_by_severity_level(filters.get('new_verified')),
- 'all_objs': filters.get('all', None),
- 'all_objs_by_severity': sum_by_severity_level(filters.get('all')),
+ 'new_objs': len(new_objs_by_severity),
+ 'new_objs_by_severity': sum_by_severity_level(new_objs_by_severity),
+ 'all_objs': len(all_objs_by_severity),
+ 'all_objs_by_severity': sum_by_severity_level(all_objs_by_severity),
'form': filters.get('form', None),
'reset_link': reverse('view_product_metrics', args=(prod.id,)) + '?type=' + view,
+ 'open_vulnerabilities_count': len(open_vulnerabilities),
'open_vulnerabilities': open_vulnerabilities,
+ 'all_vulnerabilities_count': len(all_vulnerabilities),
'all_vulnerabilities': all_vulnerabilities,
'start_date': start_date,
'punchcard': punchcard,
@@ -636,31 +688,36 @@ def async_burndown_metrics(request, pid):
@user_is_authorized(Product, Permissions.Engagement_View, 'pid')
def view_engagements(request, pid):
prod = get_object_or_404(Product, id=pid)
-
default_page_num = 10
recent_test_day_count = 7
-
+ filter_string_matching = get_system_setting("filter_string_matching", False)
+ filter_class = ProductEngagementFilterWithoutObjectLookups if filter_string_matching else ProductEngagementFilter
# In Progress Engagements
engs = Engagement.objects.filter(product=prod, active=True, status="In Progress").order_by('-updated')
- active_engs_filter = ProductEngagementFilter(request.GET, queryset=engs, prefix='active')
+ active_engs_filter = filter_class(request.GET, queryset=engs, prefix='active')
result_active_engs = get_page_items(request, active_engs_filter.qs, default_page_num, prefix="engs")
- # prefetch only after creating the filters to avoid https://code.djangoproject.com/ticket/23771 and https://code.djangoproject.com/ticket/25375
- result_active_engs.object_list = prefetch_for_view_engagements(result_active_engs.object_list,
- recent_test_day_count)
-
+ # prefetch only after creating the filters to avoid https://code.djangoproject.com/ticket/23771
+ # and https://code.djangoproject.com/ticket/25375
+ result_active_engs.object_list = prefetch_for_view_engagements(
+ result_active_engs.object_list,
+ recent_test_day_count,
+ )
# Engagements that are queued because they haven't started or paused
engs = Engagement.objects.filter(~Q(status="In Progress"), product=prod, active=True).order_by('-updated')
- queued_engs_filter = ProductEngagementFilter(request.GET, queryset=engs, prefix='queued')
+ queued_engs_filter = filter_class(request.GET, queryset=engs, prefix='queued')
result_queued_engs = get_page_items(request, queued_engs_filter.qs, default_page_num, prefix="queued_engs")
- result_queued_engs.object_list = prefetch_for_view_engagements(result_queued_engs.object_list,
- recent_test_day_count)
-
+ result_queued_engs.object_list = prefetch_for_view_engagements(
+ result_queued_engs.object_list,
+ recent_test_day_count,
+ )
# Cancelled or Completed Engagements
engs = Engagement.objects.filter(product=prod, active=False).order_by('-target_end')
- inactive_engs_filter = ProductEngagementFilter(request.GET, queryset=engs, prefix='closed')
+ inactive_engs_filter = filter_class(request.GET, queryset=engs, prefix='closed')
result_inactive_engs = get_page_items(request, inactive_engs_filter.qs, default_page_num, prefix="inactive_engs")
- result_inactive_engs.object_list = prefetch_for_view_engagements(result_inactive_engs.object_list,
- recent_test_day_count)
+ result_inactive_engs.object_list = prefetch_for_view_engagements(
+ result_inactive_engs.object_list,
+ recent_test_day_count,
+ )
product_tab = Product_Tab(prod, title=_("All Engagements"), tab="engagements")
return render(request, 'dojo/view_engagements.html', {
diff --git a/dojo/reports/views.py b/dojo/reports/views.py
index 2eea646b74d..b6c3024bb0e 100644
--- a/dojo/reports/views.py
+++ b/dojo/reports/views.py
@@ -17,7 +17,7 @@
from django.views import View
from dojo.filters import ReportFindingFilter, EndpointReportFilter, \
- EndpointFilter
+ EndpointFilter, EndpointFilterWithoutObjectLookups
from dojo.forms import ReportOptionsForm
from dojo.models import Product_Type, Finding, Product, Engagement, Test, \
Dojo_User, Endpoint, Risk_Acceptance
@@ -63,8 +63,9 @@ def report_builder(request):
finding__duplicate=False,
finding__out_of_scope=False,
).distinct()
-
- endpoints = EndpointFilter(request.GET, queryset=endpoints, user=request.user)
+ filter_string_matching = get_system_setting("filter_string_matching", False)
+ filter_class = EndpointFilterWithoutObjectLookups if filter_string_matching else EndpointFilter
+ endpoints = filter_class(request.GET, queryset=endpoints, user=request.user)
in_use_widgets = [ReportOptions(request=request)]
available_widgets = [CoverPage(request=request),
diff --git a/dojo/reports/widgets.py b/dojo/reports/widgets.py
index 36831c4ad0c..fea53696673 100644
--- a/dojo/reports/widgets.py
+++ b/dojo/reports/widgets.py
@@ -11,10 +11,10 @@
from django.utils.html import format_html
from django.utils.safestring import mark_safe
-from dojo.filters import EndpointFilter, ReportFindingFilter
+from dojo.filters import EndpointFilter, EndpointFilterWithoutObjectLookups, ReportFindingFilter
from dojo.forms import CustomReportOptionsForm
from dojo.models import Endpoint, Finding
-from dojo.utils import get_page_items, get_words_for_field
+from dojo.utils import get_page_items, get_words_for_field, get_system_setting
"""
Widgets are content sections that can be included on reports. The report builder will allow any number of widgets
@@ -407,7 +407,9 @@ def report_widget_factory(json_data=None, request=None, user=None, finding_notes
d[item['name']] = item['value']
endpoints = Endpoint.objects.filter(id__in=endpoints)
- endpoints = EndpointFilter(d, queryset=endpoints, user=request.user)
+ filter_string_matching = get_system_setting("filter_string_matching", False)
+ filter_class = EndpointFilterWithoutObjectLookups if filter_string_matching else EndpointFilter
+ endpoints = filter_class(d, queryset=endpoints, user=request.user)
user_id = user.id if user is not None else None
endpoints = EndpointList(request=request, endpoints=endpoints, finding_notes=finding_notes,
finding_images=finding_images, host=host, user_id=user_id)
diff --git a/dojo/search/views.py b/dojo/search/views.py
index b2a474eb26b..717ced20594 100644
--- a/dojo/search/views.py
+++ b/dojo/search/views.py
@@ -6,11 +6,11 @@
from django.db.models import Q
from dojo.forms import SimpleSearchForm
from dojo.models import Finding, Finding_Template, Product, Test, Engagement, Languages
-from dojo.utils import add_breadcrumb, get_page_items, get_words_for_field
+from dojo.utils import add_breadcrumb, get_page_items, get_words_for_field, get_system_setting
import re
from dojo.finding.views import prefetch_for_findings
from dojo.endpoint.views import prefetch_for_endpoints
-from dojo.filters import FindingFilter
+from dojo.filters import FindingFilter, FindingFilterWithoutObjectLookups
from django.conf import settings
import shlex
import itertools
@@ -117,8 +117,9 @@ def simple_search(request):
elif search_findings:
logger.debug('searching findings')
-
- findings_filter = FindingFilter(request.GET, queryset=findings, user=request.user, pid=None, prefix='finding')
+ filter_string_matching = get_system_setting("filter_string_matching", False)
+ finding_filter_class = FindingFilterWithoutObjectLookups if filter_string_matching else FindingFilter
+ findings_filter = finding_filter_class(request.GET, queryset=findings, user=request.user, pid=None, prefix='finding')
# setting initial values for filters is not supported and discouraged: https://django-filter.readthedocs.io/en/stable/guide/tips.html#using-initial-values-as-defaults
# we could try to modify request.GET before generating the filter, but for now we'll leave it as is
diff --git a/dojo/templates/dojo/edit_finding.html b/dojo/templates/dojo/edit_finding.html
index dc52e0fd2f0..60e29494b11 100644
--- a/dojo/templates/dojo/edit_finding.html
+++ b/dojo/templates/dojo/edit_finding.html
@@ -208,7 +208,7 @@