From b976db2d2876ce17012f1072d0567e21b8c4bd73 Mon Sep 17 00:00:00 2001 From: DefectDojo release bot Date: Mon, 6 Oct 2025 18:09:15 +0000 Subject: [PATCH 001/126] Update versions in application files --- components/package.json | 2 +- docs/content/en/open_source/upgrading/2.52.md | 7 +++++++ dojo/__init__.py | 2 +- helm/defectdojo/Chart.yaml | 4 ++-- helm/defectdojo/README.md | 2 +- 5 files changed, 12 insertions(+), 5 deletions(-) create mode 100644 docs/content/en/open_source/upgrading/2.52.md diff --git a/components/package.json b/components/package.json index 09954b463c9..e5898ca8f4d 100644 --- a/components/package.json +++ b/components/package.json @@ -1,6 +1,6 @@ { "name": "defectdojo", - "version": "2.51.0", + "version": "2.52.0-dev", "license" : "BSD-3-Clause", "private": true, "dependencies": { diff --git a/docs/content/en/open_source/upgrading/2.52.md b/docs/content/en/open_source/upgrading/2.52.md new file mode 100644 index 00000000000..2cc20c6b446 --- /dev/null +++ b/docs/content/en/open_source/upgrading/2.52.md @@ -0,0 +1,7 @@ +--- +title: 'Upgrading to DefectDojo Version 2.52.x' +toc_hide: true +weight: -20251006 +description: No special instructions. +--- +There are no special instructions for upgrading to 2.52.x. Check the [Release Notes](https://github.com/DefectDojo/django-DefectDojo/releases/tag/2.52.0) for the contents of the release. diff --git a/dojo/__init__.py b/dojo/__init__.py index 3ca651bd880..0a21544849b 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.51.0" +__version__ = "2.52.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 6401f7bb41a..7dfd0153e86 100644 --- a/helm/defectdojo/Chart.yaml +++ b/helm/defectdojo/Chart.yaml @@ -1,8 +1,8 @@ apiVersion: v2 -appVersion: "2.51.0" +appVersion: "2.52.0-dev" description: A Helm chart for Kubernetes to install DefectDojo name: defectdojo -version: 1.7.0 +version: 1.7.1-dev icon: https://defectdojo.com/hubfs/DefectDojo_favicon.png maintainers: - name: madchap diff --git a/helm/defectdojo/README.md b/helm/defectdojo/README.md index 3db53e1cb21..4ca6d85d2c8 100644 --- a/helm/defectdojo/README.md +++ b/helm/defectdojo/README.md @@ -495,7 +495,7 @@ kubectl delete pvc data-defectdojo-redis-0 data-defectdojo-postgresql-0 # General information about chart values -![Version: 1.7.0](https://img.shields.io/badge/Version-1.7.0-informational?style=flat-square) ![AppVersion: 2.51.0](https://img.shields.io/badge/AppVersion-2.51.0-informational?style=flat-square) +![Version: 1.7.1-dev](https://img.shields.io/badge/Version-1.7.1--dev-informational?style=flat-square) ![AppVersion: 2.52.0-dev](https://img.shields.io/badge/AppVersion-2.52.0--dev-informational?style=flat-square) A Helm chart for Kubernetes to install DefectDojo From bb254044bcfb2d32b077986698dff0e478c83103 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 7 Oct 2025 09:05:59 -0500 Subject: [PATCH 002/126] :arrow_up: Bump django-pghistory from 3.7.0 to 3.8.3 (#13347) Bumps [django-pghistory](https://github.com/AmbitionEng/django-pghistory) from 3.7.0 to 3.8.3. - [Release notes](https://github.com/AmbitionEng/django-pghistory/releases) - [Changelog](https://github.com/AmbitionEng/django-pghistory/blob/main/CHANGELOG.md) - [Commits](https://github.com/AmbitionEng/django-pghistory/compare/3.7.0...3.8.3) --- updated-dependencies: - dependency-name: django-pghistory dependency-version: 3.8.3 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 2c61575c349..08a88501bc1 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-pghistory==3.7.0 +django-pghistory==3.8.3 django-dbbackup==5.0.0 django-environ==0.12.0 django-filter==25.1 From 405ac921708c2b09154528bcd3fb1b81eaf4a526 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 7 Oct 2025 09:48:41 -0500 Subject: [PATCH 003/126] Update redis:7.2.11-alpine Docker digest from 7.2.11 to v (docker-compose.yml) (#13325) 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 f18651fa52e..e5238fa6b8b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -129,7 +129,7 @@ services: - defectdojo_postgres:/var/lib/postgresql/data redis: # Pinning to this version due to licensing constraints - image: redis:7.2.11-alpine@sha256:7632e82373929f39cdbead93f2e45d8b3cd295072c4755e00e7e6b19d56cc512 + image: redis:7.2.11-alpine@sha256:b19a2cb55412c26e0c3725011ac04397eba5cb0d7f090f739cbbce8dc97f8e60 volumes: - defectdojo_redis:/data volumes: From 1954c8c113b470e1190cc38dca046bcc3d8f20ef Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 7 Oct 2025 10:03:43 -0500 Subject: [PATCH 004/126] :arrow_up: Bump vulners from 2.3.7 to 3.1.1 (#13342) Bumps vulners from 2.3.7 to 3.1.1. --- updated-dependencies: - dependency-name: vulners dependency-version: 3.1.1 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 08a88501bc1..570c2eec20f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -62,7 +62,7 @@ blackduck==1.1.3 pycurl==7.45.7 # Required for Celery Broker AWS (SQS) support boto3==1.40.44 # Required for Celery Broker AWS (SQS) support netaddr==1.3.0 -vulners==2.3.7 +vulners==3.1.1 fontawesomefree==6.6.0 PyYAML==6.0.3 pyopenssl==25.3.0 From 18cd4b623a5bf9fc6bea66218044d16dbf2a2647 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 7 Oct 2025 10:11:55 -0500 Subject: [PATCH 005/126] :arrow_up: Bump social-auth-app-django from 5.4.3 to 5.5.1 (#13344) Bumps [social-auth-app-django](https://github.com/python-social-auth/social-app-django) from 5.4.3 to 5.5.1. - [Release notes](https://github.com/python-social-auth/social-app-django/releases) - [Changelog](https://github.com/python-social-auth/social-app-django/blob/master/CHANGELOG.md) - [Commits](https://github.com/python-social-auth/social-app-django/compare/5.4.3...5.5.1) --- updated-dependencies: - dependency-name: social-auth-app-django dependency-version: 5.5.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 570c2eec20f..ea5d717efb5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -39,7 +39,7 @@ uWSGI==2.0.30 vobject==0.9.9 whitenoise==5.2.0 titlecase==2.4.1 -social-auth-app-django==5.4.3 +social-auth-app-django==5.5.1 social-auth-core==4.7.0 gitpython==3.1.45 python-gitlab==6.4.0 From 1c497036563bbc7570f5d3aaa813f44cda4d4c4c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 7 Oct 2025 10:14:35 -0500 Subject: [PATCH 006/126] :arrow_up: Bump jira from 3.8.0 to 3.10.5 (#13345) Bumps [jira](https://github.com/pycontribs/jira) from 3.8.0 to 3.10.5. - [Release notes](https://github.com/pycontribs/jira/releases) - [Changelog](https://github.com/pycontribs/jira/blob/main/RELEASE.md) - [Commits](https://github.com/pycontribs/jira/compare/3.8.0...3.10.5) --- updated-dependencies: - dependency-name: jira dependency-version: 3.10.5 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 ea5d717efb5..417942a4d13 100644 --- a/requirements.txt +++ b/requirements.txt @@ -22,7 +22,7 @@ Django==5.1.12 djangorestframework==3.16.1 html2text==2025.4.15 humanize==4.13.0 -jira==3.8.0 +jira==3.10.5 PyGithub==2.8.1 lxml==6.0.2 Markdown==3.9 From b662f137ee29a2a5bf47719fd4a7ca54a88db139 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 7 Oct 2025 10:15:06 -0500 Subject: [PATCH 007/126] chore(deps): update actions/stale action from v9.1.0 to v10 (.github/workflows/close-stale.yml) (#13349) 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 0b371f1cb60..857f619c78b 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 issues and PRs that are pending closure - uses: actions/stale@5bef64f19d7facfb25b37b414482c7164d639639 # v9.1.0 + uses: actions/stale@5f858e3efba33a5ca4407a664cc011ad407f2008 # v10.1.0 with: # Disable automatic stale marking - only close manually labeled items days-before-stale: -1 From ea8b74d476e2a3cb66faf60753863cb7e48635b3 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 7 Oct 2025 10:16:07 -0500 Subject: [PATCH 008/126] chore(deps): update softprops/action-gh-release action from v2.3.4 to v2.4.0 (.github/workflows/release-x-manual-helm-chart.yml) (#13358) 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 bd09c558bf9..f0e3c586f64 100644 --- a/.github/workflows/release-x-manual-helm-chart.yml +++ b/.github/workflows/release-x-manual-helm-chart.yml @@ -87,7 +87,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@62c96d0c4e8a889135c1f3a25910db8dbe0e85f7 # v2.3.4 + uses: softprops/action-gh-release@aec2ec56f94eb8180ceec724245f64ef008b89f5 # v2.4.0 with: name: '${{ inputs.release_number }} 🌈' tag_name: ${{ inputs.release_number }} From 6a826fa4b71be6136a9b07cafc1898f78c077317 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 7 Oct 2025 11:02:15 -0500 Subject: [PATCH 009/126] Bump boto3 from 1.40.44 to 1.40.46 (#13361) Bumps [boto3](https://github.com/boto/boto3) from 1.40.44 to 1.40.46. - [Release notes](https://github.com/boto/boto3/releases) - [Commits](https://github.com/boto/boto3/compare/1.40.44...1.40.46) --- updated-dependencies: - dependency-name: boto3 dependency-version: 1.40.46 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 417942a4d13..579acbfb8ab 100644 --- a/requirements.txt +++ b/requirements.txt @@ -60,7 +60,7 @@ django-ratelimit==4.1.0 argon2-cffi==25.1.0 blackduck==1.1.3 pycurl==7.45.7 # Required for Celery Broker AWS (SQS) support -boto3==1.40.44 # Required for Celery Broker AWS (SQS) support +boto3==1.40.46 # Required for Celery Broker AWS (SQS) support netaddr==1.3.0 vulners==3.1.1 fontawesomefree==6.6.0 From 2da05de366e8cc936dd1e03a25aab24f54d65ef5 Mon Sep 17 00:00:00 2001 From: kiblik <5609770+kiblik@users.noreply.github.com> Date: Tue, 7 Oct 2025 18:41:58 +0200 Subject: [PATCH 010/126] fix(helm): Fix checker of HELM chart change (#13310) --- .github/workflows/test-helm-chart.yml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test-helm-chart.yml b/.github/workflows/test-helm-chart.yml index 7e89d2ac7fd..0e837df2167 100644 --- a/.github/workflows/test-helm-chart.yml +++ b/.github/workflows/test-helm-chart.yml @@ -68,6 +68,10 @@ jobs: - name: Check update of "artifacthub.io/changes" HELM annotation if: env.changed == 'true' run: | + # fast fail if `git show` fails + set -e + set -o pipefail + target_branch=${{ env.ct-branch }} echo "Checking Chart.yaml annotation changes" @@ -76,10 +80,10 @@ jobs: current_annotation=$(yq e '.annotations."artifacthub.io/changes"' "helm/defectdojo/Chart.yaml") # Get target branch version of Chart.yaml annotation - target_annotation=$(git show "${{ env.ct-branch }}:helm/defectdojo/Chart.yaml" | yq e '.annotations."artifacthub.io/changes"' -) + target_annotation=$(git show "origin/${{ env.ct-branch }}:helm/defectdojo/Chart.yaml" | yq e '.annotations."artifacthub.io/changes"' -) if [[ "$current_annotation" == "$target_annotation" ]]; then - echo "::error file=helm/defectdojo/Chart.yaml::The 'artifacthub.io/changes' annotation has not been updated compared to ${{ env.ct-branch }}" + echo "::error::The HELM chart has been updated but the 'artifacthub.io/changes' annotation in 'Chart.yaml' has not been changed (compared to '${{ env.ct-branch }}' branch)" exit 1 fi From 4f38f2f82b6f4524447d9acf80bf87138b0824ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=2E=20Fern=C3=A1ndez?= <7312236+fernandezcuesta@users.noreply.github.com> Date: Tue, 7 Oct 2025 18:42:58 +0200 Subject: [PATCH 011/126] fix: add missing resources, securityContext and env entries (#13210) * fix: add missing resources, securityContext and env entries * chore: docs and schema * fix: missing securityContext for initializer job * fix: add resources to all cloudsql containers * chore: add missing explicit namespace * chore: refactor, split container and pod security context * chore: docs and schema * fix: lint * chore: sort helper * fix: lint and add changes to release notes * chore: trigger CI * chore: move to 2.52, fix pending issues * chore: docs --- docs/content/en/open_source/upgrading/2.52.md | 32 +++++- helm/defectdojo/README.md | 31 ++++- helm/defectdojo/templates/_helpers.tpl | 99 ++++++++++++---- .../templates/celery-beat-deployment.yaml | 59 ++++++---- .../templates/celery-worker-deployment.yaml | 59 ++++++---- .../configmap-local-settings-py.yaml | 14 ++- helm/defectdojo/templates/configmap.yaml | 19 ++-- .../templates/django-deployment.yaml | 103 +++++++++++++---- helm/defectdojo/templates/django-ingress.yaml | 34 +++--- helm/defectdojo/templates/django-service.yaml | 19 ++-- helm/defectdojo/templates/extra-secret.yaml | 22 ++-- .../templates/gke-managed-certificate.yaml | 15 ++- .../defectdojo/templates/initializer-job.yaml | 48 ++++++-- helm/defectdojo/templates/media-pvc.yaml | 15 ++- helm/defectdojo/templates/network-policy.yaml | 34 +++--- helm/defectdojo/templates/sa.yaml | 29 ++--- .../templates/secret-postgresql.yaml | 24 ++-- helm/defectdojo/templates/secret-redis.yaml | 24 ++-- helm/defectdojo/templates/secret.yaml | 40 ++++--- helm/defectdojo/values.schema.json | 106 ++++++++++++++++-- helm/defectdojo/values.yaml | 82 +++++++++++--- 21 files changed, 661 insertions(+), 247 deletions(-) diff --git a/docs/content/en/open_source/upgrading/2.52.md b/docs/content/en/open_source/upgrading/2.52.md index 2cc20c6b446..b15986e5228 100644 --- a/docs/content/en/open_source/upgrading/2.52.md +++ b/docs/content/en/open_source/upgrading/2.52.md @@ -2,6 +2,34 @@ title: 'Upgrading to DefectDojo Version 2.52.x' toc_hide: true weight: -20251006 -description: No special instructions. +description: Helm chart changes. --- -There are no special instructions for upgrading to 2.52.x. Check the [Release Notes](https://github.com/DefectDojo/django-DefectDojo/releases/tag/2.52.0) for the contents of the release. + +## Helm Chart Changes + +This release introduces more important changes to the Helm chart configuration: + +### Breaking changes + +#### Security context + +This Helm chart extends security context capabilities to all deployed pods and containers. +You can define a default pod and container security context globally using `securityContext.podSecurityContext` and `securityContext.containerSecurityContext` keys. +Additionally, each deployment can specify its own pod and container security contexts, which will override or merge with the global ones. + +#### Fine-grained resources + +Now each container can specify the resource requests and limits. + +#### Moved values + +The following Helm chart values have been modified in this release: + +- `securityContext.djangoSecurityContext` β†’ deprecated in favor of container-specific security contexts (`celery.beat.containerSecurityContext`, `celery.worker.containerSecurityContext`, `django.uwsgi.containerSecurityContext` and `dbMigrationChecker.containerSecurityContext`) +- `securityContext.nginxSecurityContext` β†’ deprecated in favor of container-specific security contexts (`django.nginx.containerSecurityContext`) + +### Other changes + +- **Extra annotations**: Now we can add common annotations to all resources. + +There are other instructions for upgrading to 2.52.x. Check the [Release Notes](https://github.com/DefectDojo/django-DefectDojo/releases/tag/2.52.0) for the contents of the release. diff --git a/helm/defectdojo/README.md b/helm/defectdojo/README.md index 4ca6d85d2c8..fac1345d8c2 100644 --- a/helm/defectdojo/README.md +++ b/helm/defectdojo/README.md @@ -524,10 +524,11 @@ A Helm chart for Kubernetes to install DefectDojo | admin.password | string | `""` | | | admin.secretKey | string | `""` | | | admin.user | string | `"admin"` | | -| annotations | object | `{}` | | +| alternativeHosts | list | `[]` | | | celery.annotations | object | `{}` | | | celery.beat.affinity | object | `{}` | | | celery.beat.annotations | object | `{}` | | +| celery.beat.containerSecurityContext | object | `{}` | | | celery.beat.extraEnv | list | `[]` | | | celery.beat.extraInitContainers | list | `[]` | | | celery.beat.extraVolumeMounts | list | `[]` | | @@ -535,6 +536,7 @@ A Helm chart for Kubernetes to install DefectDojo | celery.beat.livenessProbe | object | `{}` | | | celery.beat.nodeSelector | object | `{}` | | | celery.beat.podAnnotations | object | `{}` | | +| celery.beat.podSecurityContext | object | `{}` | | | celery.beat.readinessProbe | object | `{}` | | | celery.beat.replicas | int | `1` | | | celery.beat.resources.limits.cpu | string | `"2000m"` | | @@ -548,6 +550,7 @@ A Helm chart for Kubernetes to install DefectDojo | celery.worker.affinity | object | `{}` | | | celery.worker.annotations | object | `{}` | | | celery.worker.appSettings.poolType | string | `"solo"` | | +| celery.worker.containerSecurityContext | object | `{}` | | | celery.worker.extraEnv | list | `[]` | | | celery.worker.extraInitContainers | list | `[]` | | | celery.worker.extraVolumeMounts | list | `[]` | | @@ -555,6 +558,7 @@ A Helm chart for Kubernetes to install DefectDojo | celery.worker.livenessProbe | object | `{}` | | | celery.worker.nodeSelector | object | `{}` | | | celery.worker.podAnnotations | object | `{}` | | +| celery.worker.podSecurityContext | object | `{}` | | | celery.worker.readinessProbe | object | `{}` | | | celery.worker.replicas | int | `1` | | | celery.worker.resources.limits.cpu | string | `"2000m"` | | @@ -563,18 +567,25 @@ A Helm chart for Kubernetes to install DefectDojo | celery.worker.resources.requests.memory | string | `"128Mi"` | | | celery.worker.startupProbe | object | `{}` | | | celery.worker.tolerations | list | `[]` | | +| cloudsql.containerSecurityContext | object | `{}` | | | cloudsql.enable_iam_login | bool | `false` | | | cloudsql.enabled | bool | `false` | | +| cloudsql.extraEnv | list | `[]` | | +| cloudsql.extraVolumeMounts | list | `[]` | | | cloudsql.image.pullPolicy | string | `"IfNotPresent"` | | | cloudsql.image.repository | string | `"gcr.io/cloudsql-docker/gce-proxy"` | | | cloudsql.image.tag | string | `"1.37.9"` | | | cloudsql.instance | string | `""` | | +| cloudsql.resources | object | `{}` | | | cloudsql.use_private_ip | bool | `false` | | | cloudsql.verbose | bool | `true` | | | createPostgresqlSecret | bool | `false` | | | createRedisSecret | bool | `false` | | | createSecret | bool | `false` | | +| dbMigrationChecker.containerSecurityContext | object | `{}` | | | dbMigrationChecker.enabled | bool | `true` | | +| dbMigrationChecker.extraEnv | list | `[]` | | +| dbMigrationChecker.extraVolumeMounts | list | `[]` | | | dbMigrationChecker.resources.limits.cpu | string | `"200m"` | | | dbMigrationChecker.resources.limits.memory | string | `"200Mi"` | | | dbMigrationChecker.resources.requests.cpu | string | `"100m"` | | @@ -582,7 +593,9 @@ A Helm chart for Kubernetes to install DefectDojo | disableHooks | bool | `false` | | | django.affinity | object | `{}` | | | django.annotations | object | `{}` | | +| django.extraEnv | list | `[]` | | | django.extraInitContainers | list | `[]` | | +| django.extraVolumeMounts | list | `[]` | | | django.extraVolumes | list | `[]` | | | django.ingress.activateTLS | bool | `true` | | | django.ingress.annotations | object | `{}` | | @@ -598,6 +611,7 @@ A Helm chart for Kubernetes to install DefectDojo | django.mediaPersistentVolume.persistentVolumeClaim.size | string | `"5Gi"` | | | django.mediaPersistentVolume.persistentVolumeClaim.storageClassName | string | `""` | | | django.mediaPersistentVolume.type | string | `"emptyDir"` | | +| django.nginx.containerSecurityContext.runAsUser | int | `1001` | | | django.nginx.extraEnv | list | `[]` | | | django.nginx.extraVolumeMounts | list | `[]` | | | django.nginx.resources.limits.cpu | string | `"2000m"` | | @@ -607,6 +621,7 @@ A Helm chart for Kubernetes to install DefectDojo | django.nginx.tls.enabled | bool | `false` | | | django.nginx.tls.generateCertificate | bool | `false` | | | django.nodeSelector | object | `{}` | | +| django.podSecurityContext.fsGroup | int | `1001` | | | django.replicas | int | `1` | | | django.service.annotations | object | `{}` | | | django.service.type | string | `""` | | @@ -619,6 +634,7 @@ A Helm chart for Kubernetes to install DefectDojo | django.uwsgi.certificates.certMountPath | string | `"/certs/"` | | | django.uwsgi.certificates.configName | string | `"defectdojo-ca-certs"` | | | django.uwsgi.certificates.enabled | bool | `false` | | +| django.uwsgi.containerSecurityContext.runAsUser | int | `1001` | | | django.uwsgi.enableDebug | bool | `false` | | | django.uwsgi.extraEnv | list | `[]` | | | django.uwsgi.extraVolumeMounts | list | `[]` | | @@ -644,6 +660,7 @@ A Helm chart for Kubernetes to install DefectDojo | django.uwsgi.startupProbe.periodSeconds | int | `5` | | | django.uwsgi.startupProbe.successThreshold | int | `1` | | | django.uwsgi.startupProbe.timeoutSeconds | int | `1` | | +| extraAnnotations | object | `{}` | | | extraConfigs | object | `{}` | | | extraEnv | list | `[]` | | | extraLabels | object | `{}` | | @@ -656,6 +673,7 @@ A Helm chart for Kubernetes to install DefectDojo | imagePullSecrets | string | `nil` | | | initializer.affinity | object | `{}` | | | initializer.annotations | object | `{}` | | +| initializer.containerSecurityContext | object | `{}` | | | initializer.extraEnv | list | `[]` | | | initializer.extraVolumeMounts | list | `[]` | | | initializer.extraVolumes | list | `[]` | | @@ -663,6 +681,7 @@ A Helm chart for Kubernetes to install DefectDojo | initializer.keepSeconds | int | `60` | | | initializer.labels | object | `{}` | | | initializer.nodeSelector | object | `{}` | | +| initializer.podSecurityContext | object | `{}` | | | initializer.resources.limits.cpu | string | `"2000m"` | | | initializer.resources.limits.memory | string | `"512Mi"` | | | initializer.resources.requests.cpu | string | `"100m"` | | @@ -672,9 +691,13 @@ A Helm chart for Kubernetes to install DefectDojo | initializer.tolerations | list | `[]` | | | localsettingspy | string | `""` | | | monitoring.enabled | bool | `false` | | +| monitoring.prometheus.containerSecurityContext | object | `{}` | | | monitoring.prometheus.enabled | bool | `false` | | +| monitoring.prometheus.extraEnv | list | `[]` | | +| monitoring.prometheus.extraVolumeMounts | list | `[]` | | | monitoring.prometheus.image | string | `"nginx/nginx-prometheus-exporter:1.4.2"` | | | monitoring.prometheus.imagePullPolicy | string | `"IfNotPresent"` | | +| monitoring.prometheus.resources | object | `{}` | | | networkPolicy.annotations | object | `{}` | | | networkPolicy.egress | list | `[]` | | | networkPolicy.enabled | bool | `false` | | @@ -715,12 +738,14 @@ A Helm chart for Kubernetes to install DefectDojo | repositoryPrefix | string | `"defectdojo"` | | | revisionHistoryLimit | int | `10` | | | secrets.annotations | object | `{}` | | -| securityContext.djangoSecurityContext.runAsUser | int | `1001` | | +| securityContext.containerSecurityContext.runAsNonRoot | bool | `true` | | | securityContext.enabled | bool | `true` | | -| securityContext.nginxSecurityContext.runAsUser | int | `1001` | | +| securityContext.podSecurityContext.runAsNonRoot | bool | `true` | | | serviceAccount.annotations | object | `{}` | | | serviceAccount.create | bool | `true` | | | serviceAccount.labels | object | `{}` | | +| serviceAccount.name | string | `""` | | +| siteUrl | string | `""` | | | tag | string | `"latest"` | | | tests.unitTests.resources.limits.cpu | string | `"500m"` | | | tests.unitTests.resources.limits.memory | string | `"512Mi"` | | diff --git a/helm/defectdojo/templates/_helpers.tpl b/helm/defectdojo/templates/_helpers.tpl index 025b35078db..c4b6f130ab0 100644 --- a/helm/defectdojo/templates/_helpers.tpl +++ b/helm/defectdojo/templates/_helpers.tpl @@ -1,15 +1,15 @@ -{{/* vim: set filetype=mustache: */}} -{{/* -Expand the name of the chart. +{{- /* vim: set filetype=mustache: */}} +{{- /* + Expand the name of the chart. */}} {{- define "defectdojo.name" -}} {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}} {{- end -}} -{{/* -Create a default fully qualified app name. -We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). -If release name contains chart name it will be used as a full name. +{{- /* + Create a default fully qualified app name. + We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). + If release name contains chart name it will be used as a full name. */}} {{- define "defectdojo.fullname" -}} {{- if .Values.fullnameOverride -}} @@ -24,15 +24,15 @@ If release name contains chart name it will be used as a full name. {{- end -}} {{- end -}} -{{/* -Create chart name and version as used by the chart label. +{{- /* + Create chart name and version as used by the chart label. */}} {{- define "defectdojo.chart" -}} {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}} {{- end -}} -{{/* -Create the name of the service account to use +{{- /* + Create the name of the service account to use */}} {{- define "defectdojo.serviceAccountName" -}} {{- if .Values.serviceAccount.create -}} @@ -42,7 +42,7 @@ Create the name of the service account to use {{- end -}} {{- end -}} -{{/* +{{- /* Determine the hostname to use for PostgreSQL/Redis. */}} {{- define "postgresql.hostname" -}} @@ -67,7 +67,7 @@ Create the name of the service account to use {{- end -}} {{- end -}} -{{/* +{{- /* Determine the protocol to use for Redis. */}} {{- define "redis.scheme" -}} @@ -82,7 +82,7 @@ Create the name of the service account to use {{- end -}} {{- end -}} -{{/* +{{- /* Builds the repository names for use with local or private registries */}} {{- define "celery.repository" -}} @@ -109,7 +109,7 @@ Create the name of the service account to use {{- end -}} {{- end -}} -{{/* +{{- /* Creates the array for DD_ALLOWED_HOSTS in configmap */}} {{- define "django.allowed_hosts" -}} @@ -121,7 +121,7 @@ Create the name of the service account to use {{- end -}} {{- end -}} -{{/* +{{- /* Creates the persistentVolumeName */}} {{- define "django.pvc_name" -}} @@ -132,7 +132,7 @@ Create the name of the service account to use {{- end -}} {{- end -}} -{{/* +{{- /* Define db-migration-checker */}} {{- define "dbMigrationChecker" -}} @@ -145,7 +145,11 @@ Create the name of the service account to use imagePullPolicy: {{ .Values.imagePullPolicy }} {{- if .Values.securityContext.enabled }} securityContext: - {{- toYaml .Values.securityContext.djangoSecurityContext | nindent 4 }} + {{- include "helpers.securityContext" (list + .Values + "securityContext.containerSecurityContext" + "dbMigrationChecker.containerSecurityContext" + ) | nindent 4 }} {{- end }} envFrom: - configMapRef: @@ -163,9 +167,64 @@ Create the name of the service account to use secretKeyRef: name: {{ .Values.postgresql.auth.existingSecret | default "defectdojo-postgresql-specific" }} key: {{ .Values.postgresql.auth.secretKeys.userPasswordKey | default "postgresql-password" }} - {{- if .Values.extraEnv }} - {{- toYaml .Values.extraEnv | nindent 2 }} + {{- with .Values.extraEnv }} + {{- toYaml . | nindent 2 }} + {{- end }} + {{- with.Values.dbMigrationChecker.extraEnv }} + {{- toYaml . | nindent 2 }} {{- end }} resources: {{- toYaml .Values.dbMigrationChecker.resources | nindent 4 }} + {{- with .Values.dbMigrationChecker.extraVolumeMounts }} + volumeMounts: + {{- . | toYaml | nindent 4 }} + {{- end }} +{{- end -}} + +{{- /* +Returns the JSON representation of the value for a dot-notation path +from a given context. + Args: + 0: context (e.g., .Values) + 1: path (e.g., "foo.bar") +*/}} +{{- define "helpers.getValue" -}} + {{- $ctx := merge dict (index . 0) -}} + {{- $path := index . 1 -}} + {{- $parts := splitList "." $path -}} + {{- $value := $ctx -}} + {{- range $idx, $part := $parts -}} + {{- if kindIs "map" $value -}} + {{- $value = index $value $part -}} + {{- else -}} + {{- $value = "" -}} + {{- /* Exit early by setting to last iteration */}} + {{- $idx = sub (len $parts) 1 -}} + {{- end -}} + {{- end -}} + {{- toJson $value -}} +{{- end -}} + +{{- /* + Build the security context. + Args: + 0: values context (.Values) + 1: the default security context key (e.g. "securityContext.containerSecurityContext") + 2: the key under the context with security context (e.g., "foo.bar") +*/}} +{{- define "helpers.securityContext" -}} +{{- $values := merge dict (index . 0) -}} +{{- $defaultSecurityContextKey := index . 1 -}} +{{- $securityContextKey := index . 2 -}} +{{- $securityContext := dict -}} +{{- with $values }} + {{- $securityContext = (merge + $securityContext + (include "helpers.getValue" (list $values $defaultSecurityContextKey) | fromJson) + (include "helpers.getValue" (list $values $securityContextKey) | fromJson) + ) -}} +{{- end -}} +{{- with $securityContext -}} +{{- . | toYaml | nindent 2 -}} +{{- end -}} {{- end -}} diff --git a/helm/defectdojo/templates/celery-beat-deployment.yaml b/helm/defectdojo/templates/celery-beat-deployment.yaml index 166f6c2afeb..8a80d0ffec7 100644 --- a/helm/defectdojo/templates/celery-beat-deployment.yaml +++ b/helm/defectdojo/templates/celery-beat-deployment.yaml @@ -2,7 +2,12 @@ apiVersion: apps/v1 kind: Deployment metadata: - name: {{ $fullName }}-celery-beat + {{- with mergeOverwrite dict .Values.extraAnnotations .Values.celery.annotations .Values.celery.beat.annotations }} + annotations: + {{- range $key, $value := . }} + {{ $key }}: {{ quote $value }} + {{- end }} + {{- end }} labels: defectdojo.org/component: celery defectdojo.org/subcomponent: beat @@ -10,13 +15,11 @@ metadata: app.kubernetes.io/instance: {{ .Release.Name }} app.kubernetes.io/managed-by: {{ .Release.Service }} helm.sh/chart: {{ include "defectdojo.chart" . }} - {{- with .Values.extraLabels }} - {{- toYaml . | nindent 4 }} + {{- range $key, $value := .Values.extraLabels }} + {{ $key }}: {{ quote $value }} {{- end }} - {{- with mergeOverwrite .Values.celery.annotations .Values.celery.beat.annotations }} - annotations: - {{- toYaml . | nindent 4 }} - {{- end }} + name: {{ $fullName }}-celery-beat + namespace: {{ .Release.Namespace }} spec: replicas: {{ .Values.celery.beat.replicas }} {{- with .Values.revisionHistoryLimit }} @@ -35,15 +38,12 @@ spec: defectdojo.org/subcomponent: beat app.kubernetes.io/name: {{ include "defectdojo.name" . }} app.kubernetes.io/instance: {{ .Release.Name }} - {{- with .Values.extraLabels }} - {{- toYaml . | nindent 8 }} - {{- end }} - {{- with .Values.podLabels }} - {{- toYaml . | nindent 8 }} + {{- range $key, $value := mergeOverwrite dict .Values.extraLabels .Values.podLabels }} + {{ $key }}: {{ quote $value }} {{- end }} annotations: - {{- with mergeOverwrite .Values.celery.annotations .Values.celery.beat.podAnnotations }} - {{- toYaml . | nindent 8 }} + {{- range $key, $value := mergeOverwrite dict .Values.extraAnnotations .Values.celery.annotations .Values.celery.beat.podAnnotations }} + {{ $key }}: {{ quote $value }} {{- end }} {{- if eq (.Values.trackConfig | default "disabled") "enabled" }} checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }} @@ -51,6 +51,14 @@ spec: checksum/esecret: {{ include (print $.Template.BasePath "/extra-secret.yaml") . | sha256sum }} {{- end }} spec: + {{- if .Values.securityContext.enabled }} + securityContext: + {{- include "helpers.securityContext" (list + .Values + "securityContext.podSecurityContext" + "celery.beat.podSecurityContext" + ) | nindent 8 }} + {{- end }} serviceAccountName: {{ include "defectdojo.serviceAccountName" . }} {{- with .Values.imagePullSecrets }} imagePullSecrets: @@ -59,12 +67,12 @@ 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 }} @@ -82,9 +90,18 @@ spec: - name: cloudsql-proxy image: {{ .Values.cloudsql.image.repository }}:{{ .Values.cloudsql.image.tag }} imagePullPolicy: {{ .Values.cloudsql.image.pullPolicy }} + {{- with .Values.cloudsql.resources }} + resources: {{- . | toYaml | nindent 10 }} + {{- end }} restartPolicy: Always + {{- if .Values.securityContext.enabled }} securityContext: - runAsNonRoot: true + {{- include "helpers.securityContext" (list + .Values + "securityContext.containerSecurityContext" + "cloudsql.containerSecurityContext" + ) | nindent 10 }} + {{- end }} command: ["/cloud_sql_proxy"] args: - "-verbose={{ .Values.cloudsql.verbose }}" @@ -118,12 +135,16 @@ spec: {{- end }} {{- if .Values.securityContext.enabled }} securityContext: - {{- toYaml .Values.securityContext.djangoSecurityContext | nindent 10 }} + {{- include "helpers.securityContext" (list + .Values + "securityContext.containerSecurityContext" + "celery.beat.containerSecurityContext" + ) | nindent 10 }} {{- end }} volumeMounts: - name: run mountPath: /run/defectdojo - {{- if .Values.localsettingspy }} + {{- if .Values.localsettingspy }} - name: localsettingspy readOnly: true mountPath: /app/dojo/settings/local_settings.py diff --git a/helm/defectdojo/templates/celery-worker-deployment.yaml b/helm/defectdojo/templates/celery-worker-deployment.yaml index ce4881094e9..fe2e0f08c6f 100644 --- a/helm/defectdojo/templates/celery-worker-deployment.yaml +++ b/helm/defectdojo/templates/celery-worker-deployment.yaml @@ -2,7 +2,12 @@ apiVersion: apps/v1 kind: Deployment metadata: - name: {{ $fullName }}-celery-worker + {{- with mergeOverwrite dict .Values.extraAnnotations .Values.celery.annotations .Values.celery.worker.annotations }} + annotations: + {{- range $key, $value := . }} + {{ $key }}: {{ quote $value }} + {{- end }} + {{- end }} labels: defectdojo.org/component: celery defectdojo.org/subcomponent: worker @@ -10,13 +15,11 @@ metadata: app.kubernetes.io/instance: {{ .Release.Name }} app.kubernetes.io/managed-by: {{ .Release.Service }} helm.sh/chart: {{ include "defectdojo.chart" . }} - {{- with .Values.extraLabels }} - {{- toYaml . | nindent 4 }} + {{- range $key, $value := .Values.extraLabels }} + {{ $key }}: {{ quote $value }} {{- end }} - {{- with mergeOverwrite .Values.celery.annotations .Values.celery.worker.annotations }} - annotations: - {{- toYaml . | nindent 4 }} - {{- end }} + name: {{ $fullName }}-celery-worker + namespace: {{ .Release.Namespace }} spec: replicas: {{ .Values.celery.worker.replicas }} {{- with .Values.revisionHistoryLimit }} @@ -35,15 +38,12 @@ spec: defectdojo.org/subcomponent: worker app.kubernetes.io/name: {{ include "defectdojo.name" . }} app.kubernetes.io/instance: {{ .Release.Name }} - {{- with .Values.extraLabels }} - {{- toYaml . | nindent 8 }} - {{- end }} - {{- with .Values.podLabels }} - {{- toYaml . | nindent 8 }} + {{- range $key, $value := mergeOverwrite dict .Values.extraLabels .Values.podLabels }} + {{ $key }}: {{ quote $value }} {{- end }} annotations: - {{- with mergeOverwrite .Values.celery.annotations .Values.celery.worker.podAnnotations }} - {{- toYaml . | nindent 8 }} + {{- range $key, $value := mergeOverwrite dict .Values.extraAnnotations .Values.celery.annotations .Values.celery.worker.podAnnotations }} + {{ $key }}: {{ quote $value }} {{- end }} {{- if eq (.Values.trackConfig | default "disabled") "enabled" }} checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }} @@ -51,18 +51,26 @@ spec: checksum/esecret: {{ include (print $.Template.BasePath "/extra-secret.yaml") . | sha256sum }} {{- end }} spec: + {{- if .Values.securityContext.enabled }} + securityContext: + {{- include "helpers.securityContext" (list + .Values + "securityContext.podSecurityContext" + "celery.worker.podSecurityContext" + ) | nindent 8 }} + {{- end }} serviceAccountName: {{ include "defectdojo.serviceAccountName" . }} {{- with .Values.imagePullSecrets }} imagePullSecrets: - name: {{ . }} {{- end }} volumes: - {{- 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 }} @@ -80,9 +88,18 @@ spec: - name: cloudsql-proxy image: {{ .Values.cloudsql.image.repository }}:{{ .Values.cloudsql.image.tag }} imagePullPolicy: {{ .Values.cloudsql.image.pullPolicy }} + {{- with .Values.cloudsql.resources }} + resources: {{- . | toYaml | nindent 10 }} + {{- end }} restartPolicy: Always + {{- if .Values.securityContext.enabled }} securityContext: - runAsNonRoot: true + {{- include "helpers.securityContext" (list + .Values + "securityContext.containerSecurityContext" + "cloudsql.containerSecurityContext" + ) | nindent 10 }} + {{- end }} command: ["/cloud_sql_proxy"] args: - "-verbose={{ .Values.cloudsql.verbose }}" @@ -114,7 +131,11 @@ spec: {{- end }} {{- if .Values.securityContext.enabled }} securityContext: - {{- toYaml .Values.securityContext.djangoSecurityContext | nindent 10 }} + {{- include "helpers.securityContext" (list + .Values + "securityContext.containerSecurityContext" + "celery.worker.containerSecurityContext" + ) | nindent 10 }} {{- end }} command: ['/entrypoint-celery-worker.sh'] volumeMounts: @@ -124,7 +145,7 @@ 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 }} diff --git a/helm/defectdojo/templates/configmap-local-settings-py.yaml b/helm/defectdojo/templates/configmap-local-settings-py.yaml index dc75942fbc0..30c42244251 100644 --- a/helm/defectdojo/templates/configmap-local-settings-py.yaml +++ b/helm/defectdojo/templates/configmap-local-settings-py.yaml @@ -1,14 +1,24 @@ -{{- if .Values.localsettingspy }} +{{- if .Values.localsettingspy }} {{- $fullName := include "defectdojo.fullname" . -}} apiVersion: v1 kind: ConfigMap metadata: - name: {{ $fullName }}-localsettingspy + {{- with .Values.extraAnnotations }} + annotations: + {{- range $key, $value := . }} + {{ $key }}: {{ quote $value }} + {{- end }} + {{- end }} labels: app.kubernetes.io/name: {{ include "defectdojo.name" . }} app.kubernetes.io/instance: {{ .Release.Name }} app.kubernetes.io/managed-by: {{ .Release.Service }} helm.sh/chart: {{ include "defectdojo.chart" . }} + {{- with .Values.extraLabels }} + {{- toYaml . | nindent 4 }} + {{- end }} + name: {{ $fullName }}-localsettingspy + namespace: {{ .Release.Namespace }} data: file: {{ toYaml .Values.localsettingspy | indent 4 }} diff --git a/helm/defectdojo/templates/configmap.yaml b/helm/defectdojo/templates/configmap.yaml index e5078f57903..d25926c2c3f 100644 --- a/helm/defectdojo/templates/configmap.yaml +++ b/helm/defectdojo/templates/configmap.yaml @@ -3,21 +3,22 @@ apiVersion: v1 kind: ConfigMap metadata: - name: {{ $fullName }} + {{- with .Values.extraAnnotations }} + annotations: + {{- range $key, $value := . }} + {{ $key }}: {{ quote $value }} + {{- end }} + {{- end }} labels: app.kubernetes.io/name: {{ include "defectdojo.name" . }} app.kubernetes.io/instance: {{ .Release.Name }} app.kubernetes.io/managed-by: {{ .Release.Service }} helm.sh/chart: {{ include "defectdojo.chart" . }} - {{- with .Values.extraLabels }} - {{- toYaml . | nindent 4 }} + {{- range $key, $value := .Values.extraLabels }} + {{ $key }}: {{ quote $value }} {{- end }} -{{- if .Values.annotations }} - annotations: -{{- with .Values.annotations }} - {{- toYaml . | nindent 4 }} -{{- end }} -{{- end }} + name: {{ $fullName }} + namespace: {{ .Release.Namespace }} data: DD_ADMIN_USER: {{ .Values.admin.user | default "admin" }} DD_ADMIN_MAIL: {{ .Values.admin.Mail | default "admin@defectdojo.local" }} diff --git a/helm/defectdojo/templates/django-deployment.yaml b/helm/defectdojo/templates/django-deployment.yaml index fb77e8f7e88..16738a91b41 100644 --- a/helm/defectdojo/templates/django-deployment.yaml +++ b/helm/defectdojo/templates/django-deployment.yaml @@ -2,20 +2,23 @@ apiVersion: apps/v1 kind: Deployment metadata: - name: {{ $fullName }}-django + {{- with mergeOverwrite dict .Values.extraAnnotations .Values.django.annotations }} + annotations: + {{- range $key, $value := . }} + {{ $key }}: {{ quote $value }} + {{- end }} + {{- end }} labels: defectdojo.org/component: django app.kubernetes.io/name: {{ include "defectdojo.name" . }} app.kubernetes.io/instance: {{ .Release.Name }} app.kubernetes.io/managed-by: {{ .Release.Service }} helm.sh/chart: {{ include "defectdojo.chart" . }} - {{- with .Values.extraLabels }} - {{- toYaml . | nindent 4 }} + {{- range $key, $value := .Values.extraLabels }} + {{ $key }}: {{ quote $value }} {{- end }} - {{- with .Values.django.annotations }} - annotations: - {{- toYaml . | nindent 4 }} - {{- end }} + name: {{ $fullName }}-django + namespace: {{ .Release.Namespace }} spec: replicas: {{ .Values.django.replicas }} {{- with .Values.django.strategy }} @@ -36,15 +39,12 @@ spec: defectdojo.org/component: django app.kubernetes.io/name: {{ include "defectdojo.name" . }} app.kubernetes.io/instance: {{ .Release.Name }} - {{- with .Values.extraLabels }} - {{- toYaml . | nindent 8 }} - {{- end }} - {{- with .Values.podLabels }} - {{- toYaml . | nindent 8 }} - {{- end }} + {{- range $key, $value := mergeOverwrite dict .Values.extraLabels .Values.podLabels }} + {{ $key }}: {{ quote $value }} + {{- end }} annotations: - {{- with .Values.django.annotations }} - {{- toYaml . | nindent 8 }} + {{- range $key, $value := mergeOverwrite dict .Values.extraAnnotations .Values.django.annotations }} + {{ $key }}: {{ quote $value }} {{- end }} {{- if and .Values.monitoring.enabled .Values.monitoring.prometheus.enabled }} prometheus.io/path: /metrics @@ -64,8 +64,14 @@ spec: - name: {{ quote . }} {{- end }} {{- if .Values.django.mediaPersistentVolume.enabled }} + {{- if .Values.securityContext.enabled }} securityContext: - fsGroup: {{ .Values.django.mediaPersistentVolume.fsGroup | default 1001 }} + {{- include "helpers.securityContext" (list + .Values + "securityContext.podSecurityContext" + "django.podSecurityContext" + ) | nindent 8 }} + {{- end }} {{- end }} volumes: - name: run @@ -101,9 +107,21 @@ spec: - name: cloudsql-proxy image: {{ .Values.cloudsql.image.repository }}:{{ .Values.cloudsql.image.tag }} imagePullPolicy: {{ .Values.cloudsql.image.pullPolicy }} + {{- with .Values.cloudsql.extraEnv }} + env: {{- . | toYaml | nindent 8 }} + {{- end }} + {{- with .Values.cloudsql.resources }} + resources: {{- . | toYaml | nindent 10 }} + {{- end }} restartPolicy: Always + {{- if .Values.securityContext.enabled }} securityContext: - runAsNonRoot: true + {{- include "helpers.securityContext" (list + .Values + "securityContext.containerSecurityContext" + "cloudsql.containerSecurityContext" + ) | nindent 10 }} + {{- end }} command: ["/cloud_sql_proxy"] args: - "-verbose={{ .Values.cloudsql.verbose }}" @@ -114,9 +132,12 @@ spec: {{- if .Values.cloudsql.use_private_ip }} - "-ip_address_types=PRIVATE" {{- end }} + {{- with .Values.cloudsql.extraVolumeMounts }} + volumeMounts: {{ . | toYaml | nindent 10 }} + {{- end }} {{- end }} {{- if .Values.dbMigrationChecker.enabled }} - {{$data := dict "fullName" $fullName }} + {{- $data := dict "fullName" $fullName }} {{- $newContext := merge . (dict "fullName" $fullName) }} {{- include "dbMigrationChecker" $newContext | nindent 6 }} {{- end }} @@ -126,7 +147,13 @@ spec: - name: metrics image: {{ .Values.monitoring.prometheus.image }} imagePullPolicy: {{ .Values.monitoring.prometheus.imagePullPolicy }} - command: [ '/usr/bin/nginx-prometheus-exporter', '--nginx.scrape-uri', 'http://127.0.0.1:8080/nginx_status'] + command: + - /usr/bin/nginx-prometheus-exporter + - --nginx.scrape-uri + - http://127.0.0.1:8080/nginx_status + {{- with .Values.monitoring.prometheus.extraEnv }} + env: {{- . | toYaml | nindent 8 }} + {{- end }} ports: - name: http-metrics protocol: TCP @@ -138,13 +165,31 @@ spec: periodSeconds: 20 initialDelaySeconds: 15 timeoutSeconds: 5 + {{- with .Values.monitoring.prometheus.resources }} + resources: {{- . | toYaml | nindent 10 }} + {{- end }} + {{- if .Values.securityContext.enabled }} + securityContext: + {{- include "helpers.securityContext" (list + .Values + "securityContext.containerSecurityContext" + "monitoring.prometheus.containerSecurityContext" + ) | nindent 10 }} + {{- end }} + {{- with .Values.monitoring.prometheus.extraVolumeMounts }} + volumeMounts: {{ . | toYaml | nindent 10 }} + {{- end }} {{- end }} - name: uwsgi image: '{{ template "django.uwsgi.repository" . }}:{{ .Values.tag }}' imagePullPolicy: {{ .Values.imagePullPolicy }} {{- if .Values.securityContext.enabled }} securityContext: - {{- toYaml .Values.securityContext.djangoSecurityContext | nindent 10 }} + {{- include "helpers.securityContext" (list + .Values + "securityContext.containerSecurityContext" + "django.uwsgi.containerSecurityContext" + ) | nindent 10 }} {{- end }} volumeMounts: - name: run @@ -159,6 +204,9 @@ spec: - name: cert-mount mountPath: {{ .Values.django.uwsgi.certificates.certMountPath }} {{- end }} + {{- with .Values.django.extraVolumeMounts }} + {{- . | toYaml | nindent 8 }} + {{- end }} {{- with .Values.django.uwsgi.extraVolumeMounts }} {{- . | toYaml | nindent 8 }} {{- end }} @@ -212,6 +260,9 @@ spec: {{- with .Values.extraEnv }} {{- . | toYaml | nindent 8 }} {{- end }} + {{- with .Values.django.extraEnv }} + {{- . | toYaml | nindent 8 }} + {{- end }} {{- with .Values.django.uwsgi.extraEnv }} {{- . | toYaml | nindent 8 }} {{- end }} @@ -236,11 +287,18 @@ spec: imagePullPolicy: {{ .Values.imagePullPolicy }} {{- if .Values.securityContext.enabled }} securityContext: - {{- toYaml .Values.securityContext.nginxSecurityContext | nindent 10 }} + {{- include "helpers.securityContext" (list + .Values + "securityContext.containerSecurityContext" + "django.nginx.containerSecurityContext" + ) | nindent 10 }} {{- end }} volumeMounts: - name: run mountPath: /run/defectdojo + {{- with .Values.django.extraVolumeMounts }} + {{- . | toYaml | nindent 8 }} + {{- end }} {{- with .Values.django.nginx.extraVolumeMounts }} {{- . | toYaml | nindent 8 }} {{- end }} @@ -268,6 +326,9 @@ spec: {{- with .Values.extraEnv }} {{- . | toYaml | nindent 8 }} {{- end }} + {{- with .Values.django.extraEnv }} + {{- . | toYaml | nindent 8 }} + {{- end }} {{- with .Values.django.nginx.extraEnv }} {{- . | toYaml | nindent 8 }} {{- end }} diff --git a/helm/defectdojo/templates/django-ingress.yaml b/helm/defectdojo/templates/django-ingress.yaml index 4a0209d15a2..aee880f23d9 100644 --- a/helm/defectdojo/templates/django-ingress.yaml +++ b/helm/defectdojo/templates/django-ingress.yaml @@ -3,28 +3,32 @@ apiVersion: networking.k8s.io/v1 kind: Ingress metadata: - name: {{ $fullName }} - labels: - defectdojo.org/component: django - app.kubernetes.io/name: {{ include "defectdojo.name" . }} - app.kubernetes.io/instance: {{ .Release.Name }} - app.kubernetes.io/managed-by: {{ .Release.Service }} - helm.sh/chart: {{ include "defectdojo.chart" . }} - {{- with .Values.extraLabels }} - {{- toYaml . | nindent 4 }} - {{- end }} -{{- if or .Values.django.ingress.annotations .Values.gke.useGKEIngress }} + {{- if or .Values.extraAnnotations .Values.django.ingress.annotations .Values.gke.useGKEIngress }} annotations: -{{- with .Values.django.ingress.annotations }} - {{- toYaml . | nindent 4 }} -{{- end }} + {{- range $key, $value := .Values.extraAnnotations }} + {{ $key }}: {{ quote $value }} + {{- end }} + {{- range $key, $value := .Values.django.ingress.annotations }} + {{ $key }}: {{ quote $value }} + {{- end }} {{- if .Values.gke.useGKEIngress }} {{- if .Values.gke.useManagedCertificate }} kubernetes.io/ingress.allow-http: "false" networking.gke.io/managed-certificates: {{ $fullName }}-django {{- end }} {{- end }} -{{- end }} + {{- end }} + labels: + defectdojo.org/component: django + app.kubernetes.io/name: {{ include "defectdojo.name" . }} + app.kubernetes.io/instance: {{ .Release.Name }} + app.kubernetes.io/managed-by: {{ .Release.Service }} + helm.sh/chart: {{ include "defectdojo.chart" . }} + {{- range $key, $value := .Values.extraLabels }} + {{ $key }}: {{ quote $value }} + {{- end }} + name: {{ $fullName }} + namespace: {{ .Release.Namespace }} spec: {{- if .Values.django.ingress.ingressClassName }} ingressClassName: {{ .Values.django.ingress.ingressClassName }} diff --git a/helm/defectdojo/templates/django-service.yaml b/helm/defectdojo/templates/django-service.yaml index f8c20aa092f..5f966c15edc 100644 --- a/helm/defectdojo/templates/django-service.yaml +++ b/helm/defectdojo/templates/django-service.yaml @@ -2,22 +2,23 @@ apiVersion: v1 kind: Service metadata: - name: {{ $fullName }}-django + {{- with mergeOverwrite dict .Values.extraAnnotations .Values.django.service.annotations }} + annotations: + {{- range $key, $value := . }} + {{ $key }}: {{ $value | quote }} + {{- end }} + {{- end }} labels: defectdojo.org/component: django app.kubernetes.io/name: {{ include "defectdojo.name" . }} app.kubernetes.io/instance: {{ .Release.Name }} app.kubernetes.io/managed-by: {{ .Release.Service }} helm.sh/chart: {{ include "defectdojo.chart" . }} - {{- with .Values.extraLabels }} - {{- toYaml . | nindent 4 }} + {{- range $key, $value := .Values.extraLabels }} + {{ $key }}: {{ quote $value }} {{- end }} -{{- if .Values.django.service.annotations }} - annotations: - {{- range $key, $value := .Values.django.service.annotations }} - {{ $key }}: {{ $value | quote }} - {{- end }} -{{- end }} + name: {{ $fullName }}-django + namespace: {{ .Release.Namespace }} spec: selector: defectdojo.org/component: django diff --git a/helm/defectdojo/templates/extra-secret.yaml b/helm/defectdojo/templates/extra-secret.yaml index d97800283a6..caa5b1fcbfa 100644 --- a/helm/defectdojo/templates/extra-secret.yaml +++ b/helm/defectdojo/templates/extra-secret.yaml @@ -3,24 +3,22 @@ apiVersion: v1 kind: Secret metadata: - name: {{ $fullName }}-extrasecrets + {{- with mergeOverwrite dict .Values.secrets.annotations .Values.extraAnnotations }} + annotations: + {{- range $key, $value := . }} + {{ $key }}: {{ $value | quote }} + {{- end }} + {{- end }} labels: app.kubernetes.io/name: {{ include "defectdojo.name" . }} app.kubernetes.io/instance: {{ .Release.Name }} app.kubernetes.io/managed-by: {{ .Release.Service }} helm.sh/chart: {{ include "defectdojo.chart" . }} - {{- with .Values.extraLabels }} - {{- toYaml . | nindent 4 }} + {{- range $key, $value := .Values.extraLabels }} + {{ $key }}: {{ quote $value }} {{- end }} - {{- if or .Values.secrets.annotations .Values.annotations }} - annotations: - {{- with .Values.secrets.annotations }} - {{- toYaml . | nindent 4 }} - {{- end }} - {{- with .Values.annotations }} - {{- toYaml . | nindent 4 }} - {{- end }} - {{- end }} + name: {{ $fullName }}-extrasecrets + namespace: {{ .Release.Namespace }} type: Opaque data: {{- range $key, $value := .Values.extraSecrets }} diff --git a/helm/defectdojo/templates/gke-managed-certificate.yaml b/helm/defectdojo/templates/gke-managed-certificate.yaml index 43399626310..14dc539e6b7 100644 --- a/helm/defectdojo/templates/gke-managed-certificate.yaml +++ b/helm/defectdojo/templates/gke-managed-certificate.yaml @@ -1,9 +1,22 @@ -{{- if .Values.gke.useManagedCertificate }} +{{- if .Values.gke.useManagedCertificate | and (.Capabilities.APIVersions.Has "networking.gke.io/v1") }} {{- $fullName := include "defectdojo.fullname" . -}} apiVersion: networking.gke.io/v1 kind: ManagedCertificate metadata: + {{- with .Values.extraAnnotations }} + annotations: + {{- range $key, $value := . }} + {{ $key }}: {{ quote $value }} + {{- end }} + {{- end }} + {{- with .Values.extraLabels }} + labels: + {{- range $key, $value := . }} + {{ $key }}: {{ quote $value }} + {{- end }} + {{- end }} name: {{ $fullName }}-django + namespace: {{ .Release.Namespace }} spec: domains: - {{ .Values.host }} diff --git a/helm/defectdojo/templates/initializer-job.yaml b/helm/defectdojo/templates/initializer-job.yaml index 668812d1a08..795427b34f1 100644 --- a/helm/defectdojo/templates/initializer-job.yaml +++ b/helm/defectdojo/templates/initializer-job.yaml @@ -3,20 +3,23 @@ apiVersion: batch/v1 kind: Job metadata: - name: {{ template "initializer.jobname" . }} + {{- with mergeOverwrite dict .Values.extraAnnotations .Values.initializer.jobAnnotations }} + annotations: + {{- range $key, $value := . }} + {{ $key }}: {{ quote $value }} + {{- end }} + {{- end }} labels: defectdojo.org/component: initializer app.kubernetes.io/name: {{ include "defectdojo.name" . }} app.kubernetes.io/instance: {{ .Release.Name }} app.kubernetes.io/managed-by: {{ .Release.Service }} helm.sh/chart: {{ include "defectdojo.chart" . }} - {{- with .Values.extraLabels }} - {{- toYaml . | nindent 4 }} - {{- end }} - annotations: - {{- with .Values.initializer.jobAnnotations }} - {{- toYaml . | nindent 4 }} + {{- range $key, $value := .Values.extraLabels }} + {{ $key }}: {{ quote $value }} {{- end }} + name: {{ template "initializer.jobname" . }} + namespace: {{ .Release.Namespace }} spec: {{- if and (int .Values.initializer.keepSeconds) (gt (int .Values.initializer.keepSeconds) 0) }} ttlSecondsAfterFinished: {{ .Values.initializer.keepSeconds }} @@ -38,6 +41,14 @@ spec: {{- toYaml . | nindent 8 }} {{- end }} spec: + {{- if .Values.securityContext.enabled }} + securityContext: + {{- include "helpers.securityContext" (list + .Values + "securityContext.podSecurityContext" + "initializer.podSecurityContext" + ) | nindent 8 }} + {{- end }} serviceAccountName: {{ include "defectdojo.serviceAccountName" . }} {{- with .Values.imagePullSecrets }} imagePullSecrets: @@ -66,9 +77,18 @@ spec: - name: cloudsql-proxy image: {{ .Values.cloudsql.image.repository }}:{{ .Values.cloudsql.image.tag }} imagePullPolicy: {{ .Values.cloudsql.image.pullPolicy }} + {{- with .Values.cloudsql.resources }} + resources: {{- . | toYaml | nindent 10 }} + {{- end }} restartPolicy: Always + {{- if .Values.securityContext.enabled }} securityContext: - runAsNonRoot: true + {{- include "helpers.securityContext" (list + .Values + "securityContext.containerSecurityContext" + "cloudsql.containerSecurityContext" + ) | nindent 10 }} + {{- end }} command: ["/cloud_sql_proxy"] args: - "-verbose={{ .Values.cloudsql.verbose }}" @@ -96,7 +116,11 @@ spec: imagePullPolicy: {{ .Values.imagePullPolicy }} {{- if .Values.securityContext.enabled }} securityContext: - {{- toYaml .Values.securityContext.djangoSecurityContext | nindent 10 }} + {{- include "helpers.securityContext" (list + .Values + "securityContext.containerSecurityContext" + "django.uwsgi.containerSecurityContext" + ) | nindent 10 }} {{- end }} envFrom: - configMapRef: @@ -123,7 +147,11 @@ spec: imagePullPolicy: {{ .Values.imagePullPolicy }} {{- if .Values.securityContext.enabled }} securityContext: - {{- toYaml .Values.securityContext.djangoSecurityContext | nindent 10 }} + {{- include "helpers.securityContext" (list + .Values + "securityContext.containerSecurityContext" + "initializer.containerSecurityContext" + ) | nindent 10 }} {{- end }} volumeMounts: {{- if .Values.localsettingspy }} diff --git a/helm/defectdojo/templates/media-pvc.yaml b/helm/defectdojo/templates/media-pvc.yaml index d31d3251b44..57fcae8e0c7 100644 --- a/helm/defectdojo/templates/media-pvc.yaml +++ b/helm/defectdojo/templates/media-pvc.yaml @@ -1,22 +1,29 @@ {{- $fullName := include "django.pvc_name" $ -}} {{ with .Values.django.mediaPersistentVolume }} -{{- if and .enabled (eq .type "pvc") .persistentVolumeClaim.create }} +{{- if and .enabled (eq .type "pvc") .persistentVolumeClaim.create }} apiVersion: v1 kind: PersistentVolumeClaim metadata: + {{- with .Values.extraAnnotations }} + annotations: + {{- range $key, $value := . }} + {{ $key }}: {{ quote $value }} + {{- end }} + {{- end }} labels: defectdojo.org/component: django app.kubernetes.io/name: {{ include "defectdojo.name" $ }} app.kubernetes.io/instance: {{ $.Release.Name }} app.kubernetes.io/managed-by: {{ $.Release.Service }} helm.sh/chart: {{ include "defectdojo.chart" $ }} - {{- with $.Values.extraLabels }} - {{- toYaml . | nindent 4 }} + {{- range $key, $value := .Values.extraLabels }} + {{ $key }}: {{ quote $value }} {{- end }} name: {{ $fullName }} + namespace: {{ .Release.Namespace }} spec: accessModes: - {{- toYaml .persistentVolumeClaim.accessModes |nindent 4 }} + {{- toYaml .persistentVolumeClaim.accessModes | nindent 4 }} resources: requests: storage: {{ .persistentVolumeClaim.size }} diff --git a/helm/defectdojo/templates/network-policy.yaml b/helm/defectdojo/templates/network-policy.yaml index e580a0df80c..333b58da3e6 100644 --- a/helm/defectdojo/templates/network-policy.yaml +++ b/helm/defectdojo/templates/network-policy.yaml @@ -3,21 +3,22 @@ apiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: - name: {{ $fullName }}-networkpolicy + {{- with mergeOverwrite dict .Values.extraAnnotations .Values.networkPolicy.annotations }} + annotations: + {{- range $key, $value := . }} + {{ $key }}: {{ quote $value }} + {{- end }} + {{- end }} labels: app.kubernetes.io/instance: {{ .Release.Name }} app.kubernetes.io/managed-by: {{ .Release.Service }} helm.sh/chart: {{ include "defectdojo.chart" . }} app.kubernetes.io/name: {{ include "defectdojo.name" . }} - {{- with .Values.extraLabels }} - {{- toYaml . | nindent 4 }} + {{- range $key, $value := .Values.extraLabels }} + {{ $key }}: {{ quote $value }} {{- end }} -{{- if .Values.networkPolicy.annotations }} - annotations: -{{- with .Values.networkPolicy.annotations }} - {{- toYaml . | nindent 4 }} -{{- end }} -{{- end }} + name: {{ $fullName }}-networkpolicy + namespace: {{ .Release.Namespace }} spec: podSelector: matchLabels: @@ -43,15 +44,22 @@ spec: apiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: - name: {{ $fullName }}-networkpolicy-django + {{- with mergeOverwrite dict .Values.extraAnnotations .Values.networkPolicy.annotations }} + annotations: + {{- range $key, $value := . }} + {{ $key }}: {{ quote $value }} + {{- end }} + {{- end }} labels: app.kubernetes.io/instance: {{ .Release.Name }} app.kubernetes.io/managed-by: {{ .Release.Service }} helm.sh/chart: {{ include "defectdojo.chart" . }} app.kubernetes.io/name: {{ include "defectdojo.name" . }} -{{- with .Values.extraLabels }} - {{- toYaml . | nindent 4 }} -{{- end }} + {{- range $key, $value := .Values.extraLabels }} + {{ $key }}: {{ quote $value }} + {{- end }} + name: {{ $fullName }}-networkpolicy-django + namespace: {{ .Release.Namespace }} spec: podSelector: matchLabels: diff --git a/helm/defectdojo/templates/sa.yaml b/helm/defectdojo/templates/sa.yaml index 4345da6360a..1394f077945 100644 --- a/helm/defectdojo/templates/sa.yaml +++ b/helm/defectdojo/templates/sa.yaml @@ -2,31 +2,26 @@ kind: ServiceAccount apiVersion: v1 metadata: - name: {{ include "defectdojo.serviceAccountName" . }} - labels: - app.kubernetes.io/name: {{ include "defectdojo.name" . }} - app.kubernetes.io/instance: {{ .Release.Name }} - app.kubernetes.io/managed-by: {{ .Release.Service }} - helm.sh/chart: {{ include "defectdojo.chart" . }} - {{- with .Values.extraLabels }} - {{- toYaml . | nindent 4 }} - {{- end }} - {{- with .Values.serviceAccount.labels }} - {{- 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 }} - {{- with .Values.serviceAccount.annotations }} - {{ toYaml . | nindent 4 }} + {{- range $key, $value := mergeOverwrite dict .Values.extraAnnotations .Values.serviceAccount.annotations }} + {{ $key }}: {{ quote $value }} {{- end }} {{- if ne .Values.gke.workloadIdentityEmail "" }} iam.gke.io/gcp-service-account: {{ .Values.gke.workloadIdentityEmail }} {{- end }} + labels: + app.kubernetes.io/name: {{ include "defectdojo.name" . }} + app.kubernetes.io/instance: {{ .Release.Name }} + app.kubernetes.io/managed-by: {{ .Release.Service }} + helm.sh/chart: {{ include "defectdojo.chart" . }} + {{- range $key, $value := mergeOverwrite dict .Values.extraLabels .Values.serviceAccount.labels }} + {{ $key }}: {{ quote $value }} + {{- end }} + name: {{ include "defectdojo.serviceAccountName" . }} + namespace: {{ .Release.Namespace }} {{- end }} \ No newline at end of file diff --git a/helm/defectdojo/templates/secret-postgresql.yaml b/helm/defectdojo/templates/secret-postgresql.yaml index 12924bb29c5..57f38a0e883 100644 --- a/helm/defectdojo/templates/secret-postgresql.yaml +++ b/helm/defectdojo/templates/secret-postgresql.yaml @@ -2,27 +2,25 @@ apiVersion: v1 kind: Secret metadata: - name: {{ .Values.postgresql.auth.existingSecret }} - labels: - app.kubernetes.io/name: {{ include "defectdojo.name" . }} - app.kubernetes.io/instance: {{ .Release.Name }} - app.kubernetes.io/managed-by: {{ .Release.Service }} - helm.sh/chart: {{ include "defectdojo.chart" . }} - {{- with .Values.extraLabels }} - {{- 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 }} + {{- range $key, $value := mergeOverwrite dict .Values.extraAnnotations .Values.secrets.annotations }} + {{ $key }}: {{ quote $value }} {{- end }} - {{- with .Values.annotations }} - {{- toYaml . | nindent 4 }} + labels: + app.kubernetes.io/name: {{ include "defectdojo.name" . }} + app.kubernetes.io/instance: {{ .Release.Name }} + app.kubernetes.io/managed-by: {{ .Release.Service }} + helm.sh/chart: {{ include "defectdojo.chart" . }} + {{- range $key, $value := .Values.extraLabels }} + {{ $key }}: {{ quote $value }} {{- end }} + name: {{ .Values.postgresql.auth.existingSecret }} + namespace: {{ .Release.Namespace }} type: Opaque data: {{- if .Values.postgresql.auth.password }} diff --git a/helm/defectdojo/templates/secret-redis.yaml b/helm/defectdojo/templates/secret-redis.yaml index f6d102c2513..b2a5a3a84c2 100644 --- a/helm/defectdojo/templates/secret-redis.yaml +++ b/helm/defectdojo/templates/secret-redis.yaml @@ -2,27 +2,25 @@ apiVersion: v1 kind: Secret metadata: - name: {{ .Values.redis.auth.existingSecret }} - labels: - app.kubernetes.io/name: {{ include "defectdojo.name" . }} - app.kubernetes.io/instance: {{ .Release.Name }} - app.kubernetes.io/managed-by: {{ .Release.Service }} - helm.sh/chart: {{ include "defectdojo.chart" . }} - {{- with .Values.extraLabels }} - {{- 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 }} + {{- range $key, $value := mergeOverwrite dict .Values.extraAnnotations .Values.secrets.annotations }} + {{ $key }}: {{ quote $value }} {{- end }} - {{- with .Values.annotations }} - {{- toYaml . | nindent 4 }} + labels: + app.kubernetes.io/name: {{ include "defectdojo.name" . }} + app.kubernetes.io/instance: {{ .Release.Name }} + app.kubernetes.io/managed-by: {{ .Release.Service }} + helm.sh/chart: {{ include "defectdojo.chart" . }} + {{- range $key, $value := .Values.extraLabels }} + {{ $key }}: {{ quote $value }} {{- end }} + name: {{ .Values.redis.auth.existingSecret }} + namespace: {{ .Release.Namespace }} type: Opaque data: {{- if .Values.redis.auth.password }} diff --git a/helm/defectdojo/templates/secret.yaml b/helm/defectdojo/templates/secret.yaml index c3a3c56f6c4..3a4a5299d64 100644 --- a/helm/defectdojo/templates/secret.yaml +++ b/helm/defectdojo/templates/secret.yaml @@ -3,47 +3,45 @@ apiVersion: v1 kind: Secret metadata: - name: {{ $fullName }} - labels: - app.kubernetes.io/name: {{ include "defectdojo.name" . }} - app.kubernetes.io/instance: {{ .Release.Name }} - app.kubernetes.io/managed-by: {{ .Release.Service }} - helm.sh/chart: {{ include "defectdojo.chart" . }} - {{- with .Values.extraLabels }} - {{- 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 }} + {{- range $key, $value := mergeOverwrite dict .Values.extraAnnotations .Values.secrets.annotations }} + {{ $key }}: {{ quote $value }} {{- end }} - {{- with .Values.annotations }} - {{- toYaml . | nindent 4 }} + labels: + app.kubernetes.io/name: {{ include "defectdojo.name" . }} + app.kubernetes.io/instance: {{ .Release.Name }} + app.kubernetes.io/managed-by: {{ .Release.Service }} + helm.sh/chart: {{ include "defectdojo.chart" . }} + {{- range $key, $value := .Values.extraLabels }} + {{ $key }}: {{ quote $value }} {{- end }} + name: {{ $fullName }} + namespace: {{ .Release.Namespace }} type: Opaque data: {{- if .Values.admin.password }} DD_ADMIN_PASSWORD: {{ .Values.admin.password | b64enc | quote }} -{{- else}} +{{- else }} DD_ADMIN_PASSWORD: {{ randAlphaNum 22 | b64enc | quote }} -{{- end}} +{{- end }} {{- if .Values.admin.secretKey }} DD_SECRET_KEY: {{ .Values.admin.secretKey | b64enc | quote }} -{{- else}} +{{- else }} DD_SECRET_KEY: {{ randAlphaNum 128 | b64enc | quote }} -{{- end}} +{{- end }} {{- if .Values.admin.credentialAes256Key }} DD_CREDENTIAL_AES_256_KEY: {{ .Values.admin.credentialAes256Key | b64enc | quote }} -{{- else}} +{{- else }} DD_CREDENTIAL_AES_256_KEY: {{ randAlphaNum 128 | b64enc | quote }} -{{- end}} +{{- end }} {{- if .Values.admin.metricsHttpAuthPassword }} METRICS_HTTP_AUTH_PASSWORD: {{ .Values.admin.metricsHttpAuthPassword | b64enc | quote }} -{{- else}} +{{- else }} METRICS_HTTP_AUTH_PASSWORD: {{ randAlphaNum 32 | b64enc | quote }} -{{- end}} +{{- end }} {{- end }} diff --git a/helm/defectdojo/values.schema.json b/helm/defectdojo/values.schema.json index 93e7b3915ff..74e14508d13 100644 --- a/helm/defectdojo/values.schema.json +++ b/helm/defectdojo/values.schema.json @@ -31,8 +31,8 @@ } } }, - "annotations": { - "type": "object" + "alternativeHosts": { + "type": "array" }, "celery": { "type": "object", @@ -49,6 +49,9 @@ "annotations": { "type": "object" }, + "containerSecurityContext": { + "type": "object" + }, "extraEnv": { "type": "array" }, @@ -70,6 +73,9 @@ "podAnnotations": { "type": "object" }, + "podSecurityContext": { + "type": "object" + }, "readinessProbe": { "type": "object" }, @@ -134,6 +140,9 @@ } } }, + "containerSecurityContext": { + "type": "object" + }, "extraEnv": { "type": "array" }, @@ -155,6 +164,9 @@ "podAnnotations": { "type": "object" }, + "podSecurityContext": { + "type": "object" + }, "readinessProbe": { "type": "object" }, @@ -201,12 +213,21 @@ "cloudsql": { "type": "object", "properties": { + "containerSecurityContext": { + "type": "object" + }, "enable_iam_login": { "type": "boolean" }, "enabled": { "type": "boolean" }, + "extraEnv": { + "type": "array" + }, + "extraVolumeMounts": { + "type": "array" + }, "image": { "type": "object", "properties": { @@ -224,6 +245,9 @@ "instance": { "type": "string" }, + "resources": { + "type": "object" + }, "use_private_ip": { "type": "boolean" }, @@ -244,9 +268,18 @@ "dbMigrationChecker": { "type": "object", "properties": { + "containerSecurityContext": { + "type": "object" + }, "enabled": { "type": "boolean" }, + "extraEnv": { + "type": "array" + }, + "extraVolumeMounts": { + "type": "array" + }, "resources": { "type": "object", "properties": { @@ -288,9 +321,15 @@ "annotations": { "type": "object" }, + "extraEnv": { + "type": "array" + }, "extraInitContainers": { "type": "array" }, + "extraVolumeMounts": { + "type": "array" + }, "extraVolumes": { "type": "array" }, @@ -357,6 +396,14 @@ "nginx": { "type": "object", "properties": { + "containerSecurityContext": { + "type": "object", + "properties": { + "runAsUser": { + "type": "integer" + } + } + }, "extraEnv": { "type": "array" }, @@ -406,6 +453,14 @@ "nodeSelector": { "type": "object" }, + "podSecurityContext": { + "type": "object", + "properties": { + "fsGroup": { + "type": "integer" + } + } + }, "replicas": { "type": "integer" }, @@ -460,6 +515,14 @@ } } }, + "containerSecurityContext": { + "type": "object", + "properties": { + "runAsUser": { + "type": "integer" + } + } + }, "enableDebug": { "type": "boolean" }, @@ -569,6 +632,9 @@ } } }, + "extraAnnotations": { + "type": "object" + }, "extraConfigs": { "type": "object" }, @@ -616,6 +682,9 @@ "annotations": { "type": "object" }, + "containerSecurityContext": { + "type": "object" + }, "extraEnv": { "type": "array" }, @@ -637,6 +706,9 @@ "nodeSelector": { "type": "object" }, + "podSecurityContext": { + "type": "object" + }, "resources": { "type": "object", "properties": { @@ -687,14 +759,26 @@ "prometheus": { "type": "object", "properties": { + "containerSecurityContext": { + "type": "object" + }, "enabled": { "type": "boolean" }, + "extraEnv": { + "type": "array" + }, + "extraVolumeMounts": { + "type": "array" + }, "image": { "type": "string" }, "imagePullPolicy": { "type": "string" + }, + "resources": { + "type": "object" } } } @@ -924,22 +1008,22 @@ "securityContext": { "type": "object", "properties": { - "djangoSecurityContext": { + "containerSecurityContext": { "type": "object", "properties": { - "runAsUser": { - "type": "integer" + "runAsNonRoot": { + "type": "boolean" } } }, "enabled": { "type": "boolean" }, - "nginxSecurityContext": { + "podSecurityContext": { "type": "object", "properties": { - "runAsUser": { - "type": "integer" + "runAsNonRoot": { + "type": "boolean" } } } @@ -956,9 +1040,15 @@ }, "labels": { "type": "object" + }, + "name": { + "type": "string" } } }, + "siteUrl": { + "type": "string" + }, "tag": { "type": "string" }, diff --git a/helm/defectdojo/values.yaml b/helm/defectdojo/values.yaml index 8415ea73067..bba288d5dbf 100644 --- a/helm/defectdojo/values.yaml +++ b/helm/defectdojo/values.yaml @@ -1,5 +1,12 @@ --- -# Global settings +# Security context settings +securityContext: + enabled: true + containerSecurityContext: + runAsNonRoot: true + podSecurityContext: + runAsNonRoot: true + # create defectdojo specific secret createSecret: false # create redis secret in defectdojo chart, outside of redis chart @@ -15,8 +22,10 @@ trackConfig: disabled # Avoid using pre-install hooks, which might cause issues with ArgoCD disableHooks: false +# Annotations globally added to all resources +extraAnnotations: {} +# Labels globally added to all resources extraLabels: {} -# Add extra labels for k8s # Enables application network policy # For more info follow https://kubernetes.io/docs/concepts/services-networking/network-policies/ @@ -55,12 +64,13 @@ networkPolicy: host: defectdojo.default.minikube.local # The full URL to your defectdojo instance, depends on the domain where DD is deployed, it also affects links in Jira +siteUrl: "" # siteUrl: 'https://' # optional list of alternative hostnames to use that gets appended to # DD_ALLOWED_HOSTS. This is necessary when your local hostname does not match # the global hostname. -# alternativeHosts: +alternativeHosts: [] # - defectdojo.example.com imagePullPolicy: Always # Where to pull the defectDojo images from. Defaults to "defectdojo/*" repositories on hub.docker.com @@ -79,22 +89,13 @@ podLabels: {} # Allow overriding of revisionHistoryLimit across all deployments. revisionHistoryLimit: 10 -securityContext: - enabled: true - djangoSecurityContext: - # django dockerfile sets USER=1001 - runAsUser: 1001 - nginxSecurityContext: - # nginx dockerfile sets USER=1001 - runAsUser: 1001 - serviceAccount: # Specifies whether a service account should be created. create: true # The name of the service account to use. # If not set and create is true, a name is generated using the fullname template - # name: "" + name: "" # Optional additional annotations to add to the DefectDojo's Service Account. annotations: {} @@ -103,7 +104,15 @@ serviceAccount: labels: {} dbMigrationChecker: + # Enable/disable the DB migration checker. enabled: true + # Container security context for the DB migration checker. + containerSecurityContext: {} + # Additional environment variables for DB migration checker. + extraEnv: [] + # Array of additional volume mount points for DB migration checker. + extraVolumeMounts: [] + # Resource requests/limits for the DB migration checker. resources: requests: cpu: 100m @@ -134,13 +143,19 @@ admin: monitoring: enabled: false - # Add the nginx prometheus exporter sidecar prometheus: + # Add the nginx prometheus exporter sidecar enabled: false image: nginx/nginx-prometheus-exporter:1.4.2 imagePullPolicy: IfNotPresent - -annotations: {} + # Optional: container security context for nginx prometheus exporter + containerSecurityContext: {} + # Optional: additional environment variables injected to the nginx prometheus exporter container + extraEnv: [] + # Array of additional volume mount points for the nginx prometheus exporter + extraVolumeMounts: [] + # Optional: add resource requests/limits for the nginx prometheus exporter container + resources: {} secrets: # Add annotations for secret resources @@ -156,6 +171,8 @@ celery: # Annotations for the Celery beat deployment. annotations: {} affinity: {} + # Container security context for the Celery beat containers. + containerSecurityContext: {} # Additional environment variables injected to Celery beat containers. extraEnv: [] # A list of additional initContainers to run before celery beat containers. @@ -178,6 +195,8 @@ celery: nodeSelector: {} # Annotations for the Celery beat pods. podAnnotations: {} + # Pod security context for the Celery beat pods. + podSecurityContext: {} # Enable readiness probe for Celery beat container. readinessProbe: {} replicas: 1 @@ -195,6 +214,8 @@ celery: # Annotations for the Celery worker deployment. annotations: {} affinity: {} + # Container security context for the Celery worker containers. + containerSecurityContext: {} # Additional environment variables injected to Celery worker containers. extraEnv: [] # A list of additional initContainers to run before celery worker containers. @@ -217,6 +238,8 @@ celery: nodeSelector: {} # Annotations for the Celery beat pods. podAnnotations: {} + # Pod security context for the Celery worker pods. + podSecurityContext: {} # Enable readiness probe for Celery worker container. readinessProbe: {} replicas: 1 @@ -246,6 +269,9 @@ django: annotations: {} type: "" affinity: {} + # Pod security context for the Django pods. + podSecurityContext: + fsGroup: 1001 ingress: enabled: true ingressClassName: "" @@ -258,6 +284,10 @@ django: # nginx.ingress.kubernetes.io/proxy-read-timeout: "1800" # nginx.ingress.kubernetes.io/proxy-send-timeout: "1800" nginx: + # Container security context for the nginx containers. + containerSecurityContext: + # nginx dockerfile sets USER=1001 + runAsUser: 1001 # To extra environment variables to the nginx container, you can use extraEnv. For example: # extraEnv: # - name: FOO @@ -283,6 +313,9 @@ django: strategy: {} tolerations: [] uwsgi: + containerSecurityContext: + # django dockerfile sets USER=1001 + runAsUser: 1001 # To add (or override) extra variables which need to be pulled from another configMap, you can # use extraEnv. For example: # extraEnv: @@ -339,8 +372,12 @@ django: certMountPath: /certs/ certFileName: ca.crt + # Additional environment variables injected to all Django containers and initContainers. + extraEnv: [] # A list of additional initContainers to run before the uwsgi and nginx containers. extraInitContainers: [] + # Array of additional volume mount points common to all containers and initContainers. + extraVolumeMounts: [] # A list of extra volumes to mount. extraVolumes: [] @@ -378,12 +415,16 @@ initializer: limits: cpu: 2000m memory: 512Mi + # Container security context for the initializer Job container + containerSecurityContext: {} # 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: [] + # Pod security context for the initializer Job + podSecurityContext: {} # staticName defines whether name of the job will be the same (e.g., "defectdojo-initializer") # or different every time - generated based on current time (e.g., "defectdojo-initializer-2024-11-11-18-57") @@ -449,6 +490,14 @@ cloudsql: enable_iam_login: false # whether to use a private IP to connect to the database use_private_ip: false + # Optional: security context for the CloudSQL proxy container. + containerSecurityContext: {} + # Additional environment variables for the CloudSQL proxy container. + extraEnv: [] + # Array of additional volume mount points for the CloudSQL proxy container + extraVolumeMounts: [] + # Optional: add resource requests/limits for the CloudSQL proxy container. + resources: {} # Settings to make running the chart on GKE simpler gke: @@ -521,6 +570,7 @@ localsettingspy: "" # MIDDLEWARE = [ # 'debug_toolbar.middleware.DebugToolbarMiddleware', # ] + MIDDLEWARE + # # External database support. # From c05fff3b54e537fe45561e45a21ae9efbab2d87d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 7 Oct 2025 12:02:46 -0500 Subject: [PATCH 012/126] Bump social-auth-core from 4.7.0 to 4.8.0 (#13360) Bumps [social-auth-core](https://github.com/python-social-auth/social-core) from 4.7.0 to 4.8.0. - [Release notes](https://github.com/python-social-auth/social-core/releases) - [Changelog](https://github.com/python-social-auth/social-core/blob/master/CHANGELOG.md) - [Commits](https://github.com/python-social-auth/social-core/compare/4.7.0...4.8.0) --- updated-dependencies: - dependency-name: social-auth-core dependency-version: 4.8.0 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 579acbfb8ab..360687eb9c5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -40,7 +40,7 @@ vobject==0.9.9 whitenoise==5.2.0 titlecase==2.4.1 social-auth-app-django==5.5.1 -social-auth-core==4.7.0 +social-auth-core==4.8.0 gitpython==3.1.45 python-gitlab==6.4.0 cpe==1.3.1 From eb3c83a3d7518107cddb48ecc8ddad9d55befe58 Mon Sep 17 00:00:00 2001 From: manuelsommer <47991713+manuel-sommer@users.noreply.github.com> Date: Wed, 8 Oct 2025 17:51:55 +0200 Subject: [PATCH 013/126] :arrow_up: Bump ruff from 0.13.2 to 0.14.0 (#13337) * :arrow_up: Bump ruff from 0.13.2 to 0.13.3 * bump * fix * Update settings.dist.py * Update requirements-lint.txt --- dojo/apps.py | 30 +++++++++++++++--------------- requirements-lint.txt | 2 +- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/dojo/apps.py b/dojo/apps.py index f47eb5184f2..f1b2769f760 100644 --- a/dojo/apps.py +++ b/dojo/apps.py @@ -72,21 +72,21 @@ def ready(self): # Load any signals here that will be ready for runtime # Importing the signals file is good enough if using the reciever decorator - import dojo.announcement.signals # noqa: PLC0415 raised: AppRegistryNotReady - import dojo.benchmark.signals # noqa: PLC0415 raised: AppRegistryNotReady - import dojo.cred.signals # noqa: PLC0415 raised: AppRegistryNotReady - import dojo.endpoint.signals # noqa: PLC0415 raised: AppRegistryNotReady - import dojo.engagement.signals # noqa: PLC0415 raised: AppRegistryNotReady - import dojo.file_uploads.signals # noqa: PLC0415 raised: AppRegistryNotReady - import dojo.finding_group.signals # noqa: PLC0415 raised: AppRegistryNotReady - import dojo.notes.signals # noqa: PLC0415 raised: AppRegistryNotReady - import dojo.product.signals # noqa: PLC0415 raised: AppRegistryNotReady - import dojo.product_type.signals # noqa: PLC0415 raised: AppRegistryNotReady - import dojo.risk_acceptance.signals # noqa: PLC0415 raised: AppRegistryNotReady - import dojo.sla_config.helpers # noqa: PLC0415 raised: AppRegistryNotReady - import dojo.tags_signals # noqa: PLC0415 raised: AppRegistryNotReady - import dojo.test.signals # noqa: PLC0415 raised: AppRegistryNotReady - import dojo.tool_product.signals # noqa: F401,PLC0415 raised: AppRegistryNotReady + import dojo.announcement.signals # noqa: PLC0415, F401 raised: AppRegistryNotReady + import dojo.benchmark.signals # noqa: PLC0415, F401 raised: AppRegistryNotReady + import dojo.cred.signals # noqa: PLC0415, F401 raised: AppRegistryNotReady + import dojo.endpoint.signals # noqa: PLC0415, F401 raised: AppRegistryNotReady + import dojo.engagement.signals # noqa: PLC0415, F401 raised: AppRegistryNotReady + import dojo.file_uploads.signals # noqa: PLC0415, F401 raised: AppRegistryNotReady + import dojo.finding_group.signals # noqa: PLC0415, F401 raised: AppRegistryNotReady + import dojo.notes.signals # noqa: PLC0415, F401 raised: AppRegistryNotReady + import dojo.product.signals # noqa: PLC0415, F401 raised: AppRegistryNotReady + import dojo.product_type.signals # noqa: PLC0415, F401 raised: AppRegistryNotReady + import dojo.risk_acceptance.signals # noqa: PLC0415, F401 raised: AppRegistryNotReady + import dojo.sla_config.helpers # noqa: PLC0415, F401 raised: AppRegistryNotReady + import dojo.tags_signals # noqa: PLC0415, F401 raised: AppRegistryNotReady + import dojo.test.signals # noqa: PLC0415, F401 raised: AppRegistryNotReady + import dojo.tool_product.signals # noqa: PLC0415, F401 raised: AppRegistryNotReady # Configure audit system after all models are loaded # This must be done in ready() to avoid "Models aren't loaded yet" errors diff --git a/requirements-lint.txt b/requirements-lint.txt index 6a0ba23ce92..3a9145e0960 100644 --- a/requirements-lint.txt +++ b/requirements-lint.txt @@ -1 +1 @@ -ruff==0.13.2 \ No newline at end of file +ruff==0.14.0 From 602e905473a47e2c18a73e8a54c67d7dfc36c46e Mon Sep 17 00:00:00 2001 From: kiblik <5609770+kiblik@users.noreply.github.com> Date: Wed, 8 Oct 2025 20:02:00 +0200 Subject: [PATCH 014/126] feat(docker): Use Python 3.13 in docker images (#13022) --- .github/pull_request_template.md | 2 +- Dockerfile.django-alpine | 2 +- Dockerfile.django-debian | 2 +- Dockerfile.integration-tests-debian | 2 +- Dockerfile.nginx-alpine | 2 +- readme-docs/CONTRIBUTING.md | 4 ++-- ruff.toml | 4 ++-- 7 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index ae0f0db498f..6a68f9ca170 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -26,7 +26,7 @@ This checklist is for your information. - [ ] Bugfixes should be submitted against the `bugfix` branch. - [ ] Give a meaningful name to your PR, as it may end up being used in the release notes. - [ ] Your code is flake8 compliant. -- [ ] Your code is python 3.12 compliant. +- [ ] Your code is python 3.13 compliant. - [ ] If this is a new feature and not a bug fix, you've included the proper documentation in the docs at https://github.com/DefectDojo/django-DefectDojo/tree/dev/docs as part of this PR. - [ ] Model changes must include the necessary migrations in the dojo/db_migrations folder. - [ ] Add applicable tests to the unit tests. diff --git a/Dockerfile.django-alpine b/Dockerfile.django-alpine index 010017b0f50..bcca856298a 100644 --- a/Dockerfile.django-alpine +++ b/Dockerfile.django-alpine @@ -5,7 +5,7 @@ # Dockerfile.nginx to use the caching mechanism of Docker. # Ref: https://devguide.python.org/#branchstatus -FROM python:3.12.11-alpine3.22@sha256:02a73ead8397e904cea6d17e18516f1df3590e05dc8823bd5b1c7f849227d272 AS base +FROM python:3.13.7-alpine3.22@sha256:9ba6d8cbebf0fb6546ae71f2a1c14f6ffd2fdab83af7fa5669734ef30ad48844 AS base FROM base AS build WORKDIR /app RUN \ diff --git a/Dockerfile.django-debian b/Dockerfile.django-debian index b8077bb0b77..e816d204e05 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.12.11-slim-trixie@sha256:d67a7b66b989ad6b6d6b10d428dcc5e0bfc3e5f88906e67d490c4d3daac57047 AS base +FROM python:3.13.7-slim-trixie@sha256:5f55cdf0c5d9dc1a415637a5ccc4a9e18663ad203673173b8cda8f8dcacef689 AS base FROM base AS build WORKDIR /app RUN \ diff --git a/Dockerfile.integration-tests-debian b/Dockerfile.integration-tests-debian index 95398cb6e8e..06cf3b7c435 100644 --- a/Dockerfile.integration-tests-debian +++ b/Dockerfile.integration-tests-debian @@ -3,7 +3,7 @@ FROM openapitools/openapi-generator-cli:v7.16.0@sha256:e56372add5e038753fb91aa1bbb470724ef58382fdfc35082bf1b3e079ce353c AS openapitools # currently only supports x64, no arm yet due to chrome and selenium dependencies -FROM python:3.12.11-slim-trixie@sha256:d67a7b66b989ad6b6d6b10d428dcc5e0bfc3e5f88906e67d490c4d3daac57047 AS build +FROM python:3.13.7-slim-trixie@sha256:5f55cdf0c5d9dc1a415637a5ccc4a9e18663ad203673173b8cda8f8dcacef689 AS build WORKDIR /app RUN \ apt-get -y update && \ diff --git a/Dockerfile.nginx-alpine b/Dockerfile.nginx-alpine index fd50cb9e472..5270e01f747 100644 --- a/Dockerfile.nginx-alpine +++ b/Dockerfile.nginx-alpine @@ -5,7 +5,7 @@ # Dockerfile.django-alpine to use the caching mechanism of Docker. # Ref: https://devguide.python.org/#branchstatus -FROM python:3.12.11-alpine3.22@sha256:02a73ead8397e904cea6d17e18516f1df3590e05dc8823bd5b1c7f849227d272 AS base +FROM python:3.13.7-alpine3.22@sha256:9ba6d8cbebf0fb6546ae71f2a1c14f6ffd2fdab83af7fa5669734ef30ad48844 AS base FROM base AS build WORKDIR /app RUN \ diff --git a/readme-docs/CONTRIBUTING.md b/readme-docs/CONTRIBUTING.md index e440f498c07..27f8093355e 100644 --- a/readme-docs/CONTRIBUTING.md +++ b/readme-docs/CONTRIBUTING.md @@ -56,7 +56,7 @@ Please use [these test scripts](../tests) to test your changes. These are the sc For changes that require additional settings, you can now use local_settings.py file. See the logging section below for more information. ## Python3 Version -For compatibility reasons, the code in dev branch should be python3.12 compliant. +For compatibility reasons, the code in dev branch should be python3.13 compliant. ## Database migrations When changes are made to the database model, a database migration is needed. This migration can be generated using something like @@ -82,7 +82,7 @@ DefectDojo. 0. Pull requests should be submitted to the `dev` or `bugfix` branch. -0. In dev branch, the code should be python 3.12 compliant. +0. In dev branch, the code should be python 3.13 compliant. [dojo_settings]: /dojo/settings/settings.dist.py "DefectDojo settings file" [pep8]: https://www.python.org/dev/peps/pep-0008/ "PEP8" diff --git a/ruff.toml b/ruff.toml index 598517dd435..092bd58fee0 100644 --- a/ruff.toml +++ b/ruff.toml @@ -1,5 +1,5 @@ -# Always generate Python 3.12-compatible code. -target-version = "py312" +# Always generate Python 3.13-compatible code. +target-version = "py313" # Same as Black. line-length = 120 From 0399b58c3bb600be362d97cfe4cd6a03ab92a1b6 Mon Sep 17 00:00:00 2001 From: valentijnscholten Date: Wed, 8 Oct 2025 21:30:39 +0200 Subject: [PATCH 015/126] apiv2: fix schema for engagements endpoint (#13336) --- dojo/api_v2/views.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/dojo/api_v2/views.py b/dojo/api_v2/views.py index 65591bfb6af..0d6e25380e0 100644 --- a/dojo/api_v2/views.py +++ b/dojo/api_v2/views.py @@ -398,7 +398,8 @@ def get_queryset(self): # @extend_schema_view(**schema_with_prefetch()) # Nested models with prefetch make the response schema too long for Swagger UI class EngagementViewSet( - PrefetchDojoModelViewSet, + # PrefetchDojoModelViewSet, + DojoModelViewSet, ra_api.AcceptedRisksMixin, ): serializer_class = serializers.EngagementSerializer From fcfee2f6fe6c60bd54505cea8ce923175a4f24e3 Mon Sep 17 00:00:00 2001 From: valentijnscholten Date: Wed, 8 Oct 2025 21:30:46 +0200 Subject: [PATCH 016/126] importers: defend against parsers returning None (#13335) --- dojo/importers/default_importer.py | 2 +- dojo/importers/default_reimporter.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/dojo/importers/default_importer.py b/dojo/importers/default_importer.py index d127ed33f6a..726e55717eb 100644 --- a/dojo/importers/default_importer.py +++ b/dojo/importers/default_importer.py @@ -108,7 +108,7 @@ def process_scan( parser = self.get_parser() # Get the findings from the parser based on what methods the parser supplies # This could either mean traditional file parsing, or API pull parsing - parsed_findings = self.parse_findings(scan, parser) + parsed_findings = self.parse_findings(scan, parser) or [] # process the findings in the foreground or background new_findings = self.determine_process_method(parsed_findings, **kwargs) # Close any old findings in the processed list if the the user specified for that diff --git a/dojo/importers/default_reimporter.py b/dojo/importers/default_reimporter.py index 7adb2c65c48..17775eb22ae 100644 --- a/dojo/importers/default_reimporter.py +++ b/dojo/importers/default_reimporter.py @@ -93,7 +93,7 @@ def process_scan( parser = self.get_parser() # Get the findings from the parser based on what methods the parser supplies # This could either mean traditional file parsing, or API pull parsing - parsed_findings = self.parse_findings(scan, parser) + parsed_findings = self.parse_findings(scan, parser) or [] # process the findings in the foreground or background ( new_findings, From fbbc7a018b23bd07682677165789e794e4dbd217 Mon Sep 17 00:00:00 2001 From: valentijnscholten Date: Wed, 8 Oct 2025 21:30:54 +0200 Subject: [PATCH 017/126] fix upload error when finding groups disabled (#13334) --- dojo/engagement/views.py | 2 +- dojo/test/views.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/dojo/engagement/views.py b/dojo/engagement/views.py index 7ae3e758ead..7bde94ae338 100644 --- a/dojo/engagement/views.py +++ b/dojo/engagement/views.py @@ -974,7 +974,7 @@ def process_form( "apply_tags_to_endpoints": form.cleaned_data.get("apply_tags_to_endpoints", False), "close_old_findings_product_scope": form.cleaned_data.get("close_old_findings_product_scope", None), "group_by": form.cleaned_data.get("group_by", None), - "create_finding_groups_for_all_findings": form.cleaned_data.get("create_finding_groups_for_all_findings"), + "create_finding_groups_for_all_findings": form.cleaned_data.get("create_finding_groups_for_all_findings", None), "environment": self.get_development_environment(environment_name=form.cleaned_data.get("environment")), }) # Create the engagement if necessary diff --git a/dojo/test/views.py b/dojo/test/views.py index 06301d20813..ad98b4e17a9 100644 --- a/dojo/test/views.py +++ b/dojo/test/views.py @@ -915,7 +915,7 @@ def process_form( "apply_tags_to_endpoints": form.cleaned_data.get("apply_tags_to_endpoints", False), "group_by": form.cleaned_data.get("group_by", None), "close_old_findings": form.cleaned_data.get("close_old_findings", None), - "create_finding_groups_for_all_findings": form.cleaned_data.get("create_finding_groups_for_all_findings"), + "create_finding_groups_for_all_findings": form.cleaned_data.get("create_finding_groups_for_all_findings", None), }) # Override the form values of active and verified if activeChoice := form.cleaned_data.get("active", None): From c8c47505572f7a1e158cd7cf5ab6193c93cc374c Mon Sep 17 00:00:00 2001 From: valentijnscholten Date: Wed, 8 Oct 2025 21:31:01 +0200 Subject: [PATCH 018/126] engagement: allow unlinking of JIRA epic (#13333) --- dojo/engagement/urls.py | 2 ++ dojo/engagement/views.py | 37 ++++++++++++++++++++++++++++- dojo/templates/dojo/view_eng.html | 39 ++++++++++++++++++++++++++----- 3 files changed, 71 insertions(+), 7 deletions(-) diff --git a/dojo/engagement/urls.py b/dojo/engagement/urls.py index c70bb56a95e..0f33c3aa697 100644 --- a/dojo/engagement/urls.py +++ b/dojo/engagement/urls.py @@ -30,6 +30,8 @@ name="close_engagement"), re_path(r"^engagement/(?P\d+)/reopen$", views.reopen_eng, name="reopen_engagement"), + re_path(r"^engagement/(?P\d+)/jira/unlink$", views.unlink_jira, + name="engagement_unlink_jira"), re_path(r"^engagement/(?P\d+)/complete_checklist$", views.complete_checklist, name="complete_checklist"), re_path(r"^engagement/(?P\d+)/risk_acceptance/add$", diff --git a/dojo/engagement/views.py b/dojo/engagement/views.py index 7bde94ae338..a02ff45f6aa 100644 --- a/dojo/engagement/views.py +++ b/dojo/engagement/views.py @@ -19,13 +19,14 @@ from django.db.models import OuterRef, Q, Value from django.db.models.functions import Coalesce from django.db.models.query import Prefetch, QuerySet -from django.http import HttpRequest, HttpResponse, HttpResponseRedirect, QueryDict, StreamingHttpResponse +from django.http import HttpRequest, HttpResponse, HttpResponseRedirect, JsonResponse, QueryDict, StreamingHttpResponse from django.shortcuts import get_object_or_404, render from django.urls import Resolver404, reverse from django.utils import timezone from django.utils.translation import gettext as _ from django.views import View from django.views.decorators.cache import cache_page +from django.views.decorators.http import require_POST from django.views.decorators.vary import vary_on_cookie from openpyxl import Workbook from openpyxl.styles import Font @@ -1134,6 +1135,40 @@ def close_eng(request, eid): return HttpResponseRedirect(reverse("view_engagements", args=(eng.product.id, ))) +@user_is_authorized(Engagement, Permissions.Engagement_Edit, "eid") +@require_POST +def unlink_jira(request, eid): + eng = get_object_or_404(Engagement, id=eid) + logger.info("trying to unlink a linked jira epic from engagement %d:%s", eng.id, eng.name) + if eng.has_jira_issue: + try: + jira_helper.unlink_jira(request, eng) + messages.add_message( + request, + messages.SUCCESS, + "Link to JIRA epic successfully deleted", + extra_tags="alert-success", + ) + return JsonResponse({"result": "OK"}) + except Exception: + logger.exception("Link to JIRA epic could not be deleted") + messages.add_message( + request, + messages.ERROR, + "Link to JIRA epic could not be deleted, see alerts for details", + extra_tags="alert-danger", + ) + return HttpResponse(status=500) + else: + messages.add_message( + request, + messages.ERROR, + "Link to JIRA epic not found", + extra_tags="alert-danger", + ) + return HttpResponse(status=400) + + @user_is_authorized(Engagement, Permissions.Engagement_Edit, "eid") def reopen_eng(request, eid): eng = Engagement.objects.get(id=eid) diff --git a/dojo/templates/dojo/view_eng.html b/dojo/templates/dojo/view_eng.html index 728b8867f7d..ab09dadb7c5 100644 --- a/dojo/templates/dojo/view_eng.html +++ b/dojo/templates/dojo/view_eng.html @@ -826,13 +826,18 @@

{% if jissue and jira_project %} - - Jira - {{ eng | jira_key }} + + Jira + + {{ eng | jira_key }} (epic) - - - + + {% if eng|has_object_permission:"Engagement_Edit" %} +   + + {% endif %} + + {% elif jira_project %} JIRA @@ -1088,6 +1093,28 @@

var host = slashes.concat(window.location.host); modal.find('p#questionnaireURL').text('Questionnaire URL: ' + host + path) }) + + function jira_action(elem, url) { + $(elem).removeClass().addClass('fa-solid fa-spin fa-spinner') + + $.ajax({ + type: "post", + dataType:'json', + data: '', + context: this, + url: url, + beforeSend: function (jqXHR, settings) { + jqXHR.setRequestHeader('X-CSRFToken', '{{ csrf_token }}'); + }, + complete: function(e) { + location.reload() + } + }); + } + + $("#unlink_eng_jira").on('click', function(e) { + jira_action(this,'{% url 'engagement_unlink_jira' eng.id %}') + }); }); {% include 'dojo/snippets/risk_acceptance_actions_snippet_js.html' %} From e3f9734e6ed2be244494a3df53a5fe410c060980 Mon Sep 17 00:00:00 2001 From: valentijnscholten Date: Wed, 8 Oct 2025 21:31:08 +0200 Subject: [PATCH 019/126] user mentioning: diplay author instead of recipient (#13332) --- dojo/templates/notifications/alert/user_mentioned.tpl | 2 +- dojo/templates/notifications/mail/user_mentioned.tpl | 2 +- dojo/templates/notifications/msteams/user_mentioned.tpl | 4 ++-- dojo/templates/notifications/slack/user_mentioned.tpl | 4 ++-- dojo/utils.py | 3 ++- 5 files changed, 8 insertions(+), 7 deletions(-) diff --git a/dojo/templates/notifications/alert/user_mentioned.tpl b/dojo/templates/notifications/alert/user_mentioned.tpl index 1fc741ee2d7..9a0b35c0470 100644 --- a/dojo/templates/notifications/alert/user_mentioned.tpl +++ b/dojo/templates/notifications/alert/user_mentioned.tpl @@ -1,4 +1,4 @@ {% load i18n %}{% blocktranslate trimmed %} -User {{ user }} jotted a note on {{ section }}{% endblocktranslate %}: +User {{ requested_by }} jotted a note on {{ section }}{% endblocktranslate %}: {{ note }} \ No newline at end of file diff --git a/dojo/templates/notifications/mail/user_mentioned.tpl b/dojo/templates/notifications/mail/user_mentioned.tpl index 9601da3c9a5..d828940400d 100644 --- a/dojo/templates/notifications/mail/user_mentioned.tpl +++ b/dojo/templates/notifications/mail/user_mentioned.tpl @@ -9,7 +9,7 @@

{% blocktranslate trimmed %} - User {{ user }} jotted a note on {{ section }}:
+ User {{ requested_by }} jotted a note on {{ section }}:

{{ note }}

diff --git a/dojo/templates/notifications/msteams/user_mentioned.tpl b/dojo/templates/notifications/msteams/user_mentioned.tpl index ed8f38ee80c..aba4d11c089 100644 --- a/dojo/templates/notifications/msteams/user_mentioned.tpl +++ b/dojo/templates/notifications/msteams/user_mentioned.tpl @@ -54,7 +54,7 @@ NOTE: This template is currently NOT USED in practice because: }, { "type": "TextBlock", - "text": "{% trans 'User' %} {{ user }} {% trans 'mentioned you in' %} {{ section }}.", + "text": "{% trans 'User' %} {{ requested_by }} {% trans 'mentioned you in' %} {{ section }}.", "wrap": true, "spacing": "Medium" }, @@ -63,7 +63,7 @@ NOTE: This template is currently NOT USED in practice because: "facts": [ { "title": "{% trans 'User' %}:", - "value": "{{ user }}" + "value": "{{ requested_by }}" }, { "title": "{% trans 'Section' %}:", diff --git a/dojo/templates/notifications/slack/user_mentioned.tpl b/dojo/templates/notifications/slack/user_mentioned.tpl index aba6c9aed6a..9131de845a8 100644 --- a/dojo/templates/notifications/slack/user_mentioned.tpl +++ b/dojo/templates/notifications/slack/user_mentioned.tpl @@ -1,12 +1,12 @@ {% load i18n %}{% blocktranslate trimmed %} -User {{ user }} jotted a note on {{ section }}: +User {{ requested_by }} jotted a note on {{ section }}: {{ note }} Full details of the note can be reviewed at {{ url }} {% endblocktranslate %} {% if system_settings.disclaimer_notifications and system_settings.disclaimer_notifications.strip %} - + {% trans "Disclaimer" %}: {{ system_settings.disclaimer_notifications }} {% endif %} diff --git a/dojo/utils.py b/dojo/utils.py index 414a8600a6f..07709c4bbbf 100644 --- a/dojo/utils.py +++ b/dojo/utils.py @@ -1461,7 +1461,8 @@ def process_tag_notifications(request, note, parent_url, parent_title): title=f"{request.user} jotted a note", url=parent_url, icon="commenting", - recipients=users_to_notify) + recipients=users_to_notify, + requested_by=get_current_user()) def encrypt(key, iv, plaintext): From 5ba26b93f43478d0d7631651c494647a680c9651 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 10 Oct 2025 15:57:23 -0600 Subject: [PATCH 020/126] Bump datatables.net-colreorder from 2.1.1 to 2.1.2 in /components (#13396) Bumps [datatables.net-colreorder](https://github.com/DataTables/Dist-DataTables-ColReorder) from 2.1.1 to 2.1.2. - [Release notes](https://github.com/DataTables/Dist-DataTables-ColReorder/releases) - [Commits](https://github.com/DataTables/Dist-DataTables-ColReorder/compare/2.1.1...2.1.2) --- updated-dependencies: - dependency-name: datatables.net-colreorder dependency-version: 2.1.2 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 e5898ca8f4d..9b3c0a01c58 100644 --- a/components/package.json +++ b/components/package.json @@ -14,7 +14,7 @@ "clipboard": "^2.0.11", "datatables.net": "^2.3.4", "datatables.net-buttons-bs": "^3.2.5", - "datatables.net-colreorder": "^2.1.1", + "datatables.net-colreorder": "^2.1.2", "drmonty-datatables-plugins": "^1.0.0", "drmonty-datatables-responsive": "^1.0.0", "easymde": "^2.20.0", diff --git a/components/yarn.lock b/components/yarn.lock index 78aa6e5e86e..9df054d62d4 100644 --- a/components/yarn.lock +++ b/components/yarn.lock @@ -204,10 +204,10 @@ datatables.net-buttons@3.2.5: datatables.net "^2" jquery ">=1.7" -datatables.net-colreorder@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/datatables.net-colreorder/-/datatables.net-colreorder-2.1.1.tgz#ddcbfb27d5e2b97fe8ce4acdb8ca35442a801fe5" - integrity sha512-alhSZYEYmxsXujl43nIHh2+Ym8o/CBm/2kPIExcUz7sOB8FOw2Q614KztqRYh46V5IA+RUuGSxzodjakZ63wAQ== +datatables.net-colreorder@^2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/datatables.net-colreorder/-/datatables.net-colreorder-2.1.2.tgz#cf45eae93f4afd0bbe2f34d47105b312defa8cc7" + integrity sha512-lIsUyOt2nBm4sD2cSzDKZcIVrGgrZkh90Z2f03s8p7DYcZSfXMHAhFBrDYf9/eAK6wJnODN8EDMsrtPHfgoSXA== dependencies: datatables.net "^2" jquery ">=1.7" From d90b09fe7470e593a7a32b5177371adf13545921 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 10 Oct 2025 15:57:38 -0600 Subject: [PATCH 021/126] Bump boto3 from 1.40.46 to 1.40.49 (#13395) Bumps [boto3](https://github.com/boto/boto3) from 1.40.46 to 1.40.49. - [Release notes](https://github.com/boto/boto3/releases) - [Commits](https://github.com/boto/boto3/compare/1.40.46...1.40.49) --- updated-dependencies: - dependency-name: boto3 dependency-version: 1.40.49 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 360687eb9c5..c03bac166ca 100644 --- a/requirements.txt +++ b/requirements.txt @@ -60,7 +60,7 @@ django-ratelimit==4.1.0 argon2-cffi==25.1.0 blackduck==1.1.3 pycurl==7.45.7 # Required for Celery Broker AWS (SQS) support -boto3==1.40.46 # Required for Celery Broker AWS (SQS) support +boto3==1.40.49 # Required for Celery Broker AWS (SQS) support netaddr==1.3.0 vulners==3.1.1 fontawesomefree==6.6.0 From b45c9462f368d0f1e27af433cd21372ef0ce00cd Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 10 Oct 2025 15:59:09 -0600 Subject: [PATCH 022/126] Bump social-auth-core from 4.8.0 to 4.8.1 (#13389) Bumps [social-auth-core](https://github.com/python-social-auth/social-core) from 4.8.0 to 4.8.1. - [Release notes](https://github.com/python-social-auth/social-core/releases) - [Changelog](https://github.com/python-social-auth/social-core/blob/master/CHANGELOG.md) - [Commits](https://github.com/python-social-auth/social-core/compare/4.8.0...4.8.1) --- updated-dependencies: - dependency-name: social-auth-core dependency-version: 4.8.1 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 c03bac166ca..8c910946213 100644 --- a/requirements.txt +++ b/requirements.txt @@ -40,7 +40,7 @@ vobject==0.9.9 whitenoise==5.2.0 titlecase==2.4.1 social-auth-app-django==5.5.1 -social-auth-core==4.8.0 +social-auth-core==4.8.1 gitpython==3.1.45 python-gitlab==6.4.0 cpe==1.3.1 From 3f94b41266717783884a3ab656848a06ca051206 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 10 Oct 2025 16:00:54 -0600 Subject: [PATCH 023/126] chore(deps): update redis:7.2.11-alpine docker digest from 7.2.11 to v (docker-compose.yml) (#13386) 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 e5238fa6b8b..06857162521 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -129,7 +129,7 @@ services: - defectdojo_postgres:/var/lib/postgresql/data redis: # Pinning to this version due to licensing constraints - image: redis:7.2.11-alpine@sha256:b19a2cb55412c26e0c3725011ac04397eba5cb0d7f090f739cbbce8dc97f8e60 + image: redis:7.2.11-alpine@sha256:cd3e4dbac9604660d08efac21b27daa2ae91dde1e19203b49ec8567050ba093f volumes: - defectdojo_redis:/data volumes: From 5687ab9776464b7f82aec9d57d0465765b57134a Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 10 Oct 2025 16:01:12 -0600 Subject: [PATCH 024/126] chore(deps): update postgres:18.0-alpine docker digest from 18.0 to 18.0-alpine (docker-compose.yml) (#13385) 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 06857162521..e13eba26e99 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:18.0-alpine@sha256:70b32afe0c274b4d93098fd724fcdaab3aba47270a4f1e63cbf9cc69d7bf1be4 + image: postgres:18.0-alpine@sha256:f898ac406e1a9e05115cc2efcb3c3abb3a92a4c0263f3b6f6aaae354cbb1953a environment: POSTGRES_DB: ${DD_DATABASE_NAME:-defectdojo} POSTGRES_USER: ${DD_DATABASE_USER:-defectdojo} From 14192115120b66e6f05e4c5666c710e9a7b2c3dd Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 10 Oct 2025 16:03:42 -0600 Subject: [PATCH 025/126] fix(deps): update dependency @docsearch/js from 4.1.0 to v4.2.0 (docs/package.json) (#13382) 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 cdd0561b267..1947f324dce 100644 --- a/docs/package-lock.json +++ b/docs/package-lock.json @@ -10,7 +10,7 @@ "license": "MIT", "dependencies": { "@docsearch/css": "4.1.0", - "@docsearch/js": "4.1.0", + "@docsearch/js": "4.2.0", "@tabler/icons": "3.35.0", "@thulite/doks-core": "1.8.0", "@thulite/images": "3.3.0", @@ -1513,9 +1513,9 @@ "license": "MIT" }, "node_modules/@docsearch/js": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@docsearch/js/-/js-4.1.0.tgz", - "integrity": "sha512-49+CzeGfOiwG85k+dDvKfOsXLd9PQACoY/FLrZfFOKmpWv166u7bAHmBLdzvxlk8nJ289UgpGf0k6GQZtC85Fg==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@docsearch/js/-/js-4.2.0.tgz", + "integrity": "sha512-KBHVPO29QiGUFJYeAqxW0oXtGf/aghNmRrIRPT4/28JAefqoCkNn/ZM/jeQ7fHjl0KNM6C+KlLVYjwyz6lNZnA==", "license": "MIT" }, "node_modules/@esbuild/aix-ppc64": { diff --git a/docs/package.json b/docs/package.json index 007c3374468..9bbc1be19b0 100644 --- a/docs/package.json +++ b/docs/package.json @@ -13,7 +13,7 @@ }, "dependencies": { "@docsearch/css": "4.1.0", - "@docsearch/js": "4.1.0", + "@docsearch/js": "4.2.0", "@thulite/doks-core": "1.8.0", "@thulite/images": "3.3.0", "@thulite/inline-svg": "1.2.1", From e6cb0aba70c7286971feca0a4a6774c0a8ef0d88 Mon Sep 17 00:00:00 2001 From: kiblik <5609770+kiblik@users.noreply.github.com> Date: Sat, 11 Oct 2025 00:04:19 +0200 Subject: [PATCH 026/126] feat(helm): Simplify k8s-tests.yml (#13379) --- .github/workflows/k8s-tests.yml | 25 +++++-------------------- 1 file changed, 5 insertions(+), 20 deletions(-) diff --git a/.github/workflows/k8s-tests.yml b/.github/workflows/k8s-tests.yml index dc30f685793..154edb299f1 100644 --- a/.github/workflows/k8s-tests.yml +++ b/.github/workflows/k8s-tests.yml @@ -5,15 +5,6 @@ on: env: DD_HOSTNAME: defectdojo.default.minikube.local - HELM_REDIS_BROKER_SETTINGS: " \ - --set redis.enabled=true \ - --set celery.broker=redis \ - --set createRedisSecret=true \ - " - HELM_PG_DATABASE_SETTINGS: " \ - --set postgresql.enabled=true \ - --set createPostgresqlSecret=true \ - " jobs: setting_minikube_cluster: name: Kubernetes Deployment @@ -25,9 +16,7 @@ jobs: # databases, broker and k8s are independent, so we don't need to test each combination # lastest k8s version (https://kubernetes.io/releases/) and oldest supported version from aws # are tested (https://docs.aws.amazon.com/eks/latest/userguide/kubernetes-versions.html#available-versions) - - databases: pgsql - brokers: redis - k8s: 'v1.34.0' + - k8s: 'v1.34.0' os: debian steps: - name: Checkout @@ -68,12 +57,6 @@ jobs: helm dependency list ./helm/defectdojo helm dependency update ./helm/defectdojo - - name: Set confings into Outputs - id: set - run: |- - echo "pgsql=${{ env.HELM_PG_DATABASE_SETTINGS }}" >> $GITHUB_ENV - echo "redis=${{ env.HELM_REDIS_BROKER_SETTINGS }}" >> $GITHUB_ENV - - name: Deploying Django application with ${{ matrix.databases }} ${{ matrix.brokers }} timeout-minutes: 15 run: |- @@ -86,8 +69,10 @@ jobs: --set django.ingress.enabled=true \ --set imagePullPolicy=Never \ --set initializer.keepSeconds="-1" \ - ${{ env[matrix.databases] }} \ - ${{ env[matrix.brokers] }} \ + --set redis.enabled=true \ + --set createRedisSecret=true \ + --set postgresql.enabled=true \ + --set createPostgresqlSecret=true \ --set createSecret=true - name: Check deployment status From 3a91a810d9d343635e807816877f844028ab19e8 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 10 Oct 2025 16:06:41 -0600 Subject: [PATCH 027/126] chore(deps): update dependency python from 3.13.8 to 3.14 (.github/workflows/test-helm-chart.yml) (#13374) 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 0e837df2167..c50f84daafa 100644 --- a/.github/workflows/test-helm-chart.yml +++ b/.github/workflows/test-helm-chart.yml @@ -24,7 +24,7 @@ jobs: - uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 with: - python-version: 3.13 + python-version: 3.14 - name: Configure Helm repos run: |- From 66054b3bf3f9fc10f45c03d1be3d5faa5e84de83 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 10 Oct 2025 16:23:50 -0600 Subject: [PATCH 028/126] Bump social-auth-app-django from 5.5.1 to 5.6.0 (#13388) Bumps [social-auth-app-django](https://github.com/python-social-auth/social-app-django) from 5.5.1 to 5.6.0. - [Release notes](https://github.com/python-social-auth/social-app-django/releases) - [Changelog](https://github.com/python-social-auth/social-app-django/blob/master/CHANGELOG.md) - [Commits](https://github.com/python-social-auth/social-app-django/compare/5.5.1...5.6.0) --- updated-dependencies: - dependency-name: social-auth-app-django dependency-version: 5.6.0 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 8c910946213..2ee1744cb84 100644 --- a/requirements.txt +++ b/requirements.txt @@ -39,7 +39,7 @@ uWSGI==2.0.30 vobject==0.9.9 whitenoise==5.2.0 titlecase==2.4.1 -social-auth-app-django==5.5.1 +social-auth-app-django==5.6.0 social-auth-core==4.8.1 gitpython==3.1.45 python-gitlab==6.4.0 From f61e3aa74c49344ec90ae165de838d2b83342d58 Mon Sep 17 00:00:00 2001 From: kiblik <5609770+kiblik@users.noreply.github.com> Date: Sat, 11 Oct 2025 19:15:39 +0200 Subject: [PATCH 029/126] feat(helm): DRY cloudsql-proxy (#13369) --- helm/defectdojo/Chart.yaml | 4 +- helm/defectdojo/templates/_helpers.tpl | 37 +++++++++++++++++++ .../templates/celery-beat-deployment.yaml | 26 +------------ .../templates/celery-worker-deployment.yaml | 26 +------------ .../templates/django-deployment.yaml | 32 +--------------- .../defectdojo/templates/initializer-job.yaml | 33 +---------------- 6 files changed, 44 insertions(+), 114 deletions(-) diff --git a/helm/defectdojo/Chart.yaml b/helm/defectdojo/Chart.yaml index 7dfd0153e86..44bf7144110 100644 --- a/helm/defectdojo/Chart.yaml +++ b/helm/defectdojo/Chart.yaml @@ -19,4 +19,6 @@ dependencies: condition: redis.enabled annotations: artifacthub.io/prerelease: "true" - artifacthub.io/changes: "" + artifacthub.io/changes: | + - kind: changed + description: DRY cloudsql-proxy diff --git a/helm/defectdojo/templates/_helpers.tpl b/helm/defectdojo/templates/_helpers.tpl index c4b6f130ab0..0e3db8a4fc1 100644 --- a/helm/defectdojo/templates/_helpers.tpl +++ b/helm/defectdojo/templates/_helpers.tpl @@ -181,6 +181,43 @@ {{- end }} {{- end -}} +{{- /* + Define cloudsql-proxy +*/}} +{{- define "cloudsqlProxy" -}} +- name: cloudsql-proxy + image: {{ .Values.cloudsql.image.repository }}:{{ .Values.cloudsql.image.tag }} + imagePullPolicy: {{ .Values.cloudsql.image.pullPolicy }} + {{- with .Values.cloudsql.extraEnv }} + env: {{- . | toYaml | nindent 4 }} + {{- end }} + {{- with .Values.cloudsql.resources }} + resources: {{- . | toYaml | nindent 4 }} + {{- end }} + restartPolicy: Always + {{- if .Values.securityContext.enabled }} + securityContext: + {{- include "helpers.securityContext" (list + .Values + "securityContext.containerSecurityContext" + "cloudsql.containerSecurityContext" + ) | nindent 4 }} + {{- end }} + command: ["/cloud_sql_proxy"] + args: + - "-verbose={{ .Values.cloudsql.verbose }}" + - "-instances={{ .Values.cloudsql.instance }}=tcp:{{ .Values.postgresql.primary.service.ports.postgresql }}" + {{- if .Values.cloudsql.enable_iam_login }} + - "-enable_iam_login" + {{- end }} + {{- if .Values.cloudsql.use_private_ip }} + - "-ip_address_types=PRIVATE" + {{- end }} + {{- with .Values.cloudsql.extraVolumeMounts }} + volumeMounts: {{ . | toYaml | nindent 4 }} + {{- end }} +{{- end -}} + {{- /* Returns the JSON representation of the value for a dot-notation path from a given context. diff --git a/helm/defectdojo/templates/celery-beat-deployment.yaml b/helm/defectdojo/templates/celery-beat-deployment.yaml index 8a80d0ffec7..232e5a54b01 100644 --- a/helm/defectdojo/templates/celery-beat-deployment.yaml +++ b/helm/defectdojo/templates/celery-beat-deployment.yaml @@ -87,31 +87,7 @@ spec: {{- end }} {{- end }} {{- if .Values.cloudsql.enabled }} - - name: cloudsql-proxy - image: {{ .Values.cloudsql.image.repository }}:{{ .Values.cloudsql.image.tag }} - imagePullPolicy: {{ .Values.cloudsql.image.pullPolicy }} - {{- with .Values.cloudsql.resources }} - resources: {{- . | toYaml | nindent 10 }} - {{- end }} - restartPolicy: Always - {{- if .Values.securityContext.enabled }} - securityContext: - {{- include "helpers.securityContext" (list - .Values - "securityContext.containerSecurityContext" - "cloudsql.containerSecurityContext" - ) | nindent 10 }} - {{- end }} - command: ["/cloud_sql_proxy"] - args: - - "-verbose={{ .Values.cloudsql.verbose }}" - - "-instances={{ .Values.cloudsql.instance }}=tcp:{{ .Values.postgresql.primary.service.ports.postgresql }}" - {{- if .Values.cloudsql.enable_iam_login }} - - "-enable_iam_login" - {{- end }} - {{- if .Values.cloudsql.use_private_ip }} - - "-ip_address_types=PRIVATE" - {{- end }} + {{- include "cloudsqlProxy" . | nindent 6 }} {{- end }} {{- if .Values.dbMigrationChecker.enabled }} {{$data := dict "fullName" $fullName }} diff --git a/helm/defectdojo/templates/celery-worker-deployment.yaml b/helm/defectdojo/templates/celery-worker-deployment.yaml index fe2e0f08c6f..10627ff3806 100644 --- a/helm/defectdojo/templates/celery-worker-deployment.yaml +++ b/helm/defectdojo/templates/celery-worker-deployment.yaml @@ -85,31 +85,7 @@ spec: {{- end }} {{- end }} {{- if .Values.cloudsql.enabled }} - - name: cloudsql-proxy - image: {{ .Values.cloudsql.image.repository }}:{{ .Values.cloudsql.image.tag }} - imagePullPolicy: {{ .Values.cloudsql.image.pullPolicy }} - {{- with .Values.cloudsql.resources }} - resources: {{- . | toYaml | nindent 10 }} - {{- end }} - restartPolicy: Always - {{- if .Values.securityContext.enabled }} - securityContext: - {{- include "helpers.securityContext" (list - .Values - "securityContext.containerSecurityContext" - "cloudsql.containerSecurityContext" - ) | nindent 10 }} - {{- end }} - command: ["/cloud_sql_proxy"] - args: - - "-verbose={{ .Values.cloudsql.verbose }}" - - "-instances={{ .Values.cloudsql.instance }}=tcp:{{ .Values.postgresql.primary.service.ports.postgresql }}" - {{- if .Values.cloudsql.enable_iam_login }} - - "-enable_iam_login" - {{- end }} - {{- if .Values.cloudsql.use_private_ip }} - - "-ip_address_types=PRIVATE" - {{- end }} + {{- include "cloudsqlProxy" . | nindent 6 }} {{- end }} {{- if .Values.dbMigrationChecker.enabled }} {{$data := dict "fullName" $fullName }} diff --git a/helm/defectdojo/templates/django-deployment.yaml b/helm/defectdojo/templates/django-deployment.yaml index 16738a91b41..9bbd8bd6ca0 100644 --- a/helm/defectdojo/templates/django-deployment.yaml +++ b/helm/defectdojo/templates/django-deployment.yaml @@ -104,37 +104,7 @@ spec: - {{- . | toYaml | nindent 8 }} {{- end }} {{- if .Values.cloudsql.enabled }} - - name: cloudsql-proxy - image: {{ .Values.cloudsql.image.repository }}:{{ .Values.cloudsql.image.tag }} - imagePullPolicy: {{ .Values.cloudsql.image.pullPolicy }} - {{- with .Values.cloudsql.extraEnv }} - env: {{- . | toYaml | nindent 8 }} - {{- end }} - {{- with .Values.cloudsql.resources }} - resources: {{- . | toYaml | nindent 10 }} - {{- end }} - restartPolicy: Always - {{- if .Values.securityContext.enabled }} - securityContext: - {{- include "helpers.securityContext" (list - .Values - "securityContext.containerSecurityContext" - "cloudsql.containerSecurityContext" - ) | nindent 10 }} - {{- end }} - command: ["/cloud_sql_proxy"] - args: - - "-verbose={{ .Values.cloudsql.verbose }}" - - "-instances={{ .Values.cloudsql.instance }}=tcp:{{ .Values.postgresql.primary.service.ports.postgresql }}" - {{- if .Values.cloudsql.enable_iam_login }} - - "-enable_iam_login" - {{- end }} - {{- if .Values.cloudsql.use_private_ip }} - - "-ip_address_types=PRIVATE" - {{- end }} - {{- with .Values.cloudsql.extraVolumeMounts }} - volumeMounts: {{ . | toYaml | nindent 10 }} - {{- end }} + {{- include "cloudsqlProxy" . | nindent 6 }} {{- end }} {{- if .Values.dbMigrationChecker.enabled }} {{- $data := dict "fullName" $fullName }} diff --git a/helm/defectdojo/templates/initializer-job.yaml b/helm/defectdojo/templates/initializer-job.yaml index 795427b34f1..84b5f671b8e 100644 --- a/helm/defectdojo/templates/initializer-job.yaml +++ b/helm/defectdojo/templates/initializer-job.yaml @@ -74,38 +74,7 @@ spec: {{- end }} initContainers: {{- if .Values.cloudsql.enabled }} - - name: cloudsql-proxy - image: {{ .Values.cloudsql.image.repository }}:{{ .Values.cloudsql.image.tag }} - imagePullPolicy: {{ .Values.cloudsql.image.pullPolicy }} - {{- with .Values.cloudsql.resources }} - resources: {{- . | toYaml | nindent 10 }} - {{- end }} - restartPolicy: Always - {{- if .Values.securityContext.enabled }} - securityContext: - {{- include "helpers.securityContext" (list - .Values - "securityContext.containerSecurityContext" - "cloudsql.containerSecurityContext" - ) | nindent 10 }} - {{- end }} - command: ["/cloud_sql_proxy"] - args: - - "-verbose={{ .Values.cloudsql.verbose }}" - - "-instances={{ .Values.cloudsql.instance }}=tcp:{{ .Values.postgresql.primary.service.ports.postgresql }}" - {{- if .Values.cloudsql.enable_iam_login }} - - "-enable_iam_login" - {{- end }} - {{- 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 }} + {{- include "cloudsqlProxy" . | nindent 6 }} {{- end }} - name: wait-for-db command: From 5f306e0d700b2b2c1b58de89200716e71fe0f5e6 Mon Sep 17 00:00:00 2001 From: kiblik <5609770+kiblik@users.noreply.github.com> Date: Sat, 11 Oct 2025 19:32:12 +0200 Subject: [PATCH 030/126] Test --- .github/workflows/test-helm-chart.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test-helm-chart.yml b/.github/workflows/test-helm-chart.yml index c50f84daafa..fc9538a2491 100644 --- a/.github/workflows/test-helm-chart.yml +++ b/.github/workflows/test-helm-chart.yml @@ -34,8 +34,8 @@ jobs: - name: Set up chart-testing uses: helm/chart-testing-action@0d28d3144d3a25ea2cc349d6e59901c4ff469b3b # v2.7.0 with: - yamale_version: 4.0.4 - yamllint_version: 1.35.1 + yamale_version: 6.0.0 + yamllint_version: 1.37.1 - name: Determine target branch id: ct-branch-target From 0372b07b1240b345f00253c01f3521df0d569b4c Mon Sep 17 00:00:00 2001 From: DefectDojo release bot Date: Tue, 14 Oct 2025 16:30:55 +0000 Subject: [PATCH 031/126] Update versions in application files --- components/package.json | 2 +- dojo/__init__.py | 2 +- helm/defectdojo/Chart.yaml | 27 ++++----------------------- helm/defectdojo/README.md | 2 +- 4 files changed, 7 insertions(+), 26 deletions(-) diff --git a/components/package.json b/components/package.json index f9b97fa55a4..e5898ca8f4d 100644 --- a/components/package.json +++ b/components/package.json @@ -1,6 +1,6 @@ { "name": "defectdojo", - "version": "2.51.1", + "version": "2.52.0-dev", "license" : "BSD-3-Clause", "private": true, "dependencies": { diff --git a/dojo/__init__.py b/dojo/__init__.py index 7f55bf358b3..0a21544849b 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.51.1" +__version__ = "2.52.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 510e34f9983..579aa3ec816 100644 --- a/helm/defectdojo/Chart.yaml +++ b/helm/defectdojo/Chart.yaml @@ -1,8 +1,8 @@ apiVersion: v2 -appVersion: "2.51.1" +appVersion: "2.52.0-dev" description: A Helm chart for Kubernetes to install DefectDojo name: defectdojo -version: 1.7.1 +version: 1.7.2-dev icon: https://defectdojo.com/hubfs/DefectDojo_favicon.png maintainers: - name: madchap @@ -18,24 +18,5 @@ dependencies: repository: "oci://us-docker.pkg.dev/os-public-container-registry/defectdojo" condition: redis.enabled annotations: - # For correct syntax, check https://artifacthub.io/docs/topics/annotations/helm/ - # This is example for "artifacthub.io/changes" - # artifacthub.io/changes: | - # - kind: added - # description: Cool feature - # - kind: fixed - # description: Minor bug - # - kind: changed - # description: Broken feature - # - kind: removed - # description: Old bug - # - kind: deprecated - # description: Not-needed feature - # - kind: security - # description: Critical bug - artifacthub.io/prerelease: "false" - artifacthub.io/changes: | - - kind: added - description: Add support for automountServiceAccountToken - - kind: changed - description: Bump DefectDojo to 2.51.1 + artifacthub.io/prerelease: "true" + artifacthub.io/changes: "" diff --git a/helm/defectdojo/README.md b/helm/defectdojo/README.md index 6fd4cdc2a2a..f37cfdaa973 100644 --- a/helm/defectdojo/README.md +++ b/helm/defectdojo/README.md @@ -495,7 +495,7 @@ kubectl delete pvc data-defectdojo-redis-0 data-defectdojo-postgresql-0 # General information about chart values -![Version: 1.7.1](https://img.shields.io/badge/Version-1.7.1-informational?style=flat-square) ![AppVersion: 2.51.1](https://img.shields.io/badge/AppVersion-2.51.1-informational?style=flat-square) +![Version: 1.7.2-dev](https://img.shields.io/badge/Version-1.7.2--dev-informational?style=flat-square) ![AppVersion: 2.52.0-dev](https://img.shields.io/badge/AppVersion-2.52.0--dev-informational?style=flat-square) A Helm chart for Kubernetes to install DefectDojo From c11d13fccbeee21dd245420483272544a7454b43 Mon Sep 17 00:00:00 2001 From: Ross Esposito Date: Tue, 14 Oct 2025 12:10:57 -0500 Subject: [PATCH 032/126] Seeing if these updated versions work with py 3.14 --- .github/workflows/test-helm-chart.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test-helm-chart.yml b/.github/workflows/test-helm-chart.yml index ef9edf85d18..3ea88f1c9e4 100644 --- a/.github/workflows/test-helm-chart.yml +++ b/.github/workflows/test-helm-chart.yml @@ -34,8 +34,8 @@ jobs: - name: Set up chart-testing uses: helm/chart-testing-action@0d28d3144d3a25ea2cc349d6e59901c4ff469b3b # v2.7.0 with: - yamale_version: 4.0.4 - yamllint_version: 1.35.1 + yamale_version: 6.0.0 + yamllint_version: 1.37.1 - name: Determine target branch id: ct-branch-target From 76f06f6964cb1716c6c51b7ddc4309415c923b9c Mon Sep 17 00:00:00 2001 From: Ross Esposito Date: Tue, 14 Oct 2025 12:41:50 -0500 Subject: [PATCH 033/126] Various doc/schema fixes --- helm/defectdojo/README.md | 4 ++++ helm/defectdojo/values.schema.json | 12 ++++++++++++ 2 files changed, 16 insertions(+) diff --git a/helm/defectdojo/README.md b/helm/defectdojo/README.md index dde981c5af1..3e34cfea3a2 100644 --- a/helm/defectdojo/README.md +++ b/helm/defectdojo/README.md @@ -529,6 +529,7 @@ A Helm chart for Kubernetes to install DefectDojo | celery.beat.affinity | object | `{}` | | | celery.beat.annotations | object | `{}` | | | celery.beat.automountServiceAccountToken | bool | `false` | | +| celery.beat.containerSecurityContext | object | `{}` | | | celery.beat.extraEnv | list | `[]` | | | celery.beat.extraInitContainers | list | `[]` | | | celery.beat.extraVolumeMounts | list | `[]` | | @@ -551,6 +552,7 @@ A Helm chart for Kubernetes to install DefectDojo | celery.worker.annotations | object | `{}` | | | celery.worker.appSettings.poolType | string | `"solo"` | | | celery.worker.automountServiceAccountToken | bool | `false` | | +| celery.worker.containerSecurityContext | object | `{}` | | | celery.worker.extraEnv | list | `[]` | | | celery.worker.extraInitContainers | list | `[]` | | | celery.worker.extraVolumeMounts | list | `[]` | | @@ -594,6 +596,7 @@ A Helm chart for Kubernetes to install DefectDojo | django.affinity | object | `{}` | | | django.annotations | object | `{}` | | | django.automountServiceAccountToken | bool | `false` | | +| django.extraEnv | list | `[]` | | | django.extraInitContainers | list | `[]` | | | django.extraVolumeMounts | list | `[]` | | | django.extraVolumes | list | `[]` | | @@ -674,6 +677,7 @@ A Helm chart for Kubernetes to install DefectDojo | initializer.affinity | object | `{}` | | | initializer.annotations | object | `{}` | | | initializer.automountServiceAccountToken | bool | `false` | | +| initializer.containerSecurityContext | object | `{}` | | | initializer.extraEnv | list | `[]` | | | initializer.extraVolumeMounts | list | `[]` | | | initializer.extraVolumes | list | `[]` | | diff --git a/helm/defectdojo/values.schema.json b/helm/defectdojo/values.schema.json index 7b182ee3b0a..477f5133d7a 100644 --- a/helm/defectdojo/values.schema.json +++ b/helm/defectdojo/values.schema.json @@ -52,6 +52,9 @@ "automountServiceAccountToken": { "type": "boolean" }, + "containerSecurityContext": { + "type": "object" + }, "extraEnv": { "type": "array" }, @@ -143,6 +146,9 @@ "automountServiceAccountToken": { "type": "boolean" }, + "containerSecurityContext": { + "type": "object" + }, "extraEnv": { "type": "array" }, @@ -324,6 +330,9 @@ "automountServiceAccountToken": { "type": "boolean" }, + "extraEnv": { + "type": "array" + }, "extraInitContainers": { "type": "array" }, @@ -688,6 +697,9 @@ "extraEnv": { "type": "array" }, + "containerSecurityContext": { + "type": "object" + }, "extraVolumeMounts": { "type": "array" }, From b3f48ed4b1b1b243dc0707cc63e0d4a5f5ae1bf0 Mon Sep 17 00:00:00 2001 From: Ross Esposito Date: Tue, 14 Oct 2025 12:48:08 -0500 Subject: [PATCH 034/126] More fixes --- helm/defectdojo/values.schema.json | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/helm/defectdojo/values.schema.json b/helm/defectdojo/values.schema.json index 477f5133d7a..d431c2f1849 100644 --- a/helm/defectdojo/values.schema.json +++ b/helm/defectdojo/values.schema.json @@ -52,9 +52,9 @@ "automountServiceAccountToken": { "type": "boolean" }, - "containerSecurityContext": { - "type": "object" - }, + "containerSecurityContext": { + "type": "object" + }, "extraEnv": { "type": "array" }, @@ -146,8 +146,8 @@ "automountServiceAccountToken": { "type": "boolean" }, - "containerSecurityContext": { - "type": "object" + "containerSecurityContext": { + "type": "object" }, "extraEnv": { "type": "array" @@ -694,12 +694,12 @@ "automountServiceAccountToken": { "type": "boolean" }, + "containerSecurityContext": { + "type": "object" + }, "extraEnv": { "type": "array" }, - "containerSecurityContext": { - "type": "object" - }, "extraVolumeMounts": { "type": "array" }, From e650c0f4b3297ae43bed49fb930bb31f0ef4a756 Mon Sep 17 00:00:00 2001 From: Ross Esposito Date: Tue, 14 Oct 2025 12:54:44 -0500 Subject: [PATCH 035/126] Debug statement and space fix --- .github/workflows/test-helm-chart.yml | 4 ++++ helm/defectdojo/values.schema.json | 10 +++++----- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/.github/workflows/test-helm-chart.yml b/.github/workflows/test-helm-chart.yml index 3ea88f1c9e4..c66dee355ff 100644 --- a/.github/workflows/test-helm-chart.yml +++ b/.github/workflows/test-helm-chart.yml @@ -78,9 +78,13 @@ jobs: # Get current branch annotation current_annotation=$(yq e '.annotations."artifacthub.io/changes"' "helm/defectdojo/Chart.yaml") + echo "Current annotation: " + echo $current_annotation # Get target branch version of Chart.yaml annotation target_annotation=$(git show "origin/${{ env.ct-branch }}:helm/defectdojo/Chart.yaml" | yq e '.annotations."artifacthub.io/changes"' -) + echo "Target annotation: " + echo $target_annotation if [[ "$current_annotation" == "$target_annotation" ]]; then echo "::error file=helm/defectdojo/Chart.yaml::The 'artifacthub.io/changes' annotation has not been updated compared to ${{ env.ct-branch }}. For more, check the hint in 'helm/defectdojo/Chart.yaml'" diff --git a/helm/defectdojo/values.schema.json b/helm/defectdojo/values.schema.json index d431c2f1849..e9c71966165 100644 --- a/helm/defectdojo/values.schema.json +++ b/helm/defectdojo/values.schema.json @@ -330,9 +330,9 @@ "automountServiceAccountToken": { "type": "boolean" }, - "extraEnv": { - "type": "array" - }, + "extraEnv": { + "type": "array" + }, "extraInitContainers": { "type": "array" }, @@ -695,8 +695,8 @@ "type": "boolean" }, "containerSecurityContext": { - "type": "object" - }, + "type": "object" + }, "extraEnv": { "type": "array" }, From 1d7ee8dcd571ecdcb71d9bc9d6682fe76a1d6803 Mon Sep 17 00:00:00 2001 From: kiblik <5609770+kiblik@users.noreply.github.com> Date: Wed, 15 Oct 2025 16:15:17 +0000 Subject: [PATCH 036/126] fix(helm): Test oldest supported k8s version (#13376) --- .github/workflows/k8s-tests.yml | 8 +++++--- helm/defectdojo/Chart.yaml | 2 ++ helm/defectdojo/README.md | 2 +- helm/defectdojo/README.md.gotmpl | 2 +- 4 files changed, 9 insertions(+), 5 deletions(-) diff --git a/.github/workflows/k8s-tests.yml b/.github/workflows/k8s-tests.yml index 154edb299f1..29a2f778d0a 100644 --- a/.github/workflows/k8s-tests.yml +++ b/.github/workflows/k8s-tests.yml @@ -14,9 +14,11 @@ jobs: matrix: include: # databases, broker and k8s are independent, so we don't need to test each combination - # lastest k8s version (https://kubernetes.io/releases/) and oldest supported version from aws - # are tested (https://docs.aws.amazon.com/eks/latest/userguide/kubernetes-versions.html#available-versions) - - k8s: 'v1.34.0' + # lastest k8s version (https://kubernetes.io/releases/) and the oldest officially supported version + # are tested (https://kubernetes.io/releases/) + - k8s: 'v1.34.1' + os: debian + - k8s: 'v1.31.13' os: debian steps: - name: Checkout diff --git a/helm/defectdojo/Chart.yaml b/helm/defectdojo/Chart.yaml index 33d94d4cce2..078a6751a5b 100644 --- a/helm/defectdojo/Chart.yaml +++ b/helm/defectdojo/Chart.yaml @@ -22,3 +22,5 @@ annotations: artifacthub.io/changes: | - kind: changed description: DRY cloudsql-proxy + - kind: added + description: Testing on the oldest officially supported k8s diff --git a/helm/defectdojo/README.md b/helm/defectdojo/README.md index 3e34cfea3a2..05ef4a1afd7 100644 --- a/helm/defectdojo/README.md +++ b/helm/defectdojo/README.md @@ -11,7 +11,7 @@ this [guide](https://helm.sh/docs/using_helm/#installing-helm). ## Supported Kubernetes Versions -The tests cover the deployment on the lastest [kubernetes version](https://kubernetes.io/releases/) and the oldest supported [version from AWS](https://docs.aws.amazon.com/eks/latest/userguide/kubernetes-versions.html#available-versions). The assumption is that version in between do not have significant differences. Current tested versions can looks up in the [github k8s workflow](https://github.com/DefectDojo/django-DefectDojo/blob/master/.github/workflows/k8s-tests.yml). +The tests cover the deployment on the lastest [kubernetes version](https://kubernetes.io/releases/) and [the oldest officially supported version](https://kubernetes.io/releases/). The assumption is that version in between do not have significant differences. Current tested versions can looks up in the [github k8s workflow](https://github.com/DefectDojo/django-DefectDojo/blob/master/.github/workflows/k8s-tests.yml). ## Helm chart diff --git a/helm/defectdojo/README.md.gotmpl b/helm/defectdojo/README.md.gotmpl index 9583a95d167..e4ab067a647 100644 --- a/helm/defectdojo/README.md.gotmpl +++ b/helm/defectdojo/README.md.gotmpl @@ -11,7 +11,7 @@ this [guide](https://helm.sh/docs/using_helm/#installing-helm). ## Supported Kubernetes Versions -The tests cover the deployment on the lastest [kubernetes version](https://kubernetes.io/releases/) and the oldest supported [version from AWS](https://docs.aws.amazon.com/eks/latest/userguide/kubernetes-versions.html#available-versions). The assumption is that version in between do not have significant differences. Current tested versions can looks up in the [github k8s workflow](https://github.com/DefectDojo/django-DefectDojo/blob/master/.github/workflows/k8s-tests.yml). +The tests cover the deployment on the lastest [kubernetes version](https://kubernetes.io/releases/) and [the oldest officially supported version](https://kubernetes.io/releases/). The assumption is that version in between do not have significant differences. Current tested versions can looks up in the [github k8s workflow](https://github.com/DefectDojo/django-DefectDojo/blob/master/.github/workflows/k8s-tests.yml). ## Helm chart From 97f10692a5ffb3b39ebbcb4b5bde71dd8660b143 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 15 Oct 2025 11:24:35 -0600 Subject: [PATCH 037/126] chore(deps): update redis:7.2.11-alpine docker digest from 7.2.11 to v (docker-compose.yml) (#13399) 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 e13eba26e99..c6838267169 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -129,7 +129,7 @@ services: - defectdojo_postgres:/var/lib/postgresql/data redis: # Pinning to this version due to licensing constraints - image: redis:7.2.11-alpine@sha256:cd3e4dbac9604660d08efac21b27daa2ae91dde1e19203b49ec8567050ba093f + image: redis:7.2.11-alpine@sha256:1a34bdba051ecd8a58ec8a3cc460acef697a1605e918149cc53d920673c1a0a7 volumes: - defectdojo_redis:/data volumes: From 0d1ed65abc9744826a626388ca010714553223a5 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 15 Oct 2025 11:25:03 -0600 Subject: [PATCH 038/126] chore(deps): update softprops/action-gh-release action from v2.4.0 to v2.4.1 (.github/workflows/release-x-manual-helm-chart.yml) (#13400) 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 59b64431b4a..1b34d931b7b 100644 --- a/.github/workflows/release-x-manual-helm-chart.yml +++ b/.github/workflows/release-x-manual-helm-chart.yml @@ -87,7 +87,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@aec2ec56f94eb8180ceec724245f64ef008b89f5 # v2.4.0 + uses: softprops/action-gh-release@6da8fa9354ddfdc4aeace5fc48d7f679b5214090 # v2.4.1 with: name: '${{ inputs.release_number }} 🌈' tag_name: ${{ inputs.release_number }} From cc0d5198b5b93308df76d89ac15b882624f0b270 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 15 Oct 2025 11:26:15 -0600 Subject: [PATCH 039/126] chore(deps): update mikefarah/yq action from v4.47.2 to v4.48.1 (.github/workflows/release-x-manual-helm-chart.yml) (#13402) 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 1b34d931b7b..8879885f86c 100644 --- a/.github/workflows/release-x-manual-helm-chart.yml +++ b/.github/workflows/release-x-manual-helm-chart.yml @@ -70,7 +70,7 @@ jobs: helm dependency update ./helm/defectdojo - name: Add yq - uses: mikefarah/yq@6251e95af8df3505def48c71f3119836701495d6 # v4.47.2 + uses: mikefarah/yq@0ecdce24e83f0fa127940334be98c86b07b0c488 # v4.48.1 - name: Pin version docker version id: pin_image From 2ccaebc110607b6a672413b5ab49bfe63d2d56bf Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 15 Oct 2025 11:26:53 -0600 Subject: [PATCH 040/126] chore(deps): update stefanzweifel/git-auto-commit-action action from v6.0.1 to v7 (.github/workflows/release-3-master-into-dev.yml) (#13404) 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 4e4b710400f..7d3f9bb64a0 100644 --- a/.github/workflows/release-1-create-pr.yml +++ b/.github/workflows/release-1-create-pr.yml @@ -98,7 +98,7 @@ jobs: chart-search-root: "helm/defectdojo" - name: Push version changes - uses: stefanzweifel/git-auto-commit-action@778341af668090896ca464160c2def5d1d1a3eb0 # v6.0.1 + uses: stefanzweifel/git-auto-commit-action@28e16e81777b558cc906c8750092100bbb34c5e3 # v7.0.0 with: commit_user_name: "${{ env.GIT_USERNAME }}" commit_user_email: "${{ env.GIT_EMAIL }}" diff --git a/.github/workflows/release-3-master-into-dev.yml b/.github/workflows/release-3-master-into-dev.yml index d13ce0a9323..15674b5af40 100644 --- a/.github/workflows/release-3-master-into-dev.yml +++ b/.github/workflows/release-3-master-into-dev.yml @@ -86,7 +86,7 @@ jobs: chart-search-root: "helm/defectdojo" - name: Push version changes - uses: stefanzweifel/git-auto-commit-action@778341af668090896ca464160c2def5d1d1a3eb0 # v6.0.1 + uses: stefanzweifel/git-auto-commit-action@28e16e81777b558cc906c8750092100bbb34c5e3 # v7.0.0 with: commit_user_name: "${{ env.GIT_USERNAME }}" commit_user_email: "${{ env.GIT_EMAIL }}" @@ -162,7 +162,7 @@ jobs: chart-search-root: "helm/defectdojo" - name: Push version changes - uses: stefanzweifel/git-auto-commit-action@778341af668090896ca464160c2def5d1d1a3eb0 # v6.0.1 + uses: stefanzweifel/git-auto-commit-action@28e16e81777b558cc906c8750092100bbb34c5e3 # v7.0.0 with: commit_user_name: "${{ env.GIT_USERNAME }}" commit_user_email: "${{ env.GIT_EMAIL }}" From 5d766e2d24396e0434ea68504cacbe38d409d457 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 15 Oct 2025 11:28:57 -0600 Subject: [PATCH 041/126] Bump uwsgi from 2.0.30 to 2.0.31 (#13410) Bumps [uwsgi](https://uwsgi-docs.readthedocs.io/en/latest/) from 2.0.30 to 2.0.31. --- updated-dependencies: - dependency-name: uwsgi dependency-version: 2.0.31 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 09f04207136..1477ea446e6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -35,7 +35,7 @@ redis==6.4.0 requests==2.32.5 sqlalchemy==2.0.43 # Required by Celery broker transport urllib3==2.5.0 -uWSGI==2.0.30 +uWSGI==2.0.31 vobject==0.9.9 whitenoise==5.2.0 titlecase==2.4.1 From 741fad024af7e2eea937281ff38503c03d113a50 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 15 Oct 2025 11:29:30 -0600 Subject: [PATCH 042/126] Bump sqlalchemy from 2.0.43 to 2.0.44 (#13411) Bumps [sqlalchemy](https://github.com/sqlalchemy/sqlalchemy) from 2.0.43 to 2.0.44. - [Release notes](https://github.com/sqlalchemy/sqlalchemy/releases) - [Changelog](https://github.com/sqlalchemy/sqlalchemy/blob/main/CHANGES.rst) - [Commits](https://github.com/sqlalchemy/sqlalchemy/commits) --- updated-dependencies: - dependency-name: sqlalchemy dependency-version: 2.0.44 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 1477ea446e6..c3a5136c2fe 100644 --- a/requirements.txt +++ b/requirements.txt @@ -33,7 +33,7 @@ cryptography==46.0.2 python-dateutil==2.9.0.post0 redis==6.4.0 requests==2.32.5 -sqlalchemy==2.0.43 # Required by Celery broker transport +sqlalchemy==2.0.44 # Required by Celery broker transport urllib3==2.5.0 uWSGI==2.0.31 vobject==0.9.9 From 1cb0fe37d823e027438f66a0a2e8d07848f84aa5 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 15 Oct 2025 11:30:00 -0600 Subject: [PATCH 043/126] chore(deps): update losisin/helm-values-schema-json-action action from v2.3.0 to v2.3.1 (.github/workflows/test-helm-chart.yml) (#13412) 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 c66dee355ff..7ebe2e2fe2f 100644 --- a/.github/workflows/test-helm-chart.yml +++ b/.github/workflows/test-helm-chart.yml @@ -129,7 +129,7 @@ jobs: # If this step fails, install https://github.com/losisin/helm-values-schema-json and run locally `helm schema --use-helm-docs` in `helm/defectdojo` before committing your changes. # The helm schema will be generated for you. - name: Generate values schema json - uses: losisin/helm-values-schema-json-action@d5847286fa04322702c4f8d45031974798c83ac7 # v2.3.0 + uses: losisin/helm-values-schema-json-action@660c441a4a507436a294fc55227e1df54aca5407 # v2.3.1 with: fail-on-diff: true working-directory: "helm/defectdojo" From 67dd77cc39e7e69e0187ef8924c79a17e0338a7a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 15 Oct 2025 11:30:28 -0600 Subject: [PATCH 044/126] Bump nginx from 1.29.1-alpine3.22 to 1.29.2-alpine3.22 (#13413) Bumps nginx from 1.29.1-alpine3.22 to 1.29.2-alpine3.22. --- updated-dependencies: - dependency-name: nginx dependency-version: 1.29.2-alpine3.22 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> --- Dockerfile.nginx-alpine | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile.nginx-alpine b/Dockerfile.nginx-alpine index 5270e01f747..7c608d08444 100644 --- a/Dockerfile.nginx-alpine +++ b/Dockerfile.nginx-alpine @@ -63,7 +63,7 @@ COPY dojo/ ./dojo/ # always collect static for debug toolbar as we can't make it dependant on env variables or build arguments without breaking docker layer caching RUN env DD_SECRET_KEY='.' DD_DJANGO_DEBUG_TOOLBAR_ENABLED=True python3 manage.py collectstatic --noinput --verbosity=2 && true -FROM nginx:1.29.1-alpine3.22@sha256:42a516af16b852e33b7682d5ef8acbd5d13fe08fecadc7ed98605ba5e3b26ab8 +FROM nginx:1.29.2-alpine3.22@sha256:61e01287e546aac28a3f56839c136b31f590273f3b41187a36f46f6a03bbfe22 ARG uid=1001 ARG appuser=defectdojo COPY --from=collectstatic /app/static/ /usr/share/nginx/html/static/ From 07ce2aa0efdbd33d5c222649faa5da87a7ada52a Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 15 Oct 2025 11:32:10 -0600 Subject: [PATCH 045/126] chore(deps): update actions/setup-node action from v5.0.0 to v6 (.github/workflows/validate_docs_build.yml) (#13417) 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 5a749e0946f..5652a2e32a0 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@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0 + uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0 with: node-version: '22.20.0' diff --git a/.github/workflows/validate_docs_build.yml b/.github/workflows/validate_docs_build.yml index 223fa2a2a0c..e38cefe0161 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@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0 + uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0 with: node-version: '22.20.0' From baeb1deb368cd174e2de639d467a61c7211d02f6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 15 Oct 2025 11:53:46 -0600 Subject: [PATCH 046/126] Bump boto3 from 1.40.49 to 1.40.52 (#13426) Bumps [boto3](https://github.com/boto/boto3) from 1.40.49 to 1.40.52. - [Release notes](https://github.com/boto/boto3/releases) - [Commits](https://github.com/boto/boto3/compare/1.40.49...1.40.52) --- updated-dependencies: - dependency-name: boto3 dependency-version: 1.40.52 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 c3a5136c2fe..5266a24f66b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -60,7 +60,7 @@ django-ratelimit==4.1.0 argon2-cffi==25.1.0 blackduck==1.1.3 pycurl==7.45.7 # Required for Celery Broker AWS (SQS) support -boto3==1.40.49 # Required for Celery Broker AWS (SQS) support +boto3==1.40.52 # Required for Celery Broker AWS (SQS) support netaddr==1.3.0 vulners==3.1.1 fontawesomefree==6.6.0 From 90214e2a4a16e09f36d32333ceab24fcfe330932 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 15 Oct 2025 15:46:27 -0600 Subject: [PATCH 047/126] Bump django-imagekit from 5.0.0 to 6.0.0 (#13414) Bumps [django-imagekit](https://github.com/matthewwithanm/django-imagekit) from 5.0.0 to 6.0.0. - [Release notes](https://github.com/matthewwithanm/django-imagekit/releases) - [Commits](https://github.com/matthewwithanm/django-imagekit/compare/5.0...6.0) --- updated-dependencies: - dependency-name: django-imagekit dependency-version: 6.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 5266a24f66b..09d6166ce32 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,7 +10,7 @@ django-pghistory==3.8.3 django-dbbackup==5.0.0 django-environ==0.12.0 django-filter==25.1 -django-imagekit==5.0.0 +django-imagekit==6.0.0 django-multiselectfield==1.0.1 django-polymorphic==4.1.0 django-crispy-forms==2.4 From 843188ee15863f63ae23aaa4ed303e80325a443e Mon Sep 17 00:00:00 2001 From: manuelsommer <47991713+manuel-sommer@users.noreply.github.com> Date: Thu, 16 Oct 2025 18:32:35 +0200 Subject: [PATCH 048/126] :hammer: Merge the MobSF scanner (#12501) * :hammer: Merge the MobSF scanner * add migration * udpate * Update 0229_merge_mobsf.py * udpate * Update settings.dist.py * update * update * update docs * Update 2.48.md * update upgrade notes * Update 2.48.md * Update 2.48.md * fix * update * update * update * update docs --- .../parsers/file/mobsf.md | 4 +- .../parsers/file/mobsfscan.md | 17 - docs/content/en/open_source/upgrading/2.52.md | 4 + dojo/settings/settings.dist.py | 2 +- dojo/tools/mobsf/api_report_json.py | 388 +++++++++++++++++ dojo/tools/mobsf/parser.py | 393 +----------------- .../{mobsfscan/parser.py => mobsf/report.py} | 17 +- dojo/tools/mobsfscan/__init__.py | 0 .../{mobsfscan => mobsf}/many_findings.json | 0 .../many_findings_cwe_lower.json | 0 .../{mobsfscan => mobsf}/no_findings.json | 0 unittests/tools/test_mobsf_parser.py | 159 +++++++ unittests/tools/test_mobsfscan_parser.py | 165 -------- 13 files changed, 566 insertions(+), 583 deletions(-) delete mode 100644 docs/content/en/connecting_your_tools/parsers/file/mobsfscan.md create mode 100644 dojo/tools/mobsf/api_report_json.py rename dojo/tools/{mobsfscan/parser.py => mobsf/report.py} (84%) delete mode 100644 dojo/tools/mobsfscan/__init__.py rename unittests/scans/{mobsfscan => mobsf}/many_findings.json (100%) rename unittests/scans/{mobsfscan => mobsf}/many_findings_cwe_lower.json (100%) rename unittests/scans/{mobsfscan => mobsf}/no_findings.json (100%) delete mode 100644 unittests/tools/test_mobsfscan_parser.py diff --git a/docs/content/en/connecting_your_tools/parsers/file/mobsf.md b/docs/content/en/connecting_your_tools/parsers/file/mobsf.md index 7bbbf564a0c..caac14fbf14 100644 --- a/docs/content/en/connecting_your_tools/parsers/file/mobsf.md +++ b/docs/content/en/connecting_your_tools/parsers/file/mobsf.md @@ -2,7 +2,9 @@ title: "MobSF Scanner" toc_hide: true --- -Export a JSON file using the API, api/v1/report\_json. +"Mobsfscan Scan" has been merged into the "MobSF Scan" parser. The "Mobsfscan Scan" scan_type has been retained to keep deduplication working for existing Tests, but users are encouraged to move to the "MobSF Scan" scan_type. + +Export a JSON file using the API, api/v1/report\_json and import it to Defectdojo or import a JSON report from ### Sample Scan Data Sample MobSF Scanner scans can be found [here](https://github.com/DefectDojo/django-DefectDojo/tree/master/unittests/scans/mobsf). diff --git a/docs/content/en/connecting_your_tools/parsers/file/mobsfscan.md b/docs/content/en/connecting_your_tools/parsers/file/mobsfscan.md deleted file mode 100644 index 2c39d114287..00000000000 --- a/docs/content/en/connecting_your_tools/parsers/file/mobsfscan.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -title: "Mobsfscan" -toc_hide: true ---- -Import JSON report from - -### Sample Scan Data -Sample Mobsfscan scans can be found [here](https://github.com/DefectDojo/django-DefectDojo/tree/master/unittests/scans/mobsfscan). - -### Default Deduplication Hashcode Fields -By default, DefectDojo identifies duplicate Findings using these [hashcode fields](https://docs.defectdojo.com/en/working_with_findings/finding_deduplication/about_deduplication/): - -- title -- severity -- cwe -- file path -- description diff --git a/docs/content/en/open_source/upgrading/2.52.md b/docs/content/en/open_source/upgrading/2.52.md index b15986e5228..1619ee0fa81 100644 --- a/docs/content/en/open_source/upgrading/2.52.md +++ b/docs/content/en/open_source/upgrading/2.52.md @@ -11,6 +11,10 @@ This release introduces more important changes to the Helm chart configuration: ### Breaking changes +#### Merge of MobSF parsers + +Mobsfscan Scan" has been merged into the "MobSF Scan" parser. The "Mobsfscan Scan" scan_type has been retained to keep deduplication working for existing Tests, but users are encouraged to move to the "MobSF Scan" scan_type. + #### Security context This Helm chart extends security context capabilities to all deployed pods and containers. diff --git a/dojo/settings/settings.dist.py b/dojo/settings/settings.dist.py index baf28ec2b38..3f6e59df3eb 100644 --- a/dojo/settings/settings.dist.py +++ b/dojo/settings/settings.dist.py @@ -1357,7 +1357,7 @@ def saml2_attrib_map_format(din): "HCLAppScan XML": ["title", "description"], "HCL AppScan on Cloud SAST XML": ["title", "file_path", "line", "severity"], "KICS Scan": ["file_path", "line", "severity", "description", "title"], - "MobSF Scan": ["title", "description", "severity"], + "MobSF Scan": ["title", "description", "severity", "file_path"], "MobSF Scorecard Scan": ["title", "description", "severity"], "OSV Scan": ["title", "description", "severity"], "Snyk Code Scan": ["vuln_id_from_tool", "file_path"], diff --git a/dojo/tools/mobsf/api_report_json.py b/dojo/tools/mobsf/api_report_json.py new file mode 100644 index 00000000000..6f5bd1c6c75 --- /dev/null +++ b/dojo/tools/mobsf/api_report_json.py @@ -0,0 +1,388 @@ +from datetime import datetime + +from html2text import html2text + +from dojo.models import Finding + + +class MobSFapireport: + def get_findings(self, data, test): + dupes = {} + find_date = datetime.now() + + test_description = "" + if "name" in data: + test_description = "**Info:**\n" + if "packagename" in data: + test_description = "{} **Package Name:** {}\n".format(test_description, data["packagename"]) + + if "mainactivity" in data: + test_description = "{} **Main Activity:** {}\n".format(test_description, data["mainactivity"]) + + if "pltfm" in data: + test_description = "{} **Platform:** {}\n".format(test_description, data["pltfm"]) + + if "sdk" in data: + test_description = "{} **SDK:** {}\n".format(test_description, data["sdk"]) + + if "min" in data: + test_description = "{} **Min SDK:** {}\n".format(test_description, data["min"]) + + if "targetsdk" in data: + test_description = "{} **Target SDK:** {}\n".format(test_description, data["targetsdk"]) + + if "minsdk" in data: + test_description = "{} **Min SDK:** {}\n".format(test_description, data["minsdk"]) + + if "maxsdk" in data: + test_description = "{} **Max SDK:** {}\n".format(test_description, data["maxsdk"]) + + test_description = f"{test_description}\n**File Information:**\n" + + if "name" in data: + test_description = "{} **Name:** {}\n".format(test_description, data["name"]) + + if "md5" in data: + test_description = "{} **MD5:** {}\n".format(test_description, data["md5"]) + + if "sha1" in data: + test_description = "{} **SHA-1:** {}\n".format(test_description, data["sha1"]) + + if "sha256" in data: + test_description = "{} **SHA-256:** {}\n".format(test_description, data["sha256"]) + + if "size" in data: + test_description = "{} **Size:** {}\n".format(test_description, data["size"]) + + if "urls" in data: + curl = "" + for url in data["urls"]: + for durl in url["urls"]: + curl = f"{durl}\n" + + if curl: + test_description = f"{test_description}\n**URL's:**\n {curl}\n" + + if "bin_anal" in data: + test_description = "{} \n**Binary Analysis:** {}\n".format(test_description, data["bin_anal"]) + + test.description = html2text(test_description) + + mobsf_findings = [] + # Mobile Permissions + if "permissions" in data: + # for permission, details in data["permissions"].items(): + if isinstance(data["permissions"], list): + for details in data["permissions"]: + mobsf_item = { + "category": "Mobile Permissions", + "title": details.get("name", ""), + "severity": self.getSeverityForPermission(details.get("status")), + "description": "**Permission Type:** " + details.get("name", "") + " (" + details.get("status", "") + ")\n\n**Description:** " + details.get("description", "") + "\n\n**Reason:** " + details.get("reason", ""), + "file_path": None, + } + mobsf_findings.append(mobsf_item) + else: + for permission, details in list(data["permissions"].items()): + mobsf_item = { + "category": "Mobile Permissions", + "title": permission, + "severity": self.getSeverityForPermission(details.get("status", "")), + "description": "**Permission Type:** " + permission + "\n\n**Description:** " + details.get("description", ""), + "file_path": None, + } + mobsf_findings.append(mobsf_item) + + # Insecure Connections + if "insecure_connections" in data: + for details in data["insecure_connections"]: + insecure_urls = "" + for url in details.split(","): + insecure_urls = insecure_urls + url + "\n" + + mobsf_item = { + "category": None, + "title": "Insecure Connections", + "severity": "Low", + "description": insecure_urls, + "file_path": None, + } + mobsf_findings.append(mobsf_item) + + # Certificate Analysis + if "certificate_analysis" in data: + if data["certificate_analysis"] != {}: + certificate_info = data["certificate_analysis"]["certificate_info"] + for details in data["certificate_analysis"]["certificate_findings"]: + if len(details) == 3: + mobsf_item = { + "category": "Certificate Analysis", + "title": details[2], + "severity": details[0].title(), + "description": details[1] + "\n\n**Certificate Info:** " + certificate_info, + "file_path": None, + } + mobsf_findings.append(mobsf_item) + elif len(details) == 2: + mobsf_item = { + "category": "Certificate Analysis", + "title": details[1], + "severity": details[0].title(), + "description": details[1] + "\n\n**Certificate Info:** " + certificate_info, + "file_path": None, + } + mobsf_findings.append(mobsf_item) + + # Manifest Analysis + if "manifest_analysis" in data: + if data["manifest_analysis"] != {} and isinstance(data["manifest_analysis"], dict): + if data["manifest_analysis"]["manifest_findings"]: + for details in data["manifest_analysis"]["manifest_findings"]: + mobsf_item = { + "category": "Manifest Analysis", + "title": details["title"], + "severity": details["severity"].title(), + "description": details["description"] + "\n\n " + details["name"], + "file_path": None, + } + mobsf_findings.append(mobsf_item) + else: + for details in data["manifest_analysis"]: + mobsf_item = { + "category": "Manifest Analysis", + "title": details["title"], + "severity": details["stat"].title(), + "description": details["desc"] + "\n\n " + details["name"], + "file_path": None, + } + mobsf_findings.append(mobsf_item) + + # Code Analysis + if "code_analysis" in data: + if data["code_analysis"] != {}: + if data["code_analysis"].get("findings"): + for details in data["code_analysis"]["findings"]: + metadata = data["code_analysis"]["findings"][details] + mobsf_item = { + "category": "Code Analysis", + "title": details, + "severity": metadata["metadata"]["severity"].title(), + "description": metadata["metadata"]["description"], + "file_path": None, + } + mobsf_findings.append(mobsf_item) + else: + for details in data["code_analysis"]: + metadata = data["code_analysis"][details] + if metadata.get("metadata"): + mobsf_item = { + "category": "Code Analysis", + "title": details, + "severity": metadata["metadata"]["severity"].title(), + "description": metadata["metadata"]["description"], + "file_path": None, + } + mobsf_findings.append(mobsf_item) + + # Binary Analysis + if "binary_analysis" in data: + if isinstance(data["binary_analysis"], list): + for details in data["binary_analysis"]: + for binary_analysis_type in details: + if binary_analysis_type != "name": + mobsf_item = { + "category": "Binary Analysis", + "title": details[binary_analysis_type]["description"].split(".")[0], + "severity": details[binary_analysis_type]["severity"].title(), + "description": details[binary_analysis_type]["description"], + "file_path": details["name"], + } + mobsf_findings.append(mobsf_item) + elif data["binary_analysis"].get("findings"): + for details in data["binary_analysis"]["findings"].values(): + # "findings":{ + # "Binary makes use of insecure API(s)":{ + # "detailed_desc":"The binary may contain the following insecure API(s) _memcpy\n, _strlen\n", + # "severity":"high", + # "cvss":6, + # "cwe":"CWE-676: Use of Potentially Dangerous Function", + # "owasp-mobile":"M7: Client Code Quality", + # "masvs":"MSTG-CODE-8" + # }, + mobsf_item = { + "category": "Binary Analysis", + "title": details["detailed_desc"], + "severity": details["severity"].title(), + "description": details["detailed_desc"], + "file_path": None, + } + mobsf_findings.append(mobsf_item) + else: + for details in data["binary_analysis"].values(): + # "Binary makes use of insecure API(s)":{ + # "detailed_desc":"The binary may contain the following insecure API(s) _vsprintf.", + # "severity":"high", + # "cvss":6, + # "cwe":"CWE-676 - Use of Potentially Dangerous Function", + # "owasp-mobile":"M7: Client Code Quality", + # "masvs":"MSTG-CODE-8" + # } + mobsf_item = { + "category": "Binary Analysis", + "title": details["detailed_desc"], + "severity": details["severity"].title(), + "description": details["detailed_desc"], + "file_path": None, + } + mobsf_findings.append(mobsf_item) + + # specific node for Android reports + if "android_api" in data: + # "android_insecure_random": { + # "files": { + # "u/c/a/b/a/c.java": "9", + # "kotlinx/coroutines/repackaged/net/bytebuddy/utility/RandomString.java": "3", + # ... + # "hu/mycompany/vbnmqweq/gateway/msg/Response.java": "13" + # }, + # "metadata": { + # "id": "android_insecure_random", + # "description": "The App uses an insecure Random Number Generator.", + # "type": "Regex", + # "pattern": "java\\.util\\.Random;", + # "severity": "high", + # "input_case": "exact", + # "cvss": 7.5, + # "cwe": "CWE-330 Use of Insufficiently Random Values", + # "owasp-mobile": "M5: Insufficient Cryptography", + # "masvs": "MSTG-CRYPTO-6" + # } + # }, + for api, details in list(data["android_api"].items()): + mobsf_item = { + "category": "Android API", + "title": details["metadata"]["description"], + "severity": details["metadata"]["severity"].title(), + "description": "**API:** " + api + "\n\n**Description:** " + details["metadata"]["description"], + "file_path": None, + } + mobsf_findings.append(mobsf_item) + + # Manifest + if "manifest" in data: + for details in data["manifest"]: + mobsf_item = { + "category": "Manifest", + "title": details["title"], + "severity": details["stat"], + "description": details["desc"], + "file_path": None, + } + mobsf_findings.append(mobsf_item) + + # MobSF Findings + if "findings" in data: + for title, finding in list(data["findings"].items()): + description = title + file_path = None + + if "path" in finding: + description += "\n\n**Files:**\n" + for path in finding["path"]: + if file_path is None: + file_path = path + description = description + " * " + path + "\n" + + mobsf_item = { + "category": "Findings", + "title": title, + "severity": finding["level"], + "description": description, + "file_path": file_path, + } + + mobsf_findings.append(mobsf_item) + if isinstance(data, list): + for finding in data: + mobsf_item = { + "category": finding["category"], + "title": finding["name"], + "severity": finding["severity"], + "description": finding["description"] + "\n" + "**apk_exploit_dict:** " + str(finding["apk_exploit_dict"]) + "\n" + "**line_number:** " + str(finding["line_number"]), + "file_path": finding["file_object"], + } + mobsf_findings.append(mobsf_item) + for mobsf_finding in mobsf_findings: + title = mobsf_finding["title"] + sev = self.getCriticalityRating(mobsf_finding["severity"]) + description = "" + file_path = None + if mobsf_finding["category"]: + description += "**Category:** " + mobsf_finding["category"] + "\n\n" + description += html2text(mobsf_finding["description"]) + finding = Finding( + title=title, + cwe=919, # Weaknesses in Mobile Applications + test=test, + description=description, + severity=sev, + references=None, + date=find_date, + static_finding=True, + dynamic_finding=False, + nb_occurences=1, + ) + if mobsf_finding["file_path"]: + finding.file_path = mobsf_finding["file_path"] + dupe_key = sev + title + description + mobsf_finding["file_path"] + else: + dupe_key = sev + title + description + if mobsf_finding["category"]: + dupe_key += mobsf_finding["category"] + if dupe_key in dupes: + find = dupes[dupe_key] + if description is not None: + find.description += description + find.nb_occurences += 1 + else: + dupes[dupe_key] = finding + return list(dupes.values()) + + def getSeverityForPermission(self, status): + """ + Convert status for permission detection to severity + + In MobSF there is only 4 know values for permission, + we map them as this: + dangerous => High (Critical?) + normal => Info + signature => Info (it's positive so... Info) + signatureOrSystem => Info (it's positive so... Info) + """ + if status == "dangerous": + return "High" + return "Info" + + # Criticality rating + def getCriticalityRating(self, rating): + criticality = "Info" + if rating.lower() == "good": + criticality = "Info" + elif rating.lower() == "warning": + criticality = "Low" + elif rating.lower() == "vulnerability": + criticality = "Medium" + else: + criticality = rating.lower().capitalize() + return criticality + + def suite_data(self, suites): + suite_info = "" + suite_info += suites["name"] + "\n" + suite_info += "Cipher Strength: " + str(suites["cipherStrength"]) + "\n" + if "ecdhBits" in suites: + suite_info += "ecdhBits: " + str(suites["ecdhBits"]) + "\n" + if "ecdhStrength" in suites: + suite_info += "ecdhStrength: " + str(suites["ecdhStrength"]) + suite_info += "\n\n" + return suite_info diff --git a/dojo/tools/mobsf/parser.py b/dojo/tools/mobsf/parser.py index c61065ea892..ff5a7122655 100644 --- a/dojo/tools/mobsf/parser.py +++ b/dojo/tools/mobsf/parser.py @@ -1,22 +1,20 @@ import json -from datetime import datetime -from html2text import html2text - -from dojo.models import Finding +from dojo.tools.mobsf.api_report_json import MobSFapireport +from dojo.tools.mobsf.report import MobSFjsonreport class MobSFParser: def get_scan_types(self): - return ["MobSF Scan"] + return ["MobSF Scan", "Mobsfscan Scan"] def get_label_for_scan_types(self, scan_type): return "MobSF Scan" def get_description_for_scan_types(self, scan_type): - return "Export a JSON file using the API, api/v1/report_json." + return "Import JSON report from mobsfscan report file or from api/v1/report_json" def get_findings(self, filename, test): tree = filename.read() @@ -24,381 +22,8 @@ def get_findings(self, filename, test): data = json.loads(str(tree, "utf-8")) except: data = json.loads(tree) - find_date = datetime.now() - dupes = {} - test_description = "" - if "name" in data: - test_description = "**Info:**\n" - if "packagename" in data: - test_description = "{} **Package Name:** {}\n".format(test_description, data["packagename"]) - - if "mainactivity" in data: - test_description = "{} **Main Activity:** {}\n".format(test_description, data["mainactivity"]) - - if "pltfm" in data: - test_description = "{} **Platform:** {}\n".format(test_description, data["pltfm"]) - - if "sdk" in data: - test_description = "{} **SDK:** {}\n".format(test_description, data["sdk"]) - - if "min" in data: - test_description = "{} **Min SDK:** {}\n".format(test_description, data["min"]) - - if "targetsdk" in data: - test_description = "{} **Target SDK:** {}\n".format(test_description, data["targetsdk"]) - - if "minsdk" in data: - test_description = "{} **Min SDK:** {}\n".format(test_description, data["minsdk"]) - - if "maxsdk" in data: - test_description = "{} **Max SDK:** {}\n".format(test_description, data["maxsdk"]) - - test_description = f"{test_description}\n**File Information:**\n" - - if "name" in data: - test_description = "{} **Name:** {}\n".format(test_description, data["name"]) - - if "md5" in data: - test_description = "{} **MD5:** {}\n".format(test_description, data["md5"]) - - if "sha1" in data: - test_description = "{} **SHA-1:** {}\n".format(test_description, data["sha1"]) - - if "sha256" in data: - test_description = "{} **SHA-256:** {}\n".format(test_description, data["sha256"]) - - if "size" in data: - test_description = "{} **Size:** {}\n".format(test_description, data["size"]) - - if "urls" in data: - curl = "" - for url in data["urls"]: - for durl in url["urls"]: - curl = f"{durl}\n" - - if curl: - test_description = f"{test_description}\n**URL's:**\n {curl}\n" - - if "bin_anal" in data: - test_description = "{} \n**Binary Analysis:** {}\n".format(test_description, data["bin_anal"]) - - test.description = html2text(test_description) - - mobsf_findings = [] - # Mobile Permissions - if "permissions" in data: - # for permission, details in data["permissions"].items(): - if isinstance(data["permissions"], list): - for details in data["permissions"]: - mobsf_item = { - "category": "Mobile Permissions", - "title": details.get("name", ""), - "severity": self.getSeverityForPermission(details.get("status")), - "description": "**Permission Type:** " + details.get("name", "") + " (" + details.get("status", "") + ")\n\n**Description:** " + details.get("description", "") + "\n\n**Reason:** " + details.get("reason", ""), - "file_path": None, - } - mobsf_findings.append(mobsf_item) - else: - for permission, details in list(data["permissions"].items()): - mobsf_item = { - "category": "Mobile Permissions", - "title": permission, - "severity": self.getSeverityForPermission(details.get("status", "")), - "description": "**Permission Type:** " + permission + "\n\n**Description:** " + details.get("description", ""), - "file_path": None, - } - mobsf_findings.append(mobsf_item) - - # Insecure Connections - if "insecure_connections" in data: - for details in data["insecure_connections"]: - insecure_urls = "" - for url in details.split(","): - insecure_urls = insecure_urls + url + "\n" - - mobsf_item = { - "category": None, - "title": "Insecure Connections", - "severity": "Low", - "description": insecure_urls, - "file_path": None, - } - mobsf_findings.append(mobsf_item) - - # Certificate Analysis - if "certificate_analysis" in data: - if data["certificate_analysis"] != {}: - certificate_info = data["certificate_analysis"]["certificate_info"] - for details in data["certificate_analysis"]["certificate_findings"]: - if len(details) == 3: - mobsf_item = { - "category": "Certificate Analysis", - "title": details[2], - "severity": details[0].title(), - "description": details[1] + "\n\n**Certificate Info:** " + certificate_info, - "file_path": None, - } - mobsf_findings.append(mobsf_item) - elif len(details) == 2: - mobsf_item = { - "category": "Certificate Analysis", - "title": details[1], - "severity": details[0].title(), - "description": details[1] + "\n\n**Certificate Info:** " + certificate_info, - "file_path": None, - } - mobsf_findings.append(mobsf_item) - - # Manifest Analysis - if "manifest_analysis" in data: - if data["manifest_analysis"] != {} and isinstance(data["manifest_analysis"], dict): - if data["manifest_analysis"]["manifest_findings"]: - for details in data["manifest_analysis"]["manifest_findings"]: - mobsf_item = { - "category": "Manifest Analysis", - "title": details["title"], - "severity": details["severity"].title(), - "description": details["description"] + "\n\n " + details["name"], - "file_path": None, - } - mobsf_findings.append(mobsf_item) - else: - for details in data["manifest_analysis"]: - mobsf_item = { - "category": "Manifest Analysis", - "title": details["title"], - "severity": details["stat"].title(), - "description": details["desc"] + "\n\n " + details["name"], - "file_path": None, - } - mobsf_findings.append(mobsf_item) - - # Code Analysis - if "code_analysis" in data: - if data["code_analysis"] != {}: - if data["code_analysis"].get("findings"): - for details in data["code_analysis"]["findings"]: - metadata = data["code_analysis"]["findings"][details] - mobsf_item = { - "category": "Code Analysis", - "title": details, - "severity": metadata["metadata"]["severity"].title(), - "description": metadata["metadata"]["description"], - "file_path": None, - } - mobsf_findings.append(mobsf_item) - else: - for details in data["code_analysis"]: - metadata = data["code_analysis"][details] - if metadata.get("metadata"): - mobsf_item = { - "category": "Code Analysis", - "title": details, - "severity": metadata["metadata"]["severity"].title(), - "description": metadata["metadata"]["description"], - "file_path": None, - } - mobsf_findings.append(mobsf_item) - - # Binary Analysis - if "binary_analysis" in data: - if isinstance(data["binary_analysis"], list): - for details in data["binary_analysis"]: - for binary_analysis_type in details: - if binary_analysis_type != "name": - mobsf_item = { - "category": "Binary Analysis", - "title": details[binary_analysis_type]["description"].split(".")[0], - "severity": details[binary_analysis_type]["severity"].title(), - "description": details[binary_analysis_type]["description"], - "file_path": details["name"], - } - mobsf_findings.append(mobsf_item) - elif data["binary_analysis"].get("findings"): - for details in data["binary_analysis"]["findings"].values(): - # "findings":{ - # "Binary makes use of insecure API(s)":{ - # "detailed_desc":"The binary may contain the following insecure API(s) _memcpy\n, _strlen\n", - # "severity":"high", - # "cvss":6, - # "cwe":"CWE-676: Use of Potentially Dangerous Function", - # "owasp-mobile":"M7: Client Code Quality", - # "masvs":"MSTG-CODE-8" - # }, - mobsf_item = { - "category": "Binary Analysis", - "title": details["detailed_desc"], - "severity": details["severity"].title(), - "description": details["detailed_desc"], - "file_path": None, - } - mobsf_findings.append(mobsf_item) - else: - for details in data["binary_analysis"].values(): - # "Binary makes use of insecure API(s)":{ - # "detailed_desc":"The binary may contain the following insecure API(s) _vsprintf.", - # "severity":"high", - # "cvss":6, - # "cwe":"CWE-676 - Use of Potentially Dangerous Function", - # "owasp-mobile":"M7: Client Code Quality", - # "masvs":"MSTG-CODE-8" - # } - mobsf_item = { - "category": "Binary Analysis", - "title": details["detailed_desc"], - "severity": details["severity"].title(), - "description": details["detailed_desc"], - "file_path": None, - } - mobsf_findings.append(mobsf_item) - - # specific node for Android reports - if "android_api" in data: - # "android_insecure_random": { - # "files": { - # "u/c/a/b/a/c.java": "9", - # "kotlinx/coroutines/repackaged/net/bytebuddy/utility/RandomString.java": "3", - # ... - # "hu/mycompany/vbnmqweq/gateway/msg/Response.java": "13" - # }, - # "metadata": { - # "id": "android_insecure_random", - # "description": "The App uses an insecure Random Number Generator.", - # "type": "Regex", - # "pattern": "java\\.util\\.Random;", - # "severity": "high", - # "input_case": "exact", - # "cvss": 7.5, - # "cwe": "CWE-330 Use of Insufficiently Random Values", - # "owasp-mobile": "M5: Insufficient Cryptography", - # "masvs": "MSTG-CRYPTO-6" - # } - # }, - for api, details in list(data["android_api"].items()): - mobsf_item = { - "category": "Android API", - "title": details["metadata"]["description"], - "severity": details["metadata"]["severity"].title(), - "description": "**API:** " + api + "\n\n**Description:** " + details["metadata"]["description"], - "file_path": None, - } - mobsf_findings.append(mobsf_item) - - # Manifest - if "manifest" in data: - for details in data["manifest"]: - mobsf_item = { - "category": "Manifest", - "title": details["title"], - "severity": details["stat"], - "description": details["desc"], - "file_path": None, - } - mobsf_findings.append(mobsf_item) - - # MobSF Findings - if "findings" in data: - for title, finding in list(data["findings"].items()): - description = title - file_path = None - - if "path" in finding: - description += "\n\n**Files:**\n" - for path in finding["path"]: - if file_path is None: - file_path = path - description = description + " * " + path + "\n" - - mobsf_item = { - "category": "Findings", - "title": title, - "severity": finding["level"], - "description": description, - "file_path": file_path, - } - - mobsf_findings.append(mobsf_item) - if isinstance(data, list): - for finding in data: - mobsf_item = { - "category": finding["category"], - "title": finding["name"], - "severity": finding["severity"], - "description": finding["description"] + "\n" + "**apk_exploit_dict:** " + str(finding["apk_exploit_dict"]) + "\n" + "**line_number:** " + str(finding["line_number"]), - "file_path": finding["file_object"], - } - mobsf_findings.append(mobsf_item) - for mobsf_finding in mobsf_findings: - title = mobsf_finding["title"] - sev = self.getCriticalityRating(mobsf_finding["severity"]) - description = "" - file_path = None - if mobsf_finding["category"]: - description += "**Category:** " + mobsf_finding["category"] + "\n\n" - description += html2text(mobsf_finding["description"]) - finding = Finding( - title=title, - cwe=919, # Weaknesses in Mobile Applications - test=test, - description=description, - severity=sev, - references=None, - date=find_date, - static_finding=True, - dynamic_finding=False, - nb_occurences=1, - ) - if mobsf_finding["file_path"]: - finding.file_path = mobsf_finding["file_path"] - dupe_key = sev + title + description + mobsf_finding["file_path"] - else: - dupe_key = sev + title + description - if mobsf_finding["category"]: - dupe_key += mobsf_finding["category"] - if dupe_key in dupes: - find = dupes[dupe_key] - if description is not None: - find.description += description - find.nb_occurences += 1 - else: - dupes[dupe_key] = finding - return list(dupes.values()) - - def getSeverityForPermission(self, status): - """ - Convert status for permission detection to severity - - In MobSF there is only 4 know values for permission, - we map them as this: - dangerous => High (Critical?) - normal => Info - signature => Info (it's positive so... Info) - signatureOrSystem => Info (it's positive so... Info) - """ - if status == "dangerous": - return "High" - return "Info" - - # Criticality rating - def getCriticalityRating(self, rating): - criticality = "Info" - if rating.lower() == "good": - criticality = "Info" - elif rating.lower() == "warning": - criticality = "Low" - elif rating.lower() == "vulnerability": - criticality = "Medium" - else: - criticality = rating.lower().capitalize() - return criticality - - def suite_data(self, suites): - suite_info = "" - suite_info += suites["name"] + "\n" - suite_info += "Cipher Strength: " + str(suites["cipherStrength"]) + "\n" - if "ecdhBits" in suites: - suite_info += "ecdhBits: " + str(suites["ecdhBits"]) + "\n" - if "ecdhStrength" in suites: - suite_info += "ecdhStrength: " + str(suites["ecdhStrength"]) - suite_info += "\n\n" - return suite_info + if isinstance(data, list) or data.get("results") is None: + return MobSFapireport().get_findings(data, test) + if len(data.get("results")) == 0: + return [] + return MobSFjsonreport().get_findings(data, test) diff --git a/dojo/tools/mobsfscan/parser.py b/dojo/tools/mobsf/report.py similarity index 84% rename from dojo/tools/mobsfscan/parser.py rename to dojo/tools/mobsf/report.py index 49995720acb..3f076e2f8a5 100644 --- a/dojo/tools/mobsfscan/parser.py +++ b/dojo/tools/mobsf/report.py @@ -1,11 +1,10 @@ import hashlib -import json import re from dojo.models import Finding -class MobsfscanParser: +class MobSFjsonreport: """A class that can be used to parse the mobsfscan (https://github.com/MobSF/mobsfscan) JSON report file.""" @@ -15,19 +14,7 @@ class MobsfscanParser: "INFO": "Low", } - def get_scan_types(self): - return ["Mobsfscan Scan"] - - def get_label_for_scan_types(self, scan_type): - return "Mobsfscan Scan" - - def get_description_for_scan_types(self, scan_type): - return "Import JSON report for mobsfscan report file." - - def get_findings(self, filename, test): - data = json.load(filename) - if len(data.get("results")) == 0: - return [] + def get_findings(self, data, test): dupes = {} for key, item in data.get("results").items(): metadata = item.get("metadata") diff --git a/dojo/tools/mobsfscan/__init__.py b/dojo/tools/mobsfscan/__init__.py deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/unittests/scans/mobsfscan/many_findings.json b/unittests/scans/mobsf/many_findings.json similarity index 100% rename from unittests/scans/mobsfscan/many_findings.json rename to unittests/scans/mobsf/many_findings.json diff --git a/unittests/scans/mobsfscan/many_findings_cwe_lower.json b/unittests/scans/mobsf/many_findings_cwe_lower.json similarity index 100% rename from unittests/scans/mobsfscan/many_findings_cwe_lower.json rename to unittests/scans/mobsf/many_findings_cwe_lower.json diff --git a/unittests/scans/mobsfscan/no_findings.json b/unittests/scans/mobsf/no_findings.json similarity index 100% rename from unittests/scans/mobsfscan/no_findings.json rename to unittests/scans/mobsf/no_findings.json diff --git a/unittests/tools/test_mobsf_parser.py b/unittests/tools/test_mobsf_parser.py index 53ddbb8a3e6..88719e57d88 100644 --- a/unittests/tools/test_mobsf_parser.py +++ b/unittests/tools/test_mobsf_parser.py @@ -136,3 +136,162 @@ def test_parse_damnvulnrablebank(self): findings = parser.get_findings(testfile, test) testfile.close() self.assertEqual(80, len(findings)) + + def test_parse_no_findings(self): + with (get_unit_tests_scans_path("mobsf") / "no_findings.json").open(encoding="utf-8") as testfile: + parser = MobSFParser() + findings = parser.get_findings(testfile, Test()) + self.assertEqual(0, len(findings)) + + def test_parse_many_findings(self): + with (get_unit_tests_scans_path("mobsf") / "many_findings.json").open(encoding="utf-8") as testfile: + parser = MobSFParser() + findings = parser.get_findings(testfile, Test()) + self.assertEqual(8, len(findings)) + + with self.subTest(i=0): + finding = findings[0] + self.assertEqual("android_certificate_transparency", finding.title) + self.assertEqual("Low", finding.severity) + self.assertEqual(1, finding.nb_occurences) + self.assertIsNotNone(finding.description) + self.assertEqual(295, finding.cwe) + self.assertIsNotNone(finding.references) + + with self.subTest(i=1): + finding = findings[1] + self.assertEqual("android_kotlin_hardcoded", finding.title) + self.assertEqual("Medium", finding.severity) + self.assertEqual(1, finding.nb_occurences) + self.assertIsNotNone(finding.description) + self.assertEqual(798, finding.cwe) + self.assertIsNotNone(finding.references) + self.assertEqual("app/src/main/java/com/routes/domain/analytics/event/Signatures.kt", finding.file_path) + self.assertEqual(10, finding.line) + + with self.subTest(i=2): + finding = findings[2] + self.assertEqual("android_kotlin_hardcoded", finding.title) + self.assertEqual("Medium", finding.severity) + self.assertEqual(1, finding.nb_occurences) + self.assertIsNotNone(finding.description) + self.assertEqual(798, finding.cwe) + self.assertIsNotNone(finding.references) + self.assertEqual("app/src/main/java/com/routes/domain/analytics/event/Signatures2.kt", finding.file_path) + self.assertEqual(20, finding.line) + + with self.subTest(i=3): + finding = findings[3] + self.assertEqual("android_prevent_screenshot", finding.title) + self.assertEqual("Low", finding.severity) + self.assertEqual(1, finding.nb_occurences) + self.assertIsNotNone(finding.description) + self.assertEqual(200, finding.cwe) + self.assertIsNotNone(finding.references) + + with self.subTest(i=4): + finding = findings[4] + self.assertEqual("android_root_detection", finding.title) + self.assertEqual("Low", finding.severity) + self.assertEqual(1, finding.nb_occurences) + self.assertIsNotNone(finding.description) + self.assertEqual(919, finding.cwe) + self.assertIsNotNone(finding.references) + + with self.subTest(i=5): + finding = findings[5] + self.assertEqual("android_safetynet", finding.title) + self.assertEqual("Low", finding.severity) + self.assertEqual(1, finding.nb_occurences) + self.assertIsNotNone(finding.description) + self.assertEqual(353, finding.cwe) + self.assertIsNotNone(finding.references) + + with self.subTest(i=6): + finding = findings[6] + self.assertEqual("android_ssl_pinning", finding.title) + self.assertEqual("Low", finding.severity) + self.assertEqual(1, finding.nb_occurences) + self.assertIsNotNone(finding.description) + self.assertEqual(295, finding.cwe) + self.assertIsNotNone(finding.references) + + with self.subTest(i=7): + finding = findings[7] + self.assertEqual("android_tapjacking", finding.title) + self.assertEqual("Low", finding.severity) + self.assertEqual(1, finding.nb_occurences) + self.assertIsNotNone(finding.description) + self.assertEqual(200, finding.cwe) + self.assertIsNotNone(finding.references) + + def test_parse_many_findings_cwe_lower(self): + with (get_unit_tests_scans_path("mobsf") / "many_findings_cwe_lower.json").open(encoding="utf-8") as testfile: + parser = MobSFParser() + findings = parser.get_findings(testfile, Test()) + self.assertEqual(7, len(findings)) + + with self.subTest(i=0): + finding = findings[0] + self.assertEqual("android_certificate_transparency", finding.title) + self.assertEqual("Low", finding.severity) + self.assertEqual(1, finding.nb_occurences) + self.assertIsNotNone(finding.description) + self.assertEqual(295, finding.cwe) + self.assertIsNotNone(finding.references) + + with self.subTest(i=1): + finding = findings[1] + self.assertEqual("android_kotlin_hardcoded", finding.title) + self.assertEqual("Medium", finding.severity) + self.assertEqual(1, finding.nb_occurences) + self.assertIsNotNone(finding.description) + self.assertEqual(798, finding.cwe) + self.assertIsNotNone(finding.references) + self.assertEqual("app/src/main/java/com/routes/domain/analytics/event/Signatures.kt", finding.file_path) + self.assertEqual(10, finding.line) + + with self.subTest(i=2): + finding = findings[2] + self.assertEqual("android_prevent_screenshot", finding.title) + self.assertEqual("Low", finding.severity) + self.assertEqual(1, finding.nb_occurences) + self.assertIsNotNone(finding.description) + self.assertEqual(200, finding.cwe) + self.assertIsNotNone(finding.references) + + with self.subTest(i=3): + finding = findings[3] + self.assertEqual("android_root_detection", finding.title) + self.assertEqual("Low", finding.severity) + self.assertEqual(1, finding.nb_occurences) + self.assertIsNotNone(finding.description) + self.assertEqual(919, finding.cwe) + self.assertIsNotNone(finding.references) + + with self.subTest(i=4): + finding = findings[4] + self.assertEqual("android_safetynet", finding.title) + self.assertEqual("Low", finding.severity) + self.assertEqual(1, finding.nb_occurences) + self.assertIsNotNone(finding.description) + self.assertEqual(353, finding.cwe) + self.assertIsNotNone(finding.references) + + with self.subTest(i=5): + finding = findings[5] + self.assertEqual("android_ssl_pinning", finding.title) + self.assertEqual("Low", finding.severity) + self.assertEqual(1, finding.nb_occurences) + self.assertIsNotNone(finding.description) + self.assertEqual(295, finding.cwe) + self.assertIsNotNone(finding.references) + + with self.subTest(i=6): + finding = findings[6] + self.assertEqual("android_tapjacking", finding.title) + self.assertEqual("Low", finding.severity) + self.assertEqual(1, finding.nb_occurences) + self.assertIsNotNone(finding.description) + self.assertEqual(200, finding.cwe) + self.assertIsNotNone(finding.references) diff --git a/unittests/tools/test_mobsfscan_parser.py b/unittests/tools/test_mobsfscan_parser.py deleted file mode 100644 index cbb6245c227..00000000000 --- a/unittests/tools/test_mobsfscan_parser.py +++ /dev/null @@ -1,165 +0,0 @@ -from dojo.models import Test -from dojo.tools.mobsfscan.parser import MobsfscanParser -from unittests.dojo_test_case import DojoTestCase, get_unit_tests_scans_path - - -class TestMobsfscanParser(DojoTestCase): - - def test_parse_no_findings(self): - with (get_unit_tests_scans_path("mobsfscan") / "no_findings.json").open(encoding="utf-8") as testfile: - parser = MobsfscanParser() - findings = parser.get_findings(testfile, Test()) - self.assertEqual(0, len(findings)) - - def test_parse_many_findings(self): - with (get_unit_tests_scans_path("mobsfscan") / "many_findings.json").open(encoding="utf-8") as testfile: - parser = MobsfscanParser() - findings = parser.get_findings(testfile, Test()) - self.assertEqual(8, len(findings)) - - with self.subTest(i=0): - finding = findings[0] - self.assertEqual("android_certificate_transparency", finding.title) - self.assertEqual("Low", finding.severity) - self.assertEqual(1, finding.nb_occurences) - self.assertIsNotNone(finding.description) - self.assertEqual(295, finding.cwe) - self.assertIsNotNone(finding.references) - - with self.subTest(i=1): - finding = findings[1] - self.assertEqual("android_kotlin_hardcoded", finding.title) - self.assertEqual("Medium", finding.severity) - self.assertEqual(1, finding.nb_occurences) - self.assertIsNotNone(finding.description) - self.assertEqual(798, finding.cwe) - self.assertIsNotNone(finding.references) - self.assertEqual("app/src/main/java/com/routes/domain/analytics/event/Signatures.kt", finding.file_path) - self.assertEqual(10, finding.line) - - with self.subTest(i=2): - finding = findings[2] - self.assertEqual("android_kotlin_hardcoded", finding.title) - self.assertEqual("Medium", finding.severity) - self.assertEqual(1, finding.nb_occurences) - self.assertIsNotNone(finding.description) - self.assertEqual(798, finding.cwe) - self.assertIsNotNone(finding.references) - self.assertEqual("app/src/main/java/com/routes/domain/analytics/event/Signatures2.kt", finding.file_path) - self.assertEqual(20, finding.line) - - with self.subTest(i=3): - finding = findings[3] - self.assertEqual("android_prevent_screenshot", finding.title) - self.assertEqual("Low", finding.severity) - self.assertEqual(1, finding.nb_occurences) - self.assertIsNotNone(finding.description) - self.assertEqual(200, finding.cwe) - self.assertIsNotNone(finding.references) - - with self.subTest(i=4): - finding = findings[4] - self.assertEqual("android_root_detection", finding.title) - self.assertEqual("Low", finding.severity) - self.assertEqual(1, finding.nb_occurences) - self.assertIsNotNone(finding.description) - self.assertEqual(919, finding.cwe) - self.assertIsNotNone(finding.references) - - with self.subTest(i=5): - finding = findings[5] - self.assertEqual("android_safetynet", finding.title) - self.assertEqual("Low", finding.severity) - self.assertEqual(1, finding.nb_occurences) - self.assertIsNotNone(finding.description) - self.assertEqual(353, finding.cwe) - self.assertIsNotNone(finding.references) - - with self.subTest(i=6): - finding = findings[6] - self.assertEqual("android_ssl_pinning", finding.title) - self.assertEqual("Low", finding.severity) - self.assertEqual(1, finding.nb_occurences) - self.assertIsNotNone(finding.description) - self.assertEqual(295, finding.cwe) - self.assertIsNotNone(finding.references) - - with self.subTest(i=7): - finding = findings[7] - self.assertEqual("android_tapjacking", finding.title) - self.assertEqual("Low", finding.severity) - self.assertEqual(1, finding.nb_occurences) - self.assertIsNotNone(finding.description) - self.assertEqual(200, finding.cwe) - self.assertIsNotNone(finding.references) - - def test_parse_many_findings_cwe_lower(self): - with (get_unit_tests_scans_path("mobsfscan") / "many_findings_cwe_lower.json").open(encoding="utf-8") as testfile: - parser = MobsfscanParser() - findings = parser.get_findings(testfile, Test()) - self.assertEqual(7, len(findings)) - - with self.subTest(i=0): - finding = findings[0] - self.assertEqual("android_certificate_transparency", finding.title) - self.assertEqual("Low", finding.severity) - self.assertEqual(1, finding.nb_occurences) - self.assertIsNotNone(finding.description) - self.assertEqual(295, finding.cwe) - self.assertIsNotNone(finding.references) - - with self.subTest(i=1): - finding = findings[1] - self.assertEqual("android_kotlin_hardcoded", finding.title) - self.assertEqual("Medium", finding.severity) - self.assertEqual(1, finding.nb_occurences) - self.assertIsNotNone(finding.description) - self.assertEqual(798, finding.cwe) - self.assertIsNotNone(finding.references) - self.assertEqual("app/src/main/java/com/routes/domain/analytics/event/Signatures.kt", finding.file_path) - self.assertEqual(10, finding.line) - - with self.subTest(i=2): - finding = findings[2] - self.assertEqual("android_prevent_screenshot", finding.title) - self.assertEqual("Low", finding.severity) - self.assertEqual(1, finding.nb_occurences) - self.assertIsNotNone(finding.description) - self.assertEqual(200, finding.cwe) - self.assertIsNotNone(finding.references) - - with self.subTest(i=3): - finding = findings[3] - self.assertEqual("android_root_detection", finding.title) - self.assertEqual("Low", finding.severity) - self.assertEqual(1, finding.nb_occurences) - self.assertIsNotNone(finding.description) - self.assertEqual(919, finding.cwe) - self.assertIsNotNone(finding.references) - - with self.subTest(i=4): - finding = findings[4] - self.assertEqual("android_safetynet", finding.title) - self.assertEqual("Low", finding.severity) - self.assertEqual(1, finding.nb_occurences) - self.assertIsNotNone(finding.description) - self.assertEqual(353, finding.cwe) - self.assertIsNotNone(finding.references) - - with self.subTest(i=5): - finding = findings[5] - self.assertEqual("android_ssl_pinning", finding.title) - self.assertEqual("Low", finding.severity) - self.assertEqual(1, finding.nb_occurences) - self.assertIsNotNone(finding.description) - self.assertEqual(295, finding.cwe) - self.assertIsNotNone(finding.references) - - with self.subTest(i=6): - finding = findings[6] - self.assertEqual("android_tapjacking", finding.title) - self.assertEqual("Low", finding.severity) - self.assertEqual(1, finding.nb_occurences) - self.assertIsNotNone(finding.description) - self.assertEqual(200, finding.cwe) - self.assertIsNotNone(finding.references) From 6b17b5ed8ab7c33a24592043987d305f00b1b623 Mon Sep 17 00:00:00 2001 From: kiblik <5609770+kiblik@users.noreply.github.com> Date: Thu, 16 Oct 2025 20:29:47 +0000 Subject: [PATCH 049/126] feat(helm): Split image locations+tags, allow digest pinning (#13370) --- .github/workflows/k8s-tests.yml | 2 + .../workflows/release-x-manual-helm-chart.yml | 10 - docs/content/en/open_source/upgrading/2.52.md | 13 +- helm/defectdojo/Chart.yaml | 4 +- helm/defectdojo/README.md | 45 ++++- helm/defectdojo/templates/_helpers.tpl | 62 +++++- .../templates/celery-beat-deployment.yaml | 2 +- .../templates/celery-worker-deployment.yaml | 2 +- .../templates/django-deployment.yaml | 6 +- .../defectdojo/templates/initializer-job.yaml | 4 +- .../templates/tests/unit-tests.yaml | 2 +- helm/defectdojo/values.schema.json | 190 +++++++++++++++++- helm/defectdojo/values.yaml | 58 +++++- 13 files changed, 354 insertions(+), 46 deletions(-) diff --git a/.github/workflows/k8s-tests.yml b/.github/workflows/k8s-tests.yml index 29a2f778d0a..edd86156670 100644 --- a/.github/workflows/k8s-tests.yml +++ b/.github/workflows/k8s-tests.yml @@ -69,6 +69,8 @@ jobs: defectdojo \ ./helm/defectdojo \ --set django.ingress.enabled=true \ + --set images.django.image.tag=latest \ + --set images.nginx.image.tag=latest \ --set imagePullPolicy=Never \ --set initializer.keepSeconds="-1" \ --set redis.enabled=true \ diff --git a/.github/workflows/release-x-manual-helm-chart.yml b/.github/workflows/release-x-manual-helm-chart.yml index 8879885f86c..a1105697c7d 100644 --- a/.github/workflows/release-x-manual-helm-chart.yml +++ b/.github/workflows/release-x-manual-helm-chart.yml @@ -69,16 +69,6 @@ jobs: helm dependency list ./helm/defectdojo helm dependency update ./helm/defectdojo - - name: Add yq - uses: mikefarah/yq@0ecdce24e83f0fa127940334be98c86b07b0c488 # v4.48.1 - - - name: Pin version docker version - id: pin_image - run: |- - yq --version - yq -i '.tag="${{ inputs.release_number }}"' helm/defectdojo/values.yaml - echo "Current image tag:`yq -r '.tag' helm/defectdojo/values.yaml`" - - name: Package Helm chart id: package-helm-chart run: | diff --git a/docs/content/en/open_source/upgrading/2.52.md b/docs/content/en/open_source/upgrading/2.52.md index 1619ee0fa81..c9f6b38418f 100644 --- a/docs/content/en/open_source/upgrading/2.52.md +++ b/docs/content/en/open_source/upgrading/2.52.md @@ -2,18 +2,25 @@ title: 'Upgrading to DefectDojo Version 2.52.x' toc_hide: true weight: -20251006 -description: Helm chart changes. +description: MobSF parsers & Helm chart changes. --- +## Merge of MobSF parsers + +Mobsfscan Scan" has been merged into the "MobSF Scan" parser. The "Mobsfscan Scan" scan_type has been retained to keep deduplication working for existing Tests, but users are encouraged to move to the "MobSF Scan" scan_type. + ## Helm Chart Changes This release introduces more important changes to the Helm chart configuration: ### Breaking changes -#### Merge of MobSF parsers +#### Tags -Mobsfscan Scan" has been merged into the "MobSF Scan" parser. The "Mobsfscan Scan" scan_type has been retained to keep deduplication working for existing Tests, but users are encouraged to move to the "MobSF Scan" scan_type. +`tag` and `repositoryPrefix` fields have been deprecated. Currently, image tags used in containers are derived by default from the `appVersion` defined in the Chart. +This behavior can be overridden by setting the `tag` value in `images.django` and `images.nginx`. +If fine-tuning is necessary, each container’s image value can also be customized individually (`celery.beat.image`, `celery.worker.image`, `django.nginx.image`, `django.uwsgi.image`, `initializer.image`, and `dbMigrationChecker.image`). +Digest pinning is now supported as well. #### Security context diff --git a/helm/defectdojo/Chart.yaml b/helm/defectdojo/Chart.yaml index 078a6751a5b..942547c7c2f 100644 --- a/helm/defectdojo/Chart.yaml +++ b/helm/defectdojo/Chart.yaml @@ -2,7 +2,7 @@ apiVersion: v2 appVersion: "2.52.0-dev" description: A Helm chart for Kubernetes to install DefectDojo name: defectdojo -version: 1.7.2-dev +version: 1.8.0-dev icon: https://defectdojo.com/hubfs/DefectDojo_favicon.png maintainers: - name: madchap @@ -22,5 +22,7 @@ annotations: artifacthub.io/changes: | - kind: changed description: DRY cloudsql-proxy + - kind: changed + description: Each component allow to specific image + allow digest pinning - kind: added description: Testing on the oldest officially supported k8s diff --git a/helm/defectdojo/README.md b/helm/defectdojo/README.md index 05ef4a1afd7..03585f6d60d 100644 --- a/helm/defectdojo/README.md +++ b/helm/defectdojo/README.md @@ -495,7 +495,7 @@ kubectl delete pvc data-defectdojo-redis-0 data-defectdojo-postgresql-0 # General information about chart values -![Version: 1.7.2-dev](https://img.shields.io/badge/Version-1.7.2--dev-informational?style=flat-square) ![AppVersion: 2.52.0-dev](https://img.shields.io/badge/AppVersion-2.52.0--dev-informational?style=flat-square) +![Version: 1.8.0-dev](https://img.shields.io/badge/Version-1.8.0--dev-informational?style=flat-square) ![AppVersion: 2.52.0-dev](https://img.shields.io/badge/AppVersion-2.52.0--dev-informational?style=flat-square) A Helm chart for Kubernetes to install DefectDojo @@ -534,6 +534,10 @@ A Helm chart for Kubernetes to install DefectDojo | celery.beat.extraInitContainers | list | `[]` | | | celery.beat.extraVolumeMounts | list | `[]` | | | celery.beat.extraVolumes | list | `[]` | | +| celery.beat.image.digest | string | `""` | | +| celery.beat.image.registry | string | `""` | | +| celery.beat.image.repository | string | `""` | | +| celery.beat.image.tag | string | `""` | | | celery.beat.livenessProbe | object | `{}` | | | celery.beat.nodeSelector | object | `{}` | | | celery.beat.podAnnotations | object | `{}` | | @@ -557,6 +561,10 @@ A Helm chart for Kubernetes to install DefectDojo | celery.worker.extraInitContainers | list | `[]` | | | celery.worker.extraVolumeMounts | list | `[]` | | | celery.worker.extraVolumes | list | `[]` | | +| celery.worker.image.digest | string | `""` | | +| celery.worker.image.registry | string | `""` | | +| celery.worker.image.repository | string | `""` | | +| celery.worker.image.tag | string | `""` | | | celery.worker.livenessProbe | object | `{}` | | | celery.worker.nodeSelector | object | `{}` | | | celery.worker.podAnnotations | object | `{}` | | @@ -588,6 +596,10 @@ A Helm chart for Kubernetes to install DefectDojo | dbMigrationChecker.enabled | bool | `true` | | | dbMigrationChecker.extraEnv | list | `[]` | | | dbMigrationChecker.extraVolumeMounts | list | `[]` | | +| dbMigrationChecker.image.digest | string | `""` | | +| dbMigrationChecker.image.registry | string | `""` | | +| dbMigrationChecker.image.repository | string | `""` | | +| dbMigrationChecker.image.tag | string | `""` | | | dbMigrationChecker.resources.limits.cpu | string | `"200m"` | | | dbMigrationChecker.resources.limits.memory | string | `"200Mi"` | | | dbMigrationChecker.resources.requests.cpu | string | `"100m"` | | @@ -617,6 +629,10 @@ A Helm chart for Kubernetes to install DefectDojo | django.nginx.containerSecurityContext.runAsUser | int | `1001` | | | django.nginx.extraEnv | list | `[]` | | | django.nginx.extraVolumeMounts | list | `[]` | | +| django.nginx.image.digest | string | `""` | | +| django.nginx.image.registry | string | `""` | | +| django.nginx.image.repository | string | `""` | | +| django.nginx.image.tag | string | `""` | | | django.nginx.resources.limits.cpu | string | `"2000m"` | | | django.nginx.resources.limits.memory | string | `"256Mi"` | | | django.nginx.resources.requests.cpu | string | `"100m"` | | @@ -641,6 +657,10 @@ A Helm chart for Kubernetes to install DefectDojo | django.uwsgi.enableDebug | bool | `false` | | | django.uwsgi.extraEnv | list | `[]` | | | django.uwsgi.extraVolumeMounts | list | `[]` | | +| django.uwsgi.image.digest | string | `""` | | +| django.uwsgi.image.registry | string | `""` | | +| django.uwsgi.image.repository | string | `""` | | +| django.uwsgi.image.tag | string | `""` | | | django.uwsgi.livenessProbe.enabled | bool | `true` | | | django.uwsgi.livenessProbe.failureThreshold | int | `6` | | | django.uwsgi.livenessProbe.initialDelaySeconds | int | `0` | | @@ -674,6 +694,14 @@ A Helm chart for Kubernetes to install DefectDojo | host | string | `"defectdojo.default.minikube.local"` | | | imagePullPolicy | string | `"Always"` | | | imagePullSecrets | string | `nil` | | +| images.django.image.digest | string | `""` | | +| images.django.image.registry | string | `""` | | +| images.django.image.repository | string | `"defectdojo/defectdojo-django"` | | +| images.django.image.tag | string | `""` | | +| images.nginx.image.digest | string | `""` | | +| images.nginx.image.registry | string | `""` | | +| images.nginx.image.repository | string | `"defectdojo/defectdojo-nginx"` | | +| images.nginx.image.tag | string | `""` | | | initializer.affinity | object | `{}` | | | initializer.annotations | object | `{}` | | | initializer.automountServiceAccountToken | bool | `false` | | @@ -681,6 +709,10 @@ A Helm chart for Kubernetes to install DefectDojo | initializer.extraEnv | list | `[]` | | | initializer.extraVolumeMounts | list | `[]` | | | initializer.extraVolumes | list | `[]` | | +| initializer.image.digest | string | `""` | | +| initializer.image.registry | string | `""` | | +| initializer.image.repository | string | `""` | | +| initializer.image.tag | string | `""` | | | initializer.jobAnnotations | object | `{}` | | | initializer.keepSeconds | int | `60` | | | initializer.labels | object | `{}` | | @@ -699,7 +731,10 @@ A Helm chart for Kubernetes to install DefectDojo | monitoring.prometheus.enabled | bool | `false` | | | monitoring.prometheus.extraEnv | list | `[]` | | | monitoring.prometheus.extraVolumeMounts | list | `[]` | | -| monitoring.prometheus.image | string | `"nginx/nginx-prometheus-exporter:1.4.2"` | | +| monitoring.prometheus.image.digest | string | `""` | | +| monitoring.prometheus.image.registry | string | `""` | | +| monitoring.prometheus.image.repository | string | `"nginx/nginx-prometheus-exporter"` | | +| monitoring.prometheus.image.tag | string | `"1.4.2"` | | | monitoring.prometheus.imagePullPolicy | string | `"IfNotPresent"` | | | monitoring.prometheus.resources | object | `{}` | | | networkPolicy.annotations | object | `{}` | | @@ -739,7 +774,6 @@ A Helm chart for Kubernetes to install DefectDojo | redis.tls.enabled | bool | `false` | | | redisParams | string | `""` | | | redisServer | string | `nil` | | -| repositoryPrefix | string | `"defectdojo"` | | | revisionHistoryLimit | int | `10` | | | secrets.annotations | object | `{}` | | | securityContext.containerSecurityContext.runAsNonRoot | bool | `true` | | @@ -750,8 +784,11 @@ A Helm chart for Kubernetes to install DefectDojo | serviceAccount.labels | object | `{}` | | | serviceAccount.name | string | `""` | | | siteUrl | string | `""` | | -| tag | string | `"latest"` | | | tests.unitTests.automountServiceAccountToken | bool | `false` | | +| tests.unitTests.image.digest | string | `""` | | +| tests.unitTests.image.registry | string | `""` | | +| tests.unitTests.image.repository | string | `""` | | +| tests.unitTests.image.tag | string | `""` | | | tests.unitTests.resources.limits.cpu | string | `"500m"` | | | tests.unitTests.resources.limits.memory | string | `"512Mi"` | | | tests.unitTests.resources.requests.cpu | string | `"100m"` | | diff --git a/helm/defectdojo/templates/_helpers.tpl b/helm/defectdojo/templates/_helpers.tpl index 0e3db8a4fc1..b6243d6ac19 100644 --- a/helm/defectdojo/templates/_helpers.tpl +++ b/helm/defectdojo/templates/_helpers.tpl @@ -85,20 +85,64 @@ {{- /* Builds the repository names for use with local or private registries */}} -{{- define "celery.repository" -}} -{{- printf "%s" .Values.repositoryPrefix -}}/defectdojo-django +{{- define "celery.beat.image" -}} +{{ include "images.image" (dict "imageRoot" (merge .Values.celery.beat.image .Values.images.django.image) "global" .Values.global "chart" .Chart ) }} {{- end -}} -{{- define "django.nginx.repository" -}} -{{- printf "%s" .Values.repositoryPrefix -}}/defectdojo-nginx +{{- define "celery.worker.image" -}} +{{ include "images.image" (dict "imageRoot" (merge .Values.celery.worker.image .Values.images.django.image) "global" .Values.global "chart" .Chart ) }} {{- end -}} -{{- define "django.uwsgi.repository" -}} -{{- printf "%s" .Values.repositoryPrefix -}}/defectdojo-django +{{- define "django.nginx.image" -}} +{{ include "images.image" (dict "imageRoot" (merge .Values.django.nginx.image .Values.images.nginx.image) "global" .Values.global "chart" .Chart ) }} {{- end -}} -{{- define "initializer.repository" -}} -{{- printf "%s" .Values.repositoryPrefix -}}/defectdojo-django +{{- define "django.uwsgi.image" -}} +{{ include "images.image" (dict "imageRoot" (merge .Values.django.uwsgi.image .Values.images.django.image) "global" .Values.global "chart" .Chart ) }} +{{- end -}} + +{{- define "initializer.image" -}} +{{ include "images.image" (dict "imageRoot" (merge .Values.initializer.image .Values.images.django.image) "global" .Values.global "chart" .Chart ) }} +{{- end -}} + +{{- define "dbMigrationChecker.image" -}} +{{ include "images.image" (dict "imageRoot" (merge .Values.dbMigrationChecker.image .Values.images.django.image) "global" .Values.global "chart" .Chart ) }} +{{- end -}} + +{{- define "unitTests.image" -}} +{{ include "images.image" (dict "imageRoot" (merge .Values.tests.unitTests.image .Values.images.django.image) "global" .Values.global "chart" .Chart ) }} +{{- end -}} + +{{- define "monitoring.prometheus.image" -}} +{{ include "images.image" (dict "imageRoot" .Values.monitoring.prometheus.image "global" .Values.global ) }} +{{- end -}} + +{{- /* +Return the proper image name. +If image tag and digest are not defined, termination fallbacks to chart appVersion. +{{ include "images.image" ( dict "imageRoot" .Values.path.to.the.image "global" .Values.global "chart" .Chart ) }} +Inspired by Bitnami Common Chart v2.31.7 +*/}} +{{- define "images.image" -}} +{{- $registryName := default .imageRoot.registry ((.global).imageRegistry) -}} +{{- $repositoryName := .imageRoot.repository -}} +{{- $separator := ":" -}} +{{- $termination := .imageRoot.tag | toString -}} + +{{- if not .imageRoot.tag }} + {{- if .chart }} + {{- $termination = .chart.AppVersion | toString -}} + {{- end -}} +{{- end -}} +{{- if .imageRoot.digest }} + {{- $separator = "@" -}} + {{- $termination = .imageRoot.digest | toString -}} +{{- end -}} +{{- if $registryName }} + {{- printf "%s/%s%s%s" $registryName $repositoryName $separator $termination -}} +{{- else -}} + {{- printf "%s%s%s" $repositoryName $separator $termination -}} +{{- end -}} {{- end -}} {{- define "initializer.jobname" -}} @@ -141,7 +185,7 @@ - sh - -c - while ! /app/manage.py migrate --check; do echo "Database is not migrated to the latest state yet"; sleep 5; done; echo "Database is migrated to the latest state"; - image: '{{ template "django.uwsgi.repository" . }}:{{ .Values.tag }}' + image: '{{ template "dbMigrationChecker.image" . }}' imagePullPolicy: {{ .Values.imagePullPolicy }} {{- if .Values.securityContext.enabled }} securityContext: diff --git a/helm/defectdojo/templates/celery-beat-deployment.yaml b/helm/defectdojo/templates/celery-beat-deployment.yaml index 213166d879b..b1832f71e29 100644 --- a/helm/defectdojo/templates/celery-beat-deployment.yaml +++ b/helm/defectdojo/templates/celery-beat-deployment.yaml @@ -99,7 +99,7 @@ spec: - command: - /entrypoint-celery-beat.sh name: celery - image: "{{ template "celery.repository" . }}:{{ .Values.tag }}" + image: "{{ template "celery.beat.image" . }}" imagePullPolicy: {{ .Values.imagePullPolicy }} {{- with .Values.celery.beat.livenessProbe }} livenessProbe: {{ toYaml . | nindent 10 }} diff --git a/helm/defectdojo/templates/celery-worker-deployment.yaml b/helm/defectdojo/templates/celery-worker-deployment.yaml index a606fad50a8..14ddcf79f4b 100644 --- a/helm/defectdojo/templates/celery-worker-deployment.yaml +++ b/helm/defectdojo/templates/celery-worker-deployment.yaml @@ -95,7 +95,7 @@ spec: {{- end }} containers: - name: celery - image: "{{ template "celery.repository" . }}:{{ .Values.tag }}" + image: "{{ template "celery.worker.image" . }}" imagePullPolicy: {{ .Values.imagePullPolicy }} {{- with .Values.celery.worker.livenessProbe }} livenessProbe: {{ toYaml . | nindent 10 }} diff --git a/helm/defectdojo/templates/django-deployment.yaml b/helm/defectdojo/templates/django-deployment.yaml index 3919bced004..9b650affdc4 100644 --- a/helm/defectdojo/templates/django-deployment.yaml +++ b/helm/defectdojo/templates/django-deployment.yaml @@ -116,7 +116,7 @@ spec: containers: {{- if and .Values.monitoring.enabled .Values.monitoring.prometheus.enabled }} - name: metrics - image: {{ .Values.monitoring.prometheus.image }} + image: '{{ template "monitoring.prometheus.image" . }}' imagePullPolicy: {{ .Values.monitoring.prometheus.imagePullPolicy }} command: - /usr/bin/nginx-prometheus-exporter @@ -152,7 +152,7 @@ spec: {{- end }} {{- end }} - name: uwsgi - image: '{{ template "django.uwsgi.repository" . }}:{{ .Values.tag }}' + image: '{{ template "django.uwsgi.image" . }}' imagePullPolicy: {{ .Values.imagePullPolicy }} {{- if .Values.securityContext.enabled }} securityContext: @@ -254,7 +254,7 @@ spec: resources: {{- toYaml .Values.django.uwsgi.resources | nindent 10 }} - name: nginx - image: '{{ template "django.nginx.repository" . }}:{{ .Values.tag }}' + image: '{{ template "django.nginx.image" . }}' imagePullPolicy: {{ .Values.imagePullPolicy }} {{- if .Values.securityContext.enabled }} securityContext: diff --git a/helm/defectdojo/templates/initializer-job.yaml b/helm/defectdojo/templates/initializer-job.yaml index 9713de08bb2..43dcd269d8f 100644 --- a/helm/defectdojo/templates/initializer-job.yaml +++ b/helm/defectdojo/templates/initializer-job.yaml @@ -82,7 +82,7 @@ spec: - '/bin/bash' - '-c' - '/wait-for-it.sh ${DD_DATABASE_HOST:-postgres}:${DD_DATABASE_PORT:-5432} -t 300 -s -- /bin/echo Database is up' - image: '{{ template "django.uwsgi.repository" . }}:{{ .Values.tag }}' + image: "{{ template "initializer.image" . }}" imagePullPolicy: {{ .Values.imagePullPolicy }} {{- if .Values.securityContext.enabled }} securityContext: @@ -113,7 +113,7 @@ spec: {{- end }} containers: - name: initializer - image: "{{ template "initializer.repository" . }}:{{ .Values.tag }}" + image: "{{ template "initializer.image" . }}" imagePullPolicy: {{ .Values.imagePullPolicy }} {{- if .Values.securityContext.enabled }} securityContext: diff --git a/helm/defectdojo/templates/tests/unit-tests.yaml b/helm/defectdojo/templates/tests/unit-tests.yaml index 08939429008..01fa4cf1041 100644 --- a/helm/defectdojo/templates/tests/unit-tests.yaml +++ b/helm/defectdojo/templates/tests/unit-tests.yaml @@ -19,7 +19,7 @@ spec: {{- end }} containers: - name: unit-tests - image: '{{ .Values.repositoryPrefix }}/defectdojo-django:{{ .Values.tag }}' + image: '{{ template "unitTests.image" . }}' imagePullPolicy: {{ .Values.imagePullPolicy }} {{- if .Values.securityContext.enabled }} securityContext: diff --git a/helm/defectdojo/values.schema.json b/helm/defectdojo/values.schema.json index e9c71966165..bd012488bdb 100644 --- a/helm/defectdojo/values.schema.json +++ b/helm/defectdojo/values.schema.json @@ -67,6 +67,23 @@ "extraVolumes": { "type": "array" }, + "image": { + "type": "object", + "properties": { + "digest": { + "type": "string" + }, + "registry": { + "type": "string" + }, + "repository": { + "type": "string" + }, + "tag": { + "type": "string" + } + } + }, "livenessProbe": { "type": "object" }, @@ -161,6 +178,23 @@ "extraVolumes": { "type": "array" }, + "image": { + "type": "object", + "properties": { + "digest": { + "type": "string" + }, + "registry": { + "type": "string" + }, + "repository": { + "type": "string" + }, + "tag": { + "type": "string" + } + } + }, "livenessProbe": { "type": "object" }, @@ -286,6 +320,23 @@ "extraVolumeMounts": { "type": "array" }, + "image": { + "type": "object", + "properties": { + "digest": { + "type": "string" + }, + "registry": { + "type": "string" + }, + "repository": { + "type": "string" + }, + "tag": { + "type": "string" + } + } + }, "resources": { "type": "object", "properties": { @@ -419,6 +470,23 @@ "extraVolumeMounts": { "type": "array" }, + "image": { + "type": "object", + "properties": { + "digest": { + "type": "string" + }, + "registry": { + "type": "string" + }, + "repository": { + "type": "string" + }, + "tag": { + "type": "string" + } + } + }, "resources": { "type": "object", "properties": { @@ -541,6 +609,23 @@ "extraVolumeMounts": { "type": "array" }, + "image": { + "type": "object", + "properties": { + "digest": { + "type": "string" + }, + "registry": { + "type": "string" + }, + "repository": { + "type": "string" + }, + "tag": { + "type": "string" + } + } + }, "livenessProbe": { "type": "object", "properties": { @@ -682,6 +767,55 @@ "null" ] }, + "images": { + "type": "object", + "properties": { + "django": { + "type": "object", + "properties": { + "image": { + "type": "object", + "properties": { + "digest": { + "type": "string" + }, + "registry": { + "type": "string" + }, + "repository": { + "type": "string" + }, + "tag": { + "type": "string" + } + } + } + } + }, + "nginx": { + "type": "object", + "properties": { + "image": { + "type": "object", + "properties": { + "digest": { + "type": "string" + }, + "registry": { + "type": "string" + }, + "repository": { + "type": "string" + }, + "tag": { + "type": "string" + } + } + } + } + } + } + }, "initializer": { "type": "object", "properties": { @@ -706,6 +840,23 @@ "extraVolumes": { "type": "array" }, + "image": { + "type": "object", + "properties": { + "digest": { + "type": "string" + }, + "registry": { + "type": "string" + }, + "repository": { + "type": "string" + }, + "tag": { + "type": "string" + } + } + }, "jobAnnotations": { "type": "object" }, @@ -784,7 +935,21 @@ "type": "array" }, "image": { - "type": "string" + "type": "object", + "properties": { + "digest": { + "type": "string" + }, + "registry": { + "type": "string" + }, + "repository": { + "type": "string" + }, + "tag": { + "type": "string" + } + } }, "imagePullPolicy": { "type": "string" @@ -1003,9 +1168,6 @@ "null" ] }, - "repositoryPrefix": { - "type": "string" - }, "revisionHistoryLimit": { "type": "integer" }, @@ -1061,9 +1223,6 @@ "siteUrl": { "type": "string" }, - "tag": { - "type": "string" - }, "tests": { "type": "object", "properties": { @@ -1073,6 +1232,23 @@ "automountServiceAccountToken": { "type": "boolean" }, + "image": { + "type": "object", + "properties": { + "digest": { + "type": "string" + }, + "registry": { + "type": "string" + }, + "repository": { + "type": "string" + }, + "tag": { + "type": "string" + } + } + }, "resources": { "type": "object", "properties": { diff --git a/helm/defectdojo/values.yaml b/helm/defectdojo/values.yaml index c16955e096e..faac6f999c0 100644 --- a/helm/defectdojo/values.yaml +++ b/helm/defectdojo/values.yaml @@ -27,6 +27,20 @@ extraAnnotations: {} # Labels globally added to all resources extraLabels: {} +images: + django: + image: + registry: "" + repository: defectdojo/defectdojo-django + tag: "" # If empty, use appVersion + digest: "" + nginx: + image: + registry: "" + repository: defectdojo/defectdojo-nginx + tag: "" # If empty, use appVersion + digest: "" + # Enables application network policy # For more info follow https://kubernetes.io/docs/concepts/services-networking/network-policies/ networkPolicy: @@ -73,13 +87,10 @@ siteUrl: "" alternativeHosts: [] # - defectdojo.example.com imagePullPolicy: Always -# Where to pull the defectDojo images from. Defaults to "defectdojo/*" repositories on hub.docker.com -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/' # @schema type:[string, null] imagePullSecrets: ~ -tag: latest # Additional labels to add to the pods: # podLabels: @@ -104,6 +115,11 @@ serviceAccount: labels: {} dbMigrationChecker: + image: # If empty, uses values from images.django.image + registry: "" + repository: "" + tag: "" + digest: "" # Enable/disable the DB migration checker. enabled: true # Container security context for the DB migration checker. @@ -123,6 +139,11 @@ dbMigrationChecker: tests: unitTests: + image: # If empty, uses values from images.django.image + registry: "" + repository: "" + tag: "" + digest: "" automountServiceAccountToken: false resources: requests: @@ -147,7 +168,11 @@ monitoring: prometheus: # Add the nginx prometheus exporter sidecar enabled: false - image: nginx/nginx-prometheus-exporter:1.4.2 + image: + registry: "" + repository: nginx/nginx-prometheus-exporter + tag: "1.4.2" + digest: "" imagePullPolicy: IfNotPresent # Optional: container security context for nginx prometheus exporter containerSecurityContext: {} @@ -169,6 +194,11 @@ celery: # Common annotations to worker and beat deployments and pods. annotations: {} beat: + image: # If empty, uses values from images.django.image + registry: "" + repository: "" + tag: "" + digest: "" automountServiceAccountToken: false # Annotations for the Celery beat deployment. annotations: {} @@ -213,6 +243,11 @@ celery: startupProbe: {} tolerations: [] worker: + image: # If empty, uses values from images.django.image + registry: "" + repository: "" + tag: "" + digest: "" automountServiceAccountToken: false # Annotations for the Celery worker deployment. annotations: {} @@ -288,6 +323,11 @@ django: # nginx.ingress.kubernetes.io/proxy-read-timeout: "1800" # nginx.ingress.kubernetes.io/proxy-send-timeout: "1800" nginx: + image: # If empty, uses values from images.nginx.image + registry: "" + repository: "" + tag: "" + digest: "" # Container security context for the nginx containers. containerSecurityContext: # nginx dockerfile sets USER=1001 @@ -317,6 +357,11 @@ django: strategy: {} tolerations: [] uwsgi: + image: # If empty, uses values from images.django.image + registry: "" + repository: "" + tag: "" + digest: "" containerSecurityContext: # django dockerfile sets USER=1001 runAsUser: 1001 @@ -413,6 +458,11 @@ initializer: affinity: {} nodeSelector: {} tolerations: [] + image: # If empty, uses values from images.django.image + registry: "" + repository: "" + tag: "" + digest: "" resources: requests: cpu: 100m From a1a497515f47b3a35a5775390fe2212d6000756a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 16 Oct 2025 20:34:07 -0600 Subject: [PATCH 050/126] Bump cryptography from 46.0.2 to 46.0.3 (#13431) Bumps [cryptography](https://github.com/pyca/cryptography) from 46.0.2 to 46.0.3. - [Changelog](https://github.com/pyca/cryptography/blob/main/CHANGELOG.rst) - [Commits](https://github.com/pyca/cryptography/compare/46.0.2...46.0.3) --- updated-dependencies: - dependency-name: cryptography dependency-version: 46.0.3 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 09d6166ce32..5d91f3d9735 100644 --- a/requirements.txt +++ b/requirements.txt @@ -29,7 +29,7 @@ Markdown==3.9 openpyxl==3.1.5 Pillow==11.3.0 # required by django-imagekit psycopg[c]==3.2.10 -cryptography==46.0.2 +cryptography==46.0.3 python-dateutil==2.9.0.post0 redis==6.4.0 requests==2.32.5 From 109c15e20599b51f44c4dd24304e28e29d5e79eb Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 16 Oct 2025 20:35:07 -0600 Subject: [PATCH 051/126] Bump boto3 from 1.40.52 to 1.40.53 (#13432) Bumps [boto3](https://github.com/boto/boto3) from 1.40.52 to 1.40.53. - [Release notes](https://github.com/boto/boto3/releases) - [Commits](https://github.com/boto/boto3/compare/1.40.52...1.40.53) --- updated-dependencies: - dependency-name: boto3 dependency-version: 1.40.53 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 5d91f3d9735..5c681bf77d9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -60,7 +60,7 @@ django-ratelimit==4.1.0 argon2-cffi==25.1.0 blackduck==1.1.3 pycurl==7.45.7 # Required for Celery Broker AWS (SQS) support -boto3==1.40.52 # Required for Celery Broker AWS (SQS) support +boto3==1.40.53 # Required for Celery Broker AWS (SQS) support netaddr==1.3.0 vulners==3.1.1 fontawesomefree==6.6.0 From a99c9bc869fd2753fc18fa02847054be5176bd95 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 16 Oct 2025 20:35:30 -0600 Subject: [PATCH 052/126] Bump humanize from 4.13.0 to 4.14.0 (#13433) Bumps [humanize](https://github.com/python-humanize/humanize) from 4.13.0 to 4.14.0. - [Release notes](https://github.com/python-humanize/humanize/releases) - [Commits](https://github.com/python-humanize/humanize/compare/4.13.0...4.14.0) --- updated-dependencies: - dependency-name: humanize dependency-version: 4.14.0 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 5c681bf77d9..b5451744054 100644 --- a/requirements.txt +++ b/requirements.txt @@ -21,7 +21,7 @@ django-prometheus==2.4.1 Django==5.1.13 djangorestframework==3.16.1 html2text==2025.4.15 -humanize==4.13.0 +humanize==4.14.0 jira==3.10.5 PyGithub==2.8.1 lxml==6.0.2 From bc177bdaa29c39b32e76ed9df308be4e5aba0141 Mon Sep 17 00:00:00 2001 From: valentijnscholten Date: Fri, 17 Oct 2025 04:39:41 +0200 Subject: [PATCH 053/126] Downgrade django-tagulous to 2.1.0 (#13441) Downgrade django-tagulous to 2.1.0 due to compatibility issues. --- requirements.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index b5451744054..5952204a610 100644 --- a/requirements.txt +++ b/requirements.txt @@ -48,7 +48,8 @@ packageurl-python==0.17.5 django-crum==0.7.9 JSON-log-formatter==1.1.1 django-split-settings==1.3.2 -django-tagulous==2.1.1 +# do not upgrade to 2.1.1 - https://github.com/DefectDojo/django-DefectDojo/issues/12918 +django-tagulous==2.1.0 PyJWT==2.10.1 cvss==3.6 django-fieldsignals==0.7.0 From 36e059eb6dac591f93294a15f2180f9bdda95139 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 16 Oct 2025 20:53:36 -0600 Subject: [PATCH 054/126] Bump pillow from 11.3.0 to 12.0.0 (#13434) Bumps [pillow](https://github.com/python-pillow/Pillow) from 11.3.0 to 12.0.0. - [Release notes](https://github.com/python-pillow/Pillow/releases) - [Changelog](https://github.com/python-pillow/Pillow/blob/main/CHANGES.rst) - [Commits](https://github.com/python-pillow/Pillow/compare/11.3.0...12.0.0) --- updated-dependencies: - dependency-name: pillow dependency-version: 12.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 5952204a610..57eaef7df3e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -27,7 +27,7 @@ PyGithub==2.8.1 lxml==6.0.2 Markdown==3.9 openpyxl==3.1.5 -Pillow==11.3.0 # required by django-imagekit +Pillow==12.0.0 # required by django-imagekit psycopg[c]==3.2.10 cryptography==46.0.3 python-dateutil==2.9.0.post0 From 13dd919732a1fc4e84fa882b3f17ea43df1e2a53 Mon Sep 17 00:00:00 2001 From: kiblik <5609770+kiblik@users.noreply.github.com> Date: Fri, 17 Oct 2025 16:37:12 +0000 Subject: [PATCH 055/126] feat(session): Single user session (#13416) --- dojo/settings/settings.dist.py | 10 ++++++++++ requirements.txt | 1 + 2 files changed, 11 insertions(+) diff --git a/dojo/settings/settings.dist.py b/dojo/settings/settings.dist.py index 3f6e59df3eb..8a17cb93aa8 100644 --- a/dojo/settings/settings.dist.py +++ b/dojo/settings/settings.dist.py @@ -214,6 +214,8 @@ # `RemoteUser` is usually used behind AuthN proxy and users should not know about this mechanism from Swagger because it is not usable by users. # It should be hidden by default. DD_AUTH_REMOTEUSER_VISIBLE_IN_SWAGGER=(bool, False), + # Some security policies require allowing users to have only one active session + DD_SINGLE_USER_SESSION=(bool, False), # if somebody is using own documentation how to use DefectDojo in his own company DD_DOCUMENTATION_URL=(str, "https://documentation.defectdojo.com"), # merging findings doesn't always work well with dedupe and reimport etc. @@ -919,6 +921,7 @@ def generate_url(scheme, double_slashes, user, password, host, port, path, param "auditlog", "pgtrigger", "pghistory", + "single_session", ) # ------------------------------------------------------------------------------ @@ -1149,6 +1152,13 @@ def saml2_attrib_map_format(din): ("dojo.remote_user.RemoteUserAuthentication",) + \ REST_FRAMEWORK["DEFAULT_AUTHENTICATION_CLASSES"] +# ------------------------------------------------------------------------------ +# SINGLE_USER_SESSION +# ------------------------------------------------------------------------------ + +SESSION_ENGINE = "django.contrib.sessions.backends.db" +SINGLE_USER_SESSION = env("DD_SINGLE_USER_SESSION") + # ------------------------------------------------------------------------------ # CELERY # ------------------------------------------------------------------------------ diff --git a/requirements.txt b/requirements.txt index 57eaef7df3e..c1304f0d033 100644 --- a/requirements.txt +++ b/requirements.txt @@ -19,6 +19,7 @@ django-slack==5.19.0 django-watson==1.6.3 django-prometheus==2.4.1 Django==5.1.13 +django-single-session==0.2.0 djangorestframework==3.16.1 html2text==2025.4.15 humanize==4.14.0 From 4e1b4c632075babdc0b48de1581fb907e99d3da7 Mon Sep 17 00:00:00 2001 From: kiblik <5609770+kiblik@users.noreply.github.com> Date: Fri, 17 Oct 2025 16:42:41 +0000 Subject: [PATCH 056/126] Ruff: Fix PLC2701 + merge PLC (#13436) --- dojo/api_v2/prefetch/schema.py | 4 ++-- dojo/api_v2/prefetch/utils.py | 2 +- dojo/endpoint/utils.py | 2 +- dojo/models.py | 18 +++++++++--------- dojo/tools/nexpose/parser.py | 2 +- dojo/tools/tenable/xml_format.py | 2 +- ruff.toml | 2 +- unittests/test_deduplication_logic.py | 8 ++++---- unittests/test_duplication_loops.py | 10 +++++----- unittests/test_false_positive_history_logic.py | 10 +++++----- unittests/test_rest_framework.py | 6 +++--- unittests/test_utils_deduplication_reopen.py | 10 +++++----- 12 files changed, 38 insertions(+), 38 deletions(-) diff --git a/dojo/api_v2/prefetch/schema.py b/dojo/api_v2/prefetch/schema.py index 21791f8daab..86078a86317 100644 --- a/dojo/api_v2/prefetch/schema.py +++ b/dojo/api_v2/prefetch/schema.py @@ -1,5 +1,5 @@ from .prefetcher import _Prefetcher -from .utils import _get_prefetchable_fields +from .utils import get_prefetchable_fields def _get_path_to_GET_serializer_map(generator): @@ -53,7 +53,7 @@ def prefetch_postprocessing_hook(result, generator, request, public): if parameter["name"] == "prefetch": prefetcher = _Prefetcher() - fields = _get_prefetchable_fields( + fields = get_prefetchable_fields( serializer_classes[path](), ) diff --git a/dojo/api_v2/prefetch/utils.py b/dojo/api_v2/prefetch/utils.py index eefb1b642ec..2c2546f9e03 100644 --- a/dojo/api_v2/prefetch/utils.py +++ b/dojo/api_v2/prefetch/utils.py @@ -33,7 +33,7 @@ def _is_one_to_one_relation(field): return isinstance(field, related.ForwardManyToOneDescriptor) -def _get_prefetchable_fields(serializer): +def get_prefetchable_fields(serializer): """ Get the fields that are prefetchable according to the serializer description. Method mainly used by for automatic schema generation. diff --git a/dojo/endpoint/utils.py b/dojo/endpoint/utils.py index 10646ba265c..2cc835aa974 100644 --- a/dojo/endpoint/utils.py +++ b/dojo/endpoint/utils.py @@ -10,7 +10,7 @@ from django.db.models import Count, Q from django.http import HttpResponseRedirect from django.urls import reverse -from hyperlink._url import SCHEME_PORT_MAP +from hyperlink._url import SCHEME_PORT_MAP # noqa: PLC2701 from dojo.models import DojoMeta, Endpoint diff --git a/dojo/models.py b/dojo/models.py index 741f630fb92..d5b672c53c7 100644 --- a/dojo/models.py +++ b/dojo/models.py @@ -129,7 +129,7 @@ def _manage_inherited_tags(obj, incoming_inherited_tags, potentially_existing_ta obj.tags.set(cleaned_tag_list) -def _copy_model_util(model_in_database, exclude_fields: list[str] | None = None): +def copy_model_util(model_in_database, exclude_fields: list[str] | None = None): if exclude_fields is None: exclude_fields = [] new_model_instance = model_in_database.__class__() @@ -750,7 +750,7 @@ class NoteHistory(models.Model): current_editor = models.ForeignKey(Dojo_User, editable=False, null=True, on_delete=models.CASCADE) def copy(self): - copy = _copy_model_util(self) + copy = copy_model_util(self) copy.save() return copy @@ -776,7 +776,7 @@ def __str__(self): return self.entry def copy(self): - copy = _copy_model_util(self) + copy = copy_model_util(self) # Save the necessary ManyToMany relationships old_history = list(self.history.all()) # Save the object before setting any ManyToMany relationships @@ -801,7 +801,7 @@ def delete(self, *args, **kwargs): storage.delete(path) def copy(self): - copy = _copy_model_util(self) + copy = copy_model_util(self) # Add unique modifier to file name copy.title = f"{self.title} - clone-{str(uuid4())[:8]}" # Create new unique file name @@ -1581,7 +1581,7 @@ def get_absolute_url(self): return reverse("view_engagement", args=[str(self.id)]) def copy(self): - copy = _copy_model_util(self) + copy = copy_model_util(self) # Save the necessary ManyToMany relationships old_notes = list(self.notes.all()) old_files = list(self.files.all()) @@ -1699,7 +1699,7 @@ def __str__(self): return f"'{self.finding}' on '{self.endpoint}'" def copy(self, finding=None): - copy = _copy_model_util(self) + copy = copy_model_util(self) current_endpoint = self.endpoint if finding: copy.finding = finding @@ -2161,7 +2161,7 @@ def get_breadcrumbs(self): return bc def copy(self, engagement=None): - copy = _copy_model_util(self) + copy = copy_model_util(self) # Save the necessary ManyToMany relationships old_notes = list(self.notes.all()) old_files = list(self.files.all()) @@ -2827,7 +2827,7 @@ def get_absolute_url(self): return reverse("view_finding", args=[str(self.id)]) def copy(self, test=None): - copy = _copy_model_util(self) + copy = copy_model_util(self) # Save the necessary ManyToMany relationships old_notes = list(self.notes.all()) old_files = list(self.files.all()) @@ -3804,7 +3804,7 @@ def engagement(self): return None def copy(self, engagement=None): - copy = _copy_model_util(self) + copy = copy_model_util(self) # Save the necessary ManyToMany relationships old_notes = list(self.notes.all()) old_accepted_findings_hash_codes = [finding.hash_code for finding in self.accepted_findings.all()] diff --git a/dojo/tools/nexpose/parser.py b/dojo/tools/nexpose/parser.py index d7f197d2b21..9c03ba8f277 100644 --- a/dojo/tools/nexpose/parser.py +++ b/dojo/tools/nexpose/parser.py @@ -4,7 +4,7 @@ import html2text from defusedxml import ElementTree from django.conf import settings -from hyperlink._url import SCHEME_PORT_MAP +from hyperlink._url import SCHEME_PORT_MAP # noqa: PLC2701 from dojo.models import Endpoint, Finding diff --git a/dojo/tools/tenable/xml_format.py b/dojo/tools/tenable/xml_format.py index 53f82a440ac..7bbf36baa66 100644 --- a/dojo/tools/tenable/xml_format.py +++ b/dojo/tools/tenable/xml_format.py @@ -3,7 +3,7 @@ from cvss import CVSS3 from defusedxml import ElementTree -from hyperlink._url import SCHEME_PORT_MAP +from hyperlink._url import SCHEME_PORT_MAP # noqa: PLC2701 from dojo.models import Endpoint, Finding, Test diff --git a/ruff.toml b/ruff.toml index 092bd58fee0..713ca372a66 100644 --- a/ruff.toml +++ b/ruff.toml @@ -82,7 +82,7 @@ select = [ "D2", "D3", "D402", "D403", "D405", "D406", "D407", "D408", "D409", "D410", "D411", "D412", "D413", "D414", "D416", "F", "PGH", - "PLC0", "PLC1", "PLC24", "PLC28", "PLC3", + "PLC", "PLE", "PLR01", "PLR02", "PLR04", "PLR0915", "PLR1711", "PLR1704", "PLR1714", "PLR1716", "PLR172", "PLR173", "PLR2044", "PLR5", "PLR6104", "PLR6201", "PLW", diff --git a/unittests/test_deduplication_logic.py b/unittests/test_deduplication_logic.py index 82ecfb177dd..9ea9ba713a4 100644 --- a/unittests/test_deduplication_logic.py +++ b/unittests/test_deduplication_logic.py @@ -13,7 +13,7 @@ System_Settings, Test, User, - _copy_model_util, + copy_model_util, ) from .dojo_test_case import DojoTestCase @@ -1199,7 +1199,7 @@ def log_summary(self, product=None, engagement=None, test=None): def copy_and_reset_finding(self, find_id): org = Finding.objects.get(id=find_id) - new = _copy_model_util(org) + new = copy_model_util(org) new.duplicate = False new.duplicate_finding = None new.active = True @@ -1236,13 +1236,13 @@ def copy_and_reset_finding_add_endpoints(self, find_id, *, static=False, dynamic def copy_and_reset_test(self, test_id): org = Test.objects.get(id=test_id) - new = _copy_model_util(org) + new = copy_model_util(org) # return unsaved new finding and reloaded existing finding return new, Test.objects.get(id=test_id) def copy_and_reset_engagement(self, eng_id): org = Engagement.objects.get(id=eng_id) - new = _copy_model_util(org) + new = copy_model_util(org) # return unsaved new finding and reloaded existing finding return new, Engagement.objects.get(id=eng_id) diff --git a/unittests/test_duplication_loops.py b/unittests/test_duplication_loops.py index cc0d250774e..d85e52e1046 100644 --- a/unittests/test_duplication_loops.py +++ b/unittests/test_duplication_loops.py @@ -4,7 +4,7 @@ from django.test.utils import override_settings from dojo.management.commands.fix_loop_duplicates import fix_loop_duplicates -from dojo.models import Engagement, Finding, Product, User, _copy_model_util +from dojo.models import Engagement, Finding, Product, User, copy_model_util from dojo.utils import set_duplicate from .dojo_test_case import DojoTestCase @@ -27,19 +27,19 @@ def run(self, result=None): super().run(result) def setUp(self): - self.finding_a = _copy_model_util(Finding.objects.get(id=2), exclude_fields=["duplicate_finding"]) + self.finding_a = copy_model_util(Finding.objects.get(id=2), exclude_fields=["duplicate_finding"]) self.finding_a.title = "A: " + self.finding_a.title self.finding_a.duplicate = False self.finding_a.hash_code = None self.finding_a.save() - self.finding_b = _copy_model_util(Finding.objects.get(id=3), exclude_fields=["duplicate_finding"]) + self.finding_b = copy_model_util(Finding.objects.get(id=3), exclude_fields=["duplicate_finding"]) self.finding_b.title = "B: " + self.finding_b.title self.finding_b.duplicate = False self.finding_b.hash_code = None self.finding_b.save() - self.finding_c = _copy_model_util(Finding.objects.get(id=4), exclude_fields=["duplicate_finding"]) + self.finding_c = copy_model_util(Finding.objects.get(id=4), exclude_fields=["duplicate_finding"]) self.finding_c.pk = None self.finding_c.title = "C: " + self.finding_c.title self.finding_c.duplicate = False @@ -262,7 +262,7 @@ def test_loop_relations_for_three(self): # Another loop-test for 4 findings def test_loop_relations_for_four(self): - self.finding_d = _copy_model_util(Finding.objects.get(id=4), exclude_fields=["duplicate_finding"]) + self.finding_d = copy_model_util(Finding.objects.get(id=4), exclude_fields=["duplicate_finding"]) self.finding_d.duplicate = False self.finding_d.save() diff --git a/unittests/test_false_positive_history_logic.py b/unittests/test_false_positive_history_logic.py index 4a380383f7d..5d8f31a15de 100644 --- a/unittests/test_false_positive_history_logic.py +++ b/unittests/test_false_positive_history_logic.py @@ -12,7 +12,7 @@ System_Settings, Test, User, - _copy_model_util, + copy_model_util, ) from .dojo_test_case import DojoTestCase @@ -1719,7 +1719,7 @@ def log_summary(self, product=None, engagement=None, test=None): def copy_and_reset_finding(self, find_id): org = Finding.objects.get(id=find_id) - new = _copy_model_util(org) + new = copy_model_util(org) new.duplicate = False new.duplicate_finding = None new.false_p = False @@ -1730,19 +1730,19 @@ def copy_and_reset_finding(self, find_id): def copy_and_reset_test(self, test_id): org = Test.objects.get(id=test_id) - new = _copy_model_util(org) + new = copy_model_util(org) # return unsaved new test and reloaded existing test return new, Test.objects.get(id=test_id) def copy_and_reset_engagement(self, eng_id): org = Engagement.objects.get(id=eng_id) - new = _copy_model_util(org) + new = copy_model_util(org) # return unsaved new engagement and reloaded existing engagement return new, Engagement.objects.get(id=eng_id) def copy_and_reset_product(self, prod_id): org = Product.objects.get(id=prod_id) - new = _copy_model_util(org) + new = copy_model_util(org) new.name = f"{org.name} (Copy {datetime.now()})" # return unsaved new product and reloaded existing product return new, Product.objects.get(id=prod_id) diff --git a/unittests/test_rest_framework.py b/unittests/test_rest_framework.py index 4f5120792d4..f32350e2e86 100644 --- a/unittests/test_rest_framework.py +++ b/unittests/test_rest_framework.py @@ -34,7 +34,7 @@ from dojo.api_v2.mixins import DeletePreviewModelMixin from dojo.api_v2.prefetch import PrefetchListMixin, PrefetchRetrieveMixin -from dojo.api_v2.prefetch.utils import _get_prefetchable_fields +from dojo.api_v2.prefetch.utils import get_prefetchable_fields from dojo.api_v2.views import ( AnnouncementViewSet, AppAnalysisViewSet, @@ -416,7 +416,7 @@ def test_detail(self): @skipIfNotSubclass(PrefetchRetrieveMixin) def test_detail_prefetch(self): # print("=======================================================") - prefetchable_fields = [x[0] for x in _get_prefetchable_fields(self.viewset.serializer_class)] + prefetchable_fields = [x[0] for x in get_prefetchable_fields(self.viewset.serializer_class)] current_objects = self.client.get(self.url, format="json").data relative_url = self.url + "{}/".format(current_objects["results"][0]["id"]) @@ -508,7 +508,7 @@ def test_list(self): @skipIfNotSubclass(PrefetchListMixin) def test_list_prefetch(self): - prefetchable_fields = [x[0] for x in _get_prefetchable_fields(self.viewset.serializer_class)] + prefetchable_fields = [x[0] for x in get_prefetchable_fields(self.viewset.serializer_class)] response = self.client.get(self.url, data={ "prefetch": ",".join(prefetchable_fields), diff --git a/unittests/test_utils_deduplication_reopen.py b/unittests/test_utils_deduplication_reopen.py index 91ba2c49d12..a7e72ede118 100644 --- a/unittests/test_utils_deduplication_reopen.py +++ b/unittests/test_utils_deduplication_reopen.py @@ -2,7 +2,7 @@ import logging from dojo.management.commands.fix_loop_duplicates import fix_loop_duplicates -from dojo.models import Finding, _copy_model_util +from dojo.models import Finding, copy_model_util from dojo.utils import set_duplicate from .dojo_test_case import DojoTestCase @@ -14,7 +14,7 @@ class TestDuplicationReopen(DojoTestCase): fixtures = ["dojo_testdata.json"] def setUp(self): - self.finding_a = _copy_model_util(Finding.objects.get(id=2), exclude_fields=["duplicate_finding"]) + self.finding_a = copy_model_util(Finding.objects.get(id=2), exclude_fields=["duplicate_finding"]) self.finding_a.duplicate = False self.finding_a.mitigated = datetime.datetime(1970, 1, 1, tzinfo=datetime.UTC) self.finding_a.is_mitigated = True @@ -22,19 +22,19 @@ def setUp(self): self.finding_a.active = False self.finding_a.save() - self.finding_b = _copy_model_util(Finding.objects.get(id=3), exclude_fields=["duplicate_finding"]) + self.finding_b = copy_model_util(Finding.objects.get(id=3), exclude_fields=["duplicate_finding"]) self.finding_a.active = True self.finding_b.duplicate = False self.finding_b.save() - self.finding_c = _copy_model_util(Finding.objects.get(id=4), exclude_fields=["duplicate_finding"]) + self.finding_c = copy_model_util(Finding.objects.get(id=4), exclude_fields=["duplicate_finding"]) self.finding_c.duplicate = False self.finding_c.out_of_scope = True self.finding_c.active = False logger.debug("creating finding_c") self.finding_c.save() - self.finding_d = _copy_model_util(Finding.objects.get(id=5), exclude_fields=["duplicate_finding"]) + self.finding_d = copy_model_util(Finding.objects.get(id=5), exclude_fields=["duplicate_finding"]) self.finding_d.duplicate = False logger.debug("creating finding_d") self.finding_d.save() From e27d99d77c372982ec2a86dcf5e77e5f060bc379 Mon Sep 17 00:00:00 2001 From: kiblik <5609770+kiblik@users.noreply.github.com> Date: Fri, 17 Oct 2025 16:42:58 +0000 Subject: [PATCH 057/126] ruff: Merge B01 rules (#13430) --- ruff.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ruff.toml b/ruff.toml index 713ca372a66..e169992b158 100644 --- a/ruff.toml +++ b/ruff.toml @@ -36,9 +36,9 @@ select = [ "FAST", "YTT", "ASYNC", - "S1", "S2", "S302", "S303", "S304", "S305", "S306", "S307", "S31", "S321", "S323", "S324", "S401", "S402", "S406", "S407", "S408", "S409", "S41", "S5", "S601", "S602", "S604", "S605", "S606", "S607", "S609", "S61", "S7", + "S1", "S2", "S302", "S303", "S304", "S305", "S306", "S307", "S31", "S32", "S401", "S402", "S406", "S407", "S408", "S409", "S41", "S5", "S601", "S602", "S604", "S605", "S606", "S607", "S609", "S61", "S7", "FBT", - "B00", "B010", "B011", "B012", "B013", "B014", "B015", "B016", "B017", "B018", "B019", "B020", "B021", "B022", "B023", "B025", "B028", "B029", "B03", "B901", "B903", "B905", "B911", + "B00", "B01", "B020", "B021", "B022", "B023", "B025", "B027", "B028", "B029", "B03", "B901", "B903", "B905", "B911", "A", "COM", "C4", From dec5a6311dfb6de05455c2348ff8d48dcb23220e Mon Sep 17 00:00:00 2001 From: rseleven <73898556+rseleven@users.noreply.github.com> Date: Fri, 17 Oct 2025 19:43:07 +0300 Subject: [PATCH 058/126] Added the definition of the SOCIAL_AUTH_LOGIN_REDIRECT_URL variable (#13428) Co-authored-by: rseleven --- dojo/settings/settings.dist.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/dojo/settings/settings.dist.py b/dojo/settings/settings.dist.py index 8a17cb93aa8..d67d289d929 100644 --- a/dojo/settings/settings.dist.py +++ b/dojo/settings/settings.dist.py @@ -624,6 +624,8 @@ def generate_url(scheme, double_slashes, user, password, host, port, path, param SOCIAL_AUTH_OIDC_KEY = env("DD_SOCIAL_AUTH_OIDC_KEY") SOCIAL_AUTH_OIDC_SECRET = env("DD_SOCIAL_AUTH_OIDC_SECRET") # Optional settings +if value := env("DD_LOGIN_REDIRECT_URL"): + SOCIAL_AUTH_LOGIN_REDIRECT_URL = value if value := env("DD_SOCIAL_AUTH_OIDC_ID_KEY"): SOCIAL_AUTH_OIDC_ID_KEY = value if value := env("DD_SOCIAL_AUTH_OIDC_USERNAME_KEY"): From 489936f97e9e578e951fea9c66722dbf1df8c20f Mon Sep 17 00:00:00 2001 From: valentijnscholten Date: Fri, 17 Oct 2025 18:44:16 +0200 Subject: [PATCH 059/126] auto_create_context: make engagement creation atomic (#13444) --- dojo/importers/auto_create_context.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/dojo/importers/auto_create_context.py b/dojo/importers/auto_create_context.py index bf4d16cee92..26d37ae65b0 100644 --- a/dojo/importers/auto_create_context.py +++ b/dojo/importers/auto_create_context.py @@ -318,10 +318,16 @@ def get_or_create_engagement( target_end = (timezone.now() + timedelta(days=365)).date() # Create the engagement with transaction.atomic(): - return Engagement.objects.select_for_update().create( + # Lock the parent product row to serialize engagement creation per product + locked_product = Product.objects.select_for_update().get(pk=product.pk) + # Re-check for an existing engagement now that we hold the lock + existing = get_last_object_or_none(Engagement, product=locked_product, name=engagement_name) + if existing: + return existing + return Engagement.objects.create( engagement_type="CI/CD", name=engagement_name, - product=product, + product=locked_product, lead=get_current_user(), target_start=target_start, target_end=target_end, From 685ce4abda09da310c26c32044513797fca2ae0a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 17 Oct 2025 11:44:50 -0500 Subject: [PATCH 060/126] Bump ruff from 0.14.0 to 0.14.1 (#13452) Bumps [ruff](https://github.com/astral-sh/ruff) from 0.14.0 to 0.14.1. - [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.14.0...0.14.1) --- updated-dependencies: - dependency-name: ruff dependency-version: 0.14.1 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 3a9145e0960..59e562b9654 100644 --- a/requirements-lint.txt +++ b/requirements-lint.txt @@ -1 +1 @@ -ruff==0.14.0 +ruff==0.14.1 From 922757b94a207c2fd2c97e776ee7855d7e82870d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 17 Oct 2025 11:47:54 -0500 Subject: [PATCH 061/126] Bump boto3 from 1.40.53 to 1.40.54 (#13450) Bumps [boto3](https://github.com/boto/boto3) from 1.40.53 to 1.40.54. - [Release notes](https://github.com/boto/boto3/releases) - [Commits](https://github.com/boto/boto3/compare/1.40.53...1.40.54) --- updated-dependencies: - dependency-name: boto3 dependency-version: 1.40.54 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 c1304f0d033..daff55f0c21 100644 --- a/requirements.txt +++ b/requirements.txt @@ -62,7 +62,7 @@ django-ratelimit==4.1.0 argon2-cffi==25.1.0 blackduck==1.1.3 pycurl==7.45.7 # Required for Celery Broker AWS (SQS) support -boto3==1.40.53 # Required for Celery Broker AWS (SQS) support +boto3==1.40.54 # Required for Celery Broker AWS (SQS) support netaddr==1.3.0 vulners==3.1.1 fontawesomefree==6.6.0 From 47ac9339f8847284aefa2514a5e12380743ce85d Mon Sep 17 00:00:00 2001 From: kiblik <5609770+kiblik@users.noreply.github.com> Date: Fri, 17 Oct 2025 21:53:46 +0000 Subject: [PATCH 062/126] feat(helm): Add descriptions (#13407) Co-authored-by: Ross E Esposito --- helm/defectdojo/Chart.yaml | 2 + helm/defectdojo/README.md | 268 ++++++++++------------- helm/defectdojo/values.schema.json | 121 ++++++++++ helm/defectdojo/values.yaml | 339 ++++++++++++++++------------- 4 files changed, 426 insertions(+), 304 deletions(-) diff --git a/helm/defectdojo/Chart.yaml b/helm/defectdojo/Chart.yaml index 942547c7c2f..6989b44333a 100644 --- a/helm/defectdojo/Chart.yaml +++ b/helm/defectdojo/Chart.yaml @@ -24,5 +24,7 @@ annotations: description: DRY cloudsql-proxy - kind: changed description: Each component allow to specific image + allow digest pinning + - kind: added + description: Convert existing comments to descriptors - kind: added description: Testing on the oldest officially supported k8s diff --git a/helm/defectdojo/README.md b/helm/defectdojo/README.md index 03585f6d60d..4b4b062b89b 100644 --- a/helm/defectdojo/README.md +++ b/helm/defectdojo/README.md @@ -524,111 +524,105 @@ A Helm chart for Kubernetes to install DefectDojo | admin.password | string | `""` | | | admin.secretKey | string | `""` | | | admin.user | string | `"admin"` | | -| alternativeHosts | list | `[]` | | -| celery.annotations | object | `{}` | | +| alternativeHosts | list | `[]` | optional list of alternative hostnames to use that gets appended to DD_ALLOWED_HOSTS. This is necessary when your local hostname does not match the global hostname. | +| celery.annotations | object | `{}` | Common annotations to worker and beat deployments and pods. | | celery.beat.affinity | object | `{}` | | -| celery.beat.annotations | object | `{}` | | +| celery.beat.annotations | object | `{}` | Annotations for the Celery beat deployment. | | celery.beat.automountServiceAccountToken | bool | `false` | | -| celery.beat.containerSecurityContext | object | `{}` | | -| celery.beat.extraEnv | list | `[]` | | -| celery.beat.extraInitContainers | list | `[]` | | -| celery.beat.extraVolumeMounts | list | `[]` | | -| celery.beat.extraVolumes | list | `[]` | | +| celery.beat.containerSecurityContext | object | `{}` | Container security context for the Celery beat containers. | +| celery.beat.extraEnv | list | `[]` | Additional environment variables injected to Celery beat containers. | +| celery.beat.extraInitContainers | list | `[]` | A list of additional initContainers to run before celery beat containers. | +| celery.beat.extraVolumeMounts | list | `[]` | Array of additional volume mount points for the celery beat containers. | +| celery.beat.extraVolumes | list | `[]` | A list of extra volumes to mount @type: array | | celery.beat.image.digest | string | `""` | | | celery.beat.image.registry | string | `""` | | | celery.beat.image.repository | string | `""` | | | celery.beat.image.tag | string | `""` | | -| celery.beat.livenessProbe | object | `{}` | | +| celery.beat.livenessProbe | object | `{}` | Enable liveness probe for Celery beat container. ``` exec: command: - bash - -c - celery -A dojo inspect ping -t 5 initialDelaySeconds: 30 periodSeconds: 60 timeoutSeconds: 10 ``` | | celery.beat.nodeSelector | object | `{}` | | -| celery.beat.podAnnotations | object | `{}` | | -| celery.beat.podSecurityContext | object | `{}` | | -| celery.beat.readinessProbe | object | `{}` | | +| celery.beat.podAnnotations | object | `{}` | Annotations for the Celery beat pods. | +| celery.beat.podSecurityContext | object | `{}` | Pod security context for the Celery beat pods. | +| celery.beat.readinessProbe | object | `{}` | Enable readiness probe for Celery beat container. | | celery.beat.replicas | int | `1` | | | celery.beat.resources.limits.cpu | string | `"2000m"` | | | celery.beat.resources.limits.memory | string | `"256Mi"` | | | celery.beat.resources.requests.cpu | string | `"100m"` | | | celery.beat.resources.requests.memory | string | `"128Mi"` | | -| celery.beat.startupProbe | object | `{}` | | +| celery.beat.startupProbe | object | `{}` | Enable startup probe for Celery beat container. | | celery.beat.tolerations | list | `[]` | | | celery.broker | string | `"redis"` | | | celery.logLevel | string | `"INFO"` | | | celery.worker.affinity | object | `{}` | | -| celery.worker.annotations | object | `{}` | | -| celery.worker.appSettings.poolType | string | `"solo"` | | +| celery.worker.annotations | object | `{}` | Annotations for the Celery worker deployment. | +| celery.worker.appSettings.poolType | string | `"solo"` | Performance improved celery worker config when needing to deal with a lot of findings (e.g deduplication ops) poolType: prefork autoscaleMin: 2 autoscaleMax: 8 concurrency: 8 prefetchMultiplier: 128 | | celery.worker.automountServiceAccountToken | bool | `false` | | -| celery.worker.containerSecurityContext | object | `{}` | | -| celery.worker.extraEnv | list | `[]` | | -| celery.worker.extraInitContainers | list | `[]` | | -| celery.worker.extraVolumeMounts | list | `[]` | | -| celery.worker.extraVolumes | list | `[]` | | +| celery.worker.containerSecurityContext | object | `{}` | Container security context for the Celery worker containers. | +| celery.worker.extraEnv | list | `[]` | Additional environment variables injected to Celery worker containers. | +| celery.worker.extraInitContainers | list | `[]` | A list of additional initContainers to run before celery worker containers. | +| celery.worker.extraVolumeMounts | list | `[]` | Array of additional volume mount points for the celery worker containers. | +| celery.worker.extraVolumes | list | `[]` | A list of extra volumes to mount. @type: array | | celery.worker.image.digest | string | `""` | | | celery.worker.image.registry | string | `""` | | | celery.worker.image.repository | string | `""` | | | celery.worker.image.tag | string | `""` | | -| celery.worker.livenessProbe | object | `{}` | | +| celery.worker.livenessProbe | object | `{}` | Enable liveness probe for Celery worker containers. ``` exec: command: - bash - -c - celery -A dojo inspect ping -t 5 initialDelaySeconds: 30 periodSeconds: 60 timeoutSeconds: 10 ``` | | celery.worker.nodeSelector | object | `{}` | | -| celery.worker.podAnnotations | object | `{}` | | -| celery.worker.podSecurityContext | object | `{}` | | -| celery.worker.readinessProbe | object | `{}` | | +| celery.worker.podAnnotations | object | `{}` | Annotations for the Celery beat pods. | +| celery.worker.podSecurityContext | object | `{}` | Pod security context for the Celery worker pods. | +| celery.worker.readinessProbe | object | `{}` | Enable readiness probe for Celery worker container. | | celery.worker.replicas | int | `1` | | | celery.worker.resources.limits.cpu | string | `"2000m"` | | | celery.worker.resources.limits.memory | string | `"512Mi"` | | | celery.worker.resources.requests.cpu | string | `"100m"` | | | celery.worker.resources.requests.memory | string | `"128Mi"` | | -| celery.worker.startupProbe | object | `{}` | | +| celery.worker.startupProbe | object | `{}` | Enable startup probe for Celery worker container. | | celery.worker.tolerations | list | `[]` | | -| cloudsql.containerSecurityContext | object | `{}` | | -| cloudsql.enable_iam_login | bool | `false` | | -| cloudsql.enabled | bool | `false` | | -| cloudsql.extraEnv | list | `[]` | | -| cloudsql.extraVolumeMounts | list | `[]` | | -| cloudsql.image.pullPolicy | string | `"IfNotPresent"` | | -| cloudsql.image.repository | string | `"gcr.io/cloudsql-docker/gce-proxy"` | | -| cloudsql.image.tag | string | `"1.37.9"` | | -| cloudsql.instance | string | `""` | | -| cloudsql.resources | object | `{}` | | -| cloudsql.use_private_ip | bool | `false` | | -| cloudsql.verbose | bool | `true` | | -| createPostgresqlSecret | bool | `false` | | -| createRedisSecret | bool | `false` | | -| createSecret | bool | `false` | | -| dbMigrationChecker.containerSecurityContext | object | `{}` | | -| dbMigrationChecker.enabled | bool | `true` | | -| dbMigrationChecker.extraEnv | list | `[]` | | -| dbMigrationChecker.extraVolumeMounts | list | `[]` | | +| cloudsql | object | `{"containerSecurityContext":{},"enable_iam_login":false,"enabled":false,"extraEnv":[],"extraVolumeMounts":[],"image":{"pullPolicy":"IfNotPresent","repository":"gcr.io/cloudsql-docker/gce-proxy","tag":"1.37.9"},"instance":"","resources":{},"use_private_ip":false,"verbose":true}` | Google CloudSQL support in GKE via gce-proxy | +| cloudsql.containerSecurityContext | object | `{}` | Optional: security context for the CloudSQL proxy container. | +| cloudsql.enable_iam_login | bool | `false` | use IAM database authentication | +| cloudsql.enabled | bool | `false` | To use CloudSQL in GKE set 'enable: true' | +| cloudsql.extraEnv | list | `[]` | Additional environment variables for the CloudSQL proxy container. | +| cloudsql.extraVolumeMounts | list | `[]` | Array of additional volume mount points for the CloudSQL proxy container | +| cloudsql.image | object | `{"pullPolicy":"IfNotPresent","repository":"gcr.io/cloudsql-docker/gce-proxy","tag":"1.37.9"}` | set repo and image tag of gce-proxy | +| cloudsql.instance | string | `""` | set CloudSQL instance: 'project:zone:instancename' | +| cloudsql.resources | object | `{}` | Optional: add resource requests/limits for the CloudSQL proxy container. | +| cloudsql.use_private_ip | bool | `false` | whether to use a private IP to connect to the database | +| cloudsql.verbose | bool | `true` | By default, the proxy has verbose logging. Set this to false to make it less verbose | +| createPostgresqlSecret | bool | `false` | create postgresql secret in defectdojo chart, outside of postgresql chart | +| createRedisSecret | bool | `false` | create redis secret in defectdojo chart, outside of redis chart | +| createSecret | bool | `false` | create defectdojo specific secret | +| dbMigrationChecker.containerSecurityContext | object | `{}` | Container security context for the DB migration checker. | +| dbMigrationChecker.enabled | bool | `true` | Enable/disable the DB migration checker. | +| dbMigrationChecker.extraEnv | list | `[]` | Additional environment variables for DB migration checker. | +| dbMigrationChecker.extraVolumeMounts | list | `[]` | Array of additional volume mount points for DB migration checker. | | dbMigrationChecker.image.digest | string | `""` | | | dbMigrationChecker.image.registry | string | `""` | | | dbMigrationChecker.image.repository | string | `""` | | | dbMigrationChecker.image.tag | string | `""` | | -| dbMigrationChecker.resources.limits.cpu | string | `"200m"` | | -| dbMigrationChecker.resources.limits.memory | string | `"200Mi"` | | -| dbMigrationChecker.resources.requests.cpu | string | `"100m"` | | -| dbMigrationChecker.resources.requests.memory | string | `"100Mi"` | | -| disableHooks | bool | `false` | | +| dbMigrationChecker.resources | object | `{"limits":{"cpu":"200m","memory":"200Mi"},"requests":{"cpu":"100m","memory":"100Mi"}}` | Resource requests/limits for the DB migration checker. | +| disableHooks | bool | `false` | Avoid using pre-install hooks, which might cause issues with ArgoCD | | django.affinity | object | `{}` | | | django.annotations | object | `{}` | | | django.automountServiceAccountToken | bool | `false` | | -| django.extraEnv | list | `[]` | | -| django.extraInitContainers | list | `[]` | | -| django.extraVolumeMounts | list | `[]` | | -| django.extraVolumes | list | `[]` | | +| django.extraEnv | list | `[]` | Additional environment variables injected to all Django containers and initContainers. | +| django.extraInitContainers | list | `[]` | A list of additional initContainers to run before the uwsgi and nginx containers. | +| django.extraVolumeMounts | list | `[]` | Array of additional volume mount points common to all containers and initContainers. | +| django.extraVolumes | list | `[]` | A list of extra volumes to mount. | | django.ingress.activateTLS | bool | `true` | | -| django.ingress.annotations | object | `{}` | | +| django.ingress.annotations | object | `{}` | Restricts the type of ingress controller that can interact with our chart (nginx, traefik, ...) `kubernetes.io/ingress.class: nginx` Depending on the size and complexity of your scans, you might want to increase the default ingress timeouts if you see repeated 504 Gateway Timeouts `nginx.ingress.kubernetes.io/proxy-read-timeout: "1800"` `nginx.ingress.kubernetes.io/proxy-send-timeout: "1800"` | | django.ingress.enabled | bool | `true` | | | django.ingress.ingressClassName | string | `""` | | | django.ingress.secretName | string | `"defectdojo-tls"` | | -| django.mediaPersistentVolume.enabled | bool | `true` | | -| django.mediaPersistentVolume.fsGroup | int | `1001` | | -| django.mediaPersistentVolume.name | string | `"media"` | | -| django.mediaPersistentVolume.persistentVolumeClaim.accessModes[0] | string | `"ReadWriteMany"` | | -| django.mediaPersistentVolume.persistentVolumeClaim.create | bool | `false` | | -| django.mediaPersistentVolume.persistentVolumeClaim.name | string | `""` | | -| django.mediaPersistentVolume.persistentVolumeClaim.size | string | `"5Gi"` | | -| django.mediaPersistentVolume.persistentVolumeClaim.storageClassName | string | `""` | | -| django.mediaPersistentVolume.type | string | `"emptyDir"` | | -| django.nginx.containerSecurityContext.runAsUser | int | `1001` | | -| django.nginx.extraEnv | list | `[]` | | -| django.nginx.extraVolumeMounts | list | `[]` | | +| django.mediaPersistentVolume | object | `{"enabled":true,"fsGroup":1001,"name":"media","persistentVolumeClaim":{"accessModes":["ReadWriteMany"],"create":false,"name":"","size":"5Gi","storageClassName":""},"type":"emptyDir"}` | This feature needs more preparation before can be enabled, please visit KUBERNETES.md#media-persistent-volume | +| django.mediaPersistentVolume.name | string | `"media"` | any name | +| django.mediaPersistentVolume.persistentVolumeClaim | object | `{"accessModes":["ReadWriteMany"],"create":false,"name":"","size":"5Gi","storageClassName":""}` | in case if pvc specified, should point to the already existing pvc | +| django.mediaPersistentVolume.persistentVolumeClaim.accessModes | list | `["ReadWriteMany"]` | check KUBERNETES.md doc first for option to choose | +| django.mediaPersistentVolume.persistentVolumeClaim.create | bool | `false` | set to true to create a new pvc and if django.mediaPersistentVolume.type is set to pvc | +| django.mediaPersistentVolume.type | string | `"emptyDir"` | could be emptyDir (not for production) or pvc | +| django.nginx.containerSecurityContext | object | `{"runAsUser":1001}` | Container security context for the nginx containers. | +| django.nginx.containerSecurityContext.runAsUser | int | `1001` | nginx dockerfile sets USER=1001 | +| django.nginx.extraEnv | list | `[]` | To extra environment variables to the nginx container, you can use extraEnv. For example: extraEnv: - name: FOO valueFrom: configMapKeyRef: name: foo key: bar | +| django.nginx.extraVolumeMounts | list | `[]` | Array of additional volume mount points for nginx containers. | | django.nginx.image.digest | string | `""` | | | django.nginx.image.registry | string | `""` | | | django.nginx.image.repository | string | `""` | | @@ -640,34 +634,34 @@ A Helm chart for Kubernetes to install DefectDojo | django.nginx.tls.enabled | bool | `false` | | | django.nginx.tls.generateCertificate | bool | `false` | | | django.nodeSelector | object | `{}` | | -| django.podSecurityContext.fsGroup | int | `1001` | | +| django.podSecurityContext | object | `{"fsGroup":1001}` | Pod security context for the Django pods. | | django.replicas | int | `1` | | | django.service.annotations | object | `{}` | | | django.service.type | string | `""` | | | django.strategy | object | `{}` | | | django.tolerations | list | `[]` | | -| django.uwsgi.appSettings.maxFd | int | `0` | | +| django.uwsgi.appSettings.maxFd | int | `0` | Use this value to set the maximum number of file descriptors. If set to 0 will be detected by uwsgi e.g. 102400 | | django.uwsgi.appSettings.processes | int | `4` | | | django.uwsgi.appSettings.threads | int | `4` | | | django.uwsgi.certificates.certFileName | string | `"ca.crt"` | | | django.uwsgi.certificates.certMountPath | string | `"/certs/"` | | | django.uwsgi.certificates.configName | string | `"defectdojo-ca-certs"` | | -| django.uwsgi.certificates.enabled | bool | `false` | | -| django.uwsgi.containerSecurityContext.runAsUser | int | `1001` | | -| django.uwsgi.enableDebug | bool | `false` | | -| django.uwsgi.extraEnv | list | `[]` | | -| django.uwsgi.extraVolumeMounts | list | `[]` | | +| django.uwsgi.certificates.enabled | bool | `false` | 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` NOTE: it reflects REQUESTS_CA_BUNDLE for celery workers, beats as well | +| django.uwsgi.containerSecurityContext.runAsUser | int | `1001` | django dockerfile sets USER=1001 | +| django.uwsgi.enableDebug | bool | `false` | this also requires DD_DEBUG to be set to True | +| django.uwsgi.extraEnv | list | `[]` | 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 | +| django.uwsgi.extraVolumeMounts | list | `[]` | Array of additional volume mount points for uwsgi containers. | | django.uwsgi.image.digest | string | `""` | | | django.uwsgi.image.registry | string | `""` | | | django.uwsgi.image.repository | string | `""` | | | django.uwsgi.image.tag | string | `""` | | -| django.uwsgi.livenessProbe.enabled | bool | `true` | | +| django.uwsgi.livenessProbe.enabled | bool | `true` | Enable liveness checks on uwsgi container. | | django.uwsgi.livenessProbe.failureThreshold | int | `6` | | | django.uwsgi.livenessProbe.initialDelaySeconds | int | `0` | | | django.uwsgi.livenessProbe.periodSeconds | int | `10` | | | django.uwsgi.livenessProbe.successThreshold | int | `1` | | | django.uwsgi.livenessProbe.timeoutSeconds | int | `5` | | -| django.uwsgi.readinessProbe.enabled | bool | `true` | | +| django.uwsgi.readinessProbe.enabled | bool | `true` | Enable readiness checks on uwsgi container. | | django.uwsgi.readinessProbe.failureThreshold | int | `6` | | | django.uwsgi.readinessProbe.initialDelaySeconds | int | `0` | | | django.uwsgi.readinessProbe.periodSeconds | int | `10` | | @@ -677,23 +671,24 @@ A Helm chart for Kubernetes to install DefectDojo | django.uwsgi.resources.limits.memory | string | `"512Mi"` | | | django.uwsgi.resources.requests.cpu | string | `"100m"` | | | django.uwsgi.resources.requests.memory | string | `"256Mi"` | | -| django.uwsgi.startupProbe.enabled | bool | `true` | | +| django.uwsgi.startupProbe.enabled | bool | `true` | Enable startup checks on uwsgi container. | | django.uwsgi.startupProbe.failureThreshold | int | `30` | | | django.uwsgi.startupProbe.initialDelaySeconds | int | `0` | | | django.uwsgi.startupProbe.periodSeconds | int | `5` | | | django.uwsgi.startupProbe.successThreshold | int | `1` | | | django.uwsgi.startupProbe.timeoutSeconds | int | `1` | | -| extraAnnotations | object | `{}` | | -| extraConfigs | object | `{}` | | -| extraEnv | list | `[]` | | -| extraLabels | object | `{}` | | -| extraSecrets | object | `{}` | | -| gke.useGKEIngress | bool | `false` | | -| gke.useManagedCertificate | bool | `false` | | -| gke.workloadIdentityEmail | string | `""` | | -| host | string | `"defectdojo.default.minikube.local"` | | +| extraAnnotations | object | `{}` | Annotations globally added to all resources | +| extraConfigs | object | `{}` | 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 ``` DD_SOCIAL_AUTH_AUTH0_OAUTH2_ENABLED: 'true' DD_SOCIAL_AUTH_AUTH0_KEY: 'dev' DD_SOCIAL_AUTH_AUTH0_DOMAIN: 'xxxxx' ``` | +| extraEnv | list | `[]` | To add (or override) extra variables which need to be pulled from another configMap, you can use extraEnv. For example: ``` - name: DD_DATABASE_HOST valueFrom: configMapKeyRef: name: my-other-postgres-configmap key: cluster_endpoint ``` | +| extraLabels | object | `{}` | Labels globally added to all resources | +| extraSecrets | object | `{}` | Extra secrets can be created inside of extraSecrets block: NOTE This is just an exmaple, do not store sensitive data in plain text form, better inject it during the deployment/upgrade by --set extraSecrets.secret=someSecret ``` DD_SOCIAL_AUTH_AUTH0_SECRET: 'xxx' ``` | +| gke | object | `{"useGKEIngress":false,"useManagedCertificate":false,"workloadIdentityEmail":""}` | Settings to make running the chart on GKE simpler | +| gke.useGKEIngress | bool | `false` | Set to true to configure the Ingress to use the GKE provided ingress controller | +| gke.useManagedCertificate | bool | `false` | Set to true to have GKE automatically provision a TLS certificate for the host specified Requires useGKEIngress to be set to true When using this option, be sure to set django.ingress.activateTLS to false | +| gke.workloadIdentityEmail | string | `""` | Workload Identity allows the K8s service account to assume the IAM access of a GCP service account to interact with other GCP services Only works with serviceAccount.create = true | +| host | string | `"defectdojo.default.minikube.local"` | Primary hostname of instance | | imagePullPolicy | string | `"Always"` | | -| imagePullSecrets | string | `nil` | | +| imagePullSecrets | string | `nil` | 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/' | | images.django.image.digest | string | `""` | | | images.django.image.registry | string | `""` | | | images.django.image.repository | string | `"defectdojo/defectdojo-django"` | | @@ -705,85 +700,64 @@ A Helm chart for Kubernetes to install DefectDojo | initializer.affinity | object | `{}` | | | initializer.annotations | object | `{}` | | | initializer.automountServiceAccountToken | bool | `false` | | -| initializer.containerSecurityContext | object | `{}` | | -| initializer.extraEnv | list | `[]` | | -| initializer.extraVolumeMounts | list | `[]` | | -| initializer.extraVolumes | list | `[]` | | +| initializer.containerSecurityContext | object | `{}` | Container security context for the initializer Job container | +| initializer.extraEnv | list | `[]` | Additional environment variables injected to the initializer job pods. | +| initializer.extraVolumeMounts | list | `[]` | Array of additional volume mount points for the initializer job (init)containers. | +| initializer.extraVolumes | list | `[]` | A list of extra volumes to attach to the initializer job pods. | | initializer.image.digest | string | `""` | | | initializer.image.registry | string | `""` | | | initializer.image.repository | string | `""` | | | initializer.image.tag | string | `""` | | | initializer.jobAnnotations | object | `{}` | | -| initializer.keepSeconds | int | `60` | | +| initializer.keepSeconds | int | `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. | | initializer.labels | object | `{}` | | | initializer.nodeSelector | object | `{}` | | -| initializer.podSecurityContext | object | `{}` | | +| initializer.podSecurityContext | object | `{}` | Pod security context for the initializer Job | | initializer.resources.limits.cpu | string | `"2000m"` | | | initializer.resources.limits.memory | string | `"512Mi"` | | | initializer.resources.requests.cpu | string | `"100m"` | | | initializer.resources.requests.memory | string | `"256Mi"` | | | initializer.run | bool | `true` | | -| initializer.staticName | bool | `false` | | +| initializer.staticName | bool | `false` | staticName defines whether name of the job will be the same (e.g., "defectdojo-initializer") or different every time - generated based on current time (e.g., "defectdojo-initializer-2024-11-11-18-57") This might be handy for ArgoCD deployments | | initializer.tolerations | list | `[]` | | -| localsettingspy | string | `""` | | +| localsettingspy | string | `""` | To add code snippet which would extend setting functionality, you might add it here It will be stored as ConfigMap and mounted `dojo/settings/local_settings.py`. For more see: https://documentation.defectdojo.com/getting_started/configuration/ For example: ``` localsettingspy: | INSTALLED_APPS += ( 'debug_toolbar', ) MIDDLEWARE = [ 'debug_toolbar.middleware.DebugToolbarMiddleware', ] + MIDDLEWARE ``` | | monitoring.enabled | bool | `false` | | -| monitoring.prometheus.containerSecurityContext | object | `{}` | | -| monitoring.prometheus.enabled | bool | `false` | | -| monitoring.prometheus.extraEnv | list | `[]` | | -| monitoring.prometheus.extraVolumeMounts | list | `[]` | | +| monitoring.prometheus.containerSecurityContext | object | `{}` | Optional: container security context for nginx prometheus exporter | +| monitoring.prometheus.enabled | bool | `false` | Add the nginx prometheus exporter sidecar | +| monitoring.prometheus.extraEnv | list | `[]` | Optional: additional environment variables injected to the nginx prometheus exporter container | +| monitoring.prometheus.extraVolumeMounts | list | `[]` | Array of additional volume mount points for the nginx prometheus exporter | | monitoring.prometheus.image.digest | string | `""` | | | monitoring.prometheus.image.registry | string | `""` | | | monitoring.prometheus.image.repository | string | `"nginx/nginx-prometheus-exporter"` | | | monitoring.prometheus.image.tag | string | `"1.4.2"` | | | monitoring.prometheus.imagePullPolicy | string | `"IfNotPresent"` | | -| monitoring.prometheus.resources | object | `{}` | | -| networkPolicy.annotations | object | `{}` | | -| networkPolicy.egress | list | `[]` | | -| networkPolicy.enabled | bool | `false` | | -| networkPolicy.ingress | list | `[]` | | -| networkPolicy.ingressExtend | list | `[]` | | -| podLabels | object | `{}` | | -| postgresServer | string | `nil` | | -| postgresql.architecture | string | `"standalone"` | | -| postgresql.auth.database | string | `"defectdojo"` | | -| postgresql.auth.existingSecret | string | `"defectdojo-postgresql-specific"` | | -| postgresql.auth.password | string | `""` | | -| postgresql.auth.secretKeys.adminPasswordKey | string | `"postgresql-postgres-password"` | | -| postgresql.auth.secretKeys.replicationPasswordKey | string | `"postgresql-replication-password"` | | -| postgresql.auth.secretKeys.userPasswordKey | string | `"postgresql-password"` | | -| postgresql.auth.username | string | `"defectdojo"` | | -| postgresql.enabled | bool | `true` | | -| postgresql.primary.affinity | object | `{}` | | -| postgresql.primary.containerSecurityContext.enabled | bool | `true` | | -| postgresql.primary.containerSecurityContext.runAsUser | int | `1001` | | -| postgresql.primary.name | string | `"primary"` | | -| postgresql.primary.nodeSelector | object | `{}` | | -| postgresql.primary.persistence.enabled | bool | `true` | | -| postgresql.primary.podSecurityContext.enabled | bool | `true` | | -| postgresql.primary.podSecurityContext.fsGroup | int | `1001` | | -| postgresql.primary.service.ports.postgresql | int | `5432` | | -| postgresql.shmVolume.chmod.enabled | bool | `false` | | -| postgresql.volumePermissions.containerSecurityContext.runAsUser | int | `1001` | | -| postgresql.volumePermissions.enabled | bool | `false` | | -| redis.architecture | string | `"standalone"` | | -| redis.auth.existingSecret | string | `"defectdojo-redis-specific"` | | -| redis.auth.existingSecretPasswordKey | string | `"redis-password"` | | -| redis.auth.password | string | `""` | | -| redis.enabled | bool | `true` | | -| redis.sentinel.enabled | bool | `false` | | -| redis.tls.enabled | bool | `false` | | -| redisParams | string | `""` | | -| redisServer | string | `nil` | | -| revisionHistoryLimit | int | `10` | | -| secrets.annotations | object | `{}` | | -| securityContext.containerSecurityContext.runAsNonRoot | bool | `true` | | -| securityContext.enabled | bool | `true` | | -| securityContext.podSecurityContext.runAsNonRoot | bool | `true` | | -| serviceAccount.annotations | object | `{}` | | -| serviceAccount.create | bool | `true` | | -| serviceAccount.labels | object | `{}` | | -| serviceAccount.name | string | `""` | | -| siteUrl | string | `""` | | +| monitoring.prometheus.resources | object | `{}` | Optional: add resource requests/limits for the nginx prometheus exporter container | +| networkPolicy | object | `{"annotations":{},"egress":[],"enabled":false,"ingress":[],"ingressExtend":[]}` | Enables application network policy For more info follow https://kubernetes.io/docs/concepts/services-networking/network-policies/ | +| networkPolicy.egress | list | `[]` | ``` egress: - to: - ipBlock: cidr: 10.0.0.0/24 ports: - protocol: TCP port: 443 ``` | +| networkPolicy.ingress | list | `[]` | For more detailed configuration with ports and peers. It will ignore ingressExtend ``` ingress: - from: - podSelector: matchLabels: app.kubernetes.io/instance: defectdojo - podSelector: matchLabels: app.kubernetes.io/instance: defectdojo-prometheus ports: - protocol: TCP port: 8443 ``` | +| networkPolicy.ingressExtend | list | `[]` | if additional labels need to be allowed (e.g. prometheus scraper) ``` ingressExtend: - podSelector: matchLabels: app.kubernetes.io/instance: defectdojo-prometheus ``` | +| podLabels | object | `{}` | Additional labels to add to the pods: ``` podLabels: key: value ``` | +| postgresServer | string | `nil` | 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: | +| postgresql | object | `{"architecture":"standalone","auth":{"database":"defectdojo","existingSecret":"defectdojo-postgresql-specific","password":"","secretKeys":{"adminPasswordKey":"postgresql-postgres-password","replicationPasswordKey":"postgresql-replication-password","userPasswordKey":"postgresql-password"},"username":"defectdojo"},"enabled":true,"primary":{"affinity":{},"containerSecurityContext":{"enabled":true,"runAsUser":1001},"name":"primary","nodeSelector":{},"persistence":{"enabled":true},"podSecurityContext":{"enabled":true,"fsGroup":1001},"service":{"ports":{"postgresql":5432}}},"shmVolume":{"chmod":{"enabled":false}},"volumePermissions":{"containerSecurityContext":{"runAsUser":1001},"enabled":false}}` | For more advance options check the bitnami chart documentation: https://github.com/bitnami/charts/tree/main/bitnami/postgresql | +| postgresql.enabled | bool | `true` | To use an external instance, switch enabled to `false` and set the address in `postgresServer` below | +| postgresql.primary.containerSecurityContext.enabled | bool | `true` | Default is true for K8s. Enabled needs to false for OpenShift restricted SCC and true for anyuid SCC | +| postgresql.primary.containerSecurityContext.runAsUser | int | `1001` | runAsUser specification below is not applied if enabled=false. enabled=false is the required setting for OpenShift "restricted SCC" to work successfully. | +| postgresql.primary.podSecurityContext.enabled | bool | `true` | Default is true for K8s. Enabled needs to false for OpenShift restricted SCC and true for anyuid SCC | +| postgresql.primary.podSecurityContext.fsGroup | int | `1001` | fsGroup specification below is not applied if enabled=false. enabled=false is the required setting for OpenShift "restricted SCC" to work successfully. | +| postgresql.volumePermissions.containerSecurityContext | object | `{"runAsUser":1001}` | if using restricted SCC set runAsUser: "auto" and if running under anyuid SCC - runAsUser needs to match the line above | +| redis | object | `{"architecture":"standalone","auth":{"existingSecret":"defectdojo-redis-specific","existingSecretPasswordKey":"redis-password","password":""},"enabled":true,"sentinel":{"enabled":false},"tls":{"enabled":false}}` | For more advance options check the bitnami chart documentation: https://github.com/bitnami/charts/tree/main/bitnami/redis | +| redis.enabled | bool | `true` | To use an external instance, switch enabled to `false`` and set the address in `redisServer` below | +| redis.tls.enabled | bool | `false` | If TLS is enabled, the Redis broker will use the redis:// and optionally mount the certificates from an existing secret. | +| redisParams | string | `""` | Parameters attached to the redis connection string, defaults to "ssl_cert_reqs=optional" if `redis.tls.enabled` | +| redisServer | string | `nil` | To use an external Redis instance, set `redis.enabled` to false and set the address here: | +| revisionHistoryLimit | int | `10` | Allow overriding of revisionHistoryLimit across all deployments. | +| secrets.annotations | object | `{}` | Add annotations for secret resources | +| securityContext | object | `{"containerSecurityContext":{"runAsNonRoot":true},"enabled":true,"podSecurityContext":{"runAsNonRoot":true}}` | Security context settings | +| serviceAccount.annotations | object | `{}` | Optional additional annotations to add to the DefectDojo's Service Account. | +| serviceAccount.create | bool | `true` | Specifies whether a service account should be created. | +| serviceAccount.labels | object | `{}` | Optional additional labels to add to the DefectDojo's Service Account. | +| serviceAccount.name | string | `""` | The name of the service account to use. If not set and create is true, a name is generated using the fullname template | +| siteUrl | string | `""` | The full URL to your defectdojo instance, depends on the domain where DD is deployed, it also affects links in Jira. Use syntax: `siteUrl: 'https://'` | | tests.unitTests.automountServiceAccountToken | bool | `false` | | | tests.unitTests.image.digest | string | `""` | | | tests.unitTests.image.registry | string | `""` | | @@ -793,7 +767,7 @@ A Helm chart for Kubernetes to install DefectDojo | tests.unitTests.resources.limits.memory | string | `"512Mi"` | | | tests.unitTests.resources.requests.cpu | string | `"100m"` | | | tests.unitTests.resources.requests.memory | string | `"128Mi"` | | -| trackConfig | string | `"disabled"` | | +| trackConfig | string | `"disabled"` | Track configuration (trackConfig): will automatically respin application pods in case of config changes detection can be: 1. disabled (default) 2. enabled, enables tracking configuration changes based on SHA256 | ---------------------------------------------- Autogenerated from chart metadata using [helm-docs v1.14.2](https://github.com/norwoodj/helm-docs/releases/v1.14.2) diff --git a/helm/defectdojo/values.schema.json b/helm/defectdojo/values.schema.json index bd012488bdb..03fb1dcc70a 100644 --- a/helm/defectdojo/values.schema.json +++ b/helm/defectdojo/values.schema.json @@ -32,12 +32,14 @@ } }, "alternativeHosts": { + "description": "optional list of alternative hostnames to use that gets appended to DD_ALLOWED_HOSTS. This is necessary when your local hostname does not match the global hostname.", "type": "array" }, "celery": { "type": "object", "properties": { "annotations": { + "description": "Common annotations to worker and beat deployments and pods.", "type": "object" }, "beat": { @@ -47,24 +49,30 @@ "type": "object" }, "annotations": { + "description": "Annotations for the Celery beat deployment.", "type": "object" }, "automountServiceAccountToken": { "type": "boolean" }, "containerSecurityContext": { + "description": "Container security context for the Celery beat containers.", "type": "object" }, "extraEnv": { + "description": "Additional environment variables injected to Celery beat containers.", "type": "array" }, "extraInitContainers": { + "description": "A list of additional initContainers to run before celery beat containers.", "type": "array" }, "extraVolumeMounts": { + "description": "Array of additional volume mount points for the celery beat containers.", "type": "array" }, "extraVolumes": { + "description": "A list of extra volumes to mount @type: array\u003cmap\u003e", "type": "array" }, "image": { @@ -85,18 +93,22 @@ } }, "livenessProbe": { + "description": "Enable liveness probe for Celery beat container. ``` exec: command: - bash - -c - celery -A dojo inspect ping -t 5 initialDelaySeconds: 30 periodSeconds: 60 timeoutSeconds: 10 ```", "type": "object" }, "nodeSelector": { "type": "object" }, "podAnnotations": { + "description": "Annotations for the Celery beat pods.", "type": "object" }, "podSecurityContext": { + "description": "Pod security context for the Celery beat pods.", "type": "object" }, "readinessProbe": { + "description": "Enable readiness probe for Celery beat container.", "type": "object" }, "replicas": { @@ -130,6 +142,7 @@ } }, "startupProbe": { + "description": "Enable startup probe for Celery beat container.", "type": "object" }, "tolerations": { @@ -150,12 +163,14 @@ "type": "object" }, "annotations": { + "description": "Annotations for the Celery worker deployment.", "type": "object" }, "appSettings": { "type": "object", "properties": { "poolType": { + "description": "Performance improved celery worker config when needing to deal with a lot of findings (e.g deduplication ops) poolType: prefork autoscaleMin: 2 autoscaleMax: 8 concurrency: 8 prefetchMultiplier: 128", "type": "string" } } @@ -164,18 +179,23 @@ "type": "boolean" }, "containerSecurityContext": { + "description": "Container security context for the Celery worker containers.", "type": "object" }, "extraEnv": { + "description": "Additional environment variables injected to Celery worker containers.", "type": "array" }, "extraInitContainers": { + "description": "A list of additional initContainers to run before celery worker containers.", "type": "array" }, "extraVolumeMounts": { + "description": "Array of additional volume mount points for the celery worker containers.", "type": "array" }, "extraVolumes": { + "description": "A list of extra volumes to mount. @type: array\u003cmap\u003e", "type": "array" }, "image": { @@ -196,18 +216,22 @@ } }, "livenessProbe": { + "description": "Enable liveness probe for Celery worker containers. ``` exec: command: - bash - -c - celery -A dojo inspect ping -t 5 initialDelaySeconds: 30 periodSeconds: 60 timeoutSeconds: 10 ```", "type": "object" }, "nodeSelector": { "type": "object" }, "podAnnotations": { + "description": "Annotations for the Celery beat pods.", "type": "object" }, "podSecurityContext": { + "description": "Pod security context for the Celery worker pods.", "type": "object" }, "readinessProbe": { + "description": "Enable readiness probe for Celery worker container.", "type": "object" }, "replicas": { @@ -241,6 +265,7 @@ } }, "startupProbe": { + "description": "Enable startup probe for Celery worker container.", "type": "object" }, "tolerations": { @@ -251,24 +276,31 @@ } }, "cloudsql": { + "description": "Google CloudSQL support in GKE via gce-proxy", "type": "object", "properties": { "containerSecurityContext": { + "description": "Optional: security context for the CloudSQL proxy container.", "type": "object" }, "enable_iam_login": { + "description": "use IAM database authentication", "type": "boolean" }, "enabled": { + "description": "To use CloudSQL in GKE set 'enable: true'", "type": "boolean" }, "extraEnv": { + "description": "Additional environment variables for the CloudSQL proxy container.", "type": "array" }, "extraVolumeMounts": { + "description": "Array of additional volume mount points for the CloudSQL proxy container", "type": "array" }, "image": { + "description": "set repo and image tag of gce-proxy", "type": "object", "properties": { "pullPolicy": { @@ -283,41 +315,52 @@ } }, "instance": { + "description": "set CloudSQL instance: 'project:zone:instancename'", "type": "string" }, "resources": { + "description": "Optional: add resource requests/limits for the CloudSQL proxy container.", "type": "object" }, "use_private_ip": { + "description": "whether to use a private IP to connect to the database", "type": "boolean" }, "verbose": { + "description": "By default, the proxy has verbose logging. Set this to false to make it less verbose", "type": "boolean" } } }, "createPostgresqlSecret": { + "description": "create postgresql secret in defectdojo chart, outside of postgresql chart", "type": "boolean" }, "createRedisSecret": { + "description": "create redis secret in defectdojo chart, outside of redis chart", "type": "boolean" }, "createSecret": { + "description": "create defectdojo specific secret", "type": "boolean" }, "dbMigrationChecker": { "type": "object", "properties": { "containerSecurityContext": { + "description": "Container security context for the DB migration checker.", "type": "object" }, "enabled": { + "description": "Enable/disable the DB migration checker.", "type": "boolean" }, "extraEnv": { + "description": "Additional environment variables for DB migration checker.", "type": "array" }, "extraVolumeMounts": { + "description": "Array of additional volume mount points for DB migration checker.", "type": "array" }, "image": { @@ -338,6 +381,7 @@ } }, "resources": { + "description": "Resource requests/limits for the DB migration checker.", "type": "object", "properties": { "limits": { @@ -367,6 +411,7 @@ } }, "disableHooks": { + "description": "Avoid using pre-install hooks, which might cause issues with ArgoCD", "type": "boolean" }, "django": { @@ -382,15 +427,19 @@ "type": "boolean" }, "extraEnv": { + "description": "Additional environment variables injected to all Django containers and initContainers.", "type": "array" }, "extraInitContainers": { + "description": "A list of additional initContainers to run before the uwsgi and nginx containers.", "type": "array" }, "extraVolumeMounts": { + "description": "Array of additional volume mount points common to all containers and initContainers.", "type": "array" }, "extraVolumes": { + "description": "A list of extra volumes to mount.", "type": "array" }, "ingress": { @@ -400,6 +449,7 @@ "type": "boolean" }, "annotations": { + "description": "Restricts the type of ingress controller that can interact with our chart (nginx, traefik, ...) `kubernetes.io/ingress.class: nginx` Depending on the size and complexity of your scans, you might want to increase the default ingress timeouts if you see repeated 504 Gateway Timeouts `nginx.ingress.kubernetes.io/proxy-read-timeout: \"1800\"` `nginx.ingress.kubernetes.io/proxy-send-timeout: \"1800\"`", "type": "object" }, "enabled": { @@ -414,6 +464,7 @@ } }, "mediaPersistentVolume": { + "description": "This feature needs more preparation before can be enabled, please visit KUBERNETES.md#media-persistent-volume", "type": "object", "properties": { "enabled": { @@ -423,18 +474,22 @@ "type": "integer" }, "name": { + "description": "any name", "type": "string" }, "persistentVolumeClaim": { + "description": "in case if pvc specified, should point to the already existing pvc", "type": "object", "properties": { "accessModes": { + "description": "check KUBERNETES.md doc first for option to choose", "type": "array", "items": { "type": "string" } }, "create": { + "description": "set to true to create a new pvc and if django.mediaPersistentVolume.type is set to pvc", "type": "boolean" }, "name": { @@ -449,6 +504,7 @@ } }, "type": { + "description": "could be emptyDir (not for production) or pvc", "type": "string" } } @@ -457,17 +513,21 @@ "type": "object", "properties": { "containerSecurityContext": { + "description": "Container security context for the nginx containers.", "type": "object", "properties": { "runAsUser": { + "description": "nginx dockerfile sets USER=1001", "type": "integer" } } }, "extraEnv": { + "description": "To extra environment variables to the nginx container, you can use extraEnv. For example: extraEnv: - name: FOO valueFrom: configMapKeyRef: name: foo key: bar", "type": "array" }, "extraVolumeMounts": { + "description": "Array of additional volume mount points for nginx containers.", "type": "array" }, "image": { @@ -531,6 +591,7 @@ "type": "object" }, "podSecurityContext": { + "description": "Pod security context for the Django pods.", "type": "object", "properties": { "fsGroup": { @@ -565,6 +626,7 @@ "type": "object", "properties": { "maxFd": { + "description": "Use this value to set the maximum number of file descriptors. If set to 0 will be detected by uwsgi e.g. 102400", "type": "integer" }, "processes": { @@ -588,6 +650,7 @@ "type": "string" }, "enabled": { + "description": "includes additional CA certificate as volume, it refrences REQUESTS_CA_BUNDLE env varible NOTE: it reflects REQUESTS_CA_BUNDLE for celery workers, beats as well", "type": "boolean" } } @@ -596,17 +659,21 @@ "type": "object", "properties": { "runAsUser": { + "description": "django dockerfile sets USER=1001", "type": "integer" } } }, "enableDebug": { + "description": "this also requires DD_DEBUG to be set to True", "type": "boolean" }, "extraEnv": { + "description": "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", "type": "array" }, "extraVolumeMounts": { + "description": "Array of additional volume mount points for uwsgi containers.", "type": "array" }, "image": { @@ -630,6 +697,7 @@ "type": "object", "properties": { "enabled": { + "description": "Enable liveness checks on uwsgi container.", "type": "boolean" }, "failureThreshold": { @@ -653,6 +721,7 @@ "type": "object", "properties": { "enabled": { + "description": "Enable readiness checks on uwsgi container.", "type": "boolean" }, "failureThreshold": { @@ -703,6 +772,7 @@ "type": "object", "properties": { "enabled": { + "description": "Enable startup checks on uwsgi container.", "type": "boolean" }, "failureThreshold": { @@ -727,41 +797,52 @@ } }, "extraAnnotations": { + "description": "Annotations globally added to all resources", "type": "object" }, "extraConfigs": { + "description": "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 ``` DD_SOCIAL_AUTH_AUTH0_OAUTH2_ENABLED: 'true' DD_SOCIAL_AUTH_AUTH0_KEY: 'dev' DD_SOCIAL_AUTH_AUTH0_DOMAIN: 'xxxxx' ```", "type": "object" }, "extraEnv": { + "description": "To add (or override) extra variables which need to be pulled from another configMap, you can use extraEnv. For example: ``` - name: DD_DATABASE_HOST valueFrom: configMapKeyRef: name: my-other-postgres-configmap key: cluster_endpoint ```", "type": "array" }, "extraLabels": { + "description": "Labels globally added to all resources", "type": "object" }, "extraSecrets": { + "description": "Extra secrets can be created inside of extraSecrets block: ``` DD_SOCIAL_AUTH_AUTH0_SECRET: 'xxx' ```", "type": "object" }, "gke": { + "description": "Settings to make running the chart on GKE simpler", "type": "object", "properties": { "useGKEIngress": { + "description": "Set to true to configure the Ingress to use the GKE provided ingress controller", "type": "boolean" }, "useManagedCertificate": { + "description": "Set to true to have GKE automatically provision a TLS certificate for the host specified Requires useGKEIngress to be set to true When using this option, be sure to set django.ingress.activateTLS to false", "type": "boolean" }, "workloadIdentityEmail": { + "description": "Workload Identity allows the K8s service account to assume the IAM access of a GCP service account to interact with other GCP services Only works with serviceAccount.create = true", "type": "string" } } }, "host": { + "description": "Primary hostname of instance", "type": "string" }, "imagePullPolicy": { "type": "string" }, "imagePullSecrets": { + "description": "When using a private registry, name of the secret that holds the registry secret (eg deploy token from gitlab-ci project)", "type": [ "string", "null" @@ -829,15 +910,19 @@ "type": "boolean" }, "containerSecurityContext": { + "description": "Container security context for the initializer Job container", "type": "object" }, "extraEnv": { + "description": "Additional environment variables injected to the initializer job pods.", "type": "array" }, "extraVolumeMounts": { + "description": "Array of additional volume mount points for the initializer job (init)containers.", "type": "array" }, "extraVolumes": { + "description": "A list of extra volumes to attach to the initializer job pods.", "type": "array" }, "image": { @@ -861,6 +946,7 @@ "type": "object" }, "keepSeconds": { + "description": "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.", "type": "integer" }, "labels": { @@ -870,6 +956,7 @@ "type": "object" }, "podSecurityContext": { + "description": "Pod security context for the initializer Job", "type": "object" }, "resources": { @@ -903,6 +990,7 @@ "type": "boolean" }, "staticName": { + "description": "staticName defines whether name of the job will be the same (e.g., \"defectdojo-initializer\") or different every time - generated based on current time (e.g., \"defectdojo-initializer-2024-11-11-18-57\") This might be handy for ArgoCD deployments", "type": "boolean" }, "tolerations": { @@ -911,6 +999,7 @@ } }, "localsettingspy": { + "description": "To add code snippet which would extend setting functionality, you might add it here It will be stored as ConfigMap and mounted `dojo/settings/local_settings.py`. For more see: https://documentation.defectdojo.com/getting_started/configuration/ For example: ``` localsettingspy: | INSTALLED_APPS += ( 'debug_toolbar', ) MIDDLEWARE = [ 'debug_toolbar.middleware.DebugToolbarMiddleware', ] + MIDDLEWARE ```", "type": "string" }, "monitoring": { @@ -923,15 +1012,19 @@ "type": "object", "properties": { "containerSecurityContext": { + "description": "Optional: container security context for nginx prometheus exporter", "type": "object" }, "enabled": { + "description": "Add the nginx prometheus exporter sidecar", "type": "boolean" }, "extraEnv": { + "description": "Optional: additional environment variables injected to the nginx prometheus exporter container", "type": "array" }, "extraVolumeMounts": { + "description": "Array of additional volume mount points for the nginx prometheus exporter", "type": "array" }, "image": { @@ -955,6 +1048,7 @@ "type": "string" }, "resources": { + "description": "Optional: add resource requests/limits for the nginx prometheus exporter container", "type": "object" } } @@ -962,35 +1056,42 @@ } }, "networkPolicy": { + "description": "Enables application network policy For more info follow https://kubernetes.io/docs/concepts/services-networking/network-policies/", "type": "object", "properties": { "annotations": { "type": "object" }, "egress": { + "description": " ``` egress: - to: - ipBlock: cidr: 10.0.0.0/24 ports: - protocol: TCP port: 443 ```", "type": "array" }, "enabled": { "type": "boolean" }, "ingress": { + "description": "For more detailed configuration with ports and peers. It will ignore ingressExtend ``` ingress: - from: - podSelector: matchLabels: app.kubernetes.io/instance: defectdojo - podSelector: matchLabels: app.kubernetes.io/instance: defectdojo-prometheus ports: - protocol: TCP port: 8443 ```", "type": "array" }, "ingressExtend": { + "description": "if additional labels need to be allowed (e.g. prometheus scraper) ``` ingressExtend: - podSelector: matchLabels: app.kubernetes.io/instance: defectdojo-prometheus ```", "type": "array" } } }, "podLabels": { + "description": "Additional labels to add to the pods: ``` podLabels: key: value ```", "type": "object" }, "postgresServer": { + "description": "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:", "type": [ "string", "null" ] }, "postgresql": { + "description": "For more advance options check the bitnami chart documentation: https://github.com/bitnami/charts/tree/main/bitnami/postgresql", "type": "object", "properties": { "architecture": { @@ -1028,6 +1129,7 @@ } }, "enabled": { + "description": "To use an external instance, switch enabled to `false` and set the address in `postgresServer` below", "type": "boolean" }, "primary": { @@ -1040,9 +1142,11 @@ "type": "object", "properties": { "enabled": { + "description": "Default is true for K8s. Enabled needs to false for OpenShift restricted SCC and true for anyuid SCC", "type": "boolean" }, "runAsUser": { + "description": "runAsUser specification below is not applied if enabled=false. enabled=false is the required setting for OpenShift \"restricted SCC\" to work successfully.", "type": "integer" } } @@ -1065,9 +1169,11 @@ "type": "object", "properties": { "enabled": { + "description": "Default is true for K8s. Enabled needs to false for OpenShift restricted SCC and true for anyuid SCC", "type": "boolean" }, "fsGroup": { + "description": "fsGroup specification below is not applied if enabled=false. enabled=false is the required setting for OpenShift \"restricted SCC\" to work successfully.", "type": "integer" } } @@ -1104,6 +1210,7 @@ "type": "object", "properties": { "containerSecurityContext": { + "description": "if using restricted SCC set runAsUser: \"auto\" and if running under anyuid SCC - runAsUser needs to match the line above", "type": "object", "properties": { "runAsUser": { @@ -1119,6 +1226,7 @@ } }, "redis": { + "description": "For more advance options check the bitnami chart documentation: https://github.com/bitnami/charts/tree/main/bitnami/redis", "type": "object", "properties": { "architecture": { @@ -1139,6 +1247,7 @@ } }, "enabled": { + "description": "To use an external instance, switch enabled to `false`` and set the address in `redisServer` below", "type": "boolean" }, "sentinel": { @@ -1153,6 +1262,7 @@ "type": "object", "properties": { "enabled": { + "description": "If TLS is enabled, the Redis broker will use the redis:// and optionally mount the certificates from an existing secret.", "type": "boolean" } } @@ -1160,26 +1270,31 @@ } }, "redisParams": { + "description": "Parameters attached to the redis connection string, defaults to \"ssl_cert_reqs=optional\" if `redis.tls.enabled`", "type": "string" }, "redisServer": { + "description": "To use an external Redis instance, set `redis.enabled` to false and set the address here:", "type": [ "string", "null" ] }, "revisionHistoryLimit": { + "description": "Allow overriding of revisionHistoryLimit across all deployments.", "type": "integer" }, "secrets": { "type": "object", "properties": { "annotations": { + "description": "Add annotations for secret resources", "type": "object" } } }, "securityContext": { + "description": "Security context settings", "type": "object", "properties": { "containerSecurityContext": { @@ -1207,20 +1322,25 @@ "type": "object", "properties": { "annotations": { + "description": "Optional additional annotations to add to the DefectDojo's Service Account.", "type": "object" }, "create": { + "description": "Specifies whether a service account should be created.", "type": "boolean" }, "labels": { + "description": "Optional additional labels to add to the DefectDojo's Service Account.", "type": "object" }, "name": { + "description": "The name of the service account to use. If not set and create is true, a name is generated using the fullname template", "type": "string" } } }, "siteUrl": { + "description": "The full URL to your defectdojo instance, depends on the domain where DD is deployed, it also affects links in Jira. Use syntax: `siteUrl: 'https://\u003cyourdomain\u003e'`", "type": "string" }, "tests": { @@ -1281,6 +1401,7 @@ } }, "trackConfig": { + "description": "Track configuration (trackConfig): will automatically respin application pods in case of config changes detection can be: 1. disabled (default) 2. enabled, enables tracking configuration changes based on SHA256", "type": "string" } } diff --git a/helm/defectdojo/values.yaml b/helm/defectdojo/values.yaml index faac6f999c0..5fd9603b82c 100644 --- a/helm/defectdojo/values.yaml +++ b/helm/defectdojo/values.yaml @@ -1,5 +1,5 @@ --- -# Security context settings +# -- Security context settings securityContext: enabled: true containerSecurityContext: @@ -7,24 +7,24 @@ securityContext: podSecurityContext: runAsNonRoot: true -# create defectdojo specific secret +# -- create defectdojo specific secret createSecret: false -# create redis secret in defectdojo chart, outside of redis chart +# -- create redis secret in defectdojo chart, outside of redis chart createRedisSecret: false -# create postgresql secret in defectdojo chart, outside of postgresql chart +# -- create postgresql secret in defectdojo chart, outside of postgresql chart createPostgresqlSecret: false -# Track configuration (trackConfig): will automatically respin application pods in case of config changes detection +# -- Track configuration (trackConfig): will automatically respin application pods in case of config changes detection # can be: -# - disabled, default -# - enabled, enables tracking configuration changes based on SHA256 +# 1. disabled (default) +# 2. enabled, enables tracking configuration changes based on SHA256 trackConfig: disabled -# Avoid using pre-install hooks, which might cause issues with ArgoCD +# -- Avoid using pre-install hooks, which might cause issues with ArgoCD disableHooks: false -# Annotations globally added to all resources +# -- Annotations globally added to all resources extraAnnotations: {} -# Labels globally added to all resources +# -- Labels globally added to all resources extraLabels: {} images: @@ -41,18 +41,20 @@ images: tag: "" # If empty, use appVersion digest: "" -# Enables application network policy +# -- Enables application network policy # For more info follow https://kubernetes.io/docs/concepts/services-networking/network-policies/ networkPolicy: enabled: false - # if additional labels need to be allowed (e.g. prometheus scraper) - ingressExtend: [] + # -- if additional labels need to be allowed (e.g. prometheus scraper) + # ``` # ingressExtend: # - podSelector: # matchLabels: # app.kubernetes.io/instance: defectdojo-prometheus - # For more detailed configuration with ports and peers. It will ignore ingressExtend - ingress: [] + # ``` + ingressExtend: [] + # -- For more detailed configuration with ports and peers. It will ignore ingressExtend + # ``` # ingress: # - from: # - podSelector: @@ -64,7 +66,10 @@ networkPolicy: # ports: # - protocol: TCP # port: 8443 - egress: [] + # ``` + ingress: [] + # -- + # ``` # egress: # - to: # - ipBlock: @@ -72,46 +77,50 @@ networkPolicy: # ports: # - protocol: TCP # port: 443 + # ``` + egress: [] annotations: {} -# Primary hostname of instance +# -- Primary hostname of instance host: defectdojo.default.minikube.local -# The full URL to your defectdojo instance, depends on the domain where DD is deployed, it also affects links in Jira +# -- The full URL to your defectdojo instance, depends on the domain where DD is deployed, it also affects links in Jira. +# Use syntax: `siteUrl: 'https://'` siteUrl: "" -# siteUrl: 'https://' -# optional list of alternative hostnames to use that gets appended to +# -- optional list of alternative hostnames to use that gets appended to # DD_ALLOWED_HOSTS. This is necessary when your local hostname does not match # the global hostname. alternativeHosts: [] # - defectdojo.example.com imagePullPolicy: Always -# 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/' # @schema type:[string, null] +# -- 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: ~ -# Additional labels to add to the pods: +# -- Additional labels to add to the pods: +# ``` # podLabels: # key: value +# ``` podLabels: {} -# Allow overriding of revisionHistoryLimit across all deployments. +# -- Allow overriding of revisionHistoryLimit across all deployments. revisionHistoryLimit: 10 serviceAccount: - # Specifies whether a service account should be created. + # -- Specifies whether a service account should be created. create: true - # The name of the service account to use. + # -- The name of the service account to use. # If not set and create is true, a name is generated using the fullname template name: "" - # Optional additional annotations to add to the DefectDojo's Service Account. + # -- Optional additional annotations to add to the DefectDojo's Service Account. annotations: {} - # Optional additional labels to add to the DefectDojo's Service Account. + # -- Optional additional labels to add to the DefectDojo's Service Account. labels: {} dbMigrationChecker: @@ -120,15 +129,15 @@ dbMigrationChecker: repository: "" tag: "" digest: "" - # Enable/disable the DB migration checker. + # -- Enable/disable the DB migration checker. enabled: true - # Container security context for the DB migration checker. + # -- Container security context for the DB migration checker. containerSecurityContext: {} - # Additional environment variables for DB migration checker. + # -- Additional environment variables for DB migration checker. extraEnv: [] - # Array of additional volume mount points for DB migration checker. + # -- Array of additional volume mount points for DB migration checker. extraVolumeMounts: [] - # Resource requests/limits for the DB migration checker. + # -- Resource requests/limits for the DB migration checker. resources: requests: cpu: 100m @@ -166,7 +175,7 @@ admin: monitoring: enabled: false prometheus: - # Add the nginx prometheus exporter sidecar + # -- Add the nginx prometheus exporter sidecar enabled: false image: registry: "" @@ -174,24 +183,24 @@ monitoring: tag: "1.4.2" digest: "" imagePullPolicy: IfNotPresent - # Optional: container security context for nginx prometheus exporter + # -- Optional: container security context for nginx prometheus exporter containerSecurityContext: {} - # Optional: additional environment variables injected to the nginx prometheus exporter container + # -- Optional: additional environment variables injected to the nginx prometheus exporter container extraEnv: [] - # Array of additional volume mount points for the nginx prometheus exporter + # -- Array of additional volume mount points for the nginx prometheus exporter extraVolumeMounts: [] - # Optional: add resource requests/limits for the nginx prometheus exporter container + # -- Optional: add resource requests/limits for the nginx prometheus exporter container resources: {} secrets: - # Add annotations for secret resources + # -- Add annotations for secret resources annotations: {} # Components celery: broker: redis logLevel: INFO - # Common annotations to worker and beat deployments and pods. + # -- Common annotations to worker and beat deployments and pods. annotations: {} beat: image: # If empty, uses values from images.django.image @@ -200,36 +209,38 @@ celery: tag: "" digest: "" automountServiceAccountToken: false - # Annotations for the Celery beat deployment. + # -- Annotations for the Celery beat deployment. annotations: {} affinity: {} - # Container security context for the Celery beat containers. + # -- Container security context for the Celery beat containers. containerSecurityContext: {} - # Additional environment variables injected to Celery beat containers. + # -- Additional environment variables injected to Celery beat containers. extraEnv: [] - # A list of additional initContainers to run before celery beat containers. + # -- A list of additional initContainers to run before celery beat containers. extraInitContainers: [] - # Array of additional volume mount points for the celery beat containers. + # -- Array of additional volume mount points for the celery beat containers. extraVolumeMounts: [] - # A list of extra volumes to mount + # -- A list of extra volumes to mount # @type: array extraVolumes: [] - # Enable liveness probe for Celery beat container. + # -- Enable liveness probe for Celery beat container. + # ``` + # exec: + # command: + # - bash + # - -c + # - celery -A dojo inspect ping -t 5 + # initialDelaySeconds: 30 + # periodSeconds: 60 + # timeoutSeconds: 10 + # ``` 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. + # -- Annotations for the Celery beat pods. podAnnotations: {} - # Pod security context for the Celery beat pods. + # -- Pod security context for the Celery beat pods. podSecurityContext: {} - # Enable readiness probe for Celery beat container. + # -- Enable readiness probe for Celery beat container. readinessProbe: {} replicas: 1 resources: @@ -239,7 +250,7 @@ celery: limits: cpu: 2000m memory: 256Mi - # Enable startup probe for Celery beat container. + # -- Enable startup probe for Celery beat container. startupProbe: {} tolerations: [] worker: @@ -249,36 +260,38 @@ celery: tag: "" digest: "" automountServiceAccountToken: false - # Annotations for the Celery worker deployment. + # -- Annotations for the Celery worker deployment. annotations: {} affinity: {} - # Container security context for the Celery worker containers. + # -- Container security context for the Celery worker containers. containerSecurityContext: {} - # Additional environment variables injected to Celery worker containers. + # -- Additional environment variables injected to Celery worker containers. extraEnv: [] - # A list of additional initContainers to run before celery worker containers. + # -- A list of additional initContainers to run before celery worker containers. extraInitContainers: [] - # Array of additional volume mount points for the celery worker containers. + # -- Array of additional volume mount points for the celery worker containers. extraVolumeMounts: [] - # A list of extra volumes to mount. + # -- A list of extra volumes to mount. # @type: array extraVolumes: [] - # Enable liveness probe for Celery worker containers. + # -- Enable liveness probe for Celery worker containers. + # ``` + # exec: + # command: + # - bash + # - -c + # - celery -A dojo inspect ping -t 5 + # initialDelaySeconds: 30 + # periodSeconds: 60 + # timeoutSeconds: 10 + # ``` 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. + # -- Annotations for the Celery beat pods. podAnnotations: {} - # Pod security context for the Celery worker pods. + # -- Pod security context for the Celery worker pods. podSecurityContext: {} - # Enable readiness probe for Celery worker container. + # -- Enable readiness probe for Celery worker container. readinessProbe: {} replicas: 1 resources: @@ -288,18 +301,17 @@ celery: limits: cpu: 2000m memory: 512Mi - # Enable startup probe for Celery worker container. + # -- Enable startup probe for Celery worker container. startupProbe: {} tolerations: [] appSettings: - poolType: solo - # Performance improved celery worker config when needing to deal with a lot of findings (e.g deduplication ops) - # Comment out the "solo" line, and uncomment the following lines. + # -- Performance improved celery worker config when needing to deal with a lot of findings (e.g deduplication ops) # poolType: prefork # autoscaleMin: 2 # autoscaleMax: 8 # concurrency: 8 # prefetchMultiplier: 128 + poolType: solo django: automountServiceAccountToken: false @@ -308,7 +320,7 @@ django: annotations: {} type: "" affinity: {} - # Pod security context for the Django pods. + # -- Pod security context for the Django pods. podSecurityContext: fsGroup: 1001 ingress: @@ -316,23 +328,23 @@ django: ingressClassName: "" activateTLS: true secretName: defectdojo-tls + # -- Restricts the type of ingress controller that can interact with our chart (nginx, traefik, ...) + # `kubernetes.io/ingress.class: nginx` + # Depending on the size and complexity of your scans, you might want to increase the default ingress timeouts if you see repeated 504 Gateway Timeouts + # `nginx.ingress.kubernetes.io/proxy-read-timeout: "1800"` + # `nginx.ingress.kubernetes.io/proxy-send-timeout: "1800"` annotations: {} - # Restricts the type of ingress controller that can interact with our chart (nginx, traefik, ...) - # kubernetes.io/ingress.class: nginx - # Depending on the size and complexity of your scans, you might want to increase the default ingress timeouts if you see repeated 504 Gateway Timeouts - # nginx.ingress.kubernetes.io/proxy-read-timeout: "1800" - # nginx.ingress.kubernetes.io/proxy-send-timeout: "1800" nginx: image: # If empty, uses values from images.nginx.image registry: "" repository: "" tag: "" digest: "" - # Container security context for the nginx containers. + # -- Container security context for the nginx containers. containerSecurityContext: - # nginx dockerfile sets USER=1001 + # -- nginx dockerfile sets USER=1001 runAsUser: 1001 - # To extra environment variables to the nginx container, you can use extraEnv. For example: + # -- To extra environment variables to the nginx container, you can use extraEnv. For example: # extraEnv: # - name: FOO # valueFrom: @@ -340,7 +352,7 @@ django: # name: foo # key: bar extraEnv: [] - # Array of additional volume mount points for nginx containers. + # -- Array of additional volume mount points for nginx containers. extraVolumeMounts: [] tls: enabled: false @@ -363,9 +375,9 @@ django: tag: "" digest: "" containerSecurityContext: - # django dockerfile sets USER=1001 + # -- django dockerfile sets USER=1001 runAsUser: 1001 - # To add (or override) extra variables which need to be pulled from another configMap, you can + # -- 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 @@ -374,10 +386,10 @@ django: # name: my-other-postgres-configmap # key: cluster_endpoint extraEnv: [] - # Array of additional volume mount points for uwsgi containers. + # -- Array of additional volume mount points for uwsgi containers. extraVolumeMounts: [] livenessProbe: - # Enable liveness checks on uwsgi container. + # -- Enable liveness checks on uwsgi container. enabled: true failureThreshold: 6 initialDelaySeconds: 0 @@ -385,7 +397,7 @@ django: successThreshold: 1 timeoutSeconds: 5 readinessProbe: - # Enable readiness checks on uwsgi container. + # -- Enable readiness checks on uwsgi container. enabled: true failureThreshold: 6 initialDelaySeconds: 0 @@ -393,7 +405,7 @@ django: successThreshold: 1 timeoutSeconds: 5 startupProbe: - # Enable startup checks on uwsgi container. + # -- Enable startup checks on uwsgi container. enabled: true failureThreshold: 30 initialDelaySeconds: 0 @@ -410,10 +422,13 @@ django: appSettings: processes: 4 threads: 4 - maxFd: 0 # 102400 # Use this value to set the maximum number of file descriptors. If set to 0 will be detected by uwsgi - enableDebug: false # this also requires DD_DEBUG to be set to True + # -- Use this value to set the maximum number of file descriptors. If set to 0 will be detected by uwsgi + # e.g. 102400 + maxFd: 0 + # -- this also requires DD_DEBUG to be set to True + enableDebug: false certificates: - # includes additional CA certificate as volume, it refrences REQUESTS_CA_BUNDLE env varible + # -- 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` # NOTE: it reflects REQUESTS_CA_BUNDLE for celery workers, beats as well enabled: false @@ -421,31 +436,32 @@ django: certMountPath: /certs/ certFileName: ca.crt - # Additional environment variables injected to all Django containers and initContainers. + # -- Additional environment variables injected to all Django containers and initContainers. extraEnv: [] - # A list of additional initContainers to run before the uwsgi and nginx containers. + # -- A list of additional initContainers to run before the uwsgi and nginx containers. extraInitContainers: [] - # Array of additional volume mount points common to all containers and initContainers. + # -- Array of additional volume mount points common to all containers and initContainers. extraVolumeMounts: [] - # A list of extra volumes to mount. + # -- A list of extra volumes to mount. extraVolumes: [] - # This feature needs more preparation before can be enabled, please visit KUBERNETES.md#media-persistent-volume + # -- This feature needs more preparation before can be enabled, please visit KUBERNETES.md#media-persistent-volume mediaPersistentVolume: enabled: true fsGroup: 1001 - # any name + # -- any name name: media - # could be emptyDir (not for production) or pvc + # -- could be emptyDir (not for production) or pvc type: emptyDir - # in case if pvc specified, should point to the already existing pvc + # -- in case if pvc specified, should point to the already existing pvc persistentVolumeClaim: - # set to true to create a new pvc and if django.mediaPersistentVolume.type is set to pvc + # -- set to true to create a new pvc and if django.mediaPersistentVolume.type is set to pvc create: false name: "" size: 5Gi + # -- check KUBERNETES.md doc first for option to choose accessModes: - - ReadWriteMany # check KUBERNETES.md doc first for option to choose + - ReadWriteMany storageClassName: "" initializer: @@ -454,7 +470,8 @@ initializer: jobAnnotations: {} annotations: {} labels: {} - 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. + # -- 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. + keepSeconds: 60 affinity: {} nodeSelector: {} tolerations: [] @@ -470,25 +487,25 @@ initializer: limits: cpu: 2000m memory: 512Mi - # Container security context for the initializer Job container + # -- Container security context for the initializer Job container containerSecurityContext: {} - # Additional environment variables injected to the initializer job pods. + # -- Additional environment variables injected to the initializer job pods. extraEnv: [] - # Array of additional volume mount points for the initializer job (init)containers. + # -- 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. + # -- A list of extra volumes to attach to the initializer job pods. extraVolumes: [] - # Pod security context for the initializer Job + # -- Pod security context for the initializer Job podSecurityContext: {} - # staticName defines whether name of the job will be the same (e.g., "defectdojo-initializer") + # -- staticName defines whether name of the job will be the same (e.g., "defectdojo-initializer") # or different every time - generated based on current time (e.g., "defectdojo-initializer-2024-11-11-18-57") # 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 +# -- For more advance options check the bitnami chart documentation: https://github.com/bitnami/charts/tree/main/bitnami/postgresql postgresql: - # To use an external instance, switch enabled to `false` and set the address in `postgresServer` below + # -- To use an external instance, switch enabled to `false` and set the address in `postgresServer` below enabled: true auth: username: defectdojo @@ -508,67 +525,67 @@ postgresql: ports: postgresql: 5432 podSecurityContext: - # Default is true for K8s. Enabled needs to false for OpenShift restricted SCC and true for anyuid SCC + # -- Default is true for K8s. Enabled needs to false for OpenShift restricted SCC and true for anyuid SCC enabled: true - # fsGroup specification below is not applied if enabled=false. enabled=false is the required setting for OpenShift "restricted SCC" to work successfully. + # -- fsGroup specification below is not applied if enabled=false. enabled=false is the required setting for OpenShift "restricted SCC" to work successfully. fsGroup: 1001 containerSecurityContext: - # Default is true for K8s. Enabled needs to false for OpenShift restricted SCC and true for anyuid SCC + # -- Default is true for K8s. Enabled needs to false for OpenShift restricted SCC and true for anyuid SCC enabled: true - # runAsUser specification below is not applied if enabled=false. enabled=false is the required setting for OpenShift "restricted SCC" to work successfully. + # -- runAsUser specification below is not applied if enabled=false. enabled=false is the required setting for OpenShift "restricted SCC" to work successfully. runAsUser: 1001 affinity: {} nodeSelector: {} volumePermissions: enabled: false - # if using restricted SCC set runAsUser: "auto" and if running under anyuid SCC - runAsUser needs to match the line above + # -- if using restricted SCC set runAsUser: "auto" and if running under anyuid SCC - runAsUser needs to match the line above containerSecurityContext: runAsUser: 1001 shmVolume: chmod: enabled: false -# Google CloudSQL support in GKE via gce-proxy +# -- Google CloudSQL support in GKE via gce-proxy cloudsql: - # To use CloudSQL in GKE set 'enable: true' + # -- To use CloudSQL in GKE set 'enable: true' enabled: false - # By default, the proxy has verbose logging. Set this to false to make it less verbose + # -- By default, the proxy has verbose logging. Set this to false to make it less verbose verbose: true + # -- set repo and image tag of gce-proxy image: - # set repo and image tag of gce-proxy repository: gcr.io/cloudsql-docker/gce-proxy tag: 1.37.9 pullPolicy: IfNotPresent - # set CloudSQL instance: 'project:zone:instancename' + # -- set CloudSQL instance: 'project:zone:instancename' instance: "" - # use IAM database authentication + # -- use IAM database authentication enable_iam_login: false - # whether to use a private IP to connect to the database + # -- whether to use a private IP to connect to the database use_private_ip: false - # Optional: security context for the CloudSQL proxy container. + # -- Optional: security context for the CloudSQL proxy container. containerSecurityContext: {} - # Additional environment variables for the CloudSQL proxy container. + # -- Additional environment variables for the CloudSQL proxy container. extraEnv: [] - # Array of additional volume mount points for the CloudSQL proxy container + # -- Array of additional volume mount points for the CloudSQL proxy container extraVolumeMounts: [] - # Optional: add resource requests/limits for the CloudSQL proxy container. + # -- Optional: add resource requests/limits for the CloudSQL proxy container. resources: {} -# Settings to make running the chart on GKE simpler +# -- Settings to make running the chart on GKE simpler gke: - # Set to true to configure the Ingress to use the GKE provided ingress controller + # -- Set to true to configure the Ingress to use the GKE provided ingress controller useGKEIngress: false - # Set to true to have GKE automatically provision a TLS certificate for the host specified + # -- Set to true to have GKE automatically provision a TLS certificate for the host specified # Requires useGKEIngress to be set to true # When using this option, be sure to set django.ingress.activateTLS to false useManagedCertificate: false - # Workload Identity allows the K8s service account to assume the IAM access of a GCP service account to interact with other GCP services + # -- Workload Identity allows the K8s service account to assume the IAM access of a GCP service account to interact with other GCP services # Only works with serviceAccount.create = true workloadIdentityEmail: "" -# For more advance options check the bitnami chart documentation: https://github.com/bitnami/charts/tree/main/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 + # -- To use an external instance, switch enabled to `false`` and set the address in `redisServer` below enabled: true auth: existingSecret: defectdojo-redis-specific @@ -583,41 +600,47 @@ redis: # 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: + # -- If TLS is enabled, the Redis broker will use the redis:// and optionally mount the certificates + # from an existing secret. 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: +# -- 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 +# ``` +# DD_SOCIAL_AUTH_AUTH0_OAUTH2_ENABLED: 'true' +# DD_SOCIAL_AUTH_AUTH0_KEY: 'dev' +# DD_SOCIAL_AUTH_AUTH0_DOMAIN: 'xxxxx' +# ``` extraConfigs: {} -# DD_SOCIAL_AUTH_AUTH0_OAUTH2_ENABLED: 'true' -# DD_SOCIAL_AUTH_AUTH0_KEY: 'dev' -# DD_SOCIAL_AUTH_AUTH0_DOMAIN: 'xxxxx' -# Extra secrets can be created inside of extraSecrets block: +# -- Extra secrets can be created inside of extraSecrets block: # NOTE This is just an exmaple, do not store sensitive data in plain text form, better inject it during the deployment/upgrade by --set extraSecrets.secret=someSecret +# ``` +# DD_SOCIAL_AUTH_AUTH0_SECRET: 'xxx' +# ``` extraSecrets: {} -# DD_SOCIAL_AUTH_AUTH0_SECRET: 'xxx' -# To add (or override) extra variables which need to be pulled from another configMap, you can +# -- 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: [] -# To add code snippet which would extend setting functionality, you might add it here +# -- To add code snippet which would extend setting functionality, you might add it here # It will be stored as ConfigMap and mounted `dojo/settings/local_settings.py`. # For more see: https://documentation.defectdojo.com/getting_started/configuration/ -localsettingspy: "" # For example: +# ``` # localsettingspy: | # INSTALLED_APPS += ( # 'debug_toolbar', @@ -625,17 +648,19 @@ localsettingspy: "" # MIDDLEWARE = [ # 'debug_toolbar.middleware.DebugToolbarMiddleware', # ] + MIDDLEWARE +# ``` +localsettingspy: "" # # External database support. # -# To use an external Redis instance, set `redis.enabled` to false and set the address here: # @schema type:[string, null] +# -- 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` +# -- 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: # @schema type:[string, null] +# -- 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 dbe8da01575d8872441d315190461cdf51ec9a34 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 20 Oct 2025 08:59:51 -0600 Subject: [PATCH 063/126] Bump python-gitlab from 6.4.0 to 6.5.0 (#13470) Bumps [python-gitlab](https://github.com/python-gitlab/python-gitlab) from 6.4.0 to 6.5.0. - [Release notes](https://github.com/python-gitlab/python-gitlab/releases) - [Changelog](https://github.com/python-gitlab/python-gitlab/blob/main/CHANGELOG.md) - [Commits](https://github.com/python-gitlab/python-gitlab/compare/v6.4.0...v6.5.0) --- updated-dependencies: - dependency-name: python-gitlab dependency-version: 6.5.0 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 daff55f0c21..17e75397d25 100644 --- a/requirements.txt +++ b/requirements.txt @@ -43,7 +43,7 @@ titlecase==2.4.1 social-auth-app-django==5.6.0 social-auth-core==4.8.1 gitpython==3.1.45 -python-gitlab==6.4.0 +python-gitlab==6.5.0 cpe==1.3.1 packageurl-python==0.17.5 django-crum==0.7.9 From 0b7e96d09413c76ef98c15e95ac03bfec2dcf67c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 20 Oct 2025 09:09:27 -0600 Subject: [PATCH 064/126] Bump boto3 from 1.40.54 to 1.40.55 (#13472) Bumps [boto3](https://github.com/boto/boto3) from 1.40.54 to 1.40.55. - [Release notes](https://github.com/boto/boto3/releases) - [Commits](https://github.com/boto/boto3/compare/1.40.54...1.40.55) --- updated-dependencies: - dependency-name: boto3 dependency-version: 1.40.55 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 17e75397d25..f45a0ea031e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -62,7 +62,7 @@ django-ratelimit==4.1.0 argon2-cffi==25.1.0 blackduck==1.1.3 pycurl==7.45.7 # Required for Celery Broker AWS (SQS) support -boto3==1.40.54 # Required for Celery Broker AWS (SQS) support +boto3==1.40.55 # Required for Celery Broker AWS (SQS) support netaddr==1.3.0 vulners==3.1.1 fontawesomefree==6.6.0 From b2eda48350adad426a647584ff081ae38b0ab387 Mon Sep 17 00:00:00 2001 From: kiblik <5609770+kiblik@users.noreply.github.com> Date: Mon, 20 Oct 2025 15:28:30 +0000 Subject: [PATCH 065/126] Ruff: Fix N805 (#13437) --- dojo/decorators.py | 2 +- dojo/importers/base_importer.py | 1 + dojo/importers/options.py | 2 ++ dojo/models.py | 18 +++++++++--------- dojo/user/views.py | 2 +- ruff.toml | 2 +- 6 files changed, 15 insertions(+), 12 deletions(-) diff --git a/dojo/decorators.py b/dojo/decorators.py index b7b84d59430..bba9efe234c 100644 --- a/dojo/decorators.py +++ b/dojo/decorators.py @@ -222,7 +222,7 @@ def _wrapped(request, *args, **kw): if username: dojo_user = Dojo_User.objects.filter(username=username).first() if dojo_user: - Dojo_User.enable_force_password_reset(dojo_user) + dojo_user.enable_force_password_reset() raise Ratelimited return fn(request, *args, **kw) return _wrapped diff --git a/dojo/importers/base_importer.py b/dojo/importers/base_importer.py index f6d754ba929..212c976dc33 100644 --- a/dojo/importers/base_importer.py +++ b/dojo/importers/base_importer.py @@ -49,6 +49,7 @@ class Parser: and is purely for the sake of type hinting """ + @staticmethod def get_findings(scan_type: str, test: Test) -> list[Finding]: """ Stub function to make the hinting happier. The actual class diff --git a/dojo/importers/options.py b/dojo/importers/options.py index b83a8b8597c..3b7c624235d 100644 --- a/dojo/importers/options.py +++ b/dojo/importers/options.py @@ -96,6 +96,7 @@ def log_translation( for field in self.field_names: logger.debug(f"{field}: {getattr(self, field)}") + @staticmethod def _compress_decorator(function): @wraps(function) def inner_compress_function(*args, **kwargs): @@ -103,6 +104,7 @@ def inner_compress_function(*args, **kwargs): return function(*args, **kwargs) return inner_compress_function + @staticmethod def _decompress_decorator(function): @wraps(function) def inner_decompress_function(*args, **kwargs): diff --git a/dojo/models.py b/dojo/models.py index d5b672c53c7..b61fe3abdd6 100644 --- a/dojo/models.py +++ b/dojo/models.py @@ -231,15 +231,15 @@ def wants_block_execution(user): def force_password_reset(user): return hasattr(user, "usercontactinfo") and user.usercontactinfo.force_password_reset - def disable_force_password_reset(user): - if hasattr(user, "usercontactinfo"): - user.usercontactinfo.force_password_reset = False - user.usercontactinfo.save() - - def enable_force_password_reset(user): - if hasattr(user, "usercontactinfo"): - user.usercontactinfo.force_password_reset = True - user.usercontactinfo.save() + def disable_force_password_reset(self): + if hasattr(self, "usercontactinfo"): + self.usercontactinfo.force_password_reset = False + self.usercontactinfo.save() + + def enable_force_password_reset(self): + if hasattr(self, "usercontactinfo"): + self.usercontactinfo.force_password_reset = True + self.usercontactinfo.save() @staticmethod def generate_full_name(user): diff --git a/dojo/user/views.py b/dojo/user/views.py index 603eb2e0db4..f4e2539d659 100644 --- a/dojo/user/views.py +++ b/dojo/user/views.py @@ -287,7 +287,7 @@ def change_password(request): new_password = form.cleaned_data["new_password"] user.set_password(new_password) - Dojo_User.disable_force_password_reset(user) + user.disable_force_password_reset() user.save() messages.add_message(request, diff --git a/ruff.toml b/ruff.toml index e169992b158..3b82e3cece0 100644 --- a/ruff.toml +++ b/ruff.toml @@ -74,7 +74,7 @@ select = [ "C90", "NPY", "PD", - "N803", "N804", "N811", "N812", "N813", "N814", "N817", "N818", "N999", + "N803", "N804", "N805", "N811", "N812", "N813", "N814", "N817", "N818", "N999", "PERF1", "PERF2", "PERF401", "PERF403", "E", "W", From 0dda8abb56c4a6cb11e09b86d5fd47edd6e8a165 Mon Sep 17 00:00:00 2001 From: kiblik <5609770+kiblik@users.noreply.github.com> Date: Mon, 20 Oct 2025 15:44:14 +0000 Subject: [PATCH 066/126] ruff: PT - simplify rules (#13435) --- ruff.toml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/ruff.toml b/ruff.toml index 3b82e3cece0..670a95b7b99 100644 --- a/ruff.toml +++ b/ruff.toml @@ -58,7 +58,7 @@ select = [ "PIE", "T20", "PYI", - "PT001", "PT002", "PT003", "PT006", "PT007", "PT008", "PT01", "PT020", "PT021", "PT022", "PT023", "PT024", "PT025", "PT026", "PT028", "PT029", "PT03", + "PT", "Q", "RSE", "RET", @@ -101,6 +101,8 @@ ignore = [ "FIX002", # TODOs need some love but we will probably not get of them "D211", # `one-blank-line-before-class` (D203) and `no-blank-line-before-class` (D211) are incompatible. "D212", # `multi-line-summary-first-line` (D212) and `multi-line-summary-second-line` (D213) are incompatible. + "PT009", # We are using a different style of tests (official Django tests), so it does not make sense to try to fix it + "PT027", # Same ^ ] # Allow autofix for all enabled rules (when `--fix`) is provided. From 70bba0cb1a978f1bdc8a0db4506e7fb6064d553a Mon Sep 17 00:00:00 2001 From: DefectDojo release bot Date: Mon, 20 Oct 2025 16:43:58 +0000 Subject: [PATCH 067/126] Update versions in application files --- components/package.json | 2 +- dojo/__init__.py | 2 +- helm/defectdojo/Chart.yaml | 14 ++++---------- helm/defectdojo/README.md | 2 +- 4 files changed, 7 insertions(+), 13 deletions(-) diff --git a/components/package.json b/components/package.json index cbbde92364d..e5898ca8f4d 100644 --- a/components/package.json +++ b/components/package.json @@ -1,6 +1,6 @@ { "name": "defectdojo", - "version": "2.51.2", + "version": "2.52.0-dev", "license" : "BSD-3-Clause", "private": true, "dependencies": { diff --git a/dojo/__init__.py b/dojo/__init__.py index 2e2f6c6c559..0a21544849b 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.51.2" +__version__ = "2.52.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 1b1074f42f3..ab39403225e 100644 --- a/helm/defectdojo/Chart.yaml +++ b/helm/defectdojo/Chart.yaml @@ -1,8 +1,8 @@ apiVersion: v2 -appVersion: "2.51.2" +appVersion: "2.52.0-dev" description: A Helm chart for Kubernetes to install DefectDojo name: defectdojo -version: 1.7.2 +version: 1.7.3-dev icon: https://defectdojo.com/hubfs/DefectDojo_favicon.png maintainers: - name: madchap @@ -33,11 +33,5 @@ dependencies: # - kind: security # description: Critical bug annotations: - artifacthub.io/prerelease: "false" - artifacthub.io/changes: | - - kind: fixed - description: Drop initialDelaySeconds if eq. zero - - kind: added - description: Add support for automountServiceAccountToken - - kind: changed - description: Bump DefectDojo to 2.51.2 + artifacthub.io/prerelease: "true" + artifacthub.io/changes: "" diff --git a/helm/defectdojo/README.md b/helm/defectdojo/README.md index d75efd43845..f968334e020 100644 --- a/helm/defectdojo/README.md +++ b/helm/defectdojo/README.md @@ -495,7 +495,7 @@ kubectl delete pvc data-defectdojo-redis-0 data-defectdojo-postgresql-0 # General information about chart values -![Version: 1.7.2](https://img.shields.io/badge/Version-1.7.2-informational?style=flat-square) ![AppVersion: 2.51.2](https://img.shields.io/badge/AppVersion-2.51.2-informational?style=flat-square) +![Version: 1.7.3-dev](https://img.shields.io/badge/Version-1.7.3--dev-informational?style=flat-square) ![AppVersion: 2.52.0-dev](https://img.shields.io/badge/AppVersion-2.52.0--dev-informational?style=flat-square) A Helm chart for Kubernetes to install DefectDojo From 0d7f0e0c9f781f897ac0a57f1a0eec35d34c45ff Mon Sep 17 00:00:00 2001 From: kiblik <5609770+kiblik@users.noreply.github.com> Date: Tue, 21 Oct 2025 17:09:57 +0000 Subject: [PATCH 068/126] feat(helm): Improve description about images/tags (#13473) --- helm/defectdojo/Chart.yaml | 2 +- helm/defectdojo/README.md | 43 ++++++++---------------------- helm/defectdojo/values.schema.json | 11 ++++++++ helm/defectdojo/values.yaml | 35 +++++++++++++++++------- 4 files changed, 49 insertions(+), 42 deletions(-) diff --git a/helm/defectdojo/Chart.yaml b/helm/defectdojo/Chart.yaml index fa3f37c86d7..9809fc3646f 100644 --- a/helm/defectdojo/Chart.yaml +++ b/helm/defectdojo/Chart.yaml @@ -38,7 +38,7 @@ annotations: - kind: changed description: DRY cloudsql-proxy - kind: changed - description: Each component allow to specific image + allow digest pinning + description: Each component allow to specific image + allow digest pinning + allow different tags for Django and Nginx - kind: added description: Convert existing comments to descriptors - kind: added diff --git a/helm/defectdojo/README.md b/helm/defectdojo/README.md index 4b4b062b89b..b6ac3127dd1 100644 --- a/helm/defectdojo/README.md +++ b/helm/defectdojo/README.md @@ -534,10 +534,7 @@ A Helm chart for Kubernetes to install DefectDojo | celery.beat.extraInitContainers | list | `[]` | A list of additional initContainers to run before celery beat containers. | | celery.beat.extraVolumeMounts | list | `[]` | Array of additional volume mount points for the celery beat containers. | | celery.beat.extraVolumes | list | `[]` | A list of extra volumes to mount @type: array | -| celery.beat.image.digest | string | `""` | | -| celery.beat.image.registry | string | `""` | | -| celery.beat.image.repository | string | `""` | | -| celery.beat.image.tag | string | `""` | | +| celery.beat.image | object | `{"digest":"","registry":"","repository":"","tag":""}` | If empty, uses values from images.django.image | | celery.beat.livenessProbe | object | `{}` | Enable liveness probe for Celery beat container. ``` exec: command: - bash - -c - celery -A dojo inspect ping -t 5 initialDelaySeconds: 30 periodSeconds: 60 timeoutSeconds: 10 ``` | | celery.beat.nodeSelector | object | `{}` | | | celery.beat.podAnnotations | object | `{}` | Annotations for the Celery beat pods. | @@ -561,10 +558,7 @@ A Helm chart for Kubernetes to install DefectDojo | celery.worker.extraInitContainers | list | `[]` | A list of additional initContainers to run before celery worker containers. | | celery.worker.extraVolumeMounts | list | `[]` | Array of additional volume mount points for the celery worker containers. | | celery.worker.extraVolumes | list | `[]` | A list of extra volumes to mount. @type: array | -| celery.worker.image.digest | string | `""` | | -| celery.worker.image.registry | string | `""` | | -| celery.worker.image.repository | string | `""` | | -| celery.worker.image.tag | string | `""` | | +| celery.worker.image | object | `{"digest":"","registry":"","repository":"","tag":""}` | If empty, uses values from images.django.image | | celery.worker.livenessProbe | object | `{}` | Enable liveness probe for Celery worker containers. ``` exec: command: - bash - -c - celery -A dojo inspect ping -t 5 initialDelaySeconds: 30 periodSeconds: 60 timeoutSeconds: 10 ``` | | celery.worker.nodeSelector | object | `{}` | | | celery.worker.podAnnotations | object | `{}` | Annotations for the Celery beat pods. | @@ -595,10 +589,7 @@ A Helm chart for Kubernetes to install DefectDojo | dbMigrationChecker.enabled | bool | `true` | Enable/disable the DB migration checker. | | dbMigrationChecker.extraEnv | list | `[]` | Additional environment variables for DB migration checker. | | dbMigrationChecker.extraVolumeMounts | list | `[]` | Array of additional volume mount points for DB migration checker. | -| dbMigrationChecker.image.digest | string | `""` | | -| dbMigrationChecker.image.registry | string | `""` | | -| dbMigrationChecker.image.repository | string | `""` | | -| dbMigrationChecker.image.tag | string | `""` | | +| dbMigrationChecker.image | object | `{"digest":"","registry":"","repository":"","tag":""}` | If empty, uses values from images.django.image | | dbMigrationChecker.resources | object | `{"limits":{"cpu":"200m","memory":"200Mi"},"requests":{"cpu":"100m","memory":"100Mi"}}` | Resource requests/limits for the DB migration checker. | | disableHooks | bool | `false` | Avoid using pre-install hooks, which might cause issues with ArgoCD | | django.affinity | object | `{}` | | @@ -623,10 +614,7 @@ A Helm chart for Kubernetes to install DefectDojo | django.nginx.containerSecurityContext.runAsUser | int | `1001` | nginx dockerfile sets USER=1001 | | django.nginx.extraEnv | list | `[]` | To extra environment variables to the nginx container, you can use extraEnv. For example: extraEnv: - name: FOO valueFrom: configMapKeyRef: name: foo key: bar | | django.nginx.extraVolumeMounts | list | `[]` | Array of additional volume mount points for nginx containers. | -| django.nginx.image.digest | string | `""` | | -| django.nginx.image.registry | string | `""` | | -| django.nginx.image.repository | string | `""` | | -| django.nginx.image.tag | string | `""` | | +| django.nginx.image | object | `{"digest":"","registry":"","repository":"","tag":""}` | If empty, uses values from images.nginx.image | | django.nginx.resources.limits.cpu | string | `"2000m"` | | | django.nginx.resources.limits.memory | string | `"256Mi"` | | | django.nginx.resources.requests.cpu | string | `"100m"` | | @@ -651,10 +639,7 @@ A Helm chart for Kubernetes to install DefectDojo | django.uwsgi.enableDebug | bool | `false` | this also requires DD_DEBUG to be set to True | | django.uwsgi.extraEnv | list | `[]` | 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 | | django.uwsgi.extraVolumeMounts | list | `[]` | Array of additional volume mount points for uwsgi containers. | -| django.uwsgi.image.digest | string | `""` | | -| django.uwsgi.image.registry | string | `""` | | -| django.uwsgi.image.repository | string | `""` | | -| django.uwsgi.image.tag | string | `""` | | +| django.uwsgi.image | object | `{"digest":"","registry":"","repository":"","tag":""}` | If empty, uses values from images.django.image | | django.uwsgi.livenessProbe.enabled | bool | `true` | Enable liveness checks on uwsgi container. | | django.uwsgi.livenessProbe.failureThreshold | int | `6` | | | django.uwsgi.livenessProbe.initialDelaySeconds | int | `0` | | @@ -689,14 +674,14 @@ A Helm chart for Kubernetes to install DefectDojo | host | string | `"defectdojo.default.minikube.local"` | Primary hostname of instance | | imagePullPolicy | string | `"Always"` | | | imagePullSecrets | string | `nil` | 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/' | -| images.django.image.digest | string | `""` | | +| images.django.image.digest | string | `""` | Prefix "sha@" is expected in this place | | images.django.image.registry | string | `""` | | | images.django.image.repository | string | `"defectdojo/defectdojo-django"` | | -| images.django.image.tag | string | `""` | | -| images.nginx.image.digest | string | `""` | | +| images.django.image.tag | string | `""` | If empty, use appVersion. Another possible values are: latest, X.X.X, X.X.X-debian, X.X.X-alpine (where X.X.X is version of DD). For dev builds (only for testing purposes): nightly-dev, nightly-dev-debian, nightly-dev-alpine. To see all, check https://hub.docker.com/r/defectdojo/defectdojo-django/tags. | +| images.nginx.image.digest | string | `""` | Prefix "sha@" is expected in this place | | images.nginx.image.registry | string | `""` | | | images.nginx.image.repository | string | `"defectdojo/defectdojo-nginx"` | | -| images.nginx.image.tag | string | `""` | | +| images.nginx.image.tag | string | `""` | If empty, use appVersion. Another possible values are: latest, X.X.X, X.X.X-alpine (where X.X.X is version of DD). For dev builds (only for testing purposes): nightly-dev, nightly-dev-alpine. To see all, check https://hub.docker.com/r/defectdojo/defectdojo-nginx/tags. | | initializer.affinity | object | `{}` | | | initializer.annotations | object | `{}` | | | initializer.automountServiceAccountToken | bool | `false` | | @@ -704,10 +689,7 @@ A Helm chart for Kubernetes to install DefectDojo | initializer.extraEnv | list | `[]` | Additional environment variables injected to the initializer job pods. | | initializer.extraVolumeMounts | list | `[]` | Array of additional volume mount points for the initializer job (init)containers. | | initializer.extraVolumes | list | `[]` | A list of extra volumes to attach to the initializer job pods. | -| initializer.image.digest | string | `""` | | -| initializer.image.registry | string | `""` | | -| initializer.image.repository | string | `""` | | -| initializer.image.tag | string | `""` | | +| initializer.image | object | `{"digest":"","registry":"","repository":"","tag":""}` | If empty, uses values from images.django.image | | initializer.jobAnnotations | object | `{}` | | | initializer.keepSeconds | int | `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. | | initializer.labels | object | `{}` | | @@ -759,10 +741,7 @@ A Helm chart for Kubernetes to install DefectDojo | serviceAccount.name | string | `""` | The name of the service account to use. If not set and create is true, a name is generated using the fullname template | | siteUrl | string | `""` | The full URL to your defectdojo instance, depends on the domain where DD is deployed, it also affects links in Jira. Use syntax: `siteUrl: 'https://'` | | tests.unitTests.automountServiceAccountToken | bool | `false` | | -| tests.unitTests.image.digest | string | `""` | | -| tests.unitTests.image.registry | string | `""` | | -| tests.unitTests.image.repository | string | `""` | | -| tests.unitTests.image.tag | string | `""` | | +| tests.unitTests.image | object | `{"digest":"","registry":"","repository":"","tag":""}` | If empty, uses values from images.django.image | | tests.unitTests.resources.limits.cpu | string | `"500m"` | | | tests.unitTests.resources.limits.memory | string | `"512Mi"` | | | tests.unitTests.resources.requests.cpu | string | `"100m"` | | diff --git a/helm/defectdojo/values.schema.json b/helm/defectdojo/values.schema.json index 03fb1dcc70a..d091be4e1a2 100644 --- a/helm/defectdojo/values.schema.json +++ b/helm/defectdojo/values.schema.json @@ -76,6 +76,7 @@ "type": "array" }, "image": { + "description": "If empty, uses values from images.django.image", "type": "object", "properties": { "digest": { @@ -199,6 +200,7 @@ "type": "array" }, "image": { + "description": "If empty, uses values from images.django.image", "type": "object", "properties": { "digest": { @@ -364,6 +366,7 @@ "type": "array" }, "image": { + "description": "If empty, uses values from images.django.image", "type": "object", "properties": { "digest": { @@ -531,6 +534,7 @@ "type": "array" }, "image": { + "description": "If empty, uses values from images.nginx.image", "type": "object", "properties": { "digest": { @@ -677,6 +681,7 @@ "type": "array" }, "image": { + "description": "If empty, uses values from images.django.image", "type": "object", "properties": { "digest": { @@ -858,6 +863,7 @@ "type": "object", "properties": { "digest": { + "description": "Prefix \"sha@\" is expected in this place", "type": "string" }, "registry": { @@ -867,6 +873,7 @@ "type": "string" }, "tag": { + "description": "If empty, use appVersion. Another possible values are: latest, X.X.X, X.X.X-debian, X.X.X-alpine (where X.X.X is version of DD). For dev builds (only for testing purposes): nightly-dev, nightly-dev-debian, nightly-dev-alpine. To see all, check https://hub.docker.com/r/defectdojo/defectdojo-django/tags.", "type": "string" } } @@ -880,6 +887,7 @@ "type": "object", "properties": { "digest": { + "description": "Prefix \"sha@\" is expected in this place", "type": "string" }, "registry": { @@ -889,6 +897,7 @@ "type": "string" }, "tag": { + "description": "If empty, use appVersion. Another possible values are: latest, X.X.X, X.X.X-alpine (where X.X.X is version of DD). For dev builds (only for testing purposes): nightly-dev, nightly-dev-alpine. To see all, check https://hub.docker.com/r/defectdojo/defectdojo-nginx/tags.", "type": "string" } } @@ -926,6 +935,7 @@ "type": "array" }, "image": { + "description": "If empty, uses values from images.django.image", "type": "object", "properties": { "digest": { @@ -1353,6 +1363,7 @@ "type": "boolean" }, "image": { + "description": "If empty, uses values from images.django.image", "type": "object", "properties": { "digest": { diff --git a/helm/defectdojo/values.yaml b/helm/defectdojo/values.yaml index 5fd9603b82c..419fe3fe743 100644 --- a/helm/defectdojo/values.yaml +++ b/helm/defectdojo/values.yaml @@ -32,13 +32,23 @@ images: image: registry: "" repository: defectdojo/defectdojo-django - tag: "" # If empty, use appVersion + # -- If empty, use appVersion. + # Another possible values are: latest, X.X.X, X.X.X-debian, X.X.X-alpine (where X.X.X is version of DD). + # For dev builds (only for testing purposes): nightly-dev, nightly-dev-debian, nightly-dev-alpine. + # To see all, check https://hub.docker.com/r/defectdojo/defectdojo-django/tags. + tag: "" + # -- Prefix "sha@" is expected in this place digest: "" nginx: image: registry: "" repository: defectdojo/defectdojo-nginx - tag: "" # If empty, use appVersion + # -- If empty, use appVersion. + # Another possible values are: latest, X.X.X, X.X.X-alpine (where X.X.X is version of DD). + # For dev builds (only for testing purposes): nightly-dev, nightly-dev-alpine. + # To see all, check https://hub.docker.com/r/defectdojo/defectdojo-nginx/tags. + tag: "" + # -- Prefix "sha@" is expected in this place digest: "" # -- Enables application network policy @@ -124,7 +134,8 @@ serviceAccount: labels: {} dbMigrationChecker: - image: # If empty, uses values from images.django.image + # -- If empty, uses values from images.django.image + image: registry: "" repository: "" tag: "" @@ -148,7 +159,8 @@ dbMigrationChecker: tests: unitTests: - image: # If empty, uses values from images.django.image + # -- If empty, uses values from images.django.image + image: registry: "" repository: "" tag: "" @@ -203,7 +215,8 @@ celery: # -- Common annotations to worker and beat deployments and pods. annotations: {} beat: - image: # If empty, uses values from images.django.image + # -- If empty, uses values from images.django.image + image: registry: "" repository: "" tag: "" @@ -254,7 +267,8 @@ celery: startupProbe: {} tolerations: [] worker: - image: # If empty, uses values from images.django.image + # -- If empty, uses values from images.django.image + image: registry: "" repository: "" tag: "" @@ -335,7 +349,8 @@ django: # `nginx.ingress.kubernetes.io/proxy-send-timeout: "1800"` annotations: {} nginx: - image: # If empty, uses values from images.nginx.image + # -- If empty, uses values from images.nginx.image + image: registry: "" repository: "" tag: "" @@ -369,7 +384,8 @@ django: strategy: {} tolerations: [] uwsgi: - image: # If empty, uses values from images.django.image + # -- If empty, uses values from images.django.image + image: registry: "" repository: "" tag: "" @@ -475,7 +491,8 @@ initializer: affinity: {} nodeSelector: {} tolerations: [] - image: # If empty, uses values from images.django.image + # -- If empty, uses values from images.django.image + image: registry: "" repository: "" tag: "" From 2591fd3952445c94e2bbcdbf84edebe2ce0e3b9a Mon Sep 17 00:00:00 2001 From: Zeke Date: Wed, 22 Oct 2025 14:25:55 -0400 Subject: [PATCH 069/126] Split Github Vulnerability Scan into separate SCA & SAST parsers (#12773) * Refactor GithubVulnerability parser and add GithubSAST parser * More GithubVulnerability and GithubSAST parser improvements * Add documentation * Add tests, update docs, and add hash code fields * Fix Github vulnerability parser unit test * Unit tests and parser tweaks * Rm files pushed by mistake * Revert certain removals from unit test * Add EPSS field population and update unit tests * Removed some unnecessary comments and formatting * Ruff formatting * Fix unit tests * Ruff formatting * Fix unit test * Github Vulnerability parser and docs tweaks, and upgrade instructions * Politeness * Fix dependabot update pr link parsing * Backwards compatability * Revert 2.49 docs change and add 2.51 * Add 2.51 upgrade doc * Smol 2.51 upgrade doc fix * Move imports to top * Ruff lint fix --------- Co-authored-by: Zeke Tierkel Co-authored-by: valentijnscholten --- .../parsers/file/github_sast.md | 9 + .../parsers/file/github_vulnerability.md | 13 +- docs/content/en/open_source/upgrading/2.51.md | 7 + dojo/settings/settings.dist.py | 2 + dojo/tools/github_sast/__init__.py | 0 dojo/tools/github_sast/parser.py | 84 +++++++ dojo/tools/github_vulnerability/parser.py | 231 ++++++++++-------- .../github_sast/github_sast_many_vul.json | 107 ++++++++ .../github_sast/github_sast_one_vul.json | 53 ++++ .../github_sast/github_sast_zero_vul.json | 1 + .../github-1-vuln-repo-dependabot-link.json | 6 + .../github-vuln-version.json | 9 + .../github_vulnerability/issue_9582.json | 222 ++++++++--------- unittests/tools/test_github_sast_parser.py | 53 ++++ .../tools/test_github_vulnerability_parser.py | 95 +++++-- 15 files changed, 647 insertions(+), 245 deletions(-) create mode 100644 docs/content/en/connecting_your_tools/parsers/file/github_sast.md create mode 100644 dojo/tools/github_sast/__init__.py create mode 100644 dojo/tools/github_sast/parser.py create mode 100644 unittests/scans/github_sast/github_sast_many_vul.json create mode 100644 unittests/scans/github_sast/github_sast_one_vul.json create mode 100644 unittests/scans/github_sast/github_sast_zero_vul.json create mode 100644 unittests/tools/test_github_sast_parser.py diff --git a/docs/content/en/connecting_your_tools/parsers/file/github_sast.md b/docs/content/en/connecting_your_tools/parsers/file/github_sast.md new file mode 100644 index 00000000000..a551d9ea0ef --- /dev/null +++ b/docs/content/en/connecting_your_tools/parsers/file/github_sast.md @@ -0,0 +1,9 @@ +--- +title: "Github SAST Scan" +toc_hide: true +--- +Import findings in JSON format from Github Code Scanning REST API: + + +### Sample Scan Data +Sample Github SAST scans can be found [here](https://github.com/DefectDojo/django-DefectDojo/tree/master/unittests/scans/github_sast). \ No newline at end of file diff --git a/docs/content/en/connecting_your_tools/parsers/file/github_vulnerability.md b/docs/content/en/connecting_your_tools/parsers/file/github_vulnerability.md index 4d92f546685..5705165913a 100644 --- a/docs/content/en/connecting_your_tools/parsers/file/github_vulnerability.md +++ b/docs/content/en/connecting_your_tools/parsers/file/github_vulnerability.md @@ -1,5 +1,5 @@ --- -title: "Github Vulnerability" +title: "Github Vulnerability Scan" toc_hide: true --- Import findings from Github vulnerability scan (GraphQL Query): @@ -15,6 +15,8 @@ vulnerabilityAlerts (RepositoryVulnerabilityAlert object) + createdAt (optional) + vulnerableManifestPath + state (optional) + + dependabotUpdate (DependabotUpdate object) (optional) + + pullRequest (PullRequest object) (optional) + securityVulnerability (SecurityVulnerability object) + severity (CRITICAL/HIGH/LOW/MODERATE) + package (optional) @@ -27,10 +29,17 @@ vulnerabilityAlerts (RepositoryVulnerabilityAlert object) + value + references (optional) + url (optional) - + cvss (optional) + + cvss (optional - deprecated, use cvssSeverities instead) + score (optional) + vectorString (optional) + + cvssSeverities (optional) + + cvssV3 (CVSS object) (optional) + + score (optional) + + vectorString (optional) + cwes (optional) + + epss (EPSS object) (optional) + + percentage (optional) + + percentile (optional) ``` References: diff --git a/docs/content/en/open_source/upgrading/2.51.md b/docs/content/en/open_source/upgrading/2.51.md index e3cf71186cc..63306aa2dd2 100644 --- a/docs/content/en/open_source/upgrading/2.51.md +++ b/docs/content/en/open_source/upgrading/2.51.md @@ -48,6 +48,13 @@ The following Helm chart values have been modified in this release: - **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. +## GitHub Scan Type and Parser Updates +The Github Vulnerability scan type and parser has been split into two disctinct scan types: +- [Github Vulnerability](https://github.com/DefectDojo/django-DefectDojo/blob/master/docs/content/en/connecting_your_tools/parsers/file/github_vulnerability.md) (original) +- [Github SAST](https://github.com/DefectDojo/django-DefectDojo/blob/master/docs/content/en/connecting_your_tools/parsers/file/github_sast.md) + +The original Github Vulnerability scan type will continue to accept SCA vulnerabilities uploaded in GitHub's GraphQL format, as it has always done. It will also continue to accept SAST uploads, however we recommend upgrading to the new Github SAST scan type for uploading these types of vulnerabilities going forward. This new scan type will accept the raw JSON response from [GitHub's REST API for code scanning alerts](https://docs.github.com/en/rest/code-scanning/code-scanning). Sample Github SAST scan data can be found [here](https://github.com/DefectDojo/django-DefectDojo/tree/master/unittests/scans/github_sast). + ### Other changes - **Celery pod annotations**: Now we can add annotations to Celery beat/worker pods separately. diff --git a/dojo/settings/settings.dist.py b/dojo/settings/settings.dist.py index d67d289d929..4e28c24b8ed 100644 --- a/dojo/settings/settings.dist.py +++ b/dojo/settings/settings.dist.py @@ -1337,6 +1337,7 @@ def saml2_attrib_map_format(din): "JFrog Xray On Demand Binary Scan": ["title", "component_name", "component_version"], "Scout Suite Scan": ["file_path", "vuln_id_from_tool"], # for now we use file_path as there is no attribute for "service" "Meterian Scan": ["cwe", "component_name", "component_version", "description", "severity"], + "Github SAST Scan": ["vuln_id_from_tool", "severity", "file_path", "line"], "Github Vulnerability Scan": ["title", "severity", "component_name", "vulnerability_ids", "file_path"], "Github Secrets Detection Report": ["title", "file_path", "line"], "Solar Appscreener Scan": ["title", "file_path", "line", "severity"], @@ -1583,6 +1584,7 @@ def saml2_attrib_map_format(din): "Scout Suite Scan": DEDUPE_ALGO_HASH_CODE, "AWS Security Hub Scan": DEDUPE_ALGO_UNIQUE_ID_FROM_TOOL, "Meterian Scan": DEDUPE_ALGO_HASH_CODE, + "Github SAST Scan": DEDUPE_ALGO_HASH_CODE, "Github Vulnerability Scan": DEDUPE_ALGO_HASH_CODE, "Github Secrets Detection Report": DEDUPE_ALGO_HASH_CODE, "Cloudsploit Scan": DEDUPE_ALGO_HASH_CODE, diff --git a/dojo/tools/github_sast/__init__.py b/dojo/tools/github_sast/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/dojo/tools/github_sast/parser.py b/dojo/tools/github_sast/parser.py new file mode 100644 index 00000000000..4d20e71623a --- /dev/null +++ b/dojo/tools/github_sast/parser.py @@ -0,0 +1,84 @@ +import json +from urllib.parse import urlparse + +from dojo.models import Finding + + +class GithubSASTParser: + def get_scan_types(self): + return ["Github SAST Scan"] + + def get_label_for_scan_types(self, scan_type): + return scan_type + + def get_description_for_scan_types(self, scan_type): + return "GitHub SAST report file can be imported in JSON format." + + def get_findings(self, filename, test): + data = json.load(filename) + if not isinstance(data, list): + error_msg = "Invalid SAST report format, expected a JSON list of alerts." + raise TypeError(error_msg) + + findings = [] + for vuln in data: + rule = vuln.get("rule", {}) + inst = vuln.get("most_recent_instance", {}) + loc = inst.get("location", {}) + html_url = vuln.get("html_url") + rule_id = rule.get("id") + title = f"{rule.get('description')} ({rule_id})" + severity = rule.get("security_severity_level", "Info").title() + active = vuln.get("state") == "open" + + # Build description with context + desc_lines = [] + if html_url: + desc_lines.append(f"GitHub Alert: [{html_url}]({html_url})") + owner = repo = None + commit_sha = inst.get("commit_sha") + if html_url: + parsed = urlparse(html_url) + parts = parsed.path.strip("/").split("/") + # URL is ///security/... so parts[0]=owner, parts[1]=repo + if len(parts) >= 2: + owner, repo = parts[0], parts[1] + if owner and repo and commit_sha and loc.get("path") and loc.get("start_line"): + file_link = ( + f"{parsed.scheme}://{parsed.netloc}/" + f"{owner}/{repo}/blob/{commit_sha}/" + f"{loc['path']}#L{loc['start_line']}" + ) + desc_lines.append(f"Location: [{loc['path']}:{loc['start_line']}]({file_link})") + elif loc.get("path") and loc.get("start_line"): + # fallback if something is missing + desc_lines.append(f"Location: {loc['path']}:{loc['start_line']}") + msg = inst.get("message", {}).get("text") + if msg: + desc_lines.append(f"Message: {msg}") + if severity: + desc_lines.append(f"Rule Severity: {severity}") + if rule.get("full_description"): + desc_lines.append(f"Description: {rule.get('full_description')}") + description = "\n".join(desc_lines) + + finding = Finding( + title=title, + test=test, + description=description, + severity=severity, + active=active, + static_finding=True, + dynamic_finding=False, + vuln_id_from_tool=rule_id, + ) + + # File path & line + finding.file_path = loc.get("path") + finding.line = loc.get("start_line") + + if html_url: + finding.url = html_url + + findings.append(finding) + return findings diff --git a/dojo/tools/github_vulnerability/parser.py b/dojo/tools/github_vulnerability/parser.py index aa958934cc7..5c646086aeb 100644 --- a/dojo/tools/github_vulnerability/parser.py +++ b/dojo/tools/github_vulnerability/parser.py @@ -18,114 +18,128 @@ def get_description_for_scan_types(self, scan_type): def get_findings(self, filename, test): data = json.load(filename) - if "data" in data: - vulnerabilityAlerts = self._search_vulnerability_alerts(data["data"]) - if not vulnerabilityAlerts: - msg = "Invalid report, no 'vulnerabilityAlerts' node found" - raise ValueError(msg) - repository_url = None - if "repository" in data["data"]: - if "nameWithOwner" in data["data"]["repository"]: - repository_url = "https://github.com/{}".format( - data["data"]["repository"]["nameWithOwner"], - ) - if "url" in data["data"]["repository"]: - repository_url = data["data"]["repository"]["url"] + + if isinstance(data, dict): + if "data" not in data: + error_msg = ( + "Invalid report format, expected a GitHub RepositoryVulnerabilityAlert GraphQL query response." + ) + raise ValueError(error_msg) + + alerts = self._search_vulnerability_alerts(data.get("data")) + if not alerts: + error_msg = "Invalid report, no 'vulnerabilityAlerts' node found" + raise ValueError(error_msg) + + repo = data.get("data").get("repository", {}) + repo_url = repo.get("url") + dupes = {} - for alert in vulnerabilityAlerts["nodes"]: - description = alert["securityVulnerability"]["advisory"][ - "description" - ] - if "number" in alert and repository_url is not None: - dependabot_url = ( - repository_url - + "/security/dependabot/{}".format(alert["number"]) - ) - description = ( - f"[{dependabot_url}]({dependabot_url})\n" - + description - ) + for alert in alerts.get("nodes", []): + vuln = alert.get("securityVulnerability", {}) + advisory = vuln.get("advisory", {}) + summary = advisory.get("summary", "") + desc = advisory.get("description", "") + + pr_link = None + dependabot_update = alert.get("dependabotUpdate", {}) + if dependabot_update: + pr = dependabot_update.get("pullRequest", {}) + if pr: + pr_link = pr.get("permalink") + desc = f"Fix PR: [{pr_link}]({pr_link})\n" + desc + + alert_num = alert.get("number") + if alert_num and repo_url: + alert_link = f"{repo_url}/security/dependabot/{alert_num}" + desc = f"Repo Alert: [{alert_link}]({alert_link})\n" + desc + finding = Finding( - title=alert["securityVulnerability"]["advisory"]["summary"], + title=summary, test=test, - description=description, - severity=self._convert_security( - alert["securityVulnerability"].get("severity", "MODERATE"), - ), + description=desc, + severity=self._convert_security(vuln.get("severity", "MODERATE")), static_finding=True, dynamic_finding=False, - unique_id_from_tool=alert["id"], + unique_id_from_tool=alert.get("id"), ) - if "vulnerableManifestPath" in alert: - finding.file_path = alert["vulnerableManifestPath"] - if "vulnerableRequirements" in alert and alert["vulnerableRequirements"].startswith("= "): - finding.component_version = alert["vulnerableRequirements"][2:] - if "createdAt" in alert: - finding.date = dateutil.parser.parse(alert["createdAt"]) - if "state" in alert and ( - alert["state"] == "FIXED" or alert["state"] == "DISMISSED" - ): + + if alert_num and repo_url: + finding.url = alert_link + + cwes = advisory.get("cwes", {}).get("nodes", []) + if cwes: + cwe_id = cwes[0].get("cweId", "")[4:] + if cwe_id.isdigit(): + finding.cwe = int(cwe_id) + + if alert.get("vulnerableManifestPath"): + finding.file_path = alert.get("vulnerableManifestPath") + req = alert.get("vulnerableRequirements", "") + if req.startswith("= "): + finding.component_version = req[2:] + elif req: + finding.component_version = req + pkg = vuln.get("package", {}) + finding.component_name = pkg.get("name") + + if alert.get("createdAt"): + finding.date = dateutil.parser.parse(alert.get("createdAt")) + if alert.get("state") in {"FIXED", "DISMISSED"}: finding.active = False finding.is_mitigated = True - # if the package is present - if "package" in alert["securityVulnerability"]: - finding.component_name = alert["securityVulnerability"][ - "package" - ].get("name") - if "references" in alert["securityVulnerability"]["advisory"]: - finding.references = "" - for ref in alert["securityVulnerability"]["advisory"][ - "references" - ]: - finding.references += ref["url"] + "\r\n" - if "identifiers" in alert["securityVulnerability"]["advisory"]: - unsaved_vulnerability_ids = [identifier.get("value") for identifier in alert["securityVulnerability"]["advisory"][ - "identifiers" - ] if identifier.get("value")] - if unsaved_vulnerability_ids: - finding.unsaved_vulnerability_ids = ( - unsaved_vulnerability_ids - ) - if "cvss" in alert["securityVulnerability"]["advisory"]: - if ( - "score" - in alert["securityVulnerability"]["advisory"]["cvss"] - ): - score = alert["securityVulnerability"]["advisory"]["cvss"][ - "score" - ] + + ref_urls = [r.get("url") for r in advisory.get("references", []) if r.get("url")] + if alert_num and repo_url: + ref_urls.append(alert_link) + if pr_link: + ref_urls.append(pr_link) + if ref_urls: + finding.references = "\r\n".join(ref_urls) + + ids = [i.get("value") for i in advisory.get("identifiers", []) if i.get("value")] + if ids: + for identifier in ids: + if identifier.startswith("CVE-"): + finding.cve = identifier + elif identifier.startswith("GHSA-"): + finding.vuln_id_from_tool = identifier + if not finding.vuln_id_from_tool: + finding.vuln_id_from_tool = ids[0] + finding.unsaved_vulnerability_ids = ids + + # cvss is deprecated, so we favor cvssSeverities if it exists + for key in ("cvssSeverities", "cvss"): + cvss = advisory.get(key, {}) + if key == "cvssSeverities" and cvss: + cvss = cvss.get("cvssV3", {}) + if cvss: + score = cvss.get("score") if score is not None: finding.cvssv3_score = score - if ( - "vectorString" - in alert["securityVulnerability"]["advisory"]["cvss"] - ): - cvss_vector_string = alert["securityVulnerability"][ - "advisory" - ]["cvss"]["vectorString"] - if cvss_vector_string is not None: - cvss_objects = cvss_parser.parse_cvss_from_text( - cvss_vector_string, - ) - if len(cvss_objects) > 0: - finding.cvssv3 = cvss_objects[0].clean_vector() - if ( - "cwes" in alert["securityVulnerability"]["advisory"] - and "nodes" - in alert["securityVulnerability"]["advisory"]["cwes"] - ): - cwe_nodes = alert["securityVulnerability"]["advisory"]["cwes"][ - "nodes" - ] - if cwe_nodes and len(cwe_nodes) > 0: - finding.cwe = int(cwe_nodes[0].get("cweId")[4:]) + vec = cvss.get("vectorString") + if vec: + parsed = cvss_parser.parse_cvss_from_text(vec) + if parsed: + finding.cvssv3 = parsed[0].clean_vector() + break + + epss = advisory.get("epss", {}) + percentage = epss.get("percentage") + percentile = epss.get("percentile") + if percentage is not None: + finding.epss_score = percentage + if percentile is not None: + finding.epss_percentile = percentile + dupe_key = finding.unique_id_from_tool if dupe_key in dupes: - find = dupes[dupe_key] - find.nb_occurences += 1 + dupes[dupe_key].nb_occurences += 1 else: dupes[dupe_key] = finding + return list(dupes.values()) + if isinstance(data, list): findings = [] for vuln in data: @@ -177,24 +191,25 @@ def get_findings(self, filename, test): ) findings.append(finding) return findings - return None + error_msg = ( + "Invalid report format, expected a GitHub RepositoryVulnerabilityAlert GraphQL query response." + ) + raise TypeError(error_msg) def _search_vulnerability_alerts(self, data): - if isinstance(data, list): + if isinstance(data, dict): + if "vulnerabilityAlerts" in data: + return data["vulnerabilityAlerts"] + for v in data.values(): + res = self._search_vulnerability_alerts(v) + if res: + return res + elif isinstance(data, list): for item in data: - result = self._search_vulnerability_alerts(item) - if result: - return result - elif isinstance(data, dict): - for key in data: - if key == "vulnerabilityAlerts": - return data[key] - result = self._search_vulnerability_alerts(data[key]) - if result: - return result + res = self._search_vulnerability_alerts(item) + if res: + return res return None def _convert_security(self, val): - if val.lower() == "moderate": - return "Medium" - return val.title() + return "Medium" if val.lower() == "moderate" else val.title() diff --git a/unittests/scans/github_sast/github_sast_many_vul.json b/unittests/scans/github_sast/github_sast_many_vul.json new file mode 100644 index 00000000000..ca03ebf4094 --- /dev/null +++ b/unittests/scans/github_sast/github_sast_many_vul.json @@ -0,0 +1,107 @@ +[ + { + "number":35, + "created_at":"2024-01-19T14:11:18Z", + "updated_at":"2024-01-19T14:11:20Z", + "url":"https://api.github.com/repos/OWASP/test-repository/code-scanning/alerts/35", + "html_url":"https://github.com/OWASP/test-repository/security/code-scanning/35", + "state":"open", + "fixed_at":"None", + "dismissed_by":"None", + "dismissed_at":"None", + "dismissed_reason":"None", + "dismissed_comment":"None", + "rule":{ + "id":"py/clear-text-storage-sensitive-data", + "severity":"error", + "description":"Clear-text storage of sensitive information", + "name":"py/clear-text-storage-sensitive-data", + "tags":[ + "external/cwe/cwe-312", + "external/cwe/cwe-315", + "external/cwe/cwe-359", + "security" + ], + "security_severity_level":"high" + }, + "tool":{ + "name":"CodeQL", + "guid":"None", + "version":"2.16.2" + }, + "most_recent_instance":{ + "ref":"refs/OWASP/test-repository", + "analysis_key":"dynamic/github-code-scanning/codeql:analyze", + "environment":"{\"language\":\"python\"}", + "category":"/language:python", + "state":"open", + "commit_sha":"XXX", + "message":{ + "text":"This expression stores sensitive data (secret) as clear text." + }, + "location":{ + "path":"src/file.py", + "start_line":42, + "end_line":42, + "start_column":17, + "end_column":23 + }, + "classifications":[] + }, + "instances_url":"https://api.github.com/repos/OWASP/test-repository/code-scanning/alerts/35/instances" + }, + { + "number":34, + "created_at":"2024-01-19T14:11:18Z", + "updated_at":"2024-01-19T14:11:20Z", + "url":"https://api.github.com/repos/OWASP/test-repository/code-scanning/alerts/34", + "html_url":"https://github.com/OWASP/test-repository/security/code-scanning/34", + "state":"open", + "fixed_at":"None", + "dismissed_by":"None", + "dismissed_at":"None", + "dismissed_reason":"None", + "dismissed_comment":"None", + "rule":{ + "id":"py/path-injection", + "severity":"error", + "description":"Uncontrolled data used in path expression", + "name":"py/path-injection", + "tags":[ + "correctness", + "external/cwe/cwe-022", + "external/cwe/cwe-023", + "external/cwe/cwe-036", + "external/cwe/cwe-073", + "external/cwe/cwe-099", + "security" + ], + "security_severity_level":"high" + }, + "tool":{ + "name":"CodeQL", + "guid":"None", + "version":"2.16.2" + }, + "most_recent_instance":{ + "ref":"refs/OWASP/test-repository", + "analysis_key":"dynamic/github-code-scanning/codeql:analyze", + "environment":"{\"language\":\"python\"}", + "category":"/language:python", + "state":"open", + "commit_sha":"XXX", + "message":{ + "text":"This path depends on a user-provided value." + }, + "location":{ + "path":"src/file2.py", + "start_line":78, + "end_line":78, + "start_column":25, + "end_column":63 + }, + "classifications":[] + }, + "instances_url":"https://api.github.com/repos/OWASP/test-repository/code-scanning/alerts/34/instances" + } + ] \ No newline at end of file diff --git a/unittests/scans/github_sast/github_sast_one_vul.json b/unittests/scans/github_sast/github_sast_one_vul.json new file mode 100644 index 00000000000..cd598f7077e --- /dev/null +++ b/unittests/scans/github_sast/github_sast_one_vul.json @@ -0,0 +1,53 @@ +[ + { + "number":35, + "created_at":"2024-01-19T14:11:18Z", + "updated_at":"2024-01-19T14:11:20Z", + "url":"https://api.github.com/repos/OWASP/test-repository/code-scanning/alerts/35", + "html_url":"https://github.com/OWASP/test-repository/security/code-scanning/35", + "state":"open", + "fixed_at":"None", + "dismissed_by":"None", + "dismissed_at":"None", + "dismissed_reason":"None", + "dismissed_comment":"None", + "rule":{ + "id":"py/clear-text-storage-sensitive-data", + "severity":"error", + "description":"Clear-text storage of sensitive information", + "name":"py/clear-text-storage-sensitive-data", + "tags":[ + "external/cwe/cwe-312", + "external/cwe/cwe-315", + "external/cwe/cwe-359", + "security" + ], + "security_severity_level":"high" + }, + "tool":{ + "name":"CodeQL", + "guid":"None", + "version":"2.16.2" + }, + "most_recent_instance":{ + "ref":"refs/OWASP/test-repository", + "analysis_key":"dynamic/github-code-scanning/codeql:analyze", + "environment":"{\"language\":\"python\"}", + "category":"/language:python", + "state":"open", + "commit_sha":"XXX", + "message":{ + "text":"This expression stores sensitive data (secret) as clear text." + }, + "location":{ + "path":"src/file.py", + "start_line":42, + "end_line":42, + "start_column":17, + "end_column":23 + }, + "classifications":[] + }, + "instances_url":"https://api.github.com/repos/OWASP/test-repository/code-scanning/alerts/35/instances" + } + ] \ No newline at end of file diff --git a/unittests/scans/github_sast/github_sast_zero_vul.json b/unittests/scans/github_sast/github_sast_zero_vul.json new file mode 100644 index 00000000000..0637a088a01 --- /dev/null +++ b/unittests/scans/github_sast/github_sast_zero_vul.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/unittests/scans/github_vulnerability/github-1-vuln-repo-dependabot-link.json b/unittests/scans/github_vulnerability/github-1-vuln-repo-dependabot-link.json index 4c493c8360a..bdb216d707a 100644 --- a/unittests/scans/github_vulnerability/github-1-vuln-repo-dependabot-link.json +++ b/unittests/scans/github_vulnerability/github-1-vuln-repo-dependabot-link.json @@ -2,6 +2,7 @@ "data": { "repository": { "nameWithOwner": "OWASP/test-repository", + "url": "https://github.com/OWASP/test-repository", "search": { "nodes": [ { @@ -10,6 +11,11 @@ { "id": "aabbccddeeff1122334401", "number": "1", + "dependabotUpdate": { + "pullRequest": { + "permalink": "https://github.com/OWASP/test-repository/pull/1" + } + }, "securityVulnerability": { "severity": "CRITICAL", "package": { diff --git a/unittests/scans/github_vulnerability/github-vuln-version.json b/unittests/scans/github_vulnerability/github-vuln-version.json index e80afe7e583..7b1ac5e0037 100644 --- a/unittests/scans/github_vulnerability/github-vuln-version.json +++ b/unittests/scans/github_vulnerability/github-vuln-version.json @@ -7,6 +7,11 @@ "id": "RVA_kwDOLJyUo88AAAABQUWapw", "createdAt": "2024-01-26T02:42:32Z", "vulnerableManifestPath": "sompath/pom.xml", + "dependabotUpdate": { + "pullRequest": { + "permalink": "https://github.com/OWASP/test-repository/pull/1" + } + }, "securityVulnerability": { "severity": "CRITICAL", "updatedAt": "2022-12-09T22:02:22Z", @@ -21,6 +26,10 @@ "advisory": { "description": "Pivotal Spring Framework before 6.0.0 suffers from a potential remote code execution (RCE) issue if used for Java deserialization of untrusted data. Depending on how the library is implemented within a product, this issue may or not occur, and authentication may be required.\n\nMaintainers recommend investigating alternative components or a potential mitigating control. Version 4.2.6 and 3.2.17 contain [enhanced documentation](https://github.com/spring-projects/spring-framework/commit/5cbe90b2cd91b866a5a9586e460f311860e11cfa) advising users to take precautions against unsafe Java deserialization, version 5.3.0 [deprecate the impacted classes](https://github.com/spring-projects/spring-framework/issues/25379) and version 6.0.0 [removed it entirely](https://github.com/spring-projects/spring-framework/issues/27422).", "summary": "Pivotal Spring Framework contains unsafe Java deserialization methods", + "epss": { + "percentage": 0.00212, + "percentile": 0.44035 + }, "identifiers": [ { "value": "GHSA-4wrc-f8pq-fpqp", diff --git a/unittests/scans/github_vulnerability/issue_9582.json b/unittests/scans/github_vulnerability/issue_9582.json index 7e297d8f1b2..4500ffcd6e7 100644 --- a/unittests/scans/github_vulnerability/issue_9582.json +++ b/unittests/scans/github_vulnerability/issue_9582.json @@ -1,111 +1,111 @@ -[ - { - "number":35, - "created_at":"2024-01-19T14:11:18Z", - "updated_at":"2024-01-19T14:11:20Z", - "url":"https://api.github.com/repos/XX/YY/code-scanning/alerts/35", - "html_url":"https://github.com/XX/YY/security/code-scanning/35", - "state":"open", - "fixed_at":"None", - "dismissed_by":"None", - "dismissed_at":"None", - "dismissed_reason":"None", - "dismissed_comment":"None", - "rule":{ - "id":"py/clear-text-storage-sensitive-data", - "severity":"error", - "description":"Clear-text storage of sensitive information", - "name":"py/clear-text-storage-sensitive-data", - "tags":[ - "external/cwe/cwe-312", - "external/cwe/cwe-315", - "external/cwe/cwe-359", - "security" - ], - "security_severity_level":"high" - }, - "tool":{ - "name":"CodeQL", - "guid":"None", - "version":"2.16.2" - }, - "most_recent_instance":{ - "ref":"refs/XX/YY", - "analysis_key":"dynamic/github-code-scanning/codeql:analyze", - "environment":"{\"language\":\"python\"}", - "category":"/language:python", - "state":"open", - "commit_sha":"XXX", - "message":{ - "text":"This expression stores sensitive data (secret) as clear text." - }, - "location":{ - "path":"Unsafe Deserialization/file.py", - "start_line":42, - "end_line":42, - "start_column":17, - "end_column":23 - }, - "classifications":[ - - ] - }, - "instances_url":"https://api.github.com/repos/XX/YY/code-scanning/alerts/35/instances" - }, - { - "number":34, - "created_at":"2024-01-19T14:11:18Z", - "updated_at":"2024-01-19T14:11:20Z", - "url":"https://api.github.com/repos/XX/YY/code-scanning/alerts/34", - "html_url":"https://github.com/XX/YY/security/code-scanning/34", - "state":"open", - "fixed_at":"None", - "dismissed_by":"None", - "dismissed_at":"None", - "dismissed_reason":"None", - "dismissed_comment":"None", - "rule":{ - "id":"py/path-injection", - "severity":"error", - "description":"Uncontrolled data used in path expression", - "name":"py/path-injection", - "tags":[ - "correctness", - "external/cwe/cwe-022", - "external/cwe/cwe-023", - "external/cwe/cwe-036", - "external/cwe/cwe-073", - "external/cwe/cwe-099", - "security" - ], - "security_severity_level":"high" - }, - "tool":{ - "name":"CodeQL", - "guid":"None", - "version":"2.16.2" - }, - "most_recent_instance":{ - "ref":"refs/XX/YY", - "analysis_key":"dynamic/github-code-scanning/codeql:analyze", - "environment":"{\"language\":\"python\"}", - "category":"/language:python", - "state":"open", - "commit_sha":"XXX", - "message":{ - "text":"This path depends on a user-provided value." - }, - "location":{ - "path":"Path Traversal/file2.py", - "start_line":78, - "end_line":78, - "start_column":25, - "end_column":63 - }, - "classifications":[ - - ] - }, - "instances_url":"https://api.github.com/repos/XX/YY/code-scanning/alerts/34/instances" - } -] \ No newline at end of file +[ + { + "number":35, + "created_at":"2024-01-19T14:11:18Z", + "updated_at":"2024-01-19T14:11:20Z", + "url":"https://api.github.com/repos/XX/YY/code-scanning/alerts/35", + "html_url":"https://github.com/XX/YY/security/code-scanning/35", + "state":"open", + "fixed_at":"None", + "dismissed_by":"None", + "dismissed_at":"None", + "dismissed_reason":"None", + "dismissed_comment":"None", + "rule":{ + "id":"py/clear-text-storage-sensitive-data", + "severity":"error", + "description":"Clear-text storage of sensitive information", + "name":"py/clear-text-storage-sensitive-data", + "tags":[ + "external/cwe/cwe-312", + "external/cwe/cwe-315", + "external/cwe/cwe-359", + "security" + ], + "security_severity_level":"high" + }, + "tool":{ + "name":"CodeQL", + "guid":"None", + "version":"2.16.2" + }, + "most_recent_instance":{ + "ref":"refs/XX/YY", + "analysis_key":"dynamic/github-code-scanning/codeql:analyze", + "environment":"{\"language\":\"python\"}", + "category":"/language:python", + "state":"open", + "commit_sha":"XXX", + "message":{ + "text":"This expression stores sensitive data (secret) as clear text." + }, + "location":{ + "path":"Unsafe Deserialization/file.py", + "start_line":42, + "end_line":42, + "start_column":17, + "end_column":23 + }, + "classifications":[ + + ] + }, + "instances_url":"https://api.github.com/repos/XX/YY/code-scanning/alerts/35/instances" + }, + { + "number":34, + "created_at":"2024-01-19T14:11:18Z", + "updated_at":"2024-01-19T14:11:20Z", + "url":"https://api.github.com/repos/XX/YY/code-scanning/alerts/34", + "html_url":"https://github.com/XX/YY/security/code-scanning/34", + "state":"open", + "fixed_at":"None", + "dismissed_by":"None", + "dismissed_at":"None", + "dismissed_reason":"None", + "dismissed_comment":"None", + "rule":{ + "id":"py/path-injection", + "severity":"error", + "description":"Uncontrolled data used in path expression", + "name":"py/path-injection", + "tags":[ + "correctness", + "external/cwe/cwe-022", + "external/cwe/cwe-023", + "external/cwe/cwe-036", + "external/cwe/cwe-073", + "external/cwe/cwe-099", + "security" + ], + "security_severity_level":"high" + }, + "tool":{ + "name":"CodeQL", + "guid":"None", + "version":"2.16.2" + }, + "most_recent_instance":{ + "ref":"refs/XX/YY", + "analysis_key":"dynamic/github-code-scanning/codeql:analyze", + "environment":"{\"language\":\"python\"}", + "category":"/language:python", + "state":"open", + "commit_sha":"XXX", + "message":{ + "text":"This path depends on a user-provided value." + }, + "location":{ + "path":"Path Traversal/file2.py", + "start_line":78, + "end_line":78, + "start_column":25, + "end_column":63 + }, + "classifications":[ + + ] + }, + "instances_url":"https://api.github.com/repos/XX/YY/code-scanning/alerts/34/instances" + } + ] \ No newline at end of file diff --git a/unittests/tools/test_github_sast_parser.py b/unittests/tools/test_github_sast_parser.py new file mode 100644 index 00000000000..9b42a795ec5 --- /dev/null +++ b/unittests/tools/test_github_sast_parser.py @@ -0,0 +1,53 @@ +import io + +from dojo.models import Test +from dojo.tools.github_sast.parser import GithubSASTParser +from unittests.dojo_test_case import DojoTestCase, get_unit_tests_scans_path + + +class TestGithubSASTParser(DojoTestCase): + def test_parse_file_with_no_vuln_has_no_findings(self): + """Empty list should yield no findings""" + with (get_unit_tests_scans_path("github_sast") / "github_sast_zero_vul.json").open( + encoding="utf-8", + ) as testfile: + parser = GithubSASTParser() + findings = parser.get_findings(testfile, Test()) + self.assertEqual(0, len(findings)) + + def test_parse_file_with_one_vuln_parsed_correctly(self): + """Single vulnerability entry parsed correctly""" + with (get_unit_tests_scans_path("github_sast") / "github_sast_one_vul.json").open(encoding="utf-8") as testfile: + parser = GithubSASTParser() + findings = parser.get_findings(testfile, Test()) + self.assertEqual(1, len(findings)) + finding = findings[0] + for ep in getattr(finding, "unsaved_endpoints", []): + ep.clean() + + expected_title = "Clear-text storage of sensitive information (py/clear-text-storage-sensitive-data)" + self.assertEqual(expected_title, finding.title) + self.assertEqual("src/file.py", finding.file_path) + self.assertEqual(42, finding.line) + self.assertEqual("py/clear-text-storage-sensitive-data", finding.vuln_id_from_tool) + self.assertEqual("High", finding.severity) + self.assertEqual("https://github.com/OWASP/test-repository/security/code-scanning/35", finding.url) + self.assertIn("This expression stores sensitive data", finding.description) + + def test_parse_file_with_multiple_vulns_has_multiple_findings(self): + """Multiple entries produce corresponding findings""" + with (get_unit_tests_scans_path("github_sast") / "github_sast_many_vul.json").open( + encoding="utf-8", + ) as testfile: + parser = GithubSASTParser() + findings = parser.get_findings(testfile, Test()) + self.assertEqual(2, len(findings)) + lines = sorted(f.line for f in findings) + self.assertListEqual([42, 78], lines) + + def test_parse_file_invalid_format_raises(self): + """Non-list JSON should raise""" + bad_json = io.StringIO('{"not": "a list"}') + parser = GithubSASTParser() + with self.assertRaises(TypeError): + parser.get_findings(bad_json, Test()) diff --git a/unittests/tools/test_github_vulnerability_parser.py b/unittests/tools/test_github_vulnerability_parser.py index 1065a31a2f5..2e5869476bf 100644 --- a/unittests/tools/test_github_vulnerability_parser.py +++ b/unittests/tools/test_github_vulnerability_parser.py @@ -10,14 +10,18 @@ class TestGithubVulnerabilityParser(DojoTestCase): def test_parse_file_with_no_vuln_has_no_findings(self): """Sample with zero vulnerability""" - with (get_unit_tests_scans_path("github_vulnerability") / "github-0-vuln.json").open(encoding="utf-8") as testfile: + with (get_unit_tests_scans_path("github_vulnerability") / "github-0-vuln.json").open( + encoding="utf-8", + ) as testfile: parser = GithubVulnerabilityParser() findings = parser.get_findings(testfile, Test()) self.assertEqual(0, len(findings)) def test_parse_file_with_one_vuln_has_one_findings(self): """Sample with one vulnerability""" - with (get_unit_tests_scans_path("github_vulnerability") / "github-1-vuln.json").open(encoding="utf-8") as testfile: + with (get_unit_tests_scans_path("github_vulnerability") / "github-1-vuln.json").open( + encoding="utf-8", + ) as testfile: parser = GithubVulnerabilityParser() findings = parser.get_findings(testfile, Test()) self.assertEqual(1, len(findings)) @@ -36,8 +40,10 @@ def test_parse_file_with_one_vuln_has_one_findings(self): self.assertEqual(finding.unique_id_from_tool, "aabbccddeeff1122334401") def test_parse_file_with_one_vuln_has_one_finding_and_dependabot_direct_link(self): - """Sample with one vulnerability""" - with (get_unit_tests_scans_path("github_vulnerability") / "github-1-vuln-repo-dependabot-link.json").open(encoding="utf-8") as testfile: + """Sample with dependabot PR and repository alert link""" + with (get_unit_tests_scans_path("github_vulnerability") / "github-1-vuln-repo-dependabot-link.json").open( + encoding="utf-8", + ) as testfile: parser = GithubVulnerabilityParser() findings = parser.get_findings(testfile, Test()) self.assertEqual(1, len(findings)) @@ -47,23 +53,31 @@ def test_parse_file_with_one_vuln_has_one_finding_and_dependabot_direct_link(sel with self.subTest(i=0): finding = findings[0] self.assertEqual(finding.title, "Critical severity vulnerability that affects package") - self.assertEqual( - finding.description, - "[https://github.com/OWASP/test-repository/security/dependabot/1](https://github.com/OWASP/test-repository/security/dependabot/1)\nThis is a sample description for sample description from Github API.", + expected_desc = ( + "Repo Alert: [https://github.com/OWASP/test-repository/security/dependabot/1]" + "(https://github.com/OWASP/test-repository/security/dependabot/1)\n" + "Fix PR: [https://github.com/OWASP/test-repository/pull/1]" + "(https://github.com/OWASP/test-repository/pull/1)\n" + "This is a sample description for sample description from Github API." ) + self.assertEqual(finding.description, expected_desc) self.assertEqual(finding.severity, "Critical") self.assertEqual(finding.component_name, "package") self.assertEqual(finding.unique_id_from_tool, "aabbccddeeff1122334401") def test_parse_file_with_multiple_vuln_has_multiple_findings(self): """Sample with five vulnerability""" - with (get_unit_tests_scans_path("github_vulnerability") / "github-5-vuln.json").open(encoding="utf-8") as testfile: + with (get_unit_tests_scans_path("github_vulnerability") / "github-5-vuln.json").open( + encoding="utf-8", + ) as testfile: parser = GithubVulnerabilityParser() findings = parser.get_findings(testfile, Test()) self.assertEqual(5, len(findings)) def test_parse_file_issue2984(self): - with (get_unit_tests_scans_path("github_vulnerability") / "github_issue2984.json").open(encoding="utf-8") as testfile: + with (get_unit_tests_scans_path("github_vulnerability") / "github_issue2984.json").open( + encoding="utf-8", + ) as testfile: parser = GithubVulnerabilityParser() findings = parser.get_findings(testfile, Test()) self.assertEqual(4, len(findings)) @@ -87,7 +101,9 @@ def test_parse_file_issue2984(self): self.assertEqual(finding.unique_id_from_tool, "DASFMMFKLNKDSAKFSDLANJKKFDSNJSAKDFNJKDFS=") def test_parse_file_search(self): - with (get_unit_tests_scans_path("github_vulnerability") / "github_search.json").open(encoding="utf-8") as testfile: + with (get_unit_tests_scans_path("github_vulnerability") / "github_search.json").open( + encoding="utf-8", + ) as testfile: parser = GithubVulnerabilityParser() findings = parser.get_findings(testfile, Test()) self.assertEqual(2, len(findings)) @@ -102,7 +118,9 @@ def test_parse_file_search(self): self.assertEqual(finding.unsaved_vulnerability_ids[0], "GHSA-2qrg-x229-3v8q") self.assertEqual(finding.unsaved_vulnerability_ids[1], "CVE-2019-17571") self.assertEqual(finding.component_name, "log4j:log4j") - self.assertEqual(finding.unique_id_from_tool, "MDI4OlJlcG9zaXRvcnlWdWxuZXJhYmlsaXR5QWxlcnQyMDg2Nzc5NzY=") + self.assertEqual( + finding.unique_id_from_tool, "MDI4OlJlcG9zaXRvcnlWdWxuZXJhYmlsaXR5QWxlcnQyMDg2Nzc5NzY=", + ) with self.subTest(i=1): finding = findings[1] self.assertEqual(finding.title, "Deserialization of Untrusted Data in Log4j") @@ -111,11 +129,15 @@ def test_parse_file_search(self): self.assertEqual(finding.unsaved_vulnerability_ids[0], "GHSA-2qrg-x229-3v8q") self.assertEqual(finding.unsaved_vulnerability_ids[1], "CVE-2019-17571") self.assertEqual(finding.component_name, "log4j:log4j") - self.assertEqual(finding.unique_id_from_tool, "MDI4OlJlcG9zaXRvcnlWdWxuZXJhYmlsaXR5QWxlcnQ1NTE5NTI2OTM=") + self.assertEqual( + finding.unique_id_from_tool, "MDI4OlJlcG9zaXRvcnlWdWxuZXJhYmlsaXR5QWxlcnQ1NTE5NTI2OTM=", + ) def test_parse_file_search2(self): """Search result with more data/attributes""" - with (get_unit_tests_scans_path("github_vulnerability") / "github_search2.json").open(encoding="utf-8") as testfile: + with (get_unit_tests_scans_path("github_vulnerability") / "github_search2.json").open( + encoding="utf-8", + ) as testfile: parser = GithubVulnerabilityParser() findings = parser.get_findings(testfile, Test()) self.assertEqual(2, len(findings)) @@ -130,7 +152,9 @@ def test_parse_file_search2(self): self.assertEqual(finding.unsaved_vulnerability_ids[0], "GHSA-2qrg-x229-3v8q") self.assertEqual(finding.unsaved_vulnerability_ids[1], "CVE-2019-17571") self.assertEqual(finding.component_name, "log4j:log4j") - self.assertEqual(finding.unique_id_from_tool, "MDI4OlJlcG9zaXRvcnlWdWxuZXJhYmlsaXR5QWxlcnQyMDg2Nzc5NzY=") + self.assertEqual( + finding.unique_id_from_tool, "MDI4OlJlcG9zaXRvcnlWdWxuZXJhYmlsaXR5QWxlcnQyMDg2Nzc5NzY=", + ) with self.subTest(i=1): finding = findings[1] self.assertEqual(finding.title, "Deserialization of Untrusted Data in Log4j") @@ -139,11 +163,15 @@ def test_parse_file_search2(self): self.assertEqual(finding.unsaved_vulnerability_ids[0], "GHSA-2qrg-x229-3v8q") self.assertEqual(finding.unsaved_vulnerability_ids[1], "CVE-2019-17571") self.assertEqual(finding.component_name, "log4j:log4j") - self.assertEqual(finding.unique_id_from_tool, "MDI4OlJlcG9zaXRvcnlWdWxuZXJhYmlsaXR5QWxlcnQ1NTE5NTI2OTM=") + self.assertEqual( + finding.unique_id_from_tool, "MDI4OlJlcG9zaXRvcnlWdWxuZXJhYmlsaXR5QWxlcnQ1NTE5NTI2OTM=", + ) def test_parse_file_search3(self): """Search result with more data/attributes""" - with (get_unit_tests_scans_path("github_vulnerability") / "github_search3.json").open(encoding="utf-8") as testfile: + with (get_unit_tests_scans_path("github_vulnerability") / "github_search3.json").open( + encoding="utf-8", + ) as testfile: parser = GithubVulnerabilityParser() findings = parser.get_findings(testfile, Test()) self.assertEqual(2, len(findings)) @@ -160,7 +188,9 @@ def test_parse_file_search3(self): self.assertEqual(finding.component_name, "log4j:log4j") self.assertEqual(finding.cvssv3, "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H") self.assertEqual(finding.file_path, "gogoph-crawler/pom.xml") - self.assertEqual(finding.unique_id_from_tool, "MDI4OlJlcG9zaXRvcnlWdWxuZXJhYmlsaXR5QWxlcnQyMDg2Nzc5NzY=") + self.assertEqual( + finding.unique_id_from_tool, "MDI4OlJlcG9zaXRvcnlWdWxuZXJhYmlsaXR5QWxlcnQyMDg2Nzc5NzY=", + ) with self.subTest(i=1): finding = findings[1] self.assertEqual(finding.title, "Deserialization of Untrusted Data in Log4j") @@ -171,11 +201,15 @@ def test_parse_file_search3(self): self.assertEqual(finding.component_name, "log4j:log4j") self.assertEqual(finding.cvssv3, "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H") self.assertEqual(finding.file_path, "gogoph/pom.xml") - self.assertEqual(finding.unique_id_from_tool, "MDI4OlJlcG9zaXRvcnlWdWxuZXJhYmlsaXR5QWxlcnQ1NTE5NTI2OTM=") + self.assertEqual( + finding.unique_id_from_tool, "MDI4OlJlcG9zaXRvcnlWdWxuZXJhYmlsaXR5QWxlcnQ1NTE5NTI2OTM=", + ) def test_parse_file_search4_null_cvss_vector(self): """Search result with more data/attributes""" - with (get_unit_tests_scans_path("github_vulnerability") / "github_search4_null_cvss_vector.json").open(encoding="utf-8") as testfile: + with (get_unit_tests_scans_path("github_vulnerability") / "github_search4_null_cvss_vector.json").open( + encoding="utf-8", + ) as testfile: parser = GithubVulnerabilityParser() findings = parser.get_findings(testfile, Test()) self.assertEqual(2, len(findings)) @@ -192,7 +226,9 @@ def test_parse_file_search4_null_cvss_vector(self): self.assertEqual(finding.component_name, "log4j:log4j") self.assertEqual(finding.cvssv3, None) self.assertEqual(finding.file_path, "gogoph-crawler/pom.xml") - self.assertEqual(finding.unique_id_from_tool, "MDI4OlJlcG9zaXRvcnlWdWxuZXJhYmlsaXR5QWxlcnQyMDg2Nzc5NzY=") + self.assertEqual( + finding.unique_id_from_tool, "MDI4OlJlcG9zaXRvcnlWdWxuZXJhYmlsaXR5QWxlcnQyMDg2Nzc5NzY=", + ) with self.subTest(i=1): finding = findings[1] self.assertEqual(finding.title, "Deserialization of Untrusted Data in Log4j") @@ -203,7 +239,9 @@ def test_parse_file_search4_null_cvss_vector(self): self.assertEqual(finding.component_name, "log4j:log4j") self.assertEqual(finding.cvssv3, "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H") self.assertEqual(finding.file_path, "gogoph/pom.xml") - self.assertEqual(finding.unique_id_from_tool, "MDI4OlJlcG9zaXRvcnlWdWxuZXJhYmlsaXR5QWxlcnQ1NTE5NTI2OTM=") + self.assertEqual( + finding.unique_id_from_tool, "MDI4OlJlcG9zaXRvcnlWdWxuZXJhYmlsaXR5QWxlcnQ1NTE5NTI2OTM=", + ) def test_parse_cwe_and_date(self): with (get_unit_tests_scans_path("github_vulnerability") / "github_h2.json").open(encoding="utf-8") as testfile: @@ -229,7 +267,9 @@ def test_parse_cwe_and_date(self): self.assertEqual(finding.active, True) def test_parse_state(self): - with (get_unit_tests_scans_path("github_vulnerability") / "github_shiro.json").open(encoding="utf-8") as testfile: + with (get_unit_tests_scans_path("github_vulnerability") / "github_shiro.json").open( + encoding="utf-8", + ) as testfile: parser = GithubVulnerabilityParser() findings = parser.get_findings(testfile, Test()) self.assertEqual(1, len(findings)) @@ -238,7 +278,10 @@ def test_parse_state(self): with self.subTest(i=0): finding = findings[0] - self.assertEqual(finding.title, "Apache Shiro vulnerable to a specially crafted HTTP request causing an authentication bypass") + self.assertEqual( + finding.title, + "Apache Shiro vulnerable to a specially crafted HTTP request causing an authentication bypass", + ) self.assertEqual(finding.severity, "Critical") self.assertEqual(len(finding.unsaved_vulnerability_ids), 2) self.assertEqual(finding.unsaved_vulnerability_ids[0], "GHSA-f6jp-j6w3-w9hm") @@ -253,7 +296,9 @@ def test_parse_state(self): self.assertEqual(finding.is_mitigated, True) def test_parser_version(self): - with (get_unit_tests_scans_path("github_vulnerability") / "github-vuln-version.json").open(encoding="utf-8") as testfile: + with (get_unit_tests_scans_path("github_vulnerability") / "github-vuln-version.json").open( + encoding="utf-8", + ) as testfile: parser = GithubVulnerabilityParser() findings = parser.get_findings(testfile, Test()) self.assertEqual(1, len(findings)) @@ -266,6 +311,8 @@ def test_parser_version(self): self.assertEqual(finding.severity, "Critical") self.assertEqual(finding.component_name, "org.springframework:spring-web") self.assertEqual(finding.component_version, "5.3.29") + self.assertAlmostEqual(finding.epss_score, 0.00212, places=5) + self.assertAlmostEqual(finding.epss_percentile, 0.44035, places=5) def test_parse_file_issue_9582(self): with (get_unit_tests_scans_path("github_vulnerability") / "issue_9582.json").open(encoding="utf-8") as testfile: From a3e67796d6b7f5d79439febdd47337a9759948d7 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 23 Oct 2025 00:25:38 -0600 Subject: [PATCH 070/126] fix(deps): update dependency @docsearch/css from 4.1.0 to v4.2.0 (docs/package.json) (#13381) 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 1947f324dce..7b0adc953df 100644 --- a/docs/package-lock.json +++ b/docs/package-lock.json @@ -9,7 +9,7 @@ "version": "0.0.0", "license": "MIT", "dependencies": { - "@docsearch/css": "4.1.0", + "@docsearch/css": "4.2.0", "@docsearch/js": "4.2.0", "@tabler/icons": "3.35.0", "@thulite/doks-core": "1.8.0", @@ -1507,9 +1507,9 @@ } }, "node_modules/@docsearch/css": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@docsearch/css/-/css-4.1.0.tgz", - "integrity": "sha512-nuNKGjHj/FQeWgE9t+i83QD/V67QiaAmGY7xS9TVCRUiCqSljOgIKlsLoQZKKVwEG8f+OWKdznzZkJxGZ7d06A==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@docsearch/css/-/css-4.2.0.tgz", + "integrity": "sha512-65KU9Fw5fGsPPPlgIghonMcndyx1bszzrDQYLfierN+Ha29yotMHzVS94bPkZS6On9LS8dE4qmW4P/fGjtCf/g==", "license": "MIT" }, "node_modules/@docsearch/js": { diff --git a/docs/package.json b/docs/package.json index 9bbc1be19b0..4c5ec81a73a 100644 --- a/docs/package.json +++ b/docs/package.json @@ -12,7 +12,7 @@ "preview": "vite preview --outDir public" }, "dependencies": { - "@docsearch/css": "4.1.0", + "@docsearch/css": "4.2.0", "@docsearch/js": "4.2.0", "@thulite/doks-core": "1.8.0", "@thulite/images": "3.3.0", From 6fd39a3b22a581efce6d8f6836513fdf431d1d2d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 23 Oct 2025 00:27:39 -0600 Subject: [PATCH 071/126] Bump psycopg[c] from 3.2.10 to 3.2.11 (#13471) Bumps [psycopg[c]](https://github.com/psycopg/psycopg) from 3.2.10 to 3.2.11. - [Changelog](https://github.com/psycopg/psycopg/blob/master/docs/news.rst) - [Commits](https://github.com/psycopg/psycopg/compare/3.2.10...3.2.11) --- updated-dependencies: - dependency-name: psycopg[c] dependency-version: 3.2.11 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 f45a0ea031e..02b93275636 100644 --- a/requirements.txt +++ b/requirements.txt @@ -29,7 +29,7 @@ lxml==6.0.2 Markdown==3.9 openpyxl==3.1.5 Pillow==12.0.0 # required by django-imagekit -psycopg[c]==3.2.10 +psycopg[c]==3.2.11 cryptography==46.0.3 python-dateutil==2.9.0.post0 redis==6.4.0 From 70145d6d304a916a5522f332e283fc64f38bb85a Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 23 Oct 2025 10:53:35 -0600 Subject: [PATCH 072/126] chore(deps): update dependency renovatebot/renovate from 41.146.0 to v41.146.8 (.github/workflows/renovate.yaml) (#13484) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/renovate.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/renovate.yaml b/.github/workflows/renovate.yaml index 0b9ee77e1c7..049c818ed52 100644 --- a/.github/workflows/renovate.yaml +++ b/.github/workflows/renovate.yaml @@ -21,4 +21,4 @@ jobs: uses: suzuki-shunsuke/github-action-renovate-config-validator@c22827f47f4f4a5364bdba19e1fe36907ef1318e # v1.1.1 with: strict: "true" - validator_version: 41.146.0 # renovate: datasource=github-releases depName=renovatebot/renovate + validator_version: 41.146.8 # renovate: datasource=github-releases depName=renovatebot/renovate From fd5b2fb5c83deb720dc07f28a8761a4ba88ea31c Mon Sep 17 00:00:00 2001 From: maxi-bee <84531851+maxi-bee@users.noreply.github.com> Date: Thu, 23 Oct 2025 23:47:08 +0200 Subject: [PATCH 073/126] jira_integration: changes risk acceptance expiration date to a better default (#13488) * jira_integration: changes risk acceptance expiration date to a better default * Update dojo/models.py * Update dojo/models.py --------- Co-authored-by: valentijnscholten --- ...ira_instance_accepted_mapping_resolution.py | 18 ++++++++++++++++++ dojo/jira_link/helper.py | 6 ++++++ dojo/models.py | 2 +- 3 files changed, 25 insertions(+), 1 deletion(-) create mode 100644 dojo/db_migrations/0245_alter_jira_instance_accepted_mapping_resolution.py diff --git a/dojo/db_migrations/0245_alter_jira_instance_accepted_mapping_resolution.py b/dojo/db_migrations/0245_alter_jira_instance_accepted_mapping_resolution.py new file mode 100644 index 00000000000..3596368327f --- /dev/null +++ b/dojo/db_migrations/0245_alter_jira_instance_accepted_mapping_resolution.py @@ -0,0 +1,18 @@ +# Generated by Django 5.1.13 on 2025-10-21 10:25 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('dojo', '0244_pghistory_indices'), + ] + + operations = [ + migrations.AlterField( + model_name='jira_instance', + name='accepted_mapping_resolution', + field=models.CharField(blank=True, help_text='JIRA issues that are closed in JIRA with one of these resolutions will result in the Finding becoming Risk Accepted in Defect Dojo. JIRA issues that are closed in JIRA with one of these resolutions will result in the Finding becoming Risk Accepted in Defect Dojo. The expiration time for this Risk Acceptance will be determined by the "Risk acceptance form default days" in "System Settings". This mapping is not used when Findings are pushed to JIRA. In that case the Risk Accepted Findings are closed in JIRA and JIRA sets the default resolution.', max_length=300, null=True, verbose_name='Risk Accepted resolution mapping'), + ), + ] diff --git a/dojo/jira_link/helper.py b/dojo/jira_link/helper.py index 9dbbd6deeee..bf2b0101fed 100644 --- a/dojo/jira_link/helper.py +++ b/dojo/jira_link/helper.py @@ -6,6 +6,7 @@ from typing import Any import requests +from dateutil.relativedelta import relativedelta from django.conf import settings from django.contrib import messages from django.template import TemplateDoesNotExist @@ -1802,9 +1803,14 @@ def process_resolution_from_jira(finding, resolution_id, resolution_name, assign if finding.test.engagement.product.enable_full_risk_acceptance: logger.debug(f"Creating risk acceptance for finding linked to {jira_issue.jira_key}.") + # loads the expiration from the system setting "Risk acceptance form default days" as otherwise + # the acceptance will never expire + risk_acceptance_form_default_days = get_system_setting("risk_acceptance_form_default_days", 90) + expiration_date_from_system_settings = timezone.now() + relativedelta(days=risk_acceptance_form_default_days) ra = Risk_Acceptance.objects.create( accepted_by=assignee_name, owner=finding.reporter, + expiration_date=expiration_date_from_system_settings, decision_details=f"Risk Acceptance automatically created from JIRA issue {jira_issue.jira_key} with resolution {resolution_name}", ) finding.test.engagement.risk_acceptance.add(ra) diff --git a/dojo/models.py b/dojo/models.py index d308ff42fb1..2c283c8d795 100644 --- a/dojo/models.py +++ b/dojo/models.py @@ -3962,7 +3962,7 @@ class JIRA_Instance(models.Model): high_mapping_severity = models.CharField(max_length=200, help_text=_("Maps to the 'Priority' field in Jira. For example: High")) critical_mapping_severity = models.CharField(max_length=200, help_text=_("Maps to the 'Priority' field in Jira. For example: Critical")) finding_text = models.TextField(null=True, blank=True, help_text=_("Additional text that will be added to the finding in Jira. For example including how the finding was created or who to contact for more information.")) - accepted_mapping_resolution = models.CharField(null=True, blank=True, max_length=300, verbose_name="Risk Accepted resolution mapping", help_text=_("JIRA issues that are closed in JIRA with one of these resolutions will result in the Finding becoming Risk Accepted in Defect Dojo. This Risk Acceptance will not have an expiration date. This mapping is not used when Findings are pushed to JIRA. In that case the Risk Accepted Findings are closed in JIRA and JIRA sets the default resolution.")) + accepted_mapping_resolution = models.CharField(null=True, blank=True, max_length=300, verbose_name="Risk Accepted resolution mapping", help_text=_('JIRA issues that are closed in JIRA with one of these resolutions will result in the Finding becoming Risk Accepted in Defect Dojo. JIRA issues that are closed in JIRA with one of these resolutions will result in the Finding becoming Risk Accepted in Defect Dojo. The expiration time for this Risk Acceptance will be determined by the "Risk acceptance form default days" in "System Settings". This mapping is not used when Findings are pushed to JIRA. In that case the Risk Accepted Findings are closed in JIRA and JIRA sets the default resolution.')) false_positive_mapping_resolution = models.CharField(null=True, blank=True, verbose_name="False Positive resolution mapping", max_length=300, help_text=_("JIRA issues that are closed in JIRA with one of these resolutions will result in the Finding being marked as False Positive Defect Dojo. This mapping is not used when Findings are pushed to JIRA. In that case the Finding is closed in JIRA and JIRA sets the default resolution.")) global_jira_sla_notification = models.BooleanField(default=True, blank=False, verbose_name=_("Globally send SLA notifications as comment?"), help_text=_("This setting can be overidden at the Product level")) finding_jira_sync = models.BooleanField(default=False, blank=False, verbose_name=_("Automatically sync Findings with JIRA?"), help_text=_("If enabled, this will sync changes to a Finding automatically to JIRA")) From 84e2f6f2ed8a13a6666611e0225d5fc6f1e192da Mon Sep 17 00:00:00 2001 From: valentijnscholten Date: Fri, 24 Oct 2025 18:30:17 +0200 Subject: [PATCH 074/126] UNIQUE_ID_OR_HASH_CODE: dont stop after one candidate (#13513) * UNIQUE_ID_OR_HASH_CODE: dont stop after one candidate * docs: add upgrade note --- docs/content/en/open_source/upgrading/2.52.md | 12 +- dojo/utils.py | 4 +- unittests/test_deduplication_logic.py | 133 ++++++++++++++---- 3 files changed, 117 insertions(+), 32 deletions(-) diff --git a/docs/content/en/open_source/upgrading/2.52.md b/docs/content/en/open_source/upgrading/2.52.md index c9f6b38418f..c15bad237f8 100644 --- a/docs/content/en/open_source/upgrading/2.52.md +++ b/docs/content/en/open_source/upgrading/2.52.md @@ -5,6 +5,10 @@ weight: -20251006 description: MobSF parsers & Helm chart changes. --- +## Deduplication fix of `UNIQUE_ID_OR_HASH_CODE` +A bug was fixed in the `UNIQUE_ID_OR_HASH_CODE` algorithm where it stopped processing candidate findings with equal `unique_id_from_tool` or `hash_code` value. +Strictly speaking this is not a breaking change, but we wanted to make you aware that you can see more (better) more deduplicatation for parsers using this algorithm. + ## Merge of MobSF parsers Mobsfscan Scan" has been merged into the "MobSF Scan" parser. The "Mobsfscan Scan" scan_type has been retained to keep deduplication working for existing Tests, but users are encouraged to move to the "MobSF Scan" scan_type. @@ -17,16 +21,16 @@ This release introduces more important changes to the Helm chart configuration: #### Tags -`tag` and `repositoryPrefix` fields have been deprecated. Currently, image tags used in containers are derived by default from the `appVersion` defined in the Chart. -This behavior can be overridden by setting the `tag` value in `images.django` and `images.nginx`. -If fine-tuning is necessary, each container’s image value can also be customized individually (`celery.beat.image`, `celery.worker.image`, `django.nginx.image`, `django.uwsgi.image`, `initializer.image`, and `dbMigrationChecker.image`). +`tag` and `repositoryPrefix` fields have been deprecated. Currently, image tags used in containers are derived by default from the `appVersion` defined in the Chart. +This behavior can be overridden by setting the `tag` value in `images.django` and `images.nginx`. +If fine-tuning is necessary, each container’s image value can also be customized individually (`celery.beat.image`, `celery.worker.image`, `django.nginx.image`, `django.uwsgi.image`, `initializer.image`, and `dbMigrationChecker.image`). Digest pinning is now supported as well. #### Security context This Helm chart extends security context capabilities to all deployed pods and containers. You can define a default pod and container security context globally using `securityContext.podSecurityContext` and `securityContext.containerSecurityContext` keys. -Additionally, each deployment can specify its own pod and container security contexts, which will override or merge with the global ones. +Additionally, each deployment can specify its own pod and container security contexts, which will override or merge with the global ones. #### Fine-grained resources diff --git a/dojo/utils.py b/dojo/utils.py index 07709c4bbbf..7469ee0ffa5 100644 --- a/dojo/utils.py +++ b/dojo/utils.py @@ -508,7 +508,7 @@ def deduplicate_uid_or_hash_code(new_finding): id=new_finding.id).exclude( duplicate=True).order_by("id") deduplicationLogger.debug("Found " - + str(len(existing_findings)) + " findings with either the same unique_id_from_tool or hash_code") + + str(len(existing_findings)) + " findings with either the same unique_id_from_tool or hash_code: " + str([find.id for find in existing_findings])) for find in existing_findings: if is_deduplication_on_engagement_mismatch(new_finding, find): deduplicationLogger.debug( @@ -517,10 +517,10 @@ def deduplicate_uid_or_hash_code(new_finding): try: if are_endpoints_duplicates(new_finding, find): set_duplicate(new_finding, find) + break except Exception as e: deduplicationLogger.debug(str(e)) continue - break def set_duplicate(new_finding, existing_finding): diff --git a/unittests/test_deduplication_logic.py b/unittests/test_deduplication_logic.py index c9e8e26e53d..c7683a23b46 100644 --- a/unittests/test_deduplication_logic.py +++ b/unittests/test_deduplication_logic.py @@ -1181,55 +1181,136 @@ def test_dedupe_same_id_different_test_type_unique_id_or_hash_code(self): # expect not duplicate as the mathcing finding is from another test_type, hash_code is also different self.assert_finding(finding_new, not_pk=224, duplicate=True, duplicate_finding_id=224, hash_code=finding_224.hash_code) - def test_identical_different_endpoints_unique_id_or_hash_code(self): + def test_identical_different_endpoints_unique_id_or_hash_code_dynamic(self): # create identical copy, so unique id is the same - finding_new, finding_224 = self.copy_and_reset_finding(find_id=224) + finding_new1, finding_224 = self.copy_and_reset_finding(find_id=224) - finding_new.save(dedupe_option=False) - ep1 = Endpoint(product=finding_new.test.engagement.product, finding=finding_new, host="myhost.com", protocol="https") + finding_new1.save(dedupe_option=False) + ep1 = Endpoint(product=finding_new1.test.engagement.product, finding=finding_new1, host="myhost.com", protocol="https") ep1.save() - finding_new.endpoints.add(ep1) - finding_new.save() + finding_new1.endpoints.add(ep1) + finding_new1.save() if settings.DEDUPE_ALGO_ENDPOINT_FIELDS == []: # expect duplicate, as endpoints shouldn't affect dedupe and hash_code due to unique_id - self.assert_finding(finding_new, not_pk=224, duplicate=True, duplicate_finding_id=224, hash_code=finding_224.hash_code) + self.assert_finding(finding_new1, not_pk=224, duplicate=True, duplicate_finding_id=224, hash_code=finding_224.hash_code) else: - self.assert_finding(finding_new, not_pk=224, duplicate=False, duplicate_finding_id=None, hash_code=finding_224.hash_code) + # endpoints don't match with 224, so not a duplicate + self.assert_finding(finding_new1, not_pk=224, duplicate=False, duplicate_finding_id=None, hash_code=finding_224.hash_code) + + # remove the finding to prevent it from being duplicated by the next finding we create + finding_new1.delete() # same scenario, now with different uid. and different endpoints, but hash will be different due the endpoints because we set dynamic_finding to True - finding_new, finding_224 = self.copy_and_reset_finding(find_id=224) + finding_new2, finding_224 = self.copy_and_reset_finding(find_id=224) - finding_new.save(dedupe_option=False) - ep1 = Endpoint(product=finding_new.test.engagement.product, finding=finding_new, host="myhost.com", protocol="https") + finding_new2.save(dedupe_option=False) + ep1 = Endpoint(product=finding_new1.test.engagement.product, finding=finding_new2, host="myhost.com", protocol="https") ep1.save() - finding_new.endpoints.add(ep1) - finding_new.unique_id_from_tool = 1 - finding_new.dynamic_finding = True - finding_new.save() + finding_new2.endpoints.add(ep1) + finding_new2.unique_id_from_tool = 1 + finding_new2.dynamic_finding = True + finding_new2.save() if settings.DEDUPE_ALGO_ENDPOINT_FIELDS == []: # different uid. and different endpoints, but endpoints not used for hash anymore -> duplicate - self.assert_finding(finding_new, not_pk=224, duplicate=True, hash_code=finding_224.hash_code) + self.assert_finding(finding_new2, not_pk=224, duplicate=True, duplicate_finding_id=224, hash_code=finding_224.hash_code) + else: + # endpoints do not match with 224 + self.assert_finding(finding_new1, not_pk=224, duplicate=False, duplicate_finding_id=None, hash_code=finding_224.hash_code) + + def test_identical_different_endpoints_unique_id_or_hash_code_static(self): + # create identical copy, so unique id is the same + finding_new1, finding_224 = self.copy_and_reset_finding(find_id=224) + + finding_new1.save(dedupe_option=False) + ep1 = Endpoint(product=finding_new1.test.engagement.product, finding=finding_new1, host="myhost.com", protocol="https") + ep1.save() + finding_new1.endpoints.add(ep1) + finding_new1.save() + + if settings.DEDUPE_ALGO_ENDPOINT_FIELDS == []: + # expect duplicate, as endpoints shouldn't affect dedupe and hash_code due to unique_id + self.assert_finding(finding_new1, not_pk=224, duplicate=True, duplicate_finding_id=224, hash_code=finding_224.hash_code) else: - self.assert_finding(finding_new, not_pk=224, duplicate=False, hash_code=finding_224.hash_code) + # endpoints don't match with 224, so not a duplicate + self.assert_finding(finding_new1, not_pk=224, duplicate=False, duplicate_finding_id=None, hash_code=finding_224.hash_code) + + # remove the finding to prevent it from being duplicated by the next finding we create + finding_new1.delete() # same scenario, now with different uid. and different endpoints - finding_new, finding_224 = self.copy_and_reset_finding(find_id=224) + finding_new3, finding_224 = self.copy_and_reset_finding(find_id=224) - finding_new.save(dedupe_option=False) - ep1 = Endpoint(product=finding_new.test.engagement.product, finding=finding_new, host="myhost.com", protocol="https") + finding_new3.save(dedupe_option=False) + ep1 = Endpoint(product=finding_new3.test.engagement.product, finding=finding_new3, host="myhost.com", protocol="https") ep1.save() - finding_new.endpoints.add(ep1) - finding_new.unique_id_from_tool = 1 - finding_new.dynamic_finding = False - finding_new.save() + finding_new3.endpoints.add(ep1) + finding_new3.unique_id_from_tool = 1 + finding_new3.dynamic_finding = False + finding_new3.save() + + if settings.DEDUPE_ALGO_ENDPOINT_FIELDS == []: + # different uid. and different endpoints, dynamic_finding is set to False hash_code still not affected by endpoints + self.assert_finding(finding_new3, not_pk=224, duplicate=True, duplicate_finding_id=224, hash_code=finding_224.hash_code) + else: + # endpoints do not match with 224 + self.assert_finding(finding_new1, not_pk=224, duplicate=False, duplicate_finding_id=None, hash_code=finding_224.hash_code) + + def test_identical_different_endpoints_unique_id_or_hash_code_multiple(self): + # create identical copy, so unique id is the same + finding_new1, finding_224 = self.copy_and_reset_finding(find_id=224) + + finding_new1.save(dedupe_option=False) + ep1 = Endpoint(product=finding_new1.test.engagement.product, finding=finding_new1, host="myhost.com", protocol="https") + ep1.save() + finding_new1.endpoints.add(ep1) + finding_new1.save() + + if settings.DEDUPE_ALGO_ENDPOINT_FIELDS == []: + # expect duplicate, as endpoints shouldn't affect dedupe and hash_code due to unique_id + self.assert_finding(finding_new1, not_pk=224, duplicate=True, duplicate_finding_id=224, hash_code=finding_224.hash_code) + else: + # endpoints don't match with 224, so not a duplicate + self.assert_finding(finding_new1, not_pk=224, duplicate=False, duplicate_finding_id=None, hash_code=finding_224.hash_code) + + # same scenario, now with different uid. and different endpoints, but hash will be different due the endpoints because we set dynamic_finding to True + finding_new2, finding_224 = self.copy_and_reset_finding(find_id=224) + + finding_new2.save(dedupe_option=False) + ep1 = Endpoint(product=finding_new1.test.engagement.product, finding=finding_new2, host="myhost.com", protocol="https") + ep1.save() + finding_new2.endpoints.add(ep1) + finding_new2.unique_id_from_tool = 1 + finding_new2.dynamic_finding = True + finding_new2.save() + + if settings.DEDUPE_ALGO_ENDPOINT_FIELDS == []: + # different uid. and different endpoints, but endpoints not used for hash anymore -> duplicate + self.assert_finding(finding_new2, not_pk=224, duplicate=True, duplicate_finding_id=224, hash_code=finding_224.hash_code) + else: + # endpoints do not match with 224, but they do match with the finding just created. this proves that the dedupe algo considers more than only the first + # candidate https://github.com/DefectDojo/django-DefectDojo/issues/13497 + self.assert_finding(finding_new2, not_pk=224, duplicate=True, duplicate_finding_id=finding_new1.pk, hash_code=finding_224.hash_code) + + # same scenario, now with different uid. and different endpoints + finding_new3, finding_224 = self.copy_and_reset_finding(find_id=224) + + finding_new3.save(dedupe_option=False) + ep1 = Endpoint(product=finding_new3.test.engagement.product, finding=finding_new3, host="myhost.com", protocol="https") + ep1.save() + finding_new3.endpoints.add(ep1) + finding_new3.unique_id_from_tool = 1 + finding_new3.dynamic_finding = False + finding_new3.save() if settings.DEDUPE_ALGO_ENDPOINT_FIELDS == []: # different uid. and different endpoints, dynamic_finding is set to False hash_code still not affected by endpoints - self.assert_finding(finding_new, not_pk=224, duplicate=True, duplicate_finding_id=224, hash_code=finding_224.hash_code) + self.assert_finding(finding_new3, not_pk=224, duplicate=True, duplicate_finding_id=224, hash_code=finding_224.hash_code) else: - self.assert_finding(finding_new, not_pk=224, duplicate=False, duplicate_finding_id=None, hash_code=finding_224.hash_code) + # endpoints do not match with 224, but they do match with the finding just created. this proves that the dedupe algo considers more than only the first + # candidate https://github.com/DefectDojo/django-DefectDojo/issues/13497 + self.assert_finding(finding_new3, not_pk=224, duplicate=True, duplicate_finding_id=finding_new1.pk, hash_code=finding_224.hash_code) # # some extra tests From b6f22d003aee372fa2ad062f6066ff2d343827b9 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 24 Oct 2025 10:42:23 -0600 Subject: [PATCH 075/126] chore(deps): update node.js from v22.20.0 to v22.21.0 (docs/package.json) (#13508) 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 +- docs/package-lock.json | 2 +- docs/package.json | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/gh-pages.yml b/.github/workflows/gh-pages.yml index 5d2ef4314d1..43d1682bd5b 100644 --- a/.github/workflows/gh-pages.yml +++ b/.github/workflows/gh-pages.yml @@ -21,7 +21,7 @@ jobs: - name: Setup Node uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0 with: - node-version: '22.20.0' # TODO: Renovate helper might not be needed here - needs to be fully tested + node-version: '22.21.0' # TODO: Renovate helper might not be needed here - needs to be fully tested - name: Cache dependencies uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 diff --git a/.github/workflows/validate_docs_build.yml b/.github/workflows/validate_docs_build.yml index fcece5635a6..986b1565e4f 100644 --- a/.github/workflows/validate_docs_build.yml +++ b/.github/workflows/validate_docs_build.yml @@ -18,7 +18,7 @@ jobs: - name: Setup Node uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0 with: - node-version: '22.20.0' # TODO: Renovate helper might not be needed here - needs to be fully tested + node-version: '22.21.0' # TODO: Renovate helper might not be needed here - needs to be fully tested - name: Cache dependencies uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 diff --git a/docs/package-lock.json b/docs/package-lock.json index 7b0adc953df..026b2b39897 100644 --- a/docs/package-lock.json +++ b/docs/package-lock.json @@ -23,7 +23,7 @@ "vite": "7.1.9" }, "engines": { - "node": "22.20.0" + "node": "22.21.0" } }, "node_modules/@ampproject/remapping": { diff --git a/docs/package.json b/docs/package.json index 4c5ec81a73a..e388907afe1 100644 --- a/docs/package.json +++ b/docs/package.json @@ -26,6 +26,6 @@ "vite": "7.1.9" }, "engines": { - "node": "22.20.0" + "node": "22.21.0" } } From d690be9668177681d02b8378e9f0db2da362d3ab Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 25 Oct 2025 17:38:39 -0500 Subject: [PATCH 076/126] chore(deps): update dependency renovatebot/renovate from 41.146.8 to v41.159.4 (.github/workflows/renovate.yaml) (#13507) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/renovate.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/renovate.yaml b/.github/workflows/renovate.yaml index 049c818ed52..2f12f66a1dc 100644 --- a/.github/workflows/renovate.yaml +++ b/.github/workflows/renovate.yaml @@ -21,4 +21,4 @@ jobs: uses: suzuki-shunsuke/github-action-renovate-config-validator@c22827f47f4f4a5364bdba19e1fe36907ef1318e # v1.1.1 with: strict: "true" - validator_version: 41.146.8 # renovate: datasource=github-releases depName=renovatebot/renovate + validator_version: 41.159.4 # renovate: datasource=github-releases depName=renovatebot/renovate From 9fc22043d1fff6d83fcdace8e3ff0c2085026edf Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 25 Oct 2025 20:07:20 -0500 Subject: [PATCH 077/126] Bump ruff from 0.14.1 to 0.14.2 (#13525) Bumps [ruff](https://github.com/astral-sh/ruff) from 0.14.1 to 0.14.2. - [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.14.1...0.14.2) --- updated-dependencies: - dependency-name: ruff dependency-version: 0.14.2 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 59e562b9654..cf2c6af13cd 100644 --- a/requirements-lint.txt +++ b/requirements-lint.txt @@ -1 +1 @@ -ruff==0.14.1 +ruff==0.14.2 From b770c152551fb26da9ed749e07cad9a5a8790374 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 25 Oct 2025 20:08:29 -0500 Subject: [PATCH 078/126] Bump boto3 from 1.40.55 to 1.40.58 (#13524) Bumps [boto3](https://github.com/boto/boto3) from 1.40.55 to 1.40.58. - [Release notes](https://github.com/boto/boto3/releases) - [Commits](https://github.com/boto/boto3/compare/1.40.55...1.40.58) --- updated-dependencies: - dependency-name: boto3 dependency-version: 1.40.58 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 02b93275636..72f40fcb713 100644 --- a/requirements.txt +++ b/requirements.txt @@ -62,7 +62,7 @@ django-ratelimit==4.1.0 argon2-cffi==25.1.0 blackduck==1.1.3 pycurl==7.45.7 # Required for Celery Broker AWS (SQS) support -boto3==1.40.55 # Required for Celery Broker AWS (SQS) support +boto3==1.40.58 # Required for Celery Broker AWS (SQS) support netaddr==1.3.0 vulners==3.1.1 fontawesomefree==6.6.0 From ab8982a0cd5592f667bcd15739532677fe566136 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 25 Oct 2025 20:17:24 -0500 Subject: [PATCH 079/126] chore(deps): update postgres:18.0-alpine docker digest from 18.0 to 18.0-alpine (docker-compose.yml) (#13503) 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 c6838267169..be6bf4468cb 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:18.0-alpine@sha256:f898ac406e1a9e05115cc2efcb3c3abb3a92a4c0263f3b6f6aaae354cbb1953a + image: postgres:18.0-alpine@sha256:48c8ad3a7284b82be4482a52076d47d879fd6fb084a1cbfccbd551f9331b0e40 environment: POSTGRES_DB: ${DD_DATABASE_NAME:-defectdojo} POSTGRES_USER: ${DD_DATABASE_USER:-defectdojo} From 7dd285ce76fccdd4b4a40dfad994959b918cb2d7 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 25 Oct 2025 20:39:09 -0500 Subject: [PATCH 080/126] chore(deps): update dependency vite from 7.1.9 to v7.1.11 [security] (#13480) 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 026b2b39897..8e02a271d62 100644 --- a/docs/package-lock.json +++ b/docs/package-lock.json @@ -20,7 +20,7 @@ }, "devDependencies": { "prettier": "3.6.2", - "vite": "7.1.9" + "vite": "7.1.11" }, "engines": { "node": "22.21.0" @@ -4811,9 +4811,9 @@ "license": "MIT" }, "node_modules/vite": { - "version": "7.1.9", - "resolved": "https://registry.npmjs.org/vite/-/vite-7.1.9.tgz", - "integrity": "sha512-4nVGliEpxmhCL8DslSAUdxlB6+SMrhB0a1v5ijlh1xB1nEPuy1mxaHxysVucLHuWryAxLWg6a5ei+U4TLn/rFg==", + "version": "7.1.11", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.1.11.tgz", + "integrity": "sha512-uzcxnSDVjAopEUjljkWh8EIrg6tlzrjFUfMcR1EVsRDGwf/ccef0qQPRyOrROwhrTDaApueq+ja+KLPlzR/zdg==", "dev": true, "license": "MIT", "dependencies": { diff --git a/docs/package.json b/docs/package.json index e388907afe1..7f03021e67d 100644 --- a/docs/package.json +++ b/docs/package.json @@ -23,7 +23,7 @@ }, "devDependencies": { "prettier": "3.6.2", - "vite": "7.1.9" + "vite": "7.1.11" }, "engines": { "node": "22.21.0" From ff4926019877787f6779e6818169f933e36f351e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 25 Oct 2025 20:49:57 -0500 Subject: [PATCH 081/126] Bump redis from 6.4.0 to 7.0.0 (#13510) Bumps [redis](https://github.com/redis/redis-py) from 6.4.0 to 7.0.0. - [Release notes](https://github.com/redis/redis-py/releases) - [Changelog](https://github.com/redis/redis-py/blob/master/CHANGES) - [Commits](https://github.com/redis/redis-py/compare/v6.4.0...v7.0.0) --- updated-dependencies: - dependency-name: redis dependency-version: 7.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 72f40fcb713..6315cc4cd8f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -32,7 +32,7 @@ Pillow==12.0.0 # required by django-imagekit psycopg[c]==3.2.11 cryptography==46.0.3 python-dateutil==2.9.0.post0 -redis==6.4.0 +redis==7.0.0 requests==2.32.5 sqlalchemy==2.0.44 # Required by Celery broker transport urllib3==2.5.0 From 686467235edfaa4a8c26d96b42919ed11230aecc Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 26 Oct 2025 19:03:58 -0600 Subject: [PATCH 082/126] chore(deps): update github artifact actions (.github/workflows/rest-framework-tests.yml) (#13531) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/build-docker-images-for-testing.yml | 2 +- .github/workflows/fetch-oas.yml | 2 +- .github/workflows/integration-tests.yml | 2 +- .github/workflows/k8s-tests.yml | 2 +- .github/workflows/release-drafter.yml | 2 +- .github/workflows/release-x-manual-docker-containers.yml | 2 +- .github/workflows/release-x-manual-merge-container-digests.yml | 2 +- .github/workflows/rest-framework-tests.yml | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/build-docker-images-for-testing.yml b/.github/workflows/build-docker-images-for-testing.yml index 53e44b5e6a9..9175b7c2993 100644 --- a/.github/workflows/build-docker-images-for-testing.yml +++ b/.github/workflows/build-docker-images-for-testing.yml @@ -67,7 +67,7 @@ jobs: # export docker images to be used in next jobs below - name: Upload image ${{ matrix.docker-image }} as artifact timeout-minutes: 15 - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 with: name: built-docker-image-${{ matrix.docker-image }}-${{ matrix.os }}-${{ env.PLATFORM }} path: ${{ matrix.docker-image }}-${{ matrix.os }}-${{ env.PLATFORM }}_img diff --git a/.github/workflows/fetch-oas.yml b/.github/workflows/fetch-oas.yml index d6ff0ffbc28..4569439e20a 100644 --- a/.github/workflows/fetch-oas.yml +++ b/.github/workflows/fetch-oas.yml @@ -51,7 +51,7 @@ jobs: run: docker compose down - name: Upload oas.${{ matrix.file-type }} as artifact - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 with: name: oas-${{ matrix.file-type }} path: oas.${{ matrix.file-type }} diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index 784ee42b676..bf74a50643b 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -58,7 +58,7 @@ jobs: # load docker images from build jobs - name: Load images from artifacts - uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0 + uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 with: path: built-docker-image pattern: built-docker-image-* diff --git a/.github/workflows/k8s-tests.yml b/.github/workflows/k8s-tests.yml index c6252e5a533..8bde0d1ea37 100644 --- a/.github/workflows/k8s-tests.yml +++ b/.github/workflows/k8s-tests.yml @@ -38,7 +38,7 @@ jobs: minikube status - name: Load images from artifacts - uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0 + uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 with: path: built-docker-image pattern: built-docker-image-* diff --git a/.github/workflows/release-drafter.yml b/.github/workflows/release-drafter.yml index 17e8324ca27..baa804441a0 100644 --- a/.github/workflows/release-drafter.yml +++ b/.github/workflows/release-drafter.yml @@ -47,7 +47,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Load OAS files from artifacts - uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0 + uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 with: pattern: oas-* diff --git a/.github/workflows/release-x-manual-docker-containers.yml b/.github/workflows/release-x-manual-docker-containers.yml index a492bed7518..eb3c001e680 100644 --- a/.github/workflows/release-x-manual-docker-containers.yml +++ b/.github/workflows/release-x-manual-docker-containers.yml @@ -89,7 +89,7 @@ jobs: # upload the digest file as artifact - name: Upload digest - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 with: name: digests-${{ matrix.docker-image}}-${{ matrix.os }}-${{ env.PLATFORM }} path: ${{ runner.temp }}/digests/* diff --git a/.github/workflows/release-x-manual-merge-container-digests.yml b/.github/workflows/release-x-manual-merge-container-digests.yml index 65abfdb7e08..156d3dfb28f 100644 --- a/.github/workflows/release-x-manual-merge-container-digests.yml +++ b/.github/workflows/release-x-manual-merge-container-digests.yml @@ -41,7 +41,7 @@ jobs: # only download digests for this image and this os - name: Download digests - uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0 + uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 with: path: ${{ runner.temp }}/digests pattern: digests-${{ matrix.docker-image}}-${{ matrix.os }}-* diff --git a/.github/workflows/rest-framework-tests.yml b/.github/workflows/rest-framework-tests.yml index 5df066ec486..23aa9a0af0c 100644 --- a/.github/workflows/rest-framework-tests.yml +++ b/.github/workflows/rest-framework-tests.yml @@ -36,7 +36,7 @@ jobs: # load docker images from build jobs - name: Load images from artifacts - uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0 + uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 with: path: built-docker-image pattern: built-docker-image-* From 0fd62d63f79430ed93292942a973450c93cb5175 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 26 Oct 2025 19:04:45 -0600 Subject: [PATCH 083/126] chore(deps): update dependency vite from 7.1.11 to v7.1.12 (docs/package.json) (#13532) 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 8e02a271d62..9576bdbfa5c 100644 --- a/docs/package-lock.json +++ b/docs/package-lock.json @@ -20,7 +20,7 @@ }, "devDependencies": { "prettier": "3.6.2", - "vite": "7.1.11" + "vite": "7.1.12" }, "engines": { "node": "22.21.0" @@ -4811,9 +4811,9 @@ "license": "MIT" }, "node_modules/vite": { - "version": "7.1.11", - "resolved": "https://registry.npmjs.org/vite/-/vite-7.1.11.tgz", - "integrity": "sha512-uzcxnSDVjAopEUjljkWh8EIrg6tlzrjFUfMcR1EVsRDGwf/ccef0qQPRyOrROwhrTDaApueq+ja+KLPlzR/zdg==", + "version": "7.1.12", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.1.12.tgz", + "integrity": "sha512-ZWyE8YXEXqJrrSLvYgrRP7p62OziLW7xI5HYGWFzOvupfAlrLvURSzv/FyGyy0eidogEM3ujU+kUG1zuHgb6Ug==", "dev": true, "license": "MIT", "dependencies": { diff --git a/docs/package.json b/docs/package.json index 7f03021e67d..d62d5d9404b 100644 --- a/docs/package.json +++ b/docs/package.json @@ -23,7 +23,7 @@ }, "devDependencies": { "prettier": "3.6.2", - "vite": "7.1.11" + "vite": "7.1.12" }, "engines": { "node": "22.21.0" From d8675fe5da98cb59ffcdecb8e17048680b03490f Mon Sep 17 00:00:00 2001 From: valentijnscholten Date: Mon, 27 Oct 2025 17:56:55 +0100 Subject: [PATCH 084/126] fix: ui must not overwrite service field from parser (#13517) * fix: ui must not overwrite service field from parser * docs: add upgrade note --- docs/content/en/open_source/upgrading/2.52.md | 14 ++++++++++++++ dojo/engagement/views.py | 16 ++++++++-------- dojo/importers/default_reimporter.py | 6 +++++- dojo/models.py | 1 + dojo/templates/dojo/view_finding.html | 4 ---- dojo/test/views.py | 14 +++++++------- 6 files changed, 35 insertions(+), 20 deletions(-) diff --git a/docs/content/en/open_source/upgrading/2.52.md b/docs/content/en/open_source/upgrading/2.52.md index c15bad237f8..04a206c74ff 100644 --- a/docs/content/en/open_source/upgrading/2.52.md +++ b/docs/content/en/open_source/upgrading/2.52.md @@ -5,6 +5,20 @@ weight: -20251006 description: MobSF parsers & Helm chart changes. --- +## Fix UI overwriting service field from parsers + +The web form in the UI by default sends an empty string, which ended up overwriting the service value provided by parsers. + +Only a few parsers do this, so the impact of this fix is low: + +- Trivy Scan +- Trivy Operator Scan +- Hydra Scan +- JFrog Xray API Summary Artifact Scan +- StackHawk HawkScan + +See [PR 13517](https://github.com/DefectDojo/django-DefectDojo/pull/13517) for more details. + ## Deduplication fix of `UNIQUE_ID_OR_HASH_CODE` A bug was fixed in the `UNIQUE_ID_OR_HASH_CODE` algorithm where it stopped processing candidate findings with equal `unique_id_from_tool` or `hash_code` value. Strictly speaking this is not a breaking change, but we wanted to make you aware that you can see more (better) more deduplicatation for parsers using this algorithm. diff --git a/dojo/engagement/views.py b/dojo/engagement/views.py index a02ff45f6aa..b45b417e39c 100644 --- a/dojo/engagement/views.py +++ b/dojo/engagement/views.py @@ -962,19 +962,19 @@ def process_form( "active": None, "verified": None, "scan_type": request.POST.get("scan_type"), - "test_title": form.cleaned_data.get("test_title"), + "test_title": form.cleaned_data.get("test_title") or None, "tags": form.cleaned_data.get("tags"), - "version": form.cleaned_data.get("version"), - "branch_tag": form.cleaned_data.get("branch_tag", None), - "build_id": form.cleaned_data.get("build_id", None), - "commit_hash": form.cleaned_data.get("commit_hash", None), - "api_scan_configuration": form.cleaned_data.get("api_scan_configuration", None), - "service": form.cleaned_data.get("service", None), + "version": form.cleaned_data.get("version") or None, + "branch_tag": form.cleaned_data.get("branch_tag") or None, + "build_id": form.cleaned_data.get("build_id") or None, + "commit_hash": form.cleaned_data.get("commit_hash") or None, + "api_scan_configuration": form.cleaned_data.get("api_scan_configuration") or None, + "service": form.cleaned_data.get("service") or None, "close_old_findings": form.cleaned_data.get("close_old_findings", None), "apply_tags_to_findings": form.cleaned_data.get("apply_tags_to_findings", False), "apply_tags_to_endpoints": form.cleaned_data.get("apply_tags_to_endpoints", False), "close_old_findings_product_scope": form.cleaned_data.get("close_old_findings_product_scope", None), - "group_by": form.cleaned_data.get("group_by", None), + "group_by": form.cleaned_data.get("group_by") or None, "create_finding_groups_for_all_findings": form.cleaned_data.get("create_finding_groups_for_all_findings", None), "environment": self.get_development_environment(environment_name=form.cleaned_data.get("environment")), }) diff --git a/dojo/importers/default_reimporter.py b/dojo/importers/default_reimporter.py index 17775eb22ae..a1625a85f33 100644 --- a/dojo/importers/default_reimporter.py +++ b/dojo/importers/default_reimporter.py @@ -170,7 +170,11 @@ def process_findings( # we need to make sure there are no side effects such as closing findings # for findings with a different service value # https://github.com/DefectDojo/django-DefectDojo/issues/12754 - original_findings = self.test.finding_set.all().filter(service=self.service) + if self.service is not None: + original_findings = self.test.finding_set.all().filter(service=self.service) + else: + original_findings = self.test.finding_set.all().filter(Q(service__isnull=True) | Q(service__exact="")) + logger.debug(f"original_findings_qyer: {original_findings.query}") self.original_items = list(original_findings) logger.debug(f"original_items: {[(item.id, item.hash_code) for item in self.original_items]}") diff --git a/dojo/models.py b/dojo/models.py index 2c283c8d795..dccfbaa4c7e 100644 --- a/dojo/models.py +++ b/dojo/models.py @@ -3010,6 +3010,7 @@ def hash_fields(self, fields_to_hash): if hasattr(settings, "HASH_CODE_FIELDS_ALWAYS"): for field in settings.HASH_CODE_FIELDS_ALWAYS: if getattr(self, field): + deduplicationLogger.debug("adding HASH_CODE_FIELDS_ALWAYSfield %s to hash_fields: %s", field, getattr(self, field)) fields_to_hash += str(getattr(self, field)) logger.debug("fields_to_hash : %s", fields_to_hash) diff --git a/dojo/templates/dojo/view_finding.html b/dojo/templates/dojo/view_finding.html index a992a22d401..c8f79b63b25 100755 --- a/dojo/templates/dojo/view_finding.html +++ b/dojo/templates/dojo/view_finding.html @@ -538,9 +538,7 @@

- {% if finding.service %} - {% endif %} {% if finding.file_path %} {% endif %} @@ -571,13 +569,11 @@

{% endif %}

- {% if finding.service %} - {% endif %} {% if finding.file_path %} {% endif %} - From 1f90ab73308b54ea35727e3e1a9ba5b7ad384e28 Mon Sep 17 00:00:00 2001 From: Cody Maffucci <46459665+Maffooch@users.noreply.github.com> Date: Mon, 3 Nov 2025 09:22:01 -0700 Subject: [PATCH 122/126] fix(CycloneDXJSONParser): handle missing severity field by defaulting to "Medium" (#13583) --- dojo/tools/cyclonedx/json_parser.py | 5 +++- unittests/scans/cyclonedx/no-severity.json | 35 ++++++++++++++++++++++ unittests/tools/test_cyclonedx_parser.py | 14 +++++++++ 3 files changed, 53 insertions(+), 1 deletion(-) create mode 100644 unittests/scans/cyclonedx/no-severity.json diff --git a/dojo/tools/cyclonedx/json_parser.py b/dojo/tools/cyclonedx/json_parser.py index a53b9dd799d..73cda102c1f 100644 --- a/dojo/tools/cyclonedx/json_parser.py +++ b/dojo/tools/cyclonedx/json_parser.py @@ -36,7 +36,10 @@ def _get_findings_json(self, file, test): # better than always 'Medium' ratings = vulnerability.get("ratings") if ratings: - severity = ratings[0]["severity"] + # Determine if we can use the severity field + # In some cases, the severity field is missing, so we can rely on either the Medium severity + # or the CVSS vector (retrieved further down below) to determine the severity: + severity = ratings[0].get("severity", "Medium") severity = Cyclonedxhelper().fix_severity(severity) else: severity = "Medium" diff --git a/unittests/scans/cyclonedx/no-severity.json b/unittests/scans/cyclonedx/no-severity.json new file mode 100644 index 00000000000..ed12833bc5c --- /dev/null +++ b/unittests/scans/cyclonedx/no-severity.json @@ -0,0 +1,35 @@ +{ + "bomFormat": "CycloneDX", + "specVersion": "1.4", + "version": 1, + "metadata": { + "timestamp": "2025-10-28T14:38:10Z" + }, + "vulnerabilities": [ + { + "id": "CVE-2021-44228", + "source": { + "name": "NVD", + "url": "https://nvd.nist.gov/vuln/detail/CVE-2021-44228" + }, + "ratings": [ + { + "source": { + "name": "NVD", + "url": "https://nvd.nist.gov/vuln/detail/CVE-2021-44228" + }, + "score": 10.0, + "method": "CVSSv3", + "vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H" + } + ], + "created": "2025-09-05T05:05:47Z", + "updated": "2025-03-03T16:51:00Z", + "affects": [ + { + "ref": "gerbwetbqt" + } + ] + } + ] +} diff --git a/unittests/tools/test_cyclonedx_parser.py b/unittests/tools/test_cyclonedx_parser.py index 898d649c38b..e98b5338ff8 100644 --- a/unittests/tools/test_cyclonedx_parser.py +++ b/unittests/tools/test_cyclonedx_parser.py @@ -357,3 +357,17 @@ def test_cyclonedx_issue_8022(self): self.assertIn(finding.severity, Finding.SEVERITIES) finding.clean() self.assertEqual(1, len(findings)) + + def test_cyclonedx_no_severity(self): + """CycloneDX version 1.4 JSON format""" + with (get_unit_tests_scans_path("cyclonedx") / "no-severity.json").open(encoding="utf-8") as file: + parser = CycloneDXParser() + findings = parser.get_findings(file, Test()) + self.assertEqual(1, len(findings)) + finding = findings[0] + # There is so little information in the vulnerability, that we cannot build a proper title + self.assertEqual("None:None | CVE-2021-44228", finding.title) + self.assertEqual("Critical", finding.severity) + # The score will be evaluated when the finding save method is ran + # self.assertEqual(10.0, finding.cvssv3_score) + self.assertEqual("CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H", finding.cvssv3) From d1e0dca0b91204027f36f5ba45ba374dc73d713a Mon Sep 17 00:00:00 2001 From: Paul Osinski <42211303+paulOsinski@users.noreply.github.com> Date: Mon, 3 Nov 2025 11:22:16 -0500 Subject: [PATCH 123/126] [docs] Prioritization Engine adjustments (#13581) * priority engine docs * Update docs/content/en/working_with_findings/priority_adjustments.md Co-authored-by: Cody Maffucci <46459665+Maffooch@users.noreply.github.com> * Update docs/content/en/working_with_findings/priority_adjustments.md Co-authored-by: Cody Maffucci <46459665+Maffooch@users.noreply.github.com> --------- Co-authored-by: Paul Osinski Co-authored-by: Cody Maffucci <46459665+Maffooch@users.noreply.github.com> --- docs/assets/images/priority_chooseengine.png | Bin 0 -> 105265 bytes docs/assets/images/priority_default.png | Bin 0 -> 108346 bytes docs/assets/images/priority_engine_new.png | Bin 0 -> 31648 bytes docs/assets/images/priority_sliders.png | Bin 0 -> 60505 bytes docs/assets/images/risk_threshold.png | Bin 0 -> 70133 bytes .../working_with_findings/finding_priority.md | 8 +-- .../priority_adjustments.md | 62 ++++++++++++++++++ 7 files changed, 65 insertions(+), 5 deletions(-) create mode 100644 docs/assets/images/priority_chooseengine.png create mode 100644 docs/assets/images/priority_default.png create mode 100644 docs/assets/images/priority_engine_new.png create mode 100644 docs/assets/images/priority_sliders.png create mode 100644 docs/assets/images/risk_threshold.png create mode 100644 docs/content/en/working_with_findings/priority_adjustments.md diff --git a/docs/assets/images/priority_chooseengine.png b/docs/assets/images/priority_chooseengine.png new file mode 100644 index 0000000000000000000000000000000000000000..c3211fa0e9252000e5ced448cc39382920650118 GIT binary patch literal 105265 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 G-Tx1C{%0ou literal 0 HcmV?d00001 diff --git a/docs/assets/images/priority_default.png b/docs/assets/images/priority_default.png new file mode 100644 index 0000000000000000000000000000000000000000..65443684094aa3e27895bad6cecc2d58622458a9 GIT binary patch literal 108346 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 HSIYkYKHum_ literal 0 HcmV?d00001 diff --git a/docs/assets/images/priority_engine_new.png b/docs/assets/images/priority_engine_new.png new file mode 100644 index 0000000000000000000000000000000000000000..9021231e76dd1d2b5643c23e284d24e86c59220b GIT binary patch literal 31648 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 O6vgK%vc+)Ifd2(xc%;Gr literal 0 HcmV?d00001 diff --git a/docs/assets/images/priority_sliders.png b/docs/assets/images/priority_sliders.png new file mode 100644 index 0000000000000000000000000000000000000000..4fc720ab8a002c2d3f07126d52b9d7e2db72be7c GIT binary patch literal 60505 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 Hf$#qZ--UFE literal 0 HcmV?d00001 diff --git a/docs/assets/images/risk_threshold.png b/docs/assets/images/risk_threshold.png new file mode 100644 index 0000000000000000000000000000000000000000..0e0a6215f724c01d8757db29f82d85c7135e946e GIT binary patch literal 70133 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%0AfEZ=veWN?lhX6Sg`>eGFP9NnD{WVsoM>zaymdh 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 z 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 Date: Mon, 3 Nov 2025 17:22:34 +0100 Subject: [PATCH 124/126] :bug: Robustify create_user to handle None value (#13572) * :bug: Robustify create_user to handle None value * fix * update * update according to review --- dojo/middleware.py | 5 +++++ dojo/pipeline.py | 3 ++- dojo/settings/settings.dist.py | 2 ++ unittests/test_social_auth_failure_handling.py | 10 ++++++++++ 4 files changed, 19 insertions(+), 1 deletion(-) diff --git a/dojo/middleware.py b/dojo/middleware.py index ab89bf5a849..5b50f3cc987 100644 --- a/dojo/middleware.py +++ b/dojo/middleware.py @@ -94,6 +94,11 @@ def process_exception(self, request, exception): if isinstance(exception, AuthForbidden): messages.error(request, "You are not authorized to log in via this method. Please contact support or use the standard login.") return redirect("/login?force_login_form") + if isinstance(exception, TypeError) and "'NoneType' object is not iterable" in str(exception): + logger.warning("OIDC login error: NoneType is not iterable") + messages.error(request, "An unexpected error occurred during social login. Please use the standard login.") + return redirect("/login?force_login_form") + logger.error(f"Unhandled exception during social login: {exception}") return super().process_exception(request, exception) diff --git a/dojo/pipeline.py b/dojo/pipeline.py index 888cce0ba06..8aaea4079bb 100644 --- a/dojo/pipeline.py +++ b/dojo/pipeline.py @@ -183,5 +183,6 @@ def sanitize_username(username): def create_user(strategy, details, backend, user=None, *args, **kwargs): if not settings.SOCIAL_AUTH_CREATE_USER: return None - details["username"] = sanitize_username(details.get("username")) + username = details.get(settings.SOCIAL_AUTH_CREATE_USER_MAPPING) + details["username"] = sanitize_username(username) return social_core.pipeline.user.create_user(strategy, details, backend, user, args, kwargs) diff --git a/dojo/settings/settings.dist.py b/dojo/settings/settings.dist.py index 04f812253c1..9228c8e57fd 100644 --- a/dojo/settings/settings.dist.py +++ b/dojo/settings/settings.dist.py @@ -113,6 +113,7 @@ DD_FORGOT_USERNAME=(bool, True), # do we show link "I forgot my username" on login screen DD_SOCIAL_AUTH_SHOW_LOGIN_FORM=(bool, True), # do we show user/pass input DD_SOCIAL_AUTH_CREATE_USER=(bool, True), # if True creates user at first login + DD_SOCIAL_AUTH_CREATE_USER_MAPPING=(str, "username"), # could also be email or fullname DD_SOCIAL_LOGIN_AUTO_REDIRECT=(bool, False), # auto-redirect if there is only one social login method DD_SOCIAL_AUTH_TRAILING_SLASH=(bool, True), DD_SOCIAL_AUTH_OIDC_AUTH_ENABLED=(bool, False), @@ -574,6 +575,7 @@ def generate_url(scheme, double_slashes, user, password, host, port, path, param SHOW_LOGIN_FORM = env("DD_SOCIAL_AUTH_SHOW_LOGIN_FORM") SOCIAL_LOGIN_AUTO_REDIRECT = env("DD_SOCIAL_LOGIN_AUTO_REDIRECT") SOCIAL_AUTH_CREATE_USER = env("DD_SOCIAL_AUTH_CREATE_USER") +SOCIAL_AUTH_CREATE_USER_MAPPING = env("DD_SOCIAL_AUTH_CREATE_USER_MAPPING") SOCIAL_AUTH_STRATEGY = "social_django.strategy.DjangoStrategy" SOCIAL_AUTH_STORAGE = "social_django.models.DjangoStorage" diff --git a/unittests/test_social_auth_failure_handling.py b/unittests/test_social_auth_failure_handling.py index 83f69471a02..0cf55f8d860 100644 --- a/unittests/test_social_auth_failure_handling.py +++ b/unittests/test_social_auth_failure_handling.py @@ -83,6 +83,16 @@ def test_non_social_auth_path_redirects_on_auth_forbidden(self): storage = list(messages.get_messages(request)) self.assertTrue(any("You are not authorized to log in via this method." in str(msg) for msg in storage)) + def test_type_error_none_type_iterable_redirect(self): + """Ensure middleware catches 'NoneType' object is not iterable TypeError and redirects.""" + request = self._prepare_request("/login/oidc/") + exception = TypeError("'NoneType' object is not iterable") + response = self.middleware.process_exception(request, exception) + self.assertEqual(response.status_code, 302) + self.assertEqual(response.url, "/login?force_login_form") + storage = list(messages.get_messages(request)) + self.assertTrue(any("An unexpected error occurred during social login." in str(msg) for msg in storage)) + @override_settings( AUTHENTICATION_BACKENDS=( From 88361c9c89e4b6399149321eecd6c8fe9b3e2cae Mon Sep 17 00:00:00 2001 From: Ross Esposito Date: Mon, 3 Nov 2025 11:15:22 -0600 Subject: [PATCH 125/126] Changing to supported k8s version for minikube --- .github/workflows/k8s-tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/k8s-tests.yml b/.github/workflows/k8s-tests.yml index 30dc7ab5cff..237c27e4dc5 100644 --- a/.github/workflows/k8s-tests.yml +++ b/.github/workflows/k8s-tests.yml @@ -16,7 +16,7 @@ jobs: # databases, broker and k8s are independent, so we don't need to test each combination # lastest k8s version (https://kubernetes.io/releases/) and the oldest officially supported version # are tested (https://kubernetes.io/releases/) - - k8s: 'v1.34.1' # renovate: datasource=github-releases depName=kubernetes/kubernetes versioning=loose + - k8s: 'v1.34.0' # renovate: datasource=github-releases depName=kubernetes/kubernetes versioning=loose os: debian - k8s: 'v1.31.13' # Do not track with renovate as we likely want to rev this manually os: debian From 4b6ddca13201888a170c61009eb17a2f4fe2c221 Mon Sep 17 00:00:00 2001 From: DefectDojo release bot Date: Mon, 3 Nov 2025 17:53:35 +0000 Subject: [PATCH 126/126] Update versions in application files --- components/package.json | 2 +- dojo/__init__.py | 2 +- helm/defectdojo/Chart.yaml | 8 +++++--- helm/defectdojo/README.md | 2 +- 4 files changed, 8 insertions(+), 6 deletions(-) diff --git a/components/package.json b/components/package.json index 9b3c0a01c58..bf6b25cf39d 100644 --- a/components/package.json +++ b/components/package.json @@ -1,6 +1,6 @@ { "name": "defectdojo", - "version": "2.52.0-dev", + "version": "2.52.0", "license" : "BSD-3-Clause", "private": true, "dependencies": { diff --git a/dojo/__init__.py b/dojo/__init__.py index 0a21544849b..784b90d2773 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.52.0-dev" +__version__ = "2.52.0" __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 d7b18755fe9..68abf43f6de 100644 --- a/helm/defectdojo/Chart.yaml +++ b/helm/defectdojo/Chart.yaml @@ -1,8 +1,8 @@ apiVersion: v2 -appVersion: "2.52.0-dev" +appVersion: "2.52.0" description: A Helm chart for Kubernetes to install DefectDojo name: defectdojo -version: 1.8.0-dev +version: 1.8.0 icon: https://defectdojo.com/hubfs/DefectDojo_favicon.png maintainers: - name: madchap @@ -33,7 +33,7 @@ dependencies: # - kind: security # description: Critical bug annotations: - artifacthub.io/prerelease: "true" + artifacthub.io/prerelease: "false" artifacthub.io/changes: | - kind: changed description: DRY cloudsql-proxy @@ -45,3 +45,5 @@ annotations: description: Testing on the oldest officially supported k8s - kind: added description: Checker for maximal number of celery beats + - kind: changed + description: Bump DefectDojo to 2.52.0 diff --git a/helm/defectdojo/README.md b/helm/defectdojo/README.md index 56e713001f1..456011dab3e 100644 --- a/helm/defectdojo/README.md +++ b/helm/defectdojo/README.md @@ -495,7 +495,7 @@ kubectl delete pvc data-defectdojo-redis-0 data-defectdojo-postgresql-0 # General information about chart values -![Version: 1.8.0-dev](https://img.shields.io/badge/Version-1.8.0--dev-informational?style=flat-square) ![AppVersion: 2.52.0-dev](https://img.shields.io/badge/AppVersion-2.52.0--dev-informational?style=flat-square) +![Version: 1.8.0](https://img.shields.io/badge/Version-1.8.0-informational?style=flat-square) ![AppVersion: 2.52.0](https://img.shields.io/badge/AppVersion-2.52.0-informational?style=flat-square) A Helm chart for Kubernetes to install DefectDojo
ServiceLocation
{{ finding.service }} diff --git a/dojo/test/views.py b/dojo/test/views.py index ad98b4e17a9..b5777f15cac 100644 --- a/dojo/test/views.py +++ b/dojo/test/views.py @@ -905,15 +905,15 @@ def process_form( "minimum_severity": form.cleaned_data.get("minimum_severity"), "do_not_reactivate": form.cleaned_data.get("do_not_reactivate"), "tags": form.cleaned_data.get("tags"), - "version": form.cleaned_data.get("version"), - "branch_tag": form.cleaned_data.get("branch_tag", None), - "build_id": form.cleaned_data.get("build_id", None), - "commit_hash": form.cleaned_data.get("commit_hash", None), - "api_scan_configuration": form.cleaned_data.get("api_scan_configuration", None), - "service": form.cleaned_data.get("service", None), + "version": form.cleaned_data.get("version") or None, + "branch_tag": form.cleaned_data.get("branch_tag") or None, + "build_id": form.cleaned_data.get("build_id") or None, + "commit_hash": form.cleaned_data.get("commit_hash") or None, + "api_scan_configuration": form.cleaned_data.get("api_scan_configuration") or None, + "service": form.cleaned_data.get("service") or None, "apply_tags_to_findings": form.cleaned_data.get("apply_tags_to_findings", False), "apply_tags_to_endpoints": form.cleaned_data.get("apply_tags_to_endpoints", False), - "group_by": form.cleaned_data.get("group_by", None), + "group_by": form.cleaned_data.get("group_by") or None, "close_old_findings": form.cleaned_data.get("close_old_findings", None), "create_finding_groups_for_all_findings": form.cleaned_data.get("create_finding_groups_for_all_findings", None), }) From e55c8b625d5572f275c3f137146cf07d3c537ec0 Mon Sep 17 00:00:00 2001 From: DefectDojo release bot Date: Mon, 27 Oct 2025 16:57:06 +0000 Subject: [PATCH 085/126] Update versions in application files --- components/package.json | 2 +- dojo/__init__.py | 2 +- helm/defectdojo/Chart.yaml | 8 ++++---- helm/defectdojo/README.md | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/components/package.json b/components/package.json index 34a3610dbbc..e5898ca8f4d 100644 --- a/components/package.json +++ b/components/package.json @@ -1,6 +1,6 @@ { "name": "defectdojo", - "version": "2.51.3", + "version": "2.52.0-dev", "license" : "BSD-3-Clause", "private": true, "dependencies": { diff --git a/dojo/__init__.py b/dojo/__init__.py index 0347bb9b284..0a21544849b 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.51.3" +__version__ = "2.52.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 a783af5f4e6..8aac55e5cf0 100644 --- a/helm/defectdojo/Chart.yaml +++ b/helm/defectdojo/Chart.yaml @@ -1,8 +1,8 @@ apiVersion: v2 -appVersion: "2.51.3" +appVersion: "2.52.0-dev" description: A Helm chart for Kubernetes to install DefectDojo name: defectdojo -version: 1.7.3 +version: 1.7.4-dev icon: https://defectdojo.com/hubfs/DefectDojo_favicon.png maintainers: - name: madchap @@ -33,5 +33,5 @@ dependencies: # - kind: security # description: Critical bug annotations: - artifacthub.io/prerelease: "false" - artifacthub.io/changes: "- kind: changed\n description: Bump DefectDojo to 2.51.3\n" + artifacthub.io/prerelease: "true" + artifacthub.io/changes: "" diff --git a/helm/defectdojo/README.md b/helm/defectdojo/README.md index b5cccadbd8f..91cde3ac906 100644 --- a/helm/defectdojo/README.md +++ b/helm/defectdojo/README.md @@ -495,7 +495,7 @@ kubectl delete pvc data-defectdojo-redis-0 data-defectdojo-postgresql-0 # General information about chart values -![Version: 1.7.3](https://img.shields.io/badge/Version-1.7.3-informational?style=flat-square) ![AppVersion: 2.51.3](https://img.shields.io/badge/AppVersion-2.51.3-informational?style=flat-square) +![Version: 1.7.4-dev](https://img.shields.io/badge/Version-1.7.4--dev-informational?style=flat-square) ![AppVersion: 2.52.0-dev](https://img.shields.io/badge/AppVersion-2.52.0--dev-informational?style=flat-square) A Helm chart for Kubernetes to install DefectDojo From ccd45f78e9bfa045e5745e97e57ed8e6d8843abb Mon Sep 17 00:00:00 2001 From: DefectDojo release bot Date: Mon, 27 Oct 2025 16:57:10 +0000 Subject: [PATCH 086/126] Update versions in application files --- components/package.json | 2 +- helm/defectdojo/Chart.yaml | 8 ++++---- helm/defectdojo/README.md | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/components/package.json b/components/package.json index 34a3610dbbc..e5898ca8f4d 100644 --- a/components/package.json +++ b/components/package.json @@ -1,6 +1,6 @@ { "name": "defectdojo", - "version": "2.51.3", + "version": "2.52.0-dev", "license" : "BSD-3-Clause", "private": true, "dependencies": { diff --git a/helm/defectdojo/Chart.yaml b/helm/defectdojo/Chart.yaml index a783af5f4e6..8aac55e5cf0 100644 --- a/helm/defectdojo/Chart.yaml +++ b/helm/defectdojo/Chart.yaml @@ -1,8 +1,8 @@ apiVersion: v2 -appVersion: "2.51.3" +appVersion: "2.52.0-dev" description: A Helm chart for Kubernetes to install DefectDojo name: defectdojo -version: 1.7.3 +version: 1.7.4-dev icon: https://defectdojo.com/hubfs/DefectDojo_favicon.png maintainers: - name: madchap @@ -33,5 +33,5 @@ dependencies: # - kind: security # description: Critical bug annotations: - artifacthub.io/prerelease: "false" - artifacthub.io/changes: "- kind: changed\n description: Bump DefectDojo to 2.51.3\n" + artifacthub.io/prerelease: "true" + artifacthub.io/changes: "" diff --git a/helm/defectdojo/README.md b/helm/defectdojo/README.md index b5cccadbd8f..91cde3ac906 100644 --- a/helm/defectdojo/README.md +++ b/helm/defectdojo/README.md @@ -495,7 +495,7 @@ kubectl delete pvc data-defectdojo-redis-0 data-defectdojo-postgresql-0 # General information about chart values -![Version: 1.7.3](https://img.shields.io/badge/Version-1.7.3-informational?style=flat-square) ![AppVersion: 2.51.3](https://img.shields.io/badge/AppVersion-2.51.3-informational?style=flat-square) +![Version: 1.7.4-dev](https://img.shields.io/badge/Version-1.7.4--dev-informational?style=flat-square) ![AppVersion: 2.52.0-dev](https://img.shields.io/badge/AppVersion-2.52.0--dev-informational?style=flat-square) A Helm chart for Kubernetes to install DefectDojo From b5a46f471c8b22fbc1837160f996bd387cb677bd Mon Sep 17 00:00:00 2001 From: valentijnscholten Date: Mon, 27 Oct 2025 17:57:13 +0100 Subject: [PATCH 087/126] watson: lower async threshold from 100 to 10 (#13518) --- dojo/settings/settings.dist.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dojo/settings/settings.dist.py b/dojo/settings/settings.dist.py index 4e28c24b8ed..38eb69d49a6 100644 --- a/dojo/settings/settings.dist.py +++ b/dojo/settings/settings.dist.py @@ -93,7 +93,7 @@ DD_CELERY_LOG_LEVEL=(str, "INFO"), DD_TAG_BULK_ADD_BATCH_SIZE=(int, 1000), # Minimum number of model updated instances before search index updates as performaed asynchronously. Set to -1 to disable async updates. - DD_WATSON_ASYNC_INDEX_UPDATE_THRESHOLD=(int, 100), + DD_WATSON_ASYNC_INDEX_UPDATE_THRESHOLD=(int, 10), DD_WATSON_ASYNC_INDEX_UPDATE_BATCH_SIZE=(int, 1000), DD_FOOTER_VERSION=(str, ""), # models should be passed to celery by ID, default is False (for now) From ea09b35bb32434f41893c1a8eadffeac4040bb67 Mon Sep 17 00:00:00 2001 From: kiblik <5609770+kiblik@users.noreply.github.com> Date: Mon, 27 Oct 2025 16:57:21 +0000 Subject: [PATCH 088/126] feat(helm): Do not allow multiple celery beats (#13527) --- helm/defectdojo/Chart.yaml | 2 ++ helm/defectdojo/README.md | 2 +- helm/defectdojo/values.schema.json | 4 +++- helm/defectdojo/values.yaml | 2 ++ 4 files changed, 8 insertions(+), 2 deletions(-) diff --git a/helm/defectdojo/Chart.yaml b/helm/defectdojo/Chart.yaml index 9809fc3646f..d7b18755fe9 100644 --- a/helm/defectdojo/Chart.yaml +++ b/helm/defectdojo/Chart.yaml @@ -43,3 +43,5 @@ annotations: description: Convert existing comments to descriptors - kind: added description: Testing on the oldest officially supported k8s + - kind: added + description: Checker for maximal number of celery beats diff --git a/helm/defectdojo/README.md b/helm/defectdojo/README.md index b6ac3127dd1..56e713001f1 100644 --- a/helm/defectdojo/README.md +++ b/helm/defectdojo/README.md @@ -540,7 +540,7 @@ A Helm chart for Kubernetes to install DefectDojo | celery.beat.podAnnotations | object | `{}` | Annotations for the Celery beat pods. | | celery.beat.podSecurityContext | object | `{}` | Pod security context for the Celery beat pods. | | celery.beat.readinessProbe | object | `{}` | Enable readiness probe for Celery beat container. | -| celery.beat.replicas | int | `1` | | +| celery.beat.replicas | int | `1` | Multiple replicas are not allowed (Beat is intended to be a singleton) because scaling to >1 will double-run schedules | | celery.beat.resources.limits.cpu | string | `"2000m"` | | | celery.beat.resources.limits.memory | string | `"256Mi"` | | | celery.beat.resources.requests.cpu | string | `"100m"` | | diff --git a/helm/defectdojo/values.schema.json b/helm/defectdojo/values.schema.json index d091be4e1a2..76b1411877d 100644 --- a/helm/defectdojo/values.schema.json +++ b/helm/defectdojo/values.schema.json @@ -113,7 +113,9 @@ "type": "object" }, "replicas": { - "type": "integer" + "description": "Multiple replicas are not allowed (Beat is intended to be a singleton) because scaling to \u003e1 will double-run schedules", + "type": "integer", + "maximum": 1 }, "resources": { "type": "object", diff --git a/helm/defectdojo/values.yaml b/helm/defectdojo/values.yaml index 419fe3fe743..cd850ace3c1 100644 --- a/helm/defectdojo/values.yaml +++ b/helm/defectdojo/values.yaml @@ -255,6 +255,8 @@ celery: podSecurityContext: {} # -- Enable readiness probe for Celery beat container. readinessProbe: {} + # @schema maximum:1 + # -- Multiple replicas are not allowed (Beat is intended to be a singleton) because scaling to >1 will double-run schedules replicas: 1 resources: requests: From ffe74355d0500c44296f275e69da9d8cc3f0027d Mon Sep 17 00:00:00 2001 From: kiblik <5609770+kiblik@users.noreply.github.com> Date: Mon, 27 Oct 2025 16:57:30 +0000 Subject: [PATCH 089/126] feat(GHA): Replace ShellCheck (#13519) --- .github/workflows/shellcheck.yml | 121 ++----------------------------- 1 file changed, 7 insertions(+), 114 deletions(-) diff --git a/.github/workflows/shellcheck.yml b/.github/workflows/shellcheck.yml index ab338bfa37b..99a51ddcf6d 100644 --- a/.github/workflows/shellcheck.yml +++ b/.github/workflows/shellcheck.yml @@ -2,10 +2,7 @@ name: Shellcheck on: pull_request: -env: - SHELLCHECK_REPO: 'koalaman/shellcheck' - SHELLCHECK_VERSION: 'v0.9.0' # renovate: datasource=github-releases depName=koalaman/shellcheck - SHELLCHECK_SHA: '038fd81de6b7e20cc651571362683853670cdc71' # Renovate config is not currently adjusted to update hash - it needs to be done manually for now + jobs: shellcheck: runs-on: ubuntu-latest @@ -13,113 +10,9 @@ jobs: - name: Checkout uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - - name: Grab shellcheck - run: | - set -e - - SHELLCHECK_TARBALL_URL="https://github.com/${SHELLCHECK_REPO}/releases/download/${SHELLCHECK_VERSION}/shellcheck-${SHELLCHECK_VERSION}.linux.x86_64.tar.xz" - SHELLCHECK_TARBALL_LOC="shellcheck.tar.xz" - curl -L "${SHELLCHECK_TARBALL_URL}" -o "${SHELLCHECK_TARBALL_LOC}" - tarball_sha=$(shasum ${SHELLCHECK_TARBALL_LOC} | awk '{print $1}') - if [ "${tarball_sha}" != "${SHELLCHECK_SHA}" ]; then - echo "Got invalid SHA for shellcheck: ${tarball_sha}" - exit 1 - fi - tar -xvf "${SHELLCHECK_TARBALL_LOC}" - cd "shellcheck-${SHELLCHECK_VERSION}" || exit 1 - mv shellcheck "${GITHUB_WORKSPACE}/shellcheck" - - - name: Run shellcheck - shell: bash - run: | - set -o pipefail - - # Make sure we already put the proper shellcheck binary in place - if [ ! -f "./shellcheck" ]; then - echo "shellcheck not found" - exit 1 - fi - - # Make sure we know what to compare the PR's changes against - if [ -z "${GITHUB_BASE_REF}" ]; then - echo "No base reference supplied" - exit 1 - fi - - num_findings=0 - - # Execute shellcheck and add errors based on the output - run_shellcheck() { - local modified_shell_script="${1}" - local findings_file="findings.txt" - - # Remove leftover findings file from previous iterations - if [ -f "${findings_file}" ]; then - rm "${findings_file}" - fi - - echo "Running shellcheck against ${modified_shell_script}..." - - # If shellcheck reported no errors (exited with 0 status code), return - if ./shellcheck -f json -S warning "${modified_shell_script}" | jq -c '.[]' > "${findings_file}"; then - return 0 - fi - - # Walk each of the individual findings - while IFS= read -r finding; do - num_findings=$((num_findings+1)) - - line=$(echo "${finding}" | jq '.line') - end_line=$(echo "${finding}" | jq '.endLine') - column=$(echo "${finding}" | jq '.column') - end_column=$(echo "${finding}" | jq '.endColumn') - code=$(echo "${finding}" | jq '.code') - title="SC${code}" - message="$(echo "${finding}" | jq -r '.message') See https://github.com/koalaman/shellcheck/wiki/${title}" - - echo "Line: ${line}" - echo "End line: ${end_line}" - echo "Column: ${column}" - echo "End column: ${end_column}" - echo "Title: ${title}" - echo "Message: ${message}" - - # Raise an error with the file/line/etc - echo "::error file=${modified_shell_script},line=${line},endLine=${end_line},column=${column},endColumn=${end_column},title=${title}::${message}" - done < ${findings_file} - } - - # Find the shell scripts that were created or modified by this PR - find_modified_shell_scripts() { - shell_scripts="shell_scripts.txt" - modified_files="modified_files.txt" - modified_shell_scripts="modified_shell_scripts.txt" - - find . -name "*.sh" -or -name "*.bash" | sed 's#^\./##' > "${shell_scripts}" - git diff --name-only "origin/${GITHUB_BASE_REF}" HEAD > "${modified_files}" - - if [ ! -s "${shell_scripts}" ] || [ ! -s "${modified_files}" ]; then - echo "No modified shell scripts detected" - exit 0 - fi - - if ! grep -Fxf "${shell_scripts}" "${modified_files}" > "${modified_shell_scripts}"; then - echo "No modified shell scripts detected" - exit 0 - fi - } - - git fetch origin "${GITHUB_BASE_REF}" || exit 1 - - find_modified_shell_scripts - - # Loop through the modified shell scripts - while IFS= read -r modified_shell_script; do - run_shellcheck "${modified_shell_script}" - done < ${modified_shell_scripts} - - # If shellcheck reported any findings, fail the workflow - if [ ${num_findings} -gt 0 ]; then - echo "shellcheck reported ${num_findings} findings." - exit 1 - fi + - name: Run ShellCheck + uses: ludeeus/action-shellcheck@00cae500b08a931fb5698e11e79bfbd38e612a38 # 2.0.0 + with: + version: 'v0.11.0' # renovate: datasource=github-releases depName=koalaman/shellcheck versioning=loose + env: + SHELLCHECK_OPTS: -e SC1091 -e SC2086 # TODO: fix following findings From 9c5bd562577800f5ece0fed51257988e59519392 Mon Sep 17 00:00:00 2001 From: valentijnscholten Date: Mon, 27 Oct 2025 17:57:42 +0100 Subject: [PATCH 090/126] scan_added_empty.tpl: fix symlink problem (#13514) Co-authored-by: Valentijn Scholten --- dojo/templates/notifications/alert/scan_added_empty.tpl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) mode change 120000 => 100644 dojo/templates/notifications/alert/scan_added_empty.tpl diff --git a/dojo/templates/notifications/alert/scan_added_empty.tpl b/dojo/templates/notifications/alert/scan_added_empty.tpl deleted file mode 120000 index 03390a2d58d..00000000000 --- a/dojo/templates/notifications/alert/scan_added_empty.tpl +++ /dev/null @@ -1 +0,0 @@ -{% include "notifications/alert/scan_added.tpl" %} \ 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 new file mode 100644 index 00000000000..6d749556aa2 --- /dev/null +++ b/dojo/templates/notifications/alert/scan_added_empty.tpl @@ -0,0 +1 @@ +{% include notifications/alert/scan_added.tpl %} From 3881936b564f544accfef8afe4fc4192854259ae Mon Sep 17 00:00:00 2001 From: kiblik <5609770+kiblik@users.noreply.github.com> Date: Mon, 27 Oct 2025 16:57:51 +0000 Subject: [PATCH 091/126] fix(HELM): Add "artifacthub.io/changes" for renovate & dependabot (#13520) --- .github/workflows/helm-docs-updates.yml | 25 ------------------------- .github/workflows/test-helm-chart.yml | 17 +++++++++++++++-- 2 files changed, 15 insertions(+), 27 deletions(-) delete mode 100644 .github/workflows/helm-docs-updates.yml diff --git a/.github/workflows/helm-docs-updates.yml b/.github/workflows/helm-docs-updates.yml deleted file mode 100644 index 0d70215e146..00000000000 --- a/.github/workflows/helm-docs-updates.yml +++ /dev/null @@ -1,25 +0,0 @@ -name: Update HELM docs for Renovate & Dependabot - -on: - pull_request: - branches: - - master - - dev - - bugfix - - release/** - - hotfix/** - -jobs: - docs_updates: - name: Update documentation - runs-on: ubuntu-latest - if: startsWith(github.head_ref, 'renovate/') || startsWith(github.head_ref, 'dependabot/') - steps: - - name: Checkout - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - - - name: Run helm-docs - uses: losisin/helm-docs-github-action@a57fae5676e4c55a228ea654a1bcaec8dd3cf5b5 # v1.6.2 - with: - chart-search-root: "helm/defectdojo" - git-push: true diff --git a/.github/workflows/test-helm-chart.yml b/.github/workflows/test-helm-chart.yml index f7e9199ab67..0521be5c09c 100644 --- a/.github/workflows/test-helm-chart.yml +++ b/.github/workflows/test-helm-chart.yml @@ -99,12 +99,25 @@ jobs: steps: - name: Checkout uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - + + - name: Update values in HELM chart + if: startsWith(github.head_ref, 'renovate/') || startsWith(github.head_ref, 'dependabot/') + run: | + yq -i '.annotations."artifacthub.io/changes" += "- kind: changed\n description: ${{ github.event.pull_request.title }}\n"' helm/defectdojo/Chart.yaml + + - name: Run helm-docs (update) + uses: losisin/helm-docs-github-action@a57fae5676e4c55a228ea654a1bcaec8dd3cf5b5 # v1.6.2 + if: startsWith(github.head_ref, 'renovate/') || startsWith(github.head_ref, 'dependabot/') + with: + chart-search-root: "helm/defectdojo" + git-push: true + # Documentation provided in the README file needs to contain the latest information from `values.yaml` and all other related assets. # If this step fails, install https://github.com/norwoodj/helm-docs and run locally `helm-docs --chart-search-root helm/defectdojo` before committing your changes. # The helm-docs documentation will be generated for you. - - name: Run helm-docs + - name: Run helm-docs (check) uses: losisin/helm-docs-github-action@a57fae5676e4c55a228ea654a1bcaec8dd3cf5b5 # v1.6.2 + if: ! startsWith(github.head_ref, 'renovate/') || startsWith(github.head_ref, 'dependabot/') with: fail-on-diff: true chart-search-root: "helm/defectdojo" From 236d8b1cb9a2ee2e7a5ce6d1f9bbc095d45f64c9 Mon Sep 17 00:00:00 2001 From: valentijnscholten Date: Mon, 27 Oct 2025 19:26:00 +0100 Subject: [PATCH 092/126] endpoint import optimize (#13521) * endpoint import optimize * rebase migration * rebase migration --- ...oint_idx_ep_product_lower_host_and_more.py | 26 ++++++++++ dojo/endpoint/utils.py | 37 +++++++------ dojo/importers/endpoint_manager.py | 12 +++-- dojo/models.py | 19 ++++++- unittests/test_importers_performance.py | 52 +++++++++---------- 5 files changed, 99 insertions(+), 47 deletions(-) create mode 100644 dojo/db_migrations/0246_endpoint_idx_ep_product_lower_host_and_more.py diff --git a/dojo/db_migrations/0246_endpoint_idx_ep_product_lower_host_and_more.py b/dojo/db_migrations/0246_endpoint_idx_ep_product_lower_host_and_more.py new file mode 100644 index 00000000000..70ae2bd5fe1 --- /dev/null +++ b/dojo/db_migrations/0246_endpoint_idx_ep_product_lower_host_and_more.py @@ -0,0 +1,26 @@ +# Generated by Django 5.1.13 on 2025-10-23 22:01 + +import django.db.models.functions.text +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('dojo', '0245_alter_jira_instance_accepted_mapping_resolution'), + ] + + operations = [ + migrations.AddIndex( + model_name='endpoint', + index=models.Index(models.F('product'), django.db.models.functions.text.Lower('host'), name='idx_ep_product_lower_host'), + ), + migrations.AddIndex( + model_name='endpoint_status', + index=models.Index(condition=models.Q(('false_positive', False), ('mitigated', False), ('out_of_scope', False), ('risk_accepted', False)), fields=['endpoint'], name='idx_eps_active_by_endpoint'), + ), + migrations.AddIndex( + model_name='endpoint_status', + index=models.Index(condition=models.Q(('false_positive', False), ('mitigated', False), ('out_of_scope', False), ('risk_accepted', False)), fields=['finding'], name='idx_eps_active_by_finding'), + ), + ] diff --git a/dojo/endpoint/utils.py b/dojo/endpoint/utils.py index 2cc835aa974..75f81e60827 100644 --- a/dojo/endpoint/utils.py +++ b/dojo/endpoint/utils.py @@ -6,7 +6,6 @@ from django.contrib import messages from django.core.exceptions import ValidationError from django.core.validators import validate_ipv46_address -from django.db import transaction from django.db.models import Count, Q from django.http import HttpResponseRedirect from django.urls import reverse @@ -55,21 +54,27 @@ def endpoint_filter(**kwargs): def endpoint_get_or_create(**kwargs): - with transaction.atomic(): - qs = endpoint_filter(**kwargs) - count = qs.count() - if count == 0: - return Endpoint.objects.get_or_create(**kwargs) - if count == 1: - return qs.order_by("id").first(), False - logger.warning( - f"Endpoints in your database are broken. " - f"Please access {reverse('endpoint_migrate')} and migrate them to new format or remove them.", - ) - # Get the oldest endpoint first, and return that instead - # a datetime is not captured on the endpoint model, so ID - # will have to work here instead - return qs.order_by("id").first(), False + # This code looks a bit ugly/complicated. + # But this method is called so frequently that we need to optimize it. + # It executes at most one SELECT and one optional INSERT. + qs = endpoint_filter(**kwargs) + # Fetch up to two matches in a single round-trip. This covers + # the common cases efficiently: zero (create) or one (reuse). + matches = list(qs.order_by("id")[:2]) + if not matches: + # Most common case: nothing exists yet + return Endpoint.objects.create(**kwargs), True + if len(matches) == 1: + # Common case: exactly one existing endpoint + return matches[0], False + logger.warning( + f"Endpoints in your database are broken. " + f"Please access {reverse('endpoint_migrate')} and migrate them to new format or remove them.", + ) + # Get the oldest endpoint first, and return that instead + # a datetime is not captured on the endpoint model, so ID + # will have to work here instead + return matches[0], False def clean_hosts_run(apps, change): diff --git a/dojo/importers/endpoint_manager.py b/dojo/importers/endpoint_manager.py index f733d5c9e5a..ccfff345c40 100644 --- a/dojo/importers/endpoint_manager.py +++ b/dojo/importers/endpoint_manager.py @@ -31,6 +31,7 @@ def add_endpoints_to_unsaved_finding( self.clean_unsaved_endpoints(endpoints) for endpoint in endpoints: ep = None + eps = [] try: ep, _ = endpoint_get_or_create( protocol=endpoint.protocol, @@ -41,6 +42,7 @@ def add_endpoints_to_unsaved_finding( query=endpoint.query, fragment=endpoint.fragment, product=finding.test.engagement.product) + eps.append(ep) except (MultipleObjectsReturned): msg = ( f"Endpoints in your database are broken. " @@ -48,10 +50,12 @@ def add_endpoints_to_unsaved_finding( ) raise Exception(msg) - Endpoint_Status.objects.get_or_create( - finding=finding, - endpoint=ep, - defaults={"date": finding.date}) + # bulk_create will translate to INSERT WITH IGNORE CONFLICTS + # much faster than get_or_create which issues two queries per endpoint + # bulk_create will not trigger endpoint_status.save and signals which is fine for now + rows = [Endpoint_Status(finding=finding, endpoint=e, date=finding.date) for e in eps] + Endpoint_Status.objects.bulk_create(rows, ignore_conflicts=True, batch_size=1000) + logger.debug(f"IMPORT_SCAN: {len(endpoints)} endpoints imported") @dojo_async_task diff --git a/dojo/models.py b/dojo/models.py index dccfbaa4c7e..396e851f9b4 100644 --- a/dojo/models.py +++ b/dojo/models.py @@ -25,7 +25,7 @@ from django.core.files.base import ContentFile from django.core.validators import MaxValueValidator, MinValueValidator, RegexValidator, validate_ipv46_address from django.db import connection, models -from django.db.models import Count, JSONField, Q +from django.db.models import Count, F, JSONField, Q from django.db.models.expressions import Case, When from django.db.models.functions import Lower from django.urls import reverse @@ -1690,6 +1690,17 @@ class Meta: indexes = [ models.Index(fields=["finding", "mitigated"]), models.Index(fields=["endpoint", "mitigated"]), + # Optimize frequent lookups of "active" statuses (mitigated/flags all False) + models.Index( + name="idx_eps_active_by_endpoint", + fields=["endpoint"], + condition=Q(mitigated=False, false_positive=False, out_of_scope=False, risk_accepted=False), + ), + models.Index( + name="idx_eps_active_by_finding", + fields=["finding"], + condition=Q(mitigated=False, false_positive=False, out_of_scope=False, risk_accepted=False), + ), ] constraints = [ models.UniqueConstraint(fields=["finding", "endpoint"], name="endpoint-finding relation"), @@ -1749,6 +1760,12 @@ class Meta: ordering = ["product", "host", "protocol", "port", "userinfo", "path", "query", "fragment"] indexes = [ models.Index(fields=["product"]), + # Fast case-insensitive equality on host within product scope + models.Index( + F("product"), + Lower("host"), + name="idx_ep_product_lower_host", + ), ] def __hash__(self): diff --git a/unittests/test_importers_performance.py b/unittests/test_importers_performance.py index 3b4ce357c85..b3a3709961d 100644 --- a/unittests/test_importers_performance.py +++ b/unittests/test_importers_performance.py @@ -178,11 +178,11 @@ def test_import_reimport_reimport_performance_async(self): configure_pghistory_triggers() self._import_reimport_performance( - expected_num_queries1=593, + expected_num_queries1=340, expected_num_async_tasks1=10, - expected_num_queries2=498, + expected_num_queries2=288, expected_num_async_tasks2=22, - expected_num_queries3=289, + expected_num_queries3=175, expected_num_async_tasks3=20, ) @@ -196,11 +196,11 @@ def test_import_reimport_reimport_performance_pghistory_async(self): configure_pghistory_triggers() self._import_reimport_performance( - expected_num_queries1=559, + expected_num_queries1=306, expected_num_async_tasks1=10, - expected_num_queries2=491, + expected_num_queries2=281, expected_num_async_tasks2=22, - expected_num_queries3=284, + expected_num_queries3=170, expected_num_async_tasks3=20, ) @@ -220,11 +220,11 @@ def test_import_reimport_reimport_performance_no_async(self): testuser.usercontactinfo.block_execution = True testuser.usercontactinfo.save() self._import_reimport_performance( - expected_num_queries1=603, + expected_num_queries1=350, expected_num_async_tasks1=10, - expected_num_queries2=515, + expected_num_queries2=305, expected_num_async_tasks2=22, - expected_num_queries3=304, + expected_num_queries3=190, expected_num_async_tasks3=20, ) @@ -242,11 +242,11 @@ def test_import_reimport_reimport_performance_pghistory_no_async(self): testuser.usercontactinfo.save() self._import_reimport_performance( - expected_num_queries1=569, + expected_num_queries1=316, expected_num_async_tasks1=10, - expected_num_queries2=508, + expected_num_queries2=298, expected_num_async_tasks2=22, - expected_num_queries3=299, + expected_num_queries3=185, expected_num_async_tasks3=20, ) @@ -268,11 +268,11 @@ def test_import_reimport_reimport_performance_no_async_with_product_grading(self self.system_settings(enable_product_grade=True) self._import_reimport_performance( - expected_num_queries1=604, + expected_num_queries1=351, expected_num_async_tasks1=11, - expected_num_queries2=516, + expected_num_queries2=306, expected_num_async_tasks2=23, - expected_num_queries3=305, + expected_num_queries3=191, expected_num_async_tasks3=21, ) @@ -291,11 +291,11 @@ def test_import_reimport_reimport_performance_pghistory_no_async_with_product_gr self.system_settings(enable_product_grade=True) self._import_reimport_performance( - expected_num_queries1=570, + expected_num_queries1=317, expected_num_async_tasks1=11, - expected_num_queries2=509, + expected_num_queries2=299, expected_num_async_tasks2=23, - expected_num_queries3=300, + expected_num_queries3=186, expected_num_async_tasks3=21, ) @@ -414,9 +414,9 @@ def test_deduplication_performance_async(self): self.system_settings(enable_deduplication=True) self._deduplication_performance( - expected_num_queries1=660, + expected_num_queries1=311, expected_num_async_tasks1=12, - expected_num_queries2=519, + expected_num_queries2=204, expected_num_async_tasks2=12, check_duplicates=False, # Async mode - deduplication happens later ) @@ -431,9 +431,9 @@ def test_deduplication_performance_pghistory_async(self): self.system_settings(enable_deduplication=True) self._deduplication_performance( - expected_num_queries1=624, + expected_num_queries1=275, expected_num_async_tasks1=12, - expected_num_queries2=500, + expected_num_queries2=185, expected_num_async_tasks2=12, check_duplicates=False, # Async mode - deduplication happens later ) @@ -452,9 +452,9 @@ def test_deduplication_performance_no_async(self): testuser.usercontactinfo.save() self._deduplication_performance( - expected_num_queries1=672, + expected_num_queries1=323, expected_num_async_tasks1=12, - expected_num_queries2=633, + expected_num_queries2=318, expected_num_async_tasks2=12, ) @@ -472,8 +472,8 @@ def test_deduplication_performance_pghistory_no_async(self): testuser.usercontactinfo.save() self._deduplication_performance( - expected_num_queries1=636, + expected_num_queries1=287, expected_num_async_tasks1=12, - expected_num_queries2=596, + expected_num_queries2=281, expected_num_async_tasks2=12, ) From 04a28aadeda83832880a8c6f87501a45b426f884 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 28 Oct 2025 10:15:24 -0600 Subject: [PATCH 093/126] chore(deps): update dependency renovatebot/renovate from 41.159.4 to v41.163.1 (.github/workflows/renovate.yaml) (#13533) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/renovate.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/renovate.yaml b/.github/workflows/renovate.yaml index 2f12f66a1dc..b772301bdac 100644 --- a/.github/workflows/renovate.yaml +++ b/.github/workflows/renovate.yaml @@ -21,4 +21,4 @@ jobs: uses: suzuki-shunsuke/github-action-renovate-config-validator@c22827f47f4f4a5364bdba19e1fe36907ef1318e # v1.1.1 with: strict: "true" - validator_version: 41.159.4 # renovate: datasource=github-releases depName=renovatebot/renovate + validator_version: 41.163.1 # renovate: datasource=github-releases depName=renovatebot/renovate From 16765177b3c3c6a09337cd114f938ca2390fab4c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 28 Oct 2025 10:15:48 -0600 Subject: [PATCH 094/126] Bump psycopg[c] from 3.2.11 to 3.2.12 (#13535) Bumps [psycopg[c]](https://github.com/psycopg/psycopg) from 3.2.11 to 3.2.12. - [Changelog](https://github.com/psycopg/psycopg/blob/master/docs/news.rst) - [Commits](https://github.com/psycopg/psycopg/compare/3.2.11...3.2.12) --- updated-dependencies: - dependency-name: psycopg[c] dependency-version: 3.2.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.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 6315cc4cd8f..7b2c61f1865 100644 --- a/requirements.txt +++ b/requirements.txt @@ -29,7 +29,7 @@ lxml==6.0.2 Markdown==3.9 openpyxl==3.1.5 Pillow==12.0.0 # required by django-imagekit -psycopg[c]==3.2.11 +psycopg[c]==3.2.12 cryptography==46.0.3 python-dateutil==2.9.0.post0 redis==7.0.0 From 5164ce9cb2522df7e2a9372ee171e6cc027672ed Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 28 Oct 2025 12:21:21 -0600 Subject: [PATCH 095/126] chore(deps): update dependency node from 22.21.0 to v24 (.github/workflows/validate_docs_build.yml) (#13550) 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 c1b4f497948..15c4bbd58c0 100644 --- a/.github/workflows/gh-pages.yml +++ b/.github/workflows/gh-pages.yml @@ -21,7 +21,7 @@ jobs: - name: Setup Node uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0 with: - node-version: '22.21.0' # TODO: Renovate helper might not be needed here - needs to be fully tested + node-version: '24.10.0' # TODO: Renovate helper might not be needed here - needs to be fully tested - name: Cache dependencies uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 diff --git a/.github/workflows/validate_docs_build.yml b/.github/workflows/validate_docs_build.yml index 25e14dc173b..d4e55bb64a8 100644 --- a/.github/workflows/validate_docs_build.yml +++ b/.github/workflows/validate_docs_build.yml @@ -18,7 +18,7 @@ jobs: - name: Setup Node uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0 with: - node-version: '22.21.0' # TODO: Renovate helper might not be needed here - needs to be fully tested + node-version: '24.10.0' # TODO: Renovate helper might not be needed here - needs to be fully tested - name: Cache dependencies uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 From d58860930a727094432e0335a1f3c2fed890c403 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 28 Oct 2025 14:10:03 -0500 Subject: [PATCH 096/126] chore(deps): bump redis from 7.0.0 to 7.0.1 (#13552) Bumps [redis](https://github.com/redis/redis-py) from 7.0.0 to 7.0.1. - [Release notes](https://github.com/redis/redis-py/releases) - [Changelog](https://github.com/redis/redis-py/blob/master/CHANGES) - [Commits](https://github.com/redis/redis-py/compare/v7.0.0...v7.0.1) --- updated-dependencies: - dependency-name: redis dependency-version: 7.0.1 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 7b2c61f1865..180b1d15b7d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -32,7 +32,7 @@ Pillow==12.0.0 # required by django-imagekit psycopg[c]==3.2.12 cryptography==46.0.3 python-dateutil==2.9.0.post0 -redis==7.0.0 +redis==7.0.1 requests==2.32.5 sqlalchemy==2.0.44 # Required by Celery broker transport urllib3==2.5.0 From bd84361d7e4bb866b1c253b8bd7044acb8809314 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 28 Oct 2025 14:12:10 -0500 Subject: [PATCH 097/126] chore(deps): bump bleach from 6.2.0 to 6.3.0 (#13553) Bumps [bleach](https://github.com/mozilla/bleach) from 6.2.0 to 6.3.0. - [Changelog](https://github.com/mozilla/bleach/blob/main/CHANGES) - [Commits](https://github.com/mozilla/bleach/compare/v6.2.0...v6.3.0) --- updated-dependencies: - dependency-name: bleach dependency-version: 6.3.0 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 180b1d15b7d..0045a712927 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,6 @@ # requirements.txt for DefectDojo using Python 3.x asteval==1.0.6 -bleach==6.2.0 +bleach==6.3.0 bleach[css] celery==5.5.3 defusedxml==0.7.1 From 6afbcbf359a4c0e803599ed3bce13f6d8200f1a8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 28 Oct 2025 14:12:34 -0500 Subject: [PATCH 098/126] chore(deps): bump boto3 from 1.40.58 to 1.40.60 (#13554) Bumps [boto3](https://github.com/boto/boto3) from 1.40.58 to 1.40.60. - [Release notes](https://github.com/boto/boto3/releases) - [Commits](https://github.com/boto/boto3/compare/1.40.58...1.40.60) --- updated-dependencies: - dependency-name: boto3 dependency-version: 1.40.60 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 0045a712927..3c374236934 100644 --- a/requirements.txt +++ b/requirements.txt @@ -62,7 +62,7 @@ django-ratelimit==4.1.0 argon2-cffi==25.1.0 blackduck==1.1.3 pycurl==7.45.7 # Required for Celery Broker AWS (SQS) support -boto3==1.40.58 # Required for Celery Broker AWS (SQS) support +boto3==1.40.60 # Required for Celery Broker AWS (SQS) support netaddr==1.3.0 vulners==3.1.1 fontawesomefree==6.6.0 From 98e7e1e8d3cdafae65c929463d80cc5a8be9e8b5 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 28 Oct 2025 14:14:12 -0500 Subject: [PATCH 099/126] chore(deps): update dependency renovatebot/renovate from 41.163.1 to v41.163.6 (.github/workflows/renovate.yaml) (#13556) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/renovate.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/renovate.yaml b/.github/workflows/renovate.yaml index b772301bdac..89968a914fd 100644 --- a/.github/workflows/renovate.yaml +++ b/.github/workflows/renovate.yaml @@ -21,4 +21,4 @@ jobs: uses: suzuki-shunsuke/github-action-renovate-config-validator@c22827f47f4f4a5364bdba19e1fe36907ef1318e # v1.1.1 with: strict: "true" - validator_version: 41.163.1 # renovate: datasource=github-releases depName=renovatebot/renovate + validator_version: 41.163.6 # renovate: datasource=github-releases depName=renovatebot/renovate From 62ba5e59bdedd81941cc3215854053a4cadc493a Mon Sep 17 00:00:00 2001 From: Jino Tesauro <53376807+Jino-T@users.noreply.github.com> Date: Wed, 29 Oct 2025 13:06:58 -0500 Subject: [PATCH 100/126] Added Ability to Edit found_by value in API (#13542) * Added code, tested on the api, not tested in pro-ui yet * Update dojo/api_v2/serializers.py Use found_by.set to better replace values Co-authored-by: Cody Maffucci <46459665+Maffooch@users.noreply.github.com> --------- Co-authored-by: Cody Maffucci <46459665+Maffooch@users.noreply.github.com> --- dojo/api_v2/serializers.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/dojo/api_v2/serializers.py b/dojo/api_v2/serializers.py index 5de0698edee..806a8a1453a 100644 --- a/dojo/api_v2/serializers.py +++ b/dojo/api_v2/serializers.py @@ -1692,6 +1692,9 @@ class FindingSerializer(serializers.ModelSerializer): many=True, read_only=True, source="risk_acceptance_set", ) push_to_jira = serializers.BooleanField(default=False) + found_by = serializers.PrimaryKeyRelatedField( + queryset=Test_Type.objects.all(), many=True, + ) age = serializers.IntegerField(read_only=True) sla_days_remaining = serializers.IntegerField(read_only=True, allow_null=True) finding_meta = FindingMetaSerializer(read_only=True, many=True) @@ -1774,6 +1777,16 @@ def update(self, instance, validated_data): if parsed_vulnerability_ids: save_vulnerability_ids(instance, parsed_vulnerability_ids) + # Get found_by from validated_data + found_by = validated_data.pop("found_by", None) + # Handle updates to found_by data + if found_by: + instance.found_by.set(found_by) + # If there is no argument entered for found_by, the user would like to clear out the values on the Finding's found_by field + # Findings still maintain original found_by value associated with their test + # In the event the user does not supply the found_by field at all, we do not modify it + elif isinstance(found_by, list) and len(found_by) == 0: + instance.found_by.clear() instance = super().update( instance, validated_data, ) From 34a937bb8fd123f789ae8273f08a72fa78fc0612 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 30 Oct 2025 08:57:22 -0600 Subject: [PATCH 101/126] chore(deps): update dependency renovatebot/renovate from 41.163.6 to v41.163.7 (.github/workflows/renovate.yaml) (#13558) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/renovate.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/renovate.yaml b/.github/workflows/renovate.yaml index 89968a914fd..b888abb9320 100644 --- a/.github/workflows/renovate.yaml +++ b/.github/workflows/renovate.yaml @@ -21,4 +21,4 @@ jobs: uses: suzuki-shunsuke/github-action-renovate-config-validator@c22827f47f4f4a5364bdba19e1fe36907ef1318e # v1.1.1 with: strict: "true" - validator_version: 41.163.6 # renovate: datasource=github-releases depName=renovatebot/renovate + validator_version: 41.163.7 # renovate: datasource=github-releases depName=renovatebot/renovate From 788572f46adc53ee2ed3c1e426ef0723a54fcbd7 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 30 Oct 2025 08:58:19 -0600 Subject: [PATCH 102/126] chore(deps): update dependency node from 24.10.0 to v24.11.0 (.github/workflows/validate_docs_build.yml) (#13560) 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 15c4bbd58c0..217f0317688 100644 --- a/.github/workflows/gh-pages.yml +++ b/.github/workflows/gh-pages.yml @@ -21,7 +21,7 @@ jobs: - name: Setup Node uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0 with: - node-version: '24.10.0' # TODO: Renovate helper might not be needed here - needs to be fully tested + node-version: '24.11.0' # TODO: Renovate helper might not be needed here - needs to be fully tested - name: Cache dependencies uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 diff --git a/.github/workflows/validate_docs_build.yml b/.github/workflows/validate_docs_build.yml index d4e55bb64a8..01e2371bec3 100644 --- a/.github/workflows/validate_docs_build.yml +++ b/.github/workflows/validate_docs_build.yml @@ -18,7 +18,7 @@ jobs: - name: Setup Node uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0 with: - node-version: '24.10.0' # TODO: Renovate helper might not be needed here - needs to be fully tested + node-version: '24.11.0' # TODO: Renovate helper might not be needed here - needs to be fully tested - name: Cache dependencies uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 From 16c749c0082981d922c419db7628f867fe9649e2 Mon Sep 17 00:00:00 2001 From: manuelsommer <47991713+manuel-sommer@users.noreply.github.com> Date: Thu, 30 Oct 2025 16:25:51 +0100 Subject: [PATCH 103/126] :bug: add middleware to handle social auth provider unavailability gracefully (#13523) * :tada: add middleware to handle social auth provider unavailability gracefully * - * - * update according to recommendation * add unittest * update * update on unittest * add integrationtest * update unittest description * udpate * udpate * fix unittest * add authforbidden --- dojo/middleware.py | 22 +++ dojo/settings/settings.dist.py | 2 +- .../test_social_auth_failure_handling.py | 142 ++++++++++++++++++ 3 files changed, 165 insertions(+), 1 deletion(-) create mode 100644 unittests/test_social_auth_failure_handling.py diff --git a/dojo/middleware.py b/dojo/middleware.py index 5d63b1a35a0..ab89bf5a849 100644 --- a/dojo/middleware.py +++ b/dojo/middleware.py @@ -6,13 +6,18 @@ from urllib.parse import quote import pghistory.middleware +import requests from auditlog.context import set_actor from auditlog.middleware import AuditlogMiddleware as _AuditlogMiddleware from django.conf import settings +from django.contrib import messages from django.db import models from django.http import HttpResponseRedirect +from django.shortcuts import redirect from django.urls import reverse from django.utils.functional import SimpleLazyObject +from social_core.exceptions import AuthCanceled, AuthFailed, AuthForbidden +from social_django.middleware import SocialAuthExceptionMiddleware from watson.middleware import SearchContextMiddleware from watson.search import search_context_manager @@ -75,6 +80,23 @@ def __call__(self, request): return self.get_response(request) +class CustomSocialAuthExceptionMiddleware(SocialAuthExceptionMiddleware): + def process_exception(self, request, exception): + if isinstance(exception, requests.exceptions.RequestException): + messages.error(request, "Please use the standard login below.") + return redirect("/login?force_login_form") + if isinstance(exception, AuthCanceled): + messages.warning(request, "Social login was canceled. Please try again or use the standard login.") + return redirect("/login?force_login_form") + if isinstance(exception, AuthFailed): + messages.error(request, "Social login failed. Please try again or use the standard login.") + return redirect("/login?force_login_form") + if isinstance(exception, AuthForbidden): + messages.error(request, "You are not authorized to log in via this method. Please contact support or use the standard login.") + return redirect("/login?force_login_form") + return super().process_exception(request, exception) + + class DojoSytemSettingsMiddleware: _thread_local = local() diff --git a/dojo/settings/settings.dist.py b/dojo/settings/settings.dist.py index 6243e44a690..b2be58bc64d 100644 --- a/dojo/settings/settings.dist.py +++ b/dojo/settings/settings.dist.py @@ -936,7 +936,7 @@ def generate_url(scheme, double_slashes, user, password, host, port, path, param "django.middleware.clickjacking.XFrameOptionsMiddleware", "dojo.middleware.LoginRequiredMiddleware", "dojo.middleware.AdditionalHeaderMiddleware", - "social_django.middleware.SocialAuthExceptionMiddleware", + "dojo.middleware.CustomSocialAuthExceptionMiddleware", "crum.CurrentRequestUserMiddleware", "dojo.middleware.AuditlogMiddleware", "dojo.middleware.AsyncSearchContextMiddleware", diff --git a/unittests/test_social_auth_failure_handling.py b/unittests/test_social_auth_failure_handling.py new file mode 100644 index 00000000000..83f69471a02 --- /dev/null +++ b/unittests/test_social_auth_failure_handling.py @@ -0,0 +1,142 @@ +from unittest.mock import patch + +from django.contrib import messages +from django.contrib.auth.models import AnonymousUser +from django.contrib.messages.storage.fallback import FallbackStorage +from django.contrib.sessions.middleware import SessionMiddleware +from django.http import HttpResponse +from django.test import RequestFactory, override_settings +from requests.exceptions import ConnectionError as RequestsConnectionError +from social_core.exceptions import AuthCanceled, AuthFailed, AuthForbidden + +from dojo.middleware import CustomSocialAuthExceptionMiddleware + +from .dojo_test_case import DojoTestCase + + +class TestSocialAuthMiddlewareUnit(DojoTestCase): + + """ + Unit tests: + Directly test CustomSocialAuthExceptionMiddleware behavior + by simulating exceptions (ConnectionError, AuthCanceled, AuthFailed, AuthForbidden), + without relying on actual backend configuration or whether the + /complete// URLs are registered and accessible. + """ + + def setUp(self): + self.factory = RequestFactory() + self.middleware = CustomSocialAuthExceptionMiddleware(lambda *_: HttpResponse("OK")) + + def _prepare_request(self, path): + request = self.factory.get(path) + request.user = AnonymousUser() + SessionMiddleware(lambda *_: None).process_request(request) + request.session.save() + request._messages = FallbackStorage(request) + return request + + def test_social_auth_exception_redirects_to_login(self): + login_paths = [ + "/login/oidc/", + "/login/auth0/", + "/login/google-oauth2/", + "/login/okta-oauth2/", + "/login/azuread-tenant-oauth2/", + "/login/gitlab/", + "/login/keycloak-oauth2/", + "/login/github/", + ] + exceptions = [ + (RequestsConnectionError("Host unreachable"), "Please use the standard login below."), + (AuthCanceled("User canceled login"), "Social login was canceled. Please try again or use the standard login."), + (AuthFailed("Token exchange failed"), "Social login failed. Please try again or use the standard login."), + (AuthForbidden("User not allowed"), "You are not authorized to log in via this method. Please contact support or use the standard login."), + ] + for path in login_paths: + for exception, expected_message in exceptions: + with self.subTest(path=path, exception=type(exception).__name__): + request = self._prepare_request(path) + response = self.middleware.process_exception(request, exception) + self.assertEqual(response.status_code, 302) + self.assertEqual(response.url, "/login?force_login_form") + storage = list(messages.get_messages(request)) + self.assertTrue(any(expected_message in str(msg) for msg in storage)) + + def test_non_social_auth_path_still_redirects_on_auth_exception(self): + """Ensure middleware handles AuthFailed even on unrelated paths.""" + request = self._prepare_request("/some/other/path/") + exception = AuthFailed("Should be handled globally") + response = self.middleware.process_exception(request, exception) + self.assertEqual(response.status_code, 302) + self.assertEqual(response.url, "/login?force_login_form") + storage = list(messages.get_messages(request)) + self.assertTrue(any("Social login failed. Please try again or use the standard login." in str(msg) for msg in storage)) + + def test_non_social_auth_path_redirects_on_auth_forbidden(self): + """Ensure middleware handles AuthForbidden even on unrelated paths.""" + request = self._prepare_request("/some/other/path/") + exception = AuthForbidden("User not allowed") + response = self.middleware.process_exception(request, exception) + self.assertEqual(response.status_code, 302) + self.assertEqual(response.url, "/login?force_login_form") + storage = list(messages.get_messages(request)) + self.assertTrue(any("You are not authorized to log in via this method." in str(msg) for msg in storage)) + + +@override_settings( + AUTHENTICATION_BACKENDS=( + "social_core.backends.github.GithubOAuth2", + "social_core.backends.gitlab.GitLabOAuth2", + "social_core.backends.keycloak.KeycloakOAuth2", + "social_core.backends.azuread_tenant.AzureADTenantOAuth2", + "social_core.backends.auth0.Auth0OAuth2", + "social_core.backends.okta.OktaOAuth2", + "social_core.backends.open_id_connect.OpenIdConnectAuth", + "django.contrib.auth.backends.ModelBackend", + ), +) +class TestSocialAuthIntegrationFailures(DojoTestCase): + + """ + Integration tests: + Simulate social login failures by calling /complete// URLs + and mocking auth_complete() to raise AuthFailed, AuthCanceled, and AuthForbidden. + Verifies that the middleware is correctly integrated and handles backend failures. + """ + + BACKEND_CLASS_PATHS = { + "github": "social_core.backends.github.GithubOAuth2", + "gitlab": "social_core.backends.gitlab.GitLabOAuth2", + "keycloak": "social_core.backends.keycloak.KeycloakOAuth2", + "azuread-tenant-oauth2": "social_core.backends.azuread_tenant.AzureADTenantOAuth2", + "auth0": "social_core.backends.auth0.Auth0OAuth2", + "okta-oauth2": "social_core.backends.okta.OktaOAuth2", + "oidc": "social_core.backends.open_id_connect.OpenIdConnectAuth", + } + + def _test_backend_exception(self, backend_slug, exception, expected_message): + backend_class_path = self.BACKEND_CLASS_PATHS[backend_slug] + with patch(f"{backend_class_path}.auth_complete", side_effect=exception): + response = self.client.get(f"/complete/{backend_slug}/", follow=True) + self.assertEqual(response.status_code, 200) + self.assertContains(response, expected_message) + + def test_all_backends_auth_failed(self): + for backend in self.BACKEND_CLASS_PATHS: + with self.subTest(backend=backend): + self._test_backend_exception(backend, AuthFailed(backend=None), "Social login failed. Please try again or use the standard login.") + + def test_all_backends_auth_canceled(self): + for backend in self.BACKEND_CLASS_PATHS: + with self.subTest(backend=backend): + self._test_backend_exception(backend, AuthCanceled(backend=None), "Social login was canceled. Please try again or use the standard login.") + + def test_all_backends_auth_forbidden(self): + for backend in self.BACKEND_CLASS_PATHS: + with self.subTest(backend=backend): + self._test_backend_exception( + backend, + AuthForbidden(backend=None), + "You are not authorized to log in via this method. Please contact support or use the standard login.", + ) From e15bdddb9fe49f8a36fbe1dfa35322f06d689ee6 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 30 Oct 2025 10:09:11 -0600 Subject: [PATCH 104/126] chore(deps): update dependency renovatebot/renovate from 41.163.7 to v41.165.5 (.github/workflows/renovate.yaml) (#13559) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/renovate.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/renovate.yaml b/.github/workflows/renovate.yaml index b888abb9320..a3c7a1c6f36 100644 --- a/.github/workflows/renovate.yaml +++ b/.github/workflows/renovate.yaml @@ -21,4 +21,4 @@ jobs: uses: suzuki-shunsuke/github-action-renovate-config-validator@c22827f47f4f4a5364bdba19e1fe36907ef1318e # v1.1.1 with: strict: "true" - validator_version: 41.163.7 # renovate: datasource=github-releases depName=renovatebot/renovate + validator_version: 41.165.5 # renovate: datasource=github-releases depName=renovatebot/renovate From 1bac2073d89d3c1a185f2eec59cf8c5d3d077057 Mon Sep 17 00:00:00 2001 From: "v." <29890336+yuwwx@users.noreply.github.com> Date: Thu, 30 Oct 2025 19:32:02 +0300 Subject: [PATCH 105/126] docs: correct LDAP authentication instructions for Alpine-based Dockerfiles (#13544) --- .../en/open_source/ldap-authentication.md | 33 ++++++++++++------- 1 file changed, 22 insertions(+), 11 deletions(-) diff --git a/docs/content/en/open_source/ldap-authentication.md b/docs/content/en/open_source/ldap-authentication.md index ba246ae8aa1..e8db98ff232 100644 --- a/docs/content/en/open_source/ldap-authentication.md +++ b/docs/content/en/open_source/ldap-authentication.md @@ -7,12 +7,12 @@ weight: 4 ## LDAP Authentication -Out of the box Defect Dojo does not support LDAP authentication. +Out of the box DefectDojo does not support LDAP authentication. -*However*, since Defect Dojo is built using Django, it isn't too difficult to add support for LDAP. +*However*, since DefectDojo is built using Django, it isn't too difficult to add support for LDAP. So long as you don't mind building your own Docker images... -We will need to modify a grand total of 4-5 files, depending on how you want to pass Dojo your LDAP secrets. +We will need to modify a grand total of 4-5 files, depending on how you want to pass DefectDojo your LDAP secrets. - Dockerfile.django-* - Dockerfile.nginx-* @@ -23,7 +23,14 @@ We will need to modify a grand total of 4-5 files, depending on how you want to #### Dockerfile modifications -In both Dockerfile.django and Dockerfile.nginx, you want to add the following lines to the apt-get install layers: +In both `Dockerfile.django-alpine` and `Dockerfile.nginx-alpine`, you need to add the following lines to the `apk add` layers: + +```bash +openldap-dev \ +cyrus-sasl-dev \ +``` + +Also, in `Dockerfile.django-debian`, you need to add the following lines to the `apt-get install` layers: ```bash libldap2-dev \ @@ -42,8 +49,8 @@ Please check for the latest version of these requirements at the time of impleme Otherwise add the following to requirements.txt: ```python -python-ldap==3.4.2 -django-auth-ldap==4.1.0 +python-ldap==3.4.5 +django-auth-ldap==5.2.0 ``` @@ -55,14 +62,17 @@ At the top of the file: ```python import ldap from django_auth_ldap.config import LDAPSearch, GroupOfNamesType +import environ ``` Then further down add LDAP settings to the env dict: ```python # LDAP -DD_LDAP_SERVER_URI=(str, 'ldap://ldap.example.com'), -DD_LDAP_BIND_DN=(str, ''), -DD_LDAP_BIND_PASSWORD=(str, ''), +env = environ.FileAwareEnv( + DD_LDAP_SERVER_URI=(str, 'ldap://ldap.example.com'), + DD_LDAP_BIND_DN=(str, ''), + DD_LDAP_BIND_PASSWORD=(str, ''), +) ``` Then under the env dict add: @@ -70,6 +80,7 @@ Then under the env dict add: AUTH_LDAP_SERVER_URI = env('DD_LDAP_SERVER_URI') AUTH_LDAP_BIND_DN = env('DD_LDAP_BIND_DN') AUTH_LDAP_BIND_PASSWORD = env('DD_LDAP_BIND_PASSWORD') + AUTH_LDAP_USER_SEARCH = LDAPSearch( "ou=Groups,dc=example,dc=com", ldap.SCOPE_SUBTREE, "(uid=%(user)s)" ) @@ -116,7 +127,7 @@ Read the docs for Django Authentication with LDAP here: https://django-auth-ldap #### docker-compose.yml -In order to pass the variables to the local_settings.py file via docker, it's a good idea to add these to the docker compose file. +In order to pass the variables to the `local_settings.py` file via docker, it's a good idea to add these to the `docker-compose.yml` file. You can do this by adding the following variables to the environment section for the uwsgi image: ```yaml @@ -125,4 +136,4 @@ DD_LDAP_BIND_DN: "${DD_LDAP_BIND_DN:-}" DD_LDAP_BIND_PASSWORD: "${DD_LDAP_BIND_PASSWORD:-}" ``` -Alternatively you can set these values in a local_settings.py file. +Alternatively you can set these values in a `local_settings.py` file. From 155a404de9e3a48f225601b8e7c0bc1207fc033e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 30 Oct 2025 11:38:54 -0500 Subject: [PATCH 106/126] chore(deps): bump boto3 from 1.40.60 to 1.40.62 (#13569) Bumps [boto3](https://github.com/boto/boto3) from 1.40.60 to 1.40.62. - [Release notes](https://github.com/boto/boto3/releases) - [Commits](https://github.com/boto/boto3/compare/1.40.60...1.40.62) --- updated-dependencies: - dependency-name: boto3 dependency-version: 1.40.62 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 3c374236934..53c5105d8b3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -62,7 +62,7 @@ django-ratelimit==4.1.0 argon2-cffi==25.1.0 blackduck==1.1.3 pycurl==7.45.7 # Required for Celery Broker AWS (SQS) support -boto3==1.40.60 # Required for Celery Broker AWS (SQS) support +boto3==1.40.62 # Required for Celery Broker AWS (SQS) support netaddr==1.3.0 vulners==3.1.1 fontawesomefree==6.6.0 From bda17ded3c4169c4ebef94318cf12b3354648123 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 30 Oct 2025 11:40:52 -0500 Subject: [PATCH 107/126] chore(deps): bump python-gitlab from 6.5.0 to 7.0.0 (#13570) Bumps [python-gitlab](https://github.com/python-gitlab/python-gitlab) from 6.5.0 to 7.0.0. - [Release notes](https://github.com/python-gitlab/python-gitlab/releases) - [Changelog](https://github.com/python-gitlab/python-gitlab/blob/main/CHANGELOG.md) - [Commits](https://github.com/python-gitlab/python-gitlab/compare/v6.5.0...v7.0.0) --- updated-dependencies: - dependency-name: python-gitlab dependency-version: 7.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 53c5105d8b3..835132088fb 100644 --- a/requirements.txt +++ b/requirements.txt @@ -43,7 +43,7 @@ titlecase==2.4.1 social-auth-app-django==5.6.0 social-auth-core==4.8.1 gitpython==3.1.45 -python-gitlab==6.5.0 +python-gitlab==7.0.0 cpe==1.3.1 packageurl-python==0.17.5 django-crum==0.7.9 From 8e2e6cd7c2ae74aa880984e71c154adcfc528069 Mon Sep 17 00:00:00 2001 From: Paul Osinski <42211303+paulOsinski@users.noreply.github.com> Date: Thu, 30 Oct 2025 13:02:33 -0400 Subject: [PATCH 108/126] [docs] Integrators/Connectors updates (#13549) * add servicenow docs * update connectors docs * Update docs/content/en/share_your_findings/integrations.md Co-authored-by: Cody Maffucci <46459665+Maffooch@users.noreply.github.com> * Update docs/content/en/share_your_findings/integrations_toolreference.md Co-authored-by: Cody Maffucci <46459665+Maffooch@users.noreply.github.com> --------- Co-authored-by: Paul Osinski Co-authored-by: Cody Maffucci <46459665+Maffooch@users.noreply.github.com> --- .../connectors/connectors_tool_reference.md | 4 +- .../en/share_your_findings/integrations.md | 2 +- .../integrations_toolreference.md | 63 ++++++++++++++++++- 3 files changed, 65 insertions(+), 4 deletions(-) diff --git a/docs/content/en/connecting_your_tools/connectors/connectors_tool_reference.md b/docs/content/en/connecting_your_tools/connectors/connectors_tool_reference.md index 5ef730db7d1..67c6b892e0c 100644 --- a/docs/content/en/connecting_your_tools/connectors/connectors_tool_reference.md +++ b/docs/content/en/connecting_your_tools/connectors/connectors_tool_reference.md @@ -172,6 +172,8 @@ The SonarQube Connector can fetch data from either a SonarCloud account or from 1. Enter the base url of your SonarQube instance in the Location field: for example `https://my.sonarqube.com/` 2. Enter a valid **API key** in the Secret field. This will need to be a **[User](https://docs.sonarsource.com/sonarqube/latest/user-guide/user-account/generating-and-using-tokens/)** [API Token Type](https://docs.sonarsource.com/sonarqube/latest/user-guide/user-account/generating-and-using-tokens/). +The token will need to have access to Projects, Vulnerabilities and Hotspots within Sonar. + API tokens can be found and generated via **My Account \-\> Security \-\> Generate Token** in the SonarQube app. For more information, [see SonarQube documentation](https://docs.sonarsource.com/sonarqube/latest/user-guide/user-account/generating-and-using-tokens/). ## **Snyk** @@ -187,7 +189,7 @@ See the [Snyk API documentation](https://docs.snyk.io/snyk-api) for more info. ## Tenable -The Tenable connector uses the **Tenable.io** REST API to fetch data. +The Tenable connector uses the **Tenable.io** REST API to fetch data. Currently, only vulnerability scans are imported - Web App Scans cannot be imported with the Connector. On\-premise Tenable Connectors are not available at this time. diff --git a/docs/content/en/share_your_findings/integrations.md b/docs/content/en/share_your_findings/integrations.md index ea18f545b02..e2dd663a9f4 100644 --- a/docs/content/en/share_your_findings/integrations.md +++ b/docs/content/en/share_your_findings/integrations.md @@ -9,7 +9,7 @@ Supported Integrations: - [Azure Devops](/en/share_your_findings/integrations_toolreference/#azure-devops-boards) - [GitHub](/en/share_your_findings/integrations_toolreference/#github) - [GitLab Boards](/en/share_your_findings/integrations_toolreference/#gitlab) -- ServiceNow (Coming Soon) +- [ServiceNow](/en/share_your_findings/integrations_toolreference/#servicenow) ## Opening the Integrations page diff --git a/docs/content/en/share_your_findings/integrations_toolreference.md b/docs/content/en/share_your_findings/integrations_toolreference.md index 68799f6bdca..e8c36e4b51c 100644 --- a/docs/content/en/share_your_findings/integrations_toolreference.md +++ b/docs/content/en/share_your_findings/integrations_toolreference.md @@ -1,6 +1,6 @@ --- title: "Integrators Tool Reference" -description: "Beta Feature" +description: "Detailed setup guides for Integrators" weight: 1 --- @@ -101,7 +101,7 @@ The GitLab integration allows you to add issues to a [GitLab Project](https://do ### Issue Tracker Mapping -- **Project Name**: The name of the project in GitLab that you want to send issues to +- **Project Name**: The name of the project in GitLab that you want to send issues to. ### Severity Mapping Details @@ -122,3 +122,62 @@ By default, GitLab has statuses of 'opened' and 'closed'. Additional status lab - **Closed Mapping**: `closed` - **False Positive Mapping**: `closed` - **Risk Accepted Mapping**: `closed` + +## ServiceNow + +The ServiceNow Integration allows you to push DefectDojo Findings as ServiceNow Incidents. + +### Instance Setup + +Your ServiceNow instance will require you to obtain a Refresh Token, associated with the User or Service account that will push Incidents to ServiceNow. + +You'll need to start by creating an OAuth registration on your ServiceNow instance for DefectDojo: + +1. In the left-hand navigation bar, search for β€œApplication Registry” and select it. +2. Click β€œNew”. +3. Choose β€œCreate an OAuth API endpoint for external clients”. +4. Fill in the required fields: + * Name: Provide a meaningful name for your application (e.g., Vulnerability Integration Client). + * (Optional) Adjust the Token Lifespan: + * Access Token Lifespan: Default is 1800 seconds (30 minutes). + * Refresh Token Lifespan: The default is 8640000 seconds (approximately 100 days). +5. Click Submit to create the application record. +6. After submission, select the application from the list and take note of the **Client ID and Client Secret** fields. + +You will then need to use this registration to obtain a Refresh Token, which can only be obtained through the ServiceNow API. Open a terminal window and paste the following (substituting the variables wrapped in `{{}}` with your user's actual information) + +``` +curl --request POST \ + --url {{INSTANCE_HOST}}/oauth_token.do \ + --header 'content-type: application/x-www-form-urlencoded' \ + --data grant_type=password \ + --data 'client_id={{CLIENT_ID}}' \ + --data 'client_secret={{CLIENT_SECRET}}' \ + --data 'username={{USERNAME}}' \ + --data 'password={{PASSWORD}}' + ``` + +If your ServiceNow credentials are correct, and allow for admin level-access to ServiceNow, you should receive a response with a RefreshToken. You'll need that token to complete integration with DefectDojo. + +- **Instance Label** should be the label that you want to use to identify this integration. +- **Location** should be set to the URL for your ServiceNow server, for example `https://your-organization.service-now.com/`. +- **Refresh Token** is where the Refresh Token should be entered. +- **Client ID** should be the Client ID set in the OAuth App Registration. +- **Client ID** should be the Client Secret set in the OAuth App Registration. + +### Severity Mapping Details + +This maps to the ServiceNow Impact field. +- **Info Mapping**: `1` +- **Low Mapping**: `1` +- **Medium Mapping**: `2` +- **High Mapping**: `3` +- **Critical Mapping**: `3` + +### Status Mapping Details + +- **Status Field Name**: `State` +- **Active Mapping**: `New` +- **Closed Mapping**: `Closed` +- **False Positive Mapping**: `Resolved` +- **Risk Accepted Mapping**: `Resolved` From 09f7ffb66283d448aa3fe2a837680990768745aa Mon Sep 17 00:00:00 2001 From: Cody Maffucci <46459665+Maffooch@users.noreply.github.com> Date: Thu, 30 Oct 2025 11:06:49 -0600 Subject: [PATCH 109/126] fix(FindingViewSet): remove prefetched tags to prevent issues with celery delegation (#13568) --- dojo/api_v2/views.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/dojo/api_v2/views.py b/dojo/api_v2/views.py index 126ac2dee56..b39c12aef22 100644 --- a/dojo/api_v2/views.py +++ b/dojo/api_v2/views.py @@ -933,6 +933,8 @@ def close(self, request, pk=None): context={"request": request}, ) if finding_close.is_valid(): + # Remove the prefetched tags to avoid issues with delegating to celery + finding.tags._remove_prefetched_objects() # Use shared helper to perform close operations finding_helper.close_finding( finding=finding, From 26fe7a9db33b5f03521fe97d992c6c72b87d7e9e Mon Sep 17 00:00:00 2001 From: Jino Tesauro <53376807+Jino-T@users.noreply.github.com> Date: Thu, 30 Oct 2025 13:28:00 -0500 Subject: [PATCH 110/126] Added handling for abnormal wazuh severity values (#13522) * Added handling for abnormal wazuh severity values * Added unit tests for wazuh abnormal severities * Fixing ruff issue --- dojo/tools/wazuh/v4_7.py | 13 +++ dojo/tools/wazuh/v4_8.py | 13 +++ .../scans/wazuh/wazuh_abnormal_severity.json | 80 +++++++++++++++++++ unittests/tools/test_wazuh_parser.py | 7 ++ 4 files changed, 113 insertions(+) create mode 100644 unittests/scans/wazuh/wazuh_abnormal_severity.json diff --git a/dojo/tools/wazuh/v4_7.py b/dojo/tools/wazuh/v4_7.py index 1357571d0d5..661dfd3c5a7 100644 --- a/dojo/tools/wazuh/v4_7.py +++ b/dojo/tools/wazuh/v4_7.py @@ -25,6 +25,19 @@ def parse_findings(self, test, data): agent_ip = item.get("agent_ip") detection_time = item.get("detection_time").split("T")[0] + # Map Wazuh severity to its equivalent in DefectDojo + SEVERITY_MAP = { + "Critical": "Critical", + "High": "High", + "Medium": "Medium", + "Low": "Low", + "Info": "Info", + "Informational": "Info", + "Untriaged": "Info", + } + # Get DefectDojo severity and default to "Info" if severity is not in the mapping + severity = SEVERITY_MAP.get(severity, "Info") + references = "\n".join(links) if links else None title = ( diff --git a/dojo/tools/wazuh/v4_8.py b/dojo/tools/wazuh/v4_8.py index 636ee0210d5..2031c759986 100644 --- a/dojo/tools/wazuh/v4_8.py +++ b/dojo/tools/wazuh/v4_8.py @@ -25,6 +25,19 @@ def parse_findings(self, test, data): detection_time = vuln.get("detected_at").split("T")[0] references = vuln.get("reference") + # Map Wazuh severity to its equivalent in DefectDojo + SEVERITY_MAP = { + "Critical": "Critical", + "High": "High", + "Medium": "Medium", + "Low": "Low", + "Info": "Info", + "Informational": "Info", + "Untriaged": "Info", + } + # Get DefectDojo severity and default to "Info" if severity is not in the mapping + severity = SEVERITY_MAP.get(severity, "Info") + title = ( cve + " affects (version: " + item.get("package").get("version") + ")" ) diff --git a/unittests/scans/wazuh/wazuh_abnormal_severity.json b/unittests/scans/wazuh/wazuh_abnormal_severity.json new file mode 100644 index 00000000000..7a35f00c559 --- /dev/null +++ b/unittests/scans/wazuh/wazuh_abnormal_severity.json @@ -0,0 +1,80 @@ +{ + "took": 8, + "timed_out": false, + "_shards": { + "total": 1, + "successful": 1, + "skipped": 0, + "failed": 0 + }, + "hits": { + "total": { + "value": 125, + "relation": "eq" + }, + "max_score": 5.596354, + "hits": [ + { + "_index": "wazuh-states-vulnerabilities-wazuh-server", + "_id": "001_c2f8c1a3b6e902b4c6d8e0g7a4b6c5d0e2b4a6n5_CVE-2025-27558", + "_score": 5.596323, + "_source": { + "agent": { + "id": "001", + "name": "myhost0", + "type": "Wazuh", + "version": "v4.11.1" + }, + "host": { + "os": { + "full": "Ubuntu 24.04.2 LTS", + "kernel": "6.8.0-62-generic", + "name": "Ubuntu", + "platform": "ubuntu", + "type": "ubuntu", + "version": "24.04.2" + } + }, + "package": { + "architecture": "amd64", + "description": "Signed kernel image generic", + "name": "linux-image-6.8.0-60-generic", + "size": 15025152, + "type": "deb", + "version": "6.8.0-60.63" + }, + "vulnerability": { + "category": "Packages", + "classification": "-", + "description": "IEEE P603.12-REVme D1.2 through D7.1 allows FragAttacks against meshnetworks. In mesh networks using Wi-Fi Protected Access (WPA, WPA2, orWPA3) or Wired Equivalent Privacy (WEP), an adversary can exploit thisvulnerability to inject arbitrary frames towards devices that supportreceiving non-SSP A-MSDU frames. NOTE: this issue exists because of anincorrect fix for CVE-2020-24588. P802.11-REVme, as of early 2025, is aplanned release of the 802.11 standard.", + "detected_at": "2025-05-25T17:07:15.204Z", + "enumeration": "CVE", + "id": "CVE-2025-27558", + "published_at": "2025-04-22T19:16:08Z", + "reference": "https://ubuntu.com/security/CVE-2025-27558, https://www.cve.org/CVERecord?id=CVE-2025-27558", + "scanner": { + "condition": "Package default status", + "reference": "https://cti.wazuh.com/vulnerabilities/cves/CVE-2025-27558", + "source": "Canonical Security Tracker", + "vendor": "Wazuh" + }, + "score": { + "base": 9.1, + "version": "3.1" + }, + "severity": "-", + "under_evaluation": false + }, + "wazuh": { + "cluster": { + "name": "wazuh-server" + }, + "schema": { + "version": "1.0.0" + } + } + } + } + ] + } + } \ No newline at end of file diff --git a/unittests/tools/test_wazuh_parser.py b/unittests/tools/test_wazuh_parser.py index 5f73fef4f47..60d741c0b9b 100644 --- a/unittests/tools/test_wazuh_parser.py +++ b/unittests/tools/test_wazuh_parser.py @@ -60,3 +60,10 @@ def test_parse_v4_8_many_findings(self): self.assertEqual("CVE-2025-27558 affects (version: 6.8.0-60.63)", findings[0].title) self.assertEqual("Critical", findings[0].severity) self.assertEqual(9.1, findings[0].cvssv3_score) + + def test_parse_wazuh_abnormal_severity(self): + with (get_unit_tests_scans_path("wazuh") / "wazuh_abnormal_severity.json").open(encoding="utf-8") as testfile: + parser = WazuhParser() + findings = parser.get_findings(testfile, Test()) + for finding in findings: + self.assertEqual("Info", finding.severity) From 1df2832c00367e3c0c04ef247bb0f0198af2937b Mon Sep 17 00:00:00 2001 From: Jino Tesauro <53376807+Jino-T@users.noreply.github.com> Date: Thu, 30 Oct 2025 13:28:15 -0500 Subject: [PATCH 111/126] Added more details to the run-unittest.sh help text (#13557) --- run-unittest.sh | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/run-unittest.sh b/run-unittest.sh index d771285c9ee..6073d4ff582 100755 --- a/run-unittest.sh +++ b/run-unittest.sh @@ -17,6 +17,14 @@ usage() { echo "You must specify a test case (arg)!" echo "Any additional arguments will be passed to the test command." echo + echo "Make sure you run this script in dev mode." + echo "You can enter dev mode using the following command:" + echo "./docker/setEnv.sh dev" + echo + echo "Lastly, make sure the application is running by using the following docker commands:" + echo "docker compose build" + echo "docker compose up" + echo echo "Example commands:" echo "./run-unittest.sh --test-case unittests.tools.test_stackhawk_parser.TestStackHawkParser" echo "./run-unittest.sh --test-case unittests.tools.test_stackhawk_parser.TestStackHawkParser -v3 --failfast" From 00d3fae3f93dfba03738c6a8df2b812ac835d25e Mon Sep 17 00:00:00 2001 From: Cody Maffucci <46459665+Maffooch@users.noreply.github.com> Date: Thu, 30 Oct 2025 12:28:34 -0600 Subject: [PATCH 112/126] fix(dependencies): update package versions to remove caret (^) for consistency (#13543) --- docs/package-lock.json | 25 ++++++++++--------------- docs/package.json | 20 ++++++++++---------- 2 files changed, 20 insertions(+), 25 deletions(-) diff --git a/docs/package-lock.json b/docs/package-lock.json index f2025f915ba..26c62b5a377 100644 --- a/docs/package-lock.json +++ b/docs/package-lock.json @@ -9,18 +9,18 @@ "version": "1.8.0", "license": "MIT", "dependencies": { - "@docsearch/css": "^4.2.0", - "@docsearch/js": "^4.2.0", - "@tabler/icons": "^3.34.1", - "@thulite/doks-core": "^1.8.3", - "@thulite/images": "^3.3.1", - "@thulite/inline-svg": "^1.2.0", - "@thulite/seo": "^2.4.1", - "thulite": "^2.6.3" + "@docsearch/css": "4.2.0", + "@docsearch/js": "4.2.0", + "@tabler/icons": "3.35.0", + "@thulite/doks-core": "1.8.3", + "@thulite/images": "3.3.3", + "@thulite/inline-svg": "1.2.1", + "@thulite/seo": "2.4.2", + "thulite": "2.6.3" }, "devDependencies": { - "prettier": "^3.6.2", - "vite": "^7.0.6" + "prettier": "3.6.2", + "vite": "7.1.11" }, "engines": { "node": ">=20.11.0" @@ -2120,7 +2120,6 @@ "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", "license": "MIT", - "peer": true, "funding": { "type": "opencollective", "url": "https://opencollective.com/popperjs" @@ -2727,7 +2726,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.19", "caniuse-lite": "^1.0.30001751", @@ -3799,7 +3797,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -4456,7 +4453,6 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -4660,7 +4656,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, diff --git a/docs/package.json b/docs/package.json index 6bbc6290c89..69785ab15ee 100644 --- a/docs/package.json +++ b/docs/package.json @@ -16,18 +16,18 @@ "preview": "vite preview --outDir public" }, "dependencies": { - "@docsearch/css": "^4.2.0", - "@docsearch/js": "^4.2.0", - "@tabler/icons": "^3.34.1", - "@thulite/doks-core": "^1.8.3", - "@thulite/images": "^3.3.1", - "@thulite/inline-svg": "^1.2.0", - "@thulite/seo": "^2.4.1", - "thulite": "^2.6.3" + "@docsearch/css": "4.2.0", + "@docsearch/js": "4.2.0", + "@tabler/icons": "3.35.0", + "@thulite/doks-core": "1.8.3", + "@thulite/images": "3.3.3", + "@thulite/inline-svg": "1.2.1", + "@thulite/seo": "2.4.2", + "thulite": "2.6.3" }, "devDependencies": { - "prettier": "^3.6.2", - "vite": "^7.0.6" + "prettier": "3.6.2", + "vite": "7.1.11" }, "engines": { "node": ">=20.11.0" From 7436cf7c669ec8d99392bf446b4bc524d884c2ea Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 31 Oct 2025 12:05:49 -0600 Subject: [PATCH 113/126] chore(deps): update dependency renovatebot/renovate from 41.165.5 to v41.165.7 (.github/workflows/renovate.yaml) (#13574) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/renovate.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/renovate.yaml b/.github/workflows/renovate.yaml index a3c7a1c6f36..8453ba375e2 100644 --- a/.github/workflows/renovate.yaml +++ b/.github/workflows/renovate.yaml @@ -21,4 +21,4 @@ jobs: uses: suzuki-shunsuke/github-action-renovate-config-validator@c22827f47f4f4a5364bdba19e1fe36907ef1318e # v1.1.1 with: strict: "true" - validator_version: 41.165.5 # renovate: datasource=github-releases depName=renovatebot/renovate + validator_version: 41.165.7 # renovate: datasource=github-releases depName=renovatebot/renovate From 39e3b9c7359699ac63392d90780321b16f426aa8 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 31 Oct 2025 12:06:39 -0600 Subject: [PATCH 114/126] chore(deps): update dependency django-debug-toolbar from 6.0.0 to v6.1.0 (requirements-dev.txt) (#13575) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 25fe9b22226..4e8b5cd1fd5 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -2,7 +2,7 @@ # These are only needed during development and testing # Debug toolbar for development -django-debug-toolbar==6.0.0 +django-debug-toolbar==6.1.0 django-debug-toolbar-request-history==0.1.4 # Testing dependencies From 554b53158d750e5659bb8551b33fc4857063285f Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 31 Oct 2025 14:08:58 -0500 Subject: [PATCH 115/126] chore(deps): update dependency renovatebot/renovate from 41.165.7 to v41.168.0 (.github/workflows/renovate.yaml) (#13576) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/renovate.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/renovate.yaml b/.github/workflows/renovate.yaml index 8453ba375e2..4639ecea596 100644 --- a/.github/workflows/renovate.yaml +++ b/.github/workflows/renovate.yaml @@ -21,4 +21,4 @@ jobs: uses: suzuki-shunsuke/github-action-renovate-config-validator@c22827f47f4f4a5364bdba19e1fe36907ef1318e # v1.1.1 with: strict: "true" - validator_version: 41.165.7 # renovate: datasource=github-releases depName=renovatebot/renovate + validator_version: 41.168.0 # renovate: datasource=github-releases depName=renovatebot/renovate From 4fda41e185b7159bacf7e310757b1907897b0ee0 Mon Sep 17 00:00:00 2001 From: valentijnscholten Date: Fri, 31 Oct 2025 20:46:37 +0100 Subject: [PATCH 116/126] docker compose: switch to Valkey as message broker (#13331) * docker compose: switch to valkey as message broker * docker compose: switch to valkey as message broker * docker compose: switch to valkey as message broker * docker compose: switch to valkey as message broker * docker compose: switch to valkey as message broker * Update 2.52.md * Update docs/content/en/open_source/upgrading/2.52.md Co-authored-by: kiblik <5609770+kiblik@users.noreply.github.com> * Update 2.52.md * Update docs/content/en/open_source/upgrading/2.52.md * Revise 2.52 upgrade notes for Valkey integration Updated documentation for version 2.52 to reflect the transition from Redis to Valkey as the message broker, including UI fixes and deduplication improvements. --------- Co-authored-by: kiblik <5609770+kiblik@users.noreply.github.com> --- .github/workflows/integration-tests.yml | 2 +- README.md | 10 ++--- docker-compose.override.unit_tests.yml | 2 +- docker-compose.override.unit_tests_cicd.yml | 2 +- docker-compose.yml | 20 +++++----- .../open_source/installation/architecture.md | 4 +- docs/content/en/open_source/upgrading/2.52.md | 38 +++++++++++++++++-- run-integration-tests.sh | 6 +-- 8 files changed, 60 insertions(+), 24 deletions(-) diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index bf74a50643b..140c4f2befd 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -76,7 +76,7 @@ jobs: run: ln -s docker-compose.override.integration_tests.yml docker-compose.override.yml - name: Start Dojo - run: docker compose up --no-deps -d postgres nginx celerybeat celeryworker mailhog uwsgi redis + run: docker compose up --no-deps -d postgres nginx celerybeat celeryworker mailhog uwsgi valkey env: DJANGO_VERSION: ${{ matrix.os }} NGINX_VERSION: alpine diff --git a/README.md b/README.md index e239a7f6baf..f9d2511b07c 100644 --- a/README.md +++ b/README.md @@ -56,8 +56,8 @@ cd django-DefectDojo # Building Docker images docker compose build -# Run the application (for other profiles besides postgres-redis see -# https://github.com/DefectDojo/django-DefectDojo/blob/dev/readme-docs/DOCKER.md) +# Run the application +# (see https://github.com/DefectDojo/django-DefectDojo/blob/dev/readme-docs/DOCKER.md for more info) docker compose up -d # Obtain admin credentials. The initializer can take up to 3 minutes to run. @@ -67,7 +67,7 @@ docker compose logs initializer | grep "Admin password:" ## For Docker Compose V1 -You can run Compose V1 by calling `docker-compose` (by adding the hyphen (-) between `docker compose`). +You can run Compose V1 by calling `docker-compose` (by adding the hyphen (-) between `docker compose`). Following commands are using original version so you might need to adjust them: ```sh @@ -132,8 +132,8 @@ Moderators can help you with pull requests or feedback on dev ideas: * Blake Owens ([@blakeaowens](https://github.com/blakeaowens)) ## Hall of Fame -* Jannik JΓΌrgens ([@alles-klar](https://github.com/alles-klar)) - Jannik was a long time contributor and moderator for - DefectDojo and made significant contributions to many areas of the platform. Jannik was instrumental in pioneering +* Jannik JΓΌrgens ([@alles-klar](https://github.com/alles-klar)) - Jannik was a long time contributor and moderator for + DefectDojo and made significant contributions to many areas of the platform. Jannik was instrumental in pioneering and optimizing deployment methods. * Valentijn Scholten ([@valentijnscholten](https://github.com/valentijnscholten) | [Sponsor](https://github.com/sponsors/valentijnscholten) | diff --git a/docker-compose.override.unit_tests.yml b/docker-compose.override.unit_tests.yml index d1b90f57fdd..439abea2d3f 100644 --- a/docker-compose.override.unit_tests.yml +++ b/docker-compose.override.unit_tests.yml @@ -42,7 +42,7 @@ services: POSTGRES_DB: ${DD_TEST_DATABASE_NAME:-test_defectdojo} volumes: - defectdojo_postgres_unit_tests:/var/lib/postgresql/data - redis: !reset + valkey: !reset "webhook.endpoint": image: mccutchen/go-httpbin:2.18.3@sha256:3992f3763e9ce5a4307eae0a869a78b4df3931dc8feba74ab823dd2444af6a6b volumes: diff --git a/docker-compose.override.unit_tests_cicd.yml b/docker-compose.override.unit_tests_cicd.yml index 8d6eec1701c..0acd340ce4c 100644 --- a/docker-compose.override.unit_tests_cicd.yml +++ b/docker-compose.override.unit_tests_cicd.yml @@ -41,7 +41,7 @@ services: POSTGRES_DB: ${DD_TEST_DATABASE_NAME:-test_defectdojo} volumes: - defectdojo_postgres_unit_tests:/var/lib/postgresql/data - redis: !reset + valkey: !reset "webhook.endpoint": image: mccutchen/go-httpbin:2.18.3@sha256:3992f3763e9ce5a4307eae0a869a78b4df3931dc8feba74ab823dd2444af6a6b volumes: diff --git a/docker-compose.yml b/docker-compose.yml index be6bf4468cb..24832c74e3e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -41,7 +41,7 @@ services: condition: service_completed_successfully postgres: condition: service_started - redis: + valkey: condition: service_started entrypoint: ['/wait-for-it.sh', '${DD_DATABASE_HOST:-postgres}:${DD_DATABASE_PORT:-5432}', '-t', '30', '--', '/entrypoint-uwsgi.sh'] environment: @@ -49,7 +49,7 @@ services: DD_DJANGO_METRICS_ENABLED: "${DD_DJANGO_METRICS_ENABLED:-False}" DD_ALLOWED_HOSTS: "${DD_ALLOWED_HOSTS:-*}" DD_DATABASE_URL: ${DD_DATABASE_URL:-postgresql://defectdojo:defectdojo@postgres:5432/defectdojo} - DD_CELERY_BROKER_URL: ${DD_CELERY_BROKER_URL:-redis://redis:6379/0} + DD_CELERY_BROKER_URL: ${DD_CELERY_BROKER_URL:-redis://valkey:6379/0} DD_SECRET_KEY: "${DD_SECRET_KEY:-hhZCp@D28z!n@NED*yB!ROMt+WzsY*iq}" DD_CREDENTIAL_AES_256_KEY: "${DD_CREDENTIAL_AES_256_KEY:-&91a*agLqesc*0DJ+2*bAbsUZfR*4nLw}" DD_DATABASE_READINESS_TIMEOUT: "${DD_DATABASE_READINESS_TIMEOUT:-30}" @@ -65,12 +65,12 @@ services: condition: service_completed_successfully postgres: condition: service_started - redis: + valkey: condition: service_started entrypoint: ['/wait-for-it.sh', '${DD_DATABASE_HOST:-postgres}:${DD_DATABASE_PORT:-5432}', '-t', '30', '--', '/entrypoint-celery-beat.sh'] environment: DD_DATABASE_URL: ${DD_DATABASE_URL:-postgresql://defectdojo:defectdojo@postgres:5432/defectdojo} - DD_CELERY_BROKER_URL: ${DD_CELERY_BROKER_URL:-redis://redis:6379/0} + DD_CELERY_BROKER_URL: ${DD_CELERY_BROKER_URL:-redis://valkey:6379/0} DD_SECRET_KEY: "${DD_SECRET_KEY:-hhZCp@D28z!n@NED*yB!ROMt+WzsY*iq}" DD_CREDENTIAL_AES_256_KEY: "${DD_CREDENTIAL_AES_256_KEY:-&91a*agLqesc*0DJ+2*bAbsUZfR*4nLw}" DD_DATABASE_READINESS_TIMEOUT: "${DD_DATABASE_READINESS_TIMEOUT:-30}" @@ -85,12 +85,12 @@ services: condition: service_completed_successfully postgres: condition: service_started - redis: + valkey: condition: service_started entrypoint: ['/wait-for-it.sh', '${DD_DATABASE_HOST:-postgres}:${DD_DATABASE_PORT:-5432}', '-t', '30', '--', '/entrypoint-celery-worker.sh'] environment: DD_DATABASE_URL: ${DD_DATABASE_URL:-postgresql://defectdojo:defectdojo@postgres:5432/defectdojo} - DD_CELERY_BROKER_URL: ${DD_CELERY_BROKER_URL:-redis://redis:6379/0} + DD_CELERY_BROKER_URL: ${DD_CELERY_BROKER_URL:-redis://valkey:6379/0} DD_SECRET_KEY: "${DD_SECRET_KEY:-hhZCp@D28z!n@NED*yB!ROMt+WzsY*iq}" DD_CREDENTIAL_AES_256_KEY: "${DD_CREDENTIAL_AES_256_KEY:-&91a*agLqesc*0DJ+2*bAbsUZfR*4nLw}" DD_DATABASE_READINESS_TIMEOUT: "${DD_DATABASE_READINESS_TIMEOUT:-30}" @@ -127,12 +127,14 @@ services: POSTGRES_PASSWORD: ${DD_DATABASE_PASSWORD:-defectdojo} volumes: - defectdojo_postgres:/var/lib/postgresql/data - redis: - # Pinning to this version due to licensing constraints - image: redis:7.2.11-alpine@sha256:1a34bdba051ecd8a58ec8a3cc460acef697a1605e918149cc53d920673c1a0a7 + valkey: + image: valkey/valkey:7.2.11-alpine@sha256:7b2019b47ad58be661fa6eba5ea66106eadde03459387113aaed29a464a5876b volumes: + # we keep using the redis volume as renaming is not possible and copying data over + # would require steps during downtime or complex commands in the intializer - defectdojo_redis:/data volumes: defectdojo_postgres: {} defectdojo_media: {} defectdojo_redis: {} + diff --git a/docs/content/en/open_source/installation/architecture.md b/docs/content/en/open_source/installation/architecture.md index cd3d70710d6..d3085609844 100644 --- a/docs/content/en/open_source/installation/architecture.md +++ b/docs/content/en/open_source/installation/architecture.md @@ -21,7 +21,9 @@ dynamic content. ## Message Broker The application server sends tasks to a [Message Broker](https://docs.celeryq.dev/en/stable/getting-started/backends-and-brokers/index.html) -for asynchronous execution. Currently, only [Redis](https://github.com/redis/redis) is supported as a broker. +for asynchronous execution. Currently, only [Valkey](https://valkey.io/) is supported as a broker in the docker compose setup. +The Helm chart still uses [Redis](https://github.com/redis/redis) is supported as a broker, but will be migrated to Valkey shortly. + ## Celery Worker diff --git a/docs/content/en/open_source/upgrading/2.52.md b/docs/content/en/open_source/upgrading/2.52.md index 04a206c74ff..20eef3fb214 100644 --- a/docs/content/en/open_source/upgrading/2.52.md +++ b/docs/content/en/open_source/upgrading/2.52.md @@ -2,7 +2,7 @@ title: 'Upgrading to DefectDojo Version 2.52.x' toc_hide: true weight: -20251006 -description: MobSF parsers & Helm chart changes. +description: Replaced Redis with Valkey & Helm chart changes & MobSF parser merge --- ## Fix UI overwriting service field from parsers @@ -23,9 +23,34 @@ See [PR 13517](https://github.com/DefectDojo/django-DefectDojo/pull/13517) for m A bug was fixed in the `UNIQUE_ID_OR_HASH_CODE` algorithm where it stopped processing candidate findings with equal `unique_id_from_tool` or `hash_code` value. Strictly speaking this is not a breaking change, but we wanted to make you aware that you can see more (better) more deduplicatation for parsers using this algorithm. -## Merge of MobSF parsers +## Valkey in `docker compose` -Mobsfscan Scan" has been merged into the "MobSF Scan" parser. The "Mobsfscan Scan" scan_type has been retained to keep deduplication working for existing Tests, but users are encouraged to move to the "MobSF Scan" scan_type. +Since the license change at Redis the fork ValKey has become widely popular and is backed by industry giants such as AWS. AWS is advising to use ValKey over Redis and is using lower prices for ValKey compared to Redis. + +Defect Dojo 2.52 now uses ValKey as a message broker. Teh existing redit volume can be used by Valkey, so this is just a drop in replacement. + +If you want to know more or have a setup where you cannot just re-use the existing volume, please visit https://valkey.io/topics/migration/. + +When you shutdown Defect Dojo to perform the upgrade, the celery tasks that are in the queue are stored to disk. After the upgrade, the celery workers will process these tasks as normal. + +If you want to be 110% sure no tasks will be lost you could perform the upgrade in two steps: + +1) Stop nginx, uwsgi, celerybeat to prevent new tasks from being created: + +`docker compose down nginx, uwsgi, celerybeat` + +2) Observe the Redis queue and/or the logs of the celeryworker(s) and wait until all tasks are finished: + +`docker compose exec redis redis-cli llen celery` -- should output 0 +`docker compose logs celeryworker` -- should stop outputting new task logs + +3) Stop the remaining services: + +`docker compose down` + +4) Continue the upgrade as normal per the [upgrade guide](upgrading_guide) +`docker compose pull` +`docker compose up -d` ## Helm Chart Changes @@ -62,3 +87,10 @@ The following Helm chart values have been modified in this release: - **Extra annotations**: Now we can add common annotations to all resources. There are other instructions for upgrading to 2.52.x. Check the [Release Notes](https://github.com/DefectDojo/django-DefectDojo/releases/tag/2.52.0) for the contents of the release. + +## Merge of MobSF parsers + +Mobsfscan Scan" has been merged into the "MobSF Scan" parser. The "Mobsfscan Scan" scan_type has been retained to keep deduplication working for existing Tests, but users are encouraged to move to the "MobSF Scan" scan_type. + +## Release notes +Check the [Release Notes](https://github.com/DefectDojo/django-DefectDojo/releases/tag/2.52.0) for the contents of the release. diff --git a/run-integration-tests.sh b/run-integration-tests.sh index 953fbbab31f..a07fe15e629 100755 --- a/run-integration-tests.sh +++ b/run-integration-tests.sh @@ -41,7 +41,7 @@ while [[ $# -gt 0 ]]; do esac done -echo "Running docker compose unit tests with profile postgres-redis and test case $TEST_CASE ..." +echo "Running docker compose unit tests and test case $TEST_CASE ..." # Compose V2 integrates compose functions into the Docker platform, # continuing to support most of the previous docker-compose features @@ -50,8 +50,8 @@ echo "Running docker compose unit tests with profile postgres-redis and test cas echo "Building images..." ./docker/setEnv.sh integration_tests docker compose build -echo "Setting up DefectDojo with Postgres and Redis..." -DD_INTEGRATION_TEST_FILENAME="$TEST_CASE" docker compose -d postgres nginx celerybeat celeryworker mailhog uwsgi redis +echo "Setting up DefectDojo" +DD_INTEGRATION_TEST_FILENAME="$TEST_CASE" docker compose -d postgres nginx celerybeat celeryworker mailhog uwsgi valkey echo "Initializing DefectDojo..." DD_INTEGRATION_TEST_FILENAME="$TEST_CASE" docker compose --exit-code-from initializer initializer echo "Running the integration tests..." From 1ba1122954b6fa214862e23345373f75434cdaa1 Mon Sep 17 00:00:00 2001 From: dorkdiaries9 <142151605+dorkdiaries9@users.noreply.github.com> Date: Sat, 1 Nov 2025 01:17:56 +0530 Subject: [PATCH 117/126] Fix recipient handling in create_notification method (#13548) --- dojo/notifications/helper.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/dojo/notifications/helper.py b/dojo/notifications/helper.py index f59060331d1..c4458daec01 100644 --- a/dojo/notifications/helper.py +++ b/dojo/notifications/helper.py @@ -627,6 +627,10 @@ def __init__(self, *args: list, **kwargs: dict) -> None: def create_notification(self, event: str | None = None, **kwargs: dict) -> None: # Process the notifications for a given list of recipients if kwargs.get("recipients") is not None: + recipients = kwargs.get("recipients", []) + if not recipients: + logger.debug("No recipients provided for event: %s", event) + return self._process_recipients(event=event, **kwargs) else: logger.debug("creating system notifications for event: %s", event) From a8869de9ebae827a269071726478415f34fd4c05 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 31 Oct 2025 14:50:41 -0500 Subject: [PATCH 118/126] chore(deps): bump ruff from 0.14.2 to 0.14.3 (#13577) Bumps [ruff](https://github.com/astral-sh/ruff) from 0.14.2 to 0.14.3. - [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.14.2...0.14.3) --- updated-dependencies: - dependency-name: ruff dependency-version: 0.14.3 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 cf2c6af13cd..fcefb6c9a0f 100644 --- a/requirements-lint.txt +++ b/requirements-lint.txt @@ -1 +1 @@ -ruff==0.14.2 +ruff==0.14.3 From e1eef7cb309b7c0ea8c3bce41f1ba252e3f41004 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 31 Oct 2025 14:51:22 -0500 Subject: [PATCH 119/126] chore(deps): bump boto3 from 1.40.62 to 1.40.63 (#13579) Bumps [boto3](https://github.com/boto/boto3) from 1.40.62 to 1.40.63. - [Release notes](https://github.com/boto/boto3/releases) - [Commits](https://github.com/boto/boto3/compare/1.40.62...1.40.63) --- updated-dependencies: - dependency-name: boto3 dependency-version: 1.40.63 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 835132088fb..82bc08c4176 100644 --- a/requirements.txt +++ b/requirements.txt @@ -62,7 +62,7 @@ django-ratelimit==4.1.0 argon2-cffi==25.1.0 blackduck==1.1.3 pycurl==7.45.7 # Required for Celery Broker AWS (SQS) support -boto3==1.40.62 # Required for Celery Broker AWS (SQS) support +boto3==1.40.63 # Required for Celery Broker AWS (SQS) support netaddr==1.3.0 vulners==3.1.1 fontawesomefree==6.6.0 From a2609672a8038aeaf2b65a1184c203afc2c8377e Mon Sep 17 00:00:00 2001 From: manuelsommer <47991713+manuel-sommer@users.noreply.github.com> Date: Mon, 3 Nov 2025 17:21:10 +0100 Subject: [PATCH 120/126] :tada: Add mal vulnid (#13588) --- dojo/settings/settings.dist.py | 1 + 1 file changed, 1 insertion(+) diff --git a/dojo/settings/settings.dist.py b/dojo/settings/settings.dist.py index b2be58bc64d..04f812253c1 100644 --- a/dojo/settings/settings.dist.py +++ b/dojo/settings/settings.dist.py @@ -1881,6 +1881,7 @@ def saml2_attrib_map_format(din): "KB": "https://support.hcl-software.com/csm?id=kb_article&sysparm_article=", # e.g. https://support.hcl-software.com/csm?id=kb_article&sysparm_article=KB0108401 "KHV": "https://avd.aquasec.com/misconfig/kubernetes/", # e.g. https://avd.aquasec.com/misconfig/kubernetes/khv045 "LEN-": "https://support.lenovo.com/cl/de/product_security/", # e.g. https://support.lenovo.com/cl/de/product_security/LEN-94953 + "MAL-": "https://cvepremium.circl.lu/vuln/", # e.g. https://cvepremium.circl.lu/vuln/mal-2025-49305 "MGAA-": "https://advisories.mageia.org/&&.html", # e.g. https://advisories.mageia.org/MGAA-2013-0054.html "MGASA-": "https://advisories.mageia.org/&&.html", # e.g. https://advisories.mageia.org/MGASA-2025-0023.html "MSRC_": "https://cvepremium.circl.lu/vuln/", # e.g. https://cvepremium.circl.lu/vuln/msrc_cve-2025-59200 From ca0fc5615f4f7f5ca29466528eb687e9c0e46f1e Mon Sep 17 00:00:00 2001 From: manuelsommer <47991713+manuel-sommer@users.noreply.github.com> Date: Mon, 3 Nov 2025 17:21:46 +0100 Subject: [PATCH 121/126] :bug: fix similiar findings severity color (#13586) --- dojo/templates/dojo/finding_related_row.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dojo/templates/dojo/finding_related_row.html b/dojo/templates/dojo/finding_related_row.html index ba5336570ab..d02e884100b 100644 --- a/dojo/templates/dojo/finding_related_row.html +++ b/dojo/templates/dojo/finding_related_row.html @@ -13,8 +13,8 @@ {% else %} Similar - + + {{ similar_finding.severity_display }}