From 496d4ffda531df4ced1a5100d3080c24941ac17a Mon Sep 17 00:00:00 2001 From: DefectDojo release bot Date: Tue, 2 Sep 2025 15:00:25 +0000 Subject: [PATCH 001/154] Update versions in application files --- components/package.json | 2 +- docs/content/en/open_source/upgrading/2.51.md | 7 +++++++ dojo/__init__.py | 2 +- helm/defectdojo/Chart.yaml | 4 ++-- 4 files changed, 11 insertions(+), 4 deletions(-) create mode 100644 docs/content/en/open_source/upgrading/2.51.md diff --git a/components/package.json b/components/package.json index aaf553c9cdf..d96e5e51133 100644 --- a/components/package.json +++ b/components/package.json @@ -1,6 +1,6 @@ { "name": "defectdojo", - "version": "2.50.0", + "version": "2.51.0-dev", "license" : "BSD-3-Clause", "private": true, "dependencies": { diff --git a/docs/content/en/open_source/upgrading/2.51.md b/docs/content/en/open_source/upgrading/2.51.md new file mode 100644 index 00000000000..587eba9613b --- /dev/null +++ b/docs/content/en/open_source/upgrading/2.51.md @@ -0,0 +1,7 @@ +--- +title: 'Upgrading to DefectDojo Version 2.51.x' +toc_hide: true +weight: -20250902 +description: No special instructions. +--- +There are no special instructions for upgrading to 2.51.x. Check the [Release Notes](https://github.com/DefectDojo/django-DefectDojo/releases/tag/2.51.0) for the contents of the release. diff --git a/dojo/__init__.py b/dojo/__init__.py index d5f0f80913c..09ee079e553 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.50.0" +__version__ = "2.51.0-dev" __url__ = "https://github.com/DefectDojo/django-DefectDojo" __docs__ = "https://documentation.defectdojo.com" diff --git a/helm/defectdojo/Chart.yaml b/helm/defectdojo/Chart.yaml index 868ec8461a2..f2617ac4715 100644 --- a/helm/defectdojo/Chart.yaml +++ b/helm/defectdojo/Chart.yaml @@ -1,8 +1,8 @@ apiVersion: v2 -appVersion: "2.50.0" +appVersion: "2.51.0-dev" description: A Helm chart for Kubernetes to install DefectDojo name: defectdojo -version: 1.6.205 +version: 1.6.206-dev icon: https://www.defectdojo.org/img/favicon.ico maintainers: - name: madchap From 37caf242ae20457edba1e7ac39aa73c74543ae65 Mon Sep 17 00:00:00 2001 From: kiblik <5609770+kiblik@users.noreply.github.com> Date: Tue, 2 Sep 2025 18:43:01 +0200 Subject: [PATCH 002/154] fix(docker): Docker Debian upgrade to Trixie (#13054) --- Dockerfile.django-debian | 2 +- Dockerfile.integration-tests-debian | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Dockerfile.django-debian b/Dockerfile.django-debian index da5b4b3a22a..cbffc609f67 100644 --- a/Dockerfile.django-debian +++ b/Dockerfile.django-debian @@ -5,7 +5,7 @@ # Dockerfile.nginx to use the caching mechanism of Docker. # Ref: https://devguide.python.org/#branchstatus -FROM python:3.11.11-slim-bookworm@sha256:42420f737ba91d509fc60d5ed65ed0492678a90c561e1fa08786ae8ba8b52eda AS base +FROM python:3.11.13-slim-trixie@sha256:1d6131b5d479888b43200645e03a78443c7157efbdb730e6b48129740727c312 AS base FROM base AS build WORKDIR /app RUN \ diff --git a/Dockerfile.integration-tests-debian b/Dockerfile.integration-tests-debian index ed843d95416..4db20eb53e4 100644 --- a/Dockerfile.integration-tests-debian +++ b/Dockerfile.integration-tests-debian @@ -3,7 +3,7 @@ FROM openapitools/openapi-generator-cli:v7.15.0@sha256:509f01c3c7eee9d1ad286506a7b6aa4624a95b410be9a238a306d209e900621f AS openapitools # currently only supports x64, no arm yet due to chrome and selenium dependencies -FROM python:3.11.11-slim-bookworm@sha256:42420f737ba91d509fc60d5ed65ed0492678a90c561e1fa08786ae8ba8b52eda AS build +FROM python:3.11.13-slim-trixie@sha256:1d6131b5d479888b43200645e03a78443c7157efbdb730e6b48129740727c312 AS build WORKDIR /app RUN \ apt-get -y update && \ @@ -47,11 +47,11 @@ RUN \ apt-get -y install $missing_chrome_deps # Install a suggested list of additional packages (https://stackoverflow.com/a/76734752) -RUN apt-get install -y libxi6 libgconf-2-4 jq libjq1 libonig5 libxkbcommon0 libxss1 libglib2.0-0 libnss3 \ - libfontconfig1 libatk-bridge2.0-0 libatspi2.0-0 libgtk-3-0 libpango-1.0-0 libgdk-pixbuf2.0-0 libxcomposite1 \ +RUN apt-get install -y libxi6 jq libjq1 libonig5 libxkbcommon0 libxss1 libglib2.0-0 libnss3 \ + libfontconfig1 libatk-bridge2.0-0 libatspi2.0-0 libgtk-3-0 libpango-1.0-0 libxcomposite1 \ libxcursor1 libxdamage1 libxtst6 libappindicator3-1 libasound2 libatk1.0-0 libc6 libcairo2 libcups2 libxfixes3 \ libdbus-1-3 libexpat1 libgcc1 libnspr4 libgbm1 libpangocairo-1.0-0 libstdc++6 libx11-6 libx11-xcb1 libxcb1 libxext6 \ - libxrandr2 libxrender1 gconf-service ca-certificates fonts-liberation libappindicator1 lsb-release xdg-utils + libxrandr2 libxrender1 ca-certificates fonts-liberation lsb-release xdg-utils # Installing the latest stable Google Chrome driver release WORKDIR /opt/chrome-driver From 218a29ade87eb47d15ab0dee3fc0eb14f3963f90 Mon Sep 17 00:00:00 2001 From: manuelsommer <47991713+manuel-sommer@users.noreply.github.com> Date: Tue, 2 Sep 2025 18:43:19 +0200 Subject: [PATCH 003/154] Ruff: Preparation for G004 (#13076) --- dojo/api_v2/serializers.py | 6 ++--- dojo/api_v2/views.py | 2 +- dojo/finding/views.py | 4 ++-- dojo/importers/endpoint_manager.py | 2 +- dojo/jira_link/helper.py | 2 +- .../commands/import_all_unittest_scans.py | 10 ++++----- .../commands/import_github_languages.py | 4 ++-- dojo/models.py | 6 ++--- dojo/notifications/helper.py | 8 +++---- dojo/pipeline.py | 2 +- dojo/product/helpers.py | 8 +++---- dojo/search/views.py | 6 ++--- dojo/settings/settings.dist.py | 4 ++-- dojo/sla_config/helpers.py | 4 ++-- dojo/tools/anchore_grype/parser.py | 12 +++++----- dojo/tools/api_bugcrowd/importer.py | 2 +- dojo/tools/api_bugcrowd/parser.py | 2 +- dojo/tools/api_sonarqube/updater.py | 6 ++--- .../api_sonarqube/updater_from_source.py | 4 ++-- .../parser.py | 2 +- dojo/tools/burp/parser.py | 2 +- dojo/tools/checkmarx_cxflow_sast/parser.py | 4 ++-- dojo/tools/cyberwatch_galeax/parser.py | 22 +++++++++---------- dojo/tools/cyclonedx/helpers.py | 4 ++-- dojo/tools/cyclonedx/json_parser.py | 2 +- dojo/tools/cyclonedx/xml_parser.py | 4 ++-- dojo/tools/dependency_check/parser.py | 4 ++-- dojo/tools/factory.py | 4 ++-- dojo/tools/fortify/fpr_parser.py | 12 +++++----- dojo/tools/qualys/csv_parser.py | 8 +++---- dojo/tools/twistlock/parser.py | 2 +- dojo/tools/wiz/parser.py | 4 ++-- dojo/user/views.py | 2 +- dojo/utils.py | 10 ++++----- dojo/validators.py | 2 +- tests/base_test_class.py | 2 +- tests/notes_test.py | 2 +- unittests/test_factory.py | 2 +- 38 files changed, 94 insertions(+), 94 deletions(-) diff --git a/dojo/api_v2/serializers.py b/dojo/api_v2/serializers.py index 77e937da85b..fefde7e24ef 100644 --- a/dojo/api_v2/serializers.py +++ b/dojo/api_v2/serializers.py @@ -219,7 +219,7 @@ def to_internal_value(self, data): except ValueError: self.fail("invalid_json") - logger.debug(f"data as json: {data}") + logger.debug("data as json: %s", data) if not isinstance(data, list): self.fail("not_a_list", input_type=type(data).__name__) @@ -238,7 +238,7 @@ def to_internal_value(self, data): tag_validator(sub, exception_class=RestFrameworkValidationError) data_safe.extend(substrings) - logger.debug(f"result after rendering tags: {data_safe}") + logger.debug("result after rendering tags: %s", data_safe) return data_safe def to_representation(self, value): @@ -1863,7 +1863,7 @@ class Meta: # Overriding this to push add Push to JIRA functionality def create(self, validated_data): - logger.debug(f"Creating finding with validated data: {validated_data}") + logger.debug("Creating finding with validated data: %s", validated_data) push_to_jira = validated_data.pop("push_to_jira", False) notes = validated_data.pop("notes", None) found_by = validated_data.pop("found_by", None) diff --git a/dojo/api_v2/views.py b/dojo/api_v2/views.py index f8cda3e8413..63bbd480f1f 100644 --- a/dojo/api_v2/views.py +++ b/dojo/api_v2/views.py @@ -2648,7 +2648,7 @@ def perform_create(self, serializer): jira_driver = test or (engagement or (product or None)) if jira_project := (jira_helper.get_jira_project(jira_driver) if jira_driver else None): push_to_jira = push_to_jira or jira_project.push_all_issues - logger.debug(f"push_to_jira: {push_to_jira}") + logger.debug("push_to_jira: %s", push_to_jira) serializer.save(push_to_jira=push_to_jira) diff --git a/dojo/finding/views.py b/dojo/finding/views.py index fad9c2f43f8..fc7fe43284f 100644 --- a/dojo/finding/views.py +++ b/dojo/finding/views.py @@ -486,7 +486,7 @@ def get_request_response(self, finding: Finding): burp_request = base64.b64decode(request_response.burpRequestBase64) burp_response = base64.b64decode(request_response.burpResponseBase64) except Exception as e: - logger.debug(f"unsuspected error: {e}") + logger.debug("unsuspected error: %s", e) return { "burp_request": burp_request, @@ -1597,7 +1597,7 @@ def request_finding_review(request, fid): reviewers = Dojo_User.objects.filter(id__in=form.cleaned_data["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") + logger.debug("Asking %s for review", reviewers_string) create_notification( event="review_requested", # TODO: - if 'review_requested' functionality will be supported by API as well, 'create_notification' needs to be migrated to place where it will be able to cover actions from both interfaces diff --git a/dojo/importers/endpoint_manager.py b/dojo/importers/endpoint_manager.py index 7f408c909dc..db8c637ad83 100644 --- a/dojo/importers/endpoint_manager.py +++ b/dojo/importers/endpoint_manager.py @@ -111,7 +111,7 @@ def clean_unsaved_endpoints( try: endpoint.clean() except ValidationError as e: - logger.warning(f"DefectDojo is storing broken endpoint because cleaning wasn't successful: {e}") + logger.warning("DefectDojo is storing broken endpoint because cleaning wasn't successful: %s", e) def chunk_endpoints_and_reactivate( self, diff --git a/dojo/jira_link/helper.py b/dojo/jira_link/helper.py index 00385e733e4..77977469240 100644 --- a/dojo/jira_link/helper.py +++ b/dojo/jira_link/helper.py @@ -204,7 +204,7 @@ def can_be_pushed_to_jira(obj, form=None): return False, f"Finding below the minimum JIRA severity threshold ({System_Settings.objects.get().jira_minimum_severity}).", "error_below_minimum_threshold" elif isinstance(obj, Finding_Group): finding_group_status = _safely_get_obj_status_for_jira(obj) - logger.error(f"Finding group status: {finding_group_status}") + logger.error("Finding group status: %s", finding_group_status) if "Empty" in finding_group_status: return False, f"{to_str_typed(obj)} cannot be pushed to jira as it contains no findings above minimum treshold.", "error_empty" diff --git a/dojo/management/commands/import_all_unittest_scans.py b/dojo/management/commands/import_all_unittest_scans.py index 04a4c84f156..cc4dd266250 100644 --- a/dojo/management/commands/import_all_unittest_scans.py +++ b/dojo/management/commands/import_all_unittest_scans.py @@ -120,7 +120,7 @@ def import_scan_with_params(self, filename, scan_type="ZAP Scan", engagement=1, return self.import_scan(payload, expected_http_status_code) def import_all_unittest_scans(self, product_name_prefix=None, tests_per_engagement=10, engagements_per_product=50, products_per_product_type=15, *, include_very_big_scans=False, **kwargs): - logger.info(f"product_name_prefix: {product_name_prefix}, tests_per_engagement: {tests_per_engagement}, engagements_per_product: {engagements_per_product}, products_per_product_type: {products_per_product_type}") + logger.info("product_name_prefix: %s, tests_per_engagement: %s, engagements_per_product: %s, products_per_product_type: %s", product_name_prefix, tests_per_engagement, engagements_per_product, products_per_product_type) product_type_prefix = "Sample scans " + datetime.now().strftime("%Y-%m-%d %H:%M:%S") product_type_index = 1 @@ -159,7 +159,7 @@ def import_all_unittest_scans(self, product_name_prefix=None, tests_per_engageme for attribute_name in dir(module): attribute = getattr(module, attribute_name) if isclass(attribute) and attribute_name.lower() == module_name.replace("_", "") + "parser": - logger.debug(f"Loading {module_name} parser") + logger.debug("Loading %s parser", module_name) scan_dir = Path("unittests") / "scans" / module_name for scan_file in scan_dir.glob("*.json"): if include_very_big_scans or scan_file.name != "very_many_vulns.json": # jfrog_xray file is huge and takes too long to import @@ -183,12 +183,12 @@ def import_all_unittest_scans(self, product_name_prefix=None, tests_per_engageme error_messages[module_name + "/" + scan_file.name] = result.get("message", str(e)) except: - logger.exception(f"failed to load {module_name}") + logger.exception("failed to load %s", module_name) raise - logger.error(f"Error count: {error_count}") + logger.error("Error count: %s", error_count) for scan, message in error_messages.items(): - logger.error(f"Error importing scan {scan}: {message}") + logger.error("Error importing scan %s: %s", scan, message) def handle(self, *args, **options): logger.info("EXPERIMENTAL: This command may be changed/deprecated/removed without prior notice.") diff --git a/dojo/management/commands/import_github_languages.py b/dojo/management/commands/import_github_languages.py index 9e1c45ffb32..89ddf80f7b8 100644 --- a/dojo/management/commands/import_github_languages.py +++ b/dojo/management/commands/import_github_languages.py @@ -43,7 +43,7 @@ def handle(self, *args, **options): try: language_type, created = Language_Type.objects.get_or_create(language=name) except Language_Type.MultipleObjectsReturned: - logger.warning(f"Language_Type {name} exists multiple times") + logger.warning("Language_Type %s exists multiple times", name) continue if created: @@ -52,4 +52,4 @@ def handle(self, *args, **options): language_type.color = element.get("color", 0) language_type.save() - logger.info(f"Finished importing languages from GitHub, added {new_language_types} Language_Types") + logger.info("Finished importing languages from GitHub, added %s Language_Types", new_language_types) diff --git a/dojo/models.py b/dojo/models.py index 9d3a238d9ca..792ddd19639 100644 --- a/dojo/models.py +++ b/dojo/models.py @@ -1866,7 +1866,7 @@ def clean(self): action_string = "Postgres does not accept NULL character. Attempting to replace with %00..." for remove_str in null_char_list: self.path = self.path.replace(remove_str, "%00") - logger.error(f'Path "{old_value}" has invalid format - It contains the NULL character. The following action was taken: {action_string}') + logger.error('Path "%s" has invalid format - It contains the NULL character. The following action was taken: %s', old_value, action_string) if self.path == "": self.path = None @@ -1879,7 +1879,7 @@ def clean(self): action_string = "Postgres does not accept NULL character. Attempting to replace with %00..." for remove_str in null_char_list: self.query = self.query.replace(remove_str, "%00") - logger.error(f'Query "{old_value}" has invalid format - It contains the NULL character. The following action was taken: {action_string}') + logger.error('Query "%s" has invalid format - It contains the NULL character. The following action was taken: %s', old_value, action_string) if self.query == "": self.query = None @@ -1892,7 +1892,7 @@ def clean(self): action_string = "Postgres does not accept NULL character. Attempting to replace with %00..." for remove_str in null_char_list: self.fragment = self.fragment.replace(remove_str, "%00") - logger.error(f'Fragment "{old_value}" has invalid format - It contains the NULL character. The following action was taken: {action_string}') + logger.error('Fragment "%s" has invalid format - It contains the NULL character. The following action was taken: %s', old_value, action_string) if self.fragment == "": self.fragment = None diff --git a/dojo/notifications/helper.py b/dojo/notifications/helper.py index 9c16d4b2cee..99edd43c593 100644 --- a/dojo/notifications/helper.py +++ b/dojo/notifications/helper.py @@ -234,7 +234,7 @@ def send_slack_notification( elif self.system_settings.slack_channel is not None: channel = self.system_settings.slack_channel logger.info( - f"Sending system notification to system channel {channel}.", + "Sending system notification to system channel %s.", channel, ) self._post_slack_message(event, user, channel, **kwargs) else: @@ -272,11 +272,11 @@ def _get_slack_user_id(self, user_email: str) -> str: if user_email == user["user"]["profile"]["email"]: if "id" in user["user"]: user_id = user["user"]["id"] - logger.debug(f"Slack user ID is {user_id}") + logger.debug("Slack user ID is %s", user_id) slack_user_is_found = True else: logger.warning( - f"A user with email {user_email} could not be found in this Slack workspace.", + "A user with email %s could not be found in this Slack workspace.", user_email, ) if not slack_user_is_found: @@ -496,7 +496,7 @@ def _get_webhook_endpoints( if not endpoints.exists(): if user: logger.info( - f"URLs for Webhooks not configured for user '{user}': skipping user notification", + "URLs for Webhooks not configured for user '%s': skipping user notification", user, ) else: logger.info( diff --git a/dojo/pipeline.py b/dojo/pipeline.py index cde01e0b0d9..e8bdd2512bf 100644 --- a/dojo/pipeline.py +++ b/dojo/pipeline.py @@ -99,7 +99,7 @@ def update_azure_groups(backend, uid, user=None, social=None, *args, **kwargs): logger.debug("Skipping group " + group_name + " due to AZUREAD_TENANT_OAUTH2_GROUPS_FILTER " + settings.AZUREAD_TENANT_OAUTH2_GROUPS_FILTER) continue except Exception as e: - logger.error(f"Could not call microsoft graph API or save groups to member: {e}") + logger.error("Could not call microsoft graph API or save groups to member: %s", e) if len(group_names) > 0: assign_user_to_groups(user, group_names, Dojo_Group.AZURE) if settings.AZUREAD_TENANT_OAUTH2_CLEANUP_GROUPS: diff --git a/dojo/product/helpers.py b/dojo/product/helpers.py index ff7ada2dbd9..aeadec0246d 100644 --- a/dojo/product/helpers.py +++ b/dojo/product/helpers.py @@ -18,16 +18,16 @@ def propagate_tags_on_product(product_id, *args, **kwargs): def propagate_tags_on_product_sync(product): # enagagements - logger.debug(f"Propagating tags from {product} to all engagements") + logger.debug("Propagating tags from %s to all engagements", product) propagate_tags_on_object_list(Engagement.objects.filter(product=product)) # tests - logger.debug(f"Propagating tags from {product} to all tests") + logger.debug("Propagating tags from %s to all tests", product) propagate_tags_on_object_list(Test.objects.filter(engagement__product=product)) # findings - logger.debug(f"Propagating tags from {product} to all findings") + logger.debug("Propagating tags from %s to all findings", product) propagate_tags_on_object_list(Finding.objects.filter(test__engagement__product=product)) # endpoints - logger.debug(f"Propagating tags from {product} to all endpoints") + logger.debug("Propagating tags from %s to all endpoints", product) propagate_tags_on_object_list(Endpoint.objects.filter(product=product)) diff --git a/dojo/search/views.py b/dojo/search/views.py index d075331bbc5..fbddc7aaa79 100644 --- a/dojo/search/views.py +++ b/dojo/search/views.py @@ -422,9 +422,9 @@ def parse_search_query(clean_query): else: keywords.append(vulnerability_id_fix(query_part)) - logger.debug(f"query: {clean_query}") - logger.debug(f"operators: {operators}") - logger.debug(f"keywords: {keywords}") + logger.debug("query: %s", clean_query) + logger.debug("operators: %s", operators) + logger.debug("keywords: %s", keywords) return operators, keywords diff --git a/dojo/settings/settings.dist.py b/dojo/settings/settings.dist.py index 8d6ebee3a35..ceec19723e6 100644 --- a/dojo/settings/settings.dist.py +++ b/dojo/settings/settings.dist.py @@ -1367,7 +1367,7 @@ def saml2_attrib_map_format(din): logger.info(f"Replacing {key} with value {value} (previously set to {HASHCODE_FIELDS_PER_SCANNER[key]}) from env var DD_HASHCODE_FIELDS_PER_SCANNER") HASHCODE_FIELDS_PER_SCANNER[key] = value if key not in HASHCODE_FIELDS_PER_SCANNER: - logger.info(f"Adding {key} with value {value} from env var DD_HASHCODE_FIELDS_PER_SCANNER") + logger.info("Adding %s with value %s from env var DD_HASHCODE_FIELDS_PER_SCANNER", key, value) HASHCODE_FIELDS_PER_SCANNER[key] = value @@ -1621,7 +1621,7 @@ def saml2_attrib_map_format(din): logger.info(f"Replacing {key} with value {value} (previously set to {DEDUPLICATION_ALGORITHM_PER_PARSER[key]}) from env var DD_DEDUPLICATION_ALGORITHM_PER_PARSER") DEDUPLICATION_ALGORITHM_PER_PARSER[key] = value if key not in DEDUPLICATION_ALGORITHM_PER_PARSER: - logger.info(f"Adding {key} with value {value} from env var DD_DEDUPLICATION_ALGORITHM_PER_PARSER") + logger.info("Adding %s with value %s from env var DD_DEDUPLICATION_ALGORITHM_PER_PARSER", key, value) DEDUPLICATION_ALGORITHM_PER_PARSER[key] = value DUPE_DELETE_MAX_PER_RUN = env("DD_DUPE_DELETE_MAX_PER_RUN") diff --git a/dojo/sla_config/helpers.py b/dojo/sla_config/helpers.py index a7422869536..57633d0c2ec 100644 --- a/dojo/sla_config/helpers.py +++ b/dojo/sla_config/helpers.py @@ -21,7 +21,7 @@ def update_sla_expiration_dates_product_async(product, sla_config, *args, **kwar def update_sla_expiration_dates_sla_config_sync(sla_config, products, severities=None): - logger.info(f"Updating finding SLA expiration dates within the {sla_config} SLA configuration") + logger.info("Updating finding SLA expiration dates within the %s SLA configuration", sla_config) # update each finding that is within the SLA configuration that was saved findings = Finding.objects.filter(test__engagement__product__sla_configuration_id=sla_config.id) if products: @@ -49,4 +49,4 @@ def update_sla_expiration_dates_sla_config_sync(sla_config, products, severities # reset the async updating flag to false for this sla config sla_config.async_updating = False super(SLA_Configuration, sla_config).save() - logger.info(f"DONE Updating finding SLA expiration dates within the {sla_config} SLA configuration") + logger.info("DONE Updating finding SLA expiration dates within the %s SLA configuration", sla_config) diff --git a/dojo/tools/anchore_grype/parser.py b/dojo/tools/anchore_grype/parser.py index 9dcd971b1fc..2cf89b87f44 100644 --- a/dojo/tools/anchore_grype/parser.py +++ b/dojo/tools/anchore_grype/parser.py @@ -30,9 +30,9 @@ def get_description_for_scan_types(self, scan_type): ) def get_findings(self, file, test): - logger.debug(f"file: {file}") + logger.debug("file: %s", file) data = json.load(file) - logger.debug(f"data: {data}") + logger.debug("data: %s", data) dupes = {} for item in data.get("matches", []): vulnerability = item["vulnerability"] @@ -223,11 +223,11 @@ def get_cvss(self, cvss): def get_epss_values(self, vuln_id, epss_list): if not isinstance(epss_list, list): - logger.debug(f"epss_list is not a list: {epss_list}") + logger.debug("epss_list is not a list: %s", epss_list) return None, None if isinstance(epss_list, list): - logger.debug(f"epss_list: {epss_list}") + logger.debug("epss_list: %s", epss_list) for epss_data in epss_list: if epss_data.get("cve") != vuln_id: continue @@ -235,10 +235,10 @@ def get_epss_values(self, vuln_id, epss_list): epss_score = float(epss_data.get("epss")) epss_percentile = float(epss_data.get("percentile")) except (TypeError, ValueError): - logger.debug(f"epss_data is not a float: {epss_data}") + logger.debug("epss_data is not a float: %s", epss_data) else: return epss_score, epss_percentile - logger.debug(f"epss not found for vuln_id: {vuln_id} in epss_list: {epss_list}") + logger.debug("epss not found for vuln_id: %s in epss_list: %s", vuln_id, epss_list) return None, None def get_vulnerability_ids(self, vuln_id, related_vulnerabilities): diff --git a/dojo/tools/api_bugcrowd/importer.py b/dojo/tools/api_bugcrowd/importer.py index d83d1edf3cd..ac14adda4a6 100644 --- a/dojo/tools/api_bugcrowd/importer.py +++ b/dojo/tools/api_bugcrowd/importer.py @@ -29,7 +29,7 @@ def get_findings(self, test): for page in submissions_paged: submissions += page counter += 1 - logger.debug(f"{counter} Bugcrowd submissions pages fetched") + logger.debug("%s Bugcrowd submissions pages fetched", counter) return submissions, config diff --git a/dojo/tools/api_bugcrowd/parser.py b/dojo/tools/api_bugcrowd/parser.py index 9f98ac9d1b6..e61025e1736 100644 --- a/dojo/tools/api_bugcrowd/parser.py +++ b/dojo/tools/api_bugcrowd/parser.py @@ -155,7 +155,7 @@ def get_findings(self, file, test): finding.unsaved_endpoints = [bug_endpoint] except Exception as e: logger.error( - f"{bug_endpoint} bug url from bugcrowd failed to parse to endpoint, error= {e}", + "%s bug url from bugcrowd failed to parse to endpoint, error= %s", bug_endpoint, e, ) except ValidationError: logger.error( diff --git a/dojo/tools/api_sonarqube/updater.py b/dojo/tools/api_sonarqube/updater.py index c964760526f..18d547fea05 100644 --- a/dojo/tools/api_sonarqube/updater.py +++ b/dojo/tools/api_sonarqube/updater.py @@ -120,7 +120,7 @@ def update_sonarqube_finding(self, finding): return logger.debug( - f"Checking if finding '{finding}' needs to be updated in SonarQube", + "Checking if finding '%s' needs to be updated in SonarQube", finding, ) client, _ = SonarQubeApiImporter.prepare_client(finding.test) @@ -141,7 +141,7 @@ def update_sonarqube_finding(self, finding): current_status = issue.get("status") logger.debug( - f"--> SQ Current status: {current_status}. Current target status: {target_status}", + "--> SQ Current status: %s. Current target status: %s", current_status, target_status, ) transitions = self.get_sonarqube_required_transitions_for( @@ -149,7 +149,7 @@ def update_sonarqube_finding(self, finding): ) if transitions: logger.info( - f"Updating finding '{finding}' in SonarQube", + "Updating finding '%s' in SonarQube", finding, ) for transition in transitions: diff --git a/dojo/tools/api_sonarqube/updater_from_source.py b/dojo/tools/api_sonarqube/updater_from_source.py index a6a467861c3..d7028bd2ca9 100644 --- a/dojo/tools/api_sonarqube/updater_from_source.py +++ b/dojo/tools/api_sonarqube/updater_from_source.py @@ -42,14 +42,14 @@ def update(self, finding): current_status = issue.get("resolution") or issue.get("status") current_finding_status = self.get_sonarqube_status_for(finding) logger.debug( - f"--> SQ Current status: {current_status}. Finding status: {current_finding_status}", + "--> SQ Current status: %s. Finding status: %s", current_status, current_finding_status, ) if ( current_status not in {"OPEN", current_finding_status} ): logger.info( - f"Original SonarQube issue '{sonarqube_issue}' has changed. Updating DefectDojo finding '{finding}'...", + "Original SonarQube issue '%s' has changed. Updating DefectDojo finding '%s'...", sonarqube_issue, finding, ) self.update_finding_status(finding, current_status) diff --git a/dojo/tools/appcheck_web_application_scanner/parser.py b/dojo/tools/appcheck_web_application_scanner/parser.py index 485d879e5be..40ece07bbbb 100644 --- a/dojo/tools/appcheck_web_application_scanner/parser.py +++ b/dojo/tools/appcheck_web_application_scanner/parser.py @@ -61,5 +61,5 @@ def get_findings(self, file, test): finding.test = test findings[dupe_key] = finding else: - LOGGER.warning(f"Skipping entry; could not find parser for scanning engine named: {scanning_engine}") + LOGGER.warning("Skipping entry; could not find parser for scanning engine named: %s", scanning_engine) return list(findings.values()) diff --git a/dojo/tools/burp/parser.py b/dojo/tools/burp/parser.py index e45e844ec1c..d8cc9aaf969 100644 --- a/dojo/tools/burp/parser.py +++ b/dojo/tools/burp/parser.py @@ -299,7 +299,7 @@ def get_item(item_node, test): if len(cwes) > 1: # TODO: support more than one CWE logger.debug( - f"more than one CWE for a finding {cwes}. NOT supported by parser API", + "more than one CWE for a finding %s. NOT supported by parser API", cwes, ) if len(cwes) > 0: finding.cwe = cwes[0] diff --git a/dojo/tools/checkmarx_cxflow_sast/parser.py b/dojo/tools/checkmarx_cxflow_sast/parser.py index 59c62b1afc7..f35dfca36a9 100644 --- a/dojo/tools/checkmarx_cxflow_sast/parser.py +++ b/dojo/tools/checkmarx_cxflow_sast/parser.py @@ -55,7 +55,7 @@ def get_findings(self, file, test): if file.name.strip().lower().endswith(".json"): return self._get_findings_json(file, test) # TODO: support CxXML format - logger.warning(f"Not supported file format ${file}") + logger.warning("Not supported file format $%s", file) return [] def _get_findings_json(self, file, test): @@ -98,7 +98,7 @@ def _get_findings_json(self, file, test): for detail_key in issue.get("details"): if detail_key not in map_paths: - logger.warning(f"{detail_key} not found in path, ignore") + logger.warning("%s not found in path, ignore", detail_key) else: detail = map_paths[detail_key] diff --git a/dojo/tools/cyberwatch_galeax/parser.py b/dojo/tools/cyberwatch_galeax/parser.py index 02c0d6f85ea..7c8353917e2 100644 --- a/dojo/tools/cyberwatch_galeax/parser.py +++ b/dojo/tools/cyberwatch_galeax/parser.py @@ -22,11 +22,11 @@ def get_description_for_scan_types(self, scan_type): return "Import Cyberwatch Cve and Security Issue data in JSON format, you can get the json from this tool : https://github.com/Galeax/Cyberwatch-API-DefectDojo" def get_findings(self, filename, test): - logger.debug(f"Starting get_findings with filename: {filename}") + logger.debug("Starting get_findings with filename: %s", filename) try: file_content = self.read_file_content(filename) except Exception as e: - logger.error(f"Error processing file: {e}") + logger.error("Error processing file: %s", e) return [] else: data = json.loads(file_content) @@ -75,12 +75,12 @@ def collect_cve_data(self, json_data, cve_data): # Safely handle when "cwes" is null cwes = json_data.get("cwes") or {} if not isinstance(cwes, dict): - logger.error(f"Invalid cwes data: {cwes}") + logger.error("Invalid cwes data: %s", cwes) cwes = {} cwes_ids = cwes.get("cwe_id", []) if not isinstance(cwes_ids, list): - logger.error(f"Invalid cwe_id data: {cwes_ids}") + logger.error("Invalid cwe_id data: %s", cwes_ids) cwes_ids = [] if cwes_ids: try: @@ -139,7 +139,7 @@ def collect_cve_data(self, json_data, cve_data): servers = json_data.get("servers", []) if not isinstance(servers, list): - logger.error(f"servers is not a list: {servers}") + logger.error("servers is not a list: %s", servers) return server_lookup = {s.get("computer_name", ""): s for s in servers if isinstance(s, dict)} @@ -327,7 +327,7 @@ def create_finding( try: finding.epss_score = float(epss) except Exception: - logger.error(f"Error converting epss score to percentage: {epss}") + logger.error("Error converting epss score to percentage: %s", epss) finding.epss_score = epss return finding finding.epss_score = epss @@ -404,7 +404,7 @@ def process_security_issue(self, json_data, test): servers = json_data.get("servers", []) if not isinstance(servers, list): - logger.error(f"servers is not a list: {servers}") + logger.error("servers is not a list: %s", servers) return None unsaved_endpoints, unsaved_endpoint_status, active_status, mitigated_date = self.process_servers_for_security_issue(servers) @@ -452,7 +452,7 @@ def process_servers_for_security_issue(self, servers): for server in servers: if not server or not isinstance(server, dict): - logger.error(f"Invalid server data: {server}") + logger.error("Invalid server data: %s", server) continue computer_name = server.get("computer_name", "Unknown Hostname") @@ -499,7 +499,7 @@ def parse_fixed_at(self, fixed_at_str): try: return datetime.strptime(fixed_at_str, "%Y-%m-%dT%H:%M:%S.%f%z") except ValueError as e: - logger.error(f'Error parsing fixed_at date "{fixed_at_str}": {e}') + logger.error('Error parsing fixed_at date "%s": %s', fixed_at_str, e) return datetime.now() def parse_datetime(self, dt_str): @@ -508,7 +508,7 @@ def parse_datetime(self, dt_str): try: return datetime.strptime(dt_str, "%Y-%m-%dT%H:%M:%S.%f%z") except (ValueError, TypeError): - logger.error(f'Error parsing datetime "{dt_str}"') + logger.error('Error parsing datetime "%s"', dt_str) return datetime.now() def parse_cvss(self, cvss_v3_vector, json_data): @@ -519,7 +519,7 @@ def parse_cvss(self, cvss_v3_vector, json_data): cvssv3_score = vectors[0].scores()[0] severity = vectors[0].severities()[0] return cvssv3, cvssv3_score, severity - logger.error(f"Invalid CVSS v3 vector: {cvss_v3_vector}") + logger.error("Invalid CVSS v3 vector: %s", cvss_v3_vector) severity = self.convert_severity(json_data.get("cve_level", "Info")) return None, None, severity diff --git a/dojo/tools/cyclonedx/helpers.py b/dojo/tools/cyclonedx/helpers.py index f9a7b67f80c..4ca188193d6 100644 --- a/dojo/tools/cyclonedx/helpers.py +++ b/dojo/tools/cyclonedx/helpers.py @@ -15,13 +15,13 @@ def _get_cvssv3(self, raw_vector): return CVSS3(raw_vector) except BaseException: LOGGER.exception( - f"error while parsing vector CVSS v3 {raw_vector}", + "error while parsing vector CVSS v3 %s", raw_vector, ) return None def _get_component(self, components, reference): if reference not in components: - LOGGER.warning(f"reference:{reference} not found in the BOM") + LOGGER.warning("reference:%s not found in the BOM", reference) return (None, None) if "version" not in components[reference]: return (components[reference]["name"], None) diff --git a/dojo/tools/cyclonedx/json_parser.py b/dojo/tools/cyclonedx/json_parser.py index a289dae7d0c..a53b9dd799d 100644 --- a/dojo/tools/cyclonedx/json_parser.py +++ b/dojo/tools/cyclonedx/json_parser.py @@ -105,7 +105,7 @@ def _get_findings_json(self, file, test): if cwes and len(cwes) > 1: # TODO: support more than one CWE LOGGER.debug( - f"more than one CWE for a finding {cwes}. NOT supported by parser API", + "more than one CWE for a finding %s. NOT supported by parser API", cwes, ) if cwes and len(cwes) > 0: finding.cwe = cwes[0] diff --git a/dojo/tools/cyclonedx/xml_parser.py b/dojo/tools/cyclonedx/xml_parser.py index 84decac9b77..54363b49217 100644 --- a/dojo/tools/cyclonedx/xml_parser.py +++ b/dojo/tools/cyclonedx/xml_parser.py @@ -156,7 +156,7 @@ def manage_vulnerability_legacy( if len(cwes) > 1: # TODO: support more than one CWE LOGGER.debug( - f"more than one CWE for a finding {cwes}. NOT supported by parser API", + "more than one CWE for a finding %s. NOT supported by parser API", cwes, ) if len(cwes) > 0: finding.cwe = cwes[0] @@ -266,7 +266,7 @@ def _manage_vulnerability_xml( if len(cwes) > 1: # TODO: support more than one CWE LOGGER.debug( - f"more than one CWE for a finding {cwes}. NOT supported by parser API", + "more than one CWE for a finding %s. NOT supported by parser API", cwes, ) if len(cwes) > 0: finding.cwe = cwes[0] diff --git a/dojo/tools/dependency_check/parser.py b/dojo/tools/dependency_check/parser.py index 9de6b3c2f75..ec14ac8f196 100644 --- a/dojo/tools/dependency_check/parser.py +++ b/dojo/tools/dependency_check/parser.py @@ -248,7 +248,7 @@ def get_severity_and_cvss_meta(self, vulnerability, namespace) -> dict: if severity: if severity.strip().lower() not in self.SEVERITY_MAPPING: logger.warning( - f"Warning: Unknow severity value detected '{severity}'. Bypass to 'Medium' value", + "Warning: Unknow severity value detected '%s'. Bypass to 'Medium' value", severity, ) severity = "Medium" else: @@ -329,7 +329,7 @@ def get_finding_from_vulnerability( if component_name is None: logger.warning( - f"component_name was None for File: {dependency_filename}, using dependency file name instead.", + "component_name was None for File: %s, using dependency file name instead.", dependency_filename, ) component_name = dependency_filename diff --git a/dojo/tools/factory.py b/dojo/tools/factory.py index b6f70033196..bbef84df731 100644 --- a/dojo/tools/factory.py +++ b/dojo/tools/factory.py @@ -24,7 +24,7 @@ def register(parser_type): def register_parser(scan_type, parser): - logger.debug(f"register scan_type:{scan_type} with parser:{parser}") + logger.debug("register scan_type:%s with parser:%s", scan_type, parser) # check double registration or registration with an existing key if scan_type in PARSERS: msg = f"Try to register an existing parser '{scan_type}'" @@ -122,4 +122,4 @@ def requires_tool_type(scan_type): if isclass(attribute) and attribute_name.lower() == module_name.replace("_", "") + "parser": register(attribute) except: - logger.exception(f"failed to load {module_name}") + logger.exception("failed to load %s", module_name) diff --git a/dojo/tools/fortify/fpr_parser.py b/dojo/tools/fortify/fpr_parser.py index f348aa265d6..dbb8365e626 100644 --- a/dojo/tools/fortify/fpr_parser.py +++ b/dojo/tools/fortify/fpr_parser.py @@ -68,19 +68,19 @@ def parse_related_data(self, root: Element, test: Test) -> None: related_data = FortifyRelatedData() for description in root.findall("Description", self.namespaces): class_id = description.attrib.get("classID") - logger.debug(f"Description: {class_id}") + logger.debug("Description: %s", class_id) if class_id: related_data.descriptions[class_id] = self.parse_description_information(description) for snippet in root.find("Snippets", self.namespaces): snippet_id = snippet.attrib.get("id") - logger.debug(f"Snippet: {snippet_id}") + logger.debug("Snippet: %s", snippet_id) if snippet_id: related_data.snippets[snippet_id] = self.parse_snippet_information(snippet) for rule in root.find("EngineData", self.namespaces).find("RuleInfo", self.namespaces): rule_id = rule.attrib.get("id") - logger.debug(f"Rule: {rule_id}") + logger.debug("Rule: %s", rule_id) if rule_id: related_data.rules[rule_id] = self.parse_rule_information(rule.find("MetaInfo", self.namespaces)) return related_data @@ -95,11 +95,11 @@ def add_audit_log(self, related_data, audit_log: Element) -> None: if instance_id: suppressed_string = issue.attrib.get("suppressed") suppressed = suppressed_string.lower() == "true" if suppressed_string else False - logger.debug(f"Issue: {instance_id} - Suppressed: {suppressed}") + logger.debug("Issue: %s - Suppressed: %s", instance_id, suppressed) related_data.suppressed[instance_id] = suppressed threaded_comments = issue.find("ThreadedComments", self.namespaces_audit_log) - logger.debug(f"ThreadedComments: {threaded_comments}") + logger.debug("ThreadedComments: %s", threaded_comments) if threaded_comments is not None: related_data.threaded_comments[instance_id] = [self.get_comment_text(comment) for comment in threaded_comments.findall("Comment", self.namespaces_audit_log)] return related_data @@ -295,7 +295,7 @@ def compute_severity(self, vulnerability, snippet, description, rule) -> str: # This comes from Fortify support documentation, requested in #11901 likelihood = (accuracy * confidence * probability) / 25 likelihood = round(likelihood, 1) - logger.debug(f"Impact: {impact}, Likelihood: {likelihood}") + logger.debug("Impact: %s, Likelihood: %s", impact, likelihood) if impact >= 2.5 and likelihood >= 2.5: return "Critical" diff --git a/dojo/tools/qualys/csv_parser.py b/dojo/tools/qualys/csv_parser.py index 49d31b1783d..b42a564c070 100644 --- a/dojo/tools/qualys/csv_parser.py +++ b/dojo/tools/qualys/csv_parser.py @@ -113,9 +113,9 @@ def _extract_cvss_vectors(cvss_base, cvss_temporal): try: cvss_vector += re.search(vector_pattern, cvss_base).group(1) except IndexError: - _logger.error(f"CVSS3 Base Vector not found in {cvss_base}") + _logger.error("CVSS3 Base Vector not found in %s", cvss_base) except AttributeError: - _logger.error(f"CVSS3 Base Vector not found in {cvss_base}") + _logger.error("CVSS3 Base Vector not found in %s", cvss_base) if cvss_temporal: try: cvss_temporal_vector = re.search( @@ -125,11 +125,11 @@ def _extract_cvss_vectors(cvss_base, cvss_temporal): cvss_vector += cvss_temporal_vector except IndexError: _logger.error( - f"CVSS3 Temporal Vector not found in {cvss_base}", + "CVSS3 Temporal Vector not found in %s", cvss_base, ) except AttributeError: _logger.error( - f"CVSS3 Temporal Vector not found in {cvss_base}", + "CVSS3 Temporal Vector not found in %s", cvss_base, ) return cvss_vector diff --git a/dojo/tools/twistlock/parser.py b/dojo/tools/twistlock/parser.py index 3042baa2e8f..99f2a8afeed 100644 --- a/dojo/tools/twistlock/parser.py +++ b/dojo/tools/twistlock/parser.py @@ -41,7 +41,7 @@ def parse_issue(self, row, test): # Handle alternative formats finding_date = datetime.strptime(date_str[:10], "%Y-%m-%d").date() except ValueError: - logger.warning(f"Could not parse date: {date_str}") + logger.warning("Could not parse date: %s", date_str) # Build container/image metadata for impact field (Item 3) impact_parts = [] diff --git a/dojo/tools/wiz/parser.py b/dojo/tools/wiz/parser.py index c869e1cac2e..41a22c3c616 100644 --- a/dojo/tools/wiz/parser.py +++ b/dojo/tools/wiz/parser.py @@ -195,7 +195,7 @@ def _parse_tags(self, tags: str) -> list[str]: def _validate_severities(self, severity: str) -> str: """Ensure the supplied severity fits what DefectDojo is expecting.""" if severity not in SEVERITIES: - logger.error(f"Severity is not supported: {severity}") + logger.error("Severity is not supported: %s", severity) # Default to Info severity return "Info" return severity @@ -260,6 +260,6 @@ def parse_wiz_datetime(row: dict, column: str) -> datetime: ) if not parsed_value: - logger.warning(f"Unable to parse Resolved Time: {value}") + logger.warning("Unable to parse Resolved Time: %s", value) return parsed_value diff --git a/dojo/user/views.py b/dojo/user/views.py index 998dd25a909..853791a319d 100644 --- a/dojo/user/views.py +++ b/dojo/user/views.py @@ -650,7 +650,7 @@ def clean(self): connection.open() connection.close() except Exception as e: - logger.error(f"SMTP Server Connection Failure: {e}") + logger.error("SMTP Server Connection Failure: %s", e) msg = "SMTP server is not configured correctly..." raise ValidationError(msg) diff --git a/dojo/utils.py b/dojo/utils.py index 991b6b84075..929fef76f0f 100644 --- a/dojo/utils.py +++ b/dojo/utils.py @@ -1956,7 +1956,7 @@ def _create_notifications(): query = Q(active=True, verified=True, is_mitigated=False, duplicate=False) elif system_settings.enable_notify_sla_active: query = Q(active=True, is_mitigated=False, duplicate=False) - logger.debug(f"My query: {query}") + logger.debug("My query: %s", query) no_jira_findings = {} if system_settings.enable_notify_sla_jira_only: @@ -2011,19 +2011,19 @@ def _create_notifications(): jira_count += 1 jira_instance = jira_helper.get_jira_instance(finding) if jira_instance is not None: - logger.debug(f"JIRA config for finding is {jira_instance}") + logger.debug("JIRA config for finding is %s", jira_instance) # global config or product config set, product level takes precedence try: # TODO: see new property from #2649 to then replace, somehow not working with prefetching though. product_jira_sla_comment_enabled = jira_helper.get_jira_project(finding).product_jira_sla_notification except Exception as e: logger.error("The product is not linked to a JIRA configuration! Something is weird here.") - logger.error(f"Error is: {e}") + logger.error("Error is: %s", e) jiraconfig_sla_notification_enabled = jira_instance.global_jira_sla_notification if jiraconfig_sla_notification_enabled or product_jira_sla_comment_enabled: - logger.debug(f"Global setting {jiraconfig_sla_notification_enabled} -- Product setting {product_jira_sla_comment_enabled}") + logger.debug("Global setting %s -- Product setting %s", jiraconfig_sla_notification_enabled, product_jira_sla_comment_enabled) do_jira_sla_comment = True logger.debug(f"JIRA issue is {jira_issue.jira_key}") @@ -2048,7 +2048,7 @@ def _create_notifications(): _add_notification(finding, "breaching") _create_notifications() - logger.info(f"SLA run results: Pre-breach: {pre_breach_count}, at-breach: {at_breach_count}, post-breach: {post_breach_count}, post-breach-no-notify: {post_breach_no_notify_count}, with-jira: {jira_count}, TOTAL: {total_count}") + logger.info("SLA run results: Pre-breach: %s, at-breach: %s, post-breach: %s, post-breach-no-notify: %s, with-jira: %s, TOTAL: %s", pre_breach_count, at_breach_count, post_breach_count, post_breach_no_notify_count, jira_count, total_count) except System_Settings.DoesNotExist: logger.info("Findings SLA is not enabled.") diff --git a/dojo/validators.py b/dojo/validators.py index fa0ed5b24e2..4c7c4f29d24 100644 --- a/dojo/validators.py +++ b/dojo/validators.py @@ -28,7 +28,7 @@ def tag_validator(value: str | list[str], exception_class: Callable = Validation error_messages.append(f"Value must be a string or list of strings: {value} - {type(value)}.") if error_messages: - logger.debug(f"Tag validation failed: {error_messages}") + logger.debug("Tag validation failed: %s", error_messages) raise exception_class(error_messages) diff --git a/tests/base_test_class.py b/tests/base_test_class.py index 9e70739c27a..939c72265eb 100644 --- a/tests/base_test_class.py +++ b/tests/base_test_class.py @@ -331,7 +331,7 @@ def enable_github(self): def set_block_execution(self, *, block_execution=True): # we set the admin user (ourselves) to have block_execution checked # this will force dedupe to happen synchronously, among other things like notifications, rules, ... - logger.info(f"setting block execution to: {block_execution}") + logger.info("setting block execution to: %s", block_execution) driver = self.driver driver.get(self.base_url + "profile") if ( diff --git a/tests/notes_test.py b/tests/notes_test.py index a569da5b052..f3aa1540b73 100644 --- a/tests/notes_test.py +++ b/tests/notes_test.py @@ -51,7 +51,7 @@ def create_private_note(self, driver, level): private_status = "(will not appear in report)" in text pass_test = note_present and private_status if not pass_test: - logger.info(f"Private note note created at the {level} level") + logger.info("Private note note created at the %s level", level) self.assertTrue(pass_test) def test_finding_note(self): diff --git a/unittests/test_factory.py b/unittests/test_factory.py index 06ad5344711..d03f7f4d35c 100644 --- a/unittests/test_factory.py +++ b/unittests/test_factory.py @@ -83,5 +83,5 @@ def test_parser_name_matches_module(self): if not found and module_name != "__pycache__": missing_parsers.append(module_name) if len(missing_parsers) > 0: - logger.error(f"Parsers with invalid names: {missing_parsers}") + logger.error("Parsers with invalid names: %s", missing_parsers) self.assertEqual(0, len(missing_parsers)) From f01c0a9b944b11b5a7932a0218208166c4f8f246 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 3 Sep 2025 09:54:58 -0600 Subject: [PATCH 004/154] Bump drf-spectacular-sidecar from 2025.8.1 to 2025.9.1 (#13089) Bumps [drf-spectacular-sidecar](https://github.com/tfranzel/drf-spectacular-sidecar) from 2025.8.1 to 2025.9.1. - [Commits](https://github.com/tfranzel/drf-spectacular-sidecar/compare/2025.8.1...2025.9.1) --- updated-dependencies: - dependency-name: drf-spectacular-sidecar dependency-version: 2025.9.1 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 350b8d03dcc..ae13de0db34 100644 --- a/requirements.txt +++ b/requirements.txt @@ -63,7 +63,7 @@ hyperlink==21.0.0 django-test-migrations==1.4.0 djangosaml2==1.11.1 drf-spectacular==0.28.0 -drf-spectacular-sidecar==2025.8.1 +drf-spectacular-sidecar==2025.9.1 django-ratelimit==4.1.0 argon2-cffi==25.1.0 blackduck==1.1.3 From 61711a155a38bf2cde81783c635fa17df795b9b1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 3 Sep 2025 09:55:30 -0600 Subject: [PATCH 005/154] Bump cryptography from 45.0.6 to 45.0.7 (#13088) Bumps [cryptography](https://github.com/pyca/cryptography) from 45.0.6 to 45.0.7. - [Changelog](https://github.com/pyca/cryptography/blob/45.0.7/CHANGELOG.rst) - [Commits](https://github.com/pyca/cryptography/compare/45.0.6...45.0.7) --- updated-dependencies: - dependency-name: cryptography dependency-version: 45.0.7 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index ae13de0db34..5042faa87dc 100644 --- a/requirements.txt +++ b/requirements.txt @@ -32,7 +32,7 @@ Markdown==3.8.2 openpyxl==3.1.5 Pillow==11.3.0 # required by django-imagekit psycopg[c]==3.2.9 -cryptography==45.0.6 +cryptography==45.0.7 python-dateutil==2.9.0.post0 redis==6.4.0 requests==2.32.5 From bddf26b23c4fc6a7f08a0a43fff8a09f20f189c1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 3 Sep 2025 09:57:09 -0600 Subject: [PATCH 006/154] Bump django-dbbackup from 4.3.0 to 5.0.0 (#13087) Bumps [django-dbbackup](https://github.com/Archmonger/django-dbbackup) from 4.3.0 to 5.0.0. - [Release notes](https://github.com/Archmonger/django-dbbackup/releases) - [Changelog](https://github.com/Archmonger/django-dbbackup/blob/master/CHANGELOG.md) - [Commits](https://github.com/Archmonger/django-dbbackup/compare/4.3.0...5.0.0) --- updated-dependencies: - dependency-name: django-dbbackup dependency-version: 5.0.0 dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 5042faa87dc..1ef08d655d1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,7 +6,7 @@ celery==5.5.3 defusedxml==0.7.1 django_celery_results==2.6.0 django-auditlog==3.2.1 -django-dbbackup==4.3.0 +django-dbbackup==5.0.0 django-environ==0.12.0 django-filter==25.1 django-imagekit==5.0.0 From a8118f134e694cd68dd064f8d77914dd15a4e480 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 3 Sep 2025 09:57:29 -0600 Subject: [PATCH 007/154] Bump boto3 from 1.40.20 to 1.40.21 (#13086) Bumps [boto3](https://github.com/boto/boto3) from 1.40.20 to 1.40.21. - [Release notes](https://github.com/boto/boto3/releases) - [Commits](https://github.com/boto/boto3/compare/1.40.20...1.40.21) --- updated-dependencies: - dependency-name: boto3 dependency-version: 1.40.21 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 1ef08d655d1..8f118fdce13 100644 --- a/requirements.txt +++ b/requirements.txt @@ -68,7 +68,7 @@ django-ratelimit==4.1.0 argon2-cffi==25.1.0 blackduck==1.1.3 pycurl==7.45.6 # Required for Celery Broker AWS (SQS) support -boto3==1.40.20 # Required for Celery Broker AWS (SQS) support +boto3==1.40.21 # Required for Celery Broker AWS (SQS) support netaddr==1.3.0 vulners==2.3.7 fontawesomefree==6.6.0 From 5329068173de3a9fbc3aed6e505e96f077fba693 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 3 Sep 2025 09:57:50 -0600 Subject: [PATCH 008/154] chore(deps): update dependency vite from 7.1.3 to v7.1.4 (docs/package.json) (#13085) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- docs/package-lock.json | 8 ++++---- docs/package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/package-lock.json b/docs/package-lock.json index 53783def753..4d13c7f8b65 100644 --- a/docs/package-lock.json +++ b/docs/package-lock.json @@ -20,7 +20,7 @@ }, "devDependencies": { "prettier": "3.6.2", - "vite": "7.1.3" + "vite": "7.1.4" }, "engines": { "node": "22.19.0" @@ -5125,9 +5125,9 @@ "license": "MIT" }, "node_modules/vite": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/vite/-/vite-7.1.3.tgz", - "integrity": "sha512-OOUi5zjkDxYrKhTV3V7iKsoS37VUM7v40+HuwEmcrsf11Cdx9y3DIr2Px6liIcZFwt3XSRpQvFpL3WVy7ApkGw==", + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.1.4.tgz", + "integrity": "sha512-X5QFK4SGynAeeIt+A7ZWnApdUyHYm+pzv/8/A57LqSGcI88U6R6ipOs3uCesdc6yl7nl+zNO0t8LmqAdXcQihw==", "dev": true, "license": "MIT", "dependencies": { diff --git a/docs/package.json b/docs/package.json index e6ff4e17a92..bad55f886d7 100644 --- a/docs/package.json +++ b/docs/package.json @@ -23,7 +23,7 @@ }, "devDependencies": { "prettier": "3.6.2", - "vite": "7.1.3" + "vite": "7.1.4" }, "engines": { "node": "22.19.0" From 2f85b1f960c30c70dcd0ea8e86166a4bf01ef262 Mon Sep 17 00:00:00 2001 From: valentijnscholten Date: Thu, 4 Sep 2025 03:51:57 +0200 Subject: [PATCH 009/154] msteams: Support Power Automate Workflows via Adaptive Card format (#13082) * msteams: Use adaptive cards format * update docs * revert webhook scan_added_empty --- ...onfigure_a_Microsoft_Teams_Integration.png | Bin 50262 -> 118880 bytes .../notifications/email_slack_teams.md | 10 +- dojo/notifications/helper.py | 3 +- .../notifications/alert/report_created.tpl | 3 - .../notifications/alert/scan_added_empty.tpl | 2 +- .../notifications/mail/report_created.tpl | 37 ---- .../notifications/mail/scan_added_empty.tpl | 2 +- .../msteams/engagement_added.tpl | 137 ++++++++++----- .../msteams/engagement_closed.tpl | 135 ++++++++++---- .../templates/notifications/msteams/other.tpl | 104 ++++++++--- .../notifications/msteams/product_added.tpl | 136 +++++++++++---- .../msteams/product_type_added.tpl | 128 ++++++++++---- .../notifications/msteams/report_created.tpl | 39 ----- .../msteams/review_requested.tpl | 165 +++++++++++++----- .../msteams/risk_acceptance_expiration.tpl | 154 ++++++++++------ .../notifications/msteams/scan_added.tpl | 161 ++++++++++++----- .../msteams/scan_added_empty.tpl | 2 +- .../notifications/msteams/sla_breach.tpl | 141 ++++++++++----- .../notifications/msteams/test_added.tpl | 141 ++++++++++----- .../msteams/upcoming_engagement.tpl | 145 ++++++++++----- .../notifications/msteams/user_mentioned.tpl | 144 ++++++++++----- .../notifications/slack/scan_added_empty.tpl | 2 +- 22 files changed, 1212 insertions(+), 579 deletions(-) delete mode 100644 dojo/templates/notifications/alert/report_created.tpl delete mode 100644 dojo/templates/notifications/mail/report_created.tpl delete mode 100644 dojo/templates/notifications/msteams/report_created.tpl diff --git a/docs/assets/images/Configure_a_Microsoft_Teams_Integration.png b/docs/assets/images/Configure_a_Microsoft_Teams_Integration.png index 22a151c6bee7891f7623276f8d059c963a84db09..1085afcd662f94a705315c2c6f8976d8c4694080 100644 GIT binary patch literal 118880 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 literal 50262 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 System Settings** from the sidebar. (In the Pro UI, this form is located under **Enterprise Settings > System Settings**.) -3. Check the **Enable Microsoft Teams notifications** box. This will open a hidden section of the form, labeled **‘Msteams url**’. +3. Check the **Enable Microsoft Teams notifications** box. This will open a hidden section of the form, labeled **‘Msteams url**’. ​ ![image](images/Configure_a_Microsoft_Teams_Integration_2.png) -4. Paste the webhook.office.com URL (created in Step 1\) in the **Msteams url** box. Your Teams app will now listen to incoming Notifications from DefectDojo and post them to the channel you selected. +4. Paste the logic.azure.com URL (created in Step 1\) in the **Msteams url** box. Your Teams app will now listen to incoming Notifications from DefectDojo and post them to the channel you selected. ### Notes on the Teams integration diff --git a/dojo/notifications/helper.py b/dojo/notifications/helper.py index 99edd43c593..a92ef11fc0a 100644 --- a/dojo/notifications/helper.py +++ b/dojo/notifications/helper.py @@ -335,9 +335,10 @@ def send_msteams_notification( "msteams", kwargs, ), + headers={"Content-Type": "application/json"}, timeout=settings.REQUESTS_TIMEOUT, ) - if res.status_code != 200: + if not (200 <= res.status_code < 300): logger.error("Error when sending message to Microsoft Teams") logger.error(res.status_code) logger.error(res.text) diff --git a/dojo/templates/notifications/alert/report_created.tpl b/dojo/templates/notifications/alert/report_created.tpl deleted file mode 100644 index 08880df26d3..00000000000 --- a/dojo/templates/notifications/alert/report_created.tpl +++ /dev/null @@ -1,3 +0,0 @@ -{% load i18n %}{% blocktranslate trimmed with name=report.name %} -Your report "{{ name }}" is ready. -{% endblocktranslate %} \ No newline at end of file diff --git a/dojo/templates/notifications/alert/scan_added_empty.tpl b/dojo/templates/notifications/alert/scan_added_empty.tpl index 4efb291e005..03390a2d58d 120000 --- a/dojo/templates/notifications/alert/scan_added_empty.tpl +++ b/dojo/templates/notifications/alert/scan_added_empty.tpl @@ -1 +1 @@ -scan_added.tpl \ No newline at end of file +{% include "notifications/alert/scan_added.tpl" %} \ No newline at end of file diff --git a/dojo/templates/notifications/mail/report_created.tpl b/dojo/templates/notifications/mail/report_created.tpl deleted file mode 100644 index 0c898d276ec..00000000000 --- a/dojo/templates/notifications/mail/report_created.tpl +++ /dev/null @@ -1,37 +0,0 @@ -{% load i18n %} -{% load navigation_tags %} -{% load display_tags %} - - - {% autoescape on %} -

- {% trans "Greetings" %}, -

-

- {% blocktranslate trimmed with report_name=report.name report_url=url|full_url %} - Your report "{{ report_name }}" is ready. It can be downloaded here: {{ report_url }} - {% endblocktranslate %} -

- {% trans "Kind regards" %},
- {% if system_settings.team_name %} - {{ system_settings.team_name }} - {% else %} - Defect Dojo - {% endif %} -
-
-

- {% url 'notifications' as notification_url %} - {% trans "You can manage your notification settings here" %}: {{ notification_url|full_url }} - -

- {% if system_settings.disclaimer_notifications and system_settings.disclaimer_notifications.strip %} -
-
- {% trans "Disclaimer" %}
-

{{ system_settings.disclaimer_notifications }}

-
- {% endif %} - {% endautoescape %} - - diff --git a/dojo/templates/notifications/mail/scan_added_empty.tpl b/dojo/templates/notifications/mail/scan_added_empty.tpl index 4efb291e005..03f4bb86dc1 120000 --- a/dojo/templates/notifications/mail/scan_added_empty.tpl +++ b/dojo/templates/notifications/mail/scan_added_empty.tpl @@ -1 +1 @@ -scan_added.tpl \ No newline at end of file +{% include "notifications/mail/scan_added.tpl" %} \ No newline at end of file diff --git a/dojo/templates/notifications/msteams/engagement_added.tpl b/dojo/templates/notifications/msteams/engagement_added.tpl index 747f8911079..0981d5574cc 100644 --- a/dojo/templates/notifications/msteams/engagement_added.tpl +++ b/dojo/templates/notifications/msteams/engagement_added.tpl @@ -1,44 +1,103 @@ -{% load i18n %} -{% load display_tags %} +{% load i18n %}{% load display_tags %}{% url 'view_engagement' engagement.id as url %} { - "@context": "https://schema.org/extensions", - "@type": "MessageCard", - "title": "{% trans "Engagement added" %}", - "summary": "{% trans "Engagement added" %}", - "sections": [ + "type": "message", + "attachments": [ { - "activityTitle": "DefectDojo", - "activityImage": "https://raw.githubusercontent.com/DefectDojo/django-DefectDojo/master/dojo/static/dojo/img/chop.png", - "text": "{% trans "A new engagement has been added" %}.", - "facts": [ - { - "name": "{% trans "Product" %}:", - "value": "{{ engagement.product.name }}" - }, - { - "name": "{% trans "Engagement" %}:", - "value": "{{ engagement.name }}" - } - ] - } - {% if system_settings.disclaimer_notifications and system_settings.disclaimer_notifications.strip %} - ,{ - "activityTitle": "{% trans "Disclaimer" %}", - "text": "{{ system_settings.disclaimer_notifications }}" + "contentType": "application/vnd.microsoft.card.adaptive", + "content": { + "$schema": "http://adaptivecards.io/schemas/adaptive-card.json", + "type": "AdaptiveCard", + "version": "1.4", + "body": [ + { + "type": "ColumnSet", + "columns": [ + { + "type": "Column", + "width": "auto", + "items": [ + { + "type": "Image", + "url": "https://raw.githubusercontent.com/DefectDojo/django-DefectDojo/master/dojo/static/dojo/img/chop.png", + "size": "Small" + } + ] + }, + { + "type": "Column", + "width": "stretch", + "items": [ + { + "type": "TextBlock", + "text": "DefectDojo", + "weight": "Bolder", + "size": "Medium" + }, + { + "type": "TextBlock", + "text": "{% trans 'Engagement Added' %}", + "weight": "Bolder", + "size": "Large", + "color": "Accent" + } + ] + } + ] + }, + { + "type": "TextBlock", + "text": "{% trans 'A new engagement' %} [{{ engagement.name }}]({{ url|full_url }}) {% trans 'has been added.' %}", + "wrap": true, + "spacing": "Medium" + }, + { + "type": "FactSet", + "facts": [ + { + "title": "{% trans 'Product' %}:", + "value": "{{ engagement.product.name }}" + }, + { + "title": "{% trans 'Engagement' %}:", + "value": "{{ engagement.name }}" + }, + { + "title": "{% trans 'Start date' %}:", + "value": "{{ engagement.target_start }}" + }, + { + "title": "{% trans 'End date' %}:", + "value": "{{ engagement.target_end }}" + } + ], + "spacing": "Medium" + }{% if system_settings.disclaimer_notifications and system_settings.disclaimer_notifications.strip %}, + { + "type": "Container", + "style": "attention", + "items": [ + { + "type": "TextBlock", + "text": "{% trans 'Disclaimer' %}", + "weight": "Bolder" + }, + { + "type": "TextBlock", + "text": "{{ system_settings.disclaimer_notifications }}", + "wrap": true + } + ], + "spacing": "Medium" + }{% endif %} + ], + "actions": [ + { + "type": "Action.OpenUrl", + "title": "{% trans 'View Engagement' %}", + "url": "{{ url|full_url }}" + } + ] } - {% endif %} - - ], - "potentialAction": [ - { - "@type": "OpenUri", - "name": "{% trans "View Engagement" %}", - "targets": [ - { - "os": "default", - "uri": "{{ url|full_url }}" - } - ] } ] -} +} \ No newline at end of file diff --git a/dojo/templates/notifications/msteams/engagement_closed.tpl b/dojo/templates/notifications/msteams/engagement_closed.tpl index 3e6bfeed0df..9c0d7178eb3 100644 --- a/dojo/templates/notifications/msteams/engagement_closed.tpl +++ b/dojo/templates/notifications/msteams/engagement_closed.tpl @@ -1,44 +1,103 @@ -{% load i18n %} -{% load display_tags %} +{% load i18n %}{% load display_tags %}{% url 'view_engagement' engagement.id as url %} { - "@context": "https://schema.org/extensions", - "@type": "MessageCard", - "title": "{% trans "Engagement closed" %}", - "summary": "{% trans "Engagement closed" %}", - "sections": [ + "type": "message", + "attachments": [ { - "activityTitle": "DefectDojo", - "activityImage": "https://raw.githubusercontent.com/DefectDojo/django-DefectDojo/master/dojo/static/dojo/img/chop.png", - "text": "{% trans "An engagement has been closed" %}.", - "facts": [ - { - "name": "{% trans "Product" %}:", - "value": "{{ engagement.product.name }}" - }, - { - "name": "{% trans "Engagement" %}:", - "value": "{{ engagement.name }}" - } - ] - } - {% if system_settings.disclaimer and system_settings.disclaimer.strip %} - ,{ - "activityTitle": "{% trans "Disclaimer" %}", - "text": "{{ system_settings.disclaimer }}" + "contentType": "application/vnd.microsoft.card.adaptive", + "content": { + "$schema": "http://adaptivecards.io/schemas/adaptive-card.json", + "type": "AdaptiveCard", + "version": "1.4", + "body": [ + { + "type": "ColumnSet", + "columns": [ + { + "type": "Column", + "width": "auto", + "items": [ + { + "type": "Image", + "url": "https://raw.githubusercontent.com/DefectDojo/django-DefectDojo/master/dojo/static/dojo/img/chop.png", + "size": "Small" + } + ] + }, + { + "type": "Column", + "width": "stretch", + "items": [ + { + "type": "TextBlock", + "text": "DefectDojo", + "weight": "Bolder", + "size": "Medium" + }, + { + "type": "TextBlock", + "text": "{% trans 'Engagement Closed' %}", + "weight": "Bolder", + "size": "Large", + "color": "Accent" + } + ] + } + ] + }, + { + "type": "TextBlock", + "text": "{% trans 'An engagement' %} [{{ engagement.name }}]({{ url|full_url }}) {% trans 'has been closed.' %}", + "wrap": true, + "spacing": "Medium" + }, + { + "type": "FactSet", + "facts": [ + { + "title": "{% trans 'Product' %}:", + "value": "{{ engagement.product.name }}" + }, + { + "title": "{% trans 'Engagement' %}:", + "value": "{{ engagement.name }}" + }, + { + "title": "{% trans 'Start date' %}:", + "value": "{{ engagement.target_start }}" + }, + { + "title": "{% trans 'End date' %}:", + "value": "{{ engagement.target_end }}" + } + ], + "spacing": "Medium" + }{% if system_settings.disclaimer_notifications and system_settings.disclaimer_notifications.strip %}, + { + "type": "Container", + "style": "attention", + "items": [ + { + "type": "TextBlock", + "text": "{% trans 'Disclaimer' %}", + "weight": "Bolder" + }, + { + "type": "TextBlock", + "text": "{{ system_settings.disclaimer_notifications }}", + "wrap": true + } + ], + "spacing": "Medium" + }{% endif %} + ], + "actions": [ + { + "type": "Action.OpenUrl", + "title": "{% trans 'View Engagement' %}", + "url": "{{ url|full_url }}" + } + ] } - {% endif %} - - ], - "potentialAction": [ - { - "@type": "OpenUri", - "name": "{% trans "View Engagement" %}", - "targets": [ - { - "os": "default", - "uri": "{{ url|full_url }}" - } - ] } ] } \ No newline at end of file diff --git a/dojo/templates/notifications/msteams/other.tpl b/dojo/templates/notifications/msteams/other.tpl index 123390f9c58..cbe261a7a5a 100644 --- a/dojo/templates/notifications/msteams/other.tpl +++ b/dojo/templates/notifications/msteams/other.tpl @@ -1,33 +1,81 @@ -{% load i18n %} -{% load display_tags %} +{% load i18n %}{% load display_tags %} { - "@context": "https://schema.org/extensions", - "@type": "MessageCard", - "title": "{% trans "Event" %}", - "summary": "{% trans "Event" %}", - "sections": [ + "type": "message", + "attachments": [ { - "activityTitle": "DefectDojo", - "activityImage": "https://raw.githubusercontent.com/DefectDojo/django-DefectDojo/master/dojo/static/dojo/img/chop.png", - "text": "{% autoescape on %} {{ description }} {% endautoescape %}" - } - {% if system_settings.disclaimer_notifications and system_settings.disclaimer_notifications.strip %} - ,{ - "activityTitle": "{% trans "Disclaimer" %}", - "text": "{{ system_settings.disclaimer_notifications }}" + "contentType": "application/vnd.microsoft.card.adaptive", + "content": { + "$schema": "http://adaptivecards.io/schemas/adaptive-card.json", + "type": "AdaptiveCard", + "version": "1.4", + "body": [ + { + "type": "ColumnSet", + "columns": [ + { + "type": "Column", + "width": "auto", + "items": [ + { + "type": "Image", + "url": "https://raw.githubusercontent.com/DefectDojo/django-DefectDojo/master/dojo/static/dojo/img/chop.png", + "size": "Small" + } + ] + }, + { + "type": "Column", + "width": "stretch", + "items": [ + { + "type": "TextBlock", + "text": "DefectDojo", + "weight": "Bolder", + "size": "Medium" + }, + { + "type": "TextBlock", + "text": "{% trans 'Event' %}", + "weight": "Bolder", + "size": "Large", + "color": "Accent" + } + ] + } + ] + }, + { + "type": "TextBlock", + "text": "{% autoescape on %}{{ description }}{% endautoescape %}", + "wrap": true, + "spacing": "Medium" + }{% if system_settings.disclaimer_notifications and system_settings.disclaimer_notifications.strip %}, + { + "type": "Container", + "style": "attention", + "items": [ + { + "type": "TextBlock", + "text": "{% trans 'Disclaimer' %}", + "weight": "Bolder" + }, + { + "type": "TextBlock", + "text": "{{ system_settings.disclaimer_notifications }}", + "wrap": true + } + ], + "spacing": "Medium" + }{% endif %} + ], + "actions": [ + { + "type": "Action.OpenUrl", + "title": "{% trans 'View' %}", + "url": "{{ url|full_url }}" + } + ] } - {% endif %} - ], - "potentialAction": [ - { - "@type": "OpenUri", - "name": "{% trans "View" %}", - "targets": [ - { - "os": "default", - "uri": "{{ url|full_url }}" - } - ] } ] -} +} \ No newline at end of file diff --git a/dojo/templates/notifications/msteams/product_added.tpl b/dojo/templates/notifications/msteams/product_added.tpl index d2def33cd0f..24ff6187241 100644 --- a/dojo/templates/notifications/msteams/product_added.tpl +++ b/dojo/templates/notifications/msteams/product_added.tpl @@ -1,39 +1,107 @@ -{% load i18n %} -{% load display_tags %} +{% load i18n %}{% load display_tags %}{% url 'view_product' product.id as url %} { - "@context": "https://schema.org/extensions", - "@type": "MessageCard", - "title": "{% trans "Product Added" %}", - "summary": "{% trans "Product Added" %}", - "sections": [ + "type": "message", + "attachments": [ { - "activityTitle": "DefectDojo", - "activityImage": "https://raw.githubusercontent.com/DefectDojo/django-DefectDojo/master/dojo/static/dojo/img/chop.png", - "text": "{% trans "A new product has been added" %}.", - "facts": [ - { - "name": "{% trans "Product" %}:", - "value": "{{ title }}" - } - ] - } - {% if system_settings.disclaimer_notifications and system_settings.disclaimer_notifications.strip %} - ,{ - "activityTitle": "{% trans "Disclaimer" %}", - "text": "{{ system_settings.disclaimer_notifications }}" + "contentType": "application/vnd.microsoft.card.adaptive", + "content": { + "$schema": "http://adaptivecards.io/schemas/adaptive-card.json", + "type": "AdaptiveCard", + "version": "1.4", + "body": [ + { + "type": "ColumnSet", + "columns": [ + { + "type": "Column", + "width": "auto", + "items": [ + { + "type": "Image", + "url": "https://raw.githubusercontent.com/DefectDojo/django-DefectDojo/master/dojo/static/dojo/img/chop.png", + "size": "Small" + } + ] + }, + { + "type": "Column", + "width": "stretch", + "items": [ + { + "type": "TextBlock", + "text": "DefectDojo", + "weight": "Bolder", + "size": "Medium" + }, + { + "type": "TextBlock", + "text": "{% trans 'Product Added' %}", + "weight": "Bolder", + "size": "Large", + "color": "Accent" + } + ] + } + ] + }, + { + "type": "TextBlock", + "text": "{% trans 'A new product' %} [{{ product.name }}]({{ url|full_url }}) {% trans 'has been added.' %}", + "wrap": true, + "spacing": "Medium" + }, + { + "type": "FactSet", + "facts": [ + { + "title": "{% trans 'Product' %}:", + "value": "{{ product.name }}" + }, + { + "title": "{% trans 'Tags' %}:", + "value": "{% for tag in product.tags.all %}{{ tag.name }}{% if not forloop.last %}, {% endif %}{% empty %}{% trans 'None' %}{% endfor %}" + }, + { + "title": "{% trans 'Product Type' %}:", + "value": "{{ product.prod_type.name }}" + }, + { + "title": "{% trans 'SLA Configuration' %}:", + "value": "{% if product.sla_configuration %}{{ product.sla_configuration.name }}{% else %}{% trans 'None' %}{% endif %}" + }, + { + "title": "{% trans 'Internet Accessible' %}:", + "value": "{% if product.internet %}{% trans 'Yes' %}{% else %}{% trans 'No' %}{% endif %}" + } + ], + "spacing": "Medium" + }{% if system_settings.disclaimer_notifications and system_settings.disclaimer_notifications.strip %}, + { + "type": "Container", + "style": "attention", + "items": [ + { + "type": "TextBlock", + "text": "{% trans 'Disclaimer' %}", + "weight": "Bolder" + }, + { + "type": "TextBlock", + "text": "{{ system_settings.disclaimer_notifications }}", + "wrap": true + } + ], + "spacing": "Medium" + }{% endif %} + ], + "actions": [ + { + "type": "Action.OpenUrl", + "title": "{% trans 'View Product' %}", + "url": "{{ url|full_url }}" + } + ] } - {% endif %} - ], - "potentialAction": [ - { - "@type": "OpenUri", - "name": "{% trans "View Product" %}", - "targets": [ - { - "os": "default", - "uri": "{{ url|full_url }}" - } - ] } ] -} +} \ No newline at end of file diff --git a/dojo/templates/notifications/msteams/product_type_added.tpl b/dojo/templates/notifications/msteams/product_type_added.tpl index b650f23d6c3..ff1395b2ed2 100644 --- a/dojo/templates/notifications/msteams/product_type_added.tpl +++ b/dojo/templates/notifications/msteams/product_type_added.tpl @@ -1,39 +1,99 @@ -{% load i18n %} -{% load display_tags %} +{% load i18n %}{% load display_tags %}{% url 'view_product_type' product_type.id as url %} { - "@context": "https://schema.org/extensions", - "@type": "MessageCard", - "title": "{% trans "Product Type Added" %}", - "summary": "{% trans "Product Type Added" %}", - "sections": [ + "type": "message", + "attachments": [ { - "activityTitle": "DefectDojo", - "activityImage": "https://raw.githubusercontent.com/DefectDojo/django-DefectDojo/master/dojo/static/dojo/img/chop.png", - "text": "{% trans "A new product type has been added" %}.", - "facts": [ - { - "name": "{% trans "Product Type" %}:", - "value": "{{ title }}" - } - ] - } - {% if system_settings.disclaimer_notifications and system_settings.disclaimer_notifications.strip %} - ,{ - "activityTitle": "{% trans "Disclaimer" %}", - "text": "{{ system_settings.disclaimer_notifications }}" + "contentType": "application/vnd.microsoft.card.adaptive", + "content": { + "$schema": "http://adaptivecards.io/schemas/adaptive-card.json", + "type": "AdaptiveCard", + "version": "1.4", + "body": [ + { + "type": "ColumnSet", + "columns": [ + { + "type": "Column", + "width": "auto", + "items": [ + { + "type": "Image", + "url": "https://raw.githubusercontent.com/DefectDojo/django-DefectDojo/master/dojo/static/dojo/img/chop.png", + "size": "Small" + } + ] + }, + { + "type": "Column", + "width": "stretch", + "items": [ + { + "type": "TextBlock", + "text": "DefectDojo", + "weight": "Bolder", + "size": "Medium" + }, + { + "type": "TextBlock", + "text": "{% trans 'Product Type Added' %}", + "weight": "Bolder", + "size": "Large", + "color": "Accent" + } + ] + } + ] + }, + { + "type": "TextBlock", + "text": "{% trans 'A new product type' %} [{{ product_type.name }}]({{ url|full_url }}) {% trans 'has been added.' %}", + "wrap": true, + "spacing": "Medium" + }, + { + "type": "FactSet", + "facts": [ + { + "title": "{% trans 'Product Type' %}:", + "value": "{{ product_type.name }}" + }, + { + "title": "{% trans 'Critical Product' %}:", + "value": "{% if product_type.critical_product %}{% trans 'Yes' %}{% else %}{% trans 'No' %}{% endif %}" + }, + { + "title": "{% trans 'Key Product' %}:", + "value": "{% if product_type.key_product %}{% trans 'Yes' %}{% else %}{% trans 'No' %}{% endif %}" + } + ], + "spacing": "Medium" + }{% if system_settings.disclaimer_notifications and system_settings.disclaimer_notifications.strip %}, + { + "type": "Container", + "style": "attention", + "items": [ + { + "type": "TextBlock", + "text": "{% trans 'Disclaimer' %}", + "weight": "Bolder" + }, + { + "type": "TextBlock", + "text": "{{ system_settings.disclaimer_notifications }}", + "wrap": true + } + ], + "spacing": "Medium" + }{% endif %} + ], + "actions": [ + { + "type": "Action.OpenUrl", + "title": "{% trans 'View Product Type' %}", + "url": "{{ url|full_url }}" + } + ] } - {% endif %} - ], - "potentialAction": [ - { - "@type": "OpenUri", - "name": "{% trans "View Product Type" %}", - "targets": [ - { - "os": "default", - "uri": "{{ url|full_url }}" - } - ] } ] -} +} \ No newline at end of file diff --git a/dojo/templates/notifications/msteams/report_created.tpl b/dojo/templates/notifications/msteams/report_created.tpl deleted file mode 100644 index 42f8398ed25..00000000000 --- a/dojo/templates/notifications/msteams/report_created.tpl +++ /dev/null @@ -1,39 +0,0 @@ -{% load i18n %} -{% load display_tags %} -{ - "@context": "https://schema.org/extensions", - "@type": "MessageCard", - "title": "{% trans "Report created" %}", - "summary": "{% trans "Report created" %}", - "sections": [ - { - "activityTitle": "DefectDojo", - "activityImage": "https://raw.githubusercontent.com/DefectDojo/django-DefectDojo/master/dojo/static/dojo/img/chop.png", - "text": "{% trans "Report is ready for download" %}.", - "facts": [ - { - "name": "{% trans "Report" %}:", - "value": "{{ report.name }}" - } - ] - } - {% if system_settings.disclaimer_notifications and system_settings.disclaimer_notifications.strip %} - ,{ - "activityTitle": "{% trans "Disclaimer" %}", - "text": "{{ system_settings.disclaimer_notifications }}" - } - {% endif %} - ], - "potentialAction": [ - { - "@type": "OpenUri", - "name": "{% trans "Download" %}", - "targets": [ - { - "os": "default", - "uri": "{{ url|full_url }}" - } - ] - } - ] -} diff --git a/dojo/templates/notifications/msteams/review_requested.tpl b/dojo/templates/notifications/msteams/review_requested.tpl index 1ec637c900d..c62496bbf28 100644 --- a/dojo/templates/notifications/msteams/review_requested.tpl +++ b/dojo/templates/notifications/msteams/review_requested.tpl @@ -1,51 +1,124 @@ -{% load i18n %} -{% load display_tags %} +{% comment %} +NOTE: This template is currently NOT USED in practice because: +- review_requested notifications are sent to specific recipients (the reviewers) +- Microsoft Teams only supports system-wide notifications, not user-specific ones +- The notification system processes recipient-specific notifications using personal settings +- Since Teams doesn't have individual user channels/usernames (unlike email or Slack), + these notifications will only work for channels that support user-specific delivery +{% endcomment %} +{% load i18n %}{% load display_tags %}{% url 'view_finding' finding.id as finding_url %} { - "@context": "https://schema.org/extensions", - "@type": "MessageCard", - "title": "{% trans "Review Requested" %}", - "summary": "{% trans "Review Requested" %}", - "sections": [ + "type": "message", + "attachments": [ { - "activityTitle": "DefectDojo", - "activityImage": "https://raw.githubusercontent.com/DefectDojo/django-DefectDojo/master/dojo/static/dojo/img/chop.png", - "text": "{% trans "A user has requested that the following users review the finding below for accuracy" %}.", - "facts": [ - { - "name": "{% trans "Requested By" %}:", - "value": "{{ requested_by }}" - }, - { - "name": "{% trans "Finding" %}:", - "value": "{{ finding }}" - }, - { - "name": "{% trans "Reviewers" %}:", - "value": "{{ reviewers }}" - }, - { - "name": "{% trans "note" %}:", - "value": "{{ note }}" - } - ] - } - {% if system_settings.disclaimer_notifications and system_settings.disclaimer_notifications.strip %} - ,{ - "activityTitle": "{% trans "Disclaimer" %}", - "text": "{{ system_settings.disclaimer_notifications }}" + "contentType": "application/vnd.microsoft.card.adaptive", + "content": { + "$schema": "http://adaptivecards.io/schemas/adaptive-card.json", + "type": "AdaptiveCard", + "version": "1.4", + "body": [ + { + "type": "ColumnSet", + "columns": [ + { + "type": "Column", + "width": "auto", + "items": [ + { + "type": "Image", + "url": "https://raw.githubusercontent.com/DefectDojo/django-DefectDojo/master/dojo/static/dojo/img/chop.png", + "size": "Small" + } + ] + }, + { + "type": "Column", + "width": "stretch", + "items": [ + { + "type": "TextBlock", + "text": "DefectDojo", + "weight": "Bolder", + "size": "Medium" + }, + { + "type": "TextBlock", + "text": "{% trans 'Review Requested' %}", + "weight": "Bolder", + "size": "Large", + "color": "Accent" + } + ] + } + ] + }, + { + "type": "TextBlock", + "text": "{% trans 'A review has been requested for finding' %} [{{ finding.title }}]({{ finding_url|full_url }}).", + "wrap": true, + "spacing": "Medium" + }, + { + "type": "TextBlock", + "text": "{% trans 'Reviewers:' %} {% for reviewer in reviewers %}{{ reviewer.get_full_name|default:reviewer.username }}{% if not forloop.last %}, {% endif %}{% endfor %}", + "wrap": true, + "spacing": "Small" + }, + { + "type": "TextBlock", + "text": "{{ note.entry }}", + "wrap": true, + "spacing": "Small", + "isSubtle": true + }, + { + "type": "FactSet", + "facts": [ + { + "title": "{% trans 'Product' %}:", + "value": "{{ finding.test.engagement.product.name }}" + }, + { + "title": "{% trans 'Engagement' %}:", + "value": "{{ finding.test.engagement.name }}" + }, + { + "title": "{% trans 'Finding' %}:", + "value": "{{ finding.title }}" + }, + { + "title": "{% trans 'Severity' %}:", + "value": "{{ finding.severity }}" + } + ], + "spacing": "Medium" + }{% if system_settings.disclaimer_notifications and system_settings.disclaimer_notifications.strip %}, + { + "type": "Container", + "style": "attention", + "items": [ + { + "type": "TextBlock", + "text": "{% trans 'Disclaimer' %}", + "weight": "Bolder" + }, + { + "type": "TextBlock", + "text": "{{ system_settings.disclaimer_notifications }}", + "wrap": true + } + ], + "spacing": "Medium" + }{% endif %} + ], + "actions": [ + { + "type": "Action.OpenUrl", + "title": "{% trans 'View Finding' %}", + "url": "{{ finding_url|full_url }}" + } + ] } - {% endif %} - ], - "potentialAction": [ - { - "@type": "OpenUri", - "name": "{% trans "View" %}", - "targets": [ - { - "os": "default", - "uri": "{{ url|full_url }}" - } - ] } ] -} +} \ No newline at end of file diff --git a/dojo/templates/notifications/msteams/risk_acceptance_expiration.tpl b/dojo/templates/notifications/msteams/risk_acceptance_expiration.tpl index 95e6279b5ff..3d77a6472f7 100644 --- a/dojo/templates/notifications/msteams/risk_acceptance_expiration.tpl +++ b/dojo/templates/notifications/msteams/risk_acceptance_expiration.tpl @@ -1,57 +1,107 @@ -{% load i18n %} -{% load display_tags %} -{% url 'view_test' test.id as test_url %} +{% load i18n %}{% load display_tags %}{% url 'view_risk_acceptance' risk_acceptance.id as url %} { - "@context": "https://schema.org/extensions", - "@type": "MessageCard", - {% if risk_acceptance.is_expired %} - "title": "{% trans "Risk Acceptance Expired" %}", - "summary": "{% trans "Risk Acceptance Expired" %}", - {% else %} - "title": "{% trans "Risk Acceptance Will Expire Soon" %}", - "summary": "{% trans "Risk Acceptance Will Expire Soon" %}", - {% endif %} - "sections": [ + "type": "message", + "attachments": [ { - "activityTitle": "DefectDojo", - "activityImage": "https://raw.githubusercontent.com/DefectDojo/django-DefectDojo/master/dojo/static/dojo/img/chop.png", - {% if risk_acceptance.is_expired %} - "text": "{% blocktranslate with accepted_findings=risk_acceptance.accepted_findings.all|length exp_date=risk_acceptance.expiration_date_handled|date %}Risk acceptance {{ risk_acceptance }} with {{ accepted_findings }} has expired {{ exp_date }}{% endblocktranslate %}", - {% else %} - "text": "{% blocktranslate with accepted_findings=risk_acceptance.accepted_findings.all|length exp_date=risk_acceptance.expiration_date|date %}Risk acceptance {{ risk_acceptance }} with {{ accepted_findings }} will expire {{ exp_date }}{% endblocktranslate %}", - {% endif %} - "facts": [ - { - "name": "{% trans "Product" %}:", - "value": "{{ risk_acceptance.engagement.product.name }}" - }, - { - "name": "{% trans "Engagement" %}:", - "value": "{{ risk_acceptance.engagement.name }}" - }, - { - "name": "{% trans "Risk Acceptance" %}:", - "value": "{{ risk_acceptance }}" - } - ] - } - {% if system_settings.disclaimer_notifications and system_settings.disclaimer_notifications.strip %} - ,{ - "activityTitle": "{% trans "Disclaimer" %}", - "text": "{{ system_settings.disclaimer_notifications }}" + "contentType": "application/vnd.microsoft.card.adaptive", + "content": { + "$schema": "http://adaptivecards.io/schemas/adaptive-card.json", + "type": "AdaptiveCard", + "version": "1.4", + "body": [ + { + "type": "ColumnSet", + "columns": [ + { + "type": "Column", + "width": "auto", + "items": [ + { + "type": "Image", + "url": "https://raw.githubusercontent.com/DefectDojo/django-DefectDojo/master/dojo/static/dojo/img/chop.png", + "size": "Small" + } + ] + }, + { + "type": "Column", + "width": "stretch", + "items": [ + { + "type": "TextBlock", + "text": "DefectDojo", + "weight": "Bolder", + "size": "Medium" + }, + { + "type": "TextBlock", + "text": "{% if risk_acceptance.is_expired %}{% trans 'Risk Acceptance Expired' %}{% else %}{% trans 'Risk Acceptance Expiring' %}{% endif %}", + "weight": "Bolder", + "size": "Large", + "color": "{% if risk_acceptance.is_expired %}Attention{% else %}Warning{% endif %}" + } + ] + } + ] + }, + { + "type": "TextBlock", + "text": "{% if risk_acceptance.is_expired %}{% trans 'Risk acceptance' %} [{{ risk_acceptance }}]({{ url|full_url }}) {% blocktranslate with risk_findings=risk_acceptance.accepted_findings.all|length risk_date=risk_acceptance.expiration_date_handled|date %}with {{ risk_findings }} finding(s) has expired on {{ risk_date }}.{% endblocktranslate %}{% else %}{% trans 'Risk acceptance' %} [{{ risk_acceptance }}]({{ url|full_url }}) {% blocktranslate with risk_findings=risk_acceptance.accepted_findings.all|length risk_date=risk_acceptance.expiration_date|date %}with {{ risk_findings }} finding(s) will expire on {{ risk_date }}.{% endblocktranslate %}{% endif %}", + "wrap": true, + "spacing": "Medium" + }, + { + "type": "FactSet", + "facts": [ + { + "title": "{% trans 'Risk Acceptance' %}:", + "value": "{{ risk_acceptance }}" + }, + { + "title": "{% trans 'Accepted Findings' %}:", + "value": "{{ risk_acceptance.accepted_findings.all|length }}" + }, + { + "title": "{% if risk_acceptance.is_expired %}{% trans 'Expired Date' %}{% else %}{% trans 'Expiration Date' %}{% endif %}:", + "value": "{% if risk_acceptance.is_expired %}{{ risk_acceptance.expiration_date_handled|date }}{% else %}{{ risk_acceptance.expiration_date|date }}{% endif %}" + }{% if risk_acceptance.reactivate_expired %}, + { + "title": "{% trans 'Reactivation Status' %}:", + "value": "{% trans 'Findings have been reactivated' %}" + }{% endif %}{% if risk_acceptance.restart_sla_expired %}, + { + "title": "{% trans 'SLA Status' %}:", + "value": "{% trans 'Findings SLA start date have been reset' %}" + }{% endif %} + ], + "spacing": "Medium" + }{% if system_settings.disclaimer_notifications and system_settings.disclaimer_notifications.strip %}, + { + "type": "Container", + "style": "attention", + "items": [ + { + "type": "TextBlock", + "text": "{% trans 'Disclaimer' %}", + "weight": "Bolder" + }, + { + "type": "TextBlock", + "text": "{{ system_settings.disclaimer_notifications }}", + "wrap": true + } + ], + "spacing": "Medium" + }{% endif %} + ], + "actions": [ + { + "type": "Action.OpenUrl", + "title": "{% trans 'View Risk Acceptance' %}", + "url": "{{ url|full_url }}" + } + ] } - {% endif %} - ], - "potentialAction": [ - { - "@type": "OpenUri", - "name": "{% trans "View Risk Acceptance" %}", - "targets": [ - { - "os": "default", - "uri": "{{ url|full_url }}" - } - ] } ] -} +} \ No newline at end of file diff --git a/dojo/templates/notifications/msteams/scan_added.tpl b/dojo/templates/notifications/msteams/scan_added.tpl index 52a67fd76c8..bdbaa2bf081 100644 --- a/dojo/templates/notifications/msteams/scan_added.tpl +++ b/dojo/templates/notifications/msteams/scan_added.tpl @@ -1,48 +1,123 @@ -{% load i18n %} -{% load display_tags %} -{% url 'view_test' test.id as test_url %} +{% load i18n %}{% load display_tags %}{% url 'view_test' test.id as url %} { - "@context": "https://schema.org/extensions", - "@type": "MessageCard", - "title": "Scan added", - "summary": "Scan added", - "sections": [ + "type": "message", + "attachments": [ { - "activityTitle": "DefectDojo", - "activityImage": "https://raw.githubusercontent.com/DefectDojo/django-DefectDojo/master/dojo/static/dojo/img/chop.png", - "text": "{% trans "A new scan has been added" %}.", - "facts": [ - { - "name": "{% trans "Product" %}:", - "value": "{{ test.engagement.product.name }}" - }, - { - "name": "{% trans "Engagement" %}:", - "value": "{{ test.engagement.name }}" - }, - { - "name": "{% trans "Scan" %}:", - "value": "{{ test }}" - } - ] - } - {% if system_settings.disclaimer_notifications and system_settings.disclaimer_notifications.strip %} - ,{ - "activityTitle": "{% trans "Disclaimer" %}", - "text": "{{ system_settings.disclaimer_notifications }}" + "contentType": "application/vnd.microsoft.card.adaptive", + "content": { + "$schema": "http://adaptivecards.io/schemas/adaptive-card.json", + "type": "AdaptiveCard", + "version": "1.4", + "body": [ + { + "type": "ColumnSet", + "columns": [ + { + "type": "Column", + "width": "auto", + "items": [ + { + "type": "Image", + "url": "https://raw.githubusercontent.com/DefectDojo/django-DefectDojo/master/dojo/static/dojo/img/chop.png", + "size": "Small" + } + ] + }, + { + "type": "Column", + "width": "stretch", + "items": [ + { + "type": "TextBlock", + "text": "DefectDojo", + "weight": "Bolder", + "size": "Medium" + }, + { + "type": "TextBlock", + "text": "{% trans 'Scan Added' %}", + "weight": "Bolder", + "size": "Large", + "color": "Accent" + } + ] + } + ] + }, + { + "type": "TextBlock", + "text": "{% blocktranslate with count=finding_count %}{{ count }} findings have been updated for scan{% endblocktranslate %} [{{ test }}]({{ url|full_url }}).", + "wrap": true, + "spacing": "Medium" + }, + { + "type": "FactSet", + "facts": [ + { + "title": "{% trans 'Product' %}:", + "value": "{{ test.engagement.product.name }}" + }, + { + "title": "{% trans 'Engagement' %}:", + "value": "{{ test.engagement.name }}" + }, + { + "title": "{% trans 'Scan' %}:", + "value": "{{ test }}" + }, + { + "title": "{% trans 'Scan Type' %}:", + "value": "{{ test.test_type.name }}" + }, + { + "title": "{% trans 'Updated Findings' %}:", + "value": "{{ finding_count }}" + }, + { + "title": "{% trans 'New Findings' %}:", + "value": "{{ findings_new|length|default:'0' }}" + }, + { + "title": "{% trans 'Reactivated Findings' %}:", + "value": "{{ findings_reactivated|length|default:'0' }}" + }, + { + "title": "{% trans 'Closed Findings' %}:", + "value": "{{ findings_mitigated|length|default:'0' }}" + }, + { + "title": "{% trans 'Untouched Findings' %}:", + "value": "{{ findings_untouched|length|default:'0' }}" + } + ], + "spacing": "Medium" + }{% if system_settings.disclaimer_notifications and system_settings.disclaimer_notifications.strip %}, + { + "type": "Container", + "style": "attention", + "items": [ + { + "type": "TextBlock", + "text": "{% trans 'Disclaimer' %}", + "weight": "Bolder" + }, + { + "type": "TextBlock", + "text": "{{ system_settings.disclaimer_notifications }}", + "wrap": true + } + ], + "spacing": "Medium" + }{% endif %} + ], + "actions": [ + { + "type": "Action.OpenUrl", + "title": "{% trans 'View Scan' %}", + "url": "{{ url|full_url }}" + } + ] } - {% endif %} - ], - "potentialAction": [ - { - "@type": "OpenUri", - "name": "{% trans "View Test" %}", - "targets": [ - { - "os": "default", - "uri": "{{ url|full_url }}" - } - ] } ] -} +} \ No newline at end of file diff --git a/dojo/templates/notifications/msteams/scan_added_empty.tpl b/dojo/templates/notifications/msteams/scan_added_empty.tpl index 4efb291e005..cf88b460f91 120000 --- a/dojo/templates/notifications/msteams/scan_added_empty.tpl +++ b/dojo/templates/notifications/msteams/scan_added_empty.tpl @@ -1 +1 @@ -scan_added.tpl \ No newline at end of file +{% include "notifications/msteams/scan_added.tpl" %} \ No newline at end of file diff --git a/dojo/templates/notifications/msteams/sla_breach.tpl b/dojo/templates/notifications/msteams/sla_breach.tpl index 31ad6991bf0..e7f5d635b5c 100644 --- a/dojo/templates/notifications/msteams/sla_breach.tpl +++ b/dojo/templates/notifications/msteams/sla_breach.tpl @@ -1,56 +1,107 @@ -{% load i18n %} -{% load display_tags %} -{% url 'view_finding' finding.id as finding_url %} - { - "@context": "https://schema.org/extensions", - "@type": "MessageCard", - "title": "{% trans "SLA breached" %}", - "summary": "{% trans "SLA breached" %}", - "sections": [ - { - "activityTitle": "DefectDojo", - "activityImage": "https://raw.githubusercontent.com/DefectDojo/django-DefectDojo/master/dojo/static/dojo/img/chop.png", - "text": "{% trans "A SLA for a finding has been breached" %}.", - "facts": [ +{% load i18n %}{% load display_tags %}{% url 'view_finding' finding.id as finding_url %} +{ + "type": "message", + "attachments": [ + { + "contentType": "application/vnd.microsoft.card.adaptive", + "content": { + "$schema": "http://adaptivecards.io/schemas/adaptive-card.json", + "type": "AdaptiveCard", + "version": "1.4", + "body": [ { - "name": "{% trans "Product" %}:", - "value": "{{ finding.test.engagement.product.name }}" + "type": "ColumnSet", + "columns": [ + { + "type": "Column", + "width": "auto", + "items": [ + { + "type": "Image", + "url": "https://raw.githubusercontent.com/DefectDojo/django-DefectDojo/master/dojo/static/dojo/img/chop.png", + "size": "Small" + } + ] + }, + { + "type": "Column", + "width": "stretch", + "items": [ + { + "type": "TextBlock", + "text": "DefectDojo", + "weight": "Bolder", + "size": "Medium" + }, + { + "type": "TextBlock", + "text": "{% trans 'SLA Breach' %}", + "weight": "Bolder", + "size": "Large", + "color": "Accent" + } + ] + } + ] }, { - "name": "{% trans "Engagement" %}:", - "value": "{{ finding.test.engagement.name }}" + "type": "TextBlock", + "text": "{% trans 'SLA breach' %} [{{ finding.title }}]({{ finding_url|full_url }}) {% blocktranslate with severity=finding.severity sla_age=sla_age %}with severity {{ severity }} has breached its SLA ({{ sla_age }} days).{% endblocktranslate %}", + "wrap": true, + "spacing": "Medium" }, { - "name": "{% trans "Finding" %}:", - "value": "{{ finding.title }}" - }, + "type": "FactSet", + "facts": [ + { + "title": "{% trans 'Product' %}:", + "value": "{{ finding.test.engagement.product.name }}" + }, + { + "title": "{% trans 'Engagement' %}:", + "value": "{{ finding.test.engagement.name }}" + }, + { + "title": "{% trans 'Finding' %}:", + "value": "{{ finding.title }}" + }, + { + "title": "{% trans 'Severity' %}:", + "value": "{{ finding.severity }}" + }, + { + "title": "{% trans 'SLA age' %}:", + "value": "{{ sla_age }} days" + } + ], + "spacing": "Medium" + }{% if system_settings.disclaimer_notifications and system_settings.disclaimer_notifications.strip %}, { - "name": "{% trans "Severity" %}:", - "value": "{{ finding.severity }}" - }, + "type": "Container", + "style": "attention", + "items": [ + { + "type": "TextBlock", + "text": "{% trans 'Disclaimer' %}", + "weight": "Bolder" + }, + { + "type": "TextBlock", + "text": "{{ system_settings.disclaimer_notifications }}", + "wrap": true + } + ], + "spacing": "Medium" + }{% endif %} + ], + "actions": [ { - "name": "{% trans "SLA age" %}:", - "value": "{{ sla_age }}" + "type": "Action.OpenUrl", + "title": "{% trans 'View Finding' %}", + "url": "{{ finding_url|full_url }}" } - ] - } - {% if system_settings.disclaimer_notifications and system_settings.disclaimer_notifications.strip %} - ,{ - "activityTitle": "{% trans "Disclaimer" %}", - "text": "{{ system_settings.disclaimer_notifications }}" + ] } - {% endif %} - ], - "potentialAction": [ - { - "@type": "OpenUri", - "name": "{% trans "View" %}", - "targets": [ - { - "os": "default", - "uri": "{{ url|full_url }}" - } - ] } ] -} +} \ No newline at end of file diff --git a/dojo/templates/notifications/msteams/test_added.tpl b/dojo/templates/notifications/msteams/test_added.tpl index 7b22e5d1314..e43bec7698f 100644 --- a/dojo/templates/notifications/msteams/test_added.tpl +++ b/dojo/templates/notifications/msteams/test_added.tpl @@ -1,48 +1,103 @@ -{% load i18n %} -{% load display_tags %} -{% url 'view_test' test.id as test_url %} +{% load i18n %}{% load display_tags %}{% url 'view_test' test.id as url %} { - "@context": "https://schema.org/extensions", - "@type": "MessageCard", - "title": "{% trans "Test added" %}", - "summary": "{% trans "Test added" %}", - "sections": [ + "type": "message", + "attachments": [ { - "activityTitle": "DefectDojo", - "activityImage": "https://raw.githubusercontent.com/DefectDojo/django-DefectDojo/master/dojo/static/dojo/img/chop.png", - "text": "{% trans "A new test has been added" %}.", - "facts": [ - { - "name": "{% trans "Product" %}:", - "value": "{{ engagement.product.name }}" - }, - { - "name": "{% trans "Engagement" %}:", - "value": "{{ engagement.name }}" - }, - { - "name": "{% trans "Test" %}:", - "value": "{{ test }}" - } - ] - } - {% if system_settings.disclaimer_notifications and system_settings.disclaimer_notifications.strip %} - ,{ - "activityTitle": "{% trans "Disclaimer" %}", - "text": "{{ system_settings.disclaimer_notifications }}" + "contentType": "application/vnd.microsoft.card.adaptive", + "content": { + "$schema": "http://adaptivecards.io/schemas/adaptive-card.json", + "type": "AdaptiveCard", + "version": "1.4", + "body": [ + { + "type": "ColumnSet", + "columns": [ + { + "type": "Column", + "width": "auto", + "items": [ + { + "type": "Image", + "url": "https://raw.githubusercontent.com/DefectDojo/django-DefectDojo/master/dojo/static/dojo/img/chop.png", + "size": "Small" + } + ] + }, + { + "type": "Column", + "width": "stretch", + "items": [ + { + "type": "TextBlock", + "text": "DefectDojo", + "weight": "Bolder", + "size": "Medium" + }, + { + "type": "TextBlock", + "text": "{% trans 'Test Added' %}", + "weight": "Bolder", + "size": "Large", + "color": "Accent" + } + ] + } + ] + }, + { + "type": "TextBlock", + "text": "{% trans 'A new test' %} [{{ test }}]({{ url|full_url }}) {% trans 'has been added.' %}", + "wrap": true, + "spacing": "Medium" + }, + { + "type": "FactSet", + "facts": [ + { + "title": "{% trans 'Product' %}:", + "value": "{{ test.engagement.product.name }}" + }, + { + "title": "{% trans 'Engagement' %}:", + "value": "{{ test.engagement.name }}" + }, + { + "title": "{% trans 'Test' %}:", + "value": "{{ test }}" + }, + { + "title": "{% trans 'Scan Type' %}:", + "value": "{{ test.test_type.name }}" + } + ], + "spacing": "Medium" + }{% if system_settings.disclaimer_notifications and system_settings.disclaimer_notifications.strip %}, + { + "type": "Container", + "style": "attention", + "items": [ + { + "type": "TextBlock", + "text": "{% trans 'Disclaimer' %}", + "weight": "Bolder" + }, + { + "type": "TextBlock", + "text": "{{ system_settings.disclaimer_notifications }}", + "wrap": true + } + ], + "spacing": "Medium" + }{% endif %} + ], + "actions": [ + { + "type": "Action.OpenUrl", + "title": "{% trans 'View Test' %}", + "url": "{{ url|full_url }}" + } + ] } - {% endif %} - ], - "potentialAction": [ - { - "@type": "OpenUri", - "name": "{% trans "View Test" %}", - "targets": [ - { - "os": "default", - "uri": "{{ url|full_url }}" - } - ] } ] -} +} \ No newline at end of file diff --git a/dojo/templates/notifications/msteams/upcoming_engagement.tpl b/dojo/templates/notifications/msteams/upcoming_engagement.tpl index 9cb22d025d7..eaaa10e7a09 100644 --- a/dojo/templates/notifications/msteams/upcoming_engagement.tpl +++ b/dojo/templates/notifications/msteams/upcoming_engagement.tpl @@ -1,52 +1,103 @@ -{% load i18n %} -{% load display_tags %} -{% url 'view_engagement' engagement.id as engagement_url %} +{% load i18n %}{% load display_tags %}{% url 'view_engagement' engagement.id as engagement_url %} { - "@context": "https://schema.org/extensions", - "@type": "MessageCard", - "title": "{% trans "Engagement is starting" %}", - "summary": "{% trans "Engagement is starting" %}", - "sections": [ + "type": "message", + "attachments": [ { - "activityTitle": "DefectDojo", - "activityImage": "https://raw.githubusercontent.com/DefectDojo/django-DefectDojo/master/dojo/static/dojo/img/chop.png", - "text": "{% trans "An Engagement is starting" %}.", - "facts": [ - { - "name": "{% trans "Product" %}:", - "value": "{{ engagement.product.name }}" - }, - { - "name": "{% trans "Engagement" %}:", - "value": "{{ engagement.name }}" - }, - { - "name": "{% trans "Start date" %}:", - "value": "{{ engagement.target_start }}" - }, - { - "name": "{% trans "End date" %}:", - "value": "{{ engagement.target_end }}" - } - ] - } - {% if system_settings.disclaimer_notifications and system_settings.disclaimer_notifications.strip %} - ,{ - "activityTitle": "{% trans "Disclaimer" %}", - "text": "{{ system_settings.disclaimer_notifications }}" + "contentType": "application/vnd.microsoft.card.adaptive", + "content": { + "$schema": "http://adaptivecards.io/schemas/adaptive-card.json", + "type": "AdaptiveCard", + "version": "1.4", + "body": [ + { + "type": "ColumnSet", + "columns": [ + { + "type": "Column", + "width": "auto", + "items": [ + { + "type": "Image", + "url": "https://raw.githubusercontent.com/DefectDojo/django-DefectDojo/master/dojo/static/dojo/img/chop.png", + "size": "Small" + } + ] + }, + { + "type": "Column", + "width": "stretch", + "items": [ + { + "type": "TextBlock", + "text": "DefectDojo", + "weight": "Bolder", + "size": "Medium" + }, + { + "type": "TextBlock", + "text": "{% trans 'Upcoming Engagement' %}", + "weight": "Bolder", + "size": "Large", + "color": "Accent" + } + ] + } + ] + }, + { + "type": "TextBlock", + "text": "{% trans 'Engagement' %} [{{ engagement.name }}]({{ engagement_url|full_url }}) {% blocktranslate with start_date=engagement.target_start %}is starting on {{ start_date }}.{% endblocktranslate %}", + "wrap": true, + "spacing": "Medium" + }, + { + "type": "FactSet", + "facts": [ + { + "title": "{% trans 'Product' %}:", + "value": "{{ engagement.product.name }}" + }, + { + "title": "{% trans 'Engagement' %}:", + "value": "{{ engagement.name }}" + }, + { + "title": "{% trans 'Start date' %}:", + "value": "{{ engagement.target_start }}" + }, + { + "title": "{% trans 'End date' %}:", + "value": "{{ engagement.target_end }}" + } + ], + "spacing": "Medium" + }{% if system_settings.disclaimer_notifications and system_settings.disclaimer_notifications.strip %}, + { + "type": "Container", + "style": "attention", + "items": [ + { + "type": "TextBlock", + "text": "{% trans 'Disclaimer' %}", + "weight": "Bolder" + }, + { + "type": "TextBlock", + "text": "{{ system_settings.disclaimer_notifications }}", + "wrap": true + } + ], + "spacing": "Medium" + }{% endif %} + ], + "actions": [ + { + "type": "Action.OpenUrl", + "title": "{% trans 'View Engagement' %}", + "url": "{{ engagement_url|full_url }}" + } + ] } - {% endif %} - ], - "potentialAction": [ - { - "@type": "OpenUri", - "name": "{% trans "View Engagement" %}", - "targets": [ - { - "os": "default", - "uri": "{{ url|full_url }}" - } - ] } ] -} +} \ No newline at end of file diff --git a/dojo/templates/notifications/msteams/user_mentioned.tpl b/dojo/templates/notifications/msteams/user_mentioned.tpl index fb27ea5ad1b..ed8f38ee80c 100644 --- a/dojo/templates/notifications/msteams/user_mentioned.tpl +++ b/dojo/templates/notifications/msteams/user_mentioned.tpl @@ -1,47 +1,107 @@ -{% load i18n %} -{% load display_tags %} +{% comment %} +NOTE: This template is currently NOT USED in practice because: +- review_requested notifications are sent to specific recipients (the reviewers) +- Microsoft Teams only supports system-wide notifications, not user-specific ones +- The notification system processes recipient-specific notifications using personal settings +- Since Teams doesn't have individual user channels/usernames (unlike email or Slack), + these notifications will only work for channels that support user-specific delivery +{% endcomment %} +{% load i18n %}{% load display_tags %} { - "@context": "https://schema.org/extensions", - "@type": "MessageCard", - "title": "{% trans "User Mentioned" %}", - "summary": "{% trans "User Mentioned" %}", - "sections": [ + "type": "message", + "attachments": [ { - "activityTitle": "DefectDojo", - "activityImage": "https://raw.githubusercontent.com/DefectDojo/django-DefectDojo/master/dojo/static/dojo/img/chop.png", - "text": "{% trans "A user has been mentioned" %}.", - "facts": [ - { - "name": "{% trans "User" %}:", - "value": "{{ user }}" - }, - { - "name": "{% trans "Section" %}:", - "value": "{{ section }}" - }, - { - "name": "{% trans "note" %}:", - "value": "{{ note }}" - } - ] - } - {% if system_settings.disclaimer_notifications and system_settings.disclaimer_notifications.strip %} - ,{ - "activityTitle": "{% trans "Disclaimer" %}", - "text": "{{ system_settings.disclaimer_notifications }}" + "contentType": "application/vnd.microsoft.card.adaptive", + "content": { + "$schema": "http://adaptivecards.io/schemas/adaptive-card.json", + "type": "AdaptiveCard", + "version": "1.4", + "body": [ + { + "type": "ColumnSet", + "columns": [ + { + "type": "Column", + "width": "auto", + "items": [ + { + "type": "Image", + "url": "https://raw.githubusercontent.com/DefectDojo/django-DefectDojo/master/dojo/static/dojo/img/chop.png", + "size": "Small" + } + ] + }, + { + "type": "Column", + "width": "stretch", + "items": [ + { + "type": "TextBlock", + "text": "DefectDojo", + "weight": "Bolder", + "size": "Medium" + }, + { + "type": "TextBlock", + "text": "{% trans 'User Mentioned' %}", + "weight": "Bolder", + "size": "Large", + "color": "Accent" + } + ] + } + ] + }, + { + "type": "TextBlock", + "text": "{% trans 'User' %} {{ user }} {% trans 'mentioned you in' %} {{ section }}.", + "wrap": true, + "spacing": "Medium" + }, + { + "type": "FactSet", + "facts": [ + { + "title": "{% trans 'User' %}:", + "value": "{{ user }}" + }, + { + "title": "{% trans 'Section' %}:", + "value": "{{ section }}" + }, + { + "title": "{% trans 'Note' %}:", + "value": "{{ note }}" + } + ], + "spacing": "Medium" + }{% if system_settings.disclaimer_notifications and system_settings.disclaimer_notifications.strip %}, + { + "type": "Container", + "style": "attention", + "items": [ + { + "type": "TextBlock", + "text": "{% trans 'Disclaimer' %}", + "weight": "Bolder" + }, + { + "type": "TextBlock", + "text": "{{ system_settings.disclaimer_notifications }}", + "wrap": true + } + ], + "spacing": "Medium" + }{% endif %} + ], + "actions": [ + { + "type": "Action.OpenUrl", + "title": "{% trans 'View Details' %}", + "url": "{{ url|full_url }}" + } + ] } - {% endif %} - ], - "potentialAction": [ - { - "@type": "OpenUri", - "name": "{% trans "View" %}", - "targets": [ - { - "os": "default", - "uri": "{{ url|full_url }}" - } - ] } ] -} +} \ No newline at end of file diff --git a/dojo/templates/notifications/slack/scan_added_empty.tpl b/dojo/templates/notifications/slack/scan_added_empty.tpl index 4efb291e005..0e129371211 120000 --- a/dojo/templates/notifications/slack/scan_added_empty.tpl +++ b/dojo/templates/notifications/slack/scan_added_empty.tpl @@ -1 +1 @@ -scan_added.tpl \ No newline at end of file +{% include "notifications/slack/scan_added.tpl" %} \ No newline at end of file From a47e2b600f72d1aa49dae8ca9d3327f2470ed7b1 Mon Sep 17 00:00:00 2001 From: manuelsommer <47991713+manuel-sommer@users.noreply.github.com> Date: Thu, 4 Sep 2025 04:32:10 +0200 Subject: [PATCH 010/154] Ruff: Add PLW0603 (#13097) --- ruff.toml | 2 +- tests/base_test_class.py | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/ruff.toml b/ruff.toml index 4ce2788a26d..8fe6abd4e1c 100644 --- a/ruff.toml +++ b/ruff.toml @@ -85,7 +85,7 @@ select = [ "PLC01", "PLC02", "PLC0414", "PLC18", "PLC24", "PLC28", "PLC3", "PLE", "PLR01", "PLR02", "PLR04", "PLR0915", "PLR1711", "PLR1704", "PLR1714", "PLR1716", "PLR172", "PLR173", "PLR2044", "PLR5", "PLR6104", "PLR6201", - "PLW01", "PLW02", "PLW04", "PLW0602", "PLW0604", "PLW07", "PLW1", "PLW2", "PLW3", + "PLW01", "PLW02", "PLW04", "PLW0602", "PLW0603", "PLW0604", "PLW07", "PLW1", "PLW2", "PLW3", "UP", "FURB", "RUF", diff --git a/tests/base_test_class.py b/tests/base_test_class.py index 939c72265eb..69cbf140a1b 100644 --- a/tests/base_test_class.py +++ b/tests/base_test_class.py @@ -57,12 +57,11 @@ def setUpClass(cls): # Path for automatic downloads, mapped to the media path cls.export_path = "/app" - global dd_driver + global dd_driver # noqa: PLW0603 global variables are dirty, but in unit tests scenario's like these they are acceptable if not dd_driver: # setupModule and tearDownModule are not working in our scenario, so for now we use setupClass and a global variable - # global variables are dirty, but in unit tests scenario's like these they are acceptable logger.info(f"launching browser for: {cls.__name__}") - global dd_driver_options + global dd_driver_options # noqa: PLW0603 global variables are dirty, but in unit tests scenario's like these they are acceptable dd_driver_options = Options() # headless means no UI, if you want to see what is happening remove headless. Adding detach will leave the window open after the test From 1507f9568453c6f68ba706ecd89dadb2f45ff802 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 4 Sep 2025 09:54:58 -0600 Subject: [PATCH 011/154] chore(deps): update actions/labeler action from v5.0.0 to v6 (.github/workflows/pr-labeler.yml) (#13102) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/pr-labeler.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pr-labeler.yml b/.github/workflows/pr-labeler.yml index cde6795db05..dfec17f1975 100644 --- a/.github/workflows/pr-labeler.yml +++ b/.github/workflows/pr-labeler.yml @@ -15,7 +15,7 @@ jobs: name: "Autolabeler" runs-on: ubuntu-latest steps: - - uses: actions/labeler@8558fd74291d67161a8a78ce36a881fa63b766a9 # v5.0.0 + - uses: actions/labeler@f1a63e87db0c6baf19c5713083f8d00d789ca184 # v6.0.0 with: repo-token: "${{ secrets.GITHUB_TOKEN }}" sync-labels: true From 5d2a1e6c61d63e82e9dc4db7d36a7c1fa7a75123 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 4 Sep 2025 09:55:42 -0600 Subject: [PATCH 012/154] chore(deps): update actions/setup-node action from v4.4.0 to v5 (.github/workflows/validate_docs_build.yml) (#13103) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/gh-pages.yml | 2 +- .github/workflows/validate_docs_build.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/gh-pages.yml b/.github/workflows/gh-pages.yml index 301047bbefc..37ce7dafa9b 100644 --- a/.github/workflows/gh-pages.yml +++ b/.github/workflows/gh-pages.yml @@ -19,7 +19,7 @@ jobs: extended: true - name: Setup Node - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 + uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0 with: node-version: '22.19.0' diff --git a/.github/workflows/validate_docs_build.yml b/.github/workflows/validate_docs_build.yml index 8fb3c8c8fca..d17c714f8e9 100644 --- a/.github/workflows/validate_docs_build.yml +++ b/.github/workflows/validate_docs_build.yml @@ -16,7 +16,7 @@ jobs: extended: true - name: Setup Node - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 + uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0 with: node-version: '22.19.0' From 888b6e22fed2a11af4a08cc935d20501b57baae4 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 4 Sep 2025 09:58:05 -0600 Subject: [PATCH 013/154] Update actions/setup-python action from v5.6.0 to v6 (.github/workflows/test-helm-chart.yml) (#13107) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/test-helm-chart.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test-helm-chart.yml b/.github/workflows/test-helm-chart.yml index 91a14845391..87cac1bd764 100644 --- a/.github/workflows/test-helm-chart.yml +++ b/.github/workflows/test-helm-chart.yml @@ -22,7 +22,7 @@ jobs: - name: Set up Helm uses: azure/setup-helm@1a275c3b69536ee54be43f2070a358922e12c8d4 # v4.3.1 - - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 + - uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 with: python-version: 3.13 From 8303d5c790227af838fa1798209290f271d6b7d4 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 4 Sep 2025 09:58:29 -0600 Subject: [PATCH 014/154] Update actions/stale action from v9.1.0 to v10 (.github/workflows/close-stale.yml) (#13108) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/close-stale.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/close-stale.yml b/.github/workflows/close-stale.yml index 491ecb5b082..85abc5a5044 100644 --- a/.github/workflows/close-stale.yml +++ b/.github/workflows/close-stale.yml @@ -16,7 +16,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Close stale issues and PRs - uses: actions/stale@5bef64f19d7facfb25b37b414482c7164d639639 # v9.1.0 + uses: actions/stale@3a9db7e6a41a89f618792c92c0e97cc736e1b13f # v10.0.0 with: # Disable automatic stale marking - only close manually labeled items days-before-stale: -1 From 7db9e6b8da8bcddb20f56906a54d3a77a5fc61fe Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 4 Sep 2025 10:05:48 -0600 Subject: [PATCH 015/154] Bump boto3 from 1.40.21 to 1.40.23 (#13110) Bumps [boto3](https://github.com/boto/boto3) from 1.40.21 to 1.40.23. - [Release notes](https://github.com/boto/boto3/releases) - [Commits](https://github.com/boto/boto3/compare/1.40.21...1.40.23) --- updated-dependencies: - dependency-name: boto3 dependency-version: 1.40.23 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 8f118fdce13..3fb55552f47 100644 --- a/requirements.txt +++ b/requirements.txt @@ -68,7 +68,7 @@ django-ratelimit==4.1.0 argon2-cffi==25.1.0 blackduck==1.1.3 pycurl==7.45.6 # Required for Celery Broker AWS (SQS) support -boto3==1.40.21 # Required for Celery Broker AWS (SQS) support +boto3==1.40.23 # Required for Celery Broker AWS (SQS) support netaddr==1.3.0 vulners==2.3.7 fontawesomefree==6.6.0 From ff4b9a84703ddee592b8cdec94bffdc795197811 Mon Sep 17 00:00:00 2001 From: valentijnscholten Date: Thu, 4 Sep 2025 21:45:30 +0200 Subject: [PATCH 016/154] Import performance: reduce number of `finding.save()` calls (#12900) * test cases: fix caching of system settings * fix tests * fix caching for github * fix caching for github * simplify cache loading * post process only when needed * set tags on (re)import * rebase set tags * reduce save with options * update counts, reduce saves with options * importers: do not save again, but postprocess directly * update counts * optimize hash_code setting * fix counts * set hash code for new findings in reimport * make smaller second save work * make smaller second save work - add no_options * update query counts * improve we_want_async decorator * test performance: force async * fix async stuff in perf test * fix async stuff in perf test * fix async stuff in perf test * update counts * remove logging * perf3b: compute hash_code on first save * fix cve for reimport * ruff * fix no async * Merge remote-tracking branch 'upstream/dev' into perf3-reduce-saves --- dojo/decorators.py | 8 +++- dojo/importers/base_importer.py | 17 +++++--- dojo/importers/default_importer.py | 17 +++++--- dojo/importers/default_reimporter.py | 30 +++++++++---- unittests/test_import_reimport.py | 9 +++- unittests/test_importers_performance.py | 57 +++++++++++++++++-------- 6 files changed, 97 insertions(+), 41 deletions(-) diff --git a/dojo/decorators.py b/dojo/decorators.py index 40174524fb7..791e76cb8ab 100644 --- a/dojo/decorators.py +++ b/dojo/decorators.py @@ -64,13 +64,17 @@ def we_want_async(*args, func=None, **kwargs): return False user = kwargs.get("async_user", get_current_user()) - logger.debug("user: %s", user) + logger.debug("async user: %s", user) + + if not user: + logger.debug("dojo_async_task %s: no current user, running task in the background", func) + return True if Dojo_User.wants_block_execution(user): logger.debug("dojo_async_task %s: running task in the foreground as block_execution is set to True for %s", func, user) return False - logger.debug("dojo_async_task %s: no current user, running task in the background", func) + logger.debug("dojo_async_task %s: running task in the background as user has not set block_execution to True for %s", func, user) return True diff --git a/dojo/importers/base_importer.py b/dojo/importers/base_importer.py index 19513566c91..503610b5426 100644 --- a/dojo/importers/base_importer.py +++ b/dojo/importers/base_importer.py @@ -682,14 +682,11 @@ def process_endpoints( logger.debug("endpoints_to_add: %s", endpoints_to_add) self.endpoint_manager.chunk_endpoints_and_disperse(finding, endpoints_to_add) - def process_vulnerability_ids( + def process_cve( self, finding: Finding, ) -> Finding: - """ - Parse the `unsaved_vulnerability_ids` field from findings after they are parsed - to create `Vulnerability_Id` objects with the finding associated correctly - """ + """Ensure cve is set from the unsaved_vulnerability_ids field, or vice versa.""" # Synchronize the cve field with the unsaved_vulnerability_ids # We do this to be as flexible as possible to handle the fields until # the cve field is not needed anymore and can be removed. @@ -703,6 +700,16 @@ def process_vulnerability_ids( # If there is no list, make one with the value of the cve field finding.unsaved_vulnerability_ids = [finding.cve] + return finding + + def process_vulnerability_ids( + self, + finding: Finding, + ) -> Finding: + """ + Parse the `unsaved_vulnerability_ids` field from findings after they are parsed + to create `Vulnerability_Id` objects with the finding associated correctly + """ if finding.unsaved_vulnerability_ids: # Remove old vulnerability ids - keeping this call only because of flake8 Vulnerability_Id.objects.filter(finding=finding).delete() diff --git a/dojo/importers/default_importer.py b/dojo/importers/default_importer.py index 14456954fe8..ed4106971b7 100644 --- a/dojo/importers/default_importer.py +++ b/dojo/importers/default_importer.py @@ -183,7 +183,7 @@ def process_findings( unsaved_finding.reporter = self.user unsaved_finding.last_reviewed_by = self.user unsaved_finding.last_reviewed = self.now - logger.debug("process_parsed_findings: active from report: %s, verified from report: %s", unsaved_finding.active, unsaved_finding.verified) + logger.debug("process_parsed_findings: unique_id_from_tool: %s, hash_code: %s, active from report: %s, verified from report: %s", unsaved_finding.unique_id_from_tool, unsaved_finding.hash_code, unsaved_finding.active, unsaved_finding.verified) # indicates an override. Otherwise, do not change the value of unsaved_finding.active if self.active is not None: unsaved_finding.active = self.active @@ -198,8 +198,13 @@ def process_findings( # Force parsers to use unsaved_tags (stored in below after saving) unsaved_finding.tags = None - # postprocessing will be done on next save. + finding = self.process_cve(unsaved_finding) + # Calculate hash_code before saving based on unsaved_endpoints and unsaved_vulnerability_ids + finding.set_hash_code(True) + + # postprocessing will be done after processing related fields like endpoints, vulnerability ids, etc. unsaved_finding.save_no_options() + finding = unsaved_finding # Determine how the finding should be grouped self.process_finding_groups( @@ -218,11 +223,11 @@ def process_findings( finding = self.process_vulnerability_ids(finding) # Categorize this finding as a new one new_findings.append(finding) + # all data is already saved on the finding, we only need to trigger post processing + # to avoid pushing a finding group multiple times, we push those outside of the loop - if self.findings_groups_enabled and self.group_by: - finding.save() - else: - finding.save(push_to_jira=self.push_to_jira) + push_to_jira = self.push_to_jira and (not self.findings_groups_enabled or not self.group_by) + finding_helper.post_process_finding_save(finding, dedupe_option=True, rules_option=True, product_grading_option=True, issue_updater_option=True, push_to_jira=push_to_jira) for (group_name, findings) in group_names_to_findings_dict.items(): finding_helper.add_findings_to_auto_group( diff --git a/dojo/importers/default_reimporter.py b/dojo/importers/default_reimporter.py index 4fd9065af90..455730a647b 100644 --- a/dojo/importers/default_reimporter.py +++ b/dojo/importers/default_reimporter.py @@ -236,12 +236,11 @@ def process_findings( finding, unsaved_finding, ) - # finding = new finding or existing finding still in the upload report + # all data is already saved on the finding, we only need to trigger post processing + # to avoid pushing a finding group multiple times, we push those outside of the loop - if self.findings_groups_enabled and self.group_by: - finding.save() - else: - finding.save(push_to_jira=self.push_to_jira) + push_to_jira = self.push_to_jira and (not self.findings_groups_enabled or not self.group_by) + finding_helper.post_process_finding_save(finding, dedupe_option=True, rules_option=True, product_grading_option=True, issue_updater_option=True, push_to_jira=push_to_jira) self.to_mitigate = (set(self.original_items) - set(self.reactivated_items) - set(self.unchanged_items)) # due to #3958 we can have duplicates inside the same report @@ -336,11 +335,14 @@ def match_new_finding_to_existing_finding( hash_code=unsaved_finding.hash_code, ).exclude(hash_code=None).order_by("id") if self.deduplication_algorithm == "unique_id_from_tool": + deduplicationLogger.debug(f"unique_id_from_tool: {unsaved_finding.unique_id_from_tool}") return Finding.objects.filter( test=self.test, unique_id_from_tool=unsaved_finding.unique_id_from_tool, ).exclude(unique_id_from_tool=None).order_by("id") if self.deduplication_algorithm == "unique_id_from_tool_or_hash_code": + deduplicationLogger.debug(f"unique_id_from_tool: {unsaved_finding.unique_id_from_tool}") + deduplicationLogger.debug(f"hash_code: {unsaved_finding.hash_code}") query = Finding.objects.filter( Q(test=self.test), (Q(hash_code__isnull=False) & Q(hash_code=unsaved_finding.hash_code)) @@ -500,10 +502,13 @@ def process_matched_mitigated_finding( if existing_finding.get_sla_configuration().restart_sla_on_reactivation: # restart the sla start date to the current date, finding.save() will set new sla_expiration_date existing_finding.sla_start_date = self.now + existing_finding = self.process_cve(existing_finding) + if existing_finding.get_sla_configuration().restart_sla_on_reactivation: + # restart the sla start date to the current date, finding.save() will set new sla_expiration_date + existing_finding.sla_start_date = self.now + # don't dedupe before endpoints are added, postprocessing will be done on next save (in calling method) + existing_finding.save_no_options() - existing_finding.save(dedupe_option=False) - # don't dedupe before endpoints are added - existing_finding.save(dedupe_option=False) note = Notes(entry=f"Re-activated by {self.scan_type} re-upload.", author=self.user) note.save() endpoint_statuses = existing_finding.status_finding.exclude( @@ -551,6 +556,9 @@ def process_matched_active_finding( existing_finding.active = False if self.verified is not None: existing_finding.verified = self.verified + existing_finding = self.process_cve(existing_finding) + existing_finding.save_no_options() + elif unsaved_finding.risk_accepted or unsaved_finding.false_p or unsaved_finding.out_of_scope: logger.debug("Reimported mitigated item matches a finding that is currently open, closing.") logger.debug( @@ -563,6 +571,8 @@ def process_matched_active_finding( existing_finding.active = False if self.verified is not None: existing_finding.verified = self.verified + existing_finding = self.process_cve(existing_finding) + existing_finding.save_no_options() else: # if finding is the same but list of affected was changed, finding is marked as unchanged. This is a known issue self.unchanged_items.append(existing_finding) @@ -597,6 +607,8 @@ def process_finding_that_was_not_matched( # scan_date was provided, override value from parser if self.scan_date_override: unsaved_finding.date = self.scan_date.date() + unsaved_finding = self.process_cve(unsaved_finding) + # Hash code is already calculated earlier as it's the primary matching criteria for reimport # Save it. Don't dedupe before endpoints are added. unsaved_finding.save_no_options() finding = unsaved_finding @@ -640,7 +652,7 @@ def finding_post_processing( # Process vulnerability IDs if finding_from_report.unsaved_vulnerability_ids: finding.unsaved_vulnerability_ids = finding_from_report.unsaved_vulnerability_ids - + # legacy cve field has already been processed/set earlier return self.process_vulnerability_ids(finding) def process_groups_for_all_findings( diff --git a/unittests/test_import_reimport.py b/unittests/test_import_reimport.py index e9d28e4c46e..98ce581626c 100644 --- a/unittests/test_import_reimport.py +++ b/unittests/test_import_reimport.py @@ -658,7 +658,8 @@ def test_import_veracode_reimport_veracode_same_hash_code_different_unique_id(se def test_import_veracode_reimport_veracode_same_unique_id_different_hash_code(self): logger.debug("reimporting report with one finding having same unique_id_from_tool but different hash_code, verified=False") - import_veracode_many_findings = self.import_scan_with_params(self.veracode_many_findings, scan_type=self.scan_type_veracode) + with assertTestImportModelsCreated(self, imports=1, created=4, affected_findings=4, closed=0, reactivated=0, untouched=0): + import_veracode_many_findings = self.import_scan_with_params(self.veracode_many_findings, scan_type=self.scan_type_veracode) test_id = import_veracode_many_findings["test"] @@ -1748,6 +1749,9 @@ def __init__(self, *args, **kwargs): def setUp(self): testuser = User.objects.get(username="admin") + testuser.usercontactinfo.block_execution = True + testuser.usercontactinfo.save() + token = Token.objects.get(user=testuser) self.client = APIClient() self.client.credentials(HTTP_AUTHORIZATION="Token " + token.key) @@ -2023,6 +2027,9 @@ def __init__(self, *args, **kwargs): def setUp(self): # still using the API to verify results testuser = User.objects.get(username="admin") + testuser.usercontactinfo.block_execution = True + testuser.usercontactinfo.save() + token = Token.objects.get(user=testuser) self.client = APIClient() self.client.credentials(HTTP_AUTHORIZATION="Token " + token.key) diff --git a/unittests/test_importers_performance.py b/unittests/test_importers_performance.py index 4f316d7350a..a629b7f97c3 100644 --- a/unittests/test_importers_performance.py +++ b/unittests/test_importers_performance.py @@ -1,6 +1,5 @@ import logging from contextlib import contextmanager -from unittest.mock import patch from crum import impersonate from django.contrib.contenttypes.models import ContentType @@ -20,12 +19,15 @@ Product_Type, Test, User, + UserContactInfo, ) from .dojo_test_case import DojoTestCase, get_unit_tests_scans_path +logging.basicConfig(level=logging.DEBUG) logger = logging.getLogger(__name__) + STACK_HAWK_FILENAME = get_unit_tests_scans_path("stackhawk") / "stackhawk_many_vul_without_duplicated_findings.json" STACK_HAWK_SUBSET_FILENAME = get_unit_tests_scans_path("stackhawk") / "stackhawk_many_vul_without_duplicated_findings_subset.json" STACK_HAWK_SCAN_TYPE = "StackHawk HawkScan" @@ -39,6 +41,9 @@ class TestDojoImporterPerformance(DojoTestCase): def setUp(self): super().setUp() + testuser = User.objects.create(username="admin") + UserContactInfo.objects.create(user=testuser, block_execution=False) + self.system_settings(enable_webhooks_notifications=False) self.system_settings(enable_product_grade=False) self.system_settings(enable_github=False) @@ -67,6 +72,14 @@ def assertNumAsyncTask(self, num): ) raise self.failureException(msg) + tasks = dojo_async_task_counter.get_tasks() + tasks_str = "\n".join(str(task) for task in tasks) + msg = ( + f"Correct number of {num} celery tasks were created.\n" + f"Tasks created:\n{tasks_str}" + ) + logger.debug(msg) + def import_reimport_performance(self, expected_num_queries1, expected_num_async_tasks1, expected_num_queries2, expected_num_async_tasks2, expected_num_queries3, expected_num_async_tasks3): """ Log output can be quite large as when the assertNumQueries fails, all queries are printed. @@ -158,18 +171,22 @@ def import_reimport_performance(self, expected_num_queries1, expected_num_async_ reimporter = DefaultReImporter(**reimport_options) test, _, _len_new_findings, _len_closed_findings, _, _, _ = reimporter.process_scan(scan) - def test_import_reimport_reimport_performance(self): + # patch the we_want_async decorator to always return True so we don't depend on block_execution flag shenanigans + # @patch("dojo.decorators.we_want_async", return_value=True) + # def test_import_reimport_reimport_performance_async(self, mock): + def test_import_reimport_reimport_performance_async(self): self.import_reimport_performance( - expected_num_queries1=712, + expected_num_queries1=682, expected_num_async_tasks1=10, - expected_num_queries2=656, + expected_num_queries2=610, expected_num_async_tasks2=22, - expected_num_queries3=332, + expected_num_queries3=292, expected_num_async_tasks3=20, ) - @patch("dojo.decorators.we_want_async", return_value=False) - def test_import_reimport_reimport_performance_no_async(self, mock): + # @patch("dojo.decorators.we_want_async", return_value=False) + # def test_import_reimport_reimport_performance_no_async(self, mock): + def test_import_reimport_reimport_performance_no_async(self): """ This test checks the performance of the importers when they are run in sync mode. The reason for this is that we also want to be aware of when a PR affects the number of queries @@ -177,17 +194,21 @@ def test_import_reimport_reimport_performance_no_async(self, mock): The impersonate context manager above does not work as expected for disabling async, so we patch the we_want_async decorator to always return False. """ + testuser = User.objects.get(username="admin") + testuser.usercontactinfo.block_execution = True + testuser.usercontactinfo.save() self.import_reimport_performance( - expected_num_queries1=712, + expected_num_queries1=682, expected_num_async_tasks1=10, - expected_num_queries2=656, + expected_num_queries2=615, expected_num_async_tasks2=22, - expected_num_queries3=332, + expected_num_queries3=297, expected_num_async_tasks3=20, ) - @patch("dojo.decorators.we_want_async", return_value=False) - def test_import_reimport_reimport_performance_no_async_with_product_grading(self, mock): + # @patch("dojo.decorators.we_want_async", return_value=False) + # def test_import_reimport_reimport_performance_no_async_with_product_grading(self, mock): + def test_import_reimport_reimport_performance_no_async_with_product_grading(self): """ This test checks the performance of the importers when they are run in sync mode. The reason for this is that we also want to be aware of when a PR affects the number of queries @@ -195,16 +216,16 @@ def test_import_reimport_reimport_performance_no_async_with_product_grading(self The impersonate context manager above does not work as expected for disabling async, so we patch the we_want_async decorator to always return False. """ + testuser = User.objects.get(username="admin") + testuser.usercontactinfo.block_execution = True + testuser.usercontactinfo.save() self.system_settings(enable_product_grade=True) - # Refresh the cache with the new settings - from dojo.middleware import DojoSytemSettingsMiddleware - DojoSytemSettingsMiddleware.load() self.import_reimport_performance( - expected_num_queries1=732, + expected_num_queries1=702, expected_num_async_tasks1=15, - expected_num_queries2=686, + expected_num_queries2=645, expected_num_async_tasks2=28, - expected_num_queries3=357, + expected_num_queries3=322, expected_num_async_tasks3=25, ) From b6eda47a1bc9f830b2d8b3910421e0ca2f2904d0 Mon Sep 17 00:00:00 2001 From: kiblik <5609770+kiblik@users.noreply.github.com> Date: Thu, 4 Sep 2025 21:45:55 +0200 Subject: [PATCH 017/154] fix(product): Drop import_scan_results_prod (#13041) --- dojo/product/views.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/dojo/product/views.py b/dojo/product/views.py index d8b16a13167..66d09475ef5 100644 --- a/dojo/product/views.py +++ b/dojo/product/views.py @@ -892,12 +892,6 @@ def prefetch_for_view_engagements(engagements, recent_test_day_count): return engagements -# Authorization is within the import_scan_results method -def import_scan_results_prod(request, pid=None): - from dojo.engagement.views import import_scan_results - return import_scan_results(request, pid=pid) - - def new_product(request, ptid=None): if get_authorized_product_types(Permissions.Product_Type_Add_Product).count() == 0: raise PermissionDenied From 3d71d77c7a429c764b10f79e004b9b5751a788f5 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 5 Sep 2025 08:28:11 -0600 Subject: [PATCH 018/154] chore(deps): update actions/github-script action from v7.0.1 to v7.1.0 (.github/workflows/release-3-master-into-dev.yml) (#13111) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/release-1-create-pr.yml | 2 +- .github/workflows/release-3-master-into-dev.yml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/release-1-create-pr.yml b/.github/workflows/release-1-create-pr.yml index dfab71f7ab3..5aa66e8b36d 100644 --- a/.github/workflows/release-1-create-pr.yml +++ b/.github/workflows/release-1-create-pr.yml @@ -97,7 +97,7 @@ jobs: branch: ${{ env.NEW_BRANCH }} - name: Create Pull Request - uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 + uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0 with: github-token: ${{ secrets.GITHUB_TOKEN }} script: | diff --git a/.github/workflows/release-3-master-into-dev.yml b/.github/workflows/release-3-master-into-dev.yml index bfca2a82091..034116764d2 100644 --- a/.github/workflows/release-3-master-into-dev.yml +++ b/.github/workflows/release-3-master-into-dev.yml @@ -84,7 +84,7 @@ jobs: branch: ${{ env.NEW_BRANCH }} - name: Create Pull Request - uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 + uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0 with: github-token: ${{ secrets.GITHUB_TOKEN }} script: | @@ -149,7 +149,7 @@ jobs: branch: ${{ env.NEW_BRANCH }} - name: Create Pull Request - uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 + uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0 with: github-token: ${{ secrets.GITHUB_TOKEN }} script: | From 774490f8841490fca50726fd564637effb67448d Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 5 Sep 2025 08:28:58 -0600 Subject: [PATCH 019/154] chore(deps): update actions/labeler action from v6.0.0 to v6.0.1 (.github/workflows/pr-labeler.yml) (#13113) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/pr-labeler.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pr-labeler.yml b/.github/workflows/pr-labeler.yml index dfec17f1975..824e7c14f44 100644 --- a/.github/workflows/pr-labeler.yml +++ b/.github/workflows/pr-labeler.yml @@ -15,7 +15,7 @@ jobs: name: "Autolabeler" runs-on: ubuntu-latest steps: - - uses: actions/labeler@f1a63e87db0c6baf19c5713083f8d00d789ca184 # v6.0.0 + - uses: actions/labeler@634933edcd8ababfe52f92936142cc22ac488b1b # v6.0.1 with: repo-token: "${{ secrets.GITHUB_TOKEN }}" sync-labels: true From 0ab3ff631f25c15f5beda0df0923d4074a8bbaa2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 5 Sep 2025 08:29:38 -0600 Subject: [PATCH 020/154] Bump boto3 from 1.40.23 to 1.40.24 (#13116) Bumps [boto3](https://github.com/boto/boto3) from 1.40.23 to 1.40.24. - [Release notes](https://github.com/boto/boto3/releases) - [Commits](https://github.com/boto/boto3/compare/1.40.23...1.40.24) --- updated-dependencies: - dependency-name: boto3 dependency-version: 1.40.24 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 3fb55552f47..a7d83eac828 100644 --- a/requirements.txt +++ b/requirements.txt @@ -68,7 +68,7 @@ django-ratelimit==4.1.0 argon2-cffi==25.1.0 blackduck==1.1.3 pycurl==7.45.6 # Required for Celery Broker AWS (SQS) support -boto3==1.40.23 # Required for Celery Broker AWS (SQS) support +boto3==1.40.24 # Required for Celery Broker AWS (SQS) support netaddr==1.3.0 vulners==2.3.7 fontawesomefree==6.6.0 From b9be612e544190b6074e600e28f99804482699f0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 5 Sep 2025 08:29:57 -0600 Subject: [PATCH 021/154] Bump ruff from 0.12.11 to 0.12.12 (#13117) Bumps [ruff](https://github.com/astral-sh/ruff) from 0.12.11 to 0.12.12. - [Release notes](https://github.com/astral-sh/ruff/releases) - [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md) - [Commits](https://github.com/astral-sh/ruff/compare/0.12.11...0.12.12) --- updated-dependencies: - dependency-name: ruff dependency-version: 0.12.12 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements-lint.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-lint.txt b/requirements-lint.txt index adb5ec90e5f..01081a79660 100644 --- a/requirements-lint.txt +++ b/requirements-lint.txt @@ -1 +1 @@ -ruff==0.12.11 \ No newline at end of file +ruff==0.12.12 \ No newline at end of file From aea8b201f8df949e02d3462afb80458125e139a4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 5 Sep 2025 08:30:25 -0600 Subject: [PATCH 022/154] Bump markdown from 3.8.2 to 3.9 (#13118) Bumps [markdown](https://github.com/Python-Markdown/markdown) from 3.8.2 to 3.9. - [Release notes](https://github.com/Python-Markdown/markdown/releases) - [Changelog](https://github.com/Python-Markdown/markdown/blob/master/docs/changelog.md) - [Commits](https://github.com/Python-Markdown/markdown/compare/3.8.2...3.9.0) --- updated-dependencies: - dependency-name: markdown dependency-version: '3.9' dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index a7d83eac828..a56d65109a2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -28,7 +28,7 @@ humanize==4.13.0 jira==3.8.0 PyGithub==2.7.0 lxml==6.0.1 -Markdown==3.8.2 +Markdown==3.9 openpyxl==3.1.5 Pillow==11.3.0 # required by django-imagekit psycopg[c]==3.2.9 From 8db1fd91ab9b8456e016cd69d72284c3fd2a94b8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 5 Sep 2025 08:56:40 -0600 Subject: [PATCH 023/154] Bump pygithub from 2.7.0 to 2.8.1 (#13100) Bumps [pygithub](https://github.com/pygithub/pygithub) from 2.7.0 to 2.8.1. - [Release notes](https://github.com/pygithub/pygithub/releases) - [Changelog](https://github.com/PyGithub/PyGithub/blob/main/doc/changes.rst) - [Commits](https://github.com/pygithub/pygithub/compare/v2.7.0...v2.8.1) --- updated-dependencies: - dependency-name: pygithub dependency-version: 2.8.1 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index a56d65109a2..e4ec5a03ad0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -26,7 +26,7 @@ djangorestframework==3.16.1 html2text==2025.4.15 humanize==4.13.0 jira==3.8.0 -PyGithub==2.7.0 +PyGithub==2.8.1 lxml==6.0.1 Markdown==3.9 openpyxl==3.1.5 From 79a292bc29ce31899872e07ffe8de51665434227 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 5 Sep 2025 09:12:54 -0600 Subject: [PATCH 024/154] chore(deps): update actions/github-script action from v7.1.0 to v8 (.github/workflows/release-3-master-into-dev.yml) (#13112) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/release-1-create-pr.yml | 2 +- .github/workflows/release-3-master-into-dev.yml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/release-1-create-pr.yml b/.github/workflows/release-1-create-pr.yml index 5aa66e8b36d..125d66536c4 100644 --- a/.github/workflows/release-1-create-pr.yml +++ b/.github/workflows/release-1-create-pr.yml @@ -97,7 +97,7 @@ jobs: branch: ${{ env.NEW_BRANCH }} - name: Create Pull Request - uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0 + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 with: github-token: ${{ secrets.GITHUB_TOKEN }} script: | diff --git a/.github/workflows/release-3-master-into-dev.yml b/.github/workflows/release-3-master-into-dev.yml index 034116764d2..18287e9f668 100644 --- a/.github/workflows/release-3-master-into-dev.yml +++ b/.github/workflows/release-3-master-into-dev.yml @@ -84,7 +84,7 @@ jobs: branch: ${{ env.NEW_BRANCH }} - name: Create Pull Request - uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0 + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 with: github-token: ${{ secrets.GITHUB_TOKEN }} script: | @@ -149,7 +149,7 @@ jobs: branch: ${{ env.NEW_BRANCH }} - name: Create Pull Request - uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0 + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 with: github-token: ${{ secrets.GITHUB_TOKEN }} script: | From 43434d637396d73e2c58b441f5ad68550df50906 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=2E=20Fern=C3=A1ndez?= <7312236+fernandezcuesta@users.noreply.github.com> Date: Fri, 5 Sep 2025 21:33:48 +0200 Subject: [PATCH 025/154] feat: improve Helm chart (#12691) * **Summary:** - Add extraInitContainers to celery+django deployments. - Add extraEnv to all deployments - Remove existing volume logic in favor of agnostic extraVolumes and extraVolumeMounts - Fix optional secret mounts + reference - Update bitnami chart reference (OCI) - Bump up redis chart * chore: add livenessProbe entries for celery * fix: reference to removed field * fix: conflict * chore: add reference to upstream chart * fix: missing default values from upstream chart used in templates * chore: rephrase * feat: allow deploy secret as regular non-hooked resources * fix: review * chore: restore Chart.lock * chore: update chart.lock * chore: wrap services url * fix: PR review suggestions * chore: mount extraVolumes in initContainers too * chore: move external db values to separate fields, add release notes * Update docs/content/en/open_source/upgrading/2.50.md Co-authored-by: kiblik <5609770+kiblik@users.noreply.github.com> * chore: bump chart version and remove bitnami dependency comment from RN * chore: move release notes to 2.51 * chore: restore 2.50.md --------- Co-authored-by: kiblik <5609770+kiblik@users.noreply.github.com> --- .github/workflows/k8s-tests.yml | 20 +- docs/content/en/open_source/upgrading/2.51.md | 46 +++- helm/defectdojo/Chart.yaml | 2 +- helm/defectdojo/templates/_helpers.tpl | 9 +- .../templates/celery-beat-deployment.yaml | 64 ++--- .../templates/celery-worker-deployment.yaml | 60 ++--- helm/defectdojo/templates/configmap.yaml | 8 +- .../templates/django-deployment.yaml | 77 +++--- .../defectdojo/templates/initializer-job.yaml | 34 ++- helm/defectdojo/templates/sa.yaml | 2 + .../templates/secret-postgresql.yaml | 2 + helm/defectdojo/templates/secret-redis.yaml | 2 + helm/defectdojo/templates/secret.yaml | 2 + .../templates/tests/unit-tests.yaml | 6 +- helm/defectdojo/values.yaml | 232 +++++++++--------- 15 files changed, 318 insertions(+), 248 deletions(-) diff --git a/.github/workflows/k8s-tests.yml b/.github/workflows/k8s-tests.yml index f9e8cd38029..251820c50ee 100644 --- a/.github/workflows/k8s-tests.yml +++ b/.github/workflows/k8s-tests.yml @@ -108,17 +108,17 @@ jobs: echo "INFO: status:" kubectl get pods echo "INFO: logs:" - kubectl logs --selector=$3 --all-containers=true + kubectl logs --selector=$3 --all-containers=true exit 1 fi return ${?} } echo "Waiting for init job..." - to_complete "condition=Complete" job "defectdojo.org/component=initializer" + to_complete "condition=Complete" job "defectdojo.org/component=initializer" echo "Waiting for celery pods..." - to_complete "condition=ready" pod "defectdojo.org/component=celery" + to_complete "condition=ready" pod "defectdojo.org/component=celery" echo "Waiting for django pod..." - to_complete "condition=ready" pod "defectdojo.org/component=django" + to_complete "condition=ready" pod "defectdojo.org/component=django" echo "Pods up and ready to rumbole" kubectl get pods RETRY=0 @@ -132,15 +132,15 @@ jobs: --max-time 20 \ --head \ --header "Host: $DD_HOSTNAME" \ - http://$DJANGO_IP/login?next=/) + "http://${DJANGO_IP}/login?next=/") echo $OUT - CR=`echo $OUT | egrep "^HTTP" | cut -d' ' -f2` + CR=$(echo $OUT | egrep "^HTTP" | cut -d' ' -f2) echo $CR if [[ $CR -ne 200 ]]; then echo $RETRY if [[ $RETRY -gt 2 ]]; then kubectl get pods - echo `kubectl logs --tail=30 -l defectdojo.org/component=django -c uwsgi` + echo $(kubectl logs --tail=30 -l defectdojo.org/component=django -c uwsgi) echo "ERROR: cannot display login screen; got HTTP code $CR" exit 1 else @@ -165,7 +165,7 @@ jobs: --data-raw "username=admin&password=$ADMIN_PASS" \ --output /dev/null \ --write-out "%{http_code}\n" \ - http://$DJANGO_IP/api/v2/api-token-auth/) + "http://${DJANGO_IP}/api/v2/api-token-auth/") echo $CR if [[ $CR -ne 200 ]]; then echo "ERROR: login is not possible; got HTTP code $CR" @@ -174,8 +174,8 @@ jobs: echo "Result received" fi echo "Final Check of components" - errors=`kubectl get pods | grep Error | awk '{print $1}'` - if [[ ! -z $errors ]]; then + errors=$(kubectl get pods | grep Error | awk '{print $1}') + if [[ ! -z $errors ]]; then echo "Few pods with errors" for line in $errors; do echo "Dumping log from $line" diff --git a/docs/content/en/open_source/upgrading/2.51.md b/docs/content/en/open_source/upgrading/2.51.md index 587eba9613b..fd9cab80d58 100644 --- a/docs/content/en/open_source/upgrading/2.51.md +++ b/docs/content/en/open_source/upgrading/2.51.md @@ -2,6 +2,48 @@ title: 'Upgrading to DefectDojo Version 2.51.x' toc_hide: true weight: -20250902 -description: No special instructions. +description: Helm chart changes. --- -There are no special instructions for upgrading to 2.51.x. Check the [Release Notes](https://github.com/DefectDojo/django-DefectDojo/releases/tag/2.51.0) for the contents of the release. + +## Helm Chart Changes + +This release introduces several important changes to the Helm chart configuration: + +### Breaking changes + +#### Volume Management Improvements + +- **Streamlined volume configuration**: The existing volume logic has been removed and replaced with more flexible `extraVolumes` and `extraVolumeMounts` options that provide deployment-agnostic volume management. + +> The previous volume implementation prevented mounting projected volumes (such as secret mounts with renamed key names) and per-container volume mounts (like nginx emptyDir when readOnlyRootFs is enforced). +> The new approach resolves these limitations. + +#### Moved values + +The following Helm chart values have been modified in this release: + +- `redis.transportEncryption.enabled` → `redis.tls.enabled` (aligned with upstream Helm chart) +- `redis.scheme` → `redis.sentinel.enabled` (controls deployment mode and aligns with upstream chart) +- `redis.redisServer` → `redisServer` (prevents potential schema conflicts with upstream chart) +- `redis.transportEncryption.params` → `redisParams` (prevents potential schema conflicts with upstream chart) +- `postgresql.postgresServer` → `postgresServer` (prevents potential schema conflicts with upstream chart) + +### New features + +#### Container and Environment Enhancements + +- **Added extraInitContainers support**: Both Celery and Django deployments now support additional init containers through the `extraInitContainers` configuration option. +- **Enhanced probe configuration for Celery**: Added support for customizing liveness, readiness, and startup probes in both Celery beat and worker deployments. +- **Enhanced environment variable management**: All deployments now include `extraEnv` support for adding custom environment variables. For backwards compatibility, `.Values.extraEnv` can be used to inject common environment variables to all workloads. + +### Other changes + +- **Celery pod annotations**: Now we can add annotations to Celery beat/worker pods separately. +- **Flexible secret deployment**: Added the capability to deploy secrets as regular (non-hooked) resources to address compatibility issues encountered with CI/CD tools (such as ArgoCD). +- **Optional secret references**: Some secret references are now optional, allowing the chart to function even when certain secrets are not created. +- **Fixed secret mounting**: Resolved issues with optional secret mounts and references. +- **Improved code organization**: Minor Helm chart refactoring to enhance readability and maintainability. + +--- + +There are other instructions for upgrading to 2.51.x. Check the [Release Notes](https://github.com/DefectDojo/django-DefectDojo/releases/tag/2.51.0) for the contents of the release. diff --git a/helm/defectdojo/Chart.yaml b/helm/defectdojo/Chart.yaml index f2617ac4715..1e1d4ef8b4c 100644 --- a/helm/defectdojo/Chart.yaml +++ b/helm/defectdojo/Chart.yaml @@ -2,7 +2,7 @@ apiVersion: v2 appVersion: "2.51.0-dev" description: A Helm chart for Kubernetes to install DefectDojo name: defectdojo -version: 1.6.206-dev +version: 1.7.0-dev icon: https://www.defectdojo.org/img/favicon.ico maintainers: - name: madchap diff --git a/helm/defectdojo/templates/_helpers.tpl b/helm/defectdojo/templates/_helpers.tpl index 311088de423..025b35078db 100644 --- a/helm/defectdojo/templates/_helpers.tpl +++ b/helm/defectdojo/templates/_helpers.tpl @@ -53,15 +53,16 @@ Create the name of the service account to use {{- printf "%s-%s" .Release.Name "postgresql" | trunc 63 | trimSuffix "-" -}} {{- end -}} {{- else -}} -{{- printf "%s" ( .Values.postgresql.postgresServer | default "127.0.0.1" ) -}} +{{- .Values.postgresServer | default "127.0.0.1" | quote -}} {{- end -}} {{- end -}} + {{- define "redis.hostname" -}} {{- if eq .Values.celery.broker "redis" -}} {{- if .Values.redis.enabled -}} {{- printf "%s-%s" .Release.Name "redis-master" | trunc 63 | trimSuffix "-" -}} {{- else -}} -{{- printf "%s" (.Values.celery.brokerHost | default .Values.redis.redisServer) -}} +{{- .Values.redisServer | default "127.0.0.1" | quote -}} {{- end -}} {{- end -}} {{- end -}} @@ -71,9 +72,9 @@ Create the name of the service account to use */}} {{- define "redis.scheme" -}} {{- if eq .Values.celery.broker "redis" -}} -{{- if .Values.redis.transportEncryption.enabled -}} +{{- if .Values.redis.tls.enabled -}} {{- printf "rediss" -}} -{{- else if eq .Values.redis.scheme "sentinel" -}} +{{- else if .Values.redis.sentinel.enabled -}} {{- printf "sentinel" -}} {{- else -}} {{- printf "redis" -}} diff --git a/helm/defectdojo/templates/celery-beat-deployment.yaml b/helm/defectdojo/templates/celery-beat-deployment.yaml index fc96dc10c6c..166f6c2afeb 100644 --- a/helm/defectdojo/templates/celery-beat-deployment.yaml +++ b/helm/defectdojo/templates/celery-beat-deployment.yaml @@ -11,18 +11,16 @@ metadata: app.kubernetes.io/managed-by: {{ .Release.Service }} helm.sh/chart: {{ include "defectdojo.chart" . }} {{- with .Values.extraLabels }} - {{- toYaml . | nindent 4 }} + {{- toYaml . | nindent 4 }} {{- end }} -{{- if .Values.celery.annotations }} + {{- with mergeOverwrite .Values.celery.annotations .Values.celery.beat.annotations }} annotations: -{{- with .Values.celery.annotations }} - {{- toYaml . | nindent 4 }} -{{- end }} -{{- end }} + {{- toYaml . | nindent 4 }} + {{- end }} spec: replicas: {{ .Values.celery.beat.replicas }} - {{- if .Values.revisionHistoryLimit }} - revisionHistoryLimit: {{ .Values.revisionHistoryLimit }} + {{- with .Values.revisionHistoryLimit }} + revisionHistoryLimit: {{ . }} {{- end }} selector: matchLabels: @@ -44,7 +42,7 @@ spec: {{- toYaml . | nindent 8 }} {{- end }} annotations: - {{- with .Values.celery.beat.annotations }} + {{- with mergeOverwrite .Values.celery.annotations .Values.celery.beat.podAnnotations }} {{- toYaml . | nindent 8 }} {{- end }} {{- if eq (.Values.trackConfig | default "disabled") "enabled" }} @@ -54,9 +52,9 @@ spec: {{- end }} spec: serviceAccountName: {{ include "defectdojo.serviceAccountName" . }} - {{- if .Values.imagePullSecrets }} + {{- with .Values.imagePullSecrets }} imagePullSecrets: - - name: {{ .Values.imagePullSecrets }} + - name: {{ . }} {{- end }} volumes: - name: run @@ -71,22 +69,16 @@ spec: configMap: name: {{ .Values.django.uwsgi.certificates.configName }} {{- end }} - {{- range .Values.celery.extraVolumes }} - - name: userconfig-{{ .name }} - {{ .type }}: - {{- if (eq .type "configMap") }} - name: {{ .name }} - {{- else if (eq .type "secret") }} - secretName: {{ .name }} - {{- else if (eq .type "hostPath") }} - type: {{ .pathType | default "Directory" }} - path: {{ .hostPath }} - {{- end }} + {{- with .Values.celery.beat.extraVolumes }} + {{- . | toYaml | nindent 6 }} {{- end }} - {{- if or .Values.dbMigrationChecker.enabled .Values.cloudsql.enabled }} + {{- if coalesce .Values.dbMigrationChecker.enabled .Values.cloudsql.enabled .Values.celery.beat.extraInitContainers }} initContainers: + {{- range .Values.celery.beat.extraInitContainers }} + - {{- . | toYaml | nindent 8 }} + {{- end }} {{- end }} - {{- if .Values.cloudsql.enabled }} + {{- if .Values.cloudsql.enabled }} - name: cloudsql-proxy image: {{ .Values.cloudsql.image.repository }}:{{ .Values.cloudsql.image.tag }} imagePullPolicy: {{ .Values.cloudsql.image.pullPolicy }} @@ -115,6 +107,15 @@ spec: name: celery image: "{{ template "celery.repository" . }}:{{ .Values.tag }}" imagePullPolicy: {{ .Values.imagePullPolicy }} + {{- with .Values.celery.beat.livenessProbe }} + livenessProbe: {{ toYaml . | nindent 10 }} + {{- end }} + {{- with .Values.celery.beat.readinessProbe }} + readinessProbe: {{ toYaml . | nindent 10 }} + {{- end }} + {{- with .Values.celery.beat.startupProbe }} + startupProbe: {{ toYaml . | nindent 10 }} + {{- end }} {{- if .Values.securityContext.enabled }} securityContext: {{- toYaml .Values.securityContext.djangoSecurityContext | nindent 10 }} @@ -128,15 +129,12 @@ spec: mountPath: /app/dojo/settings/local_settings.py subPath: file {{- end }} - {{- if .Values.django.uwsgi.certificates.enabled }} + {{- if .Values.django.uwsgi.certificates.enabled }} - name: cert-mount mountPath: {{ .Values.django.uwsgi.certificates.certMountPath }} {{- end }} - {{- range .Values.celery.extraVolumes }} - - name: userconfig-{{ .name }} - readOnly: true - mountPath: {{ .path }} - subPath: {{ .subPath }} + {{- with .Values.celery.beat.extraVolumeMounts }} + {{- . | toYaml | nindent 8 }} {{- end }} envFrom: - configMapRef: @@ -162,8 +160,12 @@ spec: secretKeyRef: name: {{ $fullName }} key: DD_SECRET_KEY + optional: true {{- with .Values.extraEnv }} - {{- toYaml . | nindent 8 }} + {{- . | toYaml | nindent 8 }} + {{- end }} + {{- with .Values.celery.beat.extraEnv }} + {{- . | toYaml | nindent 8 }} {{- end }} resources: {{- toYaml .Values.celery.beat.resources | nindent 10 }} diff --git a/helm/defectdojo/templates/celery-worker-deployment.yaml b/helm/defectdojo/templates/celery-worker-deployment.yaml index 5fe9b562888..ce4881094e9 100644 --- a/helm/defectdojo/templates/celery-worker-deployment.yaml +++ b/helm/defectdojo/templates/celery-worker-deployment.yaml @@ -13,16 +13,14 @@ metadata: {{- with .Values.extraLabels }} {{- toYaml . | nindent 4 }} {{- end }} -{{- if .Values.celery.annotations }} + {{- with mergeOverwrite .Values.celery.annotations .Values.celery.worker.annotations }} annotations: -{{- with .Values.celery.annotations }} - {{- toYaml . | nindent 4 }} -{{- end }} -{{- end }} + {{- toYaml . | nindent 4 }} + {{- end }} spec: replicas: {{ .Values.celery.worker.replicas }} - {{- if .Values.revisionHistoryLimit }} - revisionHistoryLimit: {{ .Values.revisionHistoryLimit }} + {{- with .Values.revisionHistoryLimit }} + revisionHistoryLimit: {{ . }} {{- end }} selector: matchLabels: @@ -44,7 +42,7 @@ spec: {{- toYaml . | nindent 8 }} {{- end }} annotations: - {{- with .Values.celery.worker.annotations }} + {{- with mergeOverwrite .Values.celery.annotations .Values.celery.worker.podAnnotations }} {{- toYaml . | nindent 8 }} {{- end }} {{- if eq (.Values.trackConfig | default "disabled") "enabled" }} @@ -54,9 +52,9 @@ spec: {{- end }} spec: serviceAccountName: {{ include "defectdojo.serviceAccountName" . }} - {{- if .Values.imagePullSecrets }} + {{- with .Values.imagePullSecrets }} imagePullSecrets: - - name: {{ .Values.imagePullSecrets }} + - name: {{ . }} {{- end }} volumes: {{- if .Values.localsettingspy }} @@ -69,20 +67,14 @@ spec: configMap: name: {{ .Values.django.uwsgi.certificates.configName }} {{- end }} - {{- range .Values.celery.extraVolumes }} - - name: userconfig-{{ .name }} - {{ .type }}: - {{- if (eq .type "configMap") }} - name: {{ .name }} - {{- else if (eq .type "secret") }} - secretName: {{ .name }} - {{- else if (eq .type "hostPath") }} - type: {{ .pathType | default "Directory" }} - path: {{ .hostPath }} - {{- end }} + {{- with .Values.celery.worker.extraVolumes }} + {{- . | toYaml | nindent 6 }} {{- end }} - {{- if or .Values.dbMigrationChecker.enabled .Values.cloudsql.enabled }} + {{- if coalesce .Values.dbMigrationChecker.enabled .Values.cloudsql.enabled .Values.celery.worker.extraInitContainers }} initContainers: + {{- range .Values.celery.worker.extraInitContainers }} + - {{- . | toYaml | nindent 8 }} + {{- end }} {{- end }} {{- if .Values.cloudsql.enabled }} - name: cloudsql-proxy @@ -111,13 +103,22 @@ spec: - name: celery image: "{{ template "celery.repository" . }}:{{ .Values.tag }}" imagePullPolicy: {{ .Values.imagePullPolicy }} + {{- with .Values.celery.worker.livenessProbe }} + livenessProbe: {{ toYaml . | nindent 10 }} + {{- end }} + {{- with .Values.celery.worker.readinessProbe }} + readinessProbe: {{ toYaml . | nindent 10 }} + {{- end }} + {{- with .Values.celery.worker.startupProbe }} + startupProbe: {{ toYaml . | nindent 10 }} + {{- end }} {{- if .Values.securityContext.enabled }} securityContext: {{- toYaml .Values.securityContext.djangoSecurityContext | nindent 10 }} {{- end }} command: ['/entrypoint-celery-worker.sh'] volumeMounts: - {{- if .Values.localsettingspy }} + {{- if .Values.localsettingspy }} - name: localsettingspy readOnly: true mountPath: /app/dojo/settings/local_settings.py @@ -127,11 +128,8 @@ spec: - name: cert-mount mountPath: {{ .Values.django.uwsgi.certificates.certMountPath }} {{- end }} - {{- range .Values.celery.extraVolumes }} - - name: userconfig-{{ .name }} - readOnly: true - mountPath: {{ .path }} - subPath: {{ .subPath }} + {{- with .Values.celery.worker.extraVolumeMounts }} + {{- . | toYaml | nindent 8 }} {{- end }} envFrom: - configMapRef: @@ -157,8 +155,12 @@ spec: secretKeyRef: name: {{ $fullName }} key: DD_SECRET_KEY + optional: true {{- with .Values.extraEnv }} - {{- toYaml . | nindent 8 }} + {{- . | toYaml | nindent 8 }} + {{- end }} + {{- with .Values.celery.worker.extraEnv }} + {{- . | toYaml | nindent 8 }} {{- end }} resources: {{- toYaml .Values.celery.worker.resources | nindent 10 }} diff --git a/helm/defectdojo/templates/configmap.yaml b/helm/defectdojo/templates/configmap.yaml index 74d516981c7..e89279e3eea 100644 --- a/helm/defectdojo/templates/configmap.yaml +++ b/helm/defectdojo/templates/configmap.yaml @@ -1,4 +1,5 @@ {{- $fullName := include "defectdojo.fullname" . -}} +{{- $defaultBrokerParams := ternary "ssl_cert_reqs=optional" "" .Values.redis.tls.enabled -}} apiVersion: v1 kind: ConfigMap metadata: @@ -28,7 +29,7 @@ data: DD_CELERY_BROKER_USER: '' DD_CELERY_BROKER_HOST: {{ if eq .Values.celery.broker "redis" }}{{ template "redis.hostname" . }}{{ end }} DD_CELERY_BROKER_PORT: '{{ if eq .Values.celery.broker "redis" }}{{- if ( hasKey .Values.redis "master" ) -}}{{ .Values.redis.master.service.ports.redis }}{{ else }}6379{{ end }}{{- end -}}' - DD_CELERY_BROKER_PARAMS: '{{ if eq .Values.celery.broker "redis" }}{{- if .Values.redis.transportEncryption.enabled -}}{{ .Values.redis.transportEncryption.params | default "ssl_cert_reqs=optional" }}{{ end }}{{ end }}' + DD_CELERY_BROKER_PARAMS: '{{ .Values.redisParams | default $defaultBrokerParams }}' DD_CELERY_BROKER_PATH: '{{ .Values.celery.path | default "//" }}' DD_CELERY_LOG_LEVEL: {{ .Values.celery.logLevel }} DD_CELERY_WORKER_POOL_TYPE: {{ .Values.celery.worker.appSettings.poolType | default "solo" }} @@ -54,5 +55,6 @@ data: {{- if .Values.django.uwsgi.certificates.enabled }} REQUESTS_CA_BUNDLE: {{ .Values.django.uwsgi.certificates.certMountPath }}{{ .Values.django.uwsgi.certificates.certFileName }} {{- end }} -{{- with .Values.extraConfigs }} - {{- toYaml . | nindent 2 }}{{- end }} +{{- with .Values.extraConfigs }} + {{- toYaml . | nindent 2 }} +{{- end }} diff --git a/helm/defectdojo/templates/django-deployment.yaml b/helm/defectdojo/templates/django-deployment.yaml index 0f6d5fc0076..fb77e8f7e88 100644 --- a/helm/defectdojo/templates/django-deployment.yaml +++ b/helm/defectdojo/templates/django-deployment.yaml @@ -12,20 +12,18 @@ metadata: {{- with .Values.extraLabels }} {{- toYaml . | nindent 4 }} {{- end }} -{{- if .Values.django.annotations }} + {{- with .Values.django.annotations }} annotations: -{{- with .Values.django.annotations }} - {{- toYaml . | nindent 4 }} -{{- end }} -{{- end }} + {{- toYaml . | nindent 4 }} + {{- end }} spec: replicas: {{ .Values.django.replicas }} {{- with .Values.django.strategy }} strategy: {{- toYaml . | nindent 4 }} {{- end }} - {{- if .Values.revisionHistoryLimit }} - revisionHistoryLimit: {{ .Values.revisionHistoryLimit }} + {{- with .Values.revisionHistoryLimit }} + revisionHistoryLimit: {{ . }} {{- end }} selector: matchLabels: @@ -61,9 +59,9 @@ spec: {{- end }} spec: serviceAccountName: {{ include "defectdojo.serviceAccountName" . }} - {{- if .Values.imagePullSecrets }} + {{- with .Values.imagePullSecrets }} imagePullSecrets: - - name: {{ .Values.imagePullSecrets }} + - name: {{ quote . }} {{- end }} {{- if .Values.django.mediaPersistentVolume.enabled }} securityContext: @@ -72,41 +70,34 @@ spec: volumes: - name: run emptyDir: {} - {{- if .Values.localsettingspy }} + {{- if .Values.localsettingspy }} - name: localsettingspy configMap: name: {{ $fullName }}-localsettingspy {{- end }} - {{- if .Values.django.uwsgi.certificates.enabled }} + {{- if .Values.django.uwsgi.certificates.enabled }} - name: cert-mount configMap: name: {{ .Values.django.uwsgi.certificates.configName }} {{- end }} - {{- range .Values.django.extraVolumes }} - - name: userconfig-{{ .name }} - {{ .type }}: - {{- if (eq .type "configMap") }} - name: {{ .name }} - {{- else if (eq .type "secret") }} - secretName: {{ .name }} - {{- else if (eq .type "hostPath") }} - type: {{ .pathType | default "Directory" }} - path: {{ .hostPath }} - {{- end }} + {{- with .Values.django.extraVolumes }} + {{- . | toYaml | nindent 6 }} {{- end }} {{- if .Values.django.mediaPersistentVolume.enabled }} - name: {{ .Values.django.mediaPersistentVolume.name }} - {{- if eq .Values.django.mediaPersistentVolume.type "pvc" }} + {{- if eq .Values.django.mediaPersistentVolume.type "pvc" }} persistentVolumeClaim: claimName: {{ include "django.pvc_name" $ }} {{ else }} emptyDir: {} {{- end }} {{- end }} - {{- if or .Values.dbMigrationChecker.enabled .Values.cloudsql.enabled }} + {{- if coalesce .Values.dbMigrationChecker.enabled .Values.cloudsql.enabled .Values.django.extraInitContainers }} initContainers: + {{- range .Values.django.extraInitContainers }} + - {{- . | toYaml | nindent 8 }} {{- end }} - {{- if .Values.cloudsql.enabled }} + {{- if .Values.cloudsql.enabled }} - name: cloudsql-proxy image: {{ .Values.cloudsql.image.repository }}:{{ .Values.cloudsql.image.tag }} imagePullPolicy: {{ .Values.cloudsql.image.pullPolicy }} @@ -129,6 +120,7 @@ spec: {{- $newContext := merge . (dict "fullName" $fullName) }} {{- include "dbMigrationChecker" $newContext | nindent 6 }} {{- end }} + {{- end }} containers: {{- if and .Values.monitoring.enabled .Values.monitoring.prometheus.enabled }} - name: metrics @@ -157,23 +149,18 @@ spec: volumeMounts: - name: run mountPath: /run/defectdojo - {{- if .Values.localsettingspy }} + {{- if .Values.localsettingspy }} - name: localsettingspy readOnly: true mountPath: /app/dojo/settings/local_settings.py subPath: file {{- end }} - {{- if .Values.django.uwsgi.certificates.enabled }} + {{- if .Values.django.uwsgi.certificates.enabled }} - name: cert-mount mountPath: {{ .Values.django.uwsgi.certificates.certMountPath }} {{- end }} - {{- range .Values.django.extraVolumes }} - {{- if (eq .container "uwsgi") }} - - name: userconfig-{{ .name }} - readOnly: true - mountPath: {{ .path }} - subPath: {{ .subPath }} - {{- end }} + {{- with .Values.django.uwsgi.extraVolumeMounts }} + {{- . | toYaml | nindent 8 }} {{- end }} {{- if .Values.django.mediaPersistentVolume.enabled }} - name: {{ .Values.django.mediaPersistentVolume.name }} @@ -211,17 +198,22 @@ spec: secretKeyRef: name: {{ $fullName }} key: DD_SECRET_KEY + optional: true - name: DD_CREDENTIAL_AES_256_KEY valueFrom: secretKeyRef: name: {{ $fullName }} key: DD_CREDENTIAL_AES_256_KEY + optional: true - name: DD_SESSION_COOKIE_SECURE value: {{- if or .Values.django.ingress.activateTLS .Values.django.nginx.tls.enabled }} "True" {{- else }} "False" {{- end }} - name: DD_CSRF_COOKIE_SECURE value: {{- if or .Values.django.ingress.activateTLS .Values.django.nginx.tls.enabled }} "True" {{- else }} "False" {{- end }} {{- with .Values.extraEnv }} - {{- toYaml . | nindent 8 }} + {{- . | toYaml | nindent 8 }} + {{- end }} + {{- with .Values.django.uwsgi.extraEnv }} + {{- . | toYaml | nindent 8 }} {{- end }} {{- if .Values.django.uwsgi.livenessProbe.enabled }} livenessProbe: @@ -249,13 +241,8 @@ spec: volumeMounts: - name: run mountPath: /run/defectdojo - {{- range .Values.django.extraVolumes }} - {{- if (eq .container "nginx") }} - - name: userconfig-{{ .name }} - readOnly: true - mountPath: {{ .path }} - subPath: {{ .subPath }} - {{- end }} + {{- with .Values.django.nginx.extraVolumeMounts }} + {{- . | toYaml | nindent 8 }} {{- end }} {{- if .Values.django.mediaPersistentVolume.enabled }} - name: {{ .Values.django.mediaPersistentVolume.name }} @@ -278,6 +265,12 @@ spec: value: '{{ .Values.django.nginx.tls.enabled }}' - name: GENERATE_TLS_CERTIFICATE value: '{{ .Values.django.nginx.tls.generateCertificate }}' + {{- with .Values.extraEnv }} + {{- . | toYaml | nindent 8 }} + {{- end }} + {{- with .Values.django.nginx.extraEnv }} + {{- . | toYaml | nindent 8 }} + {{- end }} {{- if .Values.django.uwsgi.livenessProbe.enabled }} livenessProbe: httpGet: diff --git a/helm/defectdojo/templates/initializer-job.yaml b/helm/defectdojo/templates/initializer-job.yaml index 223799bf444..668812d1a08 100644 --- a/helm/defectdojo/templates/initializer-job.yaml +++ b/helm/defectdojo/templates/initializer-job.yaml @@ -39,12 +39,12 @@ spec: {{- end }} spec: serviceAccountName: {{ include "defectdojo.serviceAccountName" . }} - {{- if .Values.imagePullSecrets }} + {{- with .Values.imagePullSecrets }} imagePullSecrets: - - name: {{ .Values.imagePullSecrets }} + - name: {{ . }} {{- end }} volumes: - {{- if .Values.localsettingspy }} + {{- if .Values.localsettingspy }} - name: localsettingspy configMap: name: {{ $fullName }}-localsettingspy @@ -62,7 +62,7 @@ spec: {{- end }} {{- end }} initContainers: - {{- if .Values.cloudsql.enabled }} + {{- if .Values.cloudsql.enabled }} - name: cloudsql-proxy image: {{ .Values.cloudsql.image.repository }}:{{ .Values.cloudsql.image.tag }} imagePullPolicy: {{ .Values.cloudsql.image.pullPolicy }} @@ -79,6 +79,13 @@ spec: {{- if .Values.cloudsql.use_private_ip }} - "-ip_address_types=PRIVATE" {{- end }} + volumeMounts: + {{- range .Values.initializer.extraVolumes }} + - name: userconfig-{{ .name }} + readOnly: true + mountPath: {{ .path }} + subPath: {{ .subPath }} + {{- end }} {{- end }} - name: wait-for-db command: @@ -95,14 +102,21 @@ spec: - configMapRef: name: {{ $fullName }} - secretRef: - name: {{ $fullName }} + name: {{ $fullName }}-extrasecrets optional: true env: - {{- with .Values.extraEnv }} + {{- with .Values.initializer.extraEnv }} {{- toYaml . | nindent 8 }} {{- end }} resources: {{- toYaml .Values.initializer.resources | nindent 10 }} + volumeMounts: + {{- range .Values.initializer.extraVolumes }} + - name: userconfig-{{ .name }} + readOnly: true + mountPath: {{ .path }} + subPath: {{ .subPath }} + {{- end }} containers: - name: initializer image: "{{ template "initializer.repository" . }}:{{ .Values.tag }}" @@ -112,7 +126,7 @@ spec: {{- toYaml .Values.securityContext.djangoSecurityContext | nindent 10 }} {{- end }} volumeMounts: - {{- if .Values.localsettingspy }} + {{- if .Values.localsettingspy }} - name: localsettingspy readOnly: true mountPath: /app/dojo/settings/local_settings.py @@ -131,13 +145,17 @@ spec: name: {{ $fullName }} - secretRef: name: {{ $fullName }} + optional: true + - secretRef: + name: {{ $fullName }}-extrasecrets + optional: true env: - name: DD_DATABASE_PASSWORD valueFrom: secretKeyRef: name: {{ .Values.postgresql.auth.existingSecret }} key: {{ .Values.postgresql.auth.secretKeys.userPasswordKey }} - {{- with .Values.extraEnv }} + {{- with .Values.initializer.extraEnv }} {{- toYaml . | nindent 8 }} {{- end }} resources: diff --git a/helm/defectdojo/templates/sa.yaml b/helm/defectdojo/templates/sa.yaml index 2c6b166510c..4345da6360a 100644 --- a/helm/defectdojo/templates/sa.yaml +++ b/helm/defectdojo/templates/sa.yaml @@ -15,9 +15,11 @@ metadata: {{- toYaml . | nindent 4 }} {{- end }} annotations: + {{- if (not .Values.disableHooks) }} helm.sh/resource-policy: keep helm.sh/hook: "pre-install" helm.sh/hook-delete-policy: "before-hook-creation" + {{- end }} {{- with .Values.annotations }} {{ toYaml . | nindent 4 }} {{- end }} diff --git a/helm/defectdojo/templates/secret-postgresql.yaml b/helm/defectdojo/templates/secret-postgresql.yaml index 979e4d75c9c..12924bb29c5 100644 --- a/helm/defectdojo/templates/secret-postgresql.yaml +++ b/helm/defectdojo/templates/secret-postgresql.yaml @@ -12,9 +12,11 @@ metadata: {{- toYaml . | nindent 4 }} {{- end }} annotations: + {{- if (not .Values.disableHooks) }} helm.sh/resource-policy: keep helm.sh/hook: "pre-install" helm.sh/hook-delete-policy: "before-hook-creation" + {{- end }} {{- with .Values.secrets.annotations }} {{- toYaml . | nindent 4 }} {{- end }} diff --git a/helm/defectdojo/templates/secret-redis.yaml b/helm/defectdojo/templates/secret-redis.yaml index 35abc641fe9..f6d102c2513 100644 --- a/helm/defectdojo/templates/secret-redis.yaml +++ b/helm/defectdojo/templates/secret-redis.yaml @@ -12,9 +12,11 @@ metadata: {{- toYaml . | nindent 4 }} {{- end }} annotations: + {{- if (not .Values.disableHooks) }} helm.sh/resource-policy: keep helm.sh/hook: "pre-install" helm.sh/hook-delete-policy: "before-hook-creation" + {{- end }} {{- with .Values.secrets.annotations }} {{- toYaml . | nindent 4 }} {{- end }} diff --git a/helm/defectdojo/templates/secret.yaml b/helm/defectdojo/templates/secret.yaml index e09dad92244..c3a3c56f6c4 100644 --- a/helm/defectdojo/templates/secret.yaml +++ b/helm/defectdojo/templates/secret.yaml @@ -13,9 +13,11 @@ metadata: {{- toYaml . | nindent 4 }} {{- end }} annotations: + {{- if (not .Values.disableHooks) }} helm.sh/resource-policy: keep helm.sh/hook: "pre-install" helm.sh/hook-delete-policy: "before-hook-creation" + {{- end }} {{- with .Values.secrets.annotations }} {{- toYaml . | nindent 4 }} {{- end }} diff --git a/helm/defectdojo/templates/tests/unit-tests.yaml b/helm/defectdojo/templates/tests/unit-tests.yaml index 252c92090e5..2f390733b22 100644 --- a/helm/defectdojo/templates/tests/unit-tests.yaml +++ b/helm/defectdojo/templates/tests/unit-tests.yaml @@ -12,9 +12,9 @@ metadata: helm.sh/hook: test-success spec: serviceAccountName: {{ include "defectdojo.serviceAccountName" . }} - {{- if .Values.imagePullSecrets }} + {{- with .Values.imagePullSecrets }} imagePullSecrets: - - name: {{ .Values.imagePullSecrets }} + - name: {{ . }} {{- end }} containers: - name: unit-tests @@ -51,11 +51,13 @@ spec: secretKeyRef: name: {{ $fullName }} key: DD_SECRET_KEY + optional: true - name: DD_CREDENTIAL_AES_256_KEY valueFrom: secretKeyRef: name: {{ $fullName }} key: DD_CREDENTIAL_AES_256_KEY + optional: true resources: {{- toYaml .Values.tests.unitTests.resources | nindent 8 }} restartPolicy: Never diff --git a/helm/defectdojo/values.yaml b/helm/defectdojo/values.yaml index 5d073474a08..ec4eecd601d 100644 --- a/helm/defectdojo/values.yaml +++ b/helm/defectdojo/values.yaml @@ -12,7 +12,10 @@ createPostgresqlSecret: false # - enabled, enables tracking configuration changes based on SHA256 # trackConfig: disabled -# extraLabels: {} +# Avoid using pre-install hooks, which might cause issues with ArgoCD +disableHooks: false + +extraLabels: {} # Add extra labels for k8s # Enables application network policy @@ -64,7 +67,7 @@ imagePullPolicy: Always repositoryPrefix: defectdojo # When using a private registry, name of the secret that holds the registry secret (eg deploy token from gitlab-ci project) # Create secrets as: kubectl create secret docker-registry defectdojoregistrykey --docker-username=registry_username --docker-password=registry_password --docker-server='https://index.docker.io/v1/' -# imagePullSecrets: defectdojoregistrykey +imagePullSecrets: ~ tag: latest # Additional labels to add to the pods: @@ -73,7 +76,7 @@ tag: latest podLabels: {} # Allow overriding of revisionHistoryLimit across all deployments. -# revisionHistoryLimit: 10 +revisionHistoryLimit: 10 securityContext: enabled: true @@ -145,14 +148,37 @@ secrets: # Components celery: broker: redis - # To use an external celery broker, set the hostname here - brokerHost: "" logLevel: INFO + # Common annotations to worker and beat deployments and pods. annotations: {} beat: + # Annotations for the Celery beat deployment. annotations: {} affinity: {} + # Additional environment variables injected to Celery beat containers. + extraEnv: [] + # A list of additional initContainers to run before celery beat containers. + extraInitContainers: [] + # Array of additional volume mount points for the celery beat containers. + extraVolumeMounts: [] + # A list of extra volumes to mount + # @type: array + extraVolumes: [] + # Enable liveness probe for Celery beat container. + livenessProbe: {} + # exec: + # command: + # - bash + # - -c + # - celery -A dojo inspect ping -t 5 + # initialDelaySeconds: 30 + # periodSeconds: 60 + # timeoutSeconds: 10 nodeSelector: {} + # Annotations for the Celery beat pods. + podAnnotations: {} + # Enable readiness probe for Celery beat container. + readinessProbe: {} replicas: 1 resources: requests: @@ -161,11 +187,37 @@ celery: limits: cpu: 2000m memory: 256Mi + # Enable startup probe for Celery beat container. + startupProbe: {} tolerations: [] worker: + # Annotations for the Celery worker deployment. annotations: {} affinity: {} + # Additional environment variables injected to Celery worker containers. + extraEnv: [] + # A list of additional initContainers to run before celery worker containers. + extraInitContainers: [] + # Array of additional volume mount points for the celery worker containers. + extraVolumeMounts: [] + # A list of extra volumes to mount. + # @type: array + extraVolumes: [] + # Enable liveness probe for Celery worker containers. + livenessProbe: {} + # exec: + # command: + # - bash + # - -c + # - celery -A dojo inspect ping -t 5 + # initialDelaySeconds: 30 + # periodSeconds: 60 + # timeoutSeconds: 10 nodeSelector: {} + # Annotations for the Celery beat pods. + podAnnotations: {} + # Enable readiness probe for Celery worker container. + readinessProbe: {} replicas: 1 resources: requests: @@ -174,6 +226,8 @@ celery: limits: cpu: 2000m memory: 512Mi + # Enable startup probe for Celery worker container. + startupProbe: {} tolerations: [] appSettings: poolType: solo @@ -185,39 +239,6 @@ celery: # concurrency: 8 # prefetchMultiplier: 128 - # A list of extra volumes to mount. This - # is useful for bringing in extra data that can be referenced by other configurations - # at a well known path, such as local_settings. The - # value of this should be a list of objects. - # - # Example: - # - # ```yaml - # extraVolumes: - # - type: configMap - # name: local_settings - # path: /app/dojo/settings/local_settings.py - # subPath: local_settings.py - # - type: hostPath - # name: host_directory - # path: /tmp - # hostPath: /tmp - # ``` - # - # Each object supports the following keys: - # - # - `type` - Type of the volume, must be one of "configMap", "secret", "hostPath". Case sensitive. - # Even is supported we are highly recommending to avoid hostPath for security reasons (usually blocked by PSP) - # - `name` - Name of the configMap or secret to be mounted. This also controls - # the path that it is mounted to. The volume will be mounted to `/consul/userconfig/`. - # - `path` - defines where file should be exposed - # - `subPath` - extracts only particular file from secret or configMap - # - `pathType` - only for hostPath, can be one of the "DirectoryOrCreate", "Directory" (default), "FileOrCreate", - # "File", "Socket", "CharDevice", "BlockDevice" - # - `hostPath` - only for hostPath, file or directory from local host - # @type: array - extraVolumes: [] - django: annotations: {} service: @@ -236,6 +257,16 @@ django: # nginx.ingress.kubernetes.io/proxy-read-timeout: "1800" # nginx.ingress.kubernetes.io/proxy-send-timeout: "1800" nginx: + # To extra environment variables to the nginx container, you can use extraEnv. For example: + # extraEnv: + # - name: FOO + # valueFrom: + # configMapKeyRef: + # name: foo + # key: bar + extraEnv: [] + # Array of additional volume mount points for nginx containers. + extraVolumeMounts: [] tls: enabled: false generateCertificate: false @@ -251,6 +282,17 @@ django: strategy: {} tolerations: [] uwsgi: + # To add (or override) extra variables which need to be pulled from another configMap, you can + # use extraEnv. For example: + # extraEnv: + # - name: DD_DATABASE_HOST + # valueFrom: + # configMapKeyRef: + # name: my-other-postgres-configmap + # key: cluster_endpoint + extraEnv: [] + # Array of additional volume mount points for uwsgi containers. + extraVolumeMounts: [] livenessProbe: # Enable liveness checks on uwsgi container. enabled: true @@ -289,47 +331,16 @@ django: enableDebug: false # this also requires DD_DEBUG to be set to True certificates: # includes additional CA certificate as volume, it refrences REQUESTS_CA_BUNDLE env varible - # to create configMap `kubectl create cm defectdojo-ca-certs --from-file=ca.crt` + # to create configMap `kubectl create cm defectdojo-ca-certs --from-file=ca.crt` # NOTE: it reflects REQUESTS_CA_BUNDLE for celery workers, beats as well enabled: false configName: defectdojo-ca-certs certMountPath: /certs/ certFileName: ca.crt - # A list of extra volumes to mount. This - # is useful for bringing in extra data that can be referenced by other configurations - # at a well known path, such as local_settings. The - # value of this should be a list of objects. - # - # Example: - # - # ```yaml - # extraVolumes: - # - type: configMap - # name: local_settings - # path: /app/dojo/settings/local_settings.py - # container: uwsgi - # subPath: local_settings.py - # - type: hostPath - # name: host_directory - # path: /app/dojo/settings/ - # hostPath: /var/run - # container: uwsgi - # ``` - # - # Each object supports the following keys: - # - # - `type` - Type of the volume, must be one of "configMap", "secret", "hostPath". Case sensitive. - # Even is supported we are highly recommending to avoid hostPath for security reasons (usually blocked by PSP) - # - `name` - Name of the configMap or secret to be mounted. This also controls - # the path that it is mounted to. The volume will be mounted to `/consul/userconfig/`. - # - `path` - defines where file should be exposed - # - `container` - defines where volume needs to be mounted, must be uwsgi or nginx - # - `subPath` - extracts only particular file from secret or configMap - # - `pathType` - only for hostPath, can be one of the "DirectoryOrCreate", "Directory" (default), "FileOrCreate", - # "File", "Socket", "CharDevice", "BlockDevice" - # - `hostPath` - only for hostPath, file or directory from local host - # @type: array + # A list of additional initContainers to run before the uwsgi and nginx containers. + extraInitContainers: [] + # A list of extra volumes to mount. extraVolumes: [] # This feature needs more preparation before can be enabled, please visit KUBERNETES.md#media-persistent-volume @@ -358,6 +369,7 @@ initializer: keepSeconds: 60 # A positive integer will keep this Job and Pod deployed for the specified number of seconds, after which they will be removed. For all other values, the Job and Pod will remain deployed. affinity: {} nodeSelector: {} + tolerations: [] resources: requests: cpu: 100m @@ -365,38 +377,11 @@ initializer: limits: cpu: 2000m memory: 512Mi - - # A list of extra volumes to mount. This - # is useful for bringing in extra data that can be referenced by other configurations - # at a well known path, such as local_settings. The - # value of this should be a list of objects. - # - # Example: - # - # ```yaml - # extraVolumes: - # - type: configMap - # name: local_settings - # path: /app/dojo/settings/local_settings.py - # subPath: local_settings.py - # - type: hostPath - # name: host_directory - # path: /tmp - # hostPath: /tmp - # ``` - # - # Each object supports the following keys: - # - # - `type` - Type of the volume, must be one of "configMap", "secret", "hostPath". Case sensitive. - # Even is supported we are highly recommending to avoid hostPath for security reasons (usually blocked by PSP) - # - `name` - Name of the configMap or secret to be mounted. This also controls - # the path that it is mounted to. The volume will be mounted to `/consul/userconfig/`. - # - `path` - defines where file should be exposed - # - `subPath` - extracts only particular file from secret or configMap - # - `pathType` - only for hostPath, can be one of the "DirectoryOrCreate", "Directory" (default), "FileOrCreate", - # "File", "Socket", "CharDevice", "BlockDevice" - # - `hostPath` - only for hostPath, file or directory from local host - # @type: array + # Additional environment variables injected to the initializer job pods. + extraEnv: [] + # Array of additional volume mount points for the initializer job (init)containers. + extraVolumeMounts: [] + # A list of extra volumes to attach to the initializer job pods. extraVolumes: [] # staticName defines whether name of the job will be the same (e.g., "defectdojo-initializer") @@ -404,10 +389,9 @@ initializer: # This might be handy for ArgoCD deployments staticName: false +# For more advance options check the bitnami chart documentation: https://github.com/bitnami/charts/tree/main/bitnami/postgresql postgresql: - # To use an external PostgreSQL instance (like CloudSQL), set enabled to false, set items in auth part for authentication, - # and uncomment the line below: - # postgresServer: "127.0.0.1" + # To use an external instance, switch enabled to `false` and set the address in `postgresServer` below enabled: true auth: username: defectdojo @@ -477,26 +461,31 @@ gke: # Only works with serviceAccount.create = true workloadIdentityEmail: "" -# For more advance options check the bitnami chart documentation: https://github.com/bitnami/charts/tree/master/bitnami/redis +# For more advance options check the bitnami chart documentation: https://github.com/bitnami/charts/tree/main/bitnami/redis redis: + # To use an external instance, switch enabled to `false`` and set the address in `redisServer` below enabled: true - scheme: "redis" - transportEncryption: - enabled: false - params: '' auth: existingSecret: defectdojo-redis-specific existingSecretPasswordKey: redis-password password: "" architecture: standalone - # To use an external Redis instance, set enabled to false and uncomment - # the line below: - # redisServer: myrediscluster # To use a different port for Redis (default: 6379) add a port number and uncomment the lines below: # master: # service: # ports: # redis: xxxx + # Sentinel configuration parameters + sentinel: + enabled: false + # If TLS is enabled, the Redis broker will use the redis:// and optionally mount the certificates + # from an existing secret. + tls: + enabled: false + # existingSecret: redis-tls + # certFilename: tls.crt + # certKeyFilename: tls.key + # certCAFilename: ca.crt # To add extra variables not predefined by helm config it is possible to define in extraConfigs block, e.g. below: # NOTE Do not store any kind of sensitive information inside of it @@ -531,3 +520,14 @@ extraConfigs: {} # MIDDLEWARE = [ # 'debug_toolbar.middleware.DebugToolbarMiddleware', # ] + MIDDLEWARE +# +# External database support. +# +# To use an external Redis instance, set `redis.enabled` to false and set the address here: +redisServer: ~ +# Parameters attached to the redis connection string, defaults to "ssl_cert_reqs=optional" if `redis.tls.enabled` +redisParams: "" +# +# To use an external PostgreSQL instance (like CloudSQL), set `postgresql.enabled` to false, +# set items in `postgresql.auth` part for authentication, and set the address here: +postgresServer: ~ From d41ceaf996782f1ed884f390a9a59e21ff9a1bea Mon Sep 17 00:00:00 2001 From: manuelsommer <47991713+manuel-sommer@users.noreply.github.com> Date: Fri, 5 Sep 2025 21:48:02 +0200 Subject: [PATCH 026/154] Ruff: Add PLW (#13104) * Ruff: Add PLW * update --- dojo/models.py | 4 ++-- ruff.toml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/dojo/models.py b/dojo/models.py index 792ddd19639..6cc1cf16b19 100644 --- a/dojo/models.py +++ b/dojo/models.py @@ -2789,8 +2789,8 @@ def save(self, dedupe_option=True, rules_option=True, product_grading_option=Tru if self.pk is None: # We enter here during the first call from serializers.py from dojo.utils import apply_cwe_to_template - self = apply_cwe_to_template(self) - + # No need to use the returned variable since `self` Is updated in memory + apply_cwe_to_template(self) if (self.file_path is not None) and (len(self.unsaved_endpoints) == 0): self.static_finding = True self.dynamic_finding = False diff --git a/ruff.toml b/ruff.toml index 8fe6abd4e1c..9398b76e7e2 100644 --- a/ruff.toml +++ b/ruff.toml @@ -85,7 +85,7 @@ select = [ "PLC01", "PLC02", "PLC0414", "PLC18", "PLC24", "PLC28", "PLC3", "PLE", "PLR01", "PLR02", "PLR04", "PLR0915", "PLR1711", "PLR1704", "PLR1714", "PLR1716", "PLR172", "PLR173", "PLR2044", "PLR5", "PLR6104", "PLR6201", - "PLW01", "PLW02", "PLW04", "PLW0602", "PLW0603", "PLW0604", "PLW07", "PLW1", "PLW2", "PLW3", + "PLW", "UP", "FURB", "RUF", From fc4c132e756d780270cd300b04d56a0a7f7b9d41 Mon Sep 17 00:00:00 2001 From: DefectDojo release bot Date: Mon, 8 Sep 2025 14:55:33 +0000 Subject: [PATCH 027/154] Update versions in application files --- components/package.json | 2 +- dojo/__init__.py | 2 +- helm/defectdojo/Chart.yaml | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/components/package.json b/components/package.json index bf7bc889846..d96e5e51133 100644 --- a/components/package.json +++ b/components/package.json @@ -1,6 +1,6 @@ { "name": "defectdojo", - "version": "2.50.1", + "version": "2.51.0-dev", "license" : "BSD-3-Clause", "private": true, "dependencies": { diff --git a/dojo/__init__.py b/dojo/__init__.py index 82fe1512626..09ee079e553 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.50.1" +__version__ = "2.51.0-dev" __url__ = "https://github.com/DefectDojo/django-DefectDojo" __docs__ = "https://documentation.defectdojo.com" diff --git a/helm/defectdojo/Chart.yaml b/helm/defectdojo/Chart.yaml index eaba06160a6..8365c40bf5d 100644 --- a/helm/defectdojo/Chart.yaml +++ b/helm/defectdojo/Chart.yaml @@ -1,8 +1,8 @@ apiVersion: v2 -appVersion: "2.50.1" +appVersion: "2.51.0-dev" description: A Helm chart for Kubernetes to install DefectDojo name: defectdojo -version: 1.6.206 +version: 1.6.207-dev icon: https://www.defectdojo.org/img/favicon.ico maintainers: - name: madchap From 7295fa867aafdd2680257a41df2d79977930bb2c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 8 Sep 2025 14:45:11 -0500 Subject: [PATCH 028/154] Bump psycopg[c] from 3.2.9 to 3.2.10 (#13133) Bumps [psycopg[c]](https://github.com/psycopg/psycopg) from 3.2.9 to 3.2.10. - [Changelog](https://github.com/psycopg/psycopg/blob/master/docs/news.rst) - [Commits](https://github.com/psycopg/psycopg/compare/3.2.9...3.2.10) --- updated-dependencies: - dependency-name: psycopg[c] dependency-version: 3.2.10 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index e4ec5a03ad0..d40a8eccaf1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -31,7 +31,7 @@ lxml==6.0.1 Markdown==3.9 openpyxl==3.1.5 Pillow==11.3.0 # required by django-imagekit -psycopg[c]==3.2.9 +psycopg[c]==3.2.10 cryptography==45.0.7 python-dateutil==2.9.0.post0 redis==6.4.0 From 3fb18e03005002165d18ece4f186e004f9926fe2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 8 Sep 2025 14:45:42 -0500 Subject: [PATCH 029/154] Bump boto3 from 1.40.24 to 1.40.25 (#13132) Bumps [boto3](https://github.com/boto/boto3) from 1.40.24 to 1.40.25. - [Release notes](https://github.com/boto/boto3/releases) - [Commits](https://github.com/boto/boto3/compare/1.40.24...1.40.25) --- updated-dependencies: - dependency-name: boto3 dependency-version: 1.40.25 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index d40a8eccaf1..ef561addbca 100644 --- a/requirements.txt +++ b/requirements.txt @@ -68,7 +68,7 @@ django-ratelimit==4.1.0 argon2-cffi==25.1.0 blackduck==1.1.3 pycurl==7.45.6 # Required for Celery Broker AWS (SQS) support -boto3==1.40.24 # Required for Celery Broker AWS (SQS) support +boto3==1.40.25 # Required for Celery Broker AWS (SQS) support netaddr==1.3.0 vulners==2.3.7 fontawesomefree==6.6.0 From 09966bde50ff7408edca7c38fc52f80f04ff4d15 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 8 Sep 2025 14:46:07 -0500 Subject: [PATCH 030/154] Update mikefarah/yq action from v4.47.1 to v4.47.2 (.github/workflows/release-x-manual-helm-chart.yml) (#13131) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/release-x-manual-helm-chart.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release-x-manual-helm-chart.yml b/.github/workflows/release-x-manual-helm-chart.yml index fc096eca65b..15ab15634f9 100644 --- a/.github/workflows/release-x-manual-helm-chart.yml +++ b/.github/workflows/release-x-manual-helm-chart.yml @@ -71,7 +71,7 @@ jobs: helm dependency update ./helm/defectdojo - name: Add yq - uses: mikefarah/yq@f03c9dc599c37bfcaf533427211d05e51e6fee64 # v4.47.1 + uses: mikefarah/yq@6251e95af8df3505def48c71f3119836701495d6 # v4.47.2 - name: Pin version docker version id: pin_image From 8665da5a95844a3560c5c99a4811051d4e9c3117 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 8 Sep 2025 14:46:25 -0500 Subject: [PATCH 031/154] Update dependency vite from 7.1.4 to v7.1.5 (docs/package.json) (#13130) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- docs/package-lock.json | 35 +++++++++++++++++++---------------- docs/package.json | 2 +- 2 files changed, 20 insertions(+), 17 deletions(-) diff --git a/docs/package-lock.json b/docs/package-lock.json index 4d13c7f8b65..bd26efc195b 100644 --- a/docs/package-lock.json +++ b/docs/package-lock.json @@ -20,7 +20,7 @@ }, "devDependencies": { "prettier": "3.6.2", - "vite": "7.1.4" + "vite": "7.1.5" }, "engines": { "node": "22.19.0" @@ -4971,14 +4971,14 @@ "license": "MIT" }, "node_modules/tinyglobby": { - "version": "0.2.14", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz", - "integrity": "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==", + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", "dev": true, "license": "MIT", "dependencies": { - "fdir": "^6.4.4", - "picomatch": "^4.0.2" + "fdir": "^6.5.0", + "picomatch": "^4.0.3" }, "engines": { "node": ">=12.0.0" @@ -4988,11 +4988,14 @@ } }, "node_modules/tinyglobby/node_modules/fdir": { - "version": "6.4.4", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.4.tgz", - "integrity": "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==", + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", "dev": true, "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, "peerDependencies": { "picomatch": "^3 || ^4" }, @@ -5003,9 +5006,9 @@ } }, "node_modules/tinyglobby/node_modules/picomatch": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", - "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", "engines": { @@ -5125,9 +5128,9 @@ "license": "MIT" }, "node_modules/vite": { - "version": "7.1.4", - "resolved": "https://registry.npmjs.org/vite/-/vite-7.1.4.tgz", - "integrity": "sha512-X5QFK4SGynAeeIt+A7ZWnApdUyHYm+pzv/8/A57LqSGcI88U6R6ipOs3uCesdc6yl7nl+zNO0t8LmqAdXcQihw==", + "version": "7.1.5", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.1.5.tgz", + "integrity": "sha512-4cKBO9wR75r0BeIWWWId9XK9Lj6La5X846Zw9dFfzMRw38IlTk2iCcUt6hsyiDRcPidc55ZParFYDXi0nXOeLQ==", "dev": true, "license": "MIT", "dependencies": { @@ -5136,7 +5139,7 @@ "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", - "tinyglobby": "^0.2.14" + "tinyglobby": "^0.2.15" }, "bin": { "vite": "bin/vite.js" diff --git a/docs/package.json b/docs/package.json index bad55f886d7..de75ce7b2b6 100644 --- a/docs/package.json +++ b/docs/package.json @@ -23,7 +23,7 @@ }, "devDependencies": { "prettier": "3.6.2", - "vite": "7.1.4" + "vite": "7.1.5" }, "engines": { "node": "22.19.0" From 14f227c69af01a8d6f8eaff20b4fab9ef630c3cc Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 8 Sep 2025 14:47:43 -0500 Subject: [PATCH 032/154] chore(deps): update softprops/action-gh-release action from v2.3.2 to v2.3.3 (.github/workflows/release-x-manual-helm-chart.yml) (#13128) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/release-x-manual-helm-chart.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release-x-manual-helm-chart.yml b/.github/workflows/release-x-manual-helm-chart.yml index 15ab15634f9..f380a29532f 100644 --- a/.github/workflows/release-x-manual-helm-chart.yml +++ b/.github/workflows/release-x-manual-helm-chart.yml @@ -88,7 +88,7 @@ jobs: echo "chart_version=$(ls build | cut -d '-' -f 2,3 | sed 's|\.tgz||')" >> $GITHUB_ENV - name: Create release ${{ inputs.release_number }} - uses: softprops/action-gh-release@72f2c25fcb47643c292f7107632f7a47c1df5cd8 # v2.3.2 + uses: softprops/action-gh-release@6cbd405e2c4e67a21c47fa9e383d020e4e28b836 # v2.3.3 with: name: '${{ inputs.release_number }} 🌈' tag_name: ${{ inputs.release_number }} From 3d37175a815c061b26b22d4031f10bb3d67f629a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 10 Sep 2025 13:40:54 -0500 Subject: [PATCH 033/154] Bump datatables.net from 2.3.3 to 2.3.4 in /components (#13145) Bumps [datatables.net](https://github.com/DataTables/Dist-DataTables) from 2.3.3 to 2.3.4. - [Release notes](https://github.com/DataTables/Dist-DataTables/releases) - [Commits](https://github.com/DataTables/Dist-DataTables/compare/2.3.3...2.3.4) --- updated-dependencies: - dependency-name: datatables.net dependency-version: 2.3.4 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- components/package.json | 2 +- components/yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/components/package.json b/components/package.json index d96e5e51133..d9d376f5551 100644 --- a/components/package.json +++ b/components/package.json @@ -12,7 +12,7 @@ "chosen-bootstrap": "https://github.com/dbtek/chosen-bootstrap", "chosen-js": "^1.8.7", "clipboard": "^2.0.11", - "datatables.net": "^2.3.3", + "datatables.net": "^2.3.4", "datatables.net-buttons-bs": "^3.2.4", "datatables.net-colreorder": "^2.1.1", "drmonty-datatables-plugins": "^1.0.0", diff --git a/components/yarn.lock b/components/yarn.lock index 5a8534dccb8..50109a148aa 100644 --- a/components/yarn.lock +++ b/components/yarn.lock @@ -219,10 +219,10 @@ datatables.net@2.3.2: dependencies: jquery ">=1.7" -datatables.net@^2, datatables.net@^2.3.3: - version "2.3.3" - resolved "https://registry.yarnpkg.com/datatables.net/-/datatables.net-2.3.3.tgz#fe4f96bdbc4cf47c8d11162a7af525ca6a3683d2" - integrity sha512-SWL3za6nheY6gdoiLgCc++tYmxbwrmv2bjrEiII9rXBWXXSbOZct6pjR3FueMVRM5jmt7pQcXiGovfuFDnutQg== +datatables.net@^2, datatables.net@^2.3.4: + version "2.3.4" + resolved "https://registry.yarnpkg.com/datatables.net/-/datatables.net-2.3.4.tgz#8cf69f2e6cb8d271be3d5c4f75a479684d20f253" + integrity sha512-fKuRlrBIdpAl2uIFgl9enKecHB41QmFd/2nN9LBbOvItV/JalAxLcyqdZXex7wX4ZXjnJQEnv6xeS9veOpKzSw== dependencies: jquery ">=1.7" From 83d80ced43083fc085ba43505fc71b7ee02b6708 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 10 Sep 2025 13:54:12 -0500 Subject: [PATCH 034/154] Update postgres:17.6-alpine Docker digest from 17.6 to 17.6-alpine (docker-compose.yml) (#13141) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index 727492e474d..ce41693e937 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -120,7 +120,7 @@ services: source: ./docker/extra_settings target: /app/docker/extra_settings postgres: - image: postgres:17.6-alpine@sha256:3406990b6e4c7192317b6fdc5680498744f6142f01f0287f4ee0420d8c74063c + image: postgres:17.6-alpine@sha256:d5f196a551b5cef1c70853c6dd588f456d16ca4ea733e3f31c75bc1ae2f65f3f environment: POSTGRES_DB: ${DD_DATABASE_NAME:-defectdojo} POSTGRES_USER: ${DD_DATABASE_USER:-defectdojo} From 830e120bc4458f3fd9271c0a96db40c268db2d55 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 10 Sep 2025 14:13:31 -0500 Subject: [PATCH 035/154] Bump datatables.net-buttons-bs from 3.2.4 to 3.2.5 in /components (#13144) Bumps [datatables.net-buttons-bs](https://github.com/DataTables/Dist-DataTables-Buttons-Bootstrap) from 3.2.4 to 3.2.5. - [Release notes](https://github.com/DataTables/Dist-DataTables-Buttons-Bootstrap/releases) - [Commits](https://github.com/DataTables/Dist-DataTables-Buttons-Bootstrap/compare/3.2.4...3.2.5) --- updated-dependencies: - dependency-name: datatables.net-buttons-bs dependency-version: 3.2.5 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- components/package.json | 2 +- components/yarn.lock | 18 +++++++++--------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/components/package.json b/components/package.json index d9d376f5551..88128331a38 100644 --- a/components/package.json +++ b/components/package.json @@ -13,7 +13,7 @@ "chosen-js": "^1.8.7", "clipboard": "^2.0.11", "datatables.net": "^2.3.4", - "datatables.net-buttons-bs": "^3.2.4", + "datatables.net-buttons-bs": "^3.2.5", "datatables.net-colreorder": "^2.1.1", "drmonty-datatables-plugins": "^1.0.0", "drmonty-datatables-responsive": "^1.0.0", diff --git a/components/yarn.lock b/components/yarn.lock index 50109a148aa..78aa6e5e86e 100644 --- a/components/yarn.lock +++ b/components/yarn.lock @@ -187,19 +187,19 @@ datatables.net-bs@^2: datatables.net "2.3.2" jquery ">=1.7" -datatables.net-buttons-bs@^3.2.4: - version "3.2.4" - resolved "https://registry.yarnpkg.com/datatables.net-buttons-bs/-/datatables.net-buttons-bs-3.2.4.tgz#7a883c3ee8c6428fb99e6e6e56c39d0051386039" - integrity sha512-wOljUlsJ4sU5pABim+cwbO61ZFRv4aak1PkNL812i/qFwIEcsji7uz59PAx1ZoP1YdNtetj4Vn7D5oTU+Ijedw== +datatables.net-buttons-bs@^3.2.5: + version "3.2.5" + resolved "https://registry.yarnpkg.com/datatables.net-buttons-bs/-/datatables.net-buttons-bs-3.2.5.tgz#fe9a8085a66cabd723833834f29e68e91f28ea14" + integrity sha512-7fXOIue+2jpWPWcIrAXWH3BjEhMUD8L2pInT0tqfoEcl/3T+CH0Q6dHJRI5RiYmYKO/HLjpCQ5yqYAL5DT7iHA== dependencies: datatables.net-bs "^2" - datatables.net-buttons "3.2.4" + datatables.net-buttons "3.2.5" jquery ">=1.7" -datatables.net-buttons@3.2.4: - version "3.2.4" - resolved "https://registry.yarnpkg.com/datatables.net-buttons/-/datatables.net-buttons-3.2.4.tgz#c58cc0bb518da8738bec6e64a54c1135dc257141" - integrity sha512-anA39/R0kpHA2DOwqEHy/ZMXD5vf4tWmyNO0BnO0kJG7AFNvGTUCWBnBifXYg3G64U6JYpYY+MuTFKIB1/ZMTQ== +datatables.net-buttons@3.2.5: + version "3.2.5" + resolved "https://registry.yarnpkg.com/datatables.net-buttons/-/datatables.net-buttons-3.2.5.tgz#e37fc4f06743e057e8e3e4abfda60c988e7c16da" + integrity sha512-OSTl7evbfe0SMee11lyzu5iv/z8Yp05eh3s1QBte/FNqHcoXN8hlAVSSGpYgk5pj8zwHPYIu6fHeMEue4ARUNg== dependencies: datatables.net "^2" jquery ">=1.7" From 74e28c39fa302adc4af489c30a3c6892ff063e42 Mon Sep 17 00:00:00 2001 From: valentijnscholten Date: Wed, 10 Sep 2025 22:10:54 +0200 Subject: [PATCH 036/154] docker: remove dev dependencies from release images (#13095) * docker: remove dev dependencies from release images * docker: remove dev dependencies from release images --- Dockerfile.django-alpine | 10 ++++++++-- Dockerfile.django-debian | 10 ++++++++-- docker-compose.override.dev.yml | 4 ++++ docker-compose.yml | 2 +- requirements-dev.txt | 15 +++++++++++++++ requirements.txt | 6 ------ 6 files changed, 36 insertions(+), 11 deletions(-) create mode 100644 requirements-dev.txt diff --git a/Dockerfile.django-alpine b/Dockerfile.django-alpine index 79731e55d0d..643492d6b55 100644 --- a/Dockerfile.django-alpine +++ b/Dockerfile.django-alpine @@ -31,7 +31,7 @@ COPY requirements.txt ./ # https://github.com/unbit/uwsgi/issues/1318#issuecomment-542238096 RUN CPUCOUNT=1 pip3 wheel --wheel-dir=/tmp/wheels -r ./requirements.txt -FROM base AS django +FROM base AS release WORKDIR /app ARG uid=1001 ARG gid=1337 @@ -136,5 +136,11 @@ ENV \ DD_UWSGI_NUM_OF_THREADS="4" ENTRYPOINT ["/entrypoint-uwsgi.sh"] -FROM django AS django-unittests +FROM release AS development +USER root +COPY requirements-dev.txt ./ +RUN pip3 install --no-cache-dir -r requirements-dev.txt +USER ${uid} + +FROM development AS django-unittests COPY unittests/ ./unittests/ diff --git a/Dockerfile.django-debian b/Dockerfile.django-debian index 5aea2e4f22e..6b1c4a21f1e 100644 --- a/Dockerfile.django-debian +++ b/Dockerfile.django-debian @@ -30,7 +30,7 @@ COPY requirements.txt ./ # https://github.com/unbit/uwsgi/issues/1318#issuecomment-542238096 RUN CPUCOUNT=1 pip3 wheel --wheel-dir=/tmp/wheels -r ./requirements.txt -FROM base AS django +FROM base AS release WORKDIR /app ARG uid=1001 ARG gid=1337 @@ -139,5 +139,11 @@ ENV \ DD_UWSGI_NUM_OF_THREADS="4" ENTRYPOINT ["/entrypoint-uwsgi.sh"] -FROM django AS django-unittests +FROM release AS development +USER root +COPY requirements-dev.txt ./ +RUN pip3 install --no-cache-dir -r requirements-dev.txt +USER ${uid} + +FROM development AS django-unittests COPY unittests/ ./unittests/ diff --git a/docker-compose.override.dev.yml b/docker-compose.override.dev.yml index 8dfa5a9e19b..c57dc48f749 100644 --- a/docker-compose.override.dev.yml +++ b/docker-compose.override.dev.yml @@ -1,6 +1,10 @@ --- services: uwsgi: + build: + context: . + dockerfile: Dockerfile.django-debian + target: development entrypoint: ['/wait-for-it.sh', '${DD_DATABASE_HOST:-postgres}:${DD_DATABASE_PORT:-5432}', '-t', '30', '--', '/entrypoint-uwsgi-dev.sh'] volumes: - '.:/app:z' diff --git a/docker-compose.yml b/docker-compose.yml index ce41693e937..86854aae595 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -34,7 +34,7 @@ services: build: context: ./ dockerfile: "Dockerfile.django-${DEFECT_DOJO_OS:-debian}" - target: django + target: release image: "defectdojo/defectdojo-django:${DJANGO_VERSION:-latest}" depends_on: initializer: diff --git a/requirements-dev.txt b/requirements-dev.txt new file mode 100644 index 00000000000..abf5c6c8d77 --- /dev/null +++ b/requirements-dev.txt @@ -0,0 +1,15 @@ +# Development-only dependencies for DefectDojo +# These are only needed during development and testing + +# Debug toolbar for development +django-debug-toolbar==6.0.0 +django-debug-toolbar-request-history==0.1.4 + +# Testing dependencies +vcrpy==7.0.0 +vcrpy-unittest==0.1.7 +django-test-migrations==1.4.0 +parameterized==0.9.0 + +# Development file watching (hot reload) +watchdog==6.0.0 diff --git a/requirements.txt b/requirements.txt index ef561addbca..9dea168ab17 100644 --- a/requirements.txt +++ b/requirements.txt @@ -51,16 +51,11 @@ packageurl-python==0.17.5 django-crum==0.7.9 JSON-log-formatter==1.1.1 django-split-settings==1.3.2 -django-debug-toolbar==5.2.0 -django-debug-toolbar-request-history==0.1.4 -vcrpy==7.0.0 -vcrpy-unittest==0.1.7 django-tagulous==2.1.1 PyJWT==2.10.1 cvss==3.6 django-fieldsignals==0.7.0 hyperlink==21.0.0 -django-test-migrations==1.4.0 djangosaml2==1.11.1 drf-spectacular==0.28.0 drf-spectacular-sidecar==2025.9.1 @@ -75,4 +70,3 @@ fontawesomefree==6.6.0 PyYAML==6.0.2 pyopenssl==25.1.0 parameterized==0.9.0 -watchdog==6.0.0 # only needed for development, but would require some docker refactoring if we want to exclude it for production images From 4400959503e25cab26179bb893c56269f621bcb6 Mon Sep 17 00:00:00 2001 From: testaccount90009 <122134756+testaccount90009@users.noreply.github.com> Date: Wed, 10 Sep 2025 13:11:43 -0700 Subject: [PATCH 037/154] Rename duplicate 'Dashboard' to 'Finding Groups' (#13140) Rename duplicate 'Dashboard' to 'Finding Groups' --- dojo/templates/base.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dojo/templates/base.html b/dojo/templates/base.html index 007612ce741..b0a2b914ec4 100644 --- a/dojo/templates/base.html +++ b/dojo/templates/base.html @@ -341,7 +341,7 @@