diff --git a/components/package.json b/components/package.json index 4ccd0709397..8b076e0bbbc 100644 --- a/components/package.json +++ b/components/package.json @@ -1,6 +1,6 @@ { "name": "defectdojo", - "version": "2.33.6", + "version": "2.33.7", "license" : "BSD-3-Clause", "private": true, "dependencies": { diff --git a/dojo/__init__.py b/dojo/__init__.py index 3a41507beca..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.6' +__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/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 d74ce33f257..8dc61379104 100644 --- a/dojo/filters.py +++ b/dojo/filters.py @@ -826,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(), @@ -842,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'] - - -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") - 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 - ) + fields = ["product__name", "product__prod_type"] - 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 - ) +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") - not_tag = CharFilter(field_name='tags__name', lookup_expr='icontains', label='Not tag name contains', exclude=True) + class Meta: + model = Engagement + fields = ["product__name"] - has_tags = BooleanFilter(field_name='tags', lookup_expr='isnull', exclude=True, label='Has tags') +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") 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: @@ -968,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=( @@ -1012,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') @@ -1069,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=( @@ -1194,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): @@ -1444,7 +1461,7 @@ def filter(self, qs, value): return super().filter(qs, value) -class FindingFilterNonModelFilters(FilterSet): +class FindingFilterHelper(FilterSet): title = CharFilter(lookup_expr="icontains") date = DateRangeFilter() on = DateFilter(field_name="date", lookup_expr="exact", label="On") @@ -1569,7 +1586,7 @@ def set_date_fields(self, *args: list, **kwargs: dict): self.form.fields['cwe'].choices = cwe_options(self.queryset) -class FindingFilterWithoutObjectLookups(FindingFilterNonModelFilters, FindingTagStringFilter): +class FindingFilterWithoutObjectLookups(FindingFilterHelper, FindingTagStringFilter): reporter = CharFilter( field_name="reporter__username", lookup_expr="iexact", @@ -1677,7 +1694,7 @@ def __init__(self, *args, **kwargs): del self.form.fields['test__name_contains'] -class FindingFilter(FindingFilterNonModelFilters, FindingTagFilter): +class FindingFilter(FindingFilterHelper, FindingTagFilter): reporter = ModelMultipleChoiceFilter(queryset=Dojo_User.objects.none()) reviewers = ModelMultipleChoiceFilter(queryset=Dojo_User.objects.none()) test__engagement__product__prod_type = ModelMultipleChoiceFilter( @@ -2025,113 +2042,262 @@ class Meta(FindingFilterWithoutObjectLookups.Meta): fields = get_finding_filterset_fields(metrics=True, filter_string_matching=True) -class MetricsEndpointFilter(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')) +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") - 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', + endpoint__tags = ModelMultipleChoiceFilter( + field_name='endpoint__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_tags = ModelMultipleChoiceFilter( - field_name='tags__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', - exclude=True, - queryset=Endpoint.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) - - not_tags = ModelMultipleChoiceFilter( - field_name='tags__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 EndpointFilter(DojoFilter): - product = ModelMultipleChoiceFilter( - queryset=Product.objects.none(), - label="Product") +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 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') @@ -2139,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 @@ -2213,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): @@ -2257,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=( @@ -2301,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') @@ -2319,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', diff --git a/dojo/finding/views.py b/dojo/finding/views.py index 8ac42af6598..9faef2cffc5 100644 --- a/dojo/finding/views.py +++ b/dojo/finding/views.py @@ -602,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 @@ -638,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/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 b9892633ed8..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, MetricsFindingFilterWithoutObjectLookups +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 @@ -144,6 +150,7 @@ def finding_querys(prod_type, request): 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: @@ -207,6 +214,7 @@ def finding_querys(prod_type, request): 'weeks_between': weeks_between, 'start_date': start_date, 'end_date': end_date, + 'form': form, } @@ -220,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: @@ -297,6 +307,7 @@ def endpoint_querys(prod_type, request): 'weeks_between': weeks_between, 'start_date': start_date, 'end_date': end_date, + 'form': form, } @@ -447,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 eb3c006334e..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, diff --git a/dojo/product/views.py b/dojo/product/views.py index 09f0e007b06..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, MetricsFindingFilterWithoutObjectLookups, 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) @@ -370,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 @@ -450,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')) @@ -462,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: @@ -487,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: @@ -514,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]: @@ -544,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: @@ -574,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, @@ -584,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, @@ -638,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/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 @@

