From 0321a4f167fd78c4104c4a8dda51ff93cd13e430 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pedro=20Mart=C3=ADn?= Date: Tue, 6 Oct 2026 08:48:54 +0200 Subject: [PATCH 01/17] fix(deps): patch Django, urllib3 and dulwich CVEs (#12953) --- .trivyignore.yaml | 28 ++++++++ api/changelog.d/django-5-2-17.security.md | 1 + api/changelog.d/dulwich-1-2-9.security.md | 1 + api/changelog.d/urllib3-2-8-0.security.md | 1 + api/pyproject.toml | 19 +++--- api/uv.lock | 52 +++++++------- prowler/changelog.d/dulwich-1-2-9.security.md | 1 + prowler/changelog.d/urllib3-2-8-0.security.md | 1 + pyproject.toml | 4 +- uv.lock | 68 +++++++++---------- 10 files changed, 104 insertions(+), 72 deletions(-) create mode 100644 api/changelog.d/django-5-2-17.security.md create mode 100644 api/changelog.d/dulwich-1-2-9.security.md create mode 100644 api/changelog.d/urllib3-2-8-0.security.md create mode 100644 prowler/changelog.d/dulwich-1-2-9.security.md create mode 100644 prowler/changelog.d/urllib3-2-8-0.security.md diff --git a/.trivyignore.yaml b/.trivyignore.yaml index 27e6ccf24b..72086a090a 100644 --- a/.trivyignore.yaml +++ b/.trivyignore.yaml @@ -137,6 +137,34 @@ vulnerabilities: purls: - "pkg:npm/ip-address" expired_at: 2027-01-31 + - id: CVE-2026-101898 + purls: + - "pkg:npm/axios" + expired_at: 2027-01-31 + - id: CVE-2026-101901 + purls: + - "pkg:npm/axios" + expired_at: 2027-01-31 + - id: CVE-2026-101903 + purls: + - "pkg:npm/axios" + expired_at: 2027-01-31 + - id: CVE-2026-101905 + purls: + - "pkg:npm/axios" + expired_at: 2027-01-31 + - id: CVE-2026-101906 + purls: + - "pkg:npm/axios" + expired_at: 2027-01-31 + - id: CVE-2026-101907 + purls: + - "pkg:npm/axios" + expired_at: 2027-01-31 + - id: CVE-2026-101909 + purls: + - "pkg:npm/axios" + expired_at: 2027-01-31 # CVE-2026-84304 is a DoS in grpc-go <= 1.83.0: a peer fragments a gRPC stream into # millions of tiny HTTP/2 DATA frames until the receiver runs out of heap. Fixed in diff --git a/api/changelog.d/django-5-2-17.security.md b/api/changelog.d/django-5-2-17.security.md new file mode 100644 index 0000000000..ec1e6a7450 --- /dev/null +++ b/api/changelog.d/django-5-2-17.security.md @@ -0,0 +1 @@ +`django` from 5.1.15 to 5.2.17, patching CVE-2026-15307 diff --git a/api/changelog.d/dulwich-1-2-9.security.md b/api/changelog.d/dulwich-1-2-9.security.md new file mode 100644 index 0000000000..8f1dd24253 --- /dev/null +++ b/api/changelog.d/dulwich-1-2-9.security.md @@ -0,0 +1 @@ +`dulwich` from 1.2.5 to 1.2.9, patching GHSA-5fqc-mrg8-w798, GHSA-8mcx-5rqc-vhmf, GHSA-8w8g-wq8h-fq33 and GHSA-cm62-gvxx-vmxx diff --git a/api/changelog.d/urllib3-2-8-0.security.md b/api/changelog.d/urllib3-2-8-0.security.md new file mode 100644 index 0000000000..4645bb5942 --- /dev/null +++ b/api/changelog.d/urllib3-2-8-0.security.md @@ -0,0 +1 @@ +`urllib3` from 2.7.0 to 2.8.0, patching CVE-2026-97687 and CVE-2026-97689 diff --git a/api/pyproject.toml b/api/pyproject.toml index 4d1b3be070..0770fc8389 100644 --- a/api/pyproject.toml +++ b/api/pyproject.toml @@ -25,7 +25,7 @@ authors = [{name = "Prowler Engineering", email = "engineering@prowler.com"}] dependencies = [ "celery (==5.6.2)", "dj-rest-auth[with_social,jwt] (==7.0.1)", - "django (==5.1.15)", + "django (==5.2.17)", "django-allauth[saml] (==65.15.0)", "django-celery-beat (==2.9.0)", "django-celery-results (==2.6.0)", @@ -222,7 +222,7 @@ constraint-dependencies = [ "dill==0.4.1", "distro==1.9.0", "dj-rest-auth==7.0.1", - "django==5.1.15", + "django==5.2.17", "django-allauth==65.15.0", "django-celery-beat==2.9.0", "django-celery-results==2.6.0", @@ -246,7 +246,7 @@ constraint-dependencies = [ "drf-simple-apikey==2.2.1", "drf-spectacular==0.27.2", "drf-spectacular-jsonapi==0.5.1", - "dulwich==1.2.5", + "dulwich==1.2.9", "duo-client==5.5.0", "durationpy==0.10", "email-validator==2.2.0", @@ -439,7 +439,7 @@ constraint-dependencies = [ "tzdata==2025.3", "tzlocal==5.3.1", "uritemplate==4.2.0", - "urllib3==2.7.0", + "urllib3==2.8.0", "uuid6==2024.7.10", "uvicorn==0.49.0", "uvloop==0.22.1", @@ -475,11 +475,10 @@ constraint-dependencies = [ # a constraint cannot satisfy against the SDK's hard pins, so override the whole set # to 1.9.10 until the SDK bump propagates to the pinned master rev. # -# prowler@master hard-pins dulwich==0.23.0 and pyjwt==2.12.1 in [project.dependencies]. -# dulwich 1.2.5 patches GHSA-897w-fcg9-f6xj (arbitrary file write) and pyjwt 2.14.0 -# patches GHSA-ffc3-869f-jxw9 (HMAC/PEM key-confusion); a constraint cannot satisfy these -# against the SDK's hard pins, so override them to the patched versions until the SDK -# bump propagates to the pinned master rev. pyjwt keeps the [crypto] extra because an +# The API installs prowler from a pinned master git rev whose dulwich and pyjwt pins trail +# the root pyproject.toml. dulwich 1.2.9 patches GHSA-5fqc-mrg8-w798, GHSA-8mcx-5rqc-vhmf, +# GHSA-8w8g-wq8h-fq33 and GHSA-cm62-gvxx-vmxx, and pyjwt 2.14.0 patches GHSA-ffc3-869f-jxw9, +# so override both until that rev is refreshed. pyjwt keeps the [crypto] extra because an # override replaces the whole requirement; bare pyjwt would drop it from the consumers # that request pyjwt[crypto] and leave cryptography (needed for RS256) only transitive. override-dependencies = [ @@ -499,6 +498,6 @@ override-dependencies = [ "microsoft-kiota-serialization-json==1.9.10", "microsoft-kiota-serialization-multipart==1.9.10", "microsoft-kiota-serialization-text==1.9.10", - "dulwich==1.2.5", + "dulwich==1.2.9", "pyjwt[crypto]==2.14.0" ] diff --git a/api/uv.lock b/api/uv.lock index 022bdf0632..797aa7e94e 100644 --- a/api/uv.lock +++ b/api/uv.lock @@ -138,7 +138,7 @@ constraints = [ { name = "dill", specifier = "==0.4.1" }, { name = "distro", specifier = "==1.9.0" }, { name = "dj-rest-auth", specifier = "==7.0.1" }, - { name = "django", specifier = "==5.1.15" }, + { name = "django", specifier = "==5.2.17" }, { name = "django-allauth", specifier = "==65.15.0" }, { name = "django-celery-beat", specifier = "==2.9.0" }, { name = "django-celery-results", specifier = "==2.6.0" }, @@ -162,7 +162,7 @@ constraints = [ { name = "drf-simple-apikey", specifier = "==2.2.1" }, { name = "drf-spectacular", specifier = "==0.27.2" }, { name = "drf-spectacular-jsonapi", specifier = "==0.5.1" }, - { name = "dulwich", specifier = "==1.2.5" }, + { name = "dulwich", specifier = "==1.2.9" }, { name = "duo-client", specifier = "==5.5.0" }, { name = "durationpy", specifier = "==0.10" }, { name = "email-validator", specifier = "==2.2.0" }, @@ -355,7 +355,7 @@ constraints = [ { name = "tzdata", specifier = "==2025.3" }, { name = "tzlocal", specifier = "==5.3.1" }, { name = "uritemplate", specifier = "==4.2.0" }, - { name = "urllib3", specifier = "==2.7.0" }, + { name = "urllib3", specifier = "==2.8.0" }, { name = "uuid6", specifier = "==2024.7.10" }, { name = "uvicorn", specifier = "==0.49.0" }, { name = "uvloop", specifier = "==0.22.1" }, @@ -378,7 +378,7 @@ constraints = [ overrides = [ { name = "azure-mgmt-containerservice", specifier = "==34.1.0" }, { name = "cryptography", specifier = "==50.0.0" }, - { name = "dulwich", specifier = "==1.2.5" }, + { name = "dulwich", specifier = "==1.2.9" }, { name = "microsoft-kiota-abstractions", specifier = "==1.9.10" }, { name = "microsoft-kiota-authentication-azure", specifier = "==1.9.10" }, { name = "microsoft-kiota-http", specifier = "==1.9.10" }, @@ -2329,16 +2329,16 @@ with-social = [ [[package]] name = "django" -version = "5.1.15" +version = "5.2.17" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "asgiref" }, { name = "sqlparse" }, { name = "tzdata", marker = "sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/10/45/1ac68964193cfcc0b0912a0f68025d5bdb54f71ba7b8716e85b959874bd0/django-5.1.15.tar.gz", hash = "sha256:46a356b5ff867bece73fc6365e081f21c569973403ee7e9b9a0316f27d0eb947", size = 10719662, upload-time = "2025-12-02T14:01:31.931Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d5/d8/43e9d000519adceb189620b6869ff88031e046df91c2e9da72f8f6918399/django-5.2.17.tar.gz", hash = "sha256:9d4d93be539a18ab80d058eb515900e10951e04c537c5a6b394fc49528d3251f", size = 10889740, upload-time = "2026-08-04T15:04:03.173Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/27/79/372e091f0eba4ddb8228245ccd1baaa140e9658711f5e3a0056e540b4c1e/django-5.1.15-py3-none-any.whl", hash = "sha256:117871e58d6eda37f09870b7d73a3d66567b03aecd515b386b1751177c413432", size = 8260901, upload-time = "2025-12-02T14:01:27.352Z" }, + { url = "https://files.pythonhosted.org/packages/df/f8/ce120525ca78f12b07daf65786679c5d0b54a75285a8958d3ae55e39da35/django-5.2.17-py3-none-any.whl", hash = "sha256:f04fb3b36ee119e1af4fa1d397d5fd6cf12700f49321e84d4f4c642c5b1973db", size = 8315563, upload-time = "2026-08-04T15:03:59.1Z" }, ] [[package]] @@ -2667,27 +2667,27 @@ wheels = [ [[package]] name = "dulwich" -version = "1.2.5" +version = "1.2.9" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions", marker = "python_full_version < '3.12'" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/7f/85/ceb8ecff5cdeee4ceeebb86b599476dee559041dacc6c2c50cc0d4711549/dulwich-1.2.5.tar.gz", hash = "sha256:0395b2c8924c3424bafe2d9c1edd5348cc4b21ce9c1d6655bf01f9a5c47164c8", size = 1253230, upload-time = "2026-05-28T22:27:55.17Z" } +sdist = { url = "https://files.pythonhosted.org/packages/de/17/0a0fa2b998cd9a852b1aa901e3f6145f13a4334f64b91564847e5880968d/dulwich-1.2.9.tar.gz", hash = "sha256:b395466b78eb5c36baf34381b8b831fd26da8a44b428a2eea1d57ba53f68a234", size = 1309797, upload-time = "2026-07-06T20:57:23.202Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/4a/4a/654ae1671610fdf6b65a64586ad67ddd8550d4d08a632b2a4b9614754b6d/dulwich-1.2.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:556593fd11637f80f6018bee1916b1a84f5b420423b470ebb3f1a782ad6ef081", size = 1399277, upload-time = "2026-05-28T22:27:00.801Z" }, - { url = "https://files.pythonhosted.org/packages/85/d8/06ee3bc8eded4bd7adf8adf0c9ea5f19bf96f7e5e626bfaf7311cde4208a/dulwich-1.2.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a70477c991e96cfe8fdd7c866e7251faf71b38bfeb51d6f27554c9cce1caabf3", size = 1382310, upload-time = "2026-05-28T22:27:02.216Z" }, - { url = "https://files.pythonhosted.org/packages/07/17/a03adf50b9095f9f5d863393f21d585dea39bdc4fdf60788ff3a9407a512/dulwich-1.2.5-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:9008ef25cabd379cda4fa86000fc38ca14b72afe17db798a8c85c0b2b7ce4d1e", size = 1470993, upload-time = "2026-05-28T22:27:04.075Z" }, - { url = "https://files.pythonhosted.org/packages/60/58/1dc352d2a5e80befe4338af7208febb44bcfd7496b0dde5ac6dacb07b031/dulwich-1.2.5-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:a5549f4afc973e0a15ea6b0244d57f848d3f3ee13dac557eb311024aebebf128", size = 1497820, upload-time = "2026-05-28T22:27:05.549Z" }, - { url = "https://files.pythonhosted.org/packages/c1/a8/e058959a87e7df7753b112ef66a43ccbc57338c1bbdc23a0edf3833396df/dulwich-1.2.5-cp311-cp311-win32.whl", hash = "sha256:5108acead814d1de8b6262d6d8fb90af7e82f5a4d83788b6b48e39d01800a92f", size = 1066549, upload-time = "2026-05-28T22:27:06.832Z" }, - { url = "https://files.pythonhosted.org/packages/33/91/ff0b444f686718635348986bd73dfce42e947912417893de35de399b878b/dulwich-1.2.5-cp311-cp311-win_amd64.whl", hash = "sha256:5e067b7feceb7034bc99e7c7143a704f1d97d4be7027d9a0aa5a83c0657ff091", size = 1079481, upload-time = "2026-05-28T22:27:08.33Z" }, - { url = "https://files.pythonhosted.org/packages/19/22/4f75770bbe5521cac61c4820ef46d4fbf8c2175d3519ba3d0378d4ba798e/dulwich-1.2.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:701a9ecf7a8a44f5e2459e46befa93530cf36a8b1ae3140aefc007db1d7d0207", size = 1396522, upload-time = "2026-05-28T22:27:09.997Z" }, - { url = "https://files.pythonhosted.org/packages/e5/b1/c07c347681c0cf6acd4b189bf6e8d6207c71a1347b7a1e865eb40faa46b9/dulwich-1.2.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2f90d68bfa97c4ca71de7507984365aefe27b6d248cb28dc99644d0f3ae8c60b", size = 1334826, upload-time = "2026-05-28T22:27:11.582Z" }, - { url = "https://files.pythonhosted.org/packages/13/80/6818eb7ce492e18ab2efa92ab901d173b4b0b159e5681c1424f329600c40/dulwich-1.2.5-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:00b54a1d56ddbacdd8eadd6d4787a51b3a05fefa30eadbf9165fd283a00b90ed", size = 1416616, upload-time = "2026-05-28T22:27:13.195Z" }, - { url = "https://files.pythonhosted.org/packages/14/a7/9790e60d19870f6554f7583722bb324c1355784316f20aeda1c0b5b1491a/dulwich-1.2.5-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:d8f7ea8f47e38e5b0de3fab97e07e9c9161ffddc90b3964512cab2b7749df4e6", size = 1441354, upload-time = "2026-05-28T22:27:14.683Z" }, - { url = "https://files.pythonhosted.org/packages/91/44/0ea8a69c24aa1254ff5996d682eae2eab287d471b937dcdb26d9ea9720b4/dulwich-1.2.5-cp312-cp312-win32.whl", hash = "sha256:8929134acf4ff967203df7600b38535f9b5b590462067a7e30dbce01acb97af9", size = 1017058, upload-time = "2026-05-28T22:27:16.121Z" }, - { url = "https://files.pythonhosted.org/packages/bd/9c/2fcddda7faec3bae52db7c64bfcb5dc756f597f33fae90e8d4e4b4d3b39b/dulwich-1.2.5-cp312-cp312-win_amd64.whl", hash = "sha256:9693d2c9e226b2ea855c1dc3a87e2f4d972f7523fc0f7924e5997e9f4c23d97f", size = 1031731, upload-time = "2026-05-28T22:27:17.633Z" }, - { url = "https://files.pythonhosted.org/packages/07/4b/4a18a59ad230581cd0ef460e96001f90762e566dc2dfdba22aa358eb5a0e/dulwich-1.2.5-py3-none-any.whl", hash = "sha256:1679b376433a0fc7f36586afda1d4ed7427afa7a79d4bf17e5014474eea69fa4", size = 686745, upload-time = "2026-05-28T22:27:53.695Z" }, + { url = "https://files.pythonhosted.org/packages/95/70/0345eb2b07a9b26337d7e9ee2c0fc634be81de9969d683b72eca04878f72/dulwich-1.2.9-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:b01fd4d8bd4fdd2bd99eb465e101b4fac2b50436e317b971da7ac3a1bd3a83d8", size = 1378231, upload-time = "2026-07-06T20:56:20.301Z" }, + { url = "https://files.pythonhosted.org/packages/56/dd/a699facf7371cb26233ea1f659ff172995cc06a999ba07992d2762a7dc1f/dulwich-1.2.9-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e92f76f0048b076ef692094d28645c5e0f65dae1d5e393fbb2e8db2edc017cd3", size = 1371812, upload-time = "2026-07-06T20:56:21.958Z" }, + { url = "https://files.pythonhosted.org/packages/35/02/73baf09f61b66431adb1cd01a9fbe748369a62814b6e27362daaaa924f04/dulwich-1.2.9-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:ac47e02988bdcde0c23f98275908390448617d491ad5d9cb9f33cf3eb8ad512b", size = 1493888, upload-time = "2026-07-06T20:56:23.697Z" }, + { url = "https://files.pythonhosted.org/packages/48/e4/aa2dff296c4a9cd769b5a55a8de68622c1605ad39d75b741527e63117acf/dulwich-1.2.9-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:5e6eb07fbd288059da71019f6746655cd1e136db2f1664fa3e1c81bc11973187", size = 1523051, upload-time = "2026-07-06T20:56:25.525Z" }, + { url = "https://files.pythonhosted.org/packages/24/da/73162905ffb2041a8f484e46c7bb269cb39cc984335b1e66eef23d38cb1d/dulwich-1.2.9-cp311-cp311-win32.whl", hash = "sha256:e96f5d29bf509ceb2dbf9cf4de67fe6e5d785b22f721814360563be6757e7473", size = 1043350, upload-time = "2026-07-06T20:56:27.443Z" }, + { url = "https://files.pythonhosted.org/packages/ab/e6/8e40755771b5d05c9268771f26cef8f2ba5f7a849bedcee06504d7f842ae/dulwich-1.2.9-cp311-cp311-win_amd64.whl", hash = "sha256:255c4c3605582fe8958afeff94fc1f0883aedc37495c43a74e031b904f3bb86d", size = 1103738, upload-time = "2026-07-06T20:56:29.145Z" }, + { url = "https://files.pythonhosted.org/packages/ab/b0/e4f14c603fa42e07d8a7f1d5a73f123a8148d52b79b3cc478797126dcdd3/dulwich-1.2.9-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3d5a2896b6dc3f0c794d34b3eb1af317a43b557fefbee0099ef83d35bb6e102f", size = 1372001, upload-time = "2026-07-06T20:56:30.605Z" }, + { url = "https://files.pythonhosted.org/packages/c9/b7/11c40f1c3ce304e7c2e0e759c589430f043a7967689e61ecb0142ad1ebdc/dulwich-1.2.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:30e72110630e2990d11770227191dfa730b7c045030f818eca5803c6b89bbc5b", size = 1356130, upload-time = "2026-07-06T20:56:32.176Z" }, + { url = "https://files.pythonhosted.org/packages/6d/1a/10d61b784c295a4567615579272d91ede81793c72ead207484c0ed845ddd/dulwich-1.2.9-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:5c636fcb33c4db5495a2dc7686cf6a26cf537060a859982e104dcf67635ea7f7", size = 1437923, upload-time = "2026-07-06T20:56:33.741Z" }, + { url = "https://files.pythonhosted.org/packages/6e/68/3073e2737e2c3f299e736490ec2e8b5864593858b9d08f36fbcbca80a862/dulwich-1.2.9-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:1aa1b55f59a046fc8f25a67560473c789b7ec113b85c6a08021aae10c6f78945", size = 1463517, upload-time = "2026-07-06T20:56:35.812Z" }, + { url = "https://files.pythonhosted.org/packages/f2/61/f96c0da72b08bfbc4e38571374c3000dee13e6d72dd06f25281d36d5b0b7/dulwich-1.2.9-cp312-cp312-win32.whl", hash = "sha256:e27a06f9f71275d712b616ad23008df5f2dcf54a06cb8e2a24be8819ee1ce13a", size = 1082071, upload-time = "2026-07-06T20:56:38.139Z" }, + { url = "https://files.pythonhosted.org/packages/38/96/567f32885e780aaa7441f9f5c6f162e7271d149e27d3cb661caf939cd671/dulwich-1.2.9-cp312-cp312-win_amd64.whl", hash = "sha256:81e1005ffc5d5c872bfc87ac3442f722bce27ba10e435543d5302853c34abf56", size = 1053612, upload-time = "2026-07-06T20:56:39.977Z" }, + { url = "https://files.pythonhosted.org/packages/cd/f9/a6677c0e698b5b781d663eb394bd15cc69c4a270b11cdcc128076753d75c/dulwich-1.2.9-py3-none-any.whl", hash = "sha256:79ff0ff35c4e17ccd58f9bfb23f4f8d416b0b09337b8c99ee3b4f10459bb0d68", size = 711459, upload-time = "2026-07-06T20:57:21.516Z" }, ] [[package]] @@ -5012,7 +5012,7 @@ requires-dist = [ { name = "celery", specifier = "==5.6.2" }, { name = "defusedxml", specifier = "==0.7.1" }, { name = "dj-rest-auth", extras = ["with-social", "jwt"], specifier = "==7.0.1" }, - { name = "django", specifier = "==5.1.15" }, + { name = "django", specifier = "==5.2.17" }, { name = "django-allauth", extras = ["saml"], specifier = "==65.15.0" }, { name = "django-celery-beat", specifier = "==2.9.0" }, { name = "django-celery-results", specifier = "==2.6.0" }, @@ -6332,11 +6332,11 @@ wheels = [ [[package]] name = "urllib3" -version = "2.7.0" +version = "2.8.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", size = 433602, upload-time = "2026-05-07T16:13:18.596Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e3/05/b17359e1cefb4f909b5e40b1b90a496d987258916dbbf88e842c729f510e/urllib3-2.8.0.tar.gz", hash = "sha256:63bf2ead4c879426ebf22ef2a781eeb4aa3b4ae798a0435506f8687fd5bb9b63", size = 458972, upload-time = "2026-09-15T19:29:36.253Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" }, + { url = "https://files.pythonhosted.org/packages/92/9d/c4e665119135114480843e7ab388fa94d8480650450e6f8e26b70d323a4c/urllib3-2.8.0-py3-none-any.whl", hash = "sha256:0cf3cae568d36aa9576b28dfb35f11328f1cb974ca7647d9475ebb86c75ac6e3", size = 135717, upload-time = "2026-09-15T19:29:34.577Z" }, ] [[package]] diff --git a/prowler/changelog.d/dulwich-1-2-9.security.md b/prowler/changelog.d/dulwich-1-2-9.security.md new file mode 100644 index 0000000000..8f1dd24253 --- /dev/null +++ b/prowler/changelog.d/dulwich-1-2-9.security.md @@ -0,0 +1 @@ +`dulwich` from 1.2.5 to 1.2.9, patching GHSA-5fqc-mrg8-w798, GHSA-8mcx-5rqc-vhmf, GHSA-8w8g-wq8h-fq33 and GHSA-cm62-gvxx-vmxx diff --git a/prowler/changelog.d/urllib3-2-8-0.security.md b/prowler/changelog.d/urllib3-2-8-0.security.md new file mode 100644 index 0000000000..4645bb5942 --- /dev/null +++ b/prowler/changelog.d/urllib3-2-8-0.security.md @@ -0,0 +1 @@ +`urllib3` from 2.7.0 to 2.8.0, patching CVE-2026-97687 and CVE-2026-97689 diff --git a/pyproject.toml b/pyproject.toml index 4285938e70..44ae790f2b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -76,7 +76,7 @@ dependencies = [ "dash==3.1.1", "dash-bootstrap-components==2.0.3", "defusedxml==0.7.1", - "dulwich==1.2.5", + "dulwich==1.2.9", "google-api-python-client==2.163.0", "google-auth-httplib2==0.2.0", "jsonschema==4.23.0", @@ -387,7 +387,7 @@ constraint-dependencies = [ "typing-inspection==0.4.2", "tzdata==2026.2", "uritemplate==4.2.0", - "urllib3==2.7.0", + "urllib3==2.8.0", "vulture==2.14", "websocket-client==1.9.0", "werkzeug==3.1.8", diff --git a/uv.lock b/uv.lock index c8e524b709..88a4522da7 100644 --- a/uv.lock +++ b/uv.lock @@ -219,7 +219,7 @@ constraints = [ { name = "typing-inspection", specifier = "==0.4.2" }, { name = "tzdata", specifier = "==2026.2" }, { name = "uritemplate", specifier = "==4.2.0" }, - { name = "urllib3", specifier = "==2.7.0" }, + { name = "urllib3", specifier = "==2.8.0" }, { name = "vulture", specifier = "==2.14" }, { name = "websocket-client", specifier = "==1.9.0" }, { name = "werkzeug", specifier = "==3.1.8" }, @@ -1922,41 +1922,41 @@ wheels = [ [[package]] name = "dulwich" -version = "1.2.5" +version = "1.2.9" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions", marker = "python_full_version < '3.12'" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/7f/85/ceb8ecff5cdeee4ceeebb86b599476dee559041dacc6c2c50cc0d4711549/dulwich-1.2.5.tar.gz", hash = "sha256:0395b2c8924c3424bafe2d9c1edd5348cc4b21ce9c1d6655bf01f9a5c47164c8", size = 1253230, upload-time = "2026-05-28T22:27:55.17Z" } +sdist = { url = "https://files.pythonhosted.org/packages/de/17/0a0fa2b998cd9a852b1aa901e3f6145f13a4334f64b91564847e5880968d/dulwich-1.2.9.tar.gz", hash = "sha256:b395466b78eb5c36baf34381b8b831fd26da8a44b428a2eea1d57ba53f68a234", size = 1309797, upload-time = "2026-07-06T20:57:23.202Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b3/af/60929b502d6541cb015beb9f1da82600aac64d5f705d0188aaf44a7aa77f/dulwich-1.2.5-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:46db47394ba8a95748ae739f5d3a5a3e1724a2f857bf2437bc71bfc0baaed91d", size = 1400236, upload-time = "2026-05-28T22:26:50.998Z" }, - { url = "https://files.pythonhosted.org/packages/04/f8/25de359a9249cc05a58c2500babfe2adff174931f2fa3fe97c700ca16626/dulwich-1.2.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:66aded7d364341b55941973a1562323f25bd205f0809692b687ec36ccd31242c", size = 1382996, upload-time = "2026-05-28T22:26:53.545Z" }, - { url = "https://files.pythonhosted.org/packages/04/f7/640fee144007262096173f5fafd04cc7e5a0d72b0ceeeb9c9a51d99abc43/dulwich-1.2.5-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:dd9569bc26174a3437d749114d36c81fc6c7478b55370ae50125e34e9629e4fe", size = 1471811, upload-time = "2026-05-28T22:26:55.044Z" }, - { url = "https://files.pythonhosted.org/packages/51/2a/348c1f0baa0c42dc79a9f503463ddb00452c234ec5c9e20b43530d78528e/dulwich-1.2.5-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:827366331603150de5976d72dd456a3fd5fc91e856471dc1d10fd64758c05f02", size = 1498006, upload-time = "2026-05-28T22:26:56.531Z" }, - { url = "https://files.pythonhosted.org/packages/d4/f8/95f51bcbc2ce86beea49a67f61228be86bf614a24aa714a8c59e0abdd153/dulwich-1.2.5-cp310-cp310-win32.whl", hash = "sha256:6c683c0f4a062894b6826c61102d415dae86ade61a10003c82ccc2b91858d5fb", size = 1066964, upload-time = "2026-05-28T22:26:58.049Z" }, - { url = "https://files.pythonhosted.org/packages/68/c1/ffa02a1623c3d668de8e66b654187bb8dc24c085224644e5554537ee4642/dulwich-1.2.5-cp310-cp310-win_amd64.whl", hash = "sha256:a6620963196c49212c511cd909f367dacf771f199a27d116f357cc671ea956c7", size = 1079537, upload-time = "2026-05-28T22:26:59.319Z" }, - { url = "https://files.pythonhosted.org/packages/4a/4a/654ae1671610fdf6b65a64586ad67ddd8550d4d08a632b2a4b9614754b6d/dulwich-1.2.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:556593fd11637f80f6018bee1916b1a84f5b420423b470ebb3f1a782ad6ef081", size = 1399277, upload-time = "2026-05-28T22:27:00.801Z" }, - { url = "https://files.pythonhosted.org/packages/85/d8/06ee3bc8eded4bd7adf8adf0c9ea5f19bf96f7e5e626bfaf7311cde4208a/dulwich-1.2.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a70477c991e96cfe8fdd7c866e7251faf71b38bfeb51d6f27554c9cce1caabf3", size = 1382310, upload-time = "2026-05-28T22:27:02.216Z" }, - { url = "https://files.pythonhosted.org/packages/07/17/a03adf50b9095f9f5d863393f21d585dea39bdc4fdf60788ff3a9407a512/dulwich-1.2.5-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:9008ef25cabd379cda4fa86000fc38ca14b72afe17db798a8c85c0b2b7ce4d1e", size = 1470993, upload-time = "2026-05-28T22:27:04.075Z" }, - { url = "https://files.pythonhosted.org/packages/60/58/1dc352d2a5e80befe4338af7208febb44bcfd7496b0dde5ac6dacb07b031/dulwich-1.2.5-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:a5549f4afc973e0a15ea6b0244d57f848d3f3ee13dac557eb311024aebebf128", size = 1497820, upload-time = "2026-05-28T22:27:05.549Z" }, - { url = "https://files.pythonhosted.org/packages/c1/a8/e058959a87e7df7753b112ef66a43ccbc57338c1bbdc23a0edf3833396df/dulwich-1.2.5-cp311-cp311-win32.whl", hash = "sha256:5108acead814d1de8b6262d6d8fb90af7e82f5a4d83788b6b48e39d01800a92f", size = 1066549, upload-time = "2026-05-28T22:27:06.832Z" }, - { url = "https://files.pythonhosted.org/packages/33/91/ff0b444f686718635348986bd73dfce42e947912417893de35de399b878b/dulwich-1.2.5-cp311-cp311-win_amd64.whl", hash = "sha256:5e067b7feceb7034bc99e7c7143a704f1d97d4be7027d9a0aa5a83c0657ff091", size = 1079481, upload-time = "2026-05-28T22:27:08.33Z" }, - { url = "https://files.pythonhosted.org/packages/19/22/4f75770bbe5521cac61c4820ef46d4fbf8c2175d3519ba3d0378d4ba798e/dulwich-1.2.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:701a9ecf7a8a44f5e2459e46befa93530cf36a8b1ae3140aefc007db1d7d0207", size = 1396522, upload-time = "2026-05-28T22:27:09.997Z" }, - { url = "https://files.pythonhosted.org/packages/e5/b1/c07c347681c0cf6acd4b189bf6e8d6207c71a1347b7a1e865eb40faa46b9/dulwich-1.2.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2f90d68bfa97c4ca71de7507984365aefe27b6d248cb28dc99644d0f3ae8c60b", size = 1334826, upload-time = "2026-05-28T22:27:11.582Z" }, - { url = "https://files.pythonhosted.org/packages/13/80/6818eb7ce492e18ab2efa92ab901d173b4b0b159e5681c1424f329600c40/dulwich-1.2.5-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:00b54a1d56ddbacdd8eadd6d4787a51b3a05fefa30eadbf9165fd283a00b90ed", size = 1416616, upload-time = "2026-05-28T22:27:13.195Z" }, - { url = "https://files.pythonhosted.org/packages/14/a7/9790e60d19870f6554f7583722bb324c1355784316f20aeda1c0b5b1491a/dulwich-1.2.5-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:d8f7ea8f47e38e5b0de3fab97e07e9c9161ffddc90b3964512cab2b7749df4e6", size = 1441354, upload-time = "2026-05-28T22:27:14.683Z" }, - { url = "https://files.pythonhosted.org/packages/91/44/0ea8a69c24aa1254ff5996d682eae2eab287d471b937dcdb26d9ea9720b4/dulwich-1.2.5-cp312-cp312-win32.whl", hash = "sha256:8929134acf4ff967203df7600b38535f9b5b590462067a7e30dbce01acb97af9", size = 1017058, upload-time = "2026-05-28T22:27:16.121Z" }, - { url = "https://files.pythonhosted.org/packages/bd/9c/2fcddda7faec3bae52db7c64bfcb5dc756f597f33fae90e8d4e4b4d3b39b/dulwich-1.2.5-cp312-cp312-win_amd64.whl", hash = "sha256:9693d2c9e226b2ea855c1dc3a87e2f4d972f7523fc0f7924e5997e9f4c23d97f", size = 1031731, upload-time = "2026-05-28T22:27:17.633Z" }, - { url = "https://files.pythonhosted.org/packages/f5/33/8bd311edfb312e907b4ec7ad4680922e3f5baeccd18d987aeb57141a685d/dulwich-1.2.5-cp313-cp313-android_21_arm64_v8a.whl", hash = "sha256:2f10dafa1ef5660b1331364bc8d68446448608a8d8f493ed0e260eaf5133e71c", size = 1530974, upload-time = "2026-05-28T22:27:19.301Z" }, - { url = "https://files.pythonhosted.org/packages/f5/0d/e4704188a78efbe785d6dc921b70a4f896dbbd4e37caed3a4122be31e871/dulwich-1.2.5-cp313-cp313-android_21_x86_64.whl", hash = "sha256:9b6d234f1f91335e9f01d9daac42ddc2d2e5c2fdbe285d8eeef50353b283648a", size = 1524254, upload-time = "2026-05-28T22:27:20.993Z" }, - { url = "https://files.pythonhosted.org/packages/62/95/feb632693a629314593972f9393bb0a614aee5301ebd0848cb1c7fbfa56c/dulwich-1.2.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c65230abaa52c72093b70d3b499d5689d1d8f9627e88ad3c3b4f8154e86ac0c8", size = 1394846, upload-time = "2026-05-28T22:27:22.475Z" }, - { url = "https://files.pythonhosted.org/packages/1b/4c/f0b4ae4cd2a34bf21ac37944d75c68d2297ebc0128928f988767d1542ef0/dulwich-1.2.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e128cddeccae4146b556684a0d5426454fff5bfe7306862e5a8ce6b471568af4", size = 1333523, upload-time = "2026-05-28T22:27:23.966Z" }, - { url = "https://files.pythonhosted.org/packages/0f/bf/b834306cd6a5959b1521d7f4d3e7473d39cdaea43063d97fa1eed6f1ba28/dulwich-1.2.5-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:07cb75b58216440e2c170fff4f3d55a5f387358d9489863af8cb11f24ee37121", size = 1415849, upload-time = "2026-05-28T22:27:25.329Z" }, - { url = "https://files.pythonhosted.org/packages/d9/e5/8a6110eb9314fea1bf79c13ab1a5ab699167f1401284cee4bebbf3db2e90/dulwich-1.2.5-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:df4ac3746099562c8160d78d55bb2fa10c9ada7ef970af3e2536bd133cb7830e", size = 1441075, upload-time = "2026-05-28T22:27:26.723Z" }, - { url = "https://files.pythonhosted.org/packages/00/75/e993287749f7fa9ff871a0a4fbbede81727f982b1658ba50e7273a100fd3/dulwich-1.2.5-cp313-cp313-win32.whl", hash = "sha256:9fc113c1348c7eb22c4e8790f68b562bb4f42a721fafb813e89a57e9cd632040", size = 1016835, upload-time = "2026-05-28T22:27:28.544Z" }, - { url = "https://files.pythonhosted.org/packages/50/bc/17cc36a67696e06e040e8035fcfdc2583e3834f736b7fe808d3ce01531d0/dulwich-1.2.5-cp313-cp313-win_amd64.whl", hash = "sha256:1ba83ec3cfb4c506c277400357a51523c8258fa07b841ee06e8e1071da4cfed1", size = 1031444, upload-time = "2026-05-28T22:27:30.204Z" }, - { url = "https://files.pythonhosted.org/packages/07/4b/4a18a59ad230581cd0ef460e96001f90762e566dc2dfdba22aa358eb5a0e/dulwich-1.2.5-py3-none-any.whl", hash = "sha256:1679b376433a0fc7f36586afda1d4ed7427afa7a79d4bf17e5014474eea69fa4", size = 686745, upload-time = "2026-05-28T22:27:53.695Z" }, + { url = "https://files.pythonhosted.org/packages/4a/d8/5144092504b20df4e4baf1006f099f7f53ca57353ccbf5e24f3da13db25d/dulwich-1.2.9-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:90c79e0fe54f63f6b56aacc9b5fdfd20771810607a1dc18fa2a78754bf6abade", size = 1379387, upload-time = "2026-07-06T20:56:10.005Z" }, + { url = "https://files.pythonhosted.org/packages/14/02/cc814bbae0c9763317fff593597b6d860275777f72f54d791c8d99c18257/dulwich-1.2.9-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9dcae6b21ca4ddafe30f79caf3fdb3137214886cc09e6bf1e67e9f5bb61e8206", size = 1373369, upload-time = "2026-07-06T20:56:11.854Z" }, + { url = "https://files.pythonhosted.org/packages/61/a8/6db716095113107b2c72b9a8a95480c71206d87ee994345c06ffefffe14a/dulwich-1.2.9-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:2c8760ad8cd09d26cc2da891bcb65e3ffc3b4dc4ca58d4377d9cd0712cb9dd37", size = 1452849, upload-time = "2026-07-06T20:56:13.451Z" }, + { url = "https://files.pythonhosted.org/packages/de/b1/a9c720ede886341f8b037951a5b9dac4387f8bceaad7f76d15e9e28f1b30/dulwich-1.2.9-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:a94c107eb1b8e397dd6440248f939e31756c4738eaa038d3416b52bad8ece94c", size = 1476940, upload-time = "2026-07-06T20:56:15.134Z" }, + { url = "https://files.pythonhosted.org/packages/7f/c8/6738a67a5cb112880834b8d785c24555295dfa06d8fa5ab29d7bf9c12b98/dulwich-1.2.9-cp310-cp310-win32.whl", hash = "sha256:27dc86b999f25f314205e723a51860851563b7ec144ed852acdaf9c2e6c4a714", size = 1043723, upload-time = "2026-07-06T20:56:16.673Z" }, + { url = "https://files.pythonhosted.org/packages/d3/40/3d6e4960daae19145f7e90778b017280e4b8683071077f68ee8bbe54ec2d/dulwich-1.2.9-cp310-cp310-win_amd64.whl", hash = "sha256:5a2923f2cc4abc4ced2769a54cdec51be949717208ab9a31fe2565fe27f09bbb", size = 1061250, upload-time = "2026-07-06T20:56:18.576Z" }, + { url = "https://files.pythonhosted.org/packages/95/70/0345eb2b07a9b26337d7e9ee2c0fc634be81de9969d683b72eca04878f72/dulwich-1.2.9-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:b01fd4d8bd4fdd2bd99eb465e101b4fac2b50436e317b971da7ac3a1bd3a83d8", size = 1378231, upload-time = "2026-07-06T20:56:20.301Z" }, + { url = "https://files.pythonhosted.org/packages/56/dd/a699facf7371cb26233ea1f659ff172995cc06a999ba07992d2762a7dc1f/dulwich-1.2.9-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e92f76f0048b076ef692094d28645c5e0f65dae1d5e393fbb2e8db2edc017cd3", size = 1371812, upload-time = "2026-07-06T20:56:21.958Z" }, + { url = "https://files.pythonhosted.org/packages/35/02/73baf09f61b66431adb1cd01a9fbe748369a62814b6e27362daaaa924f04/dulwich-1.2.9-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:ac47e02988bdcde0c23f98275908390448617d491ad5d9cb9f33cf3eb8ad512b", size = 1493888, upload-time = "2026-07-06T20:56:23.697Z" }, + { url = "https://files.pythonhosted.org/packages/48/e4/aa2dff296c4a9cd769b5a55a8de68622c1605ad39d75b741527e63117acf/dulwich-1.2.9-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:5e6eb07fbd288059da71019f6746655cd1e136db2f1664fa3e1c81bc11973187", size = 1523051, upload-time = "2026-07-06T20:56:25.525Z" }, + { url = "https://files.pythonhosted.org/packages/24/da/73162905ffb2041a8f484e46c7bb269cb39cc984335b1e66eef23d38cb1d/dulwich-1.2.9-cp311-cp311-win32.whl", hash = "sha256:e96f5d29bf509ceb2dbf9cf4de67fe6e5d785b22f721814360563be6757e7473", size = 1043350, upload-time = "2026-07-06T20:56:27.443Z" }, + { url = "https://files.pythonhosted.org/packages/ab/e6/8e40755771b5d05c9268771f26cef8f2ba5f7a849bedcee06504d7f842ae/dulwich-1.2.9-cp311-cp311-win_amd64.whl", hash = "sha256:255c4c3605582fe8958afeff94fc1f0883aedc37495c43a74e031b904f3bb86d", size = 1103738, upload-time = "2026-07-06T20:56:29.145Z" }, + { url = "https://files.pythonhosted.org/packages/ab/b0/e4f14c603fa42e07d8a7f1d5a73f123a8148d52b79b3cc478797126dcdd3/dulwich-1.2.9-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3d5a2896b6dc3f0c794d34b3eb1af317a43b557fefbee0099ef83d35bb6e102f", size = 1372001, upload-time = "2026-07-06T20:56:30.605Z" }, + { url = "https://files.pythonhosted.org/packages/c9/b7/11c40f1c3ce304e7c2e0e759c589430f043a7967689e61ecb0142ad1ebdc/dulwich-1.2.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:30e72110630e2990d11770227191dfa730b7c045030f818eca5803c6b89bbc5b", size = 1356130, upload-time = "2026-07-06T20:56:32.176Z" }, + { url = "https://files.pythonhosted.org/packages/6d/1a/10d61b784c295a4567615579272d91ede81793c72ead207484c0ed845ddd/dulwich-1.2.9-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:5c636fcb33c4db5495a2dc7686cf6a26cf537060a859982e104dcf67635ea7f7", size = 1437923, upload-time = "2026-07-06T20:56:33.741Z" }, + { url = "https://files.pythonhosted.org/packages/6e/68/3073e2737e2c3f299e736490ec2e8b5864593858b9d08f36fbcbca80a862/dulwich-1.2.9-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:1aa1b55f59a046fc8f25a67560473c789b7ec113b85c6a08021aae10c6f78945", size = 1463517, upload-time = "2026-07-06T20:56:35.812Z" }, + { url = "https://files.pythonhosted.org/packages/f2/61/f96c0da72b08bfbc4e38571374c3000dee13e6d72dd06f25281d36d5b0b7/dulwich-1.2.9-cp312-cp312-win32.whl", hash = "sha256:e27a06f9f71275d712b616ad23008df5f2dcf54a06cb8e2a24be8819ee1ce13a", size = 1082071, upload-time = "2026-07-06T20:56:38.139Z" }, + { url = "https://files.pythonhosted.org/packages/38/96/567f32885e780aaa7441f9f5c6f162e7271d149e27d3cb661caf939cd671/dulwich-1.2.9-cp312-cp312-win_amd64.whl", hash = "sha256:81e1005ffc5d5c872bfc87ac3442f722bce27ba10e435543d5302853c34abf56", size = 1053612, upload-time = "2026-07-06T20:56:39.977Z" }, + { url = "https://files.pythonhosted.org/packages/55/dc/86419d8e1f740a4dc29e32f529bcc32848bd726e34d71481c8e04764f8cf/dulwich-1.2.9-cp313-cp313-android_24_arm64_v8a.whl", hash = "sha256:598cfe12262f97e13a7caf7793afd76e909ea9b1f228ff2b0d0af9cd3992829b", size = 1510306, upload-time = "2026-07-06T20:56:41.863Z" }, + { url = "https://files.pythonhosted.org/packages/ea/47/43d1730d42e3f11b9f1561084101e7cdebb40e3a87879f558370647be052/dulwich-1.2.9-cp313-cp313-android_24_x86_64.whl", hash = "sha256:c68e1302c77baa6db49dde60b2878b4f56728316e9f1bd5adc28d8a22058d1ed", size = 1499940, upload-time = "2026-07-06T20:56:43.71Z" }, + { url = "https://files.pythonhosted.org/packages/67/83/1934132defd24dc8cc38c01e5ca5bb271a9f6f8e109b1ca80e6b80e36998/dulwich-1.2.9-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d154fc8cb21d393f064057c2965f481464b8d82977c4dff335ce89fb60cd46e7", size = 1370890, upload-time = "2026-07-06T20:56:45.261Z" }, + { url = "https://files.pythonhosted.org/packages/18/1e/da9e332726570f5cdaa9f1227f516fb4eff1b0e4b9121d658239c794d5eb/dulwich-1.2.9-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:300573ce5075641d18562da5343f53352c5672465ccd7974ef88d316bf043f84", size = 1355428, upload-time = "2026-07-06T20:56:47.126Z" }, + { url = "https://files.pythonhosted.org/packages/11/d6/375b8e5cab3a001163a93e610c30a3dc4829cf1b17a2afa57ee4caec3f5e/dulwich-1.2.9-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:697fc6dbec9fb33d91a53c36c27bbac7e25e2008fb70e90d6f26e05ac95dfdf2", size = 1437132, upload-time = "2026-07-06T20:56:48.774Z" }, + { url = "https://files.pythonhosted.org/packages/41/ae/c4fd50807495b909e8b7568effd06be2613ab7c6c9d4235756a3037cd817/dulwich-1.2.9-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:6d86a1baa6f6dd8a11770cf210cc9193e1debb68a7265dfe6da3682b7167d631", size = 1461912, upload-time = "2026-07-06T20:56:50.487Z" }, + { url = "https://files.pythonhosted.org/packages/17/de/4a64c796afe8862cec6fc800bd01c003e15bac11935cdd2be6565c228faa/dulwich-1.2.9-cp313-cp313-win32.whl", hash = "sha256:6ac4b9dd9d668bf757aae70641adba22f9aa0c916ad45d3f734fdd58edb1add8", size = 1081801, upload-time = "2026-07-06T20:56:52.134Z" }, + { url = "https://files.pythonhosted.org/packages/45/2e/29ab77e0553f94f4ee43b94e41c883f86e092f6fa3edea4838d9dda91071/dulwich-1.2.9-cp313-cp313-win_amd64.whl", hash = "sha256:516f843f9a03c34cf4f5f8b55a459bd8833550f4b44dade1ea7f91ef907788e7", size = 1053482, upload-time = "2026-07-06T20:56:53.963Z" }, + { url = "https://files.pythonhosted.org/packages/cd/f9/a6677c0e698b5b781d663eb394bd15cc69c4a270b11cdcc128076753d75c/dulwich-1.2.9-py3-none-any.whl", hash = "sha256:79ff0ff35c4e17ccd58f9bfb23f4f8d416b0b09337b8c99ee3b4f10459bb0d68", size = 711459, upload-time = "2026-07-06T20:57:21.516Z" }, ] [[package]] @@ -3940,7 +3940,7 @@ requires-dist = [ { name = "dash", specifier = "==3.1.1" }, { name = "dash-bootstrap-components", specifier = "==2.0.3" }, { name = "defusedxml", specifier = "==0.7.1" }, - { name = "dulwich", specifier = "==1.2.5" }, + { name = "dulwich", specifier = "==1.2.9" }, { name = "google-api-python-client", specifier = "==2.163.0" }, { name = "google-auth-httplib2", specifier = "==0.2.0" }, { name = "h2", specifier = "==4.3.0" }, @@ -5315,11 +5315,11 @@ wheels = [ [[package]] name = "urllib3" -version = "2.7.0" +version = "2.8.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", size = 433602, upload-time = "2026-05-07T16:13:18.596Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e3/05/b17359e1cefb4f909b5e40b1b90a496d987258916dbbf88e842c729f510e/urllib3-2.8.0.tar.gz", hash = "sha256:63bf2ead4c879426ebf22ef2a781eeb4aa3b4ae798a0435506f8687fd5bb9b63", size = 458972, upload-time = "2026-09-15T19:29:36.253Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" }, + { url = "https://files.pythonhosted.org/packages/92/9d/c4e665119135114480843e7ab388fa94d8480650450e6f8e26b70d323a4c/urllib3-2.8.0-py3-none-any.whl", hash = "sha256:0cf3cae568d36aa9576b28dfb35f11328f1cb974ca7647d9475ebb86c75ac6e3", size = 135717, upload-time = "2026-09-15T19:29:34.577Z" }, ] [[package]] From f85019964575739cbf49f34803a2d5c2a6e2d014 Mon Sep 17 00:00:00 2001 From: Alejandro Bailo <59607668+alejandrobailo@users.noreply.github.com> Date: Tue, 6 Oct 2026 10:10:17 +0200 Subject: [PATCH 02/17] fix(ui): pin proxy-addr to 2.0.8 to patch the IP spoofing advisory (#12957) --- ui/changelog.d/proxy-addr-ip-spoofing.security.md | 1 + ui/pnpm-lock.yaml | 9 +++++---- ui/pnpm-workspace.yaml | 5 +++++ 3 files changed, 11 insertions(+), 4 deletions(-) create mode 100644 ui/changelog.d/proxy-addr-ip-spoofing.security.md diff --git a/ui/changelog.d/proxy-addr-ip-spoofing.security.md b/ui/changelog.d/proxy-addr-ip-spoofing.security.md new file mode 100644 index 0000000000..9574c50d16 --- /dev/null +++ b/ui/changelog.d/proxy-addr-ip-spoofing.security.md @@ -0,0 +1 @@ +`proxy-addr` to 2.0.8, patching IP spoofing through IPv4-mapped IPv6 trust subnets (GHSA-jqcg-44mw-7w3h) diff --git a/ui/pnpm-lock.yaml b/ui/pnpm-lock.yaml index 744f064f7b..c9966ddf43 100644 --- a/ui/pnpm-lock.yaml +++ b/ui/pnpm-lock.yaml @@ -44,6 +44,7 @@ overrides: ip-address: 10.3.1 mermaid: 11.16.1 body-parser: 2.3.0 + proxy-addr: 2.0.8 '@humanfs/node': 0.16.8 js-yaml: 4.3.1 @@ -6164,8 +6165,8 @@ packages: property-information@7.1.0: resolution: {integrity: sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==} - proxy-addr@2.0.7: - resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} + proxy-addr@2.0.8: + resolution: {integrity: sha512-5nnx0yGyVUcY6t9RnWcARWtwT9F1D8O9rt08htPvnd49W1IgZtmLkhu9WfMzQj1cFxjHIO6connUNVW5k7AVyQ==} engines: {node: '>= 0.10'} proxy-from-env@1.1.0: @@ -12002,7 +12003,7 @@ snapshots: on-finished: 2.4.1 once: 1.4.0 parseurl: 1.3.3 - proxy-addr: 2.0.7 + proxy-addr: 2.0.8 qs: 6.16.0 range-parser: 1.2.1 router: 2.2.0 @@ -13794,7 +13795,7 @@ snapshots: property-information@7.1.0: {} - proxy-addr@2.0.7: + proxy-addr@2.0.8: dependencies: forwarded: 0.2.0 ipaddr.js: 1.9.1 diff --git a/ui/pnpm-workspace.yaml b/ui/pnpm-workspace.yaml index ab3e084e51..a6a70b20d4 100644 --- a/ui/pnpm-workspace.yaml +++ b/ui/pnpm-workspace.yaml @@ -107,6 +107,11 @@ overrides: "mermaid": "11.16.1" # body-parser (via express): invalid `limit` silently disabled size enforcement. "body-parser": "2.3.0" + # proxy-addr (via @modelcontextprotocol/sdk > express): GHSA-jqcg-44mw-7w3h + # (critical), a short IPv4-mapped IPv6 trust subnet trusts every client, so + # `req.ip` returns a spoofed X-Forwarded-For. 2.0.8 (2026-09-15) clears the + # 7-day cooldown gate. + "proxy-addr": "2.0.8" # @humanfs/node (via eslint): recursive copy followed symlinks outside the tree. "@humanfs/node": "0.16.8" # js-yaml: quadratic CPU in `!!omap` resolution (CVE-2026-59870 not backported From bee60c54711882eb130c00183bf4626aa5efe32e Mon Sep 17 00:00:00 2001 From: Alejandro Bailo <59607668+alejandrobailo@users.noreply.github.com> Date: Tue, 6 Oct 2026 10:23:07 +0200 Subject: [PATCH 03/17] fix(ui): stop reporting client-aborted RSC streams to Sentry (#12956) --- ui/sentry/event-policy.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ui/sentry/event-policy.ts b/ui/sentry/event-policy.ts index c8312abfb6..2d72d6a9b5 100644 --- a/ui/sentry/event-policy.ts +++ b/ui/sentry/event-policy.ts @@ -23,6 +23,8 @@ const EXPECTED_CONTROL_FLOW_MESSAGES = [ "NEXT_NOT_FOUND", "AbortError", "ResizeObserver", + // Client disconnected mid-stream; Next.js forwards it to onRequestError (vercel/next.js#96704) + "The destination stream closed early", ] as const; const HTTP_CONTEXT_MESSAGES = [ From 7ccdf7c0e677c1a172d273cf875416b08d89dc4d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pedro=20Mart=C3=ADn?= Date: Tue, 6 Oct 2026 10:34:58 +0200 Subject: [PATCH 04/17] fix(deps): patch pymongo CVEs and ignore proxy-addr (#12955) --- .trivyignore.yaml | 8 ++ api/changelog.d/pymongo-4-18-2.security.md | 1 + api/pyproject.toml | 3 + api/uv.lock | 47 ++++---- .../changelog.d/pymongo-4-18-2.security.md | 1 + pyproject.toml | 8 +- uv.lock | 101 ++++++++---------- 7 files changed, 91 insertions(+), 78 deletions(-) create mode 100644 api/changelog.d/pymongo-4-18-2.security.md create mode 100644 prowler/changelog.d/pymongo-4-18-2.security.md diff --git a/.trivyignore.yaml b/.trivyignore.yaml index 72086a090a..1d9f44f658 100644 --- a/.trivyignore.yaml +++ b/.trivyignore.yaml @@ -73,6 +73,10 @@ vulnerabilities: # there is no Node runtime and no node_modules anywhere in the image, and the .NET # assemblies target net472, a Windows-only framework. Nothing here is reachable, and none # of it is a dependency we declare -- only Microsoft can change the module's contents. +# proxy-addr 2.0.7 (CVE-2026-90711) is one more such declaration: no file named proxy-addr +# exists in the image, only its purl inside that manifest. The UI lock lists proxy-addr 2.0.7 +# through express, but the Next standalone output does not ship it, so the shared entry +# hides nothing real in the UI image either (checked on prowlercloud/prowler-ui:latest). - id: CVE-2020-0606 purls: - "pkg:nuget/Microsoft.WindowsDesktop.App.Ref" @@ -165,6 +169,10 @@ vulnerabilities: purls: - "pkg:npm/axios" expired_at: 2027-01-31 + - id: CVE-2026-90711 + purls: + - "pkg:npm/proxy-addr" + expired_at: 2027-01-31 # CVE-2026-84304 is a DoS in grpc-go <= 1.83.0: a peer fragments a gRPC stream into # millions of tiny HTTP/2 DATA frames until the receiver runs out of heap. Fixed in diff --git a/api/changelog.d/pymongo-4-18-2.security.md b/api/changelog.d/pymongo-4-18-2.security.md new file mode 100644 index 0000000000..b621313d25 --- /dev/null +++ b/api/changelog.d/pymongo-4-18-2.security.md @@ -0,0 +1 @@ +`pymongo` from 4.15.1 to 4.18.2, patching CVE-2026-96748 and CVE-2026-96749 diff --git a/api/pyproject.toml b/api/pyproject.toml index 0770fc8389..e20a4f3993 100644 --- a/api/pyproject.toml +++ b/api/pyproject.toml @@ -483,6 +483,9 @@ constraint-dependencies = [ # that request pyjwt[crypto] and leave cryptography (needed for RS256) only transitive. override-dependencies = [ "okta==3.4.2", + # huaweicloudsdkcore (via prowler) pins pymongo==4.15.1 exactly; CVE-2026-96748 and + # CVE-2026-96749 are fixed in 4.18.2. Only bson types are used, so the override is safe. + "pymongo==4.18.2", # prowler requires cryptography==50.0.0. Two api-only dependencies still cap it below # 49 and cannot move yet: msal, pinned exactly by azure-cli-core (2.83.0 -> 1.35.0b1, # 2.89.1 -> 1.36.0, both <49; cartography needs azure-cli-core), and workos 8.3.0 diff --git a/api/uv.lock b/api/uv.lock index 797aa7e94e..58a1fffa90 100644 --- a/api/uv.lock +++ b/api/uv.lock @@ -388,6 +388,7 @@ overrides = [ { name = "microsoft-kiota-serialization-text", specifier = "==1.9.10" }, { name = "okta", specifier = "==3.4.2" }, { name = "pyjwt", extras = ["crypto"], specifier = "==2.14.0" }, + { name = "pymongo", specifier = "==4.18.2" }, ] [[package]] @@ -4202,7 +4203,7 @@ wheels = [ [package.optional-dependencies] broker = [ - { name = "pymsalruntime", marker = "sys_platform == 'win32'" }, + { name = "pymsalruntime" }, ] [[package]] @@ -5364,33 +5365,33 @@ wheels = [ [[package]] name = "pymongo" -version = "4.15.1" +version = "4.18.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "dnspython" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/22/f5/c0c6732fbd358b75a07e17d7e588fd23d481b9812ca96ceeff90bbf879fc/pymongo-4.15.1.tar.gz", hash = "sha256:b9f379a4333dc3779a6bf7adfd077d4387404ed1561472743486a9c58286f705", size = 2470613, upload-time = "2025-09-16T16:39:47.24Z" } +sdist = { url = "https://files.pythonhosted.org/packages/dc/04/455039b475bb8afa5af9b8c363fb773a45d898016f04ee40c1c75003a3f9/pymongo-4.18.2.tar.gz", hash = "sha256:1bd4321cc63c06954953eae9eb8fb5d9075296b96c0e3ed34ccb91a0dcce31a5", size = 2746691, upload-time = "2026-09-24T16:59:59.472Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c9/da/89066930a70b4299844f1155fc23baaa7e30e77c8a0cbf62a2ae06ee34a5/pymongo-4.15.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:363445cc0e899b9e55ac9904a868c8a16a6c81f71c48dbadfd78c98e0b54de27", size = 865410, upload-time = "2025-09-16T16:38:16.279Z" }, - { url = "https://files.pythonhosted.org/packages/99/8f/a1d0402d52e5ebd14283718abefdc0c16f308cf10bee56cdff04b1f5119b/pymongo-4.15.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:da0a13f345f4b101776dbab92cec66f0b75015df0b007b47bd73bfd0305cc56a", size = 865695, upload-time = "2025-09-16T16:38:18.015Z" }, - { url = "https://files.pythonhosted.org/packages/53/38/d1ef69028923f86fd00638d9eb16400d4e60a89eabd2011fe631fd3186cf/pymongo-4.15.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9481a492851e432122a83755d4e69c06aeb087bbf8370bac9f96d112ac1303fd", size = 1434758, upload-time = "2025-09-16T16:38:20.141Z" }, - { url = "https://files.pythonhosted.org/packages/b0/eb/a8d5dff748a2dd333610b2e4c8120b623e38ea2b5e30ad190d0ce2803840/pymongo-4.15.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:625dec3e9cd7c3d336285a20728c01bfc56d37230a99ec537a6a8625af783a43", size = 1485716, upload-time = "2025-09-16T16:38:21.607Z" }, - { url = "https://files.pythonhosted.org/packages/c4/d4/17ba457a828b733182ddc01a202872fef3006eed6b54450b20dc95a2f77d/pymongo-4.15.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:26a31af455bffcc64537a7f67e2f84833a57855a82d05a085a1030c471138990", size = 1460160, upload-time = "2025-09-16T16:38:23.509Z" }, - { url = "https://files.pythonhosted.org/packages/c3/25/42b8662c09f5ca9c81d18d160f48e58842e0fa4c314ea02613c5e5d54542/pymongo-4.15.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ea4415970d2a074d5890696af10e174d84cb735f1fa7673020c7538431e1cb6e", size = 1439284, upload-time = "2025-09-16T16:38:25.248Z" }, - { url = "https://files.pythonhosted.org/packages/b3/bb/46b9d978161828eb91973bd441a3f05f73c789203e976332a8de2832d5db/pymongo-4.15.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:51ee050a2e026e2b224d2ed382830194be20a81c78e1ef98f467e469071df3ac", size = 1407933, upload-time = "2025-09-16T16:38:27.045Z" }, - { url = "https://files.pythonhosted.org/packages/4b/55/bd5af98f675001f4b06f7314b3918e45809424a7ad3510f823f6703cd8f2/pymongo-4.15.1-cp311-cp311-win32.whl", hash = "sha256:9aef07d33839f6429dc24f2ef36e4ec906979cb4f628c57a1c2676cc66625711", size = 844328, upload-time = "2025-09-16T16:38:28.513Z" }, - { url = "https://files.pythonhosted.org/packages/c3/78/90989a290dd458ed43a8a04fa561ac9c7b3391f395cdacd42e21f0f22ce4/pymongo-4.15.1-cp311-cp311-win_amd64.whl", hash = "sha256:8ea6e5ff4d6747e7b64966629a964db3089e9c1e0206d8f9cc8720c90f5a7af1", size = 858951, upload-time = "2025-09-16T16:38:30.074Z" }, - { url = "https://files.pythonhosted.org/packages/de/bb/d4d23f06e166cd773f2324cff73841a62d78a1ad16fb799cf7c5490ce32c/pymongo-4.15.1-cp311-cp311-win_arm64.whl", hash = "sha256:bb783d9001b464a6ef3ee76c30ebbb6f977caee7bbc3a9bb1bd2ff596e818c46", size = 848290, upload-time = "2025-09-16T16:38:31.741Z" }, - { url = "https://files.pythonhosted.org/packages/7e/31/bc4525312083706a59fffe6e8de868054472308230fdee8db0c452c2b831/pymongo-4.15.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:bab357c5ff36ba2340dfc94f3338ef399032089d35c3d257ce0c48630b7848b2", size = 920261, upload-time = "2025-09-16T16:38:33.614Z" }, - { url = "https://files.pythonhosted.org/packages/ae/55/4d99aec625494f21151b8b31e12e06b8ccd3b9dcff609b0dd1acf9bbbc0e/pymongo-4.15.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:46d1af3eb2c274f07815372b5a68f99ecd48750e8ab54d5c3ff36a280fb41c8e", size = 919956, upload-time = "2025-09-16T16:38:35.121Z" }, - { url = "https://files.pythonhosted.org/packages/be/60/8f1afa41521df950e13f6490ecdef48155fc63b78f926e7649045e07afd1/pymongo-4.15.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7dc31357379318881186213dc5fc49b62601c955504f65c8e72032b5048950a1", size = 1698596, upload-time = "2025-09-16T16:38:36.586Z" }, - { url = "https://files.pythonhosted.org/packages/bc/3f/e48d50ee8d6aa0a4cda7889dd73076ec2ab79a232716a5eb0b9df070ffcf/pymongo-4.15.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:12140d29da1ecbaefee2a9e65433ef15d6c2c38f97bc6dab0ff246a96f9d20cd", size = 1762833, upload-time = "2025-09-16T16:38:38.09Z" }, - { url = "https://files.pythonhosted.org/packages/63/87/db976859efc617f608754e051e1468459d9a818fe1ad5d0862e8af57720b/pymongo-4.15.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cf193d2dcd91fa1d1dfa1fd036a3b54f792915a4842d323c0548d23d30461b59", size = 1731875, upload-time = "2025-09-16T16:38:39.742Z" }, - { url = "https://files.pythonhosted.org/packages/18/59/3643ad52a5064ad3ef8c32910de6da28eb658234c25f2db5366f16bffbfb/pymongo-4.15.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a2c0bdcf4d57e4861ed323ba430b585ad98c010a83e46cb8aa3b29c248a82be1", size = 1701853, upload-time = "2025-09-16T16:38:41.333Z" }, - { url = "https://files.pythonhosted.org/packages/d8/96/441c190823f855fc6445ea574b39dca41156acf723c5e6a69ee718421700/pymongo-4.15.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:43fcfc19446e0706bbfe86f683a477d1e699b02369dd9c114ec17c7182d1fe2b", size = 1660978, upload-time = "2025-09-16T16:38:42.877Z" }, - { url = "https://files.pythonhosted.org/packages/47/49/bd7e783fb78aaf9bdaa3f88cc238449be5bc5546e930ec98845ef235f809/pymongo-4.15.1-cp312-cp312-win32.whl", hash = "sha256:e5fedea0e7b3747da836cd5f88b0fa3e2ec5a394371f9b6a6b15927cfeb5455d", size = 891175, upload-time = "2025-09-16T16:38:44.658Z" }, - { url = "https://files.pythonhosted.org/packages/2e/28/7de5858bdeaa07ea4b277f9eb06123ea358003659fe55e72e4e7c898b321/pymongo-4.15.1-cp312-cp312-win_amd64.whl", hash = "sha256:330a17c1c89e2c3bf03ed391108f928d5881298c17692199d3e0cdf097a20082", size = 910619, upload-time = "2025-09-16T16:38:46.124Z" }, - { url = "https://files.pythonhosted.org/packages/17/87/c39f4f8415e7c65f8b66413f53a9272211ff7dfe78a5128b27027bf88864/pymongo-4.15.1-cp312-cp312-win_arm64.whl", hash = "sha256:756b7a2a80ec3dd5b89cd62e9d13c573afd456452a53d05663e8ad0c5ff6632b", size = 896229, upload-time = "2025-09-16T16:38:48.563Z" }, + { url = "https://files.pythonhosted.org/packages/65/01/e8358c562ce4e3255035f16eed43a6f77669b0779f3aa918362c670e869f/pymongo-4.18.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:257519b59d8509a950b21c4a626e5295b3ad4397c12ec47e6e93353c49236145", size = 818562, upload-time = "2026-09-24T16:58:20.602Z" }, + { url = "https://files.pythonhosted.org/packages/1f/18/48a9e7f44a35560284b36bba24622a959ec1dd940e7c4f7cd308cfdba756/pymongo-4.18.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:845b890622e7a2f031140f9cb1c0f266c1baa876ea359f7e51d0c38112cb3c6e", size = 819081, upload-time = "2026-09-24T16:58:22.043Z" }, + { url = "https://files.pythonhosted.org/packages/fc/24/2d9c0bb5649f262257f507b03f84419ac1254e94894cd493bda87fb935e1/pymongo-4.18.2-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:a746f7e183c6d48aba6152d9f333e3bae2e8b6b00fac7abe69d4bdb3d10915c8", size = 1023284, upload-time = "2026-09-24T16:58:23.437Z" }, + { url = "https://files.pythonhosted.org/packages/70/40/af888435ce8e3ae1e1a201b87fb397aa91e3ce115bb1bb3999197f2b9b72/pymongo-4.18.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bf9d13d812e55d406080b8ec3d5b9ef8eb2279bac32529e4f7b41cfefd5340e2", size = 1031549, upload-time = "2026-09-24T16:58:24.869Z" }, + { url = "https://files.pythonhosted.org/packages/3c/66/f5053862cbeddf37b3184870fa179a3e9d9e27e668c720bb7ab28b65b753/pymongo-4.18.2-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:930e8bcdf781a73d3df05976f5eaca1193102571cb91726e30009d64878ef8ce", size = 1054025, upload-time = "2026-09-24T16:58:26.229Z" }, + { url = "https://files.pythonhosted.org/packages/70/29/4d7c9d7cb718ea7bbac56465de3aea5f79d1266683c16bdcd40e77746530/pymongo-4.18.2-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a695bd34d1e942eca93a186e679be196e288370f1a203e383b2187c06349e348", size = 1046337, upload-time = "2026-09-24T16:58:27.822Z" }, + { url = "https://files.pythonhosted.org/packages/d8/aa/c18c564883c5b2616744d1ee7ab073a3b4045aaa4bbbd524105448d4f630/pymongo-4.18.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d95e339e94b82950bfd62ec9ff8fea9bc8dfd9db0940e9134d1647d366560df5", size = 1032845, upload-time = "2026-09-24T16:58:29.518Z" }, + { url = "https://files.pythonhosted.org/packages/20/16/f8a1cb7ac3f1c2c45649f18b7cb398779b4b45e1954bbfcb9317c74c744d/pymongo-4.18.2-cp311-cp311-win32.whl", hash = "sha256:2e2a3b816270c493f6645a168150ecbfb19a0bd0d9d3b56be728a847954da717", size = 814203, upload-time = "2026-09-24T16:58:30.924Z" }, + { url = "https://files.pythonhosted.org/packages/04/53/e9d6350ea753b7a72d0346fb2c8e0c589e0eb443b5041989f3b4ea52ce55/pymongo-4.18.2-cp311-cp311-win_amd64.whl", hash = "sha256:812b4ad5fe577f829cdb4e39a5601025b9cd7f2e3f8cd373cd5ab64ce78b0fb0", size = 821265, upload-time = "2026-09-24T16:58:32.479Z" }, + { url = "https://files.pythonhosted.org/packages/7b/2a/be9693eca04bb6f53f3bdb027220a210bd429c7bf902b79538471849297e/pymongo-4.18.2-cp311-cp311-win_arm64.whl", hash = "sha256:c36cadbc3a7712575900c6f50cdf24ebe2b376b6ad643cb5df1b3dce93447eae", size = 815444, upload-time = "2026-09-24T16:58:34.212Z" }, + { url = "https://files.pythonhosted.org/packages/10/eb/995ec978741cfc56a29223ec0806976218b95d79a516d9b719c984ead130/pymongo-4.18.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:78a3b324407ba7228a57f207e175efc40341c1312d1e7df2d4ad8141ff6d8e00", size = 819057, upload-time = "2026-09-24T16:58:35.844Z" }, + { url = "https://files.pythonhosted.org/packages/40/e4/5a166bbac7f6e620cfb75b0640e793dd3ce69423a1f89cf6a0bfc588c107/pymongo-4.18.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:498187b5171d5efa87291b30e4cbf1e9849929c02bc436e95a3d01ff19e49f42", size = 819448, upload-time = "2026-09-24T16:58:37.221Z" }, + { url = "https://files.pythonhosted.org/packages/c9/c8/82194d9a1387a56cac996901f92ea6a25ae817622ee32d15c986638b263f/pymongo-4.18.2-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:2c003646d3fa5db4e835483fc29018eb06d8c637cf69f676ffbb3f0734005220", size = 1039602, upload-time = "2026-09-24T16:58:38.821Z" }, + { url = "https://files.pythonhosted.org/packages/cc/06/628f93f15f269d8a1ea20a024083e67871f722876a33992aec5525a6c6c4/pymongo-4.18.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c743347143298252948e038f768ea6f71b71ef8a85b7cf0dc13b734e072f74da", size = 1050746, upload-time = "2026-09-24T16:58:40.322Z" }, + { url = "https://files.pythonhosted.org/packages/bf/93/58ea76803128651ec9c732b59b8c1eb73d511c085c2e5786da3289d4015c/pymongo-4.18.2-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:09e6afa68001433c42925a258b57f2ceccb95c73fe8920452e147a82d90c1ae9", size = 1075681, upload-time = "2026-09-24T16:58:41.722Z" }, + { url = "https://files.pythonhosted.org/packages/7c/6d/702ee4593420e2a8ceb169ece6394f0bb9fc2e8e8d8101e99f2468ed6d0d/pymongo-4.18.2-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8d3445caeeb47dc64bf416dc03e0b3bfcf3b60a109dec355a563aa62c135cb33", size = 1068235, upload-time = "2026-09-24T16:58:43.228Z" }, + { url = "https://files.pythonhosted.org/packages/a9/92/a02a4108ca4faf05bca394d65dcebc7b84b0248cff23aa89927c81a0d5dd/pymongo-4.18.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecde9e735e1f86559e0a71a5bbec671e59a35549bfd8c60968d9f032b05013de", size = 1049896, upload-time = "2026-09-24T16:58:44.914Z" }, + { url = "https://files.pythonhosted.org/packages/6e/93/65ef17eecdd86c153f7907b2d490d85eb655d3a5eb35a8494e7e09376353/pymongo-4.18.2-cp312-cp312-win32.whl", hash = "sha256:a463c3fc072655b7fe9e9ded4601876bef7f1df98f6f3faeed07fa7f11309fff", size = 815124, upload-time = "2026-09-24T16:58:46.39Z" }, + { url = "https://files.pythonhosted.org/packages/3c/cc/fd66aa9d8956e8269ea63b2977a7fd38fbec45795d38b8067bfafc98992f/pymongo-4.18.2-cp312-cp312-win_amd64.whl", hash = "sha256:cbb67901ba9b6334fa94643d66587a30ffe5be05061af287364e6b5f7d239dc5", size = 821996, upload-time = "2026-09-24T16:58:47.769Z" }, + { url = "https://files.pythonhosted.org/packages/06/21/983d340cfe226c987014f1caf9568b176cbd7f25dd87536379a609e6371e/pymongo-4.18.2-cp312-cp312-win_arm64.whl", hash = "sha256:94c5e0fe9af4c05f382ae7c4addd0255a8ddcc91b3d2a6bc09b34230cb28290b", size = 815707, upload-time = "2026-09-24T16:58:49.493Z" }, ] [[package]] diff --git a/prowler/changelog.d/pymongo-4-18-2.security.md b/prowler/changelog.d/pymongo-4-18-2.security.md new file mode 100644 index 0000000000..b621313d25 --- /dev/null +++ b/prowler/changelog.d/pymongo-4-18-2.security.md @@ -0,0 +1 @@ +`pymongo` from 4.15.1 to 4.18.2, patching CVE-2026-96748 and CVE-2026-96749 diff --git a/pyproject.toml b/pyproject.toml index 44ae790f2b..a2173c3ac7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -398,7 +398,13 @@ constraint-dependencies = [ "zipp==3.23.1", "zstd==1.5.7.2" ] -override-dependencies = ["okta==3.4.2"] +override-dependencies = [ + "okta==3.4.2", + # huaweicloudsdkcore pins pymongo==4.15.1 exactly (still so in 3.1.216) and only uses + # its bson types. The container installs from this lock, so the override reaches the + # image; a pip install of the wheel still resolves 4.15.1 through the huawei pin. + "pymongo==4.18.2" +] [tool.vulture] # Suppress known false positives. The CI command only passes --exclude and diff --git a/uv.lock b/uv.lock index 88a4522da7..8643a960c3 100644 --- a/uv.lock +++ b/uv.lock @@ -230,7 +230,10 @@ constraints = [ { name = "zipp", specifier = "==3.23.1" }, { name = "zstd", specifier = "==1.5.7.2" }, ] -overrides = [{ name = "okta", specifier = "==3.4.2" }] +overrides = [ + { name = "okta", specifier = "==3.4.2" }, + { name = "pymongo", specifier = "==4.18.2" }, +] [[package]] name = "about-time" @@ -1986,7 +1989,7 @@ name = "exceptiongroup" version = "1.3.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "typing-extensions", marker = "python_full_version < '3.11'" }, + { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } wheels = [ @@ -4332,63 +4335,53 @@ wheels = [ [[package]] name = "pymongo" -version = "4.15.1" +version = "4.18.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "dnspython" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/22/f5/c0c6732fbd358b75a07e17d7e588fd23d481b9812ca96ceeff90bbf879fc/pymongo-4.15.1.tar.gz", hash = "sha256:b9f379a4333dc3779a6bf7adfd077d4387404ed1561472743486a9c58286f705", size = 2470613, upload-time = "2025-09-16T16:39:47.24Z" } +sdist = { url = "https://files.pythonhosted.org/packages/dc/04/455039b475bb8afa5af9b8c363fb773a45d898016f04ee40c1c75003a3f9/pymongo-4.18.2.tar.gz", hash = "sha256:1bd4321cc63c06954953eae9eb8fb5d9075296b96c0e3ed34ccb91a0dcce31a5", size = 2746691, upload-time = "2026-09-24T16:59:59.472Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/29/19/2de6086e3974f3a95a1fc41fd082bc4a58dc9b70268cbfd7c84067d184f2/pymongo-4.15.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:97ccf8222abd5b79daa29811f64ef8b6bb678b9c9a1c1a2cfa0a277f89facd1d", size = 811020, upload-time = "2025-09-16T16:37:57.329Z" }, - { url = "https://files.pythonhosted.org/packages/a2/a4/a340dde32818dd5c95b1c373bc4a27cef5863009faa328388ddc899527fe/pymongo-4.15.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f130b3d7540749a8788a254ceb199a03ede4ee080061bfa5e20e28237c87f2d7", size = 811313, upload-time = "2025-09-16T16:37:59.312Z" }, - { url = "https://files.pythonhosted.org/packages/e2/d9/7d64fdc9e87ec38bd36395bc730848ef56e1cd4bd29ab065d53c27559ace/pymongo-4.15.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fbe6a044a306ed974bd1788f3ceffc2f5e13f81fdb786a28c948c047f4cea38", size = 1188666, upload-time = "2025-09-16T16:38:00.896Z" }, - { url = "https://files.pythonhosted.org/packages/ef/d9/47cc69d3b22c9d971b1486e3a80d6a5d0bbf2dec6c9c4d5e39a129ee8125/pymongo-4.15.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1b96768741e0e03451ef7b07c4857490cc43999e01c7f8da704fe00b3fe5d4d3", size = 1222891, upload-time = "2025-09-16T16:38:02.574Z" }, - { url = "https://files.pythonhosted.org/packages/a9/73/a57594c956bf276069a438056330a346871b2f5e3cae4e3bcc257cffc788/pymongo-4.15.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d50b18ad6e4a55a75c30f0e669bd15ed1ceb18f9994d6835b4f5d5218592b4a0", size = 1205824, upload-time = "2025-09-16T16:38:04.277Z" }, - { url = "https://files.pythonhosted.org/packages/37/d5/1ae77ddcc376ebce0139614d51ec1fd0ba666d7cc1f198ec88272cfdac36/pymongo-4.15.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3e8e2a33613b2880d516d9c8616b64d27957c488de2f8e591945cf12094336a5", size = 1191838, upload-time = "2025-09-16T16:38:05.728Z" }, - { url = "https://files.pythonhosted.org/packages/35/07/ae3fc20a809066b35bbf470bda79d34a72948603d9f29a425bf1d0ef2cb7/pymongo-4.15.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7a2a439395f3d4c9d3dc33ba4575d52b6dd285d57db54e32062ae8ef557cab10", size = 1170996, upload-time = "2025-09-16T16:38:09.084Z" }, - { url = "https://files.pythonhosted.org/packages/2c/0f/eb654cea7586588704151ac4894cd3fb2582c0db458cd615cad1c7fe4c59/pymongo-4.15.1-cp310-cp310-win32.whl", hash = "sha256:142abf2fbd4667a3c8f4ce2e30fdbd287c015f52a838f4845d7476a45340208d", size = 798249, upload-time = "2025-09-16T16:38:11.11Z" }, - { url = "https://files.pythonhosted.org/packages/9f/6b/38184382c32695f914a5474d8de0c9f3714b7d8f4c66f090b3836d70273d/pymongo-4.15.1-cp310-cp310-win_amd64.whl", hash = "sha256:8baf46384c97f774bc84178662e1fc6e32a2755fbc8e259f424780c2a11a3566", size = 807990, upload-time = "2025-09-16T16:38:12.525Z" }, - { url = "https://files.pythonhosted.org/packages/38/eb/77a4d37b2a0673c010dd97b9911438f17bb05f407235cc9f02074175855d/pymongo-4.15.1-cp310-cp310-win_arm64.whl", hash = "sha256:b5b837df8e414e2a173722395107da981d178ba7e648f612fa49b7ab4e240852", size = 800875, upload-time = "2025-09-16T16:38:14.532Z" }, - { url = "https://files.pythonhosted.org/packages/c9/da/89066930a70b4299844f1155fc23baaa7e30e77c8a0cbf62a2ae06ee34a5/pymongo-4.15.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:363445cc0e899b9e55ac9904a868c8a16a6c81f71c48dbadfd78c98e0b54de27", size = 865410, upload-time = "2025-09-16T16:38:16.279Z" }, - { url = "https://files.pythonhosted.org/packages/99/8f/a1d0402d52e5ebd14283718abefdc0c16f308cf10bee56cdff04b1f5119b/pymongo-4.15.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:da0a13f345f4b101776dbab92cec66f0b75015df0b007b47bd73bfd0305cc56a", size = 865695, upload-time = "2025-09-16T16:38:18.015Z" }, - { url = "https://files.pythonhosted.org/packages/53/38/d1ef69028923f86fd00638d9eb16400d4e60a89eabd2011fe631fd3186cf/pymongo-4.15.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9481a492851e432122a83755d4e69c06aeb087bbf8370bac9f96d112ac1303fd", size = 1434758, upload-time = "2025-09-16T16:38:20.141Z" }, - { url = "https://files.pythonhosted.org/packages/b0/eb/a8d5dff748a2dd333610b2e4c8120b623e38ea2b5e30ad190d0ce2803840/pymongo-4.15.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:625dec3e9cd7c3d336285a20728c01bfc56d37230a99ec537a6a8625af783a43", size = 1485716, upload-time = "2025-09-16T16:38:21.607Z" }, - { url = "https://files.pythonhosted.org/packages/c4/d4/17ba457a828b733182ddc01a202872fef3006eed6b54450b20dc95a2f77d/pymongo-4.15.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:26a31af455bffcc64537a7f67e2f84833a57855a82d05a085a1030c471138990", size = 1460160, upload-time = "2025-09-16T16:38:23.509Z" }, - { url = "https://files.pythonhosted.org/packages/c3/25/42b8662c09f5ca9c81d18d160f48e58842e0fa4c314ea02613c5e5d54542/pymongo-4.15.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ea4415970d2a074d5890696af10e174d84cb735f1fa7673020c7538431e1cb6e", size = 1439284, upload-time = "2025-09-16T16:38:25.248Z" }, - { url = "https://files.pythonhosted.org/packages/b3/bb/46b9d978161828eb91973bd441a3f05f73c789203e976332a8de2832d5db/pymongo-4.15.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:51ee050a2e026e2b224d2ed382830194be20a81c78e1ef98f467e469071df3ac", size = 1407933, upload-time = "2025-09-16T16:38:27.045Z" }, - { url = "https://files.pythonhosted.org/packages/4b/55/bd5af98f675001f4b06f7314b3918e45809424a7ad3510f823f6703cd8f2/pymongo-4.15.1-cp311-cp311-win32.whl", hash = "sha256:9aef07d33839f6429dc24f2ef36e4ec906979cb4f628c57a1c2676cc66625711", size = 844328, upload-time = "2025-09-16T16:38:28.513Z" }, - { url = "https://files.pythonhosted.org/packages/c3/78/90989a290dd458ed43a8a04fa561ac9c7b3391f395cdacd42e21f0f22ce4/pymongo-4.15.1-cp311-cp311-win_amd64.whl", hash = "sha256:8ea6e5ff4d6747e7b64966629a964db3089e9c1e0206d8f9cc8720c90f5a7af1", size = 858951, upload-time = "2025-09-16T16:38:30.074Z" }, - { url = "https://files.pythonhosted.org/packages/de/bb/d4d23f06e166cd773f2324cff73841a62d78a1ad16fb799cf7c5490ce32c/pymongo-4.15.1-cp311-cp311-win_arm64.whl", hash = "sha256:bb783d9001b464a6ef3ee76c30ebbb6f977caee7bbc3a9bb1bd2ff596e818c46", size = 848290, upload-time = "2025-09-16T16:38:31.741Z" }, - { url = "https://files.pythonhosted.org/packages/7e/31/bc4525312083706a59fffe6e8de868054472308230fdee8db0c452c2b831/pymongo-4.15.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:bab357c5ff36ba2340dfc94f3338ef399032089d35c3d257ce0c48630b7848b2", size = 920261, upload-time = "2025-09-16T16:38:33.614Z" }, - { url = "https://files.pythonhosted.org/packages/ae/55/4d99aec625494f21151b8b31e12e06b8ccd3b9dcff609b0dd1acf9bbbc0e/pymongo-4.15.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:46d1af3eb2c274f07815372b5a68f99ecd48750e8ab54d5c3ff36a280fb41c8e", size = 919956, upload-time = "2025-09-16T16:38:35.121Z" }, - { url = "https://files.pythonhosted.org/packages/be/60/8f1afa41521df950e13f6490ecdef48155fc63b78f926e7649045e07afd1/pymongo-4.15.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7dc31357379318881186213dc5fc49b62601c955504f65c8e72032b5048950a1", size = 1698596, upload-time = "2025-09-16T16:38:36.586Z" }, - { url = "https://files.pythonhosted.org/packages/bc/3f/e48d50ee8d6aa0a4cda7889dd73076ec2ab79a232716a5eb0b9df070ffcf/pymongo-4.15.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:12140d29da1ecbaefee2a9e65433ef15d6c2c38f97bc6dab0ff246a96f9d20cd", size = 1762833, upload-time = "2025-09-16T16:38:38.09Z" }, - { url = "https://files.pythonhosted.org/packages/63/87/db976859efc617f608754e051e1468459d9a818fe1ad5d0862e8af57720b/pymongo-4.15.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cf193d2dcd91fa1d1dfa1fd036a3b54f792915a4842d323c0548d23d30461b59", size = 1731875, upload-time = "2025-09-16T16:38:39.742Z" }, - { url = "https://files.pythonhosted.org/packages/18/59/3643ad52a5064ad3ef8c32910de6da28eb658234c25f2db5366f16bffbfb/pymongo-4.15.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a2c0bdcf4d57e4861ed323ba430b585ad98c010a83e46cb8aa3b29c248a82be1", size = 1701853, upload-time = "2025-09-16T16:38:41.333Z" }, - { url = "https://files.pythonhosted.org/packages/d8/96/441c190823f855fc6445ea574b39dca41156acf723c5e6a69ee718421700/pymongo-4.15.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:43fcfc19446e0706bbfe86f683a477d1e699b02369dd9c114ec17c7182d1fe2b", size = 1660978, upload-time = "2025-09-16T16:38:42.877Z" }, - { url = "https://files.pythonhosted.org/packages/47/49/bd7e783fb78aaf9bdaa3f88cc238449be5bc5546e930ec98845ef235f809/pymongo-4.15.1-cp312-cp312-win32.whl", hash = "sha256:e5fedea0e7b3747da836cd5f88b0fa3e2ec5a394371f9b6a6b15927cfeb5455d", size = 891175, upload-time = "2025-09-16T16:38:44.658Z" }, - { url = "https://files.pythonhosted.org/packages/2e/28/7de5858bdeaa07ea4b277f9eb06123ea358003659fe55e72e4e7c898b321/pymongo-4.15.1-cp312-cp312-win_amd64.whl", hash = "sha256:330a17c1c89e2c3bf03ed391108f928d5881298c17692199d3e0cdf097a20082", size = 910619, upload-time = "2025-09-16T16:38:46.124Z" }, - { url = "https://files.pythonhosted.org/packages/17/87/c39f4f8415e7c65f8b66413f53a9272211ff7dfe78a5128b27027bf88864/pymongo-4.15.1-cp312-cp312-win_arm64.whl", hash = "sha256:756b7a2a80ec3dd5b89cd62e9d13c573afd456452a53d05663e8ad0c5ff6632b", size = 896229, upload-time = "2025-09-16T16:38:48.563Z" }, - { url = "https://files.pythonhosted.org/packages/a6/22/02ac885d8accb4c86ae92e99681a09f3fd310c431843fc850e141b42ab17/pymongo-4.15.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:622957eed757e44d9605c43b576ef90affb61176d9e8be7356c1a2948812cb84", size = 974492, upload-time = "2025-09-16T16:38:50.437Z" }, - { url = "https://files.pythonhosted.org/packages/56/bf/71685b6b2d085dbaadf029b1ea4a1bc7a1bc483452513dea283b47a5f7c0/pymongo-4.15.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c5283dffcf601b793a57bb86819a467473bbb1bf21cd170c0b9648f933f22131", size = 974191, upload-time = "2025-09-16T16:38:52.725Z" }, - { url = "https://files.pythonhosted.org/packages/df/98/141edc92fa97af96b4c691e10a7225ac3e552914e88b7a8d439bd6bc9fcc/pymongo-4.15.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:def51dea1f8e336aed807eb5d2f2a416c5613e97ec64f07479681d05044c217c", size = 1962311, upload-time = "2025-09-16T16:38:54.319Z" }, - { url = "https://files.pythonhosted.org/packages/f8/a9/601b91607af1dec8035b46ba67a5a023c819ccedd40d6f6232e15bf76030/pymongo-4.15.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:24171b2015052b2f0a3f8cbfa38b973fa87f6474e88236a4dfeb735983f9f49e", size = 2039667, upload-time = "2025-09-16T16:38:55.958Z" }, - { url = "https://files.pythonhosted.org/packages/4f/71/02e9a5248e0a9dfc371fd7350f8b11eac03d9eb3662328978f37613d319a/pymongo-4.15.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:64b60ed7220c52f8c78c7af8d2c58f7e415732e21b3ff7e642169efa6e0b11e7", size = 2003579, upload-time = "2025-09-16T16:38:57.576Z" }, - { url = "https://files.pythonhosted.org/packages/f9/d1/b1a9520b33e022ed1c0d2d43e8805ba18d3d686fc9c9d89a507593f6dd86/pymongo-4.15.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58236ce5ba3a79748c1813221b07b411847fd8849ff34c2891ba56f807cce3e5", size = 1964307, upload-time = "2025-09-16T16:38:59.219Z" }, - { url = "https://files.pythonhosted.org/packages/3d/d1/1d205a762020f056c05899a912364c48bac0f3438502b36d057aa1da3ca5/pymongo-4.15.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7461e777b3da96568c1f077b1fbf9e0c15667ac4d8b9a1cf90d80a69fe3be609", size = 1913879, upload-time = "2025-09-16T16:39:01.266Z" }, - { url = "https://files.pythonhosted.org/packages/44/d1/0a3ab2440ea00b6423f33c84e6433022fd51f3561dede9346f54f39cf4dd/pymongo-4.15.1-cp313-cp313-win32.whl", hash = "sha256:45f0a2fb09704ca5e0df08a794076d21cbe5521d3a8ceb8ad6d51cef12f5f4e7", size = 938007, upload-time = "2025-09-16T16:39:03.427Z" }, - { url = "https://files.pythonhosted.org/packages/7b/61/e9ea839af2caadfde91774549a6f72450b72efdc92117995e7117d4b1270/pymongo-4.15.1-cp313-cp313-win_amd64.whl", hash = "sha256:b70201a6dbe19d0d10a886989d3ba4b857ea6ef402a22a61c8ca387b937cc065", size = 962236, upload-time = "2025-09-16T16:39:05.018Z" }, - { url = "https://files.pythonhosted.org/packages/ad/f8/0a92a72993b2e1c110ee532650624ca7ae15c5e45906dbae4f063a2fd32a/pymongo-4.15.1-cp313-cp313-win_arm64.whl", hash = "sha256:6892ebf8b2bc345cacfe1301724195d87162f02d01c417175e9f27d276a2f198", size = 944138, upload-time = "2025-09-16T16:39:07.035Z" }, - { url = "https://files.pythonhosted.org/packages/e5/eb/2ba257482844bb2e3c82c6b266d6e811bc610fa80408133e352cc1afb3c9/pymongo-4.15.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:db439288516514713c8ee09c9baaf66bc4b0188fbe4cd578ef3433ee27699aab", size = 1030987, upload-time = "2025-09-16T16:39:08.914Z" }, - { url = "https://files.pythonhosted.org/packages/0d/86/8c6eab3767251ba77a3604d3b6b0826d0af246bd04b2d16aced3a54f08b0/pymongo-4.15.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:234c80a5f21c8854cc5d6c2f5541ff17dd645b99643587c5e7ed1e21d42003b6", size = 1030996, upload-time = "2025-09-16T16:39:10.429Z" }, - { url = "https://files.pythonhosted.org/packages/5b/26/c1bc0bcb64f39b9891b8b537f21cc37d668edd8b93f47ed930af7f95649c/pymongo-4.15.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b570dc8179dcab980259b885116b14462bcf39170e30d8cbcce6f17f28a2ac5b", size = 2290670, upload-time = "2025-09-16T16:39:12.348Z" }, - { url = "https://files.pythonhosted.org/packages/82/af/f5e8b6c404a3678a99bf0b704f7b19fa14a71edb42d724eb09147aa1d3be/pymongo-4.15.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cb6321bde02308d4d313b487d19bfae62ea4d37749fc2325b1c12388e05e4c31", size = 2377711, upload-time = "2025-09-16T16:39:13.992Z" }, - { url = "https://files.pythonhosted.org/packages/af/f4/63bcc1760bf3e0925cb6cb91b2b3ba756c113b1674a14b41efe7e3738b8d/pymongo-4.15.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cc808588289f693aba80fae8272af4582a7d6edc4e95fb8fbf65fe6f634116ce", size = 2337097, upload-time = "2025-09-16T16:39:15.717Z" }, - { url = "https://files.pythonhosted.org/packages/d0/dc/0cfada0426556b4b04144fb00ce6a1e7535ab49623d4d9dd052d27ea46c0/pymongo-4.15.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:99236fd0e0cf6b048a4370d0df6820963dc94f935ad55a2e29af752272abd6c9", size = 2288295, upload-time = "2025-09-16T16:39:17.385Z" }, - { url = "https://files.pythonhosted.org/packages/5b/a8/081a80f60042d2b8cd6a1c091ecaa186f1ef216b587d06acd0743e1016c6/pymongo-4.15.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2277548bb093424742325b2a88861d913d8990f358fc71fd26004d1b87029bb8", size = 2227616, upload-time = "2025-09-16T16:39:19.025Z" }, - { url = "https://files.pythonhosted.org/packages/56/d0/a6007e0c3c5727391ac5ea40e93a1e7d14146c65ac4ca731c0680962eb48/pymongo-4.15.1-cp313-cp313t-win32.whl", hash = "sha256:754a5d75c33d49691e2b09a4e0dc75959e271a38cbfd92c6b36f7e4eafc4608e", size = 987225, upload-time = "2025-09-16T16:39:20.663Z" }, - { url = "https://files.pythonhosted.org/packages/9d/97/c9bf6dcd647a8cf7abbad5814dfb7d8a16e6ab92a3e56343b3bcb454a6d3/pymongo-4.15.1-cp313-cp313t-win_amd64.whl", hash = "sha256:8d62e68ad21661e536555d0683087a14bf5c74b242a4446c602d16080eb9e293", size = 1017521, upload-time = "2025-09-16T16:39:22.319Z" }, - { url = "https://files.pythonhosted.org/packages/31/ea/102f7c9477302fa05e5303dd504781ac82400e01aab91bfba9c290253bd6/pymongo-4.15.1-cp313-cp313t-win_arm64.whl", hash = "sha256:56bbfb79b51e95f4b1324a5a7665f3629f4d27c18e2002cfaa60c907cc5369d9", size = 992963, upload-time = "2025-09-16T16:39:23.957Z" }, + { url = "https://files.pythonhosted.org/packages/94/19/fe076a243faa8bc8f41d5ecc7c3dd625437bbca6b0dae633e05e8811e6db/pymongo-4.18.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:cb4892f6edd939677e767e45087173c7292cfcfa0ea52fb2076e1fb89fbd597f", size = 819557, upload-time = "2026-09-24T16:58:04.8Z" }, + { url = "https://files.pythonhosted.org/packages/23/ad/be444b903d5e712b659ac83b23643e2df2b61631f46d7ece02521caeb7fc/pymongo-4.18.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9f953e62a054030c7c7f923f65da95cb5332230a135ee24e57af2defad7d8cff", size = 820096, upload-time = "2026-09-24T16:58:06.883Z" }, + { url = "https://files.pythonhosted.org/packages/c9/83/9e6b7364e9234689fc17c0897bbac95b6bb032d8d16d6070c4a617c06cf9/pymongo-4.18.2-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:69de0a25ce088e45c25b4ba3e84ad7f530375b48f6024dda48b1ec34a08b4d4f", size = 1041104, upload-time = "2026-09-24T16:58:08.468Z" }, + { url = "https://files.pythonhosted.org/packages/21/6a/02f792efaed73e56910bbdf3b161c0b85b651807d6a5ab498b60c2637329/pymongo-4.18.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b3fb1f90fcd5aec21535ace955bb72195751c09ea179d751855f7a066a6fb8ab", size = 1049685, upload-time = "2026-09-24T16:58:09.873Z" }, + { url = "https://files.pythonhosted.org/packages/a0/fe/3da97617e4a5c4144429308498507b326332d2271025ca23bbd8fe5b1f99/pymongo-4.18.2-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2b2dff5ef3d523caa47ca06e6832578b463e05e2ffb74d637fe633305680ab9d", size = 1074077, upload-time = "2026-09-24T16:58:11.321Z" }, + { url = "https://files.pythonhosted.org/packages/0c/95/1731d74417f1f298f004fe4e9dae19a326e8e743478a4006afab43682ec5/pymongo-4.18.2-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:078b73765963f716e12a7ef9f2247baef22f01c0a7288281cf8fe18ca0f96635", size = 1067691, upload-time = "2026-09-24T16:58:12.747Z" }, + { url = "https://files.pythonhosted.org/packages/7c/6d/cf29ce0b02f38946e8f6e55d232cbb34b6e346c9b3874427bf2c276b7ccf/pymongo-4.18.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:46d9922c08c0fc166620b5693829a2852a16082f4fc8fb431e30bf6d0a1f4984", size = 1051651, upload-time = "2026-09-24T16:58:14.26Z" }, + { url = "https://files.pythonhosted.org/packages/bb/ca/425a20beb371f3e7d379425bbbf97496632dbcf0d7b5c4fd2f325435f8b3/pymongo-4.18.2-cp310-cp310-win32.whl", hash = "sha256:899bd9012e72eeec7616456f13f9402f96ed4e0ec110974cf9055082b4c2bb78", size = 814456, upload-time = "2026-09-24T16:58:15.897Z" }, + { url = "https://files.pythonhosted.org/packages/e4/d9/4082e0fe625624bd71d53792cc49b466536425cba75d9e56fff6972d1a2d/pymongo-4.18.2-cp310-cp310-win_amd64.whl", hash = "sha256:c80e4baee9746f1239cc5b83bb983c01a04769761adb8f462ce0a1902f7cd8bd", size = 821727, upload-time = "2026-09-24T16:58:17.395Z" }, + { url = "https://files.pythonhosted.org/packages/f8/b3/c6f190cc19b873d9aa9e261ee184ae5a605e1d21d4bd3105aa9d52943f86/pymongo-4.18.2-cp310-cp310-win_arm64.whl", hash = "sha256:045f16e6b87b832b37f3acf487c48308bc6bb171bc8b4a3e97f156cf07de14b4", size = 815866, upload-time = "2026-09-24T16:58:19.258Z" }, + { url = "https://files.pythonhosted.org/packages/65/01/e8358c562ce4e3255035f16eed43a6f77669b0779f3aa918362c670e869f/pymongo-4.18.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:257519b59d8509a950b21c4a626e5295b3ad4397c12ec47e6e93353c49236145", size = 818562, upload-time = "2026-09-24T16:58:20.602Z" }, + { url = "https://files.pythonhosted.org/packages/1f/18/48a9e7f44a35560284b36bba24622a959ec1dd940e7c4f7cd308cfdba756/pymongo-4.18.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:845b890622e7a2f031140f9cb1c0f266c1baa876ea359f7e51d0c38112cb3c6e", size = 819081, upload-time = "2026-09-24T16:58:22.043Z" }, + { url = "https://files.pythonhosted.org/packages/fc/24/2d9c0bb5649f262257f507b03f84419ac1254e94894cd493bda87fb935e1/pymongo-4.18.2-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:a746f7e183c6d48aba6152d9f333e3bae2e8b6b00fac7abe69d4bdb3d10915c8", size = 1023284, upload-time = "2026-09-24T16:58:23.437Z" }, + { url = "https://files.pythonhosted.org/packages/70/40/af888435ce8e3ae1e1a201b87fb397aa91e3ce115bb1bb3999197f2b9b72/pymongo-4.18.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bf9d13d812e55d406080b8ec3d5b9ef8eb2279bac32529e4f7b41cfefd5340e2", size = 1031549, upload-time = "2026-09-24T16:58:24.869Z" }, + { url = "https://files.pythonhosted.org/packages/3c/66/f5053862cbeddf37b3184870fa179a3e9d9e27e668c720bb7ab28b65b753/pymongo-4.18.2-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:930e8bcdf781a73d3df05976f5eaca1193102571cb91726e30009d64878ef8ce", size = 1054025, upload-time = "2026-09-24T16:58:26.229Z" }, + { url = "https://files.pythonhosted.org/packages/70/29/4d7c9d7cb718ea7bbac56465de3aea5f79d1266683c16bdcd40e77746530/pymongo-4.18.2-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a695bd34d1e942eca93a186e679be196e288370f1a203e383b2187c06349e348", size = 1046337, upload-time = "2026-09-24T16:58:27.822Z" }, + { url = "https://files.pythonhosted.org/packages/d8/aa/c18c564883c5b2616744d1ee7ab073a3b4045aaa4bbbd524105448d4f630/pymongo-4.18.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d95e339e94b82950bfd62ec9ff8fea9bc8dfd9db0940e9134d1647d366560df5", size = 1032845, upload-time = "2026-09-24T16:58:29.518Z" }, + { url = "https://files.pythonhosted.org/packages/20/16/f8a1cb7ac3f1c2c45649f18b7cb398779b4b45e1954bbfcb9317c74c744d/pymongo-4.18.2-cp311-cp311-win32.whl", hash = "sha256:2e2a3b816270c493f6645a168150ecbfb19a0bd0d9d3b56be728a847954da717", size = 814203, upload-time = "2026-09-24T16:58:30.924Z" }, + { url = "https://files.pythonhosted.org/packages/04/53/e9d6350ea753b7a72d0346fb2c8e0c589e0eb443b5041989f3b4ea52ce55/pymongo-4.18.2-cp311-cp311-win_amd64.whl", hash = "sha256:812b4ad5fe577f829cdb4e39a5601025b9cd7f2e3f8cd373cd5ab64ce78b0fb0", size = 821265, upload-time = "2026-09-24T16:58:32.479Z" }, + { url = "https://files.pythonhosted.org/packages/7b/2a/be9693eca04bb6f53f3bdb027220a210bd429c7bf902b79538471849297e/pymongo-4.18.2-cp311-cp311-win_arm64.whl", hash = "sha256:c36cadbc3a7712575900c6f50cdf24ebe2b376b6ad643cb5df1b3dce93447eae", size = 815444, upload-time = "2026-09-24T16:58:34.212Z" }, + { url = "https://files.pythonhosted.org/packages/10/eb/995ec978741cfc56a29223ec0806976218b95d79a516d9b719c984ead130/pymongo-4.18.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:78a3b324407ba7228a57f207e175efc40341c1312d1e7df2d4ad8141ff6d8e00", size = 819057, upload-time = "2026-09-24T16:58:35.844Z" }, + { url = "https://files.pythonhosted.org/packages/40/e4/5a166bbac7f6e620cfb75b0640e793dd3ce69423a1f89cf6a0bfc588c107/pymongo-4.18.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:498187b5171d5efa87291b30e4cbf1e9849929c02bc436e95a3d01ff19e49f42", size = 819448, upload-time = "2026-09-24T16:58:37.221Z" }, + { url = "https://files.pythonhosted.org/packages/c9/c8/82194d9a1387a56cac996901f92ea6a25ae817622ee32d15c986638b263f/pymongo-4.18.2-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:2c003646d3fa5db4e835483fc29018eb06d8c637cf69f676ffbb3f0734005220", size = 1039602, upload-time = "2026-09-24T16:58:38.821Z" }, + { url = "https://files.pythonhosted.org/packages/cc/06/628f93f15f269d8a1ea20a024083e67871f722876a33992aec5525a6c6c4/pymongo-4.18.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c743347143298252948e038f768ea6f71b71ef8a85b7cf0dc13b734e072f74da", size = 1050746, upload-time = "2026-09-24T16:58:40.322Z" }, + { url = "https://files.pythonhosted.org/packages/bf/93/58ea76803128651ec9c732b59b8c1eb73d511c085c2e5786da3289d4015c/pymongo-4.18.2-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:09e6afa68001433c42925a258b57f2ceccb95c73fe8920452e147a82d90c1ae9", size = 1075681, upload-time = "2026-09-24T16:58:41.722Z" }, + { url = "https://files.pythonhosted.org/packages/7c/6d/702ee4593420e2a8ceb169ece6394f0bb9fc2e8e8d8101e99f2468ed6d0d/pymongo-4.18.2-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8d3445caeeb47dc64bf416dc03e0b3bfcf3b60a109dec355a563aa62c135cb33", size = 1068235, upload-time = "2026-09-24T16:58:43.228Z" }, + { url = "https://files.pythonhosted.org/packages/a9/92/a02a4108ca4faf05bca394d65dcebc7b84b0248cff23aa89927c81a0d5dd/pymongo-4.18.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecde9e735e1f86559e0a71a5bbec671e59a35549bfd8c60968d9f032b05013de", size = 1049896, upload-time = "2026-09-24T16:58:44.914Z" }, + { url = "https://files.pythonhosted.org/packages/6e/93/65ef17eecdd86c153f7907b2d490d85eb655d3a5eb35a8494e7e09376353/pymongo-4.18.2-cp312-cp312-win32.whl", hash = "sha256:a463c3fc072655b7fe9e9ded4601876bef7f1df98f6f3faeed07fa7f11309fff", size = 815124, upload-time = "2026-09-24T16:58:46.39Z" }, + { url = "https://files.pythonhosted.org/packages/3c/cc/fd66aa9d8956e8269ea63b2977a7fd38fbec45795d38b8067bfafc98992f/pymongo-4.18.2-cp312-cp312-win_amd64.whl", hash = "sha256:cbb67901ba9b6334fa94643d66587a30ffe5be05061af287364e6b5f7d239dc5", size = 821996, upload-time = "2026-09-24T16:58:47.769Z" }, + { url = "https://files.pythonhosted.org/packages/06/21/983d340cfe226c987014f1caf9568b176cbd7f25dd87536379a609e6371e/pymongo-4.18.2-cp312-cp312-win_arm64.whl", hash = "sha256:94c5e0fe9af4c05f382ae7c4addd0255a8ddcc91b3d2a6bc09b34230cb28290b", size = 815707, upload-time = "2026-09-24T16:58:49.493Z" }, + { url = "https://files.pythonhosted.org/packages/c7/be/82ca9a311b8a89e918c7387d96135786e1edd2eedca43d62dffcb3af5846/pymongo-4.18.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3be3633a89888fc7443a5a416f74c66216839a5b7b550daaec4da0b1a7283be", size = 818977, upload-time = "2026-09-24T16:58:51.109Z" }, + { url = "https://files.pythonhosted.org/packages/43/38/4817e5ce653ed70162d1412cc74d15c29e88208cea7510967707e908a145/pymongo-4.18.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bd5ecb6d6fab1c5114e66477b1d60b0f3926975c41cf5508d38b28dd0eb6676e", size = 819319, upload-time = "2026-09-24T16:58:52.703Z" }, + { url = "https://files.pythonhosted.org/packages/f3/e9/b8f108ce7c47619e531e0f4a0963178eb9caa5c5da36f4454df8ae6fd15b/pymongo-4.18.2-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:63322fa1499d855a6e63673f8d183fc2d7ecd966702f73150b21e04e01dc0144", size = 1039779, upload-time = "2026-09-24T16:58:54.292Z" }, + { url = "https://files.pythonhosted.org/packages/e5/87/3a8394386d485c9c9e518c4e15dce91e9470c3a8b3a0a19e289eb3b9d262/pymongo-4.18.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:979d1e88a20fe9d652f3db57fff55052ddb9022ac346ea55a32a8cfe4b56f90d", size = 1050543, upload-time = "2026-09-24T16:58:55.907Z" }, + { url = "https://files.pythonhosted.org/packages/68/7d/84962f4d8fe0f9e3f112c6b630c7ad9a9f74acd7865ace9ba1e82e1f096a/pymongo-4.18.2-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ef9007562b907a0db6b158ae9e08510678991cc10ad1471de3635f9be737a68e", size = 1075500, upload-time = "2026-09-24T16:58:57.424Z" }, + { url = "https://files.pythonhosted.org/packages/7b/4e/b20ffbc001958d597970567e9231664b8f4c68765376a349834577892fe3/pymongo-4.18.2-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8b41d460c2281874232e6b4a5834bc3fb6cc718d80b6097c38d87ff19ee60898", size = 1067941, upload-time = "2026-09-24T16:58:59.014Z" }, + { url = "https://files.pythonhosted.org/packages/b4/ba/535911b6b35427e3fd31c17a89c9851c024df81779fe6dd473a4f0cdd0fd/pymongo-4.18.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6a57f28271cb325574bd12ad7b3747e545c5b7f560de9f175af7db7be0655639", size = 1049780, upload-time = "2026-09-24T16:59:00.716Z" }, + { url = "https://files.pythonhosted.org/packages/fb/d2/f37bdbbc1a11955dd2609d622aae6722c6cfd9f67528a62c8f80f4db6609/pymongo-4.18.2-cp313-cp313-win32.whl", hash = "sha256:c3f0adbefa29c772f4ebf09a6ac65426405435de199828bedc9534f182ad2fe3", size = 815166, upload-time = "2026-09-24T16:59:02.434Z" }, + { url = "https://files.pythonhosted.org/packages/5a/8a/dc8dac16d8a1c72008a1707046db333a4adaf3dec712c4433ac234e7890d/pymongo-4.18.2-cp313-cp313-win_amd64.whl", hash = "sha256:84f99563560a94321b7c3eb4a3bd0dc81f29a2f67a1da065f78641eff42f4a81", size = 821994, upload-time = "2026-09-24T16:59:04.127Z" }, + { url = "https://files.pythonhosted.org/packages/5c/c9/979b68292e3229a924280ba0c8d5fe4f92604271aa49ba09e497af4aa463/pymongo-4.18.2-cp313-cp313-win_arm64.whl", hash = "sha256:f319857f858a17e1c0e48159b65c89abe93d5ebf3423bb3e3891d8b67a68accd", size = 815730, upload-time = "2026-09-24T16:59:05.629Z" }, ] [[package]] From d38e04dd3efda3b7cfbd69e98f760e66b5eea80a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9sar=20Arroba?= <19954079+cesararroba@users.noreply.github.com> Date: Tue, 6 Oct 2026 10:42:52 +0200 Subject: [PATCH 05/17] chore(github): move code ownership from platform to engineering (#12958) --- .github/CODEOWNERS | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index dc1e714b47..a1685c0ba2 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -19,10 +19,10 @@ # AI /mcp_server/ @prowler-cloud/engineering -# Platform -/.github/ @prowler-cloud/platform -/Makefile @prowler-cloud/platform -/kubernetes/ @prowler-cloud/platform -**/Dockerfile* @prowler-cloud/platform -**/docker-compose*.yml @prowler-cloud/platform -**/docker-compose*.yaml @prowler-cloud/platform +# CI and infrastructure +/.github/ @prowler-cloud/engineering +/Makefile @prowler-cloud/engineering +/kubernetes/ @prowler-cloud/engineering +**/Dockerfile* @prowler-cloud/engineering +**/docker-compose*.yml @prowler-cloud/engineering +**/docker-compose*.yaml @prowler-cloud/engineering From 3f893176bac01084b39bdc964fa4a327ec4e3fab Mon Sep 17 00:00:00 2001 From: StylusFrost <43682773+StylusFrost@users.noreply.github.com> Date: Wed, 7 Oct 2026 11:48:19 +0200 Subject: [PATCH 06/17] fix(ui): bump sharp to 0.35.5 to patch the librsvg advisory (#12968) --- .../sharp-librsvg-vulnerability.security.md | 1 + ui/dependency-log.json | 6 +- ui/package.json | 2 +- ui/pnpm-lock.yaml | 244 +++++++++--------- ui/pnpm-workspace.yaml | 7 +- 5 files changed, 131 insertions(+), 129 deletions(-) create mode 100644 ui/changelog.d/sharp-librsvg-vulnerability.security.md diff --git a/ui/changelog.d/sharp-librsvg-vulnerability.security.md b/ui/changelog.d/sharp-librsvg-vulnerability.security.md new file mode 100644 index 0000000000..4fe262e6cd --- /dev/null +++ b/ui/changelog.d/sharp-librsvg-vulnerability.security.md @@ -0,0 +1 @@ +`sharp` to 0.35.5, patching a librsvg memory vulnerability that can lead to remote code execution on glibc-based Linux (GHSA-wq5f-xc86-pv6w) diff --git a/ui/dependency-log.json b/ui/dependency-log.json index 15052e2374..b0cdfaa403 100644 --- a/ui/dependency-log.json +++ b/ui/dependency-log.json @@ -594,10 +594,10 @@ { "section": "dependencies", "name": "sharp", - "from": "0.35.3", - "to": "0.35.4", + "from": "0.35.4", + "to": "0.35.5", "strategy": "installed", - "generatedAt": "2026-09-09T12:39:08.649Z" + "generatedAt": "2026-10-07T09:34:53.599Z" }, { "section": "dependencies", diff --git a/ui/package.json b/ui/package.json index 6b8d50471e..a98088d11a 100644 --- a/ui/package.json +++ b/ui/package.json @@ -110,7 +110,7 @@ "recharts": "2.15.4", "require-in-the-middle": "8.0.1", "server-only": "0.0.1", - "sharp": "0.35.4", + "sharp": "0.35.5", "streamdown": "1.6.10", "tailwind-merge": "3.3.1", "tailwindcss-animate": "1.0.7", diff --git a/ui/pnpm-lock.yaml b/ui/pnpm-lock.yaml index c9966ddf43..b759be8076 100644 --- a/ui/pnpm-lock.yaml +++ b/ui/pnpm-lock.yaml @@ -12,7 +12,7 @@ overrides: '@react-aria/visually-hidden>react': 19.2.7 '@react-aria/interactions>react': 19.2.7 lodash: 4.18.1 - sharp: 0.35.4 + sharp: 0.35.5 lodash-es: 4.18.1 hono: 4.12.34 '@hono/node-server': 1.19.17 @@ -275,8 +275,8 @@ importers: specifier: 0.0.1 version: 0.0.1 sharp: - specifier: 0.35.4 - version: 0.35.4(@types/node@24.10.8) + specifier: 0.35.5 + version: 0.35.5(@types/node@24.10.8) streamdown: specifier: 1.6.10 version: 1.6.10(@types/mdast@4.0.4)(micromark-util-types@2.0.2)(micromark@4.0.2)(react@19.2.7) @@ -1102,160 +1102,160 @@ packages: resolution: {integrity: sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==} engines: {node: '>=18'} - '@img/sharp-darwin-arm64@0.35.4': - resolution: {integrity: sha512-Uhfl4V4lhP2nbUVF9+hyH1+luj86f1gUFeo8ALYxFoULoU+G87D43BfeMP8XHsk9boxAnCY/bf2EHwhA7MuGsA==} + '@img/sharp-darwin-arm64@0.35.5': + resolution: {integrity: sha512-QRUlFQ0WxvdWyqqG/WtI3iupfD5rBzmCHXSdPsY91sAtVtTo7Q4cb6zOccZ3gqEqkr0f1As1ehLqmEpDsRf+lg==} engines: {node: '>=20.9.0'} cpu: [arm64] os: [darwin] - '@img/sharp-darwin-x64@0.35.4': - resolution: {integrity: sha512-hWniXY3bG5qKpkKrAwPe4y+VTPmf086YQAnkxWh7uA1YrlRouWGa0M0Mxj3ZjnXFkv7/TD1bTy9lGUK26vRvWw==} + '@img/sharp-darwin-x64@0.35.5': + resolution: {integrity: sha512-+BR255RhDlpygUpOc/Jdt1nT6DQ3XG/ERo5wbcdOf5Q320dKtPCKPLR1LJs9VGXRaMa8l1uUa0tkCNOXiAxZUw==} engines: {node: '>=20.9.0'} cpu: [x64] os: [darwin] - '@img/sharp-freebsd-wasm32@0.35.4': - resolution: {integrity: sha512-lIsKw/BU+kjB4eZjxrYrZmwOJYi3Ajrv66iAlBmUPyKc3HpnloevB1g3wxGD9P/5BbQ1brBGl65VRRrCvQDEqA==} + '@img/sharp-freebsd-wasm32@0.35.5': + resolution: {integrity: sha512-Y/z91nEZ4uIBX5X3nfTovjU9lHNKFYbL2lpHCLVNmXQK03VIZvXBBt0KxbPGp2SdGSF+2mQU4e+hQaWOt86iAw==} engines: {node: '>=20.9.0'} os: [freebsd] - '@img/sharp-libvips-darwin-arm64@1.3.3': - resolution: {integrity: sha512-suTBPTDGrI9WodccaDdwZItTSaBYASlBk1NSfElSHrUfzu3szG6lvIF58+WiFvnfzuK8ZBFS5zE00PxqxnRiPg==} + '@img/sharp-libvips-darwin-arm64@1.3.4': + resolution: {integrity: sha512-5R89nBYiRdUlSWJxPhO+GVtaXzXSxKnRu/xqMn3KTA3L9EB9Oy/P+Nn2f2vlhPuUdy/Zusb2DarbyTpGCfEDuw==} cpu: [arm64] os: [darwin] - '@img/sharp-libvips-darwin-x64@1.3.3': - resolution: {integrity: sha512-FVJZ5mITMobmXIz/hPDTw0EintTW5H3WfrxwLqEqjiIihlu+hVRyGrFQ60xl0Lxn7Bt3zdpevPaQi0HEzqz9fw==} + '@img/sharp-libvips-darwin-x64@1.3.4': + resolution: {integrity: sha512-iR2OKH80yi0U+dUplyh3/xdpFvps6YkCwsXenIJxqxR1v9o+xtKTGbS9H7cps+2Vxjc8B1j96p75NmTGjIhtpQ==} cpu: [x64] os: [darwin] - '@img/sharp-libvips-linux-arm64@1.3.3': - resolution: {integrity: sha512-0DaL0A6Xu6sQSQFwe4iVCrKWU2cCTItnRsYsCdxAMm9NF6twAA9BKnoqy4hqz4+azQ0JHuA26qiUKsf1XJ/v5A==} + '@img/sharp-libvips-linux-arm64@1.3.4': + resolution: {integrity: sha512-Y3dgX/6lE2QhQb+Gxy0WZxfg9MEm/JBjamZpS2IklP7xIQoKN4hzAm7KcMVGtaVDt3neE9OKBC7vAfonA/Lr1A==} cpu: [arm64] os: [linux] libc: [glibc] - '@img/sharp-libvips-linux-arm@1.3.3': - resolution: {integrity: sha512-3rbU4vqXXc3hY/OiXdl52xZvT0F1yEngWfvqudtPJg/KkyiaQw2DRsFrNzpmLvfavbwOq3qXn36GP8obHRULQA==} + '@img/sharp-libvips-linux-arm@1.3.4': + resolution: {integrity: sha512-LmRtTsOHuvM2+wlO2Db37dx5MiZhB0FvSunciw48YjdOkZz9KAiRbm8ujeMOA1INqmei5NapFxYEK1D1ZSidmw==} cpu: [arm] os: [linux] libc: [glibc] - '@img/sharp-libvips-linux-ppc64@1.3.3': - resolution: {integrity: sha512-cdn1OvUBwsXhbC0zSzJnNzf5MZ/mTrobawDvNXBTxe8VtqKAm0sRuEY2Evzovb/w9JMk4TvRxqt1mekSuJz64w==} + '@img/sharp-libvips-linux-ppc64@1.3.4': + resolution: {integrity: sha512-Le6boB8Tai0Nis+gIxIpKx68UDVVIqdR8Tin5Yf1z2LJJQLDJvCDRqRu+jC2qCoD+eIomonmOwB4smBRxfVpYQ==} cpu: [ppc64] os: [linux] libc: [glibc] - '@img/sharp-libvips-linux-riscv64@1.3.3': - resolution: {integrity: sha512-HjPVx7yKz+0lqdhDlTw1tt90wamBoxhiXpvl1XZpJLiHH4RCJ5yDTqH+VlYPv2fwFs89JFw4c1IexYOcQUi4IQ==} + '@img/sharp-libvips-linux-riscv64@1.3.4': + resolution: {integrity: sha512-aHkkIEHPRdQEegJN20MLmGtxYD9R2wQr3Cwpddnu5+YKMt6Uzax7S9h5gpZTo8wyrGuZSlfQ63OevL5mTyOC7Q==} cpu: [riscv64] os: [linux] libc: [glibc] - '@img/sharp-libvips-linux-s390x@1.3.3': - resolution: {integrity: sha512-neWLh+3yCNThxnfy3c4BbVBeGgt9aftno+XbT56iK28RgeDs3UOFWviLWlUu0bArYVYJaFDK+RRohbicUNCm8Q==} + '@img/sharp-libvips-linux-s390x@1.3.4': + resolution: {integrity: sha512-ra/mB6MikESDUO7Yg+Mi95bFBb9GsObURuhnOv3OqknjGe9sZrG8tCe9q0xSIGrtLgvgw0gKnFWcK4blSgQOuQ==} cpu: [s390x] os: [linux] libc: [glibc] - '@img/sharp-libvips-linux-x64@1.3.3': - resolution: {integrity: sha512-4vKmvAst9nrowcqquKFAyZJUDolUaIp8uRiN0mWFguJ1IplC9/pitXtlnnlU4aa/eJw3J7i67V+pwUL+wZGdsA==} + '@img/sharp-libvips-linux-x64@1.3.4': + resolution: {integrity: sha512-GJ//SSXbnwSDes02umB3nDJLFcQzw8a18V8fyhqr6tV515tOEMdImjjxj1AoafMRz56F3PHgftnj1QEKSU1zkw==} cpu: [x64] os: [linux] libc: [glibc] - '@img/sharp-libvips-linuxmusl-arm64@1.3.3': - resolution: {integrity: sha512-Y9kQaLMuNoB0bPYOOdcZMaseNrFpPodIWWMrx+CZyydf2xn68j9WYc6sWWRrDwNkzCQjKYfc68L7jKjGlHMibw==} + '@img/sharp-libvips-linuxmusl-arm64@1.3.4': + resolution: {integrity: sha512-hvulFwtjUcagsis6BBxHwGFwWoNZjgYmULGVrZcyfNbjA8hKILbRxGg15/7w5HDyXHXUos/j6baAWqnCyQ2DWA==} cpu: [arm64] os: [linux] libc: [musl] - '@img/sharp-libvips-linuxmusl-x64@1.3.3': - resolution: {integrity: sha512-fj8Mv0HHfD1Rr+4I68+3agJynxDWtBFgicTbSOb9Bke6pIwzGcJ+RX/yHjmiEGFMCavY/dxvem7MyNaJF+wDiw==} + '@img/sharp-libvips-linuxmusl-x64@1.3.4': + resolution: {integrity: sha512-6zXKeE/p39I1AmA3cJG35eyBGNqNddLnUXjhwBnsGjFPWqf5VKkDBEqaEkPDoTEtkxwi2vv8Tcr2mDyP4So7Fg==} cpu: [x64] os: [linux] libc: [musl] - '@img/sharp-linux-arm64@0.35.4': - resolution: {integrity: sha512-De4jpEnAU8Hd5oT0j1G3uL4ZvTuipVMn7YC6vPaJhy6/7EwEae0SVAoBrUMYQbkLGDm85taVWwuPc1a44LTzCQ==} + '@img/sharp-linux-arm64@0.35.5': + resolution: {integrity: sha512-LYVx5JTsOM2CBzmxreh+nl64/3H6Xb09iSLknqH47z2T2DFFxDeFLP5y4dJwe6H7uGQlHPyEEtIqyo3DYsRwdQ==} engines: {node: '>=20.9.0'} cpu: [arm64] os: [linux] libc: [glibc] - '@img/sharp-linux-arm@0.35.4': - resolution: {integrity: sha512-7OAS8gI0EReKGVN2HssHlM6umJgxF5VI3xN0p9FA91p/YO+ou5hiNghLdZ5BEHztwaaK5+bLKRf8x/o2L2nk9A==} + '@img/sharp-linux-arm@0.35.5': + resolution: {integrity: sha512-LEaXK2WdXVK5ykcw0buWyPMsmLLL2vpHLD6yrNSW+JGEL3BZPA4tpKN6iaMc4AxTTAoaX/sU1rOL51lcIz48ZQ==} engines: {node: '>=20.9.0'} cpu: [arm] os: [linux] libc: [glibc] - '@img/sharp-linux-ppc64@0.35.4': - resolution: {integrity: sha512-2oYZJeIl4kCcMGk4ouZVjnkCtFrpQFlNEtJ6GbxzhHQchwH0NH/qEb9ykmOl29dqwMq+JhFdZn+1ak2FKhI9fQ==} + '@img/sharp-linux-ppc64@0.35.5': + resolution: {integrity: sha512-QVxAAq8evVRI9ia2vqgwrmWucn5Dfv+JdWzj75pD8omHLPSP7f8p20O8jxzjCcuCEQEOtYOZUmX1hkiZ0kdevA==} engines: {node: '>=20.9.0'} cpu: [ppc64] os: [linux] libc: [glibc] - '@img/sharp-linux-riscv64@0.35.4': - resolution: {integrity: sha512-cPbNChoRURAWdebDIHSenxRpgEdy7JkPydSnUxRm9VvKD7m0/xVaR/8Fzlu81pk5nHEvHH87UZUA7cTtwnbJSA==} + '@img/sharp-linux-riscv64@0.35.5': + resolution: {integrity: sha512-LtdreXguaavKODPIfzJ4kffx7UNt1omwtK0rch4EBbbSTXPnxWmYSayXdLJw0fJzQ97kHt1gL/yh4tvU+nCyRQ==} engines: {node: '>=20.9.0'} cpu: [riscv64] os: [linux] libc: [glibc] - '@img/sharp-linux-s390x@0.35.4': - resolution: {integrity: sha512-RY0JFY8Fd6RonCBtHz+DvadaPkXDSI1AUn6yWL9TipqkZ1vY8w8evqdgyDFnkm4/K1ve1TvZiaePP5oSd4+WVQ==} + '@img/sharp-linux-s390x@0.35.5': + resolution: {integrity: sha512-UZasTOFiYzotTsGOCu42BfUzP6Tu6Do/947iRm1RsLKvlllxwGcn4RN27LibGWceix4Y+Pmw3jsnTcCQIgWjqA==} engines: {node: '>=20.9.0'} cpu: [s390x] os: [linux] libc: [glibc] - '@img/sharp-linux-x64@0.35.4': - resolution: {integrity: sha512-9qvvEAuk8k89TfWUoX2htWjbAMX8p+NxCppjpcg5k6xMsjhBQPTsoIh36h9Qde4WRuGpJeYnOjdosDn/cnv+OA==} + '@img/sharp-linux-x64@0.35.5': + resolution: {integrity: sha512-SxFtLTeJInhAA9Q836kux2vZNeOBQEx658qvbboZScr0wIARym3IcGmW7KpVD5sbVg0Ojy+udFQdayYIZyoNog==} engines: {node: '>=20.9.0'} cpu: [x64] os: [linux] libc: [glibc] - '@img/sharp-linuxmusl-arm64@0.35.4': - resolution: {integrity: sha512-KB5jxpfWQTr0nc3xdHtWChdbifHrBGsd2SM62Eyxrl8afikm+f5qGBU75SJIZBT/S1MC8XyacdlXBMSWq6OURA==} + '@img/sharp-linuxmusl-arm64@0.35.5': + resolution: {integrity: sha512-9HbMclmI1zlNkFRs3z9/eBtDjfD0sGlrX1z6b1qwmiFY5ElDLh4BC0LPBdVp7z1DXFiKlIcznf+ZlsuZzLxQqg==} engines: {node: '>=20.9.0'} cpu: [arm64] os: [linux] libc: [musl] - '@img/sharp-linuxmusl-x64@0.35.4': - resolution: {integrity: sha512-f+eZJZIQNEEd26RPSW+76chwOf1XtA2Y/O+5ocVyLliHkeih3e+jhLVBdNTd2rS3IbNXK8+ug93Vf5ZXtF5Lxg==} + '@img/sharp-linuxmusl-x64@0.35.5': + resolution: {integrity: sha512-4KOphqB035HrVdqLZfCgMzzERrQkkzOwRhl4OAkRO1YCldbaFjySXMaK534Mo0V+LndnlJk+sbUyLeU0ULyD1A==} engines: {node: '>=20.9.0'} cpu: [x64] os: [linux] libc: [musl] - '@img/sharp-wasm32@0.35.4': - resolution: {integrity: sha512-zQnl4Kwp7Q6NHsENtU2T/00Zi+w3AQNwz3+UaTyVBy2FpXrzXzGjndpK61onhZjRtRpQXxCTeqw19bVyXOh7jA==} + '@img/sharp-wasm32@0.35.5': + resolution: {integrity: sha512-Ptsga1su4tQx+LLF1ECS9U6nz5kmrXKo6XVbtR48Ke3ZRxxgaWBu7IDtEe1quo8hiupwm6WFqxVlXaSf7IINGQ==} engines: {node: '>=20.9.0'} - '@img/sharp-webcontainers-wasm32@0.35.4': - resolution: {integrity: sha512-ESfNkywmCfPNyaZjxooddJQiQ+l/nTpGEOGthxiLnIHXC/CmcBixnfwUleX9mCz9ovrUUvKMap/pm8RYbzfwaA==} + '@img/sharp-webcontainers-wasm32@0.35.5': + resolution: {integrity: sha512-hfhF/FmoQyTUkA0bIKFOtw536BQSeBMe6BF6QyWlrPxT754+TFLaZ7sKKTfvvM0yJgKgaYTwnFCIZ/GuDw5SUA==} engines: {node: '>=20.9.0'} cpu: [wasm32] - '@img/sharp-win32-arm64@0.35.4': - resolution: {integrity: sha512-iNdlBX9gLVvqe2I3uIJSIKTq6wckP/DYxZtcqxm09x5Gi24DnFBmPAWZmr60ZyYMG0xlzo6goG3670ar+RXvRw==} + '@img/sharp-win32-arm64@0.35.5': + resolution: {integrity: sha512-X4t7g+7ZA5DKblCBEXGjUqqemj4vczING/5viFwAL8h4N3qYeyjwdCvRLHi4EdOUI+2Z7UFlp1VM+p/AuEtm6Q==} engines: {node: '>=20.9.0'} cpu: [arm64] os: [win32] - '@img/sharp-win32-ia32@0.35.4': - resolution: {integrity: sha512-kqRsbaa5CS6KHlpxnN7WhE6vAAugXyZButpRdvDWetlv6Qv4N9WTcrWzF7tXfB9T7MsoadqdI8hmwLq6UlLvtw==} + '@img/sharp-win32-ia32@0.35.5': + resolution: {integrity: sha512-5Zm82LoBc43nhwNybZlG7Y1KO//Zhsn306fQl29ZOuStHLGTo3BWL83q3cznX0poxSAMuYL1On/BHBxkBeKr6A==} engines: {node: ^20.9.0} cpu: [ia32] os: [win32] - '@img/sharp-win32-x64@0.35.4': - resolution: {integrity: sha512-XtmnYhBcrORsJ4XJngyzr/EWP0hRZLAZRFaApdKuviyqF78+ylxh2y06ZmtULAMOnObJ3ucpN0AcwSWnMowTRg==} + '@img/sharp-win32-x64@0.35.5': + resolution: {integrity: sha512-x76eH0vEiHlcMQu8Y8IenntaACtddpT6W0wmXtWrnKcnKI7ME5DdgqhAD6SEWOEl1v2zDvkZDhFA9KnURwpfqg==} engines: {node: '>=20.9.0'} cpu: [x64] os: [win32] @@ -6485,8 +6485,8 @@ packages: setprototypeof@1.2.0: resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} - sharp@0.35.4: - resolution: {integrity: sha512-n++8XWcj+jCOr2IOl7h8LbKnGBDY4aPbmprMONBNFdn0ImXqpGVv5zliDs0V9HbmbCQLpbuo2ej9rAoOQTvMDA==} + sharp@0.35.5: + resolution: {integrity: sha512-Ywn4OnzGukp7CDMrp08RQ50YKmuwG47brZgIVPTvBaaAfQlRlygrRqSrxdCiL9M+LlzLBiJ68IR1QqvzHyjC7g==} engines: {node: '>=20.9.0'} peerDependencies: '@types/node': '*' @@ -8311,108 +8311,108 @@ snapshots: '@img/colour@1.1.0': {} - '@img/sharp-darwin-arm64@0.35.4': + '@img/sharp-darwin-arm64@0.35.5': optionalDependencies: - '@img/sharp-libvips-darwin-arm64': 1.3.3 + '@img/sharp-libvips-darwin-arm64': 1.3.4 optional: true - '@img/sharp-darwin-x64@0.35.4': + '@img/sharp-darwin-x64@0.35.5': optionalDependencies: - '@img/sharp-libvips-darwin-x64': 1.3.3 + '@img/sharp-libvips-darwin-x64': 1.3.4 optional: true - '@img/sharp-freebsd-wasm32@0.35.4': + '@img/sharp-freebsd-wasm32@0.35.5': dependencies: - '@img/sharp-wasm32': 0.35.4 + '@img/sharp-wasm32': 0.35.5 optional: true - '@img/sharp-libvips-darwin-arm64@1.3.3': + '@img/sharp-libvips-darwin-arm64@1.3.4': optional: true - '@img/sharp-libvips-darwin-x64@1.3.3': + '@img/sharp-libvips-darwin-x64@1.3.4': optional: true - '@img/sharp-libvips-linux-arm64@1.3.3': + '@img/sharp-libvips-linux-arm64@1.3.4': optional: true - '@img/sharp-libvips-linux-arm@1.3.3': + '@img/sharp-libvips-linux-arm@1.3.4': optional: true - '@img/sharp-libvips-linux-ppc64@1.3.3': + '@img/sharp-libvips-linux-ppc64@1.3.4': optional: true - '@img/sharp-libvips-linux-riscv64@1.3.3': + '@img/sharp-libvips-linux-riscv64@1.3.4': optional: true - '@img/sharp-libvips-linux-s390x@1.3.3': + '@img/sharp-libvips-linux-s390x@1.3.4': optional: true - '@img/sharp-libvips-linux-x64@1.3.3': + '@img/sharp-libvips-linux-x64@1.3.4': optional: true - '@img/sharp-libvips-linuxmusl-arm64@1.3.3': + '@img/sharp-libvips-linuxmusl-arm64@1.3.4': optional: true - '@img/sharp-libvips-linuxmusl-x64@1.3.3': + '@img/sharp-libvips-linuxmusl-x64@1.3.4': optional: true - '@img/sharp-linux-arm64@0.35.4': + '@img/sharp-linux-arm64@0.35.5': optionalDependencies: - '@img/sharp-libvips-linux-arm64': 1.3.3 + '@img/sharp-libvips-linux-arm64': 1.3.4 optional: true - '@img/sharp-linux-arm@0.35.4': + '@img/sharp-linux-arm@0.35.5': optionalDependencies: - '@img/sharp-libvips-linux-arm': 1.3.3 + '@img/sharp-libvips-linux-arm': 1.3.4 optional: true - '@img/sharp-linux-ppc64@0.35.4': + '@img/sharp-linux-ppc64@0.35.5': optionalDependencies: - '@img/sharp-libvips-linux-ppc64': 1.3.3 + '@img/sharp-libvips-linux-ppc64': 1.3.4 optional: true - '@img/sharp-linux-riscv64@0.35.4': + '@img/sharp-linux-riscv64@0.35.5': optionalDependencies: - '@img/sharp-libvips-linux-riscv64': 1.3.3 + '@img/sharp-libvips-linux-riscv64': 1.3.4 optional: true - '@img/sharp-linux-s390x@0.35.4': + '@img/sharp-linux-s390x@0.35.5': optionalDependencies: - '@img/sharp-libvips-linux-s390x': 1.3.3 + '@img/sharp-libvips-linux-s390x': 1.3.4 optional: true - '@img/sharp-linux-x64@0.35.4': + '@img/sharp-linux-x64@0.35.5': optionalDependencies: - '@img/sharp-libvips-linux-x64': 1.3.3 + '@img/sharp-libvips-linux-x64': 1.3.4 optional: true - '@img/sharp-linuxmusl-arm64@0.35.4': + '@img/sharp-linuxmusl-arm64@0.35.5': optionalDependencies: - '@img/sharp-libvips-linuxmusl-arm64': 1.3.3 + '@img/sharp-libvips-linuxmusl-arm64': 1.3.4 optional: true - '@img/sharp-linuxmusl-x64@0.35.4': + '@img/sharp-linuxmusl-x64@0.35.5': optionalDependencies: - '@img/sharp-libvips-linuxmusl-x64': 1.3.3 + '@img/sharp-libvips-linuxmusl-x64': 1.3.4 optional: true - '@img/sharp-wasm32@0.35.4': + '@img/sharp-wasm32@0.35.5': dependencies: '@emnapi/runtime': 1.11.3 optional: true - '@img/sharp-webcontainers-wasm32@0.35.4': + '@img/sharp-webcontainers-wasm32@0.35.5': dependencies: - '@img/sharp-wasm32': 0.35.4 + '@img/sharp-wasm32': 0.35.5 optional: true - '@img/sharp-win32-arm64@0.35.4': + '@img/sharp-win32-arm64@0.35.5': optional: true - '@img/sharp-win32-ia32@0.35.4': + '@img/sharp-win32-ia32@0.35.5': optional: true - '@img/sharp-win32-x64@0.35.4': + '@img/sharp-win32-x64@0.35.5': optional: true '@inquirer/ansi@2.0.5': {} @@ -13472,7 +13472,7 @@ snapshots: '@opentelemetry/api': 1.9.1 '@playwright/test': 1.56.1 babel-plugin-react-compiler: 1.0.0 - sharp: 0.35.4(@types/node@24.10.8) + sharp: 0.35.5(@types/node@24.10.8) transitivePeerDependencies: - '@babel/core' - '@types/node' @@ -14229,37 +14229,37 @@ snapshots: setprototypeof@1.2.0: {} - sharp@0.35.4(@types/node@24.10.8): + sharp@0.35.5(@types/node@24.10.8): dependencies: '@img/colour': 1.1.0 detect-libc: 2.1.2 semver: 7.8.5 optionalDependencies: - '@img/sharp-darwin-arm64': 0.35.4 - '@img/sharp-darwin-x64': 0.35.4 - '@img/sharp-freebsd-wasm32': 0.35.4 - '@img/sharp-libvips-darwin-arm64': 1.3.3 - '@img/sharp-libvips-darwin-x64': 1.3.3 - '@img/sharp-libvips-linux-arm': 1.3.3 - '@img/sharp-libvips-linux-arm64': 1.3.3 - '@img/sharp-libvips-linux-ppc64': 1.3.3 - '@img/sharp-libvips-linux-riscv64': 1.3.3 - '@img/sharp-libvips-linux-s390x': 1.3.3 - '@img/sharp-libvips-linux-x64': 1.3.3 - '@img/sharp-libvips-linuxmusl-arm64': 1.3.3 - '@img/sharp-libvips-linuxmusl-x64': 1.3.3 - '@img/sharp-linux-arm': 0.35.4 - '@img/sharp-linux-arm64': 0.35.4 - '@img/sharp-linux-ppc64': 0.35.4 - '@img/sharp-linux-riscv64': 0.35.4 - '@img/sharp-linux-s390x': 0.35.4 - '@img/sharp-linux-x64': 0.35.4 - '@img/sharp-linuxmusl-arm64': 0.35.4 - '@img/sharp-linuxmusl-x64': 0.35.4 - '@img/sharp-webcontainers-wasm32': 0.35.4 - '@img/sharp-win32-arm64': 0.35.4 - '@img/sharp-win32-ia32': 0.35.4 - '@img/sharp-win32-x64': 0.35.4 + '@img/sharp-darwin-arm64': 0.35.5 + '@img/sharp-darwin-x64': 0.35.5 + '@img/sharp-freebsd-wasm32': 0.35.5 + '@img/sharp-libvips-darwin-arm64': 1.3.4 + '@img/sharp-libvips-darwin-x64': 1.3.4 + '@img/sharp-libvips-linux-arm': 1.3.4 + '@img/sharp-libvips-linux-arm64': 1.3.4 + '@img/sharp-libvips-linux-ppc64': 1.3.4 + '@img/sharp-libvips-linux-riscv64': 1.3.4 + '@img/sharp-libvips-linux-s390x': 1.3.4 + '@img/sharp-libvips-linux-x64': 1.3.4 + '@img/sharp-libvips-linuxmusl-arm64': 1.3.4 + '@img/sharp-libvips-linuxmusl-x64': 1.3.4 + '@img/sharp-linux-arm': 0.35.5 + '@img/sharp-linux-arm64': 0.35.5 + '@img/sharp-linux-ppc64': 0.35.5 + '@img/sharp-linux-riscv64': 0.35.5 + '@img/sharp-linux-s390x': 0.35.5 + '@img/sharp-linux-x64': 0.35.5 + '@img/sharp-linuxmusl-arm64': 0.35.5 + '@img/sharp-linuxmusl-x64': 0.35.5 + '@img/sharp-webcontainers-wasm32': 0.35.5 + '@img/sharp-win32-arm64': 0.35.5 + '@img/sharp-win32-ia32': 0.35.5 + '@img/sharp-win32-x64': 0.35.5 '@types/node': 24.10.8 shebang-command@2.0.0: diff --git a/ui/pnpm-workspace.yaml b/ui/pnpm-workspace.yaml index a6a70b20d4..f3c870462f 100644 --- a/ui/pnpm-workspace.yaml +++ b/ui/pnpm-workspace.yaml @@ -19,9 +19,10 @@ overrides: "@react-aria/interactions>react": "19.2.7" "lodash": "4.18.1" # Next.js 16.3.6 requests sharp ^0.35.4; pinned to fix - # GHSA-rgj7-g3m4-5g8c (libheif). This override controls resolution; - # keep it aligned with the direct dependency in package.json. - "sharp": "0.35.4" + # GHSA-rgj7-g3m4-5g8c (libheif) and GHSA-wq5f-xc86-pv6w (librsvg). + # This override controls resolution; keep it aligned with the direct + # dependency in package.json. + "sharp": "0.35.5" "lodash-es": "4.18.1" # GHSA-88fw-hqm2-52qc (CORS reflects any Origin with credentials), CVE-2026-59896 # (hono/jsx SSR context leak) and the 4.12.34 batch: CORS ReDoS via From 22bc834c09f09dc72f1820ec8b2623e8855c28b0 Mon Sep 17 00:00:00 2001 From: StylusFrost <43682773+StylusFrost@users.noreply.github.com> Date: Wed, 7 Oct 2026 12:16:04 +0200 Subject: [PATCH 07/17] chore(grype): suppress zlib CVE-2026-85091 until base images ship the fix (#12970) --- .grype.yaml | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/.grype.yaml b/.grype.yaml index 5028944529..81e6be2eb9 100644 --- a/.grype.yaml +++ b/.grype.yaml @@ -126,3 +126,18 @@ ignore: - vulnerability: CVE-2026-82049 package: name: python + + # zlib in the Alpine base images of the UI (node:24.18.1-alpine) and MCP + # (python:3.13.14-alpine3.23) containers. CVE-2026-85091 is a heap overflow in + # gz_vacate() reached only through non-blocking gzwrite() followed by gzprintf() after + # a write stall. Node links its own bundled zlib and the MCP server never writes gzip + # files, so neither image calls that path. TEMPORARY: the 1.3.2-r1 fix is in the + # Alpine index and could be taken with `apk add --upgrade`, but we wait for the base + # images to ship it instead (the newest node:24-alpine and python:3.13-alpine3.23 still + # carry 1.3.2-r0). Pinned to that version so the rule stops matching on its own once a + # base image moves forward. Remove when the base digests are bumped, by 2026-11-07. + # https://security.alpinelinux.org/vuln/CVE-2026-85091 + - vulnerability: CVE-2026-85091 + package: + name: zlib + version: 1.3.2-r0 From f1e822f7dfaa37896e42ac987885107461b38860 Mon Sep 17 00:00:00 2001 From: StylusFrost <43682773+StylusFrost@users.noreply.github.com> Date: Wed, 7 Oct 2026 12:27:44 +0200 Subject: [PATCH 08/17] chore(trivy): suppress @modelcontextprotocol/sdk CVE-2026-104850 (#12967) --- .trivyignore.yaml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.trivyignore.yaml b/.trivyignore.yaml index 1d9f44f658..3804fabaa4 100644 --- a/.trivyignore.yaml +++ b/.trivyignore.yaml @@ -77,6 +77,9 @@ vulnerabilities: # exists in the image, only its purl inside that manifest. The UI lock lists proxy-addr 2.0.7 # through express, but the Next standalone output does not ship it, so the shared entry # hides nothing real in the UI image either (checked on prowlercloud/prowler-ui:latest). +# @modelcontextprotocol/sdk 1.27.1 (CVE-2026-104850) is another: only its purl is in that +# manifest. The entry is pinned to 1.27.1 because the UI really depends on the SDK (1.26.0 +# via @langchain/mcp-adapters), and a version-less purl would hide that finding too. - id: CVE-2020-0606 purls: - "pkg:nuget/Microsoft.WindowsDesktop.App.Ref" @@ -173,6 +176,10 @@ vulnerabilities: purls: - "pkg:npm/proxy-addr" expired_at: 2027-01-31 + - id: CVE-2026-104850 + purls: + - "pkg:npm/%40modelcontextprotocol%2Fsdk@1.27.1" + expired_at: 2027-01-31 # CVE-2026-84304 is a DoS in grpc-go <= 1.83.0: a peer fragments a gRPC stream into # millions of tiny HTTP/2 DATA frames until the receiver runs out of heap. Fixed in From 9d8060adc51b9c1daa2048395e6de5107007a5a4 Mon Sep 17 00:00:00 2001 From: Alejandro Bailo <59607668+alejandrobailo@users.noreply.github.com> Date: Wed, 7 Oct 2026 16:14:23 +0200 Subject: [PATCH 09/17] test(ui): stabilize invite-teammate roles-unavailable test (#12973) --- .../invite-teammate/invite-teammate-panel.test.tsx | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/ui/components/providers/wizard/steps/invite-teammate/invite-teammate-panel.test.tsx b/ui/components/providers/wizard/steps/invite-teammate/invite-teammate-panel.test.tsx index 65139703eb..3b5ea44c04 100644 --- a/ui/components/providers/wizard/steps/invite-teammate/invite-teammate-panel.test.tsx +++ b/ui/components/providers/wizard/steps/invite-teammate/invite-teammate-panel.test.tsx @@ -282,13 +282,15 @@ describe("InviteTeammatePanel", () => { // When const { onUiState } = renderPanel(); - // Then + // Then: the footer drops the action a commit after the message shows. expect( await screen.findByText(/Roles could not be loaded right now/), ).toBeInTheDocument(); + await waitFor(() => + expect( + screen.queryByRole("button", { name: "Send invitation" }), + ).not.toBeInTheDocument(), + ); expect(lastUiState(onUiState)).toMatchObject({ showAction: false }); - expect( - screen.queryByRole("button", { name: "Send invitation" }), - ).not.toBeInTheDocument(); }); }); From b5a709664ddbe60f3efb067cf0b6a022337f90fb Mon Sep 17 00:00:00 2001 From: Alejandro Bailo <59607668+alejandrobailo@users.noreply.github.com> Date: Wed, 7 Oct 2026 16:35:20 +0200 Subject: [PATCH 10/17] fix(ui): show only the chosen registry credential variant (#12966) --- .../dynamic-provider-credentials.test.ts | 56 +++++++++ .../providers/dynamic-provider-credentials.ts | 10 +- .../steps/dynamic-credentials-step.test.tsx | 72 ++++++++++++ .../wizard/steps/dynamic-credentials-step.tsx | 65 ++++++++--- .../workflow/provider-credential-fields.tsx | 1 + .../fixtures/union-credential-schema.json | 71 ++++++++++++ .../provider-credential-schema.test.ts | 109 ++++++++++++++++++ .../provider-credential-schema.ts | 82 ++++++++++++- .../provider-credential-values.ts | 41 ++++++- 9 files changed, 479 insertions(+), 28 deletions(-) create mode 100644 ui/lib/provider-credentials/fixtures/union-credential-schema.json diff --git a/ui/actions/providers/dynamic-provider-credentials.test.ts b/ui/actions/providers/dynamic-provider-credentials.test.ts index f8ab619bc8..3f3024a772 100644 --- a/ui/actions/providers/dynamic-provider-credentials.test.ts +++ b/ui/actions/providers/dynamic-provider-credentials.test.ts @@ -2,6 +2,7 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; import openaiSchema from "@/lib/provider-credentials/fixtures/openai-credential-schema.json"; import templateSchema from "@/lib/provider-credentials/fixtures/template-credential-schema.json"; +import unionSchema from "@/lib/provider-credentials/fixtures/union-credential-schema.json"; const { fetchMock, getProviderSchemas, getAuthHeaders, revalidatePath } = vi.hoisted(() => ({ fetchMock: vi.fn(), @@ -136,6 +137,61 @@ describe("dynamic provider credential actions", () => { }); expect(fetchMock).toHaveBeenCalledTimes(1); }); + describe("when the secret type's schema offers several variants", () => { + const basic = { + auth_method: "basic", + host: "api.acme.test", + username: "fixture-user", + password: "fixture-password-not-a-secret", + }; + beforeEach(() => { + getProviderSchemas.mockResolvedValue({ + status: "success", + providerType: "acme", + secretTypes: { static: unionSchema }, + }); + }); + + it("sends a secret that one variant accepts", async () => { + // Given + fetchMock + .mockResolvedValueOnce(response(account())) + .mockResolvedValueOnce(response({ data: { id: "saved" } }, 201)); + + // When + const result = await saveDynamicProviderCredentials({ + ...input, + secretType: "static", + secret: basic, + }); + + // Then + expect(result).toEqual({ status: "saved", secretId: "saved" }); + expect( + JSON.parse(fetchMock.mock.calls[1][1].body).data.attributes, + ).toEqual({ secret_type: "static", secret: basic }); + }); + + it.each([ + ["fields of two variants", { ...basic, token: "fixture-token" }], + ["another variant's discriminator", { ...basic, auth_method: "token" }], + ["a missing required field", { ...basic, password: "" }], + ])("does not write a secret with %s", async (_name, secret) => { + // Given + fetchMock.mockResolvedValueOnce(response(account())); + + // When + const result = await saveDynamicProviderCredentials({ + ...input, + secretType: "static", + secret, + }); + + // Then + expect(result.status).toBe("invalid"); + expect(fetchMock).toHaveBeenCalledTimes(1); + }); + }); it.each([ { ...input, secretType: "invented" }, { ...input, secret: { token: "" } }, diff --git a/ui/actions/providers/dynamic-provider-credentials.ts b/ui/actions/providers/dynamic-provider-credentials.ts index 79975fa2e7..7506b657ca 100644 --- a/ui/actions/providers/dynamic-provider-credentials.ts +++ b/ui/actions/providers/dynamic-provider-credentials.ts @@ -4,8 +4,8 @@ import { revalidatePath } from "next/cache"; import { z } from "zod"; import { apiBaseUrl, getAuthHeaders } from "@/lib"; -import { parseRegistryCredentialSchema } from "@/lib/provider-credentials/provider-credential-schema"; -import { validateCredentialValues } from "@/lib/provider-credentials/provider-credential-values"; +import { parseRegistryCredentialVariants } from "@/lib/provider-credentials/provider-credential-schema"; +import { validateCredentialVariants } from "@/lib/provider-credentials/provider-credential-values"; import { isKnownProviderType } from "@/types/providers"; import { getProviderSchemas } from "./provider-schemas"; @@ -66,11 +66,11 @@ export async function saveDynamicProviderCredentials( !Object.hasOwn(schemas.secretTypes, secretType) ) return { status: "schema_unavailable" }; - const schema = parseRegistryCredentialSchema( + const variants = parseRegistryCredentialVariants( schemas.secretTypes[secretType], ); - if (!schema) return { status: "schema_unavailable" }; - const validated = validateCredentialValues(schema, secret); + if (!variants) return { status: "schema_unavailable" }; + const validated = validateCredentialVariants(variants, secret); if (!validated.valid) return { status: "invalid", errors: validated.errors }; diff --git a/ui/components/providers/wizard/steps/dynamic-credentials-step.test.tsx b/ui/components/providers/wizard/steps/dynamic-credentials-step.test.tsx index ce28a075b1..f91e5af082 100644 --- a/ui/components/providers/wizard/steps/dynamic-credentials-step.test.tsx +++ b/ui/components/providers/wizard/steps/dynamic-credentials-step.test.tsx @@ -10,6 +10,7 @@ import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import openaiSchema from "@/lib/provider-credentials/fixtures/openai-credential-schema.json"; import templateSchema from "@/lib/provider-credentials/fixtures/template-credential-schema.json"; +import unionSchema from "@/lib/provider-credentials/fixtures/union-credential-schema.json"; import { useProviderWizardStore } from "@/store/provider-wizard/store"; import type { ProviderSchemasResult } from "@/types/provider-schema"; @@ -183,6 +184,77 @@ describe("dynamic credentials in the provider wizard", () => { }, }); }); + it("shows only the chosen variant's fields when a schema offers several", async () => { + // Given + const user = userEvent.setup(); + getProviderSchemas.mockResolvedValue({ + status: "success", + providerType: "acme", + secretTypes: { static: unionSchema }, + }); + render(); + await screen.findByLabelText(/API Host/); + const method = screen.getByRole("combobox", { + name: "Authentication method", + }); + + // Then + expect(method).toHaveTextContent("API token"); + expect(screen.getByLabelText(/^Token/)).toBeVisible(); + expect(screen.queryByLabelText(/Username/)).not.toBeInTheDocument(); + expect(screen.queryByLabelText(/Password/)).not.toBeInTheDocument(); + expect( + screen.queryByLabelText(/Authentication Method/), + ).not.toBeInTheDocument(); + + // When: a token typed before switching away is gone on return. + await user.type(screen.getByLabelText(/^Token/), "previous-variant-token"); + await user.click(method); + await user.click( + screen.getByRole("option", { name: "Username and password" }), + ); + await user.click(method); + await user.click(screen.getByRole("option", { name: "API token" })); + + // Then + expect(screen.getByLabelText(/^Token/)).toHaveValue(""); + + // When + await user.click(method); + await user.click( + screen.getByRole("option", { name: "Username and password" }), + ); + await user.type(screen.getByLabelText(/API Host/), "api.acme.test"); + await user.type(screen.getByLabelText(/Username/), "fixture-user"); + await user.type( + screen.getByLabelText(/Password/), + "fixture-password-not-a-secret", + ); + + // Then + expect(screen.queryByLabelText(/^Token/)).not.toBeInTheDocument(); + + // When + act(() => + screen + .getByLabelText(/Username/) + .closest("form")! + .requestSubmit(), + ); + + // Then: the variant's fixed discriminator travels with its own fields only. + await waitFor(() => expect(props.onNext).toHaveBeenCalledOnce()); + expect(saveDynamicProviderCredentials).toHaveBeenCalledWith({ + providerId: "account", + secretType: "static", + secret: { + auth_method: "basic", + host: "api.acme.test", + username: "fixture-user", + password: "fixture-password-not-a-secret", + }, + }); + }); it.each<{ result: ProviderSchemasResult; title: string }>([ { result: { status: "success", providerType: "acme", secretTypes: {} }, diff --git a/ui/components/providers/wizard/steps/dynamic-credentials-step.tsx b/ui/components/providers/wizard/steps/dynamic-credentials-step.tsx index 35fc5f0daa..3d2c48941d 100644 --- a/ui/components/providers/wizard/steps/dynamic-credentials-step.tsx +++ b/ui/components/providers/wizard/steps/dynamic-credentials-step.tsx @@ -20,7 +20,7 @@ import { Skeleton } from "@/components/shadcn/skeleton/skeleton"; import { useToast } from "@/components/shadcn/toast"; import { StatusAlert } from "@/components/shared/status-alert"; import { - parseRegistryCredentialSchema, + parseRegistryCredentialVariants, type RegistryCredentialSchema, } from "@/lib/provider-credentials/provider-credential-schema"; import { @@ -28,7 +28,10 @@ import { validateCredentialValues, } from "@/lib/provider-credentials/provider-credential-values"; import { useProviderWizardStore } from "@/store/provider-wizard/store"; -import type { ProviderSchemasResult } from "@/types/provider-schema"; +import type { + ProviderSchemasResult, + ProviderSecretTypes, +} from "@/types/provider-schema"; import { WIZARD_FOOTER_ACTION_TYPE, @@ -43,6 +46,39 @@ interface DynamicCredentialsStepProps { onFooterChange: (config: WizardFooterConfig) => void; } +interface CredentialMethod { + id: string; + secretType: string; + label: string; + schema: RegistryCredentialSchema | null; +} + +// Each variant of a secret type's schema is a method of its own. +function listCredentialMethods( + secretTypes: ProviderSecretTypes, +): CredentialMethod[] { + return Object.entries(secretTypes).flatMap(([secretType, value]) => { + const label = secretType.replaceAll("_", " "); + const variants = parseRegistryCredentialVariants(value); + if (!variants || variants.length === 1) { + return [ + { + id: secretType, + secretType, + label, + schema: variants?.[0].schema ?? null, + }, + ]; + } + return variants.map((variant, index) => ({ + id: `${secretType}/${index}`, + secretType, + label: variant.label ?? `${label} ${index + 1}`, + schema: variant.schema, + })); + }); +} + function credentialFormError(status: ProviderSchemasResult["status"]) { switch (status) { case "access_denied": @@ -233,12 +269,11 @@ function DynamicCredentialsContent(props: DynamicCredentialsStepProps) { }, [providerType, attempt]); const methods = - schemas?.status === "success" ? Object.keys(schemas.secretTypes) : []; - const secretType = selectedMethod || methods[0]; - const schema = - schemas?.status === "success" && secretType - ? parseRegistryCredentialSchema(schemas.secretTypes[secretType]) - : null; + schemas?.status === "success" + ? listCredentialMethods(schemas.secretTypes) + : []; + const method = methods.find(({ id }) => id === selectedMethod) ?? methods[0]; + const schema = method?.schema ?? null; useEffect(() => { if (!schema) onFooterChange({ @@ -277,7 +312,7 @@ function DynamicCredentialsContent(props: DynamicCredentialsStepProps) { Authentication method )} - {schema ? ( + {method && schema ? ( diff --git a/ui/components/providers/workflow/provider-credential-fields.tsx b/ui/components/providers/workflow/provider-credential-fields.tsx index 157f4a0d7e..230f36b0e4 100644 --- a/ui/components/providers/workflow/provider-credential-fields.tsx +++ b/ui/components/providers/workflow/provider-credential-fields.tsx @@ -38,6 +38,7 @@ export function RegistryCredentialFields({ return (
{schema.fields.map((field, index) => { + if (field.kind === "constant") return null; const error = errors[field.name]; const fieldId = `registry-credential-${instanceId}-${index}`; const id = `${fieldId}-control`; diff --git a/ui/lib/provider-credentials/fixtures/union-credential-schema.json b/ui/lib/provider-credentials/fixtures/union-credential-schema.json new file mode 100644 index 0000000000..55d0024f68 --- /dev/null +++ b/ui/lib/provider-credentials/fixtures/union-credential-schema.json @@ -0,0 +1,71 @@ +{ + "description": "Acme API credentials.", + "discriminator": { + "mapping": { + "basic": "#/$defs/AcmeBasicCredentials", + "token": "#/$defs/AcmeTokenCredentials" + }, + "propertyName": "auth_method" + }, + "oneOf": [ + { + "additionalProperties": false, + "description": "A long-lived API token issued from the console.", + "properties": { + "auth_method": { + "const": "token", + "default": "token", + "title": "Authentication Method", + "type": "string" + }, + "host": { + "description": "Hostname of the API.", + "examples": ["api.acme.test"], + "title": "API Host", + "type": "string" + }, + "token": { + "description": "API token.", + "format": "password", + "title": "Token", + "type": "string", + "writeOnly": true + } + }, + "required": ["host", "token"], + "title": "API token", + "type": "object" + }, + { + "additionalProperties": false, + "description": "A username and password exchanged for a session token.", + "properties": { + "auth_method": { + "const": "basic", + "title": "Authentication Method", + "type": "string" + }, + "host": { + "description": "Hostname of the API.", + "examples": ["api.acme.test"], + "title": "API Host", + "type": "string" + }, + "username": { + "title": "Username", + "type": "string" + }, + "password": { + "format": "password", + "title": "Password", + "type": "string", + "writeOnly": true + } + }, + "required": ["auth_method", "host", "username", "password"], + "title": "Username and password", + "type": "object" + } + ], + "title": "AcmeStaticCredentials" +} diff --git a/ui/lib/provider-credentials/provider-credential-schema.test.ts b/ui/lib/provider-credentials/provider-credential-schema.test.ts index eac0ac044f..73a60eb944 100644 --- a/ui/lib/provider-credentials/provider-credential-schema.test.ts +++ b/ui/lib/provider-credentials/provider-credential-schema.test.ts @@ -2,8 +2,10 @@ import { describe, expect, it } from "vitest"; import openaiSchema from "./fixtures/openai-credential-schema.json"; import templateSchema from "./fixtures/template-credential-schema.json"; +import unionSchema from "./fixtures/union-credential-schema.json"; import { parseRegistryCredentialSchema, + parseRegistryCredentialVariants, REGISTRY_CREDENTIAL_SCHEMA_LIMITS, } from "./provider-credential-schema"; @@ -157,10 +159,22 @@ describe("parseRegistryCredentialSchema", () => { ["definitions", { definitions: {} }], ["combinators", { anyOf: [] }], ["additional properties", { additionalProperties: true }], + ["typed additional properties", { additionalProperties: {} }], ])("rejects risky root keywords: %s", (_name, keyword) => { expect(parseRegistryCredentialSchema({ ...schema, ...keyword })).toBeNull(); }); + it("accepts the closed object pydantic emits for extra='forbid'", () => { + // Given / When + const result = parseRegistryCredentialSchema({ + ...schema, + additionalProperties: false, + }); + + // Then + expect(result).toEqual(parseRegistryCredentialSchema(schema)); + }); + it.each([ ["nested objects", { type: "object", properties: {} }], ["arrays", { type: "array" }], @@ -264,3 +278,98 @@ describe("parseRegistryCredentialSchema", () => { ]); }); }); + +describe("parseRegistryCredentialVariants", () => { + it("splits a pydantic union into variants that keep only their own fields", () => { + // Given / When + const result = parseRegistryCredentialVariants(unionSchema); + + // Then + expect( + result?.map(({ label, schema }) => [ + label, + schema.fields.map(({ name, kind }) => [name, kind]), + ]), + ).toEqual([ + [ + "API token", + [ + ["auth_method", "constant"], + ["host", "text"], + ["token", "password"], + ], + ], + [ + "Username and password", + [ + ["auth_method", "constant"], + ["host", "text"], + ["username", "text"], + ["password", "password"], + ], + ], + ]); + expect(result?.[1].schema.fields[0].defaultValue).toBe("basic"); + }); + + it("accepts anyOf, which pydantic emits for a union without discriminator", () => { + // Given + const { oneOf, discriminator: _discriminator, ...root } = unionSchema; + + // When + const result = parseRegistryCredentialVariants({ ...root, anyOf: oneOf }); + + // Then + expect(result?.map(({ label }) => label)).toEqual([ + "API token", + "Username and password", + ]); + }); + + it.each([ + ["an unsupported variant", { oneOf: [templateSchema, { type: "array" }] }], + ["no variants", { oneOf: [] }], + ["variants that are not a list", { oneOf: templateSchema }], + ["both combinators", { oneOf: [templateSchema], anyOf: [templateSchema] }], + ["unknown root keywords", { oneOf: [templateSchema], $defs: {} }], + [ + "a malformed discriminator", + { oneOf: [templateSchema], discriminator: 1 }, + ], + [ + "too many variants", + { + oneOf: Array.from( + { length: REGISTRY_CREDENTIAL_SCHEMA_LIMITS.MAX_VARIANTS + 1 }, + () => templateSchema, + ), + }, + ], + ])("rejects a union with %s", (_name, union) => { + expect(parseRegistryCredentialVariants(union)).toBeNull(); + }); + + it.each([ + { type: "string", const: "token", default: "basic" }, + { type: "string", const: "" }, + { type: "string", const: "token", enum: ["token"] }, + { type: "integer", const: 1 }, + ])("rejects a discriminator that is not one fixed string: %j", (property) => { + expect( + parseRegistryCredentialSchema({ + type: "object", + properties: { auth_method: property }, + }), + ).toBeNull(); + }); + + it("returns a flat schema as its only variant", () => { + // Given / When + const result = parseRegistryCredentialVariants(templateSchema); + + // Then + expect(result).toEqual([ + { schema: parseRegistryCredentialSchema(templateSchema) }, + ]); + }); +}); diff --git a/ui/lib/provider-credentials/provider-credential-schema.ts b/ui/lib/provider-credentials/provider-credential-schema.ts index ed1e4ee0e7..cc02591203 100644 --- a/ui/lib/provider-credentials/provider-credential-schema.ts +++ b/ui/lib/provider-credentials/provider-credential-schema.ts @@ -5,6 +5,7 @@ const FIELD_KIND = { TEXTAREA: "textarea", CHECKBOX: "checkbox", INTEGER: "integer", + CONSTANT: "constant", } as const; export const REGISTRY_CREDENTIAL_SCHEMA_LIMITS = { @@ -12,6 +13,7 @@ export const REGISTRY_CREDENTIAL_SCHEMA_LIMITS = { MAX_NAME_LENGTH: 50, MAX_TEXT_LENGTH: 200, MAX_ENUM_OPTIONS: 20, + MAX_VARIANTS: 8, } as const; type FieldKind = (typeof FIELD_KIND)[keyof typeof FIELD_KIND]; @@ -34,7 +36,18 @@ export interface RegistryCredentialSchema { readonly fields: readonly RegistryCredentialField[]; } -const ROOT = new Set("type title description properties required".split(" ")); +/** One alternative of a `oneOf`/`anyOf` schema, or the whole flat schema. */ +export interface RegistryCredentialVariant { + readonly label?: string; + readonly schema: RegistryCredentialSchema; +} + +const ROOT = new Set( + "type title description properties required additionalProperties".split(" "), +); +const UNION_ROOT = new Set( + "title description oneOf anyOf discriminator".split(" "), +); const FIELD = new Set( "title description type format writeOnly enum default examples x-prowler-widget".split( " ", @@ -44,6 +57,9 @@ const BOOLEAN_FIELD = new Set("title description type default".split(" ")); const INTEGER_FIELD = new Set( "title description type default minimum maximum".split(" "), ); +const CONSTANT_FIELD = new Set( + "title description type const default".split(" "), +); const FORBIDDEN_NAMES = new Set(["__proto__", "prototype", "constructor"]); const FIELD_NAME = /^[A-Za-z][A-Za-z0-9_-]*$/; @@ -87,7 +103,14 @@ function hasOnly( export function parseRegistryCredentialSchema( value: unknown, ): RegistryCredentialSchema | null { - if (!isRecord(value) || !hasOnly(value, ROOT) || value.type !== "object") { + if ( + !isRecord(value) || + !hasOnly(value, ROOT) || + value.type !== "object" || + // Only a closed object: the form never sends undeclared keys anyway. + (value.additionalProperties !== undefined && + value.additionalProperties !== false) + ) { return null; } if ( @@ -189,6 +212,22 @@ export function parseRegistryCredentialSchema( }); continue; } + if (property.type === "string" && property.const !== undefined) { + // A union's discriminator: fixed by the variant, never typed by the user. + if ( + !hasOnly(property, CONSTANT_FIELD) || + !isText(property.const) || + (defaultValue !== undefined && defaultValue !== property.const) + ) { + return null; + } + fields.push({ + ...baseField, + kind: FIELD_KIND.CONSTANT, + defaultValue: property.const, + }); + continue; + } if (property.type !== "string" || !hasOnly(property, FIELD)) return null; const format = property.format; @@ -256,3 +295,42 @@ export function parseRegistryCredentialSchema( } return { fields }; } + +export function parseRegistryCredentialVariants( + value: unknown, +): readonly RegistryCredentialVariant[] | null { + const isUnion = + isRecord(value) && + (Object.hasOwn(value, "oneOf") || Object.hasOwn(value, "anyOf")); + if (!isUnion) { + const schema = parseRegistryCredentialSchema(value); + return schema ? [{ schema }] : null; + } + if ( + !hasOnly(value, UNION_ROOT) || + (Object.hasOwn(value, "oneOf") && Object.hasOwn(value, "anyOf")) || + (value.title !== undefined && !isText(value.title)) || + (value.description !== undefined && + typeof value.description !== "string") || + (value.discriminator !== undefined && !isRecord(value.discriminator)) + ) { + return null; + } + + const branches = value.oneOf ?? value.anyOf; + if ( + !Array.isArray(branches) || + branches.length === 0 || + branches.length > REGISTRY_CREDENTIAL_SCHEMA_LIMITS.MAX_VARIANTS + ) { + return null; + } + const variants: RegistryCredentialVariant[] = []; + for (const branch of branches) { + const schema = parseRegistryCredentialSchema(branch); + if (!schema) return null; + const label = isRecord(branch) ? branch.title : undefined; + variants.push({ ...(isText(label) ? { label } : {}), schema }); + } + return variants; +} diff --git a/ui/lib/provider-credentials/provider-credential-values.ts b/ui/lib/provider-credentials/provider-credential-values.ts index 6673adf9ea..3526cd8808 100644 --- a/ui/lib/provider-credentials/provider-credential-values.ts +++ b/ui/lib/provider-credentials/provider-credential-values.ts @@ -1,6 +1,7 @@ import type { RegistryCredentialSchema, RegistryCredentialValue, + RegistryCredentialVariant, } from "./provider-credential-schema"; export function getCredentialDefaults( @@ -17,16 +18,18 @@ export function getCredentialDefaults( ); } -export function validateCredentialValues( - schema: RegistryCredentialSchema, - values: unknown, -): +type CredentialValidation = | { valid: true; secret: Record; errors: Record; } - | { valid: false; errors: Record } { + | { valid: false; errors: Record }; + +export function validateCredentialValues( + schema: RegistryCredentialSchema, + values: unknown, +): CredentialValidation { if (!values || typeof values !== "object" || Array.isArray(values)) return { valid: false, @@ -47,7 +50,17 @@ export function validateCredentialValues( const secret: Record = {}; for (const field of schema.fields) { const value = fields.get(field.name); - if (value === undefined || (value === "" && field.kind !== "checkbox")) { + if (field.kind === "constant") { + // Always sent: it tells the API which variant the secret follows. + if (value !== undefined && value !== field.defaultValue) { + errors[field.name] = `Enter a valid ${field.label}`; + } else if (field.defaultValue !== undefined) { + secret[field.name] = field.defaultValue; + } + } else if ( + value === undefined || + (value === "" && field.kind !== "checkbox") + ) { if (field.required) errors[field.name] = `${field.label} is required`; } else if (field.kind === "checkbox") { if (typeof value !== "boolean") { @@ -83,3 +96,19 @@ export function validateCredentialValues( ? { valid: false, errors } : { valid: true, secret, errors }; } + +/** Valid when one variant accepts the values, as JSON Schema `anyOf` does. */ +export function validateCredentialVariants( + variants: readonly RegistryCredentialVariant[], + values: unknown, +): CredentialValidation { + const results = variants.map(({ schema }) => + validateCredentialValues(schema, values), + ); + // Otherwise report the variant that owns every submitted field. + return ( + results.find(({ valid }) => valid) ?? + results.find(({ errors }) => !errors._form) ?? + results[0] + ); +} From 92216f1597f1177e4d9b567ac90d9c8a93f7770a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9sar=20Arroba?= <19954079+cesararroba@users.noreply.github.com> Date: Thu, 8 Oct 2026 12:47:38 +0200 Subject: [PATCH 11/17] ci(release): auto-create minor release branch and lift freeze on publish Create the minor release branch automatically in the release preparation workflow, check versions before creating it, match the version literally, and lift the release freeze when the release is published. --- .github/workflows/prepare-release.yml | 29 +++++++++++++++++++++-- .github/workflows/release-freeze-lift.yml | 27 +++++++++++++++++++++ 2 files changed, 54 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/release-freeze-lift.yml diff --git a/.github/workflows/prepare-release.yml b/.github/workflows/prepare-release.yml index 1cf130d8c4..65809f15f0 100644 --- a/.github/workflows/prepare-release.yml +++ b/.github/workflows/prepare-release.yml @@ -84,6 +84,8 @@ jobs: fi - name: Checkout release branch + env: + GH_TOKEN: ${{ secrets.PROWLER_BOT_ACCESS_TOKEN }} run: | echo "Checking out branch $BRANCH_NAME for release $PROWLER_VERSION..." if git show-ref --verify --quiet "refs/heads/$BRANCH_NAME"; then @@ -92,8 +94,32 @@ jobs: elif git show-ref --verify --quiet "refs/remotes/origin/$BRANCH_NAME"; then echo "Branch $BRANCH_NAME exists remotely, checking out..." git checkout -b "$BRANCH_NAME" "origin/$BRANCH_NAME" + elif [ "$PATCH_VERSION" -eq 0 ]; then + # Only cut the branch once the compiled changelogs are on master + MASTER_SHA=$(git rev-parse origin/master) + VERSION_REGEX="${PROWLER_VERSION//./\\.}" + FOUND_CHANGELOG=false + for changelog in prowler/CHANGELOG.md api/CHANGELOG.md ui/CHANGELOG.md mcp_server/CHANGELOG.md; do + if git show "origin/master:$changelog" 2>/dev/null | grep -qE "^## \[.*\(Prowler v?${VERSION_REGEX}\)"; then + FOUND_CHANGELOG=true + break + fi + done + if [ "$FOUND_CHANGELOG" != "true" ]; then + echo "ERROR: No changelog entry for Prowler $PROWLER_VERSION on master. Merge the compiled changelogs PR first." + exit 1 + fi + # Check the SDK version on master before creating the remote branch + if ! git show "origin/master:pyproject.toml" | grep -qE "^version = \"${VERSION_REGEX}\"$" \ + || ! git show "origin/master:prowler/config/config.py" | grep -qE "^prowler_version = \"${VERSION_REGEX}\"$"; then + echo "ERROR: pyproject.toml or prowler/config/config.py on master is not at version $PROWLER_VERSION. Fix master before creating $BRANCH_NAME." + exit 1 + fi + echo "Branch $BRANCH_NAME does not exist, creating it from master ($MASTER_SHA)..." + gh api --method POST "repos/${GITHUB_REPOSITORY}/git/refs" -f ref="refs/heads/${BRANCH_NAME}" -f sha="${MASTER_SHA}" + git checkout -b "$BRANCH_NAME" "$MASTER_SHA" else - echo "ERROR: Branch $BRANCH_NAME does not exist. For minor releases (X.Y.0), create it manually first. For patch releases (X.Y.Z), the branch should already exist." + echo "ERROR: Branch $BRANCH_NAME does not exist. For patch releases (X.Y.Z), the branch should already exist." exit 1 fi @@ -390,4 +416,3 @@ jobs: if: always() run: | rm -f prowler_changelog.md api_changelog.md ui_changelog.md mcp_changelog.md combined_changelog.md - diff --git a/.github/workflows/release-freeze-lift.yml b/.github/workflows/release-freeze-lift.yml new file mode 100644 index 0000000000..815d9bb268 --- /dev/null +++ b/.github/workflows/release-freeze-lift.yml @@ -0,0 +1,27 @@ +name: 'Tools: Release Freeze Lift' + +run-name: 'Lift release freeze after Prowler ${{ github.event.release.tag_name }}' + +on: + release: + types: + - 'published' + +permissions: {} + +jobs: + release-freeze-lift: + if: github.repository == 'prowler-cloud/prowler' && !github.event.release.prerelease + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - name: Harden the runner (Audit all outbound calls) + uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0 + with: + egress-policy: audit + + - name: Disable release freeze + env: + GH_TOKEN: ${{ secrets.PROWLER_BOT_ACCESS_TOKEN }} + run: | + gh variable set RELEASE_FREEZE --body false --repo "${GITHUB_REPOSITORY}" From cf688e3fa1d8e8701e9420eae438223d6e6cb52c Mon Sep 17 00:00:00 2001 From: Alejandro Bailo <59607668+alejandrobailo@users.noreply.github.com> Date: Thu, 8 Oct 2026 15:17:59 +0200 Subject: [PATCH 12/17] fix(ui): bump Next.js to 16.3.8 to patch the image optimization SSRF advisory (#12987) --- .../next-image-optimization-ssrf.security.md | 1 + ui/dependency-log.json | 6 +- ui/package.json | 2 +- ui/pnpm-lock.yaml | 106 +++++++++--------- ui/pnpm-workspace.yaml | 2 +- 5 files changed, 59 insertions(+), 58 deletions(-) create mode 100644 ui/changelog.d/next-image-optimization-ssrf.security.md diff --git a/ui/changelog.d/next-image-optimization-ssrf.security.md b/ui/changelog.d/next-image-optimization-ssrf.security.md new file mode 100644 index 0000000000..37c141726f --- /dev/null +++ b/ui/changelog.d/next-image-optimization-ssrf.security.md @@ -0,0 +1 @@ +`next` to 16.3.8, patching a server-side request forgery in Image Optimization (GHSA-cjq9-62q9-8jv4) diff --git a/ui/dependency-log.json b/ui/dependency-log.json index b0cdfaa403..d80dfc4344 100644 --- a/ui/dependency-log.json +++ b/ui/dependency-log.json @@ -498,10 +498,10 @@ { "section": "dependencies", "name": "next", - "from": "16.3.3", - "to": "16.3.6", + "from": "16.3.6", + "to": "16.3.8", "strategy": "installed", - "generatedAt": "2026-10-01T09:28:54.112Z" + "generatedAt": "2026-10-08T12:43:06.476Z" }, { "section": "dependencies", diff --git a/ui/package.json b/ui/package.json index a98088d11a..abebdd0637 100644 --- a/ui/package.json +++ b/ui/package.json @@ -98,7 +98,7 @@ "marked": "15.0.12", "modern-screenshot": "4.7.0", "nanoid": "5.1.16", - "next": "16.3.6", + "next": "16.3.8", "next-auth": "5.0.0-beta.32", "next-themes": "0.2.1", "posthog-js": "1.407.2", diff --git a/ui/pnpm-lock.yaml b/ui/pnpm-lock.yaml index b759be8076..2ff1c00270 100644 --- a/ui/pnpm-lock.yaml +++ b/ui/pnpm-lock.yaml @@ -93,7 +93,7 @@ importers: version: 1.2.3 '@next/third-parties': specifier: 16.2.9 - version: 16.2.9(next@16.3.6(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@playwright/test@1.56.1)(@types/node@24.10.8)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) + version: 16.2.9(next@16.3.8(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@playwright/test@1.56.1)(@types/node@24.10.8)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) '@radix-ui/react-alert-dialog': specifier: 1.1.14 version: 1.1.14(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) @@ -168,7 +168,7 @@ importers: version: 3.26.0(react@19.2.7) '@sentry/nextjs': specifier: 10.65.0 - version: 10.65.0(@opentelemetry/core@2.9.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.9.0(@opentelemetry/api@1.9.1))(next@16.3.6(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@playwright/test@1.56.1)(@types/node@24.10.8)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7)(webpack@5.104.1(lightningcss@1.30.2)(postcss@8.5.23)) + version: 10.65.0(@opentelemetry/core@2.9.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.9.0(@opentelemetry/api@1.9.1))(next@16.3.8(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@playwright/test@1.56.1)(@types/node@24.10.8)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7)(webpack@5.104.1(lightningcss@1.30.2)(postcss@8.5.23)) '@tailwindcss/postcss': specifier: 4.1.18 version: 4.1.18 @@ -239,14 +239,14 @@ importers: specifier: 5.1.16 version: 5.1.16 next: - specifier: 16.3.6 - version: 16.3.6(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@playwright/test@1.56.1)(@types/node@24.10.8)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + specifier: 16.3.8 + version: 16.3.8(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@playwright/test@1.56.1)(@types/node@24.10.8)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) next-auth: specifier: 5.0.0-beta.32 - version: 5.0.0-beta.32(next@16.3.6(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@playwright/test@1.56.1)(@types/node@24.10.8)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) + version: 5.0.0-beta.32(next@16.3.8(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@playwright/test@1.56.1)(@types/node@24.10.8)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) next-themes: specifier: 0.2.1 - version: 0.2.1(next@16.3.6(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@playwright/test@1.56.1)(@types/node@24.10.8)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + version: 0.2.1(next@16.3.8(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@playwright/test@1.56.1)(@types/node@24.10.8)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7) posthog-js: specifier: 1.407.2 version: 1.407.2(preact-render-to-string@6.5.11(preact@10.24.3)) @@ -1415,60 +1415,60 @@ packages: '@ndaidong/bellajs@12.0.1': resolution: {integrity: sha512-1iY42uiHz0cxNMbde7O3zVN+ZX1viOOUOBRt6ht6lkRZbSjwOnFV34Zv4URp3hGzEe6L9Byk7BOq/41H0PzAOQ==} - '@next/env@16.3.6': - resolution: {integrity: sha512-x9Vblze1EbtltQYnNH38xCPWU3TVfBd1eXqA3+w9+BTpedkkdNpAaltXlGQ/nsc1+E0mVTNrtcbX3GoO09zeLQ==} + '@next/env@16.3.8': + resolution: {integrity: sha512-Al9zqHVV7TJv0eFuOU4U7Lvv74PTih4Ch63sk2xCIpSTkE3udFnaOcnzP2lQVymiL7yS9Cj2iClUXlR3EQ5sEw==} '@next/eslint-plugin-next@16.2.9': resolution: {integrity: sha512-UZi8+YT/MLgTC9nrrn2Xd4lBYv1B7lVmtWHfPcthAI5Tt/C1LuDe6DfmtCtJ+WQod3ksY4VrKSvk3oMVAnL7qw==} - '@next/swc-darwin-arm64@16.3.6': - resolution: {integrity: sha512-E/7GEqaUkt8mk/T8v9lAnrhzR06kdq1ZBkC12F8tAMkdIadwNp3H1KqHynDHrpcTlGCUdq/qu6vUL2aYVyYBdw==} + '@next/swc-darwin-arm64@16.3.8': + resolution: {integrity: sha512-2JPRMh2nmQG5CiL7cXGL9AGwnPWJQ//cTtAUCT+w511QHk79SYz3LGv/pc5X643B/WEO0rvu3Yww0hqwt3kgeA==} engines: {node: '>= 10'} cpu: [arm64] os: [darwin] - '@next/swc-darwin-x64@16.3.6': - resolution: {integrity: sha512-yBE893/nDWTlaiBD1p+qgt7NUen4U5R6FXyH0s67Npq1S3E0cVSef1WIXC2xBRgQvwAvJq6DnS6Y6PrY0cy4Ew==} + '@next/swc-darwin-x64@16.3.8': + resolution: {integrity: sha512-GZtCCOBKJ4leVIT/Th0llWKhD1ca92lzbQiS5R5ON9QkoiFnilFsebDae1JU2a3HWoKMEmEZWGs1AGLavVM72Q==} engines: {node: '>= 10'} cpu: [x64] os: [darwin] - '@next/swc-linux-arm64-gnu@16.3.6': - resolution: {integrity: sha512-KJDpjBqBPYlvkivmyrp+Qys6k/7ksbqGQvRVc6ZEGfR+cjQxx+nUkJaWmNZJsmoOrqYNbaXByF8wa0lBwDhB3Q==} + '@next/swc-linux-arm64-gnu@16.3.8': + resolution: {integrity: sha512-O659ygeQYqneJ1fBKMpFxIFqYkYswu8IAS1OCKK/4f3ZgJJm1dRz4fVJZRi/kLLWjnBKnebOePA4WNv+sV1pVA==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] libc: [glibc] - '@next/swc-linux-arm64-musl@16.3.6': - resolution: {integrity: sha512-mqNg2K+hvWskSRb/QM+Ix412DvBsuSF0XV+frTSw5vmoucNnIlynFwKYew8D01bfATErMOM7Bujrf0BA5DRKFA==} + '@next/swc-linux-arm64-musl@16.3.8': + resolution: {integrity: sha512-dSjKSyWpzxoO1d3DIZZcP4XJcNaKeLmxQMFOiYl5vuBRMmweIqnAhty8tAmRsvTss779cK1FtYnDMj40e4TQlg==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] libc: [musl] - '@next/swc-linux-x64-gnu@16.3.6': - resolution: {integrity: sha512-nFncBNGAYouRHjRVaITs9beZRfhX4ssVwpnvPIAbkZVH6LtGoAVlH4bJ8Cnf9SOo9bsXgPFer/GdHtEE3JNOkw==} + '@next/swc-linux-x64-gnu@16.3.8': + resolution: {integrity: sha512-lbqOuz3RPRcv+o9msNsJw5x4+Y1ZwPTs6vmL6DCf7i0fZfvng/F59wyeDwqHIvV0mK//RBy/jJkZ+nCKsSMXjQ==} engines: {node: '>= 10'} cpu: [x64] os: [linux] libc: [glibc] - '@next/swc-linux-x64-musl@16.3.6': - resolution: {integrity: sha512-5Mf3cHDGR/Iz0ng2Bj3zUR3p5QS9YK3Hn2QiAfavFmyF48zwThAjpFoiTKNIcOHLYS4zEk+gzyJ/9deQ2ZB8yQ==} + '@next/swc-linux-x64-musl@16.3.8': + resolution: {integrity: sha512-+316WswI8ScVgZeUd+1KGaXkHhaYQzCjvH/05TZSpJ8zBizb1a4G7DtO7F12jcBIqMOtsz9ji1t48fmKtzqsGA==} engines: {node: '>= 10'} cpu: [x64] os: [linux] libc: [musl] - '@next/swc-win32-arm64-msvc@16.3.6': - resolution: {integrity: sha512-0jkJy0C2kbrJWTk4YLa3xk80pVBpx8FCHJym7CnUfDAXe/FWv5qT7SQJbR0KuemyxaEDlEx5WT4VQJoTW+/9Qw==} + '@next/swc-win32-arm64-msvc@16.3.8': + resolution: {integrity: sha512-ji0gd4kMYUxO+1fJBIbiBVRCjzG/lloiyCccnlebvb1ZJ5qXCPZqYg4Jl1DrrixnWNMKylzgpmMWx0yNDYXlzw==} engines: {node: '>= 10'} cpu: [arm64] os: [win32] - '@next/swc-win32-x64-msvc@16.3.6': - resolution: {integrity: sha512-/YXjI1e5OXcZ7YpxRwgP/1jAV/SBKTzeVKqN2mk7mLpcICsyn3Gl5+dIfDTJp70M0ccMhyMMRso4v6mPDCGepg==} + '@next/swc-win32-x64-msvc@16.3.8': + resolution: {integrity: sha512-WcTlaKt/TWkh5kUjdJcUmB1XgZ+1c6fz4Y9fDHL73YNSdGaUWjceeWrrlwF0nv19iABYWC4iAq1oX1w4Bn0vfg==} engines: {node: '>= 10'} cpu: [x64] os: [win32] @@ -5802,8 +5802,8 @@ packages: react: '*' react-dom: '*' - next@16.3.6: - resolution: {integrity: sha512-L+otWM/aQbYTx98aZhgEoMb4bZAXx1YVW4UMA/vuCyCoWG5HJyZUili8QAkqzrcC+5///tsz3s0M+SlyB5bLMw==} + next@16.3.8: + resolution: {integrity: sha512-U7QEZaTini6wKrb8A8hqLLqYQyCetegKjCpJOyxk642vWoMoU1x5PyZCJFvgYgiptA8xc5j/9xYlZFO7w9Sjmw==} engines: {node: '>=20.9.0'} hasBin: true peerDependencies: @@ -8631,39 +8631,39 @@ snapshots: '@ndaidong/bellajs@12.0.1': {} - '@next/env@16.3.6': {} + '@next/env@16.3.8': {} '@next/eslint-plugin-next@16.2.9': dependencies: fast-glob: 3.3.1 - '@next/swc-darwin-arm64@16.3.6': + '@next/swc-darwin-arm64@16.3.8': optional: true - '@next/swc-darwin-x64@16.3.6': + '@next/swc-darwin-x64@16.3.8': optional: true - '@next/swc-linux-arm64-gnu@16.3.6': + '@next/swc-linux-arm64-gnu@16.3.8': optional: true - '@next/swc-linux-arm64-musl@16.3.6': + '@next/swc-linux-arm64-musl@16.3.8': optional: true - '@next/swc-linux-x64-gnu@16.3.6': + '@next/swc-linux-x64-gnu@16.3.8': optional: true - '@next/swc-linux-x64-musl@16.3.6': + '@next/swc-linux-x64-musl@16.3.8': optional: true - '@next/swc-win32-arm64-msvc@16.3.6': + '@next/swc-win32-arm64-msvc@16.3.8': optional: true - '@next/swc-win32-x64-msvc@16.3.6': + '@next/swc-win32-x64-msvc@16.3.8': optional: true - '@next/third-parties@16.2.9(next@16.3.6(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@playwright/test@1.56.1)(@types/node@24.10.8)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7)': + '@next/third-parties@16.2.9(next@16.3.8(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@playwright/test@1.56.1)(@types/node@24.10.8)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7)': dependencies: - next: 16.3.6(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@playwright/test@1.56.1)(@types/node@24.10.8)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + next: 16.3.8(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@playwright/test@1.56.1)(@types/node@24.10.8)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) react: 19.2.7 third-party-capital: 1.0.20 @@ -9830,7 +9830,7 @@ snapshots: dependencies: '@sentry/core': 10.65.0 - '@sentry/nextjs@10.65.0(@opentelemetry/core@2.9.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.9.0(@opentelemetry/api@1.9.1))(next@16.3.6(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@playwright/test@1.56.1)(@types/node@24.10.8)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7)(webpack@5.104.1(lightningcss@1.30.2)(postcss@8.5.23))': + '@sentry/nextjs@10.65.0(@opentelemetry/core@2.9.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.9.0(@opentelemetry/api@1.9.1))(next@16.3.8(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@playwright/test@1.56.1)(@types/node@24.10.8)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7)(webpack@5.104.1(lightningcss@1.30.2)(postcss@8.5.23))': dependencies: '@opentelemetry/api': 1.9.1 '@rollup/plugin-commonjs': 28.0.1(rollup@4.59.0) @@ -9843,7 +9843,7 @@ snapshots: '@sentry/react': 10.65.0(react@19.2.7) '@sentry/vercel-edge': 10.65.0 '@sentry/webpack-plugin': 5.4.0(rollup@4.59.0)(webpack@5.104.1(lightningcss@1.30.2)(postcss@8.5.23)) - next: 16.3.6(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@playwright/test@1.56.1)(@types/node@24.10.8)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + next: 16.3.8(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@playwright/test@1.56.1)(@types/node@24.10.8)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) rollup: 4.59.0 stacktrace-parser: 0.1.11 transitivePeerDependencies: @@ -13438,21 +13438,21 @@ snapshots: neo-async@2.6.2: {} - next-auth@5.0.0-beta.32(next@16.3.6(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@playwright/test@1.56.1)(@types/node@24.10.8)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7): + next-auth@5.0.0-beta.32(next@16.3.8(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@playwright/test@1.56.1)(@types/node@24.10.8)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7): dependencies: '@auth/core': 0.41.3 - next: 16.3.6(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@playwright/test@1.56.1)(@types/node@24.10.8)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + next: 16.3.8(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@playwright/test@1.56.1)(@types/node@24.10.8)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) react: 19.2.7 - next-themes@0.2.1(next@16.3.6(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@playwright/test@1.56.1)(@types/node@24.10.8)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7): + next-themes@0.2.1(next@16.3.8(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@playwright/test@1.56.1)(@types/node@24.10.8)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7): dependencies: - next: 16.3.6(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@playwright/test@1.56.1)(@types/node@24.10.8)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + next: 16.3.8(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@playwright/test@1.56.1)(@types/node@24.10.8)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) react: 19.2.7 react-dom: 19.2.7(react@19.2.7) - next@16.3.6(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@playwright/test@1.56.1)(@types/node@24.10.8)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7): + next@16.3.8(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@playwright/test@1.56.1)(@types/node@24.10.8)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7): dependencies: - '@next/env': 16.3.6 + '@next/env': 16.3.8 '@swc/helpers': 0.5.23 baseline-browser-mapping: 2.10.29 caniuse-lite: 1.0.30001792 @@ -13461,14 +13461,14 @@ snapshots: react-dom: 19.2.7(react@19.2.7) styled-jsx: 5.1.6(@babel/core@7.29.7)(react@19.2.7) optionalDependencies: - '@next/swc-darwin-arm64': 16.3.6 - '@next/swc-darwin-x64': 16.3.6 - '@next/swc-linux-arm64-gnu': 16.3.6 - '@next/swc-linux-arm64-musl': 16.3.6 - '@next/swc-linux-x64-gnu': 16.3.6 - '@next/swc-linux-x64-musl': 16.3.6 - '@next/swc-win32-arm64-msvc': 16.3.6 - '@next/swc-win32-x64-msvc': 16.3.6 + '@next/swc-darwin-arm64': 16.3.8 + '@next/swc-darwin-x64': 16.3.8 + '@next/swc-linux-arm64-gnu': 16.3.8 + '@next/swc-linux-arm64-musl': 16.3.8 + '@next/swc-linux-x64-gnu': 16.3.8 + '@next/swc-linux-x64-musl': 16.3.8 + '@next/swc-win32-arm64-msvc': 16.3.8 + '@next/swc-win32-x64-msvc': 16.3.8 '@opentelemetry/api': 1.9.1 '@playwright/test': 1.56.1 babel-plugin-react-compiler: 1.0.0 diff --git a/ui/pnpm-workspace.yaml b/ui/pnpm-workspace.yaml index f3c870462f..6b262d5e7f 100644 --- a/ui/pnpm-workspace.yaml +++ b/ui/pnpm-workspace.yaml @@ -18,7 +18,7 @@ overrides: "@react-aria/visually-hidden>react": "19.2.7" "@react-aria/interactions>react": "19.2.7" "lodash": "4.18.1" - # Next.js 16.3.6 requests sharp ^0.35.4; pinned to fix + # Next.js 16.3.8 requests sharp ^0.35.4; pinned to fix # GHSA-rgj7-g3m4-5g8c (libheif) and GHSA-wq5f-xc86-pv6w (librsvg). # This override controls resolution; keep it aligned with the direct # dependency in package.json. From 27c4113d29c61c817de933ea3d6225d14e657043 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9sar=20Arroba?= <19954079+cesararroba@users.noreply.github.com> Date: Thu, 8 Oct 2026 15:57:58 +0200 Subject: [PATCH 13/17] fix(aws): query Organizations in the audited partition (#12988) --- ...ws-organizations-partition-region.fixed.md | 1 + prowler/providers/aws/aws_provider.py | 11 +++-- .../aws/lib/organizations/organizations.py | 4 +- tests/providers/aws/aws_provider_test.py | 42 +++++++++++++++++-- .../lib/organizations/organizations_test.py | 11 +++++ 5 files changed, 61 insertions(+), 8 deletions(-) create mode 100644 prowler/changelog.d/aws-organizations-partition-region.fixed.md diff --git a/prowler/changelog.d/aws-organizations-partition-region.fixed.md b/prowler/changelog.d/aws-organizations-partition-region.fixed.md new file mode 100644 index 0000000000..580a69ec05 --- /dev/null +++ b/prowler/changelog.d/aws-organizations-partition-region.fixed.md @@ -0,0 +1 @@ +AWS Organizations metadata is fetched from the partition of the audited account, using the region that answered the STS bootstrap, instead of the commercial `us-east-1` endpoint a session without a region resolves to, which timed out in GovCloud diff --git a/prowler/providers/aws/aws_provider.py b/prowler/providers/aws/aws_provider.py index 37067bb516..f6d5347cb7 100644 --- a/prowler/providers/aws/aws_provider.py +++ b/prowler/providers/aws/aws_provider.py @@ -390,7 +390,7 @@ class AwsProvider(Provider): ) self._organizations_metadata = self.get_organizations_info( - aws_organizations_session, self._identity.account + aws_organizations_session, self._identity.account, sts_region ) # Fallback to the original (pre-assume) session when no explicit @@ -412,7 +412,7 @@ class AwsProvider(Provider): "Retrying AWS Organizations metadata retrieval with the original session" ) self._organizations_metadata = self.get_organizations_info( - self._session.original_session, self._identity.account + self._session.original_session, self._identity.account, sts_region ) ######## @@ -508,7 +508,10 @@ class AwsProvider(Provider): # TODO: This can be moved to another class since it doesn't need self def get_organizations_info( - self, organizations_session: Session, aws_account_id: str + self, + organizations_session: Session, + aws_account_id: str, + region: str | None = None, ) -> AWSOrganizationsInfo: """ get_organizations_info returns a AWSOrganizationsInfo object if the account to be audited is a delegated administrator for AWS Organizations or if the AWS Organizations Role ARN (--organizations-role) is passed. @@ -516,6 +519,7 @@ class AwsProvider(Provider): Args: - organizations_session: needs to be a Session object with permissions to do organizations:DescribeAccount and organizations:ListTagsForResource. - aws_account_id: is the AWS Account ID from which we want to get the AWS Organizations account metadata + - region: a region of the audited partition, so the client reaches that partition's Organizations endpoint Returns: - AWSOrganizationsInfo object with the AWS Organizations metadata for the account to be audited. @@ -529,6 +533,7 @@ class AwsProvider(Provider): get_organizations_metadata( aws_account_id=aws_account_id, session=organizations_session, + region=region, ) ) diff --git a/prowler/providers/aws/lib/organizations/organizations.py b/prowler/providers/aws/lib/organizations/organizations.py index 5e152a790b..153ca46e7b 100644 --- a/prowler/providers/aws/lib/organizations/organizations.py +++ b/prowler/providers/aws/lib/organizations/organizations.py @@ -42,9 +42,11 @@ def _get_ou_metadata(organizations_client, account_id): def get_organizations_metadata( aws_account_id: str, session: session.Session, + region: str | None = None, ) -> tuple[dict, dict, dict]: try: - organizations_client = session.client("organizations") + # Without a region botocore resolves the commercial endpoint, whatever the partition + organizations_client = session.client("organizations", region_name=region) organizations_metadata = organizations_client.describe_account( AccountId=aws_account_id diff --git a/tests/providers/aws/aws_provider_test.py b/tests/providers/aws/aws_provider_test.py index 2cf9d828b5..0f21877b65 100644 --- a/tests/providers/aws/aws_provider_test.py +++ b/tests/providers/aws/aws_provider_test.py @@ -490,10 +490,10 @@ class TestAWSProvider: captured_sessions = [] original_get_organizations_info = AwsProvider.get_organizations_info - def capture(self, organizations_session, aws_account_id): + def capture(self, organizations_session, aws_account_id, region=None): captured_sessions.append(organizations_session) return original_get_organizations_info( - self, organizations_session, aws_account_id + self, organizations_session, aws_account_id, region ) with patch.object(AwsProvider, "get_organizations_info", capture): @@ -513,10 +513,10 @@ class TestAWSProvider: captured_sessions = [] original_get_organizations_info = AwsProvider.get_organizations_info - def capture(self, organizations_session, aws_account_id): + def capture(self, organizations_session, aws_account_id, region=None): captured_sessions.append(organizations_session) return original_get_organizations_info( - self, organizations_session, aws_account_id + self, organizations_session, aws_account_id, region ) with patch.object(AwsProvider, "get_organizations_info", capture): @@ -2132,6 +2132,40 @@ aws: assert sts_regions == [AWS_REGION_GOV_CLOUD_US_WEST_1] + @mock_aws + def test_aws_provider_queries_organizations_where_validation_got_an_answer( + self, monkeypatch + ): + monkeypatch.setenv("PROWLER_AWS_PARTITION", AWS_GOV_CLOUD_PARTITION) + # No region in the session: Organizations would resolve to us-east-1 + monkeypatch.delenv("AWS_DEFAULT_REGION", raising=False) + monkeypatch.delenv("AWS_REGION", raising=False) + answered = AWSCallerIdentity( + user_id="test-user-id", + account=AWS_ACCOUNT_NUMBER, + arn=ARN(AWS_GOV_CLOUD_ACCOUNT_ARN), + region=AWS_REGION_GOV_CLOUD_US_WEST_1, + ) + organizations_regions = [] + + def get_organizations_metadata(aws_account_id, session, region=None): + organizations_regions.append(region) + return {}, {}, {} + + with ( + patch( + "prowler.providers.aws.aws_provider.AwsProvider.validate_credentials", + return_value=answered, + ), + patch( + "prowler.providers.aws.aws_provider.get_organizations_metadata", + side_effect=get_organizations_metadata, + ), + ): + AwsProvider() + + assert organizations_regions == [AWS_REGION_GOV_CLOUD_US_WEST_1] + @mock_aws def test_test_connection_with_env_credentials(self, monkeypatch): # Create a mock IAM user diff --git a/tests/providers/aws/lib/organizations/organizations_test.py b/tests/providers/aws/lib/organizations/organizations_test.py index a2d8ad0441..a49199b15b 100644 --- a/tests/providers/aws/lib/organizations/organizations_test.py +++ b/tests/providers/aws/lib/organizations/organizations_test.py @@ -238,6 +238,17 @@ class Test_AWS_Organizations: assert default_config is not None assert BOTO3_USER_AGENT_EXTRA in default_config.user_agent_extra + def test_get_organizations_metadata_uses_the_given_region(self): + # A session with no region resolves Organizations to the commercial + # global endpoint, unreachable from GovCloud + wrapper = MagicMock(wraps=boto3.Session()) + + get_organizations_metadata("123456789012", wrapper, region="us-gov-west-1") + + wrapper.client.assert_called_once_with( + "organizations", region_name="us-gov-west-1" + ) + def test_parse_organizations_metadata_with_empty_ou_metadata(self): tags = {"Tags": []} metadata = { From f44ea4b6fa5faf0be1d466273c31397d8a4bfbd7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pedro=20Mart=C3=ADn?= Date: Thu, 8 Oct 2026 18:15:10 +0200 Subject: [PATCH 14/17] fix(api): require inline kubeconfig credentials (#12952) Co-authored-by: alejandrobailo --- .../kubeconfig-inline-credentials.security.md | 1 + api/src/backend/api/tests/test_serializers.py | 274 ++++++++++++++++++ api/src/backend/api/tests/test_utils.py | 100 ++++++- api/src/backend/api/utils.py | 20 +- .../api/v1/serializer_utils/providers.py | 3 +- api/src/backend/api/v1/serializers.py | 69 +++-- .../backend/tasks/tests/test_connection.py | 42 ++- .../kubernetes/getting-started-k8s.mdx | 4 +- .../kubeconfig-inline-credentials.security.md | 1 + .../via-credentials/k8s-credentials-form.tsx | 13 +- ui/types/formSchemas.test.ts | 122 +++++++- ui/types/formSchemas.ts | 87 ++++-- 12 files changed, 664 insertions(+), 72 deletions(-) create mode 100644 api/changelog.d/kubeconfig-inline-credentials.security.md create mode 100644 ui/changelog.d/kubeconfig-inline-credentials.security.md diff --git a/api/changelog.d/kubeconfig-inline-credentials.security.md b/api/changelog.d/kubeconfig-inline-credentials.security.md new file mode 100644 index 0000000000..8d6115d4c3 --- /dev/null +++ b/api/changelog.d/kubeconfig-inline-credentials.security.md @@ -0,0 +1 @@ +Kubernetes kubeconfig validation accepts only inline credentials and known cluster fields, rejecting file-path fields (`tokenFile`, `client-certificate`, `client-key`, `certificate-authority`), command-based authentication (`exec`, `cmd-path`), `auth-provider` directives, proxy URLs and unknown user or cluster keys; stored kubeconfigs are re-validated before each scan and connection test diff --git a/api/src/backend/api/tests/test_serializers.py b/api/src/backend/api/tests/test_serializers.py index 0d7f042434..903fb05909 100644 --- a/api/src/backend/api/tests/test_serializers.py +++ b/api/src/backend/api/tests/test_serializers.py @@ -5,6 +5,8 @@ from api.v1.serializer_utils.integrations import ( ) from api.v1.serializer_utils.providers import ProviderSecretField from api.v1.serializers import ( + KUBERNETES_KUBECONFIG_INVALID_ERROR, + KUBERNETES_KUBECONFIG_NON_INLINE_CREDENTIALS_ERROR, ImageProviderSecret, IntegrationSerializer, IntegrationUpdateSerializer, @@ -340,6 +342,278 @@ current-context: test-context assert not serializer.is_valid() assert "kubeconfig_content" in serializer.errors + @staticmethod + def _kubeconfig(user_extra="", cluster_extra=""): + return f""" +apiVersion: v1 +kind: Config +clusters: + - name: test-cluster + cluster: + server: https://kubernetes.example.test +{cluster_extra} +users: + - name: test-user + user: + token: test-token +{user_extra} +contexts: + - name: test-context + context: + cluster: test-cluster + user: test-user +current-context: test-context +""" + + @staticmethod + def _assert_rejected_without_echo( + kubeconfig_content, leaked_value, expected_message + ): + serializer = KubernetesProviderSecret( + data={"kubeconfig_content": kubeconfig_content} + ) + + assert serializer.is_valid() is False + assert serializer.errors["kubeconfig_content"] == [expected_message] + assert leaked_value not in str(serializer.errors) + + def test_inline_token_with_certificate_authority_data_is_accepted(self): + kubeconfig_content = self._kubeconfig( + cluster_extra=" certificate-authority-data: dGVzdA==" + ) + + serializer = KubernetesProviderSecret( + data={"kubeconfig_content": kubeconfig_content} + ) + + assert serializer.is_valid() + + def test_kubeconfig_with_token_file_is_rejected(self): + self._assert_rejected_without_echo( + self._kubeconfig(user_extra=" tokenFile: /etc/passwd"), + "/etc/passwd", + KUBERNETES_KUBECONFIG_NON_INLINE_CREDENTIALS_ERROR, + ) + + def test_kubeconfig_with_client_certificate_file_is_rejected(self): + self._assert_rejected_without_echo( + self._kubeconfig(user_extra=" client-certificate: /etc/passwd"), + "/etc/passwd", + KUBERNETES_KUBECONFIG_NON_INLINE_CREDENTIALS_ERROR, + ) + + def test_kubeconfig_with_client_key_file_is_rejected(self): + self._assert_rejected_without_echo( + self._kubeconfig(user_extra=" client-key: /etc/passwd"), + "/etc/passwd", + KUBERNETES_KUBECONFIG_NON_INLINE_CREDENTIALS_ERROR, + ) + + def test_kubeconfig_with_certificate_authority_file_is_rejected(self): + self._assert_rejected_without_echo( + self._kubeconfig(cluster_extra=" certificate-authority: /etc/passwd"), + "/etc/passwd", + KUBERNETES_KUBECONFIG_NON_INLINE_CREDENTIALS_ERROR, + ) + + def test_kubeconfig_with_cluster_proxy_url_is_rejected(self): + self._assert_rejected_without_echo( + self._kubeconfig(cluster_extra=" proxy-url: http://proxy.evil.test"), + "proxy.evil.test", + KUBERNETES_KUBECONFIG_NON_INLINE_CREDENTIALS_ERROR, + ) + + @pytest.mark.parametrize("provider_name", ["gcp", "oidc", "azure"]) + def test_kubeconfig_with_any_auth_provider_is_rejected(self, provider_name): + self._assert_rejected_without_echo( + self._kubeconfig( + user_extra=( + " auth-provider:\n" + f" name: {provider_name}\n" + " config:\n" + " idp-issuer-url: https://idp.evil.test" + ) + ), + "idp.evil.test", + KUBERNETES_KUBECONFIG_NON_INLINE_CREDENTIALS_ERROR, + ) + + def test_kubeconfig_with_unknown_user_key_is_rejected(self): + self._assert_rejected_without_echo( + self._kubeconfig(user_extra=" bogus: /etc/passwd"), + "/etc/passwd", + KUBERNETES_KUBECONFIG_NON_INLINE_CREDENTIALS_ERROR, + ) + + def test_kubeconfig_with_unknown_cluster_key_is_rejected(self): + self._assert_rejected_without_echo( + self._kubeconfig(cluster_extra=" bogus: /etc/passwd"), + "/etc/passwd", + KUBERNETES_KUBECONFIG_NON_INLINE_CREDENTIALS_ERROR, + ) + + def test_kubeconfig_with_allowlisted_cluster_keys_is_accepted(self): + serializer = KubernetesProviderSecret( + data={ + "kubeconfig_content": self._kubeconfig( + cluster_extra=( + " certificate-authority-data: dGVzdA==\n" + " insecure-skip-tls-verify: false\n" + " tls-server-name: kubernetes.example.test\n" + " disable-compression: true\n" + " extensions: []" + ) + ) + } + ) + + assert serializer.is_valid(), serializer.errors + + def test_kubeconfig_with_non_list_clusters_is_rejected(self): + kubeconfig_content = """ +apiVersion: v1 +kind: Config +clusters: + name: /etc/passwd +""" + self._assert_rejected_without_echo( + kubeconfig_content, "/etc/passwd", KUBERNETES_KUBECONFIG_INVALID_ERROR + ) + + def test_kubeconfig_with_non_dict_cluster_entry_is_rejected(self): + kubeconfig_content = """ +apiVersion: v1 +kind: Config +clusters: + - /etc/passwd +""" + self._assert_rejected_without_echo( + kubeconfig_content, "/etc/passwd", KUBERNETES_KUBECONFIG_INVALID_ERROR + ) + + def test_kubeconfig_with_non_dict_cluster_value_is_rejected(self): + kubeconfig_content = """ +apiVersion: v1 +kind: Config +clusters: + - name: test-cluster + cluster: /etc/passwd +""" + self._assert_rejected_without_echo( + kubeconfig_content, "/etc/passwd", KUBERNETES_KUBECONFIG_INVALID_ERROR + ) + + @pytest.mark.parametrize( + "user_extra", + [ + " token: test-token", + " username: test-user\n password: test-password", + " client-certificate-data: dGVzdA==\n client-key-data: dGVzdA==", + " token: test-token\n as: other-user", + " token: test-token\n as-uid: 1234", + " token: test-token\n as-groups:\n - group-a", + " token: test-token\n as-user-extra:\n scopes:\n - read", + ], + ids=[ + "token", + "username-password", + "client-cert-data", + "as", + "as-uid", + "as-groups", + "as-user-extra", + ], + ) + def test_kubeconfig_with_allowlisted_user_keys_is_accepted(self, user_extra): + kubeconfig_content = f""" +apiVersion: v1 +kind: Config +clusters: + - name: test-cluster + cluster: + server: https://kubernetes.example.test + certificate-authority-data: dGVzdA== +users: + - name: test-user + user: +{user_extra} +contexts: + - name: test-context + context: + cluster: test-cluster + user: test-user +current-context: test-context +""" + + serializer = KubernetesProviderSecret( + data={"kubeconfig_content": kubeconfig_content} + ) + + assert serializer.is_valid() + + @pytest.mark.parametrize( + "kubeconfig_content", + [ + """ +apiVersion: v1 +kind: Config +clusters: + - name: test-cluster + cluster: + server: https://kubernetes.example.test +users: + - name: good-user + user: + token: test-token + - name: bad-user + user: + tokenFile: /etc/passwd +""", + """ +apiVersion: v1 +kind: Config +clusters: + - name: good-cluster + cluster: + server: https://kubernetes.example.test + - name: bad-cluster + cluster: + server: https://kubernetes.example.test + certificate-authority: /etc/passwd +users: + - name: test-user + user: + token: test-token +""", + ], + ids=["second-user", "second-cluster"], + ) + def test_kubeconfig_with_bad_second_entry_is_rejected(self, kubeconfig_content): + self._assert_rejected_without_echo( + kubeconfig_content, + "/etc/passwd", + KUBERNETES_KUBECONFIG_NON_INLINE_CREDENTIALS_ERROR, + ) + + def test_kubeconfig_with_indented_document_is_rejected(self): + kubeconfig_content = ( + " wrapper:\n" + " users:\n" + " - name: u\n" + " user:\n" + " tokenFile: /etc/passwd\n" + " clusters:\n" + " - name: c\n" + " cluster:\n" + " server: https://example.test\n" + ) + + self._assert_rejected_without_echo( + kubeconfig_content, + "/etc/passwd", + KUBERNETES_KUBECONFIG_NON_INLINE_CREDENTIALS_ERROR, + ) + def test_malformed_kubeconfig_is_rejected(self): serializer = KubernetesProviderSecret( data={"kubeconfig_content": "apiVersion: ["} diff --git a/api/src/backend/api/tests/test_utils.py b/api/src/backend/api/tests/test_utils.py index a6d2c10f24..3ef59ea4a5 100644 --- a/api/src/backend/api/tests/test_utils.py +++ b/api/src/backend/api/tests/test_utils.py @@ -14,6 +14,7 @@ from api.utils import ( return_prowler_provider, validate_invitation, ) +from api.v1.serializers import KUBERNETES_KUBECONFIG_NON_INLINE_CREDENTIALS_ERROR from prowler.providers.alibabacloud.alibabacloud_provider import AlibabacloudProvider from prowler.providers.aws.aws_provider import AwsProvider from prowler.providers.aws.lib.security_hub.security_hub import SecurityHubConnection @@ -35,6 +36,34 @@ from prowler.providers.oraclecloud.oraclecloud_provider import OraclecloudProvid from prowler.providers.vercel.vercel_provider import VercelProvider from rest_framework.exceptions import NotFound, ValidationError +VALID_KUBECONFIG = """\ +apiVersion: v1 +kind: Config +clusters: +- name: test-cluster + cluster: + server: https://example.invalid:6443 + certificate-authority-data: Y2E= +users: +- name: test-user + user: + token: test-token +contexts: +- name: test-context + context: + cluster: test-cluster + user: test-user +current-context: test-context +""" + +TOKEN_FILE_KUBECONFIG = VALID_KUBECONFIG.replace( + "token: test-token", "tokenFile: /etc/passwd" +) + +EXEC_KUBECONFIG = VALID_KUBECONFIG.replace( + "token: test-token", "exec:\n command: test-command" +) + class TestMergeDicts: def test_simple_merge(self): @@ -288,6 +317,40 @@ class TestProwlerProviderConnectionTest: raise_on_exception=False, ) + @patch("api.utils.return_prowler_provider") + def test_kubernetes_connection_test_rejects_invalid_kubeconfig( + self, mock_return_prowler_provider + ): + provider = MagicMock() + provider.provider = Provider.ProviderChoices.KUBERNETES.value + provider.uid = "provider_uid" + provider.secret.secret = {"kubeconfig_content": TOKEN_FILE_KUBECONFIG} + + connection = prowler_provider_connection_test(provider) + + assert connection.is_connected is False + assert ( + str(connection.error) == KUBERNETES_KUBECONFIG_NON_INLINE_CREDENTIALS_ERROR + ) + mock_return_prowler_provider.return_value.test_connection.assert_not_called() + + @patch("api.utils.return_prowler_provider") + def test_kubernetes_connection_test_with_valid_kubeconfig( + self, mock_return_prowler_provider + ): + provider = MagicMock() + provider.provider = Provider.ProviderChoices.KUBERNETES.value + provider.uid = "provider_uid" + provider.secret.secret = {"kubeconfig_content": VALID_KUBECONFIG} + + prowler_provider_connection_test(provider) + + mock_return_prowler_provider.return_value.test_connection.assert_called_once_with( + kubeconfig_content=VALID_KUBECONFIG, + provider_id="provider_uid", + raise_on_exception=False, + ) + @pytest.mark.django_db @patch("api.utils.return_prowler_provider") def test_prowler_provider_connection_test_without_secret( @@ -405,10 +468,6 @@ class TestGetProwlerProviderKwargs: Provider.ProviderChoices.GOOGLEWORKSPACE.value, {}, ), - ( - Provider.ProviderChoices.KUBERNETES.value, - {"context": "provider_uid"}, - ), ( Provider.ProviderChoices.M365.value, {}, @@ -459,6 +518,39 @@ class TestGetProwlerProviderKwargs: expected_result = {**secret_dict, **expected_extra_kwargs} assert result == expected_result + def test_get_prowler_provider_kwargs_kubernetes_valid_kubeconfig(self): + provider = MagicMock() + provider.provider = Provider.ProviderChoices.KUBERNETES.value + provider.uid = "provider_uid" + provider.secret.secret = {"kubeconfig_content": VALID_KUBECONFIG} + + assert get_prowler_provider_kwargs(provider) == { + "kubeconfig_content": VALID_KUBECONFIG, + "context": "provider_uid", + } + + @pytest.mark.parametrize( + "secret", + [ + {"kubeconfig_content": TOKEN_FILE_KUBECONFIG}, + {"kubeconfig_content": EXEC_KUBECONFIG}, + {"kubeconfig_content": "[]"}, + {"key": "value"}, + ], + ) + def test_get_prowler_provider_kwargs_kubernetes_rejects_invalid_kubeconfig( + self, secret + ): + provider = MagicMock() + provider.provider = Provider.ProviderChoices.KUBERNETES.value + provider.uid = "provider_uid" + provider.secret.secret = secret + + with pytest.raises(ValidationError) as exc_info: + get_prowler_provider_kwargs(provider) + + assert "/etc/passwd" not in str(exc_info.value) + def test_get_prowler_provider_kwargs_oraclecloud_maps_region_to_home_region( self, ): diff --git a/api/src/backend/api/utils.py b/api/src/backend/api/utils.py index a48c8c13a4..37752ce28c 100644 --- a/api/src/backend/api/utils.py +++ b/api/src/backend/api/utils.py @@ -17,7 +17,7 @@ from api.models import ( Role, UserRoleRelationship, ) -from api.v1.serializers import FindingMetadataSerializer +from api.v1.serializers import FindingMetadataSerializer, KubernetesProviderSecret from django.contrib.postgres.aggregates import ArrayAgg from django.db import transaction from django.db.models import Subquery @@ -201,6 +201,13 @@ def return_prowler_provider( return prowler_provider +def _validate_kubernetes_secret(secret: dict) -> None: + """Re-validate a stored kubeconfig at use time; errors never echo its content.""" + KubernetesProviderSecret( + data={"kubeconfig_content": secret.get("kubeconfig_content")} + ).is_valid(raise_exception=True) + + def get_prowler_provider_kwargs( provider: Provider, mutelist_processor: Processor | None = None ) -> dict: @@ -225,6 +232,7 @@ def get_prowler_provider_kwargs( "project_ids": [provider.uid], } elif provider.provider == Provider.ProviderChoices.KUBERNETES.value: + _validate_kubernetes_secret(prowler_provider_kwargs) prowler_provider_kwargs = {**prowler_provider_kwargs, "context": provider.uid} elif provider.provider == Provider.ProviderChoices.GITHUB.value: if provider.uid: @@ -392,6 +400,16 @@ def prowler_provider_connection_test(provider: Provider) -> Connection: except Provider.secret.RelatedObjectDoesNotExist as secret_error: return Connection(is_connected=False, error=secret_error) + if provider.provider == Provider.ProviderChoices.KUBERNETES.value: + # A rejected stored kubeconfig is a failed connection, not a task error. + try: + _validate_kubernetes_secret(prowler_provider_kwargs) + except ValidationError as validation_error: + return Connection( + is_connected=False, + error=Exception(validation_error.detail["kubeconfig_content"][0]), + ) + # For IaC provider, construct the kwargs properly for test_connection if provider.provider == Provider.ProviderChoices.IAC.value: # Don't pass repository_url from secret, use scan_repository_url with the UID diff --git a/api/src/backend/api/v1/serializer_utils/providers.py b/api/src/backend/api/v1/serializer_utils/providers.py index 92ce6d7d7b..cecf3d6f00 100644 --- a/api/src/backend/api/v1/serializer_utils/providers.py +++ b/api/src/backend/api/v1/serializer_utils/providers.py @@ -214,7 +214,8 @@ from rest_framework_json_api import serializers "kubeconfig_content": { "type": "string", "description": "The content of the Kubernetes kubeconfig file, encoded as a string. " - "Kubeconfig command-based authentication is not supported in Prowler Cloud for security reasons.", + "Only inline credentials are supported: token, username/password, or client-certificate-data with client-key-data for users, and certificate-authority-data for clusters. " + "File-path and auth-provider kubeconfig fields are not supported.", } }, "required": ["kubeconfig_content"], diff --git a/api/src/backend/api/v1/serializers.py b/api/src/backend/api/v1/serializers.py index 5b7e061561..c3d6622b42 100644 --- a/api/src/backend/api/v1/serializers.py +++ b/api/src/backend/api/v1/serializers.py @@ -1659,14 +1659,43 @@ class FindingMetadataSerializer(BaseSerializerV1): # Provider secrets -KUBERNETES_KUBECONFIG_UNSUPPORTED_COMMAND_AUTH_ERROR = ( - "Kubernetes kubeconfig command-based authentication is not supported in " - "Prowler Cloud for security reasons." +KUBERNETES_KUBECONFIG_NON_INLINE_CREDENTIALS_ERROR = ( + "Only inline Kubernetes credentials are supported. " + "For user authentication use token, username/password, or client-certificate-data with client-key-data; " + "clusters may use certificate-authority-data. " + "File-path fields (tokenFile, client-certificate, client-key, certificate-authority), " + "command-based authentication (exec, cmd-path), auth-provider directives, and proxy URLs are not supported." ) KUBERNETES_KUBECONFIG_INVALID_ERROR = "Invalid Kubernetes kubeconfig content." -def kubeconfig_contains_unsupported_command_auth(kubeconfig: dict) -> bool: +KUBECONFIG_ALLOWED_USER_KEYS = frozenset( + { + "token", + "username", + "password", + "client-certificate-data", + "client-key-data", + "as", + "as-uid", + "as-groups", + "as-user-extra", + } +) +KUBECONFIG_ALLOWED_CLUSTER_KEYS = frozenset( + { + "server", + "certificate-authority-data", + "insecure-skip-tls-verify", + "tls-server-name", + "disable-compression", + "extensions", + } +) + + +def kubeconfig_is_inline_only_credentials(kubeconfig: dict) -> bool: + """Accept only inline credentials; reject file-reference and auth-provider fields.""" users = kubeconfig.get("users", []) if not isinstance(users, list): raise ValidationError(KUBERNETES_KUBECONFIG_INVALID_ERROR) @@ -1679,21 +1708,25 @@ def kubeconfig_contains_unsupported_command_auth(kubeconfig: dict) -> bool: if not isinstance(user, dict): raise ValidationError(KUBERNETES_KUBECONFIG_INVALID_ERROR) - if "exec" in user: - return True + if any(key not in KUBECONFIG_ALLOWED_USER_KEYS for key in user): + return False - auth_provider = user.get("auth-provider", {}) - if not isinstance(auth_provider, dict): - continue + clusters = kubeconfig.get("clusters", []) + if not isinstance(clusters, list): + raise ValidationError(KUBERNETES_KUBECONFIG_INVALID_ERROR) - auth_provider_config = auth_provider.get("config", {}) - if not isinstance(auth_provider_config, dict): - continue + for cluster_entry in clusters: + if not isinstance(cluster_entry, dict): + raise ValidationError(KUBERNETES_KUBECONFIG_INVALID_ERROR) - if "cmd-path" in auth_provider_config: - return True + cluster = cluster_entry.get("cluster", {}) + if not isinstance(cluster, dict): + raise ValidationError(KUBERNETES_KUBECONFIG_INVALID_ERROR) - return False + if any(key not in KUBECONFIG_ALLOWED_CLUSTER_KEYS for key in cluster): + return False + + return True class BaseWriteProviderSecretSerializer(BaseWriteSerializer): @@ -1876,7 +1909,7 @@ class MongoDBAtlasProviderSecret(serializers.Serializer): class KubernetesProviderSecret(serializers.Serializer): - kubeconfig_content = serializers.CharField() + kubeconfig_content = serializers.CharField(trim_whitespace=False) def validate_kubeconfig_content(self, kubeconfig_content): try: @@ -1889,9 +1922,9 @@ class KubernetesProviderSecret(serializers.Serializer): if not isinstance(kubeconfig, dict): raise serializers.ValidationError(KUBERNETES_KUBECONFIG_INVALID_ERROR) - if kubeconfig_contains_unsupported_command_auth(kubeconfig): + if not kubeconfig_is_inline_only_credentials(kubeconfig): raise serializers.ValidationError( - KUBERNETES_KUBECONFIG_UNSUPPORTED_COMMAND_AUTH_ERROR + KUBERNETES_KUBECONFIG_NON_INLINE_CREDENTIALS_ERROR ) return kubeconfig_content diff --git a/api/src/backend/tasks/tests/test_connection.py b/api/src/backend/tasks/tests/test_connection.py index e8b27e0b00..032d34f787 100644 --- a/api/src/backend/tasks/tests/test_connection.py +++ b/api/src/backend/tasks/tests/test_connection.py @@ -3,7 +3,8 @@ from datetime import UTC, datetime from unittest.mock import MagicMock, patch import pytest -from api.models import Integration, LighthouseConfiguration, Provider +from api.models import Integration, LighthouseConfiguration, Provider, ProviderSecret +from api.v1.serializers import KUBERNETES_KUBECONFIG_NON_INLINE_CREDENTIALS_ERROR from tasks.jobs.connection import ( check_integration_connection, check_lighthouse_connection, @@ -65,17 +66,52 @@ def test_check_provider_connection_exception( mock_provider_connection_test.return_value = MagicMock() mock_provider_connection_test.return_value.is_connected = False - mock_provider_connection_test.return_value.error = Exception() + mock_provider_connection_test.return_value.error = Exception( + "Unable to assume role" + ) result = check_provider_connection(provider_id="provider_id") assert result["connected"] is False - assert result["error"] is not None + assert result["error"] == "Unable to assume role" mock_provider_instance.save.assert_called_once() assert mock_provider_instance.connected is False +@pytest.mark.django_db +def test_check_provider_connection_with_stored_non_inline_kubeconfig( + kubernetes_provider, +): + """A kubeconfig stored before the inline-only rule marks the provider as disconnected.""" + kubernetes_provider.connected = True + kubernetes_provider.save() + ProviderSecret.objects.create( + tenant_id=kubernetes_provider.tenant_id, + provider=kubernetes_provider, + secret_type=ProviderSecret.TypeChoices.STATIC, + secret={ + "kubeconfig_content": ( + "apiVersion: v1\n" + "kind: Config\n" + "users:\n" + "- name: test-user\n" + " user:\n" + " tokenFile: /etc/passwd\n" + ) + }, + ) + + result = check_provider_connection(provider_id=str(kubernetes_provider.id)) + + kubernetes_provider.refresh_from_db() + assert result == { + "connected": False, + "error": KUBERNETES_KUBECONFIG_NON_INLINE_CREDENTIALS_ERROR, + } + assert kubernetes_provider.connected is False + + @pytest.mark.parametrize( "lighthouse_data", [ diff --git a/docs/user-guide/providers/kubernetes/getting-started-k8s.mdx b/docs/user-guide/providers/kubernetes/getting-started-k8s.mdx index c62ec2b8a1..9ab064ef98 100644 --- a/docs/user-guide/providers/kubernetes/getting-started-k8s.mdx +++ b/docs/user-guide/providers/kubernetes/getting-started-k8s.mdx @@ -24,7 +24,7 @@ title: 'Getting Started with Kubernetes' For Kubernetes, Prowler Cloud uses a `kubeconfig` file to authenticate. Paste the contents of your `kubeconfig` file into the `Kubeconfig content` field. -Kubeconfigs that use `users[].user.exec` authentication are not supported in Prowler Cloud or Prowler Local Server. For security reasons, Prowler Cloud does not run commands declared by uploaded kubeconfigs. Use kubeconfig credentials that do not rely on `exec` authentication, such as the ServiceAccount token flow documented below. +Only inline kubeconfig credentials are supported: `token`, `username`/`password`, or `client-certificate-data` with `client-key-data` for users, and `certificate-authority-data` for clusters. Kubeconfigs that reference external files (`tokenFile`, `client-certificate`, `client-key`, `certificate-authority`), run commands (`exec`, `cmd-path`), use `auth-provider` directives, set a proxy URL, or include unsupported user or cluster fields are rejected. Impersonation fields (`as`, `as-uid`, `as-groups`, `as-user-extra`) are accepted but not applied, so Prowler connects with the identity of the user credentials. Use inline credentials such as the ServiceAccount token flow documented below. By default, the `kubeconfig` file is located at `~/.kube/config`. @@ -96,6 +96,8 @@ If you are adding an **EKS**, **GKE**, **AKS** or external cluster, follow these kubectl config set-context --user=prowler-sa ``` + Prowler validates the entire kubeconfig file and requires it to contain only the ServiceAccount user and its cluster entry with certificate-authority-data; remove any exec entries from EKS, GKE, or AKS or Prowler will reject the file. + Replace `` with the generated token and `` with your KubeConfig Context Name of your EKS, GKE or AKS cluster. 4. Add the modified `kubeconfig` in Prowler Cloud and test the connection. diff --git a/ui/changelog.d/kubeconfig-inline-credentials.security.md b/ui/changelog.d/kubeconfig-inline-credentials.security.md new file mode 100644 index 0000000000..051869feec --- /dev/null +++ b/ui/changelog.d/kubeconfig-inline-credentials.security.md @@ -0,0 +1 @@ +Kubernetes kubeconfig form validation accepts only inline credentials and known cluster fields, rejecting file-path, command-based, `auth-provider`, proxy URL and unknown fields before submission diff --git a/ui/components/providers/workflow/forms/via-credentials/k8s-credentials-form.tsx b/ui/components/providers/workflow/forms/via-credentials/k8s-credentials-form.tsx index 2c3e84d5ff..f6d300a5ea 100644 --- a/ui/components/providers/workflow/forms/via-credentials/k8s-credentials-form.tsx +++ b/ui/components/providers/workflow/forms/via-credentials/k8s-credentials-form.tsx @@ -5,8 +5,8 @@ import { Control, useWatch } from "react-hook-form"; import { WizardTextareaField } from "@/components/providers/workflow/forms/fields"; import { KubernetesCredentials } from "@/types"; import { - KUBECONFIG_UNSUPPORTED_COMMAND_AUTHENTICATION_ERROR, - kubeconfigContainsUnsupportedCommandAuthentication, + KUBECONFIG_NON_INLINE_CREDENTIALS_ERROR, + kubeconfigIsInlineOnlyCredentials, } from "@/types/formSchemas"; export const KubernetesCredentialsForm = ({ @@ -18,8 +18,9 @@ export const KubernetesCredentialsForm = ({ control, name: "kubeconfig_content", }); - const hasUnsupportedCommandAuthentication = - kubeconfigContainsUnsupportedCommandAuthentication(kubeconfigContent ?? ""); + const hasUnsupportedCredentials = !kubeconfigIsInlineOnlyCredentials( + kubeconfigContent ?? "", + ); return ( <> @@ -41,9 +42,9 @@ export const KubernetesCredentialsForm = ({ minRows={10} isRequired /> - {hasUnsupportedCommandAuthentication && ( + {hasUnsupportedCredentials && (

- {KUBECONFIG_UNSUPPORTED_COMMAND_AUTHENTICATION_ERROR} + {KUBECONFIG_NON_INLINE_CREDENTIALS_ERROR}

)} diff --git a/ui/types/formSchemas.test.ts b/ui/types/formSchemas.test.ts index 8206ed3311..c9b443a0bb 100644 --- a/ui/types/formSchemas.test.ts +++ b/ui/types/formSchemas.test.ts @@ -7,7 +7,7 @@ import { addCredentialsFormSchema, addCredentialsRoleFormSchema, addProviderFormSchema, - KUBECONFIG_UNSUPPORTED_COMMAND_AUTHENTICATION_ERROR, + KUBECONFIG_NON_INLINE_CREDENTIALS_ERROR, roleFormSchema, samlConfigFormSchema, } from "./formSchemas"; @@ -222,7 +222,7 @@ users: expect(result.error.issues).toContainEqual( expect.objectContaining({ path: [ProviderCredentialFields.KUBECONFIG_CONTENT], - message: KUBECONFIG_UNSUPPORTED_COMMAND_AUTHENTICATION_ERROR, + message: KUBECONFIG_NON_INLINE_CREDENTIALS_ERROR, }), ); }); @@ -249,17 +249,42 @@ users: expect(result.error.issues).toContainEqual( expect.objectContaining({ path: [ProviderCredentialFields.KUBECONFIG_CONTENT], - message: KUBECONFIG_UNSUPPORTED_COMMAND_AUTHENTICATION_ERROR, + message: KUBECONFIG_NON_INLINE_CREDENTIALS_ERROR, }), ); }); - it("accepts kubeconfig auth-provider without cmd-path", () => { - const schema = addCredentialsFormSchema("kubernetes"); - - const result = schema.safeParse({ - ...BASE_KUBERNETES_VALUES, - [ProviderCredentialFields.KUBECONFIG_CONTENT]: `apiVersion: v1 + it.each([ + [ + "user tokenFile", + `apiVersion: v1 +kind: Config +users: + - name: test-user + user: + tokenFile: /etc/passwd`, + ], + [ + "user client-certificate", + `apiVersion: v1 +kind: Config +users: + - name: test-user + user: + client-certificate: /etc/ssl/cert.pem`, + ], + [ + "user client-key", + `apiVersion: v1 +kind: Config +users: + - name: test-user + user: + client-key: /etc/ssl/key.pem`, + ], + [ + "user auth-provider directives", + `apiVersion: v1 kind: Config users: - name: test-user @@ -268,6 +293,85 @@ users: name: oidc config: client-id: prowler`, + ], + [ + "unknown user key", + `apiVersion: v1 +kind: Config +users: + - name: test-user + user: + bogus: value`, + ], + [ + "cluster certificate-authority", + `apiVersion: v1 +kind: Config +clusters: + - name: test-cluster + cluster: + server: https://example.test + certificate-authority: /etc/ssl/ca.pem`, + ], + [ + "cluster proxy-url", + `apiVersion: v1 +kind: Config +clusters: + - name: test-cluster + cluster: + server: https://example.test + proxy-url: http://proxy.evil.test`, + ], + [ + "unknown cluster key", + `apiVersion: v1 +kind: Config +clusters: + - name: test-cluster + cluster: + server: https://example.test + bogus: /etc/ssl/ca.pem`, + ], + ])( + "rejects kubeconfig with %s on kubeconfig_content field", + (_label, kubeconfigContent) => { + const schema = addCredentialsFormSchema("kubernetes"); + + const result = schema.safeParse({ + ...BASE_KUBERNETES_VALUES, + [ProviderCredentialFields.KUBECONFIG_CONTENT]: kubeconfigContent, + }); + + expect(result.success).toBe(false); + if (result.success) return; + + expect(result.error.issues).toContainEqual( + expect.objectContaining({ + path: [ProviderCredentialFields.KUBECONFIG_CONTENT], + message: KUBECONFIG_NON_INLINE_CREDENTIALS_ERROR, + }), + ); + }, + ); + + it("accepts kubeconfig with only inline credentials", () => { + const schema = addCredentialsFormSchema("kubernetes"); + + const result = schema.safeParse({ + ...BASE_KUBERNETES_VALUES, + [ProviderCredentialFields.KUBECONFIG_CONTENT]: `apiVersion: v1 +kind: Config +clusters: + - name: test-cluster + cluster: + server: https://example.test + certificate-authority-data: Zm9v +users: + - name: test-user + user: + client-certificate-data: Zm9v + client-key-data: YmFy`, }); expect(result.success).toBe(true); diff --git a/ui/types/formSchemas.ts b/ui/types/formSchemas.ts index 11c8bef03e..93101f074c 100644 --- a/ui/types/formSchemas.ts +++ b/ui/types/formSchemas.ts @@ -8,42 +8,75 @@ import { MAX_SAML_ADDITIONAL_EMAIL_DOMAINS } from "@/types/saml"; import { isKnownProviderType, PROVIDER_TYPES, ProviderType } from "./providers"; -export const KUBECONFIG_UNSUPPORTED_COMMAND_AUTHENTICATION_ERROR = - "Kubernetes kubeconfig command-based authentication is not supported in Prowler Cloud for security reasons."; +export const KUBECONFIG_NON_INLINE_CREDENTIALS_ERROR = + "Only inline Kubernetes credentials are supported. For user authentication use token, username/password, or client-certificate-data with client-key-data; clusters may use certificate-authority-data. File-path fields (tokenFile, client-certificate, client-key, certificate-authority), command-based authentication (exec, cmd-path), auth-provider directives, and proxy URLs are not supported."; const isRecord = (value: unknown): value is Record => { return typeof value === "object" && value !== null && !Array.isArray(value); }; -export const kubeconfigContainsUnsupportedCommandAuthentication = ( - value: string, -): boolean => { +const KUBECONFIG_ALLOWED_USER_KEYS = new Set([ + "token", + "username", + "password", + "client-certificate-data", + "client-key-data", + "as", + "as-uid", + "as-groups", + "as-user-extra", +]); +const KUBECONFIG_ALLOWED_CLUSTER_KEYS = new Set([ + "server", + "certificate-authority-data", + "insecure-skip-tls-verify", + "tls-server-name", + "disable-compression", + "extensions", +]); + +// Mirrors the API key checks; structural validation stays in the API. +export const kubeconfigIsInlineOnlyCredentials = (value: string): boolean => { + let parsed: unknown; try { - const parsed = yaml.load(value); + parsed = yaml.load(value); + } catch { + return true; + } - if (!isRecord(parsed) || !Array.isArray(parsed.users)) { - return false; - } + if (!isRecord(parsed)) { + return true; + } - return parsed.users.some((userEntry) => { + if (Array.isArray(parsed.users)) { + const usersInlineOnly = parsed.users.every((userEntry) => { if (!isRecord(userEntry) || !isRecord(userEntry.user)) { - return false; - } - - if ("exec" in userEntry.user) { return true; } - - const authProvider = userEntry.user["auth-provider"]; - if (!isRecord(authProvider) || !isRecord(authProvider.config)) { - return false; - } - - return "cmd-path" in authProvider.config; + return Object.keys(userEntry.user).every((key) => + KUBECONFIG_ALLOWED_USER_KEYS.has(key), + ); }); - } catch { - return false; + if (!usersInlineOnly) { + return false; + } } + + if (Array.isArray(parsed.clusters)) { + const clustersInlineOnly = parsed.clusters.every((clusterEntry) => { + if (!isRecord(clusterEntry) || !isRecord(clusterEntry.cluster)) { + return true; + } + return Object.keys(clusterEntry.cluster).every((key) => + KUBECONFIG_ALLOWED_CLUSTER_KEYS.has(key), + ); + }); + if (!clustersInlineOnly) { + return false; + } + } + + return true; }; // Create and edit share the same shape, so a single schema backs both flows. @@ -263,13 +296,9 @@ export const addCredentialsFormSchema = ( .string() .min(1, "Kubeconfig Content is required") .refine( - (value) => - !kubeconfigContainsUnsupportedCommandAuthentication( - value, - ), + (value) => kubeconfigIsInlineOnlyCredentials(value), { - error: - KUBECONFIG_UNSUPPORTED_COMMAND_AUTHENTICATION_ERROR, + error: KUBECONFIG_NON_INLINE_CREDENTIALS_ERROR, }, ), } From d8bdc8553f29ca18c7c218e8dccc2d72144306cc Mon Sep 17 00:00:00 2001 From: Alejandro Bailo <59607668+alejandrobailo@users.noreply.github.com> Date: Thu, 8 Oct 2026 18:16:08 +0200 Subject: [PATCH 15/17] fix(ui): show registry provider logos and capitalize registry labels (#12985) --- ui/__tests__/msw/handlers/registry.ts | 17 ++++ ui/actions/registry/registry.ts | 16 +++ .../providers/providers-page.utils.test.ts | 97 +++++++++++++++++++ .../providers/providers-page.utils.ts | 30 ++++-- .../providers/providers-tab-content.tsx | 1 + .../providers/providers-accounts-view.tsx | 30 +++--- .../radio-group-provider.integration.test.tsx | 59 +++++++++++ .../providers/radio-group-provider.tsx | 25 ++--- .../providers/registry-provider-logo.tsx | 33 +++++++ .../providers/table/column-providers.tsx | 6 ++ .../registry-row-logo.integration.test.tsx | 36 +++++++ .../providers/table/registry-row-logo.tsx | 39 ++++++++ .../steps/dynamic-credentials-step.test.tsx | 16 +-- .../wizard/steps/dynamic-credentials-step.tsx | 3 +- .../workflow/provider-credential-fields.tsx | 3 +- ui/lib/registry/labels.test.ts | 15 +++ ui/lib/registry/labels.ts | 9 ++ ui/lib/registry/provider-options.test.ts | 21 ++++ ui/lib/registry/provider-options.ts | 8 +- ui/types/providers-table.ts | 2 + 20 files changed, 416 insertions(+), 50 deletions(-) create mode 100644 ui/__tests__/msw/handlers/registry.ts create mode 100644 ui/components/providers/radio-group-provider.integration.test.tsx create mode 100644 ui/components/providers/registry-provider-logo.tsx create mode 100644 ui/components/providers/table/registry-row-logo.integration.test.tsx create mode 100644 ui/components/providers/table/registry-row-logo.tsx create mode 100644 ui/lib/registry/labels.test.ts create mode 100644 ui/lib/registry/labels.ts diff --git a/ui/__tests__/msw/handlers/registry.ts b/ui/__tests__/msw/handlers/registry.ts new file mode 100644 index 0000000000..1cbc2c5b22 --- /dev/null +++ b/ui/__tests__/msw/handlers/registry.ts @@ -0,0 +1,17 @@ +import { http, HttpResponse } from "msw"; + +// A 1×1 transparent PNG. +const PNG = Uint8Array.from( + atob( + "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=", + ), + (char) => char.charCodeAt(0), +); + +/** Serves a registry logo image, so a test never reaches the real registry. */ +export function handleRegistryLogo(url: string) { + return http.get( + url, + () => new HttpResponse(PNG, { headers: { "Content-Type": "image/png" } }), + ); +} diff --git a/ui/actions/registry/registry.ts b/ui/actions/registry/registry.ts index 4d470bfc6b..18cd2322ce 100644 --- a/ui/actions/registry/registry.ts +++ b/ui/actions/registry/registry.ts @@ -227,6 +227,22 @@ export async function getInstalledRegistryProviderOptions(): Promise +> { + const access = (await auth())?.accessToken; + const permission = await evaluateRegistryProviderAccess(access); + if (!access || permission.status !== REGISTRY_ACCESS.ELIGIBLE) return {}; + const result = await readRegistryProviders(access, null); + if (result.status !== "ready") return {}; + return Object.fromEntries( + result.providers.flatMap(({ type, logoUrl }) => + logoUrl?.startsWith("https://") ? [[type, logoUrl]] : [], + ), + ); +} + async function readCompleteRegistryCatalog( accessToken: string, credential: RegistryCredentialStatus | null, diff --git a/ui/app/(prowler)/providers/providers-page.utils.test.ts b/ui/app/(prowler)/providers/providers-page.utils.test.ts index 2b1cbfad1a..b14af05297 100644 --- a/ui/app/(prowler)/providers/providers-page.utils.test.ts +++ b/ui/app/(prowler)/providers/providers-page.utils.test.ts @@ -22,6 +22,10 @@ const manageGroupsActionsMock = vi.hoisted(() => ({ getAllProviderGroups: vi.fn(), })); +const registryActionsMock = vi.hoisted(() => ({ + getRegistryProviderLogos: vi.fn(), +})); + vi.mock("@/actions/providers", () => providersActionsMock); vi.mock( "@/actions/organizations/organizations", @@ -30,6 +34,7 @@ vi.mock( vi.mock("@/actions/scans", () => scansActionsMock); vi.mock("@/actions/schedules", () => schedulesActionsMock); vi.mock("@/actions/manage-groups/manage-groups", () => manageGroupsActionsMock); +vi.mock("@/actions/registry/registry", () => registryActionsMock); import { SearchParamsProps } from "@/types"; import { NODE_KIND } from "@/types/organizations"; @@ -1313,4 +1318,96 @@ describe("loadProvidersAccountsViewData", () => { false, ); }); + + const givenRegistryProviderPage = ( + logos: Promise>, + ) => { + const registryProvider = { + ...providersResponse.data[1], + id: "provider-vcf", + attributes: { + ...providersResponse.data[1].attributes, + provider: "vcf", + is_dynamic: true, + }, + }; + providersActionsMock.getProviders.mockResolvedValue({ + ...providersResponse, + data: [providersResponse.data[0], registryProvider], + }); + providersActionsMock.getAllProviders.mockResolvedValue(providersResponse); + schedulesActionsMock.getSchedules.mockResolvedValue({ data: [] }); + organizationsActionsMock.listOrganizationsSafe.mockResolvedValue({ + data: [], + }); + organizationsActionsMock.listOrganizationNodesSafe.mockResolvedValue({ + data: [], + }); + registryActionsMock.getRegistryProviderLogos.mockReturnValue(logos); + }; + + it("streams the logo the registry publishes for each provider type", async () => { + // Given + givenRegistryProviderPage( + Promise.resolve({ + vcf: "https://media.registry.example.com/providers/vcf/logo.png", + }), + ); + + // When + const viewData = await loadProvidersAccountsViewData({ + searchParams: {} satisfies SearchParamsProps, + isCloud: true, + }); + + // Then + await expect(viewData.registryLogos).resolves.toEqual({ + vcf: "https://media.registry.example.com/providers/vcf/logo.png", + }); + }); + + it("returns the provider rows without waiting for registry logos", async () => { + // Given: a registry that never answers. + givenRegistryProviderPage(new Promise(() => {})); + + // When + const result = await Promise.race([ + loadProvidersAccountsViewData({ + searchParams: {} satisfies SearchParamsProps, + isCloud: true, + }), + new Promise<"blocked">((resolve) => + setTimeout(() => resolve("blocked"), 200), + ), + ]); + + // Then + expect(result).not.toBe("blocked"); + if (result === "blocked") return; + expect(findProviderRow(result.rows, "provider-vcf")).toBeDefined(); + }); + + it("does not ask the registry for logos when the page lists no registry providers", async () => { + // Given + registryActionsMock.getRegistryProviderLogos.mockClear(); + providersActionsMock.getProviders.mockResolvedValue(providersResponse); + providersActionsMock.getAllProviders.mockResolvedValue(providersResponse); + schedulesActionsMock.getSchedules.mockResolvedValue({ data: [] }); + organizationsActionsMock.listOrganizationsSafe.mockResolvedValue({ + data: [], + }); + organizationsActionsMock.listOrganizationNodesSafe.mockResolvedValue({ + data: [], + }); + + // When + const viewData = await loadProvidersAccountsViewData({ + searchParams: {} satisfies SearchParamsProps, + isCloud: true, + }); + + // Then + await expect(viewData.registryLogos).resolves.toEqual({}); + expect(registryActionsMock.getRegistryProviderLogos).not.toHaveBeenCalled(); + }); }); diff --git a/ui/app/(prowler)/providers/providers-page.utils.ts b/ui/app/(prowler)/providers/providers-page.utils.ts index 56cc22e7ca..e867998cf9 100644 --- a/ui/app/(prowler)/providers/providers-page.utils.ts +++ b/ui/app/(prowler)/providers/providers-page.utils.ts @@ -5,6 +5,7 @@ import { } from "@/actions/organizations/organizations"; import { getAllProviders, getProviders } from "@/actions/providers"; import { PROVIDERS_FILTER_PARAM } from "@/actions/providers/providers-filters"; +import { getRegistryProviderLogos } from "@/actions/registry/registry"; import { getSchedules } from "@/actions/schedules"; import { extractFiltersAndQuery, @@ -142,6 +143,14 @@ const getProviderLastScanAt = ( return provider.attributes.connection.last_checked_at ?? null; }; +// Only a page that lists a registry provider pays for the registry round trip. +const loadRegistryLogos = async ( + providersResponse: ProvidersApiResponse | undefined, +): Promise> => + providersResponse?.data.some((provider) => provider.attributes.is_dynamic) + ? getRegistryProviderLogos().catch(() => ({})) + : {}; + const enrichProviders = ( providersResponse: ProvidersApiResponse | undefined, schedulesByProviderId: Record, @@ -557,6 +566,16 @@ export async function loadProvidersAccountsViewData({ data: [], }; + const providersRequest = resolveActionResult( + getProviders({ + filters: providerFilters, + page, + pageSize, + query, + sort: encodedSort, + }), + ); + const [ providersResponse, allProvidersResponse, @@ -565,15 +584,7 @@ export async function loadProvidersAccountsViewData({ organizationsResponse, organizationNodesResponse, ] = await Promise.all([ - resolveActionResult( - getProviders({ - filters: providerFilters, - page, - pageSize, - query, - sort: encodedSort, - }), - ), + providersRequest, // Unfiltered fetch for ProviderTypeSelector — only needs distinct types; // TODO: Replace with a dedicated lightweight endpoint when available. resolveActionResult(getAllProviders()), @@ -619,6 +630,7 @@ export async function loadProvidersAccountsViewData({ providerGroups: allProviderGroupsResponse?.data ?? [], rows, hierarchyStatus, + registryLogos: providersRequest.then(loadRegistryLogos), }; } diff --git a/ui/app/(prowler)/providers/providers-tab-content.tsx b/ui/app/(prowler)/providers/providers-tab-content.tsx index c55f8730c6..0d88ca8097 100644 --- a/ui/app/(prowler)/providers/providers-tab-content.tsx +++ b/ui/app/(prowler)/providers/providers-tab-content.tsx @@ -50,6 +50,7 @@ export const ProvidersTabContent = async ({ metadata={providersView.metadata} rows={providersView.rows} hierarchyStatus={providersView.hierarchyStatus} + registryLogos={providersView.registryLogos} scanConfigs={scanConfigsState.data} scanConfigStatus={scanConfigsState.status} /> diff --git a/ui/components/providers/providers-accounts-view.tsx b/ui/components/providers/providers-accounts-view.tsx index b94202af38..d742b58cbf 100644 --- a/ui/components/providers/providers-accounts-view.tsx +++ b/ui/components/providers/providers-accounts-view.tsx @@ -10,6 +10,7 @@ import { MutedFindingsConfigButton } from "@/components/providers/muted-findings import { NoProvidersAdded } from "@/components/providers/no-providers-added"; import { ProvidersAccountsTable } from "@/components/providers/providers-accounts-table"; import { ProvidersFilters } from "@/components/providers/providers-filters"; +import { RegistryLogosContext } from "@/components/providers/table/registry-row-logo"; import { ProviderWizardModal } from "@/components/providers/wizard"; import type { OrgWizardInitialData, @@ -81,6 +82,8 @@ interface ProvidersAccountsViewProps { scanConfigStatus?: ScanConfigurationListStatus; isScanLimitReached?: boolean; hierarchyStatus?: HierarchyStatus; + /** Registry logo per provider type; rows render before it resolves. */ + registryLogos?: Promise>; } export function ProvidersAccountsView({ @@ -95,6 +98,7 @@ export function ProvidersAccountsView({ scanConfigStatus, isScanLimitReached, hierarchyStatus = HIERARCHY_STATUS.AVAILABLE, + registryLogos, }: ProvidersAccountsViewProps) { const pathname = usePathname(); const searchParams = useSearchParams(); @@ -219,18 +223,20 @@ export function ProvidersAccountsView({ } /> - - openProviderWizard(WIZARD_OPEN_SOURCE.ROW_ACTION, initialData) - } - onOpenOrganizationWizard={openOrganizationWizard} - /> + + + openProviderWizard(WIZARD_OPEN_SOURCE.ROW_ACTION, initialData) + } + onOpenOrganizationWizard={openOrganizationWizard} + /> +
)} (); + return ( + + ); +} + +// The logo is the first element of the option's label row. +function logoBox(option: HTMLElement) { + const logo = option.lastElementChild?.firstElementChild; + if (!logo) throw new Error("Provider option without a logo"); + const { width, height } = logo.getBoundingClientRect(); + return { width, height }; +} + +describe("provider selector logos", () => { + it("draws a Registry provider's logo at the size of the built-in logos", async () => { + // Given + worker.use(handleRegistryLogo(LOGO_URL)); + const screen = await render(); + + // When + const builtIn = screen + .getByRole("option", { name: "Amazon Web Services" }) + .element() as HTMLElement; + const registry = screen + .getByRole("option", { name: "Vcf Registry" }) + .element() as HTMLElement; + await expect + .poll(() => registry.querySelector("img")?.getAttribute("src")) + .toBe(LOGO_URL); + + // Then + expect(logoBox(registry)).toEqual(logoBox(builtIn)); + }); +}); diff --git a/ui/components/providers/radio-group-provider.tsx b/ui/components/providers/radio-group-provider.tsx index f1d175da6e..a12535c3d5 100644 --- a/ui/components/providers/radio-group-provider.tsx +++ b/ui/components/providers/radio-group-provider.tsx @@ -8,11 +8,6 @@ import { PROVIDER_TYPE_DATA, } from "@/components/icons/providers-badge/provider-type-icon"; import { Badge, SearchInput } from "@/components/shadcn"; -import { - Avatar, - AvatarFallback, - AvatarImage, -} from "@/components/shadcn/avatar"; import { FormMessage } from "@/components/shadcn/form"; import { Tabs, @@ -24,6 +19,8 @@ import type { RegistryProviderOption } from "@/lib/registry/provider-options"; import { cn } from "@/lib/utils"; import type { AddProviderFormValues } from "@/types/formSchemas"; +import { RegistryProviderLogo } from "./registry-provider-logo"; + const PROVIDERS = Object.entries(PROVIDER_TYPE_DATA).map( ([value, { label }]) => ({ value, label }), ); @@ -135,19 +132,11 @@ export const RadioGroupProvider: FC = ({
{provider.registry ? ( - - - - - - + ) : ( )} diff --git a/ui/components/providers/registry-provider-logo.tsx b/ui/components/providers/registry-provider-logo.tsx new file mode 100644 index 0000000000..1d80c34319 --- /dev/null +++ b/ui/components/providers/registry-provider-logo.tsx @@ -0,0 +1,33 @@ +import { ProviderTypeIcon } from "@/components/icons/providers-badge/provider-type-icon"; +import { + Avatar, + AvatarFallback, + AvatarImage, +} from "@/components/shadcn/avatar"; +import type { ProviderType } from "@/types"; + +interface RegistryProviderLogoProps { + type: ProviderType; + logoUrl?: string; + size: number; +} + +/** The registry's logo, or the generic glyph while it loads or once its signed URL expires. */ +export function RegistryProviderLogo({ + type, + logoUrl, + size, +}: RegistryProviderLogoProps) { + // Same box as a built-in provider badge of this size. + return ( + + + + + + + ); +} diff --git a/ui/components/providers/table/column-providers.tsx b/ui/components/providers/table/column-providers.tsx index 548dda2e82..bb9efeda91 100644 --- a/ui/components/providers/table/column-providers.tsx +++ b/ui/components/providers/table/column-providers.tsx @@ -41,6 +41,7 @@ import type { import { LinkToScans } from "../link-to-scans"; import { DataTableRowActions } from "./data-table-row-actions"; +import { RegistryRowLogo } from "./registry-row-logo"; interface GroupNameChipsProps { groupNames?: string[]; @@ -247,6 +248,11 @@ export function getColumnProviders( > + ) : undefined + } entityAlias={provider.attributes.alias} entityId={provider.attributes.uid} nameAction={ diff --git a/ui/components/providers/table/registry-row-logo.integration.test.tsx b/ui/components/providers/table/registry-row-logo.integration.test.tsx new file mode 100644 index 0000000000..0831b144d5 --- /dev/null +++ b/ui/components/providers/table/registry-row-logo.integration.test.tsx @@ -0,0 +1,36 @@ +import { describe, expect, it } from "vitest"; + +import { handleRegistryLogo } from "@/__tests__/msw/handlers/registry"; +import { worker } from "@/__tests__/msw/worker"; +import { render } from "@/__tests__/render-browser"; + +import { RegistryLogosContext, RegistryRowLogo } from "./registry-row-logo"; + +const LOGO_URL = "https://media.registry.example.com/providers/vcf/logo.png"; + +describe("registry row logo", () => { + it("shows the generic glyph until the streamed logos arrive, then the registry logo", async () => { + // Given + worker.use(handleRegistryLogo(LOGO_URL)); + let resolveLogos!: (logos: Record) => void; + const logos = new Promise>((resolve) => { + resolveLogos = resolve; + }); + const screen = await render( + + + , + ); + const logoImage = () => screen.container.querySelector("img"); + + // Then: the row renders while the registry has not answered. + expect(screen.container.querySelector("svg")).not.toBeNull(); + expect(logoImage()).toBeNull(); + + // When + resolveLogos({ vcf: LOGO_URL }); + + // Then + await expect.poll(() => logoImage()?.getAttribute("src")).toBe(LOGO_URL); + }); +}); diff --git a/ui/components/providers/table/registry-row-logo.tsx b/ui/components/providers/table/registry-row-logo.tsx new file mode 100644 index 0000000000..de555b1707 --- /dev/null +++ b/ui/components/providers/table/registry-row-logo.tsx @@ -0,0 +1,39 @@ +"use client"; + +import { createContext, Suspense, use } from "react"; + +import { RegistryProviderLogo } from "@/components/providers/registry-provider-logo"; +import type { ProviderType } from "@/types"; + +const ROW_LOGO_SIZE = 35; + +/** Registry logo per provider type, resolved after the rows render. */ +export const RegistryLogosContext = createContext +> | null>(null); + +function ResolvedRegistryRowLogo({ + type, + logos, +}: { + type: ProviderType; + logos: Promise>; +}) { + const byType = use(logos); + const logoUrl = Object.hasOwn(byType, type) ? byType[type] : undefined; + return ( + + ); +} + +/** A registry row's logo; the generic glyph stands in until the logos arrive. */ +export function RegistryRowLogo({ type }: { type: ProviderType }) { + const logos = use(RegistryLogosContext); + const placeholder = ; + if (!logos) return placeholder; + return ( + + + + ); +} diff --git a/ui/components/providers/wizard/steps/dynamic-credentials-step.test.tsx b/ui/components/providers/wizard/steps/dynamic-credentials-step.test.tsx index f91e5af082..710e562e17 100644 --- a/ui/components/providers/wizard/steps/dynamic-credentials-step.test.tsx +++ b/ui/components/providers/wizard/steps/dynamic-credentials-step.test.tsx @@ -157,12 +157,16 @@ describe("dynamic credentials in the provider wizard", () => { expect(timeout).toHaveAttribute("min", "1"); expect(timeout).toHaveAttribute("max", "300"); expect(timeout).toHaveAttribute("step", "1"); - expect( - screen.getByRole("combobox", { name: "Authentication Scheme" }), - ).toHaveTextContent("bearer"); + const authScheme = screen.getByRole("combobox", { + name: "Authentication Scheme", + }); + expect(authScheme).toHaveTextContent("Bearer"); expect(apiUrl).toHaveValue(""); - // When: false must remain a boolean and numeric input must become a number. + // When: options read capitalized but submit their raw schema value. + await user.click(authScheme); + await user.click(screen.getByRole("option", { name: "Basic" })); + // False must remain a boolean and numeric input must become a number. await user.type(apiUrl, "https://api.example.test"); await user.type(apiKey, "fixture-key-not-a-secret"); await user.click(verifyTls); @@ -180,7 +184,7 @@ describe("dynamic credentials in the provider wizard", () => { api_key: "fixture-key-not-a-secret", verify_tls: false, timeout_seconds: 60, - auth_scheme: "bearer", + auth_scheme: "basic", }, }); }); @@ -347,7 +351,7 @@ describe("dynamic credentials in the provider wizard", () => { await user.click( screen.getByRole("combobox", { name: "Authentication method" }), ); - await user.click(screen.getByRole("option", { name: "personal token" })); + await user.click(screen.getByRole("option", { name: "Personal token" })); expect(screen.getByLabelText(/API token/)).toHaveValue(""); expect(JSON.stringify(sessionStorage)).not.toContain( "previous-method-secret", diff --git a/ui/components/providers/wizard/steps/dynamic-credentials-step.tsx b/ui/components/providers/wizard/steps/dynamic-credentials-step.tsx index 3d2c48941d..f8732b2853 100644 --- a/ui/components/providers/wizard/steps/dynamic-credentials-step.tsx +++ b/ui/components/providers/wizard/steps/dynamic-credentials-step.tsx @@ -27,6 +27,7 @@ import { getCredentialDefaults, validateCredentialValues, } from "@/lib/provider-credentials/provider-credential-values"; +import { formatRegistryLabel } from "@/lib/registry/labels"; import { useProviderWizardStore } from "@/store/provider-wizard/store"; import type { ProviderSchemasResult, @@ -58,7 +59,7 @@ function listCredentialMethods( secretTypes: ProviderSecretTypes, ): CredentialMethod[] { return Object.entries(secretTypes).flatMap(([secretType, value]) => { - const label = secretType.replaceAll("_", " "); + const label = formatRegistryLabel(secretType); const variants = parseRegistryCredentialVariants(value); if (!variants || variants.length === 1) { return [ diff --git a/ui/components/providers/workflow/provider-credential-fields.tsx b/ui/components/providers/workflow/provider-credential-fields.tsx index 230f36b0e4..5466af1b9e 100644 --- a/ui/components/providers/workflow/provider-credential-fields.tsx +++ b/ui/components/providers/workflow/provider-credential-fields.tsx @@ -17,6 +17,7 @@ import type { RegistryCredentialSchema, RegistryCredentialValue, } from "@/lib/provider-credentials/provider-credential-schema"; +import { formatRegistryLabel } from "@/lib/registry/labels"; interface RegistryCredentialFieldsProps { readonly errors: Readonly>; @@ -107,7 +108,7 @@ export function RegistryCredentialFields({ {field.options?.map((option) => ( - {option} + {formatRegistryLabel(option)} ))} diff --git a/ui/lib/registry/labels.test.ts b/ui/lib/registry/labels.test.ts new file mode 100644 index 0000000000..605ee27a82 --- /dev/null +++ b/ui/lib/registry/labels.test.ts @@ -0,0 +1,15 @@ +import { describe, expect, it } from "vitest"; + +import { formatRegistryLabel } from "./labels"; + +describe("formatRegistryLabel", () => { + it("capitalizes plain identifiers and keeps any other value as written", () => { + // When + const labels = ["token", "service_account", "us-east-1", "OAuth2"].map( + formatRegistryLabel, + ); + + // Then + expect(labels).toEqual(["Token", "Service account", "us-east-1", "OAuth2"]); + }); +}); diff --git a/ui/lib/registry/labels.ts b/ui/lib/registry/labels.ts new file mode 100644 index 0000000000..e029c9c9ae --- /dev/null +++ b/ui/lib/registry/labels.ts @@ -0,0 +1,9 @@ +// Only plain lowercase identifiers: "us-east-1" or "OAuth2" already read as meant. +const PLAIN_IDENTIFIER = /^[a-z][a-z0-9]*(?:[_ ][a-z0-9]+)*$/; + +/** Display text for a Registry identifier ("service_account" → "Service account"). */ +export function formatRegistryLabel(value: string): string { + if (!PLAIN_IDENTIFIER.test(value)) return value; + const words = value.replaceAll("_", " "); + return words.charAt(0).toUpperCase() + words.slice(1); +} diff --git a/ui/lib/registry/provider-options.test.ts b/ui/lib/registry/provider-options.test.ts index ecc46985b6..cda44cbffa 100644 --- a/ui/lib/registry/provider-options.test.ts +++ b/ui/lib/registry/provider-options.test.ts @@ -143,4 +143,25 @@ describe("installed Registry provider options", () => { ).map((option) => option.type), ).toEqual(["zeta"]); }); + + it("capitalizes provider names the registry publishes as plain slugs", () => { + // Given: the registry names one provider by its slug and another properly. + const catalog = [ + { ...provider, normalizedName: "vcf-package", providerSlug: "vcf" }, + { ...provider, normalizedName: "openai-package", providerSlug: "openai" }, + ]; + const installed = catalog.map(({ normalizedName }) => ({ + normalizedName, + versionSpec: "latest", + })); + + // When + const labels = buildRegistryProviderOptions(catalog, installed, [ + { type: "vcf", label: "vcf" }, + { type: "openai", label: "OpenAI" }, + ]).map((option) => option.label); + + // Then + expect(labels).toEqual(["OpenAI", "Vcf"]); + }); }); diff --git a/ui/lib/registry/provider-options.ts b/ui/lib/registry/provider-options.ts index cfe391480f..0c586484df 100644 --- a/ui/lib/registry/provider-options.ts +++ b/ui/lib/registry/provider-options.ts @@ -1,3 +1,4 @@ +import { formatRegistryLabel } from "@/lib/registry/labels"; import { isKnownProviderType } from "@/types/providers"; import type { RegistryCatalogArtifact, @@ -63,7 +64,8 @@ export function buildRegistryProviderOptions( ); } } - return Array.from(options.values()).sort((left, right) => - left.label.localeCompare(right.label), - ); + return Array.from(options.values(), (option) => ({ + ...option, + label: formatRegistryLabel(option.label), + })).sort((left, right) => left.label.localeCompare(right.label)); } diff --git a/ui/types/providers-table.ts b/ui/types/providers-table.ts index 48da50e5c3..e1628545f7 100644 --- a/ui/types/providers-table.ts +++ b/ui/types/providers-table.ts @@ -105,6 +105,8 @@ export interface ProvidersAccountsViewData { rows: ProvidersTableRow[]; /** `unavailable` when the hierarchy fetch failed (drives the degraded notice). */ hierarchyStatus: HierarchyStatus; + /** Registry logo per provider type, streamed so rows never wait for it. */ + registryLogos: Promise>; } export function isProvidersOrganizationRow( From 859a8461474e91364746a713daff920998298fe2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pedro=20Mart=C3=ADn?= Date: Fri, 9 Oct 2026 13:04:11 +0200 Subject: [PATCH 16/17] fix(api): scope user memberships and roles to tenant (#12965) --- ...-detail-cross-tenant-relations.security.md | 1 + api/src/backend/api/tests/test_views.py | 140 ++++++++++++++++++ api/src/backend/api/v1/serializers.py | 74 +++++++-- 3 files changed, 199 insertions(+), 16 deletions(-) create mode 100644 api/changelog.d/user-detail-cross-tenant-relations.security.md diff --git a/api/changelog.d/user-detail-cross-tenant-relations.security.md b/api/changelog.d/user-detail-cross-tenant-relations.security.md new file mode 100644 index 0000000000..2ada35d1ea --- /dev/null +++ b/api/changelog.d/user-detail-cross-tenant-relations.security.md @@ -0,0 +1 @@ +`GET /api/v1/users/{id}` no longer exposes another user's memberships, roles or tenant names from tenants other than the one in the request token diff --git a/api/src/backend/api/tests/test_views.py b/api/src/backend/api/tests/test_views.py index b5c5d0fcf1..2ea4415072 100644 --- a/api/src/backend/api/tests/test_views.py +++ b/api/src/backend/api/tests/test_views.py @@ -63,6 +63,7 @@ from api.models import ( from api.rbac.permissions import TASK_REVOKE_PERMISSIONS from api.rls import Tenant from api.uuid_utils import datetime_to_uuid7 +from api.v1.serializers import MembershipIncludeSerializer from api.v1.views import ( ComplianceOverviewViewSet, CustomSAMLLoginView, @@ -133,6 +134,145 @@ class TestUserViewSet: assert response.status_code == status.HTTP_200_OK assert response.json()["data"]["attributes"]["email"] == create_test_user.email + @staticmethod + def _shared_user_in_two_tenants(tenant_a, tenant_b): + shared_user = User.objects.create_user( + name="shared_user", + email=f"shared-user-{uuid4()}@prowler.com", + password="TmpPass123@", + ) + Membership.objects.create(user=shared_user, tenant=tenant_a) + Membership.objects.create(user=shared_user, tenant=tenant_b) + role_a = Role.objects.create(name="role-a", tenant=tenant_a) + role_b = Role.objects.create(name="role-b", tenant=tenant_b) + UserRoleRelationship.objects.create( + user=shared_user, role=role_a, tenant=tenant_a + ) + UserRoleRelationship.objects.create( + user=shared_user, role=role_b, tenant=tenant_b + ) + return shared_user, role_a, role_b + + @staticmethod + def _user_in_two_tenants_with_unique_roles(tenant_a, tenant_b): + suffix = uuid4() + user = User.objects.create_user( + name="scaled_user", + email=f"scaled-user-{suffix}@prowler.com", + password="TmpPass123@", + ) + for tenant in (tenant_a, tenant_b): + Membership.objects.create(user=user, tenant=tenant) + role = Role.objects.create(name=f"role-{tenant.id}-{suffix}", tenant=tenant) + UserRoleRelationship.objects.create(user=user, role=role, tenant=tenant) + return user + + def test_users_list_query_count_does_not_grow_with_user_count( + self, authenticated_client, tenants_fixture + ): + tenant_a, tenant_b, _ = tenants_fixture + self._user_in_two_tenants_with_unique_roles(tenant_a, tenant_b) + params = {"include": "memberships,roles"} + + with CaptureQueriesContext(connection) as baseline: + response = authenticated_client.get(reverse("user-list"), params) + assert response.status_code == status.HTTP_200_OK + baseline_users = len(response.json()["data"]) + + for _ in range(3): + self._user_in_two_tenants_with_unique_roles(tenant_a, tenant_b) + + with CaptureQueriesContext(connection) as scaled: + response = authenticated_client.get(reverse("user-list"), params) + assert response.status_code == status.HTTP_200_OK + assert len(response.json()["data"]) > baseline_users + + assert len(scaled.captured_queries) == len(baseline.captured_queries), ( + "Tenant-scoped relations are re-querying per user: " + f"{len(baseline.captured_queries)} queries for {baseline_users} users " + f"vs {len(scaled.captured_queries)} for " + f"{len(response.json()['data'])}. The prefetch is being discarded." + ) + + def test_users_retrieve_scopes_relations_to_active_tenant( + self, authenticated_client, tenants_fixture + ): + tenant_a, tenant_b, _ = tenants_fixture + shared_user, role_a, role_b = self._shared_user_in_two_tenants( + tenant_a, tenant_b + ) + + response = authenticated_client.get( + reverse("user-detail", kwargs={"pk": shared_user.id}), + {"include": "memberships,roles,memberships.tenant"}, + ) + + assert response.status_code == status.HTTP_200_OK + payload = response.json() + relationships = payload["data"]["relationships"] + assert len(relationships["memberships"]["data"]) == 1 + assert [item["id"] for item in relationships["roles"]["data"]] == [ + str(role_a.id) + ] + + included = payload["included"] + included_memberships = [i for i in included if i["type"] == "memberships"] + assert len(included_memberships) == 1 + assert included_memberships[0]["relationships"]["tenant"]["data"]["id"] == str( + tenant_a.id + ) + assert {i["id"] for i in included if i["type"] == "roles"} == {str(role_a.id)} + assert { + i["attributes"]["name"] for i in included if i["type"] == "tenants" + } == {tenant_a.name} + body = response.content.decode() + assert tenant_b.name not in body + assert str(tenant_b.id) not in body + assert str(role_b.id) not in body + + def test_users_me_keeps_own_memberships_and_scopes_roles( + self, authenticated_client, create_test_user, tenants_fixture + ): + tenant_a, tenant_b, _ = tenants_fixture + + response = authenticated_client.get( + reverse("user-me"), {"include": "memberships,roles,memberships.tenant"} + ) + + assert response.status_code == status.HTTP_200_OK + payload = response.json() + relationships = payload["data"]["relationships"] + assert len(relationships["memberships"]["data"]) == 2 + assert { + i["attributes"]["name"] + for i in payload["included"] + if i["type"] == "tenants" + } == {tenant_a.name, tenant_b.name} + active_roles = create_test_user.roles.filter(tenant_id=tenant_a.id) + assert {i["id"] for i in relationships["roles"]["data"]} == { + str(role.id) for role in active_roles + } + + def test_membership_include_serializer_hides_tenant_of_non_member( + self, create_test_user, tenants_fixture + ): + tenant_a, _, isolated_tenant = tenants_fixture + outsider = User.objects.create_user( + name="outsider", + email=f"outsider-{uuid4()}@prowler.com", + password="TmpPass123@", + ) + foreign_membership = Membership.objects.create( + user=outsider, tenant=isolated_tenant + ) + own_membership = Membership.objects.get(user=create_test_user, tenant=tenant_a) + request = SimpleNamespace(user=create_test_user, tenant_id=str(tenant_a.id)) + + serializer = MembershipIncludeSerializer(context={"request": request}) + + assert serializer.get_tenant(own_membership) == tenant_a + assert serializer.get_tenant(foreign_membership) is None + def test_users_create(self, client): valid_user_payload = { "name": "test", diff --git a/api/src/backend/api/v1/serializers.py b/api/src/backend/api/v1/serializers.py index c3d6622b42..e90a58c5ad 100644 --- a/api/src/backend/api/v1/serializers.py +++ b/api/src/backend/api/v1/serializers.py @@ -377,27 +377,48 @@ class UserSerializer(BaseModelSerializerV1): def _can_view_relationships(self, instance) -> bool: """Allow self to view own relationships. Require manage_account to view others.""" role = self.context.get("role") + return self._is_self(instance) or bool(role and role.manage_account) + + def _is_self(self, instance) -> bool: request = self.context.get("request") - is_self = bool( + return bool( request and getattr(request, "user", None) and getattr(instance, "id", None) == request.user.id ) - return is_self or (role and role.manage_account) + + def _active_tenant_id(self): + return getattr(self.context.get("request"), "tenant_id", None) + + @staticmethod + def _in_tenant(related_manager, tenant_id): + """Filter a prefetched related manager in memory. + + ``.filter()`` clones the queryset without its result cache, so it would + discard the viewset's prefetch and re-query once per serialized user. + """ + return [ + item + for item in related_manager.all() + if str(item.tenant_id) == str(tenant_id) + ] def get_roles(self, instance): - return ( - instance.roles.all() - if self._can_view_relationships(instance) - else Role.objects.none() - ) + tenant_id = self._active_tenant_id() + if not tenant_id or not self._can_view_relationships(instance): + return Role.objects.none() + return self._in_tenant(instance.roles, tenant_id) def get_memberships(self, instance): - return ( - instance.memberships.all() - if self._can_view_relationships(instance) - else Membership.objects.none() - ) + if not self._can_view_relationships(instance): + return Membership.objects.none() + # Own memberships span tenants on purpose: the tenant switcher reads them. + if self._is_self(instance): + return instance.memberships.all() + tenant_id = self._active_tenant_id() + if not tenant_id: + return Membership.objects.none() + return self._in_tenant(instance.memberships, tenant_id) class UserMeSerializer(UserSerializer): @@ -731,11 +752,32 @@ class MembershipIncludeSerializer(serializers.ModelSerializer): included_serializers = {"tenant": "api.v1.serializers.TenantIncludeSerializer"} + _REQUESTER_TENANTS_ATTR = "_prowler_requester_tenants" + + def _requester_tenants(self): + """Tenants the requester belongs to, resolved once per request. + + Memoised on the request because DRF builds a serializer instance per + included object, so a per-instance cache would still query per membership. + """ + request = self.context.get("request") + user = getattr(request, "user", None) + if user is None: + return {} + tenants = getattr(request, self._REQUESTER_TENANTS_ATTR, None) + if tenants is None: + tenants = { + tenant.id: tenant + for tenant in Tenant.objects.using(MainRouter.admin_db).filter( + membership__user_id=user.id + ) + } + setattr(request, self._REQUESTER_TENANTS_ATTR, tenants) + return tenants + def get_tenant(self, instance): - try: - return Tenant.objects.using(MainRouter.admin_db).get(id=instance.tenant_id) - except Tenant.DoesNotExist: - return None + """Resolve the tenant only when the requester is a member of it.""" + return self._requester_tenants().get(instance.tenant_id) # Provider Groups From 61d2cc55c4539a6b48a3cf9f3729d8f2af300882 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pedro=20Mart=C3=ADn?= Date: Fri, 9 Oct 2026 18:17:48 +0200 Subject: [PATCH 17/17] fix(container): patch Trivy and MCP base image CVEs (#13009) --- .trivyignore.yaml | 72 +++++-------------- Dockerfile | 8 +-- api/Dockerfile | 8 +-- .../api-image-trivy-0-75-0.security.md | 1 + mcp_server/Dockerfile | 26 ++----- .../mcp-image-python-3-13-16.security.md | 1 + .../sdk-image-trivy-0-75-0.security.md | 1 + 7 files changed, 36 insertions(+), 81 deletions(-) create mode 100644 api/changelog.d/api-image-trivy-0-75-0.security.md create mode 100644 mcp_server/changelog.d/mcp-image-python-3-13-16.security.md create mode 100644 prowler/changelog.d/sdk-image-trivy-0-75-0.security.md diff --git a/.trivyignore.yaml b/.trivyignore.yaml index 3804fabaa4..3683a23e0a 100644 --- a/.trivyignore.yaml +++ b/.trivyignore.yaml @@ -181,61 +181,27 @@ vulnerabilities: - "pkg:npm/%40modelcontextprotocol%2Fsdk@1.27.1" expired_at: 2027-01-31 - # CVE-2026-84304 is a DoS in grpc-go <= 1.83.0: a peer fragments a gRPC stream into - # millions of tiny HTTP/2 DATA frames until the receiver runs out of heap. Fixed in - # 1.83.1 (published 2026-09-01). Trivy 0.74.0, the latest published release and the - # version the images ship, pins 1.82.1 as an indirect dependency: - # https://github.com/aquasecurity/trivy/blob/v0.74.0/go.mod - # Upstream bump still open: https://github.com/aquasecurity/trivy/pull/11176 - # Trivy only speaks gRPC in client/server mode (`trivy server`, `--server`). Prowler - # invokes it exclusively as `trivy image` and `trivy fs` on a local path, so no gRPC - # listener or connection ever exists in the image and the affected path is not - # reachable. Remove this temporary suppression as soon as a Trivy release pins - # grpc >= 1.83.1. - - id: CVE-2026-84304 + # CVE-2026-78667 and CVE-2026-97031 are DoS bugs in the Go standard library: net/http + # burns CPU parsing a Range header with a large number of small ranges in FileServer, + # ServeContent and ServeFile, and crypto/tls lets a client exhaust server memory with + # multiple ECH outer extension references. Both are server-side code paths. Fixed in + # Go 1.26.9 and 1.27.2 (released 2026-10-08). Trivy 0.75.0, the latest published + # release and the version the images ship, is built with go1.27.1, and Trivy main + # still declares `go 1.27.0`: + # https://github.com/aquasecurity/trivy/blob/v0.75.0/go.mod + # Prowler invokes Trivy exclusively as `trivy fs` and `trivy image` on a local path, + # never `trivy server`, so no HTTP or TLS listener ever exists in the image; its only + # TLS use is as a client pulling the vulnerability database. The purl is pinned to the + # stdlib version so the entry retires itself on the next Trivy bump. Remove this + # temporary suppression as soon as a Trivy release is built with Go >= 1.27.2. + - id: CVE-2026-78667 purls: - - "pkg:golang/google.golang.org/grpc" - expired_at: 2026-10-15 - - # CVE-2026-84445 is a DoS in grpc-go servers built with `xds.NewGRPCServer()`: a request - # carrying neither `:authority` nor `Host` reaches the xDS routing interceptor, which - # indexes an empty slice of authorities and panics. The per-RPC goroutine does not - # recover, so the whole server process dies. Fixed in 1.82.2 and 1.83.2 (published - # 2026-09-08). Trivy 0.74.0, the latest published release and the version the images - # ship, pins 1.82.1 as an indirect dependency: - # https://github.com/aquasecurity/trivy/blob/v0.74.0/go.mod - # Trivy main already carries 1.83.2, but no published release includes it yet. - # The reachability argument is the one made for CVE-2026-84304 above, only narrower: - # this panic needs an xDS-managed gRPC server. Prowler invokes Trivy exclusively as - # `trivy image` and `trivy fs` on a local path, never `trivy server`, so the image runs - # no gRPC server at all, xDS or otherwise. Remove this temporary suppression as soon as - # a Trivy release pins grpc >= 1.83.2. - # https://github.com/advisories/GHSA-2v4p-qf9q-27wj - - id: CVE-2026-84445 + - "pkg:golang/stdlib@v1.27.1" + expired_at: 2026-11-15 + - id: CVE-2026-97031 purls: - - "pkg:golang/google.golang.org/grpc@v1.82.1" - expired_at: 2026-10-15 - - # CVE-2026-56855 and CVE-2026-78662 are DoS deadlocks in x/crypto/ssh: a malicious peer - # can flood or misuse channel messages (RFC 4254) to block the whole connection. - # Fixed in golang.org/x/crypto v0.56.0 (published 2026-09-02). Trivy 0.74.0, the latest - # published release and the version the images ship, still pins v0.55.0, and Trivy main - # has not bumped it either: - # https://github.com/aquasecurity/trivy/blob/v0.74.0/go.mod - # x/crypto/ssh is pulled in transitively through go-git's ssh transport, the same - # dependency chain as the CVE-2026-71556 entry above. Prowler invokes Trivy only with - # `fs` on an existing local path or with `image`; it never asks Trivy to clone over SSH - # or to run `trivy server`, so no SSH connection -- as client or server -- ever exists in - # the image and the affected code path is not reachable. Remove this temporary - # suppression as soon as a fixed Trivy release is available. - - id: CVE-2026-56855 - purls: - - "pkg:golang/golang.org/x/crypto@v0.55.0" - expired_at: 2026-10-15 - - id: CVE-2026-78662 - purls: - - "pkg:golang/golang.org/x/crypto@v0.55.0" - expired_at: 2026-10-15 + - "pkg:golang/stdlib@v1.27.1" + expired_at: 2026-11-15 - id: CVE-2026-56852 purls: diff --git a/Dockerfile b/Dockerfile index dc62fd64a6..1a1ad42c7e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.12.13-slim-trixie@sha256:57cd7c3a7a273101a6485ba99423ee568157882804b1124b4dd04266317710de AS build +FROM python:3.12.15-slim-trixie@sha256:05cda9777409a9c3ffddd94a4c476b79f0769a0b4857f0c7ed9226b6800b0d6f AS build LABEL maintainer="https://github.com/prowler-cloud/prowler" LABEL org.opencontainers.image.source="https://github.com/prowler-cloud/prowler" @@ -8,15 +8,15 @@ ENV POWERSHELL_VERSION=${POWERSHELL_VERSION} # Opt out of PowerShell telemetry (Application Insights -> dc.services.visualstudio.com) ENV POWERSHELL_TELEMETRY_OPTOUT=1 -ARG TRIVY_VERSION=0.74.0 +ARG TRIVY_VERSION=0.75.0 ENV TRIVY_VERSION=${TRIVY_VERSION} ARG ZIZMOR_VERSION=1.24.1 ENV ZIZMOR_VERSION=${ZIZMOR_VERSION} # Pinned here, not fetched with the artefact: a compromised release ships its own checksum. -ARG TRIVY_SHA256_AMD64=2ae6fe3ee734b7fdf11335663e18c75ea12dccc76062f09f164a3b0f8be4371a -ARG TRIVY_SHA256_ARM64=b94ce1976bbf3c15b514b605ee88be7c6d94a29be2302847ff01cb794d47aad5 +ARG TRIVY_SHA256_AMD64=c6e65abddb348e25f10549df887045629cf28cc72453cd1c63acb717316b3f3f +ARG TRIVY_SHA256_ARM64=a1ee9f6ffb7d112b64ff726a2a0717c21175c1114361391f4a132956751a13b3 ARG POWERSHELL_SHA256_AMD64=82a8b13d92b0f3ae48e56cf2f3f7961679371736ca90145ca71617c2913ba9d8 ARG POWERSHELL_SHA256_ARM64=830ebda118c731ece3fa7e6b7e8573a21346387cbbca5b2f5e3b9bfe24f96672 ARG ZIZMOR_SHA256_AMD64=a8000f3c683319a523d3b20df0e75457ba591f049cfcbfa98966631b56733c03 diff --git a/api/Dockerfile b/api/Dockerfile index 2262bd00d6..d19c3c9474 100644 --- a/api/Dockerfile +++ b/api/Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.12.13-slim-trixie@sha256:57cd7c3a7a273101a6485ba99423ee568157882804b1124b4dd04266317710de AS build +FROM python:3.12.15-slim-trixie@sha256:05cda9777409a9c3ffddd94a4c476b79f0769a0b4857f0c7ed9226b6800b0d6f AS build LABEL maintainer="https://github.com/prowler-cloud/api" @@ -7,15 +7,15 @@ ENV POWERSHELL_VERSION=${POWERSHELL_VERSION} # Opt out of PowerShell telemetry (Application Insights -> dc.services.visualstudio.com) ENV POWERSHELL_TELEMETRY_OPTOUT=1 -ARG TRIVY_VERSION=0.74.0 +ARG TRIVY_VERSION=0.75.0 ENV TRIVY_VERSION=${TRIVY_VERSION} ARG ZIZMOR_VERSION=1.24.1 ENV ZIZMOR_VERSION=${ZIZMOR_VERSION} # Pinned here, not fetched with the artefact: a compromised release ships its own checksum. -ARG TRIVY_SHA256_AMD64=2ae6fe3ee734b7fdf11335663e18c75ea12dccc76062f09f164a3b0f8be4371a -ARG TRIVY_SHA256_ARM64=b94ce1976bbf3c15b514b605ee88be7c6d94a29be2302847ff01cb794d47aad5 +ARG TRIVY_SHA256_AMD64=c6e65abddb348e25f10549df887045629cf28cc72453cd1c63acb717316b3f3f +ARG TRIVY_SHA256_ARM64=a1ee9f6ffb7d112b64ff726a2a0717c21175c1114361391f4a132956751a13b3 ARG POWERSHELL_SHA256_AMD64=82a8b13d92b0f3ae48e56cf2f3f7961679371736ca90145ca71617c2913ba9d8 ARG POWERSHELL_SHA256_ARM64=830ebda118c731ece3fa7e6b7e8573a21346387cbbca5b2f5e3b9bfe24f96672 ARG ZIZMOR_SHA256_AMD64=a8000f3c683319a523d3b20df0e75457ba591f049cfcbfa98966631b56733c03 diff --git a/api/changelog.d/api-image-trivy-0-75-0.security.md b/api/changelog.d/api-image-trivy-0-75-0.security.md new file mode 100644 index 0000000000..4d5759d29c --- /dev/null +++ b/api/changelog.d/api-image-trivy-0-75-0.security.md @@ -0,0 +1 @@ +Python base image from 3.12.13 to 3.12.15 and Trivy from 0.74.0 to 0.75.0 in the API container image, patching CVE-2026-19445, CVE-2026-19553 and the grpc-go and x/crypto CVEs bundled in Trivy diff --git a/mcp_server/Dockerfile b/mcp_server/Dockerfile index 7dad9e1303..db57306dfb 100644 --- a/mcp_server/Dockerfile +++ b/mcp_server/Dockerfile @@ -25,34 +25,20 @@ RUN --mount=type=cache,target=/root/.cache/uv \ # ============================================================================= # Final stage - Minimal runtime environment # ============================================================================= -FROM python:3.13.14-alpine3.23@sha256:9fdbf2e3e82628351513560b121e2ee6ce31cac212be9e070c5a5e2769fb5e76 +FROM python:3.13.16-alpine3.23@sha256:07b20c774a247152e74f622034c1482495d299e070eec620ddff13e01264290b LABEL maintainer="https://github.com/prowler-cloud" -# High CVEs fixed in Alpine 3.23 but not yet in the pinned base image: -# sqlite-libs 3.53.4-r0 CVE-2026-11822, CVE-2026-11824 (image ships 3.51.2-r0) -# libcrypto3/libssl3 3.5.8-r0 CVE-2026-14456 (image ships 3.5.7-r0) -# libuuid 2.41.6-r1 CVE-2026-53612, -53613, -53614, -76642, -78408, -78410 -# (image ships 2.41.4-r0; -78408 is the one that needs -r1 rather than -r0) -# The base image pins python 3.13.14, which has not been rebuilt since those -# packages were published, so the upgrade is taken here rather than by moving -# the pin -- the newest published python:3.13-alpine3.23 carries the same -# vulnerable versions. libcrypto3 and libssl3 are both built from openssl and -# are flagged separately, so both are named. -# `>=` rather than `=`: Alpine keeps only the newest build of a package in a -# branch's index, so an exact pin breaks this build the day one of these is -# superseded. Drop an entry once the base image ships that version or later. -RUN apk add --no-cache --upgrade \ - "sqlite-libs>=3.53.4-r0" \ - "libcrypto3>=3.5.8-r0" \ - "libssl3>=3.5.8-r0" \ - "libuuid>=2.41.6-r1" - # Create non-root user for security # Using specific UID/GID for consistency across environments RUN addgroup -g 1001 prowler && \ adduser -D -u 1001 -G prowler prowler +# pip is build-only; the entrypoint runs the venv directly. +RUN rm -rf /usr/local/lib/python3.13/site-packages/pip \ + /usr/local/lib/python3.13/site-packages/pip-*.dist-info \ + /usr/local/bin/pip /usr/local/bin/pip3 /usr/local/bin/pip3.13 + WORKDIR /app USER prowler diff --git a/mcp_server/changelog.d/mcp-image-python-3-13-16.security.md b/mcp_server/changelog.d/mcp-image-python-3-13-16.security.md new file mode 100644 index 0000000000..c926794e3e --- /dev/null +++ b/mcp_server/changelog.d/mcp-image-python-3-13-16.security.md @@ -0,0 +1 @@ +Python from 3.13.14 to 3.13.16 in the MCP container image, patching CVE-2026-19445 and CVE-2026-19553 diff --git a/prowler/changelog.d/sdk-image-trivy-0-75-0.security.md b/prowler/changelog.d/sdk-image-trivy-0-75-0.security.md new file mode 100644 index 0000000000..0a54879ed0 --- /dev/null +++ b/prowler/changelog.d/sdk-image-trivy-0-75-0.security.md @@ -0,0 +1 @@ +Python base image from 3.12.13 to 3.12.15 and Trivy from 0.74.0 to 0.75.0 in the SDK container image, patching CVE-2026-19445, CVE-2026-19553 and the grpc-go and x/crypto CVEs bundled in Trivy