From 6ebe872de9b500ad4aa1209fc42ddafb92cfb504 Mon Sep 17 00:00:00 2001 From: DefectDojo release bot Date: Tue, 3 Sep 2024 16:54:02 +0000 Subject: [PATCH 01/93] Update versions in application files --- components/package.json | 2 +- docs/content/en/getting_started/upgrading/2.39.md | 7 +++++++ dojo/__init__.py | 2 +- helm/defectdojo/Chart.yaml | 4 ++-- 4 files changed, 11 insertions(+), 4 deletions(-) create mode 100644 docs/content/en/getting_started/upgrading/2.39.md diff --git a/components/package.json b/components/package.json index 6d6b9318f6..49f5862eec 100644 --- a/components/package.json +++ b/components/package.json @@ -1,6 +1,6 @@ { "name": "defectdojo", - "version": "2.38.0", + "version": "2.39.0-dev", "license" : "BSD-3-Clause", "private": true, "dependencies": { diff --git a/docs/content/en/getting_started/upgrading/2.39.md b/docs/content/en/getting_started/upgrading/2.39.md new file mode 100644 index 0000000000..0f179d7b5d --- /dev/null +++ b/docs/content/en/getting_started/upgrading/2.39.md @@ -0,0 +1,7 @@ +--- +title: 'Upgrading to DefectDojo Version 2.39.x' +toc_hide: true +weight: -20240903 +description: No special instructions. +--- +There are no special instructions for upgrading to 2.39.x. Check the [Release Notes](https://github.com/DefectDojo/django-DefectDojo/releases/tag/2.39.0) for the contents of the release. diff --git a/dojo/__init__.py b/dojo/__init__.py index 5eb915ca70..82fc124150 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.38.0" +__version__ = "2.39.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 d167feafcb..19bf9e9d0f 100644 --- a/helm/defectdojo/Chart.yaml +++ b/helm/defectdojo/Chart.yaml @@ -1,8 +1,8 @@ apiVersion: v2 -appVersion: "2.38.0" +appVersion: "2.39.0-dev" description: A Helm chart for Kubernetes to install DefectDojo name: defectdojo -version: 1.6.148 +version: 1.6.149-dev icon: https://www.defectdojo.org/img/favicon.ico maintainers: - name: madchap From 3b55bdf2e947ae2ccdff0acef107b7db07947ef7 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 3 Sep 2024 17:51:32 -0500 Subject: [PATCH 02/93] chore(deps): update dependency postcss from 8.4.41 to v8.4.44 (docs/package.json) (#10834) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- docs/package-lock.json | 14 +++++++------- docs/package.json | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/docs/package-lock.json b/docs/package-lock.json index 56ef63cc01..ebd7eab6d9 100644 --- a/docs/package-lock.json +++ b/docs/package-lock.json @@ -6,7 +6,7 @@ "": { "devDependencies": { "autoprefixer": "10.4.20", - "postcss": "8.4.41", + "postcss": "8.4.44", "postcss-cli": "11.0.0" } }, @@ -612,9 +612,9 @@ } }, "node_modules/postcss": { - "version": "8.4.41", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.41.tgz", - "integrity": "sha512-TesUflQ0WKZqAvg52PWL6kHgLKP6xB6heTOdoYM0Wt2UHyxNa4K25EZZMgKns3BH1RLVbZCREPpLY0rhnNoHVQ==", + "version": "8.4.44", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.44.tgz", + "integrity": "sha512-Aweb9unOEpQ3ezu4Q00DPvvM2ZTUitJdNKeP/+uQgr1IBIqu574IaZoURId7BKtWMREwzKa9OgzPzezWGPWFQw==", "dev": true, "funding": [ { @@ -1390,9 +1390,9 @@ "dev": true }, "postcss": { - "version": "8.4.41", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.41.tgz", - "integrity": "sha512-TesUflQ0WKZqAvg52PWL6kHgLKP6xB6heTOdoYM0Wt2UHyxNa4K25EZZMgKns3BH1RLVbZCREPpLY0rhnNoHVQ==", + "version": "8.4.44", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.44.tgz", + "integrity": "sha512-Aweb9unOEpQ3ezu4Q00DPvvM2ZTUitJdNKeP/+uQgr1IBIqu574IaZoURId7BKtWMREwzKa9OgzPzezWGPWFQw==", "dev": true, "requires": { "nanoid": "^3.3.7", diff --git a/docs/package.json b/docs/package.json index 9eb98f0f32..ae6385e369 100644 --- a/docs/package.json +++ b/docs/package.json @@ -1,6 +1,6 @@ { "devDependencies": { - "postcss": "8.4.41", + "postcss": "8.4.44", "autoprefixer": "10.4.20", "postcss-cli": "11.0.0" } From 7b5079e8da92f2d5384ba601a32f59584f8c0da7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 3 Sep 2024 17:51:53 -0500 Subject: [PATCH 03/93] Bump boto3 from 1.35.9 to 1.35.10 (#10841) Bumps [boto3](https://github.com/boto/boto3) from 1.35.9 to 1.35.10. - [Release notes](https://github.com/boto/boto3/releases) - [Commits](https://github.com/boto/boto3/compare/1.35.9...1.35.10) --- updated-dependencies: - dependency-name: boto3 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 d37317fb05..ea109249e3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -69,7 +69,7 @@ django-ratelimit==4.1.0 argon2-cffi==23.1.0 blackduck==1.1.3 pycurl==7.45.3 # Required for Celery Broker AWS (SQS) support -boto3==1.35.9 # Required for Celery Broker AWS (SQS) support +boto3==1.35.10 # Required for Celery Broker AWS (SQS) support netaddr==1.3.0 vulners==2.2.0 fontawesomefree==6.6.0 From 285d315727b821273073c435e4abb29e7ba045d0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 4 Sep 2024 10:42:03 -0500 Subject: [PATCH 04/93] Bump boto3 from 1.35.10 to 1.35.11 (#10863) Bumps [boto3](https://github.com/boto/boto3) from 1.35.10 to 1.35.11. - [Release notes](https://github.com/boto/boto3/releases) - [Commits](https://github.com/boto/boto3/compare/1.35.10...1.35.11) --- updated-dependencies: - dependency-name: boto3 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 ea109249e3..2de62492e4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -69,7 +69,7 @@ django-ratelimit==4.1.0 argon2-cffi==23.1.0 blackduck==1.1.3 pycurl==7.45.3 # Required for Celery Broker AWS (SQS) support -boto3==1.35.10 # Required for Celery Broker AWS (SQS) support +boto3==1.35.11 # Required for Celery Broker AWS (SQS) support netaddr==1.3.0 vulners==2.2.0 fontawesomefree==6.6.0 From 78b4e6e80793fcce7f5c18baa608f795b504ba40 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 4 Sep 2024 10:42:41 -0500 Subject: [PATCH 05/93] Bump cryptography from 43.0.0 to 43.0.1 (#10862) Bumps [cryptography](https://github.com/pyca/cryptography) from 43.0.0 to 43.0.1. - [Changelog](https://github.com/pyca/cryptography/blob/main/CHANGELOG.rst) - [Commits](https://github.com/pyca/cryptography/compare/43.0.0...43.0.1) --- updated-dependencies: - dependency-name: cryptography 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 2de62492e4..a8305903f7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -32,7 +32,7 @@ Markdown==3.7 openpyxl==3.1.5 Pillow==10.4.0 # required by django-imagekit psycopg[c]==3.2.1 -cryptography==43.0.0 +cryptography==43.0.1 python-dateutil==2.9.0.post0 pytz==2024.1 redis==5.0.8 From 93c161d5e37f0137c4766dfb9d9a02503cb52a13 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 4 Sep 2024 10:43:11 -0500 Subject: [PATCH 06/93] Bump sqlalchemy from 2.0.32 to 2.0.33 (#10861) Bumps [sqlalchemy](https://github.com/sqlalchemy/sqlalchemy) from 2.0.32 to 2.0.33. - [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-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 a8305903f7..5f4e8aaae8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -37,7 +37,7 @@ python-dateutil==2.9.0.post0 pytz==2024.1 redis==5.0.8 requests==2.32.3 -sqlalchemy==2.0.32 # Required by Celery broker transport +sqlalchemy==2.0.33 # Required by Celery broker transport urllib3==1.26.18 uWSGI==2.0.26 vobject==0.9.7 From b8250ec8db946c3d2600591fadd10815d425fcd8 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 4 Sep 2024 10:43:37 -0500 Subject: [PATCH 07/93] chore(deps): update dependency postcss from 8.4.44 to v8.4.45 (docs/package.json) (#10860) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- docs/package-lock.json | 14 +++++++------- docs/package.json | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/docs/package-lock.json b/docs/package-lock.json index ebd7eab6d9..d3d81bb0ec 100644 --- a/docs/package-lock.json +++ b/docs/package-lock.json @@ -6,7 +6,7 @@ "": { "devDependencies": { "autoprefixer": "10.4.20", - "postcss": "8.4.44", + "postcss": "8.4.45", "postcss-cli": "11.0.0" } }, @@ -612,9 +612,9 @@ } }, "node_modules/postcss": { - "version": "8.4.44", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.44.tgz", - "integrity": "sha512-Aweb9unOEpQ3ezu4Q00DPvvM2ZTUitJdNKeP/+uQgr1IBIqu574IaZoURId7BKtWMREwzKa9OgzPzezWGPWFQw==", + "version": "8.4.45", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.45.tgz", + "integrity": "sha512-7KTLTdzdZZYscUc65XmjFiB73vBhBfbPztCYdUNvlaso9PrzjzcmjqBPR0lNGkcVlcO4BjiO5rK/qNz+XAen1Q==", "dev": true, "funding": [ { @@ -1390,9 +1390,9 @@ "dev": true }, "postcss": { - "version": "8.4.44", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.44.tgz", - "integrity": "sha512-Aweb9unOEpQ3ezu4Q00DPvvM2ZTUitJdNKeP/+uQgr1IBIqu574IaZoURId7BKtWMREwzKa9OgzPzezWGPWFQw==", + "version": "8.4.45", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.45.tgz", + "integrity": "sha512-7KTLTdzdZZYscUc65XmjFiB73vBhBfbPztCYdUNvlaso9PrzjzcmjqBPR0lNGkcVlcO4BjiO5rK/qNz+XAen1Q==", "dev": true, "requires": { "nanoid": "^3.3.7", diff --git a/docs/package.json b/docs/package.json index ae6385e369..a892ece566 100644 --- a/docs/package.json +++ b/docs/package.json @@ -1,6 +1,6 @@ { "devDependencies": { - "postcss": "8.4.44", + "postcss": "8.4.45", "autoprefixer": "10.4.20", "postcss-cli": "11.0.0" } From 4ab7be20774612cd0da7d780a80a23b4e006ca66 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 4 Sep 2024 12:16:42 -0500 Subject: [PATCH 08/93] Bump django-tagulous from 1.3.3 to 2.1.0 (#10821) Bumps [django-tagulous](https://github.com/radiac/django-tagulous) from 1.3.3 to 2.1.0. - [Changelog](https://github.com/radiac/django-tagulous/blob/main/docs/changelog.rst) - [Commits](https://github.com/radiac/django-tagulous/compare/v1.3.3...v2.1.0) --- updated-dependencies: - dependency-name: django-tagulous 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 5f4e8aaae8..59c1e2bdad 100644 --- a/requirements.txt +++ b/requirements.txt @@ -56,7 +56,7 @@ django-debug-toolbar==4.4.6 django-debug-toolbar-request-history==0.1.4 vcrpy==6.0.1 vcrpy-unittest==0.1.7 -django-tagulous==1.3.3 +django-tagulous==2.1.0 PyJWT==2.9.0 cvss==3.1 django-fieldsignals==0.7.0 From c45cfc7ea0ee7c93b40771ab75691b46e5cbf81d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 4 Sep 2024 12:28:33 -0500 Subject: [PATCH 09/93] Bump jquery-ui from 1.13.3 to 1.14.0 in /components (#10684) Bumps [jquery-ui](https://github.com/jquery/jquery-ui) from 1.13.3 to 1.14.0. - [Release notes](https://github.com/jquery/jquery-ui/releases) - [Commits](https://github.com/jquery/jquery-ui/compare/1.13.3...1.14.0) --- updated-dependencies: - dependency-name: jquery-ui 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> --- components/package.json | 2 +- components/yarn.lock | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/components/package.json b/components/package.json index 49f5862eec..b1a047f22b 100644 --- a/components/package.json +++ b/components/package.json @@ -26,7 +26,7 @@ "google-code-prettify": "^1.0.0", "jquery": "^3.7.1", "jquery-highlight": "3.5.0", - "jquery-ui": "1.13.3", + "jquery-ui": "1.14.0", "jquery.cookie": "1.4.1", "jquery.flot.tooltip": "^0.9.0", "jquery.hotkeys": "jeresig/jquery.hotkeys#master", diff --git a/components/yarn.lock b/components/yarn.lock index b4bfb09a42..8bd8311e89 100644 --- a/components/yarn.lock +++ b/components/yarn.lock @@ -678,12 +678,12 @@ jquery-highlight@3.5.0: dependencies: jquery ">= 1.0.0" -jquery-ui@1.13.3: - version "1.13.3" - resolved "https://registry.yarnpkg.com/jquery-ui/-/jquery-ui-1.13.3.tgz#d9f5292b2857fa1f2fdbbe8f2e66081664eb9bc5" - integrity sha512-D2YJfswSJRh/B8M/zCowDpNFfwsDmtfnMPwjJTyvl+CBqzpYwQ+gFYIbUUlzijy/Qvoy30H1YhoSui4MNYpRwA== +jquery-ui@1.14.0: + version "1.14.0" + resolved "https://registry.yarnpkg.com/jquery-ui/-/jquery-ui-1.14.0.tgz#b75d417826f0bab38125f907356d2e3313a9c6d5" + integrity sha512-mPfYKBoRCf0MzaT2cyW5i3IuZ7PfTITaasO5OFLAQxrHuI+ZxruPa+4/K1OMNT8oElLWGtIxc9aRbyw20BKr8g== dependencies: - jquery ">=1.8.0 <4.0.0" + jquery ">=1.12.0 <5.0.0" jquery.cookie@1.4.1: version "1.4.1" @@ -699,7 +699,7 @@ jquery.hotkeys@jeresig/jquery.hotkeys#master: version "0.2.0" resolved "https://codeload.github.com/jeresig/jquery.hotkeys/tar.gz/f24f1da275aab7881ab501055c256add6f690de4" -"jquery@>= 1.0.0", jquery@>=1.7, jquery@>=1.7.0, "jquery@>=1.8.0 <4.0.0", jquery@^3.7.1: +"jquery@>= 1.0.0", "jquery@>=1.12.0 <5.0.0", jquery@>=1.7, jquery@>=1.7.0, jquery@^3.7.1: version "3.7.1" resolved "https://registry.yarnpkg.com/jquery/-/jquery-3.7.1.tgz#083ef98927c9a6a74d05a6af02806566d16274de" integrity sha512-m4avr8yL8kmFN8psrbFFFmB/If14iN5o9nw/NgnnM+kybDJpRsAynV2BsfpTYrTRysYUdADVD7CkUUizgkpLfg== From 069541d16f9d3def5ff1aa770531179e8e1d0938 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 5 Sep 2024 13:46:03 -0500 Subject: [PATCH 10/93] Bump boto3 from 1.35.11 to 1.35.12 (#10867) Bumps [boto3](https://github.com/boto/boto3) from 1.35.11 to 1.35.12. - [Release notes](https://github.com/boto/boto3/releases) - [Commits](https://github.com/boto/boto3/compare/1.35.11...1.35.12) --- updated-dependencies: - dependency-name: boto3 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 59c1e2bdad..e8966bcfcc 100644 --- a/requirements.txt +++ b/requirements.txt @@ -69,7 +69,7 @@ django-ratelimit==4.1.0 argon2-cffi==23.1.0 blackduck==1.1.3 pycurl==7.45.3 # Required for Celery Broker AWS (SQS) support -boto3==1.35.11 # Required for Celery Broker AWS (SQS) support +boto3==1.35.12 # Required for Celery Broker AWS (SQS) support netaddr==1.3.0 vulners==2.2.0 fontawesomefree==6.6.0 From 7c52e50332e14a85605184c993ef468207c77204 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 5 Sep 2024 13:52:16 -0500 Subject: [PATCH 11/93] Bump sqlalchemy from 2.0.33 to 2.0.34 (#10868) Bumps [sqlalchemy](https://github.com/sqlalchemy/sqlalchemy) from 2.0.33 to 2.0.34. - [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-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 e8966bcfcc..26f4c54089 100644 --- a/requirements.txt +++ b/requirements.txt @@ -37,7 +37,7 @@ python-dateutil==2.9.0.post0 pytz==2024.1 redis==5.0.8 requests==2.32.3 -sqlalchemy==2.0.33 # Required by Celery broker transport +sqlalchemy==2.0.34 # Required by Celery broker transport urllib3==1.26.18 uWSGI==2.0.26 vobject==0.9.7 From 6e937efec705d2e65165c8a70ab27d31dab80a40 Mon Sep 17 00:00:00 2001 From: kiblik <5609770+kiblik@users.noreply.github.com> Date: Fri, 6 Sep 2024 22:39:25 +0200 Subject: [PATCH 12/93] feat(unittests): Try to avoid assertTrue/False (#10817) --- tests/base_test_class.py | 6 +-- tests/close_old_findings_dedupe_test.py | 4 +- tests/close_old_findings_test.py | 4 +- tests/dedupe_test.py | 4 +- tests/finding_test.py | 8 +-- tests/notes_test.py | 5 +- tests/product_type_member_test.py | 2 +- tests/report_builder_test.py | 2 +- unittests/test_deduplication_logic.py | 12 ++--- unittests/test_jira_config_product.py | 2 +- unittests/test_parsers.py | 9 ++-- ...appcheck_web_application_scanner_parser.py | 53 ++++++++++--------- unittests/tools/test_asff_parser.py | 2 +- unittests/tools/test_auditjs_parser.py | 4 +- unittests/tools/test_chefinspect_parser.py | 2 +- unittests/tools/test_meterian_parser.py | 4 +- unittests/tools/test_noseyparker_parser.py | 6 +-- unittests/tools/test_yarn_audit_parser.py | 6 +-- 18 files changed, 68 insertions(+), 67 deletions(-) diff --git a/tests/base_test_class.py b/tests/base_test_class.py index 9cfa91adf7..bea137e484 100644 --- a/tests/base_test_class.py +++ b/tests/base_test_class.py @@ -350,9 +350,9 @@ def set_block_execution(self, block_execution=True): # save settings driver.find_element(By.CSS_SELECTOR, "input.btn.btn-primary").click() # check if it's enabled after reload - self.assertTrue( - driver.find_element(By.ID, "id_block_execution").is_selected() - == block_execution, + self.assertEqual( + driver.find_element(By.ID, "id_block_execution").is_selected(), + block_execution, ) return driver diff --git a/tests/close_old_findings_dedupe_test.py b/tests/close_old_findings_dedupe_test.py index cba54a5f79..718b2bdad0 100644 --- a/tests/close_old_findings_dedupe_test.py +++ b/tests/close_old_findings_dedupe_test.py @@ -90,9 +90,9 @@ def test_delete_findings(self): text = driver.find_element(By.ID, "no_findings").text self.assertIsNotNone(text) - self.assertTrue("No findings found." in text) + self.assertIn("No findings found.", text) # check that user was redirect back to url where it came from based on return_url - self.assertTrue(driver.current_url.endswith("page=1")) + self.assertTrue(driver.current_url.endswith("page=1"), driver.current_url) # -------------------------------------------------------------------------------------------------------- # Same scanner deduplication - Deduplication on engagement diff --git a/tests/close_old_findings_test.py b/tests/close_old_findings_test.py index ba47ce732d..7cf7339cf9 100644 --- a/tests/close_old_findings_test.py +++ b/tests/close_old_findings_test.py @@ -50,9 +50,9 @@ def test_delete_findings(self): text = driver.find_element(By.ID, "no_findings").text self.assertIsNotNone(text) - self.assertTrue("No findings found." in text) + self.assertIn("No findings found.", text) # check that user was redirect back to url where it came from based on return_url - self.assertTrue(driver.current_url.endswith("page=1")) + self.assertTrue(driver.current_url.endswith("page=1"), driver.current_url) # -------------------------------------------------------------------------------------------------------- # Same scanner import - Close Old Findings on engagement diff --git a/tests/dedupe_test.py b/tests/dedupe_test.py index cf9e038c37..8b573a1eb1 100644 --- a/tests/dedupe_test.py +++ b/tests/dedupe_test.py @@ -88,9 +88,9 @@ def test_delete_findings(self): text = driver.find_element(By.ID, "no_findings").text self.assertIsNotNone(text) - self.assertTrue("No findings found." in text) + self.assertIn("No findings found.", text) # check that user was redirect back to url where it came from based on return_url - self.assertTrue(driver.current_url.endswith("page=1")) + self.assertTrue(driver.current_url.endswith("page=1"), driver.current_url) # -------------------------------------------------------------------------------------------------------- # Same scanner deduplication - Deduplication on engagement diff --git a/tests/finding_test.py b/tests/finding_test.py index dd4f9b7332..835e832fa5 100644 --- a/tests/finding_test.py +++ b/tests/finding_test.py @@ -280,7 +280,7 @@ def test_close_finding(self): # This will throw exception if the test fails due to invalid xpath post_status = driver.find_element(By.XPATH, '//*[@id="remd_endpoints"]/tbody/tr/td[3]').text # Assert ot the query to dtermine status of failure - self.assertTrue(pre_status != post_status) + self.assertNotEqual(pre_status, post_status) def test_open_finding(self): driver = self.driver @@ -303,7 +303,7 @@ def test_open_finding(self): # This will throw exception if the test fails due to invalid xpath post_status = driver.find_element(By.XPATH, '//*[@id="vuln_endpoints"]/tbody/tr/td[3]').text # Assert ot the query to dtermine status of failure - self.assertTrue(pre_status != post_status) + self.assertNotEqual(pre_status, post_status) @on_exception_html_source_logger def test_simple_accept_finding(self): @@ -328,7 +328,7 @@ def test_simple_accept_finding(self): # This will throw exception if the test fails due to invalid xpath # TODO: risk acceptance doesn't mitigate endpoints currently # post_status = driver.find_element(By.XPATH, '//*[@id="remd_endpoints"]/tbody/tr/td[3]').text - # self.assertTrue(pre_status != post_status) + # self.assertNotEqual(pre_status, post_status) def test_unaccept_finding(self): driver = self.driver @@ -352,7 +352,7 @@ def test_unaccept_finding(self): # This will throw exception if the test fails due to invalid xpath # TODO: risk acceptance doesn't mitigate endpoints currently # post_status = driver.find_element(By.XPATH, '//*[@id="remd_endpoints"]/tbody/tr/td[3]').text - # self.assertTrue(pre_status != post_status) + # self.assertNotEqual(pre_status, post_status) def test_make_finding_a_template(self): driver = self.driver diff --git a/tests/notes_test.py b/tests/notes_test.py index 7a376629da..a569da5b05 100644 --- a/tests/notes_test.py +++ b/tests/notes_test.py @@ -34,10 +34,7 @@ def create_public_note(self, driver, level): if not driver.find_element(By.ID, "add_note").is_displayed(): self.uncollapse_all(driver) text = driver.find_element(By.TAG_NAME, "body").text - pass_test = "Test public note" in text - if not pass_test: - logger.info(f"Public note created at the {level} level") - self.assertTrue(pass_test) + self.assertIn("Test public note", text, f"Public note created at the {level} level") def create_private_note(self, driver, level): time.sleep(1) diff --git a/tests/product_type_member_test.py b/tests/product_type_member_test.py index b990825dda..7b8a5d1896 100644 --- a/tests/product_type_member_test.py +++ b/tests/product_type_member_test.py @@ -186,7 +186,7 @@ def test_product_type_delete_product_type_member(self): # Assert the message to determine success status self.assertTrue(self.is_success_message_present(text="Product type member deleted successfully.")) # Query the site to determine if the member has been deleted - self.assertTrue(len(driver.find_elements(By.NAME, "member_user")) == 1) + self.assertEqual(len(driver.find_elements(By.NAME, "member_user")), 1) else: logger.info("--------------------------------") logger.info("test_product_delete_product_member: Not executed because legacy authorization is active") diff --git a/tests/report_builder_test.py b/tests/report_builder_test.py index 1c68c477af..cbb61583bd 100644 --- a/tests/report_builder_test.py +++ b/tests/report_builder_test.py @@ -41,7 +41,7 @@ def generate_HTML_report(self): Select(driver.find_element(By.ID, "id_report_type")).select_by_visible_text("HTML") driver.find_element(By.ID, "id_report_name").send_keys("Test Report") driver.find_element(By.CLASS_NAME, "run_report").click() - self.assertTrue(driver.current_url == self.base_url + "reports/custom") + self.assertEqual(driver.current_url, self.base_url + "reports/custom") def test_product_type_report(self): driver = self.driver diff --git a/unittests/test_deduplication_logic.py b/unittests/test_deduplication_logic.py index 2345af912f..ef1d91a0d5 100644 --- a/unittests/test_deduplication_logic.py +++ b/unittests/test_deduplication_logic.py @@ -1044,7 +1044,7 @@ def test_hash_code_onetime(self): self.assertEqual(finding_new.hash_code, None) finding_new.save() - self.assertTrue(finding_new.hash_code) # True -> not None + self.assertIsNotNone(finding_new.hash_code) hash_code_at_creation = finding_new.hash_code finding_new.title = "new_title" @@ -1111,17 +1111,17 @@ def test_hash_code_without_dedupe(self): finding_new.save(dedupe_option=False) # save skips hash_code generation if dedupe_option==False - self.assertFalse(finding_new.hash_code) + self.assertIsNone(finding_new.hash_code) finding_new.save(dedupe_option=True) - self.assertTrue(finding_new.hash_code) + self.assertIsNotNone(finding_new.hash_code) finding_new, _finding_124 = self.copy_and_reset_finding(id=124) finding_new.save() # by default hash_code should be generated - self.assertTrue(finding_new.hash_code) + self.assertIsNotNone(finding_new.hash_code) # # utility methods @@ -1248,11 +1248,11 @@ def assert_finding(self, finding, not_pk=None, duplicate=False, duplicate_findin self.assertEqual(finding.duplicate, duplicate) if not duplicate: - self.assertFalse(finding.duplicate_finding) # False -> None + self.assertIsNone(finding.duplicate_finding) if duplicate_finding_id: logger.debug("asserting that finding %i is a duplicate of %i", finding.id if finding.id is not None else "None", duplicate_finding_id if duplicate_finding_id is not None else "None") - self.assertTrue(finding.duplicate_finding) # True -> not None + self.assertIsNotNone(finding.duplicate_finding) self.assertEqual(finding.duplicate_finding.id, duplicate_finding_id) if not_hash_code: diff --git a/unittests/test_jira_config_product.py b/unittests/test_jira_config_product.py index 41c9ffdc96..ff72f34993 100644 --- a/unittests/test_jira_config_product.py +++ b/unittests/test_jira_config_product.py @@ -85,7 +85,7 @@ def test_add_jira_instance_unknown_host(self): self.assertEqual(200, response.status_code) content = response.content.decode("utf-8") # debian throws 'Name or service not known' error and alpine 'Name does not resolve' - self.assertTrue(("Name or service not known" in content) or ("Name does not resolve" in content)) + self.assertTrue(("Name or service not known" in content) or ("Name does not resolve" in content), content) # test raw connection error with self.assertRaises(requests.exceptions.RequestException): diff --git a/unittests/test_parsers.py b/unittests/test_parsers.py index e767a11039..63edff395c 100644 --- a/unittests/test_parsers.py +++ b/unittests/test_parsers.py @@ -1,5 +1,4 @@ import os -import re from pathlib import Path from .dojo_test_case import DojoTestCase, get_unit_tests_path @@ -33,17 +32,17 @@ def test_file_existence(self): ) content = Path(doc_file).read_text(encoding="utf-8") - self.assertTrue(re.search("title:", content), + self.assertRegex(content, "title:", f"Documentation file '{doc_file}' does not contain a title", ) - self.assertTrue(re.search("toc_hide: true", content), + self.assertRegex(content, "toc_hide: true", f"Documentation file '{doc_file}' does not contain toc_hide: true", ) if category == "file": - self.assertTrue(re.search("### Sample Scan Data", content), + self.assertRegex(content, "### Sample Scan Data", f"Documentation file '{doc_file}' does not contain ### Sample Scan Data", ) - self.assertTrue(re.search("https://github.com/DefectDojo/django-DefectDojo/tree/master/unittests/scans", content), + self.assertRegex(content, "https://github.com/DefectDojo/django-DefectDojo/tree/master/unittests/scans", f"Documentation file '{doc_file}' does not contain https://github.com/DefectDojo/django-DefectDojo/tree/master/unittests/scans", ) diff --git a/unittests/tools/test_appcheck_web_application_scanner_parser.py b/unittests/tools/test_appcheck_web_application_scanner_parser.py index 8928f89abd..0775a3de5a 100644 --- a/unittests/tools/test_appcheck_web_application_scanner_parser.py +++ b/unittests/tools/test_appcheck_web_application_scanner_parser.py @@ -42,9 +42,10 @@ def test_appcheck_web_application_scanner_parser_with_one_criticle_vuln_has_one_ finding.description.startswith( "The remote host is running a FTP service that allows cleartext logins over\n unencrypted connections.", ), + finding.description, ) for section in ["**Impact**:", "**Detection**:", "**Technical Details**:"]: - self.assertTrue(section in finding.description) + self.assertIn(section, finding.description) self.assertEqual(1, len(finding.unsaved_endpoints)) endpoint = finding.unsaved_endpoints[0] @@ -77,10 +78,11 @@ def test_appcheck_web_application_scanner_parser_with_many_vuln_has_many_finding finding.description.startswith( "The dedicated port scanner found open ports on this host, along with other\nhost-specific information, which can be viewed in Technical Details.", ), + finding.description, ) - self.assertTrue( - "Host: 0.0.0.1 (0.0.0.1)\nHost is up, received user-set (0.015s latency).\nScanned at 2020-01-29 15:44:46 UTC for 15763s\nNot shown: 65527 filtered ports, 4 closed ports\nReason: 65527 no-responses and 4 resets\nSome closed ports may be reported as filtered due to --defeat-rst-ratelimit\nPORT STATE SERVICE REASON VERSION\n21/tcp open ftp syn-ack ttl 116 Microsoft ftpd\n45000/tcp open ssl/asmp? syn-ack ttl 116\n45010/tcp open unknown syn-ack ttl 116\n60001/tcp open ssl/unknown syn-ack ttl 116\n60011/tcp open unknown syn-ack ttl 116\nService Info: OS: Windows; CPE: cpe:/o:microsoft:windows" - in finding.description, + self.assertIn( + "Host: 0.0.0.1 (0.0.0.1)\nHost is up, received user-set (0.015s latency).\nScanned at 2020-01-29 15:44:46 UTC for 15763s\nNot shown: 65527 filtered ports, 4 closed ports\nReason: 65527 no-responses and 4 resets\nSome closed ports may be reported as filtered due to --defeat-rst-ratelimit\nPORT STATE SERVICE REASON VERSION\n21/tcp open ftp syn-ack ttl 116 Microsoft ftpd\n45000/tcp open ssl/asmp? syn-ack ttl 116\n45010/tcp open unknown syn-ack ttl 116\n60001/tcp open ssl/unknown syn-ack ttl 116\n60011/tcp open unknown syn-ack ttl 116\nService Info: OS: Windows; CPE: cpe:/o:microsoft:windows", + finding.description, ) expected_ports = [21, 45000, 45010, 60001, 60011] @@ -106,9 +108,9 @@ def test_appcheck_web_application_scanner_parser_with_many_vuln_has_many_finding self.assertEqual("8.0.32", finding.component_version) self.assertEqual(1, len(finding.unsaved_vulnerability_ids)) self.assertEqual("CVE-2016-6796", finding.unsaved_vulnerability_ids[0]) - self.assertTrue(finding.description.startswith('**Product Background**\n\n**Apache Tomcat** is a free and open-source Java web application server. It provides a "pure Java" HTTP web server environment in which Java code can also run, implementing the Jakarta Servlet, Jakarta Expression Language, and WebSocket technologies. Tomcat is released with **Catalina** (a servlet and JSP Java Server Pages container), **Coyote** (an HTTP connector), **Coyote JK** (JK protocol proxy connector) and **Jasper** (a JSP engine). Tomcat can optionally be bundled with Java Enterprise Edition (Jakarta EE) as **Apache TomEE** to deliver a complete application server with enterprise features such as distributed computing and web services.\n\n**Vulnerability Summary**\n\nA malicious web application running on Apache Tomcat 9.0.0.M1 to 9.0.0.M9, 8.5.0 to 8.5.4, 8.0.0.RC1 to 8.0.36, 7.0.0 to 7.0.70 and 6.0.0 to 6.0.45 was able to bypass a configured SecurityManager via manipulation of the configuration parameters for the JSP Servlet.\n\n**References**\n\n* http://www.securitytracker.com/id/1038757\n\n* http://www.securitytracker.com/id/1037141\n\n* http://www.securityfocus.com/bid/93944\n\n* http://www.debian.org/security/2016/dsa-3720\n\n* https://access.redhat.com/errata/RHSA-2017:2247\n\n* https://access.redhat.com/errata/RHSA-2017:1552\n\n* https://access.redhat.com/errata/RHSA-2017:1550\n\n* https://access.redhat.com/errata/RHSA-2017:1549\n\n* https://access.redhat.com/errata/RHSA-2017:1548\n\n* https://access.redhat.com/errata/RHSA-2017:0456\n\n* https://access.redhat.com/errata/RHSA-2017:0455\n\n* http://rhn.redhat.com/errata/RHSA-2017-1551.html\n\n* http://rhn.redhat.com/errata/RHSA-2017-0457.html\n\n* https://security.netapp.com/advisory/ntap-20180605-0001/\n\n* https://usn.ubuntu.com/4557-1/\n\n* https://www.oracle.com/security-alerts/cpuoct2021.html\n\n')) + self.assertTrue(finding.description.startswith('**Product Background**\n\n**Apache Tomcat** is a free and open-source Java web application server. It provides a "pure Java" HTTP web server environment in which Java code can also run, implementing the Jakarta Servlet, Jakarta Expression Language, and WebSocket technologies. Tomcat is released with **Catalina** (a servlet and JSP Java Server Pages container), **Coyote** (an HTTP connector), **Coyote JK** (JK protocol proxy connector) and **Jasper** (a JSP engine). Tomcat can optionally be bundled with Java Enterprise Edition (Jakarta EE) as **Apache TomEE** to deliver a complete application server with enterprise features such as distributed computing and web services.\n\n**Vulnerability Summary**\n\nA malicious web application running on Apache Tomcat 9.0.0.M1 to 9.0.0.M9, 8.5.0 to 8.5.4, 8.0.0.RC1 to 8.0.36, 7.0.0 to 7.0.70 and 6.0.0 to 6.0.45 was able to bypass a configured SecurityManager via manipulation of the configuration parameters for the JSP Servlet.\n\n**References**\n\n* http://www.securitytracker.com/id/1038757\n\n* http://www.securitytracker.com/id/1037141\n\n* http://www.securityfocus.com/bid/93944\n\n* http://www.debian.org/security/2016/dsa-3720\n\n* https://access.redhat.com/errata/RHSA-2017:2247\n\n* https://access.redhat.com/errata/RHSA-2017:1552\n\n* https://access.redhat.com/errata/RHSA-2017:1550\n\n* https://access.redhat.com/errata/RHSA-2017:1549\n\n* https://access.redhat.com/errata/RHSA-2017:1548\n\n* https://access.redhat.com/errata/RHSA-2017:0456\n\n* https://access.redhat.com/errata/RHSA-2017:0455\n\n* http://rhn.redhat.com/errata/RHSA-2017-1551.html\n\n* http://rhn.redhat.com/errata/RHSA-2017-0457.html\n\n* https://security.netapp.com/advisory/ntap-20180605-0001/\n\n* https://usn.ubuntu.com/4557-1/\n\n* https://www.oracle.com/security-alerts/cpuoct2021.html\n\n'), finding.description) for section in ["**Technical Details**:", "**Classifications**:"]: - self.assertTrue(section in finding.description) + self.assertIn(section, finding.description) self.assertEqual(1, len(finding.unsaved_endpoints)) endpoint = finding.unsaved_endpoints[0] @@ -134,9 +136,10 @@ def test_appcheck_web_application_scanner_parser_with_many_vuln_has_many_finding finding.description.startswith( "This is simply a report of HTTP request methods supported by the web application.", ), + finding.description, ) for section in ["**Permitted HTTP Methods**:"]: - self.assertTrue(section in finding.description) + self.assertIn(section, finding.description) self.assertEqual(1, len(finding.unsaved_endpoints)) endpoint = finding.unsaved_endpoints[0] @@ -171,9 +174,10 @@ def test_appcheck_web_application_scanner_parser_with_many_vuln_has_many_finding finding.description.startswith( "This routine reports all SSL/TLS cipher suites accepted by a service where attack vectors exists only on HTTPS services.\n\nThese rules are applied for the evaluation of the vulnerable cipher suites:\n\n- 64-bit block cipher 3DES vulnerable to the SWEET32 attack (CVE-2016-2183).", ), + finding.description, ) for section in ["**Technical Details**:", "**External Sources**"]: - self.assertTrue(section in finding.description) + self.assertIn(section, finding.description) self.assertEqual(1, len(finding.unsaved_endpoints)) endpoint = finding.unsaved_endpoints[0] @@ -202,9 +206,10 @@ def test_appcheck_web_application_scanner_parser_with_many_vuln_has_many_finding finding.description.startswith( "The server responded with a HTTP status code that may indicate that the remote server is experiencing technical\ndifficulties that are likely to affect the scan and may also be affecting other application users.", ), + finding.description, ) for section in ["**Technical Details**:"]: - self.assertTrue(section in finding.description) + self.assertIn(section, finding.description) self.assertEqual(1, len(finding.unsaved_endpoints)) endpoint = finding.unsaved_endpoints[0] @@ -232,12 +237,12 @@ def test_appcheck_web_application_scanner_parser_http2(self): self.assertEqual("2024-08-06", finding.date) self.assertEqual("HTTP/2 Supported", finding.title) self.assertEqual(1, len(finding.unsaved_endpoints)) - self.assertTrue("**Messages**" not in finding.description) - self.assertTrue("\x00" not in finding.description) + self.assertNotIn("**Messages**", finding.description) + self.assertNotIn("\x00", finding.description) self.assertIsNotNone(finding.unsaved_request) - self.assertTrue(finding.unsaved_request.startswith(":method = GET")) + self.assertTrue(finding.unsaved_request.startswith(":method = GET"), finding.unsaved_request) self.assertIsNotNone(finding.unsaved_response) - self.assertTrue(finding.unsaved_response.startswith(":status: 200")) + self.assertTrue(finding.unsaved_response.startswith(":status: 200"), finding.unsaved_response) endpoint = finding.unsaved_endpoints[0] endpoint.clean() self.assertEqual("www.xzzvwy.com", endpoint.host) @@ -250,13 +255,13 @@ def test_appcheck_web_application_scanner_parser_http2(self): self.assertEqual("4e7c0b570ff6083376b99e1897102a87907effe2199dc8d4", finding.unique_id_from_tool) self.assertEqual("2024-08-06", finding.date) self.assertEqual("HTTP/2 Protocol: Transfer-Encoding Header Accepted", finding.title) - self.assertTrue("**Messages**" not in finding.description) - self.assertTrue("\x00" not in finding.description) - self.assertTrue("**HTTP2 Headers**" in finding.description) + self.assertNotIn("**Messages**", finding.description) + self.assertNotIn("\x00", finding.description) + self.assertIn("**HTTP2 Headers**", finding.description) self.assertIsNotNone(finding.unsaved_request) - self.assertTrue(finding.unsaved_request.startswith(":method = POST")) + self.assertTrue(finding.unsaved_request.startswith(":method = POST"), finding.unsaved_request) self.assertIsNotNone(finding.unsaved_response) - self.assertTrue(finding.unsaved_response.startswith(":status: 200")) + self.assertTrue(finding.unsaved_response.startswith(":status: 200"), finding.unsaved_response) self.assertEqual(1, len(finding.unsaved_endpoints)) endpoint = finding.unsaved_endpoints[0] endpoint.clean() @@ -270,13 +275,13 @@ def test_appcheck_web_application_scanner_parser_http2(self): self.assertEqual("2f1fb384e6a866f9ee0c6f7550e3b607e8b1dd2b1ab0fd02", finding.unique_id_from_tool) self.assertEqual("2024-08-06", finding.date) self.assertEqual("HTTP/2 Protocol: Transfer-Encoding Header Accepted", finding.title) - self.assertTrue("**Messages**" not in finding.description) - self.assertTrue("**HTTP2 Headers**" in finding.description) - self.assertTrue("\x00" not in finding.description) + self.assertNotIn("**Messages**", finding.description) + self.assertIn("**HTTP2 Headers**", finding.description) + self.assertNotIn("\x00", finding.description) self.assertIsNotNone(finding.unsaved_request) - self.assertTrue(finding.unsaved_request.startswith(":method = POST")) + self.assertTrue(finding.unsaved_request.startswith(":method = POST"), finding.unsaved_request) self.assertIsNotNone(finding.unsaved_response) - self.assertTrue(finding.unsaved_response.startswith(":status: 200")) + self.assertTrue(finding.unsaved_response.startswith(":status: 200"), finding.unsaved_response) self.assertEqual(1, len(finding.unsaved_endpoints)) endpoint = finding.unsaved_endpoints[0] endpoint.clean() @@ -495,7 +500,7 @@ def test_appcheck_web_application_scanner_parser_appcheck_engine_parser(self): self.assertIsNone(f.unsaved_response) # If the dict originally has a 'Messages' entry, it should remain there since no req/res was extracted if has_messages_entry: - self.assertTrue("Messages" in no_rr) + self.assertIn("Messages", no_rr) for template, test_data in { # HTTP/1 diff --git a/unittests/tools/test_asff_parser.py b/unittests/tools/test_asff_parser.py index 602bcda013..fe01bb06cf 100644 --- a/unittests/tools/test_asff_parser.py +++ b/unittests/tools/test_asff_parser.py @@ -36,7 +36,7 @@ def common_check_finding(self, finding, data, index, guarddutydate=False): "IpV4Addresses" ] for endpoint in finding.unsaved_endpoints: - self.assertTrue(endpoint, expected_ipv4s) + self.assertIn(str(endpoint), expected_ipv4s) endpoint.clean() def test_asff_one_vuln(self): diff --git a/unittests/tools/test_auditjs_parser.py b/unittests/tools/test_auditjs_parser.py index d9ca55e745..4a367a7ca2 100644 --- a/unittests/tools/test_auditjs_parser.py +++ b/unittests/tools/test_auditjs_parser.py @@ -63,8 +63,8 @@ def test_auditjs_parser_empty_with_error(self): parser = AuditJSParser() parser.get_findings(testfile, Test()) - self.assertTrue( - "Invalid JSON format. Are you sure you used --json option ?" in str(context.exception), + self.assertIn( + "Invalid JSON format. Are you sure you used --json option ?", str(context.exception), ) def test_auditjs_parser_with_package_name_has_namespace(self): diff --git a/unittests/tools/test_chefinspect_parser.py b/unittests/tools/test_chefinspect_parser.py index a725ab9341..65aa626281 100644 --- a/unittests/tools/test_chefinspect_parser.py +++ b/unittests/tools/test_chefinspect_parser.py @@ -21,4 +21,4 @@ def test_parse_file_with_multiple_vuln_has_multiple_findings(self): with open("unittests/scans/chefinspect/many_findings.log", encoding="utf-8") as testfile: parser = ChefInspectParser() findings = parser.get_findings(testfile, Test()) - self.assertTrue(10, len(findings)) + self.assertEqual(10, len(findings)) diff --git a/unittests/tools/test_meterian_parser.py b/unittests/tools/test_meterian_parser.py index 8fcafb3972..2a5a9f3c27 100644 --- a/unittests/tools/test_meterian_parser.py +++ b/unittests/tools/test_meterian_parser.py @@ -52,7 +52,7 @@ def test_meterianParser_finding_has_fields(self): self.assertEqual(1, len(finding.unsaved_vulnerability_ids)) self.assertEqual("CVE-2020-26289", finding.unsaved_vulnerability_ids[0]) self.assertEqual(400, finding.cwe) - self.assertTrue(finding.mitigation.startswith("## Remediation")) + self.assertTrue(finding.mitigation.startswith("## Remediation"), finding.mitigation) self.assertIn("Upgrade date-and-time to version 0.14.2 or higher.", finding.mitigation) self.assertIn("https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-26289", finding.references, "found " + finding.references) self.assertIn("https://nvd.nist.gov/vuln/detail/CVE-2020-26289", finding.references, "found " + finding.references) @@ -68,7 +68,7 @@ def test_meterianParser_finding_has_no_remediation(self): findings = parser.get_findings(testfile, Test()) finding = findings[0] - self.assertTrue(finding.mitigation.startswith("We were not able to provide a safe version for this library.")) + self.assertTrue(finding.mitigation.startswith("We were not able to provide a safe version for this library."), finding.mitigation) self.assertIn("You should consider replacing this component as it could be an " + "issue for the safety of your application.", finding.mitigation) diff --git a/unittests/tools/test_noseyparker_parser.py b/unittests/tools/test_noseyparker_parser.py index ed4e9ba91d..714e8a4fa7 100644 --- a/unittests/tools/test_noseyparker_parser.py +++ b/unittests/tools/test_noseyparker_parser.py @@ -39,7 +39,7 @@ def test_noseyparker_parser_error(self): findings = parser.get_findings(testfile, Test()) testfile.close() self.assertEqual(0, len(findings)) - self.assertTrue( - "Invalid Nosey Parker data, make sure to use Nosey Parker v0.16.0" in str(context.exception), + self.assertIn( + "Invalid Nosey Parker data, make sure to use Nosey Parker v0.16.0", str(context.exception), ) - self.assertTrue("ECONNREFUSED" in str(context.exception)) + self.assertIn("ECONNREFUSED", str(context.exception)) diff --git a/unittests/tools/test_yarn_audit_parser.py b/unittests/tools/test_yarn_audit_parser.py index 55d28b5b22..6c95592960 100644 --- a/unittests/tools/test_yarn_audit_parser.py +++ b/unittests/tools/test_yarn_audit_parser.py @@ -67,10 +67,10 @@ def test_yarn_audit_parser_empty_with_error(self): with open("unittests/scans/yarn_audit/empty_with_error.json", encoding="utf-8") as testfile: parser = YarnAuditParser() parser.get_findings(testfile, self.get_test()) - self.assertTrue( - "yarn audit report contains errors:" in str(context.exception), + self.assertIn( + "yarn audit report contains errors:", str(context.exception), ) - self.assertTrue("ECONNREFUSED" in str(context.exception)) + self.assertIn("ECONNREFUSED", str(context.exception)) def test_yarn_audit_parser_issue_6495(self): with open("unittests/scans/yarn_audit/issue_6495.json", encoding="utf-8") as testfile: From c87c41981288f300521775f4f02939ad7ac266f2 Mon Sep 17 00:00:00 2001 From: Zaza Date: Fri, 6 Sep 2024 21:39:54 +0100 Subject: [PATCH 13/93] Fixed replica reference for celery worker in Kubernetes.MD (#10842) * Update KUBERNETES.md Fixed the replica reference for celery(beat & workers). The old reference doesn't exist based on the fields in the helm values file. * Update readme-docs/KUBERNETES.md Co-authored-by: kiblik <5609770+kiblik@users.noreply.github.com> * Update readme-docs/KUBERNETES.md Co-authored-by: kiblik <5609770+kiblik@users.noreply.github.com> --------- Co-authored-by: kiblik <5609770+kiblik@users.noreply.github.com> --- readme-docs/KUBERNETES.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/readme-docs/KUBERNETES.md b/readme-docs/KUBERNETES.md index fa2328c784..a4f825e0a8 100644 --- a/readme-docs/KUBERNETES.md +++ b/readme-docs/KUBERNETES.md @@ -288,7 +288,7 @@ helm install \ --set host="defectdojo.${TLS_CERT_DOMAIN}" \ --set django.ingress.secretName="minikube-tls" \ --set django.replicas=3 \ - --set celery.replicas=3 \ + --set celery.worker.replicas=3 \ --set redis.replicas=3 \ --set createSecret=true \ --set createRedisSecret=true \ @@ -302,7 +302,7 @@ helm install \ --namespace="${K8S_NAMESPACE}" \ --set host="defectdojo.${TLS_CERT_DOMAIN}" \ --set django.replicas=3 \ - --set celery.replicas=3 \ + --set celery.worker.replicas=3 \ --set redis.replicas=3 \ --set django.ingress.secretName="minikube-tls" \ --set database=postgresql \ From 573b3e3b3317f9958e3ff0b9db0009ec0bb6d291 Mon Sep 17 00:00:00 2001 From: kiblik <5609770+kiblik@users.noreply.github.com> Date: Fri, 6 Sep 2024 22:40:12 +0200 Subject: [PATCH 14/93] fix(ruff): consolidate RUF rules (#10828) --- ruff.toml | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/ruff.toml b/ruff.toml index 200a0752ae..c2cc53bc0d 100644 --- a/ruff.toml +++ b/ruff.toml @@ -77,13 +77,16 @@ select = [ "FAST", "AIR", "FURB", - "RUF1","RUF2", - "RUF001","RUF002", "RUF003", "RUF005", - "RUF013", - "RUF019", - "RUF021", + "RUF", +] +ignore = [ + "E501", + "E722", + "RUF010", + "RUF012", + "RUF015", + "RUF027", ] -ignore = ["E501", "E722"] # Allow autofix for all enabled rules (when `--fix`) is provided. fixable = ["ALL"] From 55895fc1e3b31b4480b9e9e088fec63eef13fc07 Mon Sep 17 00:00:00 2001 From: kiblik <5609770+kiblik@users.noreply.github.com> Date: Fri, 6 Sep 2024 22:40:33 +0200 Subject: [PATCH 15/93] Ruff: Add and fix ISC001 (#10847) --- dojo/api_v2/serializers.py | 4 ++-- dojo/jira_link/helper.py | 2 +- dojo/tools/dependency_check/parser.py | 2 +- dojo/tools/sysdig_reports/parser.py | 2 +- dojo/utils.py | 2 +- ruff.toml | 1 + unittests/test_tags.py | 2 +- unittests/tools/test_sonarqube_parser.py | 4 ++-- 8 files changed, 10 insertions(+), 9 deletions(-) diff --git a/dojo/api_v2/serializers.py b/dojo/api_v2/serializers.py index 5bed7935f9..9f63975dea 100644 --- a/dojo/api_v2/serializers.py +++ b/dojo/api_v2/serializers.py @@ -1761,10 +1761,10 @@ def validate(self, data): is_risk_accepted = data.get("risk_accepted", False) if (is_active or is_verified) and is_duplicate: - msg = "Duplicate findings cannot be" " verified or active" + msg = "Duplicate findings cannot be verified or active" raise serializers.ValidationError(msg) if is_false_p and is_verified: - msg = "False positive findings cannot " "be verified." + msg = "False positive findings cannot be verified." raise serializers.ValidationError(msg) if is_risk_accepted and not self.instance.risk_accepted: diff --git a/dojo/jira_link/helper.py b/dojo/jira_link/helper.py index 3ccff3df81..b5e3ba8b21 100644 --- a/dojo/jira_link/helper.py +++ b/dojo/jira_link/helper.py @@ -391,7 +391,7 @@ def get_jira_connection_raw(jira_server, jira_username, jira_password): connect_method = get_jira_connect_method() jira = connect_method(jira_server, jira_username, jira_password) - logger.debug("logged in to JIRA ""%s"" successfully", jira_server) + logger.debug("logged in to JIRA %s successfully", jira_server) return jira except JIRAError as e: diff --git a/dojo/tools/dependency_check/parser.py b/dojo/tools/dependency_check/parser.py index 010e84854f..9694004998 100644 --- a/dojo/tools/dependency_check/parser.py +++ b/dojo/tools/dependency_check/parser.py @@ -282,7 +282,7 @@ def get_finding_from_vulnerability( ref_name = reference_node.findtext(f"{namespace}name") if ref_url == ref_name: reference_detail += ( - f"**Source:** {ref_source}\n" f"**URL:** {ref_url}\n\n" + f"**Source:** {ref_source}\n**URL:** {ref_url}\n\n" ) else: reference_detail += ( diff --git a/dojo/tools/sysdig_reports/parser.py b/dojo/tools/sysdig_reports/parser.py index bc2ebea455..2db34b4a52 100644 --- a/dojo/tools/sysdig_reports/parser.py +++ b/dojo/tools/sysdig_reports/parser.py @@ -147,7 +147,7 @@ def parse_csv(self, arr_data, test): if row.k8s_cluster_name != "": finding.dynamic_finding = True finding.static_finding = False - finding.description += f"###Runtime Context {row.k8s_cluster_name}" f"\n - **Cluster:** {row.k8s_cluster_name}" + finding.description += f"###Runtime Context {row.k8s_cluster_name}\n - **Cluster:** {row.k8s_cluster_name}" finding.description += f"\n - **Namespace:** {row.k8s_namespace_name}" finding.description += f"\n - **Workload Name:** {row.k8s_workload_name} " finding.description += f"\n - **Workload Type:** {row.k8s_workload_type} " diff --git a/dojo/utils.py b/dojo/utils.py index 1bac1487dd..9446888b3e 100644 --- a/dojo/utils.py +++ b/dojo/utils.py @@ -2183,7 +2183,7 @@ def mass_model_updater(model_type, models, function, fields, page_size=1000, ord # get maximum, which is the first due to descending order last_id = models.first().id + 1 else: - msg = "order must be ""asc"" or ""desc""" + msg = "order must be asc or desc" raise ValueError(msg) # use filter to make count fast on mysql total_count = models.filter(id__gt=0).count() diff --git a/ruff.toml b/ruff.toml index c2cc53bc0d..bfd276cd70 100644 --- a/ruff.toml +++ b/ruff.toml @@ -49,6 +49,7 @@ select = [ "DJ003", "DJ012", "DJ013", "EM", "EXE", + "ISC001", "ICN", "LOG", "G001", "G002", "G1", "G2", diff --git a/unittests/test_tags.py b/unittests/test_tags.py index 3330d42f0b..a1e1aa20cc 100644 --- a/unittests/test_tags.py +++ b/unittests/test_tags.py @@ -363,7 +363,7 @@ def test_remove_tag_from_product_then_add_tag_to_product(self): self.assertEqual(product_tags_post_removal, self._convert_instance_tags_to_list(objects.get("test"))) self.assertEqual(product_tags_post_removal, self._convert_instance_tags_to_list(objects.get("finding"))) # Add a tag from the product - self.product.tags.add("more", "tags" "!") + self.product.tags.add("more", "tags!") # This triggers an async function with celery that will fail, so run it manually here propagate_tags_on_product_sync(self.product) # Save the tags post removal diff --git a/unittests/tools/test_sonarqube_parser.py b/unittests/tools/test_sonarqube_parser.py index 7f4cf04c7d..352d001ca4 100644 --- a/unittests/tools/test_sonarqube_parser.py +++ b/unittests/tools/test_sonarqube_parser.py @@ -227,7 +227,7 @@ def test_detailed_parse_file_with_table_in_table(self): ) self.assertEqual(str, type(item.references)) self.assertMultiLineEqual( - "squid:S2975\n" "Copy Constructor versus Cloning\n" "S2157\n" "S1182", + "squid:S2975\nCopy Constructor versus Cloning\nS2157\nS1182", item.references, ) self.assertEqual(str, type(item.file_path)) @@ -444,7 +444,7 @@ def test_detailed_parse_file_table_has_whitespace(self): ) self.assertEqual(str, type(item.references)) self.assertMultiLineEqual( - "squid:S2975\n" "Copy Constructor versus Cloning\n" "S2157\n" "S1182", + "squid:S2975\nCopy Constructor versus Cloning\nS2157\nS1182", item.references, ) self.assertEqual(str, type(item.file_path)) From ab40f97231b073d35949b793a6574caed8424b25 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 6 Sep 2024 17:35:23 -0500 Subject: [PATCH 16/93] Bump boto3 from 1.35.12 to 1.35.13 (#10873) Bumps [boto3](https://github.com/boto/boto3) from 1.35.12 to 1.35.13. - [Release notes](https://github.com/boto/boto3/releases) - [Commits](https://github.com/boto/boto3/compare/1.35.12...1.35.13) --- updated-dependencies: - dependency-name: boto3 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 26f4c54089..81853ea3e0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -69,7 +69,7 @@ django-ratelimit==4.1.0 argon2-cffi==23.1.0 blackduck==1.1.3 pycurl==7.45.3 # Required for Celery Broker AWS (SQS) support -boto3==1.35.12 # Required for Celery Broker AWS (SQS) support +boto3==1.35.13 # Required for Celery Broker AWS (SQS) support netaddr==1.3.0 vulners==2.2.0 fontawesomefree==6.6.0 From b26b4596e270f4beab4049046dadea4e5e3add16 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 6 Sep 2024 17:38:19 -0500 Subject: [PATCH 17/93] Bump ruff from 0.6.3 to 0.6.4 (#10874) Bumps [ruff](https://github.com/astral-sh/ruff) from 0.6.3 to 0.6.4. - [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.6.3...0.6.4) --- updated-dependencies: - dependency-name: ruff 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 bcaa2e1f73..0b0a585331 100644 --- a/requirements-lint.txt +++ b/requirements-lint.txt @@ -1 +1 @@ -ruff==0.6.3 \ No newline at end of file +ruff==0.6.4 \ No newline at end of file From 422f0aa3bd7f2e212a33a0ed1a29235711a6251a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 6 Sep 2024 17:50:11 -0500 Subject: [PATCH 18/93] Bump vulners from 2.2.0 to 2.2.1 (#10875) Bumps vulners from 2.2.0 to 2.2.1. --- updated-dependencies: - dependency-name: vulners 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 81853ea3e0..ae858f1c91 100644 --- a/requirements.txt +++ b/requirements.txt @@ -71,5 +71,5 @@ blackduck==1.1.3 pycurl==7.45.3 # Required for Celery Broker AWS (SQS) support boto3==1.35.13 # Required for Celery Broker AWS (SQS) support netaddr==1.3.0 -vulners==2.2.0 +vulners==2.2.1 fontawesomefree==6.6.0 From 4eafb9601f958424b403fbe7ee218bc0f38e55c1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 9 Sep 2024 09:21:21 -0500 Subject: [PATCH 19/93] Bump cvss from 3.1 to 3.2 (#10882) Bumps [cvss](https://github.com/RedHatProductSecurity/cvss) from 3.1 to 3.2. - [Release notes](https://github.com/RedHatProductSecurity/cvss/releases) - [Commits](https://github.com/RedHatProductSecurity/cvss/compare/v3.1...v3.2) --- updated-dependencies: - dependency-name: cvss 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 ae858f1c91..a684c0b435 100644 --- a/requirements.txt +++ b/requirements.txt @@ -58,7 +58,7 @@ vcrpy==6.0.1 vcrpy-unittest==0.1.7 django-tagulous==2.1.0 PyJWT==2.9.0 -cvss==3.1 +cvss==3.2 django-fieldsignals==0.7.0 hyperlink==21.0.0 django-test-migrations==1.4.0 From f901bb1855d4bfb79e2c2ebeb0a7d46f90e0a9b5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 9 Sep 2024 09:22:03 -0500 Subject: [PATCH 20/93] Bump boto3 from 1.35.13 to 1.35.14 (#10881) Bumps [boto3](https://github.com/boto/boto3) from 1.35.13 to 1.35.14. - [Release notes](https://github.com/boto/boto3/releases) - [Commits](https://github.com/boto/boto3/compare/1.35.13...1.35.14) --- updated-dependencies: - dependency-name: boto3 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 a684c0b435..5a1e3d0ca6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -69,7 +69,7 @@ django-ratelimit==4.1.0 argon2-cffi==23.1.0 blackduck==1.1.3 pycurl==7.45.3 # Required for Celery Broker AWS (SQS) support -boto3==1.35.13 # Required for Celery Broker AWS (SQS) support +boto3==1.35.14 # Required for Celery Broker AWS (SQS) support netaddr==1.3.0 vulners==2.2.1 fontawesomefree==6.6.0 From dd332462a8069bc8c263ea43b5f2ded2ac1855d6 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 9 Sep 2024 09:22:35 -0500 Subject: [PATCH 21/93] chore(deps): update postgres:16.4-alpine docker digest from 16.4 to 16.4-alpine (docker-compose.yml) (#10877) 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 df2182f72e..f38dd246be 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -103,7 +103,7 @@ services: source: ./docker/extra_settings target: /app/docker/extra_settings postgres: - image: postgres:16.4-alpine@sha256:492898505cb45f9835acc327e98711eaa9298ed804e0bb36f29e08394229550d + image: postgres:16.4-alpine@sha256:d898b0b78a2627cb4ee63464a14efc9d296884f1b28c841b0ab7d7c42f1fffdf environment: POSTGRES_DB: ${DD_DATABASE_NAME:-defectdojo} POSTGRES_USER: ${DD_DATABASE_USER:-defectdojo} From d7d2bbbaa217a1004f1122d4297174af284eb5d6 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 9 Sep 2024 09:23:00 -0500 Subject: [PATCH 22/93] chore(deps): update redis:7.2.5-alpine docker digest from 7.2.5 to v (docker-compose.yml) (#10878) 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 f38dd246be..095e69f6dc 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -111,7 +111,7 @@ services: volumes: - defectdojo_postgres:/var/lib/postgresql/data redis: - image: redis:7.2.5-alpine@sha256:0bc09d9f486508aa42ecc2f18012bb1e3a1b2744ef3a6ad30942fa12579f0b03 + image: redis:7.2.5-alpine@sha256:6aaf3f5e6bc8a592fbfe2cccf19eb36d27c39d12dab4f4b01556b7449e7b1f44 volumes: - defectdojo_redis:/data volumes: From d7c3ea0ba3411cd0edf3a3eecf93eab09d8812b8 Mon Sep 17 00:00:00 2001 From: DefectDojo release bot Date: Mon, 9 Sep 2024 16:10:17 +0000 Subject: [PATCH 23/93] Update versions in application files --- components/package.json | 2 +- dojo/__init__.py | 2 +- helm/defectdojo/Chart.yaml | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/components/package.json b/components/package.json index 8b293de953..49f5862eec 100644 --- a/components/package.json +++ b/components/package.json @@ -1,6 +1,6 @@ { "name": "defectdojo", - "version": "2.38.1", + "version": "2.39.0-dev", "license" : "BSD-3-Clause", "private": true, "dependencies": { diff --git a/dojo/__init__.py b/dojo/__init__.py index 729d5f3ea8..82fc124150 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.38.1" +__version__ = "2.39.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 4216303364..61744bdfbd 100644 --- a/helm/defectdojo/Chart.yaml +++ b/helm/defectdojo/Chart.yaml @@ -1,8 +1,8 @@ apiVersion: v2 -appVersion: "2.38.1" +appVersion: "2.39.0-dev" description: A Helm chart for Kubernetes to install DefectDojo name: defectdojo -version: 1.6.149 +version: 1.6.150-dev icon: https://www.defectdojo.org/img/favicon.ico maintainers: - name: madchap From 4c5a4cf9406fee8327fce5defb9649540fbddfb3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 10 Sep 2024 13:37:49 -0500 Subject: [PATCH 24/93] Bump boto3 from 1.35.14 to 1.35.15 (#10888) Bumps [boto3](https://github.com/boto/boto3) from 1.35.14 to 1.35.15. - [Release notes](https://github.com/boto/boto3/releases) - [Commits](https://github.com/boto/boto3/compare/1.35.14...1.35.15) --- updated-dependencies: - dependency-name: boto3 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 5a1e3d0ca6..e21c7a0eaf 100644 --- a/requirements.txt +++ b/requirements.txt @@ -69,7 +69,7 @@ django-ratelimit==4.1.0 argon2-cffi==23.1.0 blackduck==1.1.3 pycurl==7.45.3 # Required for Celery Broker AWS (SQS) support -boto3==1.35.14 # Required for Celery Broker AWS (SQS) support +boto3==1.35.15 # Required for Celery Broker AWS (SQS) support netaddr==1.3.0 vulners==2.2.1 fontawesomefree==6.6.0 From 0ae340c4b974073cacdcce96ba334f567f8adc09 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 11 Sep 2024 22:34:08 -0500 Subject: [PATCH 25/93] Bump boto3 from 1.35.15 to 1.35.16 (#10895) Bumps [boto3](https://github.com/boto/boto3) from 1.35.15 to 1.35.16. - [Release notes](https://github.com/boto/boto3/releases) - [Commits](https://github.com/boto/boto3/compare/1.35.15...1.35.16) --- updated-dependencies: - dependency-name: boto3 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 e21c7a0eaf..e96a083356 100644 --- a/requirements.txt +++ b/requirements.txt @@ -69,7 +69,7 @@ django-ratelimit==4.1.0 argon2-cffi==23.1.0 blackduck==1.1.3 pycurl==7.45.3 # Required for Celery Broker AWS (SQS) support -boto3==1.35.15 # Required for Celery Broker AWS (SQS) support +boto3==1.35.16 # Required for Celery Broker AWS (SQS) support netaddr==1.3.0 vulners==2.2.1 fontawesomefree==6.6.0 From ea4e733b24339aecbed4ee90becb4388c61ca3a0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 11 Sep 2024 22:35:12 -0500 Subject: [PATCH 26/93] Bump pytz from 2024.1 to 2024.2 (#10896) Bumps [pytz](https://github.com/stub42/pytz) from 2024.1 to 2024.2. - [Release notes](https://github.com/stub42/pytz/releases) - [Commits](https://github.com/stub42/pytz/compare/release_2024.1...release_2024.2) --- updated-dependencies: - dependency-name: pytz 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 e96a083356..a6f7cd7929 100644 --- a/requirements.txt +++ b/requirements.txt @@ -34,7 +34,7 @@ Pillow==10.4.0 # required by django-imagekit psycopg[c]==3.2.1 cryptography==43.0.1 python-dateutil==2.9.0.post0 -pytz==2024.1 +pytz==2024.2 redis==5.0.8 requests==2.32.3 sqlalchemy==2.0.34 # Required by Celery broker transport From e004cb43c72462cdbc1071b371b429d706b35028 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 13 Sep 2024 16:56:36 -0500 Subject: [PATCH 27/93] Bump boto3 from 1.35.16 to 1.35.18 (#10904) Bumps [boto3](https://github.com/boto/boto3) from 1.35.16 to 1.35.18. - [Release notes](https://github.com/boto/boto3/releases) - [Commits](https://github.com/boto/boto3/compare/1.35.16...1.35.18) --- updated-dependencies: - dependency-name: boto3 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 a6f7cd7929..a6ac748122 100644 --- a/requirements.txt +++ b/requirements.txt @@ -69,7 +69,7 @@ django-ratelimit==4.1.0 argon2-cffi==23.1.0 blackduck==1.1.3 pycurl==7.45.3 # Required for Celery Broker AWS (SQS) support -boto3==1.35.16 # Required for Celery Broker AWS (SQS) support +boto3==1.35.18 # Required for Celery Broker AWS (SQS) support netaddr==1.3.0 vulners==2.2.1 fontawesomefree==6.6.0 From d87a3c35dda68b1d55d225e0b727eba8409930a3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 13 Sep 2024 16:57:01 -0500 Subject: [PATCH 28/93] Bump asteval from 1.0.2 to 1.0.3 (#10903) Bumps [asteval](https://github.com/lmfit/asteval) from 1.0.2 to 1.0.3. - [Release notes](https://github.com/lmfit/asteval/releases) - [Commits](https://github.com/lmfit/asteval/compare/1.0.2...1.0.3) --- updated-dependencies: - dependency-name: asteval 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 a6ac748122..7941ba1544 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ # requirements.txt for DefectDojo using Python 3.x -asteval==1.0.2 +asteval==1.0.3 bleach==6.1.0 bleach[css] celery==5.4.0 From 330462d1025f55aa7be5d42539c88851238c05c9 Mon Sep 17 00:00:00 2001 From: kiblik <5609770+kiblik@users.noreply.github.com> Date: Sat, 14 Sep 2024 02:00:28 +0200 Subject: [PATCH 29/93] Notifications: Add support for webhooks (#7311) * Add go-httpbin * First round of changes * move webhooks to separated model,fix err handliing * flake8 * Uset contant instead of strings * Add basic API endpoints * Add owner of endpoint * Update go-httpbin * Basic GUI * per line * upgrade go-httpbin, move db_mig * Disable view and changes if not enabled in setting * Fix full text of status * Update go-httpbin * Move migration * Rename model + flake8 * Rebase db mig * Rearange setting buttons, add connectivity validator * Handle more generic errors from 'requests' * flake8 * Rewrite YAML template to JSON request body * update go-httpbin * Update go-httpbin * Inc db_mig * Upgrade * Ruff * Update httpbin, move db_mig, use as_view * Fix nones, more verbose "missing template" * Prepare templates * Usable by admins only * API tests * Add main unittests * Update 4xx test * Docs: add Transition graph * ruff * Rewrite * Start "webhook.endpoint" in unit-tests * Extend webhook_status_cleanup, add note to related places * More tests * Small adjustments * Set max_length * Better handle nones * Add basic doc + fix findings_list * Update docs * Clean ruff * Fix db_mig * Fix long notes * Clean ruff * Move "webhook.endpoint" from debug docker to dev * Make fields "editable=False" * Try to fix accesslint * Use class-based choices * Shorter default timeout * Update dojo/notifications/views.py Co-authored-by: Cody Maffucci <46459665+Maffooch@users.noreply.github.com> * Finish preprocess_request * Update dojo/notifications/helper.py Co-authored-by: Cody Maffucci <46459665+Maffooch@users.noreply.github.com> * Show error-times as hint * Try to fix accesslint * Rename `url` to `url_ui` and add `url_api` * inc db_mig * Accept any 2xx as successful * Add permission checker for item in menu * Fix editing for superadmin --------- Co-authored-by: Cody Maffucci <46459665+Maffooch@users.noreply.github.com> --- .github/workflows/rest-framework-tests.yml | 4 +- docker-compose.override.dev.yml | 2 + docker-compose.override.unit_tests.yml | 2 + docker-compose.override.unit_tests_cicd.yml | 2 + docs/content/en/integrations/burp-plugin.md | 2 +- docs/content/en/integrations/exporting.md | 2 +- .../en/integrations/google-sheets-sync.md | 2 +- docs/content/en/integrations/languages.md | 2 +- .../notification_webhooks/_index.md | 79 +++ .../notification_webhooks/engagement_added.md | 38 ++ .../notification_webhooks/product_added.md | 32 ++ .../product_type_added.md | 26 + .../notification_webhooks/scan_added.md | 90 ++++ .../notification_webhooks/test_added.md | 44 ++ docs/content/en/integrations/notifications.md | 7 +- docs/content/en/integrations/rate_limiting.md | 2 +- dojo/api_v2/serializers.py | 7 + dojo/api_v2/views.py | 11 + .../0215_webhooks_notifications.py | 130 +++++ dojo/engagement/signals.py | 2 +- dojo/fixtures/dojo_testdata.json | 57 ++- dojo/forms.py | 27 + dojo/models.py | 38 ++ dojo/notifications/helper.py | 180 ++++++- dojo/notifications/urls.py | 4 + dojo/notifications/views.py | 305 ++++++++++- dojo/product/signals.py | 4 +- dojo/product_type/signals.py | 4 +- dojo/settings/.settings.dist.py.sha256sum | 2 +- dojo/settings/settings.dist.py | 5 +- dojo/templates/base.html | 7 + .../dojo/add_notification_webhook.html | 13 + .../dojo/delete_notification_webhook.html | 12 + .../dojo/edit_notification_webhook.html | 15 + dojo/templates/dojo/notifications.html | 3 + dojo/templates/dojo/system_settings.html | 2 +- .../dojo/view_notification_webhooks.html | 101 ++++ dojo/templates/dojo/view_product_details.html | 2 +- .../webhooks/engagement_added.tpl | 2 + .../notifications/webhooks/other.tpl | 1 + .../notifications/webhooks/product_added.tpl | 2 + .../webhooks/product_type_added.tpl | 2 + .../notifications/webhooks/scan_added.tpl | 12 + .../webhooks/scan_added_empty.tpl | 1 + .../webhooks/subtemplates/base.tpl | 13 + .../webhooks/subtemplates/engagement.tpl | 13 + .../webhooks/subtemplates/findings_list.tpl | 12 + .../webhooks/subtemplates/product.tpl | 13 + .../webhooks/subtemplates/product_type.tpl | 8 + .../webhooks/subtemplates/test.tpl | 13 + .../notifications/webhooks/test_added.tpl | 2 + dojo/templatetags/display_tags.py | 5 + dojo/urls.py | 2 + requirements.txt | 1 + tests/notifications_test.py | 5 + unittests/test_notifications.py | 483 +++++++++++++++++- unittests/test_rest_framework.py | 23 + 57 files changed, 1848 insertions(+), 32 deletions(-) create mode 100644 docs/content/en/integrations/notification_webhooks/_index.md create mode 100644 docs/content/en/integrations/notification_webhooks/engagement_added.md create mode 100644 docs/content/en/integrations/notification_webhooks/product_added.md create mode 100644 docs/content/en/integrations/notification_webhooks/product_type_added.md create mode 100644 docs/content/en/integrations/notification_webhooks/scan_added.md create mode 100644 docs/content/en/integrations/notification_webhooks/test_added.md create mode 100644 dojo/db_migrations/0215_webhooks_notifications.py create mode 100644 dojo/templates/dojo/add_notification_webhook.html create mode 100644 dojo/templates/dojo/delete_notification_webhook.html create mode 100644 dojo/templates/dojo/edit_notification_webhook.html create mode 100644 dojo/templates/dojo/view_notification_webhooks.html create mode 100644 dojo/templates/notifications/webhooks/engagement_added.tpl create mode 100644 dojo/templates/notifications/webhooks/other.tpl create mode 100644 dojo/templates/notifications/webhooks/product_added.tpl create mode 100644 dojo/templates/notifications/webhooks/product_type_added.tpl create mode 100644 dojo/templates/notifications/webhooks/scan_added.tpl create mode 120000 dojo/templates/notifications/webhooks/scan_added_empty.tpl create mode 100644 dojo/templates/notifications/webhooks/subtemplates/base.tpl create mode 100644 dojo/templates/notifications/webhooks/subtemplates/engagement.tpl create mode 100644 dojo/templates/notifications/webhooks/subtemplates/findings_list.tpl create mode 100644 dojo/templates/notifications/webhooks/subtemplates/product.tpl create mode 100644 dojo/templates/notifications/webhooks/subtemplates/product_type.tpl create mode 100644 dojo/templates/notifications/webhooks/subtemplates/test.tpl create mode 100644 dojo/templates/notifications/webhooks/test_added.tpl diff --git a/.github/workflows/rest-framework-tests.yml b/.github/workflows/rest-framework-tests.yml index 907ecf9296..f153a368ba 100644 --- a/.github/workflows/rest-framework-tests.yml +++ b/.github/workflows/rest-framework-tests.yml @@ -34,8 +34,8 @@ jobs: run: docker/setEnv.sh unit_tests_cicd # phased startup so we can use the exit code from unit test container - - name: Start Postgres - run: docker compose up -d postgres + - name: Start Postgres and webhook.endpoint + run: docker compose up -d postgres webhook.endpoint # no celery or initializer needed for unit tests - name: Unit tests diff --git a/docker-compose.override.dev.yml b/docker-compose.override.dev.yml index f3a281af06..cf60d8d00a 100644 --- a/docker-compose.override.dev.yml +++ b/docker-compose.override.dev.yml @@ -53,3 +53,5 @@ services: published: 8025 protocol: tcp mode: host + "webhook.endpoint": + image: mccutchen/go-httpbin:v2.14.0@sha256:e0f398a0a29e7cf00a2467326344d70b4d89d0786d8f9a3287c2a0371c804823 diff --git a/docker-compose.override.unit_tests.yml b/docker-compose.override.unit_tests.yml index 164d7a8708..ccf3c84030 100644 --- a/docker-compose.override.unit_tests.yml +++ b/docker-compose.override.unit_tests.yml @@ -51,6 +51,8 @@ services: redis: image: busybox:1.36.1-musl entrypoint: ['echo', 'skipping', 'redis'] + "webhook.endpoint": + image: mccutchen/go-httpbin:v2.14.0@sha256:e0f398a0a29e7cf00a2467326344d70b4d89d0786d8f9a3287c2a0371c804823 volumes: defectdojo_postgres_unit_tests: {} defectdojo_media_unit_tests: {} diff --git a/docker-compose.override.unit_tests_cicd.yml b/docker-compose.override.unit_tests_cicd.yml index b39f4cf034..141ad7227d 100644 --- a/docker-compose.override.unit_tests_cicd.yml +++ b/docker-compose.override.unit_tests_cicd.yml @@ -50,6 +50,8 @@ services: redis: image: busybox:1.36.1-musl entrypoint: ['echo', 'skipping', 'redis'] + "webhook.endpoint": + image: mccutchen/go-httpbin:v2.14.0@sha256:e0f398a0a29e7cf00a2467326344d70b4d89d0786d8f9a3287c2a0371c804823 volumes: defectdojo_postgres_unit_tests: {} defectdojo_media_unit_tests: {} diff --git a/docs/content/en/integrations/burp-plugin.md b/docs/content/en/integrations/burp-plugin.md index 400b37c0f2..ab3285ceda 100644 --- a/docs/content/en/integrations/burp-plugin.md +++ b/docs/content/en/integrations/burp-plugin.md @@ -2,7 +2,7 @@ title: "Defect Dojo Burp plugin" description: "Export findings directly from Burp to DefectDojo." draft: false -weight: 8 +weight: 9 --- **Please note: The DefectDojo Burp Plugin has been sunset and is no longer a supported feature.** diff --git a/docs/content/en/integrations/exporting.md b/docs/content/en/integrations/exporting.md index da17df7d93..7a42d27b17 100644 --- a/docs/content/en/integrations/exporting.md +++ b/docs/content/en/integrations/exporting.md @@ -2,7 +2,7 @@ title: "Exporting" description: "DefectDojo has the ability to export findings." draft: false -weight: 11 +weight: 12 --- diff --git a/docs/content/en/integrations/google-sheets-sync.md b/docs/content/en/integrations/google-sheets-sync.md index b6e97f72f8..456a694fc6 100644 --- a/docs/content/en/integrations/google-sheets-sync.md +++ b/docs/content/en/integrations/google-sheets-sync.md @@ -2,7 +2,7 @@ title: "Google Sheets synchronisation" description: "Export finding details to Google Sheets and upload changes from Google Sheets." draft: false -weight: 7 +weight: 8 --- **Please note - the Google Sheets feature has been deprecated as of DefectDojo version 2.21.0 - these documents are for reference only.** diff --git a/docs/content/en/integrations/languages.md b/docs/content/en/integrations/languages.md index 17a322c8f9..a78ed137e6 100644 --- a/docs/content/en/integrations/languages.md +++ b/docs/content/en/integrations/languages.md @@ -2,7 +2,7 @@ title: "Languages and lines of code" description: "You can import an analysis of languages used in a project, including lines of code." draft: false -weight: 9 +weight: 10 --- ## Import of languages for a project diff --git a/docs/content/en/integrations/notification_webhooks/_index.md b/docs/content/en/integrations/notification_webhooks/_index.md new file mode 100644 index 0000000000..d8fe606cff --- /dev/null +++ b/docs/content/en/integrations/notification_webhooks/_index.md @@ -0,0 +1,79 @@ +--- +title: "Notification Webhooks (experimental)" +description: "How to setup and use webhooks" +weight: 7 +chapter: true +--- + +Webhooks are HTTP requests coming from the DefectDojo instance towards user-defined webserver which expects this kind of incoming traffic. + +## Transition graph: + +It is not unusual that in some cases webhook can not be performed. It is usually connected to network issues, server misconfiguration, or running upgrades on the server. DefectDojo needs to react to these outages. It might temporarily or permanently disable related endpoints. The following graph shows how it might change the status of the webhook definition based on HTTP responses (or manual user interaction). + +```mermaid +flowchart TD + + START{{Endpoint created}} + ALL{All states} + STATUS_ACTIVE([STATUS_ACTIVE]) + STATUS_INACTIVE_TMP + STATUS_INACTIVE_PERMANENT + STATUS_ACTIVE_TMP([STATUS_ACTIVE_TMP]) + END{{Endpoint removed}} + + START ==> STATUS_ACTIVE + STATUS_ACTIVE --HTTP 200 or 201 --> STATUS_ACTIVE + STATUS_ACTIVE --HTTP 5xx
or HTTP 429
or Timeout--> STATUS_INACTIVE_TMP + STATUS_ACTIVE --Any HTTP 4xx response
or any other HTTP response
or non-HTTP error--> STATUS_INACTIVE_PERMANENT + STATUS_INACTIVE_TMP -.After 60s.-> STATUS_ACTIVE_TMP + STATUS_ACTIVE_TMP --HTTP 5xx
or HTTP 429
or Timeout
within 24h
from the first error-->STATUS_INACTIVE_TMP + STATUS_ACTIVE_TMP -.After 24h.-> STATUS_ACTIVE + STATUS_ACTIVE_TMP --HTTP 200 or 201 --> STATUS_ACTIVE_TMP + STATUS_ACTIVE_TMP --HTTP 5xx
or HTTP 429
or Timeout
within 24h from the first error
or any other HTTP response or error--> STATUS_INACTIVE_PERMANENT + ALL ==Activation by user==> STATUS_ACTIVE + ALL ==Deactivation by user==> STATUS_INACTIVE_PERMANENT + ALL ==Removal of endpoint by user==> END +``` + +Notes: + +1. Transitions: + - bold: manual changes by user + - dotted: automated by celery + - others: based on responses on webhooks +1. Nodes: + - Stadium-shaped: Active - following webhook can be sent + - Rectangles: Inactive - performing of webhook will fail (and not retried) + - Hexagonal: Initial and final states + - Rhombus: All states (meta node to make the graph more readable) + +## Body and Headers + +The body of each request is JSON which contains data about related events like names and IDs of affected elements. +Examples of bodies are on pages related to each event (see below). + +Each request contains the following headers. They might be useful for better handling of events by server this process events. + +```yaml +User-Agent: DefectDojo- +X-DefectDojo-Event: +X-DefectDojo-Instance: +``` +## Disclaimer + +This functionality is new and in experimental mode. This means Functionality might generate breaking changes in following DefectDojo releases and might not be considered final. + +However, the community is open to feedback to make this functionality better and transform it stable as soon as possible. + +## Roadmap + +There are a couple of known issues that are expected to be implemented as soon as core functionality is considered ready. + +- Support events - Not only adding products, product types, engagements, tests, or upload of new scans but also events around SLA +- User webhook - right now only admins can define webhooks; in the future also users will be able to define their own +- Improvement in UI - add filtering and pagination of webhook endpoints + +## Events + + \ No newline at end of file diff --git a/docs/content/en/integrations/notification_webhooks/engagement_added.md b/docs/content/en/integrations/notification_webhooks/engagement_added.md new file mode 100644 index 0000000000..64fd7746ec --- /dev/null +++ b/docs/content/en/integrations/notification_webhooks/engagement_added.md @@ -0,0 +1,38 @@ +--- +title: "Event: engagement_added" +weight: 3 +chapter: true +--- + +## Event HTTP header +```yaml +X-DefectDojo-Event: engagement_added +``` + +## Event HTTP body +```json +{ + "description": null, + "engagement": { + "id": 7, + "name": "notif eng", + "url_api": "http://localhost:8080/api/v2/engagements/7/", + "url_ui": "http://localhost:8080/engagement/7" + }, + "product": { + "id": 4, + "name": "notif prod", + "url_api": "http://localhost:8080/api/v2/products/4/", + "url_ui": "http://localhost:8080/product/4" + }, + "product_type": { + "id": 4, + "name": "notif prod type", + "url_api": "http://localhost:8080/api/v2/product_types/4/", + "url_ui": "http://localhost:8080/product/type/4" + }, + "url_api": "http://localhost:8080/api/v2/engagements/7/", + "url_ui": "http://localhost:8080/engagement/7", + "user": null +} +``` \ No newline at end of file diff --git a/docs/content/en/integrations/notification_webhooks/product_added.md b/docs/content/en/integrations/notification_webhooks/product_added.md new file mode 100644 index 0000000000..2d90a6a681 --- /dev/null +++ b/docs/content/en/integrations/notification_webhooks/product_added.md @@ -0,0 +1,32 @@ +--- +title: "Event: product_added" +weight: 2 +chapter: true +--- + +## Event HTTP header +```yaml +X-DefectDojo-Event: product_added +``` + +## Event HTTP body +```json +{ + "description": null, + "product": { + "id": 4, + "name": "notif prod", + "url_api": "http://localhost:8080/api/v2/products/4/", + "url_ui": "http://localhost:8080/product/4" + }, + "product_type": { + "id": 4, + "name": "notif prod type", + "url_api": "http://localhost:8080/api/v2/product_types/4/", + "url_ui": "http://localhost:8080/product/type/4" + }, + "url_api": "http://localhost:8080/api/v2/products/4/", + "url_ui": "http://localhost:8080/product/4", + "user": null +} +``` \ No newline at end of file diff --git a/docs/content/en/integrations/notification_webhooks/product_type_added.md b/docs/content/en/integrations/notification_webhooks/product_type_added.md new file mode 100644 index 0000000000..1171f51383 --- /dev/null +++ b/docs/content/en/integrations/notification_webhooks/product_type_added.md @@ -0,0 +1,26 @@ +--- +title: "Event: product_type_added" +weight: 1 +chapter: true +--- + +## Event HTTP header +```yaml +X-DefectDojo-Event: product_type_added +``` + +## Event HTTP body +```json +{ + "description": null, + "product_type": { + "id": 4, + "name": "notif prod type", + "url_api": "http://localhost:8080/api/v2/product_types/4/", + "url_ui": "http://localhost:8080/product/type/4" + }, + "url_api": "http://localhost:8080/api/v2/product_types/4/", + "url_ui": "http://localhost:8080/product/type/4", + "user": null +} +``` \ No newline at end of file diff --git a/docs/content/en/integrations/notification_webhooks/scan_added.md b/docs/content/en/integrations/notification_webhooks/scan_added.md new file mode 100644 index 0000000000..27a40e6cab --- /dev/null +++ b/docs/content/en/integrations/notification_webhooks/scan_added.md @@ -0,0 +1,90 @@ +--- +title: "Event: scan_added and scan_added_empty" +weight: 5 +chapter: true +--- + +Event `scan_added_empty` describes a situation when reimport did not affect the existing test (no finding has been created or closed). + +## Event HTTP header for scan_added +```yaml +X-DefectDojo-Event: scan_added +``` + +## Event HTTP header for scan_added_empty +```yaml +X-DefectDojo-Event: scan_added_empty +``` + +## Event HTTP body +```json +{ + "description": null, + "engagement": { + "id": 7, + "name": "notif eng", + "url_api": "http://localhost:8080/api/v2/engagements/7/", + "url_ui": "http://localhost:8080/engagement/7" + }, + "finding_count": 4, + "findings": { + "mitigated": [ + { + "id": 233, + "severity": "Medium", + "title": "Mitigated Finding", + "url_api": "http://localhost:8080/api/v2/findings/233/", + "url_ui": "http://localhost:8080/finding/233" + } + ], + "new": [ + { + "id": 232, + "severity": "Critical", + "title": "New Finding", + "url_api": "http://localhost:8080/api/v2/findings/232/", + "url_ui": "http://localhost:8080/finding/232" + } + ], + "reactivated": [ + { + "id": 234, + "severity": "Low", + "title": "Reactivated Finding", + "url_api": "http://localhost:8080/api/v2/findings/234/", + "url_ui": "http://localhost:8080/finding/234" + } + ], + "untouched": [ + { + "id": 235, + "severity": "Info", + "title": "Untouched Finding", + "url_api": "http://localhost:8080/api/v2/findings/235/", + "url_ui": "http://localhost:8080/finding/235" + } + ] + }, + "product": { + "id": 4, + "name": "notif prod", + "url_api": "http://localhost:8080/api/v2/products/4/", + "url_ui": "http://localhost:8080/product/4" + }, + "product_type": { + "id": 4, + "name": "notif prod type", + "url_api": "http://localhost:8080/api/v2/product_types/4/", + "url_ui": "http://localhost:8080/product/type/4" + }, + "test": { + "id": 90, + "title": "notif test", + "url_api": "http://localhost:8080/api/v2/tests/90/", + "url_ui": "http://localhost:8080/test/90" + }, + "url_api": "http://localhost:8080/api/v2/tests/90/", + "url_ui": "http://localhost:8080/test/90", + "user": null +} +``` \ No newline at end of file diff --git a/docs/content/en/integrations/notification_webhooks/test_added.md b/docs/content/en/integrations/notification_webhooks/test_added.md new file mode 100644 index 0000000000..8614a80e0a --- /dev/null +++ b/docs/content/en/integrations/notification_webhooks/test_added.md @@ -0,0 +1,44 @@ +--- +title: "Event: test_added" +weight: 4 +chapter: true +--- + +## Event HTTP header +```yaml +X-DefectDojo-Event: test_added +``` + +## Event HTTP body +```json +{ + "description": null, + "engagement": { + "id": 7, + "name": "notif eng", + "url_api": "http://localhost:8080/api/v2/engagements/7/", + "url_ui": "http://localhost:8080/engagement/7" + }, + "product": { + "id": 4, + "name": "notif prod", + "url_api": "http://localhost:8080/api/v2/products/4/", + "url_ui": "http://localhost:8080/product/4" + }, + "product_type": { + "id": 4, + "name": "notif prod type", + "url_api": "http://localhost:8080/api/v2/product_types/4/", + "url_ui": "http://localhost:8080/product/type/4" + }, + "test": { + "id": 90, + "title": "notif test", + "url_api": "http://localhost:8080/api/v2/tests/90/", + "url_ui": "http://localhost:8080/test/90" + }, + "url_api": "http://localhost:8080/api/v2/tests/90/", + "url_ui": "http://localhost:8080/test/90", + "user": null +} +``` \ No newline at end of file diff --git a/docs/content/en/integrations/notifications.md b/docs/content/en/integrations/notifications.md index d5af295f0e..803388797c 100644 --- a/docs/content/en/integrations/notifications.md +++ b/docs/content/en/integrations/notifications.md @@ -18,6 +18,7 @@ The following notification methods currently exist: - Email - Slack - Microsoft Teams + - Webhooks - Alerts within DefectDojo (default) You can set these notifications on a global scope (if you have @@ -124,4 +125,8 @@ However, there is a specific use-case when the user decides to disable notificat The scope of this setting is customizable (see environmental variable `DD_NOTIFICATIONS_SYSTEM_LEVEL_TRUMP`). -For more information about this behavior see the [related pull request #9699](https://github.com/DefectDojo/django-DefectDojo/pull/9699/) \ No newline at end of file +For more information about this behavior see the [related pull request #9699](https://github.com/DefectDojo/django-DefectDojo/pull/9699/) + +## Webhooks (experimental) + +DefectDojo also supports webhooks that follow the same events as other notifications (you can be notified in the same situations). Details about setup are described in [related page](../notification_webhooks/). diff --git a/docs/content/en/integrations/rate_limiting.md b/docs/content/en/integrations/rate_limiting.md index 0cac784c5f..1ea76ace5b 100644 --- a/docs/content/en/integrations/rate_limiting.md +++ b/docs/content/en/integrations/rate_limiting.md @@ -2,7 +2,7 @@ title: "Rate Limiting" description: "Configurable rate limiting on the login page to mitigate brute force attacks" draft: false -weight: 9 +weight: 11 --- diff --git a/dojo/api_v2/serializers.py b/dojo/api_v2/serializers.py index c9a87a8362..dc8acb4028 100644 --- a/dojo/api_v2/serializers.py +++ b/dojo/api_v2/serializers.py @@ -77,6 +77,7 @@ Note_Type, NoteHistory, Notes, + Notification_Webhooks, Notifications, Product, Product_API_Scan_Configuration, @@ -3172,3 +3173,9 @@ def create(self, validated_data): raise serializers.ValidationError(msg) else: raise + + +class NotificationWebhooksSerializer(serializers.ModelSerializer): + class Meta: + model = Notification_Webhooks + fields = "__all__" diff --git a/dojo/api_v2/views.py b/dojo/api_v2/views.py index 05d1652106..7ae9925479 100644 --- a/dojo/api_v2/views.py +++ b/dojo/api_v2/views.py @@ -111,6 +111,7 @@ Network_Locations, Note_Type, Notes, + Notification_Webhooks, Notifications, Product, Product_API_Scan_Configuration, @@ -3332,3 +3333,13 @@ class AnnouncementViewSet( def get_queryset(self): return Announcement.objects.all().order_by("id") + + +class NotificationWebhooksViewSet( + PrefetchDojoModelViewSet, +): + serializer_class = serializers.NotificationWebhooksSerializer + queryset = Notification_Webhooks.objects.all() + filter_backends = (DjangoFilterBackend,) + filterset_fields = "__all__" + permission_classes = (permissions.IsSuperUser, DjangoModelPermissions) # TODO: add permission also for other users diff --git a/dojo/db_migrations/0215_webhooks_notifications.py b/dojo/db_migrations/0215_webhooks_notifications.py new file mode 100644 index 0000000000..cc65ce43f1 --- /dev/null +++ b/dojo/db_migrations/0215_webhooks_notifications.py @@ -0,0 +1,130 @@ +# Generated by Django 5.0.8 on 2024-08-16 17:07 + +import django.db.models.deletion +import multiselectfield.db.fields +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('dojo', '0214_test_type_dynamically_generated'), + ] + + operations = [ + migrations.AddField( + model_name='system_settings', + name='enable_webhooks_notifications', + field=models.BooleanField(default=False, verbose_name='Enable Webhook notifications'), + ), + migrations.AddField( + model_name='system_settings', + name='webhooks_notifications_timeout', + field=models.IntegerField(default=10, help_text='How many seconds will DefectDojo waits for response from webhook endpoint'), + ), + migrations.AlterField( + model_name='notifications', + name='auto_close_engagement', + field=multiselectfield.db.fields.MultiSelectField(blank=True, choices=[('slack', 'slack'), ('msteams', 'msteams'), ('mail', 'mail'), ('webhooks', 'webhooks'), ('alert', 'alert')], default=('alert', 'alert'), max_length=33), + ), + migrations.AlterField( + model_name='notifications', + name='close_engagement', + field=multiselectfield.db.fields.MultiSelectField(blank=True, choices=[('slack', 'slack'), ('msteams', 'msteams'), ('mail', 'mail'), ('webhooks', 'webhooks'), ('alert', 'alert')], default=('alert', 'alert'), max_length=33), + ), + migrations.AlterField( + model_name='notifications', + name='code_review', + field=multiselectfield.db.fields.MultiSelectField(blank=True, choices=[('slack', 'slack'), ('msteams', 'msteams'), ('mail', 'mail'), ('webhooks', 'webhooks'), ('alert', 'alert')], default=('alert', 'alert'), max_length=33), + ), + migrations.AlterField( + model_name='notifications', + name='engagement_added', + field=multiselectfield.db.fields.MultiSelectField(blank=True, choices=[('slack', 'slack'), ('msteams', 'msteams'), ('mail', 'mail'), ('webhooks', 'webhooks'), ('alert', 'alert')], default=('alert', 'alert'), max_length=33), + ), + migrations.AlterField( + model_name='notifications', + name='jira_update', + field=multiselectfield.db.fields.MultiSelectField(blank=True, choices=[('slack', 'slack'), ('msteams', 'msteams'), ('mail', 'mail'), ('webhooks', 'webhooks'), ('alert', 'alert')], default=('alert', 'alert'), help_text='JIRA sync happens in the background, errors will be shown as notifications/alerts so make sure to subscribe', max_length=33, verbose_name='JIRA problems'), + ), + migrations.AlterField( + model_name='notifications', + name='other', + field=multiselectfield.db.fields.MultiSelectField(blank=True, choices=[('slack', 'slack'), ('msteams', 'msteams'), ('mail', 'mail'), ('webhooks', 'webhooks'), ('alert', 'alert')], default=('alert', 'alert'), max_length=33), + ), + migrations.AlterField( + model_name='notifications', + name='product_added', + field=multiselectfield.db.fields.MultiSelectField(blank=True, choices=[('slack', 'slack'), ('msteams', 'msteams'), ('mail', 'mail'), ('webhooks', 'webhooks'), ('alert', 'alert')], default=('alert', 'alert'), max_length=33), + ), + migrations.AlterField( + model_name='notifications', + name='product_type_added', + field=multiselectfield.db.fields.MultiSelectField(blank=True, choices=[('slack', 'slack'), ('msteams', 'msteams'), ('mail', 'mail'), ('webhooks', 'webhooks'), ('alert', 'alert')], default=('alert', 'alert'), max_length=33), + ), + migrations.AlterField( + model_name='notifications', + name='review_requested', + field=multiselectfield.db.fields.MultiSelectField(blank=True, choices=[('slack', 'slack'), ('msteams', 'msteams'), ('mail', 'mail'), ('webhooks', 'webhooks'), ('alert', 'alert')], default=('alert', 'alert'), max_length=33), + ), + migrations.AlterField( + model_name='notifications', + name='risk_acceptance_expiration', + field=multiselectfield.db.fields.MultiSelectField(blank=True, choices=[('slack', 'slack'), ('msteams', 'msteams'), ('mail', 'mail'), ('webhooks', 'webhooks'), ('alert', 'alert')], default=('alert', 'alert'), help_text='Get notified of (upcoming) Risk Acceptance expiries', max_length=33, verbose_name='Risk Acceptance Expiration'), + ), + migrations.AlterField( + model_name='notifications', + name='scan_added', + field=multiselectfield.db.fields.MultiSelectField(blank=True, choices=[('slack', 'slack'), ('msteams', 'msteams'), ('mail', 'mail'), ('webhooks', 'webhooks'), ('alert', 'alert')], default=('alert', 'alert'), help_text='Triggered whenever an (re-)import has been done that created/updated/closed findings.', max_length=33), + ), + migrations.AlterField( + model_name='notifications', + name='scan_added_empty', + field=multiselectfield.db.fields.MultiSelectField(blank=True, choices=[('slack', 'slack'), ('msteams', 'msteams'), ('mail', 'mail'), ('webhooks', 'webhooks'), ('alert', 'alert')], default=[], help_text='Triggered whenever an (re-)import has been done (even if that created/updated/closed no findings).', max_length=33), + ), + migrations.AlterField( + model_name='notifications', + name='sla_breach', + field=multiselectfield.db.fields.MultiSelectField(blank=True, choices=[('slack', 'slack'), ('msteams', 'msteams'), ('mail', 'mail'), ('webhooks', 'webhooks'), ('alert', 'alert')], default=('alert', 'alert'), help_text='Get notified of (upcoming) SLA breaches', max_length=33, verbose_name='SLA breach'), + ), + migrations.AlterField( + model_name='notifications', + name='sla_breach_combined', + field=multiselectfield.db.fields.MultiSelectField(blank=True, choices=[('slack', 'slack'), ('msteams', 'msteams'), ('mail', 'mail'), ('webhooks', 'webhooks'), ('alert', 'alert')], default=('alert', 'alert'), help_text='Get notified of (upcoming) SLA breaches (a message per project)', max_length=33, verbose_name='SLA breach (combined)'), + ), + migrations.AlterField( + model_name='notifications', + name='stale_engagement', + field=multiselectfield.db.fields.MultiSelectField(blank=True, choices=[('slack', 'slack'), ('msteams', 'msteams'), ('mail', 'mail'), ('webhooks', 'webhooks'), ('alert', 'alert')], default=('alert', 'alert'), max_length=33), + ), + migrations.AlterField( + model_name='notifications', + name='test_added', + field=multiselectfield.db.fields.MultiSelectField(blank=True, choices=[('slack', 'slack'), ('msteams', 'msteams'), ('mail', 'mail'), ('webhooks', 'webhooks'), ('alert', 'alert')], default=('alert', 'alert'), max_length=33), + ), + migrations.AlterField( + model_name='notifications', + name='upcoming_engagement', + field=multiselectfield.db.fields.MultiSelectField(blank=True, choices=[('slack', 'slack'), ('msteams', 'msteams'), ('mail', 'mail'), ('webhooks', 'webhooks'), ('alert', 'alert')], default=('alert', 'alert'), max_length=33), + ), + migrations.AlterField( + model_name='notifications', + name='user_mentioned', + field=multiselectfield.db.fields.MultiSelectField(blank=True, choices=[('slack', 'slack'), ('msteams', 'msteams'), ('mail', 'mail'), ('webhooks', 'webhooks'), ('alert', 'alert')], default=('alert', 'alert'), max_length=33), + ), + migrations.CreateModel( + name='Notification_Webhooks', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(default='', help_text='Name of the incoming webhook', max_length=100, unique=True)), + ('url', models.URLField(default='', help_text='The full URL of the incoming webhook')), + ('header_name', models.CharField(blank=True, default='', help_text='Name of the header required for interacting with Webhook endpoint', max_length=100, null=True)), + ('header_value', models.CharField(blank=True, default='', help_text='Content of the header required for interacting with Webhook endpoint', max_length=100, null=True)), + ('status', models.CharField(choices=[('active', 'Active'), ('active_tmp', 'Active but 5xx (or similar) error detected'), ('inactive_tmp', 'Temporary inactive because of 5xx (or similar) error'), ('inactive_permanent', 'Permanently inactive')], default='active', editable=False, help_text='Status of the incoming webhook', max_length=20)), + ('first_error', models.DateTimeField(blank=True, editable=False, help_text='If endpoint is active, when error happened first time', null=True)), + ('last_error', models.DateTimeField(blank=True, editable=False, help_text='If endpoint is active, when error happened last time', null=True)), + ('note', models.CharField(blank=True, default='', editable=False, help_text='Description of the latest error', max_length=1000, null=True)), + ('owner', models.ForeignKey(blank=True, help_text='Owner/receiver of notification, if empty processed as system notification', null=True, on_delete=django.db.models.deletion.CASCADE, to='dojo.dojo_user')), + ], + ), + ] diff --git a/dojo/engagement/signals.py b/dojo/engagement/signals.py index c2f09c9abb..7b95d6fe87 100644 --- a/dojo/engagement/signals.py +++ b/dojo/engagement/signals.py @@ -16,7 +16,7 @@ def engagement_post_save(sender, instance, created, **kwargs): if created: title = _('Engagement created for "%(product)s": %(name)s') % {"product": instance.product, "name": instance.name} create_notification(event="engagement_added", title=title, engagement=instance, product=instance.product, - url=reverse("view_engagement", args=(instance.id,))) + url=reverse("view_engagement", args=(instance.id,)), url_api=reverse("engagement-detail", args=(instance.id,))) @receiver(pre_save, sender=Engagement) diff --git a/dojo/fixtures/dojo_testdata.json b/dojo/fixtures/dojo_testdata.json index 62486cb90c..ae550f8bf8 100644 --- a/dojo/fixtures/dojo_testdata.json +++ b/dojo/fixtures/dojo_testdata.json @@ -227,6 +227,7 @@ "url_prefix": "", "enable_slack_notifications": false, "enable_mail_notifications": false, + "enable_webhooks_notifications": true, "email_from": "no-reply@example.com", "false_positive_history": false, "msteams_url": "", @@ -2926,11 +2927,27 @@ "pk": 1, "model": "dojo.notifications", "fields": { - "product": 1, - "user": 2, - "product_type_added": [ - "slack" - ] + "product": null, + "user": null, + "template": false, + "product_type_added": "webhooks,alert", + "product_added": "webhooks,alert", + "engagement_added": "webhooks,alert", + "test_added": "webhooks,alert", + "scan_added": "webhooks,alert", + "scan_added_empty": "webhooks", + "jira_update": "alert", + "upcoming_engagement": "alert", + "stale_engagement": "alert", + "auto_close_engagement": "alert", + "close_engagement": "alert", + "user_mentioned": "alert", + "code_review": "alert", + "review_requested": "alert", + "other": "alert", + "sla_breach": "alert", + "risk_acceptance_expiration": "alert", + "sla_breach_combined": "alert" } }, { @@ -3045,5 +3062,35 @@ "dismissable": true, "style": "danger" } + }, + { + "model": "dojo.notification_webhooks", + "pk": 1, + "fields": { + "name": "My webhook endpoint", + "url": "http://webhook.endpoint:8080/post", + "header_name": "Auth", + "header_value": "Token xxx", + "status": "active", + "first_error": null, + "last_error": null, + "note": null, + "owner": null + } + }, + { + "model": "dojo.notification_webhooks", + "pk": 2, + "fields": { + "name": "My personal webhook endpoint", + "url": "http://webhook.endpoint:8080/post", + "header_name": "Auth", + "header_value": "Token secret", + "status": "active", + "first_error": null, + "last_error": null, + "note": null, + "owner": 2 + } } ] \ No newline at end of file diff --git a/dojo/forms.py b/dojo/forms.py index dde58a38b6..acf3546285 100644 --- a/dojo/forms.py +++ b/dojo/forms.py @@ -72,6 +72,7 @@ JIRA_Project, Note_Type, Notes, + Notification_Webhooks, Notifications, Objects_Product, Product, @@ -2778,6 +2779,32 @@ class Meta: exclude = ["template"] +class NotificationsWebhookForm(forms.ModelForm): + class Meta: + model = Notification_Webhooks + exclude = [] + + def __init__(self, *args, **kwargs): + is_superuser = kwargs.pop("is_superuser", False) + super().__init__(*args, **kwargs) + if not is_superuser: # Only superadmins can edit owner + self.fields["owner"].disabled = True # TODO: needs to be tested + + +class DeleteNotificationsWebhookForm(forms.ModelForm): + id = forms.IntegerField(required=True, + widget=forms.widgets.HiddenInput()) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.fields["name"].disabled = True + self.fields["url"].disabled = True + + class Meta: + model = Notification_Webhooks + fields = ["id", "name", "url"] + + class ProductNotificationsForm(forms.ModelForm): def __init__(self, *args, **kwargs): diff --git a/dojo/models.py b/dojo/models.py index 5048f30427..308db96522 100644 --- a/dojo/models.py +++ b/dojo/models.py @@ -353,6 +353,13 @@ class System_Settings(models.Model): mail_notifications_to = models.CharField(max_length=200, default="", blank=True) + enable_webhooks_notifications = \ + models.BooleanField(default=False, + verbose_name=_("Enable Webhook notifications"), + blank=False) + webhooks_notifications_timeout = models.IntegerField(default=10, + help_text=_("How many seconds will DefectDojo waits for response from webhook endpoint")) + false_positive_history = models.BooleanField( default=False, help_text=_( "(EXPERIMENTAL) DefectDojo will automatically mark the finding as a " @@ -4015,12 +4022,14 @@ def set_obj(self, obj): NOTIFICATION_CHOICE_SLACK = ("slack", "slack") NOTIFICATION_CHOICE_MSTEAMS = ("msteams", "msteams") NOTIFICATION_CHOICE_MAIL = ("mail", "mail") +NOTIFICATION_CHOICE_WEBHOOKS = ("webhooks", "webhooks") NOTIFICATION_CHOICE_ALERT = ("alert", "alert") NOTIFICATION_CHOICES = ( NOTIFICATION_CHOICE_SLACK, NOTIFICATION_CHOICE_MSTEAMS, NOTIFICATION_CHOICE_MAIL, + NOTIFICATION_CHOICE_WEBHOOKS, NOTIFICATION_CHOICE_ALERT, ) @@ -4109,6 +4118,33 @@ def get_list_display(self, request): return list_fields +class Notification_Webhooks(models.Model): + class Status(models.TextChoices): + __STATUS_ACTIVE = "active" + __STATUS_INACTIVE = "inactive" + STATUS_ACTIVE = f"{__STATUS_ACTIVE}", _("Active") + STATUS_ACTIVE_TMP = f"{__STATUS_ACTIVE}_tmp", _("Active but 5xx (or similar) error detected") + STATUS_INACTIVE_TMP = f"{__STATUS_INACTIVE}_tmp", _("Temporary inactive because of 5xx (or similar) error") + STATUS_INACTIVE_PERMANENT = f"{__STATUS_INACTIVE}_permanent", _("Permanently inactive") + + name = models.CharField(max_length=100, default="", blank=False, unique=True, + help_text=_("Name of the incoming webhook")) + url = models.URLField(max_length=200, default="", blank=False, + help_text=_("The full URL of the incoming webhook")) + header_name = models.CharField(max_length=100, default="", blank=True, null=True, + help_text=_("Name of the header required for interacting with Webhook endpoint")) + header_value = models.CharField(max_length=100, default="", blank=True, null=True, + help_text=_("Content of the header required for interacting with Webhook endpoint")) + status = models.CharField(max_length=20, choices=Status, default="active", blank=False, + help_text=_("Status of the incoming webhook"), editable=False) + first_error = models.DateTimeField(help_text=_("If endpoint is active, when error happened first time"), blank=True, null=True, editable=False) + last_error = models.DateTimeField(help_text=_("If endpoint is active, when error happened last time"), blank=True, null=True, editable=False) + note = models.CharField(max_length=1000, default="", blank=True, null=True, help_text=_("Description of the latest error"), editable=False) + owner = models.ForeignKey(Dojo_User, editable=True, null=True, blank=True, on_delete=models.CASCADE, + help_text=_("Owner/receiver of notification, if empty processed as system notification")) + # TODO: Test that `editable` will block editing via API + + class Tool_Product_Settings(models.Model): name = models.CharField(max_length=200, null=False) description = models.CharField(max_length=2000, null=True, blank=True) @@ -4581,6 +4617,7 @@ def __str__(self): auditlog.register(Risk_Acceptance) auditlog.register(Finding_Template) auditlog.register(Cred_User, exclude_fields=["password"]) + auditlog.register(Notification_Webhooks, exclude_fields=["header_name", "header_value"]) from dojo.utils import calculate_grade, to_str_typed # noqa: E402 # there is issue due to a circular import @@ -4642,6 +4679,7 @@ def __str__(self): admin.site.register(GITHUB_Details_Cache) admin.site.register(GITHUB_PKey) admin.site.register(Tool_Configuration, Tool_Configuration_Admin) +admin.site.register(Notification_Webhooks) admin.site.register(Tool_Product_Settings) admin.site.register(Tool_Type) admin.site.register(Cred_User) diff --git a/dojo/notifications/helper.py b/dojo/notifications/helper.py index 5a7ccf0dc6..9acbf94d21 100644 --- a/dojo/notifications/helper.py +++ b/dojo/notifications/helper.py @@ -1,6 +1,9 @@ +import json import logging +from datetime import timedelta import requests +import yaml from django.conf import settings from django.core.exceptions import FieldDoesNotExist from django.core.mail import EmailMessage @@ -10,10 +13,19 @@ from django.urls import reverse from django.utils.translation import gettext as _ +from dojo import __version__ as dd_version from dojo.authorization.roles_permissions import Permissions from dojo.celery import app from dojo.decorators import dojo_async_task, we_want_async -from dojo.models import Alerts, Dojo_User, Notifications, System_Settings, UserContactInfo +from dojo.models import ( + Alerts, + Dojo_User, + Notification_Webhooks, + Notifications, + System_Settings, + UserContactInfo, + get_current_datetime, +) from dojo.user.queries import get_authorized_users_for_product_and_product_type, get_authorized_users_for_product_type logger = logging.getLogger(__name__) @@ -144,8 +156,9 @@ def create_notification_message(event, user, notification_type, *args, **kwargs) try: notification_message = render_to_string(template, kwargs) logger.debug("Rendering from the template %s", template) - except TemplateDoesNotExist: - logger.debug("template not found or not implemented yet: %s", template) + except TemplateDoesNotExist as e: + # In some cases, template includes another templates, if the interior one is missing, we will see it in "specifically" section + logger.debug(f"template not found or not implemented yet: {template} (specifically: {e.args})") except Exception as e: logger.error("error during rendering of template %s exception is %s", template, e) finally: @@ -170,6 +183,7 @@ def process_notifications(event, notifications=None, **kwargs): slack_enabled = get_system_setting("enable_slack_notifications") msteams_enabled = get_system_setting("enable_msteams_notifications") mail_enabled = get_system_setting("enable_mail_notifications") + webhooks_enabled = get_system_setting("enable_webhooks_notifications") if slack_enabled and "slack" in getattr(notifications, event, getattr(notifications, "other")): logger.debug("Sending Slack Notification") @@ -183,6 +197,10 @@ def process_notifications(event, notifications=None, **kwargs): logger.debug("Sending Mail Notification") send_mail_notification(event, notifications.user, **kwargs) + if webhooks_enabled and "webhooks" in getattr(notifications, event, getattr(notifications, "other")): + logger.debug("Sending Webhooks Notification") + send_webhooks_notification(event, notifications.user, **kwargs) + if "alert" in getattr(notifications, event, getattr(notifications, "other")): logger.debug(f"Sending Alert to {notifications.user}") send_alert_notification(event, notifications.user, **kwargs) @@ -309,6 +327,157 @@ def send_mail_notification(event, user=None, *args, **kwargs): log_alert(e, "Email Notification", title=kwargs["title"], description=str(e), url=kwargs["url"]) +def webhooks_notification_request(endpoint, event, *args, **kwargs): + from dojo.utils import get_system_setting + + headers = { + "User-Agent": f"DefectDojo-{dd_version}", + "X-DefectDojo-Event": event, + "X-DefectDojo-Instance": settings.SITE_URL, + "Accept": "application/json", + } + if endpoint.header_name is not None: + headers[endpoint.header_name] = endpoint.header_value + yaml_data = create_notification_message(event, endpoint.owner, "webhooks", *args, **kwargs) + data = yaml.safe_load(yaml_data) + + timeout = get_system_setting("webhooks_notifications_timeout") + + res = requests.request( + method="POST", + url=endpoint.url, + headers=headers, + json=data, + timeout=timeout, + ) + return res + + +def test_webhooks_notification(endpoint): + res = webhooks_notification_request(endpoint, "ping", description="Test webhook notification") + res.raise_for_status() + # in "send_webhooks_notification", we are doing deeper analysis, why it failed + # for now, "raise_for_status" should be enough + + +@app.task(ignore_result=True) +def webhook_reactivation(endpoint_id: int, *args, **kwargs): + endpoint = Notification_Webhooks.objects.get(pk=endpoint_id) + + # User already changed status of endpoint + if endpoint.status != Notification_Webhooks.Status.STATUS_INACTIVE_TMP: + return + + endpoint.status = Notification_Webhooks.Status.STATUS_ACTIVE_TMP + endpoint.save() + logger.debug(f"Webhook endpoint '{endpoint.name}' reactivated to '{Notification_Webhooks.Status.STATUS_ACTIVE_TMP}'") + + +@app.task(ignore_result=True) +def webhook_status_cleanup(*args, **kwargs): + # If some endpoint was affected by some outage (5xx, 429, Timeout) but it was clean during last 24 hours, + # we consider this endpoint as healthy so need to reset it + endpoints = Notification_Webhooks.objects.filter( + status=Notification_Webhooks.Status.STATUS_ACTIVE_TMP, + last_error__lt=get_current_datetime() - timedelta(hours=24), + ) + for endpoint in endpoints: + endpoint.status = Notification_Webhooks.Status.STATUS_ACTIVE + endpoint.first_error = None + endpoint.last_error = None + endpoint.note = f"Reactivation from {Notification_Webhooks.Status.STATUS_ACTIVE_TMP}" + endpoint.save() + logger.debug(f"Webhook endpoint '{endpoint.name}' reactivated from '{Notification_Webhooks.Status.STATUS_ACTIVE_TMP}' to '{Notification_Webhooks.Status.STATUS_ACTIVE}'") + + # Reactivation of STATUS_INACTIVE_TMP endpoints. + # They should reactive automatically in 60s, however in case of some unexpected event (e.g. start of whole stack), + # endpoints should not be left in STATUS_INACTIVE_TMP state + broken_endpoints = Notification_Webhooks.objects.filter( + status=Notification_Webhooks.Status.STATUS_INACTIVE_TMP, + last_error__lt=get_current_datetime() - timedelta(minutes=5), + ) + for endpoint in broken_endpoints: + webhook_reactivation(endpoint_id=endpoint.pk) + + +@dojo_async_task +@app.task +def send_webhooks_notification(event, user=None, *args, **kwargs): + + ERROR_PERMANENT = "permanent" + ERROR_TEMPORARY = "temporary" + + endpoints = Notification_Webhooks.objects.filter(owner=user) + + if not endpoints: + if user: + logger.info(f"URLs for Webhooks not configured for user '{user}': skipping user notification") + else: + logger.info("URLs for Webhooks not configured: skipping system notification") + return + + for endpoint in endpoints: + + error = None + if endpoint.status not in [Notification_Webhooks.Status.STATUS_ACTIVE, Notification_Webhooks.Status.STATUS_ACTIVE_TMP]: + logger.info(f"URL for Webhook '{endpoint.name}' is not active: {endpoint.get_status_display()} ({endpoint.status})") + continue + + try: + logger.debug(f"Sending webhook message to endpoint '{endpoint.name}'") + res = webhooks_notification_request(endpoint, event, *args, **kwargs) + + if 200 <= res.status_code < 300: + logger.debug(f"Message sent to endpoint '{endpoint.name}' successfully.") + continue + + # HTTP request passed successfully but we still need to check status code + if 500 <= res.status_code < 600 or res.status_code == 429: + error = ERROR_TEMPORARY + else: + error = ERROR_PERMANENT + + endpoint.note = f"Response status code: {res.status_code}" + logger.error(f"Error when sending message to Webhooks '{endpoint.name}' (status: {res.status_code}): {res.text}") + + except requests.exceptions.Timeout as e: + error = ERROR_TEMPORARY + endpoint.note = f"Requests exception: {e}" + logger.error(f"Timeout when sending message to Webhook '{endpoint.name}'") + + except Exception as e: + error = ERROR_PERMANENT + endpoint.note = f"Exception: {e}"[:1000] + logger.exception(e) + log_alert(e, "Webhooks Notification") + + now = get_current_datetime() + + if error == ERROR_TEMPORARY: + + # If endpoint is unstable for more then one day, it needs to be deactivated + if endpoint.first_error is not None and (now - endpoint.first_error).total_seconds() > 60 * 60 * 24: + endpoint.status = Notification_Webhooks.Status.STATUS_INACTIVE_PERMANENT + + else: + # We need to monitor when outage started + if endpoint.status == Notification_Webhooks.Status.STATUS_ACTIVE: + endpoint.first_error = now + + endpoint.status = Notification_Webhooks.Status.STATUS_INACTIVE_TMP + + # In case of failure within one day, endpoint can be deactivated temporally only for one minute + webhook_reactivation.apply_async(kwargs={"endpoint_id": endpoint.pk}, countdown=60) + + # There is no reason to keep endpoint active if it is returning 4xx errors + else: + endpoint.status = Notification_Webhooks.Status.STATUS_INACTIVE_PERMANENT + endpoint.first_error = now + + endpoint.last_error = now + endpoint.save() + + def send_alert_notification(event, user=None, *args, **kwargs): logger.debug("sending alert notification to %s", user) try: @@ -335,7 +504,6 @@ def send_alert_notification(event, user=None, *args, **kwargs): def get_slack_user_id(user_email): - import json from dojo.utils import get_system_setting @@ -390,7 +558,7 @@ def log_alert(e, notification_type=None, *args, **kwargs): def notify_test_created(test): title = "Test created for " + str(test.engagement.product) + ": " + str(test.engagement.name) + ": " + str(test) create_notification(event="test_added", title=title, test=test, engagement=test.engagement, product=test.engagement.product, - url=reverse("view_test", args=(test.id,))) + url=reverse("view_test", args=(test.id,)), url_api=reverse("test-detail", args=(test.id,))) def notify_scan_added(test, updated_count, new_findings=[], findings_mitigated=[], findings_reactivated=[], findings_untouched=[]): @@ -410,4 +578,4 @@ def notify_scan_added(test, updated_count, new_findings=[], findings_mitigated=[ create_notification(event=event, title=title, findings_new=new_findings, findings_mitigated=findings_mitigated, findings_reactivated=findings_reactivated, finding_count=updated_count, test=test, engagement=test.engagement, product=test.engagement.product, findings_untouched=findings_untouched, - url=reverse("view_test", args=(test.id,))) + url=reverse("view_test", args=(test.id,)), url_api=reverse("test-detail", args=(test.id,))) diff --git a/dojo/notifications/urls.py b/dojo/notifications/urls.py index dc91f7a04e..6f4cba7bb6 100644 --- a/dojo/notifications/urls.py +++ b/dojo/notifications/urls.py @@ -7,4 +7,8 @@ re_path(r"^notifications/system$", views.SystemNotificationsView.as_view(), name="system_notifications"), re_path(r"^notifications/personal$", views.PersonalNotificationsView.as_view(), name="personal_notifications"), re_path(r"^notifications/template$", views.TemplateNotificationsView.as_view(), name="template_notifications"), + re_path(r"^notifications/webhooks$", views.ListNotificationWebhooksView.as_view(), name="notification_webhooks"), + re_path(r"^notifications/webhooks/add$", views.AddNotificationWebhooksView.as_view(), name="add_notification_webhook"), + re_path(r"^notifications/webhooks/(?P\d+)/edit$", views.EditNotificationWebhooksView.as_view(), name="edit_notification_webhook"), + re_path(r"^notifications/webhooks/(?P\d+)/delete$", views.DeleteNotificationWebhooksView.as_view(), name="delete_notification_webhook"), ] diff --git a/dojo/notifications/views.py b/dojo/notifications/views.py index 8a94d2ad7c..6a2495330d 100644 --- a/dojo/notifications/views.py +++ b/dojo/notifications/views.py @@ -1,15 +1,18 @@ import logging +import requests from django.contrib import messages from django.core.exceptions import PermissionDenied -from django.http import HttpRequest -from django.shortcuts import render +from django.http import Http404, HttpRequest, HttpResponseRedirect +from django.shortcuts import get_object_or_404, render +from django.urls import reverse from django.utils.translation import gettext as _ from django.views import View -from dojo.forms import NotificationsForm -from dojo.models import Notifications -from dojo.utils import add_breadcrumb, get_enabled_notifications_list +from dojo.forms import DeleteNotificationsWebhookForm, NotificationsForm, NotificationsWebhookForm +from dojo.models import Notification_Webhooks, Notifications +from dojo.notifications.helper import test_webhooks_notification +from dojo.utils import add_breadcrumb, get_enabled_notifications_list, get_system_setting logger = logging.getLogger(__name__) @@ -129,3 +132,295 @@ def get_scope(self): def set_breadcrumbs(self, request: HttpRequest): add_breadcrumb(title=_("Template notification settings"), top_level=False, request=request) return request + + +class NotificationWebhooksView(View): + + def check_webhooks_enabled(self): + if not get_system_setting("enable_webhooks_notifications"): + raise Http404 + + def check_user_permissions(self, request: HttpRequest): + if not request.user.is_superuser: + raise PermissionDenied + # TODO: finished access for other users + # if not user_has_configuration_permission(request.user, self.permission): + # raise PermissionDenied() + + def set_breadcrumbs(self, request: HttpRequest): + add_breadcrumb(title=self.breadcrumb, top_level=False, request=request) + return request + + def get_form( + self, + request: HttpRequest, + **kwargs: dict, + ) -> NotificationsWebhookForm: + if request.method == "POST": + return NotificationsWebhookForm(request.POST, is_superuser=request.user.is_superuser, **kwargs) + else: + return NotificationsWebhookForm(is_superuser=request.user.is_superuser, **kwargs) + + def preprocess_request(self, request: HttpRequest): + # Check Webhook notifications are enabled + self.check_webhooks_enabled() + # Check permissions + self.check_user_permissions(request) + + +class ListNotificationWebhooksView(NotificationWebhooksView): + template = "dojo/view_notification_webhooks.html" + permission = "dojo.view_notification_webhooks" + breadcrumb = "Notification Webhook List" + + def get_initial_context(self, request: HttpRequest, nwhs: Notification_Webhooks): + return { + "name": "Notification Webhook List", + "metric": False, + "user": request.user, + "nwhs": nwhs, + } + + def get_notification_webhooks(self, request: HttpRequest): + nwhs = Notification_Webhooks.objects.all().order_by("name") + # TODO: finished pagination + # TODO: restrict based on user - not only superadmins have access and they see everything + return nwhs + + def get(self, request: HttpRequest): + # Run common checks + super().preprocess_request(request) + # Get Notification Webhooks + nwhs = self.get_notification_webhooks(request) + # Set up the initial context + context = self.get_initial_context(request, nwhs) + # Add any breadcrumbs + request = self.set_breadcrumbs(request) + # Render the page + return render(request, self.template, context) + + +class AddNotificationWebhooksView(NotificationWebhooksView): + template = "dojo/add_notification_webhook.html" + permission = "dojo.add_notification_webhooks" + breadcrumb = "Add Notification Webhook" + + # TODO: Disable Owner if not superadmin + + def get_initial_context(self, request: HttpRequest): + return { + "name": "Add Notification Webhook", + "user": request.user, + "form": self.get_form(request), + } + + def process_form(self, request: HttpRequest, context: dict): + form = context["form"] + if form.is_valid(): + try: + test_webhooks_notification(form.instance) + except requests.exceptions.RequestException as e: + messages.add_message( + request, + messages.ERROR, + _("Test of endpoint was not successful: %(error)s") % {"error": str(e)}, + extra_tags="alert-danger", + ) + return request, False + else: + # User can put here what ever he want + # we override it with our only valid defaults + nwh = form.save(commit=False) + nwh.status = Notification_Webhooks.Status.STATUS_ACTIVE + nwh.first_error = None + nwh.last_error = None + nwh.note = None + nwh.save() + messages.add_message( + request, + messages.SUCCESS, + _("Notification Webhook added successfully."), + extra_tags="alert-success", + ) + return request, True + return request, False + + def get(self, request: HttpRequest): + # Run common checks + super().preprocess_request(request) + # Set up the initial context + context = self.get_initial_context(request) + # Add any breadcrumbs + request = self.set_breadcrumbs(request) + # Render the page + return render(request, self.template, context) + + def post(self, request: HttpRequest): + # Run common checks + super().preprocess_request(request) + # Set up the initial context + context = self.get_initial_context(request) + # Determine the validity of the form + request, success = self.process_form(request, context) + if success: + return HttpResponseRedirect(reverse("notification_webhooks")) + # Add any breadcrumbs + request = self.set_breadcrumbs(request) + # Render the page + return render(request, self.template, context) + + +class EditNotificationWebhooksView(NotificationWebhooksView): + template = "dojo/edit_notification_webhook.html" + permission = "dojo.change_notification_webhooks" + # TODO: this could be better: @user_is_authorized(Finding, Permissions.Finding_Delete, 'fid') + breadcrumb = "Edit Notification Webhook" + + def get_notification_webhook(self, nwhid: int): + return get_object_or_404(Notification_Webhooks, id=nwhid) + + # TODO: Disable Owner if not superadmin + + def get_initial_context(self, request: HttpRequest, nwh: Notification_Webhooks): + return { + "name": "Edit Notification Webhook", + "user": request.user, + "form": self.get_form(request, instance=nwh), + "nwh": nwh, + } + + def process_form(self, request: HttpRequest, nwh: Notification_Webhooks, context: dict): + form = context["form"] + if "deactivate_webhook" in request.POST: # TODO: add this to API as well + nwh.status = Notification_Webhooks.Status.STATUS_INACTIVE_PERMANENT + nwh.first_error = None + nwh.last_error = None + nwh.note = "Deactivate from UI" + nwh.save() + messages.add_message( + request, + messages.SUCCESS, + _("Notification Webhook deactivated successfully."), + extra_tags="alert-success", + ) + return request, True + + if form.is_valid(): + try: + test_webhooks_notification(form.instance) + except requests.exceptions.RequestException as e: + messages.add_message( + request, + messages.ERROR, + _("Test of endpoint was not successful: %(error)s") % {"error": str(e)}, + extra_tags="alert-danger") + return request, False + else: + # correct definition reset defaults + nwh = form.save(commit=False) + nwh.status = Notification_Webhooks.Status.STATUS_ACTIVE + nwh.first_error = None + nwh.last_error = None + nwh.note = None + nwh.save() + messages.add_message( + request, + messages.SUCCESS, + _("Notification Webhook updated successfully."), + extra_tags="alert-success", + ) + return request, True + return request, False + + def get(self, request: HttpRequest, nwhid: int): + # Run common checks + super().preprocess_request(request) + nwh = self.get_notification_webhook(nwhid) + # Set up the initial context + context = self.get_initial_context(request, nwh) + # Add any breadcrumbs + request = self.set_breadcrumbs(request) + # Render the page + return render(request, self.template, context) + + def post(self, request: HttpRequest, nwhid: int): + # Run common checks + super().preprocess_request(request) + nwh = self.get_notification_webhook(nwhid) + # Set up the initial context + context = self.get_initial_context(request, nwh) + # Determine the validity of the form + request, success = self.process_form(request, nwh, context) + if success: + return HttpResponseRedirect(reverse("notification_webhooks")) + # Add any breadcrumbs + request = self.set_breadcrumbs(request) + # Render the page + return render(request, self.template, context) + + +class DeleteNotificationWebhooksView(NotificationWebhooksView): + template = "dojo/delete_notification_webhook.html" + permission = "dojo.delete_notification_webhooks" + # TODO: this could be better: @user_is_authorized(Finding, Permissions.Finding_Delete, 'fid') + breadcrumb = "Edit Notification Webhook" + + def get_notification_webhook(self, nwhid: int): + return get_object_or_404(Notification_Webhooks, id=nwhid) + + # TODO: Disable Owner if not superadmin + + def get_form( + self, + request: HttpRequest, + **kwargs: dict, + ) -> NotificationsWebhookForm: + if request.method == "POST": + return DeleteNotificationsWebhookForm(request.POST, **kwargs) + else: + return DeleteNotificationsWebhookForm(**kwargs) + + def get_initial_context(self, request: HttpRequest, nwh: Notification_Webhooks): + return { + "form": self.get_form(request, instance=nwh), + "nwh": nwh, + } + + def process_form(self, request: HttpRequest, nwh: Notification_Webhooks, context: dict): + form = context["form"] + if form.is_valid(): + nwh.delete() + messages.add_message( + request, + messages.SUCCESS, + _("Notification Webhook deleted successfully."), + extra_tags="alert-success", + ) + return request, True + return request, False + + def get(self, request: HttpRequest, nwhid: int): + # Run common checks + super().preprocess_request(request) + nwh = self.get_notification_webhook(nwhid) + # Set up the initial context + context = self.get_initial_context(request, nwh) + # Add any breadcrumbs + request = self.set_breadcrumbs(request) + # Render the page + return render(request, self.template, context) + + def post(self, request: HttpRequest, nwhid: int): + # Run common checks + super().preprocess_request(request) + nwh = self.get_notification_webhook(nwhid) + # Set up the initial context + context = self.get_initial_context(request, nwh) + # Determine the validity of the form + request, success = self.process_form(request, nwh, context) + if success: + return HttpResponseRedirect(reverse("notification_webhooks")) + # Add any breadcrumbs + request = self.set_breadcrumbs(request) + # Render the page + return render(request, self.template, context) diff --git a/dojo/product/signals.py b/dojo/product/signals.py index 6871f5490d..72e9771e82 100644 --- a/dojo/product/signals.py +++ b/dojo/product/signals.py @@ -16,7 +16,9 @@ def product_post_save(sender, instance, created, **kwargs): create_notification(event="product_added", title=instance.name, product=instance, - url=reverse("view_product", args=(instance.id,))) + url=reverse("view_product", args=(instance.id,)), + url_api=reverse("product-detail", args=(instance.id,)), + ) @receiver(post_delete, sender=Product) diff --git a/dojo/product_type/signals.py b/dojo/product_type/signals.py index dde3ff502c..743995768e 100644 --- a/dojo/product_type/signals.py +++ b/dojo/product_type/signals.py @@ -16,7 +16,9 @@ def product_type_post_save(sender, instance, created, **kwargs): create_notification(event="product_type_added", title=instance.name, product_type=instance, - url=reverse("view_product_type", args=(instance.id,))) + url=reverse("view_product_type", args=(instance.id,)), + url_api=reverse("product_type-detail", args=(instance.id,)), + ) @receiver(post_delete, sender=Product_Type) diff --git a/dojo/settings/.settings.dist.py.sha256sum b/dojo/settings/.settings.dist.py.sha256sum index 878a104af5..4686d63afe 100644 --- a/dojo/settings/.settings.dist.py.sha256sum +++ b/dojo/settings/.settings.dist.py.sha256sum @@ -1 +1 @@ -5adedc433a342d675492b86dc18786f72e167115f9718a397dc9b91c5fdc9c94 +8cd4668bdc4dec192dd5bd3fd767b87a4f6d5441ae8d4a001d2ba61c452e59aa diff --git a/dojo/settings/settings.dist.py b/dojo/settings/settings.dist.py index ebf0283dd6..3a01d93543 100644 --- a/dojo/settings/settings.dist.py +++ b/dojo/settings/settings.dist.py @@ -1142,6 +1142,10 @@ def saml2_attrib_map_format(dict): "task": "dojo.risk_acceptance.helper.expiration_handler", "schedule": crontab(minute=0, hour="*/3"), # every 3 hours }, + "notification_webhook_status_cleanup": { + "task": "dojo.notifications.helper.webhook_status_cleanup", + "schedule": timedelta(minutes=1), + }, # 'jira_status_reconciliation': { # 'task': 'dojo.tasks.jira_status_reconciliation_task', # 'schedule': timedelta(hours=12), @@ -1152,7 +1156,6 @@ def saml2_attrib_map_format(dict): # 'schedule': timedelta(hours=12) # }, - } # ------------------------------------ diff --git a/dojo/templates/base.html b/dojo/templates/base.html index 765ec10dc5..722656ae6a 100644 --- a/dojo/templates/base.html +++ b/dojo/templates/base.html @@ -541,6 +541,13 @@ {% trans "Notifications" %} + {% if system_settings.enable_webhooks_notifications and "dojo.view_notification_webhooks"|has_configuration_permission:request %} +
  • + + {% trans "Notification Webhooks" %} + +
  • + {% endif %}
  • {% trans "Regulations" %} diff --git a/dojo/templates/dojo/add_notification_webhook.html b/dojo/templates/dojo/add_notification_webhook.html new file mode 100644 index 0000000000..12056373af --- /dev/null +++ b/dojo/templates/dojo/add_notification_webhook.html @@ -0,0 +1,13 @@ +{% extends "base.html" %} +{% block content %} + {{ block.super }} +

    Add a new Notification Webhook

    +
    {% csrf_token %} + {% include "dojo/form_fields.html" with form=form %} +
    +
    + +
    +
    +
    +{% endblock %} diff --git a/dojo/templates/dojo/delete_notification_webhook.html b/dojo/templates/dojo/delete_notification_webhook.html new file mode 100644 index 0000000000..f196ad94fc --- /dev/null +++ b/dojo/templates/dojo/delete_notification_webhook.html @@ -0,0 +1,12 @@ +{% extends "base.html" %} +{% block content %} +

    Delete Notification Webhook

    +
    {% csrf_token %} + {% include "dojo/form_fields.html" with form=form %} +
    +
    + +
    +
    +
    +{% endblock %} diff --git a/dojo/templates/dojo/edit_notification_webhook.html b/dojo/templates/dojo/edit_notification_webhook.html new file mode 100644 index 0000000000..94bd56c230 --- /dev/null +++ b/dojo/templates/dojo/edit_notification_webhook.html @@ -0,0 +1,15 @@ +{% extends "base.html" %} + {% block content %} + {{ block.super }} +

    Edit Notification Webhook

    +
    {% csrf_token %} + {% include "dojo/form_fields.html" with form=form %} +
    +
    + + +
    +
    +
    + {% endblock %} + \ No newline at end of file diff --git a/dojo/templates/dojo/notifications.html b/dojo/templates/dojo/notifications.html index 52d87393c4..81fac49d5c 100644 --- a/dojo/templates/dojo/notifications.html +++ b/dojo/templates/dojo/notifications.html @@ -89,6 +89,9 @@

    {% if 'mail' in enabled_notifications %} {% trans "Mail" %} {% endif %} + {% if 'webhooks' in enabled_notifications %} + {% trans "Webhooks" %} + {% endif %} {% trans "Alert" %} diff --git a/dojo/templates/dojo/system_settings.html b/dojo/templates/dojo/system_settings.html index 693abe712f..02510452e1 100644 --- a/dojo/templates/dojo/system_settings.html +++ b/dojo/templates/dojo/system_settings.html @@ -62,7 +62,7 @@

    System Settings

    } $(function () { - $.each(['slack','msteams','mail', 'grade'], function (index, value) { + $.each(['slack','msteams','mail','webhooks','grade'], function (index, value) { updatenotificationsgroup(value); $('#id_enable_' + value + '_notifications').change(function() { updatenotificationsgroup(value)}); }); diff --git a/dojo/templates/dojo/view_notification_webhooks.html b/dojo/templates/dojo/view_notification_webhooks.html new file mode 100644 index 0000000000..6b02c0888d --- /dev/null +++ b/dojo/templates/dojo/view_notification_webhooks.html @@ -0,0 +1,101 @@ +{% extends "base.html" %} +{% load navigation_tags %} +{% load display_tags %} +{% load i18n %} +{% load authorization_tags %} +{% block content %} + {{ block.super }} +
    +{% endblock %} +{% block postscript %} + {{ block.super }} + {% include "dojo/filter_js_snippet.html" %} +{% endblock %} diff --git a/dojo/templates/dojo/view_product_details.html b/dojo/templates/dojo/view_product_details.html index 30dd863fc3..076215121f 100644 --- a/dojo/templates/dojo/view_product_details.html +++ b/dojo/templates/dojo/view_product_details.html @@ -668,7 +668,7 @@