diff --git a/dojo/api_v2/serializers.py b/dojo/api_v2/serializers.py index cdc951fdb8..5cdc2db4d8 100644 --- a/dojo/api_v2/serializers.py +++ b/dojo/api_v2/serializers.py @@ -1324,6 +1324,11 @@ def validate(self, data): msg = "Either engagement or finding or finding_group has to be set." raise serializers.ValidationError(msg) + if finding: + if (linked_finding := jira_helper.jira_already_linked(finding, data.get("jira_key"), data.get("jira_id"))) is not None: + msg = "JIRA issue " + data.get("jira_key") + " already linked to " + reverse("view_finding", args=(linked_finding.id,)) + raise serializers.ValidationError(msg) + return data diff --git a/dojo/api_v2/views.py b/dojo/api_v2/views.py index 9676983684..384fc91c97 100644 --- a/dojo/api_v2/views.py +++ b/dojo/api_v2/views.py @@ -503,6 +503,10 @@ def notes(self, request, pk=None): new_note.errors, status=status.HTTP_400_BAD_REQUEST, ) + notes = engagement.notes.filter(note_type=note_type).first() + if notes and note_type and note_type.is_single: + return Response("Only one instance of this note_type allowed on an engagement.", status=status.HTTP_400_BAD_REQUEST) + author = request.user note = Notes( entry=entry, @@ -1078,6 +1082,11 @@ def notes(self, request, pk=None): new_note.errors, status=status.HTTP_400_BAD_REQUEST, ) + if finding.notes: + notes = finding.notes.filter(note_type=note_type).first() + if notes and note_type and note_type.is_single: + return Response("Only one instance of this note_type allowed on a finding.", status=status.HTTP_400_BAD_REQUEST) + author = request.user note = Notes( entry=entry, @@ -2131,6 +2140,10 @@ def notes(self, request, pk=None): new_note.errors, status=status.HTTP_400_BAD_REQUEST, ) + notes = test.notes.filter(note_type=note_type).first() + if notes and note_type and note_type.is_single: + return Response("Only one instance of this note_type allowed on a test.", status=status.HTTP_400_BAD_REQUEST) + author = request.user note = Notes( entry=entry, diff --git a/dojo/filters.py b/dojo/filters.py index a2c40685cd..6a1228865b 100644 --- a/dojo/filters.py +++ b/dojo/filters.py @@ -235,6 +235,35 @@ def filter(self, qs, value): return self.options[value][1](self, qs, self.field_name) +class FindingHasJIRAFilter(ChoiceFilter): + def no_jira(self, qs, name): + return qs.filter(Q(jira_issue=None) & Q(finding_group__jira_issue=None)) + + def any_jira(self, qs, name): + return qs.filter(~Q(jira_issue=None) | ~Q(finding_group__jira_issue=None)) + + def all_items(self, qs, name): + return qs + + options = { + 0: (_("Yes"), any_jira), + 1: (_("No"), no_jira), + } + + def __init__(self, *args, **kwargs): + kwargs["choices"] = [ + (key, value[0]) for key, value in six.iteritems(self.options)] + super().__init__(*args, **kwargs) + + def filter(self, qs, value): + try: + value = int(value) + except (ValueError, TypeError): + return self.all_items(qs, self.field_name) + + return self.options[value][1](self, qs, self.field_name) + + class ProductSLAFilter(ChoiceFilter): def any(self, qs, name): return qs @@ -1576,6 +1605,7 @@ class FindingFilterHelper(FilterSet): test_import_finding_action__test_import = NumberFilter(widget=HiddenInput()) endpoints = NumberFilter(widget=HiddenInput()) status = FindingStatusFilter(label="Status") + has_component = BooleanFilter( field_name="component_name", lookup_expr="isnull", @@ -1610,6 +1640,7 @@ class FindingFilterHelper(FilterSet): lookup_expr="isnull", exclude=True, label="Has Group JIRA") + has_any_jira = FindingHasJIRAFilter(label="Has Any JIRA") outside_of_sla = FindingSLAFilter(label="Outside of SLA") has_tags = BooleanFilter(field_name="tags", lookup_expr="isnull", exclude=True, label="Has tags") diff --git a/dojo/finding/views.py b/dojo/finding/views.py index a5d6824329..18faed336b 100644 --- a/dojo/finding/views.py +++ b/dojo/finding/views.py @@ -1696,7 +1696,7 @@ def request_finding_review(request, fid): jira_helper.push_to_jira(finding.finding_group) reviewers = Dojo_User.objects.filter(id__in=form.cleaned_data["reviewers"]) - reviewers_string = ", ".join([str(user) for user in reviewers]) + reviewers_string = ", ".join([f"{user} ({user.id})" for user in reviewers]) reviewers_usernames = [user.username for user in reviewers] logger.debug(f"Asking {reviewers_string} for review") @@ -1708,7 +1708,7 @@ def request_finding_review(request, fid): finding=finding, reviewers=reviewers, recipients=reviewers_usernames, - description=f'User {user.get_full_name()} has requested that user(s) {reviewers_string} review the finding "{finding.title}" for accuracy:\n\n{new_note}', + description=f'User {user.get_full_name()}({user.id}) has requested that user(s) {reviewers_string} review the finding "{finding.title}" for accuracy:\n\n{new_note}', icon="check", url=reverse("view_finding", args=(finding.id,)), ) diff --git a/dojo/jira_link/helper.py b/dojo/jira_link/helper.py index f10ea69916..308331987a 100644 --- a/dojo/jira_link/helper.py +++ b/dojo/jira_link/helper.py @@ -1428,6 +1428,13 @@ def add_simple_jira_comment(jira_instance, jira_issue, comment): return False +def jira_already_linked(finding, jira_issue_key, jira_id) -> Finding | None: + jira_issues = JIRA_Issue.objects.filter(jira_id=jira_id, jira_key=jira_issue_key).exclude(engagement__isnull=False) + jira_issues = jira_issues.exclude(finding=finding) + + return jira_issues.first() + + def finding_link_jira(request, finding, new_jira_issue_key): logger.debug("linking existing jira issue %s for finding %i", new_jira_issue_key, finding.id) diff --git a/dojo/settings/settings.dist.py b/dojo/settings/settings.dist.py index 95fd8768c2..b50ec84876 100644 --- a/dojo/settings/settings.dist.py +++ b/dojo/settings/settings.dist.py @@ -1759,6 +1759,9 @@ def saml2_attrib_map_format(dict): "ALSA": "https://osv.dev/vulnerability/", # e.g. https://osv.dev/vulnerability/ALSA-2024:0827 "USN": "https://ubuntu.com/security/notices/", # e.g. https://ubuntu.com/security/notices/USN-6642-1 "DLA": "https://security-tracker.debian.org/tracker/", # e.g. https://security-tracker.debian.org/tracker/DLA-3917-1 + "DSA": "https://security-tracker.debian.org/tracker/", # e.g. https://security-tracker.debian.org/tracker/DSA-5791-1 + "DTSA": "https://security-tracker.debian.org/tracker/", # e.g. https://security-tracker.debian.org/tracker/DTSA-41-1 + "TEMP": "https://security-tracker.debian.org/tracker/", # e.g. https://security-tracker.debian.org/tracker/TEMP-0841856-B18BAF "ELSA": "https://linux.oracle.com/errata/&&.html", # e.g. https://linux.oracle.com/errata/ELSA-2024-12714.html "ELBA": "https://linux.oracle.com/errata/&&.html", # e.g. https://linux.oracle.com/errata/ELBA-2024-7457.html "RXSA": "https://errata.rockylinux.org/", # e.g. https://errata.rockylinux.org/RXSA-2024:4928 @@ -1767,8 +1770,6 @@ def saml2_attrib_map_format(dict): "KHV": "https://avd.aquasec.com/misconfig/kubernetes/", # e.g. https://avd.aquasec.com/misconfig/kubernetes/khv045 "CAPEC": "https://capec.mitre.org/data/definitions/&&.html", # e.g. https://capec.mitre.org/data/definitions/157.html "CWE": "https://cwe.mitre.org/data/definitions/&&.html", # e.g. https://cwe.mitre.org/data/definitions/79.html - "TEMP": "https://security-tracker.debian.org/tracker/", # e.g. https://security-tracker.debian.org/tracker/TEMP-0841856-B18BAF - "DSA": "https://security-tracker.debian.org/tracker/", # e.g. https://security-tracker.debian.org/tracker/DSA-5791-1 "RLSA": "https://errata.rockylinux.org/", # e.g. https://errata.rockylinux.org/RLSA-2024:7001 "RLBA": "https://errata.rockylinux.org/", # e.g. https://errata.rockylinux.org/RLBA-2024:6968 } diff --git a/dojo/static/dojo/js/metrics.js b/dojo/static/dojo/js/metrics.js index 2fd518aa3a..140c46c1b2 100644 --- a/dojo/static/dojo/js/metrics.js +++ b/dojo/static/dojo/js/metrics.js @@ -57,7 +57,8 @@ function homepage_pie_chart(critical, high, medium, low, info) { function homepage_severity_plot(critical, high, medium, low) { var options = { xaxes: [{ - mode: 'time' + mode: 'time', + minTickSize: [1, "month"] }], yaxes: [{ min: 0