diff --git a/.github/workflows/test-helm-chart.yml b/.github/workflows/test-helm-chart.yml index 8c2837706ca..5fcf845076a 100644 --- a/.github/workflows/test-helm-chart.yml +++ b/.github/workflows/test-helm-chart.yml @@ -20,13 +20,11 @@ jobs: fetch-depth: 0 - name: Set up Helm - uses: azure/setup-helm@v4 - with: - version: v3.4.0 + uses: azure/setup-helm@v4.1.0 - uses: actions/setup-python@v5 with: - python-version: 3.7 + python-version: 3.9 - name: Configure Helm repos run: |- @@ -36,14 +34,17 @@ jobs: - name: Set up chart-testing uses: helm/chart-testing-action@v2.6.1 + with: + yamale_version: 4.0.4 + yamllint_version: 1.35.1 - name: Determine target branch id: ct-branch-target run: | if [ ! -z ${GITHUB_BASE_REF} ]; then - echo "ct-branch=${GITHUB_BASE_REF}" >> $GITHUB_ENV + echo "ct-branch=${GITHUB_BASE_REF}" >> $GITHUB_ENV else - echo "ct-branch=${GITHUB_REF#refs/heads/}" >> $GITHUB_ENV + echo "ct-branch=${GITHUB_REF#refs/heads/}" >> $GITHUB_ENV fi - name: Run chart-testing (list-changed) @@ -58,7 +59,7 @@ jobs: # x.y.z gets bumped automatically when doing a release - name: Run chart-testing (lint) run: ct lint --config ct.yaml --target-branch ${{ env.ct-branch }} --check-version-increment=true - if: ${{ env.changed == 'true' && env.ct-branch != 'dev' && env.ct-branch != 'bugfix' }} + if: ${{ env.changed == 'true' && env.ct-branch != 'dev' && env.ct-branch != 'bugfix' }} # run all checks but version increment always when something changed - name: Run chart-testing (lint) diff --git a/components/package.json b/components/package.json index 5e46baad2d7..5528766c57a 100644 --- a/components/package.json +++ b/components/package.json @@ -1,6 +1,6 @@ { "name": "defectdojo", - "version": "2.32.0", + "version": "2.33.0-dev", "license" : "BSD-3-Clause", "private": true, "dependencies": { diff --git a/components/yarn.lock b/components/yarn.lock index ffe72a3aaf0..d3d65c363f5 100644 --- a/components/yarn.lock +++ b/components/yarn.lock @@ -538,10 +538,6 @@ fast-levenshtein@~2.0.6: resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw== -flot-axis@markrcote/flot-axislabels#*: - version "0.0.0" - resolved "https://codeload.github.com/markrcote/flot-axislabels/tar.gz/a181e09d04d120d05e5bc2baaa8738b5b3670428" - flot@flot/flot#~0.8.3: version "0.8.3" resolved "https://codeload.github.com/flot/flot/tar.gz/453b017cc5acfd75e252b93e8635f57f4196d45d" diff --git a/docs/content/en/getting_started/upgrading/2.33.md b/docs/content/en/getting_started/upgrading/2.33.md new file mode 100644 index 00000000000..19e9449cba4 --- /dev/null +++ b/docs/content/en/getting_started/upgrading/2.33.md @@ -0,0 +1,7 @@ +--- +title: 'Upgrading to DefectDojo Version 2.33.x' +toc_hide: true +weight: -20240304 +description: No special instructions. +--- +There are no special instructions for upgrading to 2.33.x. Check the [Release Notes](https://github.com/DefectDojo/django-DefectDojo/releases/tag/2.33.0) for the contents of the release. diff --git a/dojo/__init__.py b/dojo/__init__.py index 1ee28916860..61db2f0d7a1 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.32.0' +__version__ = '2.33.0-dev' __url__ = 'https://github.com/DefectDojo/django-DefectDojo' __docs__ = 'https://documentation.defectdojo.com' diff --git a/dojo/db_migrations/0204_jira_project_epic_issue_type_name.py b/dojo/db_migrations/0204_jira_project_epic_issue_type_name.py new file mode 100644 index 00000000000..88b5f922a03 --- /dev/null +++ b/dojo/db_migrations/0204_jira_project_epic_issue_type_name.py @@ -0,0 +1,18 @@ +# Generated by Django 4.1.13 on 2024-03-01 20:39 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('dojo', '0203_alter_finding_options_finding_epss_percentile_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='jira_project', + name='epic_issue_type_name', + field=models.CharField(default='Epic', blank=True, help_text='The name of the of structure that represents an Epic', max_length=64), + ), + ] diff --git a/dojo/forms.py b/dojo/forms.py index 10ea312ea91..4047ac7aeb3 100755 --- a/dojo/forms.py +++ b/dojo/forms.py @@ -2709,7 +2709,7 @@ class JIRAProjectForm(forms.ModelForm): class Meta: model = JIRA_Project exclude = ['product', 'engagement'] - fields = ['inherit_from_product', 'jira_instance', 'project_key', 'issue_template_dir', 'component', 'custom_fields', 'jira_labels', 'default_assignee', 'add_vulnerability_id_to_jira_label', 'push_all_issues', 'enable_engagement_epic_mapping', 'push_notes', 'product_jira_sla_notification', 'risk_acceptance_expiration_notification'] + fields = ['inherit_from_product', 'jira_instance', 'project_key', 'issue_template_dir', 'epic_issue_type_name', 'component', 'custom_fields', 'jira_labels', 'default_assignee', 'add_vulnerability_id_to_jira_label', 'push_all_issues', 'enable_engagement_epic_mapping', 'push_notes', 'product_jira_sla_notification', 'risk_acceptance_expiration_notification'] def __init__(self, *args, **kwargs): from dojo.jira_link import helper as jira_helper @@ -2742,6 +2742,7 @@ def __init__(self, *args, **kwargs): self.fields['jira_instance'].disabled = False self.fields['project_key'].disabled = False self.fields['issue_template_dir'].disabled = False + self.fields['epic_issue_type_name'].disabled = False self.fields['component'].disabled = False self.fields['custom_fields'].disabled = False self.fields['default_assignee'].disabled = False @@ -2765,6 +2766,7 @@ def __init__(self, *args, **kwargs): self.initial['jira_instance'] = jira_project_product.jira_instance.id if jira_project_product.jira_instance else None self.initial['project_key'] = jira_project_product.project_key self.initial['issue_template_dir'] = jira_project_product.issue_template_dir + self.initial['epic_issue_type_name'] = jira_project_product.epic_issue_type_name self.initial['component'] = jira_project_product.component self.initial['custom_fields'] = jira_project_product.custom_fields self.initial['default_assignee'] = jira_project_product.default_assignee @@ -2779,6 +2781,7 @@ def __init__(self, *args, **kwargs): self.fields['jira_instance'].disabled = True self.fields['project_key'].disabled = True self.fields['issue_template_dir'].disabled = True + self.fields['epic_issue_type_name'].disabled = True self.fields['component'].disabled = True self.fields['custom_fields'].disabled = True self.fields['default_assignee'].disabled = True @@ -2798,6 +2801,7 @@ def __init__(self, *args, **kwargs): if self.instance.id: self.fields['jira_instance'].required = True self.fields['project_key'].required = True + self.fields['epic_issue_type_name'].required = True def clean(self): logger.debug('validating jira project form') @@ -2807,17 +2811,18 @@ def clean(self): if not self.cleaned_data.get('inherit_from_product', False): jira_instance = self.cleaned_data.get('jira_instance') project_key = self.cleaned_data.get('project_key') + epic_issue_type_name = self.cleaned_data.get('epic_issue_type_name') - if project_key and jira_instance: + if project_key and jira_instance and epic_issue_type_name: return cleaned_data - if not project_key and not jira_instance: + if not project_key and not jira_instance and not epic_issue_type_name: return cleaned_data if self.target == 'engagement': - raise ValidationError('JIRA Project needs a JIRA Instance and JIRA Project Key, or choose to inherit settings from product') + raise ValidationError('JIRA Project needs a JIRA Instance, JIRA Project Key, and Epic issue type name, or choose to inherit settings from product') else: - raise ValidationError('JIRA Project needs a JIRA Instance and JIRA Project Key, leave empty to have no JIRA integration setup') + raise ValidationError('JIRA Project needs a JIRA Instance, JIRA Project Key, and Epic issue type name, leave empty to have no JIRA integration setup') class GITHUBFindingForm(forms.Form): diff --git a/dojo/jira_link/helper.py b/dojo/jira_link/helper.py index 5318aa0e3ed..4f7360fc465 100644 --- a/dojo/jira_link/helper.py +++ b/dojo/jira_link/helper.py @@ -1223,7 +1223,7 @@ def add_epic(engagement, **kwargs): 'summary': epic_name, 'description': epic_name, 'issuetype': { - 'name': 'Epic' + 'name': getattr(jira_project, "epic_issue_type_name", "Epic"), }, get_epic_name_field_name(jira_instance): epic_name, } diff --git a/dojo/models.py b/dojo/models.py index 98922853f47..36918c88dec 100755 --- a/dojo/models.py +++ b/dojo/models.py @@ -3890,6 +3890,7 @@ class JIRA_Project(models.Model): help_text=_("Automatically maintain parity with JIRA. Always create and update JIRA tickets for findings in this Product.")) enable_engagement_epic_mapping = models.BooleanField(default=False, blank=True) + epic_issue_type_name = models.CharField(max_length=64, blank=True, default="Epic", help_text=_("The name of the of structure that represents an Epic")) push_notes = models.BooleanField(default=False, blank=True) product_jira_sla_notification = models.BooleanField(default=False, blank=True, verbose_name=_("Send SLA notifications as comment?")) risk_acceptance_expiration_notification = models.BooleanField(default=False, blank=True, verbose_name=_("Send Risk Acceptance expiration notifications as comment?")) diff --git a/dojo/reports/urls.py b/dojo/reports/urls.py index e4489708fc1..31503b6c512 100644 --- a/dojo/reports/urls.py +++ b/dojo/reports/urls.py @@ -31,9 +31,9 @@ re_path(r'^reports/custom$', views.custom_report, name='custom_report'), re_path(r'^reports/quick$', - views.quick_report, name='quick_report'), + views.QuickReportView.as_view(), name='quick_report'), re_path(r'^reports/csv_export$', - views.csv_export, name='csv_export'), + views.CSVExportView.as_view(), name='csv_export'), re_path(r'^reports/excel_export$', - views.excel_export, name='excel_export'), + views.ExcelExportView.as_view(), name='excel_export'), ] diff --git a/dojo/reports/views.py b/dojo/reports/views.py index b40efb85ffc..3380d70e954 100644 --- a/dojo/reports/views.py +++ b/dojo/reports/views.py @@ -14,6 +14,7 @@ from django.shortcuts import render, get_object_or_404 from django.utils import timezone from django.core.exceptions import PermissionDenied +from django.views import View from dojo.filters import ReportFindingFilter, EndpointReportFilter, \ EndpointFilter @@ -669,30 +670,6 @@ def prefetch_related_endpoints_for_report(endpoints): ) -def generate_quick_report(request, findings, obj=None): - product = engagement = test = None - - if obj: - if type(obj).__name__ == "Product": - product = obj - elif type(obj).__name__ == "Engagement": - engagement = obj - elif type(obj).__name__ == "Test": - test = obj - - return render(request, 'dojo/finding_pdf_report.html', { - 'report_name': 'Finding Report', - 'product': product, - 'engagement': engagement, - 'test': test, - 'findings': findings, - 'user': request.user, - 'team_name': settings.TEAM_NAME, - 'title': 'Finding Report', - 'user_id': request.user.id, - }) - - def get_list_index(list, index): try: element = list[index] @@ -787,9 +764,41 @@ def get_findings(request): return findings, obj -def quick_report(request): - findings, obj = get_findings(request) - return generate_quick_report(request, findings, obj) +class QuickReportView(View): + def add_findings_data(self): + return self.findings + + def get_template(self): + return 'dojo/finding_pdf_report.html' + + def get(self, request): + findings, obj = get_findings(request) + self.findings = findings + findings = self.add_findings_data() + return self.generate_quick_report(request, findings, obj) + + def generate_quick_report(self, request, findings, obj=None): + product = engagement = test = None + + if obj: + if type(obj).__name__ == "Product": + product = obj + elif type(obj).__name__ == "Engagement": + engagement = obj + elif type(obj).__name__ == "Test": + test = obj + + return render(request, self.get_template(), { + 'report_name': 'Finding Report', + 'product': product, + 'engagement': engagement, + 'test': test, + 'findings': findings, + 'user': request.user, + 'team_name': settings.TEAM_NAME, + 'title': 'Finding Report', + 'user_id': request.user.id, + }) def get_excludes(): @@ -809,219 +818,286 @@ def get_attributes(): return ["sla_age", "sla_deadline", "sla_days_remaining"] -def csv_export(request): - findings, obj = get_findings(request) - response = HttpResponse(content_type='text/csv') - response['Content-Disposition'] = 'attachment; filename=findings.csv' - writer = csv.writer(response) - allowed_attributes = get_attributes() - excludes_list = get_excludes() - allowed_foreign_keys = get_attributes() - first_row = True - - for finding in findings: - if first_row: - fields = [] - for key in dir(finding): - try: - if key not in excludes_list and (not callable(getattr(finding, key)) or key in allowed_attributes) and not key.startswith('_'): - if callable(getattr(finding, key)) and key not in allowed_attributes: - continue +class CSVExportView(View): + def add_findings_data(self): + return self.findings + + def add_extra_headers(self): + pass + + def add_extra_values(self): + pass + + def get(self, request): + findings, obj = get_findings(request) + self.findings = findings + findings = self.add_findings_data() + response = HttpResponse(content_type='text/csv') + response['Content-Disposition'] = 'attachment; filename=findings.csv' + writer = csv.writer(response) + allowed_attributes = get_attributes() + excludes_list = get_excludes() + allowed_foreign_keys = get_attributes() + first_row = True + + for finding in findings: + self.finding = finding + if first_row: + fields = [] + self.fields = fields + for key in dir(finding): + try: + if key not in excludes_list and (not callable(getattr(finding, key)) or key in allowed_attributes) and not key.startswith('_'): + if callable(getattr(finding, key)) and key not in allowed_attributes: + continue + fields.append(key) + except Exception as exc: + logger.error('Error in attribute: ' + str(exc)) fields.append(key) - except Exception as exc: - logger.error('Error in attribute: ' + str(exc)) - fields.append(key) - continue - fields.append('test') - fields.append('found_by') - fields.append('engagement_id') - fields.append('engagement') - fields.append('product_id') - fields.append('product') - fields.append('endpoints') - fields.append('vulnerability_ids') - - writer.writerow(fields) - - first_row = False - if not first_row: - fields = [] - for key in dir(finding): - try: - if key not in excludes_list and (not callable(getattr(finding, key)) or key in allowed_attributes) and not key.startswith('_'): - if not callable(getattr(finding, key)): - value = finding.__dict__.get(key) - if (key in allowed_foreign_keys or key in allowed_attributes) and getattr(finding, key): - if callable(getattr(finding, key)): - func = getattr(finding, key) - result = func() - value = result - else: - value = str(getattr(finding, key)) - if value and isinstance(value, str): - value = value.replace('\n', ' NEWLINE ').replace('\r', '') - fields.append(value) - except Exception as exc: - logger.error('Error in attribute: ' + str(exc)) - fields.append("Value not supported") - continue - fields.append(finding.test.title) - fields.append(finding.test.test_type.name) - fields.append(finding.test.engagement.id) - fields.append(finding.test.engagement.name) - fields.append(finding.test.engagement.product.id) - fields.append(finding.test.engagement.product.name) - - endpoint_value = '' - num_endpoints = 0 - for endpoint in finding.endpoints.all(): - num_endpoints += 1 - if num_endpoints > 5: - endpoint_value += '...' - break - endpoint_value += f'{str(endpoint)}; ' - if endpoint_value.endswith('; '): - endpoint_value = endpoint_value[:-2] - fields.append(endpoint_value) - - vulnerability_ids_value = '' - num_vulnerability_ids = 0 - for vulnerability_id in finding.vulnerability_ids: - num_vulnerability_ids += 1 - if num_vulnerability_ids > 5: - vulnerability_ids_value += '...' - break - vulnerability_ids_value += f'{str(vulnerability_id)}; ' - if finding.cve and vulnerability_ids_value.find(finding.cve) < 0: - vulnerability_ids_value += finding.cve - if vulnerability_ids_value.endswith('; '): - vulnerability_ids_value = vulnerability_ids_value[:-2] - fields.append(vulnerability_ids_value) - - writer.writerow(fields) - - return response - - -def excel_export(request): - findings, obj = get_findings(request) - workbook = Workbook() - workbook.iso_dates = True - worksheet = workbook.active - worksheet.title = 'Findings' - font_bold = Font(bold=True) - allowed_attributes = get_attributes() - excludes_list = get_excludes() - allowed_foreign_keys = get_attributes() - - row_num = 1 - for finding in findings: - if row_num == 1: - col_num = 1 - for key in dir(finding): - try: - if key not in excludes_list and (not callable(getattr(finding, key)) or key in allowed_attributes) and not key.startswith('_'): - if callable(getattr(finding, key)) and key not in allowed_attributes: - continue + continue + fields.append('test') + fields.append('found_by') + fields.append('engagement_id') + fields.append('engagement') + fields.append('product_id') + fields.append('product') + fields.append('endpoints') + fields.append('vulnerability_ids') + fields.append('tags') + self.fields = fields + self.add_extra_headers() + + writer.writerow(fields) + + first_row = False + if not first_row: + fields = [] + for key in dir(finding): + try: + if key not in excludes_list and (not callable(getattr(finding, key)) or key in allowed_attributes) and not key.startswith('_'): + if not callable(getattr(finding, key)): + value = finding.__dict__.get(key) + if (key in allowed_foreign_keys or key in allowed_attributes) and getattr(finding, key): + if callable(getattr(finding, key)): + func = getattr(finding, key) + result = func() + value = result + else: + value = str(getattr(finding, key)) + if value and isinstance(value, str): + value = value.replace('\n', ' NEWLINE ').replace('\r', '') + fields.append(value) + except Exception as exc: + logger.error('Error in attribute: ' + str(exc)) + fields.append("Value not supported") + continue + fields.append(finding.test.title) + fields.append(finding.test.test_type.name) + fields.append(finding.test.engagement.id) + fields.append(finding.test.engagement.name) + fields.append(finding.test.engagement.product.id) + fields.append(finding.test.engagement.product.name) + + endpoint_value = '' + num_endpoints = 0 + for endpoint in finding.endpoints.all(): + num_endpoints += 1 + if num_endpoints > 5: + endpoint_value += '...' + break + endpoint_value += f'{str(endpoint)}; ' + if endpoint_value.endswith('; '): + endpoint_value = endpoint_value[:-2] + fields.append(endpoint_value) + + vulnerability_ids_value = '' + num_vulnerability_ids = 0 + for vulnerability_id in finding.vulnerability_ids: + num_vulnerability_ids += 1 + if num_vulnerability_ids > 5: + vulnerability_ids_value += '...' + break + vulnerability_ids_value += f'{str(vulnerability_id)}; ' + if finding.cve and vulnerability_ids_value.find(finding.cve) < 0: + vulnerability_ids_value += finding.cve + if vulnerability_ids_value.endswith('; '): + vulnerability_ids_value = vulnerability_ids_value[:-2] + fields.append(vulnerability_ids_value) + # Tags + tags_value = '' + num_tags = 0 + for tag in finding.tags.all(): + num_tags += 1 + if num_tags > 5: + tags_value += '...' + break + tags_value += f'{str(tag)}; ' + if tags_value.endswith('; '): + tags_value = tags_value[:-2] + fields.append(tags_value) + + self.fields = fields + self.finding = finding + self.add_extra_values() + + writer.writerow(fields) + + return response + + +class ExcelExportView(View): + + def add_findings_data(self): + return self.findings + + def add_extra_headers(self): + pass + + def add_extra_values(self): + pass + + def get(self, request): + findings, obj = get_findings(request) + self.findings = findings + findings = self.add_findings_data() + workbook = Workbook() + workbook.iso_dates = True + worksheet = workbook.active + worksheet.title = 'Findings' + self.worksheet = worksheet + font_bold = Font(bold=True) + self.font_bold = font_bold + allowed_attributes = get_attributes() + excludes_list = get_excludes() + allowed_foreign_keys = get_attributes() + + row_num = 1 + for finding in findings: + if row_num == 1: + col_num = 1 + for key in dir(finding): + try: + if key not in excludes_list and (not callable(getattr(finding, key)) or key in allowed_attributes) and not key.startswith('_'): + if callable(getattr(finding, key)) and key not in allowed_attributes: + continue + cell = worksheet.cell(row=row_num, column=col_num, value=key) + cell.font = font_bold + col_num += 1 + except Exception as exc: + logger.error('Error in attribute: ' + str(exc)) cell = worksheet.cell(row=row_num, column=col_num, value=key) - cell.font = font_bold - col_num += 1 - except Exception as exc: - logger.error('Error in attribute: ' + str(exc)) - cell = worksheet.cell(row=row_num, column=col_num, value=key) - continue - cell = worksheet.cell(row=row_num, column=col_num, value='found_by') - cell.font = font_bold - col_num += 1 - worksheet.cell(row=row_num, column=col_num, value='engagement_id') - cell = cell.font = font_bold - col_num += 1 - cell = worksheet.cell(row=row_num, column=col_num, value='engagement') - cell.font = font_bold - col_num += 1 - cell = worksheet.cell(row=row_num, column=col_num, value='product_id') - cell.font = font_bold - col_num += 1 - cell = worksheet.cell(row=row_num, column=col_num, value='product') - cell.font = font_bold - col_num += 1 - cell = worksheet.cell(row=row_num, column=col_num, value='endpoints') - cell.font = font_bold - col_num += 1 - cell = worksheet.cell(row=row_num, column=col_num, value='vulnerability_ids') - cell.font = font_bold - - row_num = 2 - if row_num > 1: - col_num = 1 - for key in dir(finding): - try: - if key not in excludes_list and (not callable(getattr(finding, key)) or key in allowed_attributes) and not key.startswith('_'): - if not callable(getattr(finding, key)): - value = finding.__dict__.get(key) - if (key in allowed_foreign_keys or key in allowed_attributes) and getattr(finding, key): - if callable(getattr(finding, key)): - func = getattr(finding, key) - result = func() - value = result - else: - value = str(getattr(finding, key)) - if value and isinstance(value, datetime): - value = value.replace(tzinfo=None) - worksheet.cell(row=row_num, column=col_num, value=value) - col_num += 1 - except Exception as exc: - logger.error('Error in attribute: ' + str(exc)) - worksheet.cell(row=row_num, column=col_num, value="Value not supported") - continue - worksheet.cell(row=row_num, column=col_num, value=finding.test.test_type.name) - col_num += 1 - worksheet.cell(row=row_num, column=col_num, value=finding.test.engagement.id) - col_num += 1 - worksheet.cell(row=row_num, column=col_num, value=finding.test.engagement.name) - col_num += 1 - worksheet.cell(row=row_num, column=col_num, value=finding.test.engagement.product.id) - col_num += 1 - worksheet.cell(row=row_num, column=col_num, value=finding.test.engagement.product.name) - col_num += 1 - - endpoint_value = '' - num_endpoints = 0 - for endpoint in finding.endpoints.all(): - num_endpoints += 1 - if num_endpoints > 5: - endpoint_value += '...' - break - endpoint_value += f'{str(endpoint)}; \n' - if endpoint_value.endswith('; \n'): - endpoint_value = endpoint_value[:-3] - worksheet.cell(row=row_num, column=col_num, value=endpoint_value) - col_num += 1 - - vulnerability_ids_value = '' - num_vulnerability_ids = 0 - for vulnerability_id in finding.vulnerability_ids: - num_vulnerability_ids += 1 - if num_vulnerability_ids > 5: - vulnerability_ids_value += '...' - break - vulnerability_ids_value += f'{str(vulnerability_id)}; \n' - if finding.cve and vulnerability_ids_value.find(finding.cve) < 0: - vulnerability_ids_value += finding.cve - if vulnerability_ids_value.endswith('; \n'): - vulnerability_ids_value = vulnerability_ids_value[:-3] - worksheet.cell(row=row_num, column=col_num, value=vulnerability_ids_value) - - row_num += 1 - - with NamedTemporaryFile() as tmp: - workbook.save(tmp.name) - tmp.seek(0) - stream = tmp.read() - - response = HttpResponse( - content=stream, - content_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' - ) - response['Content-Disposition'] = 'attachment; filename=findings.xlsx' - return response + continue + cell = worksheet.cell(row=row_num, column=col_num, value='found_by') + cell.font = font_bold + col_num += 1 + worksheet.cell(row=row_num, column=col_num, value='engagement_id') + cell = cell.font = font_bold + col_num += 1 + cell = worksheet.cell(row=row_num, column=col_num, value='engagement') + cell.font = font_bold + col_num += 1 + cell = worksheet.cell(row=row_num, column=col_num, value='product_id') + cell.font = font_bold + col_num += 1 + cell = worksheet.cell(row=row_num, column=col_num, value='product') + cell.font = font_bold + col_num += 1 + cell = worksheet.cell(row=row_num, column=col_num, value='endpoints') + cell.font = font_bold + col_num += 1 + cell = worksheet.cell(row=row_num, column=col_num, value='vulnerability_ids') + cell.font = font_bold + col_num += 1 + cell = worksheet.cell(row=row_num, column=col_num, value='tags') + cell.font = font_bold + col_num += 1 + self.row_num = row_num + self.col_num = col_num + self.add_extra_headers() + + row_num = 2 + if row_num > 1: + col_num = 1 + for key in dir(finding): + try: + if key not in excludes_list and (not callable(getattr(finding, key)) or key in allowed_attributes) and not key.startswith('_'): + if not callable(getattr(finding, key)): + value = finding.__dict__.get(key) + if (key in allowed_foreign_keys or key in allowed_attributes) and getattr(finding, key): + if callable(getattr(finding, key)): + func = getattr(finding, key) + result = func() + value = result + else: + value = str(getattr(finding, key)) + if value and isinstance(value, datetime): + value = value.replace(tzinfo=None) + worksheet.cell(row=row_num, column=col_num, value=value) + col_num += 1 + except Exception as exc: + logger.error('Error in attribute: ' + str(exc)) + worksheet.cell(row=row_num, column=col_num, value="Value not supported") + continue + worksheet.cell(row=row_num, column=col_num, value=finding.test.test_type.name) + col_num += 1 + worksheet.cell(row=row_num, column=col_num, value=finding.test.engagement.id) + col_num += 1 + worksheet.cell(row=row_num, column=col_num, value=finding.test.engagement.name) + col_num += 1 + worksheet.cell(row=row_num, column=col_num, value=finding.test.engagement.product.id) + col_num += 1 + worksheet.cell(row=row_num, column=col_num, value=finding.test.engagement.product.name) + col_num += 1 + + endpoint_value = '' + num_endpoints = 0 + for endpoint in finding.endpoints.all(): + num_endpoints += 1 + if num_endpoints > 5: + endpoint_value += '...' + break + endpoint_value += f'{str(endpoint)}; \n' + if endpoint_value.endswith('; \n'): + endpoint_value = endpoint_value[:-3] + worksheet.cell(row=row_num, column=col_num, value=endpoint_value) + col_num += 1 + + vulnerability_ids_value = '' + num_vulnerability_ids = 0 + for vulnerability_id in finding.vulnerability_ids: + num_vulnerability_ids += 1 + if num_vulnerability_ids > 5: + vulnerability_ids_value += '...' + break + vulnerability_ids_value += f'{str(vulnerability_id)}; \n' + if finding.cve and vulnerability_ids_value.find(finding.cve) < 0: + vulnerability_ids_value += finding.cve + if vulnerability_ids_value.endswith('; \n'): + vulnerability_ids_value = vulnerability_ids_value[:-3] + worksheet.cell(row=row_num, column=col_num, value=vulnerability_ids_value) + col_num += 1 + # tags + tags_value = '' + for tag in finding.tags.all(): + tags_value += f'{str(tag)}; \n' + if tags_value.endswith('; \n'): + tags_value = tags_value[:-3] + worksheet.cell(row=row_num, column=col_num, value=tags_value) + col_num += 1 + self.col_num = col_num + self.row_num = row_num + self.finding = finding + self.add_extra_values() + row_num += 1 + + with NamedTemporaryFile() as tmp: + workbook.save(tmp.name) + tmp.seek(0) + stream = tmp.read() + + response = HttpResponse( + content=stream, + content_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' + ) + response['Content-Disposition'] = 'attachment; filename=findings.xlsx' + return response diff --git a/dojo/templates/dojo/finding_pdf_report.html b/dojo/templates/dojo/finding_pdf_report.html index 8a229b072ac..2b809deb71f 100644 --- a/dojo/templates/dojo/finding_pdf_report.html +++ b/dojo/templates/dojo/finding_pdf_report.html @@ -87,6 +87,7 @@
+ {% block finding_header %} @@ -103,8 +104,10 @@
{% if finding.cwe > 0 %}
{% endif %} + {% endblock finding_header %} + {% block finding_data %} {% endif %} - + {% endblock finding_data %}
Severity EPSS Score / Percentile StatusCWE
{% if finding.severity %} @@ -144,7 +147,7 @@
diff --git a/dojo/tools/checkmarx/parser.py b/dojo/tools/checkmarx/parser.py index d8be5b8b680..4f1f07d7256 100755 --- a/dojo/tools/checkmarx/parser.py +++ b/dojo/tools/checkmarx/parser.py @@ -58,7 +58,7 @@ def _get_findings_xml(self, filename, test): language = "" findingdetail = "" group = "" - find_date = parser.parse(root.get("ScanStart")) + find_date = parser.parse(root.get("ScanStart")).date() if query.get("Language") is not None: language = query.get("Language") @@ -389,9 +389,9 @@ def get_findings(self, file, test): def _parse_date(self, value): if isinstance(value, str): - return parser.parse(value) + return parser.parse(value).date() elif isinstance(value, dict) and isinstance(value.get("seconds"), int): - return datetime.datetime.utcfromtimestamp(value.get("seconds")) + return datetime.datetime.utcfromtimestamp(value.get("seconds")).date() else: return None diff --git a/helm/defectdojo/Chart.yaml b/helm/defectdojo/Chart.yaml index 82d01cfa8c2..e5eb9b0e926 100644 --- a/helm/defectdojo/Chart.yaml +++ b/helm/defectdojo/Chart.yaml @@ -1,8 +1,8 @@ apiVersion: v2 -appVersion: "2.32.0" +appVersion: "2.33.0-dev" description: A Helm chart for Kubernetes to install DefectDojo name: defectdojo -version: 1.6.114 +version: 1.6.115-dev icon: https://www.defectdojo.org/img/favicon.ico maintainers: - name: madchap diff --git a/requirements-lint.txt b/requirements-lint.txt index c9a88bba675..3b558127b00 100644 --- a/requirements-lint.txt +++ b/requirements-lint.txt @@ -1 +1 @@ -ruff==0.3.0 \ No newline at end of file +ruff==0.3.1 \ No newline at end of file diff --git a/unittests/dojo_test_case.py b/unittests/dojo_test_case.py index f6f08679b42..48647f69524 100644 --- a/unittests/dojo_test_case.py +++ b/unittests/dojo_test_case.py @@ -158,6 +158,7 @@ def get_new_product_with_jira_project_data(self): 'jira-project-form-project_key': 'IFFFNEW', 'jira-project-form-jira_instance': 2, 'jira-project-form-enable_engagement_epic_mapping': 'on', + 'jira-project-form-epic_issue_type_name': 'Epic', 'jira-project-form-push_notes': 'on', 'jira-project-form-product_jira_sla_notification': 'on', 'jira-project-form-custom_fields': 'null', @@ -170,7 +171,9 @@ def get_new_product_without_jira_project_data(self): 'name': 'new product', 'description': 'new description', 'prod_type': 1, - 'sla_configuration': 1 + 'sla_configuration': 1, + # A value is set by default by the model, so we need to add it here as well + 'jira-project-form-epic_issue_type_name': 'Epic', # 'project_key': 'IFFF', # 'jira_instance': 2, # 'enable_engagement_epic_mapping': 'on', @@ -186,6 +189,7 @@ def get_product_with_jira_project_data(self, product): 'jira-project-form-project_key': 'IFFF', 'jira-project-form-jira_instance': 2, 'jira-project-form-enable_engagement_epic_mapping': 'on', + 'jira-project-form-epic_issue_type_name': 'Epic', 'jira-project-form-push_notes': 'on', 'jira-project-form-product_jira_sla_notification': 'on', 'jira-project-form-custom_fields': 'null', @@ -201,6 +205,7 @@ def get_product_with_jira_project_data2(self, product): 'jira-project-form-project_key': 'IFFF2', 'jira-project-form-jira_instance': 2, 'jira-project-form-enable_engagement_epic_mapping': 'on', + 'jira-project-form-epic_issue_type_name': 'Epic', 'jira-project-form-push_notes': 'on', 'jira-project-form-product_jira_sla_notification': 'on', 'jira-project-form-custom_fields': 'null', @@ -214,7 +219,8 @@ def get_product_with_empty_jira_project_data(self, product): 'description': product.description, 'prod_type': product.prod_type.id, 'sla_configuration': 1, - + # A value is set by default by the model, so we need to add it here as well + 'jira-project-form-epic_issue_type_name': 'Epic', 'jira-project-form-custom_fields': 'null', # 'project_key': 'IFFF', # 'jira_instance': 2, diff --git a/unittests/test_jira_config_engagement.py b/unittests/test_jira_config_engagement.py index f6922c19166..9506a264f60 100644 --- a/unittests/test_jira_config_engagement.py +++ b/unittests/test_jira_config_engagement.py @@ -24,6 +24,7 @@ def get_new_engagement_with_jira_project_data(self): # 'jira-project-form-inherit_from_product': 'on', # absence = False in html forms 'jira-project-form-jira_instance': 2, 'jira-project-form-project_key': 'IUNSEC', + 'jira-project-form-epic_issue_type_name': 'Epic', 'jira-project-form-product_jira_sla_notification': 'on', 'jira-project-form-custom_fields': 'null', } @@ -40,6 +41,7 @@ def get_new_engagement_with_jira_project_data_and_epic_mapping(self): # 'jira-project-form-inherit_from_product': 'on', # absence = False in html forms 'jira-project-form-jira_instance': 2, 'jira-project-form-project_key': 'IUNSEC', + 'jira-project-form-epic_issue_type_name': 'Epic', 'jira-project-form-product_jira_sla_notification': 'on', 'jira-project-form-enable_engagement_epic_mapping': 'on', 'jira-epic-form-push_to_jira': 'on', @@ -56,6 +58,8 @@ def get_new_engagement_without_jira_project_data(self): 'target_end': '2070-12-04', 'status': 'Not Started', 'jira-project-form-inherit_from_product': 'on', + # A value is set by default by the model, so we need to add it here as well + 'jira-project-form-epic_issue_type_name': 'Epic', # 'project_key': 'IFFF', # 'jira_instance': 2, # 'enable_engagement_epic_mapping': 'on', @@ -75,6 +79,7 @@ def get_engagement_with_jira_project_data(self, engagement): # 'jira-project-form-inherit_from_product': 'on', # absence = False in html forms 'jira-project-form-jira_instance': 2, 'jira-project-form-project_key': 'ISEC', + 'jira-project-form-epic_issue_type_name': 'Epic', 'jira-project-form-product_jira_sla_notification': 'on', 'jira-project-form-custom_fields': 'null', } @@ -91,6 +96,7 @@ def get_engagement_with_jira_project_data2(self, engagement): # 'jira-project-form-inherit_from_product': 'on', # absence = False in html forms 'jira-project-form-jira_instance': 2, 'jira-project-form-project_key': 'ISEC2', + 'jira-project-form-epic_issue_type_name': 'Epic', 'jira-project-form-product_jira_sla_notification': 'on', 'jira-project-form-custom_fields': 'null', } @@ -105,6 +111,8 @@ def get_engagement_with_empty_jira_project_data(self, engagement): 'target_end': '2070-12-04', 'status': 'Not Started', 'jira-project-form-inherit_from_product': 'on', + # A value is set by default by the model, so we need to add it here as well + 'jira-project-form-epic_issue_type_name': 'Epic', # 'project_key': 'IFFF', # 'jira_instance': 2, # 'enable_engagement_epic_mapping': 'on', diff --git a/unittests/test_jira_config_engagement_epic.py b/unittests/test_jira_config_engagement_epic.py index 457f392f273..51989a3829e 100644 --- a/unittests/test_jira_config_engagement_epic.py +++ b/unittests/test_jira_config_engagement_epic.py @@ -55,6 +55,7 @@ def get_new_engagement_with_jira_project_data_and_epic_mapping(self): 'status': 'Not Started', 'jira-project-form-jira_instance': 2, 'jira-project-form-project_key': 'NTEST', + 'jira-project-form-epic_issue_type_name': 'Epic', 'jira-project-form-product_jira_sla_notification': 'on', 'jira-project-form-enable_engagement_epic_mapping': 'on', 'jira-epic-form-push_to_jira': 'on', diff --git a/unittests/tools/test_checkmarx_parser.py b/unittests/tools/test_checkmarx_parser.py index c43e24fb572..f09e7d7da13 100644 --- a/unittests/tools/test_checkmarx_parser.py +++ b/unittests/tools/test_checkmarx_parser.py @@ -203,8 +203,8 @@ def check_parse_file_with_single_vulnerability_has_single_finding(self, findings item.file_path, ) # ScanStart - self.assertEqual(datetime.datetime, type(item.date)) - self.assertEqual(datetime.datetime(2018, 2, 25, 11, 35, 52), item.date) + self.assertEqual(datetime.date, type(item.date)) + self.assertEqual(datetime.date(2018, 2, 25), item.date) self.assertEqual(bool, type(item.static_finding)) self.assertEqual(True, item.static_finding) @@ -293,7 +293,7 @@ def test_file_name_aggregated_parse_file_with_multiple_vulnerabilities_has_multi finding = findings[0] self.assertEqual("SQL Injection (Assignment5.java)", finding.title) self.assertEqual("High", finding.severity) - self.assertEqual(datetime.datetime(2018, 2, 25, 11, 35, 52), finding.date) + self.assertEqual(datetime.date(2018, 2, 25), finding.date) self.assertEqual(True, finding.static_finding) self.assertEqual("WebGoat/webgoat-lessons/challenge/src/main/java/org/owasp/webgoat/plugin/challenge5/challenge6/Assignment5.java", finding.file_path) @@ -312,7 +312,7 @@ def test_detailed_parse_file_with_multiple_vulnerabilities_has_multiple_findings finding = findings[0] self.assertEqual("SQL Injection (Assignment5.java)", finding.title) self.assertEqual("High", finding.severity) - self.assertEqual(datetime.datetime(2018, 2, 25, 11, 35, 52), finding.date) + self.assertEqual(datetime.date(2018, 2, 25), finding.date) self.assertEqual(True, finding.static_finding) self.assertEqual("WebGoat/webgoat-lessons/challenge/src/main/java/org/owasp/webgoat/plugin/challenge5/challenge6/Assignment5.java", finding.file_path) self.assertEqual(50, finding.line) @@ -516,8 +516,8 @@ def check_parse_file_with_utf8_replacement_char(self, findings): item.file_path, ) # ScanStart - self.assertEqual(datetime.datetime, type(item.date)) - self.assertEqual(datetime.datetime(2018, 2, 25, 11, 35, 52), item.date) + self.assertEqual(datetime.date, type(item.date)) + self.assertEqual(datetime.date(2018, 2, 25), item.date) self.assertEqual(bool, type(item.static_finding)) self.assertEqual(True, item.static_finding) @@ -665,8 +665,8 @@ def check_parse_file_with_utf8_various_non_ascii_char(self, findings): item.file_path, ) # ScanStart - self.assertEqual(datetime.datetime, type(item.date)) - self.assertEqual(datetime.datetime(2018, 2, 25, 11, 35, 52), item.date) + self.assertEqual(datetime.date, type(item.date)) + self.assertEqual(datetime.date(2018, 2, 25), item.date) self.assertEqual(bool, type(item.static_finding)) self.assertEqual(True, item.static_finding) @@ -685,8 +685,8 @@ def test_file_with_multiple_findings_is_aggregated_with_query_id(self, mock): # ScanStart self.assertEqual("Client Potential ReDoS In Match (prettify.js)", finding.title) self.assertEqual("Low", finding.severity) - self.assertEqual(datetime.datetime, type(finding.date)) - self.assertEqual(datetime.datetime(2021, 11, 17, 13, 50, 45), finding.date) + self.assertEqual(datetime.date, type(finding.date)) + self.assertEqual(datetime.date(2021, 11, 17), finding.date) self.assertEqual(bool, type(finding.static_finding)) self.assertEqual(True, finding.static_finding) @@ -705,8 +705,8 @@ def test_file_with_empty_filename(self, mock): # ScanStart self.assertEqual("Missing HSTS Header", finding.title) self.assertEqual("Medium", finding.severity) - self.assertEqual(datetime.datetime, type(finding.date)) - self.assertEqual(datetime.datetime(2021, 12, 24, 9, 12, 14), finding.date) + self.assertEqual(datetime.date, type(finding.date)) + self.assertEqual(datetime.date(2021, 12, 24), finding.date) self.assertEqual(bool, type(finding.static_finding)) self.assertEqual(True, finding.static_finding) @@ -791,7 +791,7 @@ def test_file_issue6956(self, mock): self.assertEqual(89, finding.cwe) self.assertEqual("/webgoat-lessons/challenge/src/main/java/org/owasp/webgoat/challenges/challenge5/Assignment5.java", finding.file_path) self.assertEqual(61, finding.line) - self.assertEqual(datetime.date(2022, 5, 6), finding.date.date()) + self.assertEqual(datetime.date(2022, 5, 6), finding.date) if finding.unique_id_from_tool == "SYlu22e7ZQydKJFOlC/o1EsyixQ=": with self.subTest(i="SYlu22e7ZQydKJFOlC/o1EsyixQ="): self.assertEqual("SQL Injection", finding.title) @@ -799,7 +799,7 @@ def test_file_issue6956(self, mock): self.assertEqual(89, finding.cwe) self.assertEqual("/webgoat-lessons/sql-injection/src/main/java/org/owasp/webgoat/sql_injection/introduction/SqlInjectionLesson5.java", finding.file_path) self.assertEqual(72, finding.line) - self.assertEqual(datetime.date(2022, 5, 6), finding.date.date()) + self.assertEqual(datetime.date(2022, 5, 6), finding.date) # test one in SCA part if finding.unique_id_from_tool == "GkVx1zoIKcd1EF72zqWrGzeVTmo=": with self.subTest(i="GkVx1zoIKcd1EF72zqWrGzeVTmo="): @@ -812,7 +812,7 @@ def test_file_issue6956(self, mock): self.assertTrue(finding.active) self.assertFalse(finding.verified) self.assertIsNone(finding.line) - self.assertEqual(datetime.date(2022, 5, 6), finding.date.date()) + self.assertEqual(datetime.date(2022, 5, 6), finding.date) # test one in KICS part if finding.unique_id_from_tool == "eZrh18HAPbe2LbDAprSPrwncAC0=": with self.subTest(i="eZrh18HAPbe2LbDAprSPrwncAC0="): @@ -822,4 +822,26 @@ def test_file_issue6956(self, mock): self.assertTrue(finding.active) self.assertFalse(finding.verified) self.assertEqual("/webgoat-server/Dockerfile", finding.file_path) - self.assertEqual(datetime.date(2022, 5, 6), finding.date.date()) + self.assertEqual(datetime.date(2022, 5, 6), finding.date) + + @patch('dojo.tools.checkmarx.parser.add_language') + def test_finding_date_should_be_date_xml(self, mock): + my_file_handle, product, engagement, test = self.init( + get_unit_tests_path() + "/scans/checkmarx/single_finding.xml" + ) + parser = CheckmarxParser() + parser.set_mode('detailed') + findings = parser.get_findings(my_file_handle, test) + self.teardown(my_file_handle) + self.assertEqual(findings[0].date, datetime.date(2018, 2, 25)) + + @patch('dojo.tools.checkmarx.parser.add_language') + def test_finding_date_should_be_date_json(self, mock): + my_file_handle, product, engagement, test = self.init( + get_unit_tests_path() + "/scans/checkmarx/multiple_findings.json" + ) + parser = CheckmarxParser() + parser.set_mode('detailed') + findings = parser.get_findings(my_file_handle, test) + self.teardown(my_file_handle) + self.assertEqual(findings[0].date, datetime.date(2022, 2, 25))