GitHub

if ($("#id_duplicate").prop("checked")) { $("#id_duplicate").parent().parent().append(original_finding) } else { - $("#id_duplicate").click(function(){ alert('findings can only be marked as duplicates from the view finding screen'); return false; }); + $("#id_duplicate").click(function(){ alert('findings can only be marked as duplicates from the view finding screen. Similar Findings must be enabled for this operation.'); return false; }); } }; diff --git a/dojo/templates/dojo/metrics.html b/dojo/templates/dojo/metrics.html index fd32fb06876..f44b469a8be 100644 --- a/dojo/templates/dojo/metrics.html +++ b/dojo/templates/dojo/metrics.html @@ -150,7 +150,7 @@

- {% include "dojo/filter_snippet.html" with form=findings.form clear_link="/metrics/product/type" %} + {% include "dojo/filter_snippet.html" with form=form clear_link="/metrics/product/type" %}
diff --git a/dojo/templates/dojo/product_metrics.html b/dojo/templates/dojo/product_metrics.html index dc9d447f833..e50ea32d33f 100644 --- a/dojo/templates/dojo/product_metrics.html +++ b/dojo/templates/dojo/product_metrics.html @@ -50,11 +50,11 @@

- {{ verified_objs|length }} + {{ verified_objs }} Verified - {{ view }}{{ verified_objs|length|pluralize }}
@@ -79,10 +79,10 @@

- {{ open_objs|length }} + {{ open_objs }} Open - {{ view }}{{ open_objs|length|pluralize }}
@@ -107,7 +107,7 @@

- {{ accepted_objs|length }} + {{ accepted_objs }} Risk Accepted @@ -135,10 +135,10 @@

@@ -163,10 +163,10 @@

- {{ false_positive_objs|length }} + {{ false_positive_objs }} False-postive - {{ view }}{{ false_positive_objs|length|pluralize }}
@@ -191,11 +191,11 @@

- {{ out_of_scope_objs|length }} + {{ out_of_scope_objs }} Out Of Scope - {{ view }}{{ out_of_scope_objs|length|pluralize }}
@@ -220,10 +220,10 @@

- {{ all_objs|length }} + {{ all_objs }} Total - {{ view }}{{ all_objs|length|pluralize }}
@@ -248,10 +248,10 @@

- {{ inactive_objs|length }} + {{ inactive_objs }} Inactive - {{ view }}{{ inactive_objs|length|pluralize }}
@@ -406,8 +406,8 @@

@@ -456,8 +456,8 @@

@@ -471,8 +471,8 @@

@@ -701,17 +701,8 @@

}); finding_age(data_2, ticks); - data = []; - {% for x in open_vulnerabilities %} - data.push(['CWE-{{x.cwe}}', {{x.count}}]); - {% endfor %} - draw_vulnerabilities_graph("#open_vulnerabilities", data); - - data = []; - {% for x in all_vulnerabilities %} - data.push(['CWE-{{x.cwe}}', {{x.count}}]); - {% endfor %} - draw_vulnerabilities_graph("#all_vulnerabilities", data); + draw_vulnerabilities_graph("#open_vulnerabilities", {{ open_vulnerabilities|safe }}); + draw_vulnerabilities_graph("#all_vulnerabilities", {{ all_vulnerabilities|safe }}); //$(".product-graphs").hide(); $("#meta_accordion").accordion(); diff --git a/dojo/templates/dojo/view_finding.html b/dojo/templates/dojo/view_finding.html index c19d5f96578..f623f16d6aa 100755 --- a/dojo/templates/dojo/view_finding.html +++ b/dojo/templates/dojo/view_finding.html @@ -729,6 +729,7 @@

Duplicate Cluster ({{ finding|finding_duplicate_cluster_size }}) {% endif %} + {% if similar_findings_enabled %}

Similar Findings ({{ similar_findings.paginator.count }}) @@ -759,8 +760,8 @@

Similar Findings ({{ similar_findings.paginator.count }}

{% endif %} -
- + + {% endif %} {% comment %} Add a form to (ab)use to submit any actions related to similar/duplicates as POST requests {% endcomment %}